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