Home | History | Annotate | Download | only in common
      1 /*
      2  * Copyright 2018 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 com.android.car.media.common;
     18 
     19 import android.annotation.Nullable;
     20 import android.content.Context;
     21 import android.content.SharedPreferences;
     22 import android.media.session.MediaController;
     23 import android.media.session.MediaSessionManager;
     24 import android.media.session.PlaybackState;
     25 import android.os.Handler;
     26 import android.util.Log;
     27 
     28 import java.util.ArrayList;
     29 import java.util.List;
     30 import java.util.function.Consumer;
     31 
     32 /**
     33  * This is an abstractions over {@link MediaSessionManager} that provides information about the
     34  * currently "active" media session.
     35  * <p>
     36  * It automatically determines the foreground media app (the one that would normally
     37  * receive playback events) and exposes metadata and events from such app, or when a different app
     38  * becomes foreground.
     39  * <p>
     40  * This requires the android.Manifest.permission.MEDIA_CONTENT_CONTROL permission to be held by the
     41  * calling app.
     42  */
     43 public class ActiveMediaSourceManager {
     44     private static final String TAG = "ActiveSourceManager";
     45 
     46     private static final String PLAYBACK_MODEL_SHARED_PREFS =
     47             "com.android.car.media.PLAYBACK_MODEL";
     48     private static final String PLAYBACK_MODEL_ACTIVE_PACKAGE_NAME_KEY =
     49             "active_packagename";
     50 
     51     private final MediaSessionManager mMediaSessionManager;
     52     private final Handler mHandler = new Handler();
     53     private final Context mContext;
     54     private final List<Observer> mObservers = new ArrayList<>();
     55     private final MediaSessionUpdater mMediaSessionUpdater = new MediaSessionUpdater();
     56     private final SharedPreferences mSharedPreferences;
     57     @Nullable
     58     private MediaController mMediaController;
     59     private boolean mIsStarted;
     60 
     61     /**
     62      * Temporary work-around to bug b/76017849.
     63      * MediaSessionManager is not notifying media session priority changes.
     64      * As a work-around we subscribe to playback state changes on all controllers to detect
     65      * potential priority changes.
     66      * This might cause a few unnecessary checks, but selecting the top-most controller is a
     67      * cheap operation.
     68      */
     69     private class MediaSessionUpdater {
     70         private List<MediaController> mControllers = new ArrayList<>();
     71 
     72         private MediaController.Callback mCallback = new MediaController.Callback() {
     73             @Override
     74             public void onPlaybackStateChanged(PlaybackState state) {
     75                 selectMediaController(mMediaSessionManager.getActiveSessions(null));
     76             }
     77 
     78             @Override
     79             public void onSessionDestroyed() {
     80                 selectMediaController(mMediaSessionManager.getActiveSessions(null));
     81             }
     82         };
     83 
     84         void setControllersByPackageName(List<MediaController> newControllers) {
     85             for (MediaController oldController : mControllers) {
     86                 oldController.unregisterCallback(mCallback);
     87             }
     88             for (MediaController newController : newControllers) {
     89                 newController.registerCallback(mCallback);
     90             }
     91             mControllers.clear();
     92             mControllers.addAll(newControllers);
     93         }
     94     }
     95 
     96     /**
     97      * An observer of this model
     98      */
     99     public interface Observer {
    100         /**
    101          * Called when the top source media app changes.
    102          */
    103         void onActiveSourceChanged();
    104     }
    105 
    106     private MediaSessionManager.OnActiveSessionsChangedListener mSessionChangeListener =
    107             this::selectMediaController;
    108 
    109     /**
    110      * Creates a {@link ActiveMediaSourceManager}. This instance is going to be inactive until
    111      * {@link #start()} method is invoked.
    112      */
    113     public ActiveMediaSourceManager(Context context) {
    114         mContext = context;
    115         mMediaSessionManager = mContext.getSystemService(MediaSessionManager.class);
    116         mSharedPreferences = mContext.getSharedPreferences(PLAYBACK_MODEL_SHARED_PREFS,
    117                 Context.MODE_PRIVATE);
    118     }
    119 
    120     /**
    121      * Selects one of the provided controllers as the "currently playing" one.
    122      */
    123     private void selectMediaController(List<MediaController> controllers) {
    124         if (Log.isLoggable(TAG, Log.DEBUG)) {
    125             dump("Selecting a media controller from: ", controllers);
    126         }
    127         changeMediaController(getTopMostController(controllers));
    128         mMediaSessionUpdater.setControllersByPackageName(controllers);
    129     }
    130 
    131     private void dump(String title, List<MediaController> controllers) {
    132         Log.d(TAG, title + " (total: " + controllers.size() + ")");
    133         for (MediaController controller : controllers) {
    134             String stateName = getStateName(controller.getPlaybackState() != null
    135                     ? controller.getPlaybackState().getState()
    136                     : PlaybackState.STATE_NONE);
    137             Log.d(TAG, String.format("\t%s: %s",
    138                     controller.getPackageName(),
    139                     stateName));
    140         }
    141     }
    142 
    143     private String getStateName(@PlaybackState.State int state) {
    144         switch (state) {
    145             case PlaybackState.STATE_NONE:
    146                 return "NONE";
    147             case PlaybackState.STATE_STOPPED:
    148                 return "STOPPED";
    149             case PlaybackState.STATE_PAUSED:
    150                 return "PAUSED";
    151             case PlaybackState.STATE_PLAYING:
    152                 return "PLAYING";
    153             case PlaybackState.STATE_FAST_FORWARDING:
    154                 return "FORWARDING";
    155             case PlaybackState.STATE_REWINDING:
    156                 return "REWINDING";
    157             case PlaybackState.STATE_BUFFERING:
    158                 return "BUFFERING";
    159             case PlaybackState.STATE_ERROR:
    160                 return "ERROR";
    161             case PlaybackState.STATE_CONNECTING:
    162                 return "CONNECTING";
    163             case PlaybackState.STATE_SKIPPING_TO_PREVIOUS:
    164                 return "SKIPPING_TO_PREVIOUS";
    165             case PlaybackState.STATE_SKIPPING_TO_NEXT:
    166                 return "SKIPPING_TO_NEXT";
    167             case PlaybackState.STATE_SKIPPING_TO_QUEUE_ITEM:
    168                 return "SKIPPING_TO_QUEUE_ITEM";
    169             default:
    170                 return "UNKNOWN";
    171         }
    172     }
    173 
    174     /**
    175      * @return the controller most likely to be the currently active one, out of the list of
    176      * active controllers repoted by {@link MediaSessionManager}. It does so by picking the first
    177      * one (in order of priority) which an active state as reported by
    178      * {@link MediaController#getPlaybackState()}
    179      */
    180     private MediaController getTopMostController(List<MediaController> controllers) {
    181         if (controllers != null && controllers.size() > 0) {
    182             for (MediaController candidate : controllers) {
    183                 @PlaybackState.State int state = candidate.getPlaybackState() != null
    184                         ? candidate.getPlaybackState().getState()
    185                         : PlaybackState.STATE_NONE;
    186                 if (state == PlaybackState.STATE_BUFFERING
    187                         || state == PlaybackState.STATE_CONNECTING
    188                         || state == PlaybackState.STATE_FAST_FORWARDING
    189                         || state == PlaybackState.STATE_PLAYING
    190                         || state == PlaybackState.STATE_REWINDING
    191                         || state == PlaybackState.STATE_SKIPPING_TO_NEXT
    192                         || state == PlaybackState.STATE_SKIPPING_TO_PREVIOUS
    193                         || state == PlaybackState.STATE_SKIPPING_TO_QUEUE_ITEM) {
    194                     return candidate;
    195                 }
    196             }
    197             // If no source is active, we go for the last known source
    198             String packageName = getLastKnownActivePackageName();
    199             if (packageName != null) {
    200                 for (MediaController candidate : controllers) {
    201                     if (candidate.getPackageName().equals(packageName)) {
    202                         return candidate;
    203                     }
    204                 }
    205             }
    206             return controllers.get(0);
    207         }
    208         return null;
    209     }
    210 
    211     private void changeMediaController(MediaController mediaController) {
    212         if (Log.isLoggable(TAG, Log.DEBUG)) {
    213             Log.d(TAG, "New media controller: " + (mediaController != null
    214                     ? mediaController.getPackageName() : null));
    215         }
    216         if ((mediaController == null && mMediaController == null)
    217                 || (mediaController != null && mMediaController != null
    218                 && mediaController.getPackageName().equals(mMediaController.getPackageName()))) {
    219             // If no change, do nothing.
    220             return;
    221         }
    222         mMediaController = mediaController;
    223         setLastKnownActivePackageName(mMediaController != null
    224                 ? mMediaController.getPackageName()
    225                 : null);
    226         notify(Observer::onActiveSourceChanged);
    227     }
    228 
    229     /**
    230      * Starts following changes on the list of active media sources. If any changes happen, all
    231      * observers registered through {@link #registerObserver(Observer)} will be notified.
    232      * <p>
    233      * Calling this method might cause an immediate {@link Observer#onActiveSourceChanged()}
    234      * event in case the current media source is different than the last known one.
    235      */
    236     private void start() {
    237         mMediaSessionManager.addOnActiveSessionsChangedListener(mSessionChangeListener, null);
    238         selectMediaController(mMediaSessionManager.getActiveSessions(null));
    239         mIsStarted = true;
    240     }
    241 
    242     /**
    243      * Stops following changes on the list of active media sources. This method could cause an
    244      * immediate {@link PlaybackModel.PlaybackObserver#onSourceChanged()} event if a media source
    245      * was already connected.
    246      */
    247     private void stop() {
    248         mMediaSessionUpdater.setControllersByPackageName(new ArrayList<>());
    249         mMediaSessionManager.removeOnActiveSessionsChangedListener(mSessionChangeListener);
    250         changeMediaController(null);
    251         mIsStarted = false;
    252     }
    253 
    254     private void notify(Consumer<Observer> notification) {
    255         mHandler.post(() -> {
    256             List<Observer> observers = new ArrayList<>(mObservers);
    257             for (Observer observer : observers) {
    258                 notification.accept(observer);
    259             }
    260         });
    261     }
    262 
    263     /**
    264      * @return a {@link MediaController} providing access to metadata of the currently playing media
    265      * source, or NULL if no media source has an active session. Changes on this value will
    266      * be notified through {@link Observer#onActiveSourceChanged()}
    267      */
    268     @Nullable
    269     public MediaController getMediaController() {
    270         return mIsStarted
    271                 ? mMediaController
    272                 : getTopMostController(mMediaSessionManager.getActiveSessions(null));
    273     }
    274 
    275     /**
    276      * Registers an observer to be notified of media events. If the model is not started yet it
    277      * will start right away. If the model was already started, the observer will receive an
    278      * immediate {@link Observer#onActiveSourceChanged()} event.
    279      */
    280     public void registerObserver(Observer observer) {
    281         mObservers.add(observer);
    282         if (!mIsStarted) {
    283             start();
    284         } else {
    285             observer.onActiveSourceChanged();
    286         }
    287     }
    288 
    289     /**
    290      * Unregisters an observer previously registered using
    291      * {@link #registerObserver(Observer)}. There are no other observers the model will
    292      * stop tracking changes right away.
    293      */
    294     public void unregisterObserver(Observer observer) {
    295         mObservers.remove(observer);
    296         if (mObservers.isEmpty() && mIsStarted) {
    297             stop();
    298         }
    299     }
    300 
    301     private String getLastKnownActivePackageName() {
    302         return mSharedPreferences.getString(PLAYBACK_MODEL_ACTIVE_PACKAGE_NAME_KEY, null);
    303     }
    304 
    305     private void setLastKnownActivePackageName(String packageName) {
    306         mSharedPreferences.edit()
    307                 .putString(PLAYBACK_MODEL_ACTIVE_PACKAGE_NAME_KEY, packageName)
    308                 .apply();
    309     }
    310 
    311     /**
    312      * Returns the {@link MediaController} corresponding to the given package name, or NULL if
    313      * no active session exists for it.
    314      */
    315     public @Nullable MediaController getControllerForPackage(String packageName) {
    316         List<MediaController> controllers = mMediaSessionManager.getActiveSessions(null);
    317         for (MediaController controller : controllers) {
    318             if (controller.getPackageName().equals(packageName)) {
    319                 return controller;
    320             }
    321         }
    322         return null;
    323     }
    324 
    325     /**
    326      * Returns true if the given package name corresponds to the top most media source.
    327      */
    328     public boolean isPlaying(String packageName) {
    329         return mMediaController != null && mMediaController.getPackageName().equals(packageName);
    330     }
    331 }
    332