blob: 5d0143a505f321328cce47dbc38f498bca55da94 [file] [log] [blame]
Andrii Kuliand3134692017-06-26 14:57:02 -07001/**
2 * Copyright (c) 2017 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 android.annotation.NonNull;
Andrii Kuliancf8f6832018-01-23 19:43:30 -080020import android.app.ActivityManager.StackInfo;
Andrii Kuliand3134692017-06-26 14:57:02 -070021import android.content.Context;
22import android.content.Intent;
23import android.hardware.display.DisplayManager;
24import android.hardware.display.VirtualDisplay;
25import android.hardware.input.InputManager;
26import android.os.RemoteException;
27import android.util.AttributeSet;
28import android.util.DisplayMetrics;
29import android.util.Log;
30import android.view.InputDevice;
31import android.view.InputEvent;
32import android.view.MotionEvent;
33import android.view.Surface;
34import android.view.SurfaceHolder;
35import android.view.SurfaceView;
36import android.view.ViewGroup;
37import android.view.WindowManager;
Andrii Kulian4b6599e2018-01-15 17:24:08 -080038import android.view.WindowManagerGlobal;
Andrii Kuliand3134692017-06-26 14:57:02 -070039
40import dalvik.system.CloseGuard;
41
Andrii Kuliancf8f6832018-01-23 19:43:30 -080042import java.util.List;
43
Andrii Kuliand3134692017-06-26 14:57:02 -070044/**
45 * Activity container that allows launching activities into itself and does input forwarding.
46 * <p>Creation of this view is only allowed to callers who have
47 * {@link android.Manifest.permission#INJECT_EVENTS} permission.
48 * <p>Activity launching into this container is restricted by the same rules that apply to launching
49 * on VirtualDisplays.
50 * @hide
51 */
52public class ActivityView extends ViewGroup {
53
54 private static final String DISPLAY_NAME = "ActivityViewVirtualDisplay";
55 private static final String TAG = "ActivityView";
56
57 private VirtualDisplay mVirtualDisplay;
58 private final SurfaceView mSurfaceView;
59 private Surface mSurface;
60
61 private final SurfaceCallback mSurfaceCallback;
62 private StateCallback mActivityViewCallback;
63
Andrii Kuliancf8f6832018-01-23 19:43:30 -080064 private IActivityManager mActivityManager;
Andrii Kuliand3134692017-06-26 14:57:02 -070065 private IInputForwarder mInputForwarder;
Andrii Kulian4b6599e2018-01-15 17:24:08 -080066 // Temp container to store view coordinates on screen.
67 private final int[] mLocationOnScreen = new int[2];
Andrii Kuliand3134692017-06-26 14:57:02 -070068
Andrii Kuliancf8f6832018-01-23 19:43:30 -080069 private TaskStackListener mTaskStackListener;
70
Andrii Kuliand3134692017-06-26 14:57:02 -070071 private final CloseGuard mGuard = CloseGuard.get();
72 private boolean mOpened; // Protected by mGuard.
73
74 public ActivityView(Context context) {
75 this(context, null /* attrs */);
76 }
77
78 public ActivityView(Context context, AttributeSet attrs) {
79 this(context, attrs, 0 /* defStyle */);
80 }
81
82 public ActivityView(Context context, AttributeSet attrs, int defStyle) {
83 super(context, attrs, defStyle);
84
Andrii Kuliancf8f6832018-01-23 19:43:30 -080085 mActivityManager = ActivityManager.getService();
Andrii Kuliand3134692017-06-26 14:57:02 -070086 mSurfaceView = new SurfaceView(context);
87 mSurfaceCallback = new SurfaceCallback();
88 mSurfaceView.getHolder().addCallback(mSurfaceCallback);
89 addView(mSurfaceView);
90
91 mOpened = true;
92 mGuard.open("release");
93 }
94
95 /** Callback that notifies when the container is ready or destroyed. */
96 public abstract static class StateCallback {
97 /**
98 * Called when the container is ready for launching activities. Calling
99 * {@link #startActivity(Intent)} prior to this callback will result in an
100 * {@link IllegalStateException}.
101 *
102 * @see #startActivity(Intent)
103 */
104 public abstract void onActivityViewReady(ActivityView view);
105 /**
106 * Called when the container can no longer launch activities. Calling
107 * {@link #startActivity(Intent)} after this callback will result in an
108 * {@link IllegalStateException}.
109 *
110 * @see #startActivity(Intent)
111 */
112 public abstract void onActivityViewDestroyed(ActivityView view);
113 }
114
115 /**
116 * Set the callback to be notified about state changes.
117 * <p>This class must finish initializing before {@link #startActivity(Intent)} can be called.
118 * <p>Note: If the instance was ready prior to this call being made, then
119 * {@link StateCallback#onActivityViewReady(ActivityView)} will be called from within
120 * this method call.
121 *
122 * @param callback The callback to report events to.
123 *
124 * @see StateCallback
125 * @see #startActivity(Intent)
126 */
127 public void setCallback(StateCallback callback) {
128 mActivityViewCallback = callback;
129
130 if (mVirtualDisplay != null && mActivityViewCallback != null) {
131 mActivityViewCallback.onActivityViewReady(this);
132 }
133 }
134
135 /**
136 * Launch a new activity into this container.
137 * <p>Activity resolved by the provided {@link Intent} must have
138 * {@link android.R.attr#resizeableActivity} attribute set to {@code true} in order to be
139 * launched here. Also, if activity is not owned by the owner of this container, it must allow
140 * embedding and the caller must have permission to embed.
141 * <p>Note: This class must finish initializing and
142 * {@link StateCallback#onActivityViewReady(ActivityView)} callback must be triggered before
143 * this method can be called.
144 *
145 * @param intent Intent used to launch an activity.
146 *
147 * @see StateCallback
148 * @see #startActivity(PendingIntent)
149 */
150 public void startActivity(@NonNull Intent intent) {
151 final ActivityOptions options = prepareActivityOptions();
152 getContext().startActivity(intent, options.toBundle());
153 }
154
155 /**
156 * Launch a new activity into this container.
157 * <p>Activity resolved by the provided {@link PendingIntent} must have
158 * {@link android.R.attr#resizeableActivity} attribute set to {@code true} in order to be
159 * launched here. Also, if activity is not owned by the owner of this container, it must allow
160 * embedding and the caller must have permission to embed.
161 * <p>Note: This class must finish initializing and
162 * {@link StateCallback#onActivityViewReady(ActivityView)} callback must be triggered before
163 * this method can be called.
164 *
165 * @param pendingIntent Intent used to launch an activity.
166 *
167 * @see StateCallback
168 * @see #startActivity(Intent)
169 */
170 public void startActivity(@NonNull PendingIntent pendingIntent) {
171 final ActivityOptions options = prepareActivityOptions();
172 try {
173 pendingIntent.send(null /* context */, 0 /* code */, null /* intent */,
174 null /* onFinished */, null /* handler */, null /* requiredPermission */,
175 options.toBundle());
176 } catch (PendingIntent.CanceledException e) {
177 throw new RuntimeException(e);
178 }
179 }
180
181 /**
182 * Check if container is ready to launch and create {@link ActivityOptions} to target the
183 * virtual display.
184 */
185 private ActivityOptions prepareActivityOptions() {
186 if (mVirtualDisplay == null) {
187 throw new IllegalStateException(
188 "Trying to start activity before ActivityView is ready.");
189 }
190 final ActivityOptions options = ActivityOptions.makeBasic();
191 options.setLaunchDisplayId(mVirtualDisplay.getDisplay().getDisplayId());
192 return options;
193 }
194
195 /**
196 * Release this container. Activity launching will no longer be permitted.
197 * <p>Note: Calling this method is allowed after
198 * {@link StateCallback#onActivityViewReady(ActivityView)} callback was triggered and before
199 * {@link StateCallback#onActivityViewDestroyed(ActivityView)}.
200 *
201 * @see StateCallback
202 */
203 public void release() {
204 if (mVirtualDisplay == null) {
205 throw new IllegalStateException(
206 "Trying to release container that is not initialized.");
207 }
208 performRelease();
209 }
210
Andrii Kulian4b6599e2018-01-15 17:24:08 -0800211 /**
212 * Triggers an update of {@link ActivityView}'s location on screen to properly set touch exclude
213 * regions and avoid focus switches by touches on this view.
214 */
215 public void onLocationChanged() {
216 updateLocation();
217 }
218
Andrii Kuliand3134692017-06-26 14:57:02 -0700219 @Override
220 public void onLayout(boolean changed, int l, int t, int r, int b) {
221 mSurfaceView.layout(0 /* left */, 0 /* top */, r - l /* right */, b - t /* bottom */);
222 }
223
Andrii Kulian4b6599e2018-01-15 17:24:08 -0800224 /** Send current location and size to the WM to set tap exclude region for this view. */
225 private void updateLocation() {
226 try {
227 getLocationOnScreen(mLocationOnScreen);
228 WindowManagerGlobal.getWindowSession().updateTapExcludeRegion(getWindow(), hashCode(),
229 mLocationOnScreen[0], mLocationOnScreen[1], getWidth(), getHeight());
230 } catch (RemoteException e) {
231 e.rethrowAsRuntimeException();
232 }
233 }
234
Andrii Kuliand3134692017-06-26 14:57:02 -0700235 @Override
236 public boolean onTouchEvent(MotionEvent event) {
237 return injectInputEvent(event) || super.onTouchEvent(event);
238 }
239
240 @Override
241 public boolean onGenericMotionEvent(MotionEvent event) {
242 if (event.isFromSource(InputDevice.SOURCE_CLASS_POINTER)) {
243 if (injectInputEvent(event)) {
244 return true;
245 }
246 }
247 return super.onGenericMotionEvent(event);
248 }
249
250 private boolean injectInputEvent(InputEvent event) {
251 if (mInputForwarder != null) {
252 try {
253 return mInputForwarder.forwardEvent(event);
254 } catch (RemoteException e) {
255 e.rethrowAsRuntimeException();
256 }
257 }
258 return false;
259 }
260
261 private class SurfaceCallback implements SurfaceHolder.Callback {
262 @Override
263 public void surfaceCreated(SurfaceHolder surfaceHolder) {
Andrii Kuliand93da5c2017-07-22 14:59:51 -0700264 mSurface = mSurfaceView.getHolder().getSurface();
Andrii Kuliand3134692017-06-26 14:57:02 -0700265 if (mVirtualDisplay == null) {
Andrii Kuliand3134692017-06-26 14:57:02 -0700266 initVirtualDisplay();
267 if (mVirtualDisplay != null && mActivityViewCallback != null) {
268 mActivityViewCallback.onActivityViewReady(ActivityView.this);
269 }
270 } else {
271 mVirtualDisplay.setSurface(surfaceHolder.getSurface());
272 }
Andrii Kulian4b6599e2018-01-15 17:24:08 -0800273 updateLocation();
Andrii Kuliand3134692017-06-26 14:57:02 -0700274 }
275
276 @Override
277 public void surfaceChanged(SurfaceHolder surfaceHolder, int format, int width, int height) {
278 if (mVirtualDisplay != null) {
279 mVirtualDisplay.resize(width, height, getBaseDisplayDensity());
280 }
Andrii Kulian4b6599e2018-01-15 17:24:08 -0800281 updateLocation();
Andrii Kuliand3134692017-06-26 14:57:02 -0700282 }
283
284 @Override
285 public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
286 mSurface.release();
287 mSurface = null;
288 if (mVirtualDisplay != null) {
289 mVirtualDisplay.setSurface(null);
290 }
Andrii Kulian4b6599e2018-01-15 17:24:08 -0800291 cleanTapExcludeRegion();
Andrii Kuliand3134692017-06-26 14:57:02 -0700292 }
293 }
294
295 private void initVirtualDisplay() {
296 if (mVirtualDisplay != null) {
297 throw new IllegalStateException("Trying to initialize for the second time.");
298 }
299
300 final int width = mSurfaceView.getWidth();
301 final int height = mSurfaceView.getHeight();
302 final DisplayManager displayManager = mContext.getSystemService(DisplayManager.class);
303 mVirtualDisplay = displayManager.createVirtualDisplay(
304 DISPLAY_NAME + "@" + System.identityHashCode(this),
305 width, height, getBaseDisplayDensity(), mSurface, 0 /* flags */);
306 if (mVirtualDisplay == null) {
307 Log.e(TAG, "Failed to initialize ActivityView");
308 return;
309 }
310
311 mInputForwarder = InputManager.getInstance().createInputForwarder(
312 mVirtualDisplay.getDisplay().getDisplayId());
Andrii Kuliancf8f6832018-01-23 19:43:30 -0800313 mTaskStackListener = new TaskBackgroundChangeListener();
314 try {
315 mActivityManager.registerTaskStackListener(mTaskStackListener);
316 } catch (RemoteException e) {
317 Log.e(TAG, "Failed to register task stack listener", e);
318 }
Andrii Kuliand3134692017-06-26 14:57:02 -0700319 }
320
321 private void performRelease() {
322 if (!mOpened) {
323 return;
324 }
325
326 mSurfaceView.getHolder().removeCallback(mSurfaceCallback);
327
328 if (mInputForwarder != null) {
329 mInputForwarder = null;
330 }
Andrii Kulian4b6599e2018-01-15 17:24:08 -0800331 cleanTapExcludeRegion();
Andrii Kuliand3134692017-06-26 14:57:02 -0700332
Andrii Kuliancf8f6832018-01-23 19:43:30 -0800333 if (mTaskStackListener != null) {
334 try {
335 mActivityManager.unregisterTaskStackListener(mTaskStackListener);
336 } catch (RemoteException e) {
337 Log.e(TAG, "Failed to unregister task stack listener", e);
338 }
339 mTaskStackListener = null;
340 }
341
Andrii Kuliand3134692017-06-26 14:57:02 -0700342 final boolean displayReleased;
343 if (mVirtualDisplay != null) {
344 mVirtualDisplay.release();
345 mVirtualDisplay = null;
346 displayReleased = true;
347 } else {
348 displayReleased = false;
349 }
350
351 if (mSurface != null) {
352 mSurface.release();
353 mSurface = null;
354 }
355
356 if (displayReleased && mActivityViewCallback != null) {
357 mActivityViewCallback.onActivityViewDestroyed(this);
358 }
359
360 mGuard.close();
361 mOpened = false;
362 }
363
Andrii Kulian4b6599e2018-01-15 17:24:08 -0800364 /** Report to server that tap exclude region on hosting display should be cleared. */
365 private void cleanTapExcludeRegion() {
366 // Update tap exclude region with an empty rect to clean the state on server.
367 try {
368 WindowManagerGlobal.getWindowSession().updateTapExcludeRegion(getWindow(), hashCode(),
369 0 /* left */, 0 /* top */, 0 /* width */, 0 /* height */);
370 } catch (RemoteException e) {
371 e.rethrowAsRuntimeException();
372 }
373 }
374
Andrii Kuliand3134692017-06-26 14:57:02 -0700375 /** Get density of the hosting display. */
376 private int getBaseDisplayDensity() {
377 final WindowManager wm = mContext.getSystemService(WindowManager.class);
378 final DisplayMetrics metrics = new DisplayMetrics();
379 wm.getDefaultDisplay().getMetrics(metrics);
380 return metrics.densityDpi;
381 }
382
383 @Override
384 protected void finalize() throws Throwable {
385 try {
386 if (mGuard != null) {
387 mGuard.warnIfOpen();
388 performRelease();
389 }
390 } finally {
391 super.finalize();
392 }
393 }
Andrii Kuliancf8f6832018-01-23 19:43:30 -0800394
395 /**
396 * A task change listener that detects background color change of the topmost stack on our
397 * virtual display and updates the background of the surface view. This background will be shown
398 * when surface view is resized, but the app hasn't drawn its content in new size yet.
399 */
400 private class TaskBackgroundChangeListener extends TaskStackListener {
401
402 @Override
403 public void onTaskDescriptionChanged(int taskId, ActivityManager.TaskDescription td)
404 throws RemoteException {
405 if (mVirtualDisplay == null) {
406 return;
407 }
408
409 // Find the topmost task on our virtual display - it will define the background
410 // color of the surface view during resizing.
411 final int displayId = mVirtualDisplay.getDisplay().getDisplayId();
412 final List<StackInfo> stackInfoList = mActivityManager.getAllStackInfos();
413
414 // Iterate through stacks from top to bottom.
415 final int stackCount = stackInfoList.size();
416 for (int i = 0; i < stackCount; i++) {
417 final StackInfo stackInfo = stackInfoList.get(i);
418 // Only look for stacks on our virtual display.
419 if (stackInfo.displayId != displayId) {
420 continue;
421 }
422 // Found the topmost stack on target display. Now check if the topmost task's
423 // description changed.
424 if (taskId == stackInfo.taskIds[stackInfo.taskIds.length - 1]) {
425 mSurfaceView.setResizeBackgroundColor(td.getBackgroundColor());
426 }
427 break;
428 }
429 }
430 }
431
Andrii Kuliand3134692017-06-26 14:57:02 -0700432}