Получаем разрешение MANAGE_EXTERNAL_STORAGE для приложения

Безопасность данных в операционной системе является очень важной задачей, и Android здесь не является исключением. Так, Google в Android 10 добавили новый способ обеспечения безопасности, называемый хранилищем с ограниченной областью видимости (Scoped storage).

До Android 10 всё работало достаточно просто: приложение запрашивало доступ к хранилищу, используя одно из разрешений (WRITE_EXTERNAL_STORAGE либо READ_EXTERNAL_STORAGE), и, после того как пользователь предоставлял разрешение, приложение получало возможность прочесть и изменить практически любой файл, хранящийся на устройстве, за исключением системных файлов и папок, а также папок других приложений. Иначе говоря, приложение просто получало доступ ко всей файловой системе. Scoped storage же изменил этот подход. Теперь приложение по умолчанию имеет доступ только к некоторым участкам памяти, где хранятся общедоступные файлы, такие как медиа, загруженные файлы, некоторые документы. При этом полный доступ приложение имеет только к тем файлам, которые находятся непосредственно в папке приложения, расположенной в Android/data/{имя пакета приложения}. Если приложению нужно изменить или удалить файл, находящийся вне этой папки, то приложение должно запросить у пользователя разрешение на конкретную операцию с помощью MediaStore API (для медиа-файлов) или через Storage Access Framework (для всех остальных файлов). Однако этот способ очень неудобен, если мы разрабатываем файловый менеджер или приложение, которое должно работать не с медиа-файлами.

Scoped storage был добавлен в Android 10 как опциональная функция, которую легко можно было отключить, добавив в AndroidManifest.xml приложения строчку android:requestLegacyExternalStorage=»true» внутри элемента <application>. Таким образом, можно было вернуть работу с файлами так, как было раньше. Однако, начиная с Android 11, использование Scoped storage стало обязательным. Безусловно, это улучшило безопасность и сохранность файлов в операционной системе, но и прибавило проблем разработчикам, заставив их пересмотреть принципы работы своих приложений с файловой системой.

Поскольку часть приложений всё же требует для своей работы полный доступ к хранилищу, а не ограниченный, Google добавили новое разрешение MANAGE_EXTERNAL_STORAGE для таких случаев. Это разрешение, как и раньше, позволяет получить доступ ко всей файловой системе, однако для его использования в приложении требуется подтверждение со стороны Google. В этой статье мы разберём, как использовать разрешение в приложении, а также рассмотрим процесс заполнения заявки в консоли Google Play.

Для начала создадим новый проект с пустой активностью. Для начала нам нужно определить в манифесте, что приложению нужны следующие разрешения для работы. Добавим их в файле AndroidManifest.xml.

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="ru.androidtools.permissiontest">

  <uses-permission
      android:name="android.permission.WRITE_EXTERNAL_STORAGE"
      android:maxSdkVersion="29"/>
  <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
  ...

Поскольку MANAGE_EXTERNAL_STORAGE используется в обязательном порядке только начиная с API 30, то нам всё ещё нужно обрабатывать запрос разрешений на предыдущих версиях так, как мы это делали раньше. Для этого мы добавляем также WRITE_EXTERNAL_STORAGE, но ограничиваем его использование API 29. Таким образом, мы будем каждый раз проверять уровень API устройства и, в зависимости от этого, использовать либо старое разрешение, либо новое.

Как уже упоминалось выше, в случае с Android 10 для работы с файловой системой нам также нужно в манифесте добавить следующий флаг в элемент <application>.

<application
    android:requestLegacyExternalStorage="true"
    ...

В файле разметки activity_main.xml добавим несколько элементов для тестирования:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    android:gravity="center"
    android:orientation="vertical"
    android:padding="12dp"
    >

  <TextView
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="Разрешение не предоставлено"
      android:id="@+id/tvPermission"
      />

  <Button
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="Запросить разрешение"
      android:id="@+id/btnPermission"
      android:layout_marginTop="12dp"
      />

</LinearLayout>

Перейдём к написанию кода. Создадим класс PermissionUtils, в котором будет находиться проверка разрешений и их запрос.

public class PermissionUtils {
  public static boolean hasPermissions(Context context) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
      return Environment.isExternalStorageManager();
    } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
      return ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE)
          == PackageManager.PERMISSION_GRANTED;
    } else {
      return true;
    }
  }

  public static void requestPermissions(Activity activity, int requestCode) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
      try {
        Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
        intent.addCategory("android.intent.category.DEFAULT");
        intent.setData(Uri.parse(String.format("package:%s", activity.getPackageName())));
        activity.startActivityForResult(intent, requestCode);
      } catch (Exception e) {
        Intent intent = new Intent();
        intent.setAction(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
        activity.startActivityForResult(intent, requestCode);
      }
    } else {
      ActivityCompat.requestPermissions(activity,
          new String[] { Manifest.permission.WRITE_EXTERNAL_STORAGE },
          requestCode);
    }
  }
}

