• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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