1/*
2 * Copyright (C) 2014 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
17package android.media.session;
18
19import android.app.PendingIntent;
20import android.app.PendingIntent.CanceledException;
21import android.content.ComponentName;
22import android.content.Context;
23import android.content.Intent;
24import android.graphics.Bitmap;
25import android.graphics.Canvas;
26import android.graphics.Paint;
27import android.graphics.RectF;
28import android.media.AudioManager;
29import android.media.MediaMetadata;
30import android.media.MediaMetadataEditor;
31import android.media.MediaMetadataRetriever;
32import android.media.Rating;
33import android.media.RemoteControlClient;
34import android.media.RemoteControlClient.MetadataEditor;
35import android.os.Bundle;
36import android.os.Handler;
37import android.os.Looper;
38import android.os.RemoteException;
39import android.util.ArrayMap;
40import android.util.Log;
41import android.view.KeyEvent;
42
43/**
44 * Helper for connecting existing APIs up to the new session APIs. This can be
45 * used by RCC, AudioFocus, etc. to create a single session that translates to
46 * all those components.
47 *
48 * @hide
49 */
50public class MediaSessionLegacyHelper {
51    private static final String TAG = "MediaSessionHelper";
52    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
53
54    private static final Object sLock = new Object();
55    private static MediaSessionLegacyHelper sInstance;
56
57    private Context mContext;
58    private MediaSessionManager mSessionManager;
59    private Handler mHandler = new Handler(Looper.getMainLooper());
60    // The legacy APIs use PendingIntents to register/unregister media button
61    // receivers and these are associated with RCC.
62    private ArrayMap<PendingIntent, SessionHolder> mSessions
63            = new ArrayMap<PendingIntent, SessionHolder>();
64
65    private MediaSessionLegacyHelper(Context context) {
66        mContext = context;
67        mSessionManager = (MediaSessionManager) context
68                .getSystemService(Context.MEDIA_SESSION_SERVICE);
69    }
70
71    public static MediaSessionLegacyHelper getHelper(Context context) {
72        synchronized (sLock) {
73            if (sInstance == null) {
74                sInstance = new MediaSessionLegacyHelper(context.getApplicationContext());
75            }
76        }
77        return sInstance;
78    }
79
80    public static Bundle getOldMetadata(MediaMetadata metadata, int artworkWidth,
81            int artworkHeight) {
82        boolean includeArtwork = artworkWidth != -1 && artworkHeight != -1;
83        Bundle oldMetadata = new Bundle();
84        if (metadata.containsKey(MediaMetadata.METADATA_KEY_ALBUM)) {
85            oldMetadata.putString(String.valueOf(MediaMetadataRetriever.METADATA_KEY_ALBUM),
86                    metadata.getString(MediaMetadata.METADATA_KEY_ALBUM));
87        }
88        if (includeArtwork && metadata.containsKey(MediaMetadata.METADATA_KEY_ART)) {
89            Bitmap art = metadata.getBitmap(MediaMetadata.METADATA_KEY_ART);
90            oldMetadata.putParcelable(String.valueOf(MediaMetadataEditor.BITMAP_KEY_ARTWORK),
91                    scaleBitmapIfTooBig(art, artworkWidth, artworkHeight));
92        } else if (includeArtwork && metadata.containsKey(MediaMetadata.METADATA_KEY_ALBUM_ART)) {
93            // Fall back to album art if the track art wasn't available
94            Bitmap art = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART);
95            oldMetadata.putParcelable(String.valueOf(MediaMetadataEditor.BITMAP_KEY_ARTWORK),
96                    scaleBitmapIfTooBig(art, artworkWidth, artworkHeight));
97        }
98        if (metadata.containsKey(MediaMetadata.METADATA_KEY_ALBUM_ARTIST)) {
99            oldMetadata.putString(String.valueOf(MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST),
100                    metadata.getString(MediaMetadata.METADATA_KEY_ALBUM_ARTIST));
101        }
102        if (metadata.containsKey(MediaMetadata.METADATA_KEY_ARTIST)) {
103            oldMetadata.putString(String.valueOf(MediaMetadataRetriever.METADATA_KEY_ARTIST),
104                    metadata.getString(MediaMetadata.METADATA_KEY_ARTIST));
105        }
106        if (metadata.containsKey(MediaMetadata.METADATA_KEY_AUTHOR)) {
107            oldMetadata.putString(String.valueOf(MediaMetadataRetriever.METADATA_KEY_AUTHOR),
108                    metadata.getString(MediaMetadata.METADATA_KEY_AUTHOR));
109        }
110        if (metadata.containsKey(MediaMetadata.METADATA_KEY_COMPILATION)) {
111            oldMetadata.putString(String.valueOf(MediaMetadataRetriever.METADATA_KEY_COMPILATION),
112                    metadata.getString(MediaMetadata.METADATA_KEY_COMPILATION));
113        }
114        if (metadata.containsKey(MediaMetadata.METADATA_KEY_COMPOSER)) {
115            oldMetadata.putString(String.valueOf(MediaMetadataRetriever.METADATA_KEY_COMPOSER),
116                    metadata.getString(MediaMetadata.METADATA_KEY_COMPOSER));
117        }
118        if (metadata.containsKey(MediaMetadata.METADATA_KEY_DATE)) {
119            oldMetadata.putString(String.valueOf(MediaMetadataRetriever.METADATA_KEY_DATE),
120                    metadata.getString(MediaMetadata.METADATA_KEY_DATE));
121        }
122        if (metadata.containsKey(MediaMetadata.METADATA_KEY_DISC_NUMBER)) {
123            oldMetadata.putLong(String.valueOf(MediaMetadataRetriever.METADATA_KEY_DISC_NUMBER),
124                    metadata.getLong(MediaMetadata.METADATA_KEY_DISC_NUMBER));
125        }
126        if (metadata.containsKey(MediaMetadata.METADATA_KEY_DURATION)) {
127            oldMetadata.putLong(String.valueOf(MediaMetadataRetriever.METADATA_KEY_DURATION),
128                    metadata.getLong(MediaMetadata.METADATA_KEY_DURATION));
129        }
130        if (metadata.containsKey(MediaMetadata.METADATA_KEY_GENRE)) {
131            oldMetadata.putString(String.valueOf(MediaMetadataRetriever.METADATA_KEY_GENRE),
132                    metadata.getString(MediaMetadata.METADATA_KEY_GENRE));
133        }
134        if (metadata.containsKey(MediaMetadata.METADATA_KEY_NUM_TRACKS)) {
135            oldMetadata.putLong(String.valueOf(MediaMetadataRetriever.METADATA_KEY_NUM_TRACKS),
136                    metadata.getLong(MediaMetadata.METADATA_KEY_NUM_TRACKS));
137        }
138        if (metadata.containsKey(MediaMetadata.METADATA_KEY_RATING)) {
139            oldMetadata.putParcelable(String.valueOf(MediaMetadataEditor.RATING_KEY_BY_OTHERS),
140                    metadata.getRating(MediaMetadata.METADATA_KEY_RATING));
141        }
142        if (metadata.containsKey(MediaMetadata.METADATA_KEY_USER_RATING)) {
143            oldMetadata.putParcelable(String.valueOf(MediaMetadataEditor.RATING_KEY_BY_USER),
144                    metadata.getRating(MediaMetadata.METADATA_KEY_USER_RATING));
145        }
146        if (metadata.containsKey(MediaMetadata.METADATA_KEY_TITLE)) {
147            oldMetadata.putString(String.valueOf(MediaMetadataRetriever.METADATA_KEY_TITLE),
148                    metadata.getString(MediaMetadata.METADATA_KEY_TITLE));
149        }
150        if (metadata.containsKey(MediaMetadata.METADATA_KEY_TRACK_NUMBER)) {
151            oldMetadata.putLong(
152                    String.valueOf(MediaMetadataRetriever.METADATA_KEY_CD_TRACK_NUMBER),
153                    metadata.getLong(MediaMetadata.METADATA_KEY_TRACK_NUMBER));
154        }
155        if (metadata.containsKey(MediaMetadata.METADATA_KEY_WRITER)) {
156            oldMetadata.putString(String.valueOf(MediaMetadataRetriever.METADATA_KEY_WRITER),
157                    metadata.getString(MediaMetadata.METADATA_KEY_WRITER));
158        }
159        if (metadata.containsKey(MediaMetadata.METADATA_KEY_YEAR)) {
160            oldMetadata.putString(String.valueOf(MediaMetadataRetriever.METADATA_KEY_YEAR),
161                    metadata.getString(MediaMetadata.METADATA_KEY_YEAR));
162        }
163        return oldMetadata;
164    }
165
166    public MediaSession getSession(PendingIntent pi) {
167        SessionHolder holder = mSessions.get(pi);
168        return holder == null ? null : holder.mSession;
169    }
170
171    public void sendMediaButtonEvent(KeyEvent keyEvent, boolean needWakeLock) {
172        if (keyEvent == null) {
173            Log.w(TAG, "Tried to send a null key event. Ignoring.");
174            return;
175        }
176        mSessionManager.dispatchMediaKeyEvent(keyEvent, needWakeLock);
177        if (DEBUG) {
178            Log.d(TAG, "dispatched media key " + keyEvent);
179        }
180    }
181
182    public void sendVolumeKeyEvent(KeyEvent keyEvent, boolean musicOnly) {
183        if (keyEvent == null) {
184            Log.w(TAG, "Tried to send a null key event. Ignoring.");
185            return;
186        }
187        boolean down = keyEvent.getAction() == KeyEvent.ACTION_DOWN;
188        boolean up = keyEvent.getAction() == KeyEvent.ACTION_UP;
189        int direction = 0;
190        boolean isMute = false;
191        switch (keyEvent.getKeyCode()) {
192            case KeyEvent.KEYCODE_VOLUME_UP:
193                direction = AudioManager.ADJUST_RAISE;
194                break;
195            case KeyEvent.KEYCODE_VOLUME_DOWN:
196                direction = AudioManager.ADJUST_LOWER;
197                break;
198            case KeyEvent.KEYCODE_VOLUME_MUTE:
199                isMute = true;
200                break;
201        }
202        if (down || up) {
203            int flags;
204            if (musicOnly) {
205                // This flag is used when the screen is off to only affect
206                // active media
207                flags = AudioManager.FLAG_ACTIVE_MEDIA_ONLY;
208            } else {
209                // These flags are consistent with the home screen
210                if (up) {
211                    flags = AudioManager.FLAG_PLAY_SOUND | AudioManager.FLAG_VIBRATE;
212                } else {
213                    flags = AudioManager.FLAG_SHOW_UI | AudioManager.FLAG_VIBRATE;
214                }
215            }
216            if (direction != 0) {
217                // If this is action up we want to send a beep for non-music events
218                if (up) {
219                    direction = 0;
220                }
221                mSessionManager.dispatchAdjustVolume(AudioManager.USE_DEFAULT_STREAM_TYPE,
222                        direction, flags);
223            } else if (isMute) {
224                if (down) {
225                    // We need to send two volume events on down, one to mute
226                    // and one to show the UI
227                    mSessionManager.dispatchAdjustVolume(AudioManager.USE_DEFAULT_STREAM_TYPE,
228                            MediaSessionManager.DIRECTION_MUTE, flags);
229                }
230                mSessionManager.dispatchAdjustVolume(AudioManager.USE_DEFAULT_STREAM_TYPE,
231                        0 /* direction, causes UI to show on down */, flags);
232            }
233        }
234    }
235
236    public void sendAdjustVolumeBy(int suggestedStream, int delta, int flags) {
237        mSessionManager.dispatchAdjustVolume(suggestedStream, delta, flags);
238        if (DEBUG) {
239            Log.d(TAG, "dispatched volume adjustment");
240        }
241    }
242
243    public boolean isGlobalPriorityActive() {
244        return mSessionManager.isGlobalPriorityActive();
245    }
246
247    public void addRccListener(PendingIntent pi, MediaSession.Callback listener) {
248        if (pi == null) {
249            Log.w(TAG, "Pending intent was null, can't add rcc listener.");
250            return;
251        }
252        SessionHolder holder = getHolder(pi, true);
253        if (holder == null) {
254            return;
255        }
256        if (holder.mRccListener != null) {
257            if (holder.mRccListener == listener) {
258                if (DEBUG) {
259                    Log.d(TAG, "addRccListener listener already added.");
260                }
261                // This is already the registered listener, ignore
262                return;
263            }
264        }
265        holder.mRccListener = listener;
266        holder.mFlags |= MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS;
267        holder.mSession.setFlags(holder.mFlags);
268        holder.update();
269        if (DEBUG) {
270            Log.d(TAG, "Added rcc listener for " + pi + ".");
271        }
272    }
273
274    public void removeRccListener(PendingIntent pi) {
275        if (pi == null) {
276            return;
277        }
278        SessionHolder holder = getHolder(pi, false);
279        if (holder != null && holder.mRccListener != null) {
280            holder.mRccListener = null;
281            holder.mFlags &= ~MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS;
282            holder.mSession.setFlags(holder.mFlags);
283            holder.update();
284            if (DEBUG) {
285                Log.d(TAG, "Removed rcc listener for " + pi + ".");
286            }
287        }
288    }
289
290    public void addMediaButtonListener(PendingIntent pi, ComponentName mbrComponent,
291            Context context) {
292        if (pi == null) {
293            Log.w(TAG, "Pending intent was null, can't addMediaButtonListener.");
294            return;
295        }
296        SessionHolder holder = getHolder(pi, true);
297        if (holder == null) {
298            return;
299        }
300        if (holder.mMediaButtonListener != null) {
301            // Already have this listener registered
302            if (DEBUG) {
303                Log.d(TAG, "addMediaButtonListener already added " + pi);
304            }
305        }
306        holder.mMediaButtonListener = new MediaButtonListener(pi, context);
307        // TODO determine if handling transport performer commands should also
308        // set this flag
309        holder.mFlags |= MediaSession.FLAG_HANDLES_MEDIA_BUTTONS;
310        holder.mSession.setFlags(holder.mFlags);
311        holder.mSession.setMediaButtonReceiver(pi);
312        holder.update();
313        if (DEBUG) {
314            Log.d(TAG, "addMediaButtonListener added " + pi);
315        }
316    }
317
318    public void removeMediaButtonListener(PendingIntent pi) {
319        if (pi == null) {
320            return;
321        }
322        SessionHolder holder = getHolder(pi, false);
323        if (holder != null && holder.mMediaButtonListener != null) {
324            holder.mFlags &= ~MediaSession.FLAG_HANDLES_MEDIA_BUTTONS;
325            holder.mSession.setFlags(holder.mFlags);
326            holder.mMediaButtonListener = null;
327
328            holder.update();
329            if (DEBUG) {
330                Log.d(TAG, "removeMediaButtonListener removed " + pi);
331            }
332        }
333    }
334
335    /**
336     * Scale a bitmap to fit the smallest dimension by uniformly scaling the
337     * incoming bitmap. If the bitmap fits, then do nothing and return the
338     * original.
339     *
340     * @param bitmap
341     * @param maxWidth
342     * @param maxHeight
343     * @return
344     */
345    private static Bitmap scaleBitmapIfTooBig(Bitmap bitmap, int maxWidth, int maxHeight) {
346        if (bitmap != null) {
347            final int width = bitmap.getWidth();
348            final int height = bitmap.getHeight();
349            if (width > maxWidth || height > maxHeight) {
350                float scale = Math.min((float) maxWidth / width, (float) maxHeight / height);
351                int newWidth = Math.round(scale * width);
352                int newHeight = Math.round(scale * height);
353                Bitmap.Config newConfig = bitmap.getConfig();
354                if (newConfig == null) {
355                    newConfig = Bitmap.Config.ARGB_8888;
356                }
357                Bitmap outBitmap = Bitmap.createBitmap(newWidth, newHeight, newConfig);
358                Canvas canvas = new Canvas(outBitmap);
359                Paint paint = new Paint();
360                paint.setAntiAlias(true);
361                paint.setFilterBitmap(true);
362                canvas.drawBitmap(bitmap, null,
363                        new RectF(0, 0, outBitmap.getWidth(), outBitmap.getHeight()), paint);
364                bitmap = outBitmap;
365            }
366        }
367        return bitmap;
368    }
369
370    private SessionHolder getHolder(PendingIntent pi, boolean createIfMissing) {
371        SessionHolder holder = mSessions.get(pi);
372        if (holder == null && createIfMissing) {
373            MediaSession session;
374            session = new MediaSession(mContext, TAG + "-" + pi.getCreatorPackage());
375            session.setActive(true);
376            holder = new SessionHolder(session, pi);
377            mSessions.put(pi, holder);
378        }
379        return holder;
380    }
381
382    private static void sendKeyEvent(PendingIntent pi, Context context, Intent intent) {
383        try {
384            pi.send(context, 0, intent);
385        } catch (CanceledException e) {
386            Log.e(TAG, "Error sending media key down event:", e);
387            // Don't bother sending up if down failed
388            return;
389        }
390    }
391
392    private static final class MediaButtonListener extends MediaSession.Callback {
393        private final PendingIntent mPendingIntent;
394        private final Context mContext;
395
396        public MediaButtonListener(PendingIntent pi, Context context) {
397            mPendingIntent = pi;
398            mContext = context;
399        }
400
401        @Override
402        public boolean onMediaButtonEvent(Intent mediaButtonIntent) {
403            MediaSessionLegacyHelper.sendKeyEvent(mPendingIntent, mContext, mediaButtonIntent);
404            return true;
405        }
406
407        @Override
408        public void onPlay() {
409            sendKeyEvent(KeyEvent.KEYCODE_MEDIA_PLAY);
410        }
411
412        @Override
413        public void onPause() {
414            sendKeyEvent(KeyEvent.KEYCODE_MEDIA_PAUSE);
415        }
416
417        @Override
418        public void onSkipToNext() {
419            sendKeyEvent(KeyEvent.KEYCODE_MEDIA_NEXT);
420        }
421
422        @Override
423        public void onSkipToPrevious() {
424            sendKeyEvent(KeyEvent.KEYCODE_MEDIA_PREVIOUS);
425        }
426
427        @Override
428        public void onFastForward() {
429            sendKeyEvent(KeyEvent.KEYCODE_MEDIA_FAST_FORWARD);
430        }
431
432        @Override
433        public void onRewind() {
434            sendKeyEvent(KeyEvent.KEYCODE_MEDIA_REWIND);
435        }
436
437        @Override
438        public void onStop() {
439            sendKeyEvent(KeyEvent.KEYCODE_MEDIA_STOP);
440        }
441
442        private void sendKeyEvent(int keyCode) {
443            KeyEvent ke = new KeyEvent(KeyEvent.ACTION_DOWN, keyCode);
444            Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON);
445            intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
446
447            intent.putExtra(Intent.EXTRA_KEY_EVENT, ke);
448            MediaSessionLegacyHelper.sendKeyEvent(mPendingIntent, mContext, intent);
449
450            ke = new KeyEvent(KeyEvent.ACTION_UP, keyCode);
451            intent.putExtra(Intent.EXTRA_KEY_EVENT, ke);
452            MediaSessionLegacyHelper.sendKeyEvent(mPendingIntent, mContext, intent);
453
454            if (DEBUG) {
455                Log.d(TAG, "Sent " + keyCode + " to pending intent " + mPendingIntent);
456            }
457        }
458    }
459
460    private class SessionHolder {
461        public final MediaSession mSession;
462        public final PendingIntent mPi;
463        public MediaButtonListener mMediaButtonListener;
464        public MediaSession.Callback mRccListener;
465        public int mFlags;
466
467        public SessionCallback mCb;
468
469        public SessionHolder(MediaSession session, PendingIntent pi) {
470            mSession = session;
471            mPi = pi;
472        }
473
474        public void update() {
475            if (mMediaButtonListener == null && mRccListener == null) {
476                mSession.setCallback(null);
477                mSession.release();
478                mCb = null;
479                mSessions.remove(mPi);
480            } else if (mCb == null) {
481                mCb = new SessionCallback();
482                Handler handler = new Handler(Looper.getMainLooper());
483                mSession.setCallback(mCb, handler);
484            }
485        }
486
487        private class SessionCallback extends MediaSession.Callback {
488
489            @Override
490            public boolean onMediaButtonEvent(Intent mediaButtonIntent) {
491                if (mMediaButtonListener != null) {
492                    mMediaButtonListener.onMediaButtonEvent(mediaButtonIntent);
493                }
494                return true;
495            }
496
497            @Override
498            public void onPlay() {
499                if (mMediaButtonListener != null) {
500                    mMediaButtonListener.onPlay();
501                }
502            }
503
504            @Override
505            public void onPause() {
506                if (mMediaButtonListener != null) {
507                    mMediaButtonListener.onPause();
508                }
509            }
510
511            @Override
512            public void onSkipToNext() {
513                if (mMediaButtonListener != null) {
514                    mMediaButtonListener.onSkipToNext();
515                }
516            }
517
518            @Override
519            public void onSkipToPrevious() {
520                if (mMediaButtonListener != null) {
521                    mMediaButtonListener.onSkipToPrevious();
522                }
523            }
524
525            @Override
526            public void onFastForward() {
527                if (mMediaButtonListener != null) {
528                    mMediaButtonListener.onFastForward();
529                }
530            }
531
532            @Override
533            public void onRewind() {
534                if (mMediaButtonListener != null) {
535                    mMediaButtonListener.onRewind();
536                }
537            }
538
539            @Override
540            public void onStop() {
541                if (mMediaButtonListener != null) {
542                    mMediaButtonListener.onStop();
543                }
544            }
545
546            @Override
547            public void onSeekTo(long pos) {
548                if (mRccListener != null) {
549                    mRccListener.onSeekTo(pos);
550                }
551            }
552
553            @Override
554            public void onSetRating(Rating rating) {
555                if (mRccListener != null) {
556                    mRccListener.onSetRating(rating);
557                }
558            }
559        }
560    }
561}
562