From ca1639fdd94bd8baf10f0a7e3415eca7a4f3b7f4 Mon Sep 17 00:00:00 2001 From: Sikander Saleem Date: Wed, 3 May 2023 11:24:07 +0300 Subject: [PATCH] storage improvement for android 29 and above. --- android/app/src/main/AndroidManifest.xml | 4 +- lib/classes/utils.dart | 44 ++++++++++++++++++ lib/ui/screens/pdf_viewer_screen.dart | 57 ++++++++++++++++++++++-- lib/widgets/aya_player_widget.dart | 53 +++++++++++++++++++++- lib/widgets/aya_record_widget.dart | 50 ++++++++++++++++++++- pubspec.yaml | 3 ++ 6 files changed, 204 insertions(+), 7 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index dbff0b0..eb0f32c 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -7,7 +7,9 @@ additional functionality it is fine to subclass or reimplement FlutterApplication and put your custom class here. --> - + diff --git a/lib/classes/utils.dart b/lib/classes/utils.dart index 8a01de6..194da3b 100644 --- a/lib/classes/utils.dart +++ b/lib/classes/utils.dart @@ -186,4 +186,48 @@ class Utils { await file.copy(storagePath); Utils.showToast("تم التنزيل"); } + + static void downloadFileAboveAndroid32(String url, String path, {Function(List bytes) onResponse}) { + print("url:$url"); + print("path:$path"); + var httpClient = http.Client(); + var request = http.Request('GET', Uri.parse(url)); + var response = httpClient.send(request); + String dir = path; + + List> chunks = []; + int downloaded = 0; + int lastVal; + + response.asStream().listen((http.StreamedResponse r) { + r.stream.listen((List chunk) { + debugPrint('downloadPercentage: ${downloaded / r.contentLength * 100}'); + lastVal = (downloaded / r.contentLength * 100).toInt(); + + chunks.add(chunk); + downloaded += chunk.length; + }, onDone: () async { + debugPrint('downloadPercentage: ${downloaded / r.contentLength * 100}'); + if (lastVal == 0) { + onResponse([]); + return; + } + final Uint8List bytes = Uint8List(r.contentLength); + int offset = 0; + for (List chunk in chunks) { + bytes.setRange(offset, offset + chunk.length, chunk); + offset += chunk.length; + } + onResponse(bytes); + return; + }, onError: (ex) { + debugPrint("onError:$ex"); + onResponse([]); + }, cancelOnError: true); + }).onError((ex) { + debugPrint("onError:$ex"); + onResponse([]); + }); + } + } diff --git a/lib/ui/screens/pdf_viewer_screen.dart b/lib/ui/screens/pdf_viewer_screen.dart index 844d30b..d47d730 100644 --- a/lib/ui/screens/pdf_viewer_screen.dart +++ b/lib/ui/screens/pdf_viewer_screen.dart @@ -2,11 +2,13 @@ import 'dart:async'; import 'dart:io'; import 'dart:ui'; +import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; +import 'package:shared_storage/shared_storage.dart'; import 'package:syncfusion_flutter_pdfviewer/pdfviewer.dart'; import 'package:tangheem/api/tangheem_user_api_client.dart'; import 'package:tangheem/app_state/app_state.dart'; @@ -121,10 +123,14 @@ class _PdfListScreenState extends State { width: 44, height: 50, ).onPress(() async { - if (await _requestStoragePermission()) { - startFileDownload(contentList[index].exposeFilePath, contentList[index].fileName); + if (Platform.isAndroid && (await DeviceInfoPlugin().androidInfo).version.sdkInt > 29) { + savePdfToAboveAndroid32(contentList[index].exposeFilePath, contentList[index].fileName); } else { - Utils.showToast("يجب أن تعطي الإذن للتنزيل."); + if (await _requestStoragePermission()) { + startFileDownload(contentList[index].exposeFilePath, contentList[index].fileName); + } else { + Utils.showToast("يجب أن تعطي الإذن للتنزيل."); + } } }), 7.width, @@ -181,6 +187,51 @@ class _PdfListScreenState extends State { ), ); } + + void savePdfToAboveAndroid32(String pathUrl, String fileName) async { + List _persistedPermissionUris = await persistedUriPermissions(); + if (_persistedPermissionUris.isNotEmpty) { + UriPermission permissionUri = _persistedPermissionUris.first; + startFileDownloadAboveAndroid32(pathUrl, fileName, permissionUri.uri); + } else { + Uri selectedDocumentUris = await openDocumentTree(); + if (selectedDocumentUris == null) return; + savePdfToAboveAndroid32(pathUrl, fileName); + } + } + + void startFileDownloadAboveAndroid32(String url, String fileName, Uri persistentUri) async { + try { + Utils.showLoading(context); + //String path = await Utils.getStoragePath(fileName); + Utils.downloadFileAboveAndroid32(ApiConsts.baseUrl + url, "path", onResponse: (bytes) async { + Utils.hideLoading(context); + if (bytes.isNotEmpty) { + final documentFile = await persistentUri.toDocumentFile(); + final child = await documentFile?.child(fileName); + if (child == null) { + documentFile?.createFileAsBytes( + mimeType: 'application/pdf ', + bytes: bytes, + displayName: fileName, + ); + } else { + documentFile?.writeToFileAsBytes( + bytes: bytes, + mode: FileMode.write, + ); + } + + Utils.showToast("تم حفظ الملف بنجاح"); + } else { + Utils.showToast("فشل حفظ الملف ، حاول مرة أخرى"); + } + }); + } catch (ex) { + print(ex); + Utils.hideLoading(context); + } + } } class PdfViewerScreen extends StatelessWidget { diff --git a/lib/widgets/aya_player_widget.dart b/lib/widgets/aya_player_widget.dart index 16ede2e..855a05d 100644 --- a/lib/widgets/aya_player_widget.dart +++ b/lib/widgets/aya_player_widget.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'dart:math' as math; import 'dart:typed_data'; +import 'package:device_info_plus/device_info_plus.dart'; import 'package:dotted_border/dotted_border.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; @@ -12,6 +13,7 @@ import 'package:http/http.dart' as http; import 'package:just_audio/just_audio.dart'; import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; +import 'package:shared_storage/shared_storage.dart' as saf; import 'package:tangheem/classes/colors.dart'; import 'package:tangheem/classes/consts.dart'; import 'package:tangheem/classes/utils.dart'; @@ -185,7 +187,11 @@ class _AyaPlayerWidgetState extends State { ), if (widget.voiceNoteList?.isNotEmpty ?? false) commonIconButton("assets/icons/download_aya.svg", () async { - if (await _requestPermission()) { + if (Platform.isAndroid && (await DeviceInfoPlugin().androidInfo).version.sdkInt > 29) { + String filePath = widget.voiceNoteList[_player.currentIndex].exposeFilePath; + saveAudioToAboveAndroid32(filePath, "Tangheem-${widget.voiceNoteList[_player.currentIndex].userName ?? ""}-${widget.voiceNoteList[_player.currentIndex].fileName}", + widget.voiceNoteList[_player.currentIndex].fileType); + } else if (await _requestPermission()) { saveToPhoneStorage(widget.voiceNoteList[_player.currentIndex].exposeFilePath, widget.voiceNoteList[_player.currentIndex].userName ?? ""); } else { Utils.showToast("يجب اعطاء الاذن لتنزيل الآية"); @@ -393,4 +399,49 @@ class _AyaPlayerWidgetState extends State { onResponse(false); }); } + + void saveAudioToAboveAndroid32(String pathUrl, String fileName, String fileType) async { + List _persistedPermissionUris = await saf.persistedUriPermissions(); + if (_persistedPermissionUris.isNotEmpty) { + saf.UriPermission permissionUri = _persistedPermissionUris.first; + startFileDownloadAboveAndroid32(pathUrl, fileName, permissionUri.uri, fileType); + } else { + Uri selectedDocumentUris = await saf.openDocumentTree(); + if (selectedDocumentUris == null) return; + saveAudioToAboveAndroid32(pathUrl, fileName, fileType); + } + } + + void startFileDownloadAboveAndroid32(String url, String fileName, Uri persistentUri, String fileType) async { + try { + Utils.showLoading(context); + //String path = await Utils.getStoragePath(fileName); + Utils.downloadFileAboveAndroid32(ApiConsts.baseUrl + url, "path", onResponse: (bytes) async { + Utils.hideLoading(context); + if (bytes.isNotEmpty) { + final documentFile = await persistentUri.toDocumentFile(); + final child = await documentFile?.child(fileName); + if (child == null) { + documentFile?.createFileAsBytes( + mimeType: fileType, + bytes: bytes, + displayName: fileName, + ); + } else { + documentFile?.writeToFileAsBytes( + bytes: bytes, + mode: FileMode.write, + ); + } + + Utils.showToast("تم حفظ الملف بنجاح"); + } else { + Utils.showToast("فشل حفظ الملف ، حاول مرة أخرى"); + } + }); + } catch (ex) { + print(ex); + Utils.hideLoading(context); + } + } } diff --git a/lib/widgets/aya_record_widget.dart b/lib/widgets/aya_record_widget.dart index e3be5b7..41557d5 100644 --- a/lib/widgets/aya_record_widget.dart +++ b/lib/widgets/aya_record_widget.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:just_audio/just_audio.dart'; @@ -7,9 +8,11 @@ import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:record_mp3/record_mp3.dart'; import 'package:tangheem/classes/colors.dart'; +import 'package:tangheem/classes/consts.dart'; import 'package:tangheem/classes/utils.dart'; import 'package:tangheem/extensions/string_extensions.dart'; import 'package:tangheem/extensions/widget_extensions.dart'; +import 'package:shared_storage/shared_storage.dart' as saf; class AyaRecordWidget extends StatefulWidget { AyaRecordWidget({Key key}) : super(key: key); @@ -219,7 +222,13 @@ class _AyaRecordWidgetState extends State { InkWell( borderRadius: BorderRadius.circular(20), onTap: () async { - if (await _requestStoragePermission()) { + if (Platform.isAndroid && (await DeviceInfoPlugin().androidInfo).version.sdkInt > 29) { + if (recordFilePath != null && File(recordFilePath).existsSync()) { + saveAudioToAboveAndroid32(recordFilePath, "Tangheem${DateTime.now().millisecondsSinceEpoch}.mp3", "audio/mpeg"); + } else { + Utils.showToast("يجب عليك تسجيل صوتك أولا"); + } + } else if (await _requestStoragePermission()) { if (recordFilePath != null && File(recordFilePath).existsSync()) { saveToPhoneStorage(recordFilePath); } else { @@ -231,7 +240,7 @@ class _AyaRecordWidgetState extends State { }, child: Container( height: 31, - width: 135, + width: 135, padding: EdgeInsets.only(left: 12, right: 12), alignment: Alignment.center, decoration: BoxDecoration( @@ -321,4 +330,41 @@ class _AyaRecordWidgetState extends State { await file.copy(storagePath); Utils.showToast("تم التنزيل"); } + + void saveAudioToAboveAndroid32(String pathUrl, String fileName, String fileType) async { + List _persistedPermissionUris = await saf.persistedUriPermissions(); + if (_persistedPermissionUris.isNotEmpty) { + saf.UriPermission permissionUri = _persistedPermissionUris.first; + startFileAboveAndroid32(pathUrl, fileName, permissionUri.uri, fileType); + } else { + Uri selectedDocumentUris = await saf.openDocumentTree(); + if (selectedDocumentUris == null) return; + saveAudioToAboveAndroid32(pathUrl, fileName, fileType); + } + } + + void startFileAboveAndroid32(String filePath, String fileName, Uri persistentUri, String fileType) async { + try { + File file = File(filePath); + final documentFile = await persistentUri.toDocumentFile(); + final child = await documentFile?.child(fileName); + if (child == null) { + documentFile?.createFileAsBytes( + mimeType: fileType, + bytes: file.readAsBytesSync(), + displayName: fileName, + ); + } else { + documentFile?.writeToFileAsBytes( + bytes: file.readAsBytesSync(), + mode: FileMode.write, + ); + } + + Utils.showToast("تم حفظ الملف بنجاح"); + } catch (ex) { + print(ex); + Utils.hideLoading(context); + } + } } diff --git a/pubspec.yaml b/pubspec.yaml index ce3409e..062b11e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -54,6 +54,9 @@ dependencies: flutter_html: ^2.2.1 flutter_inappwebview: ^5.7.2+3 dotted_border: ^2.0.0+1 + device_info_plus: ^6.0.0 + shared_storage: ^0.7.1 + dev_dependencies: flutter_test: sdk: flutter