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