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