1 /*
2  * Copyright (C) 2020 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;
17 
18 import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
19 
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.annotation.SystemApi;
23 import android.app.Service;
24 import android.content.Intent;
25 import android.content.IntentSender;
26 import android.graphics.PixelFormat;
27 import android.os.BaseBundle;
28 import android.os.Bundle;
29 import android.os.Handler;
30 import android.os.IBinder;
31 import android.os.Looper;
32 import android.os.RemoteCallback;
33 import android.os.RemoteException;
34 import android.util.Log;
35 import android.util.LruCache;
36 import android.util.Size;
37 import android.view.Display;
38 import android.view.SurfaceControlViewHost;
39 import android.view.View;
40 import android.view.ViewGroup;
41 import android.view.WindowManager;
42 
43 import java.io.FileDescriptor;
44 import java.io.PrintWriter;
45 import java.lang.ref.WeakReference;
46 
47 /**
48  * A service that renders an inline presentation view given the {@link InlinePresentation}.
49  *
50  * {@hide}
51  */
52 @SystemApi
53 public abstract class InlineSuggestionRenderService extends Service {
54 
55     private static final String TAG = "InlineSuggestionRenderService";
56 
57     /**
58      * The {@link Intent} that must be declared as handled by the service.
59      *
60      * <p>To be supported, the service must also require the
61      * {@link android.Manifest.permission#BIND_INLINE_SUGGESTION_RENDER_SERVICE} permission so that
62      * other applications can not abuse it.
63      */
64     public static final String SERVICE_INTERFACE =
65             "android.service.autofill.InlineSuggestionRenderService";
66 
67     private final Handler mMainHandler = new Handler(Looper.getMainLooper(), null, true);
68 
69     private IInlineSuggestionUiCallback mCallback;
70 
71 
72     /**
73      * A local LRU cache keeping references to the inflated {@link SurfaceControlViewHost}s, so
74      * they can be released properly when no longer used. Each view needs to be tracked separately,
75      * therefore for simplicity we use the hash code of the value object as key in the cache.
76      */
77     private final LruCache<InlineSuggestionUiImpl, Boolean> mActiveInlineSuggestions =
78             new LruCache<InlineSuggestionUiImpl, Boolean>(30) {
79                 @Override
80                 public void entryRemoved(boolean evicted, InlineSuggestionUiImpl key,
81                         Boolean oldValue,
82                         Boolean newValue) {
83                     if (evicted) {
84                         Log.w(TAG,
85                                 "Hit max=30 entries in the cache. Releasing oldest one to make "
86                                         + "space.");
87                         key.releaseSurfaceControlViewHost();
88                     }
89                 }
90             };
91 
92     /**
93      * If the specified {@code width}/{@code height} is an exact value, then it will be returned as
94      * is, otherwise the method tries to measure a size that is just large enough to fit the view
95      * content, within constraints posed by {@code minSize} and {@code maxSize}.
96      *
97      * @param view    the view for which we measure the size
98      * @param width   the expected width of the view, either an exact value or {@link
99      *                ViewGroup.LayoutParams#WRAP_CONTENT}
100      * @param height  the expected width of the view, either an exact value or {@link
101      *                ViewGroup.LayoutParams#WRAP_CONTENT}
102      * @param minSize the lower bound of the size to be returned
103      * @param maxSize the upper bound of the size to be returned
104      * @return the measured size of the view based on the given size constraints.
105      */
measuredSize(@onNull View view, int width, int height, @NonNull Size minSize, @NonNull Size maxSize)106     private Size measuredSize(@NonNull View view, int width, int height, @NonNull Size minSize,
107             @NonNull Size maxSize) {
108         if (width != ViewGroup.LayoutParams.WRAP_CONTENT
109                 && height != ViewGroup.LayoutParams.WRAP_CONTENT) {
110             return new Size(width, height);
111         }
112         int widthMeasureSpec;
113         if (width == ViewGroup.LayoutParams.WRAP_CONTENT) {
114             widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(maxSize.getWidth(),
115                     View.MeasureSpec.AT_MOST);
116         } else {
117             widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY);
118         }
119         int heightMeasureSpec;
120         if (height == ViewGroup.LayoutParams.WRAP_CONTENT) {
121             heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(maxSize.getHeight(),
122                     View.MeasureSpec.AT_MOST);
123         } else {
124             heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY);
125         }
126         view.measure(widthMeasureSpec, heightMeasureSpec);
127         return new Size(Math.max(view.getMeasuredWidth(), minSize.getWidth()),
128                 Math.max(view.getMeasuredHeight(), minSize.getHeight()));
129     }
130 
handleRenderSuggestion(IInlineSuggestionUiCallback callback, InlinePresentation presentation, int width, int height, IBinder hostInputToken, int displayId, int userId, int sessionId)131     private void handleRenderSuggestion(IInlineSuggestionUiCallback callback,
132             InlinePresentation presentation, int width, int height, IBinder hostInputToken,
133             int displayId, int userId, int sessionId) {
134         if (hostInputToken == null) {
135             try {
136                 callback.onError();
137             } catch (RemoteException e) {
138                 Log.w(TAG, "RemoteException calling onError()");
139             }
140             return;
141         }
142 
143         // When we create the UI it should be for the IME display
144         updateDisplay(displayId);
145         try {
146             final View suggestionView = onRenderSuggestion(presentation, width, height);
147             if (suggestionView == null) {
148                 Log.w(TAG, "ExtServices failed to render the inline suggestion view.");
149                 try {
150                     callback.onError();
151                 } catch (RemoteException e) {
152                     Log.w(TAG, "Null suggestion view returned by renderer");
153                 }
154                 return;
155             }
156             mCallback = callback;
157             final Size measuredSize = measuredSize(suggestionView, width, height,
158                     presentation.getInlinePresentationSpec().getMinSize(),
159                     presentation.getInlinePresentationSpec().getMaxSize());
160             Log.v(TAG, "width=" + width + ", height=" + height + ", measuredSize=" + measuredSize);
161 
162             final InlineSuggestionRoot suggestionRoot = new InlineSuggestionRoot(this, callback);
163             suggestionRoot.addView(suggestionView);
164             WindowManager.LayoutParams lp = new WindowManager.LayoutParams(measuredSize.getWidth(),
165                     measuredSize.getHeight(), WindowManager.LayoutParams.TYPE_APPLICATION, 0,
166                     PixelFormat.TRANSPARENT);
167 
168             final SurfaceControlViewHost host = new SurfaceControlViewHost(this, getDisplay(),
169                     hostInputToken, "InlineSuggestionRenderService");
170             host.setView(suggestionRoot, lp);
171 
172             // Set the suggestion view to be non-focusable so that if its background is set to a
173             // ripple drawable, the ripple won't be shown initially.
174             suggestionView.setFocusable(false);
175             suggestionView.setOnClickListener((v) -> {
176                 try {
177                     callback.onClick();
178                 } catch (RemoteException e) {
179                     Log.w(TAG, "RemoteException calling onClick()");
180                 }
181             });
182             final View.OnLongClickListener onLongClickListener =
183                     suggestionView.getOnLongClickListener();
184             suggestionView.setOnLongClickListener((v) -> {
185                 if (onLongClickListener != null) {
186                     onLongClickListener.onLongClick(v);
187                 }
188                 try {
189                     callback.onLongClick();
190                 } catch (RemoteException e) {
191                     Log.w(TAG, "RemoteException calling onLongClick()");
192                 }
193                 return true;
194             });
195             final InlineSuggestionUiImpl uiImpl = new InlineSuggestionUiImpl(host, mMainHandler,
196                     userId, sessionId);
197             mActiveInlineSuggestions.put(uiImpl, true);
198 
199             // We post the callback invocation to the end of the main thread handler queue, to make
200             // sure the callback happens after the views are drawn. This is needed because calling
201             // {@link SurfaceControlViewHost#setView()} will post a task to the main thread
202             // to draw the view asynchronously.
203             mMainHandler.post(() -> {
204                 try {
205                     callback.onContent(new InlineSuggestionUiWrapper(uiImpl),
206                             host.getSurfacePackage(),
207                             measuredSize.getWidth(), measuredSize.getHeight());
208                 } catch (RemoteException e) {
209                     Log.w(TAG, "RemoteException calling onContent()");
210                 }
211             });
212         } finally {
213             updateDisplay(Display.DEFAULT_DISPLAY);
214         }
215     }
216 
handleGetInlineSuggestionsRendererInfo(@onNull RemoteCallback callback)217     private void handleGetInlineSuggestionsRendererInfo(@NonNull RemoteCallback callback) {
218         final Bundle rendererInfo = onGetInlineSuggestionsRendererInfo();
219         callback.sendResult(rendererInfo);
220     }
221 
handleDestroySuggestionViews(int userId, int sessionId)222     private void handleDestroySuggestionViews(int userId, int sessionId) {
223         Log.v(TAG, "handleDestroySuggestionViews called for " + userId + ":" + sessionId);
224         for (final InlineSuggestionUiImpl inlineSuggestionUi :
225                 mActiveInlineSuggestions.snapshot().keySet()) {
226             if (inlineSuggestionUi.mUserId == userId
227                     && inlineSuggestionUi.mSessionId == sessionId) {
228                 Log.v(TAG, "Destroy " + inlineSuggestionUi);
229                 inlineSuggestionUi.releaseSurfaceControlViewHost();
230             }
231         }
232     }
233 
234     /**
235      * A wrapper class around the {@link InlineSuggestionUiImpl} to ensure it's not strongly
236      * reference by the remote system server process.
237      */
238     private static final class InlineSuggestionUiWrapper extends
239             android.service.autofill.IInlineSuggestionUi.Stub {
240 
241         private final WeakReference<InlineSuggestionUiImpl> mUiImpl;
242 
InlineSuggestionUiWrapper(InlineSuggestionUiImpl uiImpl)243         InlineSuggestionUiWrapper(InlineSuggestionUiImpl uiImpl) {
244             mUiImpl = new WeakReference<>(uiImpl);
245         }
246 
247         @Override
releaseSurfaceControlViewHost()248         public void releaseSurfaceControlViewHost() {
249             final InlineSuggestionUiImpl uiImpl = mUiImpl.get();
250             if (uiImpl != null) {
251                 uiImpl.releaseSurfaceControlViewHost();
252             }
253         }
254 
255         @Override
getSurfacePackage(ISurfacePackageResultCallback callback)256         public void getSurfacePackage(ISurfacePackageResultCallback callback) {
257             final InlineSuggestionUiImpl uiImpl = mUiImpl.get();
258             if (uiImpl != null) {
259                 uiImpl.getSurfacePackage(callback);
260             }
261         }
262     }
263 
264     /**
265      * Keeps track of a SurfaceControlViewHost to ensure it's released when its lifecycle ends.
266      *
267      * <p>This class is thread safe, because all the outside calls are piped into a single
268      *  handler thread to be processed.
269      */
270     private final class InlineSuggestionUiImpl {
271 
272         @Nullable
273         private SurfaceControlViewHost mViewHost;
274         @NonNull
275         private final Handler mHandler;
276         private final int mUserId;
277         private final int mSessionId;
278 
InlineSuggestionUiImpl(SurfaceControlViewHost viewHost, Handler handler, int userId, int sessionId)279         InlineSuggestionUiImpl(SurfaceControlViewHost viewHost, Handler handler, int userId,
280                 int sessionId) {
281             this.mViewHost = viewHost;
282             this.mHandler = handler;
283             this.mUserId = userId;
284             this.mSessionId = sessionId;
285         }
286 
287         /**
288          * Call {@link SurfaceControlViewHost#release()} to release it. After this, this view is
289          * not usable, and any further calls to the
290          * {@link #getSurfacePackage(ISurfacePackageResultCallback)} will get {@code null} result.
291          */
releaseSurfaceControlViewHost()292         public void releaseSurfaceControlViewHost() {
293             mHandler.post(() -> {
294                 if (mViewHost == null) {
295                     return;
296                 }
297                 Log.v(TAG, "Releasing inline suggestion view host");
298                 mViewHost.release();
299                 mViewHost = null;
300                 InlineSuggestionRenderService.this.mActiveInlineSuggestions.remove(
301                         InlineSuggestionUiImpl.this);
302                 Log.v(TAG, "Removed the inline suggestion from the cache, current size="
303                         + InlineSuggestionRenderService.this.mActiveInlineSuggestions.size());
304             });
305         }
306 
307         /**
308          * Sends back a new {@link android.view.SurfaceControlViewHost.SurfacePackage} if the view
309          * is not released, {@code null} otherwise.
310          */
getSurfacePackage(ISurfacePackageResultCallback callback)311         public void getSurfacePackage(ISurfacePackageResultCallback callback) {
312             Log.d(TAG, "getSurfacePackage");
313             mHandler.post(() -> {
314                 try {
315                     callback.onResult(mViewHost == null ? null : mViewHost.getSurfacePackage());
316                 } catch (RemoteException e) {
317                     Log.w(TAG, "RemoteException calling onSurfacePackage");
318                 }
319             });
320         }
321     }
322 
323     /** @hide */
324     @Override
dump(@onNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args)325     protected final void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw,
326             @NonNull String[] args) {
327         pw.println("mActiveInlineSuggestions: " + mActiveInlineSuggestions.size());
328         for (InlineSuggestionUiImpl impl : mActiveInlineSuggestions.snapshot().keySet()) {
329             pw.printf("ui: [%s] - [%d]  [%d]\n", impl, impl.mUserId, impl.mSessionId);
330         }
331     }
332 
333     @Override
334     @Nullable
onBind(@onNull Intent intent)335     public final IBinder onBind(@NonNull Intent intent) {
336         BaseBundle.setShouldDefuse(true);
337         if (SERVICE_INTERFACE.equals(intent.getAction())) {
338             return new IInlineSuggestionRenderService.Stub() {
339                 @Override
340                 public void renderSuggestion(@NonNull IInlineSuggestionUiCallback callback,
341                         @NonNull InlinePresentation presentation, int width, int height,
342                         @Nullable IBinder hostInputToken, int displayId, int userId,
343                         int sessionId) {
344                     mMainHandler.sendMessage(
345                             obtainMessage(InlineSuggestionRenderService::handleRenderSuggestion,
346                                     InlineSuggestionRenderService.this, callback, presentation,
347                                     width, height, hostInputToken, displayId, userId, sessionId));
348                 }
349 
350                 @Override
351                 public void getInlineSuggestionsRendererInfo(@NonNull RemoteCallback callback) {
352                     mMainHandler.sendMessage(obtainMessage(
353                             InlineSuggestionRenderService::handleGetInlineSuggestionsRendererInfo,
354                             InlineSuggestionRenderService.this, callback));
355                 }
356                 @Override
357                 public void destroySuggestionViews(int userId, int sessionId) {
358                     mMainHandler.sendMessage(obtainMessage(
359                             InlineSuggestionRenderService::handleDestroySuggestionViews,
360                             InlineSuggestionRenderService.this, userId, sessionId));
361                 }
362             }.asBinder();
363         }
364 
365         Log.w(TAG, "Tried to bind to wrong intent (should be " + SERVICE_INTERFACE + ": " + intent);
366         return null;
367     }
368 
369     /**
370      * Starts the {@link IntentSender} from the client app.
371      *
372      * @param intentSender the {@link IntentSender} to start the attribution UI from the client
373      *                     app.
374      */
375     public final void startIntentSender(@NonNull IntentSender intentSender) {
376         if (mCallback == null) return;
377         try {
378             mCallback.onStartIntentSender(intentSender);
379         } catch (RemoteException e) {
380             e.rethrowFromSystemServer();
381         }
382     }
383 
384     /**
385      * Returns the metadata about the renderer. Returns {@code Bundle.Empty} if no metadata is
386      * provided.
387      */
388     @NonNull
389     public Bundle onGetInlineSuggestionsRendererInfo() {
390         return Bundle.EMPTY;
391     }
392 
393     /**
394      * Renders the slice into a view.
395      */
396     @Nullable
397     public View onRenderSuggestion(@NonNull InlinePresentation presentation, int width,
398             int height) {
399         Log.e(TAG, "service implementation (" + getClass() + " does not implement "
400                 + "onRenderSuggestion()");
401         return null;
402     }
403 }
404