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