diff --git a/Android.mk b/Android.mk index 74717ff74c51a..69c8c2cae8587 100644 --- a/Android.mk +++ b/Android.mk @@ -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 index 0000000000000..5dd47acfb9406 --- /dev/null +++ b/core/java/android/app/ActivityView.java @@ -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. + *
Creation of this view is only allowed to callers who have + * {@link android.Manifest.permission#INJECT_EVENTS} permission. + *
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. + *
This class must finish initializing before {@link #startActivity(Intent)} can be called. + *
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. + *
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. + *
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. + *
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. + *
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. + *
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 index 0000000000000..d6be63eb54e5d --- /dev/null +++ b/core/java/android/app/IInputForwarder.aidl @@ -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 diff --git a/core/java/android/hardware/input/IInputManager.aidl b/core/java/android/hardware/input/IInputManager.aidl index 45863167fa1b1..9e0c680cafa1f 100644 --- a/core/java/android/hardware/input/IInputManager.aidl +++ b/core/java/android/hardware/input/IInputManager.aidl @@ -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); } diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java index 4898c1a0802fe..c531a899f45d6 100644 --- a/core/java/android/hardware/input/InputManager.java +++ b/core/java/android/hardware/input/InputManager.java @@ -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 index 0000000000000..bebbc93e9ea59 --- /dev/null +++ b/services/core/java/com/android/server/input/InputForwarder.java @@ -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 diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index 717efbf714db2..fa9b1078e8ad3 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -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();