#383 optional media management permission
This commit is contained in:
parent
c89f17fe8a
commit
22149ffca2
8 changed files with 138 additions and 2 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue