Android
AndroidN
AndroidNougat

Android 7.0 Nougat マルチウインドウ周りのコード読んでみたメモ

More than 1 year has passed since last update.

概要

Android 7.0 Nougatのコードが出てきたのでもっと突っ込んで調べられるようになりました。
Anrdoid 7.0 Nougatで追加されたマルチウインドウについてもうちょっとつっこんで調べてみました。
間違っているところなどございましたらご指摘お願いします。

3行まとめ

タスクごとにマルチウインドウのフラグがある
dumpsysで確認できる
force_resizable_activitiesを有効にすることで全アプリでマルチウインドウ入れる

Activityがリサイズするかどうかのフラグ

AndroidManifestファイルから情報を取り出している部分をまず読んでみました。
AndroidManifestに書いてある結果以下の4つに分類されるようです。
RESIZE_MODE_RESIZEABLE
RESIZE_MODE_RESIZEABLE_AND_PIPABLE
RESIZE_MODE_UNRESIZEABLE
RESIZE_MODE_FORCE_RESIZEABLE

フラグの判定方法

https://android.googlesource.com/platform/frameworks/base/+/android-7.0.0_r1/core/java/android/content/pm/PackageParser.java#3533

a.info.screenOrientation = sa.getInt(
        R.styleable.AndroidManifestActivity_screenOrientation,
        SCREEN_ORIENTATION_UNSPECIFIED);
a.info.resizeMode = RESIZE_MODE_UNRESIZEABLE;
final boolean appDefault = (owner.applicationInfo.privateFlags
        & PRIVATE_FLAG_RESIZEABLE_ACTIVITIES) != 0;
// This flag is used to workaround the issue with ignored resizeableActivity param when
// either targetSdkVersion is not set at all or <uses-sdk> tag is below <application>
// tag in AndroidManifest. If this param was explicitly set to 'false' we need to set
// corresponding resizeMode regardless of targetSdkVersion value at this point in time.
final boolean resizeableSetExplicitly
        = sa.hasValue(R.styleable.AndroidManifestActivity_resizeableActivity);
final boolean resizeable = sa.getBoolean(
        R.styleable.AndroidManifestActivity_resizeableActivity, appDefault);
if (resizeable) {
    if (sa.getBoolean(R.styleable.AndroidManifestActivity_supportsPictureInPicture,
            false)) {
        a.info.resizeMode = RESIZE_MODE_RESIZEABLE_AND_PIPABLE;
    } else {
        a.info.resizeMode = RESIZE_MODE_RESIZEABLE;
    }
} else if (owner.applicationInfo.targetSdkVersion >= Build.VERSION_CODES.N
        || resizeableSetExplicitly) {
    a.info.resizeMode = RESIZE_MODE_UNRESIZEABLE;
} else if (!a.info.isFixedOrientation() && (a.info.flags & FLAG_IMMERSIVE) == 0) {
    a.info.resizeMode = RESIZE_MODE_FORCE_RESIZEABLE;
}

プログラムをそのままフローチャートにすると以下のように分類される

image

こんな感じになるはず、、

targetSdk > N resizableActivityが設定 resizableActivityがtrue 画面回転固定またはimmersive=true supportsPictureInPicture=true RESIZE_MODE
RESIZE_MODE_RESIZEABLE_AND_PIPABLE
RESIZE_MODE_RESIZEABLE
RESIZE_MODE_RESIZEABLE_AND_PIPABLE
RESIZE_MODE_UNRESIZEABLE
- RESIZE_MODE_RESIZEABLE_AND_PIPABLE
- RESIZE_MODE_UNRESIZEABLE
- RESIZE_MODE_FORCE_RESIZEABLE

アプリに設定されているRESIZABLE_MODEを見るには
adb shell dumpsys activity activities
または
adb shell dumpsys activity recents
を使って確認してください。

