├── .google └── packaging.yaml ├── CONTRIBUTING.md ├── LICENSE ├── NOTICE ├── README.md ├── app ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── example │ │ └── android │ │ └── pictureinpicture │ │ ├── MainActivityTest.java │ │ └── MediaSessionPlaybackActivityTest.java │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── example │ │ └── android │ │ └── pictureinpicture │ │ ├── MainActivity.java │ │ ├── MediaSessionPlaybackActivity.java │ │ └── widget │ │ └── MovieView.java │ └── res │ ├── drawable │ ├── ic_fast_forward_64dp.xml │ ├── ic_fast_rewind_64dp.xml │ ├── ic_info_24dp.xml │ ├── ic_minimize_24dp.xml │ ├── ic_pause_24dp.xml │ ├── ic_pause_64dp.xml │ ├── ic_picture_in_picture_alt.xml │ ├── ic_play_arrow_24dp.xml │ ├── ic_play_arrow_64dp.xml │ └── shade.xml │ ├── layout │ ├── activity_main.xml │ └── view_movie.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── mipmap-xxxhdpi │ └── ic_launcher.png │ ├── raw │ └── vid_bigbuckbunny.mp4 │ ├── values-w820dp │ └── dimens.xml │ └── values │ ├── attrs.xml │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── kotlinApp ├── .google │ └── packaging.yaml ├── CONTRIBUTING.md ├── LICENSE ├── NOTICE ├── README.md ├── app │ ├── build.gradle │ └── src │ │ ├── androidTest │ │ └── java │ │ │ └── com │ │ │ └── example │ │ │ └── android │ │ │ └── pictureinpicture │ │ │ ├── MainActivityTest.kt │ │ │ └── MediaSessionPlaybackActivityTest.kt │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── com │ │ │ └── example │ │ │ └── android │ │ │ └── pictureinpicture │ │ │ ├── MainActivity.kt │ │ │ ├── MediaSessionPlaybackActivity.kt │ │ │ └── widget │ │ │ └── MovieView.kt │ │ └── res │ │ ├── drawable │ │ ├── ic_fast_forward_64dp.xml │ │ ├── ic_fast_rewind_64dp.xml │ │ ├── ic_info_24dp.xml │ │ ├── ic_minimize_24dp.xml │ │ ├── ic_pause_24dp.xml │ │ ├── ic_pause_64dp.xml │ │ ├── ic_picture_in_picture_alt.xml │ │ ├── ic_play_arrow_24dp.xml │ │ ├── ic_play_arrow_64dp.xml │ │ └── shade.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ └── view_movie.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ ├── raw │ │ └── vid_bigbuckbunny.mp4 │ │ ├── values-w820dp │ │ └── dimens.xml │ │ └── values │ │ ├── attrs.xml │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── proguard-rules.pro ├── screenshots │ ├── 1-main.png │ ├── 2-pip.png │ └── icon-web.png └── settings.gradle ├── screenshots ├── 1-main.png ├── 2-pip.png └── icon-web.png └── settings.gradle /.google/packaging.yaml: -------------------------------------------------------------------------------- 1 | 2 | # GOOGLE SAMPLE PACKAGING DATA 3 | # 4 | # This file is used by Google as part of our samples packaging process. 5 | # End users may safely ignore this file. It has no relevance to other systems. 6 | --- 7 | status: PUBLISHED 8 | technologies: [Android] 9 | categories: [Media, Android Oreo] 10 | languages: [Java] 11 | solutions: [Mobile] 12 | github: android-PictureInPicture 13 | level: ADVANCED 14 | icon: screenshots/icon-web.png 15 | apiRefs: 16 | - android:android.app.PictureInPictureParams 17 | - android:android.app.RemoteAction 18 | - android:android.app.PendingIntent 19 | - android:android.support.v4.media.session.MediaSessionCompat 20 | license: apache2 21 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to become a contributor and submit your own code 2 | 3 | ## Contributor License Agreements 4 | 5 | We'd love to accept your sample apps and patches! Before we can take them, we 6 | have to jump a couple of legal hurdles. 7 | 8 | Please fill out either the individual or corporate Contributor License Agreement (CLA). 9 | 10 | * If you are an individual writing original source code and you're sure you 11 | own the intellectual property, then you'll need to sign an [individual CLA] 12 | (https://cla.developers.google.com). 13 | * If you work for a company that wants to allow you to contribute your work, 14 | then you'll need to sign a [corporate CLA] 15 | (https://cla.developers.google.com). 16 | 17 | Follow either of the two links above to access the appropriate CLA and 18 | instructions for how to sign and return it. Once we receive it, we'll be able to 19 | accept your pull requests. 20 | 21 | ## Contributing A Patch 22 | 23 | 1. Submit an issue describing your proposed change to the repo in question. 24 | 1. The repo owner will respond to your issue promptly. 25 | 1. If your proposed change is accepted, and you haven't already done so, sign a 26 | Contributor License Agreement (see details above). 27 | 1. Fork the desired repo, develop and test your code changes. 28 | 1. Ensure that your code adheres to the existing style in the sample to which 29 | you are contributing. Refer to the 30 | [Android Code Style Guide] 31 | (https://source.android.com/source/code-style.html) for the 32 | recommended coding standards for this organization. 33 | 1. Ensure that your code has an appropriate set of unit tests which all pass. 34 | 1. Submit a pull request. 35 | 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | -------------- 3 | 4 | Version 2.0, January 2004 5 | http://www.apache.org/licenses/ 6 | 7 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 8 | 9 | 1. Definitions. 10 | 11 | "License" shall mean the terms and conditions for use, reproduction, 12 | and distribution as defined by Sections 1 through 9 of this document. 13 | 14 | "Licensor" shall mean the copyright owner or entity authorized by 15 | the copyright owner that is granting the License. 16 | 17 | "Legal Entity" shall mean the union of the acting entity and all 18 | other entities that control, are controlled by, or are under common 19 | control with that entity. For the purposes of this definition, 20 | "control" means (i) the power, direct or indirect, to cause the 21 | direction or management of such entity, whether by contract or 22 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 23 | outstanding shares, or (iii) beneficial ownership of such entity. 24 | 25 | "You" (or "Your") shall mean an individual or Legal Entity 26 | exercising permissions granted by this License. 27 | 28 | "Source" form shall mean the preferred form for making modifications, 29 | including but not limited to software source code, documentation 30 | source, and configuration files. 31 | 32 | "Object" form shall mean any form resulting from mechanical 33 | transformation or translation of a Source form, including but 34 | not limited to compiled object code, generated documentation, 35 | and conversions to other media types. 36 | 37 | "Work" shall mean the work of authorship, whether in Source or 38 | Object form, made available under the License, as indicated by a 39 | copyright notice that is included in or attached to the work 40 | (an example is provided in the Appendix below). 41 | 42 | "Derivative Works" shall mean any work, whether in Source or Object 43 | form, that is based on (or derived from) the Work and for which the 44 | editorial revisions, annotations, elaborations, or other modifications 45 | represent, as a whole, an original work of authorship. For the purposes 46 | of this License, Derivative Works shall not include works that remain 47 | separable from, or merely link (or bind by name) to the interfaces of, 48 | the Work and Derivative Works thereof. 49 | 50 | "Contribution" shall mean any work of authorship, including 51 | the original version of the Work and any modifications or additions 52 | to that Work or Derivative Works thereof, that is intentionally 53 | submitted to Licensor for inclusion in the Work by the copyright owner 54 | or by an individual or Legal Entity authorized to submit on behalf of 55 | the copyright owner. For the purposes of this definition, "submitted" 56 | means any form of electronic, verbal, or written communication sent 57 | to the Licensor or its representatives, including but not limited to 58 | communication on electronic mailing lists, source code control systems, 59 | and issue tracking systems that are managed by, or on behalf of, the 60 | Licensor for the purpose of discussing and improving the Work, but 61 | excluding communication that is conspicuously marked or otherwise 62 | designated in writing by the copyright owner as "Not a Contribution." 63 | 64 | "Contributor" shall mean Licensor and any individual or Legal Entity 65 | on behalf of whom a Contribution has been received by Licensor and 66 | subsequently incorporated within the Work. 67 | 68 | 2. Grant of Copyright License. Subject to the terms and conditions of 69 | this License, each Contributor hereby grants to You a perpetual, 70 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 71 | copyright license to reproduce, prepare Derivative Works of, 72 | publicly display, publicly perform, sublicense, and distribute the 73 | Work and such Derivative Works in Source or Object form. 74 | 75 | 3. Grant of Patent License. Subject to the terms and conditions of 76 | this License, each Contributor hereby grants to You a perpetual, 77 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 78 | (except as stated in this section) patent license to make, have made, 79 | use, offer to sell, sell, import, and otherwise transfer the Work, 80 | where such license applies only to those patent claims licensable 81 | by such Contributor that are necessarily infringed by their 82 | Contribution(s) alone or by combination of their Contribution(s) 83 | with the Work to which such Contribution(s) was submitted. If You 84 | institute patent litigation against any entity (including a 85 | cross-claim or counterclaim in a lawsuit) alleging that the Work 86 | or a Contribution incorporated within the Work constitutes direct 87 | or contributory patent infringement, then any patent licenses 88 | granted to You under this License for that Work shall terminate 89 | as of the date such litigation is filed. 90 | 91 | 4. Redistribution. You may reproduce and distribute copies of the 92 | Work or Derivative Works thereof in any medium, with or without 93 | modifications, and in Source or Object form, provided that You 94 | meet the following conditions: 95 | 96 | (a) You must give any other recipients of the Work or 97 | Derivative Works a copy of this License; and 98 | 99 | (b) You must cause any modified files to carry prominent notices 100 | stating that You changed the files; and 101 | 102 | (c) You must retain, in the Source form of any Derivative Works 103 | that You distribute, all copyright, patent, trademark, and 104 | attribution notices from the Source form of the Work, 105 | excluding those notices that do not pertain to any part of 106 | the Derivative Works; and 107 | 108 | (d) If the Work includes a "NOTICE" text file as part of its 109 | distribution, then any Derivative Works that You distribute must 110 | include a readable copy of the attribution notices contained 111 | within such NOTICE file, excluding those notices that do not 112 | pertain to any part of the Derivative Works, in at least one 113 | of the following places: within a NOTICE text file distributed 114 | as part of the Derivative Works; within the Source form or 115 | documentation, if provided along with the Derivative Works; or, 116 | within a display generated by the Derivative Works, if and 117 | wherever such third-party notices normally appear. The contents 118 | of the NOTICE file are for informational purposes only and 119 | do not modify the License. You may add Your own attribution 120 | notices within Derivative Works that You distribute, alongside 121 | or as an addendum to the NOTICE text from the Work, provided 122 | that such additional attribution notices cannot be construed 123 | as modifying the License. 124 | 125 | You may add Your own copyright statement to Your modifications and 126 | may provide additional or different license terms and conditions 127 | for use, reproduction, or distribution of Your modifications, or 128 | for any such Derivative Works as a whole, provided Your use, 129 | reproduction, and distribution of the Work otherwise complies with 130 | the conditions stated in this License. 131 | 132 | 5. Submission of Contributions. Unless You explicitly state otherwise, 133 | any Contribution intentionally submitted for inclusion in the Work 134 | by You to the Licensor shall be under the terms and conditions of 135 | this License, without any additional terms or conditions. 136 | Notwithstanding the above, nothing herein shall supersede or modify 137 | the terms of any separate license agreement you may have executed 138 | with Licensor regarding such Contributions. 139 | 140 | 6. Trademarks. This License does not grant permission to use the trade 141 | names, trademarks, service marks, or product names of the Licensor, 142 | except as required for reasonable and customary use in describing the 143 | origin of the Work and reproducing the content of the NOTICE file. 144 | 145 | 7. Disclaimer of Warranty. Unless required by applicable law or 146 | agreed to in writing, Licensor provides the Work (and each 147 | Contributor provides its Contributions) on an "AS IS" BASIS, 148 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 149 | implied, including, without limitation, any warranties or conditions 150 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 151 | PARTICULAR PURPOSE. You are solely responsible for determining the 152 | appropriateness of using or redistributing the Work and assume any 153 | risks associated with Your exercise of permissions under this License. 154 | 155 | 8. Limitation of Liability. In no event and under no legal theory, 156 | whether in tort (including negligence), contract, or otherwise, 157 | unless required by applicable law (such as deliberate and grossly 158 | negligent acts) or agreed to in writing, shall any Contributor be 159 | liable to You for damages, including any direct, indirect, special, 160 | incidental, or consequential damages of any character arising as a 161 | result of this License or out of the use or inability to use the 162 | Work (including but not limited to damages for loss of goodwill, 163 | work stoppage, computer failure or malfunction, or any and all 164 | other commercial damages or losses), even if such Contributor 165 | has been advised of the possibility of such damages. 166 | 167 | 9. Accepting Warranty or Additional Liability. While redistributing 168 | the Work or Derivative Works thereof, You may choose to offer, 169 | and charge a fee for, acceptance of support, warranty, indemnity, 170 | or other liability obligations and/or rights consistent with this 171 | License. However, in accepting such obligations, You may act only 172 | on Your own behalf and on Your sole responsibility, not on behalf 173 | of any other Contributor, and only if You agree to indemnify, 174 | defend, and hold each Contributor harmless for any liability 175 | incurred by, or claims asserted against, such Contributor by reason 176 | of your accepting any such warranty or additional liability. 177 | 178 | END OF TERMS AND CONDITIONS 179 | 180 | APPENDIX: How to apply the Apache License to your work. 181 | 182 | To apply the Apache License to your work, attach the following 183 | boilerplate notice, with the fields enclosed by brackets "{}" 184 | replaced with your own identifying information. (Don't include 185 | the brackets!) The text should be enclosed in the appropriate 186 | comment syntax for the file format. We also recommend that a 187 | file or class name and description of purpose be included on the 188 | same "printed page" as the copyright notice for easier 189 | identification within third-party archives. 190 | 191 | Copyright {yyyy} {name of copyright owner} 192 | 193 | Licensed under the Apache License, Version 2.0 (the "License"); 194 | you may not use this file except in compliance with the License. 195 | You may obtain a copy of the License at 196 | 197 | http://www.apache.org/licenses/LICENSE-2.0 198 | 199 | Unless required by applicable law or agreed to in writing, software 200 | distributed under the License is distributed on an "AS IS" BASIS, 201 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 202 | See the License for the specific language governing permissions and 203 | limitations under the License. 204 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | 2 | This sample uses the following software: 3 | 4 | Copyright 2018 The Android Open Source Project 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Android PictureInPicture Sample 3 | =============================== 4 | 5 | This repo has been migrated to [github.com/android/media-samples][1]. Please check that repo for future updates. Thank you! 6 | 7 | [1]: https://github.com/android/media-samples 8 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 27 5 | defaultConfig { 6 | applicationId "com.example.android.pictureinpicture" 7 | minSdkVersion 26 8 | targetSdkVersion 27 9 | versionCode 1 10 | versionName "1.0" 11 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 12 | } 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 17 | } 18 | } 19 | } 20 | 21 | dependencies { 22 | compile fileTree(dir: 'libs', include: ['*.jar']) 23 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { 24 | exclude group: 'com.android.support', module: 'support-annotations' 25 | }) 26 | compile 'com.android.support:appcompat-v7:27.0.0' 27 | compile 'com.android.support:support-media-compat:27.0.0' 28 | 29 | testCompile 'junit:junit:4.12' 30 | } 31 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/google/home/yaraki/Android/Sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/example/android/pictureinpicture/MainActivityTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 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.pictureinpicture; 18 | 19 | import static android.support.test.espresso.Espresso.onView; 20 | import static android.support.test.espresso.action.ViewActions.click; 21 | import static android.support.test.espresso.assertion.ViewAssertions.matches; 22 | import static android.support.test.espresso.matcher.ViewMatchers.isAssignableFrom; 23 | import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; 24 | import static android.support.test.espresso.matcher.ViewMatchers.withId; 25 | 26 | import static org.hamcrest.Matchers.not; 27 | import static org.hamcrest.core.AllOf.allOf; 28 | import static org.junit.Assert.assertNotNull; 29 | import static org.junit.Assert.assertThat; 30 | import static org.junit.Assert.assertTrue; 31 | 32 | import android.content.pm.ActivityInfo; 33 | import android.support.test.InstrumentationRegistry; 34 | import android.support.test.espresso.UiController; 35 | import android.support.test.espresso.ViewAction; 36 | import android.support.test.rule.ActivityTestRule; 37 | import android.support.test.runner.AndroidJUnit4; 38 | import android.view.View; 39 | 40 | import com.example.android.pictureinpicture.widget.MovieView; 41 | 42 | import org.hamcrest.Description; 43 | import org.hamcrest.Matcher; 44 | import org.hamcrest.TypeSafeMatcher; 45 | import org.junit.Rule; 46 | import org.junit.Test; 47 | import org.junit.runner.RunWith; 48 | 49 | 50 | @RunWith(AndroidJUnit4.class) 51 | public class MainActivityTest { 52 | 53 | @Rule 54 | public ActivityTestRule rule = new ActivityTestRule<>(MainActivity.class); 55 | 56 | @Test 57 | public void movie_playingOnPip() throws Throwable { 58 | // The movie should be playing on start 59 | onView(withId(R.id.movie)) 60 | .check(matches(allOf(isDisplayed(), isPlaying()))) 61 | .perform(showControls()); 62 | // Click on the button to enter Picture-in-Picture mode 63 | onView(withId(R.id.minimize)).perform(click()); 64 | InstrumentationRegistry.getInstrumentation().waitForIdleSync(); 65 | // The Activity is paused. We cannot use Espresso to test paused activities. 66 | rule.runOnUiThread(new Runnable() { 67 | @Override 68 | public void run() { 69 | // We are now in Picture-in-Picture mode 70 | assertTrue(rule.getActivity().isInPictureInPictureMode()); 71 | final MovieView view = rule.getActivity().findViewById(R.id.movie); 72 | assertNotNull(view); 73 | // The video should still be playing 74 | assertTrue(view.isPlaying()); 75 | } 76 | }); 77 | } 78 | 79 | @Test 80 | public void movie_pauseAndResume() throws Throwable { 81 | // The movie should be playing on start 82 | onView(withId(R.id.movie)) 83 | .check(matches(allOf(isDisplayed(), isPlaying()))) 84 | .perform(showControls()); 85 | // Pause 86 | onView(withId(R.id.toggle)).perform(click()); 87 | onView(withId(R.id.movie)).check(matches((not(isPlaying())))); 88 | // Resume 89 | onView(withId(R.id.toggle)).perform(click()); 90 | onView(withId(R.id.movie)).check(matches(isPlaying())); 91 | } 92 | 93 | @Test 94 | public void fullscreen_enabledOnLandscape() throws Throwable { 95 | rule.runOnUiThread(new Runnable() { 96 | @Override 97 | public void run() { 98 | rule.getActivity() 99 | .setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); 100 | } 101 | }); 102 | InstrumentationRegistry.getInstrumentation().waitForIdleSync(); 103 | rule.runOnUiThread(new Runnable() { 104 | @Override 105 | public void run() { 106 | final View decorView = rule.getActivity().getWindow().getDecorView(); 107 | assertThat(decorView.getSystemUiVisibility(), 108 | hasFlag(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)); 109 | } 110 | }); 111 | } 112 | 113 | @Test 114 | public void fullscreen_disabledOnPortrait() throws Throwable { 115 | rule.runOnUiThread(new Runnable() { 116 | @Override 117 | public void run() { 118 | rule.getActivity() 119 | .setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); 120 | } 121 | }); 122 | InstrumentationRegistry.getInstrumentation().waitForIdleSync(); 123 | rule.runOnUiThread(new Runnable() { 124 | @Override 125 | public void run() { 126 | final View decorView = rule.getActivity().getWindow().getDecorView(); 127 | assertThat(decorView.getSystemUiVisibility(), 128 | not(hasFlag(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN))); 129 | } 130 | }); 131 | } 132 | 133 | private static Matcher isPlaying() { 134 | return new TypeSafeMatcher() { 135 | @Override 136 | protected boolean matchesSafely(View view) { 137 | return ((MovieView) view).isPlaying(); 138 | } 139 | 140 | @Override 141 | public void describeTo(Description description) { 142 | description.appendText("MovieView is playing"); 143 | } 144 | }; 145 | } 146 | 147 | private static ViewAction showControls() { 148 | return new ViewAction() { 149 | @Override 150 | public Matcher getConstraints() { 151 | return isAssignableFrom(MovieView.class); 152 | } 153 | 154 | @Override 155 | public String getDescription() { 156 | return "Show controls of MovieView"; 157 | } 158 | 159 | @Override 160 | public void perform(UiController uiController, View view) { 161 | uiController.loopMainThreadUntilIdle(); 162 | ((MovieView) view).showControls(); 163 | uiController.loopMainThreadUntilIdle(); 164 | } 165 | }; 166 | } 167 | 168 | private static Matcher hasFlag(final int flag) { 169 | return new TypeSafeMatcher() { 170 | @Override 171 | protected boolean matchesSafely(Integer i) { 172 | return (i & flag) == flag; 173 | } 174 | 175 | @Override 176 | public void describeTo(Description description) { 177 | description.appendText("Flag integer contains " + flag); 178 | } 179 | }; 180 | } 181 | 182 | } 183 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/example/android/pictureinpicture/MediaSessionPlaybackActivityTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 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.pictureinpicture; 18 | 19 | import android.content.pm.ActivityInfo; 20 | import android.media.session.PlaybackState; 21 | import android.support.test.InstrumentationRegistry; 22 | import android.support.test.espresso.UiController; 23 | import android.support.test.espresso.ViewAction; 24 | import android.support.test.rule.ActivityTestRule; 25 | import android.support.test.runner.AndroidJUnit4; 26 | import android.support.v4.media.session.PlaybackStateCompat; 27 | import android.view.View; 28 | 29 | import com.example.android.pictureinpicture.widget.MovieView; 30 | 31 | import org.hamcrest.Description; 32 | import org.hamcrest.Matcher; 33 | import org.hamcrest.TypeSafeMatcher; 34 | import org.junit.Rule; 35 | import org.junit.Test; 36 | import org.junit.runner.RunWith; 37 | 38 | import static android.support.test.espresso.Espresso.onView; 39 | import static android.support.test.espresso.action.ViewActions.click; 40 | import static android.support.test.espresso.assertion.ViewAssertions.matches; 41 | import static android.support.test.espresso.matcher.ViewMatchers.isAssignableFrom; 42 | import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; 43 | import static android.support.test.espresso.matcher.ViewMatchers.withId; 44 | import static org.hamcrest.Matchers.not; 45 | import static org.hamcrest.core.AllOf.allOf; 46 | import static org.hamcrest.core.Is.is; 47 | import static org.hamcrest.core.IsEqual.equalTo; 48 | import static org.junit.Assert.assertNotNull; 49 | import static org.junit.Assert.assertThat; 50 | import static org.junit.Assert.assertTrue; 51 | 52 | 53 | @RunWith(AndroidJUnit4.class) 54 | public class MediaSessionPlaybackActivityTest { 55 | 56 | @Rule 57 | public ActivityTestRule rule = 58 | new ActivityTestRule<>(MediaSessionPlaybackActivity.class); 59 | 60 | @Test 61 | public void movie_playingOnPip() throws Throwable { 62 | // The movie should be playing on start 63 | onView(withId(R.id.movie)) 64 | .check(matches(allOf(isDisplayed(), isPlaying()))) 65 | .perform(showControls()); 66 | // Click on the button to enter Picture-in-Picture mode 67 | onView(withId(R.id.minimize)).perform(click()); 68 | InstrumentationRegistry.getInstrumentation().waitForIdleSync(); 69 | // The Activity is paused. We cannot use Espresso to test paused activities. 70 | rule.runOnUiThread(new Runnable() { 71 | @Override 72 | public void run() { 73 | // We are now in Picture-in-Picture mode 74 | assertTrue(rule.getActivity().isInPictureInPictureMode()); 75 | final MovieView view = rule.getActivity().findViewById(R.id.movie); 76 | assertNotNull(view); 77 | // The video should still be playing 78 | assertTrue(view.isPlaying()); 79 | 80 | // The media session state should be playing. 81 | assertMediaStateIs(PlaybackStateCompat.STATE_PLAYING); 82 | } 83 | }); 84 | } 85 | 86 | @Test 87 | public void movie_pauseAndResume() throws Throwable { 88 | // The movie should be playing on start 89 | onView(withId(R.id.movie)) 90 | .check(matches(allOf(isDisplayed(), isPlaying()))) 91 | .perform(showControls()); 92 | // Pause 93 | onView(withId(R.id.toggle)).perform(click()); 94 | onView(withId(R.id.movie)).check(matches((not(isPlaying())))); 95 | // The media session state should be paused. 96 | assertMediaStateIs(PlaybackStateCompat.STATE_PAUSED); 97 | // Resume 98 | onView(withId(R.id.toggle)).perform(click()); 99 | onView(withId(R.id.movie)).check(matches(isPlaying())); 100 | // The media session state should be playing. 101 | assertMediaStateIs(PlaybackStateCompat.STATE_PLAYING); 102 | } 103 | 104 | @Test 105 | public void fullscreen_enabledOnLandscape() throws Throwable { 106 | rule.runOnUiThread(new Runnable() { 107 | @Override 108 | public void run() { 109 | rule.getActivity() 110 | .setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); 111 | } 112 | }); 113 | InstrumentationRegistry.getInstrumentation().waitForIdleSync(); 114 | rule.runOnUiThread(new Runnable() { 115 | @Override 116 | public void run() { 117 | final View decorView = rule.getActivity().getWindow().getDecorView(); 118 | assertThat(decorView.getSystemUiVisibility(), 119 | hasFlag(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)); 120 | } 121 | }); 122 | } 123 | 124 | @Test 125 | public void fullscreen_disabledOnPortrait() throws Throwable { 126 | rule.runOnUiThread(new Runnable() { 127 | @Override 128 | public void run() { 129 | rule.getActivity() 130 | .setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); 131 | } 132 | }); 133 | InstrumentationRegistry.getInstrumentation().waitForIdleSync(); 134 | rule.runOnUiThread(new Runnable() { 135 | @Override 136 | public void run() { 137 | final View decorView = rule.getActivity().getWindow().getDecorView(); 138 | assertThat(decorView.getSystemUiVisibility(), 139 | not(hasFlag(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN))); 140 | } 141 | }); 142 | } 143 | 144 | private void assertMediaStateIs(@PlaybackStateCompat.State int expectedState) { 145 | PlaybackState state = rule.getActivity().getMediaController().getPlaybackState(); 146 | assertNotNull(state); 147 | assertThat( 148 | "MediaSession is not in the correct state", 149 | state.getState(), 150 | is(equalTo(expectedState))); 151 | } 152 | 153 | private static Matcher isPlaying() { 154 | return new TypeSafeMatcher() { 155 | @Override 156 | protected boolean matchesSafely(View view) { 157 | return ((MovieView) view).isPlaying(); 158 | } 159 | 160 | @Override 161 | public void describeTo(Description description) { 162 | description.appendText("MovieView is playing"); 163 | } 164 | }; 165 | } 166 | 167 | private static ViewAction showControls() { 168 | return new ViewAction() { 169 | @Override 170 | public Matcher getConstraints() { 171 | return isAssignableFrom(MovieView.class); 172 | } 173 | 174 | @Override 175 | public String getDescription() { 176 | return "Show controls of MovieView"; 177 | } 178 | 179 | @Override 180 | public void perform(UiController uiController, View view) { 181 | uiController.loopMainThreadUntilIdle(); 182 | ((MovieView) view).showControls(); 183 | uiController.loopMainThreadUntilIdle(); 184 | } 185 | }; 186 | } 187 | 188 | private static Matcher hasFlag(final int flag) { 189 | return new TypeSafeMatcher() { 190 | @Override 191 | protected boolean matchesSafely(Integer i) { 192 | return (i & flag) == flag; 193 | } 194 | 195 | @Override 196 | public void describeTo(Description description) { 197 | description.appendText("Flag integer contains " + flag); 198 | } 199 | }; 200 | } 201 | 202 | } 203 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 20 | 21 | 27 | 28 | 32 | 33 | 34 | 35 | 36 | 37 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/pictureinpicture/MainActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 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.pictureinpicture; 18 | 19 | import android.app.PendingIntent; 20 | import android.app.PictureInPictureParams; 21 | import android.app.RemoteAction; 22 | import android.content.BroadcastReceiver; 23 | import android.content.Context; 24 | import android.content.Intent; 25 | import android.content.IntentFilter; 26 | import android.content.res.Configuration; 27 | import android.graphics.drawable.Icon; 28 | import android.net.Uri; 29 | import android.os.Bundle; 30 | import android.support.annotation.DrawableRes; 31 | import android.support.v7.app.AppCompatActivity; 32 | import android.util.Rational; 33 | import android.view.View; 34 | import android.widget.Button; 35 | import android.widget.ScrollView; 36 | 37 | import com.example.android.pictureinpicture.widget.MovieView; 38 | 39 | import java.util.ArrayList; 40 | 41 | /** Demonstrates usage of Picture-in-Picture mode on phones and tablets. */ 42 | public class MainActivity extends AppCompatActivity { 43 | 44 | /** Intent action for media controls from Picture-in-Picture mode. */ 45 | private static final String ACTION_MEDIA_CONTROL = "media_control"; 46 | 47 | /** Intent extra for media controls from Picture-in-Picture mode. */ 48 | private static final String EXTRA_CONTROL_TYPE = "control_type"; 49 | 50 | /** The request code for play action PendingIntent. */ 51 | private static final int REQUEST_PLAY = 1; 52 | 53 | /** The request code for pause action PendingIntent. */ 54 | private static final int REQUEST_PAUSE = 2; 55 | 56 | /** The request code for info action PendingIntent. */ 57 | private static final int REQUEST_INFO = 3; 58 | 59 | /** The intent extra value for play action. */ 60 | private static final int CONTROL_TYPE_PLAY = 1; 61 | 62 | /** The intent extra value for pause action. */ 63 | private static final int CONTROL_TYPE_PAUSE = 2; 64 | 65 | /** The arguments to be used for Picture-in-Picture mode. */ 66 | private final PictureInPictureParams.Builder mPictureInPictureParamsBuilder = 67 | new PictureInPictureParams.Builder(); 68 | 69 | /** This shows the video. */ 70 | private MovieView mMovieView; 71 | 72 | /** The bottom half of the screen; hidden on landscape */ 73 | private ScrollView mScrollView; 74 | 75 | /** A {@link BroadcastReceiver} to receive action item events from Picture-in-Picture mode. */ 76 | private BroadcastReceiver mReceiver; 77 | 78 | private String mPlay; 79 | private String mPause; 80 | 81 | private final View.OnClickListener mOnClickListener = 82 | new View.OnClickListener() { 83 | @Override 84 | public void onClick(View view) { 85 | switch (view.getId()) { 86 | case R.id.pip: 87 | minimize(); 88 | break; 89 | } 90 | } 91 | }; 92 | 93 | /** Callbacks from the {@link MovieView} showing the video playback. */ 94 | private MovieView.MovieListener mMovieListener = 95 | new MovieView.MovieListener() { 96 | 97 | @Override 98 | public void onMovieStarted() { 99 | // We are playing the video now. In PiP mode, we want to show an action item to 100 | // pause 101 | // the video. 102 | updatePictureInPictureActions( 103 | R.drawable.ic_pause_24dp, mPause, CONTROL_TYPE_PAUSE, REQUEST_PAUSE); 104 | } 105 | 106 | @Override 107 | public void onMovieStopped() { 108 | // The video stopped or reached its end. In PiP mode, we want to show an action 109 | // item to play the video. 110 | updatePictureInPictureActions( 111 | R.drawable.ic_play_arrow_24dp, mPlay, CONTROL_TYPE_PLAY, REQUEST_PLAY); 112 | } 113 | 114 | @Override 115 | public void onMovieMinimized() { 116 | // The MovieView wants us to minimize it. We enter Picture-in-Picture mode now. 117 | minimize(); 118 | } 119 | }; 120 | 121 | /** 122 | * Update the state of pause/resume action item in Picture-in-Picture mode. 123 | * 124 | * @param iconId The icon to be used. 125 | * @param title The title text. 126 | * @param controlType The type of the action. either {@link #CONTROL_TYPE_PLAY} or {@link 127 | * #CONTROL_TYPE_PAUSE}. 128 | * @param requestCode The request code for the {@link PendingIntent}. 129 | */ 130 | void updatePictureInPictureActions( 131 | @DrawableRes int iconId, String title, int controlType, int requestCode) { 132 | final ArrayList actions = new ArrayList<>(); 133 | 134 | // This is the PendingIntent that is invoked when a user clicks on the action item. 135 | // You need to use distinct request codes for play and pause, or the PendingIntent won't 136 | // be properly updated. 137 | final PendingIntent intent = 138 | PendingIntent.getBroadcast( 139 | MainActivity.this, 140 | requestCode, 141 | new Intent(ACTION_MEDIA_CONTROL).putExtra(EXTRA_CONTROL_TYPE, controlType), 142 | 0); 143 | final Icon icon = Icon.createWithResource(MainActivity.this, iconId); 144 | actions.add(new RemoteAction(icon, title, title, intent)); 145 | 146 | // Another action item. This is a fixed action. 147 | actions.add( 148 | new RemoteAction( 149 | Icon.createWithResource(MainActivity.this, R.drawable.ic_info_24dp), 150 | getString(R.string.info), 151 | getString(R.string.info_description), 152 | PendingIntent.getActivity( 153 | MainActivity.this, 154 | REQUEST_INFO, 155 | new Intent( 156 | Intent.ACTION_VIEW, 157 | Uri.parse(getString(R.string.info_uri))), 158 | 0))); 159 | 160 | mPictureInPictureParamsBuilder.setActions(actions); 161 | 162 | // This is how you can update action items (or aspect ratio) for Picture-in-Picture mode. 163 | // Note this call can happen even when the app is not in PiP mode. In that case, the 164 | // arguments will be used for at the next call of #enterPictureInPictureMode. 165 | setPictureInPictureParams(mPictureInPictureParamsBuilder.build()); 166 | } 167 | 168 | @Override 169 | protected void onCreate(Bundle savedInstanceState) { 170 | super.onCreate(savedInstanceState); 171 | setContentView(R.layout.activity_main); 172 | 173 | // Prepare string resources for Picture-in-Picture actions. 174 | mPlay = getString(R.string.play); 175 | mPause = getString(R.string.pause); 176 | 177 | // View references 178 | mMovieView = findViewById(R.id.movie); 179 | mScrollView = findViewById(R.id.scroll); 180 | 181 | Button switchExampleButton = findViewById(R.id.switch_example); 182 | switchExampleButton.setText(getString(R.string.switch_media_session)); 183 | switchExampleButton.setOnClickListener(new SwitchActivityOnClick()); 184 | 185 | // Set up the video; it automatically starts. 186 | mMovieView.setMovieListener(mMovieListener); 187 | findViewById(R.id.pip).setOnClickListener(mOnClickListener); 188 | } 189 | 190 | @Override 191 | protected void onStop() { 192 | // On entering Picture-in-Picture mode, onPause is called, but not onStop. 193 | // For this reason, this is the place where we should pause the video playback. 194 | mMovieView.pause(); 195 | super.onStop(); 196 | } 197 | 198 | @Override 199 | protected void onRestart() { 200 | super.onRestart(); 201 | if (!isInPictureInPictureMode()) { 202 | // Show the video controls so the video can be easily resumed. 203 | mMovieView.showControls(); 204 | } 205 | } 206 | 207 | @Override 208 | public void onConfigurationChanged(Configuration newConfig) { 209 | super.onConfigurationChanged(newConfig); 210 | adjustFullScreen(newConfig); 211 | } 212 | 213 | @Override 214 | public void onWindowFocusChanged(boolean hasFocus) { 215 | super.onWindowFocusChanged(hasFocus); 216 | if (hasFocus) { 217 | adjustFullScreen(getResources().getConfiguration()); 218 | } 219 | } 220 | 221 | @Override 222 | public void onPictureInPictureModeChanged( 223 | boolean isInPictureInPictureMode, Configuration configuration) { 224 | super.onPictureInPictureModeChanged(isInPictureInPictureMode, configuration); 225 | if (isInPictureInPictureMode) { 226 | // Starts receiving events from action items in PiP mode. 227 | mReceiver = 228 | new BroadcastReceiver() { 229 | @Override 230 | public void onReceive(Context context, Intent intent) { 231 | if (intent == null 232 | || !ACTION_MEDIA_CONTROL.equals(intent.getAction())) { 233 | return; 234 | } 235 | 236 | // This is where we are called back from Picture-in-Picture action 237 | // items. 238 | final int controlType = intent.getIntExtra(EXTRA_CONTROL_TYPE, 0); 239 | switch (controlType) { 240 | case CONTROL_TYPE_PLAY: 241 | mMovieView.play(); 242 | break; 243 | case CONTROL_TYPE_PAUSE: 244 | mMovieView.pause(); 245 | break; 246 | } 247 | } 248 | }; 249 | registerReceiver(mReceiver, new IntentFilter(ACTION_MEDIA_CONTROL)); 250 | } else { 251 | // We are out of PiP mode. We can stop receiving events from it. 252 | unregisterReceiver(mReceiver); 253 | mReceiver = null; 254 | // Show the video controls if the video is not playing 255 | if (mMovieView != null && !mMovieView.isPlaying()) { 256 | mMovieView.showControls(); 257 | } 258 | } 259 | } 260 | 261 | /** Enters Picture-in-Picture mode. */ 262 | void minimize() { 263 | if (mMovieView == null) { 264 | return; 265 | } 266 | // Hide the controls in picture-in-picture mode. 267 | mMovieView.hideControls(); 268 | // Calculate the aspect ratio of the PiP screen. 269 | Rational aspectRatio = new Rational(mMovieView.getWidth(), mMovieView.getHeight()); 270 | mPictureInPictureParamsBuilder.setAspectRatio(aspectRatio).build(); 271 | enterPictureInPictureMode(mPictureInPictureParamsBuilder.build()); 272 | } 273 | 274 | /** 275 | * Adjusts immersive full-screen flags depending on the screen orientation. 276 | * 277 | * @param config The current {@link Configuration}. 278 | */ 279 | private void adjustFullScreen(Configuration config) { 280 | final View decorView = getWindow().getDecorView(); 281 | if (config.orientation == Configuration.ORIENTATION_LANDSCAPE) { 282 | decorView.setSystemUiVisibility( 283 | View.SYSTEM_UI_FLAG_LAYOUT_STABLE 284 | | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION 285 | | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN 286 | | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION 287 | | View.SYSTEM_UI_FLAG_FULLSCREEN 288 | | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); 289 | mScrollView.setVisibility(View.GONE); 290 | mMovieView.setAdjustViewBounds(false); 291 | } else { 292 | decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE); 293 | mScrollView.setVisibility(View.VISIBLE); 294 | mMovieView.setAdjustViewBounds(true); 295 | } 296 | } 297 | 298 | /** Launches {@link MediaSessionPlaybackActivity} and closes this activity. */ 299 | private class SwitchActivityOnClick implements View.OnClickListener { 300 | @Override 301 | public void onClick(View view) { 302 | startActivity(new Intent(view.getContext(), MediaSessionPlaybackActivity.class)); 303 | finish(); 304 | } 305 | } 306 | } 307 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/pictureinpicture/MediaSessionPlaybackActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 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.pictureinpicture; 18 | 19 | import android.app.PictureInPictureParams; 20 | import android.content.Intent; 21 | import android.content.res.Configuration; 22 | import android.os.Bundle; 23 | import android.support.v4.media.MediaMetadataCompat; 24 | import android.support.v4.media.session.MediaControllerCompat; 25 | import android.support.v4.media.session.MediaSessionCompat; 26 | import android.support.v4.media.session.PlaybackStateCompat; 27 | import android.support.v7.app.AppCompatActivity; 28 | import android.util.Rational; 29 | import android.view.View; 30 | import android.widget.Button; 31 | import android.widget.ScrollView; 32 | 33 | import com.example.android.pictureinpicture.widget.MovieView; 34 | 35 | /** 36 | * Demonstrates usage of Picture-in-Picture when using {@link 37 | * android.support.v4.media.session.MediaSessionCompat}. 38 | */ 39 | public class MediaSessionPlaybackActivity extends AppCompatActivity { 40 | 41 | private static final String TAG = "MediaSessionPlaybackActivity"; 42 | 43 | public static final long MEDIA_ACTIONS_PLAY_PAUSE = 44 | PlaybackStateCompat.ACTION_PLAY 45 | | PlaybackStateCompat.ACTION_PAUSE 46 | | PlaybackStateCompat.ACTION_PLAY_PAUSE; 47 | 48 | public static final long MEDIA_ACTIONS_ALL = 49 | MEDIA_ACTIONS_PLAY_PAUSE 50 | | PlaybackStateCompat.ACTION_SKIP_TO_NEXT 51 | | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS; 52 | 53 | private MediaSessionCompat mSession; 54 | 55 | /** The arguments to be used for Picture-in-Picture mode. */ 56 | private final PictureInPictureParams.Builder mPictureInPictureParamsBuilder = 57 | new PictureInPictureParams.Builder(); 58 | 59 | /** This shows the video. */ 60 | private MovieView mMovieView; 61 | 62 | /** The bottom half of the screen; hidden on landscape */ 63 | private ScrollView mScrollView; 64 | 65 | private final View.OnClickListener mOnClickListener = 66 | new View.OnClickListener() { 67 | @Override 68 | public void onClick(View view) { 69 | switch (view.getId()) { 70 | case R.id.pip: 71 | minimize(); 72 | break; 73 | } 74 | } 75 | }; 76 | 77 | /** Callbacks from the {@link MovieView} showing the video playback. */ 78 | private MovieView.MovieListener mMovieListener = 79 | new MovieView.MovieListener() { 80 | 81 | @Override 82 | public void onMovieStarted() { 83 | // We are playing the video now. Update the media session state and the PiP 84 | // window will 85 | // update the actions. 86 | updatePlaybackState( 87 | PlaybackStateCompat.STATE_PLAYING, 88 | mMovieView.getCurrentPosition(), 89 | mMovieView.getVideoResourceId()); 90 | } 91 | 92 | @Override 93 | public void onMovieStopped() { 94 | // The video stopped or reached its end. Update the media session state and the 95 | // PiP window will 96 | // update the actions. 97 | updatePlaybackState( 98 | PlaybackStateCompat.STATE_PAUSED, 99 | mMovieView.getCurrentPosition(), 100 | mMovieView.getVideoResourceId()); 101 | } 102 | 103 | @Override 104 | public void onMovieMinimized() { 105 | // The MovieView wants us to minimize it. We enter Picture-in-Picture mode now. 106 | minimize(); 107 | } 108 | }; 109 | 110 | @Override 111 | protected void onCreate(Bundle savedInstanceState) { 112 | super.onCreate(savedInstanceState); 113 | setContentView(R.layout.activity_main); 114 | 115 | // View references 116 | mMovieView = findViewById(R.id.movie); 117 | mScrollView = findViewById(R.id.scroll); 118 | Button switchExampleButton = findViewById(R.id.switch_example); 119 | switchExampleButton.setText(getString(R.string.switch_custom)); 120 | switchExampleButton.setOnClickListener(new SwitchActivityOnClick()); 121 | 122 | // Set up the video; it automatically starts. 123 | mMovieView.setMovieListener(mMovieListener); 124 | findViewById(R.id.pip).setOnClickListener(mOnClickListener); 125 | } 126 | 127 | @Override 128 | protected void onStart() { 129 | super.onStart(); 130 | initializeMediaSession(); 131 | } 132 | 133 | private void initializeMediaSession() { 134 | mSession = new MediaSessionCompat(this, TAG); 135 | mSession.setFlags( 136 | MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS 137 | | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS); 138 | mSession.setActive(true); 139 | MediaControllerCompat.setMediaController(this, mSession.getController()); 140 | 141 | MediaMetadataCompat metadata = new MediaMetadataCompat.Builder() 142 | .putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, mMovieView.getTitle()) 143 | .build(); 144 | mSession.setMetadata(metadata); 145 | 146 | MediaSessionCallback mMediaSessionCallback = new MediaSessionCallback(mMovieView); 147 | mSession.setCallback(mMediaSessionCallback); 148 | 149 | int state = 150 | mMovieView.isPlaying() 151 | ? PlaybackStateCompat.STATE_PLAYING 152 | : PlaybackStateCompat.STATE_PAUSED; 153 | updatePlaybackState( 154 | state, 155 | MEDIA_ACTIONS_ALL, 156 | mMovieView.getCurrentPosition(), 157 | mMovieView.getVideoResourceId()); 158 | } 159 | 160 | @Override 161 | protected void onStop() { 162 | super.onStop(); 163 | // On entering Picture-in-Picture mode, onPause is called, but not onStop. 164 | // For this reason, this is the place where we should pause the video playback. 165 | mMovieView.pause(); 166 | mSession.release(); 167 | mSession = null; 168 | } 169 | 170 | @Override 171 | protected void onRestart() { 172 | super.onRestart(); 173 | if (!isInPictureInPictureMode()) { 174 | // Show the video controls so the video can be easily resumed. 175 | mMovieView.showControls(); 176 | } 177 | } 178 | 179 | @Override 180 | public void onConfigurationChanged(Configuration newConfig) { 181 | super.onConfigurationChanged(newConfig); 182 | adjustFullScreen(newConfig); 183 | } 184 | 185 | @Override 186 | public void onWindowFocusChanged(boolean hasFocus) { 187 | super.onWindowFocusChanged(hasFocus); 188 | if (hasFocus) { 189 | adjustFullScreen(getResources().getConfiguration()); 190 | } 191 | } 192 | 193 | @Override 194 | public void onPictureInPictureModeChanged( 195 | boolean isInPictureInPictureMode, Configuration configuration) { 196 | super.onPictureInPictureModeChanged(isInPictureInPictureMode, configuration); 197 | if (!isInPictureInPictureMode) { 198 | // Show the video controls if the video is not playing 199 | if (mMovieView != null && !mMovieView.isPlaying()) { 200 | mMovieView.showControls(); 201 | } 202 | } 203 | } 204 | 205 | /** Enters Picture-in-Picture mode. */ 206 | void minimize() { 207 | if (mMovieView == null) { 208 | return; 209 | } 210 | // Hide the controls in picture-in-picture mode. 211 | mMovieView.hideControls(); 212 | // Calculate the aspect ratio of the PiP screen. 213 | Rational aspectRatio = new Rational(mMovieView.getWidth(), mMovieView.getHeight()); 214 | mPictureInPictureParamsBuilder.setAspectRatio(aspectRatio).build(); 215 | enterPictureInPictureMode(mPictureInPictureParamsBuilder.build()); 216 | } 217 | 218 | /** 219 | * Adjusts immersive full-screen flags depending on the screen orientation. 220 | * 221 | * @param config The current {@link Configuration}. 222 | */ 223 | private void adjustFullScreen(Configuration config) { 224 | final View decorView = getWindow().getDecorView(); 225 | if (config.orientation == Configuration.ORIENTATION_LANDSCAPE) { 226 | decorView.setSystemUiVisibility( 227 | View.SYSTEM_UI_FLAG_LAYOUT_STABLE 228 | | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION 229 | | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN 230 | | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION 231 | | View.SYSTEM_UI_FLAG_FULLSCREEN 232 | | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); 233 | mScrollView.setVisibility(View.GONE); 234 | mMovieView.setAdjustViewBounds(false); 235 | } else { 236 | decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE); 237 | mScrollView.setVisibility(View.VISIBLE); 238 | mMovieView.setAdjustViewBounds(true); 239 | } 240 | } 241 | 242 | /** 243 | * Overloaded method that persists previously set media actions. 244 | * 245 | * @param state The state of the video, e.g. playing, paused, etc. 246 | * @param position The position of playback in the video. 247 | * @param mediaId The media id related to the video in the media session. 248 | */ 249 | private void updatePlaybackState( 250 | @PlaybackStateCompat.State int state, int position, int mediaId) { 251 | long actions = mSession.getController().getPlaybackState().getActions(); 252 | updatePlaybackState(state, actions, position, mediaId); 253 | } 254 | 255 | private void updatePlaybackState( 256 | @PlaybackStateCompat.State int state, long playbackActions, int position, int mediaId) { 257 | PlaybackStateCompat.Builder builder = 258 | new PlaybackStateCompat.Builder() 259 | .setActions(playbackActions) 260 | .setActiveQueueItemId(mediaId) 261 | .setState(state, position, 1.0f); 262 | mSession.setPlaybackState(builder.build()); 263 | } 264 | 265 | /** 266 | * Updates the {@link MovieView} based on the callback actions.
267 | * Simulates a playlist that will disable actions when you cannot skip through the playlist in a 268 | * certain direction. 269 | */ 270 | private class MediaSessionCallback extends MediaSessionCompat.Callback { 271 | 272 | private static final int PLAYLIST_SIZE = 2; 273 | 274 | private MovieView movieView; 275 | private int indexInPlaylist; 276 | 277 | public MediaSessionCallback(MovieView movieView) { 278 | this.movieView = movieView; 279 | indexInPlaylist = 1; 280 | } 281 | 282 | @Override 283 | public void onPlay() { 284 | super.onPlay(); 285 | movieView.play(); 286 | } 287 | 288 | @Override 289 | public void onPause() { 290 | super.onPause(); 291 | movieView.pause(); 292 | } 293 | 294 | @Override 295 | public void onSkipToNext() { 296 | super.onSkipToNext(); 297 | movieView.startVideo(); 298 | if (indexInPlaylist < PLAYLIST_SIZE) { 299 | indexInPlaylist++; 300 | if (indexInPlaylist >= PLAYLIST_SIZE) { 301 | updatePlaybackState( 302 | PlaybackStateCompat.STATE_PLAYING, 303 | MEDIA_ACTIONS_PLAY_PAUSE | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS, 304 | movieView.getCurrentPosition(), 305 | movieView.getVideoResourceId()); 306 | } else { 307 | updatePlaybackState( 308 | PlaybackStateCompat.STATE_PLAYING, 309 | MEDIA_ACTIONS_ALL, 310 | movieView.getCurrentPosition(), 311 | movieView.getVideoResourceId()); 312 | } 313 | } 314 | } 315 | 316 | @Override 317 | public void onSkipToPrevious() { 318 | super.onSkipToPrevious(); 319 | movieView.startVideo(); 320 | if (indexInPlaylist > 0) { 321 | indexInPlaylist--; 322 | if (indexInPlaylist <= 0) { 323 | updatePlaybackState( 324 | PlaybackStateCompat.STATE_PLAYING, 325 | MEDIA_ACTIONS_PLAY_PAUSE | PlaybackStateCompat.ACTION_SKIP_TO_NEXT, 326 | movieView.getCurrentPosition(), 327 | movieView.getVideoResourceId()); 328 | } else { 329 | updatePlaybackState( 330 | PlaybackStateCompat.STATE_PLAYING, 331 | MEDIA_ACTIONS_ALL, 332 | movieView.getCurrentPosition(), 333 | movieView.getVideoResourceId()); 334 | } 335 | } 336 | } 337 | } 338 | 339 | private class SwitchActivityOnClick implements View.OnClickListener { 340 | @Override 341 | public void onClick(View view) { 342 | startActivity(new Intent(view.getContext(), MainActivity.class)); 343 | finish(); 344 | } 345 | } 346 | } 347 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_fast_forward_64dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 14 | 19 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_fast_rewind_64dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 14 | 19 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_info_24dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 14 | 19 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_minimize_24dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 14 | 19 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_pause_24dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 14 | 19 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_pause_64dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 14 | 19 | 20 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_picture_in_picture_alt.xml: -------------------------------------------------------------------------------- 1 | 2 | 14 | 19 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_play_arrow_24dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 14 | 19 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_play_arrow_64dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 14 | 19 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/shade.xml: -------------------------------------------------------------------------------- 1 | 2 | 14 | 16 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 25 | 26 | 33 | 34 | 39 | 40 | 49 | 50 |