⛏️

ファイルを他のアプリで開きたい ▶︎Intentを勉強する

2022/10/22に公開

Intentを完全に理解した(していない)ので共有...何卒何卒

Intent とは

https://developer.android.com/guide/components/intents-common?hl=ja

AndroidではActivityがアプリの画面の土台を表しますが、Activityから別のActivityへ移動したい、または他のActivityを使って何かデータを取得したいといったことをしたい時に使うのがIntentです。


Activityから別のActivityへ移動


他のActivityを使って何かデータを取得したい

この時移動先のActivityに何か情報を渡すことができます。
例えば「ブラウザからTwitterアプリへ移動するときにユーザ名が渡されることで、さっきまでブラウザで表示されていたユーザのページをTwitterアプリで見れる。」といった具合です。

この移動元から渡される 「どのActivityへどのように(どのような情報を引数に)移動するか」 をしているするための依頼書が Intent です。

Intentには2種類ある

  • 明示的Intent ... どのActivityへ移動したいか 具体的なActivity名 で指定する
    例) Twitterアプリを開く
  • 暗黙的Intent ... そのActivityへ移動することで 何がしたいのか を指定することで一番最適なActivityを選んで移動する
    例) 画像ビューアを開く (どのアプリが開かれるかはこちらから指定しない,画像が見れればなんでもいい)

これら二つを使い分けます。

実装方法

移動するとき

呼び出しもとのActivityからstartActivity()で呼び出します。

明示的インテントで別のActivityへ移動
val intent = Intent(移動元Activity, 移動先Activity::class.java)
移動元Activity.startActivity(intent)
暗黙的インテントで別のActivityへ移動
val intent = Intent(Intent.移動内容)
移動元Activity.startActivity(intent)

移動内容には「何かを表示したい」時に使うIntent.ACTION_VIEWや「ファイルなどを選択したい」時に使うIntent.ACTION_OpenDocumentなどを指定します。また必要に応じてapplyスコープ関数で情報を追加できます。詳しい内容やサンプルについてはドキュメントを参照してください。

https://developer.android.com/guide/components/intents-common?hl=ja

移動先から結果が欲しい時

例えばファイルを選択してくれるActivityがありますが、それから結果を取得するには一手間必要です。

Composeで
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri :Uri? ->
    // 選択結果を受け取った時にしたいこと
    // 引数に選んだものが入ってくる
  }
// 選択を実行したいところで
launcher.launch("image/*")
Android Viewでは...

Activity Result APIというものを使うそうです。

詳しくはこちらの方の記事がいいかと...!
https://zenn.dev/t2low/articles/ea610398e29154e1a887

特にこの章で解説されているActivityResultContractの解説はComposeでも役だったりします。

従来はActivity#onActivityResultというメソッドをオーバーライドして実装していたようですが、これが非推奨になったり、そもそもこれではComposeで使えなかったりでこちらを使うことはなさそうですが、ググるとよく出てくるので注意。

僕が触ったことのあるよくやる使い方

ケース1 ファイルを選ぶ

val launcher = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) {
  if (it == null) return@rememberLauncherForActivityResult
 // ここ以降でit(Uri)を煮るなり焼くなり好きにする
  Log.d("my-app", "uri:$it")
 // 後述
  val takeFlags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION or
      Intent.FLAG_GRANT_WRITE_URI_PERMISSION
  context.contentResolver.takePersistableUriPermission(it, takeFlags)
}

// ファイルを選択したいところで
launcher.launch("image/*")  // ファイルの種類(MIME TYPE)を指定する 全ファイルの場合は"*/*"

GetContentとOpenDocumentの違い

ドキュメントではファイルなどの選択のためにOpenDocumentだけでなくGetContentも使っています。

https://developer.android.com/guide/components/intents-common?hl=ja#GetFile

アプリに返されるファイルへの参照は、アクティビティの現在のライフサイクル内でのみ利用できます

ドキュメントより