mResizeModeで確認できます。
以下のようにポケモンGOはRESIZE_MODE_UNRESIZEABLEになっています。

  * Recent #16: TaskRecord{d8f9253 #2030 A=com.nianticlabs.pokemongo U=0 StackId=1 sz=1}
    userId=0 effectiveUid=u0a88 mCallingUid=u0a31 mUserSetupComplete=true mCallingPackage=com.google.android.googlequicksearchbox
    affinity=com.nianticlabs.pokemongo
    intent={act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10200000 cmp=com.nianticlabs.pokemongo/com.unity3d.player.UnityPlayerNativeActivity bnds=[332,693][500,861]}
    realActivity=com.nianticlabs.pokemongo/com.unity3d.player.UnityPlayerNativeActivity
    autoRemoveRecents=false isPersistable=true numFullscreen=1 taskType=0 mTaskToReturnTo=1
    rootWasReset=true mNeverRelinquishIdentity=true mReuseTask=false mLockTaskAuth=LOCK_TASK_AUTH_PINNABLE
    Activities=[ActivityRecord{80d2402 u0 com.nianticlabs.pokemongo/com.unity3d.player.UnityPlayerNativeActivity t2030}]
    askedCompatMode=false inRecents=true isAvailable=true
    lastThumbnail=null lastThumbnailFile=/data/system_ce/0/recent_images/2030_task_thumbnail.png
    stackId=1
    hasBeenVisible=true mResizeMode=RESIZE_MODE_UNRESIZEABLE isResizeable=false firstActiveTime=1472365271355 lastActiveTime=1472365271355 (inactive for 9015s)

フラグのそれぞれの意味

    /**
     * Activity can not be resized and always occupies the fullscreen area with all windows fully
     * visible.
     * @hide
     */
    public static final int RESIZE_MODE_UNRESIZEABLE = 0;

RESIZE_MODE_UNRESIZEABLEは名前の通り画面全体を常に専有する模様

    /**
     * Activity can not be resized and always occupies the fullscreen area with all windows cropped
     * to either the task or stack bounds.
     * @hide
     */
    public static final int RESIZE_MODE_CROP_WINDOWS = 1;

RESIZE_MODE_CROP_WINDOWSは今は使っていないっぽい?です。croppedがどういう状態を示すのかよくわからないので、ここはパスします。

    /**
     * Activity is resizeable.
     * @hide
     */
    public static final int RESIZE_MODE_RESIZEABLE = 2;

RESIZE_MODE_RESIZEABLEはそのままですね

    /**
     * Activity is resizeable and supported picture-in-picture mode.
     * @hide
     */
    public static final int RESIZE_MODE_RESIZEABLE_AND_PIPABLE = 3;

PIPABLEでsupported picture-in-picture modeという意味みたいですね

    /**
     * Activity is does not support resizing, but we are forcing it to be resizeable.
     * @hide
     */
    public static final int RESIZE_MODE_FORCE_RESIZEABLE = 4;

RESIZE_MODE_FORCE_RESIZEABLEはActivityはRESIABLEじゃないけど、OSがRESIABLEを強制している場合です。
targetSdkが23以前で、画面回転非対応かimmersiveになっていないとなるようなときですね。

マルチウインドウに入るところの判定を見てみる

以下がオーバービュー画面に入るところを長押ししたときのリスナーとなっています。
この最初の判定は端末内であまり変わらないものっぽいので一旦無視します。

    private View.OnLongClickListener mRecentsLongClickListener = new View.OnLongClickListener() {

        @Override
        public boolean onLongClick(View v) {
            if (mRecents == null || !ActivityManager.supportsMultiWindow()
                    || !getComponent(Divider.class).getView().getSnapAlgorithm()
                            .isSplitScreenFeasible()) {
                return false;
            }

            toggleSplitScreenMode(MetricsEvent.ACTION_WINDOW_DOCK_LONGPRESS,
                    MetricsEvent.ACTION_WINDOW_UNDOCK_LONGPRESS);
            return true;
        }
    };

toggleSplitScreenModeです。
WindowManagerProxy.getInstance().getDockSide()でどこにDOCKされているかを取得しているようです。
DOCKされているというのはマルチウインドウで、画面分割されて、アプリが上に画面があれば、DOCKED_TOP、下にあればDOCKED_BOTTOMと言った感じです。
DOCKED_INVALIDで画面全体に広がっています。

    @Override
    protected void toggleSplitScreenMode(int metricsDockAction, int metricsUndockAction) {
        if (mRecents == null) {
            return;
        }
        int dockSide = WindowManagerProxy.getInstance().getDockSide();
        if (dockSide == WindowManager.DOCKED_INVALID) {
            // ** ここでマルチウインドウモードに入る ** 
            mRecents.dockTopTask(NavigationBarGestureHelper.DRAG_MODE_NONE,
                    ActivityManager.DOCKED_STACK_CREATE_MODE_TOP_OR_LEFT, null, metricsDockAction);
        } else {
            // ** ここでマルチウインドウモード解除 ** 
            EventBus.getDefault().send(new UndockingTaskEvent());
            if (metricsUndockAction != -1) {
                MetricsLogger.action(mContext, metricsUndockAction);
            }
        }
    }

