1 /* 2 * Copyright (C) 2018 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 com.example.android.apis.app; 18 19 import static android.app.PendingIntent.FLAG_IMMUTABLE; 20 import static android.app.PendingIntent.FLAG_UPDATE_CURRENT; 21 22 import android.app.Activity; 23 import android.app.ActivityOptions; 24 import android.app.PendingIntent; 25 import android.app.PictureInPictureParams; 26 import android.app.PictureInPictureUiState; 27 import android.app.RemoteAction; 28 import android.content.BroadcastReceiver; 29 import android.content.Context; 30 import android.content.Intent; 31 import android.content.IntentFilter; 32 import android.content.res.Configuration; 33 import android.graphics.Rect; 34 import android.graphics.drawable.Icon; 35 import android.os.Bundle; 36 import android.os.Handler; 37 import android.os.Looper; 38 import android.os.ResultReceiver; 39 import android.util.Rational; 40 import android.view.Gravity; 41 import android.view.View; 42 import android.view.WindowManager; 43 import android.widget.AdapterView; 44 import android.widget.ArrayAdapter; 45 import android.widget.CompoundButton; 46 import android.widget.LinearLayout; 47 import android.widget.RadioGroup; 48 import android.widget.Spinner; 49 import android.widget.Switch; 50 import android.window.OnBackInvokedDispatcher; 51 52 import com.example.android.apis.R; 53 import com.example.android.apis.view.FixedAspectRatioImageView; 54 55 import java.util.ArrayList; 56 import java.util.List; 57 58 public class PictureInPicture extends Activity { 59 private static final String EXTRA_ENABLE_AUTO_PIP = "auto_pip"; 60 private static final String EXTRA_ENABLE_SOURCE_RECT_HINT = "source_rect_hint"; 61 private static final String EXTRA_ENABLE_SEAMLESS_RESIZE = "seamless_resize"; 62 private static final String EXTRA_ENTER_PIP_ON_BACK = "enter_pip_on_back"; 63 private static final String EXTRA_CURRENT_POSITION = "current_position"; 64 private static final String EXTRA_ASPECT_RATIO = "aspect_ratio"; 65 66 private static final int TABLET_BREAK_POINT_DP = 700; 67 68 private static final String ACTION_CUSTOM_CLOSE = "demo.pip.custom_close"; 69 private static final String ACTION_MOVE_TO_BACK = "demo.pip.move_to_back"; 70 private final BroadcastReceiver mRemoteActionReceiver = new BroadcastReceiver() { 71 @Override 72 public void onReceive(Context context, Intent intent) { 73 switch (intent.getAction()) { 74 case ACTION_CUSTOM_CLOSE: 75 finish(); 76 break; 77 case ACTION_MOVE_TO_BACK: 78 moveTaskToBack(false /* nonRoot */); 79 break; 80 } 81 } 82 }; 83 84 public static final String KEY_ON_STOP_RECEIVER = "on_stop_receiver"; 85 private final ResultReceiver mOnStopReceiver = new ResultReceiver( 86 new Handler(Looper.myLooper())) { 87 @Override 88 protected void onReceiveResult(int resultCode, Bundle resultData) { 89 // Container activity for content-pip has stopped, replace the placeholder 90 // with actual content in this host activity. 91 mImageView.setImageResource(R.drawable.sample_1); 92 } 93 }; 94 95 private final View.OnLayoutChangeListener mOnLayoutChangeListener = 96 (v, oldLeft, oldTop, oldRight, oldBottom, newLeft, newTop, newRight, newBottom) -> { 97 updatePictureInPictureParams(); 98 }; 99 100 private final CompoundButton.OnCheckedChangeListener mOnToggleChangedListener = 101 (v, isChecked) -> updatePictureInPictureParams(); 102 103 private final RadioGroup.OnCheckedChangeListener mOnPositionChangedListener = 104 (v, id) -> updateContentPosition(id); 105 106 private LinearLayout mContainer; 107 private FixedAspectRatioImageView mImageView; 108 private View mControlGroup; 109 private Switch mAutoPipToggle; 110 private Switch mSourceRectHintToggle; 111 private Switch mSeamlessResizeToggle; 112 private Switch mEnterPipOnBackToggle; 113 private RadioGroup mCurrentPositionGroup; 114 private Spinner mAspectRatioSpinner; 115 private List<RemoteAction> mPipActions; 116 private RemoteAction mCloseAction; 117 private RemoteAction mMoveToBackAction; 118 119 @Override onCreate(Bundle savedInstanceState)120 protected void onCreate(Bundle savedInstanceState) { 121 super.onCreate(savedInstanceState); 122 setContentView(R.layout.picture_in_picture); 123 124 // Find views 125 mContainer = findViewById(R.id.container); 126 mImageView = findViewById(R.id.image); 127 mControlGroup = findViewById(R.id.control_group); 128 mAutoPipToggle = findViewById(R.id.auto_pip_toggle); 129 mSourceRectHintToggle = findViewById(R.id.source_rect_hint_toggle); 130 mSeamlessResizeToggle = findViewById(R.id.seamless_resize_toggle); 131 mEnterPipOnBackToggle = findViewById(R.id.enter_pip_on_back); 132 mCurrentPositionGroup = findViewById(R.id.current_position); 133 mAspectRatioSpinner = findViewById(R.id.aspect_ratio); 134 135 // Initiate views if applicable 136 final ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, 137 R.array.aspect_ratio_list, android.R.layout.simple_spinner_item); 138 adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); 139 mAspectRatioSpinner.setAdapter(adapter); 140 141 // Attach listeners 142 mImageView.addOnLayoutChangeListener(mOnLayoutChangeListener); 143 mAutoPipToggle.setOnCheckedChangeListener(mOnToggleChangedListener); 144 mSourceRectHintToggle.setOnCheckedChangeListener(mOnToggleChangedListener); 145 mSeamlessResizeToggle.setOnCheckedChangeListener(mOnToggleChangedListener); 146 mEnterPipOnBackToggle.setOnCheckedChangeListener(mOnToggleChangedListener); 147 getOnBackInvokedDispatcher().registerOnBackInvokedCallback( 148 OnBackInvokedDispatcher.PRIORITY_DEFAULT, () -> { 149 if (mEnterPipOnBackToggle.isChecked()) { 150 enterPictureInPictureMode(); 151 } else { 152 finish(); 153 } 154 }); 155 mCurrentPositionGroup.setOnCheckedChangeListener(mOnPositionChangedListener); 156 mAspectRatioSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { 157 @Override 158 public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { 159 final String rawText = parent.getItemAtPosition(position).toString(); 160 final String textToParse = rawText.substring( 161 rawText.indexOf('(') + 1, 162 rawText.indexOf(')')); 163 mImageView.addOnLayoutChangeListener(mOnLayoutChangeListener); 164 mImageView.setAspectRatio(Rational.parseRational(textToParse)); 165 } 166 167 @Override 168 public void onNothingSelected(AdapterView<?> parent) { 169 // Do nothing. 170 } 171 }); 172 findViewById(R.id.enter_pip_button).setOnClickListener(v -> enterPictureInPictureMode()); 173 findViewById(R.id.enter_content_pip_button).setOnClickListener(v -> enterContentPip()); 174 175 // Set defaults 176 final Intent intent = getIntent(); 177 mAutoPipToggle.setChecked(intent.getBooleanExtra(EXTRA_ENABLE_AUTO_PIP, false)); 178 mSourceRectHintToggle.setChecked( 179 intent.getBooleanExtra(EXTRA_ENABLE_SOURCE_RECT_HINT, false)); 180 mSeamlessResizeToggle.setChecked( 181 intent.getBooleanExtra(EXTRA_ENABLE_SEAMLESS_RESIZE, false)); 182 mEnterPipOnBackToggle.setChecked( 183 intent.getBooleanExtra(EXTRA_ENTER_PIP_ON_BACK, false)); 184 final int positionId = "end".equalsIgnoreCase( 185 intent.getStringExtra(EXTRA_CURRENT_POSITION)) 186 ? R.id.radio_current_end 187 : R.id.radio_current_start; 188 mCurrentPositionGroup.check(positionId); 189 mAspectRatioSpinner.setSelection(1); 190 191 updateLayout(getResources().getConfiguration()); 192 } 193 194 @Override onStart()195 protected void onStart() { 196 super.onStart(); 197 setupPipActions(); 198 } 199 200 @Override onResume()201 protected void onResume() { 202 super.onResume(); 203 findViewById(R.id.text_to_hide).setVisibility(View.VISIBLE); 204 } 205 206 @Override onUserLeaveHint()207 protected void onUserLeaveHint() { 208 // Only used when auto PiP is disabled. This is to simulate the behavior that an app 209 // supports regular PiP but not auto PiP. 210 if (!mAutoPipToggle.isChecked()) { 211 enterPictureInPictureMode(); 212 } 213 } 214 215 @Override onConfigurationChanged(Configuration newConfiguration)216 public void onConfigurationChanged(Configuration newConfiguration) { 217 super.onConfigurationChanged(newConfiguration); 218 updateLayout(newConfiguration); 219 } 220 221 @Override onPictureInPictureModeChanged(boolean isInPictureInPictureMode, Configuration newConfig)222 public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode, 223 Configuration newConfig) { 224 if (!isInPictureInPictureMode) { 225 // When it's about to exit PiP mode, always reset the mImageView position to start. 226 // If position is previously set to end, this should demonstrate the exit 227 // source rect hint behavior introduced in S. 228 mCurrentPositionGroup.check(R.id.radio_current_start); 229 } 230 } 231 232 @Override onPictureInPictureUiStateChanged(PictureInPictureUiState pipState)233 public void onPictureInPictureUiStateChanged(PictureInPictureUiState pipState) { 234 if (pipState.isTransitioningToPip()) { 235 findViewById(R.id.text_to_hide).setVisibility(View.INVISIBLE); 236 } 237 } 238 239 @Override onStop()240 protected void onStop() { 241 super.onStop(); 242 unregisterReceiver(mRemoteActionReceiver); 243 } 244 245 /** 246 * This is what we expect most host Activity would do to trigger content PiP. 247 * - Get the bounds of the view to be transferred to content PiP 248 * - Construct the PictureInPictureParams with source rect hint and aspect ratio from bounds 249 * - Start the new content PiP container Activity with the ActivityOptions 250 */ enterContentPip()251 private void enterContentPip() { 252 final Intent intent = new Intent(this, ContentPictureInPicture.class); 253 intent.putExtra(KEY_ON_STOP_RECEIVER, mOnStopReceiver); 254 final Rect bounds = new Rect(); 255 mImageView.getGlobalVisibleRect(bounds); 256 final PictureInPictureParams params = new PictureInPictureParams.Builder() 257 .setSourceRectHint(bounds) 258 .setAspectRatio(new Rational(bounds.width(), bounds.height())) 259 .build(); 260 final ActivityOptions opts = ActivityOptions.makeLaunchIntoPip(params); 261 startActivity(intent, opts.toBundle()); 262 // Swap the mImageView to placeholder content. 263 mImageView.setImageResource(R.drawable.black_box); 264 } 265 updateLayout(Configuration configuration)266 private void updateLayout(Configuration configuration) { 267 mImageView.addOnLayoutChangeListener(mOnLayoutChangeListener); 268 final boolean isTablet = configuration.smallestScreenWidthDp >= TABLET_BREAK_POINT_DP; 269 final boolean isLandscape = 270 (configuration.orientation == Configuration.ORIENTATION_LANDSCAPE); 271 final boolean isPictureInPicture = isInPictureInPictureMode(); 272 if (isPictureInPicture) { 273 setupPictureInPictureLayout(); 274 } else if (isTablet && isLandscape) { 275 setupTabletLandscapeLayout(); 276 } else if (isLandscape) { 277 setupFullScreenLayout(); 278 } else { 279 setupRegularLayout(); 280 } 281 } 282 setupPipActions()283 private void setupPipActions() { 284 final IntentFilter remoteActionFilter = new IntentFilter(); 285 mPipActions = new ArrayList<>(); 286 287 remoteActionFilter.addAction(ACTION_CUSTOM_CLOSE); 288 final Intent closeIntent = new Intent(ACTION_CUSTOM_CLOSE).setPackage(getPackageName()); 289 mCloseAction = new RemoteAction( 290 Icon.createWithResource(this, R.drawable.ic_call_end), 291 getString(R.string.action_custom_close), 292 getString(R.string.action_custom_close), 293 PendingIntent.getBroadcast(this, 0 /* requestCode */, closeIntent, 294 FLAG_UPDATE_CURRENT | FLAG_IMMUTABLE)); 295 mPipActions.add(mCloseAction); 296 297 remoteActionFilter.addAction(ACTION_MOVE_TO_BACK); 298 final Intent backIntent = new Intent(ACTION_MOVE_TO_BACK).setPackage(getPackageName()); 299 mMoveToBackAction = new RemoteAction( 300 Icon.createWithResource(this, R.drawable.ic_eject), 301 getString(R.string.action_move_to_back), 302 getString(R.string.action_move_to_back), 303 PendingIntent.getBroadcast(this, 0 /* requestCode */, backIntent, 304 FLAG_UPDATE_CURRENT | FLAG_IMMUTABLE)); 305 mPipActions.add(mMoveToBackAction); 306 307 registerReceiver(mRemoteActionReceiver, remoteActionFilter); 308 } 309 setupPictureInPictureLayout()310 private void setupPictureInPictureLayout() { 311 mControlGroup.setVisibility(View.GONE); 312 final LinearLayout.LayoutParams imageLp = new LinearLayout.LayoutParams( 313 LinearLayout.LayoutParams.MATCH_PARENT, 314 LinearLayout.LayoutParams.MATCH_PARENT); 315 imageLp.gravity = Gravity.NO_GRAVITY; 316 mImageView.setLayoutParams(imageLp); 317 } 318 setupTabletLandscapeLayout()319 private void setupTabletLandscapeLayout() { 320 mControlGroup.setVisibility(View.VISIBLE); 321 exitFullScreenMode(); 322 323 final LinearLayout.LayoutParams imageLp = new LinearLayout.LayoutParams( 324 LinearLayout.LayoutParams.MATCH_PARENT, 325 LinearLayout.LayoutParams.WRAP_CONTENT); 326 imageLp.gravity = Gravity.NO_GRAVITY; 327 enterTwoPaneMode(imageLp); 328 } 329 setupFullScreenLayout()330 private void setupFullScreenLayout() { 331 mControlGroup.setVisibility(View.GONE); 332 enterFullScreenMode(); 333 334 final LinearLayout.LayoutParams imageLp = new LinearLayout.LayoutParams( 335 LinearLayout.LayoutParams.WRAP_CONTENT, 336 LinearLayout.LayoutParams.MATCH_PARENT); 337 imageLp.gravity = Gravity.CENTER_HORIZONTAL; 338 enterOnePaneMode(imageLp); 339 } 340 setupRegularLayout()341 private void setupRegularLayout() { 342 mControlGroup.setVisibility(View.VISIBLE); 343 exitFullScreenMode(); 344 345 final LinearLayout.LayoutParams imageLp = new LinearLayout.LayoutParams( 346 LinearLayout.LayoutParams.MATCH_PARENT, 347 LinearLayout.LayoutParams.WRAP_CONTENT); 348 imageLp.gravity = Gravity.NO_GRAVITY; 349 enterOnePaneMode(imageLp); 350 } 351 enterOnePaneMode(LinearLayout.LayoutParams imageLp)352 private void enterOnePaneMode(LinearLayout.LayoutParams imageLp) { 353 mContainer.setOrientation(LinearLayout.VERTICAL); 354 355 final LinearLayout.LayoutParams controlLp = 356 (LinearLayout.LayoutParams) mControlGroup.getLayoutParams(); 357 controlLp.width = LinearLayout.LayoutParams.MATCH_PARENT; 358 controlLp.height = 0; 359 controlLp.weight = 1; 360 mControlGroup.setLayoutParams(controlLp); 361 362 imageLp.weight = 0; 363 mImageView.setLayoutParams(imageLp); 364 } 365 enterTwoPaneMode(LinearLayout.LayoutParams imageLp)366 private void enterTwoPaneMode(LinearLayout.LayoutParams imageLp) { 367 mContainer.setOrientation(LinearLayout.HORIZONTAL); 368 369 final LinearLayout.LayoutParams controlLp = 370 (LinearLayout.LayoutParams) mControlGroup.getLayoutParams(); 371 controlLp.width = 0; 372 controlLp.height = LinearLayout.LayoutParams.MATCH_PARENT; 373 controlLp.weight = 1; 374 mControlGroup.setLayoutParams(controlLp); 375 376 imageLp.width = 0; 377 imageLp.height = LinearLayout.LayoutParams.WRAP_CONTENT; 378 imageLp.weight = 1; 379 mImageView.setLayoutParams(imageLp); 380 } 381 enterFullScreenMode()382 private void enterFullScreenMode() { 383 // TODO(b/188001699) switch to use insets controller once the bug is fixed. 384 final View decorView = getWindow().getDecorView(); 385 final int systemUiNavigationBarFlags = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION 386 | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN; 387 getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, 388 WindowManager.LayoutParams.FLAG_FULLSCREEN); 389 decorView.setSystemUiVisibility(decorView.getSystemUiVisibility() 390 | systemUiNavigationBarFlags); 391 } 392 exitFullScreenMode()393 private void exitFullScreenMode() { 394 final View decorView = getWindow().getDecorView(); 395 final int systemUiNavigationBarFlags = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION 396 | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN; 397 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); 398 decorView.setSystemUiVisibility(decorView.getSystemUiVisibility() 399 & ~systemUiNavigationBarFlags); 400 } 401 updatePictureInPictureParams()402 private void updatePictureInPictureParams() { 403 mImageView.removeOnLayoutChangeListener(mOnLayoutChangeListener); 404 // do not bother PictureInPictureParams update when it's already in pip mode. 405 if (isInPictureInPictureMode()) return; 406 final Rect imageViewRect = new Rect(); 407 mImageView.getGlobalVisibleRect(imageViewRect); 408 // bail early if mImageView has not been measured yet 409 if (imageViewRect.isEmpty()) return; 410 final PictureInPictureParams.Builder builder = new PictureInPictureParams.Builder() 411 .setAutoEnterEnabled(mAutoPipToggle.isChecked()) 412 .setSourceRectHint(mSourceRectHintToggle.isChecked() 413 ? new Rect(imageViewRect) : null) 414 .setSeamlessResizeEnabled(mSeamlessResizeToggle.isChecked()) 415 .setAspectRatio(new Rational(imageViewRect.width(), imageViewRect.height())) 416 .setActions(mPipActions) 417 .setCloseAction(mCloseAction); 418 setPictureInPictureParams(builder.build()); 419 } 420 updateContentPosition(int checkedId)421 private void updateContentPosition(int checkedId) { 422 mContainer.removeAllViews(); 423 mImageView.addOnLayoutChangeListener(mOnLayoutChangeListener); 424 if (checkedId == R.id.radio_current_start) { 425 mContainer.addView(mImageView, 0); 426 mContainer.addView(mControlGroup, 1); 427 } else { 428 mContainer.addView(mControlGroup, 0); 429 mContainer.addView(mImageView, 1); 430 } 431 } 432 } 433