PDFファイルを共有ストレージに保存する方法

前回の記事ではAndroidアプリでPDFファイルを作成する方法について説明しましたが、保存先は暫定的にアプリ固有ストレージに保存していたので、保存したファイルはAndroid Studioを使わないと見れませんでした。今回はちゃんとデバイスのPDF Viewerなどで見れる形でファイルを保存する方法を説明します。

アプリ固有ストレージと共有ストレージ

デベロッパーガイドの「データ ストレージとファイル ストレージの概要」に書かれているように、Androidのストレージは「アプリ固有ストレージ」と「共有ストレージ」の2種類があります。これは、Androidシステム上の制約として、アプリ間でデータを共有できるかどうかという区別になります。デバイス内部ストレージかSDカードなどの外部ストレージか、といった物理的な話ではありません。

アプリ固有ストレージは、そのアプリだけがアクセスできる領域です。アプリAのアプリ固有ストレージには、アプリAしかアクセスできません。他のアプリからアクセスできないため、セキュリティ的に安全な場所で、特別な権限を必要とせずにファイルの読み書きができるのがメリットです。しかし、ドキュメントの共有には向きません。PDFファイルをアプリ固有ストレージに作成しても、Adobe Acrobat ReaderなどのPDF ViewerでPDFファイルを表示することはできません。また、アプリをアンインストールすると、アプリ固有ストレージ内のデータはすべて削除されてしまいます。

一方の共有ストレージは、その名の通り、どのアプリからもアクセスできるストレージです。アプリAが作成したドキュメントや画像を、別のアプリBで表示したり、アプリCで編集したりすることができます。この領域にPDFファイルを作成すれば、PDF Viewerでファイルを表示したり、メールアプリに添付したりといったことができるようになります。また、アプリをアンインストールしても、共有ストレージ上のデータは削除されずに残ります。

ということで今回は「共有ストレージ」にファイルを保存する方法について説明していきます。

ストレージアクセスフレームワーク

共有ストレージには、扱うデータの種類によっていくつかのフレームワークが用意されています。PDFファイルなどのドキュメントは、ストレージアクセスフレームワークを使います。ストレージアクセスフレームワークを使うと、WindowsやMacなどでファイルを保存するときのダイアログと同じような、ディレクトリ選択とファイル名入力のUIを使用できます。デベロッパーガイドの「共有ストレージのドキュメントやファイルにアクセスする」に概要が載っていますが、例として使われているAPIが古かったりするので、詳しく説明していきます。

ちなみに写真や画像などのメディアは、MediaStore APIを使います。MediaStoreについてはこれまでの記事でも何度か紹介しています。

ストレージアクセスフレームワークを使う主なメリットは2つあります。

一つ目は、ディレクトリの選択などのUIをアプリ側で実装する必要がないことです。アプリからはIntentを作成して呼び出すだけで、Androidのシステム側のUIでディレクトリの選択やファイル名の指定などを行えます。さらに、Google Driveもデバイスローカルストレージも同じように扱えます。アプリ個別にGoogle Driveなどクラウドストレージを扱うというのはかなり大変だと思うので、これは大きなメリットだと思います。

二つ目のメリットは、特別な権限を必要としないということです。共有ストレージへのアクセスは一般的にはREAD_EXTERNAL_STORAGEWRITE_EXTERNAL_STORAGE権限が必要で、ユーザーにリクエストする必要があるのですが、ストレージアクセスフレームワークを使うとこれらの権限が不要です。ストレージアクセスフレームワークでは、アプリがアクセスするディレクトリやファイルをユーザー自身が選択するため(ユーザーの知らないところでアプリが勝手にディレクトリやファイルにアクセスすることがないため)、権限が不要なのです。

サポートするユースケース

ストレージアクセスフレームワークは、以下の3つのユースケースをサポートする、とデベロッパーガイドに書いてあります。

  • 新しいファイルを作成する
  • ドキュメントやファイルを開く
  • ディレクトリの内容へのアクセス権を与える

日本人だからかどうか分かりませんが、ユースケースという言葉がピンときませんね。。。ですが、アプリが作成したPDFなどのファイルを保存する場合は、「新しいファイルを作成する」に当てはまります。

大雑把な仕組み

ストレージアクセスフレームワークは、上記のユースケースに対応したIntentを呼び出して使います。Intentを呼び出すと、Androidシステム標準のUIがActivityとして起動し、そのActivity上でユーザーがフォルダ選択などの操作を行います。そしてユーザー操作により処理内容が決定すると、Uriで元のActivityに結果を返します。「新しいファイルを作成する」ユースケースであれば、作成したファイルを指し示すUriが返されるので、そのUriに対してアプリが処理を行うことでファイルにデータを書き込んだりすることができます。

以下ではこのファイル作成の具体的な方法を説明していきます。

ファイルを作成(保存)する

デベロッパーガイドでは、startActivityForResult()Intentを呼び出し、onActivityResult()で結果を受け取っています。ですがandroidxFragmentではこれらの関数がDeprecatedになっておりActivityでも新しいActivity Result APIを使うことを勧められていることから、ここではActivity Result APIを使います。

結果取得時の処理を登録

まず初めに、registerForActivityResult()で、ファイルのUriを取得したときの処理を登録します。

class MainActivity : AppCompatActivity() {
    private val launcher = registerForActivityResult(ActivityResultContracts.CreateDocument()) { uri ->
        uri.getFileOutputStream(this)?.let { createPdf(it) }
    }
}

Activity Result APIでは、Intentを直接は操作しません。その代わり、システムがどんなIntentを呼び出すかを決めるための情報として、第一引数にActivityResultContractを指定します。ファイルを作成する場合は、ActivityResultContracts.CreateDocument()を指定します。

第二引数は結果のコールバックで、作成したファイルのUriが返ってきますので、必要な処理を行います。ここでは取得したUriに対してPDFファイルのデータを書き込みたいので、UriからFileOutputStreamを取得し、前回の記事で説明したcreatePdf()関数に渡しています。getFileOutputStream()は独自のUri拡張関数です。

fun Uri.getFileOutputStream(context: Context): FileOutputStream? {
    val contentResolver = context.contentResolver
    val parcelFileDescriptor = contentResolver.openFileDescriptor(this, "w")
    return parcelFileDescriptor?.let {
        FileOutputStream(it.fileDescriptor)
    }
}

ファイル作成APIの起動

上記でコールバックを登録したActivityResultLauncherを使って実際のファイル作成APIを起動し、フォルダ選択のActivityを呼び出します。

launcher.launch("PDFTest.pdf")

引数にはファイル名を指定します。

・・・以上。実際の呼び出し時の処理はびっくりするくらいシンプルですね。これで図のようにフォルダ選択とファイル名入力のActivityが表示されます。初期状態のフォルダはダウンロードフォルダになっていました。launch()関数で指定したファイル名は初期値として入力された状態で表示されますが、後からユーザーが変更することもできます。フォルダとファイル名を決定して、右下の保存ボタンを押すと、Activityが終了して元のアプリに戻り、最初に登録したコールバックが呼ばれます。

まとめ

前回はPDFの作成方法を説明し、今回は作成したファイルを保存する方法を説明しました。ファイルの保存は、ストレージアクセスフレームワークを使用して共有ストレージに保存しました。具体的な処理としては、Activity Result APIでAndroidシステムが用意しているActivityを呼び出し、結果をUriで受け取って、Uriに対して処理を行うことで、ファイルの保存を実現しました。