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