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.am; 18 19 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; 20 import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK; 21 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; 22 import static android.server.am.Components.PipActivity.ACTION_ENTER_PIP; 23 import static android.server.am.Components.PipActivity.ACTION_EXPAND_PIP; 24 import static android.server.am.Components.PipActivity.ACTION_FINISH; 25 import static android.server.am.Components.PipActivity.ACTION_MOVE_TO_BACK; 26 import static android.server.am.Components.PipActivity.ACTION_SET_REQUESTED_ORIENTATION; 27 import static android.server.am.Components.PipActivity.EXTRA_ASSERT_NO_ON_STOP_BEFORE_PIP; 28 import static android.server.am.Components.PipActivity.EXTRA_ENTER_PIP; 29 import static android.server.am.Components.PipActivity.EXTRA_ENTER_PIP_ASPECT_RATIO_DENOMINATOR; 30 import static android.server.am.Components.PipActivity.EXTRA_ENTER_PIP_ASPECT_RATIO_NUMERATOR; 31 import static android.server.am.Components.PipActivity.EXTRA_ENTER_PIP_ON_PAUSE; 32 import static android.server.am.Components.PipActivity.EXTRA_FINISH_SELF_ON_RESUME; 33 import static android.server.am.Components.PipActivity.EXTRA_ON_PAUSE_DELAY; 34 import static android.server.am.Components.PipActivity.EXTRA_PIP_ORIENTATION; 35 import static android.server.am.Components.PipActivity.EXTRA_REENTER_PIP_ON_EXIT; 36 import static android.server.am.Components.PipActivity.EXTRA_SET_ASPECT_RATIO_DENOMINATOR; 37 import static android.server.am.Components.PipActivity.EXTRA_SET_ASPECT_RATIO_NUMERATOR; 38 import static android.server.am.Components.PipActivity.EXTRA_SET_ASPECT_RATIO_WITH_DELAY_DENOMINATOR; 39 import static android.server.am.Components.PipActivity.EXTRA_SET_ASPECT_RATIO_WITH_DELAY_NUMERATOR; 40 import static android.server.am.Components.PipActivity.EXTRA_SHOW_OVER_KEYGUARD; 41 import static android.server.am.Components.PipActivity.EXTRA_START_ACTIVITY; 42 import static android.server.am.Components.PipActivity.EXTRA_TAP_TO_FINISH; 43 44 import android.app.Activity; 45 import android.app.ActivityOptions; 46 import android.app.PictureInPictureParams; 47 import android.content.BroadcastReceiver; 48 import android.content.ComponentName; 49 import android.content.Context; 50 import android.content.Intent; 51 import android.content.IntentFilter; 52 import android.content.res.Configuration; 53 import android.graphics.Rect; 54 import android.os.Bundle; 55 import android.os.Handler; 56 import android.os.SystemClock; 57 import android.util.Log; 58 import android.util.Rational; 59 import android.view.WindowManager; 60 61 public class PipActivity extends AbstractLifecycleLogActivity { 62 63 private boolean mEnteredPictureInPicture; 64 65 private Handler mHandler = new Handler(); 66 private BroadcastReceiver mReceiver = new BroadcastReceiver() { 67 @Override 68 public void onReceive(Context context, Intent intent) { 69 if (intent != null) { 70 switch (intent.getAction()) { 71 case ACTION_ENTER_PIP: 72 enterPictureInPictureMode(); 73 break; 74 case ACTION_MOVE_TO_BACK: 75 moveTaskToBack(false /* nonRoot */); 76 break; 77 case ACTION_EXPAND_PIP: 78 // Trigger the activity to expand 79 Intent startIntent = new Intent(PipActivity.this, PipActivity.class); 80 startIntent.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); 81 startActivity(startIntent); 82 83 if (intent.hasExtra(EXTRA_SET_ASPECT_RATIO_WITH_DELAY_NUMERATOR) 84 && intent.hasExtra(EXTRA_SET_ASPECT_RATIO_WITH_DELAY_DENOMINATOR)) { 85 // Ugly, but required to wait for the startActivity to actually start 86 // the activity... 87 mHandler.postDelayed(() -> { 88 final PictureInPictureParams.Builder builder = 89 new PictureInPictureParams.Builder(); 90 builder.setAspectRatio(getAspectRatio(intent, 91 EXTRA_SET_ASPECT_RATIO_WITH_DELAY_NUMERATOR, 92 EXTRA_SET_ASPECT_RATIO_WITH_DELAY_DENOMINATOR)); 93 setPictureInPictureParams(builder.build()); 94 }, 100); 95 } 96 break; 97 case ACTION_SET_REQUESTED_ORIENTATION: 98 setRequestedOrientation(Integer.parseInt(intent.getStringExtra( 99 EXTRA_PIP_ORIENTATION))); 100 break; 101 case ACTION_FINISH: 102 finish(); 103 break; 104 } 105 } 106 } 107 }; 108 109 @Override 110 protected void onCreate(Bundle savedInstanceState) { 111 super.onCreate(savedInstanceState); 112 113 // Set the fixed orientation if requested 114 if (getIntent().hasExtra(EXTRA_PIP_ORIENTATION)) { 115 final int ori = Integer.parseInt(getIntent().getStringExtra(EXTRA_PIP_ORIENTATION)); 116 setRequestedOrientation(ori); 117 } 118 119 // Set the window flag to show over the keyguard 120 if (getIntent().hasExtra(EXTRA_SHOW_OVER_KEYGUARD)) { 121 setShowWhenLocked(true); 122 } 123 124 // Enter picture in picture with the given aspect ratio if provided 125 if (getIntent().hasExtra(EXTRA_ENTER_PIP)) { 126 if (getIntent().hasExtra(EXTRA_ENTER_PIP_ASPECT_RATIO_NUMERATOR) 127 && getIntent().hasExtra(EXTRA_ENTER_PIP_ASPECT_RATIO_DENOMINATOR)) { 128 try { 129 final PictureInPictureParams.Builder builder = 130 new PictureInPictureParams.Builder(); 131 builder.setAspectRatio(getAspectRatio(getIntent(), 132 EXTRA_ENTER_PIP_ASPECT_RATIO_NUMERATOR, 133 EXTRA_ENTER_PIP_ASPECT_RATIO_DENOMINATOR)); 134 enterPictureInPictureMode(builder.build()); 135 } catch (Exception e) { 136 // This call can fail intentionally if the aspect ratio is too extreme 137 } 138 } else { 139 enterPictureInPictureMode(new PictureInPictureParams.Builder().build()); 140 } 141 } 142 143 // We need to wait for either enterPictureInPicture() or requestAutoEnterPictureInPicture() 144 // to be called before setting the aspect ratio 145 if (getIntent().hasExtra(EXTRA_SET_ASPECT_RATIO_NUMERATOR) 146 && getIntent().hasExtra(EXTRA_SET_ASPECT_RATIO_DENOMINATOR)) { 147 final PictureInPictureParams.Builder builder = 148 new PictureInPictureParams.Builder(); 149 builder.setAspectRatio(getAspectRatio(getIntent(), 150 EXTRA_SET_ASPECT_RATIO_NUMERATOR, EXTRA_SET_ASPECT_RATIO_DENOMINATOR)); 151 try { 152 setPictureInPictureParams(builder.build()); 153 } catch (Exception e) { 154 // This call can fail intentionally if the aspect ratio is too extreme 155 } 156 } 157 158 // Enable tap to finish if necessary 159 if (getIntent().hasExtra(EXTRA_TAP_TO_FINISH)) { 160 setContentView(R.layout.tap_to_finish_pip_layout); 161 findViewById(R.id.content).setOnClickListener(v -> { 162 finish(); 163 }); 164 } 165 166 // Launch a new activity if requested 167 String launchActivityComponent = getIntent().getStringExtra(EXTRA_START_ACTIVITY); 168 if (launchActivityComponent != null) { 169 Intent launchIntent = new Intent(); 170 launchIntent.setComponent(ComponentName.unflattenFromString(launchActivityComponent)); 171 startActivity(launchIntent); 172 } 173 174 // Register the broadcast receiver 175 IntentFilter filter = new IntentFilter(); 176 filter.addAction(ACTION_ENTER_PIP); 177 filter.addAction(ACTION_MOVE_TO_BACK); 178 filter.addAction(ACTION_EXPAND_PIP); 179 filter.addAction(ACTION_SET_REQUESTED_ORIENTATION); 180 filter.addAction(ACTION_FINISH); 181 registerReceiver(mReceiver, filter); 182 183 // Dump applied display metrics. 184 Configuration config = getResources().getConfiguration(); 185 dumpDisplaySize(config); 186 dumpConfiguration(config); 187 } 188 189 @Override 190 protected void onResume() { 191 super.onResume(); 192 193 // Finish self if requested 194 if (getIntent().hasExtra(EXTRA_FINISH_SELF_ON_RESUME)) { 195 finish(); 196 } 197 } 198 199 @Override 200 protected void onPause() { 201 super.onPause(); 202 203 // Pause if requested 204 if (getIntent().hasExtra(EXTRA_ON_PAUSE_DELAY)) { 205 SystemClock.sleep(Long.valueOf(getIntent().getStringExtra(EXTRA_ON_PAUSE_DELAY))); 206 } 207 208 // Enter PIP on move to background 209 if (getIntent().hasExtra(EXTRA_ENTER_PIP_ON_PAUSE)) { 210 enterPictureInPictureMode(new PictureInPictureParams.Builder().build()); 211 } 212 } 213 214 @Override 215 protected void onStop() { 216 super.onStop(); 217 218 if (getIntent().hasExtra(EXTRA_ASSERT_NO_ON_STOP_BEFORE_PIP) && !mEnteredPictureInPicture) { 219 Log.w(getTag(), "Unexpected onStop() called before entering picture-in-picture"); 220 finish(); 221 } 222 } 223 224 @Override 225 protected void onDestroy() { 226 super.onDestroy(); 227 228 unregisterReceiver(mReceiver); 229 } 230 231 @Override 232 public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode, 233 Configuration newConfig) { 234 super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig); 235 236 // Fail early if the activity state does not match the dispatched state 237 if (isInPictureInPictureMode() != isInPictureInPictureMode) { 238 Log.w(getTag(), "Received onPictureInPictureModeChanged mode=" 239 + isInPictureInPictureMode + " activityState=" + isInPictureInPictureMode()); 240 finish(); 241 } 242 243 // Mark that we've entered picture-in-picture so that we can stop checking for 244 // EXTRA_ASSERT_NO_ON_STOP_BEFORE_PIP 245 if (isInPictureInPictureMode) { 246 mEnteredPictureInPicture = true; 247 } 248 249 if (!isInPictureInPictureMode && getIntent().hasExtra(EXTRA_REENTER_PIP_ON_EXIT)) { 250 // This call to re-enter PIP can happen too quickly (host side tests can have difficulty 251 // checking that the stacks ever changed). Therefor, we need to delay here slightly to 252 // allow the tests to verify that the stacks have changed before re-entering. 253 mHandler.postDelayed(() -> { 254 enterPictureInPictureMode(new PictureInPictureParams.Builder().build()); 255 }, 1000); 256 } 257 } 258 259 @Override 260 public void onConfigurationChanged(Configuration newConfig) { 261 super.onConfigurationChanged(newConfig); 262 dumpDisplaySize(newConfig); 263 dumpConfiguration(newConfig); 264 } 265 266 /** 267 * Launches a new instance of the PipActivity directly into the pinned stack. 268 */ 269 static void launchActivityIntoPinnedStack(Activity caller, Rect bounds) { 270 final Intent intent = new Intent(caller, PipActivity.class); 271 intent.setFlags(FLAG_ACTIVITY_CLEAR_TASK | FLAG_ACTIVITY_NEW_TASK); 272 intent.putExtra(EXTRA_ASSERT_NO_ON_STOP_BEFORE_PIP, "true"); 273 274 final ActivityOptions options = ActivityOptions.makeBasic(); 275 options.setLaunchBounds(bounds); 276 options.setLaunchWindowingMode(WINDOWING_MODE_PINNED); 277 caller.startActivity(intent, options.toBundle()); 278 } 279 280 /** 281 * Launches a new instance of the PipActivity in the same task that will automatically enter 282 * PiP. 283 */ 284 static void launchEnterPipActivity(Activity caller) { 285 final Intent intent = new Intent(caller, PipActivity.class); 286 intent.putExtra(EXTRA_ENTER_PIP, "true"); 287 intent.putExtra(EXTRA_ASSERT_NO_ON_STOP_BEFORE_PIP, "true"); 288 caller.startActivity(intent); 289 } 290 291 /** 292 * @return a {@link Rational} aspect ratio from the given intent and extras. 293 */ 294 private Rational getAspectRatio(Intent intent, String extraNum, String extraDenom) { 295 return new Rational( 296 Integer.valueOf(intent.getStringExtra(extraNum)), 297 Integer.valueOf(intent.getStringExtra(extraDenom))); 298 } 299 } 300