ML-25 Revise permission handling (#26)

* fixed permission handling

* translations cleanup

* [Android] removed unused permissions
This commit is contained in:
Vadim 2023-02-11 00:49:51 +03:00 committed by GitHub
parent f75afbb6c2
commit a183a5433e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 200 additions and 191 deletions

1
.vscode/launch.json vendored
View file

@ -8,6 +8,7 @@
"name": "dev (android)",
"request": "launch",
"type": "dart",
//"flutterMode": "profile",
"args": [
"--flavor",
"dev",

View file

@ -1,5 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.vodemn.lightmeter">
<application
android:label="Lightmeter"
android:name="${applicationName}"
@ -35,4 +37,8 @@
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" android:required="true" />
<uses-permission android:name="android.permission.RECORD_AUDIO" tools:node="remove" />
<uses-permission android:name="android.permission.MICROPHONE" tools:node="remove" />
<uses-feature android:name="android.hardware.microphone" android:required="false" />
</manifest>

View file

@ -1,17 +1,22 @@
import 'dart:io';
import 'package:app_settings/app_settings.dart';
import 'package:lightmeter/data/haptics_service.dart';
import 'package:lightmeter/data/light_sensor_service.dart';
import 'package:lightmeter/data/permissions_service.dart';
import 'package:lightmeter/data/shared_prefs_service.dart';
import 'package:permission_handler/permission_handler.dart';
class MeteringInteractor {
final UserPreferencesService _userPreferencesService;
final HapticsService _hapticsService;
final PermissionsService _permissionsService;
final LightSensorService _lightSensorService;
const MeteringInteractor(
this._userPreferencesService,
this._hapticsService,
this._permissionsService,
this._lightSensorService,
);
@ -30,6 +35,22 @@ class MeteringInteractor {
if (_userPreferencesService.haptics) _hapticsService.responseVibration();
}
Future<bool> checkCameraPermission() async {
return _permissionsService
.checkCameraPermission()
.then((value) => value == PermissionStatus.granted);
}
Future<bool> requestPermission() async {
return _permissionsService
.requestCameraPermission()
.then((value) => value == PermissionStatus.granted);
}
void openAppSettings() {
AppSettings.openAppSettings();
}
void enableHaptics(bool enable) => _userPreferencesService.haptics = enable;
Future<bool> hasAmbientLightSensor() async {

View file

@ -1,8 +1,5 @@
{
"@@locale": "en",
"permissionNeeded": "Permission needed",
"permissionNeededMessage": "To use Lightmeter, turn on Camera permissions.",
"openSettings": "Open settings",
"fastestExposurePair": "Fastest",
"slowestExposurePair": "Slowest",
"ev": "{value} EV",
@ -18,6 +15,9 @@
"nd": "ND",
"ndFilterFactor": "Neutral density filter factor",
"noExposurePairs": "There are no exposure pairs for the selected settings.",
"noCamerasDetected": "Seems like your device doesn't have any cameras connected",
"noCameraPermission": "Camera permission is not granted.",
"otherCameraError": "An error occurred when connecting to the camera.",
"none": "None",
"cancel": "Cancel",
"select": "Select",

View file

@ -7,6 +7,7 @@ import 'package:exif/exif.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/interactors/metering_interactor.dart';
import 'package:lightmeter/screens/metering/components/camera_container/models/camera_error_type.dart';
import 'package:lightmeter/screens/metering/components/shared/ev_source_base/bloc_base_ev_source.dart';
import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart';
import 'package:lightmeter/screens/metering/communication/event_communication_metering.dart'
@ -43,12 +44,14 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
_observer = _WidgetsBindingObserver(_appLifecycleStateObserver);
WidgetsBinding.instance.addObserver(_observer);
on<RequestPermissionEvent>(_onRequestPermission);
on<OpenAppSettingsEvent>(_onOpenAppSettings);
on<InitializeEvent>(_onInitialize);
on<ZoomChangedEvent>(_onZoomChanged);
on<ExposureOffsetChangedEvent>(_onExposureOffsetChanged);
on<ExposureOffsetResetEvent>(_onExposureOffsetResetEvent);
add(const InitializeEvent());
add(const RequestPermissionEvent());
}
@override
@ -71,10 +74,33 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
}
}
Future<void> _onRequestPermission(_, Emitter emit) async {
final hasPermission = await _meteringInteractor.requestPermission();
if (!hasPermission) {
emit(const CameraErrorState(CameraErrorType.permissionNotGranted));
} else {
add(const InitializeEvent());
}
}
Future<void> _onOpenAppSettings(_, Emitter emit) async {
_meteringInteractor.openAppSettings();
}
Future<void> _onInitialize(_, Emitter emit) async {
emit(const CameraLoadingState());
final hasPermission = await _meteringInteractor.checkCameraPermission();
if (!hasPermission) {
emit(const CameraErrorState(CameraErrorType.permissionNotGranted));
return;
}
try {
final cameras = await availableCameras();
if (cameras.isEmpty) {
emit(const CameraErrorState(CameraErrorType.noCamerasDetected));
return;
}
_cameraController = CameraController(
cameras.firstWhere(
(camera) => camera.lensDirection == CameraLensDirection.back,
@ -111,7 +137,7 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
_emitActiveState(emit);
} catch (e) {
emit(const CameraErrorState());
emit(const CameraErrorState(CameraErrorType.other));
}
}
@ -174,7 +200,7 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
add(const InitializeEvent());
break;
case AppLifecycleState.paused:
case AppLifecycleState.inactive:
case AppLifecycleState.detached:
_cameraController?.dispose();
_cameraController = null;
break;
@ -186,11 +212,16 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
/// This is needed only because we cannot use `with` with mixins
class _WidgetsBindingObserver with WidgetsBindingObserver {
final ValueChanged<AppLifecycleState> onLifecycleStateChanged;
AppLifecycleState? _prevState;
_WidgetsBindingObserver(this.onLifecycleStateChanged);
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (_prevState == AppLifecycleState.inactive && state == AppLifecycleState.resumed) {
return;
}
_prevState = state;
onLifecycleStateChanged(state);
}
}

View file

@ -0,0 +1,37 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/screens/metering/components/camera_container/models/camera_error_type.dart';
class CameraControlsPlaceholder extends StatelessWidget {
final CameraErrorType error;
final VoidCallback onReset;
const CameraControlsPlaceholder({
required this.error,
required this.onReset,
super.key,
});
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
onPressed: onReset,
icon: Icon(error == CameraErrorType.permissionNotGranted ? Icons.settings : Icons.sync),
),
const SizedBox(height: Dimens.grid8),
Text(
error.toStringLocalized(context),
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(color: Theme.of(context).colorScheme.onBackground),
textAlign: TextAlign.center,
),
],
);
}
}

