Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LA-971 added explanation for contact permission #972

Merged
merged 3 commits into from
Jan 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions lib/presentation/localizations/app_localizations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3082,6 +3082,44 @@ class AppLocalizations {
name: 'are_you_sure_you_want_to_discard_recording',
);
}
String get allow {
return Intl.message(
'Allow',
name: 'allow',
);
}

String get not_now {
return Intl.message(
'Not now',
name: 'not_now',
);
}

String get explain_contact_permission {
return Intl.message(
'LinShare requests access to your contacts solely to provide autocomplete suggestions when you are adding a recipient to share files or collaborate with others. Please note that your contacts are not synchronized with the server.',
name: 'explain_contact_permission',
);
}
String get explain_audio_recorder_permission {
return Intl.message(
'LinShare requests audio permission to record audio messages and phone state permission to handle pausing while recording during calls',
name: 'explain_audio_recorder_permission');
}

String get explain_camera_permission {
return Intl.message(
'Linshare requests camera and microphone permissions to capture photos and videos and phone state permission to handle pausing while recording during calls ',
name: 'explain_camera_permission');
}

String get explain_storage_permission {
return Intl.message(
'Linshare requests storage permission to let you upload files from your file system, ensuring you can easily share your documents, photos, and videos.',
name: 'explain_storage_permission',
);
}
}

