blob: a1389bdbaf7b78fd4cb659d37a255772f45a7c3d [file] [log] [blame]
Mark Renouf89ac9882019-10-07 16:28:18 -04001/*
2 * Copyright (C) 2019 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
17package android.app;
18
19import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_DESTROY_CONTENT_ON_REMOVAL;
20import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY;
21import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC;
22
23import android.annotation.NonNull;
24import android.annotation.Nullable;
25import android.content.ComponentName;
26import android.content.Context;
27import android.content.Intent;
28import android.graphics.Insets;
29import android.graphics.Matrix;
30import android.graphics.Point;
31import android.graphics.Region;
32import android.hardware.display.DisplayManager;
33import android.hardware.display.VirtualDisplay;
34import android.hardware.input.InputManager;
35import android.os.RemoteException;
36import android.os.SystemClock;
37import android.os.UserHandle;
38import android.util.DisplayMetrics;
39import android.util.Log;
40import android.view.Display;
41import android.view.IWindow;
42import android.view.IWindowManager;
43import android.view.IWindowSession;
44import android.view.InputDevice;
45import android.view.KeyCharacterMap;
46import android.view.KeyEvent;
47import android.view.SurfaceControl;
48import android.view.WindowManager;
49import android.view.WindowManagerGlobal;
50import android.view.inputmethod.InputMethodManager;
51
52import dalvik.system.CloseGuard;
53
54import java.util.List;
55
56/**
57 * A component which handles embedded display of tasks within another window. The embedded task can
58 * be presented using the SurfaceControl provided from {@link #getSurfaceControl()}.
59 *
60 * @hide
61 */
62public class TaskEmbedder {
63 private static final String TAG = "TaskEmbedder";
64 private static final String DISPLAY_NAME = "TaskVirtualDisplay";
65
66 /**
67 * A component which will host the task.
68 */
69 public interface Host {
70 /** @return the screen area where touches should be dispatched to the embedded Task */
71 Region getTapExcludeRegion();
72
73 /** @return a matrix which transforms from screen-space to the embedded task surface */
74 Matrix getScreenToTaskMatrix();
75
76 /** @return the window containing the parent surface, if attached and available */
77 @Nullable IWindow getWindow();
78
79 /** @return the x/y offset from the origin of the window to the surface */
80 Point getPositionInWindow();
81
82 /** @return whether this surface is able to receive pointer events */
83 boolean canReceivePointerEvents();
84
85 /** @return the width of the container for the embedded task */
86 int getWidth();
87
88 /** @return the height of the container for the embedded task */
89 int getHeight();
90
91 /**
92 * Called to inform the host of the task's background color. This can be used to
93 * fill unpainted areas if necessary.
94 */
95 void onTaskBackgroundColorChanged(TaskEmbedder ts, int bgColor);
96 }
97
98 /**
99 * Describes changes to the state of the TaskEmbedder as well the tasks within.
100 */
101 public interface Listener {
102 /** Called when the container is ready for launching activities. */
103 default void onInitialized() {}
104
105 /** Called when the container can no longer launch activities. */
106 default void onReleased() {}
107
108 /** Called when a task is created inside the container. */
109 default void onTaskCreated(int taskId, ComponentName name) {}
110
111 /** Called when a task is moved to the front of the stack inside the container. */
112 default void onTaskMovedToFront(int taskId) {}
113
114 /** Called when a task is about to be removed from the stack inside the container. */
115 default void onTaskRemovalStarted(int taskId) {}
116 }
117
118 private IActivityTaskManager mActivityTaskManager = ActivityTaskManager.getService();
119
120 private final Context mContext;
121 private TaskEmbedder.Host mHost;
122 private int mDisplayDensityDpi;
123 private final boolean mSingleTaskInstance;
124 private SurfaceControl.Transaction mTransaction;
125 private SurfaceControl mSurfaceControl;
126 private VirtualDisplay mVirtualDisplay;
127 private Insets mForwardedInsets;
128 private TaskStackListener mTaskStackListener;
129 private Listener mListener;
130 private boolean mOpened; // Protected by mGuard.
131
132 private final CloseGuard mGuard = CloseGuard.get();
133
134
135 /**
136 * Constructs a new TaskEmbedder.
137 *
138 * @param context the context
139 * @param host the host for this embedded task
140 * @param singleTaskInstance whether to apply a single-task constraint to this container
141 */
142 public TaskEmbedder(Context context, TaskEmbedder.Host host, boolean singleTaskInstance) {
143 mContext = context;
144 mHost = host;
145 mSingleTaskInstance = singleTaskInstance;
146 }
147
148 /**
149 * Whether this container has been initialized.
150 *
151 * @return true if initialized
152 */
153 public boolean isInitialized() {
154 return mVirtualDisplay != null;
155 }
156
157 /**
158 * Initialize this container.
159 *
160 * @param parent the surface control for the parent surface
161 * @return true if initialized successfully
162 */
163 public boolean initialize(SurfaceControl parent) {
164 if (mVirtualDisplay != null) {
165 throw new IllegalStateException("Trying to initialize for the second time.");
166 }
167
168 mTransaction = new SurfaceControl.Transaction();
169
170 final DisplayManager displayManager = mContext.getSystemService(DisplayManager.class);
171 mDisplayDensityDpi = getBaseDisplayDensity();
172
173 mVirtualDisplay = displayManager.createVirtualDisplay(
174 DISPLAY_NAME + "@" + System.identityHashCode(this), mHost.getWidth(),
175 mHost.getHeight(), mDisplayDensityDpi, null,
176 VIRTUAL_DISPLAY_FLAG_PUBLIC | VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY
177 | VIRTUAL_DISPLAY_FLAG_DESTROY_CONTENT_ON_REMOVAL);
178
179 if (mVirtualDisplay == null) {
180 Log.e(TAG, "Failed to initialize TaskEmbedder");
181 return false;
182 }
183
184 // Create a container surface to which the ActivityDisplay will be reparented
185 final String name = "TaskEmbedder - " + Integer.toHexString(System.identityHashCode(this));
186 mSurfaceControl = new SurfaceControl.Builder()
187 .setContainerLayer()
188 .setParent(parent)
189 .setName(name)
190 .build();
191
192 final int displayId = getDisplayId();
193
194 final IWindowManager wm = WindowManagerGlobal.getWindowManagerService();
195 try {
196 // TODO: Find a way to consolidate these calls to the server.
197 WindowManagerGlobal.getWindowSession().reparentDisplayContent(
198 mHost.getWindow(), mSurfaceControl, displayId);
199 wm.dontOverrideDisplayInfo(displayId);
200 if (mSingleTaskInstance) {
201 mContext.getSystemService(ActivityTaskManager.class)
202 .setDisplayToSingleTaskInstance(displayId);
203 }
204 setForwardedInsets(mForwardedInsets);
205 if (mHost.getWindow() != null) {
206 updateLocationAndTapExcludeRegion();
207 }
208 mTaskStackListener = new TaskStackListenerImpl();
209 try {
210 mActivityTaskManager.registerTaskStackListener(mTaskStackListener);
211 } catch (RemoteException e) {
212 Log.e(TAG, "Failed to register task stack listener", e);
213 }
214 } catch (RemoteException e) {
215 e.rethrowAsRuntimeException();
216 }
217 if (mListener != null && mVirtualDisplay != null) {
218 mListener.onInitialized();
219 }
220 mOpened = true;
221 mGuard.open("release");
222 return true;
223 }
224
225 /**
226 * Returns the surface control for the task surface. This should be parented to a screen
227 * surface for display/embedding purposes.
228 *
229 * @return the surface control for the task
230 */
231 public SurfaceControl getSurfaceControl() {
232 return mSurfaceControl;
233 }
234
235 /**
236 * Set forwarded insets on the virtual display.
237 *
238 * @see IWindowManager#setForwardedInsets
239 */
240 public void setForwardedInsets(Insets insets) {
241 mForwardedInsets = insets;
242 if (mVirtualDisplay == null) {
243 return;
244 }
245 try {
246 final IWindowManager wm = WindowManagerGlobal.getWindowManagerService();
247 wm.setForwardedInsets(getDisplayId(), mForwardedInsets);
248 } catch (RemoteException e) {
249 e.rethrowAsRuntimeException();
250 }
251 }
252
253 /** An opaque unique identifier for this task surface among others being managed by the app. */
254 public int getId() {
255 return getDisplayId();
256 }
257
258 int getDisplayId() {
259 if (mVirtualDisplay != null) {
260 return mVirtualDisplay.getDisplay().getDisplayId();
261 }
262 return Display.INVALID_DISPLAY;
263 }
264
265 /**
266 * Set the callback to be notified about state changes.
267 * <p>This class must finish initializing before {@link #startActivity(Intent)} can be called.
268 * <p>Note: If the instance was ready prior to this call being made, then
269 * {@link Listener#onInitialized()} will be called from within this method call.
270 *
271 * @param listener The listener to report events to.
272 *
273 * @see ActivityView.StateCallback
274 * @see #startActivity(Intent)
275 */
276 void setListener(TaskEmbedder.Listener listener) {
277 mListener = listener;
278 if (mListener != null && isInitialized()) {
279 mListener.onInitialized();
280 }
281 }
282
283 /**
284 * Launch a new activity into this container.
285 *
286 * @param intent Intent used to launch an activity
287 *
288 * @see #startActivity(PendingIntent)
289 */
290 public void startActivity(@NonNull Intent intent) {
291 final ActivityOptions options = prepareActivityOptions();
292 mContext.startActivity(intent, options.toBundle());
293 }
294
295 /**
296 * Launch a new activity into this container.
297 *
298 * @param intent Intent used to launch an activity
299 * @param user The UserHandle of the user to start this activity for
300 *
301 * @see #startActivity(PendingIntent)
302 */
303 public void startActivity(@NonNull Intent intent, UserHandle user) {
304 final ActivityOptions options = prepareActivityOptions();
305 mContext.startActivityAsUser(intent, options.toBundle(), user);
306 }
307
308 /**
309 * Launch a new activity into this container.
310 *
311 * @param pendingIntent Intent used to launch an activity
312 *
313 * @see #startActivity(Intent)
314 */
315 public void startActivity(@NonNull PendingIntent pendingIntent) {
316 final ActivityOptions options = prepareActivityOptions();
317 try {
318 pendingIntent.send(null /* context */, 0 /* code */, null /* intent */,
319 null /* onFinished */, null /* handler */, null /* requiredPermission */,
320 options.toBundle());
321 } catch (PendingIntent.CanceledException e) {
322 throw new RuntimeException(e);
323 }
324 }
325
326 /**
327 * Launch a new activity into this container.
328 *
329 * @param pendingIntent Intent used to launch an activity
330 * @param fillInIntent Additional Intent data, see {@link Intent#fillIn Intent.fillIn()}
331 * @param options options for the activity
332 *
333 * @see #startActivity(Intent)
334 */
335 public void startActivity(@NonNull PendingIntent pendingIntent, @Nullable Intent fillInIntent,
336 @NonNull ActivityOptions options) {
337
338 options.setLaunchDisplayId(mVirtualDisplay.getDisplay().getDisplayId());
339 try {
340 pendingIntent.send(mContext, 0 /* code */, fillInIntent,
341 null /* onFinished */, null /* handler */, null /* requiredPermission */,
342 options.toBundle());
343 } catch (PendingIntent.CanceledException e) {
344 throw new RuntimeException(e);
345 }
346 }
347
348 /**
349 * Check if container is ready to launch and create {@link ActivityOptions} to target the
350 * virtual display.
351 */
352 private ActivityOptions prepareActivityOptions() {
353 if (mVirtualDisplay == null) {
354 throw new IllegalStateException(
355 "Trying to start activity before ActivityView is ready.");
356 }
357 final ActivityOptions options = ActivityOptions.makeBasic();
358 options.setLaunchDisplayId(getDisplayId());
359 return options;
360 }
361
362 /**
363 * Stops presentation of tasks in this container.
364 */
365 public void stop() {
366 if (mVirtualDisplay != null) {
367 mVirtualDisplay.setDisplayState(false);
368 clearActivityViewGeometryForIme();
369 clearTapExcludeRegion();
370 }
371 }
372
373 /**
374 * Starts presentation of tasks in this container.
375 */
376 public void start() {
377 if (mVirtualDisplay != null) {
378 mVirtualDisplay.setDisplayState(true);
379 updateLocationAndTapExcludeRegion();
380 }
381 }
382
383 /**
384 * This should be called whenever the position or size of the surface changes
385 * or if touchable areas above the surface are added or removed.
386 */
387 public void notifyBoundsChanged() {
388 updateLocationAndTapExcludeRegion();
389 }
390
391 /**
392 * Updates position and bounds information needed by WM and IME to manage window
393 * focus and touch events properly.
394 * <p>
395 * This should be called whenever the position or size of the surface changes
396 * or if touchable areas above the surface are added or removed.
397 */
398 private void updateLocationAndTapExcludeRegion() {
399 if (mVirtualDisplay == null || mHost.getWindow() == null) {
400 return;
401 }
402 reportLocation(mHost.getScreenToTaskMatrix(), mHost.getPositionInWindow());
403 applyTapExcludeRegion(mHost.getWindow(), hashCode(), mHost.getTapExcludeRegion());
404 }
405
406 /**
407 * Call to update the position and transform matrix for the embedded surface.
408 * <p>
409 * This should not normally be called directly, but through
410 * {@link #updateLocationAndTapExcludeRegion()}. This method
411 * is provided as an optimization when managing multiple TaskSurfaces within a view.
412 *
413 * @param screenToViewMatrix the matrix/transform from screen space to view space
414 * @param positionInWindow the window-relative position of the surface
415 *
416 * @see InputMethodManager#reportActivityView(int, Matrix)
417 */
418 private void reportLocation(Matrix screenToViewMatrix, Point positionInWindow) {
419 try {
420 final int displayId = getDisplayId();
421 mContext.getSystemService(InputMethodManager.class)
422 .reportActivityView(displayId, screenToViewMatrix);
423 IWindowSession session = WindowManagerGlobal.getWindowSession();
424 session.updateDisplayContentLocation(mHost.getWindow(), positionInWindow.x,
425 positionInWindow.y, displayId);
426 } catch (RemoteException e) {
427 e.rethrowAsRuntimeException();
428 }
429 }
430
431 /**
432 * Call to update the tap exclude region for the window.
433 * <p>
434 * This should not normally be called directly, but through
435 * {@link #updateLocationAndTapExcludeRegion()}. This method
436 * is provided as an optimization when managing multiple TaskSurfaces within a view.
437 *
438 * @see IWindowSession#updateTapExcludeRegion(IWindow, int, Region)
439 */
440 private void applyTapExcludeRegion(IWindow window, int regionId,
441 @Nullable Region tapExcludeRegion) {
442 try {
443 IWindowSession session = WindowManagerGlobal.getWindowSession();
444 session.updateTapExcludeRegion(window, regionId, tapExcludeRegion);
445 } catch (RemoteException e) {
446 e.rethrowAsRuntimeException();
447 }
448 }
449
450 /**
451 * @see InputMethodManager#reportActivityView(int, Matrix)
452 */
453 private void clearActivityViewGeometryForIme() {
454 final int displayId = getDisplayId();
455 mContext.getSystemService(InputMethodManager.class).reportActivityView(displayId, null);
456 }
457
458 /**
459 * Removes the tap exclude region set by {@link #updateLocationAndTapExcludeRegion()}.
460 */
461 private void clearTapExcludeRegion() {
462 if (mHost.getWindow() == null) {
463 Log.w(TAG, "clearTapExcludeRegion: not attached to window!");
464 return;
465 }
466 applyTapExcludeRegion(mHost.getWindow(), hashCode(), null);
467 }
468
469 /**
470 * Called to update the dimensions whenever the host size changes.
471 *
472 * @param width the new width of the surface
473 * @param height the new height of the surface
474 */
475 public void resizeTask(int width, int height) {
476 mDisplayDensityDpi = getBaseDisplayDensity();
477 if (mVirtualDisplay != null) {
478 mVirtualDisplay.resize(width, height, mDisplayDensityDpi);
479 }
480 }
481
482 /**
483 * Injects a pair of down/up key events with keycode {@link KeyEvent#KEYCODE_BACK} to the
484 * virtual display.
485 */
486 public void performBackPress() {
487 if (mVirtualDisplay == null) {
488 return;
489 }
490 final int displayId = mVirtualDisplay.getDisplay().getDisplayId();
491 final InputManager im = InputManager.getInstance();
492 im.injectInputEvent(createKeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK, displayId),
493 InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
494 im.injectInputEvent(createKeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_BACK, displayId),
495 InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
496 }
497
498 private static KeyEvent createKeyEvent(int action, int code, int displayId) {
499 long when = SystemClock.uptimeMillis();
500 final KeyEvent ev = new KeyEvent(when, when, action, code, 0 /* repeat */,
501 0 /* metaState */, KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /* scancode */,
502 KeyEvent.FLAG_FROM_SYSTEM | KeyEvent.FLAG_VIRTUAL_HARD_KEY,
503 InputDevice.SOURCE_KEYBOARD);
504 ev.setDisplayId(displayId);
505 return ev;
506 }
507
508 /**
509 * Releases the resources for this TaskEmbedder. Tasks will no longer be launchable
510 * within this container.
511 *
512 * <p>Note: Calling this method is allowed after {@link Listener#onInitialized()} callback is
513 * triggered and before {@link Listener#onReleased()}.
514 */
515 public void release() {
516 if (mVirtualDisplay == null) {
517 throw new IllegalStateException(
518 "Trying to release container that is not initialized.");
519 }
520 performRelease();
521 }
522
523 private boolean performRelease() {
524 if (!mOpened) {
525 return false;
526 }
527 mTransaction.reparent(mSurfaceControl, null).apply();
528 mSurfaceControl.release();
529
530 // Clear activity view geometry for IME on this display
531 clearActivityViewGeometryForIme();
532
533 // Clear tap-exclude region (if any) for this window.
534 clearTapExcludeRegion();
535
536 if (mTaskStackListener != null) {
537 try {
538 mActivityTaskManager.unregisterTaskStackListener(mTaskStackListener);
539 } catch (RemoteException e) {
540 Log.e(TAG, "Failed to unregister task stack listener", e);
541 }
542 mTaskStackListener = null;
543 }
544
545 boolean reportReleased = false;
546 if (mVirtualDisplay != null) {
547 mVirtualDisplay.release();
548 mVirtualDisplay = null;
549 reportReleased = true;
550
551 }
552
553 if (mListener != null && reportReleased) {
554 mListener.onReleased();
555 }
556 mOpened = false;
557 mGuard.close();
558 return true;
559 }
560
561 @Override
562 protected void finalize() throws Throwable {
563 try {
564 if (mGuard != null) {
565 mGuard.warnIfOpen();
566 performRelease();
567 }
568 } finally {
569 super.finalize();
570 }
571 }
572
573 /** Get density of the hosting display. */
574 private int getBaseDisplayDensity() {
575 final WindowManager wm = mContext.getSystemService(WindowManager.class);
576 final DisplayMetrics metrics = new DisplayMetrics();
577 wm.getDefaultDisplay().getMetrics(metrics);
578 return metrics.densityDpi;
579 }
580
581 /**
582 * A task change listener that detects background color change of the topmost stack on our
583 * virtual display and updates the background of the surface view. This background will be shown
584 * when surface view is resized, but the app hasn't drawn its content in new size yet.
585 * It also calls StateCallback.onTaskMovedToFront to notify interested parties that the stack
586 * associated with the {@link ActivityView} has had a Task moved to the front. This is useful
587 * when needing to also bring the host Activity to the foreground at the same time.
588 */
589 private class TaskStackListenerImpl extends TaskStackListener {
590
591 @Override
592 public void onTaskDescriptionChanged(ActivityManager.RunningTaskInfo taskInfo)
593 throws RemoteException {
594 if (!isInitialized()
595 || taskInfo.displayId != getDisplayId()) {
596 return;
597 }
598
599 ActivityManager.StackInfo stackInfo = getTopMostStackInfo();
600 if (stackInfo == null) {
601 return;
602 }
603 // Found the topmost stack on target display. Now check if the topmost task's
604 // description changed.
605 if (taskInfo.taskId == stackInfo.taskIds[stackInfo.taskIds.length - 1]) {
606 mHost.onTaskBackgroundColorChanged(TaskEmbedder.this,
607 taskInfo.taskDescription.getBackgroundColor());
608 }
609 }
610
611 @Override
612 public void onTaskMovedToFront(ActivityManager.RunningTaskInfo taskInfo)
613 throws RemoteException {
614 if (!isInitialized() || mListener == null
615 || taskInfo.displayId != getDisplayId()) {
616 return;
617 }
618
619 ActivityManager.StackInfo stackInfo = getTopMostStackInfo();
620 // if StackInfo was null or unrelated to the "move to front" then there's no use
621 // notifying the callback
622 if (stackInfo != null
623 && taskInfo.taskId == stackInfo.taskIds[stackInfo.taskIds.length - 1]) {
624 mListener.onTaskMovedToFront(taskInfo.taskId);
625 }
626 }
627
628 @Override
629 public void onTaskCreated(int taskId, ComponentName componentName) throws RemoteException {
630 if (mListener == null || !isInitialized()) {
631 return;
632 }
633
634 ActivityManager.StackInfo stackInfo = getTopMostStackInfo();
635 // if StackInfo was null or unrelated to the task creation then there's no use
636 // notifying the callback
637 if (stackInfo != null
638 && taskId == stackInfo.taskIds[stackInfo.taskIds.length - 1]) {
639 mListener.onTaskCreated(taskId, componentName);
640 }
641 }
642
643 @Override
644 public void onTaskRemovalStarted(ActivityManager.RunningTaskInfo taskInfo)
645 throws RemoteException {
646 if (mListener == null || !isInitialized()
647 || taskInfo.displayId != getDisplayId()) {
648 return;
649 }
650 mListener.onTaskRemovalStarted(taskInfo.taskId);
651 }
652
653 private ActivityManager.StackInfo getTopMostStackInfo() throws RemoteException {
654 // Find the topmost task on our virtual display - it will define the background
655 // color of the surface view during resizing.
656 final int displayId = getDisplayId();
657 final List<ActivityManager.StackInfo> stackInfoList =
658 mActivityTaskManager.getAllStackInfos();
659
660 // Iterate through stacks from top to bottom.
661 final int stackCount = stackInfoList.size();
662 for (int i = 0; i < stackCount; i++) {
663 final ActivityManager.StackInfo stackInfo = stackInfoList.get(i);
664 // Only look for stacks on our virtual display.
665 if (stackInfo.displayId != displayId) {
666 continue;
667 }
668 // Found the topmost stack on target display.
669 return stackInfo;
670 }
671 return null;
672 }
673 }
674}