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