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