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