]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blob - app/src/main/java/com/stoutner/privacybrowser/dialogs/EditBookmarkFolderDatabaseViewDialog.kt
Allow duplicate bookmark folders. https://redmine.stoutner.com/issues/199
[PrivacyBrowserAndroid.git] / app / src / main / java / com / stoutner / privacybrowser / dialogs / EditBookmarkFolderDatabaseViewDialog.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.MatrixCursor
27 import android.database.MergeCursor
28 import android.graphics.Bitmap
29 import android.graphics.BitmapFactory
30 import android.os.Bundle
31 import android.text.Editable
32 import android.text.TextWatcher
33 import android.view.KeyEvent
34 import android.view.View
35 import android.view.WindowManager
36 import android.widget.AdapterView
37 import android.widget.Button
38 import android.widget.EditText
39 import android.widget.ImageView
40 import android.widget.LinearLayout
41 import android.widget.RadioButton
42 import android.widget.Spinner
43 import android.widget.TextView
44
45 import androidx.appcompat.app.AlertDialog
46 import androidx.core.content.ContextCompat
47 import androidx.cursoradapter.widget.ResourceCursorAdapter
48 import androidx.fragment.app.DialogFragment
49 import androidx.preference.PreferenceManager
50
51 import com.stoutner.privacybrowser.R
52 import com.stoutner.privacybrowser.activities.HOME_FOLDER_DATABASE_ID
53 import com.stoutner.privacybrowser.activities.HOME_FOLDER_ID
54 import com.stoutner.privacybrowser.helpers.BOOKMARK_NAME
55 import com.stoutner.privacybrowser.helpers.DISPLAY_ORDER
56 import com.stoutner.privacybrowser.helpers.FAVORITE_ICON
57 import com.stoutner.privacybrowser.helpers.FOLDER_ID
58 import com.stoutner.privacybrowser.helpers.ID
59 import com.stoutner.privacybrowser.helpers.PARENT_FOLDER_ID
60 import com.stoutner.privacybrowser.helpers.BookmarksDatabaseHelper
61
62 import java.io.ByteArrayOutputStream
63
64 // Define the class constants.
65 private const val DATABASE_ID = "database_id"
66 private const val FAVORITE_ICON_BYTE_ARRAY = "favorite_icon_byte_array"
67
68 class EditBookmarkFolderDatabaseViewDialog : DialogFragment() {
69     companion object {
70         fun folderDatabaseId(databaseId: Int, favoriteIconBitmap: Bitmap): EditBookmarkFolderDatabaseViewDialog {
71             // Create a favorite icon byte array output stream.
72             val favoriteIconByteArrayOutputStream = ByteArrayOutputStream()
73
74             // Convert the favorite icon to a PNG and place it in the byte array output stream.  `0` is for lossless compression (the only option for a PNG).
75             favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream)
76
77             // Convert the byte array output stream to a byte array.
78             val favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray()
79
80             // Create an arguments bundle.
81             val argumentsBundle = Bundle()
82
83             // Store the variables in the bundle.
84             argumentsBundle.putInt(DATABASE_ID, databaseId)
85             argumentsBundle.putByteArray(FAVORITE_ICON_BYTE_ARRAY, favoriteIconByteArray)
86
87             // Create a new instance of the dialog.
88             val editBookmarkFolderDatabaseViewDialog = EditBookmarkFolderDatabaseViewDialog()
89
90             // Add the arguments bundle to the dialog.
91             editBookmarkFolderDatabaseViewDialog.arguments = argumentsBundle
92
93             // Return the new dialog.
94             return editBookmarkFolderDatabaseViewDialog
95         }
96     }
97
98     // Declare the class variables.
99     private lateinit var editBookmarkFolderDatabaseViewListener: EditBookmarkFolderDatabaseViewListener
100
101     // Declare the class views.
102     private lateinit var currentIconRadioButton: RadioButton
103     private lateinit var nameEditText: EditText
104     private lateinit var parentFolderSpinner: Spinner
105     private lateinit var displayOrderEditText: EditText
106     private lateinit var saveButton: Button
107
108     // The public interface is used to send information back to the parent activity.
109     interface EditBookmarkFolderDatabaseViewListener {
110         fun saveBookmarkFolder(dialogFragment: DialogFragment, selectedFolderDatabaseId: Int, favoriteIconBitmap: Bitmap)
111     }
112
113     override fun onAttach(context: Context) {
114         // Run the default commands.
115         super.onAttach(context)
116
117         // Get a handle for edit bookmark database view listener from the launching context.
118         editBookmarkFolderDatabaseViewListener = context as EditBookmarkFolderDatabaseViewListener
119     }
120
121     override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
122         // Get a handle for the arguments.
123         val arguments = requireArguments()
124
125         // Get the variables from the arguments.
126         val folderDatabaseId = arguments.getInt(DATABASE_ID)
127         val favoriteIconByteArray = arguments.getByteArray(FAVORITE_ICON_BYTE_ARRAY)!!
128
129         // Convert the favorite icon byte array to a bitmap.
130         val favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.size)
131
132         // Initialize the bookmarks database helper.
133         val bookmarksDatabaseHelper = BookmarksDatabaseHelper(requireContext())
134
135         // Get a cursor with the selected bookmark.
136         val folderCursor = bookmarksDatabaseHelper.getBookmark(folderDatabaseId)
137
138         // Move the cursor to the first position.
139         folderCursor.moveToFirst()
140
141         // Use an alert dialog builder to create the alert dialog.
142         val dialogBuilder = AlertDialog.Builder(requireContext(), R.style.PrivacyBrowserAlertDialog)
143
144         // Set the title.
145         dialogBuilder.setTitle(R.string.edit_folder)
146
147         // Set the view.
148         dialogBuilder.setView(R.layout.edit_bookmark_folder_databaseview_dialog)
149
150         // Set the cancel button listener.  Using `null` as the listener closes the dialog without doing anything else.
151         dialogBuilder.setNegativeButton(R.string.cancel, null)
152
153         // Set the save button listener.
154         dialogBuilder.setPositiveButton(R.string.save) { _: DialogInterface?, _: Int ->
155             // Return the dialog fragment to the parent activity.
156             editBookmarkFolderDatabaseViewListener.saveBookmarkFolder(this, folderDatabaseId, favoriteIconBitmap)
157         }
158
159         // Create an alert dialog from the alert dialog builder.
160         val alertDialog = dialogBuilder.create()
161
162         // Get a handle for the shared preferences.
163         val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
164
165         // Get the screenshot preference.
166         val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
167
168         // Disable screenshots if not allowed.
169         if (!allowScreenshots) {
170             alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
171         }
172
173         // The alert dialog must be shown before items in the layout can be modified.
174         alertDialog.show()
175
176         // Get handles for the views in the alert dialog.
177         val databaseIdTextView = alertDialog.findViewById<TextView>(R.id.folder_database_id_textview)!!
178         val folderIdTextView = alertDialog.findViewById<TextView>(R.id.folder_id_textview)!!
179         val currentIconLinearLayout = alertDialog.findViewById<LinearLayout>(R.id.current_icon_linearlayout)!!
180         currentIconRadioButton = alertDialog.findViewById(R.id.current_icon_radiobutton)!!
181         val currentIconImageView = alertDialog.findViewById<ImageView>(R.id.current_icon_imageview)!!
182         val defaultIconLinearLayout = alertDialog.findViewById<LinearLayout>(R.id.default_icon_linearlayout)!!
183         val defaultIconRadioButton = alertDialog.findViewById<RadioButton>(R.id.default_icon_radiobutton)!!
184         val webpageFavoriteIconLinearLayout = alertDialog.findViewById<LinearLayout>(R.id.webpage_favorite_icon_linearlayout)!!
185         val webpageFavoriteIconRadioButton = alertDialog.findViewById<RadioButton>(R.id.webpage_favorite_icon_radiobutton)!!
186         val webpageFavoriteIconImageView = alertDialog.findViewById<ImageView>(R.id.webpage_favorite_icon_imageview)!!
187         nameEditText = alertDialog.findViewById(R.id.folder_name_edittext)!!
188         parentFolderSpinner = alertDialog.findViewById(R.id.parent_folder_spinner)!!
189         displayOrderEditText = alertDialog.findViewById(R.id.display_order_edittext)!!
190         saveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE)
191
192         // Store the current folder values.
193         val currentFolderName = folderCursor.getString(folderCursor.getColumnIndexOrThrow(BOOKMARK_NAME))
194         val currentDisplayOrder = folderCursor.getInt(folderCursor.getColumnIndexOrThrow(DISPLAY_ORDER))
195         val parentFolderId = folderCursor.getLong(folderCursor.getColumnIndexOrThrow(PARENT_FOLDER_ID))
196         val currentFolderId = folderCursor.getLong(folderCursor.getColumnIndexOrThrow(FOLDER_ID))
197
198         // Populate the database ID text view.
199         databaseIdTextView.text = folderCursor.getInt(folderCursor.getColumnIndexOrThrow(ID)).toString()
200
201         // Populate the folder ID text view.
202         folderIdTextView.text = folderCursor.getLong(folderCursor.getColumnIndexOrThrow(FOLDER_ID)).toString()
203
204         // Get the current favorite icon byte array from the cursor.
205         val currentIconByteArray = folderCursor.getBlob(folderCursor.getColumnIndexOrThrow(FAVORITE_ICON))
206
207         // Convert the byte array to a bitmap beginning at the first byte and ending at the last.
208         val currentIconBitmap = BitmapFactory.decodeByteArray(currentIconByteArray, 0, currentIconByteArray.size)
209
210         // Populate the current icon image view.
211         currentIconImageView.setImageBitmap(currentIconBitmap)
212
213         // Populate the webpage favorite icon image view.
214         webpageFavoriteIconImageView.setImageBitmap(favoriteIconBitmap)
215
216         // Populate the folder name edit text.
217         nameEditText.setText(currentFolderName)
218
219         // Define an array of matrix cursor column names.
220         val matrixCursorColumnNames = arrayOf(ID, BOOKMARK_NAME, PARENT_FOLDER_ID)
221
222         // Create a matrix cursor.
223         val matrixCursor = MatrixCursor(matrixCursorColumnNames)
224
225         // Add `Home Folder` to the matrix cursor.
226         matrixCursor.addRow(arrayOf(HOME_FOLDER_DATABASE_ID, getString(R.string.home_folder), HOME_FOLDER_ID))
227
228         // Create a list of folder IDs.
229         val currentAndSubfolderIds = mutableListOf<Long>()
230
231         // Add the current folder ID to the list.
232         currentAndSubfolderIds.add(currentFolderId)
233
234         // Get a long array of all the subfolders IDs.
235         val subfolderIdLongList = getListOfSubfolderIds(currentFolderId, bookmarksDatabaseHelper)
236
237         // Add the subfolder IDs to the list.
238         for (subfolderId in subfolderIdLongList)
239             currentAndSubfolderIds.add(subfolderId)
240
241         // Get a cursor with the list of all the folders except for those specified..
242         val foldersCursor = bookmarksDatabaseHelper.getFoldersExcept(currentAndSubfolderIds)
243
244         // Combine the matrix cursor and the folders cursor.
245         val combinedFoldersCursor = MergeCursor(arrayOf(matrixCursor, foldersCursor))
246
247         // Create a resource cursor adapter for the spinner.
248         val foldersCursorAdapter: ResourceCursorAdapter = object: ResourceCursorAdapter(context, R.layout.databaseview_spinner_item, combinedFoldersCursor, 0) {
249             override fun bindView(view: View, context: Context, cursor: Cursor) {
250                 // Get handles for the spinner views.
251                 val subfolderSpacerTextView = view.findViewById<TextView>(R.id.subfolder_spacer_textview)
252                 val folderIconImageView = view.findViewById<ImageView>(R.id.folder_icon_imageview)
253                 val folderNameTextView = view.findViewById<TextView>(R.id.folder_name_textview)
254
255                 // Populate the subfolder spacer if it is not null (the spinner is open).
256                 if (subfolderSpacerTextView != null) {
257                     // Indent subfolders.
258                     if (cursor.getLong(cursor.getColumnIndexOrThrow(PARENT_FOLDER_ID)) != HOME_FOLDER_ID) {  // The folder is not in the home folder.
259                         // Get the subfolder spacer.
260                         subfolderSpacerTextView.text = bookmarksDatabaseHelper.getSubfolderSpacer(cursor.getLong(cursor.getColumnIndexOrThrow(FOLDER_ID)))
261                     } else {  // The folder is in the home folder.
262                         // Reset the subfolder spacer.
263                         subfolderSpacerTextView.text = ""
264                     }
265                 }
266
267                 // Set the folder icon according to the type.
268                 if (combinedFoldersCursor.position == 0) {  // Set the `Home Folder` icon.
269                     // Set the gray folder image.  `ContextCompat` must be used until the minimum API >= 21.
270                     folderIconImageView.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.folder_gray))
271                 } else {  // Set a user folder icon.
272                     // Get the folder icon byte array.
273                     val folderIconByteArray = cursor.getBlob(cursor.getColumnIndexOrThrow(FAVORITE_ICON))
274
275                     // Convert the byte array to a bitmap beginning at the first byte and ending at the last.
276                     val folderIconBitmap = BitmapFactory.decodeByteArray(folderIconByteArray, 0, folderIconByteArray.size)
277
278                     // Set the folder icon.
279                     folderIconImageView.setImageBitmap(folderIconBitmap)
280                 }
281
282                 // Set the folder name.
283                 folderNameTextView.text = cursor.getString(cursor.getColumnIndexOrThrow(BOOKMARK_NAME))
284             }
285         }
286
287         // Set the folders cursor adapter drop drown view resource.
288         foldersCursorAdapter.setDropDownViewResource(R.layout.databaseview_spinner_dropdown_items)
289
290         // Set the parent folder spinner adapter.
291         parentFolderSpinner.adapter = foldersCursorAdapter
292
293         // Select the current folder in the spinner if the bookmark isn't in the home folder.
294         if (parentFolderId != HOME_FOLDER_ID) {
295             // Get the database ID of the parent folder as a long.
296             val parentFolderDatabaseId = bookmarksDatabaseHelper.getFolderDatabaseId(parentFolderId).toLong()
297
298             // Initialize the parent folder position and the iteration variable.
299             var parentFolderPosition = 0
300             var i = 0
301
302             // Find the parent folder position in the folders cursor adapter.
303             do {
304                 if (foldersCursorAdapter.getItemId(i) == parentFolderDatabaseId) {
305                     // Store the current position for the parent folder.
306                     parentFolderPosition = i
307                 } else {
308                     // Try the next entry.
309                     i++
310                 }
311                 // Stop when the parent folder position is found or all the items in the folders cursor adapter have been checked.
312             } while (parentFolderPosition == 0 && i < foldersCursorAdapter.count)
313
314             // Select the parent folder in the spinner.
315             parentFolderSpinner.setSelection(parentFolderPosition)
316         }
317
318         // Store the current folder database ID.
319         val currentParentFolderDatabaseId = parentFolderSpinner.selectedItemId.toInt()
320
321         // Populate the display order edit text.
322         displayOrderEditText.setText(folderCursor.getInt(folderCursor.getColumnIndexOrThrow(DISPLAY_ORDER)).toString())
323
324         // Initially disable the edit button.
325         saveButton.isEnabled = false
326
327         // Set the radio button listeners.  These perform a click on the linear layout, which contains the necessary logic.
328         currentIconRadioButton.setOnClickListener { currentIconLinearLayout.performClick() }
329         defaultIconRadioButton.setOnClickListener { defaultIconLinearLayout.performClick() }
330         webpageFavoriteIconRadioButton.setOnClickListener { webpageFavoriteIconLinearLayout.performClick() }
331
332         // Set the current icon linear layout click listener.
333         currentIconLinearLayout.setOnClickListener {
334             // Check the current icon radio button.
335             currentIconRadioButton.isChecked = true
336
337             // Uncheck the other radio buttons.
338             defaultIconRadioButton.isChecked = false
339             webpageFavoriteIconRadioButton.isChecked = false
340
341             // Update the save button.
342             updateSaveButton(currentFolderName, currentParentFolderDatabaseId, currentDisplayOrder)
343         }
344
345         // Set the default icon linear layout click listener.
346         defaultIconLinearLayout.setOnClickListener {
347             // Check the default icon radio button.
348             defaultIconRadioButton.isChecked = true
349
350             // Uncheck the other radio buttons.
351             currentIconRadioButton.isChecked = false
352             webpageFavoriteIconRadioButton.isChecked = false
353
354             // Update the save button.
355             updateSaveButton(currentFolderName, currentParentFolderDatabaseId, currentDisplayOrder)
356         }
357
358         // Set the webpage favorite icon linear layout click listener.
359         webpageFavoriteIconLinearLayout.setOnClickListener {
360             // Check the webpage favorite icon radio button.
361             webpageFavoriteIconRadioButton.isChecked = true
362
363             // Uncheck the other radio buttons.
364             currentIconRadioButton.isChecked = false
365             defaultIconRadioButton.isChecked = false
366
367             // Update the save button.
368             updateSaveButton(currentFolderName, currentParentFolderDatabaseId, currentDisplayOrder)
369         }
370
371         // Update the save button if the bookmark name changes.
372         nameEditText.addTextChangedListener(object: TextWatcher {
373             override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
374                 // Do nothing.
375             }
376
377             override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
378                 // Do nothing.
379             }
380
381             override fun afterTextChanged(s: Editable) {
382                 // Update the save button.
383                 updateSaveButton(currentFolderName, currentParentFolderDatabaseId, currentDisplayOrder)
384             }
385         })
386
387         // Wait to set the on item selected listener until the spinner has been inflated.  Otherwise the dialog will crash on restart.
388         parentFolderSpinner.post {
389             // Update the save button if the parent folder changes.
390             parentFolderSpinner.onItemSelectedListener = object: AdapterView.OnItemSelectedListener {
391                 override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) {
392                     // Update the save button.
393                     updateSaveButton(currentFolderName, currentParentFolderDatabaseId, currentDisplayOrder)
394                 }
395
396                 override fun onNothingSelected(parent: AdapterView<*>) {
397                     // Do nothing.
398                 }
399             }
400         }
401
402         // Update the save button if the display order changes.
403         displayOrderEditText.addTextChangedListener(object: TextWatcher {
404             override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
405                 // Do nothing.
406             }
407
408             override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
409                 // Do nothing.
410             }
411
412             override fun afterTextChanged(s: Editable) {
413                 // Update the save button.
414                 updateSaveButton(currentFolderName, currentParentFolderDatabaseId, currentDisplayOrder)
415             }
416         })
417
418         // Allow the enter key on the keyboard to save the bookmark from the bookmark name edit text.
419         nameEditText.setOnKeyListener { _: View?, keyCode: Int, event: KeyEvent ->
420             // Check the key code, event, and button status.
421             if (event.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER && saveButton.isEnabled) {  // The enter key was pressed and the save button is enabled.
422                 // Trigger the listener and return the dialog fragment to the parent activity.
423                 editBookmarkFolderDatabaseViewListener.saveBookmarkFolder(this, folderDatabaseId, favoriteIconBitmap)
424
425                 // Manually dismiss the alert dialog.
426                 alertDialog.dismiss()
427
428                 // Consume the event.
429                 return@setOnKeyListener true
430             } else {  // If any other key was pressed, or if the save button is currently disabled, do not consume the event.
431                 return@setOnKeyListener false
432             }
433         }
434
435         // Allow the enter key on the keyboard to save the bookmark from the display order edit text.
436         displayOrderEditText.setOnKeyListener { _: View?, keyCode: Int, event: KeyEvent ->
437             // Check the key code, event, and button status.
438             if (event.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_ENTER && saveButton.isEnabled) {  // The enter key was pressed and the save button is enabled.
439                 // Trigger the listener and return the dialog fragment to the parent activity.
440                 editBookmarkFolderDatabaseViewListener.saveBookmarkFolder(this, folderDatabaseId, favoriteIconBitmap)
441
442                 // Manually dismiss the alert dialog.
443                 alertDialog.dismiss()
444
445                 // Consume the event.
446                 return@setOnKeyListener true
447             } else { // If any other key was pressed, or if the save button is currently disabled, do not consume the event.
448                 return@setOnKeyListener false
449             }
450         }
451
452         // Return the alert dialog.
453         return alertDialog
454     }
455
456     private fun updateSaveButton(currentFolderName: String, currentParentFolderDatabaseId: Int, currentDisplayOrder: Int) {
457         // Get the values from the views.
458         val newFolderName = nameEditText.text.toString()
459         val newParentFolderDatabaseId = parentFolderSpinner.selectedItemId.toInt()
460         val newDisplayOrder = displayOrderEditText.text.toString()
461
462         // Has the favorite icon changed?
463         val iconChanged = !currentIconRadioButton.isChecked
464
465         // Has the folder been renamed?
466         val folderRenamed = (newFolderName != currentFolderName)
467
468         // Has the parent folder changed?
469         val parentFolderChanged = newParentFolderDatabaseId != currentParentFolderDatabaseId
470
471         // Has the display order changed?
472         val displayOrderChanged = newDisplayOrder != currentDisplayOrder.toString()
473
474         // Update the enabled status of the edit button.
475         saveButton.isEnabled = (iconChanged || folderRenamed || parentFolderChanged || displayOrderChanged) && newFolderName.isNotBlank() && newDisplayOrder.isNotBlank()
476     }
477
478     private fun getListOfSubfolderIds(folderId: Long, bookmarksDatabaseHelper: BookmarksDatabaseHelper): List<Long> {
479         // Create a subfolder long list.
480         val subfolderIdLongList = mutableListOf<Long>()
481
482         // Get a cursor with all the immediate subfolders.
483         val subfoldersCursor = bookmarksDatabaseHelper.getSubfolderNamesAndFolderIds(folderId)
484
485         // Populate the subfolder list.
486         for (i in 0 until subfoldersCursor.count) {
487             // Move the subfolder cursor to the current item.
488             subfoldersCursor.moveToPosition(i)
489
490             // Get the subfolder ID.
491             val subfolderId = subfoldersCursor.getLong(subfoldersCursor.getColumnIndexOrThrow(FOLDER_ID))
492
493             // Add the folder ID to the list.
494             subfolderIdLongList.add(subfolderId)
495
496             // Get a list of any subfolders of the subfolder.
497             val nestedSubfolderIdList = getListOfSubfolderIds(subfolderId, bookmarksDatabaseHelper)
498
499             // Add each of the subfolder IDs to the list.
500             for (nestedSubfolderId in nestedSubfolderIdList)
501                 subfolderIdLongList.add(nestedSubfolderId)
502         }
503
504         // Return the list of subfolder IDs.
505         return subfolderIdLongList
506     }
507 }