1 /* 2 * Copyright (C) 2015 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 17 package android.server.wm.app; 18 19 import static android.server.wm.app.Components.PipActivity.ACTION_CHANGE_ASPECT_RATIO; 20 import static android.server.wm.app.Components.PipActivity.ACTION_ENTER_PIP; 21 import static android.server.wm.app.Components.PipActivity.ACTION_ENTER_PIP_AND_WAIT_FOR_UI_STATE; 22 import static android.server.wm.app.Components.PipActivity.ACTION_EXPAND_PIP; 23 import static android.server.wm.app.Components.PipActivity.ACTION_FINISH; 24 import static android.server.wm.app.Components.PipActivity.ACTION_LAUNCH_TRANSLUCENT_ACTIVITY; 25 import static android.server.wm.app.Components.PipActivity.ACTION_MOVE_TO_BACK; 26 import static android.server.wm.app.Components.PipActivity.ACTION_ON_PIP_REQUESTED; 27 import static android.server.wm.app.Components.PipActivity.ACTION_SET_ON_PAUSE_REMOTE_CALLBACK; 28 import static android.server.wm.app.Components.PipActivity.ACTION_SET_REQUESTED_ORIENTATION; 29 import static android.server.wm.app.Components.PipActivity.ACTION_UPDATE_PIP_STATE; 30 import static android.server.wm.app.Components.PipActivity.EXTRA_ALLOW_AUTO_PIP; 31 import static android.server.wm.app.Components.PipActivity.EXTRA_ASSERT_NO_ON_STOP_BEFORE_PIP; 32 import static android.server.wm.app.Components.PipActivity.EXTRA_CLOSE_ACTION; 33 import static android.server.wm.app.Components.PipActivity.EXTRA_DISMISS_KEYGUARD; 34 import static android.server.wm.app.Components.PipActivity.EXTRA_ENTER_PIP; 35 import static android.server.wm.app.Components.PipActivity.EXTRA_ENTER_PIP_ASPECT_RATIO_DENOMINATOR; 36 import static android.server.wm.app.Components.PipActivity.EXTRA_ENTER_PIP_ASPECT_RATIO_NUMERATOR; 37 import static android.server.wm.app.Components.PipActivity.EXTRA_ENTER_PIP_ON_BACK_PRESSED; 38 import static android.server.wm.app.Components.PipActivity.EXTRA_ENTER_PIP_ON_PAUSE; 39 import static android.server.wm.app.Components.PipActivity.EXTRA_ENTER_PIP_ON_PIP_REQUESTED; 40 import static android.server.wm.app.Components.PipActivity.EXTRA_ENTER_PIP_ON_USER_LEAVE_HINT; 41 import static android.server.wm.app.Components.PipActivity.EXTRA_EXPANDED_PIP_ASPECT_RATIO_DENOMINATOR; 42 import static android.server.wm.app.Components.PipActivity.EXTRA_EXPANDED_PIP_ASPECT_RATIO_NUMERATOR; 43 import static android.server.wm.app.Components.PipActivity.EXTRA_FINISH_SELF_ON_RESUME; 44 import static android.server.wm.app.Components.PipActivity.EXTRA_IS_SEAMLESS_RESIZE_ENABLED; 45 import static android.server.wm.app.Components.PipActivity.EXTRA_NUMBER_OF_CUSTOM_ACTIONS; 46 import static android.server.wm.app.Components.PipActivity.EXTRA_ON_PAUSE_DELAY; 47 import static android.server.wm.app.Components.PipActivity.EXTRA_PIP_ON_PAUSE_CALLBACK; 48 import static android.server.wm.app.Components.PipActivity.EXTRA_PIP_ORIENTATION; 49 import static android.server.wm.app.Components.PipActivity.EXTRA_SET_ASPECT_RATIO_DENOMINATOR; 50 import static android.server.wm.app.Components.PipActivity.EXTRA_SET_ASPECT_RATIO_NUMERATOR; 51 import static android.server.wm.app.Components.PipActivity.EXTRA_SET_ASPECT_RATIO_WITH_DELAY_DENOMINATOR; 52 import static android.server.wm.app.Components.PipActivity.EXTRA_SET_ASPECT_RATIO_WITH_DELAY_NUMERATOR; 53 import static android.server.wm.app.Components.PipActivity.EXTRA_SET_PIP_CALLBACK; 54 import static android.server.wm.app.Components.PipActivity.EXTRA_SET_PIP_STASHED; 55 import static android.server.wm.app.Components.PipActivity.EXTRA_SHOW_OVER_KEYGUARD; 56 import static android.server.wm.app.Components.PipActivity.EXTRA_START_ACTIVITY; 57 import static android.server.wm.app.Components.PipActivity.EXTRA_SUBTITLE; 58 import static android.server.wm.app.Components.PipActivity.EXTRA_TAP_TO_FINISH; 59 import static android.server.wm.app.Components.PipActivity.EXTRA_TITLE; 60 import static android.server.wm.app.Components.PipActivity.IS_IN_PIP_MODE_RESULT; 61 import static android.server.wm.app.Components.PipActivity.UI_STATE_ENTERING_PIP_RESULT; 62 import static android.server.wm.app.Components.PipActivity.UI_STATE_STASHED_RESULT; 63 import static android.view.WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD; 64 65 import android.app.Activity; 66 import android.app.PendingIntent; 67 import android.app.PictureInPictureParams; 68 import android.app.PictureInPictureUiState; 69 import android.app.RemoteAction; 70 import android.content.BroadcastReceiver; 71 import android.content.ComponentName; 72 import android.content.Context; 73 import android.content.Intent; 74 import android.content.IntentFilter; 75 import android.content.res.Configuration; 76 import android.graphics.drawable.Icon; 77 import android.os.Bundle; 78 import android.os.Handler; 79 import android.os.RemoteCallback; 80 import android.os.SystemClock; 81 import android.server.wm.CommandSession; 82 import android.util.Log; 83 import android.util.Rational; 84 85 import androidx.annotation.Nullable; 86 87 import java.util.ArrayList; 88 import java.util.List; 89 90 public class PipActivity extends AbstractLifecycleLogActivity { 91 92 private boolean mEnteredPictureInPicture; 93 private boolean mEnterPipOnBackPressed; 94 private RemoteCallback mCb; 95 private RemoteCallback mOnPauseCallback; 96 97 private Handler mHandler = new Handler(); 98 private BroadcastReceiver mReceiver = new BroadcastReceiver() { 99 @Override 100 public void onReceive(Context context, Intent intent) { 101 if (intent != null) { 102 switch (intent.getAction()) { 103 case ACTION_ENTER_PIP: 104 enterPictureInPictureMode(new PictureInPictureParams.Builder().build()); 105 if (intent.getExtras() != null) { 106 mCb = (RemoteCallback) intent.getExtras().get(EXTRA_SET_PIP_CALLBACK); 107 if (mCb != null) { 108 mCb.sendResult(new Bundle()); 109 } 110 } 111 break; 112 case ACTION_ENTER_PIP_AND_WAIT_FOR_UI_STATE: 113 // mCb will be callback-ed in onPictureInPictureUiStateChanged. 114 mCb = (RemoteCallback) intent.getExtras().get(EXTRA_SET_PIP_CALLBACK); 115 enterPictureInPictureMode(new PictureInPictureParams.Builder().build()); 116 break; 117 case ACTION_MOVE_TO_BACK: 118 moveTaskToBack(false /* nonRoot */); 119 break; 120 case ACTION_UPDATE_PIP_STATE: 121 mCb = (RemoteCallback) intent.getExtras().get(EXTRA_SET_PIP_CALLBACK); 122 boolean stashed = intent.getBooleanExtra(EXTRA_SET_PIP_STASHED, false); 123 onPictureInPictureUiStateChanged(new PictureInPictureUiState(stashed)); 124 break; 125 case ACTION_EXPAND_PIP: 126 // Trigger the activity to expand 127 Intent startIntent = new Intent(PipActivity.this, PipActivity.class); 128 startIntent.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); 129 startActivity(startIntent); 130 131 if (intent.hasExtra(EXTRA_SET_ASPECT_RATIO_WITH_DELAY_NUMERATOR) 132 && intent.hasExtra(EXTRA_SET_ASPECT_RATIO_WITH_DELAY_DENOMINATOR)) { 133 // Ugly, but required to wait for the startActivity to actually start 134 // the activity... 135 mHandler.postDelayed(() -> { 136 final PictureInPictureParams.Builder builder = 137 new PictureInPictureParams.Builder(); 138 builder.setAspectRatio(getAspectRatio(intent, 139 EXTRA_SET_ASPECT_RATIO_WITH_DELAY_NUMERATOR, 140 EXTRA_SET_ASPECT_RATIO_WITH_DELAY_DENOMINATOR)); 141 setPictureInPictureParams(builder.build()); 142 }, 100); 143 } 144 break; 145 case ACTION_SET_REQUESTED_ORIENTATION: 146 setRequestedOrientation(Integer.parseInt(intent.getStringExtra( 147 EXTRA_PIP_ORIENTATION))); 148 break; 149 case ACTION_FINISH: 150 finish(); 151 break; 152 case ACTION_ON_PIP_REQUESTED: 153 onPictureInPictureRequested(); 154 break; 155 case ACTION_CHANGE_ASPECT_RATIO: 156 setPictureInPictureParams(new PictureInPictureParams.Builder() 157 .setAspectRatio(getAspectRatio(intent, 158 EXTRA_SET_ASPECT_RATIO_NUMERATOR, 159 EXTRA_SET_ASPECT_RATIO_DENOMINATOR)) 160 .build()); 161 break; 162 case ACTION_LAUNCH_TRANSLUCENT_ACTIVITY: 163 startActivity(new Intent(PipActivity.this, TranslucentTestActivity.class)); 164 break; 165 case ACTION_SET_ON_PAUSE_REMOTE_CALLBACK: 166 mOnPauseCallback = intent.getParcelableExtra( 167 EXTRA_PIP_ON_PAUSE_CALLBACK, RemoteCallback.class); 168 // Signals the caller that we have received the mOnPauseCallback 169 final RemoteCallback setCallback = intent.getParcelableExtra( 170 EXTRA_SET_PIP_CALLBACK, RemoteCallback.class); 171 setCallback.sendResult(Bundle.EMPTY); 172 break; 173 } 174 } 175 } 176 }; 177 178 @Override onCreate(Bundle savedInstanceState)179 protected void onCreate(Bundle savedInstanceState) { 180 super.onCreate(savedInstanceState); 181 182 // Set the fixed orientation if requested 183 if (getIntent().hasExtra(EXTRA_PIP_ORIENTATION)) { 184 final int ori = Integer.parseInt(getIntent().getStringExtra(EXTRA_PIP_ORIENTATION)); 185 setRequestedOrientation(ori); 186 } 187 188 // Set the window flag to show over the keyguard 189 setShowWhenLocked(parseBooleanExtra(EXTRA_SHOW_OVER_KEYGUARD)); 190 191 // Set the window flag to dismiss the keyguard 192 if (parseBooleanExtra(EXTRA_DISMISS_KEYGUARD)) { 193 getWindow().addFlags(FLAG_DISMISS_KEYGUARD); 194 } 195 196 boolean enteringPip = false; 197 // Enter picture in picture with the given aspect ratio if provided 198 if (parseBooleanExtra(EXTRA_ENTER_PIP)) { 199 if (getIntent().hasExtra(EXTRA_ENTER_PIP_ASPECT_RATIO_NUMERATOR) 200 && getIntent().hasExtra(EXTRA_ENTER_PIP_ASPECT_RATIO_DENOMINATOR)) { 201 try { 202 final PictureInPictureParams.Builder builder = 203 new PictureInPictureParams.Builder(); 204 builder.setAspectRatio(getAspectRatio(getIntent(), 205 EXTRA_ENTER_PIP_ASPECT_RATIO_NUMERATOR, 206 EXTRA_ENTER_PIP_ASPECT_RATIO_DENOMINATOR)); 207 if (shouldAddExpandedPipAspectRatios()) { 208 builder.setExpandedAspectRatio(getAspectRatio(getIntent(), 209 EXTRA_EXPANDED_PIP_ASPECT_RATIO_NUMERATOR, 210 EXTRA_EXPANDED_PIP_ASPECT_RATIO_DENOMINATOR)); 211 } 212 enteringPip = enterPictureInPictureMode(builder.build()); 213 } catch (Exception e) { 214 // This call can fail intentionally if the aspect ratio is too extreme 215 } 216 } else { 217 enteringPip = enterPictureInPictureMode( 218 new PictureInPictureParams.Builder().build()); 219 } 220 } 221 222 // We need to wait for either enterPictureInPicture() or requestAutoEnterPictureInPicture() 223 // to be called before setting the aspect ratio 224 if (getIntent().hasExtra(EXTRA_SET_ASPECT_RATIO_NUMERATOR) 225 && getIntent().hasExtra(EXTRA_SET_ASPECT_RATIO_DENOMINATOR)) { 226 final PictureInPictureParams.Builder builder = 227 new PictureInPictureParams.Builder(); 228 builder.setAspectRatio(getAspectRatio(getIntent(), 229 EXTRA_SET_ASPECT_RATIO_NUMERATOR, EXTRA_SET_ASPECT_RATIO_DENOMINATOR)); 230 try { 231 setPictureInPictureParams(builder.build()); 232 } catch (Exception e) { 233 // This call can fail intentionally if the aspect ratio is too extreme 234 } 235 } 236 237 final PictureInPictureParams.Builder sharedBuilder = new PictureInPictureParams.Builder(); 238 boolean sharedBuilderChanged = false; 239 240 if (parseBooleanExtra(EXTRA_ALLOW_AUTO_PIP)) { 241 sharedBuilder.setAutoEnterEnabled(true); 242 sharedBuilderChanged = true; 243 } 244 245 if (getIntent().hasExtra(EXTRA_IS_SEAMLESS_RESIZE_ENABLED)) { 246 sharedBuilder.setSeamlessResizeEnabled( 247 getIntent().getBooleanExtra(EXTRA_IS_SEAMLESS_RESIZE_ENABLED, true)); 248 sharedBuilderChanged = true; 249 } 250 251 if (getIntent().hasExtra(EXTRA_TITLE)) { 252 sharedBuilder.setTitle(getIntent().getStringExtra(EXTRA_TITLE)); 253 sharedBuilderChanged = true; 254 } 255 256 if (getIntent().hasExtra(EXTRA_SUBTITLE)) { 257 sharedBuilder.setSubtitle(getIntent().getStringExtra(EXTRA_SUBTITLE)); 258 sharedBuilderChanged = true; 259 } 260 261 if (getIntent().hasExtra(EXTRA_CLOSE_ACTION)) { 262 if (getIntent().getBooleanExtra(EXTRA_CLOSE_ACTION, false)) { 263 sharedBuilder.setCloseAction(createRemoteAction(0)); 264 } else { 265 sharedBuilder.setCloseAction(null); 266 } 267 sharedBuilderChanged = true; 268 } 269 270 // Enable tap to finish if necessary 271 if (parseBooleanExtra(EXTRA_TAP_TO_FINISH)) { 272 setContentView(R.layout.tap_to_finish_pip_layout); 273 findViewById(R.id.content).setOnClickListener(v -> { 274 finish(); 275 }); 276 } 277 278 // Launch a new activity if requested 279 String launchActivityComponent = getIntent().getStringExtra(EXTRA_START_ACTIVITY); 280 if (launchActivityComponent != null) { 281 Intent launchIntent = new Intent(); 282 launchIntent.setComponent(ComponentName.unflattenFromString(launchActivityComponent)); 283 startActivity(launchIntent); 284 } 285 286 // Set custom actions if requested 287 if (getIntent().hasExtra(EXTRA_NUMBER_OF_CUSTOM_ACTIONS)) { 288 final int numberOfCustomActions = Integer.valueOf( 289 getIntent().getStringExtra(EXTRA_NUMBER_OF_CUSTOM_ACTIONS)); 290 final List<RemoteAction> actions = new ArrayList<>(numberOfCustomActions); 291 for (int i = 0; i< numberOfCustomActions; i++) { 292 actions.add(createRemoteAction(i)); 293 } 294 sharedBuilder.setActions(actions); 295 sharedBuilderChanged = true; 296 } 297 298 if (sharedBuilderChanged) { 299 setPictureInPictureParams(sharedBuilder.build()); 300 } 301 302 // Register the broadcast receiver 303 IntentFilter filter = new IntentFilter(); 304 filter.addAction(ACTION_ENTER_PIP); 305 filter.addAction(ACTION_MOVE_TO_BACK); 306 filter.addAction(ACTION_EXPAND_PIP); 307 filter.addAction(ACTION_UPDATE_PIP_STATE); 308 filter.addAction(ACTION_ENTER_PIP_AND_WAIT_FOR_UI_STATE); 309 filter.addAction(ACTION_SET_REQUESTED_ORIENTATION); 310 filter.addAction(ACTION_FINISH); 311 filter.addAction(ACTION_ON_PIP_REQUESTED); 312 filter.addAction(ACTION_CHANGE_ASPECT_RATIO); 313 filter.addAction(ACTION_LAUNCH_TRANSLUCENT_ACTIVITY); 314 filter.addAction(ACTION_SET_ON_PAUSE_REMOTE_CALLBACK); 315 registerReceiver(mReceiver, filter, Context.RECEIVER_EXPORTED); 316 317 // Don't dump configuration when entering PIP to avoid the verifier getting the intermediate 318 // state. In this case it is expected that the verifier will check the changed configuration 319 // after onConfigurationChanged. 320 if (!enteringPip) { 321 // Dump applied display metrics. 322 dumpConfiguration(getResources().getConfiguration()); 323 dumpConfigInfo(); 324 } 325 326 mEnterPipOnBackPressed = parseBooleanExtra(EXTRA_ENTER_PIP_ON_BACK_PRESSED); 327 } 328 329 @Override onResume()330 protected void onResume() { 331 super.onResume(); 332 333 // Finish self if requested 334 if (parseBooleanExtra(EXTRA_FINISH_SELF_ON_RESUME)) { 335 finish(); 336 } 337 } 338 339 @Override onPause()340 protected void onPause() { 341 super.onPause(); 342 343 // Pause if requested 344 if (getIntent().hasExtra(EXTRA_ON_PAUSE_DELAY)) { 345 SystemClock.sleep(Long.valueOf(getIntent().getStringExtra(EXTRA_ON_PAUSE_DELAY))); 346 } 347 348 // Enter PIP on move to background 349 if (parseBooleanExtra(EXTRA_ENTER_PIP_ON_PAUSE)) { 350 enterPictureInPictureMode(new PictureInPictureParams.Builder().build()); 351 } 352 353 if (mOnPauseCallback != null) { 354 Bundle res = new Bundle(1); 355 res.putBoolean(IS_IN_PIP_MODE_RESULT, isInPictureInPictureMode()); 356 mOnPauseCallback.sendResult(res); 357 } 358 } 359 360 @Override onStop()361 protected void onStop() { 362 super.onStop(); 363 364 if (parseBooleanExtra(EXTRA_ASSERT_NO_ON_STOP_BEFORE_PIP) && !mEnteredPictureInPicture) { 365 Log.w(getTag(), "Unexpected onStop() called before entering picture-in-picture"); 366 finish(); 367 } 368 } 369 370 @Override onDestroy()371 protected void onDestroy() { 372 super.onDestroy(); 373 374 unregisterReceiver(mReceiver); 375 } 376 377 @Override onUserLeaveHint()378 protected void onUserLeaveHint() { 379 super.onUserLeaveHint(); 380 if (parseBooleanExtra(EXTRA_ENTER_PIP_ON_USER_LEAVE_HINT)) { 381 enterPictureInPictureMode(new PictureInPictureParams.Builder().build()); 382 } 383 } 384 385 @Override onPictureInPictureRequested()386 public boolean onPictureInPictureRequested() { 387 onCallback(CommandSession.ActivityCallback.ON_PICTURE_IN_PICTURE_REQUESTED); 388 if (parseBooleanExtra(EXTRA_ENTER_PIP_ON_PIP_REQUESTED)) { 389 enterPictureInPictureMode(new PictureInPictureParams.Builder().build()); 390 return true; 391 } 392 return super.onPictureInPictureRequested(); 393 } 394 395 @Override onPictureInPictureModeChanged(boolean isInPictureInPictureMode, Configuration newConfig)396 public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode, 397 Configuration newConfig) { 398 super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig); 399 400 // Fail early if the activity state does not match the dispatched state 401 if (isInPictureInPictureMode() != isInPictureInPictureMode) { 402 Log.w(getTag(), "Received onPictureInPictureModeChanged mode=" 403 + isInPictureInPictureMode + " activityState=" + isInPictureInPictureMode()); 404 finish(); 405 } 406 407 // Mark that we've entered picture-in-picture so that we can stop checking for 408 // EXTRA_ASSERT_NO_ON_STOP_BEFORE_PIP 409 if (isInPictureInPictureMode) { 410 mEnteredPictureInPicture = true; 411 } 412 } 413 414 @Override onPictureInPictureUiStateChanged(PictureInPictureUiState pipState)415 public void onPictureInPictureUiStateChanged(PictureInPictureUiState pipState) { 416 Bundle res = new Bundle(); 417 res.putBoolean(UI_STATE_STASHED_RESULT, pipState.isStashed()); 418 res.putBoolean(UI_STATE_ENTERING_PIP_RESULT, pipState.isTransitioningToPip()); 419 if (mCb != null) { 420 mCb.sendResult(res); 421 } 422 } 423 424 @Override onConfigurationChanged(Configuration newConfig)425 public void onConfigurationChanged(Configuration newConfig) { 426 super.onConfigurationChanged(newConfig); 427 dumpConfiguration(newConfig); 428 dumpConfigInfo(); 429 } 430 431 @Override onBackPressed()432 public void onBackPressed() { 433 if (mEnterPipOnBackPressed) { 434 enterPictureInPictureMode(new PictureInPictureParams.Builder().build()); 435 } else { 436 super.onBackPressed(); 437 } 438 } 439 440 /** 441 * Launches a new instance of the PipActivity in the same task that will automatically enter 442 * PiP. 443 */ launchEnterPipActivity(Activity caller, @Nullable Bundle overrides)444 static void launchEnterPipActivity(Activity caller, @Nullable Bundle overrides) { 445 final Intent intent = new Intent(caller, PipActivity.class); 446 intent.putExtra(EXTRA_ENTER_PIP, "true"); 447 intent.putExtra(EXTRA_ASSERT_NO_ON_STOP_BEFORE_PIP, "true"); 448 if (overrides != null) { 449 intent.putExtras(overrides); 450 } 451 caller.startActivity(intent); 452 } 453 parseBooleanExtra(String key)454 private boolean parseBooleanExtra(String key) { 455 return getIntent().hasExtra(key) && Boolean.parseBoolean(getIntent().getStringExtra(key)); 456 } 457 458 /** 459 * @return a {@link Rational} aspect ratio from the given intent and extras. 460 */ getAspectRatio(Intent intent, String extraNum, String extraDenom)461 private Rational getAspectRatio(Intent intent, String extraNum, String extraDenom) { 462 return new Rational( 463 Integer.valueOf(intent.getStringExtra(extraNum)), 464 Integer.valueOf(intent.getStringExtra(extraDenom))); 465 } 466 467 /** @return {@link RemoteAction} instance titled after a given index */ createRemoteAction(int index)468 private RemoteAction createRemoteAction(int index) { 469 return new RemoteAction(Icon.createWithResource(this, R.drawable.red), 470 "action " + index, 471 "contentDescription " + index, 472 PendingIntent.getBroadcast(this, 0, new Intent(), PendingIntent.FLAG_IMMUTABLE)); 473 } 474 shouldAddExpandedPipAspectRatios()475 private boolean shouldAddExpandedPipAspectRatios() { 476 return getIntent().hasExtra(EXTRA_EXPANDED_PIP_ASPECT_RATIO_NUMERATOR) 477 && getIntent().hasExtra(EXTRA_EXPANDED_PIP_ASPECT_RATIO_DENOMINATOR); 478 } 479 } 480