From bf5e720cfde1dbb34df83ec160e02770a875957a Mon Sep 17 00:00:00 2001 From: Hai Zhang Date: Sun, 10 Sep 2023 03:45:31 -0700 Subject: [PATCH] [Feature] Request notification permission and target Android 13. Fixes: #998 --- app/build.gradle | 2 +- app/src/main/AndroidManifest.xml | 1 + .../files/app/BackgroundActivityStarter.kt | 3 - .../files/filelist/FileListFragment.kt | 91 ++++++++++++++++--- .../files/filelist/FileListViewModel.kt | 7 ++ ...issionInSettingsRationaleDialogFragment.kt | 39 ++++++++ ...cationPermissionRationaleDialogFragment.kt | 39 ++++++++ .../util/ForegroundNotificationManager.kt | 3 - app/src/main/res/values-zh-rCN/strings.xml | 4 +- app/src/main/res/values-zh-rTW/strings.xml | 4 +- app/src/main/res/values/strings.xml | 6 +- 11 files changed, 177 insertions(+), 22 deletions(-) create mode 100644 app/src/main/java/me/zhanghai/android/files/filelist/ShowRequestNotificationPermissionInSettingsRationaleDialogFragment.kt create mode 100644 app/src/main/java/me/zhanghai/android/files/filelist/ShowRequestNotificationPermissionRationaleDialogFragment.kt diff --git a/app/build.gradle b/app/build.gradle index 83cdd996a..258a91c06 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -34,7 +34,7 @@ android { minSdk 21 // Not supporting notification runtime permission yet. //noinspection OldTargetApi - targetSdk 32 + targetSdk 33 versionCode 33 versionName '1.6.1' resValue 'string', 'app_version', versionName + ' (' + versionCode + ')' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b811860b6..87e66a657 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -21,6 +21,7 @@ + diff --git a/app/src/main/java/me/zhanghai/android/files/app/BackgroundActivityStarter.kt b/app/src/main/java/me/zhanghai/android/files/app/BackgroundActivityStarter.kt index 1637a4ea0..3f3392ca4 100644 --- a/app/src/main/java/me/zhanghai/android/files/app/BackgroundActivityStarter.kt +++ b/app/src/main/java/me/zhanghai/android/files/app/BackgroundActivityStarter.kt @@ -5,7 +5,6 @@ package me.zhanghai.android.files.app -import android.annotation.SuppressLint import android.app.PendingIntent import android.content.Context import android.content.Intent @@ -52,8 +51,6 @@ object BackgroundActivityStarter { Lifecycle.State.STARTED ) - // TODO: Add POST_NOTIFICATIONS permission when targeting API 33. - @SuppressLint("MissingPermission") private fun notifyStartActivity( intent: Intent, title: CharSequence, diff --git a/app/src/main/java/me/zhanghai/android/files/filelist/FileListFragment.kt b/app/src/main/java/me/zhanghai/android/files/filelist/FileListFragment.kt index 2443724e4..1fdbf5d4b 100644 --- a/app/src/main/java/me/zhanghai/android/files/filelist/FileListFragment.kt +++ b/app/src/main/java/me/zhanghai/android/files/filelist/FileListFragment.kt @@ -124,6 +124,8 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter. CreateFileDialogFragment.Listener, CreateDirectoryDialogFragment.Listener, NavigateToPathDialogFragment.Listener, NavigationFragment.Listener, ShowRequestAllFilesAccessRationaleDialogFragment.Listener, + ShowRequestNotificationPermissionRationaleDialogFragment.Listener, + ShowRequestNotificationPermissionInSettingsRationaleDialogFragment.Listener, ShowRequestStoragePermissionRationaleDialogFragment.Listener, ShowRequestStoragePermissionInSettingsRationaleDialogFragment.Listener { private val requestAllFilesAccessLauncher = registerForActivityResult( @@ -133,9 +135,18 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter. ActivityResultContracts.RequestPermission(), this::onRequestStoragePermissionResult ) private val requestStoragePermissionInSettingsLauncher = registerForActivityResult( - RequestStoragePermissionInSettingsContract(), + RequestPermissionInSettingsContract(android.Manifest.permission.WRITE_EXTERNAL_STORAGE), this::onRequestStoragePermissionInSettingsResult ) + @RequiresApi(Build.VERSION_CODES.TIRAMISU) + private val requestNotificationPermissionLauncher = registerForActivityResult( + ActivityResultContracts.RequestPermission(), this::onRequestNotificationPermissionResult + ) + @RequiresApi(Build.VERSION_CODES.TIRAMISU) + private val requestNotificationPermissionInSettingsLauncher = registerForActivityResult( + RequestPermissionInSettingsContract(android.Manifest.permission.POST_NOTIFICATIONS), + this::onRequestNotificationPermissionInSettingsResult + ) private val args by args() private val argsPath by lazy { args.intent.extraPath } @@ -320,7 +331,9 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter. override fun onResume() { super.onResume() - ensureStorageAccess() + if (ensureStorageAccess()) { + ensureNotificationPermission() + } } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { @@ -1308,18 +1321,20 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter. binding.drawerLayout?.closeDrawer(GravityCompat.START) } - private fun ensureStorageAccess() { + private fun ensureStorageAccess(): Boolean { if (viewModel.isStorageAccessRequested) { - return + return true } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (!Environment.isExternalStorageManager()) { ShowRequestAllFilesAccessRationaleDialogFragment.show(this) viewModel.isStorageAccessRequested = true + return false } } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - if (checkSelfPermission(android.Manifest.permission.WRITE_EXTERNAL_STORAGE) - != PackageManager.PERMISSION_GRANTED) { + if (checkSelfPermission(android.Manifest.permission.WRITE_EXTERNAL_STORAGE) != + PackageManager.PERMISSION_GRANTED + ) { if (shouldShowRequestPermissionRationale( android.Manifest.permission.WRITE_EXTERNAL_STORAGE )) { @@ -1328,8 +1343,10 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter. requestStoragePermission() } viewModel.isStorageAccessRequested = true + return false } } + return true } override fun requestAllFilesAccess() { @@ -1352,8 +1369,8 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter. viewModel.isStorageAccessRequested = false refresh() } else if (!shouldShowRequestPermissionRationale( - android.Manifest.permission.WRITE_EXTERNAL_STORAGE - )) { + android.Manifest.permission.WRITE_EXTERNAL_STORAGE + )) { ShowRequestStoragePermissionInSettingsRationaleDialogFragment.show(this) } } @@ -1369,6 +1386,57 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter. } } + private fun ensureNotificationPermission(): Boolean { + if (viewModel.isNotificationPermissionRequested) { + return true + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (checkSelfPermission(android.Manifest.permission.POST_NOTIFICATIONS) != + PackageManager.PERMISSION_GRANTED) { + if (shouldShowRequestPermissionRationale( + android.Manifest.permission.POST_NOTIFICATIONS + )) { + ShowRequestNotificationPermissionRationaleDialogFragment.show(this) + } else { + requestNotificationPermission() + } + viewModel.isNotificationPermissionRequested = true + return false + } + } + return true + } + + @RequiresApi(Build.VERSION_CODES.TIRAMISU) + override fun requestNotificationPermission() { + requestNotificationPermissionLauncher.launch(android.Manifest.permission.POST_NOTIFICATIONS) + } + + @RequiresApi(Build.VERSION_CODES.TIRAMISU) + private fun onRequestNotificationPermissionResult(isGranted: Boolean) { + if (isGranted) { + viewModel.isNotificationPermissionRequested = false + refresh() + } else if (!shouldShowRequestPermissionRationale( + android.Manifest.permission.POST_NOTIFICATIONS + )) { + ShowRequestNotificationPermissionInSettingsRationaleDialogFragment.show(this) + } + } + + @RequiresApi(Build.VERSION_CODES.TIRAMISU) + override fun requestNotificationPermissionInSettings() { + requestNotificationPermissionInSettingsLauncher.launch(Unit) + } + + @RequiresApi(Build.VERSION_CODES.TIRAMISU) + private fun onRequestNotificationPermissionInSettingsResult(isGranted: Boolean) { + if (isGranted) { + viewModel.isNotificationPermissionRequested = false + refresh() + } + } + companion object { private const val ACTION_VIEW_DOWNLOADS = "me.zhanghai.android.files.intent.action.VIEW_DOWNLOADS" @@ -1389,7 +1457,7 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter. Environment.isExternalStorageManager() } - private class RequestStoragePermissionInSettingsContract + private class RequestPermissionInSettingsContract(private val permissionName: String) : ActivityResultContract() { override fun createIntent(context: Context, input: Unit): Intent = Intent( @@ -1398,9 +1466,8 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter. ) override fun parseResult(resultCode: Int, intent: Intent?): Boolean = - application.checkSelfPermissionCompat( - android.Manifest.permission.WRITE_EXTERNAL_STORAGE - ) == PackageManager.PERMISSION_GRANTED + application.checkSelfPermissionCompat(permissionName) == + PackageManager.PERMISSION_GRANTED } @Parcelize diff --git a/app/src/main/java/me/zhanghai/android/files/filelist/FileListViewModel.kt b/app/src/main/java/me/zhanghai/android/files/filelist/FileListViewModel.kt index 176c68790..7e047a864 100644 --- a/app/src/main/java/me/zhanghai/android/files/filelist/FileListViewModel.kt +++ b/app/src/main/java/me/zhanghai/android/files/filelist/FileListViewModel.kt @@ -227,6 +227,13 @@ class FileListViewModel : ViewModel() { _isRequestingStorageAccessLiveData.value = value } + private val _isRequestingNotificationPermissionLiveData = MutableLiveData(false) + var isNotificationPermissionRequested: Boolean + get() = _isRequestingNotificationPermissionLiveData.valueCompat + set(value) { + _isRequestingNotificationPermissionLiveData.value = value + } + override fun onCleared() { _fileListLiveData.close() } diff --git a/app/src/main/java/me/zhanghai/android/files/filelist/ShowRequestNotificationPermissionInSettingsRationaleDialogFragment.kt b/app/src/main/java/me/zhanghai/android/files/filelist/ShowRequestNotificationPermissionInSettingsRationaleDialogFragment.kt new file mode 100644 index 000000000..062dae144 --- /dev/null +++ b/app/src/main/java/me/zhanghai/android/files/filelist/ShowRequestNotificationPermissionInSettingsRationaleDialogFragment.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 Hai Zhang + * All Rights Reserved. + */ + +package me.zhanghai.android.files.filelist + +import android.app.Dialog +import android.os.Bundle +import androidx.appcompat.app.AppCompatDialogFragment +import androidx.fragment.app.Fragment +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import me.zhanghai.android.files.R +import me.zhanghai.android.files.util.show + +class ShowRequestNotificationPermissionInSettingsRationaleDialogFragment : AppCompatDialogFragment() { + private val listener: Listener + get() = requireParentFragment() as Listener + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return MaterialAlertDialogBuilder(requireContext(), theme) + .setMessage(R.string.notification_permission_rationale_message) + .setPositiveButton(R.string.open_settings) { _, _ -> + listener.requestNotificationPermissionInSettings() + } + .setNegativeButton(android.R.string.cancel, null) + .create() + } + + companion object { + fun show(fragment: Fragment) { + ShowRequestNotificationPermissionInSettingsRationaleDialogFragment().show(fragment) + } + } + + interface Listener { + fun requestNotificationPermissionInSettings() + } +} diff --git a/app/src/main/java/me/zhanghai/android/files/filelist/ShowRequestNotificationPermissionRationaleDialogFragment.kt b/app/src/main/java/me/zhanghai/android/files/filelist/ShowRequestNotificationPermissionRationaleDialogFragment.kt new file mode 100644 index 000000000..66cc5cee5 --- /dev/null +++ b/app/src/main/java/me/zhanghai/android/files/filelist/ShowRequestNotificationPermissionRationaleDialogFragment.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 Hai Zhang + * All Rights Reserved. + */ + +package me.zhanghai.android.files.filelist + +import android.app.Dialog +import android.os.Bundle +import androidx.appcompat.app.AppCompatDialogFragment +import androidx.fragment.app.Fragment +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import me.zhanghai.android.files.R +import me.zhanghai.android.files.util.show + +class ShowRequestNotificationPermissionRationaleDialogFragment : AppCompatDialogFragment() { + private val listener: Listener + get() = requireParentFragment() as Listener + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return MaterialAlertDialogBuilder(requireContext(), theme) + .setMessage(R.string.notification_permission_rationale_message) + .setPositiveButton(android.R.string.ok) { _, _ -> + listener.requestNotificationPermission() + } + .setNegativeButton(android.R.string.cancel, null) + .create() + } + + companion object { + fun show(fragment: Fragment) { + ShowRequestNotificationPermissionRationaleDialogFragment().show(fragment) + } + } + + interface Listener { + fun requestNotificationPermission() + } +} diff --git a/app/src/main/java/me/zhanghai/android/files/util/ForegroundNotificationManager.kt b/app/src/main/java/me/zhanghai/android/files/util/ForegroundNotificationManager.kt index 8fac77581..10af1587e 100644 --- a/app/src/main/java/me/zhanghai/android/files/util/ForegroundNotificationManager.kt +++ b/app/src/main/java/me/zhanghai/android/files/util/ForegroundNotificationManager.kt @@ -5,7 +5,6 @@ package me.zhanghai.android.files.util -import android.annotation.SuppressLint import android.app.Notification import android.app.Service import me.zhanghai.android.files.app.notificationManager @@ -15,8 +14,6 @@ class ForegroundNotificationManager(private val service: Service) { private var foregroundId = 0 - // TODO: Add POST_NOTIFICATIONS permission when targeting API 33. - @SuppressLint("MissingPermission") fun notify(id: Int, notification: Notification) { synchronized(notifications) { if (notifications.isEmpty()) { diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 752e0ed93..4d68beaae 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -56,9 +56,11 @@ %1$,d 字节 + 应用需要管理所有文件的权限。请在接下来的系统设置中授予权限。 应用需要访问文件的权限。请在接下来的系统对话框中点击“允许”。 应用需要访问文件的权限。请在系统设置中授予“存储空间”权限。 - 应用需要管理所有文件的权限。请在接下来的系统设置中授予权限。 + 应用需要发布文件操作相关通知的权限。请在接下来的系统对话框中点击“允许”。 + 应用需要发布文件操作相关通知的权限。请在系统设置中授予“通知”权限。 后台期间动作 在应用处于后台期间采取动作 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 1d0142815..612830b2f 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -56,9 +56,11 @@ %1$,d 位元組 + 程式需要管理所有檔案的權限。請在接下來的系統設定中授予權限。 程式需要存取檔案的權限。請在接下來的系統對話框中點擊「允許」。 程式需要存取檔案的權限。請在系統設定中授予「儲存空間」權限。 - 程式需要管理所有檔案的權限。請在接下來的系統設定中授予權限。 + 程式需要發布檔案作業相關通知的權限。請在接下來的系統對話框中點擊「允許」。 + 程式需要發布檔案作業相關通知的權限。請在系統設定中授予「通知」權限。 背景期間動作 在應用程式處於背景期間採取動作 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 073b23f09..4c9632637 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -59,9 +59,13 @@ %1$,d bytes + App needs access to manage all files. Please allow the access in the upcoming system setting. + App needs permission to access files. Please click “ALLOW” in the upcoming system dialog. App needs permission to access files. Please grant the “Storage” permission in system settings. - App needs access to manage all files. Please allow the access in the upcoming system setting. + + App needs permission to post notifications about file operations. Please click “Allow” in the upcoming system dialog. + App needs permission to post notifications about file operations. Please grant the “Notification” permission in system settings. Actions while background Take actions while app is in the background