• 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 
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.annotation.TestApi;
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.util.Preconditions;
41 
42 import dalvik.system.CloseGuard;
43 
44 import java.io.PrintWriter;
45 import java.lang.ref.WeakReference;
46 
47 /**
48  * Handle to a window used to display the augmented autofill UI.
49  *
50  * <p>The steps to create an augmented autofill UI are:
51  *
52  * <ol>
53  *   <li>Gets the {@link PresentationParams} from the {@link FillRequest}.
54  *   <li>Gets the {@link Area} to display the UI (for example, through
55  *   {@link PresentationParams#getSuggestionArea()}.
56  *   <li>Creates a {@link View} that must fit in the {@link Area#getBounds() area boundaries}.
57  *   <li>Set the proper listeners to the view (for example, a click listener that
58  *   triggers {@link FillController#autofill(java.util.List)}
59  *   <li>Call {@link #update(Area, View, long)} with these arguments.
60  *   <li>Create a {@link FillResponse} with the {@link FillWindow}.
61  *   <li>Pass such {@link FillResponse} to {@link FillCallback#onSuccess(FillResponse)}.
62  * </ol>
63  *
64  * @hide
65  */
66 @SystemApi
67 @TestApi
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         Preconditions.checkNotNull(area);
109         Preconditions.checkNotNull(area.proxy);
110         Preconditions.checkNotNull(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 (!mShowing) {
214                         mWm.addView(mFillView, p);
215                         mShowing = true;
216                     } else {
217                         mWm.updateViewLayout(mFillView, p);
218                     }
219                 } catch (WindowManager.BadTokenException e) {
220                     if (sDebug) Log.d(TAG, "Filed with token " + p.token + " gone.");
221                 } catch (IllegalStateException e) {
222                     if (sDebug) Log.d(TAG, "Exception showing window.");
223                 }
224             }
225         }
226     }
227 
handleHide()228     private void handleHide() {
229         if (sDebug) Log.d(TAG, "handleHide()");
230         synchronized (mLock) {
231             if (mWm != null && mFillView != null && mShowing) {
232                 try {
233                     mWm.removeView(mFillView);
234                     mShowing = false;
235                 } catch (IllegalStateException e) {
236                     if (sDebug) Log.d(TAG, "Exception hiding window.");
237                 }
238             }
239         }
240     }
241 
242     /**
243      * Destroys the window.
244      *
245      * <p>Once destroyed, this window cannot be used anymore
246      */
destroy()247     public void destroy() {
248         if (sDebug) {
249             Log.d(TAG,
250                     "destroy(): mDestroyed=" + mDestroyed + " mShowing=" + mShowing + " mFillView="
251                             + mFillView);
252         }
253         synchronized (mLock) {
254             if (mDestroyed) return;
255             if (mUpdateCalled) {
256                 mFillView.setOnClickListener(null);
257                 hide();
258                 mProxy.logEvent(AutofillProxy.REPORT_EVENT_UI_DESTROYED);
259             }
260             mDestroyed = true;
261             mCloseGuard.close();
262         }
263     }
264 
265     @Override
finalize()266     protected void finalize() throws Throwable {
267         try {
268             mCloseGuard.warnIfOpen();
269             destroy();
270         } finally {
271             super.finalize();
272         }
273     }
274 
checkNotDestroyedLocked()275     private void checkNotDestroyedLocked() {
276         if (mDestroyed) {
277             throw new IllegalStateException("already destroyed()");
278         }
279     }
280 
281     /** @hide */
dump(@onNull String prefix, @NonNull PrintWriter pw)282     public void dump(@NonNull String prefix, @NonNull PrintWriter pw) {
283         synchronized (this) {
284             pw.print(prefix); pw.print("destroyed: "); pw.println(mDestroyed);
285             pw.print(prefix); pw.print("updateCalled: "); pw.println(mUpdateCalled);
286             if (mFillView != null) {
287                 pw.print(prefix); pw.print("fill window: ");
288                 pw.println(mShowing ? "shown" : "hidden");
289                 pw.print(prefix); pw.print("fill view: ");
290                 pw.println(mFillView);
291                 pw.print(prefix); pw.print("mBounds: ");
292                 pw.println(mBounds);
293                 pw.print(prefix); pw.print("mWm: ");
294                 pw.println(mWm);
295             }
296         }
297     }
298 
299     /** @hide */
300     @Override
close()301     public void close() {
302         destroy();
303     }
304 
305     private static final class FillWindowPresenter extends IAutofillWindowPresenter.Stub {
306         private final @NonNull WeakReference<FillWindow> mFillWindowReference;
307 
FillWindowPresenter(@onNull FillWindow fillWindow)308         FillWindowPresenter(@NonNull FillWindow fillWindow) {
309             mFillWindowReference = new WeakReference<>(fillWindow);
310         }
311 
312         @Override
show(WindowManager.LayoutParams p, Rect transitionEpicenter, boolean fitsSystemWindows, int layoutDirection)313         public void show(WindowManager.LayoutParams p, Rect transitionEpicenter,
314                 boolean fitsSystemWindows, int layoutDirection) {
315             if (sDebug) Log.d(TAG, "FillWindowPresenter.show()");
316             final FillWindow fillWindow = mFillWindowReference.get();
317             if (fillWindow != null) {
318                 fillWindow.mUiThreadHandler.sendMessage(
319                         obtainMessage(FillWindow::handleShow, fillWindow, p));
320             }
321         }
322 
323         @Override
hide(Rect transitionEpicenter)324         public void hide(Rect transitionEpicenter) {
325             if (sDebug) Log.d(TAG, "FillWindowPresenter.hide()");
326             final FillWindow fillWindow = mFillWindowReference.get();
327             if (fillWindow != null) {
328                 fillWindow.mUiThreadHandler.sendMessage(
329                         obtainMessage(FillWindow::handleHide, fillWindow));
330             }
331         }
332     }
333 }
334