2012/03/09

Android:Broadcastを受信できないアプリのSTOP状態

Android3.1以降、アプリケーションに新たなステータス"STOP"が追加されました。
アプリインストール後、一度も起動されていないアプリはSTOP状態となります。
システムアプリを除いて、STOP状態のアプリはBroadcastIntentを受け取れない場合があり
ます。
# 概要はhttp://developer.android.com/sdk/android-3.1.htmlで紹介されています。

今回は、アプリのSTOP状態について調査します。

調査は下記のコードで実施しています。
BOOT_BOMPLETEを受け取るだけのレシーバをAndroidManifestで静的に登録しているアプリです。

AndroidManifest.xml
<receiver
        android:name="MyBroadcastReceiver">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED" />
    </intent-filter>
</receiver>

yuki.broadcast.MyBroadcastReceiver
public class MyBroadcastReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        android.util.Log.e("yuki", "yuki onReceive");
    }
}

上記で作成したアプリで下記手順を実施し、ブロードキャストインテントを受け取れてい
るかどうかをログ("yuki onReceive")から判断します。
# 手順は上から順番に実施。

1)端末にアプリを新規にインストールしてブロードキャスト送信
adb install apk/BroadcastTest.apk
adb shell am broadcast -a android.intent.action.BOOT_COMPLETED
結果:反応なし/インストール直後はSTOP状態


2)再起動して再度ブロードキャスト送信
adb shell reboot
adb shell am broadcast -a android.intent.action.BOOT_COMPLETED
結果:反応なし/STOP状態は再起動しても変わらない


3)アプリ起動後にプロセスkillした後にブロードキャスト送信
adb shell am start -n yuki.broadcast/.BroadcastTestActivity
adb shell kill <アプリPID>
adb shell am broadcast -a android.intent.action.BOOT_COMPLETED
結果:反応あり/一度アプリ起動すれば、今まで通りブロードキャスト受信は可能


4)端末再起動後にブロードキャスト送信
adb shell reboot
adb shell am broadcast -a android.intent.action.BOOT_COMPLETED
結果:反応あり/一度でもアプリ起動していれば、再起動後でも受信可能


5)アプリを更新した後にブロードキャスト送信
adb install -r apk/BroadcastTest.apk
adb shell am broadcast -a android.intent.action.BOOT_COMPLETED
結果:反応あり/アプリ更新後も、継続して受信可能


6)アプリをアンインストール→再インストール後にブロードキャスト送信
adb uninstall yuki.broadcast
adb install apk/BroadcastTest.apk
adb shell am broadcast -a android.intent.action.BOOT_COMPLETED
結果:反応なし/再インストール後は再度STOP状態になる


7)FLAG_INCLUDE_STOPPED_PACKAGESを指定してブロードキャスト送信
# パッケージ再インストール
adb uninstall yuki.broadcast
adb install apk/BroadcastTest.apk

adb shell am broadcast -a android.intent.action.BOOT_COMPLETED --include-stopped-packages
結果:反応あり/FLAG_INCLUDE_STOPPED_PACKAGESを付与すればSTOP状態でも受信可能


8)FLAG_INCLUDE_STOPPED_PACKAGESで受信後にFLAG_EXCLUDE_STOPPED_PACKAGES指定で送信
# パッケージ再インストール
adb uninstall yuki.broadcast
adb install apk/BroadcastTest.apk

# FLAG_INCLUDE_STOPPED_PACKAGES付与したブロードキャスト送信
adb shell am broadcast -a android.intent.action.BOOT_COMPLETED --include-stopped-packages

# FLAG_EXCLUDE_STOPPED_PACKAGES付与したブロードキャスト送信
adb shell am broadcast -a android.intent.action.BOOT_COMPLETED --exclude-stopped-package
結果:反応あり/FLAG_INCLUDE_STOPPED_PACKAGES指定で受信さえすればSTOP状態は解除
される


