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