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