1 /*
2  * Copyright (C) 2020 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.view.inputmethod.cts.util;
18 
19 import static android.view.Display.DEFAULT_DISPLAY;
20 import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
21 import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
22 import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
23 import static android.view.WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
24 import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN;
25 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
26 
27 import android.app.Service;
28 import android.content.Context;
29 import android.content.Intent;
30 import android.graphics.Color;
31 import android.graphics.PixelFormat;
32 import android.graphics.Point;
33 import android.graphics.drawable.ColorDrawable;
34 import android.hardware.display.DisplayManager;
35 import android.os.Binder;
36 import android.os.Handler;
37 import android.os.HandlerThread;
38 import android.os.IBinder;
39 import android.util.Log;
40 import android.view.Display;
41 import android.view.MotionEvent;
42 import android.view.ViewTreeObserver;
43 import android.view.WindowManager;
44 import android.widget.EditText;
45 
46 import androidx.annotation.AnyThread;
47 import androidx.annotation.MainThread;
48 import androidx.annotation.Nullable;
49 import androidx.annotation.UiThread;
50 
51 import java.util.concurrent.CountDownLatch;
52 import java.util.concurrent.TimeUnit;
53 import java.util.concurrent.atomic.AtomicBoolean;
54 
55 /**
56  * A Service class to create popup window with edit text by handler thread.
57  * For verifying IME focus handle between windows on different UI thread.
58  */
59 public class WindowFocusHandleService extends Service {
60     private @Nullable static WindowFocusHandleService sInstance = null;
61     private static final long TIMEOUT = TimeUnit.SECONDS.toMillis(5);
62     private static final String TAG = WindowFocusHandleService.class.getSimpleName();
63 
64     private EditText mPopupTextView;
65     private Handler mThreadHandler;
66     private CountDownLatch mUiThreadSignal;
67 
68     @Override
onCreate()69     public void onCreate() {
70         super.onCreate();
71         // Create a popup text view with different UI thread.
72         final HandlerThread localThread = new HandlerThread("TestThreadHandler");
73         localThread.start();
74         mThreadHandler = new Handler(localThread.getLooper());
75         mThreadHandler.post(() -> mPopupTextView = createPopupTextView(new Point(150, 150)));
76     }
77 
getInstance()78     public @Nullable static WindowFocusHandleService getInstance() {
79         return sInstance;
80     }
81 
82     @Override
onBind(Intent intent)83     public IBinder onBind(Intent intent) {
84         sInstance = this;
85         return new Binder();
86     }
87 
88     @Override
onUnbind(Intent intent)89     public boolean onUnbind(Intent intent) {
90         sInstance = null;
91         return true;
92     }
93 
94     @UiThread
createPopupTextView(Point pos)95     private EditText createPopupTextView(Point pos) {
96         final Context windowContext = createWindowContext(DEFAULT_DISPLAY);
97         final WindowManager wm = windowContext.getSystemService(WindowManager.class);
98         final EditText editText = new EditText(windowContext) {
99             @Override
100             public void onWindowFocusChanged(boolean hasWindowFocus) {
101                 if (Log.isLoggable(TAG, Log.VERBOSE)) {
102                     Log.v(TAG, "onWindowFocusChanged for view=" + this
103                             + ", hasWindowfocus: " + hasWindowFocus);
104                 }
105             }
106 
107             @Override
108             public boolean onCheckIsTextEditor() {
109                 super.onCheckIsTextEditor();
110                 if (getHandler() != null && mUiThreadSignal != null) {
111                     if (Thread.currentThread().getId()
112                             == getHandler().getLooper().getThread().getId()) {
113                         mUiThreadSignal.countDown();
114                     }
115                 }
116                 return true;
117             }
118         };
119         editText.setOnFocusChangeListener((v, hasFocus) -> {
120             if (v == editText) {
121                 WindowManager.LayoutParams params =
122                         (WindowManager.LayoutParams) editText.getLayoutParams();
123                 if (hasFocus) {
124                     params.flags &= ~FLAG_NOT_FOCUSABLE;
125                     params.flags |= FLAG_LAYOUT_IN_SCREEN
126                                     | FLAG_WATCH_OUTSIDE_TOUCH
127                                     | FLAG_NOT_TOUCH_MODAL;
128                     wm.updateViewLayout(editText, params);
129                 }
130             }
131         });
132         editText.setOnTouchListener((v, event) -> {
133             if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
134                 WindowManager.LayoutParams params =
135                         (WindowManager.LayoutParams) editText.getLayoutParams();
136                 if ((params.flags & FLAG_NOT_FOCUSABLE) == 0) {
137                     params.flags |= FLAG_NOT_FOCUSABLE;
138                     wm.updateViewLayout(editText, params);
139                 }
140             }
141             return false;
142         });
143         editText.setBackground(new ColorDrawable(Color.CYAN));
144 
145         WindowManager.LayoutParams params = new WindowManager.LayoutParams(
146                 150, 150, pos.x, pos.y,
147                 TYPE_APPLICATION_OVERLAY, FLAG_NOT_FOCUSABLE,
148                 PixelFormat.OPAQUE);
149         // Currently SOFT_INPUT_STATE_UNSPECIFIED isn't appropriate for CTS test because there is no
150         // clear spec about how it behaves.  In order to make our tests deterministic, currently we
151         // must use SOFT_INPUT_STATE_HIDDEN to make sure soft-keyboard will hide after navigating
152         // forward to next window.
153         // TODO(Bug 77152727): Remove the following code once we define how
154         params.softInputMode = SOFT_INPUT_STATE_HIDDEN;
155         wm.addView(editText, params);
156         return editText;
157     }
158 
createWindowContext(int displayId)159     private Context createWindowContext(int displayId) {
160         final Display display = getSystemService(DisplayManager.class).getDisplay(displayId);
161         return createDisplayContext(display).createWindowContext(TYPE_APPLICATION_OVERLAY,
162                 null /* options */);
163     }
164 
165     @AnyThread
getPopupTextView( @ullable AtomicBoolean outPopupTextHasWindowFocusRef)166     public EditText getPopupTextView(
167             @Nullable AtomicBoolean outPopupTextHasWindowFocusRef) throws Exception {
168         if (outPopupTextHasWindowFocusRef != null) {
169             TestUtils.waitOnMainUntil(() -> mPopupTextView != null,
170                     TIMEOUT, "PopupTextView should be created");
171 
172             mPopupTextView.post(() -> {
173                 final ViewTreeObserver observerForPopupTextView =
174                         mPopupTextView.getViewTreeObserver();
175                 observerForPopupTextView.addOnWindowFocusChangeListener((hasFocus) ->
176                         outPopupTextHasWindowFocusRef.set(hasFocus));
177             });
178         }
179         return mPopupTextView;
180     }
181 
182     /**
183      * Tests can set a {@link CountDownLatch} to wait until associated action performed on
184      * UI thread.
185      *
186      * @param uiThreadSignal the {@link CountDownLatch} used to countdown.
187      */
188     @AnyThread
setUiThreadSignal(CountDownLatch uiThreadSignal)189     public void setUiThreadSignal(CountDownLatch uiThreadSignal) {
190         mUiThreadSignal = uiThreadSignal;
191     }
192 
193     @MainThread
handleReset()194     public void handleReset() {
195         if (mPopupTextView != null) {
196             final WindowManager wm = mPopupTextView.getContext().getSystemService(
197                     WindowManager.class);
198             wm.removeView(mPopupTextView);
199             mPopupTextView = null;
200         }
201     }
202 
203     @MainThread
onDestroy()204     public void onDestroy() {
205         super.onDestroy();
206         handleReset();
207     }
208 }
209