Merge "Initial check-in of picture-in-picture system-ui"

This commit is contained in:
Youngsang Cho
2016-01-15 23:39:14 +00:00
committed by Android (Google) Code Review
13 changed files with 698 additions and 0 deletions

View File

@@ -23,4 +23,12 @@
<!-- Flags enabling default window features. See Window.java -->
<bool name="config_defaultWindowFeatureOptionsPanel">false</bool>
<!-- Default bounds [left top right bottom] on screen for picture-in-picture windows. -->
<string translatable="false" name="config_defaultPictureInPictureBounds">"1420 100 1820 325"</string>
<!-- Bounds [left top right bottom] on screen for picture-in-picture (PIP) windows, when the PIP
is located in center. -->
<string translatable="false" name="config_centeredPictureInPictureBounds">"600 331 1320 749"</string>
</resources>

View File

@@ -2419,6 +2419,10 @@
<!-- Default bounds [left top right bottom] on screen for picture-in-picture windows. -->
<string translatable="false" name="config_defaultPictureInPictureBounds">"0 0 100 100"</string>
<!-- Bounds [left top right bottom] on screen for picture-in-picture (PIP) windows, when the PIP
is located in center. -->
<string translatable="false" name="config_centeredPictureInPictureBounds">"0 0 300 300"</string>
<!-- Controls the snap mode for the docked stack divider
0 - 3 snap targets: left/top has 16:9 ratio, 1:1, and right/bottom has 16:9 ratio
1 - 3 snap targets: fixed ratio, 1:1, (1 - fixed ratio)

View File

@@ -305,6 +305,7 @@
<java-symbol type="bool" name="config_supportSpeakerNearUltrasound" />
<java-symbol type="bool" name="config_freeformWindowManagement" />
<java-symbol type="string" name="config_defaultPictureInPictureBounds" />
<java-symbol type="string" name="config_centeredPictureInPictureBounds" />
<java-symbol type="integer" name="config_wifi_framework_5GHz_preference_boost_threshold" />
<java-symbol type="integer" name="config_wifi_framework_5GHz_preference_boost_factor" />
<java-symbol type="integer" name="config_wifi_framework_5GHz_preference_penalty_threshold" />

View File

@@ -309,6 +309,27 @@
android:launchMode="singleTop"
android:excludeFromRecents="true" />
<!-- started from PipUI -->
<activity
android:name="com.android.systemui.tv.pip.PipMenuActivity"
android:exported="true"
android:theme="@style/PipTheme"
android:launchMode="singleTop"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation|layoutDirection"
android:resizeable="true"
android:supportsPictureInPicture="true"
androidprv:alwaysFocusable="true"
android:excludeFromRecents="true" />
<activity
android:name="com.android.systemui.tv.pip.PipOverlayActivity"
android:exported="true"
android:theme="@style/PipTheme"
android:launchMode="singleTop"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation|layoutDirection"
android:resizeable="true"
android:supportsPictureInPicture="true"
android:excludeFromRecents="true" />
<!-- platform logo easter egg activity -->
<activity
android:name=".DessertCase"

View File

@@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
**
** Copyright 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.
*/
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:layout_alignParentEnd="true"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:paddingStart="10dp"
android:paddingEnd="10dp"
android:background="#88FFFFFF">
<LinearLayout
android:orientation="vertical"
android:layout_gravity="center_vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content" >
<Button android:id="@+id/full"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="@string/pip_fullscreen"
android:textSize="10sp"
android:focusable="true" />
<Button android:id="@+id/exit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="@string/pip_exit"
android:textSize="10sp"
android:focusable="true" />
<Button android:id="@+id/cancel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="@string/pip_cancel"
android:textSize="10sp"
android:focusable="true" />
</LinearLayout>
</FrameLayout>
</RelativeLayout>

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
**
** Copyright 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.
*/
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/guide_overlay"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:padding="3dp"
android:textSize="13sp"
android:textColor="#111111"
android:background="#99EEEEEE"
android:text="@string/pip_hold_home" />
</FrameLayout>

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
/**
* 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.
*/
-->
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<!-- Button to close PIP on PIP UI -->
<string name="pip_exit">Close PIP</string>
<!-- Button to move PIP screen to the fullscreen on PIP UI -->
<string name="pip_fullscreen">Full screen</string>
<!-- Button to play the current media on PIP UI -->
<string name="pip_play">Play</string>
<!-- Button to pause the current media on PIP UI -->
<string name="pip_pause">Pause</string>
<!-- Button to close PIP overlay menu on PIP UI -->
<string name="pip_cancel">Cancel</string>
<!-- Overlay text on PIP -->
<string name="pip_hold_home">Hold HOME to control PIP</string>
</resources>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
/**
* 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.
*/
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android">
<style name="PipTheme" parent="@android:style/Theme.Translucent.NoTitleBar.Fullscreen">
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:backgroundDimEnabled">false</item>
</style>
</resources>

