Skip to content

Commit

Permalink
[Feature] Request notification permission and target Android 13.
Browse files Browse the repository at this point in the history
Fixes: #998
  • Loading branch information
zhanghai committed Sep 10, 2023
1 parent 4ed9fb5 commit bf5e720
Show file tree
Hide file tree
Showing 11 changed files with 177 additions and 22 deletions.
2 changes: 1 addition & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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 + ')'
Expand Down
1 change: 1 addition & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
<uses-permission
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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<Args>()
private val argsPath by lazy { args.intent.extraPath }
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
)) {
Expand All @@ -1328,8 +1343,10 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter.
requestStoragePermission()
}
viewModel.isStorageAccessRequested = true
return false
}
}
return true
}

override fun requestAllFilesAccess() {
Expand All @@ -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)
}
}
Expand All @@ -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"
Expand All @@ -1389,7 +1457,7 @@ class FileListFragment : Fragment(), BreadcrumbLayout.Listener, FileListAdapter.
Environment.isExternalStorageManager()
}

private class RequestStoragePermissionInSettingsContract
private class RequestPermissionInSettingsContract(private val permissionName: String)
: ActivityResultContract<Unit, Boolean>() {
override fun createIntent(context: Context, input: Unit): Intent =
Intent(
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright (c) 2023 Hai Zhang <dreaming.in.code.zh@gmail.com>
* 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()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright (c) 2023 Hai Zhang <dreaming.in.code.zh@gmail.com>
* 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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()) {
Expand Down
4 changes: 3 additions & 1 deletion app/src/main/res/values-zh-rCN/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,11 @@
<item quantity="other">%1$,d 字节</item>
</plurals>

<string name="all_files_access_rationale_message">应用需要管理所有文件的权限。请在接下来的系统设置中授予权限。</string>
<string name="storage_permission_rationale_message">应用需要访问文件的权限。请在接下来的系统对话框中点击“允许”。</string>
<string name="storage_permission_permanently_denied_message">应用需要访问文件的权限。请在系统设置中授予“存储空间”权限。</string>
<string name="all_files_access_rationale_message">应用需要管理所有文件的权限。请在接下来的系统设置中授予权限。</string>
<string name="notification_permission_rationale_message">应用需要发布文件操作相关通知的权限。请在接下来的系统对话框中点击“允许”。</string>
<string name="notification_permission_permanently_denied_message">应用需要发布文件操作相关通知的权限。请在系统设置中授予“通知”权限。</string>

<string name="notification_channel_background_activity_start_name">后台期间动作</string>
<string name="notification_channel_background_activity_start_description">在应用处于后台期间采取动作</string>
Expand Down
4 changes: 3 additions & 1 deletion app/src/main/res/values-zh-rTW/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,11 @@
<item quantity="other">%1$,d 位元組</item>
</plurals>

<string name="all_files_access_rationale_message">程式需要管理所有檔案的權限。請在接下來的系統設定中授予權限。</string>
<string name="storage_permission_rationale_message">程式需要存取檔案的權限。請在接下來的系統對話框中點擊「允許」。</string>
<string name="storage_permission_permanently_denied_message">程式需要存取檔案的權限。請在系統設定中授予「儲存空間」權限。</string>
<string name="all_files_access_rationale_message">程式需要管理所有檔案的權限。請在接下來的系統設定中授予權限。</string>
<string name="notification_permission_rationale_message">程式需要發布檔案作業相關通知的權限。請在接下來的系統對話框中點擊「允許」。</string>
<string name="notification_permission_permanently_denied_message">程式需要發布檔案作業相關通知的權限。請在系統設定中授予「通知」權限。</string>

<string name="notification_channel_background_activity_start_name">背景期間動作</string>
<string name="notification_channel_background_activity_start_description">在應用程式處於背景期間採取動作</string>
Expand Down
6 changes: 5 additions & 1 deletion app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,13 @@
<item quantity="other">%1$,d bytes</item>
</plurals>

<string name="all_files_access_rationale_message">App needs access to manage all files. Please allow the access in the upcoming system setting.</string>
<!-- The allow button before Android 10 was in all-caps. -->
<string name="storage_permission_rationale_message">App needs permission to access files. Please click “ALLOW” in the upcoming system dialog.</string>
<string name="storage_permission_permanently_denied_message">App needs permission to access files. Please grant the “Storage” permission in system settings.</string>
<string name="all_files_access_rationale_message">App needs access to manage all files. Please allow the access in the upcoming system setting.</string>
<!-- The allow button is in sentence case now. -->
<string name="notification_permission_rationale_message">App needs permission to post notifications about file operations. Please click “Allow” in the upcoming system dialog.</string>
<string name="notification_permission_permanently_denied_message">App needs permission to post notifications about file operations. Please grant the “Notification” permission in system settings.</string>

<string name="notification_channel_background_activity_start_name">Actions while background</string>
<string name="notification_channel_background_activity_start_description">Take actions while app is in the background</string>
Expand Down

0 comments on commit bf5e720

Please sign in to comment.