Android 12から「コンポーネントのエクスポートの安全性を改善」ということで、インテントフィルタを持つ、Activity/Service/BroadcastReceiverに対して、android:exported
属性を明示的に宣言する必要があります。
Android 12 をターゲットとするアプリに、インテント フィルタを使用するアクティビティ、サービス、またはブロードキャスト レシーバが含まれている場合は、それらのアプリ コンポーネントで android:exported 属性を明示的に宣言する必要があります。
警告: インテント フィルタを使用するアクティビティ、サービス、またはブロードキャスト レシーバが android:exported の値を明示的に宣言していない場合、Android 12 を実行しているデバイスにアプリをインストールできません。
さて、このandroid:exported
ですが、
この要素では、アクティビティを他のアプリのコンポーネントから起動できるかどうかを設定します。起動できる場合は "true"、起動できない場合は "false" を指定します。"false" の場合、同じアプリまたは同じユーザー ID を持つアプリのコンポーネントからのみアクティビティを起動できます。
と説明があるように、自分自身で呼び出す場合はfalse
で良いけど、外部から呼び出してもらう場合はtrue
にしておく必要があるとのこと。従来intent-filterを設定しているならtrue
していなければfalse
がデフォルト値だったことからも、意図はなんとなく分かります。
しかし、この対応を行っている中で、ACTION_BOOT_COMPLETED
のIntentはandroid:exported
がfalse
の状態でも受け取れることに気づきました。ACTION_BOOT_COMPLETED
はAndroidシステムが投げているIntentなので、true
でなければならないと思っていたのに、どういうことだ?と疑問に思い、調査してみることにしました。
準備
準備として、テスト用アプリMyApplicationを作ります。
Activity/Service/Receiverで起動されたらIntentをLogcatに表示するだけのものを作っておきます。
class MyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.e("XXXX", "Activity onCreate: $intent")
}
}
class MyService : Service() {
override fun onBind(intent: Intent): IBinder? = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.e("XXXX", "Service onStartCommand: $intent")
return super.onStartCommand(intent, flags, startId)
}
}
class MyReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
Log.e("XXXX", "BroadcastReceiver onReceive: $intent")
}
}
Manifestには以下のように書き、android:exported
をfalse
にしておきます。intent-filterもありません。
<activity
android:name=".MyActivity"
android:exported="false" />
<service
android:name=".MyService"
android:enabled="true"
android:exported="false" />
<receiver
android:name=".MyReceiver"
android:enabled="true"
android:exported="false" />
自分のアプリから呼び出す
まずは、自分のアプリから呼び出してみます。当然、全部成功します。
Activity onCreate: Intent { cmp=com.android.myapplication/.MyActivity }
Service onStartCommand: Intent { cmp=com.android.myapplication/.MyService }
BroadcastReceiver onReceive: Intent { flg=0x10 cmp=com.android.myapplication/.MyReceiver }
別のアプリから呼び出す
次に別のアプリ、MyApplication2をつくって、そこから呼び出してみます。
ちょっと話がそれますが、Android 11以上の場合、startServcieを他のアプリから行う場合は、呼び出し元から呼び出される側のパッケージが見える状態にしておく必要があるようです。見えない場合、android:exported
がtrue
であっても、not foundというログが出るだけで何も起こりません。
ActivityManager: Unable to start service Intent { cmp=com.android.myapplication/.MyService } U=0: not found
AndroidManifestにqueriesタグを追加して見えるようにしておきます。
<queries>
<package android:name="com.android.myapplication" />
</queries>
実行してみます。
E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.android.myapplication2, PID: 5975
java.lang.SecurityException: Permission Denial: starting Intent { cmp=com.android.myapplication/.MyActivity } from ProcessRecord{aabf993 5975:com.android.myapplication2/u0a152} (pid=5975, uid=10152) not exported from uid 10151
E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.android.myapplication2, PID: 5903
java.lang.SecurityException: Not allowed to start service Intent { cmp=com.android.myapplication/.MyService } without permission not exported from uid 10151
W/BroadcastQueue: Permission Denial: broadcasting Intent { flg=0x10 cmp=com.android.myapplication/.MyReceiver } from com.android.myapplication2 (pid=6018, uid=10152) to com.android.myapplication/.MyReceiver is not exported from uid 10151
startActivityとstartServiceはどちらもSecurityExceptionが発生してクラッシュしてしまいました。
sendBroadcastはPermission Denialというワーニングログが出るだけでないも起こりません。
いずれにせよ、Intentを送ることはできませんでした。
PendingIntent
コンポーネントを持つアプリが作ったPendingIntentを他のアプリが投げる場合
自分のアプリから呼び出すことができるのであれば、自分のアプリから呼び出すPendingIntentをつくって、それを別のアプリに渡して、嘆かしてもらう場合はどうなるでしょうか?
MyApplication2のMainActivityでIntentのExtraで渡ってきたPendingIntentをそのまま投げ返すようにしておきます。
if (intent.action == "PendingIntent") {
intent.getParcelableExtra<PendingIntent>("PendingIntent")?.send()
}
MyApplicationではシンプルにPendingIntentをExtraにいれて投げるようにしています。
val intent = Intent().also {
it.component = ComponentName("com.android.myapplication", "com.android.myapplication.MyActivity")
}
val pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE)
startActivity(Intent().also {
it.component = ComponentName("com.android.myapplication2", "com.android.myapplication2.MainActivity")
it.action = "PendingIntent"
it.putExtra("PendingIntent", pendingIntent)
})
実行してみます。
Activity onCreate: Intent { flg=0x10000000 cmp=com.android.myapplication/.MyActivity }
Service onStartCommand: Intent { cmp=com.android.myapplication/.MyService }
BroadcastReceiver onReceive: Intent { flg=0x10 cmp=com.android.myapplication/.MyReceiver }
いずれも成功です。
他のアプリが作ったPendingIntentをコンポーネントを持つアプリが投げる
では逆に、MyAppication2でMyApplicationのコンポーネントに向けたPendingIntentを作成し、それをMyApplicationに投げさせた場合はどうでしょう?通常やることはまずないでしょうが、念のため試してみましょう。
W/ActivityManager: Unable to send startActivity intent
java.lang.SecurityException: Permission Denial: starting Intent { cmp=com.android.myapplication/.MyActivity } from null (pid=-1, uid=10152) not exported from uid 10151
W/ActivityManager: Permission Denial: Accessing service com.android.myapplication/.MyService from pid=-1, uid=10152 that is not exported from uid 10151
W/BroadcastQueue: Permission Denial: broadcasting Intent { flg=0x10 cmp=com.android.myapplication/.MyReceiver } from com.android.myapplication2 (pid=-1, uid=10152) to com.android.myapplication/.MyReceiver is not exported from uid 10151
結果、いずれも失敗しました。
PendingIntentはsendをコールしたアプリではなく、制作したアプリが投げているような扱いになるようですね。
システムが投げるBroadcastIntent
アプリではなくシステムが投げる系のBroadcastIntentはどうでしょう?
android.intent.action.BOOT_COMPLETED
とかandroid.intent.action.MY_PACKAGE_REPLACED
あたりがよく使われるBroadcastIntentでしょうか?これはintent-filterを書かないといけないのでAndroidManifestを変更します。
<receiver
android:name=".MyReceiver"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter>
</receiver>
結果、受け取れてしまいました。
BroadcastReceiver onReceive: Intent { act=android.intent.action.BOOT_COMPLETED flg=0x89000010 cmp=com.android.myapplication/.MyReceiver (has extras) }
BroadcastReceiver onReceive: Intent { act=android.intent.action.MY_PACKAGE_REPLACED flg=0x4000010 pkg=com.android.myapplication cmp=com.android.myapplication/.MyReceiver (has extras) }
AppWidgetProviderは?
AppWidgetProviderはコンポーネントとしてはBroadcastReceiverですね。AppWidgetが貼り付けられるAppWidgetHostは一般アプリと同じ扱いであるはずのホームアプリではありますが、AppWidgetの仕組み自体はシステムが提供しているのでどちらの扱いになるのかチョット気になりますね。
AppWidgetを追加するとAndroidManifestには以下のようにandroid:exported="true"
と明示された形で記述されます。ということは、falseにするとダメな奴でしょうか?
<receiver
android:name=".MyAppWidget"
android:exported="true"
>
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/my_app_widget_info"
/>
</receiver>
さて、android:exported="false"
にしてBroadcastReceiverと同様にonReceiveで受け取ったIntentを表示させてみます。
BroadcastReceiver onReceive: Intent { act=android.appwidget.action.APPWIDGET_ENABLED flg=0x10 cmp=com.android.myapplication/.MyAppWidget }
BroadcastReceiver onReceive: Intent { act=android.appwidget.action.APPWIDGET_UPDATE flg=0x10 cmp=com.android.myapplication/.MyAppWidget (has extras) }
BroadcastReceiver onReceive: Intent { act=android.appwidget.action.APPWIDGET_UPDATE_OPTIONS flg=0x10 cmp=com.android.myapplication/.MyAppWidget (has extras) }
BroadcastReceiver onReceive: Intent { act=android.appwidget.action.APPWIDGET_DELETED flg=0x10 cmp=com.android.myapplication/.MyAppWidget (has extras) }
BroadcastReceiver onReceive: Intent { act=android.appwidget.action.APPWIDGET_DISABLED flg=0x10 cmp=com.android.myapplication/.MyAppWidget }
はい、受け取れてしまいました。
まとめ
android:exported
をfalse
にした場合
- 自分のアプリからそのコンポーネントを起動することは可能
- 別のアプリからそのコンポーネントを起動することは不可能
- 自分のアプリで作ったPendingIntentから起動させることは可能(sendメソッドをコールするアプリが自アプリである必要は無い)
- システムが投げるBroadcastIntentを受け取ることは可能
- AppWidgetProviderもfalseでも問題無く動作する
という結果となりました。
最初の3つは納得できるのですが、4つ目が納得しがたいですね。
理由が分かる方いらっしゃいましたらコメントいただけますとありがたいです。
以上です。