#1160 secure view: prevent access to export actions

This commit is contained in:
Thibault Deckers 2024-09-12 23:29:25 +02:00
parent 31f4737d3c
commit f9bfbd5bea
12 changed files with 65 additions and 33 deletions

View file

@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.app.SearchManager
import android.appwidget.AppWidgetManager
import android.content.ClipData
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
@ -317,6 +318,13 @@ open class MainActivity : FlutterFragmentActivity() {
INTENT_DATA_KEY_URI to uri.toString(),
)
val keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as android.app.KeyguardManager
val isLocked = keyguardManager.isKeyguardLocked
if (isLocked) {
// device is locked, so access to content is limited to intent URI by default
fields[INTENT_DATA_KEY_SECURE_URIS] = listOf(uri.toString())
}
if (action == MediaStore.ACTION_REVIEW_SECURE) {
val uris = ArrayList<String>()
intent.clipData?.let { clipData ->
@ -324,7 +332,9 @@ open class MainActivity : FlutterFragmentActivity() {
clipData.getItemAt(i).uri?.let { uris.add(it.toString()) }
}
}
fields[INTENT_DATA_KEY_SECURE_URIS] = uris
if (uris.isNotEmpty()) {
fields[INTENT_DATA_KEY_SECURE_URIS] = uris
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && intent.hasExtra(MediaStore.EXTRA_BRIGHTNESS)) {
fields[INTENT_DATA_KEY_BRIGHTNESS] = intent.getFloatExtra(MediaStore.EXTRA_BRIGHTNESS, 0f)

View file

@ -33,6 +33,7 @@ class DeviceHandler(private val context: Context) : MethodCallHandler {
"getDefaultTimeZoneRawOffsetMillis" -> safe(call, result, ::getDefaultTimeZoneRawOffsetMillis)
"getLocales" -> safe(call, result, ::getLocales)
"getPerformanceClass" -> safe(call, result, ::getPerformanceClass)
"isLocked" -> safe(call, result, ::isLocked)
"isSystemFilePickerEnabled" -> safe(call, result, ::isSystemFilePickerEnabled)
"requestMediaManagePermission" -> safe(call, result, ::requestMediaManagePermission)
"getAvailableHeapSize" -> safe(call, result, ::getAvailableHeapSize)
@ -49,13 +50,11 @@ class DeviceHandler(private val context: Context) : MethodCallHandler {
val sdkInt = Build.VERSION.SDK_INT
result.success(
hashMapOf(
"canGrantDirectoryAccess" to (sdkInt >= Build.VERSION_CODES.LOLLIPOP),
"canPinShortcut" to ShortcutManagerCompat.isRequestPinShortcutSupported(context),
"canRenderFlagEmojis" to (sdkInt >= Build.VERSION_CODES.M),
"canRenderSubdivisionFlagEmojis" to (sdkInt >= Build.VERSION_CODES.O),
"canRequestManageMedia" to (sdkInt >= Build.VERSION_CODES.S),
"canSetLockScreenWallpaper" to (sdkInt >= Build.VERSION_CODES.N),
"canUseCrypto" to (sdkInt >= Build.VERSION_CODES.LOLLIPOP),
"hasGeocoder" to Geocoder.isPresent(),
"isDynamicColorAvailable" to DynamicColors.isDynamicColorAvailable(),
"showPinShortcutFeedback" to (sdkInt >= Build.VERSION_CODES.O),
@ -100,6 +99,12 @@ class DeviceHandler(private val context: Context) : MethodCallHandler {
result.success(Build.VERSION.SDK_INT)
}
private fun isLocked(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
val keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as android.app.KeyguardManager
val isLocked = keyguardManager.isKeyguardLocked
result.success(isLocked)
}
private fun isSystemFilePickerEnabled(@Suppress("unused_parameter") call: MethodCall, result: MethodChannel.Result) {
val enabled = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).resolveActivity(context.packageManager) != null
result.success(enabled)

View file

@ -6,8 +6,12 @@ import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/foundation.dart';
abstract class AvesAvailability {
Future<void> onNewIntent();
void onResume();
bool get isLocked;
Future<bool> get isConnected;
Future<bool> get canLocatePlaces;
@ -16,15 +20,24 @@ abstract class AvesAvailability {
}
class LiveAvesAvailability implements AvesAvailability {
bool? _isConnected;
bool? _isConnected, _isLocked;
LiveAvesAvailability() {
Connectivity().onConnectivityChanged.listen(_updateConnectivityFromResult);
}
@override
Future<void> onNewIntent() async {
_isLocked = await deviceService.isLocked();
debugPrint('Device is locked=$_isLocked');
}
@override
void onResume() => _isConnected = null;
@override
bool get isLocked => _isLocked ?? false;
@override
Future<bool> get isConnected async {
if (_isConnected != null) return SynchronousFuture(_isConnected!);

View file

@ -9,8 +9,8 @@ final Device device = Device._private();
class Device {
late final String _packageName, _packageVersion, _userAgent;
late final bool _canAuthenticateUser, _canGrantDirectoryAccess, _canPinShortcut;
late final bool _canRenderFlagEmojis, _canRenderSubdivisionFlagEmojis, _canRequestManageMedia, _canSetLockScreenWallpaper, _canUseCrypto;
late final bool _canAuthenticateUser, _canPinShortcut;
late final bool _canRenderFlagEmojis, _canRenderSubdivisionFlagEmojis, _canRequestManageMedia, _canSetLockScreenWallpaper;
late final bool _hasGeocoder, _isDynamicColorAvailable, _isTelevision, _showPinShortcutFeedback, _supportEdgeToEdgeUIMode, _supportPictureInPicture;
String get packageName => _packageName;
@ -21,8 +21,6 @@ class Device {
bool get canAuthenticateUser => _canAuthenticateUser;
bool get canGrantDirectoryAccess => _canGrantDirectoryAccess;
bool get canPinShortcut => _canPinShortcut;
bool get canRenderFlagEmojis => _canRenderFlagEmojis;
@ -33,10 +31,6 @@ class Device {
bool get canSetLockScreenWallpaper => _canSetLockScreenWallpaper;
bool get canUseCrypto => _canUseCrypto;
bool get canUseVaults => canAuthenticateUser || canUseCrypto;
bool get hasGeocoder => _hasGeocoder;
bool get isDynamicColorAvailable => _isDynamicColorAvailable;
@ -71,13 +65,11 @@ class Device {
}
final capabilities = await deviceService.getCapabilities();
_canGrantDirectoryAccess = capabilities['canGrantDirectoryAccess'] ?? false;
_canPinShortcut = capabilities['canPinShortcut'] ?? false;
_canRenderFlagEmojis = capabilities['canRenderFlagEmojis'] ?? false;
_canRenderSubdivisionFlagEmojis = capabilities['canRenderSubdivisionFlagEmojis'] ?? false;
_canRequestManageMedia = capabilities['canRequestManageMedia'] ?? false;
_canSetLockScreenWallpaper = capabilities['canSetLockScreenWallpaper'] ?? false;
_canUseCrypto = capabilities['canUseCrypto'] ?? false;
_hasGeocoder = capabilities['hasGeocoder'] ?? false;
_isDynamicColorAvailable = capabilities['isDynamicColorAvailable'] ?? false;
_showPinShortcutFeedback = capabilities['showPinShortcutFeedback'] ?? false;

View file

@ -14,6 +14,8 @@ abstract class DeviceService {
Future<int> getPerformanceClass();
Future<bool> isLocked();
Future<bool> isSystemFilePickerEnabled();
Future<void> requestMediaManagePermission();
@ -89,6 +91,17 @@ class PlatformDeviceService implements DeviceService {
return 0;
}
@override
Future<bool> isLocked() async {
try {
final result = await _platform.invokeMethod('isLocked');
if (result != null) return result as bool;
} on PlatformException catch (e, stack) {
await reportService.recordError(e, stack);
}
return false;
}
@override
Future<bool> isSystemFilePickerEnabled() async {
try {

View file

@ -25,14 +25,11 @@ class _DebugDeviceSectionState extends State<DebugDeviceSection> with AutomaticK
'packageVersion': device.packageVersion,
'userAgent': device.userAgent,
'canAuthenticateUser': '${device.canAuthenticateUser}',
'canGrantDirectoryAccess': '${device.canGrantDirectoryAccess}',
'canPinShortcut': '${device.canPinShortcut}',
'canRenderFlagEmojis': '${device.canRenderFlagEmojis}',
'canRenderSubdivisionFlagEmojis': '${device.canRenderSubdivisionFlagEmojis}',
'canRequestManageMedia': '${device.canRequestManageMedia}',
'canSetLockScreenWallpaper': '${device.canSetLockScreenWallpaper}',
'canUseCrypto': '${device.canUseCrypto}',
'canUseVaults': '${device.canUseVaults}',
'hasGeocoder': '${device.hasGeocoder}',
'isDynamicColorAvailable': '${device.isDynamicColorAvailable}',
'isTelevision': '${device.isTelevision}',

View file

@ -42,11 +42,9 @@ class _EditVaultDialogState extends State<EditVaultDialog> with FeedbackMixin, V
final List<VaultLockType> _lockTypeOptions = [
if (device.canAuthenticateUser) VaultLockType.system,
if (device.canUseCrypto) ...[
VaultLockType.pattern,
VaultLockType.pin,
VaultLockType.password,
],
VaultLockType.pattern,
VaultLockType.pin,
VaultLockType.password,
];
VaultDetails? get initialDetails => widget.initialDetails;

View file

@ -1,7 +1,6 @@
import 'dart:io';
import 'package:aves/app_mode.dart';
import 'package:aves/model/device.dart';
import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/filters/album.dart';
import 'package:aves/model/filters/filters.dart';
@ -78,12 +77,10 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> with
final selectedSingleItem = selectedFilters.length == 1;
final isMain = appMode == AppMode.main;
final canCreate = !settings.isReadOnly && appMode.canCreateFilter && !isSelecting;
switch (action) {
case ChipSetAction.createAlbum:
return canCreate;
case ChipSetAction.createVault:
return canCreate && device.canUseVaults;
return !settings.isReadOnly && appMode.canCreateFilter && !isSelecting;
case ChipSetAction.delete:
case ChipSetAction.rename:
return isMain && isSelecting && !settings.isReadOnly;

View file

@ -98,6 +98,7 @@ class _HomePageState extends State<HomePage> {
_initialExplorerPath = null;
_secureUris = null;
await availability.onNewIntent();
await androidFileUtils.init();
if (!{
IntentActions.edit,

View file

@ -43,7 +43,7 @@ class PrivacySection extends SettingsSection {
SettingsTilePrivacySaveSearchHistory(),
if (!settings.useTvLayout) SettingsTilePrivacyEnableBin(),
SettingsTilePrivacyHiddenItems(),
if (!settings.useTvLayout && device.canGrantDirectoryAccess) SettingsTilePrivacyStorageAccess(),
if (!settings.useTvLayout) SettingsTilePrivacyStorageAccess(),
];
}
}

View file

@ -159,6 +159,10 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
case EntryAction.convertMotionPhotoToStillImage:
case EntryAction.viewMotionPhotoVideo:
return _metadataActionDelegate.canApply(targetEntry, action);
case EntryAction.convert:
case EntryAction.copy:
case EntryAction.move:
return !availability.isLocked;
default:
return true;
}

View file

@ -7,6 +7,7 @@ import 'package:aves/model/entry/extensions/props.dart';
import 'package:aves/model/settings/enums/accessibility_animations.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/theme/icons.dart';
import 'package:aves/view/view.dart';
import 'package:aves/widgets/common/action_controls/quick_choosers/move_button.dart';
@ -278,6 +279,7 @@ class _ViewerButtonRowContentState extends State<ViewerButtonRowContent> {
...topLevelActions.map((action) => _buildPopupMenuItem(context, action, videoController)),
if (exportActions.isNotEmpty)
PopupMenuExpansionPanel<EntryAction>(
enabled: !availability.isLocked,
value: 'export',
expandedNotifier: _popupExpandedNotifier,
icon: AIcons.export,
@ -345,18 +347,18 @@ class _ViewerButtonRowContentState extends State<ViewerButtonRowContent> {
}
PopupMenuItem<EntryAction> _buildPopupMenuItem(BuildContext context, EntryAction action, AvesVideoController? videoController) {
late final bool enabled;
var enabled = widget.actionDelegate.canApply(action);
switch (action) {
case EntryAction.videoCaptureFrame:
enabled = videoController?.canCaptureFrameNotifier.value ?? false;
enabled &= videoController?.canCaptureFrameNotifier.value ?? false;
case EntryAction.videoToggleMute:
enabled = videoController?.canMuteNotifier.value ?? false;
enabled &= videoController?.canMuteNotifier.value ?? false;
case EntryAction.videoSelectStreams:
enabled = videoController?.canSelectStreamNotifier.value ?? false;
enabled &= videoController?.canSelectStreamNotifier.value ?? false;
case EntryAction.videoSetSpeed:
enabled = videoController?.canSetSpeedNotifier.value ?? false;
enabled &= videoController?.canSetSpeedNotifier.value ?? false;
default:
enabled = true;
break;
}
Widget? child;