Experiment with allowing tap to break through to interact with the PIP.

Test: Enable SysUI tuner, tap once on PIP to interact with the activity.
      This is only experimental behaviour, and
      android.server.cts.ActivityManagerPinnedStackTests will be updated
      accordingly if we keep this behavior.

Change-Id: I278ab8c360c44718cfcac0fd761f476a875f9b15
This commit is contained in:
Winson Chung
2016-11-02 18:11:36 -07:00
parent 2bbf96cfde
commit 15504af3f7
9 changed files with 428 additions and 16 deletions

View File

@@ -415,6 +415,18 @@
android:launchMode="singleTop"
android:excludeFromRecents="true" />
<activity
android:name=".pip.phone.PipMenuActivity"
android:theme="@style/PipPhoneOverlayControlTheme"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
android:excludeFromRecents="true"
android:exported="false"
android:resizeableActivity="true"
android:supportsPictureInPicture="true"
android:stateNotNeeded="true"
android:taskAffinity=""
androidprv:alwaysFocusable="true" />
<!-- platform logo easter egg activity -->
<activity
android:name=".DessertCase"

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2014 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.
-->
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#33000000">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center">
<Button
android:id="@+id/expand_pip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textSize="14sp"
android:textColor="#ffffffff"
android:text="@string/pip_phone_expand"
android:fontFamily="sans-serif" />
</LinearLayout>
</FrameLayout>

View File

@@ -1675,6 +1675,9 @@
not appear on production builds ever. -->
<string name="tuner_doze_always_on" translatable="false">Always on</string>
<!-- Making the PIP fullscreen -->
<string name="pip_phone_expand">Expand</string>
<!-- PIP section of the tuner. Non-translatable since it should
not appear on production builds ever. -->
<string name="picture_in_picture" translatable="false">Picture-in-Picture</string>
@@ -1695,4 +1698,12 @@
not appear on production builds ever. -->
<string name="pip_drag_to_dismiss_summary" translatable="false">Drag to the dismiss target at the bottom of the screen to close the PIP</string>
<!-- PIP tap once to break through to the activity. Non-translatable since it should
not appear on production builds ever. -->
<string name="pip_tap_through_title" translatable="false">Tap to interact</string>
<!-- PIP tap once to break through to the activity. Non-translatable since it should
not appear on production builds ever. -->
<string name="pip_tap_through_summary" translatable="false">Tap once to interact with the activity</string>
</resources>

View File

@@ -56,6 +56,24 @@
<item name="android:activityCloseExitAnimation">@anim/forced_resizable_exit</item>
</style>
<style name="PipPhoneOverlayControlTheme" parent="@android:style/Theme.Material">
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowBackground">@drawable/forced_resizable_background</item>
<item name="android:colorBackgroundCacheHint">@null</item>
<item name="android:statusBarColor">@color/transparent</item>
<item name="android:windowAnimationStyle">@style/Animation.PipPhoneOverlayControl</item>
</style>
<style name="Animation.PipPhoneOverlayControl" parent="@android:style/Animation">
<item name="android:activityOpenEnterAnimation">@anim/forced_resizable_enter</item>
<!-- If the target stack doesn't have focus, we do a task to front animation. -->
<item name="android:taskToFrontEnterAnimation">@anim/forced_resizable_enter</item>
<item name="android:activityCloseExitAnimation">@anim/forced_resizable_exit</item>
</style>
<style name="TextAppearance.StatusBar.HeadsUp"
parent="@*android:style/TextAppearance.StatusBar">
</style>

View File

@@ -137,6 +137,12 @@
android:summary="@string/pip_drag_to_dismiss_summary"
sysui:defValue="true" />
<com.android.systemui.tuner.TunerSwitch
android:key="pip_tap_through"
android:title="@string/pip_tap_through_title"
android:summary="@string/pip_tap_through_summary"
sysui:defValue="false" />
</PreferenceScreen>
<PreferenceScreen

View File

