📖

App Overlay Controlsの仕組み

2021/12/11に公開

App overlay controls Overview

Android12からシステムオーバーレイしているアプリを問答無用でWindowごと非表示することができるようになりました。

今までシステムオーバーレイはユーザーが許可さえあれば実行可能、且つオーバーレイされる側の3rd partyなアプリにはそれを制限する方法はありませんでした。
これにより、ユーザーの許可を得ているとはいえカード情報などの機密性の高い情報を扱う場合でもシステムオーバーレイを許容せざる得ませんでした。

この状況の改善などのためにオーバーレイウィンドウの表示制御をオーバーレイされる側のアプリからできるようにしたのが今回の機能です。
https://android-developers.googleblog.com/2021/03/android-12-developer-preview-2.html
https://developer.android.com/about/versions/12/features#hide-application-overlay-windows

Behavior

サンプルはこちらです。
この機能を有効にするとをActivityにオーバーレイしているViewを消すことができます。

実装は非常にシンプルでManifestにHIDE_OVERLAY_WINDOWSの権限を宣言し、

AndroidManifest.xml
<uses-permission android:name="android.permission.HIDE_OVERLAY_WINDOWS"/>

Activityなどから取得したWindowに対して、setHideOverlayWindows()呼び出すのみです。

Activity.kt
override fun onCreate(savedInstanceState: Bundle?) {
    window.setHideOverlayWindows(true)
}

ちなみにこの時、非表示にされた側のオーバーレイしているアプリは、自身が非表示になっていることを検知することができません(ViewのVisibilityなども変わりません)
表示されている時同じ様に振る舞います。
もし分かってしまったら機密性が高いコンテンツを表示していると教えているようなものなのでそれはそうか...という感じですね。

Proposal for app updates

この機能にオーバーレイしている側がのアプリはどうやって対応していけばよいかを簡単にコメントします。
主にユーザーにとっては改善となる機能ではあるので、非表示にされてしまうのを受け入れるしかないのですが何ができるのか?のみ記したいと思います。

For 3rd Party app

先にも述べましたが、この機能を利用して消された側は自身が消されたことを検知できません。
正直なす術がありません。
どうしてもこの制限を回避したい場合は変更差分の資料にもあるようにシステムオーバーレイからPinPやBubblesへ移行をしていくしかありません。

Apps that show windows of type TYPE_APPLICATION_OVERLAY
should consider alternatives that
may be more appropriate for their use case,
such as picture-in-picture or bubbles.

For privileged app

system windowの権限を付与すればこの制御の影響を受けなくなります。
android.permission.INTERNAL_SYSTEM_WINDOW

How the feature works

どのようにしてSystem Overlay消しているのかを見ていこうと思います。

始まりはここ。
やっていることは単純でWindowに対して、setPrivateFlagsSYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWSをセットしているのみです。

Window.setHideOverlayWindows()
    @RequiresPermission(HIDE_OVERLAY_WINDOWS)
    public final void setHideOverlayWindows(boolean hide) {
        // This permission check is here to throw early and let the developer know that they need
        // to hold HIDE_OVERLAY_WINDOWS for the flag to have any effect. The WM verifies that the
        // owner of the window has the permission before applying the flag, but this is done
        // asynchronously.
        if (mContext.checkSelfPermission(HIDE_NON_SYSTEM_OVERLAY_WINDOWS) != PERMISSION_GRANTED
                && mContext.checkSelfPermission(HIDE_OVERLAY_WINDOWS) != PERMISSION_GRANTED) {
            throw new SecurityException(
                    "Permission denial: setHideOverlayWindows: HIDE_OVERLAY_WINDOWS");
        }
        setPrivateFlags(hide ? SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS : 0,
                SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS);
    }

https://cs.android.com/android/platform/superproject/+/android-12.0.0_r4:frameworks/base/core/java/android/view/Window.java;l=1004;drc=551a988b6060040e0768d9afbdfdb3da9a9d967a

次にflagがセットされるとWindowはその変更を通知(dispatchWindowAttributesChanged)をします。

Window.setPrivateFlags()
    private void setPrivateFlags(int flags, int mask) {
        final WindowManager.LayoutParams attrs = getAttributes();
        attrs.privateFlags = (attrs.privateFlags & ~mask) | (flags & mask);
        dispatchWindowAttributesChanged(attrs);
    }

https://cs.android.com/android/platform/superproject/+/android-12.0.0_r4:frameworks/base/core/java/android/view/Window.java;drc=551a988b6060040e0768d9afbdfdb3da9a9d967a;l=1222

Windown.Callback.onWindowAttributesChanged()が呼び出されます。

Window.dispatchWindowAttributesChanged()
    protected void dispatchWindowAttributesChanged(WindowManager.LayoutParams attrs) {
        if (mCallback != null) {
            mCallback.onWindowAttributesChanged(attrs);
        }
    }

https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/view/Window.java;l=1231?q=dispatchWindowAttributesChanged&ss=android%2Fplatform%2Fsuperproject

今回はActivityのWindowに対する操作なのでActivityが実装しているonWindowAttributesChangedが呼びされます。
そのなかで WindowManger.updateViewLayout()が呼び出されてLayoutが走ります

Activity.onWindowAttributesChanged()
    public void onWindowAttributesChanged(WindowManager.LayoutParams params) {
        // Update window manager if: we have a view, that view is
        // attached to its parent (which will be a RootView), and
        // this activity is not embedded.
        if (mParent == null) {
            View decor = mDecor;
            if (decor != null && decor.getParent() != null) {
                getWindowManager().updateViewLayout(decor, params);
                if (mContentCaptureManager != null) {
                    mContentCaptureManager.updateWindowAttributes(params);
                }
            }
        }
    }

https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/Activity.java;l=4021?q=onWindowAttributesChanged&ss=android%2Fplatform%2Fsuperproject&start=11

WindowManger.updateViewLayout()ではViewRootImplを取得し、もろもろのパラメーターがセットされます。

WindowManagerGlobal.updateViewLayout()
    public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
        ...
        synchronized (mLock) {
            int index = findViewLocked(view, true);
            ViewRootImpl root = mRoots.get(index);
            mParams.remove(index);
            mParams.add(index, wparams);
            root.setLayoutParams(wparams, false);
        }
    }

https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/view/WindowManagerGlobal.java;l=410?q=WindowManagerGlobal.java &ss=android%2Fplatform%2Fsuperproject

すごく色々やってますが、最終的にscheduleTraversals()が呼ばれるのでその先を見ていきます。

ViewRootImpl.setLayoutParams()
    public void setLayoutParams(WindowManager.LayoutParams attrs, boolean newView) {
        synchronized (this) {
	...
	    mWindowAttributesChanged = true;
            scheduleTraversals();
        }
    }

https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/view/ViewRootImpl.java;l=1496?q=ViewRootImpl.java &ss=android%2Fplatform%2Fsuperproject

ここでは、RunnableのmTraversalRunnableが実行されます。
実体はTraversalRunnableクラスで doTraversal()が非同期で実行されます。
doTraversal()で処理の実体となるperformTraversals()が呼び出されます。

ViewRootImpl.scheduleTraversals()->doTraversal()
    void scheduleTraversals() {
        ...
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        ...
    }

    final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }
    
    void doTraversal() {
        ...
        performTraversals();
        ...
    }

https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/view/ViewRootImpl.java;l=2097;drc=master?q=scheduleTraversals&sq=&ss=android%2Fplatform%2Fsuperproject

performTraversals()では色々な...とても色々なViewの調整が行われていますが...
WindowMangerService.relayoutWindow()にViewのVisibillityを渡してその先でIWindowSessionを実装したSession.relayout()を呼び出して処理をしています。

