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