blob: ac6cba138eedc92e45787438765de01ea8dcb213 [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;
20import android.content.Context;
21import android.content.Intent;
22import android.hardware.display.DisplayManager;
23import android.hardware.display.VirtualDisplay;
24import android.hardware.input.InputManager;
25import android.os.RemoteException;
26import android.util.AttributeSet;
27import android.util.DisplayMetrics;
28import android.util.Log;
29import android.view.InputDevice;
30import android.view.InputEvent;
31import android.view.MotionEvent;
32import android.view.Surface;
33import android.view.SurfaceHolder;
34import android.view.SurfaceView;
35import android.view.ViewGroup;
36import android.view.WindowManager;
Andrii Kulian4b6599e2018-01-15 17:24:08 -080037import android.view.WindowManagerGlobal;
Andrii Kuliand3134692017-06-26 14:57:02 -070038
39import dalvik.system.CloseGuard;
40
41/**
42 * Activity container that allows launching activities into itself and does input forwarding.
43 * <p>Creation of this view is only allowed to callers who have
44 * {@link android.Manifest.permission#INJECT_EVENTS} permission.
45 * <p>Activity launching into this container is restricted by the same rules that apply to launching
46 * on VirtualDisplays.
47 * @hide
48 */
49public class ActivityView extends ViewGroup {
50
51 private static final String DISPLAY_NAME = "ActivityViewVirtualDisplay";
52 private static final String TAG = "ActivityView";
53
54 private VirtualDisplay mVirtualDisplay;
55 private final SurfaceView mSurfaceView;
56 private Surface mSurface;
57
58 private final SurfaceCallback mSurfaceCallback;
59 private StateCallback mActivityViewCallback;
60
61 private IInputForwarder mInputForwarder;
Andrii Kulian4b6599e2018-01-15 17:24:08 -080062 // Temp container to store view coordinates on screen.
63 private final int[] mLocationOnScreen = new int[2];
Andrii Kuliand3134692017-06-26 14:57:02 -070064
65 private final CloseGuard mGuard = CloseGuard.get();
66 private boolean mOpened; // Protected by mGuard.
67
68 public ActivityView(Context context) {
69 this(context, null /* attrs */);
70 }
71
72 public ActivityView(Context context, AttributeSet attrs) {
73 this(context, attrs, 0 /* defStyle */);
74 }
75
76 public ActivityView(Context context, AttributeSet attrs, int defStyle) {
77 super(context, attrs, defStyle);
78
79 mSurfaceView = new SurfaceView(context);
80 mSurfaceCallback = new SurfaceCallback();
81 mSurfaceView.getHolder().addCallback(mSurfaceCallback);
82 addView(mSurfaceView);
83
84 mOpened = true;
85 mGuard.open("release");
86 }
87
88 /** Callback that notifies when the container is ready or destroyed. */
89 public abstract static class StateCallback {
90 /**
91 * Called when the container is ready for launching activities. Calling
92 * {@link #startActivity(Intent)} prior to this callback will result in an
93 * {@link IllegalStateException}.
94 *
95 * @see #startActivity(Intent)
96 */
97 public abstract void onActivityViewReady(ActivityView view);
98 /**
99 * Called when the container can no longer launch activities. Calling
100 * {@link #startActivity(Intent)} after this callback will result in an
101 * {@link IllegalStateException}.
102 *
103 * @see #startActivity(Intent)
104 */
105 public abstract void onActivityViewDestroyed(ActivityView view);
106 }
107
108 /**
109 * Set the callback to be notified about state changes.
110 * <p>This class must finish initializing before {@link #startActivity(Intent)} can be called.
111 * <p>Note: If the instance was ready prior to this call being made, then
112 * {@link StateCallback#onActivityViewReady(ActivityView)} will be called from within
113 * this method call.
114 *
115 * @param callback The callback to report events to.
116 *
117 * @see StateCallback
118 * @see #startActivity(Intent)
119 */
120 public void setCallback(StateCallback callback) {
121 mActivityViewCallback = callback;
122
123 if (mVirtualDisplay != null && mActivityViewCallback != null) {
124 mActivityViewCallback.onActivityViewReady(this);
125 }
126 }
127
128 /**
129 * Launch a new activity into this container.
130 * <p>Activity resolved by the provided {@link Intent} must have
131 * {@link android.R.attr#resizeableActivity} attribute set to {@code true} in order to be
132 * launched here. Also, if activity is not owned by the owner of this container, it must allow
133 * embedding and the caller must have permission to embed.
134 * <p>Note: This class must finish initializing and
135 * {@link StateCallback#onActivityViewReady(ActivityView)} callback must be triggered before
136 * this method can be called.
137 *
138 * @param intent Intent used to launch an activity.
139 *
140 * @see StateCallback
141 * @see #startActivity(PendingIntent)
142 */
143 public void startActivity(@NonNull Intent intent) {
144 final ActivityOptions options = prepareActivityOptions();
145 getContext().startActivity(intent, options.toBundle());
146 }
147
148 /**
149 * Launch a new activity into this container.
150 * <p>Activity resolved by the provided {@link PendingIntent} must have
151 * {@link android.R.attr#resizeableActivity} attribute set to {@code true} in order to be
152 * launched here. Also, if activity is not owned by the owner of this container, it must allow
153 * embedding and the caller must have permission to embed.
154 * <p>Note: This class must finish initializing and
155 * {@link StateCallback#onActivityViewReady(ActivityView)} callback must be triggered before
156 * this method can be called.
157 *
158 * @param pendingIntent Intent used to launch an activity.
159 *
160 * @see StateCallback
161 * @see #startActivity(Intent)
162 */
163 public void startActivity(@NonNull PendingIntent pendingIntent) {
164 final ActivityOptions options = prepareActivityOptions();
165 try {
166 pendingIntent.send(null /* context */, 0 /* code */, null /* intent */,
167 null /* onFinished */, null /* handler */, null /* requiredPermission */,
168 options.toBundle());
169 } catch (PendingIntent.CanceledException e) {
170 throw new RuntimeException(e);
171 }
172 }
173
174 /**
175 * Check if container is ready to launch and create {@link ActivityOptions} to target the
176 * virtual display.
177 */
178 private ActivityOptions prepareActivityOptions() {
179 if (mVirtualDisplay == null) {
180 throw new IllegalStateException(
181 "Trying to start activity before ActivityView is ready.");
182 }
183 final ActivityOptions options = ActivityOptions.makeBasic();
184 options.setLaunchDisplayId(mVirtualDisplay.getDisplay().getDisplayId());
185 return options;
186 }
187
188 /**
189 * Release this container. Activity launching will no longer be permitted.
190 * <p>Note: Calling this method is allowed after
191 * {@link StateCallback#onActivityViewReady(ActivityView)} callback was triggered and before
192 * {@link StateCallback#onActivityViewDestroyed(ActivityView)}.
193 *
194 * @see StateCallback
195 */
196 public void release() {
197 if (mVirtualDisplay == null) {
198 throw new IllegalStateException(
199 "Trying to release container that is not initialized.");
200 }
201 performRelease();
202 }
203
Andrii Kulian4b6599e2018-01-15 17:24:08 -0800204 /**
205 * Triggers an update of {@link ActivityView}'s location on screen to properly set touch exclude
206 * regions and avoid focus switches by touches on this view.
207 */
208 public void onLocationChanged() {
209 updateLocation();
210 }
211
Andrii Kuliand3134692017-06-26 14:57:02 -0700212 @Override
213 public void onLayout(boolean changed, int l, int t, int r, int b) {
214 mSurfaceView.layout(0 /* left */, 0 /* top */, r - l /* right */, b - t /* bottom */);
215 }
216
Andrii Kulian4b6599e2018-01-15 17:24:08 -0800217 /** Send current location and size to the WM to set tap exclude region for this view. */
218 private void updateLocation() {
219 try {
220 getLocationOnScreen(mLocationOnScreen);
221 WindowManagerGlobal.getWindowSession().updateTapExcludeRegion(getWindow(), hashCode(),
222 mLocationOnScreen[0], mLocationOnScreen[1], getWidth(), getHeight());
223 } catch (RemoteException e) {
224 e.rethrowAsRuntimeException();
225 }
226 }
227
Andrii Kuliand3134692017-06-26 14:57:02 -0700228 @Override
229 public boolean onTouchEvent(MotionEvent event) {
230 return injectInputEvent(event) || super.onTouchEvent(event);
231 }
232
233 @Override
234 public boolean onGenericMotionEvent(MotionEvent event) {
235 if (event.isFromSource(InputDevice.SOURCE_CLASS_POINTER)) {
236 if (injectInputEvent(event)) {
237 return true;
238 }
239 }
240 return super.onGenericMotionEvent(event);
241 }
242
243 private boolean injectInputEvent(InputEvent event) {
244 if (mInputForwarder != null) {
245 try {
246 return mInputForwarder.forwardEvent(event);
247 } catch (RemoteException e) {
248 e.rethrowAsRuntimeException();
249 }
250 }
251 return false;
252 }
253
254 private class SurfaceCallback implements SurfaceHolder.Callback {
255 @Override
256 public void surfaceCreated(SurfaceHolder surfaceHolder) {
Andrii Kuliand93da5c2017-07-22 14:59:51 -0700257 mSurface = mSurfaceView.getHolder().getSurface();
Andrii Kuliand3134692017-06-26 14:57:02 -0700258 if (mVirtualDisplay == null) {
Andrii Kuliand3134692017-06-26 14:57:02 -0700259 initVirtualDisplay();
260 if (mVirtualDisplay != null && mActivityViewCallback != null) {
261 mActivityViewCallback.onActivityViewReady(ActivityView.this);
262 }
263 } else {
264 mVirtualDisplay.setSurface(surfaceHolder.getSurface());
265 }
Andrii Kulian4b6599e2018-01-15 17:24:08 -0800266 updateLocation();
Andrii Kuliand3134692017-06-26 14:57:02 -0700267 }
268
269 @Override
270 public void surfaceChanged(SurfaceHolder surfaceHolder, int format, int width, int height) {
271 if (mVirtualDisplay != null) {
272 mVirtualDisplay.resize(width, height, getBaseDisplayDensity());
273 }
Andrii Kulian4b6599e2018-01-15 17:24:08 -0800274 updateLocation();
Andrii Kuliand3134692017-06-26 14:57:02 -0700275 }
276
277 @Override
278 public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
279 mSurface.release();
280 mSurface = null;
281 if (mVirtualDisplay != null) {
282 mVirtualDisplay.setSurface(null);
283 }
Andrii Kulian4b6599e2018-01-15 17:24:08 -0800284 cleanTapExcludeRegion();
Andrii Kuliand3134692017-06-26 14:57:02 -0700285 }
286 }
287
288 private void initVirtualDisplay() {
289 if (mVirtualDisplay != null) {
290 throw new IllegalStateException("Trying to initialize for the second time.");
291 }
292
293 final int width = mSurfaceView.getWidth();
294 final int height = mSurfaceView.getHeight();
295 final DisplayManager displayManager = mContext.getSystemService(DisplayManager.class);
296 mVirtualDisplay = displayManager.createVirtualDisplay(
297 DISPLAY_NAME + "@" + System.identityHashCode(this),
298 width, height, getBaseDisplayDensity(), mSurface, 0 /* flags */);
299 if (mVirtualDisplay == null) {
300 Log.e(TAG, "Failed to initialize ActivityView");
301 return;
302 }
303
304 mInputForwarder = InputManager.getInstance().createInputForwarder(
305 mVirtualDisplay.getDisplay().getDisplayId());
306 }
307
308 private void performRelease() {
309 if (!mOpened) {
310 return;
311 }
312
313 mSurfaceView.getHolder().removeCallback(mSurfaceCallback);
314
315 if (mInputForwarder != null) {
316 mInputForwarder = null;
317 }
Andrii Kulian4b6599e2018-01-15 17:24:08 -0800318 cleanTapExcludeRegion();
Andrii Kuliand3134692017-06-26 14:57:02 -0700319
320 final boolean displayReleased;
321 if (mVirtualDisplay != null) {
322 mVirtualDisplay.release();
323 mVirtualDisplay = null;
324 displayReleased = true;
325 } else {
326 displayReleased = false;
327 }
328
329 if (mSurface != null) {
330 mSurface.release();
331 mSurface = null;
332 }
333
334 if (displayReleased && mActivityViewCallback != null) {
335 mActivityViewCallback.onActivityViewDestroyed(this);
336 }
337
338 mGuard.close();
339 mOpened = false;
340 }
341
Andrii Kulian4b6599e2018-01-15 17:24:08 -0800342 /** Report to server that tap exclude region on hosting display should be cleared. */
343 private void cleanTapExcludeRegion() {
344 // Update tap exclude region with an empty rect to clean the state on server.
345 try {
346 WindowManagerGlobal.getWindowSession().updateTapExcludeRegion(getWindow(), hashCode(),
347 0 /* left */, 0 /* top */, 0 /* width */, 0 /* height */);
348 } catch (RemoteException e) {
349 e.rethrowAsRuntimeException();
350 }
351 }
352
Andrii Kuliand3134692017-06-26 14:57:02 -0700353 /** Get density of the hosting display. */
354 private int getBaseDisplayDensity() {
355 final WindowManager wm = mContext.getSystemService(WindowManager.class);
356 final DisplayMetrics metrics = new DisplayMetrics();
357 wm.getDefaultDisplay().getMetrics(metrics);
358 return metrics.densityDpi;
359 }
360
361 @Override
362 protected void finalize() throws Throwable {
363 try {
364 if (mGuard != null) {
365 mGuard.warnIfOpen();
366 performRelease();
367 }
368 } finally {
369 super.finalize();
370 }
371 }
372}