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