Затем в классе активности MainActivity.java определим элементы из разметки.

public class MainActivity extends AppCompatActivity {
  private TextView tvPermission;
  private Button btnPermission;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    tvPermission = findViewById(R.id.tvPermission);
    btnPermission = findViewById(R.id.btnPermission);
  }
}

Текстовое поле будет отображать, если у приложения необходимые разрешения. Для этого после объявления переменных добавим проверку.

tvPermission = findViewById(R.id.tvPermission);
btnPermission = findViewById(R.id.btnPermission);

if (PermissionUtils.hasPermissions(this)) {
  tvPermission.setText("Разрешение получено");
} else {
  tvPermission.setText("Разрешение не предоставлено");
}

Если запустить приложение сейчас, то у нас всегда будет выводиться сообщение о том, что разрешений нет, поскольку мы их пока что не запрашивали.

 

Добавим обработчик для кнопки, который будет отправлять запрос на предоставление разрешения.

if (PermissionUtils.hasPermissions(this)) {
  tvPermission.setText("Разрешение получено");
} else {
  tvPermission.setText("Разрешение не предоставлено");
}

btnPermission.setOnClickListener(new View.OnClickListener() {
  @Override public void onClick(View v) {
    if (PermissionUtils.hasPermissions(MainActivity.this)) return;
    PermissionUtils.requestPermissions(MainActivity.this, PERMISSION_STORAGE);
  }
});

Нам также необходимо задать код запроса, чтобы мы могли после определить, что ответ пришёл именно для этого запроса.

private TextView tvPermission;
private Button btnPermission, btnOpenFile;
private ImageView ivOpenedFile;

private static final int PERMISSION_STORAGE = 101;

Когда пользователь нажмёт на кнопку, у него откроется новая активность с настройками приложения, в которой будет предложено дать полный доступ данному приложению. Пользователь может как согласиться, так и отклонить запрос.

Поскольку это также работает и для старых версий Android, то в случае с ними пользователю вместо активности будет предложен старый диалог с запросом на предоставление разрешений.

 

Любой результат действий пользователя в итоге возвращается в нашу активность. Поскольку для API 30 мы запускаем отдельную активность, то результат работы мы должны отслеживать в методе onActivityResult(). Переопределим его и добавим следующий код.

@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
  if (requestCode == PERMISSION_STORAGE) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
      if (PermissionUtils.hasPermissions(this)) {
        tvPermission.setText("Разрешение получено");
      } else {
        tvPermission.setText("Разрешение не предоставлено");
      }
    }
  }
  super.onActivityResult(requestCode, resultCode, data);
}

Здесь мы снова проверяем, дал ли пользователь разрешение или нет, и обновляем текстовое поле в соответствии с результатом.

 

Аналогичным образом проверяем результат операции для старых уровней API, но здесь уже переопределяем метод onRequestPermissionResult().

@Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
    @NonNull int[] grantResults) {
  if (requestCode == PERMISSION_STORAGE) {
    if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
      tvPermission.setText("Разрешение получено");
    } else {
      tvPermission.setText("Разрешение не предоставлено");
    }
  }
  super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}

Теперь у нашего приложения есть полный доступ к файловой системе, и технически этого уже достаточно. Однако если попытаться опубликовать такое приложение в Google Play, то публикацию запретят по причине отсутствия заявки на получение доступа ко всем файлам. Поэтому, после того, как в консоли будет создан выпуск и загружена туда новая версия приложения, нам нужно перейти в раздел Контент приложения и выбрать там появившийся пункт Важные разрешения и API.

В появившейся форме требуется описать подробно, зачем приложению требуется доступ ко всем файлам, относится ли это к основному функционалу приложения, а также объяснить с технической точки зрения почему невозможно использовать в приложении альтернативные способы.

Также, в случае если показать работу приложения проще, чем объяснить, можно также записать демо-ролик работы приложения, загрузить его на Youtube и прикрепить в данной форме ссылку на видео. В некоторых случаях это может ускорить получение доступа.

Итак, заполнив форму и сохранив её, можно вернуться обратно в настройку выпуска и проверить его. Предупреждение об отсутствии заявки должно пропасть и мы может отправить выпуск на публикацию. В процессе публикации Google тщательно проверит приложении, чтобы убедиться, что доступ ко всем файлам действительно необходим приложению. Как показывает практика, для приложений, представляющих собой файловые менеджеры, заявку одобрят достаточно быстро.

Когда Google одобрил доступ для приложения, новая версия успешно публикуется, а в разделе Контент приложения указано разрешение, которое было одобрено.

Однако, Google также могут и не одобрить заявку, если посчитают, что это не является обязательной функцией или если можно ограничиться MediaStore\Storage Access Framework. Здесь может помочь изменение в работе приложения либо переоформление заявки, это нужно учитывать.

Таким образом, с помощью пары строк кода можно обновить своё приложения для работы с файловой системой на новых версиях Android.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *