App Overlay Controlsの仕組み
App overlay controls Overview
Android12からシステムオーバーレイしているアプリを問答無用でWindowごと非表示することができるようになりました。
今までシステムオーバーレイはユーザーが許可さえあれば実行可能、且つオーバーレイされる側の3rd partyなアプリにはそれを制限する方法はありませんでした。
これにより、ユーザーの許可を得ているとはいえカード情報などの機密性の高い情報を扱う場合でもシステムオーバーレイを許容せざる得ませんでした。
この状況の改善などのためにオーバーレイウィンドウの表示制御をオーバーレイされる側のアプリからできるようにしたのが今回の機能です。
Behavior
サンプルはこちらです。
この機能を有効にするとをActivityにオーバーレイしているViewを消すことができます。
実装は非常にシンプルでManifestにHIDE_OVERLAY_WINDOWS
の権限を宣言し、
<uses-permission android:name="android.permission.HIDE_OVERLAY_WINDOWS"/>
Activityなどから取得したWindowに対して、setHideOverlayWindows()呼び出すのみです。
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に対して、setPrivateFlags
でSYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS
をセットしているのみです。
@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);
}
次にflagがセットされるとWindowはその変更を通知(dispatchWindowAttributesChanged
)をします。
private void setPrivateFlags(int flags, int mask) {
final WindowManager.LayoutParams attrs = getAttributes();
attrs.privateFlags = (attrs.privateFlags & ~mask) | (flags & mask);
dispatchWindowAttributesChanged(attrs);
}
Windown.Callback.onWindowAttributesChanged()
が呼び出されます。
protected void dispatchWindowAttributesChanged(WindowManager.LayoutParams attrs) {
if (mCallback != null) {
mCallback.onWindowAttributesChanged(attrs);
}
}
今回はActivityのWindowに対する操作なのでActivityが実装しているonWindowAttributesChanged
が呼びされます。
そのなかで WindowManger.updateViewLayout()
が呼び出されてLayoutが走ります
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);
}
}
}
}
WindowManger.updateViewLayout()
ではViewRootImpl
を取得し、もろもろのパラメーターがセットされます。
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);
}
}
すごく色々やってますが、最終的にscheduleTraversals()
が呼ばれるのでその先を見ていきます。
public void setLayoutParams(WindowManager.LayoutParams attrs, boolean newView) {
synchronized (this) {
...
mWindowAttributesChanged = true;
scheduleTraversals();
}
}
ここでは、RunnableのmTraversalRunnable
が実行されます。
実体はTraversalRunnable
クラスで doTraversal()
が非同期で実行されます。
doTraversal()
で処理の実体となるperformTraversals()
が呼び出されます。
void scheduleTraversals() {
...
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
...
}
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
void doTraversal() {
...
performTraversals();
...
}
performTraversals()
では色々な...とても色々なViewの調整が行われていますが...
WindowMangerService.relayoutWindow()
にViewのVisibillityを渡してその先でIWindowSessionを実装したSession.relayout()
を呼び出して処理をしています。
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);
...
}
SessionではWindowManagerService.relayoutWindow
へ処理を流しています。
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);
...
}
やっとでてきました...SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS
がセットされていること確認して updateNonSystemOverlayWindowsVisibilityIfNeeded()
を呼び出して、RootViewに紐づく全てのWindowを取り出し、WindowState.setForceHideNonSystemOverlayWindowIfNeeded()
を実行しています。
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 */);
}
setForceHideNonSystemOverlayWindowIfNeeded()
以降ではアニメーション(TRANSIT_EXIT
)を設定してWindowをhideにしています。
以上がSystem Overlay Windowを消すまでの処理です。
※Hideアニメーション処理自体は割愛させていただきます(Choreographer
などにもふれることになるので別の機会で)
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;
...
}
Discussion