67
59

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Android 8.0調査メモ:もう裏でこっそりサービスを動かせない件(前編)

Last updated at Posted at 2017-11-30

これは何?

https://developer.android.com/about/versions/oreo/background.html?hl=ja#services
Android 8.0では、バックグラウンド動作制限の一環として、 **裏でこっそりサービス起動しようとしてもできなくなります!**だって。

まじかよ!!!!!

でも、 リファレンスに書いてあるとおりの実装になっているとは限らないのがAndroidのいいところだ(違w
いちおうソースはちゃんと読んでおこう、ということで調査した自分用メモです。

(長くなったので、前編・後編に分けましたw (が、後半は適当です

実際にアプリを動かすとどんな感じになっているか?

  • JobScheduler(とか、それをラップしているGcmNetworkManager)から起動されるジョブサービスはちゃんと裏でも動ける
    • でもそこからstartServiceやるとクラッシュ
  • Activityから立ち上げたサービスはちゃんと起動できる
    • でもActivityを閉じてしばらく経つと、サービスが勝手に終了する
  • startForegroundServiceすれば、バックグラウンドからの起動でも死なない。でも
    • 5秒以内にstartForegroundを呼ばないとクラッシュ
    • startForegroundを呼ばずにstopSelf()してもクラッシュ
      • IntentServiceの場合は、どこかでstartForeground呼ばないとクラッシュ

ということで、割と簡単にFATAL EXCEPTIONでクラッシュしてしまう。怖い。

参考:Android Oからのバックグラウンド・サービスの制限事項を実演する。

ソースを読むぞ

startServiceで落ちて、startForegroundServiceで落ちないカラクリ

Context#startServiceContext#startForegroundService も、結局は ContextImpl#startServiceCommon を呼び出している。
違いは第2引数の boolean requireForeground がtrue/falseだけ。

/frameworks/base/core/java/android/app/ContextImpl.java
1486    private ComponentName startServiceCommon(Intent service, boolean requireForeground,
1487            UserHandle user) {
1488        try {
1489            validateServiceIntent(service);
1490            service.prepareToLeaveProcess(this);
1491            ComponentName cn = ActivityManager.getService().startService(
1492                mMainThread.getApplicationThread(), service, service.resolveTypeIfNeeded(
1493                            getContentResolver()), requireForeground,
1494                            getOpPackageName(), user.getIdentifier());
1495            if (cn != null) {
1496                if (cn.getPackageName().equals("!")) {
1497                    throw new SecurityException(
1498                            "Not allowed to start service " + service
1499                            + " without permission " + cn.getClassName());
1500                } else if (cn.getPackageName().equals("!!")) {
1501                    throw new SecurityException(
1502                            "Unable to start service " + service
1503                            + ": " + cn.getClassName());
1504                } else if (cn.getPackageName().equals("?")) {
1505                    throw new IllegalStateException(
1506                            "Not allowed to start service " + service + ": " + cn.getClassName());
1507                }
1508            }
1509            return cn;
1510        } catch (RemoteException e) {
1511            throw e.rethrowFromSystemServer();
1512        }
1513    }

ActivityManagerServiceにstartServiceを要求してみて、その結果がおかしなもの("!"とか"?"とか)だったら例外を送出するようになっている。

バックグラウンド制限の例外ログをみると

11-29 23:57:37.516 W/AndroidGcmController(  772): java.lang.IllegalStateException: Not allowed to start service Intent { cmp=com.chrome.canary/com.google.ipc.invalidation.ticl.android2.TiclService (has extras) }: app is in background uid UidRecord{3da5198 u0a79 RCVR bg:+3m53s39ms idle procs:2 seq(0,0,0)}

こんなかんじなので、まさに "?" のルートを通ってるみたい。

この "?" を設定しているのは、ActivityManagerService側。 app is in background uid でソース検索すると定義場所が特定できた。

/frameworks/base/services/core/java/com/android/server/am/ActiveServices.java
327    ComponentName startServiceLocked(IApplicationThread caller, Intent service, String resolvedType,
328            int callingPid, int callingUid, boolean fgRequired, String callingPackage, final int userId)
329            throws TransactionTooLargeException {

 ...

365        // If this isn't a direct-to-foreground start, check our ability to kick off an
366        // arbitrary service
367        if (!r.startRequested && !fgRequired) {
368            // Before going further -- if this app is not allowed to start services in the
369            // background, then at this point we aren't going to let it period.
370            final int allowed = mAm.getAppStartModeLocked(r.appInfo.uid, r.packageName,
371                    r.appInfo.targetSdkVersion, callingPid, false, false);
372            if (allowed != ActivityManager.APP_START_MODE_NORMAL) {
373                Slog.w(TAG, "Background start not allowed: service "
374                        + service + " to " + r.name.flattenToShortString()
375                        + " from pid=" + callingPid + " uid=" + callingUid
376                        + " pkg=" + callingPackage);
377                if (allowed == ActivityManager.APP_START_MODE_DELAYED) {
378                    // In this case we are silently disabling the app, to disrupt as
379                    // little as possible existing apps.
380                    return null;
381                }
382                // This app knows it is in the new model where this operation is not
383                // allowed, so tell it what has happened.
384                UidRecord uidRec = mAm.mActiveUids.get(r.appInfo.uid);
385                return new ComponentName("?", "app is in background uid " + uidRec);
386            }
387        }

? が設定されるまで

Context#startForegroundService
 ↓ requireForeground=true

Contextimpl#startServiceCommon
 ↓ requireForeground=true
ActivityManagerService#startService
 ↓ fgRequired=true
ActiveService#startServiceLocked

って感じで呼ばれているようだ。

365        // If this isn't a direct-to-foreground start, check our ability to kick off an
366        // arbitrary service
367        if (!r.startRequested && !fgRequired) {
368            // Before going further -- if this app is not allowed to start services in the
369            // background, then at this point we aren't going to let it period.
370            final int allowed = mAm.getAppStartModeLocked(r.appInfo.uid, r.packageName,
371                    r.appInfo.targetSdkVersion, callingPid, false, false);
372            if (allowed != ActivityManager.APP_START_MODE_NORMAL) {

ここの2つのif文がキモ。

1つ目のif文は

  • サービスが実行開始(=r.startRequested)されていない状態
  • startForegroundServiceではなくstartServiceで起動した状態

ほぼほぼ通りそう。

2つ目のif文では、AppStartModeってのを取得してチェックしている。
ここでNORMAL以外の場合には実際にstartServiceせずに、例外を出すような "?" のComponentNameを定義している。

AppStartModeってなんだ???

ActivityManagerServiceのソースを雑に読んだところ

  • NORMAL→今すぐ動いていい状態
  • DELAYED→何らかの理由により、"後回し"にされた状態
  • DISABLED→何らかの理由により、"動かないで!"ってされた状態

みたいなイメージ。

AppStartModeがNORMALじゃなくしているのは誰だ?

ActivityManagerServiceで APP_START_MODE_NORMAL をreturnしている関数をざっくり見てみる。

1つめ、 appRestrictedInBackgroundLocked。こいつはパット見かなり激しいことをやっていて、 Android Oreo以降であれば問答無用で APP_START_MODE_DELAYED_RIGID を返すようだ。

8372    // Unified app-op and target sdk check
8373    int appRestrictedInBackgroundLocked(int uid, String packageName, int packageTargetSdk) {
8374        // Apps that target O+ are always subject to background check
8375        if (packageTargetSdk >= Build.VERSION_CODES.O) {
8376            if (DEBUG_BACKGROUND_CHECK) {
8377                Slog.i(TAG, "App " + uid + "/" + packageName + " targets O+, restricted");
8378            }
8379            return ActivityManager.APP_START_MODE_DELAYED_RIGID;
8380        }

で、こいつがどこから呼ばれているかというと、

8397    // Service launch is available to apps with run-in-background exemptions but
8398    // some other background operations are not.  If we're doing a check
8399    // of service-launch policy, allow those callers to proceed unrestricted.
8400    int appServicesRestrictedInBackgroundLocked(int uid, String packageName, int packageTargetSdk) {
8401        // Persistent app?
8402        if (mPackageManagerInt.isPackagePersistent(packageName)) {
8403            if (DEBUG_BACKGROUND_CHECK) {
8404                Slog.i(TAG, "App " + uid + "/" + packageName
8405                        + " is persistent; not restricted in background");
8406            }
8407            return ActivityManager.APP_START_MODE_NORMAL;
8408        }
8409
8410        // Non-persistent but background whitelisted?
8411        if (uidOnBackgroundWhitelist(uid)) {
8412            if (DEBUG_BACKGROUND_CHECK) {
8413                Slog.i(TAG, "App " + uid + "/" + packageName
8414                        + " on background whitelist; not restricted in background");
8415            }
8416            return ActivityManager.APP_START_MODE_NORMAL;
8417        }
8418
8419        // Is this app on the battery whitelist?
8420        if (isOnDeviceIdleWhitelistLocked(uid)) {
8421            if (DEBUG_BACKGROUND_CHECK) {
8422                Slog.i(TAG, "App " + uid + "/" + packageName
8423                        + " on idle whitelist; not restricted in background");
8424            }
8425            return ActivityManager.APP_START_MODE_NORMAL;
8426        }
8427
8428        // None of the service-policy criteria apply, so we apply the common criteria
8429        return appRestrictedInBackgroundLocked(uid, packageName, packageTargetSdk);
8430    }

ここだ。

  • Persistentなアプリ
    • AndroidManidestでpersistent=true指定をしたアプリ。システムアプリ向けのフラグなので一般アプリには該当しない
  • UIDがBackgroundWhitelistに入ってる
    • OreoではBLUETOOTH_UIDだけが固定でホワイトリストに入ってるので大抵のアプリには該当しない
  • UIDがDeviceIdleWhitelistに入っている
    • ちゃんと読んでないけど、多分大抵の場合には該当しないんじゃないかなー(てきとうw

つまり、 appServicesRestrictedInBackgroundLocked も、Android Oreo以降であれば ほぼほぼ問答無用で APP_START_MODE_DELAYED_RIGID を返すということになる。

appServicesRestrictedInBackgroundLockedを呼んでいるのは誰だ?

ソース検索してみると・・・ getAppStartModeLocked

おおおおおお!! さっきの2つ目のif文に登場してたやつだ。

/frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java
8432    int getAppStartModeLocked(int uid, String packageName, int packageTargetSdk,
8433            int callingPid, boolean alwaysRestrict, boolean disabledOnly) {
8434        UidRecord uidRec = mActiveUids.get(uid);

         ...

8438        if (uidRec == null || alwaysRestrict || uidRec.idle) {

         ...

8458                final int startMode = (alwaysRestrict)
8459                        ? appRestrictedInBackgroundLocked(uid, packageName, packageTargetSdk)
8460                        : appServicesRestrictedInBackgroundLocked(uid, packageName,
8461                                packageTargetSdk);
         ...
8482                return startMode;
8483            }
8484        }
8485        return ActivityManager.APP_START_MODE_NORMAL;
8486    }

重要ポイントだけ抽出すると、こんなロジックをしている。

  • alwaysRestrictがtrue
    • 今回の場合はfalseが指定されて呼ばれているので関係ない
  • ActiveUidsに含まれていない
  • ActiveUidsには含まれているけどidle

だったら、APP_START_MODE_NORMALではなくAPP_START_MODE_DELAYED_RIGIDが返る。

今回の処理ルートには登場してこないけど、ActivityManagerには以下のようなメソッドがあって、「ActiveUidsに含まれていない or ActiveUidsには含まれているけどidle」であればフォアグラウンド動作していないと見なされているらしい。

/frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java
7847    @Override
7848    public boolean isAppForeground(int uid) throws RemoteException {
7849        synchronized (this) {
7850            UidRecord uidRec = mActiveUids.get(uid);
7851            if (uidRec == null || uidRec.idle) {
7852                return false;
7853            }
7854            return uidRec.curProcState <= ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND;
7855        }
7856    }

 
とりあえず突き詰めればきりがないので、
バックグラウンドでstartServiceすると落ちるカラクリはここまで。

startForegroundServiceしたあと5秒以内にstartForegroundしないと落ちるカラクリ

(長くなりそうなので 後編 で説明します...

まとめ

① そもそもActivityManagerServiceは

UidRecord uidRec = mActiveUids.get(uid);
if (uidRec == null || uidRec.idle) {
    //バックグラウンドで動いてるやつだ!!

と認識してる。

② ActivityManagerServiceは、startService要求が来たときに、まずstartServiceForegroundかstartSeriviceかをチェックする。

③ startServiceだったら、アプリがフォアグラウンド動作しているかどうかを①の条件にしたがい判定し、バックグラウンド動作しているUIDからのサービス開始要求であれば、APP_START_MODE_DELAYED_RIGID(おまえは今すぐうごくんじゃない!)と判定して、コンポーネント名を ? に書き換える

④ ContextImpl側で、startServiceの返り値をみて、変なもの( !とか ?とか)だったら例外を投げる。

⑤ FATAL EXCEPTION \(^o^)/


ソースを自分で読み進めたい人は
http://androidxref.com/8.0.0_r4/xref/frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java#8400
このあたりから読むのがオススメです。

67
59
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
67
59

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?