I need to open a file that is an attachment in my app, those file could be any type really, images, videos, pdfs, excel files... Android 13 has permissions for READ_MEDIA_IMAGES, READ_MEDIA_VIDEO and READ_MEDIA_AUDIO, and can request them with permission_handler:
[Permission.photos, Permission.videos, Permission.audio].request()
After that when I can open the downloaded file from notification with open_file package:
OpenFile.open(path)
But when I try to open a pdf file, it just doesn't work. It gives me an error: Permission denied: android.permission.MANAGE_EXTERNAL_STORAGE flutter
That permission is permanently denied in settings, and that is not intuitive to user to send him to the app whenever I need to open a file from notification. Also I read here that I can't put it in manifest because it will be rejected by the Play Store.
If you can help me out, here is the code below:
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:open_file/open_file.dart';
import 'package:pagedesk/data/model/ticket/attachemnt_item.dart';
import 'package:pagedesk/data/network/firebase_api.dart';
import 'package:pagedesk/utils/Utils.dart';
import 'package:pagedesk/view_model/ticket_attachemnts_view_model.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
class Notifications {
int maxProgress = 5;
bool isCompleted = false;
Future getDownloadNotification(
AttachemntItem item,
String downloadingText,
String downloadCompletedText,
String downoadFaildMessage,
String cantOpenFailMessage,
TicketAttachemntsViewModel viewModel) async {
final AndroidNotificationDetails androidNotificationDetails =
AndroidNotificationDetails(
'name',
'name',
channelDescription: 'progress channel description',
channelShowBadge: false,
importance: Importance.max,
priority: Priority.high,
onlyAlertOnce: true,
showProgress: false,
);
final NotificationDetails notificationDetails =
NotificationDetails(android: androidNotificationDetails);
String newPath = "";
Directory directory;
try {
if (Platform.isAndroid) {
if (await requestStoragePermissions()) {
directory = (await getExternalStorageDirectory())!;
print(directory);
List<String> paths = directory.path.split("/");
for (int x = 1; x < paths.length; x++) {
String folder = paths[x];
if (folder != "Android") {
newPath += "/$folder";
} else {
break;
}
}
newPath = "$newPath/Download";
directory = Directory(newPath);
} else {
return;
}
} else {
if (await _requestPermission(Permission.storage)) {
directory = await getApplicationDocumentsDirectory();
} else {
return;
}
}
if (!await directory.exists()) {
await directory.create(recursive: true);
}
String filename = item.name;
String path = directory.path;
print('FILENAME: $filename');
print('OATH: $path');
File file = File('$path/$filename');
if (await file.exists()) {
int suffix = 1;
String newFileName;
while (await file.exists()) {
newFileName =
'${filename.replaceAll(RegExp(r'\..+'), '')}($suffix)${filename.substring(filename.lastIndexOf('.'))}';
file = File('$path/$newFileName');
suffix++;
}
} else {
print("File doesn't exist");
}
FirebaseApi.localNotifications.show(
item.id,
item.name,
isCompleted ? downloadCompletedText : downloadingText,
notificationDetails,
payload: newPath);
viewModel.getAttachment(item).then((value) => {
if (value != null)
{
file.writeAsBytes(value),
isCompleted = true,
FirebaseApi.localNotifications.cancel(item.id),
FirebaseApi.localNotifications.show(item.id, item.name,
downloadCompletedText, notificationDetails,
payload: file.path)
}
else
{
isCompleted = false,
FirebaseApi.localNotifications.cancel(item.id),
FirebaseApi.localNotifications.show(item.id, item.name,
downoadFaildMessage, notificationDetails,
payload: null)
}
});
} catch (e) {
print('ERROR');
}
}
}
void openFile(String path, String cantOpenFileMessage) async {
try {
final result = await OpenFile.open(path);
if (result.type == ResultType.done) {
print('File opened successfully');
} else if (result.type == ResultType.noAppToOpen) {
Utils.toastMessage(cantOpenFileMessage);
} else {
Utils.toastMessage("Can't open file throgh this app.");
}
} catch (e) {
print('Error opening file: $e');
}
}
Future<bool> _requestPermission(Permission permission) async {
if (await permission.isGranted) {
return true;
} else if (await permission.isPermanentlyDenied) {
openAppSettings();
return false;
} else {
var result = await permission.request();
if (result == PermissionStatus.granted) {
return true;
}
}
return false;
}
Future<bool> requestStoragePermissions() async {
List<Permission> permissions = [];
final deviceInfo = await DeviceInfoPlugin().androidInfo;
if (deviceInfo.version.sdkInt > 32) {
permissions = [Permission.photos, Permission.videos, Permission.audio];
} else {
permissions = [Permission.storage];
}
Map<Permission, PermissionStatus> statuses = await permissions.request();
return !statuses.containsKey(false);
}
Manifest permissions:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission. READ_MEDIA_AUDIO" />
I'm pretty sure (not 100%) that as it's a new permission that is by default denied, you need to make the user change it manually via its settings, even if it's not intuitive for users like you said.
I dont know if you have seen that you can programmatically open the setting page so it's easier for the user.
Unfortunately I don't think that there is a better solution than that.