まとめ:
  • 一度でもアプリを起動すれば、再起動後でも受信可能
  • アプリの更新でSTOP状態にはならない
  • アプリの再インストールはSTOP状態になる
  • FLAG_INCLUDE_STOPPED_PACKAGESで一度でも受信すればSTOP状態は解除される

Android4.x以降でもこれは有効です。
「ブロードキャストを受信できない」といった不具合が出た場合、STOP状態となっていな
いか確認したほうがよいでしょう。



●ソースコード解析
本動作・仕様についてシステム側のソースコードを解析してみます。
ブロードキャストインテント送信開始からのコードを追いました。

IntentResolverでIntentを処理するコンポーネントを探索。
com.android.server.IntentResolver.buildResolveList(...)

final boolean excludingStopped = intent.isExcludingStopped();
final int N = src != null ? src.size() : 0;
boolean hasNonDefaults = false;
int i;
for (i=0; i<N; i++) {
    F filter = src.get(i);
    int match;
    if (debug) Slog.v(TAG, "Matching against filter " + filter);

    if (excludingStopped && isFilterStopped(filter)) {
        if (debug) {
            Slog.v(TAG, "  Filter's target is stopped; skipping");
        }
        continue;
    }

excludingStoppedはIntentフラグのFLAG_EXCLUDE_STOPPED_PACKAGESがONである場合True。
isFilterStoppedは対象がSTOP状態である場合True。
両方Trueの場合は、Filter targe(アプリ)がSTOP状態と判断してIntent処理しない。


対象のSTOP状態を調べるメソッドを追ってみます。
com.android.server.pm.PackageManagerService.ActivityIntentResolver.isFilterStopped(...)
@Override
protected boolean isFilterStopped(PackageParser.ActivityIntentInfo filter) {
    PackageParser.Package p = filter.activity.owner;
    if (p != null) {
        PackageSetting ps = (PackageSetting)p.mExtras;
        if (ps != null) {
            // System apps are never considered stopped for purposes of
            // filtering, because there may be no way for the user to
            // actually re-launch them.
            return ps.stopped && (ps.pkgFlags&ApplicationInfo.FLAG_SYSTEM) == 0;
        }
    }
    return false;
}

コメントを見るとシステムアプリはSTOP状態でもIntentの受信は可能のようです。
ps.stoppedの設定元を追っていくとcom.android.server.pm.Settings.readStoppedLPw()
の下記にたどり着きます。
String tagName = parser.getName();
if (tagName.equals("pkg")) {
    String name = parser.getAttributeValue(null, "name");
    PackageSetting ps = mPackages.get(name);
    if (ps != null) {
        ps.stopped = true;
        if ("1".equals(parser.getAttributeValue(null, "nl"))) {
            ps.notLaunched = true;
        }
    } else {
        Slog.w(PackageManagerService.TAG, "No package known for stopped package: " + name);
    }

なにやらXMLをパースして、pkgタグのname属性に記載されているパッケージのnl属性値が
1であればパッケージSTOP状態としています。

パースしているXMLは/data/system/packages-stopped.xmlのようです。
yuki.broadcastアプリを一度も起動していないと下記のようなXMLの内容になります。
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<stopped-packages>
<pkg name="yuki.broadcast" nl="1" />
   ... その他色々
</stopped-packages>

一度でも起動するとpkgタグは削除されるので、
ps.stopped = true;
となるルートは通らなくなります。

これらのことから、下記手順を試してみます。
# パッケージ再インストール
adb uninstall yuki.broadcast
adb install apk/BroadcastTest.apk

# packages-stopped.xmlを取得
adb pull /data/system/packages-stopped.xml packages-stopped.xml

# ...
#  packages-stopped.xmlからyuki.broadcastのtagを削除
# ...

# 編集したpackages-stopped.xmlで置き換える
adb pull packages-stopped.xml /data/system/packages-stopped.xml

# ブロードキャスト送信
adb shell am broadcast -a android.intent.action.BOOT_COMPLETED
結果:反応あり/STOP状態はpackages-stopped.xmlで管理されている

以上です。