mirror of
https://github.com/immich-app/immich
synced 2026-08-15 13:03:57 +00:00
refactor: gallery permission notifier (#30477)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
This commit is contained in:
parent
79e61c47c7
commit
e9eafc3161
10 changed files with 260 additions and 127 deletions
|
|
@ -24,3 +24,14 @@ enum SlideshowLook { contain, cover, blurredBackground }
|
|||
enum SlideshowDirection { forward, backward, shuffle }
|
||||
|
||||
enum PartnerDirection { sharedBy, sharedWith }
|
||||
|
||||
enum DevicePermission { photos, videos, storage, mediaLocation }
|
||||
|
||||
enum DevicePermissionStatus {
|
||||
denied,
|
||||
granted,
|
||||
limited,
|
||||
permanentlyDenied;
|
||||
|
||||
bool get hasAccess => this == granted || this == limited;
|
||||
}
|
||||
|
|
|
|||
47
mobile/lib/domain/services/device_permission.service.dart
Normal file
47
mobile/lib/domain/services/device_permission.service.dart
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||
import 'package:immich_mobile/repositories/permission.repository.dart';
|
||||
|
||||
typedef _Handler = Future<DevicePermissionStatus> Function(DevicePermission);
|
||||
|
||||
class DevicePermissionService {
|
||||
const DevicePermissionService(this._permissionRepository);
|
||||
|
||||
final DevicePermissionRepository _permissionRepository;
|
||||
|
||||
Future<DevicePermissionStatus> galleryStatus() => _gallery(_permissionRepository.getStatus);
|
||||
|
||||
Future<DevicePermissionStatus> requestGallery() => _gallery(_permissionRepository.request);
|
||||
|
||||
Future<DevicePermissionStatus> _gallery(_Handler handler) async {
|
||||
if (CurrentPlatform.isIOS) {
|
||||
return handler(.photos);
|
||||
}
|
||||
|
||||
final sdkVersion = await _permissionRepository.getAndroidSdkVersion();
|
||||
const maxExternalStorageSdk = 32; // READ/WRITE_EXTERNAL_STORAGE - Android 12.1
|
||||
final status = sdkVersion <= maxExternalStorageSdk ? await handler(.storage) : await _photosAndVideos(handler);
|
||||
|
||||
const minMediaLocationSdk = 29; // ACCESS_MEDIA_LOCATION - Android 10
|
||||
if (status.hasAccess && sdkVersion >= minMediaLocationSdk) {
|
||||
final mediaLocation = await handler(.mediaLocation);
|
||||
return mediaLocation.hasAccess ? status : mediaLocation;
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
Future<DevicePermissionStatus> _photosAndVideos(_Handler handler) async {
|
||||
final photos = await handler(.photos);
|
||||
if (!photos.hasAccess) {
|
||||
return photos;
|
||||
}
|
||||
|
||||
final videos = await handler(.videos);
|
||||
if (!videos.hasAccess) {
|
||||
return videos;
|
||||
}
|
||||
|
||||
return photos == .granted && videos == .granted ? .granted : .limited;
|
||||
}
|
||||
}
|
||||
|
|
@ -27,7 +27,7 @@ class LocalSyncService {
|
|||
final NativeSyncApi _nativeSyncApi;
|
||||
final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository;
|
||||
final AssetMediaRepository _assetMediaRepository;
|
||||
final IPermissionRepository _permissionRepository;
|
||||
final DevicePermissionRepository _permissionRepository;
|
||||
final Completer<void>? _cancellation;
|
||||
final Logger _log = Logger("DeviceSyncService");
|
||||
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ class SyncStreamService {
|
|||
final DriftLocalAssetRepository _localAssetRepository;
|
||||
final DriftTrashedLocalAssetRepository _trashedLocalAssetRepository;
|
||||
final AssetMediaRepository _assetMediaRepository;
|
||||
final IPermissionRepository _permissionRepository;
|
||||
final DevicePermissionRepository _permissionRepository;
|
||||
final SyncMigrationRepository _syncMigrationRepository;
|
||||
final ApiService _api;
|
||||
final Completer<void>? _cancellation;
|
||||
|
|
|
|||
|
|
@ -1,109 +1,28 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/services/device_permission.service.dart';
|
||||
import 'package:immich_mobile/repositories/permission.repository.dart';
|
||||
|
||||
class GalleryPermissionNotifier extends StateNotifier<PermissionStatus> {
|
||||
GalleryPermissionNotifier()
|
||||
: super(PermissionStatus.denied) // Denied is the initial state
|
||||
{
|
||||
// Sets the initial state
|
||||
final galleryPermissionServiceProvider = Provider((ref) {
|
||||
return DevicePermissionService(ref.watch(permissionRepositoryProvider));
|
||||
});
|
||||
|
||||
class GalleryPermissionNotifier extends StateNotifier<DevicePermissionStatus> {
|
||||
GalleryPermissionNotifier(this._service) : super(.denied) {
|
||||
unawaited(getGalleryPermissionStatus());
|
||||
}
|
||||
|
||||
bool get hasPermission => state.isGranted || state.isLimited;
|
||||
final DevicePermissionService _service;
|
||||
|
||||
/// Requests the gallery permission
|
||||
Future<PermissionStatus> requestGalleryPermission() async {
|
||||
PermissionStatus result;
|
||||
// Android 32 and below uses Permission.storage
|
||||
if (Platform.isAndroid) {
|
||||
final androidInfo = await DeviceInfoPlugin().androidInfo;
|
||||
if (androidInfo.version.sdkInt <= 32) {
|
||||
// Android 32 and below need storage
|
||||
final permission = await Permission.storage.request();
|
||||
result = permission;
|
||||
} else {
|
||||
// Android 33 need photo & video
|
||||
final photos = await Permission.photos.request();
|
||||
if (!photos.isGranted) {
|
||||
// Don't ask twice for the same permission
|
||||
state = photos;
|
||||
return photos;
|
||||
}
|
||||
final videos = await Permission.videos.request();
|
||||
bool get hasPermission => state.hasAccess;
|
||||
|
||||
// Return the joint result of those two permissions
|
||||
final PermissionStatus status;
|
||||
if ((photos.isGranted && videos.isGranted) || (photos.isLimited && videos.isLimited)) {
|
||||
status = PermissionStatus.granted;
|
||||
} else if (photos.isDenied || videos.isDenied) {
|
||||
status = PermissionStatus.denied;
|
||||
} else if (photos.isPermanentlyDenied || videos.isPermanentlyDenied) {
|
||||
status = PermissionStatus.permanentlyDenied;
|
||||
} else {
|
||||
status = PermissionStatus.denied;
|
||||
Future<DevicePermissionStatus> requestGalleryPermission() async => state = await _service.requestGallery();
|
||||
|
||||
Future<DevicePermissionStatus> getGalleryPermissionStatus() async => state = await _service.galleryStatus();
|
||||
}
|
||||
|
||||
result = status;
|
||||
}
|
||||
if (result == PermissionStatus.granted && androidInfo.version.sdkInt >= 29) {
|
||||
result = await Permission.accessMediaLocation.request();
|
||||
}
|
||||
} else {
|
||||
// iOS can use photos
|
||||
final photos = await Permission.photos.request();
|
||||
result = photos;
|
||||
}
|
||||
state = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Checks the current state of the gallery permissions without
|
||||
/// requesting them again
|
||||
Future<PermissionStatus> getGalleryPermissionStatus() async {
|
||||
PermissionStatus result;
|
||||
// Android 32 and below uses Permission.storage
|
||||
if (Platform.isAndroid) {
|
||||
final androidInfo = await DeviceInfoPlugin().androidInfo;
|
||||
if (androidInfo.version.sdkInt <= 32) {
|
||||
// Android 32 and below need storage
|
||||
final permission = await Permission.storage.status;
|
||||
result = permission;
|
||||
} else {
|
||||
// Android 33 needs photo & video
|
||||
final photos = await Permission.photos.status;
|
||||
final videos = await Permission.videos.status;
|
||||
|
||||
// Return the joint result of those two permissions
|
||||
final PermissionStatus status;
|
||||
if ((photos.isGranted && videos.isGranted) || (photos.isLimited && videos.isLimited)) {
|
||||
status = PermissionStatus.granted;
|
||||
} else if (photos.isDenied || videos.isDenied) {
|
||||
status = PermissionStatus.denied;
|
||||
} else if (photos.isPermanentlyDenied || videos.isPermanentlyDenied) {
|
||||
status = PermissionStatus.permanentlyDenied;
|
||||
} else {
|
||||
status = PermissionStatus.denied;
|
||||
}
|
||||
|
||||
result = status;
|
||||
}
|
||||
if (state == PermissionStatus.granted && androidInfo.version.sdkInt >= 29) {
|
||||
result = await Permission.accessMediaLocation.status;
|
||||
}
|
||||
} else {
|
||||
// iOS can use photos
|
||||
final photos = await Permission.photos.status;
|
||||
result = photos;
|
||||
}
|
||||
state = result;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
final galleryPermissionNotifier = StateNotifierProvider<GalleryPermissionNotifier, PermissionStatus>(
|
||||
(ref) => GalleryPermissionNotifier(),
|
||||
final galleryPermissionNotifier = StateNotifierProvider<GalleryPermissionNotifier, DevicePermissionStatus>(
|
||||
(ref) => GalleryPermissionNotifier(ref.watch(galleryPermissionServiceProvider)),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,67 +1,83 @@
|
|||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||
import 'package:immich_mobile/platform/permission_api.g.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:permission_handler/permission_handler.dart' as ph;
|
||||
|
||||
final permissionRepositoryProvider = Provider((ref) {
|
||||
return PermissionRepository(ref.watch(permissionApiProvider));
|
||||
return DevicePermissionRepository(ref.watch(permissionApiProvider));
|
||||
});
|
||||
|
||||
class PermissionRepository implements IPermissionRepository {
|
||||
class DevicePermissionRepository {
|
||||
final PermissionApi _permissionApi;
|
||||
|
||||
const PermissionRepository(this._permissionApi);
|
||||
const DevicePermissionRepository(this._permissionApi);
|
||||
|
||||
@override
|
||||
Future<bool> hasLocationWhenInUsePermission() {
|
||||
return Permission.locationWhenInUse.isGranted;
|
||||
Future<DevicePermissionStatus> getStatus(DevicePermission permission) async =>
|
||||
(await permission.handler.status).toDevicePermissionStatus();
|
||||
|
||||
Future<DevicePermissionStatus> request(DevicePermission permission) async =>
|
||||
(await permission.handler.request()).toDevicePermissionStatus();
|
||||
|
||||
// TODO(shenlong): Move this to it's own device info repo
|
||||
Future<int> getAndroidSdkVersion() async {
|
||||
if (CurrentPlatform.isIOS) {
|
||||
throw UnsupportedError("This method is available only on Android");
|
||||
}
|
||||
|
||||
@override
|
||||
final androidInfo = await DeviceInfoPlugin().androidInfo;
|
||||
return androidInfo.version.sdkInt;
|
||||
}
|
||||
|
||||
Future<bool> hasLocationWhenInUsePermission() => ph.Permission.locationWhenInUse.isGranted;
|
||||
|
||||
Future<bool> requestLocationWhenInUsePermission() async {
|
||||
final result = await Permission.locationWhenInUse.request();
|
||||
final result = await ph.Permission.locationWhenInUse.request();
|
||||
return result.isGranted;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> hasLocationAlwaysPermission() {
|
||||
return Permission.locationAlways.isGranted;
|
||||
return ph.Permission.locationAlways.isGranted;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> requestLocationAlwaysPermission() async {
|
||||
final result = await Permission.locationAlways.request();
|
||||
final result = await ph.Permission.locationAlways.request();
|
||||
return result.isGranted;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> openSettings() {
|
||||
return openAppSettings();
|
||||
return ph.openAppSettings();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> hasManageMediaPermission() {
|
||||
return _permissionApi.hasManageMediaPermission();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> requestManageMediaPermission() {
|
||||
return _permissionApi.requestManageMediaPermission();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> manageMediaPermission() {
|
||||
return _permissionApi.manageMediaPermission();
|
||||
}
|
||||
}
|
||||
|
||||
abstract interface class IPermissionRepository {
|
||||
Future<bool> hasLocationWhenInUsePermission();
|
||||
Future<bool> requestLocationWhenInUsePermission();
|
||||
Future<bool> hasLocationAlwaysPermission();
|
||||
Future<bool> requestLocationAlwaysPermission();
|
||||
Future<bool> openSettings();
|
||||
Future<bool> hasManageMediaPermission();
|
||||
Future<bool> requestManageMediaPermission();
|
||||
Future<bool> manageMediaPermission();
|
||||
extension on DevicePermission {
|
||||
ph.Permission get handler => switch (this) {
|
||||
.photos => ph.Permission.photos,
|
||||
.videos => ph.Permission.videos,
|
||||
.storage => ph.Permission.storage,
|
||||
.mediaLocation => ph.Permission.accessMediaLocation,
|
||||
};
|
||||
}
|
||||
|
||||
extension on ph.PermissionStatus {
|
||||
DevicePermissionStatus toDevicePermissionStatus() => switch (this) {
|
||||
.granted => .granted,
|
||||
.limited => .limited,
|
||||
.permanentlyDenied => .permanentlyDenied,
|
||||
_ => .denied,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ final networkServiceProvider = Provider((ref) {
|
|||
|
||||
class NetworkService {
|
||||
final NetworkRepository _repository;
|
||||
final IPermissionRepository _permissionRepository;
|
||||
final DevicePermissionRepository _permissionRepository;
|
||||
|
||||
const NetworkService(this._repository, this._permissionRepository);
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ class MockAssetApiRepository extends Mock implements AssetApiRepository {}
|
|||
|
||||
class MockAssetMediaRepository extends Mock implements AssetMediaRepository {}
|
||||
|
||||
class MockPermissionRepository extends Mock implements IPermissionRepository {}
|
||||
class MockPermissionRepository extends Mock implements DevicePermissionRepository {}
|
||||
|
||||
class MockAuthApiRepository extends Mock implements AuthApiRepository {}
|
||||
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ class RepositoryMocks {
|
|||
final trashedAsset = MockTrashedLocalAssetRepository();
|
||||
final remoteAlbum = MockRemoteAlbumRepository();
|
||||
final albumApi = MockDriftAlbumApiRepository();
|
||||
final permission = PermissionRepositoryStub(MockPermissionRepository());
|
||||
|
||||
final nativeApi = NativeSyncApiStub(MockNativeSyncApi());
|
||||
final assetApi = AssetApiRepositoryStub(MockAssetApiRepository());
|
||||
|
|
@ -55,6 +56,7 @@ class RepositoryMocks {
|
|||
assetApi.reset();
|
||||
assetMedia.reset();
|
||||
download.reset();
|
||||
permission.reset();
|
||||
_stubLocalAlbumRepository();
|
||||
_stubLocalAssetRepository();
|
||||
_stubRemoteAssetRepository();
|
||||
|
|
@ -63,6 +65,7 @@ class RepositoryMocks {
|
|||
_stubAssetApiRepository();
|
||||
_stubAssetMediaRepository();
|
||||
_stubDownloadRepository();
|
||||
_stubPermissionRepository();
|
||||
}
|
||||
|
||||
void _stubRemoteAssetRepository() {
|
||||
|
|
@ -101,6 +104,12 @@ class RepositoryMocks {
|
|||
void _stubDownloadRepository() {
|
||||
when(download.downloadAllAssets).thenAnswer((_) async => const []);
|
||||
}
|
||||
|
||||
void _stubPermissionRepository() {
|
||||
when(permission.getStatus).thenAnswer((_) async => DevicePermissionStatus.denied);
|
||||
when(permission.request).thenAnswer((_) async => DevicePermissionStatus.denied);
|
||||
when(permission.getAndroidSdkVersion).thenAnswer((_) async => 34);
|
||||
}
|
||||
}
|
||||
|
||||
class ServiceMocks {
|
||||
|
|
@ -220,6 +229,8 @@ void _registerFallbacks() {
|
|||
registerFallbackValue(ShareAssetType.original);
|
||||
registerFallbackValue(const UploadCallbacks());
|
||||
registerFallbackValue(_FakeBuildContext());
|
||||
registerFallbackValue(DevicePermissionStatus.granted);
|
||||
registerFallbackValue(DevicePermission.photos);
|
||||
}
|
||||
|
||||
class _FakeBuildContext extends Fake implements BuildContext {}
|
||||
|
|
@ -400,6 +411,17 @@ extension type const DownloadRepositoryStub(MockDownloadRepository repo) impleme
|
|||
() => repo.downloadAllAssets(any());
|
||||
}
|
||||
|
||||
extension type const PermissionRepositoryStub(MockPermissionRepository repo) implements Stub<MockPermissionRepository> {
|
||||
Future<DevicePermissionStatus> Function() get getStatus =>
|
||||
() => repo.getStatus(any());
|
||||
|
||||
Future<DevicePermissionStatus> Function() get request =>
|
||||
() => repo.request(any());
|
||||
|
||||
Future<int> Function() get getAndroidSdkVersion =>
|
||||
() => repo.getAndroidSdkVersion();
|
||||
}
|
||||
|
||||
extension type const TagServiceStub(MockTagService service) implements Stub<MockTagService> {
|
||||
Future<int> Function() get bulkTagAssets =>
|
||||
() => service.bulkTagAssets(any(), any());
|
||||
|
|
|
|||
118
mobile/test/unit/services/gallery_permission_service_test.dart
Normal file
118
mobile/test/unit/services/gallery_permission_service_test.dart
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/services/device_permission.service.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
import '../mocks.dart';
|
||||
|
||||
void main() {
|
||||
late RepositoryMocks mocks;
|
||||
late DevicePermissionService sut;
|
||||
|
||||
void stubPermissions(Map<DevicePermission, DevicePermissionStatus> statuses) {
|
||||
DevicePermissionStatus answer(Invocation i) => statuses[i.positionalArguments.first] ?? .denied;
|
||||
when(mocks.permission.getStatus).thenAnswer((i) async => answer(i));
|
||||
when(mocks.permission.request).thenAnswer((i) async => answer(i));
|
||||
}
|
||||
|
||||
setUp(() {
|
||||
debugDefaultTargetPlatformOverride = .android;
|
||||
mocks = .new();
|
||||
sut = .new(mocks.permission.repo);
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
debugDefaultTargetPlatformOverride = null;
|
||||
});
|
||||
|
||||
group('DevicePermissionService', () {
|
||||
group('Gallery', () {
|
||||
test('is denied when media location is missing but photos and videos are granted', () async {
|
||||
when(mocks.permission.getAndroidSdkVersion).thenAnswer((_) async => 34);
|
||||
stubPermissions({.photos: .granted, .videos: .granted, .mediaLocation: .denied});
|
||||
expect(await sut.galleryStatus(), DevicePermissionStatus.denied);
|
||||
verify(() => mocks.permission.repo.getStatus(DevicePermission.mediaLocation)).called(1);
|
||||
});
|
||||
|
||||
test('is granted when media location is granted', () async {
|
||||
when(mocks.permission.getAndroidSdkVersion).thenAnswer((_) async => 34);
|
||||
stubPermissions({.photos: .granted, .videos: .granted, .mediaLocation: .granted});
|
||||
expect(await sut.galleryStatus(), DevicePermissionStatus.granted);
|
||||
verify(() => mocks.permission.repo.getStatus(DevicePermission.mediaLocation)).called(1);
|
||||
});
|
||||
|
||||
test('checks media location on the legacy storage path', () async {
|
||||
when(mocks.permission.getAndroidSdkVersion).thenAnswer((_) async => 30);
|
||||
stubPermissions({.storage: .granted, .mediaLocation: .denied});
|
||||
expect(await sut.galleryStatus(), DevicePermissionStatus.denied);
|
||||
verify(() => mocks.permission.repo.getStatus(DevicePermission.mediaLocation)).called(1);
|
||||
});
|
||||
|
||||
test('ignores media location below SDK 29', () async {
|
||||
when(mocks.permission.getAndroidSdkVersion).thenAnswer((_) async => 28);
|
||||
stubPermissions({.storage: .granted, .mediaLocation: .denied});
|
||||
expect(await sut.galleryStatus(), DevicePermissionStatus.granted);
|
||||
verifyNever(() => mocks.permission.repo.getStatus(.mediaLocation));
|
||||
});
|
||||
|
||||
test('ignores media location on iOS', () async {
|
||||
debugDefaultTargetPlatformOverride = .iOS;
|
||||
stubPermissions({.photos: .granted, .mediaLocation: .denied});
|
||||
expect(await sut.galleryStatus(), DevicePermissionStatus.granted);
|
||||
verifyNever(() => mocks.permission.repo.getStatus(.mediaLocation));
|
||||
});
|
||||
|
||||
test('is limited when only one of photos and videos is limited', () async {
|
||||
stubPermissions({.photos: .granted, .videos: .limited, .mediaLocation: .granted});
|
||||
expect(await sut.galleryStatus(), DevicePermissionStatus.limited);
|
||||
|
||||
stubPermissions({.photos: .limited, .videos: .granted, .mediaLocation: .granted});
|
||||
expect(await sut.galleryStatus(), DevicePermissionStatus.limited);
|
||||
|
||||
stubPermissions({.photos: .limited, .videos: .limited, .mediaLocation: .granted});
|
||||
expect(await sut.galleryStatus(), DevicePermissionStatus.limited);
|
||||
});
|
||||
|
||||
test('is denied when only one of photos and videos is denied', () async {
|
||||
stubPermissions({.photos: .granted, .videos: .denied, .mediaLocation: .granted});
|
||||
expect(await sut.galleryStatus(), DevicePermissionStatus.denied);
|
||||
|
||||
stubPermissions({.photos: .denied, .videos: .granted, .mediaLocation: .granted});
|
||||
expect(await sut.galleryStatus(), DevicePermissionStatus.denied);
|
||||
|
||||
stubPermissions({.photos: .limited, .videos: .denied, .mediaLocation: .granted});
|
||||
expect(await sut.galleryStatus(), DevicePermissionStatus.denied);
|
||||
});
|
||||
|
||||
test('is permanently denied when only one of photos and videos is permanently denied', () async {
|
||||
stubPermissions({.photos: .granted, .videos: .permanentlyDenied, .mediaLocation: .granted});
|
||||
expect(await sut.galleryStatus(), DevicePermissionStatus.permanentlyDenied);
|
||||
|
||||
stubPermissions({.photos: .permanentlyDenied, .videos: .denied, .mediaLocation: .granted});
|
||||
expect(await sut.galleryStatus(), DevicePermissionStatus.permanentlyDenied);
|
||||
|
||||
stubPermissions({.photos: .permanentlyDenied, .videos: .granted, .mediaLocation: .granted});
|
||||
expect(await sut.galleryStatus(), DevicePermissionStatus.permanentlyDenied);
|
||||
});
|
||||
|
||||
test('checks media location when access is only partial', () async {
|
||||
stubPermissions({.photos: .limited, .videos: .limited, .mediaLocation: .denied});
|
||||
expect(await sut.galleryStatus(), DevicePermissionStatus.denied);
|
||||
verify(() => mocks.permission.repo.getStatus(DevicePermission.mediaLocation)).called(1);
|
||||
});
|
||||
|
||||
test('stays limited when media location is granted, but photos or video is limited', () async {
|
||||
stubPermissions({.photos: .granted, .videos: .limited, .mediaLocation: .granted});
|
||||
expect(await sut.galleryStatus(), DevicePermissionStatus.limited);
|
||||
});
|
||||
});
|
||||
|
||||
test('does not ask for videos when photos was refused', () async {
|
||||
stubPermissions({.photos: .permanentlyDenied});
|
||||
|
||||
expect(await sut.requestGallery(), DevicePermissionStatus.permanentlyDenied);
|
||||
verifyNever(() => mocks.permission.repo.request(.videos));
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue