• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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