blob: 8790fb2299f5e5681d29bb234f3b53f0cb8d8490 [file] [log] [blame]
Adam He04302372020-01-23 13:40:57 -08001/*
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 */
16package android.service.autofill;
17
18import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
19
20import android.annotation.NonNull;
21import android.annotation.Nullable;
22import android.annotation.SystemApi;
23import android.annotation.TestApi;
24import android.app.Service;
Adam He04302372020-01-23 13:40:57 -080025import android.content.Intent;
Adam Hed41ce922020-03-19 14:30:32 -070026import android.content.IntentSender;
Adam He40116252020-02-04 14:55:39 -080027import android.graphics.PixelFormat;
TYM Tsai850c8122020-04-01 02:26:58 +080028import android.os.BaseBundle;
Feng Cao36960ee2020-02-18 18:23:30 -080029import android.os.Bundle;
Adam He04302372020-01-23 13:40:57 -080030import android.os.Handler;
31import android.os.IBinder;
32import android.os.Looper;
Adam He89afcd62020-03-18 17:28:32 -070033import android.os.RemoteCallback;
Adam He40116252020-02-04 14:55:39 -080034import android.os.RemoteException;
Adam He04302372020-01-23 13:40:57 -080035import android.util.Log;
Feng Caob46851c2020-04-29 18:22:49 -070036import android.util.LruCache;
Feng Caobd26abb2020-04-24 11:35:58 -070037import android.util.Size;
Svet Ganov02fdd342020-02-20 20:48:34 -080038import android.view.Display;
Adam He40116252020-02-04 14:55:39 -080039import android.view.SurfaceControlViewHost;
Adam He04302372020-01-23 13:40:57 -080040import android.view.View;
Feng Caobd26abb2020-04-24 11:35:58 -070041import android.view.ViewGroup;
Adam He40116252020-02-04 14:55:39 -080042import android.view.WindowManager;
Adam He04302372020-01-23 13:40:57 -080043
Feng Caob46851c2020-04-29 18:22:49 -070044import java.lang.ref.WeakReference;
45
Adam He04302372020-01-23 13:40:57 -080046/**
Feng Caobd26abb2020-04-24 11:35:58 -070047 * A service that renders an inline presentation view given the {@link InlinePresentation}.
Adam He04302372020-01-23 13:40:57 -080048 *
49 * {@hide}
50 */
51@SystemApi
52@TestApi
53public 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
Feng Caobd26abb2020-04-24 11:35:58 -070061 * {@link android.Manifest.permission#BIND_INLINE_SUGGESTION_RENDER_SERVICE} permission so that
62 * other applications can not abuse it.
Adam He04302372020-01-23 13:40:57 -080063 */
64 public static final String SERVICE_INTERFACE =
65 "android.service.autofill.InlineSuggestionRenderService";
66
Feng Caoffd9aff2020-05-26 23:10:26 -070067 private final Handler mMainHandler = new Handler(Looper.getMainLooper(), null, true);
Adam He04302372020-01-23 13:40:57 -080068
Adam Hed41ce922020-03-19 14:30:32 -070069 private IInlineSuggestionUiCallback mCallback;
70
Feng Caob46851c2020-04-29 18:22:49 -070071
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=100 entries in the cache. Releasing oldest one to make "
86 + "space.");
87 key.releaseSurfaceControlViewHost();
88 }
89 }
90 };
91
Feng Caobd26abb2020-04-24 11:35:58 -070092 /**
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 */
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
Adam He04302372020-01-23 13:40:57 -0800131 private void handleRenderSuggestion(IInlineSuggestionUiCallback callback,
Svet Ganov02fdd342020-02-20 20:48:34 -0800132 InlinePresentation presentation, int width, int height, IBinder hostInputToken,
133 int displayId) {
Adam He40116252020-02-04 14:55:39 -0800134 if (hostInputToken == null) {
135 try {
136 callback.onError();
137 } catch (RemoteException e) {
138 Log.w(TAG, "RemoteException calling onError()");
139 }
140 return;
141 }
Svet Ganov02fdd342020-02-20 20:48:34 -0800142
Svet Ganov17a647e2020-02-24 17:31:15 -0800143 // 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) {
Feng Caobd26abb2020-04-24 11:35:58 -0700148 Log.w(TAG, "ExtServices failed to render the inline suggestion view.");
Svet Ganov17a647e2020-02-24 17:31:15 -0800149 try {
150 callback.onError();
151 } catch (RemoteException e) {
152 Log.w(TAG, "Null suggestion view returned by renderer");
153 }
154 return;
Adam He40116252020-02-04 14:55:39 -0800155 }
Adam Hed41ce922020-03-19 14:30:32 -0700156 mCallback = callback;
Feng Caobd26abb2020-04-24 11:35:58 -0700157 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);
Adam He40116252020-02-04 14:55:39 -0800161
Svet Ganov17a647e2020-02-24 17:31:15 -0800162 final InlineSuggestionRoot suggestionRoot = new InlineSuggestionRoot(this, callback);
163 suggestionRoot.addView(suggestionView);
Feng Caobd26abb2020-04-24 11:35:58 -0700164 WindowManager.LayoutParams lp = new WindowManager.LayoutParams(measuredSize.getWidth(),
165 measuredSize.getHeight(), WindowManager.LayoutParams.TYPE_APPLICATION, 0,
166 PixelFormat.TRANSPARENT);
Svet Ganov17a647e2020-02-24 17:31:15 -0800167
168 final SurfaceControlViewHost host = new SurfaceControlViewHost(this, getDisplay(),
169 hostInputToken);
Robert Carr271d9c72020-03-12 12:39:24 -0700170 host.setView(suggestionRoot, lp);
Feng Caoc23bfc782020-04-29 00:35:10 -0700171
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) -> {
Svet Ganov17a647e2020-02-24 17:31:15 -0800176 try {
Feng Caodeec8f02020-03-10 18:14:13 -0700177 callback.onClick();
Svet Ganov17a647e2020-02-24 17:31:15 -0800178 } catch (RemoteException e) {
Feng Caodeec8f02020-03-10 18:14:13 -0700179 Log.w(TAG, "RemoteException calling onClick()");
Svet Ganov17a647e2020-02-24 17:31:15 -0800180 }
181 });
Feng Caoc23bfc782020-04-29 00:35:10 -0700182 final View.OnLongClickListener onLongClickListener =
183 suggestionView.getOnLongClickListener();
184 suggestionView.setOnLongClickListener((v) -> {
185 if (onLongClickListener != null) {
186 onLongClickListener.onLongClick(v);
187 }
Feng Caodeec8f02020-03-10 18:14:13 -0700188 try {
189 callback.onLongClick();
190 } catch (RemoteException e) {
191 Log.w(TAG, "RemoteException calling onLongClick()");
192 }
193 return true;
194 });
Feng Caoffd9aff2020-05-26 23:10:26 -0700195 final InlineSuggestionUiImpl uiImpl = new InlineSuggestionUiImpl(host, mMainHandler);
196 mActiveInlineSuggestions.put(uiImpl, true);
Feng Caodeec8f02020-03-10 18:14:13 -0700197
Feng Caoffd9aff2020-05-26 23:10:26 -0700198 // We post the callback invocation to the end of the main thread handler queue, to make
199 // sure the callback happens after the views are drawn. This is needed because calling
200 // {@link SurfaceControlViewHost#setView()} will post a task to the main thread
201 // to draw the view asynchronously.
202 mMainHandler.post(() -> {
203 try {
204 callback.onContent(new InlineSuggestionUiWrapper(uiImpl),
205 host.getSurfacePackage(),
206 measuredSize.getWidth(), measuredSize.getHeight());
207 } catch (RemoteException e) {
208 Log.w(TAG, "RemoteException calling onContent()");
209 }
210 });
Svet Ganov17a647e2020-02-24 17:31:15 -0800211 } finally {
212 updateDisplay(Display.DEFAULT_DISPLAY);
213 }
Svet Ganov02fdd342020-02-20 20:48:34 -0800214 }
215
Adam He89afcd62020-03-18 17:28:32 -0700216 private void handleGetInlineSuggestionsRendererInfo(@NonNull RemoteCallback callback) {
217 final Bundle rendererInfo = onGetInlineSuggestionsRendererInfo();
218 callback.sendResult(rendererInfo);
219 }
220
Feng Caob46851c2020-04-29 18:22:49 -0700221 /**
222 * A wrapper class around the {@link InlineSuggestionUiImpl} to ensure it's not strongly
223 * reference by the remote system server process.
224 */
225 private static final class InlineSuggestionUiWrapper extends
226 android.service.autofill.IInlineSuggestionUi.Stub {
227
228 private final WeakReference<InlineSuggestionUiImpl> mUiImpl;
229
230 InlineSuggestionUiWrapper(InlineSuggestionUiImpl uiImpl) {
231 mUiImpl = new WeakReference<>(uiImpl);
232 }
233
234 @Override
235 public void releaseSurfaceControlViewHost() {
236 final InlineSuggestionUiImpl uiImpl = mUiImpl.get();
237 if (uiImpl != null) {
238 uiImpl.releaseSurfaceControlViewHost();
239 }
240 }
241
242 @Override
243 public void getSurfacePackage(ISurfacePackageResultCallback callback) {
244 final InlineSuggestionUiImpl uiImpl = mUiImpl.get();
245 if (uiImpl != null) {
246 uiImpl.getSurfacePackage(callback);
247 }
248 }
249 }
250
251 /**
252 * Keeps track of a SurfaceControlViewHost to ensure it's released when its lifecycle ends.
253 *
254 * <p>This class is thread safe, because all the outside calls are piped into a single
255 * handler thread to be processed.
256 */
257 private final class InlineSuggestionUiImpl {
258
259 @Nullable
260 private SurfaceControlViewHost mViewHost;
261 @NonNull
262 private final Handler mHandler;
263
264 InlineSuggestionUiImpl(SurfaceControlViewHost viewHost, Handler handler) {
265 this.mViewHost = viewHost;
266 this.mHandler = handler;
267 }
268
269 /**
270 * Call {@link SurfaceControlViewHost#release()} to release it. After this, this view is
271 * not usable, and any further calls to the
272 * {@link #getSurfacePackage(ISurfacePackageResultCallback)} will get {@code null} result.
273 */
274 public void releaseSurfaceControlViewHost() {
275 mHandler.post(() -> {
276 if (mViewHost == null) {
277 return;
278 }
279 Log.v(TAG, "Releasing inline suggestion view host");
280 mViewHost.release();
281 mViewHost = null;
282 InlineSuggestionRenderService.this.mActiveInlineSuggestions.remove(
283 InlineSuggestionUiImpl.this);
284 Log.v(TAG, "Removed the inline suggestion from the cache, current size="
285 + InlineSuggestionRenderService.this.mActiveInlineSuggestions.size());
286 });
287 }
288
289 /**
290 * Sends back a new {@link android.view.SurfaceControlViewHost.SurfacePackage} if the view
291 * is not released, {@code null} otherwise.
292 */
293 public void getSurfacePackage(ISurfacePackageResultCallback callback) {
294 Log.d(TAG, "getSurfacePackage");
295 mHandler.post(() -> {
296 try {
297 callback.onResult(mViewHost == null ? null : mViewHost.getSurfacePackage());
298 } catch (RemoteException e) {
299 Log.w(TAG, "RemoteException calling onSurfacePackage");
300 }
301 });
Adam He40116252020-02-04 14:55:39 -0800302 }
Adam He04302372020-01-23 13:40:57 -0800303 }
304
305 @Override
306 @Nullable
307 public final IBinder onBind(@NonNull Intent intent) {
TYM Tsai850c8122020-04-01 02:26:58 +0800308 BaseBundle.setShouldDefuse(true);
Adam He04302372020-01-23 13:40:57 -0800309 if (SERVICE_INTERFACE.equals(intent.getAction())) {
310 return new IInlineSuggestionRenderService.Stub() {
311 @Override
312 public void renderSuggestion(@NonNull IInlineSuggestionUiCallback callback,
Adam He40116252020-02-04 14:55:39 -0800313 @NonNull InlinePresentation presentation, int width, int height,
Svet Ganov02fdd342020-02-20 20:48:34 -0800314 @Nullable IBinder hostInputToken, int displayId) {
Feng Caoffd9aff2020-05-26 23:10:26 -0700315 mMainHandler.sendMessage(
Feng Caobd26abb2020-04-24 11:35:58 -0700316 obtainMessage(InlineSuggestionRenderService::handleRenderSuggestion,
317 InlineSuggestionRenderService.this, callback, presentation,
318 width, height, hostInputToken, displayId));
Adam He04302372020-01-23 13:40:57 -0800319 }
Adam He89afcd62020-03-18 17:28:32 -0700320
321 @Override
322 public void getInlineSuggestionsRendererInfo(@NonNull RemoteCallback callback) {
Feng Caoffd9aff2020-05-26 23:10:26 -0700323 mMainHandler.sendMessage(obtainMessage(
Adam He89afcd62020-03-18 17:28:32 -0700324 InlineSuggestionRenderService::handleGetInlineSuggestionsRendererInfo,
325 InlineSuggestionRenderService.this, callback));
326 }
Adam He04302372020-01-23 13:40:57 -0800327 }.asBinder();
328 }
329
330 Log.w(TAG, "Tried to bind to wrong intent (should be " + SERVICE_INTERFACE + ": " + intent);
331 return null;
332 }
333
334 /**
Adam Hed41ce922020-03-19 14:30:32 -0700335 * Starts the {@link IntentSender} from the client app.
336 *
Feng Caobd26abb2020-04-24 11:35:58 -0700337 * @param intentSender the {@link IntentSender} to start the attribution UI from the client
338 * app.
Adam Hed41ce922020-03-19 14:30:32 -0700339 */
340 public final void startIntentSender(@NonNull IntentSender intentSender) {
341 if (mCallback == null) return;
342 try {
343 mCallback.onStartIntentSender(intentSender);
344 } catch (RemoteException e) {
345 e.rethrowFromSystemServer();
346 }
347 }
348
349 /**
Feng Caobd26abb2020-04-24 11:35:58 -0700350 * Returns the metadata about the renderer. Returns {@code Bundle.Empty} if no metadata is
351 * provided.
Feng Cao36960ee2020-02-18 18:23:30 -0800352 */
Feng Caoedb332c2020-03-30 14:26:42 -0700353 @NonNull
Feng Cao36960ee2020-02-18 18:23:30 -0800354 public Bundle onGetInlineSuggestionsRendererInfo() {
Feng Caoedb332c2020-03-30 14:26:42 -0700355 return Bundle.EMPTY;
Feng Cao36960ee2020-02-18 18:23:30 -0800356 }
357
358 /**
Adam He04302372020-01-23 13:40:57 -0800359 * Renders the slice into a view.
360 */
361 @Nullable
Feng Caobd26abb2020-04-24 11:35:58 -0700362 public View onRenderSuggestion(@NonNull InlinePresentation presentation, int width,
363 int height) {
Adam He04302372020-01-23 13:40:57 -0800364 Log.e(TAG, "service implementation (" + getClass() + " does not implement "
365 + "onRenderSuggestion()");
366 return null;
367 }
368}