1 /*
<lambda>null2  * Copyright (C) 2020 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 android.systemui.cts.tv.pip
18 
19 import android.app.Activity
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.graphics.Rect
27 import android.media.MediaMetadata
28 import android.media.session.MediaSession
29 import android.media.session.PlaybackState
30 import android.media.session.PlaybackState.ACTION_PAUSE
31 import android.media.session.PlaybackState.ACTION_PLAY
32 import android.media.session.PlaybackState.STATE_PAUSED
33 import android.media.session.PlaybackState.STATE_PLAYING
34 import android.media.session.PlaybackState.STATE_STOPPED
35 import android.os.Bundle
36 import android.systemui.tv.cts.PipActivity.ACTION_ENTER_PIP
37 import android.systemui.tv.cts.PipActivity.ACTION_MEDIA_PAUSE
38 import android.systemui.tv.cts.PipActivity.ACTION_MEDIA_PLAY
39 import android.systemui.tv.cts.PipActivity.ACTION_SET_MEDIA_TITLE
40 import android.systemui.tv.cts.PipActivity.ACTION_NO_OP
41 import android.systemui.tv.cts.PipActivity.EXTRA_ASPECT_RATIO_DENOMINATOR
42 import android.systemui.tv.cts.PipActivity.EXTRA_ASPECT_RATIO_NUMERATOR
43 import android.systemui.tv.cts.PipActivity.EXTRA_SET_CUSTOM_ACTIONS
44 import android.systemui.tv.cts.PipActivity.EXTRA_ENTER_PIP
45 import android.systemui.tv.cts.PipActivity.EXTRA_MEDIA_SESSION_ACTIONS
46 import android.systemui.tv.cts.PipActivity.EXTRA_MEDIA_SESSION_ACTIVE
47 import android.systemui.tv.cts.PipActivity.EXTRA_MEDIA_SESSION_TITLE
48 import android.systemui.tv.cts.PipActivity.EXTRA_SOURCE_RECT_HINT
49 import android.systemui.tv.cts.PipActivity.EXTRA_TURN_ON_SCREEN
50 import android.systemui.tv.cts.PipActivity.MEDIA_SESSION_TITLE
51 import android.util.Log
52 import android.util.Rational
53 import java.net.URLDecoder
54 
55 /** A simple PiP test activity */
56 class PipTestActivity : Activity() {
57     companion object {
58         private const val TAG = "PipTestActivity"
59     }
60 
61     private lateinit var pipParams: PictureInPictureParams
62     private lateinit var mediaSession: MediaSession
63     private val playbackBuilder = PlaybackState.Builder()
64         .setActions(ACTION_PAUSE or ACTION_PLAY)
65         .setState(STATE_STOPPED)
66 
67     private val broadcastReceiver: BroadcastReceiver = object : BroadcastReceiver() {
68         override fun onReceive(context: Context?, intent: Intent?) = handle(intent)
69     }
70 
71     private val intentFilter = IntentFilter().apply {
72         addAction(ACTION_SET_MEDIA_TITLE)
73         addAction(ACTION_MEDIA_PLAY)
74         addAction(ACTION_MEDIA_PAUSE)
75         addAction(ACTION_NO_OP)
76     }
77 
78     private val mediaCallback = object : MediaSession.Callback() {
79         override fun onPlay() =
80             mediaSession.setPlaybackState(playbackBuilder.setState(STATE_PLAYING).build())
81 
82         override fun onPause() =
83             mediaSession.setPlaybackState(playbackBuilder.setState(STATE_PAUSED).build())
84     }
85 
86     override fun onCreate(savedInstanceState: Bundle?) {
87         super.onCreate(savedInstanceState)
88 
89         mediaSession = MediaSession(this, MEDIA_SESSION_TITLE).apply {
90             setPlaybackState(playbackBuilder.build())
91             setCallback(mediaCallback)
92         }
93         registerReceiver(broadcastReceiver, intentFilter)
94         handle(intent)
95     }
96 
97     override fun onNewIntent(intent: Intent?) = handle(intent)
98 
99     private fun handle(intent: Intent?) {
100         if (intent == null) {
101             return
102         }
103 
104         handleScreenExtras(intent)
105 
106         handleMediaExtras(intent)
107 
108         handlePipExtras(intent)
109 
110         when (intent.action) {
111             ACTION_NO_OP -> {
112                 // explicitly do nothing
113             }
114             ACTION_MEDIA_PLAY -> {
115                 Log.d(TAG, "Playing media")
116                 mediaSession.controller.transportControls.play()
117             }
118             ACTION_MEDIA_PAUSE -> {
119                 Log.d(TAG, "Pausing media")
120                 mediaSession.controller.transportControls.pause()
121             }
122         }
123 
124         if (intent.action == ACTION_ENTER_PIP || intent.getBooleanExtra(EXTRA_ENTER_PIP, false)) {
125             Log.d(TAG, "Entering PIP. Currently in PIP = $isInPictureInPictureMode")
126             val res = enterPictureInPictureMode(pipParams)
127             Log.d(TAG, "Entered PIP = $res. Currently in PIP = $isInPictureInPictureMode")
128         }
129     }
130 
131     /**
132      * Applies the pip parameters from the intent to the current pip window if there is one, or
133      * sets them for when pip mode will be entered next.
134      *
135      * Also stores the new parameters in [pipParams].
136      */
137     private fun handlePipExtras(intent: Intent) {
138         pipParams = buildPipParams(intent.extras)
139         setPictureInPictureParams(pipParams)
140     }
141 
142     /**  Updates the state of the [mediaSession]. */
143     private fun handleMediaExtras(intent: Intent) {
144         if (intent.hasExtra(EXTRA_MEDIA_SESSION_ACTIVE)) {
145             intent.extras?.getBoolean(EXTRA_MEDIA_SESSION_ACTIVE)?.let {
146                 Log.d(TAG, "Setting media session active = $it")
147                 mediaSession.isActive = it
148             }
149         }
150 
151         intent.getStringExtra(EXTRA_MEDIA_SESSION_TITLE)?.let {
152             // We expect the media session title to be url encoded.
153             // This is needed to be able to set arbitrary titles over adb
154             val title: String = URLDecoder.decode(it, "UTF-8")
155             Log.d(TAG, "Setting media session title = $title")
156             mediaSession.setMetadata(
157                 MediaMetadata.Builder()
158                     .putText(MediaMetadata.METADATA_KEY_TITLE, title)
159                     .build()
160             )
161         }
162 
163         if (intent.hasExtra(EXTRA_MEDIA_SESSION_ACTIONS)) {
164             val requestedActions =
165                 intent.getLongExtra(EXTRA_MEDIA_SESSION_ACTIONS, ACTION_PAUSE or ACTION_PLAY)
166             mediaSession.setPlaybackState(playbackBuilder.setActions(requestedActions).build())
167         }
168     }
169 
170     /** Calls [android.app.Activity.setTurnScreenOn] if needed. */
171     private fun handleScreenExtras(intent: Intent) {
172         if (intent.getBooleanExtra(EXTRA_TURN_ON_SCREEN, false)) {
173             Log.d(TAG, "Setting setTurnScreenOn")
174             setTurnScreenOn(true)
175         }
176     }
177 
178     private fun buildPipParams(bundle: Bundle?): PictureInPictureParams {
179         val builder = PictureInPictureParams.Builder()
180         bundle?.run {
181             if (containsKey(EXTRA_ASPECT_RATIO_NUMERATOR) &&
182                 containsKey(EXTRA_ASPECT_RATIO_DENOMINATOR)) {
183                 builder.setAspectRatio(Rational(
184                     getInt(EXTRA_ASPECT_RATIO_NUMERATOR),
185                     getInt(EXTRA_ASPECT_RATIO_DENOMINATOR)))
186             }
187 
188             getString(EXTRA_SOURCE_RECT_HINT)?.let {
189                 builder.setSourceRectHint(Rect.unflattenFromString(it))
190             }
191 
192             getParcelableArrayList<RemoteAction>(EXTRA_SET_CUSTOM_ACTIONS)?.let { actions ->
193                 val names = actions.joinToString(", ") { it.title }
194                 Log.d(TAG, "Setting custom pip actions: $names")
195                 builder.setActions(actions)
196             }
197         }
198         return builder.build()
199     }
200 
201     /** Just set the playback state without updating the position or playback speed. */
202     private fun PlaybackState.Builder.setState(state: Int) = apply {
203         setState(state, 0, 0f)
204     }
205 
206     override fun onDestroy() {
207         unregisterReceiver(broadcastReceiver)
208         super.onDestroy()
209     }
210 }