1 /* 2 * Copyright (C) 2018 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 package android.service.autofill.augmented; 17 18 import static android.service.autofill.augmented.AugmentedAutofillService.sDebug; 19 import static android.service.autofill.augmented.AugmentedAutofillService.sVerbose; 20 21 import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; 22 23 import android.annotation.NonNull; 24 import android.annotation.SystemApi; 25 import android.annotation.TestApi; 26 import android.graphics.Rect; 27 import android.os.Handler; 28 import android.os.Looper; 29 import android.os.RemoteException; 30 import android.service.autofill.augmented.AugmentedAutofillService.AutofillProxy; 31 import android.service.autofill.augmented.PresentationParams.Area; 32 import android.util.Log; 33 import android.view.MotionEvent; 34 import android.view.View; 35 import android.view.WindowManager; 36 import android.view.autofill.IAutofillWindowPresenter; 37 38 import com.android.internal.annotations.GuardedBy; 39 import com.android.internal.util.Preconditions; 40 41 import dalvik.system.CloseGuard; 42 43 import java.io.PrintWriter; 44 45 /** 46 * Handle to a window used to display the augmented autofill UI. 47 * 48 * <p>The steps to create an augmented autofill UI are: 49 * 50 * <ol> 51 * <li>Gets the {@link PresentationParams} from the {@link FillRequest}. 52 * <li>Gets the {@link Area} to display the UI (for example, through 53 * {@link PresentationParams#getSuggestionArea()}. 54 * <li>Creates a {@link View} that must fit in the {@link Area#getBounds() area boundaries}. 55 * <li>Set the proper listeners to the view (for example, a click listener that 56 * triggers {@link FillController#autofill(java.util.List)} 57 * <li>Call {@link #update(Area, View, long)} with these arguments. 58 * <li>Create a {@link FillResponse} with the {@link FillWindow}. 59 * <li>Pass such {@link FillResponse} to {@link FillCallback#onSuccess(FillResponse)}. 60 * </ol> 61 * 62 * @hide 63 */ 64 @SystemApi 65 @TestApi 66 public final class FillWindow implements AutoCloseable { 67 private static final String TAG = FillWindow.class.getSimpleName(); 68 69 private final Object mLock = new Object(); 70 private final CloseGuard mCloseGuard = CloseGuard.get(); 71 72 private final @NonNull Handler mUiThreadHandler = new Handler(Looper.getMainLooper()); 73 private final @NonNull FillWindowPresenter mFillWindowPresenter = new FillWindowPresenter(); 74 75 @GuardedBy("mLock") 76 private WindowManager mWm; 77 @GuardedBy("mLock") 78 private View mFillView; 79 @GuardedBy("mLock") 80 private boolean mShowing; 81 @GuardedBy("mLock") 82 private Rect mBounds; 83 84 @GuardedBy("mLock") 85 private boolean mUpdateCalled; 86 @GuardedBy("mLock") 87 private boolean mDestroyed; 88 89 private AutofillProxy mProxy; 90 91 /** 92 * Updates the content of the window. 93 * 94 * @param rootView new root view 95 * @param area coordinates to render the view. 96 * @param flags currently not used. 97 * 98 * @return boolean whether the window was updated or not. 99 * 100 * @throws IllegalArgumentException if the area is not compatible with this window 101 */ update(@onNull Area area, @NonNull View rootView, long flags)102 public boolean update(@NonNull Area area, @NonNull View rootView, long flags) { 103 if (sDebug) { 104 Log.d(TAG, "Updating " + area + " + with " + rootView); 105 } 106 // TODO(b/123100712): add test case for null 107 Preconditions.checkNotNull(area); 108 Preconditions.checkNotNull(area.proxy); 109 Preconditions.checkNotNull(rootView); 110 // TODO(b/123100712): must check the area is a valid object returned by 111 // SmartSuggestionParams, throw IAE if not 112 113 final PresentationParams smartSuggestion = area.proxy.getSmartSuggestionParams(); 114 if (smartSuggestion == null) { 115 Log.w(TAG, "No SmartSuggestionParams"); 116 return false; 117 } 118 119 final Rect rect = area.getBounds(); 120 if (rect == null) { 121 Log.wtf(TAG, "No Rect on SmartSuggestionParams"); 122 return false; 123 } 124 125 synchronized (mLock) { 126 checkNotDestroyedLocked(); 127 128 mProxy = area.proxy; 129 130 // TODO(b/123227534): once we have the SurfaceControl approach, we should update the 131 // window instead of destroying. In fact, it might be better to allocate a full window 132 // initially, which is transparent (and let touches get through) everywhere but in the 133 // rect boundaries. 134 135 // TODO(b/123099468): make sure all touch events are handled, window is always closed, 136 // etc. 137 138 mWm = rootView.getContext().getSystemService(WindowManager.class); 139 mFillView = rootView; 140 // Listen to the touch outside to destroy the window when typing is detected. 141 mFillView.setOnTouchListener( 142 (view, motionEvent) -> { 143 if (motionEvent.getAction() == MotionEvent.ACTION_OUTSIDE) { 144 if (sVerbose) Log.v(TAG, "Outside touch detected, hiding the window"); 145 hide(); 146 } 147 return false; 148 } 149 ); 150 mShowing = false; 151 mBounds = new Rect(area.getBounds()); 152 if (sDebug) { 153 Log.d(TAG, "Created FillWindow: params= " + smartSuggestion + " view=" + rootView); 154 } 155 mUpdateCalled = true; 156 mDestroyed = false; 157 mProxy.setFillWindow(this); 158 return true; 159 } 160 } 161 162 /** @hide */ show()163 void show() { 164 // TODO(b/123100712): check if updated first / throw exception 165 if (sDebug) Log.d(TAG, "show()"); 166 synchronized (mLock) { 167 checkNotDestroyedLocked(); 168 if (mWm == null || mFillView == null) { 169 throw new IllegalStateException("update() not called yet, or already destroyed()"); 170 } 171 if (mProxy != null) { 172 try { 173 mProxy.requestShowFillUi(mBounds.right - mBounds.left, 174 mBounds.bottom - mBounds.top, 175 /*anchorBounds=*/ null, mFillWindowPresenter); 176 } catch (RemoteException e) { 177 Log.w(TAG, "Error requesting to show fill window", e); 178 } 179 mProxy.report(AutofillProxy.REPORT_EVENT_UI_SHOWN); 180 } 181 } 182 } 183 184 /** 185 * Hides the window. 186 * 187 * <p>The window is not destroyed and can be shown again 188 */ hide()189 private void hide() { 190 if (sDebug) Log.d(TAG, "hide()"); 191 synchronized (mLock) { 192 checkNotDestroyedLocked(); 193 if (mWm == null || mFillView == null) { 194 throw new IllegalStateException("update() not called yet, or already destroyed()"); 195 } 196 if (mProxy != null && mShowing) { 197 try { 198 mProxy.requestHideFillUi(); 199 } catch (RemoteException e) { 200 Log.w(TAG, "Error requesting to hide fill window", e); 201 } 202 } 203 } 204 } 205 handleShow(WindowManager.LayoutParams p)206 private void handleShow(WindowManager.LayoutParams p) { 207 if (sDebug) Log.d(TAG, "handleShow()"); 208 synchronized (mLock) { 209 if (mWm != null && mFillView != null) { 210 p.flags |= WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH; 211 if (!mShowing) { 212 mWm.addView(mFillView, p); 213 mShowing = true; 214 } else { 215 mWm.updateViewLayout(mFillView, p); 216 } 217 } 218 } 219 } 220 handleHide()221 private void handleHide() { 222 if (sDebug) Log.d(TAG, "handleHide()"); 223 synchronized (mLock) { 224 if (mWm != null && mFillView != null && mShowing) { 225 mWm.removeView(mFillView); 226 mShowing = false; 227 } 228 } 229 } 230 231 /** 232 * Destroys the window. 233 * 234 * <p>Once destroyed, this window cannot be used anymore 235 */ destroy()236 public void destroy() { 237 if (sDebug) { 238 Log.d(TAG, 239 "destroy(): mDestroyed=" + mDestroyed + " mShowing=" + mShowing + " mFillView=" 240 + mFillView); 241 } 242 synchronized (mLock) { 243 if (mDestroyed) return; 244 if (mUpdateCalled) { 245 hide(); 246 mProxy.report(AutofillProxy.REPORT_EVENT_UI_DESTROYED); 247 } 248 mDestroyed = true; 249 mCloseGuard.close(); 250 } 251 } 252 253 @Override finalize()254 protected void finalize() throws Throwable { 255 try { 256 if (mCloseGuard != null) { 257 mCloseGuard.warnIfOpen(); 258 } 259 destroy(); 260 } finally { 261 super.finalize(); 262 } 263 } 264 checkNotDestroyedLocked()265 private void checkNotDestroyedLocked() { 266 if (mDestroyed) { 267 throw new IllegalStateException("already destroyed()"); 268 } 269 } 270 271 /** @hide */ dump(@onNull String prefix, @NonNull PrintWriter pw)272 public void dump(@NonNull String prefix, @NonNull PrintWriter pw) { 273 synchronized (this) { 274 pw.print(prefix); pw.print("destroyed: "); pw.println(mDestroyed); 275 pw.print(prefix); pw.print("updateCalled: "); pw.println(mUpdateCalled); 276 if (mFillView != null) { 277 pw.print(prefix); pw.print("fill window: "); 278 pw.println(mShowing ? "shown" : "hidden"); 279 pw.print(prefix); pw.print("fill view: "); 280 pw.println(mFillView); 281 pw.print(prefix); pw.print("mBounds: "); 282 pw.println(mBounds); 283 pw.print(prefix); pw.print("mWm: "); 284 pw.println(mWm); 285 } 286 } 287 } 288 289 /** @hide */ 290 @Override close()291 public void close() throws Exception { 292 destroy(); 293 } 294 295 private final class FillWindowPresenter extends IAutofillWindowPresenter.Stub { 296 @Override show(WindowManager.LayoutParams p, Rect transitionEpicenter, boolean fitsSystemWindows, int layoutDirection)297 public void show(WindowManager.LayoutParams p, Rect transitionEpicenter, 298 boolean fitsSystemWindows, int layoutDirection) { 299 if (sDebug) Log.d(TAG, "FillWindowPresenter.show()"); 300 mUiThreadHandler.sendMessage(obtainMessage(FillWindow::handleShow, FillWindow.this, p)); 301 } 302 303 @Override hide(Rect transitionEpicenter)304 public void hide(Rect transitionEpicenter) { 305 if (sDebug) Log.d(TAG, "FillWindowPresenter.hide()"); 306 mUiThreadHandler.sendMessage(obtainMessage(FillWindow::handleHide, FillWindow.this)); 307 } 308 } 309 } 310