]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blob - app/src/main/java/com/stoutner/privacybrowser/dialogs/MoveToFolderDialog.kt
Use a coroutine to compress the bitmap in MoveToFolderDialog. https://redmine.stoutne...
[PrivacyBrowserAndroid.git] / app / src / main / java / com / stoutner / privacybrowser / dialogs / MoveToFolderDialog.kt
1 /*
2  * Copyright 2016-2022 Soren Stoutner <soren@stoutner.com>.
3  *
4  * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
5  *
6  * Privacy Browser Android is free software: you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation, either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * Privacy Browser Android is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with Privacy Browser Android.  If not, see <http://www.gnu.org/licenses/>.
18  */
19
20 package com.stoutner.privacybrowser.dialogs
21
22 import android.app.Dialog
23 import android.content.Context
24 import android.content.DialogInterface
25 import android.database.Cursor
26 import android.database.DatabaseUtils
27 import android.database.MatrixCursor
28 import android.database.MergeCursor
29 import android.graphics.Bitmap
30 import android.graphics.BitmapFactory
31 import android.graphics.drawable.BitmapDrawable
32 import android.os.Bundle
33 import android.view.View
34 import android.view.ViewGroup
35 import android.view.WindowManager
36 import android.widget.AdapterView
37 import android.widget.AdapterView.OnItemClickListener
38 import android.widget.ImageView
39 import android.widget.ListView
40 import android.widget.TextView
41
42 import androidx.appcompat.app.AlertDialog
43 import androidx.core.content.ContextCompat
44 import androidx.cursoradapter.widget.CursorAdapter
45 import androidx.fragment.app.DialogFragment
46 import androidx.preference.PreferenceManager
47
48 import com.stoutner.privacybrowser.R
49 import com.stoutner.privacybrowser.helpers.BookmarksDatabaseHelper
50
51 import kotlinx.coroutines.CoroutineScope
52 import kotlinx.coroutines.Dispatchers
53 import kotlinx.coroutines.launch
54 import kotlinx.coroutines.withContext
55
56 import java.io.ByteArrayOutputStream
57 import java.lang.StringBuilder
58
59 // Define the class constants.
60 private const val CURRENT_FOLDER = "current_folder"
61 private const val SELECTED_BOOKMARKS_LONG_ARRAY = "selected_bookmarks_long_array"
62
63 class MoveToFolderDialog : DialogFragment() {
64     // Declare the class variables.
65     private lateinit var moveToFolderListener: MoveToFolderListener
66     private lateinit var bookmarksDatabaseHelper: BookmarksDatabaseHelper
67     private lateinit var exceptFolders: StringBuilder
68
69     // The public interface is used to send information back to the parent activity.
70     interface MoveToFolderListener {
71         fun onMoveToFolder(dialogFragment: DialogFragment)
72     }
73
74     override fun onAttach(context: Context) {
75         // Run the default commands.
76         super.onAttach(context)
77
78         // Get a handle for the move to folder listener from the launching context.
79         moveToFolderListener = context as MoveToFolderListener
80     }
81
82     companion object {
83         // `@JvmStatic` will no longer be required once all the code has transitioned to Kotlin.
84         @JvmStatic
85         fun moveBookmarks(currentFolder: String, selectedBookmarksLongArray: LongArray): MoveToFolderDialog {
86             // Create an arguments bundle.
87             val argumentsBundle = Bundle()
88
89             // Store the arguments in the bundle.
90             argumentsBundle.putString(CURRENT_FOLDER, currentFolder)
91             argumentsBundle.putLongArray(SELECTED_BOOKMARKS_LONG_ARRAY, selectedBookmarksLongArray)
92
93             // Create a new instance of the dialog.
94             val moveToFolderDialog = MoveToFolderDialog()
95
96             // And the bundle to the dialog.
97             moveToFolderDialog.arguments = argumentsBundle
98
99             // Return the new dialog.
100             return moveToFolderDialog
101         }
102     }
103
104     override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
105         // Get the data from the arguments.
106         val currentFolder = requireArguments().getString(CURRENT_FOLDER)!!
107         val selectedBookmarksLongArray = requireArguments().getLongArray(SELECTED_BOOKMARKS_LONG_ARRAY)!!
108
109         // Initialize the database helper.
110         bookmarksDatabaseHelper = BookmarksDatabaseHelper(requireContext())
111
112         // Use an alert dialog builder to create the alert dialog.
113         val dialogBuilder = AlertDialog.Builder(requireContext(), R.style.PrivacyBrowserAlertDialog)
114
115         // Set the icon.
116         dialogBuilder.setIcon(R.drawable.move_to_folder_blue)
117
118         // Set the title.
119         dialogBuilder.setTitle(R.string.move_to_folder)
120
121         // Set the view.
122         dialogBuilder.setView(R.layout.move_to_folder_dialog)
123
124         // Set the listener for the cancel button.  Using `null` as the listener closes the dialog without doing anything else.
125         dialogBuilder.setNegativeButton(R.string.cancel, null)
126
127         // Set the listener fo the move button.
128         dialogBuilder.setPositiveButton(R.string.move) { _: DialogInterface?, _: Int ->
129             // Return the dialog fragment to the parent activity on move.
130             moveToFolderListener.onMoveToFolder(this)
131         }
132
133         // Create an alert dialog from the alert dialog builder.
134         val alertDialog = dialogBuilder.create()
135
136         // Get a handle for the shared preferences.
137         val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
138
139         // Get the screenshot preference.
140         val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
141
142         // Disable screenshots if not allowed.
143         if (!allowScreenshots) {
144             // Disable screenshots.
145             alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
146         }
147
148         // The alert dialog must be shown before items in the layout can be modified.
149         alertDialog.show()
150
151         // Get a handle for the positive button.
152         val moveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE)
153
154         // Initially disable the positive button.
155         moveButton.isEnabled = false
156
157         // Initialize the except folders string builder.
158         exceptFolders = StringBuilder()
159
160         // Declare the cursor variables.
161         val foldersCursor: Cursor
162         val foldersCursorAdapter: CursorAdapter
163
164         // Check to see if the bookmark is currently in the home folder.
165         if (currentFolder.isEmpty()) {  // The bookmark is currently in the home folder.  Don't display `Home Folder` at the top of the list view.
166             // If a folder is selected, add it and all children to the list of folders not to display.
167             for (databaseIdLong in selectedBookmarksLongArray) {
168                 // Get the database ID int for each selected bookmark.
169                 val databaseIdInt = databaseIdLong.toInt()
170
171                 // Check to see if the bookmark is a folder.
172                 if (bookmarksDatabaseHelper.isFolder(databaseIdInt)) {
173                     // Add the folder to the list of folders not to display.
174                     addFolderToExceptFolders(databaseIdInt)
175                 }
176             }
177
178             // Get a cursor containing the folders to display.
179             foldersCursor = bookmarksDatabaseHelper.getFoldersExcept(exceptFolders.toString())
180
181             // Populate the folders cursor adapter.
182             foldersCursorAdapter = populateFoldersCursorAdapter(requireContext(), foldersCursor)
183         } else {  // The current folder is not directly in the home folder.  Display `Home Folder` at the top of the list view.
184             // Get the home folder icon drawable.
185             val homeFolderIconDrawable = ContextCompat.getDrawable(requireActivity().applicationContext, R.drawable.folder_gray_bitmap)
186
187             // Convert the home folder icon drawable to a bitmap drawable.
188             val homeFolderIconBitmapDrawable = homeFolderIconDrawable as BitmapDrawable
189
190             // Convert the home folder bitmap drawable to a bitmap.
191             val homeFolderIconBitmap = homeFolderIconBitmapDrawable.bitmap
192
193             // Create a home folder icon byte array output stream.
194             val homeFolderIconByteArrayOutputStream = ByteArrayOutputStream()
195
196             // Compress the bitmap using a coroutine with Dispatchers.Default.
197             CoroutineScope(Dispatchers.Main).launch {
198                 withContext(Dispatchers.Default) {
199                     // Convert the home folder bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
200                     homeFolderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, homeFolderIconByteArrayOutputStream)
201                 }
202             }
203
204             // Convert the home folder icon byte array output stream to a byte array.
205             val homeFolderIconByteArray = homeFolderIconByteArrayOutputStream.toByteArray()
206
207             // Setup the home folder matrix cursor column names.
208             val homeFolderMatrixCursorColumnNames = arrayOf(BookmarksDatabaseHelper.ID, BookmarksDatabaseHelper.BOOKMARK_NAME, BookmarksDatabaseHelper.FAVORITE_ICON)
209
210             // Setup a matrix cursor for the `Home Folder`.
211             val homeFolderMatrixCursor = MatrixCursor(homeFolderMatrixCursorColumnNames)
212
213             // Add the home folder to the home folder matrix cursor.
214             homeFolderMatrixCursor.addRow(arrayOf<Any>(0, getString(R.string.home_folder), homeFolderIconByteArray))
215
216             // Add the parent folder to the list of folders not to display.
217             exceptFolders.append(DatabaseUtils.sqlEscapeString(currentFolder))
218
219             // If a folder is selected, add it and all children to the list of folders not to display.
220             for (databaseIdLong in selectedBookmarksLongArray) {
221                 // Get the database ID int for each selected bookmark.
222                 val databaseIdInt = databaseIdLong.toInt()
223
224                 // Check to see if the bookmark is a folder.
225                 if (bookmarksDatabaseHelper.isFolder(databaseIdInt)) {
226                     // Add the folder to the list of folders not to display.
227                     addFolderToExceptFolders(databaseIdInt)
228                 }
229             }
230
231             // Get a cursor containing the folders to display.
232             foldersCursor = bookmarksDatabaseHelper.getFoldersExcept(exceptFolders.toString())
233
234             // Combine the home folder matrix cursor and the folders cursor.
235             val foldersMergeCursor = MergeCursor(arrayOf(homeFolderMatrixCursor, foldersCursor))
236
237             // Populate the folders cursor adapter.
238             foldersCursorAdapter = populateFoldersCursorAdapter(requireContext(), foldersMergeCursor)
239         }
240
241         // Get a handle for the folders list view.
242         val foldersListView = alertDialog.findViewById<ListView>(R.id.move_to_folder_listview)!!
243
244         // Set the folder list view adapter.
245         foldersListView.adapter = foldersCursorAdapter
246
247         // Enable the move button when a folder is selected.
248         foldersListView.onItemClickListener = OnItemClickListener { _: AdapterView<*>?, _: View?, _: Int, _: Long ->
249             // Enable the move button.
250             moveButton.isEnabled = true
251         }
252
253         // Return the alert dialog.
254         return alertDialog
255     }
256
257     private fun addFolderToExceptFolders(databaseIdInt: Int) {
258         // Get the name of the selected folder.
259         val folderName = bookmarksDatabaseHelper.getFolderName(databaseIdInt)
260
261         // Populate the list of folders not to get.
262         if (exceptFolders.isEmpty()) {
263             // Add the selected folder to the list of folders not to display.
264             exceptFolders.append(DatabaseUtils.sqlEscapeString(folderName))
265         } else {
266             // Add the selected folder to the end of the list of folders not to display.
267             exceptFolders.append(",")
268             exceptFolders.append(DatabaseUtils.sqlEscapeString(folderName))
269         }
270
271         // Add the selected folder's subfolders to the list of folders not to display.
272         addSubfoldersToExceptFolders(folderName)
273     }
274
275     private fun addSubfoldersToExceptFolders(folderName: String) {
276         // Get a cursor with all the immediate subfolders.
277         val subfoldersCursor = bookmarksDatabaseHelper.getSubfolders(folderName)
278
279         // Add each subfolder to the list of folders not to display.
280         for (i in 0 until subfoldersCursor.count) {
281             // Move the subfolder cursor to the current item.
282             subfoldersCursor.moveToPosition(i)
283
284             // Get the name of the subfolder.
285             val subfolderName = subfoldersCursor.getString(subfoldersCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_NAME))
286
287             // Add the subfolder to except folders.
288             exceptFolders.append(",")
289             exceptFolders.append(DatabaseUtils.sqlEscapeString(subfolderName))
290
291             // Run the same tasks for any subfolders of the subfolder.
292             addSubfoldersToExceptFolders(subfolderName)
293         }
294     }
295
296     private fun populateFoldersCursorAdapter(context: Context, cursor: Cursor): CursorAdapter {
297         // Return the folders cursor adapter.
298         return object : CursorAdapter(context, cursor, false) {
299             override fun newView(context: Context, cursor: Cursor, parent: ViewGroup): View {
300                 // Inflate the individual item layout.
301                 return requireActivity().layoutInflater.inflate(R.layout.move_to_folder_item_linearlayout, parent, false)
302             }
303
304             override fun bindView(view: View, context: Context, cursor: Cursor) {
305                 // Get the data from the cursor.
306                 val folderIconByteArray = cursor.getBlob(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.FAVORITE_ICON))
307                 val folderName = cursor.getString(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_NAME))
308
309                 // Get handles for the views.
310                 val folderIconImageView = view.findViewById<ImageView>(R.id.move_to_folder_icon)
311                 val folderNameTextView = view.findViewById<TextView>(R.id.move_to_folder_name_textview)
312
313                 // Convert the byte array to a bitmap beginning at the first byte and ending at the last.
314                 val folderIconBitmap = BitmapFactory.decodeByteArray(folderIconByteArray, 0, folderIconByteArray.size)
315
316                 // Display the folder icon bitmap.
317                 folderIconImageView.setImageBitmap(folderIconBitmap)
318
319                 // Display the folder name.
320                 folderNameTextView.text = folderName
321             }
322         }
323     }
324 }