@@ -34,6 +34,7 @@ public class PipManager {
private IActivityManager mActivityManager;
private IWindowManager mWindowManager;
private PipMenuActivityController mMenuController;
private PipTouchHandler mTouchHandler;
private PipManager() {}
@@ -46,7 +47,9 @@ public class PipManager {
mActivityManager = ActivityManagerNative.getDefault();
mWindowManager = WindowManagerGlobal.getWindowManagerService();
mTouchHandler = new PipTouchHandler(context, mActivityManager, mWindowManager);
mMenuController = new PipMenuActivityController(context, mActivityManager, mWindowManager);
mTouchHandler = new PipTouchHandler(context, mMenuController, mActivityManager,
mWindowManager);
}
/**

View File

@@ -0,0 +1,137 @@
/*
* Copyright (C) 2016 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.systemui.pip.phone;
import android.annotation.Nullable;
import android.app.Activity;
import android.app.ActivityManager;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.util.Log;
import android.view.View;
import com.android.systemui.R;
/**
* Translucent activity that gets started on top of a task in PIP to allow the user to control it.
*/
public class PipMenuActivity extends Activity {
private static final String TAG = "PipMenuActivity";
public static final int MESSAGE_FINISH_SELF = 2;
private static final long INITIAL_DISMISS_DELAY = 2000;
private static final long POST_INTERACTION_DISMISS_DELAY = 1500;
private Messenger mToControllerMessenger;
private Messenger mMessenger = new Messenger(new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MESSAGE_FINISH_SELF:
finish();
break;
}
}
});
private final Runnable mFinishRunnable = new Runnable() {
@Override
public void run() {
finish();
}
};
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent startingIntet = getIntent();
mToControllerMessenger = startingIntet.getParcelableExtra(
PipMenuActivityController.EXTRA_CONTROLLER_MESSENGER);
setContentView(R.layout.pip_menu_activity);
findViewById(R.id.expand_pip).setOnClickListener((view) -> {
finish();
notifyExpandPip();
});
}
@Override
protected void onStart() {
super.onStart();
notifyActivityVisibility(true);
repostDelayedFinish(INITIAL_DISMISS_DELAY);
}
@Override
public void onUserInteraction() {
repostDelayedFinish(POST_INTERACTION_DISMISS_DELAY);
}
@Override
protected void onStop() {
super.onStop();
finish();
}
@Override
public void finish() {
View v = getWindow().getDecorView();
v.removeCallbacks(mFinishRunnable);
notifyActivityVisibility(false);
super.finish();
overridePendingTransition(0, R.anim.forced_resizable_exit);
}
@Override
public void setTaskDescription(ActivityManager.TaskDescription taskDescription) {
// Do nothing
}
private void notifyActivityVisibility(boolean visible) {
Message m = Message.obtain();
m.what = PipMenuActivityController.MESSAGE_ACTIVITY_VISIBILITY_CHANGED;
m.arg1 = visible ? 1 : 0;
m.replyTo = visible ? mMessenger : null;
try {
mToControllerMessenger.send(m);
} catch (RemoteException e) {
Log.e(TAG, "Could not notify controller of PIP menu visibility", e);
}
}
private void notifyExpandPip() {
Message m = Message.obtain();
m.what = PipMenuActivityController.MESSAGE_EXPAND_PIP;
try {
mToControllerMessenger.send(m);
} catch (RemoteException e) {
Log.e(TAG, "Could not notify controller to expand PIP", e);
}
}
private void repostDelayedFinish(long delay) {
View v = getWindow().getDecorView();
v.removeCallbacks(mFinishRunnable);
v.postDelayed(mFinishRunnable, delay);
}
}

View File

@@ -0,0 +1,126 @@
package com.android.systemui.pip.phone;
import static android.app.ActivityManager.StackId.PINNED_STACK_ID;
import static android.view.WindowManager.INPUT_CONSUMER_PIP;
import android.app.ActivityManager.StackInfo;
import android.app.ActivityOptions;
import android.app.IActivityManager;
import android.content.Context;
import android.content.Intent;
import android.graphics.Rect;
import android.os.Handler;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.os.UserHandle;
import android.util.Log;
import android.view.IWindowManager;
import java.util.ArrayList;
public class PipMenuActivityController {
private static final String TAG = "PipMenuActivityController";
public static final String EXTRA_CONTROLLER_MESSENGER = "messenger";
public static final int MESSAGE_ACTIVITY_VISIBILITY_CHANGED = 1;
public static final int MESSAGE_EXPAND_PIP = 3;
/**
* A listener interface to receive notification on changes in PIP.
*/
public interface Listener {
/**
* Called when the PIP menu visibility changes.
*/
void onPipMenuVisibilityChanged(boolean visible);
}
private Context mContext;
private IActivityManager mActivityManager;
private IWindowManager mWindowManager;
private ArrayList<Listener> mListeners = new ArrayList<>();
private Messenger mToActivityMessenger;
private Messenger mMessenger = new Messenger(new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MESSAGE_ACTIVITY_VISIBILITY_CHANGED: {
boolean visible = msg.arg1 > 0;
int listenerCount = mListeners.size();
for (int i = 0; i < listenerCount; i++) {
mListeners.get(i).onPipMenuVisibilityChanged(visible);
}
mToActivityMessenger = msg.replyTo;
break;
}
case MESSAGE_EXPAND_PIP: {
try {
mActivityManager.resizeStack(PINNED_STACK_ID, null, true, true, true, 225);
} catch (RemoteException e) {
Log.e(TAG, "Error showing PIP menu activity", e);
}
break;
}
}
}
});
public PipMenuActivityController(Context context, IActivityManager activityManager,
IWindowManager windowManager) {
mContext = context;
mActivityManager = activityManager;
mWindowManager = windowManager;
}
/**
* Adds a new menu activity listener.
*/
public void addListener(Listener listener) {
if (!mListeners.contains(listener)) {
mListeners.add(listener);
}
}
/**
* Shows the menu activity.
*/
public void showMenu() {
// Start the menu activity on the top task of the pinned stack
try {
StackInfo pinnedStackInfo = mActivityManager.getStackInfo(PINNED_STACK_ID);
if (pinnedStackInfo != null && pinnedStackInfo.taskIds != null &&
pinnedStackInfo.taskIds.length > 0) {
Intent intent = new Intent(mContext, PipMenuActivity.class);
intent.putExtra(EXTRA_CONTROLLER_MESSENGER, mMessenger);
ActivityOptions options = ActivityOptions.makeBasic();
options.setLaunchTaskId(
pinnedStackInfo.taskIds[pinnedStackInfo.taskIds.length - 1]);
options.setTaskOverlay(true);
mContext.startActivityAsUser(intent, options.toBundle(), UserHandle.CURRENT);
} else {
Log.e(TAG, "No PIP tasks found");
}
} catch (RemoteException e) {
Log.e(TAG, "Error showing PIP menu activity", e);
}
}
/**
* Hides the menu activity.
*/
public void hideMenu() {
if (mToActivityMessenger != null) {
Message m = Message.obtain();
m.what = PipMenuActivity.MESSAGE_FINISH_SELF;
try {
mToActivityMessenger.send(m);
} catch (RemoteException e) {
Log.e(TAG, "Could not notify menu activity to finish", e);
}
mToActivityMessenger = null;
}
}
}

