1 /*
2  * 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 com.android.wm.shell.flicker.testapp;
18 
19 import static android.media.MediaMetadata.METADATA_KEY_TITLE;
20 import static android.media.session.PlaybackState.ACTION_PAUSE;
21 import static android.media.session.PlaybackState.ACTION_PLAY;
22 import static android.media.session.PlaybackState.ACTION_STOP;
23 import static android.media.session.PlaybackState.STATE_PAUSED;
24 import static android.media.session.PlaybackState.STATE_PLAYING;
25 import static android.media.session.PlaybackState.STATE_STOPPED;
26 
27 import static com.android.wm.shell.flicker.testapp.Components.PipActivity.ACTION_ENTER_PIP;
28 import static com.android.wm.shell.flicker.testapp.Components.PipActivity.ACTION_SET_REQUESTED_ORIENTATION;
29 import static com.android.wm.shell.flicker.testapp.Components.PipActivity.EXTRA_ENTER_PIP;
30 import static com.android.wm.shell.flicker.testapp.Components.PipActivity.EXTRA_PIP_ORIENTATION;
31 
32 import android.app.Activity;
33 import android.app.PendingIntent;
34 import android.app.PictureInPictureParams;
35 import android.app.RemoteAction;
36 import android.content.BroadcastReceiver;
37 import android.content.Context;
38 import android.content.Intent;
39 import android.content.IntentFilter;
40 import android.graphics.drawable.Icon;
41 import android.media.MediaMetadata;
42 import android.media.session.MediaSession;
43 import android.media.session.PlaybackState;
44 import android.os.Bundle;
45 import android.util.Log;
46 import android.util.Rational;
47 import android.view.View;
48 import android.view.Window;
49 import android.view.WindowManager;
50 import android.widget.CheckBox;
51 
52 import java.util.ArrayList;
53 import java.util.Arrays;
54 import java.util.Collections;
55 import java.util.List;
56 
57 public class PipActivity extends Activity {
58     private static final String TAG = PipActivity.class.getSimpleName();
59     /**
60      * A media session title for when the session is in {@link STATE_PLAYING}.
61      * TvPipNotificationTests check whether the actual notification title matches this string.
62      */
63     private static final String TITLE_STATE_PLAYING = "TestApp media is playing";
64     /**
65      * A media session title for when the session is in {@link STATE_PAUSED}.
66      * TvPipNotificationTests check whether the actual notification title matches this string.
67      */
68     private static final String TITLE_STATE_PAUSED = "TestApp media is paused";
69 
70     private static final Rational RATIO_DEFAULT = null;
71     private static final Rational RATIO_SQUARE = new Rational(1, 1);
72     private static final Rational RATIO_WIDE = new Rational(2, 1);
73     private static final Rational RATIO_TALL = new Rational(1, 2);
74 
75     private static final String PIP_ACTION_NO_OP = "No-Op";
76     private static final String PIP_ACTION_OFF = "Off";
77     private static final String PIP_ACTION_ON = "On";
78     private static final String PIP_ACTION_CLEAR = "Clear";
79     private static final String ACTION_NO_OP = "com.android.wm.shell.flicker.testapp.NO_OP";
80     private static final String ACTION_SWITCH_OFF =
81             "com.android.wm.shell.flicker.testapp.SWITCH_OFF";
82     private static final String ACTION_SWITCH_ON = "com.android.wm.shell.flicker.testapp.SWITCH_ON";
83     private static final String ACTION_CLEAR = "com.android.wm.shell.flicker.testapp.CLEAR";
84 
85     private final PictureInPictureParams.Builder mPipParamsBuilder =
86             new PictureInPictureParams.Builder()
87                     .setAspectRatio(RATIO_DEFAULT);
88     private MediaSession mMediaSession;
89     private final PlaybackState.Builder mPlaybackStateBuilder = new PlaybackState.Builder()
90             .setActions(ACTION_PLAY | ACTION_PAUSE | ACTION_STOP)
91             .setState(STATE_STOPPED, 0, 1f);
92     private PlaybackState mPlaybackState = mPlaybackStateBuilder.build();
93     private final MediaMetadata.Builder mMediaMetadataBuilder = new MediaMetadata.Builder();
94 
95     private final List<RemoteAction> mSwitchOffActions = new ArrayList<>();
96     private final List<RemoteAction> mSwitchOnActions = new ArrayList<>();
97     private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
98         @Override
99         public void onReceive(Context context, Intent intent) {
100             if (isInPictureInPictureMode()) {
101                 switch (intent.getAction()) {
102                     case ACTION_SWITCH_ON:
103                         mPipParamsBuilder.setActions(mSwitchOnActions);
104                         break;
105                     case ACTION_SWITCH_OFF:
106                         mPipParamsBuilder.setActions(mSwitchOffActions);
107                         break;
108                     case ACTION_CLEAR:
109                         mPipParamsBuilder.setActions(Collections.emptyList());
110                         break;
111                     case ACTION_NO_OP:
112                         return;
113                     default:
114                         Log.w(TAG, "Unhandled action=" + intent.getAction());
115                         return;
116                 }
117                 setPictureInPictureParams(mPipParamsBuilder.build());
118             } else {
119                 switch (intent.getAction()) {
120                     case ACTION_ENTER_PIP:
121                         enterPip(null);
122                         break;
123                     case ACTION_SET_REQUESTED_ORIENTATION:
124                         setRequestedOrientation(Integer.parseInt(intent.getStringExtra(
125                                 EXTRA_PIP_ORIENTATION)));
126                         break;
127                     default:
128                         Log.w(TAG, "Unhandled action=" + intent.getAction());
129                         return;
130                 }
131             }
132         }
133     };
134 
135     @Override
onCreate(Bundle savedInstanceState)136     public void onCreate(Bundle savedInstanceState) {
137         super.onCreate(savedInstanceState);
138 
139         final Window window = getWindow();
140         final WindowManager.LayoutParams layoutParams = window.getAttributes();
141         layoutParams.layoutInDisplayCutoutMode = WindowManager.LayoutParams
142                 .LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
143         window.setAttributes(layoutParams);
144 
145         setContentView(R.layout.activity_pip);
146 
147         findViewById(R.id.media_session_start)
148                 .setOnClickListener(v -> updateMediaSessionState(STATE_PLAYING));
149         findViewById(R.id.media_session_stop)
150                 .setOnClickListener(v -> updateMediaSessionState(STATE_STOPPED));
151 
152         mMediaSession = new MediaSession(this, "WMShell_TestApp");
153         mMediaSession.setPlaybackState(mPlaybackStateBuilder.build());
154         mMediaSession.setCallback(new MediaSession.Callback() {
155             @Override
156             public void onPlay() {
157                 updateMediaSessionState(STATE_PLAYING);
158             }
159 
160             @Override
161             public void onPause() {
162                 updateMediaSessionState(STATE_PAUSED);
163             }
164 
165             @Override
166             public void onStop() {
167                 updateMediaSessionState(STATE_STOPPED);
168             }
169         });
170 
171         // Build two sets of the custom actions. We'll replace one with the other when 'On'/'Off'
172         // action is invoked.
173         // The first set consists of 3 actions: 1) Off; 2) No-Op; 3) Clear.
174         // The second set consists of 2 actions: 1) On; 2) Clear.
175         // Upon invocation 'Clear' action clear-off all the custom actions, including itself.
176         final Icon icon = Icon.createWithResource(this, android.R.drawable.ic_menu_help);
177         final RemoteAction noOpAction = buildRemoteAction(icon, PIP_ACTION_NO_OP, ACTION_NO_OP);
178         final RemoteAction switchOnAction =
179                 buildRemoteAction(icon, PIP_ACTION_ON, ACTION_SWITCH_ON);
180         final RemoteAction switchOffAction =
181                 buildRemoteAction(icon, PIP_ACTION_OFF, ACTION_SWITCH_OFF);
182         final RemoteAction clearAllAction = buildRemoteAction(icon, PIP_ACTION_CLEAR, ACTION_CLEAR);
183         mSwitchOffActions.addAll(Arrays.asList(switchOnAction, clearAllAction));
184         mSwitchOnActions.addAll(Arrays.asList(noOpAction, switchOffAction, clearAllAction));
185 
186         final IntentFilter filter = new IntentFilter();
187         filter.addAction(ACTION_NO_OP);
188         filter.addAction(ACTION_SWITCH_ON);
189         filter.addAction(ACTION_SWITCH_OFF);
190         filter.addAction(ACTION_CLEAR);
191         filter.addAction(ACTION_SET_REQUESTED_ORIENTATION);
192         filter.addAction(ACTION_ENTER_PIP);
193         registerReceiver(mBroadcastReceiver, filter);
194 
195         handleIntentExtra(getIntent());
196     }
197 
198     @Override
onDestroy()199     protected void onDestroy() {
200         unregisterReceiver(mBroadcastReceiver);
201         super.onDestroy();
202     }
203 
buildRemoteAction(Icon icon, String label, String action)204     private RemoteAction buildRemoteAction(Icon icon, String label, String action) {
205         final Intent intent = new Intent(action);
206         final PendingIntent pendingIntent =
207                 PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
208         return new RemoteAction(icon, label, label, pendingIntent);
209     }
210 
enterPip(View v)211     public void enterPip(View v) {
212         final boolean withCustomActions =
213                 ((CheckBox) findViewById(R.id.with_custom_actions)).isChecked();
214         mPipParamsBuilder.setActions(
215                 withCustomActions ? mSwitchOnActions : Collections.emptyList());
216         enterPictureInPictureMode(mPipParamsBuilder.build());
217     }
218 
onRatioSelected(View v)219     public void onRatioSelected(View v) {
220         switch (v.getId()) {
221             case R.id.ratio_default:
222                 mPipParamsBuilder.setAspectRatio(RATIO_DEFAULT);
223                 break;
224 
225             case R.id.ratio_square:
226                 mPipParamsBuilder.setAspectRatio(RATIO_SQUARE);
227                 break;
228 
229             case R.id.ratio_wide:
230                 mPipParamsBuilder.setAspectRatio(RATIO_WIDE);
231                 break;
232 
233             case R.id.ratio_tall:
234                 mPipParamsBuilder.setAspectRatio(RATIO_TALL);
235                 break;
236         }
237     }
238 
updateMediaSessionState(int newState)239     private void updateMediaSessionState(int newState) {
240         if (mPlaybackState.getState() == newState) {
241             return;
242         }
243         final String title;
244         switch (newState) {
245             case STATE_PLAYING:
246                 title = TITLE_STATE_PLAYING;
247                 break;
248             case STATE_PAUSED:
249                 title = TITLE_STATE_PAUSED;
250                 break;
251             case STATE_STOPPED:
252                 title = "";
253                 break;
254 
255             default:
256                 throw new IllegalArgumentException("Unknown state " + newState);
257         }
258 
259         mPlaybackStateBuilder.setState(newState, 0, 1f);
260         mPlaybackState = mPlaybackStateBuilder.build();
261 
262         mMediaMetadataBuilder.putText(METADATA_KEY_TITLE, title);
263 
264         mMediaSession.setPlaybackState(mPlaybackState);
265         mMediaSession.setMetadata(mMediaMetadataBuilder.build());
266         mMediaSession.setActive(newState != STATE_STOPPED);
267     }
268 
handleIntentExtra(Intent intent)269     private void handleIntentExtra(Intent intent) {
270         // Set the fixed orientation if requested
271         if (intent.hasExtra(EXTRA_PIP_ORIENTATION)) {
272             final int ori = Integer.parseInt(getIntent().getStringExtra(EXTRA_PIP_ORIENTATION));
273             setRequestedOrientation(ori);
274         }
275         // Enter picture in picture with the given aspect ratio if provided
276         if (intent.hasExtra(EXTRA_ENTER_PIP)) {
277             mPipParamsBuilder.setActions(mSwitchOnActions);
278             enterPip(null);
279         }
280     }
281 }
282