OSDN Git Service

DO NOT MERGE VirtualDisplay wrapper with input forwarding
authorAndrii Kulian <akulian@google.com>
Mon, 26 Jun 2017 21:57:02 +0000 (14:57 -0700)
committerAndrii Kulian <akulian@google.com>
Fri, 21 Jul 2017 20:14:37 +0000 (13:14 -0700)
New version of ActivityView that doesn't use expensive
ActivityContainer, but utilizes VirtualDisplays instead.

Creation of this view is only allowed for callers who have
android.Manifest.permission.INJECT_EVENTS permission.

Launching activities into this container is restricted by
the same rules that apply to launching on VirtualDisplays:
- Owner is allowed to launch its own activities.
- If activity that's being launched is not from the same
  app, then it must be embeddable and launcher must have
  permission to embed.

Bug: 63338670
Test: go/wm-smoke
Change-Id: Id9a25752367ebe8e59d2fc21c5d9de5cf597ea01

Android.mk
core/java/android/app/ActivityView.java [new file with mode: 0644]
core/java/android/app/IInputForwarder.aidl [new file with mode: 0644]
core/java/android/hardware/input/IInputManager.aidl
core/java/android/hardware/input/InputManager.java
services/core/java/com/android/server/input/InputForwarder.java [new file with mode: 0644]
services/core/java/com/android/server/input/InputManagerService.java

index 74717ff..69c8c2c 100644 (file)
@@ -82,6 +82,7 @@ LOCAL_SRC_FILES += \
        core/java/android/app/ITaskStackListener.aidl \
        core/java/android/app/IBackupAgent.aidl \
        core/java/android/app/IEphemeralResolver.aidl \
+       core/java/android/app/IInputForwarder.aidl \
        core/java/android/app/IInstantAppResolver.aidl \
        core/java/android/app/IInstrumentationWatcher.aidl \
        core/java/android/app/INotificationManager.aidl \
