mirror of
https://github.com/nextcloud/android.git
synced 2024-11-28 02:17:43 +03:00
Merge pull request #9873 from nextcloud/optional-files-permission
Make external storage permission optional
This commit is contained in:
commit
91520971cd
12 changed files with 381 additions and 153 deletions
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -373,4 +373,8 @@ public interface AppPreferences {
|
|||
void setPdfZoomTipShownCount(int count);
|
||||
|
||||
int getPdfZoomTipShownCount();
|
||||
|
||||
boolean isStoragePermissionRequested();
|
||||
|
||||
void setStoragePermissionRequested(boolean value);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
*
|
||||
|
|
58
app/src/main/res/layout/storage_permission_dialog.xml
Normal file
58
app/src/main/res/layout/storage_permission_dialog.xml
Normal 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>
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue