From cd4fc3fabd5ce8ee63f5651a95591647dfcd9898 Mon Sep 17 00:00:00 2001 From: ryan Date: Fri, 21 Aug 2026 22:19:42 +0800 Subject: [PATCH 01/12] add folder path to home screen --- .../android/extensions/FileMenuOptionExt.kt | 3 + .../AddToHomeScreenDialogFragment.kt | 120 ++++++++++++++++++ .../addtohomescreen/FolderShortcutHelper.kt | 60 +++++++++ .../files/filelist/MainFileListFragment.kt | 17 ++- .../ui/activity/FileDisplayActivity.kt | 32 ++++- .../files/FilterFileMenuOptionsUseCase.kt | 4 + .../res/drawable/ic_action_add_to_home.xml | 9 ++ .../src/main/res/menu/file_actions_menu.xml | 6 + opencloudApp/src/main/res/values/strings.xml | 6 + .../domain/files/model/FileMenuOption.kt | 3 +- 10 files changed, 257 insertions(+), 3 deletions(-) create mode 100644 opencloudApp/src/main/java/eu/opencloud/android/presentation/files/addtohomescreen/AddToHomeScreenDialogFragment.kt create mode 100644 opencloudApp/src/main/java/eu/opencloud/android/presentation/files/addtohomescreen/FolderShortcutHelper.kt create mode 100644 opencloudApp/src/main/res/drawable/ic_action_add_to_home.xml diff --git a/opencloudApp/src/main/java/eu/opencloud/android/extensions/FileMenuOptionExt.kt b/opencloudApp/src/main/java/eu/opencloud/android/extensions/FileMenuOptionExt.kt index 57491071e2..a3669ca26a 100644 --- a/opencloudApp/src/main/java/eu/opencloud/android/extensions/FileMenuOptionExt.kt +++ b/opencloudApp/src/main/java/eu/opencloud/android/extensions/FileMenuOptionExt.kt @@ -40,6 +40,7 @@ fun FileMenuOption.toResId() = FileMenuOption.SEND -> R.id.action_send_file FileMenuOption.SET_AV_OFFLINE -> R.id.action_set_available_offline FileMenuOption.UNSET_AV_OFFLINE -> R.id.action_unset_available_offline + FileMenuOption.ADD_TO_HOME_SCREEN -> R.id.action_add_to_home } fun FileMenuOption.toStringResId() = @@ -59,6 +60,7 @@ fun FileMenuOption.toStringResId() = FileMenuOption.SEND -> R.string.actionbar_send_file FileMenuOption.SET_AV_OFFLINE -> R.string.set_available_offline FileMenuOption.UNSET_AV_OFFLINE -> R.string.unset_available_offline + FileMenuOption.ADD_TO_HOME_SCREEN -> R.string.add_to_home_screen } fun FileMenuOption.toDrawableResId() = @@ -78,4 +80,5 @@ fun FileMenuOption.toDrawableResId() = FileMenuOption.SEND -> R.drawable.ic_send_white FileMenuOption.SET_AV_OFFLINE -> R.drawable.ic_action_set_available_offline FileMenuOption.UNSET_AV_OFFLINE -> R.drawable.ic_action_unset_available_offline + FileMenuOption.ADD_TO_HOME_SCREEN -> R.drawable.ic_action_add_to_home } diff --git a/opencloudApp/src/main/java/eu/opencloud/android/presentation/files/addtohomescreen/AddToHomeScreenDialogFragment.kt b/opencloudApp/src/main/java/eu/opencloud/android/presentation/files/addtohomescreen/AddToHomeScreenDialogFragment.kt new file mode 100644 index 0000000000..b89d6d972b --- /dev/null +++ b/opencloudApp/src/main/java/eu/opencloud/android/presentation/files/addtohomescreen/AddToHomeScreenDialogFragment.kt @@ -0,0 +1,120 @@ +package eu.opencloud.android.presentation.files.addtohomescreen + +import android.app.Dialog +import android.os.Bundle +import android.view.WindowManager +import android.widget.EditText +import androidx.appcompat.app.AlertDialog +import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.widget.doOnTextChanged +import androidx.fragment.app.DialogFragment +import com.google.android.material.textfield.TextInputLayout +import eu.opencloud.android.R +import eu.opencloud.android.domain.files.model.OCFile +import eu.opencloud.android.presentation.files.filelist.MainFileListFragment.Companion.MAX_FILENAME_LENGTH +import eu.opencloud.android.presentation.files.filelist.MainFileListFragment.Companion.forbiddenChars +import eu.opencloud.android.utils.PreferenceUtils + +class AddToHomeScreenDialogFragment : DialogFragment() { + + private lateinit var folder: OCFile + private lateinit var listener: AddToHomeScreenListener + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + folder = requireArguments().getParcelable(ARG_FOLDER)!! + listener = parentFragment as AddToHomeScreenListener + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val inflater = requireActivity().layoutInflater + val view = inflater.inflate(R.layout.edit_box_dialog, null) + + view.filterTouchesWhenObscured = + PreferenceUtils.shouldDisallowTouchesWithOtherVisibleWindows(context) + + val coordinatorLayout: CoordinatorLayout = requireActivity().findViewById(R.id.coordinator_layout) + coordinatorLayout.filterTouchesWhenObscured = + PreferenceUtils.shouldDisallowTouchesWithOtherVisibleWindows(context) + + val inputText: EditText = view.findViewById(R.id.user_input) + val inputLayout: TextInputLayout = view.findViewById(R.id.edit_box_input_text_layout) + + inputText.setText(folder.fileName) + inputText.selectAll() + inputText.requestFocus() + + val builder = AlertDialog.Builder(requireActivity()) + builder.setView(view) + .setPositiveButton(R.string.add_to_home_screen_dialog_add_button) { dialog, _ -> + val name = inputText.text.toString().trim() + if (name.isNotBlank()) { + listener.onAddToHomeScreen(name, folder) + } + dialog.dismiss() + } + .setNegativeButton(android.R.string.cancel, null) + .setTitle(R.string.add_to_home_screen_dialog_title) + + val alertDialog = builder.create() + + alertDialog.setOnShowListener { + val okButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE) + okButton.isEnabled = inputText.text.isNullOrBlank().not() + + okButton.setOnClickListener { + val name = inputText.text.toString().trim() + if (name.isNotBlank()) { + listener.onAddToHomeScreen(name, folder) + } + alertDialog.dismiss() + } + } + + inputText.doOnTextChanged { text, _, _, _ -> + val okButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE) + var error: String? = null + + if (text.isNullOrBlank()) { + okButton.isEnabled = false + error = getString(R.string.add_to_home_screen_dialog_error_empty) + } else if (text.length > MAX_FILENAME_LENGTH) { + error = String.format( + getString(R.string.uploader_upload_text_dialog_filename_error_length_max), + MAX_FILENAME_LENGTH + ) + } else if (forbiddenChars.any { text.contains(it) }) { + error = getString(R.string.filename_forbidden_characters) + } else { + okButton.isEnabled = true + } + + if (error != null) { + okButton.isEnabled = false + inputLayout.error = error + } else { + inputLayout.error = null + } + } + + alertDialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE) + return alertDialog + } + + interface AddToHomeScreenListener { + fun onAddToHomeScreen(shortcutName: String, folder: OCFile) + } + + companion object { + const val TAG = "ADD_TO_HOME_SCREEN_DIALOG" + private const val ARG_FOLDER = "ARG_FOLDER" + + @JvmStatic + fun newInstance(folder: OCFile): AddToHomeScreenDialogFragment = + AddToHomeScreenDialogFragment().apply { + arguments = Bundle().apply { + putParcelable(ARG_FOLDER, folder) + } + } + } +} diff --git a/opencloudApp/src/main/java/eu/opencloud/android/presentation/files/addtohomescreen/FolderShortcutHelper.kt b/opencloudApp/src/main/java/eu/opencloud/android/presentation/files/addtohomescreen/FolderShortcutHelper.kt new file mode 100644 index 0000000000..27ea728dc1 --- /dev/null +++ b/opencloudApp/src/main/java/eu/opencloud/android/presentation/files/addtohomescreen/FolderShortcutHelper.kt @@ -0,0 +1,60 @@ +package eu.opencloud.android.presentation.files.addtohomescreen + +import android.content.Context +import android.content.Intent +import android.content.pm.ShortcutInfo +import android.content.pm.ShortcutManager +import android.graphics.drawable.Icon +import android.os.Build +import android.widget.Toast +import androidx.annotation.RequiresApi +import eu.opencloud.android.R +import eu.opencloud.android.domain.files.model.OCFile +import eu.opencloud.android.ui.activity.FileDisplayActivity + +object FolderShortcutHelper { + + const val EXTRA_SHORTCUT_FOLDER_REMOTE_ID = "SHORTCUT_FOLDER_REMOTE_ID" + const val EXTRA_SHORTCUT_FOLDER_REMOTE_PATH = "SHORTCUT_FOLDER_REMOTE_PATH" + const val EXTRA_SHORTCUT_FOLDER_SPACE_ID = "SHORTCUT_FOLDER_SPACE_ID" + + fun createPinnedShortcut(context: Context, folder: OCFile, shortcutName: String) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + createPinnedShortcutApi26(context, folder, shortcutName) + } else { + Toast.makeText(context, context.getString(R.string.add_to_home_screen_shortcut_added), Toast.LENGTH_SHORT).show() + } + } + + @RequiresApi(Build.VERSION_CODES.O) + private fun createPinnedShortcutApi26(context: Context, folder: OCFile, shortcutName: String) { + val shortcutManager = context.getSystemService(ShortcutManager::class.java) + + if (shortcutManager?.isRequestPinShortcutSupported != true) { + Toast.makeText(context, context.getString(R.string.add_to_home_screen_shortcut_added), Toast.LENGTH_SHORT).show() + return + } + + val shortcutId = "folder_${folder.id}" + + val shortcutIntent = Intent(context, FileDisplayActivity::class.java).apply { + action = ACTION_OPEN_SHORTCUT + putExtra(EXTRA_SHORTCUT_FOLDER_REMOTE_ID, folder.remoteId) + putExtra(EXTRA_SHORTCUT_FOLDER_REMOTE_PATH, folder.remotePath) + putExtra(EXTRA_SHORTCUT_FOLDER_SPACE_ID, folder.spaceId) + flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK + } + + val shortcut = ShortcutInfo.Builder(context, shortcutId) + .setShortLabel(shortcutName) + .setLongLabel(shortcutName) + .setIcon(Icon.createWithResource(context, R.mipmap.icon)) + .setIntent(shortcutIntent) + .build() + + shortcutManager.requestPinShortcut(shortcut, null) + Toast.makeText(context, context.getString(R.string.add_to_home_screen_shortcut_added), Toast.LENGTH_SHORT).show() + } + + const val ACTION_OPEN_SHORTCUT = "eu.opencloud.android.ui.activity.action.OPEN_SHORTCUT" +} diff --git a/opencloudApp/src/main/java/eu/opencloud/android/presentation/files/filelist/MainFileListFragment.kt b/opencloudApp/src/main/java/eu/opencloud/android/presentation/files/filelist/MainFileListFragment.kt index 41d7a25d7d..6e3b22d828 100644 --- a/opencloudApp/src/main/java/eu/opencloud/android/presentation/files/filelist/MainFileListFragment.kt +++ b/opencloudApp/src/main/java/eu/opencloud/android/presentation/files/filelist/MainFileListFragment.kt @@ -107,6 +107,8 @@ import eu.opencloud.android.presentation.files.SortType import eu.opencloud.android.presentation.files.ViewType import eu.opencloud.android.presentation.files.createfolder.CreateFolderDialogFragment import eu.opencloud.android.presentation.files.createshortcut.CreateShortcutDialogFragment +import eu.opencloud.android.presentation.files.addtohomescreen.AddToHomeScreenDialogFragment +import eu.opencloud.android.presentation.files.addtohomescreen.FolderShortcutHelper import eu.opencloud.android.presentation.files.operations.FileOperation import eu.opencloud.android.presentation.files.operations.FileOperationsViewModel import eu.opencloud.android.presentation.files.removefile.RemoveFilesDialogFragment @@ -138,7 +140,8 @@ class MainFileListFragment : Fragment(), SortDialogListener, SortOptionsView.CreateFolderListener, SortOptionsView.SortOptionsListener, - CreateShortcutDialogFragment.CreateShortcutListener { + CreateShortcutDialogFragment.CreateShortcutListener, + AddToHomeScreenDialogFragment.AddToHomeScreenListener { private val mainFileListViewModel by viewModel { parametersOf( @@ -737,6 +740,13 @@ class MainFileListFragment : Fragment(), FileMenuOption.UNSET_AV_OFFLINE -> { fileOperationsViewModel.performOperation(FileOperation.UnsetFilesAsAvailableOffline(listOf(file))) } + + FileMenuOption.ADD_TO_HOME_SCREEN -> { + if (file.isFolder) { + val dialog = AddToHomeScreenDialogFragment.newInstance(file) + dialog.show(childFragmentManager, DIALOG_ADD_TO_HOME_SCREEN) + } + } } dialog.hide() dialog.dismiss() @@ -1260,6 +1270,10 @@ class MainFileListFragment : Fragment(), uploadActions?.uploadShortcutFileFromApp(arrayOf(shortcutFilePath)) } + override fun onAddToHomeScreen(shortcutName: String, folder: OCFile) { + FolderShortcutHelper.createPinnedShortcut(requireContext(), folder, shortcutName) + } + override fun onFolderNameSet(newFolderName: String, parentFolder: OCFile) { fileOperationsViewModel.performOperation(FileOperation.CreateFolder(newFolderName, parentFolder)) fileOperationsViewModel.createFolder.observe(viewLifecycleOwner, Event.EventObserver { uiResult: UIResult -> @@ -1617,6 +1631,7 @@ class MainFileListFragment : Fragment(), private const val DIALOG_CREATE_FOLDER = "DIALOG_CREATE_FOLDER" private const val DIALOG_CREATE_SHORTCUT = "DIALOG_CREATE_SHORTCUT" + private const val DIALOG_ADD_TO_HOME_SCREEN = "DIALOG_ADD_TO_HOME_SCREEN" private const val FILE_DOCXF_EXTENSION = "docxf" diff --git a/opencloudApp/src/main/java/eu/opencloud/android/ui/activity/FileDisplayActivity.kt b/opencloudApp/src/main/java/eu/opencloud/android/ui/activity/FileDisplayActivity.kt index a4c9c95f16..7ba2cca55f 100644 --- a/opencloudApp/src/main/java/eu/opencloud/android/ui/activity/FileDisplayActivity.kt +++ b/opencloudApp/src/main/java/eu/opencloud/android/ui/activity/FileDisplayActivity.kt @@ -201,6 +201,7 @@ class FileDisplayActivity : FileActivity(), private var isLightUser = false private var isMultiPersonal = false + private var shortcutFolderToNavigate: OCFile? = null override fun onCreate(savedInstanceState: Bundle?) { Timber.v("onCreate() start") @@ -216,6 +217,7 @@ class FileDisplayActivity : FileActivity(), localBroadcastManager = LocalBroadcastManager.getInstance(this) handleDeepLink() + handleShortcutIntent() /// Load of saved instance state if (savedInstanceState != null) { @@ -348,7 +350,17 @@ class FileDisplayActivity : FileActivity(), }) isLightUser = manageAccountsViewModel.checkUserLight(account.name) isMultiPersonal = capabilitiesViewModel.checkMultiPersonal() - navigateTo(fileListOption, initialState = true) + + if (shortcutFolderToNavigate != null) { + fileListOption = FileListOption.ALL_FILES + setFile(shortcutFolderToNavigate) + initAndShowListOfFiles(fileListOption) + refreshListOfFilesFragment() + updateToolbar(shortcutFolderToNavigate) + shortcutFolderToNavigate = null + } else { + navigateTo(fileListOption, initialState = true) + } } @@ -1966,6 +1978,24 @@ class FileDisplayActivity : FileActivity(), } } + private fun handleShortcutIntent() { + if (intent?.action != eu.opencloud.android.presentation.files.addtohomescreen.FolderShortcutHelper.ACTION_OPEN_SHORTCUT) return + + val shortcutRemotePath = intent?.getStringExtra(eu.opencloud.android.presentation.files.addtohomescreen.FolderShortcutHelper.EXTRA_SHORTCUT_FOLDER_REMOTE_PATH) + val shortcutSpaceId = intent?.getStringExtra(eu.opencloud.android.presentation.files.addtohomescreen.FolderShortcutHelper.EXTRA_SHORTCUT_FOLDER_SPACE_ID) + if (shortcutRemotePath != null) { + val file = storageManager.getFileByPath(shortcutRemotePath, shortcutSpaceId) + if (file != null) { + shortcutFolderToNavigate = file + } else { + showMessageInSnackbar(R.id.list_layout, getString(R.string.default_error_msg)) + } + } + intent?.removeExtra(eu.opencloud.android.presentation.files.addtohomescreen.FolderShortcutHelper.EXTRA_SHORTCUT_FOLDER_REMOTE_ID) + intent?.removeExtra(eu.opencloud.android.presentation.files.addtohomescreen.FolderShortcutHelper.EXTRA_SHORTCUT_FOLDER_REMOTE_PATH) + intent?.removeExtra(eu.opencloud.android.presentation.files.addtohomescreen.FolderShortcutHelper.EXTRA_SHORTCUT_FOLDER_SPACE_ID) + } + private fun onDeepLinkManaged() { collectLatestLifecycleFlow(fileOperationsViewModel.deepLinkFlow) { it?.getContentIfNotHandled()?.let { uiResult -> diff --git a/opencloudApp/src/main/java/eu/opencloud/android/usecases/files/FilterFileMenuOptionsUseCase.kt b/opencloudApp/src/main/java/eu/opencloud/android/usecases/files/FilterFileMenuOptionsUseCase.kt index 416e567b61..5f629f4515 100644 --- a/opencloudApp/src/main/java/eu/opencloud/android/usecases/files/FilterFileMenuOptionsUseCase.kt +++ b/opencloudApp/src/main/java/eu/opencloud/android/usecases/files/FilterFileMenuOptionsUseCase.kt @@ -151,6 +151,10 @@ class FilterFileMenuOptionsUseCase( if (!isAnyFileSynchronizing && !onlyAvailableOfflineFiles && !onlySharedByLinkFiles && hasRemovePermission) { optionsToShow.add(FileMenuOption.REMOVE) } + // Add to Home screen + if (isSingleSelection(files) && anyFolder(files)) { + optionsToShow.add(FileMenuOption.ADD_TO_HOME_SCREEN) + } return optionsToShow } diff --git a/opencloudApp/src/main/res/drawable/ic_action_add_to_home.xml b/opencloudApp/src/main/res/drawable/ic_action_add_to_home.xml new file mode 100644 index 0000000000..45624f5f44 --- /dev/null +++ b/opencloudApp/src/main/res/drawable/ic_action_add_to_home.xml @@ -0,0 +1,9 @@ + + + diff --git a/opencloudApp/src/main/res/menu/file_actions_menu.xml b/opencloudApp/src/main/res/menu/file_actions_menu.xml index c5ad8eeeec..c9c0a625a4 100644 --- a/opencloudApp/src/main/res/menu/file_actions_menu.xml +++ b/opencloudApp/src/main/res/menu/file_actions_menu.xml @@ -110,5 +110,11 @@ android:icon="@drawable/ic_action_delete_white" app:showAsAction="ifRoom" android:orderInCategory="1" /> + diff --git a/opencloudApp/src/main/res/values/strings.xml b/opencloudApp/src/main/res/values/strings.xml index 32bd9ca389..5d58602846 100644 --- a/opencloudApp/src/main/res/values/strings.xml +++ b/opencloudApp/src/main/res/values/strings.xml @@ -861,4 +861,10 @@ Added text labels on bottom bar Text labels were added and default active indicator is used to show which section is selected on the bottom bar + Add to Home screen + Add to Home screen + Add + The name cannot be empty + Shortcut added to Home screen + diff --git a/opencloudDomain/src/main/java/eu/opencloud/android/domain/files/model/FileMenuOption.kt b/opencloudDomain/src/main/java/eu/opencloud/android/domain/files/model/FileMenuOption.kt index 3ea6b6cd36..8983a045db 100644 --- a/opencloudDomain/src/main/java/eu/opencloud/android/domain/files/model/FileMenuOption.kt +++ b/opencloudDomain/src/main/java/eu/opencloud/android/domain/files/model/FileMenuOption.kt @@ -22,5 +22,6 @@ package eu.opencloud.android.domain.files.model enum class FileMenuOption { SELECT_ALL, SELECT_INVERSE, DOWNLOAD, RENAME, MOVE, COPY, REMOVE, OPEN_WITH, - SYNC, CANCEL_SYNC, SHARE, DETAILS, SEND, SET_AV_OFFLINE, UNSET_AV_OFFLINE; + SYNC, CANCEL_SYNC, SHARE, DETAILS, SEND, SET_AV_OFFLINE, UNSET_AV_OFFLINE, + ADD_TO_HOME_SCREEN; } From dd2968d582438abe5fe184fc1cebe0d5f15d701a Mon Sep 17 00:00:00 2001 From: Lightz2002 <68954690+Lightz2002@users.noreply.github.com> Date: Mon, 31 Aug 2026 22:04:08 +0700 Subject: [PATCH 02/12] Add Filename Validation Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../AddToHomeScreenDialogFragment.kt | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/opencloudApp/src/main/java/eu/opencloud/android/presentation/files/addtohomescreen/AddToHomeScreenDialogFragment.kt b/opencloudApp/src/main/java/eu/opencloud/android/presentation/files/addtohomescreen/AddToHomeScreenDialogFragment.kt index b89d6d972b..d718637692 100644 --- a/opencloudApp/src/main/java/eu/opencloud/android/presentation/files/addtohomescreen/AddToHomeScreenDialogFragment.kt +++ b/opencloudApp/src/main/java/eu/opencloud/android/presentation/files/addtohomescreen/AddToHomeScreenDialogFragment.kt @@ -64,10 +64,22 @@ class AddToHomeScreenDialogFragment : DialogFragment() { okButton.setOnClickListener { val name = inputText.text.toString().trim() - if (name.isNotBlank()) { + val error = when { + name.isBlank() -> getString(R.string.add_to_home_screen_dialog_error_empty) + name.length > MAX_FILENAME_LENGTH -> String.format( + getString(R.string.uploader_upload_text_dialog_filename_error_length_max), + MAX_FILENAME_LENGTH + ) + forbiddenChars.any { name.contains(it) } -> getString(R.string.filename_forbidden_characters) + else -> null + } + + if (error == null) { listener.onAddToHomeScreen(name, folder) + alertDialog.dismiss() + } else { + inputLayout.error = error } - alertDialog.dismiss() } } From 2df383dbce104f38f1e6f3dd28d586438ea7d93b Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 10 Sep 2026 21:10:02 +0800 Subject: [PATCH 03/12] shorten method name --- .../addtohomescreen/FolderShortcutHelper.kt | 4 +- .../ui/activity/FileDisplayActivity.kt | 13 +- .../files/FilterFileMenuOptionsUseCase.kt | 152 +++++++++++------- 3 files changed, 103 insertions(+), 66 deletions(-) diff --git a/opencloudApp/src/main/java/eu/opencloud/android/presentation/files/addtohomescreen/FolderShortcutHelper.kt b/opencloudApp/src/main/java/eu/opencloud/android/presentation/files/addtohomescreen/FolderShortcutHelper.kt index 27ea728dc1..d5478012b9 100644 --- a/opencloudApp/src/main/java/eu/opencloud/android/presentation/files/addtohomescreen/FolderShortcutHelper.kt +++ b/opencloudApp/src/main/java/eu/opencloud/android/presentation/files/addtohomescreen/FolderShortcutHelper.kt @@ -17,6 +17,8 @@ object FolderShortcutHelper { const val EXTRA_SHORTCUT_FOLDER_REMOTE_ID = "SHORTCUT_FOLDER_REMOTE_ID" const val EXTRA_SHORTCUT_FOLDER_REMOTE_PATH = "SHORTCUT_FOLDER_REMOTE_PATH" const val EXTRA_SHORTCUT_FOLDER_SPACE_ID = "SHORTCUT_FOLDER_SPACE_ID" + const val ACTION_OPEN_SHORTCUT = + "eu.opencloud.android.ui.activity.action.OPEN_SHORTCUT" fun createPinnedShortcut(context: Context, folder: OCFile, shortcutName: String) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { @@ -55,6 +57,4 @@ object FolderShortcutHelper { shortcutManager.requestPinShortcut(shortcut, null) Toast.makeText(context, context.getString(R.string.add_to_home_screen_shortcut_added), Toast.LENGTH_SHORT).show() } - - const val ACTION_OPEN_SHORTCUT = "eu.opencloud.android.ui.activity.action.OPEN_SHORTCUT" } diff --git a/opencloudApp/src/main/java/eu/opencloud/android/ui/activity/FileDisplayActivity.kt b/opencloudApp/src/main/java/eu/opencloud/android/ui/activity/FileDisplayActivity.kt index 7ba2cca55f..d0524a568e 100644 --- a/opencloudApp/src/main/java/eu/opencloud/android/ui/activity/FileDisplayActivity.kt +++ b/opencloudApp/src/main/java/eu/opencloud/android/ui/activity/FileDisplayActivity.kt @@ -1979,10 +1979,11 @@ class FileDisplayActivity : FileActivity(), } private fun handleShortcutIntent() { - if (intent?.action != eu.opencloud.android.presentation.files.addtohomescreen.FolderShortcutHelper.ACTION_OPEN_SHORTCUT) return + val sc = eu.opencloud.android.presentation.files.addtohomescreen.FolderShortcutHelper + if (intent?.action != sc.ACTION_OPEN_SHORTCUT) return - val shortcutRemotePath = intent?.getStringExtra(eu.opencloud.android.presentation.files.addtohomescreen.FolderShortcutHelper.EXTRA_SHORTCUT_FOLDER_REMOTE_PATH) - val shortcutSpaceId = intent?.getStringExtra(eu.opencloud.android.presentation.files.addtohomescreen.FolderShortcutHelper.EXTRA_SHORTCUT_FOLDER_SPACE_ID) + val shortcutRemotePath = intent?.getStringExtra(sc.EXTRA_SHORTCUT_FOLDER_REMOTE_PATH) + val shortcutSpaceId = intent?.getStringExtra(sc.EXTRA_SHORTCUT_FOLDER_SPACE_ID) if (shortcutRemotePath != null) { val file = storageManager.getFileByPath(shortcutRemotePath, shortcutSpaceId) if (file != null) { @@ -1991,9 +1992,9 @@ class FileDisplayActivity : FileActivity(), showMessageInSnackbar(R.id.list_layout, getString(R.string.default_error_msg)) } } - intent?.removeExtra(eu.opencloud.android.presentation.files.addtohomescreen.FolderShortcutHelper.EXTRA_SHORTCUT_FOLDER_REMOTE_ID) - intent?.removeExtra(eu.opencloud.android.presentation.files.addtohomescreen.FolderShortcutHelper.EXTRA_SHORTCUT_FOLDER_REMOTE_PATH) - intent?.removeExtra(eu.opencloud.android.presentation.files.addtohomescreen.FolderShortcutHelper.EXTRA_SHORTCUT_FOLDER_SPACE_ID) + intent?.removeExtra(sc.EXTRA_SHORTCUT_FOLDER_REMOTE_ID) + intent?.removeExtra(sc.EXTRA_SHORTCUT_FOLDER_REMOTE_PATH) + intent?.removeExtra(sc.EXTRA_SHORTCUT_FOLDER_SPACE_ID) } private fun onDeepLinkManaged() { diff --git a/opencloudApp/src/main/java/eu/opencloud/android/usecases/files/FilterFileMenuOptionsUseCase.kt b/opencloudApp/src/main/java/eu/opencloud/android/usecases/files/FilterFileMenuOptionsUseCase.kt index 5f629f4515..7554d853ff 100644 --- a/opencloudApp/src/main/java/eu/opencloud/android/usecases/files/FilterFileMenuOptionsUseCase.kt +++ b/opencloudApp/src/main/java/eu/opencloud/android/usecases/files/FilterFileMenuOptionsUseCase.kt @@ -37,27 +37,43 @@ class FilterFileMenuOptionsUseCase( private val getSpaceWithSpecialsByIdForAccountUseCase: GetSpaceWithSpecialsByIdForAccountUseCase, ) : BaseUseCase, FilterFileMenuOptionsUseCase.Params>() { override fun run(params: Params): MutableList { - val optionsToShow = mutableListOf() val files = params.files - if (files.isEmpty()) { return mutableListOf() } + val state = buildMenuState(params) + return buildMenuOptions(files, params, state) + } + + private data class MenuState( + val isAnyFileSynchronizing: Boolean, + val isAnyFileVideoStreaming: Boolean, + val hasRenamePermission: Boolean, + val hasMovePermission: Boolean, + val hasRemovePermission: Boolean, + val hasResharePermission: Boolean, + val isPersonalSpace: Boolean, + val resharingAllowed: Boolean, + ) + private fun buildMenuState(params: Params): MenuState { + val files = params.files val filesSyncInfo = params.filesSyncInfo val capability = capabilityRepository.getStoredCapabilities(params.accountName) - val space = getSpaceWithSpecialsByIdForAccountUseCase(GetSpaceWithSpecialsByIdForAccountUseCase.Params( - spaceId = files.first().spaceId, - accountName = params.accountName, - )) - + val space = getSpaceWithSpecialsByIdForAccountUseCase( + GetSpaceWithSpecialsByIdForAccountUseCase.Params( + spaceId = files.first().spaceId, + accountName = params.accountName, + ) + ) val isAnyFileSynchronizing: Boolean = if (filesSyncInfo.isEmpty()) { anyFileSynchronizingLookingIntoWorkers(files, params.accountName) } else { anyFileSynchronizingLookingIIntoFilesSyncInfo(filesSyncInfo) } val isAnyFileVideoPreviewing = params.isAnyFileVideoPreviewing - val isAnyFileVideoStreaming = isAnyFileVideoPreviewing && !anyFileDownloaded(files) + val isAnyFileVideoStreaming = + isAnyFileVideoPreviewing && !anyFileDownloaded(files) val hasRenamePermission: Boolean = if (isSingleSelection(files)) { files.first().hasRenamePermission } else { @@ -71,87 +87,107 @@ class FilterFileMenuOptionsUseCase( false } val isPersonalSpace = space?.isPersonal ?: true - val resharingAllowed = capability?.let { !anyFileSharedWithMe(files) || it.filesSharingResharing.isTrue } ?: false - val displaySelectAll = params.displaySelectAll - val displaySelectInverse = params.displaySelectInverse - val onlyAvailableOfflineFiles = params.onlyAvailableOfflineFiles - val onlySharedByLinkFiles = params.onlySharedByLinkFiles - val shareViaLinkAllowed = params.shareViaLinkAllowed - val shareWithUsersAllowed = params.shareWithUsersAllowed - val sendAllowed = params.sendAllowed - - val noSyncAndPreviewing = !isAnyFileSynchronizing && !isAnyFileVideoPreviewing - val noSyncAndStreaming = !isAnyFileSynchronizing && !isAnyFileVideoStreaming - val shareViaLinkOrWithUsersAllowed = shareViaLinkAllowed || shareWithUsersAllowed - val noFilesDownloadedOrIsSingleFile = allFilesDownloaded(files) || isSingleFile(files) - - // Select all - if (displaySelectAll) { + val resharingAllowed = capability?.let { + !anyFileSharedWithMe(files) || it.filesSharingResharing.isTrue + } ?: false + return MenuState( + isAnyFileSynchronizing = isAnyFileSynchronizing, + isAnyFileVideoStreaming = isAnyFileVideoStreaming, + hasRenamePermission = hasRenamePermission, + hasMovePermission = hasMovePermission, + hasRemovePermission = hasRemovePermission, + hasResharePermission = hasResharePermission, + isPersonalSpace = isPersonalSpace, + resharingAllowed = resharingAllowed, + ) + } + + private fun buildMenuOptions( + files: List, + params: Params, + state: MenuState, + ): MutableList { + val optionsToShow = mutableListOf() + val noSyncAndPreviewing = + !state.isAnyFileSynchronizing && !params.isAnyFileVideoPreviewing + val noSyncAndStreaming = + !state.isAnyFileSynchronizing && !state.isAnyFileVideoStreaming + val shareViaLinkOrWithUsersAllowed = + params.shareViaLinkAllowed || params.shareWithUsersAllowed + val noFilesDownloadedOrIsSingleFile = + allFilesDownloaded(files) || isSingleFile(files) + + if (params.displaySelectAll) { optionsToShow.add(FileMenuOption.SELECT_ALL) } - // Select inverse - if (displaySelectInverse) { + if (params.displaySelectInverse) { optionsToShow.add(FileMenuOption.SELECT_INVERSE) } - // Share - if (!onlyAvailableOfflineFiles && shareViaLinkOrWithUsersAllowed && resharingAllowed && - isPersonalSpace && hasResharePermission) { + if (!params.onlyAvailableOfflineFiles && shareViaLinkOrWithUsersAllowed && + state.resharingAllowed && state.isPersonalSpace && + state.hasResharePermission + ) { optionsToShow.add(FileMenuOption.SHARE) } - // Open with (different to preview!) - if (!isAnyFileSynchronizing && isSingleFile(files)) { + if (!state.isAnyFileSynchronizing && isSingleFile(files)) { optionsToShow.add(FileMenuOption.OPEN_WITH) } - // Download - if (noSyncAndPreviewing && !onlyAvailableOfflineFiles && !onlySharedByLinkFiles && - !anyFolder(files) && !anyFileDownloaded(files)) { + if (noSyncAndPreviewing && !params.onlyAvailableOfflineFiles && + !params.onlySharedByLinkFiles && !anyFolder(files) && + !anyFileDownloaded(files) + ) { optionsToShow.add(FileMenuOption.DOWNLOAD) } - // Synchronize - if (!isAnyFileSynchronizing && !onlyAvailableOfflineFiles && !onlySharedByLinkFiles && - (anyFileDownloaded(files) || anyFolder(files))) { + if (!state.isAnyFileSynchronizing && !params.onlyAvailableOfflineFiles && + !params.onlySharedByLinkFiles && + (anyFileDownloaded(files) || anyFolder(files)) + ) { optionsToShow.add(FileMenuOption.SYNC) } - // Cancel sync - if (isAnyFileSynchronizing && !onlyAvailableOfflineFiles && !onlySharedByLinkFiles && !anyAvailableOfflineFile(files)) { + if (state.isAnyFileSynchronizing && !params.onlyAvailableOfflineFiles && + !params.onlySharedByLinkFiles && + !anyAvailableOfflineFile(files) + ) { optionsToShow.add(FileMenuOption.CANCEL_SYNC) } - // Rename - if (noSyncAndPreviewing && !onlyAvailableOfflineFiles && !onlySharedByLinkFiles && - hasRenamePermission) { + if (noSyncAndPreviewing && !params.onlyAvailableOfflineFiles && + !params.onlySharedByLinkFiles && state.hasRenamePermission + ) { optionsToShow.add(FileMenuOption.RENAME) } - // Move - if (noSyncAndPreviewing && !onlyAvailableOfflineFiles && !onlySharedByLinkFiles && - hasMovePermission) { + if (noSyncAndPreviewing && !params.onlyAvailableOfflineFiles && + !params.onlySharedByLinkFiles && state.hasMovePermission + ) { optionsToShow.add(FileMenuOption.MOVE) } - // Copy - if (noSyncAndPreviewing && !onlyAvailableOfflineFiles && !onlySharedByLinkFiles) { + if (noSyncAndPreviewing && !params.onlyAvailableOfflineFiles && + !params.onlySharedByLinkFiles + ) { optionsToShow.add(FileMenuOption.COPY) } - // Send - if (noSyncAndStreaming && !onlyAvailableOfflineFiles && !anyFolder(files) && - noFilesDownloadedOrIsSingleFile && sendAllowed) { + if (noSyncAndStreaming && !params.onlyAvailableOfflineFiles && + !anyFolder(files) && noFilesDownloadedOrIsSingleFile && + params.sendAllowed + ) { optionsToShow.add(FileMenuOption.SEND) } - // Set as available offline - if (!isAnyFileSynchronizing && anyNotAvailableOfflineFile(files) && !isAnyFileVideoStreaming) { + if (!state.isAnyFileSynchronizing && + anyNotAvailableOfflineFile(files) && + !state.isAnyFileVideoStreaming + ) { optionsToShow.add(FileMenuOption.SET_AV_OFFLINE) } - // Unset as available offline - if (anyAvailableOfflineFile(files) && !isAnyFileVideoStreaming) { + if (anyAvailableOfflineFile(files) && !state.isAnyFileVideoStreaming) { optionsToShow.add(FileMenuOption.UNSET_AV_OFFLINE) } - // Details if (isSingleFile(files)) { optionsToShow.add(FileMenuOption.DETAILS) } - // Remove - if (!isAnyFileSynchronizing && !onlyAvailableOfflineFiles && !onlySharedByLinkFiles && hasRemovePermission) { + if (!state.isAnyFileSynchronizing && !params.onlyAvailableOfflineFiles && + !params.onlySharedByLinkFiles && state.hasRemovePermission + ) { optionsToShow.add(FileMenuOption.REMOVE) } - // Add to Home screen if (isSingleSelection(files) && anyFolder(files)) { optionsToShow.add(FileMenuOption.ADD_TO_HOME_SCREEN) } From 530696f1d541401d0a772420218b8f420123e12b Mon Sep 17 00:00:00 2001 From: ryan Date: Fri, 18 Sep 2026 23:14:57 +0800 Subject: [PATCH 04/12] prevent app crash after folder deleted or renamed --- .../eu/opencloud/android/extensions/ActivityExt.kt | 11 +++++++++-- .../android/ui/activity/FileDisplayActivity.kt | 8 ++++++-- opencloudApp/src/main/res/values/strings.xml | 1 + 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/opencloudApp/src/main/java/eu/opencloud/android/extensions/ActivityExt.kt b/opencloudApp/src/main/java/eu/opencloud/android/extensions/ActivityExt.kt index 2c9a6171bf..cb25383850 100644 --- a/opencloudApp/src/main/java/eu/opencloud/android/extensions/ActivityExt.kt +++ b/opencloudApp/src/main/java/eu/opencloud/android/extensions/ActivityExt.kt @@ -32,6 +32,7 @@ import android.content.pm.ResolveInfo import android.net.Uri import android.text.method.LinkMovementMethod import android.util.TypedValue +import android.view.View import android.view.inputmethod.InputMethodManager import android.webkit.MimeTypeMap import android.widget.LinearLayout @@ -93,8 +94,14 @@ fun Activity.showMessageInSnackbar( duration: Int = Snackbar.LENGTH_LONG ) { // edge-to-edge - val view = if (layoutId == android.R.id.content) window.decorView else findViewById(layoutId) - Snackbar.make(view, message, duration).show() + val view = if (layoutId == android.R.id.content) window.decorView else findViewById(layoutId) + if (view != null) { + Snackbar.make(view, message, duration).show() + } else { + // The requested layout may not be attached yet (e.g. during activity creation). + // Fall back to a Toast, which only needs a Context, so we never crash. + Toast.makeText(this, message, Toast.LENGTH_LONG).show() + } } fun Activity.showErrorInToast( diff --git a/opencloudApp/src/main/java/eu/opencloud/android/ui/activity/FileDisplayActivity.kt b/opencloudApp/src/main/java/eu/opencloud/android/ui/activity/FileDisplayActivity.kt index d0524a568e..2b5a3b6cfe 100644 --- a/opencloudApp/src/main/java/eu/opencloud/android/ui/activity/FileDisplayActivity.kt +++ b/opencloudApp/src/main/java/eu/opencloud/android/ui/activity/FileDisplayActivity.kt @@ -217,7 +217,6 @@ class FileDisplayActivity : FileActivity(), localBroadcastManager = LocalBroadcastManager.getInstance(this) handleDeepLink() - handleShortcutIntent() /// Load of saved instance state if (savedInstanceState != null) { @@ -256,6 +255,8 @@ class FileDisplayActivity : FileActivity(), val view = binding.root setContentView(view) + handleShortcutIntent() + // setup toolbar setupRootToolbar( isSearchEnabled = true, @@ -1989,7 +1990,10 @@ class FileDisplayActivity : FileActivity(), if (file != null) { shortcutFolderToNavigate = file } else { - showMessageInSnackbar(R.id.list_layout, getString(R.string.default_error_msg)) + showMessageInSnackbar( + R.id.list_layout, + getString(R.string.add_to_home_screen_shortcut_folder_missing) + ) } } intent?.removeExtra(sc.EXTRA_SHORTCUT_FOLDER_REMOTE_ID) diff --git a/opencloudApp/src/main/res/values/strings.xml b/opencloudApp/src/main/res/values/strings.xml index ede9aeb7da..6e88621e31 100644 --- a/opencloudApp/src/main/res/values/strings.xml +++ b/opencloudApp/src/main/res/values/strings.xml @@ -867,5 +867,6 @@ Add The name cannot be empty Shortcut added to Home screen + This folder is no longer available. It may have been removed or renamed. From be341906cb2ce14c4b6e99be32232340a1684b9d Mon Sep 17 00:00:00 2001 From: ryan Date: Sat, 3 Oct 2026 09:31:38 +0800 Subject: [PATCH 05/12] fix wrong notif message handling --- .../addtohomescreen/FolderShortcutHelper.kt | 40 +++++++++++++------ opencloudApp/src/main/res/values/strings.xml | 2 + 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/opencloudApp/src/main/java/eu/opencloud/android/presentation/files/addtohomescreen/FolderShortcutHelper.kt b/opencloudApp/src/main/java/eu/opencloud/android/presentation/files/addtohomescreen/FolderShortcutHelper.kt index d5478012b9..f2b7d571a4 100644 --- a/opencloudApp/src/main/java/eu/opencloud/android/presentation/files/addtohomescreen/FolderShortcutHelper.kt +++ b/opencloudApp/src/main/java/eu/opencloud/android/presentation/files/addtohomescreen/FolderShortcutHelper.kt @@ -2,12 +2,12 @@ package eu.opencloud.android.presentation.files.addtohomescreen import android.content.Context import android.content.Intent +import android.content.pm.PinShortcutInfo import android.content.pm.ShortcutInfo import android.content.pm.ShortcutManager import android.graphics.drawable.Icon -import android.os.Build import android.widget.Toast -import androidx.annotation.RequiresApi +import androidx.core.content.ContextCompat import eu.opencloud.android.R import eu.opencloud.android.domain.files.model.OCFile import eu.opencloud.android.ui.activity.FileDisplayActivity @@ -21,22 +21,35 @@ object FolderShortcutHelper { "eu.opencloud.android.ui.activity.action.OPEN_SHORTCUT" fun createPinnedShortcut(context: Context, folder: OCFile, shortcutName: String) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - createPinnedShortcutApi26(context, folder, shortcutName) - } else { - Toast.makeText(context, context.getString(R.string.add_to_home_screen_shortcut_added), Toast.LENGTH_SHORT).show() + if (shortcutName.isBlank()) { + showMessage(context, R.string.add_to_home_screen_dialog_error_empty) + return } - } - @RequiresApi(Build.VERSION_CODES.O) - private fun createPinnedShortcutApi26(context: Context, folder: OCFile, shortcutName: String) { val shortcutManager = context.getSystemService(ShortcutManager::class.java) if (shortcutManager?.isRequestPinShortcutSupported != true) { - Toast.makeText(context, context.getString(R.string.add_to_home_screen_shortcut_added), Toast.LENGTH_SHORT).show() + showMessage(context, R.string.add_to_home_screen_shortcut_not_supported) return } + PinShortcutInfo.requestPinShortcut(context, null).requestPinShortcut( + shortcutManager, + buildShortcutInfo(context, folder, shortcutName), + ContextCompat.getMainExecutor(context), + object : PinShortcutInfo.CallbackHandler() { + override fun onResultShortcutAdded(resultInfo: PinShortcutInfo) { + showMessage(context, R.string.add_to_home_screen_shortcut_added) + } + + override fun onResultShortcutFailed(resultInfo: PinShortcutInfo) { + showMessage(context, R.string.add_to_home_screen_shortcut_add_failed) + } + } + ) + } + + private fun buildShortcutInfo(context: Context, folder: OCFile, shortcutName: String): ShortcutInfo { val shortcutId = "folder_${folder.id}" val shortcutIntent = Intent(context, FileDisplayActivity::class.java).apply { @@ -47,14 +60,15 @@ object FolderShortcutHelper { flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK } - val shortcut = ShortcutInfo.Builder(context, shortcutId) + return ShortcutInfo.Builder(context, shortcutId) .setShortLabel(shortcutName) .setLongLabel(shortcutName) .setIcon(Icon.createWithResource(context, R.mipmap.icon)) .setIntent(shortcutIntent) .build() + } - shortcutManager.requestPinShortcut(shortcut, null) - Toast.makeText(context, context.getString(R.string.add_to_home_screen_shortcut_added), Toast.LENGTH_SHORT).show() + private fun showMessage(context: Context, messageRes: Int) { + Toast.makeText(context, context.getString(messageRes), Toast.LENGTH_SHORT).show() } } diff --git a/opencloudApp/src/main/res/values/strings.xml b/opencloudApp/src/main/res/values/strings.xml index 6e88621e31..cfbec9a121 100644 --- a/opencloudApp/src/main/res/values/strings.xml +++ b/opencloudApp/src/main/res/values/strings.xml @@ -867,6 +867,8 @@ Add The name cannot be empty Shortcut added to Home screen + Adding a Home screen shortcut is not supported by your launcher. + The shortcut could not be added to the Home screen. This folder is no longer available. It may have been removed or renamed. From ba4bd664ba5d1a2b96bb608e74ac0b580f2b4929 Mon Sep 17 00:00:00 2001 From: ryan Date: Sat, 3 Oct 2026 10:50:36 +0800 Subject: [PATCH 06/12] Revert "fix wrong notif message handling" This reverts commit be341906cb2ce14c4b6e99be32232340a1684b9d. --- .../addtohomescreen/FolderShortcutHelper.kt | 40 ++++++------------- opencloudApp/src/main/res/values/strings.xml | 2 - 2 files changed, 13 insertions(+), 29 deletions(-) diff --git a/opencloudApp/src/main/java/eu/opencloud/android/presentation/files/addtohomescreen/FolderShortcutHelper.kt b/opencloudApp/src/main/java/eu/opencloud/android/presentation/files/addtohomescreen/FolderShortcutHelper.kt index f2b7d571a4..d5478012b9 100644 --- a/opencloudApp/src/main/java/eu/opencloud/android/presentation/files/addtohomescreen/FolderShortcutHelper.kt +++ b/opencloudApp/src/main/java/eu/opencloud/android/presentation/files/addtohomescreen/FolderShortcutHelper.kt @@ -2,12 +2,12 @@ package eu.opencloud.android.presentation.files.addtohomescreen import android.content.Context import android.content.Intent -import android.content.pm.PinShortcutInfo import android.content.pm.ShortcutInfo import android.content.pm.ShortcutManager import android.graphics.drawable.Icon +import android.os.Build import android.widget.Toast -import androidx.core.content.ContextCompat +import androidx.annotation.RequiresApi import eu.opencloud.android.R import eu.opencloud.android.domain.files.model.OCFile import eu.opencloud.android.ui.activity.FileDisplayActivity @@ -21,35 +21,22 @@ object FolderShortcutHelper { "eu.opencloud.android.ui.activity.action.OPEN_SHORTCUT" fun createPinnedShortcut(context: Context, folder: OCFile, shortcutName: String) { - if (shortcutName.isBlank()) { - showMessage(context, R.string.add_to_home_screen_dialog_error_empty) - return + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + createPinnedShortcutApi26(context, folder, shortcutName) + } else { + Toast.makeText(context, context.getString(R.string.add_to_home_screen_shortcut_added), Toast.LENGTH_SHORT).show() } + } + @RequiresApi(Build.VERSION_CODES.O) + private fun createPinnedShortcutApi26(context: Context, folder: OCFile, shortcutName: String) { val shortcutManager = context.getSystemService(ShortcutManager::class.java) if (shortcutManager?.isRequestPinShortcutSupported != true) { - showMessage(context, R.string.add_to_home_screen_shortcut_not_supported) + Toast.makeText(context, context.getString(R.string.add_to_home_screen_shortcut_added), Toast.LENGTH_SHORT).show() return } - PinShortcutInfo.requestPinShortcut(context, null).requestPinShortcut( - shortcutManager, - buildShortcutInfo(context, folder, shortcutName), - ContextCompat.getMainExecutor(context), - object : PinShortcutInfo.CallbackHandler() { - override fun onResultShortcutAdded(resultInfo: PinShortcutInfo) { - showMessage(context, R.string.add_to_home_screen_shortcut_added) - } - - override fun onResultShortcutFailed(resultInfo: PinShortcutInfo) { - showMessage(context, R.string.add_to_home_screen_shortcut_add_failed) - } - } - ) - } - - private fun buildShortcutInfo(context: Context, folder: OCFile, shortcutName: String): ShortcutInfo { val shortcutId = "folder_${folder.id}" val shortcutIntent = Intent(context, FileDisplayActivity::class.java).apply { @@ -60,15 +47,14 @@ object FolderShortcutHelper { flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK } - return ShortcutInfo.Builder(context, shortcutId) + val shortcut = ShortcutInfo.Builder(context, shortcutId) .setShortLabel(shortcutName) .setLongLabel(shortcutName) .setIcon(Icon.createWithResource(context, R.mipmap.icon)) .setIntent(shortcutIntent) .build() - } - private fun showMessage(context: Context, messageRes: Int) { - Toast.makeText(context, context.getString(messageRes), Toast.LENGTH_SHORT).show() + shortcutManager.requestPinShortcut(shortcut, null) + Toast.makeText(context, context.getString(R.string.add_to_home_screen_shortcut_added), Toast.LENGTH_SHORT).show() } } diff --git a/opencloudApp/src/main/res/values/strings.xml b/opencloudApp/src/main/res/values/strings.xml index cfbec9a121..6e88621e31 100644 --- a/opencloudApp/src/main/res/values/strings.xml +++ b/opencloudApp/src/main/res/values/strings.xml @@ -867,8 +867,6 @@ Add The name cannot be empty Shortcut added to Home screen - Adding a Home screen shortcut is not supported by your launcher. - The shortcut could not be added to the Home screen. This folder is no longer available. It may have been removed or renamed. From d0800fa824ad400862301cf46cac9bca64f94956 Mon Sep 17 00:00:00 2001 From: ryan Date: Sat, 3 Oct 2026 11:00:28 +0800 Subject: [PATCH 07/12] invert wrong toast message --- .../files/addtohomescreen/FolderShortcutHelper.kt | 4 ++-- opencloudApp/src/main/res/values/strings.xml | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/opencloudApp/src/main/java/eu/opencloud/android/presentation/files/addtohomescreen/FolderShortcutHelper.kt b/opencloudApp/src/main/java/eu/opencloud/android/presentation/files/addtohomescreen/FolderShortcutHelper.kt index d5478012b9..a2ad1fbf7a 100644 --- a/opencloudApp/src/main/java/eu/opencloud/android/presentation/files/addtohomescreen/FolderShortcutHelper.kt +++ b/opencloudApp/src/main/java/eu/opencloud/android/presentation/files/addtohomescreen/FolderShortcutHelper.kt @@ -24,7 +24,7 @@ object FolderShortcutHelper { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { createPinnedShortcutApi26(context, folder, shortcutName) } else { - Toast.makeText(context, context.getString(R.string.add_to_home_screen_shortcut_added), Toast.LENGTH_SHORT).show() + Toast.makeText(context, context.getString(R.string.add_to_home_screen_shortcut_not_supported), Toast.LENGTH_SHORT).show() } } @@ -33,7 +33,7 @@ object FolderShortcutHelper { val shortcutManager = context.getSystemService(ShortcutManager::class.java) if (shortcutManager?.isRequestPinShortcutSupported != true) { - Toast.makeText(context, context.getString(R.string.add_to_home_screen_shortcut_added), Toast.LENGTH_SHORT).show() + Toast.makeText(context, context.getString(R.string.add_to_home_screen_shortcut_not_supported), Toast.LENGTH_SHORT).show() return } diff --git a/opencloudApp/src/main/res/values/strings.xml b/opencloudApp/src/main/res/values/strings.xml index 6e88621e31..ef30203c8e 100644 --- a/opencloudApp/src/main/res/values/strings.xml +++ b/opencloudApp/src/main/res/values/strings.xml @@ -867,6 +867,7 @@ Add The name cannot be empty Shortcut added to Home screen + Add Shortcut to Home screen is not supported This folder is no longer available. It may have been removed or renamed. From 62f843b7d0c8c043f536ef0332659dcc822c167b Mon Sep 17 00:00:00 2001 From: ryan Date: Tue, 6 Oct 2026 14:39:54 +0000 Subject: [PATCH 08/12] Add exported trampoline so pinned folder shortcuts can open the app The launcher starts shortcut intents from another UID, so the non-exported FileDisplayActivity was rejected with a SecurityException. A dedicated exported trampoline validates the shortcut action and forwards only the shortcut extras to the real activity. --- opencloudApp/src/main/AndroidManifest.xml | 13 ++++++++ .../addtohomescreen/FolderShortcutHelper.kt | 3 +- .../ShortcutTrampolineActivity.kt | 33 +++++++++++++++++++ 3 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 opencloudApp/src/main/java/eu/opencloud/android/presentation/files/addtohomescreen/ShortcutTrampolineActivity.kt diff --git a/opencloudApp/src/main/AndroidManifest.xml b/opencloudApp/src/main/AndroidManifest.xml index 38c990ad1a..0e410c765c 100644 --- a/opencloudApp/src/main/AndroidManifest.xml +++ b/opencloudApp/src/main/AndroidManifest.xml @@ -111,6 +111,19 @@ android:configChanges="orientation|screenSize" android:theme="@style/Theme.openCloud.Toolbar.Drawer" android:windowSoftInputMode="adjustPan" /> + + + + + + + + Date: Tue, 6 Oct 2026 14:40:59 +0000 Subject: [PATCH 09/12] Switch to the shortcut folder's account before resolving it The shortcut carried only path and space, so after an account switch the same path resolved against the wrong account (or not at all). Include the folder owner in the shortcut intent and restart with that account selected before looking the folder up. --- .../addtohomescreen/FolderShortcutHelper.kt | 2 ++ .../ShortcutTrampolineActivity.kt | 1 + .../ui/activity/FileDisplayActivity.kt | 23 +++++++++++++++++++ 3 files changed, 26 insertions(+) diff --git a/opencloudApp/src/main/java/eu/opencloud/android/presentation/files/addtohomescreen/FolderShortcutHelper.kt b/opencloudApp/src/main/java/eu/opencloud/android/presentation/files/addtohomescreen/FolderShortcutHelper.kt index e02853a061..8c33085c45 100644 --- a/opencloudApp/src/main/java/eu/opencloud/android/presentation/files/addtohomescreen/FolderShortcutHelper.kt +++ b/opencloudApp/src/main/java/eu/opencloud/android/presentation/files/addtohomescreen/FolderShortcutHelper.kt @@ -16,6 +16,7 @@ object FolderShortcutHelper { const val EXTRA_SHORTCUT_FOLDER_REMOTE_ID = "SHORTCUT_FOLDER_REMOTE_ID" const val EXTRA_SHORTCUT_FOLDER_REMOTE_PATH = "SHORTCUT_FOLDER_REMOTE_PATH" const val EXTRA_SHORTCUT_FOLDER_SPACE_ID = "SHORTCUT_FOLDER_SPACE_ID" + const val EXTRA_SHORTCUT_FOLDER_ACCOUNT = "SHORTCUT_FOLDER_ACCOUNT" const val ACTION_OPEN_SHORTCUT = "eu.opencloud.android.ui.activity.action.OPEN_SHORTCUT" @@ -43,6 +44,7 @@ object FolderShortcutHelper { putExtra(EXTRA_SHORTCUT_FOLDER_REMOTE_ID, folder.remoteId) putExtra(EXTRA_SHORTCUT_FOLDER_REMOTE_PATH, folder.remotePath) putExtra(EXTRA_SHORTCUT_FOLDER_SPACE_ID, folder.spaceId) + putExtra(EXTRA_SHORTCUT_FOLDER_ACCOUNT, folder.owner) flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK } diff --git a/opencloudApp/src/main/java/eu/opencloud/android/presentation/files/addtohomescreen/ShortcutTrampolineActivity.kt b/opencloudApp/src/main/java/eu/opencloud/android/presentation/files/addtohomescreen/ShortcutTrampolineActivity.kt index e5836b277d..4f9968787b 100644 --- a/opencloudApp/src/main/java/eu/opencloud/android/presentation/files/addtohomescreen/ShortcutTrampolineActivity.kt +++ b/opencloudApp/src/main/java/eu/opencloud/android/presentation/files/addtohomescreen/ShortcutTrampolineActivity.kt @@ -22,6 +22,7 @@ class ShortcutTrampolineActivity : Activity() { copyStringExtra(shortcutIntent, FolderShortcutHelper.EXTRA_SHORTCUT_FOLDER_REMOTE_ID) copyStringExtra(shortcutIntent, FolderShortcutHelper.EXTRA_SHORTCUT_FOLDER_REMOTE_PATH) copyStringExtra(shortcutIntent, FolderShortcutHelper.EXTRA_SHORTCUT_FOLDER_SPACE_ID) + copyStringExtra(shortcutIntent, FolderShortcutHelper.EXTRA_SHORTCUT_FOLDER_ACCOUNT) flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK } startActivity(target) diff --git a/opencloudApp/src/main/java/eu/opencloud/android/ui/activity/FileDisplayActivity.kt b/opencloudApp/src/main/java/eu/opencloud/android/ui/activity/FileDisplayActivity.kt index 2b5a3b6cfe..bd936c1b51 100644 --- a/opencloudApp/src/main/java/eu/opencloud/android/ui/activity/FileDisplayActivity.kt +++ b/opencloudApp/src/main/java/eu/opencloud/android/ui/activity/FileDisplayActivity.kt @@ -1985,6 +1985,28 @@ class FileDisplayActivity : FileActivity(), val shortcutRemotePath = intent?.getStringExtra(sc.EXTRA_SHORTCUT_FOLDER_REMOTE_PATH) val shortcutSpaceId = intent?.getStringExtra(sc.EXTRA_SHORTCUT_FOLDER_SPACE_ID) + val shortcutAccountName = intent?.getStringExtra(sc.EXTRA_SHORTCUT_FOLDER_ACCOUNT) + + if (shortcutRemotePath != null && + !shortcutAccountName.isNullOrBlank() && + shortcutAccountName != account?.name + ) { + val targetAccount = eu.opencloud.android.presentation.authentication.AccountUtils + .getOpenCloudAccountByName(this, shortcutAccountName) + if (targetAccount != null) { + MainApp.initDependencyInjection() + startActivity(Intent(this, FileDisplayActivity::class.java).apply { + action = sc.ACTION_OPEN_SHORTCUT + putExtra(EXTRA_ACCOUNT, targetAccount) + putExtra(sc.EXTRA_SHORTCUT_FOLDER_REMOTE_PATH, shortcutRemotePath) + putExtra(sc.EXTRA_SHORTCUT_FOLDER_SPACE_ID, shortcutSpaceId) + putExtra(sc.EXTRA_SHORTCUT_FOLDER_ACCOUNT, shortcutAccountName) + flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK + }) + return + } + } + if (shortcutRemotePath != null) { val file = storageManager.getFileByPath(shortcutRemotePath, shortcutSpaceId) if (file != null) { @@ -1999,6 +2021,7 @@ class FileDisplayActivity : FileActivity(), intent?.removeExtra(sc.EXTRA_SHORTCUT_FOLDER_REMOTE_ID) intent?.removeExtra(sc.EXTRA_SHORTCUT_FOLDER_REMOTE_PATH) intent?.removeExtra(sc.EXTRA_SHORTCUT_FOLDER_SPACE_ID) + intent?.removeExtra(sc.EXTRA_SHORTCUT_FOLDER_ACCOUNT) } private fun onDeepLinkManaged() { From 21d6489260984b23f95d1a85a94d46a35b8117b8 Mon Sep 17 00:00:00 2001 From: ryan Date: Tue, 6 Oct 2026 14:44:32 +0000 Subject: [PATCH 10/12] Enforce Android shortcut label length limits The dialog accepted names up to MAX_FILENAME_LENGTH (223 chars) and passed them unchanged to setShortLabel/setLongLabel, which Android caps at 10/25 recommended characters. Validate the name in the dialog and truncate labels defensively before building the ShortcutInfo. --- .../AddToHomeScreenDialogFragment.kt | 13 ++++++------- .../files/addtohomescreen/FolderShortcutHelper.kt | 12 ++++++++++-- opencloudApp/src/main/res/values/strings.xml | 1 + 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/opencloudApp/src/main/java/eu/opencloud/android/presentation/files/addtohomescreen/AddToHomeScreenDialogFragment.kt b/opencloudApp/src/main/java/eu/opencloud/android/presentation/files/addtohomescreen/AddToHomeScreenDialogFragment.kt index d718637692..79875fb4ef 100644 --- a/opencloudApp/src/main/java/eu/opencloud/android/presentation/files/addtohomescreen/AddToHomeScreenDialogFragment.kt +++ b/opencloudApp/src/main/java/eu/opencloud/android/presentation/files/addtohomescreen/AddToHomeScreenDialogFragment.kt @@ -11,7 +11,6 @@ import androidx.fragment.app.DialogFragment import com.google.android.material.textfield.TextInputLayout import eu.opencloud.android.R import eu.opencloud.android.domain.files.model.OCFile -import eu.opencloud.android.presentation.files.filelist.MainFileListFragment.Companion.MAX_FILENAME_LENGTH import eu.opencloud.android.presentation.files.filelist.MainFileListFragment.Companion.forbiddenChars import eu.opencloud.android.utils.PreferenceUtils @@ -66,9 +65,9 @@ class AddToHomeScreenDialogFragment : DialogFragment() { val name = inputText.text.toString().trim() val error = when { name.isBlank() -> getString(R.string.add_to_home_screen_dialog_error_empty) - name.length > MAX_FILENAME_LENGTH -> String.format( - getString(R.string.uploader_upload_text_dialog_filename_error_length_max), - MAX_FILENAME_LENGTH + name.length > FolderShortcutHelper.MAX_LONG_LABEL_LENGTH -> String.format( + getString(R.string.add_to_home_screen_dialog_error_length_max), + FolderShortcutHelper.MAX_LONG_LABEL_LENGTH ) forbiddenChars.any { name.contains(it) } -> getString(R.string.filename_forbidden_characters) else -> null @@ -90,10 +89,10 @@ class AddToHomeScreenDialogFragment : DialogFragment() { if (text.isNullOrBlank()) { okButton.isEnabled = false error = getString(R.string.add_to_home_screen_dialog_error_empty) - } else if (text.length > MAX_FILENAME_LENGTH) { + } else if (text.length > FolderShortcutHelper.MAX_LONG_LABEL_LENGTH) { error = String.format( - getString(R.string.uploader_upload_text_dialog_filename_error_length_max), - MAX_FILENAME_LENGTH + getString(R.string.add_to_home_screen_dialog_error_length_max), + FolderShortcutHelper.MAX_LONG_LABEL_LENGTH ) } else if (forbiddenChars.any { text.contains(it) }) { error = getString(R.string.filename_forbidden_characters) diff --git a/opencloudApp/src/main/java/eu/opencloud/android/presentation/files/addtohomescreen/FolderShortcutHelper.kt b/opencloudApp/src/main/java/eu/opencloud/android/presentation/files/addtohomescreen/FolderShortcutHelper.kt index 8c33085c45..7a76f25446 100644 --- a/opencloudApp/src/main/java/eu/opencloud/android/presentation/files/addtohomescreen/FolderShortcutHelper.kt +++ b/opencloudApp/src/main/java/eu/opencloud/android/presentation/files/addtohomescreen/FolderShortcutHelper.kt @@ -20,6 +20,9 @@ object FolderShortcutHelper { const val ACTION_OPEN_SHORTCUT = "eu.opencloud.android.ui.activity.action.OPEN_SHORTCUT" + const val MAX_SHORT_LABEL_LENGTH = 10 + const val MAX_LONG_LABEL_LENGTH = 25 + fun createPinnedShortcut(context: Context, folder: OCFile, shortcutName: String) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { createPinnedShortcutApi26(context, folder, shortcutName) @@ -49,8 +52,8 @@ object FolderShortcutHelper { } val shortcut = ShortcutInfo.Builder(context, shortcutId) - .setShortLabel(shortcutName) - .setLongLabel(shortcutName) + .setShortLabel(shortcutName.truncateForLabel(MAX_SHORT_LABEL_LENGTH)) + .setLongLabel(shortcutName.truncateForLabel(MAX_LONG_LABEL_LENGTH)) .setIcon(Icon.createWithResource(context, R.mipmap.icon)) .setIntent(shortcutIntent) .build() @@ -58,4 +61,9 @@ object FolderShortcutHelper { shortcutManager.requestPinShortcut(shortcut, null) Toast.makeText(context, context.getString(R.string.add_to_home_screen_shortcut_added), Toast.LENGTH_SHORT).show() } + + private fun String.truncateForLabel(maxLength: Int): String { + if (length <= maxLength) return this + return take(maxLength - 1) + "…" + } } diff --git a/opencloudApp/src/main/res/values/strings.xml b/opencloudApp/src/main/res/values/strings.xml index ef30203c8e..9c94b8ec5c 100644 --- a/opencloudApp/src/main/res/values/strings.xml +++ b/opencloudApp/src/main/res/values/strings.xml @@ -869,5 +869,6 @@ Shortcut added to Home screen Add Shortcut to Home screen is not supported This folder is no longer available. It may have been removed or renamed. + Shortcut name must not be longer than %d characters. From a87ab9e85abea3dfe762860539f8f6f1dc8a3f14 Mon Sep 17 00:00:00 2001 From: ryan Date: Tue, 6 Oct 2026 14:45:45 +0000 Subject: [PATCH 11/12] Show shortcut-added toast only from the pin result callback requestPinShortcut is asynchronous and the success toast fired unconditionally before the user even confirmed the launcher dialog. Pass an IntentSender backed by ShortcutPinResultReceiver and report success only when the launcher actually pinned the shortcut; surface an error toast when requestPinShortcut returns false. --- opencloudApp/src/main/AndroidManifest.xml | 4 ++++ .../addtohomescreen/FolderShortcutHelper.kt | 22 ++++++++++++++----- .../ShortcutPinResultReceiver.kt | 19 ++++++++++++++++ opencloudApp/src/main/res/values/strings.xml | 1 + 4 files changed, 40 insertions(+), 6 deletions(-) create mode 100644 opencloudApp/src/main/java/eu/opencloud/android/presentation/files/addtohomescreen/ShortcutPinResultReceiver.kt diff --git a/opencloudApp/src/main/AndroidManifest.xml b/opencloudApp/src/main/AndroidManifest.xml index 0e410c765c..4ced565012 100644 --- a/opencloudApp/src/main/AndroidManifest.xml +++ b/opencloudApp/src/main/AndroidManifest.xml @@ -124,6 +124,10 @@ + + maxLength) take(maxLength - 1) + "…" else this } diff --git a/opencloudApp/src/main/java/eu/opencloud/android/presentation/files/addtohomescreen/ShortcutPinResultReceiver.kt b/opencloudApp/src/main/java/eu/opencloud/android/presentation/files/addtohomescreen/ShortcutPinResultReceiver.kt new file mode 100644 index 0000000000..8b139517f0 --- /dev/null +++ b/opencloudApp/src/main/java/eu/opencloud/android/presentation/files/addtohomescreen/ShortcutPinResultReceiver.kt @@ -0,0 +1,19 @@ +package eu.opencloud.android.presentation.files.addtohomescreen + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.widget.Toast +import eu.opencloud.android.R + +class ShortcutPinResultReceiver : BroadcastReceiver() { + + override fun onReceive(context: Context, intent: Intent) { + if (intent.action != FolderShortcutHelper.ACTION_PIN_SHORTCUT_RESULT) return + Toast.makeText( + context, + context.getString(R.string.add_to_home_screen_shortcut_added), + Toast.LENGTH_SHORT + ).show() + } +} diff --git a/opencloudApp/src/main/res/values/strings.xml b/opencloudApp/src/main/res/values/strings.xml index 9c94b8ec5c..eb1f8750db 100644 --- a/opencloudApp/src/main/res/values/strings.xml +++ b/opencloudApp/src/main/res/values/strings.xml @@ -868,6 +868,7 @@ The name cannot be empty Shortcut added to Home screen Add Shortcut to Home screen is not supported + Could not add shortcut to Home screen This folder is no longer available. It may have been removed or renamed. Shortcut name must not be longer than %d characters. From 4fcea71fc693de762f4708cbaae6903e175b8ac0 Mon Sep 17 00:00:00 2001 From: ryan Date: Tue, 6 Oct 2026 14:46:02 +0000 Subject: [PATCH 12/12] Handle Add to Home screen in the selection action mode The action was only wired to the bottom-sheet menu; when chosen from the long-press selection action mode it fell through onSingleFileActionChosen and did nothing. Open the dialog via the child fragment manager and clear the selection like other actions. --- .../files/filelist/MainFileListFragment.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/opencloudApp/src/main/java/eu/opencloud/android/presentation/files/filelist/MainFileListFragment.kt b/opencloudApp/src/main/java/eu/opencloud/android/presentation/files/filelist/MainFileListFragment.kt index a0994b69af..c0d426a126 100644 --- a/opencloudApp/src/main/java/eu/opencloud/android/presentation/files/filelist/MainFileListFragment.kt +++ b/opencloudApp/src/main/java/eu/opencloud/android/presentation/files/filelist/MainFileListFragment.kt @@ -1448,6 +1448,16 @@ class MainFileListFragment : Fragment(), true } + R.id.action_add_to_home -> { + if (singleFile.isFolder) { + val dialog = AddToHomeScreenDialogFragment.newInstance(singleFile) + dialog.show(childFragmentManager, DIALOG_ADD_TO_HOME_SCREEN) + } + fileListAdapter.clearSelection() + updateActionModeAfterTogglingSelected() + true + } + else -> { false }