PipManager.java revision c552b04cb4aac9d31dbaf9744f32ddc14886e222
1/* 2 * Copyright (C) 2016 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 com.android.systemui.tv.pip; 18 19import android.app.ActivityManager.StackInfo; 20import android.app.ActivityManagerNative; 21import android.app.ActivityOptions; 22import android.app.IActivityManager; 23import android.app.ITaskStackListener; 24import android.content.BroadcastReceiver; 25import android.content.Context; 26import android.content.Intent; 27import android.content.IntentFilter; 28import android.content.res.Resources; 29import android.graphics.Rect; 30import android.os.Handler; 31import android.os.RemoteException; 32import android.util.Log; 33 34import java.util.ArrayList; 35import java.util.List; 36 37import static android.app.ActivityManager.StackId.FULLSCREEN_WORKSPACE_STACK_ID; 38import static android.app.ActivityManager.StackId.PINNED_STACK_ID; 39 40import android.app.ActivityManager.RunningTaskInfo; 41 42/** 43 * Manages the picture-in-picture (PIP) UI and states. 44 */ 45public class PipManager { 46 private static final String TAG = "PipManager"; 47 private static final boolean DEBUG = false; 48 49 private static PipManager sPipManager; 50 51 private static final int MAX_RUNNING_TASKS_COUNT = 10; 52 53 private static final int STATE_NO_PIP = 0; 54 private static final int STATE_PIP_OVERLAY = 1; 55 private static final int STATE_PIP_MENU = 2; 56 57 private static final int TASK_ID_NO_PIP = -1; 58 private static final int INVALID_RESOURCE_TYPE = -1; 59 60 private Context mContext; 61 private IActivityManager mActivityManager; 62 private int mState = STATE_NO_PIP; 63 private final Handler mHandler = new Handler(); 64 private List<Listener> mListeners = new ArrayList<>(); 65 private Rect mPipBound; 66 private Rect mMenuModePipBound; 67 private boolean mInitialized; 68 private int mPipTaskId = TASK_ID_NO_PIP; 69 70 private final Runnable mOnActivityPinnedRunnable = new Runnable() { 71 @Override 72 public void run() { 73 StackInfo stackInfo = null; 74 try { 75 stackInfo = mActivityManager.getStackInfo(PINNED_STACK_ID); 76 if (stackInfo == null) { 77 Log.w(TAG, "There is no pinned stack"); 78 return; 79 } 80 } catch (RemoteException e) { 81 Log.e(TAG, "getStackInfo failed", e); 82 return; 83 } 84 if (DEBUG) Log.d(TAG, "PINNED_STACK:" + stackInfo); 85 mPipTaskId = stackInfo.taskIds[stackInfo.taskIds.length - 1]; 86 showPipOverlay(false); 87 } 88 }; 89 private final Runnable mOnTaskStackChanged = new Runnable() { 90 @Override 91 public void run() { 92 if (mState != STATE_NO_PIP) { 93 // TODO: check whether PIP task is closed. 94 } 95 } 96 }; 97 private final Runnable mOnPinnedActivityRestartAttempt = new Runnable() { 98 @Override 99 public void run() { 100 movePipToFullscreen(); 101 } 102 }; 103 104 private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { 105 @Override 106 public void onReceive(Context context, Intent intent) { 107 String action = intent.getAction(); 108 if (Intent.ACTION_MEDIA_RESOURCE_GRANTED.equals(action)) { 109 String[] packageNames = intent.getStringArrayExtra(Intent.EXTRA_PACKAGES); 110 int resourceType = intent.getIntExtra(Intent.EXTRA_MEDIA_RESOURCE_TYPE, 111 INVALID_RESOURCE_TYPE); 112 if (mState != STATE_NO_PIP && packageNames != null && packageNames.length > 0 113 && resourceType == Intent.EXTRA_MEDIA_RESOURCE_TYPE_VIDEO_CODEC) { 114 handleMediaResourceGranted(packageNames); 115 } 116 } 117 118 } 119 }; 120 121 private PipManager() { } 122 123 /** 124 * Initializes {@link PipManager}. 125 */ 126 public void initialize(Context context) { 127 if (mInitialized) { 128 return; 129 } 130 mInitialized = true; 131 mContext = context; 132 Resources res = context.getResources(); 133 mPipBound = Rect.unflattenFromString(res.getString( 134 com.android.internal.R.string.config_defaultPictureInPictureBounds)); 135 mMenuModePipBound = Rect.unflattenFromString(res.getString( 136 com.android.internal.R.string.config_centeredPictureInPictureBounds)); 137 138 mActivityManager = ActivityManagerNative.getDefault(); 139 TaskStackListener taskStackListener = new TaskStackListener(); 140 IActivityManager iam = ActivityManagerNative.getDefault(); 141 try { 142 iam.registerTaskStackListener(taskStackListener); 143 } catch (RemoteException e) { 144 Log.e(TAG, "registerTaskStackListener failed", e); 145 } 146 IntentFilter intentFilter = new IntentFilter(); 147 intentFilter.addAction(Intent.ACTION_MEDIA_RESOURCE_GRANTED); 148 mContext.registerReceiver(mBroadcastReceiver, intentFilter); 149 } 150 151 /** 152 * Request PIP. 153 * It could either start PIP if there's none, and show PIP menu otherwise. 154 */ 155 public void requestTvPictureInPicture() { 156 if (DEBUG) Log.d(TAG, "requestTvPictureInPicture()"); 157 if (!hasPipTasks()) { 158 startPip(); 159 } else if (mState == STATE_PIP_OVERLAY) { 160 showPipMenu(); 161 } 162 } 163 164 private void startPip() { 165 try { 166 mActivityManager.moveTopActivityToPinnedStack(FULLSCREEN_WORKSPACE_STACK_ID, mPipBound); 167 } catch (RemoteException|IllegalArgumentException e) { 168 Log.e(TAG, "moveTopActivityToPinnedStack failed", e); 169 } 170 } 171 172 /** 173 * Closes PIP (PIPped activity and PIP system UI). 174 */ 175 public void closePip() { 176 mState = STATE_NO_PIP; 177 mPipTaskId = TASK_ID_NO_PIP; 178 StackInfo stackInfo = null; 179 try { 180 stackInfo = mActivityManager.getStackInfo(PINNED_STACK_ID); 181 if (stackInfo == null) { 182 return; 183 } 184 } catch (RemoteException e) { 185 Log.e(TAG, "getStackInfo failed", e); 186 return; 187 } 188 for (int taskId : stackInfo.taskIds) { 189 try { 190 mActivityManager.removeTask(taskId); 191 } catch (RemoteException e) { 192 Log.e(TAG, "removeTask failed", e); 193 } 194 } 195 } 196 197 /** 198 * Moves the PIPped activity to the fullscreen and closes PIP system UI. 199 */ 200 public void movePipToFullscreen() { 201 mState = STATE_NO_PIP; 202 mPipTaskId = TASK_ID_NO_PIP; 203 for (int i = mListeners.size() - 1; i >= 0; --i) { 204 mListeners.get(i).onMoveToFullscreen(); 205 } 206 try { 207 mActivityManager.moveTasksToFullscreenStack(PINNED_STACK_ID, true); 208 } catch (RemoteException e) { 209 Log.e(TAG, "moveTasksToFullscreenStack failed", e); 210 } 211 } 212 213 /** 214 * Shows PIP overlay UI by launching {@link PipOverlayActivity}. It also locates the pinned 215 * stack to the default PIP bound {@link com.android.internal.R.string 216 * .config_defaultPictureInPictureBounds}. 217 */ 218 public void showPipOverlay(boolean resizeStack) { 219 if (DEBUG) Log.d(TAG, "showPipOverlay()"); 220 mState = STATE_PIP_OVERLAY; 221 Intent intent = new Intent(mContext, PipOverlayActivity.class); 222 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 223 final ActivityOptions options = ActivityOptions.makeBasic(); 224 options.setLaunchStackId(PINNED_STACK_ID); 225 if (resizeStack) { 226 options.setLaunchBounds(mPipBound); 227 } 228 mContext.startActivity(intent, options.toBundle()); 229 } 230 231 /** 232 * Shows PIP menu UI by launching {@link PipMenuActivity}. It also locates the pinned 233 * stack to the centered PIP bound {@link com.android.internal.R.string 234 * .config_centeredPictureInPictureBounds}. 235 */ 236 public void showPipMenu() { 237 if (DEBUG) Log.d(TAG, "showPipMenu()"); 238 mState = STATE_PIP_MENU; 239 for (int i = mListeners.size() - 1; i >= 0; --i) { 240 mListeners.get(i).onShowPipMenu(); 241 } 242 Intent intent = new Intent(mContext, PipMenuActivity.class); 243 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 244 final ActivityOptions options = ActivityOptions.makeBasic(); 245 options.setLaunchStackId(PINNED_STACK_ID); 246 options.setLaunchBounds(mMenuModePipBound); 247 mContext.startActivity(intent, options.toBundle()); 248 } 249 250 /** 251 * Adds {@link Listener}. 252 */ 253 public void addListener(Listener listener) { 254 mListeners.add(listener); 255 } 256 257 /** 258 * Removes {@link Listener}. 259 */ 260 public void removeListener(Listener listener) { 261 mListeners.remove(listener); 262 } 263 264 private boolean hasPipTasks() { 265 try { 266 StackInfo stackInfo = mActivityManager.getStackInfo(PINNED_STACK_ID); 267 return stackInfo != null; 268 } catch (RemoteException e) { 269 Log.e(TAG, "getStackInfo failed", e); 270 return false; 271 } 272 } 273 274 private void handleMediaResourceGranted(String[] packageNames) { 275 StackInfo fullscreenStack = null; 276 try { 277 fullscreenStack = mActivityManager.getStackInfo(FULLSCREEN_WORKSPACE_STACK_ID); 278 } catch (RemoteException e) { 279 Log.e(TAG, "getStackInfo failed", e); 280 } 281 if (fullscreenStack == null) { 282 return; 283 } 284 int fullscreenTopTaskId = fullscreenStack.taskIds[fullscreenStack.taskIds.length - 1]; 285 List<RunningTaskInfo> tasks = null; 286 try { 287 tasks = mActivityManager.getTasks(MAX_RUNNING_TASKS_COUNT, 0); 288 } catch (RemoteException e) { 289 Log.e(TAG, "getTasks failed", e); 290 } 291 if (tasks == null) { 292 return; 293 } 294 boolean wasGrantedInFullscreen = false; 295 boolean wasGrantedInPip = false; 296 for (int i = tasks.size() - 1; i >= 0; --i) { 297 RunningTaskInfo task = tasks.get(i); 298 for (int j = packageNames.length - 1; j >= 0; --j) { 299 if (task.topActivity.getPackageName().equals(packageNames[j])) { 300 if (task.id == fullscreenTopTaskId) { 301 wasGrantedInFullscreen = true; 302 } else if (task.id == mPipTaskId) { 303 wasGrantedInPip= true; 304 } 305 } 306 } 307 } 308 if (wasGrantedInFullscreen && !wasGrantedInPip) { 309 closePip(); 310 } 311 } 312 313 private class TaskStackListener extends ITaskStackListener.Stub { 314 @Override 315 public void onTaskStackChanged() throws RemoteException { 316 // Post the message back to the UI thread. 317 mHandler.post(mOnTaskStackChanged); 318 } 319 320 @Override 321 public void onActivityPinned() throws RemoteException { 322 // Post the message back to the UI thread. 323 mHandler.post(mOnActivityPinnedRunnable); 324 } 325 326 @Override 327 public void onPinnedActivityRestartAttempt() { 328 // Post the message back to the UI thread. 329 mHandler.post(mOnPinnedActivityRestartAttempt); 330 } 331 } 332 333 /** 334 * A listener interface to receive notification on changes in PIP. 335 */ 336 public interface Listener { 337 /** 338 * Invoked when a PIPped activity is closed. 339 */ 340 void onPipActivityClosed(); 341 /** 342 * Invoked when the PIP menu gets shown. 343 */ 344 void onShowPipMenu(); 345 /** 346 * Invoked when the PIPped activity is returned back to the fullscreen. 347 */ 348 void onMoveToFullscreen(); 349 } 350 351 /** 352 * Gets an instance of {@link PipManager}. 353 */ 354 public static PipManager getInstance() { 355 if (sPipManager == null) { 356 sPipManager = new PipManager(); 357 } 358 return sPipManager; 359 } 360} 361