View File

@@ -54,6 +54,7 @@ public class SystemUIApplication extends Application {
com.android.systemui.power.PowerUI.class,
com.android.systemui.media.RingtonePlayer.class,
com.android.systemui.keyboard.KeyboardUI.class,
com.android.systemui.tv.pip.PipUI.class
};
/**

View File

@@ -0,0 +1,295 @@
/*
* 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.tv.pip;
import android.app.ActivityManager.StackInfo;
import android.app.ActivityManagerNative;
import android.app.ActivityOptions;
import android.app.IActivityManager;
import android.app.ITaskStackListener;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Resources;
import android.graphics.Rect;
import android.os.Handler;
import android.os.RemoteException;
import android.util.Log;
import java.util.ArrayList;
import java.util.List;
import static android.app.ActivityManager.StackId.FULLSCREEN_WORKSPACE_STACK_ID;
import static android.app.ActivityManager.StackId.PINNED_STACK_ID;
/**
* Manages the picture-in-picture (PIP) UI and states.
*/
public class PipManager {
private static final String TAG = "PipManager";
private static final boolean DEBUG = false;
private static PipManager sPipManager;
private static final int STATE_NO_PIP = 0;
private static final int STATE_PIP_OVERLAY = 1;
private static final int STATE_PIP_MENU = 2;
private Context mContext;
private IActivityManager mActivityManager;
private int mState = STATE_NO_PIP;
private final Handler mHandler = new Handler();
private List<Listener> mListeners = new ArrayList<>();
private Rect mPipBound;
private Rect mMenuModePipBound;
private boolean mInitialized;
private final Runnable mOnActivityPinnedRunnable = new Runnable() {
@Override
public void run() {
StackInfo stackInfo = null;
try {
stackInfo = mActivityManager.getStackInfo(PINNED_STACK_ID);
if (stackInfo == null) {
Log.w(TAG, "There is no pinned stack");
return;
}
} catch (RemoteException e) {
Log.e(TAG, "getStackInfo failed", e);
return;
}
if (DEBUG) Log.d(TAG, "PINNED_STACK:" + stackInfo);
mState = STATE_PIP_OVERLAY;
launchPipOverlayActivity();
}
};
private final Runnable mOnTaskStackChanged = new Runnable() {
@Override
public void run() {
if (mState != STATE_NO_PIP) {
// TODO: check whether PIP task is closed.
}
}
};
private final BroadcastReceiver mPipButtonReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (DEBUG) Log.d(TAG, "PIP button pressed");
if (!hasPipTasks()) {
startPip();
} else if (mState == STATE_PIP_OVERLAY) {
showPipMenu();
}
}
};
private PipManager() { }
/**
* Initializes {@link PipManager}.
*/
public void initialize(Context context) {
if (mInitialized) {
return;
}
mInitialized = true;
mContext = context;
Resources res = context.getResources();
mPipBound = Rect.unflattenFromString(res.getString(
com.android.internal.R.string.config_defaultPictureInPictureBounds));
mMenuModePipBound = Rect.unflattenFromString(res.getString(
com.android.internal.R.string.config_centeredPictureInPictureBounds));
mActivityManager = ActivityManagerNative.getDefault();
TaskStackListener taskStackListener = new TaskStackListener();
IActivityManager iam = ActivityManagerNative.getDefault();
try {
iam.registerTaskStackListener(taskStackListener);
} catch (RemoteException e) {
Log.e(TAG, "registerTaskStackListener failed", e);
}
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(Intent.ACTION_PICTURE_IN_PICTURE_BUTTON);
mContext.registerReceiver(mPipButtonReceiver, intentFilter);
}
private void startPip() {
try {
mActivityManager.moveTopActivityToPinnedStack(FULLSCREEN_WORKSPACE_STACK_ID, mPipBound);
} catch (RemoteException|IllegalArgumentException e) {
Log.e(TAG, "moveTopActivityToPinnedStack failed", e);
}
}
/**
* Closes PIP (PIPped activity and PIP system UI).
*/
public void closePip() {
mState = STATE_NO_PIP;
StackInfo stackInfo = null;
try {
stackInfo = mActivityManager.getStackInfo(PINNED_STACK_ID);
if (stackInfo == null) {
return;
}
} catch (RemoteException e) {
Log.e(TAG, "getStackInfo failed", e);
return;
}
for (int taskId : stackInfo.taskIds) {
try {
mActivityManager.removeTask(taskId);
} catch (RemoteException e) {
Log.e(TAG, "removeTask failed", e);
}
}
}
/**
* Moves the PIPped activity to the fullscreen and closes PIP system UI.
*/
public void movePipToFullscreen() {
mState = STATE_NO_PIP;
for (int i = mListeners.size() - 1; i >= 0; --i) {
mListeners.get(i).onMoveToFullscreen();
}
try {
mActivityManager.moveTasksToFullscreenStack(PINNED_STACK_ID, true);
} catch (RemoteException e) {
Log.e(TAG, "moveTasksToFullscreenStack failed", e);
}
}
/**
* Shows PIP overlay UI by launching {@link PipOverlayActivity}. It also locates the pinned
* stack to the default PIP bound {@link com.android.internal.R.string
* .config_defaultPictureInPictureBounds}.
*/
public void showPipOverlay() {
if (DEBUG) Log.d(TAG, "showPipOverlay()");
try {
mActivityManager.resizeStack(PINNED_STACK_ID, mPipBound, false);
} catch (Exception e) {
Log.e(TAG, "resizeStack failed", e);
closePip();
return;
}
mState = STATE_PIP_OVERLAY;
launchPipOverlayActivity();
}
/**
* Shows PIP menu UI by launching {@link PipMenuActivity}. It also locates the pinned
* stack to the centered PIP bound {@link com.android.internal.R.string
* .config_centeredPictureInPictureBounds}.
*/
public void showPipMenu() {
if (DEBUG) Log.d(TAG, "showPipMenu()");
try {
mActivityManager.resizeStack(PINNED_STACK_ID, mMenuModePipBound, false);
} catch (Exception e) {
Log.e(TAG, "resizeStack failed", e);
closePip();
return;
}
mState = STATE_PIP_MENU;
for (int i = mListeners.size() - 1; i >= 0; --i) {
mListeners.get(i).onShowPipMenu();
}
Intent intent = new Intent(mContext, PipMenuActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
final ActivityOptions options = ActivityOptions.makeBasic();
options.setLaunchStackId(PINNED_STACK_ID);
mContext.startActivity(intent, options.toBundle());
}
/**
* Adds {@link Listener}.
*/
public void addListener(Listener listener) {
mListeners.add(listener);
}
/**
* Removes {@link Listener}.
*/
public void removeListener(Listener listener) {
mListeners.remove(listener);
}
private void launchPipOverlayActivity() {
Intent intent = new Intent(mContext, PipOverlayActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
final ActivityOptions options = ActivityOptions.makeBasic();
options.setLaunchStackId(PINNED_STACK_ID);
mContext.startActivity(intent, options.toBundle());
}
private boolean hasPipTasks() {
try {
StackInfo stackInfo = mActivityManager.getStackInfo(PINNED_STACK_ID);
return stackInfo != null;
} catch (RemoteException e) {
Log.e(TAG, "getStackInfo failed", e);
return false;
}
}
private class TaskStackListener extends ITaskStackListener.Stub {
@Override
public void onTaskStackChanged() throws RemoteException {
// Post the message back to the UI thread.
mHandler.post(mOnTaskStackChanged);
}
@Override
public void onActivityPinned() throws RemoteException {
// Post the message back to the UI thread.
mHandler.post(mOnActivityPinnedRunnable);
}
}
/**
* A listener interface to receive notification on changes in PIP.
*/
public interface Listener {
/**
* Invoked when a PIPped activity is closed.
*/
void onPipActivityClosed();
/**
* Invoked when the PIP menu gets shown.
*/
void onShowPipMenu();
/**
* Invoked when the PIPped activity is returned back to the fullscreen.
*/
void onMoveToFullscreen();
}
/**
* Gets an instance of {@link PipManager}.
*/
public static PipManager getInstance() {
if (sPipManager == null) {
sPipManager = new PipManager();
}
return sPipManager;
}
}

View File

@@ -0,0 +1,88 @@
/*
* 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.tv.pip;
import android.app.Activity;
import android.media.session.MediaController;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import com.android.systemui.R;
/**
* Activity to show the PIP menu to control PIP.
*/
public class PipMenuActivity extends Activity implements PipManager.Listener {
private static final String TAG = "PipMenuActivity";
private static final boolean DEBUG = false;
private final PipManager mPipManager = PipManager.getInstance();
private MediaController mMediaController;
@Override
protected void onCreate(Bundle bundle) {
super.onCreate(bundle);
setContentView(R.layout.tv_pip_menu);
mPipManager.addListener(this);
findViewById(R.id.full).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mPipManager.movePipToFullscreen();
}
});
findViewById(R.id.exit).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mPipManager.closePip();
finish();
}
});
findViewById(R.id.cancel).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mPipManager.showPipOverlay();
finish();
}
});
}
@Override
protected void onDestroy() {
mPipManager.removeListener(this);
super.onDestroy();
}
@Override
public void onBackPressed() {
mPipManager.showPipOverlay();
finish();
}
@Override
public void onPipActivityClosed() {
finish();
}
@Override
public void onShowPipMenu() { }
@Override
public void onMoveToFullscreen() {
finish();
}
}