class AppLocalizationsDelegate extends LocalizationsDelegate<AppLocalizations> {
Expand Down
26 changes: 22 additions & 4 deletions lib/presentation/util/audio_recorder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,37 @@ import 'dart:io';
import 'package:audio_waveforms/audio_waveforms.dart';
import 'package:dartz/dartz.dart';
import 'package:domain/domain.dart';
import 'package:flutter/material.dart';
import 'package:linshare_flutter_app/presentation/localizations/app_localizations.dart';
import 'package:linshare_flutter_app/presentation/util/permission_service.dart';
import 'package:linshare_flutter_app/presentation/view/dialog/permission_dialog.dart';
import 'package:permission_handler/permission_handler.dart';

class AudioRecorder {
final RecorderController recorderController = RecorderController();
final PermissionService permissionService = PermissionService();
String? recordingPath;

Future<Either<Failure, Success>> startRecordingAudio() async {
Future<Either<Failure, Success>> startRecordingAudio(
BuildContext context) async {
try {
if (!await PermissionService.arePermissionsGranted(
[Permission.microphone, Permission.phone])) {
final confirmExplanation =
await PermissionDialog.showPermissionExplanationDialog(
context,
Center(
child: Icon(Icons.warning, color: Colors.orange, size: 40),
),
AppLocalizations.of(context)
.explain_audio_recorder_permission) ??
false;
if (!confirmExplanation) {
return Left(AudioPermissionDenied(false));
}
}
final microphonePermission =
await permissionService.tryToGetPermissionForAudioRecording();
await permissionService.tryToGetPermissionForPhoneState();
await PermissionService.tryToGetPermissionForAudioRecording();
await PermissionService.tryToGetPermissionForPhoneState();
if (microphonePermission.isGranted) {
final tempPath = Directory.systemTemp.path;
final currentTime = DateTime.now().millisecondsSinceEpoch;
Expand Down
38 changes: 34 additions & 4 deletions lib/presentation/util/local_file_picker.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,46 @@
// <http://www.gnu.org/licenses/> for the GNU Affero General Public License version
// 3 and <http://www.linshare.org/licenses/LinShare-License_AfferoGPL-v3.pdf> for
// the Additional Terms applicable to LinShare software.

import 'dart:io';
import 'package:dartz/dartz.dart';
import 'package:domain/domain.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:linshare_flutter_app/presentation/localizations/app_localizations.dart';
import 'package:linshare_flutter_app/presentation/util/permission_service.dart';
import 'package:linshare_flutter_app/presentation/view/dialog/permission_dialog.dart';
import 'package:permission_handler/permission_handler.dart';

class LocalFilePicker {

Future<Either<Failure, FilePickerSuccessViewState>> pickFiles({FileType fileType = FileType.any}) async {
Future<Either<Failure, FilePickerSuccessViewState>> pickFiles(
BuildContext context,
{FileType fileType = FileType.any}) async {
try {
final filesResult = await FilePicker.platform.pickFiles(type: fileType, allowMultiple: true);
if (Platform.isAndroid && await PermissionService.isAndroid32AndLower()) {
final permissionStatus = await Permission.storage.status;
if (!permissionStatus.isGranted) {
final confirmExplanation =
await PermissionDialog.showPermissionExplanationDialog(
context,
Center(
child:
Icon(Icons.warning, color: Colors.orange, size: 40),
),
AppLocalizations.of(context)
.explain_storage_permission) ??
false;
if (!confirmExplanation) {
return Left(FilePickerFailure(Exception('Permission denied')));
}
final requestedPermission = await PermissionService
.tryToGetPermissionForStorageForAndroid32AndLower();
if (requestedPermission != PermissionStatus.granted) {
return Left(FilePickerFailure(Exception('Permission denied')));
}
}
}
final filesResult = await FilePicker.platform
.pickFiles(type: fileType, allowMultiple: true);
if (filesResult != null && filesResult.files.isNotEmpty) {
final filesInfoResult = filesResult.files.map((platformFile) {
return FileInfo(
Expand Down
25 changes: 22 additions & 3 deletions lib/presentation/util/media_picker_from_camera.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,12 @@
import 'package:dartz/dartz.dart';
import 'package:domain/domain.dart';
import 'package:flutter/material.dart';
import 'package:linshare_flutter_app/presentation/localizations/app_localizations.dart';
import 'package:linshare_flutter_app/presentation/util/permission_service.dart';
import 'package:linshare_flutter_app/presentation/util/router/app_navigation.dart';
import 'package:linshare_flutter_app/presentation/view/camera_picker/custom_camera_picker_viewer.dart';
import 'package:linshare_flutter_app/presentation/view/dialog/open_settings_dialog.dart';
import 'package:linshare_flutter_app/presentation/view/dialog/permission_dialog.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:phone_state/phone_state.dart';
import 'package:wechat_camera_picker/wechat_camera_picker.dart';
Expand Down Expand Up @@ -63,12 +65,29 @@ class MediaPickerFromCamera {
AppNavigation appNavigation,
) async {
try {
if (!await PermissionService.arePermissionsGranted(
[Permission.camera, Permission.microphone])) {
final confirmExplanation =
await PermissionDialog.showPermissionExplanationDialog(
context,
Center(
child: Icon(Icons.warning, color: Colors.orange, size: 40),
),
AppLocalizations.of(context).explain_camera_permission) ??
false;
if (!confirmExplanation) {
return Left(
CameraPermissionDenied(),
);
}
}
final cameraPermission =
await PermissionService().tryToGetPermissionForCamera();
await PermissionService.tryToGetPermissionForCamera();

final microphonePermission =
await PermissionService().tryToGetPermissionForAudioRecording();
await PermissionService.tryToGetPermissionForAudioRecording();
final phonePermission =
await PermissionService().tryToGetPermissionForPhoneState();
await PermissionService.tryToGetPermissionForPhoneState();

if (cameraPermission.isGranted && microphonePermission.isGranted) {
List<FileInfo> pickedFiles = [];
Expand Down
35 changes: 31 additions & 4 deletions lib/presentation/util/permission_service.dart
Original file line number Diff line number Diff line change
@@ -1,21 +1,34 @@
import 'package:device_info/device_info.dart';
import 'package:permission_handler/permission_handler.dart';

class PermissionService {
Future<PermissionStatus> tryToGetPermissionForCamera() async {
static Future<bool> arePermissionsGranted(
List<Permission> permissions) async {
for (var permission in permissions) {
if (!await permission.isGranted) {
return false;
}
}
return true;
}

static Future<PermissionStatus> tryToGetPermissionForCamera() async {
final status = await Permission.camera.request();
return status;
}

Future<PermissionStatus> tryToGetPermissionForAudioRecording() async {
static Future<PermissionStatus> tryToGetPermissionForAudioRecording() async {
final status = await Permission.microphone.request();
return status;
}
Future<PermissionStatus> tryToGetPermissionForPhoneState() async {

static Future<PermissionStatus> tryToGetPermissionForPhoneState() async {
final status = await Permission.phone.request();
return status;
}

Future<PermissionStatus> handleMediaPickerPermissionAndroidHigher33() async {
static Future<PermissionStatus>
handleMediaPickerPermissionAndroidHigher33() async {
PermissionStatus? photoPermission = await Permission.photos.status;
if (photoPermission == PermissionStatus.denied) {
photoPermission = await Permission.photos.request();
Expand All @@ -37,4 +50,18 @@ class PermissionService {

return PermissionStatus.denied;
}

static Future<PermissionStatus>
tryToGetPermissionForStorageForAndroid32AndLower() async {
final status = await Permission.storage.request();
return status;
}

static Future<bool> isAndroid32AndLower() async {
final deviceInfoPlugin = DeviceInfoPlugin();
final androidInfo = await deviceInfoPlugin.androidInfo;
final apiLevel = androidInfo.version.sdkInt;
return apiLevel <= 32;
}

}
25 changes: 25 additions & 0 deletions lib/presentation/view/dialog/permission_dialog.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import 'package:flutter/material.dart';
import 'package:linshare_flutter_app/presentation/localizations/app_localizations.dart';

class PermissionDialog {
static Future<bool?> showPermissionExplanationDialog(
BuildContext context, Widget title, String content) async {
return showDialog<bool>(
barrierDismissible: false,
context: context,
builder: (context) => AlertDialog(
title: title,
content: Text(content),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: Text(AppLocalizations.of(context).not_now),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: Text(AppLocalizations.of(context).allow),
),
],
));
}
}
3 changes: 2 additions & 1 deletion lib/presentation/widget/myspace/my_space_viewmodel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -726,7 +726,8 @@ class MySpaceViewModel extends BaseViewModel {
ThunkAction<AppState> _pickFileAction(BuildContext context, FileType fileType) {
return (Store<AppState> store) async {
store.dispatch(OutsideAppAction(outsideAppType: ActionOutsideAppType.PICKING_FILE));
await _localFilePicker.pickFiles(fileType: fileType)
await _localFilePicker
.pickFiles(context, fileType: fileType)
.then((result) {
store.dispatch(OutsideAppAction(outsideAppType: ActionOutsideAppType.NONE));
result.fold(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ class RecordAudioViewModel extends BaseViewModel {
);
}

void startAudioRecording() {
audioRecorder.startRecordingAudio().then((result) {
void startAudioRecording(BuildContext context) {
audioRecorder.startRecordingAudio(context).then((result) {
result.fold((failure) {
store.dispatch(
StopRecording(),
Expand Down Expand Up @@ -188,12 +188,12 @@ class RecordAudioViewModel extends BaseViewModel {
);
}

void pauseAndStartAudioRecording() {
void pauseAndStartAudioRecording(BuildContext context) {
store.state.audioRecorderState.viewState.fold(
(failure) => null,
(success) {
if (success is IdleState) {
startAudioRecording();
startAudioRecording(context);
} else if (success is AudioRecorderStarted) {
pauseAudioRecording();
} else if (success is AudioRecorderPaused) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,8 @@ class RecordAudioWidgetState extends State<RecordAudioWidget> {
Widget recordAudioStartPauseButton() {
return FloatingActionButton(
heroTag: 'record_audio_start_pause_button',
onPressed: recordAudioViewModel.pauseAndStartAudioRecording,
onPressed: () =>
recordAudioViewModel.pauseAndStartAudioRecording(context),
backgroundColor: Colors.red,
child: StoreConnector<AppState, AudioRecorderState>(
converter: (store) => store.state.audioRecorderState,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -439,16 +439,17 @@ class SharedSpaceDocumentNodeViewModel extends BaseViewModel {
store.dispatch(_handleUploadFileMenuAction(context, actionTiles));
}

void openFilePickerByType(FileType fileType) {
void openFilePickerByType(FileType fileType, BuildContext context) {
_appNavigation.popBack();
store.dispatch(_pickFileAction(fileType));
store.dispatch(_pickFileAction(fileType, context));
}

ThunkAction<AppState> _pickFileAction(FileType fileType) {
ThunkAction<AppState> _pickFileAction(
FileType fileType, BuildContext context) {
return (Store<AppState> store) async {
store.dispatch(OutsideAppAction(outsideAppType: ActionOutsideAppType.PICKING_FILE));
await _localFilePicker
.pickFiles(fileType: fileType)
.pickFiles(context, fileType: fileType)
.then((result) {
store.dispatch(OutsideAppAction(outsideAppType: ActionOutsideAppType.NONE));
result.fold(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -920,7 +920,8 @@ class _SharedSpaceDocumentWidgetState extends State<SharedSpaceDocumentWidget> {
Key('pick_photo_and_video_context_menu_action'),
SvgPicture.asset(imagePath.icPhotoLibrary, width: 24, height: 24, fit: BoxFit.fill,color: AppColor.primaryColor,),
AppLocalizations.of(context).photos_and_videos)
.onActionClick((_) => sharedSpaceDocumentViewModel.openFilePickerByType(FileType.media))
.onActionClick((_) => sharedSpaceDocumentViewModel.openFilePickerByType(
FileType.media, context))
.build();
}

Expand All @@ -935,7 +936,8 @@ class _SharedSpaceDocumentWidgetState extends State<SharedSpaceDocumentWidget> {
color: AppColor.primaryColor,
),
AppLocalizations.of(context).browse)
.onActionClick((_) => sharedSpaceDocumentViewModel.openFilePickerByType(FileType.any))
.onActionClick((_) => sharedSpaceDocumentViewModel.openFilePickerByType(
FileType.any, context))
.build();
}

Expand Down
16 changes: 13 additions & 3 deletions lib/presentation/widget/upload_file/upload_file_viewmodel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import 'package:linshare_flutter_app/presentation/redux/states/app_state.dart';
import 'package:linshare_flutter_app/presentation/util/extensions/media_type_extension.dart';
import 'package:linshare_flutter_app/presentation/util/router/app_navigation.dart';
import 'package:linshare_flutter_app/presentation/util/router/route_paths.dart';
import 'package:linshare_flutter_app/presentation/view/dialog/permission_dialog.dart';
import 'package:linshare_flutter_app/presentation/widget/base/base_viewmodel.dart';
import 'package:linshare_flutter_app/presentation/widget/destination_picker/destination_picker_action/choose_destination_picker_action.dart';
import 'package:linshare_flutter_app/presentation/widget/destination_picker/destination_picker_action/negative_destination_picker_action.dart';
Expand Down Expand Up @@ -117,8 +118,6 @@ class UploadFileViewModel extends BaseViewModel {
break;
}
});

Future.delayed(Duration(milliseconds: 500), () => _checkContactPermission());
}

void backToMySpace() {
Expand Down Expand Up @@ -338,11 +337,22 @@ class UploadFileViewModel extends BaseViewModel {
store.dispatch(MySpaceClearSelectedDocumentsAction());
}

void _checkContactPermission() async {
void checkContactPermission(BuildContext context) async {
final permissionStatus = await Permission.contacts.status;
if (permissionStatus.isGranted) {
_contactSuggestionSource = ContactSuggestionSource.all;
} else if (!permissionStatus.isPermanentlyDenied) {
final confirmExplanation =
await PermissionDialog.showPermissionExplanationDialog(
context,
Center(
child: Icon(Icons.warning, color: Colors.orange, size: 40),
),
AppLocalizations.of(context).explain_contact_permission) ??
false;
if (!confirmExplanation) {
return;
}
final requestedPermission = await Permission.contacts.request();
_contactSuggestionSource = requestedPermission == PermissionStatus.granted
? ContactSuggestionSource.all
Expand Down
Loading
Loading