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