ActivityView.java revision a1dbe9cbfa0610107df7c47a4d5388a8e484d7d7
1/**
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.app.ActivityManager.StackInfo;
21import 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.os.UserHandle;
28import android.util.AttributeSet;
29import android.util.DisplayMetrics;
30import android.util.Log;
31import android.view.IWindowManager;
32import android.view.InputDevice;
33import android.view.InputEvent;
34import android.view.MotionEvent;
35import android.view.Surface;
36import android.view.SurfaceHolder;
37import android.view.SurfaceView;
38import android.view.ViewGroup;
39import android.view.WindowManager;
40import android.view.WindowManagerGlobal;
41
42import dalvik.system.CloseGuard;
43
44import java.util.List;
45
46/**
47 * Activity container that allows launching activities into itself and does input forwarding.
48 * <p>Creation of this view is only allowed to callers who have
49 * {@link android.Manifest.permission#INJECT_EVENTS} permission.
50 * <p>Activity launching into this container is restricted by the same rules that apply to launching
51 * on VirtualDisplays.
52 * @hide
53 */
54public class ActivityView extends ViewGroup {
55
56    private static final String DISPLAY_NAME = "ActivityViewVirtualDisplay";
57    private static final String TAG = "ActivityView";
58
59    private VirtualDisplay mVirtualDisplay;
60    private final SurfaceView mSurfaceView;
61    private Surface mSurface;
62
63    private final SurfaceCallback mSurfaceCallback;
64    private StateCallback mActivityViewCallback;
65
66    private IActivityManager mActivityManager;
67    private IInputForwarder mInputForwarder;
68    // Temp container to store view coordinates on screen.
69    private final int[] mLocationOnScreen = new int[2];
70
71    private TaskStackListener mTaskStackListener;
72
73    private final CloseGuard mGuard = CloseGuard.get();
74    private boolean mOpened; // Protected by mGuard.
75
76    public ActivityView(Context context) {
77        this(context, null /* attrs */);
78    }
79
80    public ActivityView(Context context, AttributeSet attrs) {
81        this(context, attrs, 0 /* defStyle */);
82    }
83
84    public ActivityView(Context context, AttributeSet attrs, int defStyle) {
85        super(context, attrs, defStyle);
86
87        mActivityManager = ActivityManager.getService();
88        mSurfaceView = new SurfaceView(context);
89        mSurfaceCallback = new SurfaceCallback();
90        mSurfaceView.getHolder().addCallback(mSurfaceCallback);
91        addView(mSurfaceView);
92
93        mOpened = true;
94        mGuard.open("release");
95    }
96
97    /** Callback that notifies when the container is ready or destroyed. */
98    public abstract static class StateCallback {
99        /**
100         * Called when the container is ready for launching activities. Calling
101         * {@link #startActivity(Intent)} prior to this callback will result in an
102         * {@link IllegalStateException}.
103         *
104         * @see #startActivity(Intent)
105         */
106        public abstract void onActivityViewReady(ActivityView view);
107        /**
108         * Called when the container can no longer launch activities. Calling
109         * {@link #startActivity(Intent)} after this callback will result in an
110         * {@link IllegalStateException}.
111         *
112         * @see #startActivity(Intent)
113         */
114        public abstract void onActivityViewDestroyed(ActivityView view);
115        /**
116         * Called when a task is moved to the front of the stack inside the container.
117         * This is a filtered version of {@link TaskStackListener}
118         */
119        public void onTaskMovedToFront(ActivityManager.StackInfo stackInfo) { }
120    }
121
122    /**
123     * Set the callback to be notified about state changes.
124     * <p>This class must finish initializing before {@link #startActivity(Intent)} can be called.
125     * <p>Note: If the instance was ready prior to this call being made, then
126     * {@link StateCallback#onActivityViewReady(ActivityView)} will be called from within
127     * this method call.
128     *
129     * @param callback The callback to report events to.
130     *
131     * @see StateCallback
132     * @see #startActivity(Intent)
133     */
134    public void setCallback(StateCallback callback) {
135        mActivityViewCallback = callback;
136
137        if (mVirtualDisplay != null && mActivityViewCallback != null) {
138            mActivityViewCallback.onActivityViewReady(this);
139        }
140    }
141
142    /**
143     * Launch a new activity into this container.
144     * <p>Activity resolved by the provided {@link Intent} must have
145     * {@link android.R.attr#resizeableActivity} attribute set to {@code true} in order to be
146     * launched here. Also, if activity is not owned by the owner of this container, it must allow
147     * embedding and the caller must have permission to embed.
148     * <p>Note: This class must finish initializing and
149     * {@link StateCallback#onActivityViewReady(ActivityView)} callback must be triggered before
150     * this method can be called.
151     *
152     * @param intent Intent used to launch an activity.
153     *
154     * @see StateCallback
155     * @see #startActivity(PendingIntent)
156     */
157    public void startActivity(@NonNull Intent intent) {
158        final ActivityOptions options = prepareActivityOptions();
159        getContext().startActivity(intent, options.toBundle());
160    }
161
162    /**
163     * Launch a new activity into this container.
164     * <p>Activity resolved by the provided {@link Intent} must have
165     * {@link android.R.attr#resizeableActivity} attribute set to {@code true} in order to be
166     * launched here. Also, if activity is not owned by the owner of this container, it must allow
167     * embedding and the caller must have permission to embed.
168     * <p>Note: This class must finish initializing and
169     * {@link StateCallback#onActivityViewReady(ActivityView)} callback must be triggered before
170     * this method can be called.
171     *
172     * @param intent Intent used to launch an activity.
173     * @param user The UserHandle of the user to start this activity for.
174     *
175     *
176     * @see StateCallback
177     * @see #startActivity(PendingIntent)
178     */
179    public void startActivity(@NonNull Intent intent, UserHandle user) {
180        final ActivityOptions options = prepareActivityOptions();
181        getContext().startActivityAsUser(intent, options.toBundle(), user);
182    }
183
184    /**
185     * Launch a new activity into this container.
186     * <p>Activity resolved by the provided {@link PendingIntent} must have
187     * {@link android.R.attr#resizeableActivity} attribute set to {@code true} in order to be
188     * launched here. Also, if activity is not owned by the owner of this container, it must allow
189     * embedding and the caller must have permission to embed.
190     * <p>Note: This class must finish initializing and
191     * {@link StateCallback#onActivityViewReady(ActivityView)} callback must be triggered before
192     * this method can be called.
193     *
194     * @param pendingIntent Intent used to launch an activity.
195     *
196     * @see StateCallback
197     * @see #startActivity(Intent)
198     */
199    public void startActivity(@NonNull PendingIntent pendingIntent) {
200        final ActivityOptions options = prepareActivityOptions();
201        try {
202            pendingIntent.send(null /* context */, 0 /* code */, null /* intent */,
203                    null /* onFinished */, null /* handler */, null /* requiredPermission */,
204                    options.toBundle());
205        } catch (PendingIntent.CanceledException e) {
206            throw new RuntimeException(e);
207        }
208    }
209
210    /**
211     * Check if container is ready to launch and create {@link ActivityOptions} to target the
212     * virtual display.
213     */
214    private ActivityOptions prepareActivityOptions() {
215        if (mVirtualDisplay == null) {
216            throw new IllegalStateException(
217                    "Trying to start activity before ActivityView is ready.");
218        }
219        final ActivityOptions options = ActivityOptions.makeBasic();
220        options.setLaunchDisplayId(mVirtualDisplay.getDisplay().getDisplayId());
221        return options;
222    }
223
224    /**
225     * Release this container. Activity launching will no longer be permitted.
226     * <p>Note: Calling this method is allowed after
227     * {@link StateCallback#onActivityViewReady(ActivityView)} callback was triggered and before
228     * {@link StateCallback#onActivityViewDestroyed(ActivityView)}.
229     *
230     * @see StateCallback
231     */
232    public void release() {
233        if (mVirtualDisplay == null) {
234            throw new IllegalStateException(
235                    "Trying to release container that is not initialized.");
236        }
237        performRelease();
238    }
239
240    /**
241     * Triggers an update of {@link ActivityView}'s location on screen to properly set touch exclude
242     * regions and avoid focus switches by touches on this view.
243     */
244    public void onLocationChanged() {
245        updateLocation();
246    }
247
248    @Override
249    public void onLayout(boolean changed, int l, int t, int r, int b) {
250        mSurfaceView.layout(0 /* left */, 0 /* top */, r - l /* right */, b - t /* bottom */);
251    }
252
253    /** Send current location and size to the WM to set tap exclude region for this view. */
254    private void updateLocation() {
255        try {
256            getLocationOnScreen(mLocationOnScreen);
257            WindowManagerGlobal.getWindowSession().updateTapExcludeRegion(getWindow(), hashCode(),
258                    mLocationOnScreen[0], mLocationOnScreen[1], getWidth(), getHeight());
259        } catch (RemoteException e) {
260            e.rethrowAsRuntimeException();
261        }
262    }
263
264    @Override
265    public boolean onTouchEvent(MotionEvent event) {
266        return injectInputEvent(event) || super.onTouchEvent(event);
267    }
268
269    @Override
270    public boolean onGenericMotionEvent(MotionEvent event) {
271        if (event.isFromSource(InputDevice.SOURCE_CLASS_POINTER)) {
272            if (injectInputEvent(event)) {
273                return true;
274            }
275        }
276        return super.onGenericMotionEvent(event);
277    }
278
279    private boolean injectInputEvent(InputEvent event) {
280        if (mInputForwarder != null) {
281            try {
282                return mInputForwarder.forwardEvent(event);
283            } catch (RemoteException e) {
284                e.rethrowAsRuntimeException();
285            }
286        }
287        return false;
288    }
289
290    private class SurfaceCallback implements SurfaceHolder.Callback {
291        @Override
292        public void surfaceCreated(SurfaceHolder surfaceHolder) {
293            mSurface = mSurfaceView.getHolder().getSurface();
294            if (mVirtualDisplay == null) {
295                initVirtualDisplay();
296                if (mVirtualDisplay != null && mActivityViewCallback != null) {
297                    mActivityViewCallback.onActivityViewReady(ActivityView.this);
298                }
299            } else {
300                mVirtualDisplay.setSurface(surfaceHolder.getSurface());
301            }
302            updateLocation();
303        }
304
305        @Override
306        public void surfaceChanged(SurfaceHolder surfaceHolder, int format, int width, int height) {
307            if (mVirtualDisplay != null) {
308                mVirtualDisplay.resize(width, height, getBaseDisplayDensity());
309            }
310            updateLocation();
311        }
312
313        @Override
314        public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
315            mSurface.release();
316            mSurface = null;
317            if (mVirtualDisplay != null) {
318                mVirtualDisplay.setSurface(null);
319            }
320            cleanTapExcludeRegion();
321        }
322    }
323
324    private void initVirtualDisplay() {
325        if (mVirtualDisplay != null) {
326            throw new IllegalStateException("Trying to initialize for the second time.");
327        }
328
329        final int width = mSurfaceView.getWidth();
330        final int height = mSurfaceView.getHeight();
331        final DisplayManager displayManager = mContext.getSystemService(DisplayManager.class);
332        mVirtualDisplay = displayManager.createVirtualDisplay(
333                DISPLAY_NAME + "@" + System.identityHashCode(this),
334                width, height, getBaseDisplayDensity(), mSurface,
335                DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC
336                        | DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY);
337        if (mVirtualDisplay == null) {
338            Log.e(TAG, "Failed to initialize ActivityView");
339            return;
340        }
341
342        final int displayId = mVirtualDisplay.getDisplay().getDisplayId();
343        final IWindowManager wm = WindowManagerGlobal.getWindowManagerService();
344        try {
345            wm.dontOverrideDisplayInfo(displayId);
346        } catch (RemoteException e) {
347            e.rethrowAsRuntimeException();
348        }
349        mInputForwarder = InputManager.getInstance().createInputForwarder(displayId);
350        mTaskStackListener = new TaskStackListenerImpl();
351        try {
352            mActivityManager.registerTaskStackListener(mTaskStackListener);
353        } catch (RemoteException e) {
354            Log.e(TAG, "Failed to register task stack listener", e);
355        }
356    }
357
358    private void performRelease() {
359        if (!mOpened) {
360            return;
361        }
362
363        mSurfaceView.getHolder().removeCallback(mSurfaceCallback);
364
365        if (mInputForwarder != null) {
366            mInputForwarder = null;
367        }
368        cleanTapExcludeRegion();
369
370        if (mTaskStackListener != null) {
371            try {
372                mActivityManager.unregisterTaskStackListener(mTaskStackListener);
373            } catch (RemoteException e) {
374                Log.e(TAG, "Failed to unregister task stack listener", e);
375            }
376            mTaskStackListener = null;
377        }
378
379        final boolean displayReleased;
380        if (mVirtualDisplay != null) {
381            mVirtualDisplay.release();
382            mVirtualDisplay = null;
383            displayReleased = true;
384        } else {
385            displayReleased = false;
386        }
387
388        if (mSurface != null) {
389            mSurface.release();
390            mSurface = null;
391        }
392
393        if (displayReleased && mActivityViewCallback != null) {
394            mActivityViewCallback.onActivityViewDestroyed(this);
395        }
396
397        mGuard.close();
398        mOpened = false;
399    }
400
401    /** Report to server that tap exclude region on hosting display should be cleared. */
402    private void cleanTapExcludeRegion() {
403        // Update tap exclude region with an empty rect to clean the state on server.
404        try {
405            WindowManagerGlobal.getWindowSession().updateTapExcludeRegion(getWindow(), hashCode(),
406                    0 /* left */, 0 /* top */, 0 /* width */, 0 /* height */);
407        } catch (RemoteException e) {
408            e.rethrowAsRuntimeException();
409        }
410    }
411
412    /** Get density of the hosting display. */
413    private int getBaseDisplayDensity() {
414        final WindowManager wm = mContext.getSystemService(WindowManager.class);
415        final DisplayMetrics metrics = new DisplayMetrics();
416        wm.getDefaultDisplay().getMetrics(metrics);
417        return metrics.densityDpi;
418    }
419
420    @Override
421    protected void finalize() throws Throwable {
422        try {
423            if (mGuard != null) {
424                mGuard.warnIfOpen();
425                performRelease();
426            }
427        } finally {
428            super.finalize();
429        }
430    }
431
432    /**
433     * A task change listener that detects background color change of the topmost stack on our
434     * virtual display and updates the background of the surface view. This background will be shown
435     * when surface view is resized, but the app hasn't drawn its content in new size yet.
436     * It also calls StateCallback.onTaskMovedToFront to notify interested parties that the stack
437     * associated with the {@link ActivityView} has had a Task moved to the front. This is useful
438     * when needing to also bring the host Activity to the foreground at the same time.
439     */
440    private class TaskStackListenerImpl extends TaskStackListener {
441
442        @Override
443        public void onTaskDescriptionChanged(int taskId, ActivityManager.TaskDescription td)
444                throws RemoteException {
445            if (mVirtualDisplay == null) {
446                return;
447            }
448
449            StackInfo stackInfo = getTopMostStackInfo();
450            if (stackInfo == null) {
451                return;
452            }
453            // Found the topmost stack on target display. Now check if the topmost task's
454            // description changed.
455            if (taskId == stackInfo.taskIds[stackInfo.taskIds.length - 1]) {
456                mSurfaceView.setResizeBackgroundColor(td.getBackgroundColor());
457            }
458        }
459
460        @Override
461        public void onTaskMovedToFront(int taskId) throws RemoteException {
462            if (mActivityViewCallback  != null) {
463                StackInfo stackInfo = getTopMostStackInfo();
464                // if StackInfo was null or unrelated to the "move to front" then there's no use
465                // notifying the callback
466                if (stackInfo != null
467                        && taskId == stackInfo.taskIds[stackInfo.taskIds.length - 1]) {
468                    mActivityViewCallback.onTaskMovedToFront(stackInfo);
469                }
470            }
471        }
472
473        private StackInfo getTopMostStackInfo() throws RemoteException {
474            // Find the topmost task on our virtual display - it will define the background
475            // color of the surface view during resizing.
476            final int displayId = mVirtualDisplay.getDisplay().getDisplayId();
477            final List<StackInfo> stackInfoList = mActivityManager.getAllStackInfos();
478
479            // Iterate through stacks from top to bottom.
480            final int stackCount = stackInfoList.size();
481            for (int i = 0; i < stackCount; i++) {
482                final StackInfo stackInfo = stackInfoList.get(i);
483                // Only look for stacks on our virtual display.
484                if (stackInfo.displayId != displayId) {
485                    continue;
486                }
487                // Found the topmost stack on target display.
488                return stackInfo;
489            }
490            return null;
491        }
492    }
493}
494