Merge pull request #9873 from nextcloud/optional-files-permission

Make external storage permission optional
This commit is contained in:
Tobias Kaminsky 2022-03-15 16:49:54 +01:00 committed by GitHub
commit 91520971cd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 381 additions and 153 deletions

View file

@ -21,10 +21,10 @@
package com.nextcloud.client
import android.Manifest
import android.os.Build
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
import com.owncloud.android.utils.PermissionUtil
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
@ -35,8 +35,7 @@ class GrantStoragePermissionRule private constructor() {
@JvmStatic
fun grant(): TestRule = when {
Build.VERSION.SDK_INT < Build.VERSION_CODES.R -> GrantPermissionRule.grant(
PermissionUtil
.getExternalStoragePermission()
Manifest.permission.WRITE_EXTERNAL_STORAGE
)
else -> GrantManageExternalStoragePermissionRule()
}

View file

@ -35,6 +35,8 @@
<uses-permission
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.VIBRATE" />

View file

@ -373,4 +373,8 @@ public interface AppPreferences {
void setPdfZoomTipShownCount(int count);
int getPdfZoomTipShownCount();
boolean isStoragePermissionRequested();
void setStoragePermissionRequested(boolean value);
}

View file

@ -97,6 +97,8 @@ public final class AppPreferencesImpl implements AppPreferences {
private static final String PREF__PDF_ZOOM_TIP_SHOWN = "pdf_zoom_tip_shown";
private static final String PREF__STORAGE_PERMISSION_REQUESTED = "storage_permission_requested";
private final Context context;
private final SharedPreferences preferences;
private final CurrentAccountProvider currentAccountProvider;
@ -696,6 +698,16 @@ public final class AppPreferencesImpl implements AppPreferences {
return preferences.getInt(PREF__PDF_ZOOM_TIP_SHOWN, 0);
}
@Override
public boolean isStoragePermissionRequested() {
return preferences.getBoolean(PREF__STORAGE_PERMISSION_REQUESTED, false);
}
@Override
public void setStoragePermissionRequested(boolean value) {
preferences.edit().putBoolean(PREF__STORAGE_PERMISSION_REQUESTED, value).apply();
}
@VisibleForTesting
public int computeBruteForceDelay(int count) {
return (int) Math.min(count / 3d, 10);

View file

@ -21,18 +21,14 @@
package com.owncloud.android.datamodel;
import android.app.Activity;
import android.content.ContentResolver;
import android.database.Cursor;
import android.net.Uri;
import android.provider.MediaStore;
import android.util.Log;
import com.google.android.material.snackbar.Snackbar;
import com.owncloud.android.MainApp;
import com.owncloud.android.R;
import com.owncloud.android.utils.PermissionUtil;
import com.owncloud.android.utils.theme.ThemeSnackbarUtils;
import java.io.File;
import java.util.ArrayList;
@ -42,6 +38,8 @@ import java.util.Map;
import javax.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
/**
* Media queries to gain access to media lists for the device.
*/
@ -72,7 +70,7 @@ public final class MediaProvider {
* @return list with media folders
*/
public static List<MediaFolder> getImageFolders(ContentResolver contentResolver, int itemLimit,
@Nullable final Activity activity, boolean getWithoutActivity) {
@Nullable final AppCompatActivity activity, boolean getWithoutActivity) {
// check permissions
checkPermissions(activity);
@ -171,29 +169,15 @@ public final class MediaProvider {
return filePath != null && filePath.lastIndexOf('/') > 0 && new File(filePath).exists();
}
private static void checkPermissions(@Nullable Activity activity) {
private static void checkPermissions(@Nullable AppCompatActivity activity) {
if (activity != null &&
!PermissionUtil.checkExternalStoragePermission(activity.getApplicationContext())) {
// Check if we should show an explanation
if (PermissionUtil
.shouldShowRequestPermissionRationale(activity, PermissionUtil.getExternalStoragePermission())) {
// Show explanation to the user and then request permission
Snackbar snackbar = Snackbar.make(activity.findViewById(R.id.ListLayout),
R.string.permission_storage_access, Snackbar.LENGTH_INDEFINITE)
.setAction(R.string.common_ok, v -> PermissionUtil.requestExternalStoragePermission(activity));
ThemeSnackbarUtils.colorSnackbar(activity.getApplicationContext(), snackbar);
snackbar.show();
} else {
// No explanation needed, request the permission.
PermissionUtil.requestExternalStoragePermission(activity);
}
PermissionUtil.requestExternalStoragePermission(activity, true);
}
}
public static List<MediaFolder> getVideoFolders(ContentResolver contentResolver, int itemLimit,
@Nullable final Activity activity, boolean getWithoutActivity) {
@Nullable final AppCompatActivity activity, boolean getWithoutActivity) {
// check permissions
checkPermissions(activity);

View file

@ -121,7 +121,6 @@ import com.owncloud.android.utils.PermissionUtil;
import com.owncloud.android.utils.PushUtils;
import com.owncloud.android.utils.StringUtils;
import com.owncloud.android.utils.theme.ThemeButtonUtils;
import com.owncloud.android.utils.theme.ThemeSnackbarUtils;
import com.owncloud.android.utils.theme.ThemeToolbarUtils;
import org.greenrobot.eventbus.EventBus;
@ -316,22 +315,7 @@ public class FileDisplayActivity extends FileActivity
super.onPostCreate(savedInstanceState);
if (!PermissionUtil.checkExternalStoragePermission(this)) {
// Check if we should show an explanation
if (PermissionUtil.shouldShowRequestPermissionRationale(this,
PermissionUtil.getExternalStoragePermission())) {
// Show explanation to the user and then request permission
Snackbar snackbar = Snackbar.make(binding.rootLayout,
R.string.permission_storage_access,
Snackbar.LENGTH_INDEFINITE)
.setAction(R.string.common_ok, v -> PermissionUtil.requestExternalStoragePermission(this));
ThemeSnackbarUtils.colorSnackbar(this, snackbar);
snackbar.show();
} else {
// No explanation needed, request the permission.
PermissionUtil.requestExternalStoragePermission(this);
}
}
PermissionUtil.requestExternalStoragePermission(this);
if (getIntent().getParcelableExtra(OCFileListFragment.SEARCH_EVENT) != null) {
switchToSearchFragment(savedInstanceState);
@ -399,7 +383,7 @@ public class FileDisplayActivity extends FileActivity
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
switch (requestCode) {
case PermissionUtil.PERMISSIONS_EXTERNAL_STORAGE: {
case PermissionUtil.PERMISSIONS_EXTERNAL_STORAGE:
// If request is cancelled, result arrays are empty.
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
@ -407,24 +391,16 @@ public class FileDisplayActivity extends FileActivity
EventBus.getDefault().post(new TokenPushEvent());
syncAndUpdateFolder(true);
// toggle on is save since this is the only scenario this code gets accessed
} else {
// permission denied --> do nothing
return;
}
return;
}
case PermissionUtil.PERMISSIONS_CAMERA: {
break;
case PermissionUtil.PERMISSIONS_CAMERA:
// If request is cancelled, result arrays are empty.
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// permission was granted
getFileOperationsHelper()
.uploadFromCamera(this, FileDisplayActivity.REQUEST_CODE__UPLOAD_FROM_CAMERA);
} else {
// permission denied
return;
}
return;
}
break;
default:
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
@ -856,6 +832,8 @@ public class FileDisplayActivity extends FileActivity
},
DELAY_TO_REQUEST_OPERATIONS_LATER
);
} else if (requestCode == PermissionUtil.REQUEST_CODE_MANAGE_ALL_FILES) {
syncAndUpdateFolder(true);
} else {
super.onActivityResult(requestCode, resultCode, data);
}
@ -1097,7 +1075,7 @@ public class FileDisplayActivity extends FileActivity
OCFileListFragment ocFileListFragment = (OCFileListFragment) leftFragment;
ocFileListFragment.setLoading(mSyncInProgress);
syncAndUpdateFolder(false);
syncAndUpdateFolder(false, true);
OCFile startFile = null;
if (getIntent() != null && getIntent().getParcelableExtra(EXTRA_FILE) != null) {
@ -2243,11 +2221,15 @@ public class FileDisplayActivity extends FileActivity
}
private void syncAndUpdateFolder(boolean ignoreETag) {
syncAndUpdateFolder(ignoreETag, false);
}
private void syncAndUpdateFolder(boolean ignoreETag, boolean ignoreFocus) {
OCFileListFragment listOfFiles = getListOfFilesFragment();
if (listOfFiles != null && !listOfFiles.isSearchFragment()) {
OCFile folder = listOfFiles.getCurrentFile();
if (folder != null) {
startSyncFolderOperation(folder, ignoreETag);
startSyncFolderOperation(folder, ignoreETag, ignoreFocus);
}
}
}

View file

@ -514,24 +514,28 @@ class SyncedFoldersActivity :
android.R.id.home -> finish()
R.id.action_create_custom_folder -> {
Log.d(TAG, "Show custom folder dialog")
val emptyCustomFolder = SyncedFolderDisplayItem(
SyncedFolder.UNPERSISTED_ID,
null,
null,
true,
false,
true,
false,
account.name,
FileUploader.LOCAL_BEHAVIOUR_FORGET,
NameCollisionPolicy.ASK_USER.serialize(),
false,
clock.currentTime,
null,
MediaFolderType.CUSTOM,
false
)
onSyncFolderSettingsClick(0, emptyCustomFolder)
if (PermissionUtil.checkExternalStoragePermission(this)) {
val emptyCustomFolder = SyncedFolderDisplayItem(
SyncedFolder.UNPERSISTED_ID,
null,
null,
true,
false,
true,
false,
account.name,
FileUploader.LOCAL_BEHAVIOUR_FORGET,
NameCollisionPolicy.ASK_USER.serialize(),
false,
clock.currentTime,
null,
MediaFolderType.CUSTOM,
false
)
onSyncFolderSettingsClick(0, emptyCustomFolder)
} else {
PermissionUtil.requestExternalStoragePermission(this, true)
}
result = super.onOptionsItemSelected(item)
}
else -> result = super.onOptionsItemSelected(item)
@ -751,17 +755,14 @@ class SyncedFoldersActivity :
) {
when (requestCode) {
PermissionUtil.PERMISSIONS_EXTERNAL_STORAGE -> {
// If request is cancelled, result arrays are empty.
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// permission was granted
load(getItemsDisplayedPerFolder(), true)
} else {
// permission denied --> do nothing
return
// permission denied --> request again
PermissionUtil.requestExternalStoragePermission(this, true)
}
return
}
else -> super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}

View file

@ -37,7 +37,6 @@ import android.widget.Spinner;
import android.widget.TextView;
import com.google.android.material.button.MaterialButton;
import com.google.android.material.snackbar.Snackbar;
import com.nextcloud.client.account.User;
import com.nextcloud.client.di.Injectable;
import com.nextcloud.client.preferences.AppPreferences;
@ -59,7 +58,6 @@ import com.owncloud.android.utils.PermissionUtil;
import com.owncloud.android.utils.theme.ThemeButtonUtils;
import com.owncloud.android.utils.theme.ThemeColorUtils;
import com.owncloud.android.utils.theme.ThemeDrawableUtils;
import com.owncloud.android.utils.theme.ThemeSnackbarUtils;
import com.owncloud.android.utils.theme.ThemeToolbarUtils;
import com.owncloud.android.utils.theme.ThemeUtils;
@ -265,10 +263,21 @@ public class UploadFilesActivity extends DrawerActivity implements LocalFileList
Log_OC.d(TAG, "onCreate() end");
}
private void requestPermissions() {
PermissionUtil.requestExternalStoragePermission(this, true);
}
public void showToolbarSpinner() {
mToolbarSpinner.setVisibility(View.VISIBLE);
}
@Override
protected void onResume() {
super.onResume();
requestPermissions();
}
private void fillDirectoryDropdown() {
File currentDir = mCurrentDir;
while (currentDir != null && currentDir.getParentFile() != null) {
@ -324,25 +333,10 @@ public class UploadFilesActivity extends DrawerActivity implements LocalFileList
private void checkLocalStoragePathPickerPermission() {
if (!PermissionUtil.checkExternalStoragePermission(this)) {
// Check if we should show an explanation
if (PermissionUtil.shouldShowRequestPermissionRationale(this,
PermissionUtil.getExternalStoragePermission())) {
// Show explanation to the user and then request permission
Snackbar snackbar = Snackbar.make(findViewById(android.R.id.content),
R.string.permission_storage_access,
Snackbar.LENGTH_INDEFINITE)
.setAction(R.string.common_ok, v -> PermissionUtil.requestExternalStoragePermission(this));
ThemeSnackbarUtils.colorSnackbar(this, snackbar);
snackbar.show();
} else {
// No explanation needed, request the permission.
PermissionUtil.requestExternalStoragePermission(this);
}
return;
requestPermissions();
} else {
showLocalStoragePathPickerDialog();
}
showLocalStoragePathPickerDialog();
}
private void showLocalStoragePathPickerDialog() {
@ -642,20 +636,24 @@ public class UploadFilesActivity extends DrawerActivity implements LocalFileList
finish();
} else if (v.getId() == R.id.upload_files_btn_upload) {
if (mCurrentDir != null) {
preferences.setUploadFromLocalLastPath(mCurrentDir.getAbsolutePath());
}
if (mLocalFolderPickerMode) {
Intent data = new Intent();
if (PermissionUtil.checkExternalStoragePermission(this)) {
if (mCurrentDir != null) {
data.putExtra(EXTRA_CHOSEN_FILES, mCurrentDir.getAbsolutePath());
preferences.setUploadFromLocalLastPath(mCurrentDir.getAbsolutePath());
}
setResult(RESULT_OK, data);
if (mLocalFolderPickerMode) {
Intent data = new Intent();
if (mCurrentDir != null) {
data.putExtra(EXTRA_CHOSEN_FILES, mCurrentDir.getAbsolutePath());
}
setResult(RESULT_OK, data);
finish();
finish();
} else {
new CheckAvailableSpaceTask(this, mFileListFragment.getCheckedFilePaths())
.execute(mBehaviourSpinner.getSelectedItemPosition() == 0);
}
} else {
new CheckAvailableSpaceTask(this, mFileListFragment.getCheckedFilePaths())
.execute(mBehaviourSpinner.getSelectedItemPosition() == 0);
requestPermissions();
}
}
}

View file

@ -0,0 +1,104 @@
/*
* Nextcloud Android client application
*
* @author Álvaro Brey Vilas
* Copyright (C) 2022 Álvaro Brey Vilas
* Copyright (C) 2022 Nextcloud GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.owncloud.android.ui.dialog
import android.app.Dialog
import android.os.Build
import android.os.Bundle
import android.view.View
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.owncloud.android.R
import com.owncloud.android.databinding.StoragePermissionDialogBinding
import com.owncloud.android.ui.dialog.StoragePermissionDialogFragment.Listener
import com.owncloud.android.utils.theme.ThemeButtonUtils
/**
* Dialog that shows permission options in SDK >= 30
*
* Allows choosing "full access" (MANAGE_ALL_FILES) or "read-only media" (READ_EXTERNAL_STORAGE)
*
* @param listener a [Listener] for button clicks. The dialog will auto-dismiss after the callback is called.
* @param permissionRequired Whether the permission is absolutely required by the calling component.
* This changes the texts to a more strict version.
*/
@RequiresApi(Build.VERSION_CODES.R)
class StoragePermissionDialogFragment(val listener: Listener, val permissionRequired: Boolean = false) :
DialogFragment() {
private lateinit var binding: StoragePermissionDialogBinding
override fun onStart() {
super.onStart()
dialog?.let {
val alertDialog = it as AlertDialog
ThemeButtonUtils.themeBorderlessButton(alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE))
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
// Inflate the layout for the dialog
val inflater = requireActivity().layoutInflater
binding = StoragePermissionDialogBinding.inflate(inflater, null, false)
val view: View = binding.root
val explanationResource = when {
permissionRequired -> R.string.file_management_permission_text
else -> R.string.file_management_permission_optional_text
}
binding.storagePermissionExplanation.text = getString(explanationResource, getString(R.string.app_name))
// Setup layout
ThemeButtonUtils.colorPrimaryButton(binding.btnFullAccess, context)
binding.btnFullAccess.setOnClickListener {
listener.onClickFullAccess()
dismiss()
}
ThemeButtonUtils.themeBorderlessButton(binding.btnReadOnly)
binding.btnReadOnly.setOnClickListener {
listener.onClickMediaReadOnly()
dismiss()
}
// Build the dialog
val titleResource = when {
permissionRequired -> R.string.file_management_permission
else -> R.string.file_management_permission_optional
}
val dialog = MaterialAlertDialogBuilder(requireActivity(), R.style.Theme_ownCloud_Dialog)
.setTitle(titleResource)
.setView(view)
.setNegativeButton(R.string.common_cancel) { _, _ ->
listener.onCancel()
dismiss()
}
.create()
return dialog
}
interface Listener {
fun onCancel()
fun onClickFullAccess()
fun onClickMediaReadOnly()
}
}

View file

@ -28,16 +28,21 @@ import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.Settings
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.google.android.material.snackbar.Snackbar
import com.nextcloud.client.preferences.AppPreferences
import com.nextcloud.client.preferences.AppPreferencesImpl
import com.owncloud.android.R
import com.owncloud.android.utils.theme.ThemeButtonUtils
import com.owncloud.android.ui.dialog.StoragePermissionDialogFragment
import com.owncloud.android.utils.theme.ThemeSnackbarUtils
object PermissionUtil {
const val PERMISSIONS_EXTERNAL_STORAGE = 1
@ -49,6 +54,8 @@ object PermissionUtil {
const val REQUEST_CODE_MANAGE_ALL_FILES = 19203
const val PERMISSION_CHOICE_DIALOG_TAG = "PERMISSION_CHOICE_DIALOG"
/**
* Wrapper method for ContextCompat.checkSelfPermission().
* Determine whether *the app* has been granted a particular permission.
@ -76,70 +83,143 @@ object PermissionUtil {
ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)
/**
* For SDK < 30, we can do whatever we want using WRITE_EXTERNAL_STORAGE.
* For SDK above 30, scoped storage is in effect, and WRITE_EXTERNAL_STORAGE is useless. However, we do still need
* READ_EXTERNAL_STORAGE to read and upload files from folders that we don't manage and are not public access.
* Determine whether the app has been granted external storage permissions depending on SDK.
*
* @return The relevant external storage permission, depending on SDK
*/
@JvmStatic
fun getExternalStoragePermission(): String = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> Manifest.permission.MANAGE_EXTERNAL_STORAGE
else -> Manifest.permission.WRITE_EXTERNAL_STORAGE
}
/**
* Determine whether *the app* has been granted external storage permissions depending on SDK.
* For sdk >= 30 we use the storage manager special permission for full access, or READ_EXTERNAL_STORAGE
* for limited access
*
* Under sdk 30 we use WRITE_EXTERNAL_STORAGE
*
* @return `true` if app has the permission, or `false` if not.
*/
@JvmStatic
fun checkExternalStoragePermission(context: Context): Boolean = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> Environment.isExternalStorageManager()
else -> checkSelfPermission(context, getExternalStoragePermission())
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> Environment.isExternalStorageManager() || checkSelfPermission(
context,
Manifest.permission.READ_EXTERNAL_STORAGE
)
else -> checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
/**
* Request relevant external storage permission depending on SDK.
* Request relevant external storage permission depending on SDK, if needed.
*
* Activities should implement [Activity.onRequestPermissionsResult]
* and handle the [PERMISSIONS_EXTERNAL_STORAGE] code, as well as [Activity.onActivityResult]
* with `requestCode=`[REQUEST_CODE_MANAGE_ALL_FILES]
*
* @param activity The target activity.
* @param permissionRequired for SDK >=30 specifically, show again even if already denied in the past
*/
@JvmStatic
fun requestExternalStoragePermission(activity: Activity) = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> requestManageFilesPermission(activity)
else -> {
@JvmOverloads
fun requestExternalStoragePermission(activity: AppCompatActivity, permissionRequired: Boolean = false) {
if (!checkExternalStoragePermission(activity)) {
when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
if (canRequestAllFilesPermission(activity)) {
// can request All Files, show choice
showPermissionChoiceDialog(activity, permissionRequired)
} else {
// can not request all files, request READ_EXTERNAL_STORAGE
requestStoragePermission(activity, Manifest.permission.READ_EXTERNAL_STORAGE)
}
}
else -> requestStoragePermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
}
}
/**
* Request a storage permission
*/
private fun requestStoragePermission(activity: Activity, permission: String) {
fun doRequest() {
ActivityCompat.requestPermissions(
activity, arrayOf(getExternalStoragePermission()),
activity, arrayOf(permission),
PERMISSIONS_EXTERNAL_STORAGE
)
}
// Check if we should show an explanation
if (shouldShowRequestPermissionRationale(activity, permission)) {
// Show explanation to the user and then request permission
Snackbar
.make(
activity.findViewById(android.R.id.content),
R.string.permission_storage_access,
Snackbar.LENGTH_INDEFINITE
)
.setAction(R.string.common_ok) {
doRequest()
}
.also {
ThemeSnackbarUtils.colorSnackbar(activity, it)
}
.show()
} else {
// No explanation needed, request the permission.
doRequest()
}
}
@RequiresApi(Build.VERSION_CODES.R)
private fun requestManageFilesPermission(activity: Activity) {
val alertDialog = AlertDialog.Builder(activity, R.style.Theme_ownCloud_Dialog)
.setTitle(R.string.file_management_permission)
.setMessage(
String.format(
activity.getString(R.string.file_management_permission_text),
activity.getString(R.string.app_name)
)
)
.setCancelable(false)
.setPositiveButton(R.string.common_ok) { dialog, _ ->
val intent = Intent().apply {
action = Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION
data = Uri.parse("package:${activity.applicationContext.packageName}")
}
activity.startActivityForResult(intent, REQUEST_CODE_MANAGE_ALL_FILES)
dialog.dismiss()
}
.create()
private fun canRequestAllFilesPermission(context: Context) =
manifestHasAllFilesPermission(context) && hasManageAllFilesActivity(context)
alertDialog.show()
ThemeButtonUtils.themeBorderlessButton(alertDialog.getButton(AlertDialog.BUTTON_POSITIVE))
@RequiresApi(Build.VERSION_CODES.R)
private fun hasManageAllFilesActivity(context: Context): Boolean {
val intent = getManageAllFilesIntent(context)
val launchables: List<ResolveInfo> = context.packageManager
.queryIntentActivities(intent, PackageManager.GET_RESOLVED_FILTER)
return launchables.isNotEmpty()
}
@RequiresApi(Build.VERSION_CODES.R)
private fun manifestHasAllFilesPermission(context: Context): Boolean {
val packageInfo = context.packageManager.getPackageInfo(context.packageName, PackageManager.GET_PERMISSIONS)
return packageInfo?.requestedPermissions?.contains(Manifest.permission.MANAGE_EXTERNAL_STORAGE) ?: false
}
/**
* sdk >= 30: Choice between All Files access or read_external_storage
*/
@RequiresApi(Build.VERSION_CODES.R)
private fun showPermissionChoiceDialog(activity: AppCompatActivity, permissionRequired: Boolean) {
val preferences: AppPreferences = AppPreferencesImpl.fromContext(activity)
if (!preferences.isStoragePermissionRequested || permissionRequired) {
if (activity.supportFragmentManager.findFragmentByTag(PERMISSION_CHOICE_DIALOG_TAG) == null) {
val listener = object : StoragePermissionDialogFragment.Listener {
override fun onCancel() {
preferences.isStoragePermissionRequested = true
}
override fun onClickFullAccess() {
preferences.isStoragePermissionRequested = true
val intent = getManageAllFilesIntent(activity)
activity.startActivityForResult(intent, REQUEST_CODE_MANAGE_ALL_FILES)
preferences.isStoragePermissionRequested = true
}
override fun onClickMediaReadOnly() {
preferences.isStoragePermissionRequested = true
requestStoragePermission(activity, Manifest.permission.READ_EXTERNAL_STORAGE)
}
}
val dialogFragment = StoragePermissionDialogFragment(listener, permissionRequired)
dialogFragment.show(activity.supportFragmentManager, PERMISSION_CHOICE_DIALOG_TAG)
}
}
}
@RequiresApi(Build.VERSION_CODES.R)
private fun getManageAllFilesIntent(context: Context) =
Intent().apply {
action = Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION
data = Uri.parse("package:${context.applicationContext.packageName}")
}
/**
* request camera permission.
*

View file

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Nextcloud Android client application
~
~ @author Álvaro Brey Vilas
~ Copyright (C) 2022 Álvaro Brey Vilas
~ Copyright (C) 2022 Nextcloud GmbH
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="true"
android:focusable="true"
android:orientation="vertical"
android:paddingHorizontal="?dialogPreferredPadding">
<TextView
android:id="@+id/storage_permission_explanation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
tools:text="@string/file_management_permission_optional_text" />
<com.google.android.material.button.MaterialButton
android:layout_marginTop="@dimen/standard_padding"
android:id="@+id/btn_full_access"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/storage_permission_full_access"
android:theme="@style/Button.Primary"
app:cornerRadius="@dimen/button_corner_radius"
app:layout_constraintTop_toBottomOf="@id/storage_permission_explanation" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_read_only"
style="@style/OutlinedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/storage_permission_media_read_only"
app:cornerRadius="@dimen/button_corner_radius"
app:layout_constraintTop_toBottomOf="@id/btn_full_access" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -998,10 +998,14 @@
<string name="search_error">Error getting search results</string>
<string name="load_more_results">Load more results</string>
<string name="file_management_permission">Permissions needed</string>
<string name="file_management_permission_text">%1$s needs file management permissions to work properly. Please enable it in the following screen to continue.</string>
<string name="file_management_permission_optional">Storage permissions</string>
<string name="file_management_permission_text">%1$s needs file management permissions to upload files. You can choose full access to all files, or read-only access to photos and videos.</string>
<string name="file_management_permission_optional_text">%1$s works best with permissions to access storage. You can choose full access to all files, or read-only access to photos and videos.</string>
<string name="file_list_empty_unified_search_no_results">No results found for your query</string>
<string name="file_list_empty_gallery">Found no images or videos</string>
<string name="error_creating_file_from_template">Error creating file from template</string>
<string name="no_send_app">No app available for sending the selected files</string>
<string name="pdf_zoom_tip">Tap on a page to zoom in</string>
<string name="storage_permission_full_access">Full access</string>
<string name="storage_permission_media_read_only">Media read-only</string>
</resources>