LoginSignup
40
17

android:exported="false" とはどういうことか?

Last updated at Posted at 2021-09-19

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:exportedfalseの状態でも受け取れることに気づきました。ACTION_BOOT_COMPLETEDはAndroidシステムが投げているIntentなので、trueでなければならないと思っていたのに、どういうことだ?と疑問に思い、調査してみることにしました。

準備

準備として、テスト用アプリMyApplicationを作ります。
Activity/Service/Receiverで起動されたらIntentをLogcatに表示するだけのものを作っておきます。

MyActivity.kt
class MyActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        Log.e("XXXX", "Activity onCreate: $intent")
    }
}
MyService.kt
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)
    }
}
MyReceiver.kt
class MyReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        Log.e("XXXX", "BroadcastReceiver onReceive: $intent")
    }
}

Manifestには以下のように書き、android:exportedfalseにしておきます。intent-filterもありません。

AndroidManifest.xml
<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:exportedtrueであっても、not foundというログが出るだけで何も起こりません。

ActivityManager: Unable to start service Intent { cmp=com.android.myapplication/.MyService } U=0: not found

AndroidManifestにqueriesタグを追加して見えるようにしておきます。

AndroidManifest.xml
<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を変更します。

AndroidManifest.xml
<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にするとダメな奴でしょうか?

AndroidManifest.xml
<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:exportedfalseにした場合

  • 自分のアプリからそのコンポーネントを起動することは可能
  • 別のアプリからそのコンポーネントを起動することは不可能
  • 自分のアプリで作ったPendingIntentから起動させることは可能(sendメソッドをコールするアプリが自アプリである必要は無い)
  • システムが投げるBroadcastIntentを受け取ることは可能
    • AppWidgetProviderもfalseでも問題無く動作する

という結果となりました。
最初の3つは納得できるのですが、4つ目が納得しがたいですね。
理由が分かる方いらっしゃいましたらコメントいただけますとありがたいです。

以上です。

40
17
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
40
17