View File

@@ -0,0 +1,76 @@
/*
* 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.tv.pip;
import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.view.View;
import com.android.systemui.R;
/**
* Activity to show an overlay on top of PIP activity to show how to pop up PIP menu.
*/
public class PipOverlayActivity extends Activity implements PipManager.Listener {
private static final String TAG = "PipOverlayActivity";
private static final boolean DEBUG = false;
private static final long SHOW_GUIDE_OVERLAY_VIEW_DURATION_MS = 2000;
private final PipManager mPipManager = PipManager.getInstance();
private final Handler mHandler = new Handler();
@Override
protected void onCreate(Bundle bundle) {
super.onCreate(bundle);
setContentView(R.layout.tv_pip_overlay);
mPipManager.addListener(this);
final View overlayView = findViewById(R.id.guide_overlay);
// TODO: apply animation
overlayView.setVisibility(View.VISIBLE);
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
overlayView.setVisibility(View.INVISIBLE);
}
}, SHOW_GUIDE_OVERLAY_VIEW_DURATION_MS);
}
@Override
protected void onDestroy() {
super.onDestroy();
mHandler.removeCallbacksAndMessages(null);
mPipManager.removeListener(this);
}
@Override
public void onPipActivityClosed() {
finish();
}
@Override
public void onShowPipMenu() {
finish();
}
@Override
public void onMoveToFullscreen() {
finish();
}
}

View File

@@ -0,0 +1,53 @@
/*
* 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.tv.pip;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import com.android.systemui.SystemUI;
import static android.content.pm.PackageManager.FEATURE_LEANBACK;
import static android.content.pm.PackageManager.FEATURE_PICTURE_IN_PICTURE;
/**
* Controls the picture-in-picture window for TV devices.
*/
public class PipUI extends SystemUI {
private boolean mSupportPip;
@Override
public void start() {
PackageManager pm = mContext.getPackageManager();
mSupportPip = pm.hasSystemFeature(FEATURE_PICTURE_IN_PICTURE)
&& pm.hasSystemFeature(FEATURE_LEANBACK);
if (!mSupportPip) {
return;
}
PipManager pipManager = PipManager.getInstance();
pipManager.initialize(mContext);
}
@Override
protected void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if (!mSupportPip) {
return;
}
// TODO: handle configuration change.
}
}