1 /* 2 * Copyright (C) 2019 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.media; 18 19 import android.annotation.NonNull; 20 import android.app.ActivityThread; 21 import android.content.Context; 22 import android.media.MediaMetadata; 23 import android.media.session.ISessionManager; 24 import android.media.session.MediaController; 25 import android.media.session.MediaSession; 26 import android.media.session.MediaSessionManager; 27 import android.media.session.PlaybackState; 28 import android.os.Bundle; 29 import android.os.HandlerThread; 30 import android.os.Looper; 31 import android.os.RemoteException; 32 import android.os.ServiceManager; 33 import android.os.ShellCommand; 34 import android.os.SystemClock; 35 import android.text.TextUtils; 36 import android.view.InputDevice; 37 import android.view.KeyCharacterMap; 38 import android.view.KeyEvent; 39 40 import java.io.BufferedReader; 41 import java.io.IOException; 42 import java.io.InputStream; 43 import java.io.InputStreamReader; 44 import java.io.PrintWriter; 45 import java.util.List; 46 47 /** 48 * ShellCommand for MediaSessionService. 49 */ 50 public class MediaShellCommand extends ShellCommand { 51 private static ActivityThread sThread; 52 private static MediaSessionManager sMediaSessionManager; 53 54 private final String mPackageName; 55 private ISessionManager mSessionService; 56 private PrintWriter mWriter; 57 private PrintWriter mErrorWriter; 58 private InputStream mInput; 59 MediaShellCommand(String packageName)60 public MediaShellCommand(String packageName) { 61 mPackageName = packageName; 62 } 63 64 @Override onCommand(String cmd)65 public int onCommand(String cmd) { 66 mWriter = getOutPrintWriter(); 67 mErrorWriter = getErrPrintWriter(); 68 mInput = getRawInputStream(); 69 70 if (TextUtils.isEmpty(cmd)) { 71 return handleDefaultCommands(cmd); 72 } 73 if (sThread == null) { 74 Looper.prepare(); 75 sThread = ActivityThread.currentActivityThread(); 76 Context context = sThread.getSystemContext(); 77 sMediaSessionManager = 78 (MediaSessionManager) context.getSystemService(Context.MEDIA_SESSION_SERVICE); 79 } 80 mSessionService = ISessionManager.Stub.asInterface(ServiceManager.checkService( 81 Context.MEDIA_SESSION_SERVICE)); 82 if (mSessionService == null) { 83 throw new IllegalStateException( 84 "Can't connect to media session service; is the system running?"); 85 } 86 87 try { 88 if (cmd.equals("dispatch")) { 89 runDispatch(); 90 } else if (cmd.equals("list-sessions")) { 91 runListSessions(); 92 } else if (cmd.equals("monitor")) { 93 runMonitor(); 94 } else if (cmd.equals("volume")) { 95 runVolume(); 96 } else if (cmd.equals("expire-temp-engaged-sessions")) { 97 expireTempEngagedSessions(); 98 } else { 99 showError("Error: unknown command '" + cmd + "'"); 100 return -1; 101 } 102 } catch (Exception e) { 103 showError(e.toString()); 104 return -1; 105 } 106 return 0; 107 } 108 109 @Override onHelp()110 public void onHelp() { 111 mWriter.println("usage: media_session [subcommand] [options]"); 112 mWriter.println(" media_session dispatch KEY"); 113 mWriter.println(" media_session list-sessions"); 114 mWriter.println(" media_session monitor <tag>"); 115 mWriter.println(" media_session volume [options]"); 116 mWriter.println(" media_session expire-temp-engaged-sessions"); 117 mWriter.println(); 118 mWriter.println("media_session dispatch: dispatch a media key to the system."); 119 mWriter.println(" KEY may be: play, pause, play-pause, mute, headsethook,"); 120 mWriter.println(" stop, next, previous, rewind, record, fast-forward."); 121 mWriter.println("media_session list-sessions: print a list of the current sessions."); 122 mWriter.println("media_session monitor: monitor updates to the specified session."); 123 mWriter.println(" Use the tag from list-sessions."); 124 mWriter.println("media_session volume: " + VolumeCtrl.USAGE); 125 mWriter.println("media_session expire-temp-engaged-sessions: Expires any ongoing"); 126 mWriter.println(" timers for media sessions in a temporary user-engaged"); 127 mWriter.println(" state."); 128 mWriter.println(); 129 } 130 sendMediaKey(KeyEvent event)131 private void sendMediaKey(KeyEvent event) { 132 try { 133 mSessionService.dispatchMediaKeyEvent( 134 mPackageName, /* asSystemService= */ false, event, /* needWakeLock= */ false); 135 } catch (RemoteException e) { 136 } 137 } 138 runMonitor()139 private void runMonitor() throws Exception { 140 String id = getNextArgRequired(); 141 if (id == null) { 142 showError("Error: must include a session id"); 143 return; 144 } 145 146 boolean success = false; 147 try { 148 List<MediaController> controllers = sMediaSessionManager.getActiveSessions(null); 149 for (MediaController controller : controllers) { 150 try { 151 if (controller != null && id.equals(controller.getTag())) { 152 MediaShellCommand.ControllerMonitor monitor = 153 new MediaShellCommand.ControllerMonitor(controller); 154 monitor.run(); 155 success = true; 156 break; 157 } 158 } catch (RemoteException e) { 159 // ignore 160 } 161 } 162 } catch (Exception e) { 163 mErrorWriter.println("***Error monitoring session*** " + e.getMessage()); 164 } 165 if (!success) { 166 mErrorWriter.println("No session found with id " + id); 167 } 168 } 169 runDispatch()170 private void runDispatch() throws Exception { 171 String cmd = getNextArgRequired(); 172 int keycode; 173 if ("play".equals(cmd)) { 174 keycode = KeyEvent.KEYCODE_MEDIA_PLAY; 175 } else if ("pause".equals(cmd)) { 176 keycode = KeyEvent.KEYCODE_MEDIA_PAUSE; 177 } else if ("play-pause".equals(cmd)) { 178 keycode = KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE; 179 } else if ("mute".equals(cmd)) { 180 keycode = KeyEvent.KEYCODE_MUTE; 181 } else if ("headsethook".equals(cmd)) { 182 keycode = KeyEvent.KEYCODE_HEADSETHOOK; 183 } else if ("stop".equals(cmd)) { 184 keycode = KeyEvent.KEYCODE_MEDIA_STOP; 185 } else if ("next".equals(cmd)) { 186 keycode = KeyEvent.KEYCODE_MEDIA_NEXT; 187 } else if ("previous".equals(cmd)) { 188 keycode = KeyEvent.KEYCODE_MEDIA_PREVIOUS; 189 } else if ("rewind".equals(cmd)) { 190 keycode = KeyEvent.KEYCODE_MEDIA_REWIND; 191 } else if ("record".equals(cmd)) { 192 keycode = KeyEvent.KEYCODE_MEDIA_RECORD; 193 } else if ("fast-forward".equals(cmd)) { 194 keycode = KeyEvent.KEYCODE_MEDIA_FAST_FORWARD; 195 } else { 196 showError("Error: unknown dispatch code '" + cmd + "'"); 197 return; 198 } 199 final long now = SystemClock.uptimeMillis(); 200 sendMediaKey(new KeyEvent(now, now, KeyEvent.ACTION_DOWN, keycode, 0, 0, 201 KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, InputDevice.SOURCE_KEYBOARD)); 202 sendMediaKey(new KeyEvent(now, now, KeyEvent.ACTION_UP, keycode, 0, 0, 203 KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, InputDevice.SOURCE_KEYBOARD)); 204 } 205 log(String code, String msg)206 void log(String code, String msg) { 207 mWriter.println(code + " " + msg); 208 } 209 showError(String errMsg)210 void showError(String errMsg) { 211 onHelp(); 212 mErrorWriter.println(errMsg); 213 } 214 215 class ControllerCallback extends MediaController.Callback { 216 @Override onSessionDestroyed()217 public void onSessionDestroyed() { 218 mWriter.println("onSessionDestroyed. Enter q to quit."); 219 } 220 221 @Override onSessionEvent(String event, Bundle extras)222 public void onSessionEvent(String event, Bundle extras) { 223 mWriter.println("onSessionEvent event=" + event + ", extras=" + extras); 224 } 225 226 @Override onPlaybackStateChanged(PlaybackState state)227 public void onPlaybackStateChanged(PlaybackState state) { 228 mWriter.println("onPlaybackStateChanged " + state); 229 } 230 231 @Override onMetadataChanged(MediaMetadata metadata)232 public void onMetadataChanged(MediaMetadata metadata) { 233 String mmString = metadata == null ? null : "title=" + metadata 234 .getDescription(); 235 mWriter.println("onMetadataChanged " + mmString); 236 } 237 238 @Override onQueueChanged(List<MediaSession.QueueItem> queue)239 public void onQueueChanged(List<MediaSession.QueueItem> queue) { 240 mWriter.println("onQueueChanged, " 241 + (queue == null ? "null queue" : " size=" + queue.size())); 242 } 243 244 @Override onQueueTitleChanged(CharSequence title)245 public void onQueueTitleChanged(CharSequence title) { 246 mWriter.println("onQueueTitleChange " + title); 247 } 248 249 @Override onExtrasChanged(Bundle extras)250 public void onExtrasChanged(Bundle extras) { 251 mWriter.println("onExtrasChanged " + extras); 252 } 253 254 @Override onAudioInfoChanged(@onNull MediaController.PlaybackInfo info)255 public void onAudioInfoChanged(@NonNull MediaController.PlaybackInfo info) { 256 mWriter.println("onAudioInfoChanged " + info); 257 } 258 } 259 260 private class ControllerMonitor { 261 private final MediaController mController; 262 private final MediaShellCommand.ControllerCallback mControllerCallback; 263 ControllerMonitor(MediaController controller)264 ControllerMonitor(MediaController controller) { 265 mController = controller; 266 mControllerCallback = new MediaShellCommand.ControllerCallback(); 267 } 268 printUsageMessage()269 void printUsageMessage() { 270 try { 271 mWriter.println("V2Monitoring session " + mController.getTag() 272 + "... available commands: play, pause, next, previous"); 273 } catch (RuntimeException e) { 274 mWriter.println("Error trying to monitor session!"); 275 } 276 mWriter.println("(q)uit: finish monitoring"); 277 } 278 run()279 void run() throws RemoteException { 280 printUsageMessage(); 281 HandlerThread cbThread = new HandlerThread("MediaCb") { 282 @Override 283 protected void onLooperPrepared() { 284 try { 285 mController.registerCallback(mControllerCallback); 286 } catch (RuntimeException e) { 287 mErrorWriter.println("Error registering monitor callback"); 288 } 289 } 290 }; 291 cbThread.start(); 292 293 try { 294 InputStreamReader converter = new InputStreamReader(mInput); 295 BufferedReader in = new BufferedReader(converter); 296 String line; 297 298 while (true) { 299 mWriter.flush(); 300 mErrorWriter.flush(); 301 if ((line = in.readLine()) == null) break; 302 boolean addNewline = true; 303 if (line.length() <= 0) { 304 addNewline = false; 305 } else if ("q".equals(line) || "quit".equals(line)) { 306 break; 307 } else if ("play".equals(line)) { 308 dispatchKeyCode(KeyEvent.KEYCODE_MEDIA_PLAY); 309 } else if ("pause".equals(line)) { 310 dispatchKeyCode(KeyEvent.KEYCODE_MEDIA_PAUSE); 311 } else if ("next".equals(line)) { 312 dispatchKeyCode(KeyEvent.KEYCODE_MEDIA_NEXT); 313 } else if ("previous".equals(line)) { 314 dispatchKeyCode(KeyEvent.KEYCODE_MEDIA_PREVIOUS); 315 } else { 316 mErrorWriter.println("Invalid command: " + line); 317 } 318 319 synchronized (this) { 320 if (addNewline) { 321 mWriter.println(""); 322 } 323 printUsageMessage(); 324 } 325 } 326 } catch (IOException e) { 327 e.printStackTrace(); 328 } finally { 329 cbThread.getLooper().quit(); 330 try { 331 mController.unregisterCallback(mControllerCallback); 332 } catch (Exception e) { 333 // ignoring 334 } 335 } 336 } 337 dispatchKeyCode(int keyCode)338 private void dispatchKeyCode(int keyCode) { 339 final long now = SystemClock.uptimeMillis(); 340 KeyEvent down = new KeyEvent(now, now, KeyEvent.ACTION_DOWN, keyCode, 0, 0, 341 KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, InputDevice.SOURCE_KEYBOARD); 342 KeyEvent up = new KeyEvent(now, now, KeyEvent.ACTION_UP, keyCode, 0, 0, 343 KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, InputDevice.SOURCE_KEYBOARD); 344 try { 345 mController.dispatchMediaButtonEvent(down); 346 mController.dispatchMediaButtonEvent(up); 347 } catch (RuntimeException e) { 348 mErrorWriter.println("Failed to dispatch " + keyCode); 349 } 350 } 351 } 352 runListSessions()353 private void runListSessions() { 354 mWriter.println("Sessions:"); 355 try { 356 List<MediaController> controllers = sMediaSessionManager.getActiveSessions(null); 357 for (MediaController controller : controllers) { 358 if (controller != null) { 359 try { 360 mWriter.println(" tag=" + controller.getTag() 361 + ", package=" + controller.getPackageName()); 362 } catch (RuntimeException e) { 363 // ignore 364 } 365 } 366 } 367 } catch (Exception e) { 368 mErrorWriter.println("***Error listing sessions***"); 369 } 370 } 371 372 //================================= 373 // "volume" command for stream volume control runVolume()374 private void runVolume() throws Exception { 375 VolumeCtrl.run(this); 376 } 377 expireTempEngagedSessions()378 private void expireTempEngagedSessions() throws Exception { 379 mSessionService.expireTempEngagedSessions(); 380 } 381 } 382