↑↑↑でも解説されているとおりGetContentアクティビティの現在のライフサイクルでのみ利用できるため、画面をひっくり返したりするとクリアされてしまいます。よってより長くファイルの参照を保持したいときはOpenDocumentを使用します。(MIME TYPEの指定も忘れずに...)

落とし穴

OpenDocument で選択しても、アプリを閉じた後に再度同じUriを開こうとすると権限(パーミッション)の期限が切れるのか、SecurityExceptionが発生してしまいます。

java.lang.SecurityException: Permission Denial: reading com.android.providers.media.MediaDocumentsProvider uri content://com.android.providers.media.documents/document/image%3A1000000026 from pid=2548, uid=10168 requires that you obtain access using ACTION_OPEN_DOCUMENT or related APIs
	at android.os.Parcel.createExceptionOrNull(Parcel.java:3011)
	at android.os.Parcel.createException(Parcel.java:2995)
...(略)...

これを回避するには Uriを取得したときに ContentResolver#takePersistableUriPermission() を呼び出す必要があります。

takePersistableUriPermissionでパーミッションを永続化
  val launcher = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) {
  if (it == null) return@rememberLauncherForActivityResult
  // itを使う
+ val takeFlags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION or
+   Intent.FLAG_GRANT_WRITE_URI_PERMISSION
+ context.contentResolver.takePersistableUriPermission(it, takeFlags)

ケース2 ファイルをビューアで見る

Intentを呼び出すときは

val intent = Intent(Intent.ACTION_VIEW).apply {
  type = "image/*"
  data = uri  // OpenDocumentなどで取得したUri
}
activity.startActivity(Intent.ACTION_VIEW)

また、uriを見ていい感じにtypeを推測してくれる関数も作っておくと便利です。

Uri->MimeTypeに変換してくれる関数
fun ContentResolver.getMimeType(uri: Uri): String? {
  var mimeType: String? = null
  this.query(
    uri,
    arrayOf(MediaColumns.MIME_TYPE),
    null, null, null
  ).use { cur ->
    cur ?: return@use
    cur.moveToFirst()
    mimeType = cur.getString(0)
    Log.d("test", "mimeType${mimeType}")
  }
  return mimeType
}

これで送られてくるファイルの種類が不定でも対応できるはずです。

  val intent = Intent(Intent.ACTION_VIEW).apply {
+   type = activity.contextResolver.getMimeType(uri)
    data = uri  // OpenDocumentなどで取得したUri
  }
activity.startActivity(Intent.ACTION_VIEW)

他にもできること

本当にいろんなインテントがデフォルトであって

  • アラーム・タイマーの作成、表示
  • カレンダー
  • カメラで画像・動画を撮って返す
  • 連絡先関係
  • メール
  • ファイル操作
  • タクシーを呼ぶ(?!)
  • 電話をかける
  • 特定の設定画面を開く
  • Webブラウザを開く

のようなものが用意されています。一部APIはパーミッションが必要なのでAndroidManifest.xmlに権限の追加もお忘れなく...!

おのおの呼び出す際に指定しなければいけない項目が多いのでドキュメントをしっかり見ることをお勧めします...

https://developer.android.com/guide/components/intents-common?hl=ja

自分のActivityを他のActivityにも暗黙的Intentで呼び出してもらえるようにするには?

しっかり調べてはいませんが、AndroidManifest.xmlactivity.intent-filterにタグを追加していく感じっぽいです。(有識者の方知識求む)

ドキュメントの通り見てみると以下の通りです。

AndroidManifest.xml
<activity android:name="ShareActivity">
    <intent-filter>
        <action android:name="android.intent.action.SEND"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <data android:mimeType="text/plain"/>
    </intent-filter>
</activity>

あとはドキュメント見るなりググるなりしてください。

(TBStenは職務放棄した!)

https://developer.android.com/guide/components/intents-filters?hl=ja#Receiving

Intent、(呼び出し方は)完全に理解した!

⭐️⭐️⭐️これであなたもAndroidレベルアップです⭐️⭐️⭐️

Discussion