1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.clipboardoverlay;
18 
19 import android.annotation.MainThread;
20 import android.annotation.NonNull;
21 import android.app.ICompatCameraControlCallback;
22 import android.content.Context;
23 import android.content.res.Configuration;
24 import android.util.Log;
25 import android.view.View;
26 import android.view.ViewRootImpl;
27 import android.view.ViewTreeObserver;
28 import android.view.Window;
29 import android.view.WindowInsets;
30 import android.view.WindowManager;
31 
32 import com.android.internal.policy.PhoneWindow;
33 import com.android.systemui.clipboardoverlay.dagger.ClipboardOverlayModule.OverlayWindowContext;
34 import com.android.systemui.screenshot.FloatingWindowUtil;
35 
36 import java.util.function.BiConsumer;
37 
38 import javax.inject.Inject;
39 
40 /**
41  * Handles attaching the window and the window insets for the clipboard overlay.
42  */
43 public class ClipboardOverlayWindow extends PhoneWindow
44         implements ViewRootImpl.ActivityConfigCallback {
45     private static final String TAG = "ClipboardOverlayWindow";
46 
47     private final Context mContext;
48     private final WindowManager mWindowManager;
49     private final WindowManager.LayoutParams mWindowLayoutParams;
50 
51     private boolean mKeyboardVisible;
52     private final int mOrientation;
53     private BiConsumer<WindowInsets, Integer> mOnKeyboardChangeListener;
54     private Runnable mOnOrientationChangeListener;
55 
56     @Inject
ClipboardOverlayWindow(@verlayWindowContext Context context)57     ClipboardOverlayWindow(@OverlayWindowContext Context context) {
58         super(context);
59         mContext = context;
60         mOrientation = mContext.getResources().getConfiguration().orientation;
61 
62         // Setup the window that we are going to use
63         requestFeature(Window.FEATURE_NO_TITLE);
64         requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS);
65         setBackgroundDrawableResource(android.R.color.transparent);
66         mWindowManager = mContext.getSystemService(WindowManager.class);
67         mWindowLayoutParams = FloatingWindowUtil.getFloatingWindowParams();
68         mWindowLayoutParams.setTitle("ClipboardOverlay");
69         setWindowManager(mWindowManager, null, null);
70         setWindowFocusable(false);
71     }
72 
73     /**
74      * Set callbacks for keyboard state change and orientation change and attach the window
75      *
76      * @param onKeyboardChangeListener callback for IME visibility changes
77      * @param onOrientationChangeListener callback for device orientation changes
78      */
init(@onNull BiConsumer<WindowInsets, Integer> onKeyboardChangeListener, @NonNull Runnable onOrientationChangeListener)79     public void init(@NonNull BiConsumer<WindowInsets, Integer> onKeyboardChangeListener,
80             @NonNull Runnable onOrientationChangeListener) {
81         mOnKeyboardChangeListener = onKeyboardChangeListener;
82         mOnOrientationChangeListener = onOrientationChangeListener;
83 
84         attach();
85         withWindowAttached(() -> {
86             WindowInsets currentInsets = mWindowManager.getCurrentWindowMetrics().getWindowInsets();
87             mKeyboardVisible = currentInsets.isVisible(WindowInsets.Type.ime());
88             peekDecorView().getViewTreeObserver().addOnGlobalLayoutListener(() -> {
89                 WindowInsets insets = mWindowManager.getCurrentWindowMetrics().getWindowInsets();
90                 boolean keyboardVisible = insets.isVisible(WindowInsets.Type.ime());
91                 if (keyboardVisible != mKeyboardVisible) {
92                     mKeyboardVisible = keyboardVisible;
93                     mOnKeyboardChangeListener.accept(insets, mOrientation);
94                 }
95             });
96             peekDecorView().getViewRootImpl().setActivityConfigCallback(this);
97         });
98     }
99 
100     @Override // ViewRootImpl.ActivityConfigCallback
onConfigurationChanged(Configuration overrideConfig, int newDisplayId)101     public void onConfigurationChanged(Configuration overrideConfig, int newDisplayId) {
102         if (mContext.getResources().getConfiguration().orientation != mOrientation) {
103             mOnOrientationChangeListener.run();
104         }
105     }
106 
107     @Override // ViewRootImpl.ActivityConfigCallback
requestCompatCameraControl(boolean showControl, boolean transformationApplied, ICompatCameraControlCallback callback)108     public void requestCompatCameraControl(boolean showControl, boolean transformationApplied,
109             ICompatCameraControlCallback callback) {
110         Log.w(TAG, "unexpected requestCompatCameraControl call");
111     }
112 
remove()113     void remove() {
114         final View decorView = peekDecorView();
115         if (decorView != null && decorView.isAttachedToWindow()) {
116             mWindowManager.removeViewImmediate(decorView);
117         }
118     }
119 
getWindowInsets()120     WindowInsets getWindowInsets() {
121         return mWindowManager.getCurrentWindowMetrics().getWindowInsets();
122     }
123 
withWindowAttached(Runnable action)124     void withWindowAttached(Runnable action) {
125         View decorView = getDecorView();
126         if (decorView.isAttachedToWindow()) {
127             action.run();
128         } else {
129             decorView.getViewTreeObserver().addOnWindowAttachListener(
130                     new ViewTreeObserver.OnWindowAttachListener() {
131                         @Override
132                         public void onWindowAttached() {
133                             decorView.getViewTreeObserver().removeOnWindowAttachListener(this);
134                             action.run();
135                         }
136 
137                         @Override
138                         public void onWindowDetached() {
139                         }
140                     });
141         }
142     }
143 
144     @MainThread
attach()145     private void attach() {
146         View decorView = getDecorView();
147         if (decorView.isAttachedToWindow()) {
148             return;
149         }
150         mWindowManager.addView(decorView, mWindowLayoutParams);
151         decorView.requestApplyInsets();
152     }
153 
154     /**
155      * Updates the window focusability.  If the window is already showing, then it updates the
156      * window immediately, otherwise the layout params will be applied when the window is next
157      * shown.
158      */
setWindowFocusable(boolean focusable)159     private void setWindowFocusable(boolean focusable) {
160         int flags = mWindowLayoutParams.flags;
161         if (focusable) {
162             mWindowLayoutParams.flags &= ~WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
163         } else {
164             mWindowLayoutParams.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
165         }
166         if (mWindowLayoutParams.flags == flags) {
167             return;
168         }
169         final View decorView = peekDecorView();
170         if (decorView != null && decorView.isAttachedToWindow()) {
171             mWindowManager.updateViewLayout(decorView, mWindowLayoutParams);
172         }
173     }
174 }
175