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, int stream, boolean musicOnly) {
180        if (keyEvent == null) {
181            Log.w(TAG, "Tried to send a null key event. Ignoring.");
182            return;
183        }
184        mSessionManager.dispatchVolumeKeyEvent(keyEvent, stream, musicOnly);
185    }
186
187    public void sendAdjustVolumeBy(int suggestedStream, int delta, int flags) {
188        mSessionManager.dispatchAdjustVolume(suggestedStream, delta, flags);
189        if (DEBUG) {
190            Log.d(TAG, "dispatched volume adjustment");
191        }
192    }
193
194    public boolean isGlobalPriorityActive() {
195        return mSessionManager.isGlobalPriorityActive();
196    }
197
198    public void addRccListener(PendingIntent pi, MediaSession.Callback listener) {
199        if (pi == null) {
200            Log.w(TAG, "Pending intent was null, can't add rcc listener.");
201            return;
202        }
203        SessionHolder holder = getHolder(pi, true);
204        if (holder == null) {
205            return;
206        }
207        if (holder.mRccListener != null) {
208            if (holder.mRccListener == listener) {
209                if (DEBUG) {
210                    Log.d(TAG, "addRccListener listener already added.");
211                }
212                // This is already the registered listener, ignore
213                return;
214            }
215        }
216        holder.mRccListener = listener;
217        holder.mFlags |= MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS;
218        holder.mSession.setFlags(holder.mFlags);
219        holder.update();
220        if (DEBUG) {
221            Log.d(TAG, "Added rcc listener for " + pi + ".");
222        }
223    }
224
225    public void removeRccListener(PendingIntent pi) {
226        if (pi == null) {
227            return;
228        }
229        SessionHolder holder = getHolder(pi, false);
230        if (holder != null && holder.mRccListener != null) {
231            holder.mRccListener = null;
232            holder.mFlags &= ~MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS;
233            holder.mSession.setFlags(holder.mFlags);
234            holder.update();
235            if (DEBUG) {
236                Log.d(TAG, "Removed rcc listener for " + pi + ".");
237            }
238        }
239    }
240
241    public void addMediaButtonListener(PendingIntent pi, ComponentName mbrComponent,
242            Context context) {
243        if (pi == null) {
244            Log.w(TAG, "Pending intent was null, can't addMediaButtonListener.");
245            return;
246        }
247        SessionHolder holder = getHolder(pi, true);
248        if (holder == null) {
249            return;
250        }
251        if (holder.mMediaButtonListener != null) {
252            // Already have this listener registered
253            if (DEBUG) {
254                Log.d(TAG, "addMediaButtonListener already added " + pi);
255            }
256        }
257        holder.mMediaButtonListener = new MediaButtonListener(pi, context);
258        // TODO determine if handling transport performer commands should also
259        // set this flag
260        holder.mFlags |= MediaSession.FLAG_HANDLES_MEDIA_BUTTONS;
261        holder.mSession.setFlags(holder.mFlags);
262        holder.mSession.setMediaButtonReceiver(pi);
263        holder.update();
264        if (DEBUG) {
265            Log.d(TAG, "addMediaButtonListener added " + pi);
266        }
267    }
268
269    public void removeMediaButtonListener(PendingIntent pi) {
270        if (pi == null) {
271            return;
272        }
273        SessionHolder holder = getHolder(pi, false);
274        if (holder != null && holder.mMediaButtonListener != null) {
275            holder.mFlags &= ~MediaSession.FLAG_HANDLES_MEDIA_BUTTONS;
276            holder.mSession.setFlags(holder.mFlags);
277            holder.mMediaButtonListener = null;
278
279            holder.update();
280            if (DEBUG) {
281                Log.d(TAG, "removeMediaButtonListener removed " + pi);
282            }
283        }
284    }
285
286    /**
287     * Scale a bitmap to fit the smallest dimension by uniformly scaling the
288     * incoming bitmap. If the bitmap fits, then do nothing and return the
289     * original.
290     *
291     * @param bitmap
292     * @param maxWidth
293     * @param maxHeight
294     * @return
295     */
296    private static Bitmap scaleBitmapIfTooBig(Bitmap bitmap, int maxWidth, int maxHeight) {
297        if (bitmap != null) {
298            final int width = bitmap.getWidth();
299            final int height = bitmap.getHeight();
300            if (width > maxWidth || height > maxHeight) {
301                float scale = Math.min((float) maxWidth / width, (float) maxHeight / height);
302                int newWidth = Math.round(scale * width);
303                int newHeight = Math.round(scale * height);
304                Bitmap.Config newConfig = bitmap.getConfig();
305                if (newConfig == null) {
306                    newConfig = Bitmap.Config.ARGB_8888;
307                }
308                Bitmap outBitmap = Bitmap.createBitmap(newWidth, newHeight, newConfig);
309                Canvas canvas = new Canvas(outBitmap);
310                Paint paint = new Paint();
311                paint.setAntiAlias(true);
312                paint.setFilterBitmap(true);
313                canvas.drawBitmap(bitmap, null,
314                        new RectF(0, 0, outBitmap.getWidth(), outBitmap.getHeight()), paint);
315                bitmap = outBitmap;
316            }
317        }
318        return bitmap;
319    }
320
321    private SessionHolder getHolder(PendingIntent pi, boolean createIfMissing) {
322        SessionHolder holder = mSessions.get(pi);
323        if (holder == null && createIfMissing) {
324            MediaSession session;
325            session = new MediaSession(mContext, TAG + "-" + pi.getCreatorPackage());
326            session.setActive(true);
327            holder = new SessionHolder(session, pi);
328            mSessions.put(pi, holder);
329        }
330        return holder;
331    }
332
333    private static void sendKeyEvent(PendingIntent pi, Context context, Intent intent) {
334        try {
335            pi.send(context, 0, intent);
336        } catch (CanceledException e) {
337            Log.e(TAG, "Error sending media key down event:", e);
338            // Don't bother sending up if down failed
339            return;
340        }
341    }
342
343    private static final class MediaButtonListener extends MediaSession.Callback {
344        private final PendingIntent mPendingIntent;
345        private final Context mContext;
346
347        public MediaButtonListener(PendingIntent pi, Context context) {
348            mPendingIntent = pi;
349            mContext = context;
350        }
351
352        @Override
353        public boolean onMediaButtonEvent(Intent mediaButtonIntent) {
354            MediaSessionLegacyHelper.sendKeyEvent(mPendingIntent, mContext, mediaButtonIntent);
355            return true;
356        }
357
358        @Override
359        public void onPlay() {
360            sendKeyEvent(KeyEvent.KEYCODE_MEDIA_PLAY);
361        }
362
363        @Override
364        public void onPause() {
365            sendKeyEvent(KeyEvent.KEYCODE_MEDIA_PAUSE);
366        }
367
368        @Override
369        public void onSkipToNext() {
370            sendKeyEvent(KeyEvent.KEYCODE_MEDIA_NEXT);
371        }
372
373        @Override
374        public void onSkipToPrevious() {
375            sendKeyEvent(KeyEvent.KEYCODE_MEDIA_PREVIOUS);
376        }
377
378        @Override
379        public void onFastForward() {
380            sendKeyEvent(KeyEvent.KEYCODE_MEDIA_FAST_FORWARD);
381        }
382
383        @Override
384        public void onRewind() {
385            sendKeyEvent(KeyEvent.KEYCODE_MEDIA_REWIND);
386        }
387
388        @Override
389        public void onStop() {
390            sendKeyEvent(KeyEvent.KEYCODE_MEDIA_STOP);
391        }
392
393        private void sendKeyEvent(int keyCode) {
394            KeyEvent ke = new KeyEvent(KeyEvent.ACTION_DOWN, keyCode);
395            Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON);
396            intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
397
398            intent.putExtra(Intent.EXTRA_KEY_EVENT, ke);
399            MediaSessionLegacyHelper.sendKeyEvent(mPendingIntent, mContext, intent);
400
401            ke = new KeyEvent(KeyEvent.ACTION_UP, keyCode);
402            intent.putExtra(Intent.EXTRA_KEY_EVENT, ke);
403            MediaSessionLegacyHelper.sendKeyEvent(mPendingIntent, mContext, intent);
404
405            if (DEBUG) {
406                Log.d(TAG, "Sent " + keyCode + " to pending intent " + mPendingIntent);
407            }
408        }
409    }
410
411    private class SessionHolder {
412        public final MediaSession mSession;
413        public final PendingIntent mPi;
414        public MediaButtonListener mMediaButtonListener;
415        public MediaSession.Callback mRccListener;
416        public int mFlags;
417
418        public SessionCallback mCb;
419
420        public SessionHolder(MediaSession session, PendingIntent pi) {
421            mSession = session;
422            mPi = pi;
423        }
424
425        public void update() {
426            if (mMediaButtonListener == null && mRccListener == null) {
427                mSession.setCallback(null);
428                mSession.release();
429                mCb = null;
430                mSessions.remove(mPi);
431            } else if (mCb == null) {
432                mCb = new SessionCallback();
433                Handler handler = new Handler(Looper.getMainLooper());
434                mSession.setCallback(mCb, handler);
435            }
436        }
437
438        private class SessionCallback extends MediaSession.Callback {
439
440            @Override
441            public boolean onMediaButtonEvent(Intent mediaButtonIntent) {
442                if (mMediaButtonListener != null) {
443                    mMediaButtonListener.onMediaButtonEvent(mediaButtonIntent);
444                }
445                return true;
446            }
447
448            @Override
449            public void onPlay() {
450                if (mMediaButtonListener != null) {
451                    mMediaButtonListener.onPlay();
452                }
453            }
454
455            @Override
456            public void onPause() {
457                if (mMediaButtonListener != null) {
458                    mMediaButtonListener.onPause();
459                }
460            }
461
462            @Override
463            public void onSkipToNext() {
464                if (mMediaButtonListener != null) {
465                    mMediaButtonListener.onSkipToNext();
466                }
467            }
468
469            @Override
470            public void onSkipToPrevious() {
471                if (mMediaButtonListener != null) {
472                    mMediaButtonListener.onSkipToPrevious();
473                }
474            }
475
476            @Override
477            public void onFastForward() {
478                if (mMediaButtonListener != null) {
479                    mMediaButtonListener.onFastForward();
480                }
481            }
482
483            @Override
484            public void onRewind() {
485                if (mMediaButtonListener != null) {
486                    mMediaButtonListener.onRewind();
487                }
488            }
489
490            @Override
491            public void onStop() {
492                if (mMediaButtonListener != null) {
493                    mMediaButtonListener.onStop();
494                }
495            }
496
497            @Override
498            public void onSeekTo(long pos) {
499                if (mRccListener != null) {
500                    mRccListener.onSeekTo(pos);
501                }
502            }
503
504            @Override
505            public void onSetRating(Rating rating) {
506                if (mRccListener != null) {
507                    mRccListener.onSetRating(rating);
508                }
509            }
510        }
511    }
512}
513