skip to Main Content

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

2

Answers


  1. Now if you said that your app is downloading data itself and creating a file for it itself then you dont need any permission to do so on Android 13+ devices. And certainly not the manage external storage.

    And once your app created and wrote the file it is automatically permitted to read the file using the same path.

    Login or Signup to reply.
  2. 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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search