1 /*
2  * Copyright (C) 2021 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 android.inputmethodservice;
18 
19 import static android.view.WindowManager.LayoutParams;
20 import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
21 import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS;
22 import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
23 import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
24 
25 import android.annotation.NonNull;
26 import android.content.Context;
27 import android.os.IBinder;
28 import android.util.Slog;
29 import android.view.MotionEvent;
30 import android.view.View;
31 import android.view.ViewGroup;
32 import android.view.ViewRootImpl;
33 import android.view.ViewTreeObserver;
34 import android.view.WindowManager;
35 
36 import com.android.internal.policy.PhoneWindow;
37 
38 import java.util.Objects;
39 
40 /**
41  * Window of type {@code LayoutParams.TYPE_INPUT_METHOD_DIALOG} for drawing
42  * Handwriting Ink on screen.
43  * @hide
44  */
45 final class InkWindow extends PhoneWindow {
46 
47     private final WindowManager mWindowManager;
48     private boolean mIsViewAdded;
49     private View mInkView;
50     private InkVisibilityListener mInkViewVisibilityListener;
51     private ViewTreeObserver.OnGlobalLayoutListener mGlobalLayoutListener;
52 
InkWindow(@onNull Context context)53     public InkWindow(@NonNull Context context) {
54         super(context);
55 
56         setType(LayoutParams.TYPE_INPUT_METHOD);
57         final LayoutParams attrs = getAttributes();
58         attrs.layoutInDisplayCutoutMode = LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
59         attrs.setFitInsetsTypes(0);
60         // disable window animations.
61         // TODO(b/253477462): replace with API when available
62         attrs.windowAnimations = -1;
63         // TODO(b/210039666): use INPUT_FEATURE_NO_INPUT_CHANNEL once b/216179339 is fixed.
64         setAttributes(attrs);
65         // Ink window is not touchable with finger.
66         addFlags(FLAG_LAYOUT_IN_SCREEN | FLAG_LAYOUT_NO_LIMITS | FLAG_NOT_TOUCHABLE
67                 | FLAG_NOT_FOCUSABLE);
68         setBackgroundDrawableResource(android.R.color.transparent);
69         setLayout(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
70         mWindowManager = context.getSystemService(WindowManager.class);
71     }
72 
73     /**
74      * Initialize InkWindow if we only want to create and draw surface but not show it.
75      */
initOnly()76     void initOnly() {
77         show(true /* keepInvisible */);
78     }
79 
80     /**
81      * Method to show InkWindow on screen.
82      * Emulates internal behavior similar to Dialog.show().
83      */
show()84     void show() {
85         show(false /* keepInvisible */);
86     }
87 
show(boolean keepInvisible)88     private void show(boolean keepInvisible) {
89         if (getDecorView() == null) {
90             Slog.i(InputMethodService.TAG, "DecorView is not set for InkWindow. show() failed.");
91             return;
92         }
93         getDecorView().setVisibility(keepInvisible ? View.INVISIBLE : View.VISIBLE);
94         if (!mIsViewAdded) {
95             mWindowManager.addView(getDecorView(), getAttributes());
96             mIsViewAdded = true;
97         }
98     }
99 
100     /**
101      * Method to hide InkWindow from screen.
102      * Emulates internal behavior similar to Dialog.hide().
103      * @param remove set {@code true} to remove InkWindow surface completely.
104      */
hide(boolean remove)105     void hide(boolean remove) {
106         if (getDecorView() != null) {
107             if (remove) {
108                 mWindowManager.removeViewImmediate(getDecorView());
109             } else {
110                 getDecorView().setVisibility(View.INVISIBLE);
111             }
112         }
113     }
114 
setToken(@onNull IBinder token)115     void setToken(@NonNull IBinder token) {
116         WindowManager.LayoutParams lp = getAttributes();
117         lp.token = token;
118         setAttributes(lp);
119     }
120 
121     @Override
addContentView(View view, ViewGroup.LayoutParams params)122     public void addContentView(View view, ViewGroup.LayoutParams params) {
123         if (mInkView == null) {
124             mInkView = view;
125         } else if (mInkView != view) {
126             throw new IllegalStateException("Only one Child Inking view is permitted.");
127         }
128         super.addContentView(view, params);
129         initInkViewVisibilityListener();
130     }
131 
132     @Override
setContentView(View view, ViewGroup.LayoutParams params)133     public void setContentView(View view, ViewGroup.LayoutParams params) {
134         mInkView = view;
135         super.setContentView(view, params);
136         initInkViewVisibilityListener();
137     }
138 
139     @Override
setContentView(View view)140     public void setContentView(View view) {
141         mInkView = view;
142         super.setContentView(view);
143         initInkViewVisibilityListener();
144     }
145 
146     @Override
clearContentView()147     public void clearContentView() {
148         if (mGlobalLayoutListener != null && mInkView != null) {
149             mInkView.getViewTreeObserver().removeOnGlobalLayoutListener(mGlobalLayoutListener);
150         }
151         mGlobalLayoutListener = null;
152         mInkView = null;
153         super.clearContentView();
154     }
155 
156     /**
157     * Listener used by InkWindow to time the dispatching of {@link MotionEvent}s to Ink view, once
158     * it is visible to user.
159     */
160     interface InkVisibilityListener {
onInkViewVisible()161         void onInkViewVisible();
162     }
163 
setInkViewVisibilityListener(InkVisibilityListener listener)164     void setInkViewVisibilityListener(InkVisibilityListener listener) {
165         mInkViewVisibilityListener = listener;
166         initInkViewVisibilityListener();
167     }
168 
initInkViewVisibilityListener()169     void initInkViewVisibilityListener() {
170         if (mInkView == null || mInkViewVisibilityListener == null
171                 || mGlobalLayoutListener != null) {
172             return;
173         }
174         mGlobalLayoutListener = new ViewTreeObserver.OnGlobalLayoutListener() {
175             @Override
176             public void onGlobalLayout() {
177                 if (mInkView == null) {
178                     return;
179                 }
180                 if (mInkView.isVisibleToUser()) {
181                     if (mInkViewVisibilityListener != null) {
182                         mInkViewVisibilityListener.onInkViewVisible();
183                     }
184                     mInkView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
185                     mGlobalLayoutListener = null;
186                 }
187             }
188         };
189         mInkView.getViewTreeObserver().addOnGlobalLayoutListener(mGlobalLayoutListener);
190     }
191 
isInkViewVisible()192     boolean isInkViewVisible() {
193         return getDecorView().getVisibility() == View.VISIBLE
194                 && mInkView != null && mInkView.isVisibleToUser();
195     }
196 
dispatchHandwritingEvent(@onNull MotionEvent event)197     void dispatchHandwritingEvent(@NonNull MotionEvent event) {
198         final View decor = getDecorView();
199         Objects.requireNonNull(decor);
200         final ViewRootImpl viewRoot = decor.getViewRootImpl();
201         Objects.requireNonNull(viewRoot);
202         // The view root will own the event that we enqueue, so provide a copy of the event.
203         viewRoot.enqueueInputEvent(MotionEvent.obtain(event));
204     }
205 }
206