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