1 /*
2  * Copyright (C) 2022 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.server.wm.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.server.wm.flicker.testapp.ActivityOptions.Pip.ACTION_ENTER_PIP;
28 import static com.android.server.wm.flicker.testapp.ActivityOptions.Pip.ACTION_SET_REQUESTED_ORIENTATION;
29 import static com.android.server.wm.flicker.testapp.ActivityOptions.Pip.EXTRA_ENTER_PIP;
30 import static com.android.server.wm.flicker.testapp.ActivityOptions.Pip.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 import android.widget.RadioButton;
52 
53 import java.util.ArrayList;
54 import java.util.Arrays;
55 import java.util.Collections;
56 import java.util.List;
57 
58 public class PipActivity extends Activity {
59     private static final String TAG = PipActivity.class.getSimpleName();
60     /**
61      * A media session title for when the session is in {@link STATE_PLAYING}.
62      * TvPipNotificationTests check whether the actual notification title matches this string.
63      */
64     private static final String TITLE_STATE_PLAYING = "TestApp media is playing";
65     /**
66      * A media session title for when the session is in {@link STATE_PAUSED}.
67      * TvPipNotificationTests check whether the actual notification title matches this string.
68      */
69     private static final String TITLE_STATE_PAUSED = "TestApp media is paused";
70 
71     private static final Rational RATIO_DEFAULT = null;
72     private static final Rational RATIO_SQUARE = new Rational(1, 1);
73     private static final Rational RATIO_WIDE = new Rational(2, 1);
74     private static final Rational RATIO_TALL = new Rational(1, 2);
75 
76     private static final String PIP_ACTION_NO_OP = "No-Op";
77     private static final String PIP_ACTION_OFF = "Off";
78     private static final String PIP_ACTION_ON = "On";
79     private static final String PIP_ACTION_CLEAR = "Clear";
80     private static final String ACTION_NO_OP = "com.android.wm.shell.flicker.testapp.NO_OP";
81     private static final String ACTION_SWITCH_OFF =
82             "com.android.wm.shell.flicker.testapp.SWITCH_OFF";
83     private static final String ACTION_SWITCH_ON = "com.android.wm.shell.flicker.testapp.SWITCH_ON";
84     private static final String ACTION_CLEAR = "com.android.wm.shell.flicker.testapp.CLEAR";
85 
86     private final PictureInPictureParams.Builder mPipParamsBuilder =
87             new PictureInPictureParams.Builder()
88                     .setAspectRatio(RATIO_DEFAULT);
89     private MediaSession mMediaSession;
90     private final PlaybackState.Builder mPlaybackStateBuilder = new PlaybackState.Builder()
91             .setActions(ACTION_PLAY | ACTION_PAUSE | ACTION_STOP)
92             .setState(STATE_STOPPED, 0, 1f);
93     private PlaybackState mPlaybackState = mPlaybackStateBuilder.build();
94     private final MediaMetadata.Builder mMediaMetadataBuilder = new MediaMetadata.Builder();
95 
96     private final List<RemoteAction> mSwitchOffActions = new ArrayList<>();
97     private final List<RemoteAction> mSwitchOnActions = new ArrayList<>();
98     private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
99         @Override
100         public void onReceive(Context context, Intent intent) {
101             if (isInPictureInPictureMode()) {
102                 switch (intent.getAction()) {
103                     case ACTION_SWITCH_ON:
104                         mPipParamsBuilder.setActions(mSwitchOnActions);
105                         break;
106                     case ACTION_SWITCH_OFF:
107                         mPipParamsBuilder.setActions(mSwitchOffActions);
108                         break;
109                     case ACTION_CLEAR:
110                         mPipParamsBuilder.setActions(Collections.emptyList());
111                         break;
112                     case ACTION_NO_OP:
113                         return;
114                     default:
115                         Log.w(TAG, "Unhandled action=" + intent.getAction());
116                         return;
117                 }
118                 setPictureInPictureParams(mPipParamsBuilder.build());
119             } else {
120                 switch (intent.getAction()) {
121                     case ACTION_ENTER_PIP:
122                         enterPip(null);
123                         break;
124                     case ACTION_SET_REQUESTED_ORIENTATION:
125                         setRequestedOrientation(Integer.parseInt(intent.getStringExtra(
126                                 EXTRA_PIP_ORIENTATION)));
127                         break;
128                     default:
129                         Log.w(TAG, "Unhandled action=" + intent.getAction());
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 
204     @Override
onUserLeaveHint()205     protected void onUserLeaveHint() {
206         // Only used when auto PiP is disabled. This is to simulate the behavior that an app
207         // supports regular PiP but not auto PiP.
208         final boolean manuallyEnterPip =
209                 ((RadioButton) findViewById(R.id.enter_pip_on_leave_manual)).isChecked();
210         if (manuallyEnterPip) {
211             enterPictureInPictureMode();
212         }
213     }
214 
buildRemoteAction(Icon icon, String label, String action)215     private RemoteAction buildRemoteAction(Icon icon, String label, String action) {
216         final Intent intent = new Intent(action);
217         final PendingIntent pendingIntent =
218                 PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
219         return new RemoteAction(icon, label, label, pendingIntent);
220     }
221 
enterPip(View v)222     public void enterPip(View v) {
223         final boolean withCustomActions =
224                 ((CheckBox) findViewById(R.id.with_custom_actions)).isChecked();
225         mPipParamsBuilder.setActions(
226                 withCustomActions ? mSwitchOnActions : Collections.emptyList());
227         enterPictureInPictureMode(mPipParamsBuilder.build());
228     }
229 
onAutoPipSelected(View v)230     public void onAutoPipSelected(View v) {
231         switch (v.getId()) {
232             case R.id.enter_pip_on_leave_manual:
233                 // disable auto enter PiP
234             case R.id.enter_pip_on_leave_disabled:
235                 mPipParamsBuilder.setAutoEnterEnabled(false);
236                 setPictureInPictureParams(mPipParamsBuilder.build());
237                 break;
238             case R.id.enter_pip_on_leave_autoenter:
239                 mPipParamsBuilder.setAutoEnterEnabled(true);
240                 setPictureInPictureParams(mPipParamsBuilder.build());
241                 break;
242         }
243     }
244 
onRatioSelected(View v)245     public void onRatioSelected(View v) {
246         switch (v.getId()) {
247             case R.id.ratio_default:
248                 mPipParamsBuilder.setAspectRatio(RATIO_DEFAULT);
249                 break;
250 
251             case R.id.ratio_square:
252                 mPipParamsBuilder.setAspectRatio(RATIO_SQUARE);
253                 break;
254 
255             case R.id.ratio_wide:
256                 mPipParamsBuilder.setAspectRatio(RATIO_WIDE);
257                 break;
258 
259             case R.id.ratio_tall:
260                 mPipParamsBuilder.setAspectRatio(RATIO_TALL);
261                 break;
262         }
263     }
264 
updateMediaSessionState(int newState)265     private void updateMediaSessionState(int newState) {
266         if (mPlaybackState.getState() == newState) {
267             return;
268         }
269         final String title;
270         switch (newState) {
271             case STATE_PLAYING:
272                 title = TITLE_STATE_PLAYING;
273                 break;
274             case STATE_PAUSED:
275                 title = TITLE_STATE_PAUSED;
276                 break;
277             case STATE_STOPPED:
278                 title = "";
279                 break;
280 
281             default:
282                 throw new IllegalArgumentException("Unknown state " + newState);
283         }
284 
285         mPlaybackStateBuilder.setState(newState, 0, 1f);
286         mPlaybackState = mPlaybackStateBuilder.build();
287 
288         mMediaMetadataBuilder.putText(METADATA_KEY_TITLE, title);
289 
290         mMediaSession.setPlaybackState(mPlaybackState);
291         mMediaSession.setMetadata(mMediaMetadataBuilder.build());
292         mMediaSession.setActive(newState != STATE_STOPPED);
293     }
294 
handleIntentExtra(Intent intent)295     private void handleIntentExtra(Intent intent) {
296         // Set the fixed orientation if requested
297         if (intent.hasExtra(EXTRA_PIP_ORIENTATION)) {
298             final int ori = Integer.parseInt(getIntent().getStringExtra(EXTRA_PIP_ORIENTATION));
299             setRequestedOrientation(ori);
300         }
301         // Enter picture in picture with the given aspect ratio if provided
302         if (intent.hasExtra(EXTRA_ENTER_PIP)) {
303             mPipParamsBuilder.setActions(mSwitchOnActions);
304             enterPip(null);
305         }
306     }
307 }
308