PageRenderTime 98ms CodeModel.GetById 30ms RepoModel.GetById 0ms app.codeStats 1ms

/packages/SystemUI/src/com/android/systemui/media/NotificationPlayer.java

https://github.com/android/platform_frameworks_base
Java | 451 lines | 309 code | 36 blank | 106 comment | 74 complexity | 360f048cf46af8d42a419feac1c02102 MD5 | raw file
Possible License(s): Apache-2.0
  1. /*
  2. * Copyright (C) 2008 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. package com.android.systemui.media;
  17. import android.content.Context;
  18. import android.media.AudioAttributes;
  19. import android.media.AudioManager;
  20. import android.media.MediaPlayer;
  21. import android.media.MediaPlayer.OnCompletionListener;
  22. import android.media.MediaPlayer.OnErrorListener;
  23. import android.media.PlayerBase;
  24. import android.net.Uri;
  25. import android.os.Looper;
  26. import android.os.PowerManager;
  27. import android.os.SystemClock;
  28. import android.util.Log;
  29. import com.android.internal.annotations.GuardedBy;
  30. import java.util.LinkedList;
  31. /**
  32. * @hide
  33. * This class is provides the same interface and functionality as android.media.AsyncPlayer
  34. * with the following differences:
  35. * - whenever audio is played, audio focus is requested,
  36. * - whenever audio playback is stopped or the playback completed, audio focus is abandoned.
  37. */
  38. public class NotificationPlayer implements OnCompletionListener, OnErrorListener {
  39. private static final int PLAY = 1;
  40. private static final int STOP = 2;
  41. private static final boolean DEBUG = false;
  42. private static final class Command {
  43. int code;
  44. Context context;
  45. Uri uri;
  46. boolean looping;
  47. AudioAttributes attributes;
  48. long requestTime;
  49. public String toString() {
  50. return "{ code=" + code + " looping=" + looping + " attributes=" + attributes
  51. + " uri=" + uri + " }";
  52. }
  53. }
  54. private final LinkedList<Command> mCmdQueue = new LinkedList<Command>();
  55. private final Object mCompletionHandlingLock = new Object();
  56. @GuardedBy("mCompletionHandlingLock")
  57. private CreationAndCompletionThread mCompletionThread;
  58. @GuardedBy("mCompletionHandlingLock")
  59. private Looper mLooper;
  60. /*
  61. * Besides the use of audio focus, the only implementation difference between AsyncPlayer and
  62. * NotificationPlayer resides in the creation of the MediaPlayer. For the completion callback,
  63. * OnCompletionListener, to be called at the end of the playback, the MediaPlayer needs to
  64. * be created with a looper running so its event handler is not null.
  65. */
  66. private final class CreationAndCompletionThread extends Thread {
  67. public Command mCmd;
  68. public CreationAndCompletionThread(Command cmd) {
  69. super();
  70. mCmd = cmd;
  71. }
  72. public void run() {
  73. Looper.prepare();
  74. // ok to modify mLooper as here we are
  75. // synchronized on mCompletionHandlingLock due to the Object.wait() in startSound(cmd)
  76. mLooper = Looper.myLooper();
  77. if (DEBUG) Log.d(mTag, "in run: new looper " + mLooper);
  78. synchronized(this) {
  79. AudioManager audioManager =
  80. (AudioManager) mCmd.context.getSystemService(Context.AUDIO_SERVICE);
  81. try {
  82. MediaPlayer player = new MediaPlayer();
  83. if (mCmd.attributes == null) {
  84. mCmd.attributes = new AudioAttributes.Builder()
  85. .setUsage(AudioAttributes.USAGE_NOTIFICATION)
  86. .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
  87. .build();
  88. }
  89. player.setAudioAttributes(mCmd.attributes);
  90. player.setDataSource(mCmd.context, mCmd.uri);
  91. player.setLooping(mCmd.looping);
  92. player.setOnCompletionListener(NotificationPlayer.this);
  93. player.setOnErrorListener(NotificationPlayer.this);
  94. player.prepare();
  95. if ((mCmd.uri != null) && (mCmd.uri.getEncodedPath() != null)
  96. && (mCmd.uri.getEncodedPath().length() > 0)) {
  97. if (!audioManager.isMusicActiveRemotely()) {
  98. synchronized (mQueueAudioFocusLock) {
  99. if (mAudioManagerWithAudioFocus == null) {
  100. if (DEBUG) Log.d(mTag, "requesting AudioFocus");
  101. int focusGain = AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK;
  102. if (mCmd.looping) {
  103. focusGain = AudioManager.AUDIOFOCUS_GAIN;
  104. }
  105. mNotificationRampTimeMs = audioManager.getFocusRampTimeMs(
  106. focusGain, mCmd.attributes);
  107. audioManager.requestAudioFocus(null, mCmd.attributes,
  108. focusGain, 0);
  109. mAudioManagerWithAudioFocus = audioManager;
  110. } else {
  111. if (DEBUG) Log.d(mTag, "AudioFocus was previously requested");
  112. }
  113. }
  114. }
  115. }
  116. // FIXME Having to start a new thread so we can receive completion callbacks
  117. // is wrong, as we kill this thread whenever a new sound is to be played. This
  118. // can lead to AudioFocus being released too early, before the second sound is
  119. // done playing. This class should be modified to use a single thread, on which
  120. // command are issued, and on which it receives the completion callbacks.
  121. if (DEBUG) { Log.d(mTag, "notification will be delayed by "
  122. + mNotificationRampTimeMs + "ms"); }
  123. try {
  124. Thread.sleep(mNotificationRampTimeMs);
  125. player.start();
  126. } catch (InterruptedException e) {
  127. Log.e(mTag, "Exception while sleeping to sync notification playback"
  128. + " with ducking", e);
  129. }
  130. if (DEBUG) { Log.d(mTag, "player.start"); }
  131. if (mPlayer != null) {
  132. if (DEBUG) { Log.d(mTag, "mPlayer.release"); }
  133. mPlayer.release();
  134. }
  135. mPlayer = player;
  136. }
  137. catch (Exception e) {
  138. Log.w(mTag, "error loading sound for " + mCmd.uri, e);
  139. }
  140. this.notify();
  141. }
  142. Looper.loop();
  143. }
  144. };
  145. private void startSound(Command cmd) {
  146. // Preparing can be slow, so if there is something else
  147. // is playing, let it continue until we're done, so there
  148. // is less of a glitch.
  149. try {
  150. if (DEBUG) { Log.d(mTag, "startSound()"); }
  151. //-----------------------------------
  152. // This is were we deviate from the AsyncPlayer implementation and create the
  153. // MediaPlayer in a new thread with which we're synchronized
  154. synchronized(mCompletionHandlingLock) {
  155. // if another sound was already playing, it doesn't matter we won't get notified
  156. // of the completion, since only the completion notification of the last sound
  157. // matters
  158. if((mLooper != null)
  159. && (mLooper.getThread().getState() != Thread.State.TERMINATED)) {
  160. if (DEBUG) { Log.d(mTag, "in startSound quitting looper " + mLooper); }
  161. mLooper.quit();
  162. }
  163. mCompletionThread = new CreationAndCompletionThread(cmd);
  164. synchronized (mCompletionThread) {
  165. mCompletionThread.start();
  166. mCompletionThread.wait();
  167. }
  168. }
  169. //-----------------------------------
  170. long delay = SystemClock.uptimeMillis() - cmd.requestTime;
  171. if (delay > 1000) {
  172. Log.w(mTag, "Notification sound delayed by " + delay + "msecs");
  173. }
  174. }
  175. catch (Exception e) {
  176. Log.w(mTag, "error loading sound for " + cmd.uri, e);
  177. }
  178. }
  179. private final class CmdThread extends java.lang.Thread {
  180. CmdThread() {
  181. super("NotificationPlayer-" + mTag);
  182. }
  183. public void run() {
  184. while (true) {
  185. Command cmd = null;
  186. synchronized (mCmdQueue) {
  187. if (DEBUG) Log.d(mTag, "RemoveFirst");
  188. cmd = mCmdQueue.removeFirst();
  189. }
  190. switch (cmd.code) {
  191. case PLAY:
  192. if (DEBUG) Log.d(mTag, "PLAY");
  193. startSound(cmd);
  194. break;
  195. case STOP:
  196. if (DEBUG) Log.d(mTag, "STOP");
  197. if (mPlayer != null) {
  198. long delay = SystemClock.uptimeMillis() - cmd.requestTime;
  199. if (delay > 1000) {
  200. Log.w(mTag, "Notification stop delayed by " + delay + "msecs");
  201. }
  202. mPlayer.stop();
  203. mPlayer.release();
  204. mPlayer = null;
  205. synchronized(mQueueAudioFocusLock) {
  206. if (mAudioManagerWithAudioFocus != null) {
  207. if (DEBUG) { Log.d(mTag, "in STOP: abandonning AudioFocus"); }
  208. mAudioManagerWithAudioFocus.abandonAudioFocus(null);
  209. mAudioManagerWithAudioFocus = null;
  210. }
  211. }
  212. synchronized (mCompletionHandlingLock) {
  213. if ((mLooper != null) &&
  214. (mLooper.getThread().getState() != Thread.State.TERMINATED))
  215. {
  216. if (DEBUG) { Log.d(mTag, "in STOP: quitting looper "+ mLooper); }
  217. mLooper.quit();
  218. }
  219. }
  220. } else {
  221. Log.w(mTag, "STOP command without a player");
  222. }
  223. break;
  224. }
  225. synchronized (mCmdQueue) {
  226. if (mCmdQueue.size() == 0) {
  227. // nothing left to do, quit
  228. // doing this check after we're done prevents the case where they
  229. // added it during the operation from spawning two threads and
  230. // trying to do them in parallel.
  231. mThread = null;
  232. releaseWakeLock();
  233. return;
  234. }
  235. }
  236. }
  237. }
  238. }
  239. public void onCompletion(MediaPlayer mp) {
  240. synchronized(mQueueAudioFocusLock) {
  241. if (mAudioManagerWithAudioFocus != null) {
  242. if (DEBUG) Log.d(mTag, "onCompletion() abandonning AudioFocus");
  243. mAudioManagerWithAudioFocus.abandonAudioFocus(null);
  244. mAudioManagerWithAudioFocus = null;
  245. } else {
  246. if (DEBUG) Log.d(mTag, "onCompletion() no need to abandon AudioFocus");
  247. }
  248. }
  249. // if there are no more sounds to play, end the Looper to listen for media completion
  250. synchronized (mCmdQueue) {
  251. synchronized(mCompletionHandlingLock) {
  252. if (DEBUG) { Log.d(mTag, "onCompletion queue size=" + mCmdQueue.size()); }
  253. if ((mCmdQueue.size() == 0)) {
  254. if (mLooper != null) {
  255. if (DEBUG) { Log.d(mTag, "in onCompletion quitting looper " + mLooper); }
  256. mLooper.quit();
  257. }
  258. mCompletionThread = null;
  259. }
  260. }
  261. }
  262. }
  263. public boolean onError(MediaPlayer mp, int what, int extra) {
  264. Log.e(mTag, "error " + what + " (extra=" + extra + ") playing notification");
  265. // error happened, handle it just like a completion
  266. onCompletion(mp);
  267. return true;
  268. }
  269. private String mTag;
  270. @GuardedBy("mCmdQueue")
  271. private CmdThread mThread;
  272. private MediaPlayer mPlayer;
  273. @GuardedBy("mCmdQueue")
  274. private PowerManager.WakeLock mWakeLock;
  275. private final Object mQueueAudioFocusLock = new Object();
  276. @GuardedBy("mQueueAudioFocusLock")
  277. private AudioManager mAudioManagerWithAudioFocus;
  278. private int mNotificationRampTimeMs = 0;
  279. // The current state according to the caller. Reality lags behind
  280. // because of the asynchronous nature of this class.
  281. private int mState = STOP;
  282. /**
  283. * Construct a NotificationPlayer object.
  284. *
  285. * @param tag a string to use for debugging
  286. */
  287. public NotificationPlayer(String tag) {
  288. if (tag != null) {
  289. mTag = tag;
  290. } else {
  291. mTag = "NotificationPlayer";
  292. }
  293. }
  294. /**
  295. * Start playing the sound. It will actually start playing at some
  296. * point in the future. There are no guarantees about latency here.
  297. * Calling this before another audio file is done playing will stop
  298. * that one and start the new one.
  299. *
  300. * @param context Your application's context.
  301. * @param uri The URI to play. (see {@link MediaPlayer#setDataSource(Context, Uri)})
  302. * @param looping Whether the audio should loop forever.
  303. * (see {@link MediaPlayer#setLooping(boolean)})
  304. * @param stream the AudioStream to use.
  305. * (see {@link MediaPlayer#setAudioStreamType(int)})
  306. * @deprecated use {@link #play(Context, Uri, boolean, AudioAttributes)} instead.
  307. */
  308. @Deprecated
  309. public void play(Context context, Uri uri, boolean looping, int stream) {
  310. if (DEBUG) { Log.d(mTag, "play uri=" + uri.toString()); }
  311. PlayerBase.deprecateStreamTypeForPlayback(stream, "NotificationPlayer", "play");
  312. Command cmd = new Command();
  313. cmd.requestTime = SystemClock.uptimeMillis();
  314. cmd.code = PLAY;
  315. cmd.context = context;
  316. cmd.uri = uri;
  317. cmd.looping = looping;
  318. cmd.attributes = new AudioAttributes.Builder().setInternalLegacyStreamType(stream).build();
  319. synchronized (mCmdQueue) {
  320. enqueueLocked(cmd);
  321. mState = PLAY;
  322. }
  323. }
  324. /**
  325. * Start playing the sound. It will actually start playing at some
  326. * point in the future. There are no guarantees about latency here.
  327. * Calling this before another audio file is done playing will stop
  328. * that one and start the new one.
  329. *
  330. * @param context Your application's context.
  331. * @param uri The URI to play. (see {@link MediaPlayer#setDataSource(Context, Uri)})
  332. * @param looping Whether the audio should loop forever.
  333. * (see {@link MediaPlayer#setLooping(boolean)})
  334. * @param attributes the AudioAttributes to use.
  335. * (see {@link MediaPlayer#setAudioAttributes(AudioAttributes)})
  336. */
  337. public void play(Context context, Uri uri, boolean looping, AudioAttributes attributes) {
  338. if (DEBUG) { Log.d(mTag, "play uri=" + uri.toString()); }
  339. Command cmd = new Command();
  340. cmd.requestTime = SystemClock.uptimeMillis();
  341. cmd.code = PLAY;
  342. cmd.context = context;
  343. cmd.uri = uri;
  344. cmd.looping = looping;
  345. cmd.attributes = attributes;
  346. synchronized (mCmdQueue) {
  347. enqueueLocked(cmd);
  348. mState = PLAY;
  349. }
  350. }
  351. /**
  352. * Stop a previously played sound. It can't be played again or unpaused
  353. * at this point. Calling this multiple times has no ill effects.
  354. */
  355. public void stop() {
  356. if (DEBUG) { Log.d(mTag, "stop"); }
  357. synchronized (mCmdQueue) {
  358. // This check allows stop to be called multiple times without starting
  359. // a thread that ends up doing nothing.
  360. if (mState != STOP) {
  361. Command cmd = new Command();
  362. cmd.requestTime = SystemClock.uptimeMillis();
  363. cmd.code = STOP;
  364. enqueueLocked(cmd);
  365. mState = STOP;
  366. }
  367. }
  368. }
  369. @GuardedBy("mCmdQueue")
  370. private void enqueueLocked(Command cmd) {
  371. mCmdQueue.add(cmd);
  372. if (mThread == null) {
  373. acquireWakeLock();
  374. mThread = new CmdThread();
  375. mThread.start();
  376. }
  377. }
  378. /**
  379. * We want to hold a wake lock while we do the prepare and play. The stop probably is
  380. * optional, but it won't hurt to have it too. The problem is that if you start a sound
  381. * while you're holding a wake lock (e.g. an alarm starting a notification), you want the
  382. * sound to play, but if the CPU turns off before mThread gets to work, it won't. The
  383. * simplest way to deal with this is to make it so there is a wake lock held while the
  384. * thread is starting or running. You're going to need the WAKE_LOCK permission if you're
  385. * going to call this.
  386. *
  387. * This must be called before the first time play is called.
  388. *
  389. * @hide
  390. */
  391. public void setUsesWakeLock(Context context) {
  392. synchronized (mCmdQueue) {
  393. if (mWakeLock != null || mThread != null) {
  394. // if either of these has happened, we've already played something.
  395. // and our releases will be out of sync.
  396. throw new RuntimeException("assertion failed mWakeLock=" + mWakeLock
  397. + " mThread=" + mThread);
  398. }
  399. PowerManager pm = (PowerManager)context.getSystemService(Context.POWER_SERVICE);
  400. mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, mTag);
  401. }
  402. }
  403. @GuardedBy("mCmdQueue")
  404. private void acquireWakeLock() {
  405. if (mWakeLock != null) {
  406. mWakeLock.acquire();
  407. }
  408. }
  409. @GuardedBy("mCmdQueue")
  410. private void releaseWakeLock() {
  411. if (mWakeLock != null) {
  412. mWakeLock.release();
  413. }
  414. }
  415. }