ViewRootImpl.performTraversals()->relayoutWindow()
    private void performTraversals() {
        ...
            relayoutResult = relayoutWindow(params, viewVisibility, insetsPending);
	...
    }
    
    private int relayoutWindow(WindowManager.LayoutParams params, int viewVisibility,
            boolean insetsPending) throws RemoteException {
        ...
        int relayoutResult = mWindowSession.relayout(mWindow, params,
                (int) (mView.getMeasuredWidth() * appScale + 0.5f),
                (int) (mView.getMeasuredHeight() * appScale + 0.5f), viewVisibility,
                insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0, frameNumber,
                mTmpFrames, mPendingMergedConfiguration, mSurfaceControl, mTempInsets,
                mTempControls, mSurfaceSize);
	...
    }

https://cs.android.com/android/platform/superproject/+/android-12.0.0_r4:frameworks/base/core/java/android/view/ViewRootImpl.java;drc=android-12.0.0_r4;bpv=1;bpt=1;l=2510

SessionではWindowManagerService.relayoutWindowへ処理を流しています。

Session.relayout
    public int relayout(IWindow window, WindowManager.LayoutParams attrs,
            int requestedWidth, int requestedHeight, int viewFlags, int flags, long frameNumber,
            ClientWindowFrames outFrames, MergedConfiguration mergedConfiguration,
            SurfaceControl outSurfaceControl, InsetsState outInsetsState,
            InsetsSourceControl[] outActiveControls, Point outSurfaceSize) {
        ...
        int res = mService.relayoutWindow(this, window, attrs,
                requestedWidth, requestedHeight, viewFlags, flags, frameNumber,
                outFrames, mergedConfiguration, outSurfaceControl, outInsetsState,
                outActiveControls, outSurfaceSize);
		...
    }

https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/Session.java;l=227?q=Session window&ss=android%2Fplatform%2Fsuperproject

やっとでてきました...SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWSがセットされていること確認して updateNonSystemOverlayWindowsVisibilityIfNeeded()を呼び出して、RootViewに紐づく全てのWindowを取り出し、WindowState.setForceHideNonSystemOverlayWindowIfNeeded()を実行しています。

WindowManager.relayoutWindow() -> updateNonSystemOverlayWindowsVisibilityIfNeeded()
   public int relayoutWindow(...){
        ...
                if ((privateFlagChanges & SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS) != 0) {
                    updateNonSystemOverlayWindowsVisibilityIfNeeded(
                            win, win.mWinAnimator.getShown());
                }
       ...
   }
   
   void updateNonSystemOverlayWindowsVisibilityIfNeeded(WindowState win, boolean surfaceShown) {
       ...
       mRoot.forAllWindows((w) -> {
            w.setForceHideNonSystemOverlayWindowIfNeeded(hideSystemAlertWindows);
        }, false /* traverseTopToBottom */);
    }

https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/WindowManagerService.java;l=2215?q=WindowManagerService&ss=android%2Fplatform%2Fsuperproject

setForceHideNonSystemOverlayWindowIfNeeded()以降ではアニメーション(TRANSIT_EXIT)を設定してWindowをhideにしています。
以上がSystem Overlay Windowを消すまでの処理です。
※Hideアニメーション処理自体は割愛させていただきます(Choreographerなどにもふれることになるので別の機会で)

WindowManager.setForceHideNonSystemOverlayWindowIfNeeded() -> hide()
    void setForceHideNonSystemOverlayWindowIfNeeded(boolean forceHide) {
        ...
        if (forceHide) {
            hide(true /* doAnimation */, true /* requestAnim */);
        } else {
            show(true /* doAnimation */, true /* requestAnim */);
        }
    }
    
    /** Forces the window to be hidden, regardless of whether the client like it shown. */
    boolean hide(boolean doAnimation, boolean requestAnim) {
        ...
        if (doAnimation) {
            mWinAnimator.applyAnimationLocked(TRANSIT_EXIT, false);
            if (!isAnimating(TRANSITION | PARENTS)) {
                doAnimation = false;
            }
        }
        mLegacyPolicyVisibilityAfterAnim = false;
	...
    }

https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/wm/WindowState.java;l=3271?q=setForceHideNonSystemOverlayWindowIfNeeded&ss=android%2Fplatform%2Fsuperproject

Discussion