View File

@@ -32,6 +32,7 @@ import android.app.IActivityManager;
import android.content.Context;
import android.graphics.PointF;
import android.graphics.Rect;
import android.os.Handler;
import android.os.Looper;
import android.os.RemoteException;
import android.util.Log;
@@ -61,6 +62,7 @@ public class PipTouchHandler implements TunerService.Tunable {
private static final String TUNER_KEY_SWIPE_TO_DISMISS = "pip_swipe_to_dismiss";
private static final String TUNER_KEY_DRAG_TO_DISMISS = "pip_drag_to_dismiss";
private static final String TUNER_KEY_TAP_THROUGH = "pip_tap_through";
private static final int SNAP_STACK_DURATION = 225;
private static final int DISMISS_STACK_DURATION = 375;
@@ -70,17 +72,19 @@ public class PipTouchHandler implements TunerService.Tunable {
private final IActivityManager mActivityManager;
private final IWindowManager mWindowManager;
private final ViewConfiguration mViewConfig;
private final InputChannel mInputChannel = new InputChannel();
private final PinnedStackListener mPinnedStackListener = new PinnedStackListener();
private final PipMenuListener mMenuListener = new PipMenuListener();
private IPinnedStackController mPinnedStackController;
private final PipInputEventReceiver mInputEventReceiver;
private PipInputEventReceiver mInputEventReceiver;
private PipMenuActivityController mMenuController;
private PipDismissViewController mDismissViewController;
private final PipSnapAlgorithm mSnapAlgorithm;
private PipMotionHelper mMotionHelper;
private boolean mEnableSwipeToDismiss = true;
private boolean mEnableDragToDismiss = true;
private boolean mEnableTapThrough = false;
private final Rect mPinnedStackBounds = new Rect();
private final Rect mBoundedPinnedStackBounds = new Rect();
@@ -97,6 +101,7 @@ public class PipTouchHandler implements TunerService.Tunable {
private final PointF mLastTouch = new PointF();
private boolean mIsDragging;
private boolean mIsSwipingToDismiss;
private boolean mIsTappingThrough;
private int mActivePointerId;
private final FlingAnimationUtils mFlingAnimationUtils;
@@ -120,7 +125,7 @@ public class PipTouchHandler implements TunerService.Tunable {
// To be implemented for input handling over Pip windows
if (event instanceof MotionEvent) {
MotionEvent ev = (MotionEvent) event;
handleTouchEvent(ev);
handled = handleTouchEvent(ev);
}
} finally {
finishInputEvent(event, handled);
@@ -144,13 +149,26 @@ public class PipTouchHandler implements TunerService.Tunable {
}
}
public PipTouchHandler(Context context, IActivityManager activityManager,
IWindowManager windowManager) {
/**
* A listener for the PIP menu activity.
*/
private class PipMenuListener implements PipMenuActivityController.Listener {
@Override
public void onPipMenuVisibilityChanged(boolean visible) {
if (!visible) {
mIsTappingThrough = false;
registerInputConsumer();
} else {
unregisterInputConsumer();
}
}
}
public PipTouchHandler(Context context, PipMenuActivityController menuController,
IActivityManager activityManager, IWindowManager windowManager) {
// Initialize the Pip input consumer
try {
windowManager.destroyInputConsumer(INPUT_CONSUMER_PIP);
windowManager.createInputConsumer(INPUT_CONSUMER_PIP, mInputChannel);
windowManager.registerPinnedStackListener(DEFAULT_DISPLAY, mPinnedStackListener);
} catch (RemoteException e) {
Log.e(TAG, "Failed to create PIP input consumer", e);
@@ -159,22 +177,27 @@ public class PipTouchHandler implements TunerService.Tunable {
mActivityManager = activityManager;
mWindowManager = windowManager;
mViewConfig = ViewConfiguration.get(context);
mInputEventReceiver = new PipInputEventReceiver(mInputChannel, Looper.myLooper());
if (mEnableDragToDismiss) {
mDismissViewController = new PipDismissViewController(context);
}
mMenuController = menuController;
mMenuController.addListener(mMenuListener);
mDismissViewController = new PipDismissViewController(context);
mSnapAlgorithm = new PipSnapAlgorithm(mContext);
mFlingAnimationUtils = new FlingAnimationUtils(context, 2f);
mMotionHelper = new PipMotionHelper(BackgroundThread.getHandler());
registerInputConsumer();
// Register any tuner settings changes
TunerService.get(context).addTunable(this, TUNER_KEY_SWIPE_TO_DISMISS,
TUNER_KEY_DRAG_TO_DISMISS);
TUNER_KEY_DRAG_TO_DISMISS, TUNER_KEY_TAP_THROUGH);
}
@Override
public void onTuningChanged(String key, String newValue) {
if (newValue == null) {
// Reset back to default
mEnableSwipeToDismiss = true;
mEnableDragToDismiss = true;
mEnableTapThrough = false;
mIsTappingThrough = false;
return;
}
switch (key) {
@@ -184,6 +207,10 @@ public class PipTouchHandler implements TunerService.Tunable {
case TUNER_KEY_DRAG_TO_DISMISS:
mEnableDragToDismiss = Integer.parseInt(newValue) != 0;
break;
case TUNER_KEY_TAP_THROUGH:
mEnableTapThrough = Integer.parseInt(newValue) != 0;
mIsTappingThrough = false;
break;
}
}
@@ -192,10 +219,10 @@ public class PipTouchHandler implements TunerService.Tunable {
updateBoundedPinnedStackBounds(false /* updatePinnedStackBounds */);
}
private void handleTouchEvent(MotionEvent ev) {
private boolean handleTouchEvent(MotionEvent ev) {
// Skip touch handling until we are bound to the controller
if (mPinnedStackController == null) {
return;
return true;
}
switch (ev.getAction()) {
@@ -239,6 +266,8 @@ public class PipTouchHandler implements TunerService.Tunable {
float movement = PointF.length(mDownTouch.x - x, mDownTouch.y - y);
if (movement > mViewConfig.getScaledTouchSlop()) {
mIsDragging = true;
mIsTappingThrough = false;
mMenuController.hideMenu();
if (mEnableSwipeToDismiss) {
// TODO: this check can have some buffer so that we only start swiping
// after a significant move out of bounds
@@ -328,7 +357,14 @@ public class PipTouchHandler implements TunerService.Tunable {
}
}
} else {
expandPinnedStackToFullscreen();
if (mEnableTapThrough) {
if (!mIsTappingThrough) {
mMenuController.showMenu();
mIsTappingThrough = true;
}
} else {
expandPinnedStackToFullscreen();
}
}
if (mEnableDragToDismiss) {
mDismissViewController.destroyDismissTarget();
@@ -348,6 +384,7 @@ public class PipTouchHandler implements TunerService.Tunable {
break;
}
}
return !mIsTappingThrough;
}
private void initOrResetVelocityTracker() {
@@ -365,6 +402,32 @@ public class PipTouchHandler implements TunerService.Tunable {
}
}
/**
* Registers the input consumer.
*/
private void registerInputConsumer() {
final InputChannel inputChannel = new InputChannel();
try {
mWindowManager.destroyInputConsumer(INPUT_CONSUMER_PIP);
mWindowManager.createInputConsumer(INPUT_CONSUMER_PIP, inputChannel);
} catch (RemoteException e) {
Log.e(TAG, "Failed to create PIP input consumer", e);
}
mInputEventReceiver = new PipInputEventReceiver(inputChannel, Looper.myLooper());
}
/**
* Unregisters the input consumer.
*/
private void unregisterInputConsumer() {
try {
mWindowManager.destroyInputConsumer(INPUT_CONSUMER_PIP);
} catch (RemoteException e) {
Log.e(TAG, "Failed to destroy PIP input consumer", e);
}
mInputEventReceiver.dispose();
}
/**
* Flings the PIP to the closest snap target.
*/