diff --git a/core/java/android/app/ActivityView.java b/core/java/android/app/ActivityView.java
new file mode 100644 (file)
index 0000000..5dd47ac
--- /dev/null
@@ -0,0 +1,335 @@
+/**
+ * Copyright (c) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.content.Intent;
+import android.hardware.display.DisplayManager;
+import android.hardware.display.VirtualDisplay;
+import android.hardware.input.InputManager;
+import android.os.RemoteException;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.InputDevice;
+import android.view.InputEvent;
+import android.view.MotionEvent;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+
+import dalvik.system.CloseGuard;
+
+/**
+ * Activity container that allows launching activities into itself and does input forwarding.
+ * <p>Creation of this view is only allowed to callers who have
+ * {@link android.Manifest.permission#INJECT_EVENTS} permission.
+ * <p>Activity launching into this container is restricted by the same rules that apply to launching
+ * on VirtualDisplays.
+ * @hide
+ */
+public class ActivityView extends ViewGroup {
+
+    private static final String DISPLAY_NAME = "ActivityViewVirtualDisplay";
+    private static final String TAG = "ActivityView";
+
+    private VirtualDisplay mVirtualDisplay;
+    private final SurfaceView mSurfaceView;
+    private Surface mSurface;
+
+    private final SurfaceCallback mSurfaceCallback;
+    private StateCallback mActivityViewCallback;
+
+    private IInputForwarder mInputForwarder;
+
+    private final CloseGuard mGuard = CloseGuard.get();
+    private boolean mOpened; // Protected by mGuard.
+
+    public ActivityView(Context context) {
+        this(context, null /* attrs */);
+    }
+
+    public ActivityView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0 /* defStyle */);
+    }
+
+    public ActivityView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+
+        mSurfaceView = new SurfaceView(context);
+        mSurfaceCallback = new SurfaceCallback();
+        mSurfaceView.getHolder().addCallback(mSurfaceCallback);
+        addView(mSurfaceView);
+
+        mOpened = true;
+        mGuard.open("release");
+    }
+
+    /** Callback that notifies when the container is ready or destroyed. */
+    public abstract static class StateCallback {
+        /**
+         * Called when the container is ready for launching activities. Calling
+         * {@link #startActivity(Intent)} prior to this callback will result in an
+         * {@link IllegalStateException}.
+         *
+         * @see #startActivity(Intent)
+         */
+        public abstract void onActivityViewReady(ActivityView view);
+        /**
+         * Called when the container can no longer launch activities. Calling
+         * {@link #startActivity(Intent)} after this callback will result in an
+         * {@link IllegalStateException}.
+         *
+         * @see #startActivity(Intent)
+         */
+        public abstract void onActivityViewDestroyed(ActivityView view);
+    }
+
+    /**
+     * Set the callback to be notified about state changes.
+     * <p>This class must finish initializing before {@link #startActivity(Intent)} can be called.
+     * <p>Note: If the instance was ready prior to this call being made, then
+     * {@link StateCallback#onActivityViewReady(ActivityView)} will be called from within
+     * this method call.
+     *
+     * @param callback The callback to report events to.
+     *
+     * @see StateCallback
+     * @see #startActivity(Intent)
+     */
+    public void setCallback(StateCallback callback) {
+        mActivityViewCallback = callback;
+
+        if (mVirtualDisplay != null && mActivityViewCallback != null) {
+            mActivityViewCallback.onActivityViewReady(this);
+        }
+    }
+
+    /**
+     * Launch a new activity into this container.
+     * <p>Activity resolved by the provided {@link Intent} must have
+     * {@link android.R.attr#resizeableActivity} attribute set to {@code true} in order to be
+     * launched here. Also, if activity is not owned by the owner of this container, it must allow
+     * embedding and the caller must have permission to embed.
+     * <p>Note: This class must finish initializing and
+     * {@link StateCallback#onActivityViewReady(ActivityView)} callback must be triggered before
+     * this method can be called.
+     *
+     * @param intent Intent used to launch an activity.
+     *
+     * @see StateCallback
+     * @see #startActivity(PendingIntent)
+     */
+    public void startActivity(@NonNull Intent intent) {
+        final ActivityOptions options = prepareActivityOptions();
+        getContext().startActivity(intent, options.toBundle());
+    }
+
+    /**
+     * Launch a new activity into this container.
+     * <p>Activity resolved by the provided {@link PendingIntent} must have
+     * {@link android.R.attr#resizeableActivity} attribute set to {@code true} in order to be
+     * launched here. Also, if activity is not owned by the owner of this container, it must allow
+     * embedding and the caller must have permission to embed.
+     * <p>Note: This class must finish initializing and
+     * {@link StateCallback#onActivityViewReady(ActivityView)} callback must be triggered before
+     * this method can be called.
+     *
+     * @param pendingIntent Intent used to launch an activity.
+     *
+     * @see StateCallback
+     * @see #startActivity(Intent)
+     */
+    public void startActivity(@NonNull PendingIntent pendingIntent) {
+        final ActivityOptions options = prepareActivityOptions();
+        try {
+            pendingIntent.send(null /* context */, 0 /* code */, null /* intent */,
+                    null /* onFinished */, null /* handler */, null /* requiredPermission */,
+                    options.toBundle());
+        } catch (PendingIntent.CanceledException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * Check if container is ready to launch and create {@link ActivityOptions} to target the
+     * virtual display.
+     */
+    private ActivityOptions prepareActivityOptions() {
+        if (mVirtualDisplay == null) {
+            throw new IllegalStateException(
+                    "Trying to start activity before ActivityView is ready.");
+        }
+        final ActivityOptions options = ActivityOptions.makeBasic();
+        options.setLaunchDisplayId(mVirtualDisplay.getDisplay().getDisplayId());
+        return options;
+    }
+
+    /**
+     * Release this container. Activity launching will no longer be permitted.
+     * <p>Note: Calling this method is allowed after
+     * {@link StateCallback#onActivityViewReady(ActivityView)} callback was triggered and before
+     * {@link StateCallback#onActivityViewDestroyed(ActivityView)}.
+     *
+     * @see StateCallback
+     */
+    public void release() {
+        if (mVirtualDisplay == null) {
+            throw new IllegalStateException(
+                    "Trying to release container that is not initialized.");
+        }
+        performRelease();
+    }
+
+    @Override
+    public void onLayout(boolean changed, int l, int t, int r, int b) {
+        mSurfaceView.layout(0 /* left */, 0 /* top */, r - l /* right */, b - t /* bottom */);
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        return injectInputEvent(event) || super.onTouchEvent(event);
+    }
+
+    @Override
+    public boolean onGenericMotionEvent(MotionEvent event) {
+        if (event.isFromSource(InputDevice.SOURCE_CLASS_POINTER)) {
+            if (injectInputEvent(event)) {
+                return true;
+            }
+        }
+        return super.onGenericMotionEvent(event);
+    }
+
+    private boolean injectInputEvent(InputEvent event) {
+        if (mInputForwarder != null) {
+            try {
+                return mInputForwarder.forwardEvent(event);
+            } catch (RemoteException e) {
+                e.rethrowAsRuntimeException();
+            }
+        }
+        return false;
+    }
+
+    private class SurfaceCallback implements SurfaceHolder.Callback {
+        @Override
+        public void surfaceCreated(SurfaceHolder surfaceHolder) {
+            if (mVirtualDisplay == null) {
+                mSurface = mSurfaceView.getHolder().getSurface();
+                initVirtualDisplay();
+                if (mVirtualDisplay != null && mActivityViewCallback != null) {
+                    mActivityViewCallback.onActivityViewReady(ActivityView.this);
+                }
+            } else {
+                mVirtualDisplay.setSurface(surfaceHolder.getSurface());
+            }
+        }
+
+        @Override
+        public void surfaceChanged(SurfaceHolder surfaceHolder, int format, int width, int height) {
+            if (mVirtualDisplay != null) {
+                mVirtualDisplay.resize(width, height, getBaseDisplayDensity());
+            }
+        }
+
+        @Override
+        public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
+            mSurface.release();
+            mSurface = null;
+            if (mVirtualDisplay != null) {
+                mVirtualDisplay.setSurface(null);
+            }
+        }
+    }
+
+    private void initVirtualDisplay() {
+        if (mVirtualDisplay != null) {
+            throw new IllegalStateException("Trying to initialize for the second time.");
+        }
+
+        final int width = mSurfaceView.getWidth();
+        final int height = mSurfaceView.getHeight();
+        final DisplayManager displayManager = mContext.getSystemService(DisplayManager.class);
+        mVirtualDisplay = displayManager.createVirtualDisplay(
+                DISPLAY_NAME + "@" + System.identityHashCode(this),
+                width, height, getBaseDisplayDensity(), mSurface, 0 /* flags */);
+        if (mVirtualDisplay == null) {
+            Log.e(TAG, "Failed to initialize ActivityView");
+            return;
+        }
+
+        mInputForwarder = InputManager.getInstance().createInputForwarder(
+                mVirtualDisplay.getDisplay().getDisplayId());
+    }
+
+    private void performRelease() {
+        if (!mOpened) {
+            return;
+        }
+
+        mSurfaceView.getHolder().removeCallback(mSurfaceCallback);
+
+        if (mInputForwarder != null) {
+            mInputForwarder = null;
+        }
+
+        final boolean displayReleased;
+        if (mVirtualDisplay != null) {
+            mVirtualDisplay.release();
+            mVirtualDisplay = null;
+            displayReleased = true;
+        } else {
+            displayReleased = false;
+        }
+
+        if (mSurface != null) {
+            mSurface.release();
+            mSurface = null;
+        }
+
+        if (displayReleased && mActivityViewCallback != null) {
+            mActivityViewCallback.onActivityViewDestroyed(this);
+        }
+
+        mGuard.close();
+        mOpened = false;
+    }
+
+    /** Get density of the hosting display. */
+    private int getBaseDisplayDensity() {
+        final WindowManager wm = mContext.getSystemService(WindowManager.class);
+        final DisplayMetrics metrics = new DisplayMetrics();
+        wm.getDefaultDisplay().getMetrics(metrics);
+        return metrics.densityDpi;
+    }
+
+    @Override
+    protected void finalize() throws Throwable {
+        try {
+            if (mGuard != null) {
+                mGuard.warnIfOpen();
+                performRelease();
+            }
+        } finally {
+            super.finalize();
+        }
+    }
+}
diff --git a/core/java/android/app/IInputForwarder.aidl b/core/java/android/app/IInputForwarder.aidl
new file mode 100644 (file)
index 0000000..d6be63e
--- /dev/null
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2017, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.view.InputEvent;
+
+/**
+ * Forwards input events into owned activity container, used in {@link android.app.ActivityView}.
+ * To forward input to other apps {@link android.Manifest.permission.INJECT_EVENTS} permission is
+ * required.
+ * @hide
+ */
+interface IInputForwarder {
+    boolean forwardEvent(in InputEvent event);
+}
\ No newline at end of file
index 4586316..9e0c680 100644 (file)
@@ -16,6 +16,7 @@
 
 package android.hardware.input;
 
+import android.app.IInputForwarder;
 import android.hardware.input.InputDeviceIdentifier;
 import android.hardware.input.KeyboardLayout;
 import android.hardware.input.IInputDevicesChangedListener;
@@ -88,4 +89,7 @@ interface IInputManager {
     void setCustomPointerIcon(in PointerIcon icon);
 
     void requestPointerCapture(IBinder windowToken, boolean enabled);
+
+    /** Create input forwarder to deliver touch events to owned display. */
+    IInputForwarder createInputForwarder(int displayId);
 }
index 4898c1a..c531a89 100644 (file)
@@ -19,8 +19,9 @@ package android.hardware.input;
 import android.annotation.IntDef;
 import android.annotation.Nullable;
 import android.annotation.SdkConstant;
-import android.annotation.SystemService;
 import android.annotation.SdkConstant.SdkConstantType;
+import android.annotation.SystemService;
+import android.app.IInputForwarder;
 import android.content.Context;
 import android.media.AudioAttributes;
 import android.os.Binder;
@@ -971,6 +972,25 @@ public final class InputManager {
         }
     }
 
+
+    /**
+     * Create an {@link IInputForwarder} targeted to provided display.
+     * {@link android.Manifest.permission.INJECT_EVENTS} permission is required to call this method.
+     *
+     * @param displayId Id of the target display where input events should be forwarded.
+     *                  Display must exist and must be owned by the caller.
+     * @return The forwarder instance.
+     *
+     * @hide
+     */
+    public IInputForwarder createInputForwarder(int displayId) {
+        try {
+            return mIm.createInputForwarder(displayId);
+        } catch (RemoteException ex) {
+            throw ex.rethrowFromSystemServer();
+        }
+    }
+
     private void populateInputDevicesLocked() {
         if (mInputDevicesChangedListener == null) {
             final InputDevicesChangedListener listener = new InputDevicesChangedListener();
diff --git a/services/core/java/com/android/server/input/InputForwarder.java b/services/core/java/com/android/server/input/InputForwarder.java
new file mode 100644 (file)
index 0000000..bebbc93
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.input;
+
+import android.app.IInputForwarder;
+import android.hardware.input.InputManagerInternal;
+import android.view.InputEvent;
+import android.os.Binder;
+
+import com.android.server.LocalServices;
+
+import static android.hardware.input.InputManager.INJECT_INPUT_EVENT_MODE_ASYNC;
+
+/**
+ * Basic implementation of {@link IInputForwarder}.
+ */
+class InputForwarder extends IInputForwarder.Stub {
+
+    private final InputManagerInternal mInputManagerInternal;
+    private final int mDisplayId;
+
+    InputForwarder(int displayId) {
+        mDisplayId = displayId;
+        mInputManagerInternal = LocalServices.getService(InputManagerInternal.class);
+    }
+
+    @Override
+    public boolean forwardEvent(InputEvent event) {
+        return mInputManagerInternal.injectInputEvent(event, mDisplayId,
+                INJECT_INPUT_EVENT_MODE_ASYNC);
+    }
+}
\ No newline at end of file
index 717efbf..fa9b107 100644 (file)
@@ -37,6 +37,7 @@ import com.android.server.Watchdog;
 import org.xmlpull.v1.XmlPullParser;
 
 import android.Manifest;
+import android.app.IInputForwarder;
 import android.app.Notification;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
@@ -57,6 +58,7 @@ import android.content.res.Resources.NotFoundException;
 import android.content.res.TypedArray;
 import android.content.res.XmlResourceParser;
 import android.database.ContentObserver;
+import android.hardware.display.DisplayManager;
 import android.hardware.display.DisplayViewport;
 import android.hardware.input.IInputDevicesChangedListener;
 import android.hardware.input.IInputManager;
@@ -85,6 +87,7 @@ import android.text.TextUtils;
 import android.util.Slog;
 import android.util.SparseArray;
 import android.util.Xml;
+import android.view.Display;
 import android.view.IInputFilter;
 import android.view.IInputFilterHost;
 import android.view.IWindow;
@@ -1862,6 +1865,29 @@ public class InputManagerService extends IInputManager.Stub
         nativeMonitor(mPtr);
     }
 
+    // Binder call
+    @Override
+    public IInputForwarder createInputForwarder(int displayId) throws RemoteException {
+        if (!checkCallingPermission(android.Manifest.permission.INJECT_EVENTS,
+                "createInputForwarder()")) {
+            throw new SecurityException("Requires INJECT_EVENTS permission");
+        }
+        final DisplayManager displayManager = mContext.getSystemService(DisplayManager.class);
+        final Display display = displayManager.getDisplay(displayId);
+        if (display == null) {
+            throw new IllegalArgumentException(
+                    "Can't create input forwarder for non-existent displayId: " + displayId);
+        }
+        final int callingUid = Binder.getCallingUid();
+        final int displayOwnerUid = display.getOwnerUid();
+        if (callingUid != displayOwnerUid) {
+            throw new SecurityException(
+                    "Only owner of the display can forward input events to it.");
+        }
+
+        return new InputForwarder(displayId);
+    }
+
     // Native callback.
     private void notifyConfigurationChanged(long whenNanos) {
         mWindowManagerCallbacks.notifyConfigurationChanged();