Skip to content

Commit 0934a41

Browse files
committed
Improve the permission handling flow
1 parent e6c98a3 commit 0934a41

File tree

10 files changed

+149
-82
lines changed

10 files changed

+149
-82
lines changed

lib/core/clients/permission_client/permission_client.dart

+33-40
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import 'package:permission_handler/permission_handler.dart';
77
abstract interface class PermissionClient {
88
Future<bool> hasCameraPermission();
99
Future<bool> hasGalleryPermission();
10+
Future<void> requestCameraPermission();
11+
Future<void> requestGalleryPermission();
1012
}
1113

1214
@Injectable(as: PermissionClient)
@@ -17,88 +19,79 @@ final class PermissionClientImpl implements PermissionClient {
1719

1820
@override
1921
Future<bool> hasCameraPermission() async {
20-
final hasPermission = await _isCameraPermissionGranted();
21-
22-
if (hasPermission) {
23-
return true;
24-
} else {
25-
await _requestCameraPermission();
26-
return false;
27-
}
22+
final hasCameraPermission = await _isCameraPermissionGranted();
23+
return hasCameraPermission;
2824
}
2925

3026
@override
3127
Future<bool> hasGalleryPermission() async {
32-
final hasPermission = await _isGalleryPermissionGranted();
33-
if (hasPermission) {
34-
return true;
35-
} else {
36-
await _requestGalleryPermission();
37-
return false;
38-
}
28+
final hasGalleryPermission = await _isGalleryPermissionGranted();
29+
return hasGalleryPermission;
3930
}
4031

41-
Future<bool> _isGalleryPermissionGranted() async {
42-
late final PermissionStatus status;
43-
if (Platform.isAndroid) {
44-
final permission = await _getAndroidGalleryPermissionType();
45-
status = await permission.status;
46-
} else if (Platform.isIOS) {
47-
status = await Permission.photos.status;
48-
}
49-
return status.isGranted;
50-
}
51-
52-
Future<bool> _isCameraPermissionGranted() async {
53-
final status = await Permission.camera.status;
54-
return status.isGranted;
55-
}
56-
57-
Future<void> _requestGalleryPermission() async {
32+
@override
33+
Future<void> requestCameraPermission() async {
5834
if (Platform.isAndroid) {
59-
final permission = await _getAndroidGalleryPermissionType();
35+
const permission = Permission.camera;
6036

6137
final before = await permission.shouldShowRequestRationale;
6238
final rs = await permission.request();
6339
final after = await permission.shouldShowRequestRationale;
40+
6441
// If the user denies the permission twice, openAppSettings will be called
6542
if (!rs.isGranted && !before && !after) {
6643
await openAppSettings();
6744
}
6845
} else if (Platform.isIOS) {
69-
final result = await Permission.photos.status;
46+
final result = await Permission.camera.status;
7047

7148
if (result.isDenied) {
72-
await Permission.photos.request();
49+
await Permission.camera.request();
7350
} else if (result.isPermanentlyDenied) {
7451
await openAppSettings();
7552
}
7653
}
7754
}
7855

79-
Future<void> _requestCameraPermission() async {
56+
@override
57+
Future<void> requestGalleryPermission() async {
8058
if (Platform.isAndroid) {
81-
const permission = Permission.camera;
59+
final permission = await _getAndroidGalleryPermissionType();
8260

8361
final before = await permission.shouldShowRequestRationale;
8462
final rs = await permission.request();
8563
final after = await permission.shouldShowRequestRationale;
86-
8764
// If the user denies the permission twice, openAppSettings will be called
8865
if (!rs.isGranted && !before && !after) {
8966
await openAppSettings();
9067
}
9168
} else if (Platform.isIOS) {
92-
final result = await Permission.camera.status;
69+
final result = await Permission.photos.status;
9370

9471
if (result.isDenied) {
95-
await Permission.camera.request();
72+
await Permission.photos.request();
9673
} else if (result.isPermanentlyDenied) {
9774
await openAppSettings();
9875
}
9976
}
10077
}
10178

79+
Future<bool> _isGalleryPermissionGranted() async {
80+
late final PermissionStatus status;
81+
if (Platform.isAndroid) {
82+
final permission = await _getAndroidGalleryPermissionType();
83+
status = await permission.status;
84+
} else if (Platform.isIOS) {
85+
status = await Permission.photos.status;
86+
}
87+
return status.isGranted;
88+
}
89+
90+
Future<bool> _isCameraPermissionGranted() async {
91+
final status = await Permission.camera.status;
92+
return status.isGranted;
93+
}
94+
10295
/// Returns the type of the permission depending on the sdk version
10396
///* Returns [Permission.storage] if device sdk version is <=32
10497
///* Returns [Permission.photos] if device sdk version is >32

lib/feature/detector/data/repositories/detector_repository_impl.dart

+38-36
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,22 @@ final class DetectorRepositoryImpl implements DetectorRepository {
4444

4545
final List<String> _supportedLanguages = ['en'];
4646

47+
@override
48+
Future<bool> hasGalleryPermission() async {
49+
final hasGalleryPermission = await _permissionClient.hasGalleryPermission();
50+
if (hasGalleryPermission) return true;
51+
await _permissionClient.requestGalleryPermission();
52+
return _permissionClient.hasGalleryPermission();
53+
}
54+
55+
@override
56+
Future<bool> hasCameraPermission() async {
57+
final hasCameraPermission = await _permissionClient.hasCameraPermission();
58+
if (hasCameraPermission) return true;
59+
await _permissionClient.requestCameraPermission();
60+
return _permissionClient.hasCameraPermission();
61+
}
62+
4763
@override
4864
Future<Either<Failure, DetectorEntity>> detect(String userInput) async {
4965
if (await _networkInfoClient.isConnected) {
@@ -64,53 +80,39 @@ final class DetectorRepositoryImpl implements DetectorRepository {
6480

6581
@override
6682
Future<Either<Failure, String>> ocrFromGallery() async {
67-
final hasPermission = await _permissionClient.hasGalleryPermission();
68-
69-
if (hasPermission) {
70-
final filePath = await _galleryLocalDataSource.selectFromGallery();
71-
if (filePath == null) {
72-
// No file selected or unknown path
83+
final filePath = await _galleryLocalDataSource.selectFromGallery();
84+
if (filePath == null) {
85+
// No file selected or unknown path
86+
return left(const Failure.ocrFailure());
87+
} else {
88+
final croppedFilePath = await _imageCropperClient.cropPhoto(filePath: filePath);
89+
if (croppedFilePath == null) {
90+
// File not cropped or unknown path
7391
return left(const Failure.ocrFailure());
7492
} else {
75-
final croppedFilePath = await _imageCropperClient.cropPhoto(filePath: filePath);
76-
if (croppedFilePath == null) {
77-
// File not cropped or unknown path
78-
return left(const Failure.ocrFailure());
79-
} else {
80-
final recognizedText = await _textRecognizerClient.recognizeTextFormFilePath(filePath: croppedFilePath);
81-
// No text detected
82-
return recognizedText.isEmpty ? left(const Failure.ocrFailure()) : right(recognizedText);
83-
}
93+
final recognizedText = await _textRecognizerClient.recognizeTextFormFilePath(filePath: croppedFilePath);
94+
// No text detected
95+
return recognizedText.isEmpty ? left(const Failure.ocrFailure()) : right(recognizedText);
8496
}
85-
} else {
86-
// No permission
87-
return left(const Failure.noPermission());
8897
}
8998
}
9099

91100
@override
92101
Future<Either<Failure, String>> ocrFromCamera() async {
93-
final hasPermission = await _permissionClient.hasCameraPermission();
94-
95-
if (hasPermission) {
96-
final filePath = await _cameraLocalDataSource.takePhoto();
97-
if (filePath == null) {
98-
// No photo taken or unknown path
102+
final filePath = await _cameraLocalDataSource.takePhoto();
103+
if (filePath == null) {
104+
// No photo taken or unknown path
105+
return left(const Failure.ocrFailure());
106+
} else {
107+
final croppedFilePath = await _imageCropperClient.cropPhoto(filePath: filePath);
108+
if (croppedFilePath == null) {
109+
// File not cropped or unknown path
99110
return left(const Failure.ocrFailure());
100111
} else {
101-
final croppedFilePath = await _imageCropperClient.cropPhoto(filePath: filePath);
102-
if (croppedFilePath == null) {
103-
// File not cropped or unknown path
104-
return left(const Failure.ocrFailure());
105-
} else {
106-
final recognizedText = await _textRecognizerClient.recognizeTextFormFilePath(filePath: croppedFilePath);
107-
// No text detected
108-
return recognizedText.isEmpty ? left(const Failure.ocrFailure()) : right(recognizedText);
109-
}
112+
final recognizedText = await _textRecognizerClient.recognizeTextFormFilePath(filePath: croppedFilePath);
113+
// No text detected
114+
return recognizedText.isEmpty ? left(const Failure.ocrFailure()) : right(recognizedText);
110115
}
111-
} else {
112-
// No permission
113-
return left(const Failure.noPermission());
114116
}
115117
}
116118
}

lib/feature/detector/domain/repositories/detector_repository.dart

+2
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,6 @@ abstract interface class DetectorRepository {
66
Future<Either<Failure, DetectorEntity>> detect(String userInput);
77
Future<Either<Failure, String>> ocrFromGallery();
88
Future<Either<Failure, String>> ocrFromCamera();
9+
Future<bool> hasGalleryPermission();
10+
Future<bool> hasCameraPermission();
911
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import 'package:gpt_detector/feature/detector/domain/repositories/detector_repository.dart';
2+
import 'package:injectable/injectable.dart';
3+
4+
@injectable
5+
class HasCameraPermissionUseCase {
6+
HasCameraPermissionUseCase({required DetectorRepository detectorRepository})
7+
: _detectorRepository = detectorRepository;
8+
9+
final DetectorRepository _detectorRepository;
10+
11+
Future<bool> call() {
12+
return _detectorRepository.hasCameraPermission();
13+
}
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import 'package:gpt_detector/feature/detector/domain/repositories/detector_repository.dart';
2+
import 'package:injectable/injectable.dart';
3+
4+
@injectable
5+
class HasGalleryPermissionUseCase {
6+
HasGalleryPermissionUseCase({required DetectorRepository detectorRepository})
7+
: _detectorRepository = detectorRepository;
8+
9+
final DetectorRepository _detectorRepository;
10+
11+
Future<bool> call() {
12+
return _detectorRepository.hasGalleryPermission();
13+
}
14+
}

lib/feature/detector/presentation/cubit/detector_cubit.dart

+18
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import 'package:freezed_annotation/freezed_annotation.dart';
55
import 'package:gpt_detector/app/errors/failure.dart';
66
import 'package:gpt_detector/feature/detector/domain/entities/detector/detector_entity.dart';
77
import 'package:gpt_detector/feature/detector/domain/use_cases/detect_use_case.dart';
8+
import 'package:gpt_detector/feature/detector/domain/use_cases/has_camera_permission_use_case.dart';
9+
import 'package:gpt_detector/feature/detector/domain/use_cases/has_gallery_permission_use_case.dart';
810
import 'package:gpt_detector/feature/detector/domain/use_cases/ocr_from_camera_use_case.dart';
911
import 'package:gpt_detector/feature/detector/domain/use_cases/ocr_from_gallery_use_case.dart';
1012
import 'package:injectable/injectable.dart';
@@ -18,14 +20,30 @@ class DetectorCubit extends Cubit<DetectorState> {
1820
required DetectUseCase detectUseCase,
1921
required OCRFromGalleryUseCase ocrFromGalleryUseCase,
2022
required OCRFromCameraUseCase ocrFromCameraUseCase,
23+
required HasCameraPermissionUseCase hasCameraPermissionUseCase,
24+
required HasGalleryPermissionUseCase hasGalleryPermissionUseCase,
2125
}) : _detectUseCase = detectUseCase,
2226
_ocrFromGalleryUseCase = ocrFromGalleryUseCase,
2327
_ocrFromCameraUseCase = ocrFromCameraUseCase,
28+
_hasCameraPermissionUseCase = hasCameraPermissionUseCase,
29+
_hasGalleryPermissionUseCase = hasGalleryPermissionUseCase,
2430
super(DetectorState.initial());
2531

2632
final DetectUseCase _detectUseCase;
2733
final OCRFromGalleryUseCase _ocrFromGalleryUseCase;
2834
final OCRFromCameraUseCase _ocrFromCameraUseCase;
35+
final HasCameraPermissionUseCase _hasCameraPermissionUseCase;
36+
final HasGalleryPermissionUseCase _hasGalleryPermissionUseCase;
37+
38+
Future<void> checkCameraPermission() async {
39+
final hasCameraPermission = await _hasCameraPermissionUseCase.call();
40+
emit(state.copyWith(hasCameraPermission: hasCameraPermission));
41+
}
42+
43+
Future<void> checkGalleryPermission() async {
44+
final hasGalleryPermission = await _hasGalleryPermissionUseCase.call();
45+
emit(state.copyWith(hasGalleryPermission: hasGalleryPermission));
46+
}
2947

3048
Future<void> detectionRequested({required String text}) async {
3149
emit(state.copyWith(status: FormzStatus.submissionInProgress));

lib/feature/detector/presentation/cubit/detector_state.dart

+2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ class DetectorState with _$DetectorState {
77
required UserInputForm userInput,
88
required DetectorEntity result,
99
Failure? failure,
10+
bool? hasCameraPermission,
11+
bool? hasGalleryPermission,
1012
}) = _DetectorState;
1113

1214
factory DetectorState.initial() => DetectorState(

lib/feature/detector/presentation/view/detect_view.dart

+14-3
Original file line numberDiff line numberDiff line change
@@ -146,15 +146,26 @@ class _DetectViewBodyState extends State<_DetectViewBody> {
146146
IconButton(
147147
icon: const Icon(Icons.photo_library),
148148
onPressed: () async {
149+
// Control the gallery permission
150+
await context.read<DetectorCubit>().checkGalleryPermission();
151+
if (!context.mounted) return;
152+
// If there is no gallery access, then exit
153+
if (!(context.read<DetectorCubit>().state.hasGalleryPermission ?? true)) return;
154+
if (!context.mounted) return;
149155
await context.read<DetectorCubit>().ocrFromGalleryPressed();
150-
if (mounted) {
151-
_controller.text = context.read<DetectorCubit>().state.userInput.value;
152-
}
156+
if (!context.mounted) return;
157+
_controller.text = context.read<DetectorCubit>().state.userInput.value;
153158
},
154159
),
155160
IconButton(
156161
icon: const Icon(Icons.photo_camera),
157162
onPressed: () async {
163+
// Control the camera permission
164+
await context.read<DetectorCubit>().checkCameraPermission();
165+
if (!context.mounted) return;
166+
// If there is no camera access, then exit
167+
if (!(context.read<DetectorCubit>().state.hasCameraPermission ?? true)) return;
168+
if (!context.mounted) return;
158169
await context.read<DetectorCubit>().ocrFromCameraPressed();
159170
if (!context.mounted) return;
160171
_controller.text = context.read<DetectorCubit>().state.userInput.value;

lib/feature/onboarding/presentation/view/onboarding_view.dart

+2-3
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,8 @@ class _OnboardingViewBody extends StatelessWidget {
9696
ElevatedButton(
9797
onPressed: () async {
9898
await context.read<OnboardingCubit>().completeOnboarding();
99-
if (context.mounted) {
100-
unawaited(AppRouter.pushReplacement(context, const DetectView()));
101-
}
99+
if (!context.mounted) return;
100+
unawaited(AppRouter.pushReplacement(context, const DetectView()));
102101
},
103102
child: Text(context.l10n.getStarted),
104103
),

test/feature/detector/presentation/cubit/detector_cubit_test.dart

+12
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import 'package:gpt_detector/app/errors/failure.dart';
88
import 'package:gpt_detector/feature/detector/data/model/detector/detector_model.dart';
99
import 'package:gpt_detector/feature/detector/domain/entities/detector/detector_entity.dart';
1010
import 'package:gpt_detector/feature/detector/domain/use_cases/detect_use_case.dart';
11+
import 'package:gpt_detector/feature/detector/domain/use_cases/has_camera_permission_use_case.dart';
12+
import 'package:gpt_detector/feature/detector/domain/use_cases/has_gallery_permission_use_case.dart';
1113
import 'package:gpt_detector/feature/detector/domain/use_cases/ocr_from_camera_use_case.dart';
1214
import 'package:gpt_detector/feature/detector/domain/use_cases/ocr_from_gallery_use_case.dart';
1315
import 'package:gpt_detector/feature/detector/presentation/cubit/detector_cubit.dart';
@@ -19,6 +21,10 @@ class MockOCRFromGalleryUseCase extends Mock implements OCRFromGalleryUseCase {}
1921

2022
class MockOCRFromCameraUseCase extends Mock implements OCRFromCameraUseCase {}
2123

24+
class MockHasCameraPermissionUseCase extends Mock implements HasCameraPermissionUseCase {}
25+
26+
class MockHasGalleryPermissionUseCase extends Mock implements HasGalleryPermissionUseCase {}
27+
2228
class MockDetectorEntity extends Mock implements DetectorEntity {}
2329

2430
String generateRandomString(int len) {
@@ -37,6 +43,8 @@ void main() {
3743
late MockDetectUseCase mockDetectUseCase;
3844
late MockOCRFromGalleryUseCase mockOCRFromGalleryUseCase;
3945
late MockOCRFromCameraUseCase mockOCRFromCameraUseCase;
46+
late MockHasCameraPermissionUseCase mockHasCameraPermissionUseCase;
47+
late MockHasGalleryPermissionUseCase mockHasGalleryPermissionUseCase;
4048
late MockDetectorEntity mockDetectorEntity;
4149
late String validUserInput;
4250
late UserInputForm validInputForm;
@@ -47,10 +55,14 @@ void main() {
4755
mockDetectUseCase = MockDetectUseCase();
4856
mockOCRFromGalleryUseCase = MockOCRFromGalleryUseCase();
4957
mockOCRFromCameraUseCase = MockOCRFromCameraUseCase();
58+
mockHasCameraPermissionUseCase = MockHasCameraPermissionUseCase();
59+
mockHasGalleryPermissionUseCase = MockHasGalleryPermissionUseCase();
5060
detectorCubit = DetectorCubit(
5161
detectUseCase: mockDetectUseCase,
5262
ocrFromGalleryUseCase: mockOCRFromGalleryUseCase,
5363
ocrFromCameraUseCase: mockOCRFromCameraUseCase,
64+
hasCameraPermissionUseCase: mockHasCameraPermissionUseCase,
65+
hasGalleryPermissionUseCase: mockHasGalleryPermissionUseCase,
5466
);
5567
mockDetectorEntity = MockDetectorEntity();
5668
validUserInput = generateRandomString(250);

0 commit comments

Comments
 (0)