Search code examples
androidflutterpermissionsnotifications

How can I open file (that is not image, video or audio) through my Flutter App in Android 13?


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" />

Solution

  • 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.