次はマルチウインドウに入るdockTopTaskを見ていきましょう。

一部省略指定可のようになっており、runningTask.isDockableでなければ、"アプリで分割画面がサポートされていません"というToastを表示します。

    @Override
    public boolean dockTopTask(int dragMode, int stackCreateMode, Rect initialBounds,
            int metricsDockAction) {
        int currentUser = sSystemServicesProxy.getCurrentUser();
        SystemServicesProxy ssp = Recents.getSystemServices();
        ActivityManager.RunningTaskInfo runningTask = ssp.getRunningTask();
        boolean screenPinningActive = ssp.isScreenPinningActive();
        boolean isRunningTaskInHomeStack = runningTask != null &&
                SystemServicesProxy.isHomeStack(runningTask.stackId);
        if (runningTask != null && !isRunningTaskInHomeStack && !screenPinningActive) {
            if (runningTask.isDockable) {
                if (sSystemServicesProxy.isSystemUser(currentUser)) {
                    mImpl.dockTopTask(runningTask.id, dragMode, stackCreateMode, initialBounds);
                } else {
                    if (mSystemToUserCallbacks != null) {
                        IRecentsNonSystemUserCallbacks callbacks =
                                mSystemToUserCallbacks.getNonSystemUserRecentsForUser(currentUser);
                        if (callbacks != null) {
                            try {
                                callbacks.dockTopTask(runningTask.id, dragMode, stackCreateMode,
                                        initialBounds);
                            } catch (RemoteException e) {
                                Log.e(TAG, "Callback failed", e);
                            }
                        } else {
                            Log.e(TAG, "No SystemUI callbacks found for user: " + currentUser);
                        }
                    }
                }
                mDraggingInRecentsCurrentUser = currentUser;
                return true;
            } else {
                Toast.makeText(mContext, R.string.recents_incompatible_app_message,
                        Toast.LENGTH_SHORT).show();
                return false;
            }
        } else {
            return false;
        }
    }

ではrunningTask.isDockableというのはどうやって設定されているでしょうか。

ActivityStack.getTasksLockedから最終的にTaskを取得するようですが、以下のように作っているようです。

            RunningTaskInfo ci = new RunningTaskInfo();
            ci.id = task.taskId;
...
            ci.numRunning = numRunning;
            ci.isDockable = task.canGoInDockedStack();
            ci.resizeMode = task.mResizeMode;
            list.add(ci);

canGoInDockedStackは以下のようになっていて、まとめると以下のように判定されているようです。

Homeアプリでない場合 && (RESIZE_MODE_CROP_WINDOWS || RESIZE_MODE_RESIZEABLE || RESIZE_MODE_RESIZEABLE_AND_PIPABLE || RESIZE_MODE_FORCE_RESIZEABLE || ActivityManagerServiceでmForceResizableActivitiesになっている)

    boolean canGoInDockedStack() {
        return !isHomeActivity()
                && (isResizeableOrForced() || info.resizeMode == RESIZE_MODE_CROP_WINDOWS);
    }

    boolean isResizeableOrForced() {
        return !isHomeActivity() && (isResizeable() || service.mForceResizableActivities);
    }

    boolean isResizeable() {
        return !isHomeActivity() && ActivityInfo.isResizeableMode(info.resizeMode);
    }

ActivityInfo.isResizeableMode 

    public static boolean isResizeableMode(int mode) {
        return mode == RESIZE_MODE_RESIZEABLE
                || mode == RESIZE_MODE_RESIZEABLE_AND_PIPABLE
                || mode == RESIZE_MODE_FORCE_RESIZEABLE;
    }


これでフラグによってマルチウインドウに入るかどうかの判定が利用されていることが確認できました。

おまけ

ActivityManagerServiceでmForceResizableActivitiesってなんだろうって調べてみました。
db shell settings put global force_resizable_activities 1
をして再起動すると有効化できるみたいです。
あと他のブログから以下のようにするとfreeformモードにも入れようです。
adb shell settings put global enable_freeform_support 1
http://blog.fenrir-inc.com/jp/2016/07/android_n_multi_window.html

つまり、何ができるかというと、全部のアプリでマルチウインドウでフリーフォームできますね
Screenshot_20160828-205131.png