diff options
Diffstat (limited to 'packages/SystemUI')
4 files changed, 507 insertions, 0 deletions
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index a31c264..cef21b0 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -5,6 +5,7 @@ <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> + <uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.INJECT_EVENTS" /> <uses-permission android:name="android.permission.WRITE_SETTINGS" /> @@ -12,6 +13,7 @@ <uses-permission android:name="android.permission.STATUS_BAR_SERVICE" /> <uses-permission android:name="android.permission.STATUS_BAR" /> <uses-permission android:name="android.permission.EXPAND_STATUS_BAR" /> + <uses-permission android:name="android.permission.REMOTE_AUDIO_PLAYBACK" /> <!-- Networking and telephony --> <uses-permission android:name="android.permission.BLUETOOTH" /> diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIService.java b/packages/SystemUI/src/com/android/systemui/SystemUIService.java index ae568f8..0a57499 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUIService.java +++ b/packages/SystemUI/src/com/android/systemui/SystemUIService.java @@ -41,6 +41,7 @@ public class SystemUIService extends Service { final Object[] SERVICES = new Object[] { 0, // system bar or status bar, filled in below. com.android.systemui.power.PowerUI.class, + com.android.systemui.media.RingtonePlayer.class, }; /** diff --git a/packages/SystemUI/src/com/android/systemui/media/NotificationPlayer.java b/packages/SystemUI/src/com/android/systemui/media/NotificationPlayer.java new file mode 100644 index 0000000..6a12eb1 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/NotificationPlayer.java @@ -0,0 +1,340 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media; + +import android.content.Context; +import android.media.AudioManager; +import android.media.MediaPlayer; +import android.media.MediaPlayer.OnCompletionListener; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.PowerManager; +import android.os.SystemClock; +import android.util.Log; + +import java.io.IOException; +import java.lang.IllegalStateException; +import java.lang.Thread; +import java.util.LinkedList; + +/** + * @hide + * This class is provides the same interface and functionality as android.media.AsyncPlayer + * with the following differences: + * - whenever audio is played, audio focus is requested, + * - whenever audio playback is stopped or the playback completed, audio focus is abandoned. + */ +public class NotificationPlayer implements OnCompletionListener { + private static final int PLAY = 1; + private static final int STOP = 2; + private static final boolean mDebug = false; + + private static final class Command { + int code; + Context context; + Uri uri; + boolean looping; + int stream; + long requestTime; + + public String toString() { + return "{ code=" + code + " looping=" + looping + " stream=" + stream + + " uri=" + uri + " }"; + } + } + + private LinkedList<Command> mCmdQueue = new LinkedList(); + + private Looper mLooper; + + /* + * Besides the use of audio focus, the only implementation difference between AsyncPlayer and + * NotificationPlayer resides in the creation of the MediaPlayer. For the completion callback, + * OnCompletionListener, to be called at the end of the playback, the MediaPlayer needs to + * be created with a looper running so its event handler is not null. + */ + private final class CreationAndCompletionThread extends Thread { + public Command mCmd; + public CreationAndCompletionThread(Command cmd) { + super(); + mCmd = cmd; + } + + public void run() { + Looper.prepare(); + mLooper = Looper.myLooper(); + synchronized(this) { + AudioManager audioManager = + (AudioManager) mCmd.context.getSystemService(Context.AUDIO_SERVICE); + try { + MediaPlayer player = new MediaPlayer(); + player.setAudioStreamType(mCmd.stream); + player.setDataSource(mCmd.context, mCmd.uri); + player.setLooping(mCmd.looping); + player.prepare(); + if ((mCmd.uri != null) && (mCmd.uri.getEncodedPath() != null) + && (mCmd.uri.getEncodedPath().length() > 0)) { + if (mCmd.looping) { + audioManager.requestAudioFocus(null, mCmd.stream, + AudioManager.AUDIOFOCUS_GAIN); + } else { + audioManager.requestAudioFocus(null, mCmd.stream, + AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK); + } + } + player.setOnCompletionListener(NotificationPlayer.this); + player.start(); + if (mPlayer != null) { + mPlayer.release(); + } + mPlayer = player; + } + catch (Exception e) { + Log.w(mTag, "error loading sound for " + mCmd.uri, e); + } + mAudioManager = audioManager; + this.notify(); + } + Looper.loop(); + } + }; + + private void startSound(Command cmd) { + // Preparing can be slow, so if there is something else + // is playing, let it continue until we're done, so there + // is less of a glitch. + try { + if (mDebug) Log.d(mTag, "Starting playback"); + //----------------------------------- + // This is were we deviate from the AsyncPlayer implementation and create the + // MediaPlayer in a new thread with which we're synchronized + synchronized(mCompletionHandlingLock) { + // if another sound was already playing, it doesn't matter we won't get notified + // of the completion, since only the completion notification of the last sound + // matters + if((mLooper != null) + && (mLooper.getThread().getState() != Thread.State.TERMINATED)) { + mLooper.quit(); + } + mCompletionThread = new CreationAndCompletionThread(cmd); + synchronized(mCompletionThread) { + mCompletionThread.start(); + mCompletionThread.wait(); + } + } + //----------------------------------- + + long delay = SystemClock.uptimeMillis() - cmd.requestTime; + if (delay > 1000) { + Log.w(mTag, "Notification sound delayed by " + delay + "msecs"); + } + } + catch (Exception e) { + Log.w(mTag, "error loading sound for " + cmd.uri, e); + } + } + + private final class CmdThread extends java.lang.Thread { + CmdThread() { + super("NotificationPlayer-" + mTag); + } + + public void run() { + while (true) { + Command cmd = null; + + synchronized (mCmdQueue) { + if (mDebug) Log.d(mTag, "RemoveFirst"); + cmd = mCmdQueue.removeFirst(); + } + + switch (cmd.code) { + case PLAY: + if (mDebug) Log.d(mTag, "PLAY"); + startSound(cmd); + break; + case STOP: + if (mDebug) Log.d(mTag, "STOP"); + if (mPlayer != null) { + long delay = SystemClock.uptimeMillis() - cmd.requestTime; + if (delay > 1000) { + Log.w(mTag, "Notification stop delayed by " + delay + "msecs"); + } + mPlayer.stop(); + mPlayer.release(); + mPlayer = null; + mAudioManager.abandonAudioFocus(null); + mAudioManager = null; + if((mLooper != null) + && (mLooper.getThread().getState() != Thread.State.TERMINATED)) { + mLooper.quit(); + } + } else { + Log.w(mTag, "STOP command without a player"); + } + break; + } + + synchronized (mCmdQueue) { + if (mCmdQueue.size() == 0) { + // nothing left to do, quit + // doing this check after we're done prevents the case where they + // added it during the operation from spawning two threads and + // trying to do them in parallel. + mThread = null; + releaseWakeLock(); + return; + } + } + } + } + } + + public void onCompletion(MediaPlayer mp) { + if (mAudioManager != null) { + mAudioManager.abandonAudioFocus(null); + } + // if there are no more sounds to play, end the Looper to listen for media completion + synchronized (mCmdQueue) { + if (mCmdQueue.size() == 0) { + synchronized(mCompletionHandlingLock) { + if(mLooper != null) { + mLooper.quit(); + } + mCompletionThread = null; + } + } + } + } + + private String mTag; + private CmdThread mThread; + private CreationAndCompletionThread mCompletionThread; + private final Object mCompletionHandlingLock = new Object(); + private MediaPlayer mPlayer; + private PowerManager.WakeLock mWakeLock; + private AudioManager mAudioManager; + + // The current state according to the caller. Reality lags behind + // because of the asynchronous nature of this class. + private int mState = STOP; + + /** + * Construct a NotificationPlayer object. + * + * @param tag a string to use for debugging + */ + public NotificationPlayer(String tag) { + if (tag != null) { + mTag = tag; + } else { + mTag = "NotificationPlayer"; + } + } + + /** + * Start playing the sound. It will actually start playing at some + * point in the future. There are no guarantees about latency here. + * Calling this before another audio file is done playing will stop + * that one and start the new one. + * + * @param context Your application's context. + * @param uri The URI to play. (see {@link MediaPlayer#setDataSource(Context, Uri)}) + * @param looping Whether the audio should loop forever. + * (see {@link MediaPlayer#setLooping(boolean)}) + * @param stream the AudioStream to use. + * (see {@link MediaPlayer#setAudioStreamType(int)}) + */ + public void play(Context context, Uri uri, boolean looping, int stream) { + Command cmd = new Command(); + cmd.requestTime = SystemClock.uptimeMillis(); + cmd.code = PLAY; + cmd.context = context; + cmd.uri = uri; + cmd.looping = looping; + cmd.stream = stream; + synchronized (mCmdQueue) { + enqueueLocked(cmd); + mState = PLAY; + } + } + + /** + * Stop a previously played sound. It can't be played again or unpaused + * at this point. Calling this multiple times has no ill effects. + */ + public void stop() { + synchronized (mCmdQueue) { + // This check allows stop to be called multiple times without starting + // a thread that ends up doing nothing. + if (mState != STOP) { + Command cmd = new Command(); + cmd.requestTime = SystemClock.uptimeMillis(); + cmd.code = STOP; + enqueueLocked(cmd); + mState = STOP; + } + } + } + + private void enqueueLocked(Command cmd) { + mCmdQueue.add(cmd); + if (mThread == null) { + acquireWakeLock(); + mThread = new CmdThread(); + mThread.start(); + } + } + + /** + * We want to hold a wake lock while we do the prepare and play. The stop probably is + * optional, but it won't hurt to have it too. The problem is that if you start a sound + * while you're holding a wake lock (e.g. an alarm starting a notification), you want the + * sound to play, but if the CPU turns off before mThread gets to work, it won't. The + * simplest way to deal with this is to make it so there is a wake lock held while the + * thread is starting or running. You're going to need the WAKE_LOCK permission if you're + * going to call this. + * + * This must be called before the first time play is called. + * + * @hide + */ + public void setUsesWakeLock(Context context) { + if (mWakeLock != null || mThread != null) { + // if either of these has happened, we've already played something. + // and our releases will be out of sync. + throw new RuntimeException("assertion failed mWakeLock=" + mWakeLock + + " mThread=" + mThread); + } + PowerManager pm = (PowerManager)context.getSystemService(Context.POWER_SERVICE); + mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, mTag); + } + + private void acquireWakeLock() { + if (mWakeLock != null) { + mWakeLock.acquire(); + } + } + + private void releaseWakeLock() { + if (mWakeLock != null) { + mWakeLock.release(); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java b/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java new file mode 100644 index 0000000..9e273d4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/RingtonePlayer.java @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media; + +import android.content.Context; +import android.media.IAudioService; +import android.media.IRingtonePlayer; +import android.media.Ringtone; +import android.net.Uri; +import android.os.Binder; +import android.os.IBinder; +import android.os.Process; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.util.Slog; + +import com.android.systemui.SystemUI; +import com.google.android.collect.Maps; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.HashMap; + +/** + * Service that offers to play ringtones by {@link Uri}, since our process has + * {@link android.Manifest.permission#READ_EXTERNAL_STORAGE}. + */ +public class RingtonePlayer extends SystemUI { + private static final String TAG = "RingtonePlayer"; + private static final boolean LOGD = true; + + // TODO: support Uri switching under same IBinder + + private IAudioService mAudioService; + + private final NotificationPlayer mAsyncPlayer = new NotificationPlayer(TAG); + private final HashMap<IBinder, Client> mClients = Maps.newHashMap(); + + @Override + public void start() { + mAsyncPlayer.setUsesWakeLock(mContext); + + mAudioService = IAudioService.Stub.asInterface( + ServiceManager.getService(Context.AUDIO_SERVICE)); + try { + mAudioService.setRingtonePlayer(mCallback); + } catch (RemoteException e) { + Slog.e(TAG, "Problem registering RingtonePlayer: " + e); + } + } + + /** + * Represents an active remote {@link Ringtone} client. + */ + private class Client implements IBinder.DeathRecipient { + private final IBinder mToken; + private final Ringtone mRingtone; + + public Client(IBinder token, Uri uri, int streamType) { + mToken = token; + mRingtone = new Ringtone(mContext, false); + mRingtone.setStreamType(streamType); + mRingtone.setUri(uri); + } + + @Override + public void binderDied() { + if (LOGD) Slog.d(TAG, "binderDied() token=" + mToken); + synchronized (mClients) { + mClients.remove(mToken); + } + mRingtone.stop(); + } + } + + private IRingtonePlayer mCallback = new IRingtonePlayer.Stub() { + @Override + public void play(IBinder token, Uri uri, int streamType) throws RemoteException { + if (LOGD) Slog.d(TAG, "play(token=" + token + ", uri=" + uri + ")"); + Client client; + synchronized (mClients) { + client = mClients.get(token); + if (client == null) { + client = new Client(token, uri, streamType); + token.linkToDeath(client, 0); + mClients.put(token, client); + } + } + client.mRingtone.play(); + } + + @Override + public void stop(IBinder token) { + if (LOGD) Slog.d(TAG, "stop(token=" + token + ")"); + Client client; + synchronized (mClients) { + client = mClients.remove(token); + } + if (client != null) { + client.mToken.unlinkToDeath(client, 0); + client.mRingtone.stop(); + } + } + + @Override + public boolean isPlaying(IBinder token) { + if (LOGD) Slog.d(TAG, "isPlaying(token=" + token + ")"); + Client client; + synchronized (mClients) { + client = mClients.get(token); + } + if (client != null) { + return client.mRingtone.isPlaying(); + } else { + return false; + } + } + + @Override + public void playAsync(Uri uri, boolean looping, int streamType) { + if (LOGD) Slog.d(TAG, "playAsync(uri=" + uri + ")"); + if (Binder.getCallingUid() != Process.SYSTEM_UID) { + throw new SecurityException("Async playback only available from system UID."); + } + mAsyncPlayer.play(mContext, uri, looping, streamType); + } + + @Override + public void stopAsync() { + if (LOGD) Slog.d(TAG, "stopAsync()"); + if (Binder.getCallingUid() != Process.SYSTEM_UID) { + throw new SecurityException("Async playback only available from system UID."); + } + mAsyncPlayer.stop(); + } + }; + + @Override + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.println("Clients:"); + synchronized (mClients) { + for (Client client : mClients.values()) { + pw.print(" mToken="); + pw.print(client.mToken); + pw.print(" mUri="); + pw.println(client.mRingtone.getUri()); + } + } + } +} |