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