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