]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blob - app/src/main/java/com/stoutner/privacybrowser/dialogs/MoveToFolderDialog.kt
Standardize suggested file names. https://redmine.stoutner.com/issues/951
[PrivacyBrowserAndroid.git] / app / src / main / java / com / stoutner / privacybrowser / dialogs / MoveToFolderDialog.kt
1 /*
2  * Copyright 2016-2023 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     companion object {
65         fun moveBookmarks(currentFolder: String, selectedBookmarksLongArray: LongArray): MoveToFolderDialog {
66             // Create an arguments bundle.
67             val argumentsBundle = Bundle()
68
69             // Store the arguments in the bundle.
70             argumentsBundle.putString(CURRENT_FOLDER, currentFolder)
71             argumentsBundle.putLongArray(SELECTED_BOOKMARKS_LONG_ARRAY, selectedBookmarksLongArray)
72
73             // Create a new instance of the dialog.
74             val moveToFolderDialog = MoveToFolderDialog()
75
76             // And the bundle to the dialog.
77             moveToFolderDialog.arguments = argumentsBundle
78
79             // Return the new dialog.
80             return moveToFolderDialog
81         }
82     }
83
84     // Declare the class variables.
85     private lateinit var moveToFolderListener: MoveToFolderListener
86     private lateinit var bookmarksDatabaseHelper: BookmarksDatabaseHelper
87     private lateinit var exceptFolders: StringBuilder
88
89     // The public interface is used to send information back to the parent activity.
90     interface MoveToFolderListener {
91         fun onMoveToFolder(dialogFragment: DialogFragment)
92     }
93
94     override fun onAttach(context: Context) {
95         // Run the default commands.
96         super.onAttach(context)
97
98         // Get a handle for the move to folder listener from the launching context.
99         moveToFolderListener = context as MoveToFolderListener
100     }
101
102     override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
103         // Get the data from the arguments.
104         val currentFolder = requireArguments().getString(CURRENT_FOLDER)!!
105         val selectedBookmarksLongArray = requireArguments().getLongArray(SELECTED_BOOKMARKS_LONG_ARRAY)!!
106
107         // Initialize the database helper.
108         bookmarksDatabaseHelper = BookmarksDatabaseHelper(requireContext())
109
110         // Use an alert dialog builder to create the alert dialog.
111         val dialogBuilder = AlertDialog.Builder(requireContext(), R.style.PrivacyBrowserAlertDialog)
112
113         // Set the icon.
114         dialogBuilder.setIcon(R.drawable.move_to_folder_blue)
115
116         // Set the title.
117         dialogBuilder.setTitle(R.string.move_to_folder)
118
119         // Set the view.
120         dialogBuilder.setView(R.layout.move_to_folder_dialog)
121
122         // Set the listener for the cancel button.  Using `null` as the listener closes the dialog without doing anything else.
123         dialogBuilder.setNegativeButton(R.string.cancel, null)
124
125         // Set the listener fo the move button.
126         dialogBuilder.setPositiveButton(R.string.move) { _: DialogInterface?, _: Int ->
127             // Return the dialog fragment to the parent activity on move.
128             moveToFolderListener.onMoveToFolder(this)
129         }
130
131         // Create an alert dialog from the alert dialog builder.
132         val alertDialog = dialogBuilder.create()
133
134         // Get a handle for the shared preferences.
135         val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
136
137         // Get the screenshot preference.
138         val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
139
140         // Disable screenshots if not allowed.
141         if (!allowScreenshots) {
142             // Disable screenshots.
143             alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
144         }
145
146         // The alert dialog must be shown before items in the layout can be modified.
147         alertDialog.show()
148
149         // Get a handle for the positive button.
150         val moveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE)
151
152         // Initially disable the positive button.
153         moveButton.isEnabled = false
154
155         // Initialize the except folders string builder.
156         exceptFolders = StringBuilder()
157
158         // Declare the cursor variables.
159         val foldersCursor: Cursor
160         val foldersCursorAdapter: CursorAdapter
161
162         // Check to see if the bookmark is currently in the home folder.
163         if (currentFolder.isEmpty()) {  // The bookmark is currently in the home folder.  Don't display `Home Folder` at the top of the list view.
164             // If a folder is selected, add it and all children to the list of folders not to display.
165             for (databaseIdLong in selectedBookmarksLongArray) {
166                 // Get the database ID int for each selected bookmark.
167                 val databaseIdInt = databaseIdLong.toInt()
168
169                 // Check to see if the bookmark is a folder.
170                 if (bookmarksDatabaseHelper.isFolder(databaseIdInt)) {
171                     // Add the folder to the list of folders not to display.
172                     addFolderToExceptFolders(databaseIdInt)
173                 }
174             }
175
176             // Get a cursor containing the folders to display.
177             foldersCursor = bookmarksDatabaseHelper.getFoldersExcept(exceptFolders.toString())
178
179             // Populate the folders cursor adapter.
180             foldersCursorAdapter = populateFoldersCursorAdapter(requireContext(), foldersCursor)
181         } else {  // The current folder is not directly in the home folder.  Display `Home Folder` at the top of the list view.
182             // Get the home folder icon drawable.
183             val homeFolderIconDrawable = ContextCompat.getDrawable(requireActivity().applicationContext, R.drawable.folder_gray_bitmap)
184
185             // Convert the home folder icon drawable to a bitmap drawable.
186             val homeFolderIconBitmapDrawable = homeFolderIconDrawable as BitmapDrawable
187
188             // Convert the home folder bitmap drawable to a bitmap.
189             val homeFolderIconBitmap = homeFolderIconBitmapDrawable.bitmap
190
191             // Create a home folder icon byte array output stream.
192             val homeFolderIconByteArrayOutputStream = ByteArrayOutputStream()
193
194             // Compress the bitmap using a coroutine with Dispatchers.Default.
195             CoroutineScope(Dispatchers.Main).launch {
196                 withContext(Dispatchers.Default) {
197                     // Convert the home folder bitmap to a byte array.  `0` is for lossless compression (the only option for a PNG).
198                     homeFolderIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, homeFolderIconByteArrayOutputStream)
199                 }
200             }
201
202             // Convert the home folder icon byte array output stream to a byte array.
203             val homeFolderIconByteArray = homeFolderIconByteArrayOutputStream.toByteArray()
204
205             // Setup the home folder matrix cursor column names.
206             val homeFolderMatrixCursorColumnNames = arrayOf(BookmarksDatabaseHelper.ID, BookmarksDatabaseHelper.BOOKMARK_NAME, BookmarksDatabaseHelper.FAVORITE_ICON)
207
208             // Setup a matrix cursor for the `Home Folder`.
209             val homeFolderMatrixCursor = MatrixCursor(homeFolderMatrixCursorColumnNames)
210
211             // Add the home folder to the home folder matrix cursor.
212             homeFolderMatrixCursor.addRow(arrayOf<Any>(0, getString(R.string.home_folder), homeFolderIconByteArray))
213
214             // Add the parent folder to the list of folders not to display.
215             exceptFolders.append(DatabaseUtils.sqlEscapeString(currentFolder))
216
217             // If a folder is selected, add it and all children to the list of folders not to display.
218             for (databaseIdLong in selectedBookmarksLongArray) {
219                 // Get the database ID int for each selected bookmark.
220                 val databaseIdInt = databaseIdLong.toInt()
221
222                 // Check to see if the bookmark is a folder.
223                 if (bookmarksDatabaseHelper.isFolder(databaseIdInt)) {
224                     // Add the folder to the list of folders not to display.
225                     addFolderToExceptFolders(databaseIdInt)
226                 }
227             }
228
229             // Get a cursor containing the folders to display.
230             foldersCursor = bookmarksDatabaseHelper.getFoldersExcept(exceptFolders.toString())
231
232             // Combine the home folder matrix cursor and the folders cursor.
233             val foldersMergeCursor = MergeCursor(arrayOf(homeFolderMatrixCursor, foldersCursor))
234
235             // Populate the folders cursor adapter.
236             foldersCursorAdapter = populateFoldersCursorAdapter(requireContext(), foldersMergeCursor)
237         }
238
239         // Get a handle for the folders list view.
240         val foldersListView = alertDialog.findViewById<ListView>(R.id.move_to_folder_listview)!!
241
242         // Set the folder list view adapter.
243         foldersListView.adapter = foldersCursorAdapter
244
245         // Enable the move button when a folder is selected.
246         foldersListView.onItemClickListener = OnItemClickListener { _: AdapterView<*>?, _: View?, _: Int, _: Long ->
247             // Enable the move button.
248             moveButton.isEnabled = true
249         }
250
251         // Return the alert dialog.
252         return alertDialog
253     }
254
255     private fun addFolderToExceptFolders(databaseIdInt: Int) {
256         // Get the name of the selected folder.
257         val folderName = bookmarksDatabaseHelper.getFolderName(databaseIdInt)
258
259         // Populate the list of folders not to get.
260         if (exceptFolders.isEmpty()) {
261             // Add the selected folder to the list of folders not to display.
262             exceptFolders.append(DatabaseUtils.sqlEscapeString(folderName))
263         } else {
264             // Add the selected folder to the end of the list of folders not to display.
265             exceptFolders.append(",")
266             exceptFolders.append(DatabaseUtils.sqlEscapeString(folderName))
267         }
268
269         // Add the selected folder's subfolders to the list of folders not to display.
270         addSubfoldersToExceptFolders(folderName)
271     }
272
273     private fun addSubfoldersToExceptFolders(folderName: String) {
274         // Get a cursor with all the immediate subfolders.
275         val subfoldersCursor = bookmarksDatabaseHelper.getSubfolders(folderName)
276
277         // Add each subfolder to the list of folders not to display.
278         for (i in 0 until subfoldersCursor.count) {
279             // Move the subfolder cursor to the current item.
280             subfoldersCursor.moveToPosition(i)
281
282             // Get the name of the subfolder.
283             val subfolderName = subfoldersCursor.getString(subfoldersCursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_NAME))
284
285             // Add the subfolder to except folders.
286             exceptFolders.append(",")
287             exceptFolders.append(DatabaseUtils.sqlEscapeString(subfolderName))
288
289             // Run the same tasks for any subfolders of the subfolder.
290             addSubfoldersToExceptFolders(subfolderName)
291         }
292     }
293
294     private fun populateFoldersCursorAdapter(context: Context, cursor: Cursor): CursorAdapter {
295         // Return the folders cursor adapter.
296         return object : CursorAdapter(context, cursor, false) {
297             override fun newView(context: Context, cursor: Cursor, parent: ViewGroup): View {
298                 // Inflate the individual item layout.
299                 return requireActivity().layoutInflater.inflate(R.layout.move_to_folder_item_linearlayout, parent, false)
300             }
301
302             override fun bindView(view: View, context: Context, cursor: Cursor) {
303                 // Get the data from the cursor.
304                 val folderIconByteArray = cursor.getBlob(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.FAVORITE_ICON))
305                 val folderName = cursor.getString(cursor.getColumnIndexOrThrow(BookmarksDatabaseHelper.BOOKMARK_NAME))
306
307                 // Get handles for the views.
308                 val folderIconImageView = view.findViewById<ImageView>(R.id.move_to_folder_icon)
309                 val folderNameTextView = view.findViewById<TextView>(R.id.move_to_folder_name_textview)
310
311                 // Convert the byte array to a bitmap beginning at the first byte and ending at the last.
312                 val folderIconBitmap = BitmapFactory.decodeByteArray(folderIconByteArray, 0, folderIconByteArray.size)
313
314                 // Display the folder icon bitmap.
315                 folderIconImageView.setImageBitmap(folderIconBitmap)
316
317                 // Display the folder name.
318                 folderNameTextView.text = folderName
319             }
320         }
321     }
322 }