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