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