View file

@ -1,14 +1,19 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/screens/metering/components/camera_container/models/camera_error_type.dart';
class CameraViewPlaceholder extends StatelessWidget {
const CameraViewPlaceholder({super.key});
final CameraErrorType? error;
const CameraViewPlaceholder({required this.error, super.key});
@override
Widget build(BuildContext context) {
return Card(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(Dimens.borderRadiusM)),
child: const Center(child: Icon(Icons.no_photography)),
child: Center(
child: error != null ? const Icon(Icons.no_photography) : const CircularProgressIndicator(),
),
);
}
}

View file

@ -2,10 +2,22 @@ abstract class CameraContainerEvent {
const CameraContainerEvent();
}
class RequestPermissionEvent extends CameraContainerEvent {
const RequestPermissionEvent();
}
class OpenAppSettingsEvent extends CameraContainerEvent {
const OpenAppSettingsEvent();
}
class InitializeEvent extends CameraContainerEvent {
const InitializeEvent();
}
class ReinitializeEvent extends CameraContainerEvent {
const ReinitializeEvent();
}
class ZoomChangedEvent extends CameraContainerEvent {
final double value;

View file

@ -0,0 +1,17 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart';
enum CameraErrorType { noCamerasDetected, permissionNotGranted, other }
extension CameraErrorTypeString on CameraErrorType {
String toStringLocalized(BuildContext context) {
switch (this) {
case CameraErrorType.noCamerasDetected:
return S.of(context).noCamerasDetected;
case CameraErrorType.permissionNotGranted:
return S.of(context).noCameraPermission;
default:
return S.of(context).otherCameraError;
}
}
}

View file

@ -32,6 +32,7 @@ class CameraContainerProvider extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
lazy: false,
create: (context) => CameraContainerBloc(
context.read<MeteringInteractor>(),
context.read<MeteringCommunicationBloc>(),

View file

@ -1,5 +1,6 @@
import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:lightmeter/screens/metering/components/camera_container/models/camera_error_type.dart';
abstract class CameraContainerState {
const CameraContainerState();
@ -36,5 +37,7 @@ class CameraActiveState extends CameraContainerState {
}
class CameraErrorState extends CameraContainerState {
const CameraErrorState();
final CameraErrorType error;
const CameraErrorState(this.error);
}

View file

@ -6,12 +6,14 @@ import 'package:lightmeter/data/models/photography_values/nd_value.dart';
import 'package:lightmeter/platform_config.dart';
import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/screens/metering/components/camera_container/components/camera_view/widget_camera_view.dart';
import 'package:lightmeter/screens/metering/components/camera_container/models/camera_error_type.dart';
import 'package:lightmeter/screens/metering/components/shared/exposure_pairs_list/widget_list_exposure_pairs.dart';
import 'package:lightmeter/screens/metering/components/shared/metering_top_bar/widget_top_bar_metering.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/widget_container_readings.dart';
import 'bloc_container_camera.dart';
import 'components/camera_controls/widget_camera_controls.dart';
import 'components/camera_controls_placeholder/widget_placeholder_camera_controls.dart';
import 'components/camera_view_placeholder/widget_placeholder_camera_view.dart';
import 'event_container_camera.dart';
import 'state_container_camera.dart';
@ -78,10 +80,17 @@ class _CameraViewBuilder extends StatelessWidget {
return AspectRatio(
aspectRatio: PlatformConfig.cameraPreviewAspectRatio,
child: BlocBuilder<CameraContainerBloc, CameraContainerState>(
buildWhen: (previous, current) => current is CameraInitializedState,
builder: (context, state) => state is CameraInitializedState
? Center(child: CameraView(controller: state.controller))
: const CameraViewPlaceholder(),
buildWhen: (previous, current) =>
current is CameraLoadingState ||
current is CameraInitializedState ||
current is CameraErrorState,
builder: (context, state) {
if (state is CameraInitializedState) {
return Center(child: CameraView(controller: state.controller));
} else {
return CameraViewPlaceholder(error: state is CameraErrorState ? state.error : null);
}
},
),
);
}
@ -95,10 +104,10 @@ class _CameraControlsBuilder extends StatelessWidget {
return Padding(
padding: const EdgeInsets.symmetric(vertical: Dimens.paddingM),
child: BlocBuilder<CameraContainerBloc, CameraContainerState>(
builder: (context, state) => AnimatedSwitcher(
duration: Dimens.durationS,
child: state is CameraActiveState
? CameraControls(
builder: (context, state) {
late final Widget child;
if (state is CameraActiveState) {
child = CameraControls(
exposureOffsetRange: state.exposureOffsetRange,
exposureOffsetValue: state.currentExposureOffset,
onExposureOffsetChanged: (value) {
@ -109,9 +118,26 @@ class _CameraControlsBuilder extends StatelessWidget {
onZoomChanged: (value) {
context.read<CameraContainerBloc>().add(ZoomChangedEvent(value));
},
)
: const SizedBox.shrink(),
),
);
} else if (state is CameraErrorState) {
child = CameraControlsPlaceholder(
error: state.error,
onReset: () {
context.read<CameraContainerBloc>().add(
state.error == CameraErrorType.permissionNotGranted
? const OpenAppSettingsEvent()
: const InitializeEvent());
},
);
} else {
child = const SizedBox.shrink();
}
return AnimatedSwitcher(
duration: Dimens.durationS,
child: child,
);
},
),
);
}

View file

@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/data/haptics_service.dart';
import 'package:lightmeter/data/light_sensor_service.dart';
import 'package:lightmeter/data/models/photography_values/photography_value.dart';
import 'package:lightmeter/data/permissions_service.dart';
import 'package:lightmeter/data/shared_prefs_service.dart';
import 'package:lightmeter/interactors/metering_interactor.dart';
import 'package:provider/provider.dart';
@ -25,6 +26,7 @@ class _MeteringFlowState extends State<MeteringFlow> {
create: (context) => MeteringInteractor(
context.read<UserPreferencesService>(),
context.read<HapticsService>(),
context.read<PermissionsService>(),
context.read<LightSensorService>(),
),
child: MultiBlocProvider(

View file

@ -1,46 +0,0 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/data/permissions_service.dart';
import 'package:permission_handler/permission_handler.dart';
import 'event_permissions_check.dart';
import 'state_permissions_check.dart';
class PermissionsCheckBloc extends Bloc<PermissionsCheckEvent, PermissionsCheckState> {
final PermissionsService _permissionsService;
PermissionsCheckBloc(this._permissionsService) : super(const LoadingState()) {
on<PermissionsGrantedEvent>((event, emit) => emit(const PermissionsGrantedState()));
on<PermissionsDeniedEvent>((event, emit) => emit(const PermissionsDeniedState()));
_checkAndRequestPermissions();
}
Future<void> _checkAndRequestPermissions() async {
_permissionsService.checkCameraPermission().then((value) {
switch (value) {
case PermissionStatus.permanentlyDenied:
case PermissionStatus.restricted:
add(const PermissionsDeniedEvent());
break;
case PermissionStatus.denied:
_permissionsService.requestCameraPermission().then((value) {
switch (value) {
case PermissionStatus.permanentlyDenied:
case PermissionStatus.restricted:
case PermissionStatus.denied:
add(const PermissionsDeniedEvent());
break;
case PermissionStatus.limited:
case PermissionStatus.granted:
add(const PermissionsGrantedEvent());
break;
}
});
break;
case PermissionStatus.limited:
case PermissionStatus.granted:
add(const PermissionsGrantedEvent());
break;
}
});
}
}

View file

@ -1,11 +0,0 @@
abstract class PermissionsCheckEvent {
const PermissionsCheckEvent();
}
class PermissionsDeniedEvent extends PermissionsCheckEvent {
const PermissionsDeniedEvent();
}
class PermissionsGrantedEvent extends PermissionsCheckEvent {
const PermissionsGrantedEvent();
}

View file

@ -1,18 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/data/permissions_service.dart';
import 'package:lightmeter/screens/permissions_check/screen_permissions_check.dart';
import 'bloc_permissions_check.dart';
class PermissionsCheckFlow extends StatelessWidget {
const PermissionsCheckFlow({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => PermissionsCheckBloc(context.read<PermissionsService>()),
child: const PermissionsCheckScreen(),
);
}
}

View file

@ -1,64 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/res/dimens.dart';
import 'bloc_permissions_check.dart';
import 'state_permissions_check.dart';
class PermissionsCheckScreen extends StatelessWidget {
const PermissionsCheckScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.surface,
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(Dimens.paddingM * 2),
child: Center(
child: BlocConsumer<PermissionsCheckBloc, PermissionsCheckState>(
listener: (context, state) {
if (state is PermissionsGrantedState) {
Navigator.of(context).pushReplacementNamed("metering");
}
},
builder: (context, state) {
return AnimatedSwitcher(
duration: Dimens.durationS,
child: state is LoadingState
? const CircularProgressIndicator()
: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
S.of(context).permissionNeeded,
style: Theme.of(context).textTheme.headlineLarge,
textAlign: TextAlign.center,
),
const SizedBox(height: Dimens.grid16),
Text(
S.of(context).permissionNeededMessage,
style: Theme.of(context).textTheme.bodyLarge,
textAlign: TextAlign.center,
),
const SizedBox(height: Dimens.grid24),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor: Theme.of(context).colorScheme.onPrimary,
),
onPressed: () {},
child: Text(S.of(context).openSettings),
),
],
),
);
},
),
),
),
),
);
}
}

View file

@ -1,15 +0,0 @@
abstract class PermissionsCheckState {
const PermissionsCheckState();
}
class LoadingState extends PermissionsCheckState {
const LoadingState();
}
class PermissionsGrantedState extends PermissionsCheckState {
const PermissionsGrantedState();
}
class PermissionsDeniedState extends PermissionsCheckState {
const PermissionsDeniedState();
}

View file

@ -7,6 +7,7 @@ environment:
sdk: ">=2.18.0 <3.0.0"
dependencies:
app_settings: 4.2.0
camera: 0.10.0+4
exif: 3.1.2
dynamic_color: 1.5.4