#383 optional media management permission

This commit is contained in:
Thibault Deckers 2022-11-04 23:50:26 +01:00
parent c89f17fe8a
commit 22149ffca2
8 changed files with 138 additions and 2 deletions

View file

@ -10,6 +10,7 @@ All notable changes to this project will be documented in this file.
- Info: edit location by copying from other item
- Info: edit tags with dynamic placeholders for country / place
- Widget: option to open collection on tap
- optional MANAGE_MEDIA permission to modify media without asking
### Changed

View file

@ -24,9 +24,14 @@ This change eventually prevents building the app with Flutter v3.3.3.
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29"
tools:ignore="ScopedStorage" />
<!-- from Android 12 (API 31), users can optionally grant access to the media management special permission -->
<uses-permission
android:name="android.permission.MANAGE_MEDIA"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.SET_WALLPAPER" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!-- to show foreground service progress via notification -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

View file

@ -3,7 +3,10 @@ package deckers.thibault.aves.channel.calls
import android.content.Context
import android.content.Intent
import android.content.res.Resources
import android.net.Uri
import android.os.Build
import android.provider.MediaStore
import android.provider.Settings
import androidx.core.content.pm.ShortcutManagerCompat
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.model.FieldMap
@ -15,15 +18,21 @@ import java.util.*
class DeviceHandler(private val context: Context) : MethodCallHandler {
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"canManageMedia" -> safe(call, result, ::canManageMedia)
"getCapabilities" -> safe(call, result, ::getCapabilities)
"getDefaultTimeZone" -> safe(call, result, ::getDefaultTimeZone)
"getLocales" -> safe(call, result, ::getLocales)
"getPerformanceClass" -> safe(call, result, ::getPerformanceClass)
"isSystemFilePickerEnabled" -> safe(call, result, ::isSystemFilePickerEnabled)
"requestMediaManagePermission" -> safe(call, result, ::requestMediaManagePermission)
else -> result.notImplemented()
}
}
private fun canManageMedia(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
result.success(if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) MediaStore.canManageMedia(context) else false)
}
private fun getCapabilities(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
val sdkInt = Build.VERSION.SDK_INT
result.success(
@ -32,6 +41,7 @@ class DeviceHandler(private val context: Context) : MethodCallHandler {
"canPinShortcut" to ShortcutManagerCompat.isRequestPinShortcutSupported(context),
"canPrint" to (sdkInt >= Build.VERSION_CODES.KITKAT),
"canRenderFlagEmojis" to (sdkInt >= Build.VERSION_CODES.LOLLIPOP),
"canRequestManageMedia" to (sdkInt >= Build.VERSION_CODES.S),
"canSetLockScreenWallpaper" to (sdkInt >= Build.VERSION_CODES.N),
"isDynamicColorAvailable" to (sdkInt >= Build.VERSION_CODES.S),
"showPinShortcutFeedback" to (sdkInt >= Build.VERSION_CODES.O),
@ -90,6 +100,17 @@ class DeviceHandler(private val context: Context) : MethodCallHandler {
result.success(enabled)
}
private fun requestMediaManagePermission(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
result.error("requestMediaManagePermission-unsupported", "media management permission is not available before Android 12", null)
return
}
val intent = Intent(Settings.ACTION_REQUEST_MANAGE_MEDIA, Uri.parse("package:${context.packageName}"))
context.startActivity(intent)
result.success(true)
}
companion object {
const val CHANNEL = "deckers.thibault/aves/device"
}

View file

@ -760,6 +760,7 @@
"settingsSaveSearchHistory": "Save search history",
"settingsEnableBin": "Use recycle bin",
"settingsEnableBinSubtitle": "Keep deleted items for 30 days",
"settingsAllowMediaManagement": "Allow media management",
"settingsHiddenItemsTile": "Hidden items",
"settingsHiddenItemsPageTitle": "Hidden Items",

View file

@ -5,7 +5,7 @@ final Device device = Device._private();
class Device {
late final String _userAgent;
late final bool _canGrantDirectoryAccess, _canPinShortcut, _canPrint, _canRenderFlagEmojis, _canSetLockScreenWallpaper;
late final bool _canGrantDirectoryAccess, _canPinShortcut, _canPrint, _canRenderFlagEmojis, _canRequestManageMedia, _canSetLockScreenWallpaper;
late final bool _isDynamicColorAvailable, _showPinShortcutFeedback, _supportEdgeToEdgeUIMode;
String get userAgent => _userAgent;
@ -18,6 +18,8 @@ class Device {
bool get canRenderFlagEmojis => _canRenderFlagEmojis;
bool get canRequestManageMedia => _canRequestManageMedia;
bool get canSetLockScreenWallpaper => _canSetLockScreenWallpaper;
bool get isDynamicColorAvailable => _isDynamicColorAvailable;
@ -37,6 +39,7 @@ class Device {
_canPinShortcut = capabilities['canPinShortcut'] ?? false;
_canPrint = capabilities['canPrint'] ?? false;
_canRenderFlagEmojis = capabilities['canRenderFlagEmojis'] ?? false;
_canRequestManageMedia = capabilities['canRequestManageMedia'] ?? false;
_canSetLockScreenWallpaper = capabilities['canSetLockScreenWallpaper'] ?? false;
_isDynamicColorAvailable = capabilities['isDynamicColorAvailable'] ?? false;
_showPinShortcutFeedback = capabilities['showPinShortcutFeedback'] ?? false;

View file

@ -4,6 +4,8 @@ import 'package:aves/services/common/services.dart';
import 'package:flutter/services.dart';
abstract class DeviceService {
Future<bool> canManageMedia();
Future<Map<String, dynamic>> getCapabilities();
Future<String?> getDefaultTimeZone();
@ -13,11 +15,24 @@ abstract class DeviceService {
Future<int> getPerformanceClass();
Future<bool> isSystemFilePickerEnabled();
Future<void> requestMediaManagePermission();
}
class PlatformDeviceService implements DeviceService {
static const _platform = MethodChannel('deckers.thibault/aves/device');
@override
Future<bool> canManageMedia() async {
try {
final result = await _platform.invokeMethod('canManageMedia');
if (result != null) return result as bool;
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
return false;
}
@override
Future<Map<String, dynamic>> getCapabilities() async {
try {
@ -80,4 +95,13 @@ class PlatformDeviceService implements DeviceService {
}
return false;
}
@override
Future<void> requestMediaManagePermission() async {
try {
await _platform.invokeMethod('requestMediaManagePermission');
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
}
}

View file

@ -3,6 +3,7 @@ import 'dart:async';
import 'package:aves/app_flavor.dart';
import 'package:aves/model/device.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/theme/colors.dart';
import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
@ -33,6 +34,7 @@ class PrivacySection extends SettingsSection {
return [
SettingsTilePrivacyAllowInstalledAppAccess(),
if (canEnableErrorReporting) SettingsTilePrivacyAllowErrorReporting(),
if (device.canRequestManageMedia) SettingsTilePrivacyManageMedia(),
SettingsTilePrivacySaveSearchHistory(),
SettingsTilePrivacyEnableBin(),
SettingsTilePrivacyHiddenItems(),
@ -124,3 +126,65 @@ class SettingsTilePrivacyStorageAccess extends SettingsTile {
builder: (context) => const StorageAccessPage(),
);
}
class SettingsTilePrivacyManageMedia extends SettingsTile {
@override
String title(BuildContext context) => context.l10n.settingsAllowMediaManagement;
@override
Widget build(BuildContext context) => _ManageMediaTile(title: title(context));
}
class _ManageMediaTile extends StatefulWidget {
final String title;
const _ManageMediaTile({
required this.title,
});
@override
State<_ManageMediaTile> createState() => _ManageMediaTileState();
}
class _ManageMediaTileState extends State<_ManageMediaTile> with WidgetsBindingObserver {
late Future<bool> _loader;
@override
void initState() {
super.initState();
_initLoader();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
void _initLoader() => _loader = deviceService.canManageMedia();
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
_initLoader();
setState(() {});
}
}
@override
Widget build(BuildContext context) {
return FutureBuilder<bool>(
future: _loader,
builder: (context, snapshot) {
final loading = snapshot.connectionState != ConnectionState.done;
final current = snapshot.data ?? false;
return SwitchListTile(
value: current,
onChanged: loading ? null : (v) => deviceService.requestMediaManagePermission(),
title: Text(widget.title),
);
},
);
}
}

View file

@ -1,6 +1,7 @@
{
"de": [
"editEntryLocationDialogSetCustom",
"settingsAllowMediaManagement",
"tagEditorSectionPlaceholders",
"tagPlaceholderCountry",
"tagPlaceholderPlace"
@ -8,6 +9,7 @@
"el": [
"editEntryLocationDialogSetCustom",
"settingsAllowMediaManagement",
"tagEditorSectionPlaceholders",
"tagPlaceholderCountry",
"tagPlaceholderPlace"
@ -18,6 +20,7 @@
"widgetOpenPageCollection",
"widgetOpenPageViewer",
"editEntryLocationDialogSetCustom",
"settingsAllowMediaManagement",
"tagEditorSectionPlaceholders",
"tagPlaceholderCountry",
"tagPlaceholderPlace"
@ -507,6 +510,7 @@
"settingsSaveSearchHistory",
"settingsEnableBin",
"settingsEnableBinSubtitle",
"settingsAllowMediaManagement",
"settingsHiddenItemsTile",
"settingsHiddenItemsPageTitle",
"settingsHiddenItemsTabFilters",
@ -606,6 +610,7 @@
"fr": [
"editEntryLocationDialogSetCustom",
"settingsAllowMediaManagement",
"tagEditorSectionPlaceholders",
"tagPlaceholderCountry",
"tagPlaceholderPlace"
@ -962,6 +967,7 @@
"settingsSaveSearchHistory",
"settingsEnableBin",
"settingsEnableBinSubtitle",
"settingsAllowMediaManagement",
"settingsHiddenItemsTile",
"settingsHiddenItemsPageTitle",
"settingsHiddenItemsTabFilters",
@ -1073,6 +1079,7 @@
"albumMimeTypeMixed",
"settingsDisabled",
"settingsSlideshowAnimatedZoomEffect",
"settingsAllowMediaManagement",
"settingsWidgetOpenPage",
"statsTopAlbumsSectionTitle",
"wallpaperUseScrollEffect",
@ -1083,6 +1090,7 @@
"it": [
"editEntryLocationDialogSetCustom",
"settingsAllowMediaManagement",
"tagEditorSectionPlaceholders",
"tagPlaceholderCountry",
"tagPlaceholderPlace"
@ -1118,6 +1126,7 @@
"settingsConfirmationAfterMoveToBinItems",
"settingsViewerGestureSideTapNext",
"settingsSlideshowAnimatedZoomEffect",
"settingsAllowMediaManagement",
"settingsWidgetOpenPage",
"statsTopAlbumsSectionTitle",
"viewerInfoLabelDescription",
@ -1129,6 +1138,7 @@
"ko": [
"editEntryLocationDialogSetCustom",
"settingsAllowMediaManagement",
"tagEditorSectionPlaceholders",
"tagPlaceholderCountry",
"tagPlaceholderPlace"
@ -1222,6 +1232,7 @@
"settingsSlideshowShuffle",
"settingsSubtitleThemeSample",
"settingsAllowInstalledAppAccess",
"settingsAllowMediaManagement",
"settingsHiddenFiltersBanner",
"settingsHiddenFiltersEmpty",
"settingsHiddenItemsTabPaths",
@ -1253,6 +1264,7 @@
"editEntryLocationDialogSetCustom",
"aboutLinkPolicy",
"policyPageTitle",
"settingsAllowMediaManagement",
"tagEditorSectionPlaceholders",
"tagPlaceholderCountry",
"tagPlaceholderPlace"
@ -1649,6 +1661,7 @@
"settingsSaveSearchHistory",
"settingsEnableBin",
"settingsEnableBinSubtitle",
"settingsAllowMediaManagement",
"settingsHiddenItemsTile",
"settingsHiddenItemsPageTitle",
"settingsHiddenItemsTabFilters",
@ -1748,6 +1761,7 @@
"pt": [
"editEntryLocationDialogSetCustom",
"settingsAllowMediaManagement",
"tagEditorSectionPlaceholders",
"tagPlaceholderCountry",
"tagPlaceholderPlace"
@ -1755,6 +1769,7 @@
"ru": [
"editEntryLocationDialogSetCustom",
"settingsAllowMediaManagement",
"tagEditorSectionPlaceholders",
"tagPlaceholderCountry",
"tagPlaceholderPlace"
@ -1813,6 +1828,7 @@
"settingsSlideshowIntervalTile",
"settingsSlideshowVideoPlaybackTile",
"settingsSlideshowVideoPlaybackDialogTitle",
"settingsAllowMediaManagement",
"settingsScreenSaverPageTitle",
"settingsWidgetShowOutline",
"settingsWidgetOpenPage",
@ -1827,6 +1843,7 @@
"zh": [
"editEntryLocationDialogSetCustom",
"settingsAllowMediaManagement",
"tagEditorSectionPlaceholders",
"tagPlaceholderCountry",
"tagPlaceholderPlace"