Merge "Decoupling PlaybackModel from ActiveMediaSourceManager." into pi-dev
diff --git a/car-media-common/src/com/android/car/media/common/ActiveMediaSourceManager.java b/car-media-common/src/com/android/car/media/common/ActiveMediaSourceManager.java
new file mode 100644
index 0000000..6806748
--- /dev/null
+++ b/car-media-common/src/com/android/car/media/common/ActiveMediaSourceManager.java
@@ -0,0 +1,331 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.media.common;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.media.session.MediaController;
+import android.media.session.MediaSessionManager;
+import android.media.session.PlaybackState;
+import android.os.Handler;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+/**
+ * This is an abstractions over {@link MediaSessionManager} that provides information about the
+ * currently "active" media session.
+ * <p>
+ * It automatically determines the foreground media app (the one that would normally
+ * receive playback events) and exposes metadata and events from such app, or when a different app
+ * becomes foreground.
+ * <p>
+ * This requires the android.Manifest.permission.MEDIA_CONTENT_CONTROL permission to be held by the
+ * calling app.
+ */
+public class ActiveMediaSourceManager {
+    private static final String TAG = "ActiveSourceManager";
+
+    private static final String PLAYBACK_MODEL_SHARED_PREFS =
+            "com.android.car.media.PLAYBACK_MODEL";
+    private static final String PLAYBACK_MODEL_ACTIVE_PACKAGE_NAME_KEY =
+            "active_packagename";
+
+    private final MediaSessionManager mMediaSessionManager;
+    private final Handler mHandler = new Handler();
+    private final Context mContext;
+    private final List<Observer> mObservers = new ArrayList<>();
+    private final MediaSessionUpdater mMediaSessionUpdater = new MediaSessionUpdater();
+    private final SharedPreferences mSharedPreferences;
+    @Nullable
+    private MediaController mMediaController;
+    private boolean mIsStarted;
+
+    /**
+     * Temporary work-around to bug b/76017849.
+     * MediaSessionManager is not notifying media session priority changes.
+     * As a work-around we subscribe to playback state changes on all controllers to detect
+     * potential priority changes.
+     * This might cause a few unnecessary checks, but selecting the top-most controller is a
+     * cheap operation.
+     */
+    private class MediaSessionUpdater {
+        private List<MediaController> mControllers = new ArrayList<>();
+
+        private MediaController.Callback mCallback = new MediaController.Callback() {
+            @Override
+            public void onPlaybackStateChanged(PlaybackState state) {
+                selectMediaController(mMediaSessionManager.getActiveSessions(null));
+            }
+
+            @Override
+            public void onSessionDestroyed() {
+                selectMediaController(mMediaSessionManager.getActiveSessions(null));
+            }
+        };
+
+        void setControllersByPackageName(List<MediaController> newControllers) {
+            for (MediaController oldController : mControllers) {
+                oldController.unregisterCallback(mCallback);
+            }
+            for (MediaController newController : newControllers) {
+                newController.registerCallback(mCallback);
+            }
+            mControllers.clear();
+            mControllers.addAll(newControllers);
+        }
+    }
+
+    /**
+     * An observer of this model
+     */
+    public interface Observer {
+        /**
+         * Called when the top source media app changes.
+         */
+        void onActiveSourceChanged();
+    }
+
+    private MediaSessionManager.OnActiveSessionsChangedListener mSessionChangeListener =
+            this::selectMediaController;
+
+    /**
+     * Creates a {@link ActiveMediaSourceManager}. This instance is going to be inactive until
+     * {@link #start()} method is invoked.
+     */
+    public ActiveMediaSourceManager(Context context) {
+        mContext = context;
+        mMediaSessionManager = mContext.getSystemService(MediaSessionManager.class);
+        mSharedPreferences = mContext.getSharedPreferences(PLAYBACK_MODEL_SHARED_PREFS,
+                Context.MODE_PRIVATE);
+    }
+
+    /**
+     * Selects one of the provided controllers as the "currently playing" one.
+     */
+    private void selectMediaController(List<MediaController> controllers) {
+        if (Log.isLoggable(TAG, Log.DEBUG)) {
+            dump("Selecting a media controller from: ", controllers);
+        }
+        changeMediaController(getTopMostController(controllers));
+        mMediaSessionUpdater.setControllersByPackageName(controllers);
+    }
+
+    private void dump(String title, List<MediaController> controllers) {
+        Log.d(TAG, title + " (total: " + controllers.size() + ")");
+        for (MediaController controller : controllers) {
+            String stateName = getStateName(controller.getPlaybackState() != null
+                    ? controller.getPlaybackState().getState()
+                    : PlaybackState.STATE_NONE);
+            Log.d(TAG, String.format("\t%s: %s",
+                    controller.getPackageName(),
+                    stateName));
+        }
+    }
+
+    private String getStateName(@PlaybackState.State int state) {
+        switch (state) {
+            case PlaybackState.STATE_NONE:
+                return "NONE";
+            case PlaybackState.STATE_STOPPED:
+                return "STOPPED";
+            case PlaybackState.STATE_PAUSED:
+                return "PAUSED";
+            case PlaybackState.STATE_PLAYING:
+                return "PLAYING";
+            case PlaybackState.STATE_FAST_FORWARDING:
+                return "FORWARDING";
+            case PlaybackState.STATE_REWINDING:
+                return "REWINDING";
+            case PlaybackState.STATE_BUFFERING:
+                return "BUFFERING";
+            case PlaybackState.STATE_ERROR:
+                return "ERROR";
+            case PlaybackState.STATE_CONNECTING:
+                return "CONNECTING";
+            case PlaybackState.STATE_SKIPPING_TO_PREVIOUS:
+                return "SKIPPING_TO_PREVIOUS";
+            case PlaybackState.STATE_SKIPPING_TO_NEXT:
+                return "SKIPPING_TO_NEXT";
+            case PlaybackState.STATE_SKIPPING_TO_QUEUE_ITEM:
+                return "SKIPPING_TO_QUEUE_ITEM";
+            default:
+                return "UNKNOWN";
+        }
+    }
+
+    /**
+     * @return the controller most likely to be the currently active one, out of the list of
+     * active controllers repoted by {@link MediaSessionManager}. It does so by picking the first
+     * one (in order of priority) which an active state as reported by
+     * {@link MediaController#getPlaybackState()}
+     */
+    private MediaController getTopMostController(List<MediaController> controllers) {
+        if (controllers != null && controllers.size() > 0) {
+            for (MediaController candidate : controllers) {
+                @PlaybackState.State int state = candidate.getPlaybackState() != null
+                        ? candidate.getPlaybackState().getState()
+                        : PlaybackState.STATE_NONE;
+                if (state == PlaybackState.STATE_BUFFERING
+                        || state == PlaybackState.STATE_CONNECTING
+                        || state == PlaybackState.STATE_FAST_FORWARDING
+                        || state == PlaybackState.STATE_PLAYING
+                        || state == PlaybackState.STATE_REWINDING
+                        || state == PlaybackState.STATE_SKIPPING_TO_NEXT
+                        || state == PlaybackState.STATE_SKIPPING_TO_PREVIOUS
+                        || state == PlaybackState.STATE_SKIPPING_TO_QUEUE_ITEM) {
+                    return candidate;
+                }
+            }
+            // If no source is active, we go for the last known source
+            String packageName = getLastKnownActivePackageName();
+            if (packageName != null) {
+                for (MediaController candidate : controllers) {
+                    if (candidate.getPackageName().equals(packageName)) {
+                        return candidate;
+                    }
+                }
+            }
+            return controllers.get(0);
+        }
+        return null;
+    }
+
+    private void changeMediaController(MediaController mediaController) {
+        if (Log.isLoggable(TAG, Log.DEBUG)) {
+            Log.d(TAG, "New media controller: " + (mediaController != null
+                    ? mediaController.getPackageName() : null));
+        }
+        if ((mediaController == null && mMediaController == null)
+                || (mediaController != null && mMediaController != null
+                && mediaController.getPackageName().equals(mMediaController.getPackageName()))) {
+            // If no change, do nothing.
+            return;
+        }
+        mMediaController = mediaController;
+        setLastKnownActivePackageName(mMediaController != null
+                ? mMediaController.getPackageName()
+                : null);
+        notify(Observer::onActiveSourceChanged);
+    }
+
+    /**
+     * Starts following changes on the list of active media sources. If any changes happen, all
+     * observers registered through {@link #registerObserver(Observer)} will be notified.
+     * <p>
+     * Calling this method might cause an immediate {@link Observer#onActiveSourceChanged()}
+     * event in case the current media source is different than the last known one.
+     */
+    private void start() {
+        mMediaSessionManager.addOnActiveSessionsChangedListener(mSessionChangeListener, null);
+        selectMediaController(mMediaSessionManager.getActiveSessions(null));
+        mIsStarted = true;
+    }
+
+    /**
+     * Stops following changes on the list of active media sources. This method could cause an
+     * immediate {@link PlaybackModel.PlaybackObserver#onSourceChanged()} event if a media source
+     * was already connected.
+     */
+    private void stop() {
+        mMediaSessionUpdater.setControllersByPackageName(new ArrayList<>());
+        mMediaSessionManager.removeOnActiveSessionsChangedListener(mSessionChangeListener);
+        changeMediaController(null);
+        mIsStarted = false;
+    }
+
+    private void notify(Consumer<Observer> notification) {
+        mHandler.post(() -> {
+            List<Observer> observers = new ArrayList<>(mObservers);
+            for (Observer observer : observers) {
+                notification.accept(observer);
+            }
+        });
+    }
+
+    /**
+     * @return a {@link MediaController} providing access to metadata of the currently playing media
+     * source, or NULL if no media source has an active session. Changes on this value will
+     * be notified through {@link Observer#onActiveSourceChanged()}
+     */
+    @Nullable
+    public MediaController getMediaController() {
+        return mIsStarted
+                ? mMediaController
+                : getTopMostController(mMediaSessionManager.getActiveSessions(null));
+    }
+
+    /**
+     * Registers an observer to be notified of media events. If the model is not started yet it
+     * will start right away. If the model was already started, the observer will receive an
+     * immediate {@link Observer#onActiveSourceChanged()} event.
+     */
+    public void registerObserver(Observer observer) {
+        mObservers.add(observer);
+        if (!mIsStarted) {
+            start();
+        } else {
+            observer.onActiveSourceChanged();
+        }
+    }
+
+    /**
+     * Unregisters an observer previously registered using
+     * {@link #registerObserver(Observer)}. There are no other observers the model will
+     * stop tracking changes right away.
+     */
+    public void unregisterObserver(Observer observer) {
+        mObservers.remove(observer);
+        if (mObservers.isEmpty() && mIsStarted) {
+            stop();
+        }
+    }
+
+    private String getLastKnownActivePackageName() {
+        return mSharedPreferences.getString(PLAYBACK_MODEL_ACTIVE_PACKAGE_NAME_KEY, null);
+    }
+
+    private void setLastKnownActivePackageName(String packageName) {
+        mSharedPreferences.edit()
+                .putString(PLAYBACK_MODEL_ACTIVE_PACKAGE_NAME_KEY, packageName)
+                .apply();
+    }
+
+    /**
+     * Returns the {@link MediaController} corresponding to the given package name, or NULL if
+     * no active session exists for it.
+     */
+    public @Nullable MediaController getControllerForPackage(String packageName) {
+        List<MediaController> controllers = mMediaSessionManager.getActiveSessions(null);
+        for (MediaController controller : controllers) {
+            if (controller.getPackageName().equals(packageName)) {
+                return controller;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns true if the given package name corresponds to the top most media source.
+     */
+    public boolean isPlaying(String packageName) {
+        return mMediaController != null && mMediaController.getPackageName().equals(packageName);
+    }
+}
diff --git a/car-media-common/src/com/android/car/media/common/MediaSource.java b/car-media-common/src/com/android/car/media/common/MediaSource.java
index 6beb309..78bbd64 100644
--- a/car-media-common/src/com/android/car/media/common/MediaSource.java
+++ b/car-media-common/src/com/android/car/media/common/MediaSource.java
@@ -553,21 +553,17 @@
     }
 
     /**
-     * @return a {@link PlaybackModel} that allows controlling this media source. This method
-     * should only be used if this {@link MediaSource} is connected.
-     * @see #subscribe(Observer)
+     * Returns a {@link MediaController} that allows controlling this media source, or NULL
+     * if the media source doesn't support browsing or the browser is not connected.
      */
     @Nullable
-    public PlaybackModel getPlaybackModel() {
-        if (mBrowser == null) {
+    public MediaController getMediaController() {
+        if (mBrowser == null || !mBrowser.isConnected()) {
             return null;
         }
 
         MediaSession.Token token = mBrowser.getSessionToken();
-        MediaController controller = new MediaController(mContext, token);
-        PlaybackModel playbackModel = new PlaybackModel(mContext);
-        playbackModel.setMediaController(controller);
-        return playbackModel;
+        return new MediaController(mContext, token);
     }
 
     /**
diff --git a/car-media-common/src/com/android/car/media/common/PlaybackControlsActionBar.java b/car-media-common/src/com/android/car/media/common/PlaybackControlsActionBar.java
index 8f131e0..8605690 100644
--- a/car-media-common/src/com/android/car/media/common/PlaybackControlsActionBar.java
+++ b/car-media-common/src/com/android/car/media/common/PlaybackControlsActionBar.java
@@ -99,6 +99,7 @@
         if (mModel != null) {
             mModel.registerObserver(mObserver);
         }
+        updateAccentColor();
     }
 
     private void init(Context context) {
@@ -155,10 +156,18 @@
     }
 
     private void updateState() {
-        mPlayPauseStopImageView.setAction(convertMainAction(mModel.getMainAction()));
-        mSpinner.setVisibility(mModel.isBuffering() ? VISIBLE : INVISIBLE);
-        mSkipPrevButton.setVisibility(mModel.isSkipPreviewsEnabled() ? VISIBLE : INVISIBLE);
-        mSkipNextButton.setVisibility(mModel.isSkipNextEnabled() ? VISIBLE : INVISIBLE);
+        if (mModel != null) {
+            mPlayPauseStopImageView.setVisibility(View.VISIBLE);
+            mPlayPauseStopImageView.setAction(convertMainAction(mModel.getMainAction()));
+        } else {
+            mPlayPauseStopImageView.setVisibility(View.INVISIBLE);
+        }
+        mSpinner.setVisibility(mModel != null && mModel.isBuffering()
+                ? View.VISIBLE : View.INVISIBLE);
+        mSkipPrevButton.setVisibility(mModel != null && mModel.isSkipPreviewsEnabled()
+                ? View.VISIBLE : View.INVISIBLE);
+        mSkipNextButton.setVisibility(mModel != null && mModel.isSkipNextEnabled()
+                ? View.VISIBLE : View.INVISIBLE);
     }
 
     @PlayPauseStopImageView.Action
@@ -178,23 +187,31 @@
     }
 
     private void updateAccentColor() {
-        int defaultColor = mContext.getResources().getColor(android.R.color.background_dark, null);
-        MediaSource mediaSource = mModel.getMediaSource();
-        int color = mediaSource == null ? defaultColor : mediaSource.getAccentColor(defaultColor);
+        int color = getMediaSourceColor();
         int tintColor = ColorChecker.getTintColor(mContext, color);
         mPlayPauseStopImageView.setPrimaryActionColor(color, tintColor);
         mSpinner.setIndeterminateTintList(ColorStateList.valueOf(color));
     }
 
+    private int getMediaSourceColor() {
+        int defaultColor = mContext.getResources().getColor(android.R.color.background_dark, null);
+        MediaSource mediaSource = mModel != null ? mModel.getMediaSource() : null;
+        return mediaSource != null ? mediaSource.getAccentColor(defaultColor) : defaultColor;
+    }
+
     private List<ImageButton> getExtraActions() {
         List<ImageButton> extraActions = new ArrayList<>();
-        if (mModel.hasQueue()) {
+        if (mModel != null && mModel.hasQueue()) {
             extraActions.add(mTrackListButton);
         }
         return extraActions;
     }
 
     private void updateCustomActions() {
+        if (mModel == null) {
+            setViews(new ImageButton[0]);
+            return;
+        }
         List<ImageButton> combinedActions = new ArrayList<>();
         combinedActions.addAll(getExtraActions());
         combinedActions.addAll(mModel.getCustomActions()
diff --git a/car-media-common/src/com/android/car/media/common/PlaybackFragment.java b/car-media-common/src/com/android/car/media/common/PlaybackFragment.java
index a68208f..d11bef0 100644
--- a/car-media-common/src/com/android/car/media/common/PlaybackFragment.java
+++ b/car-media-common/src/com/android/car/media/common/PlaybackFragment.java
@@ -38,6 +38,7 @@
  * hosting application.
  */
 public class PlaybackFragment extends Fragment {
+    private ActiveMediaSourceManager mActiveMediaSourceManager;
     private PlaybackModel mModel;
     private CrossfadeImageView mAlbumBackground;
     private PlaybackControls mPlaybackControls;
@@ -47,7 +48,8 @@
     private TextView mSubtitle;
     private MediaItemMetadata mCurrentMetadata;
 
-    private PlaybackModel.PlaybackObserver mObserver = new PlaybackModel.PlaybackObserver() {
+    private PlaybackModel.PlaybackObserver mPlaybackObserver =
+            new PlaybackModel.PlaybackObserver() {
         @Override
         public void onSourceChanged() {
             updateMetadata();
@@ -58,12 +60,20 @@
             updateMetadata();
         }
     };
+    private ActiveMediaSourceManager.Observer mActiveSourceObserver =
+            new ActiveMediaSourceManager.Observer() {
+        @Override
+        public void onActiveSourceChanged() {
+            mModel.setMediaController(mActiveMediaSourceManager.getMediaController());
+        }
+    };
 
     @Nullable
     @Override
     public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
             Bundle savedInstanceState) {
         View view = inflater.inflate(R.layout.car_playback_fragment, container, false);
+        mActiveMediaSourceManager = new ActiveMediaSourceManager(getContext());
         mModel = new PlaybackModel(getContext());
         mAlbumBackground = view.findViewById(R.id.album_background);
         mPlaybackControls = view.findViewById(R.id.playback_controls);
@@ -97,13 +107,15 @@
     @Override
     public void onStart() {
         super.onStart();
-        mModel.registerObserver(mObserver);
+        mActiveMediaSourceManager.registerObserver(mActiveSourceObserver);
+        mModel.registerObserver(mPlaybackObserver);
     }
 
     @Override
     public void onStop() {
         super.onStop();
-        mModel.unregisterObserver(mObserver);
+        mActiveMediaSourceManager.unregisterObserver(mActiveSourceObserver);
+        mModel.unregisterObserver(mPlaybackObserver);
     }
 
     private void updateMetadata() {
diff --git a/car-media-common/src/com/android/car/media/common/PlaybackModel.java b/car-media-common/src/com/android/car/media/common/PlaybackModel.java
index bfb358d..c472911 100644
--- a/car-media-common/src/com/android/car/media/common/PlaybackModel.java
+++ b/car-media-common/src/com/android/car/media/common/PlaybackModel.java
@@ -20,7 +20,6 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
-import android.content.SharedPreferences;
 import android.content.pm.PackageManager;
 import android.content.res.Resources;
 import android.graphics.drawable.Drawable;
@@ -29,7 +28,6 @@
 import android.media.session.MediaController;
 import android.media.session.MediaController.TransportControls;
 import android.media.session.MediaSession;
-import android.media.session.MediaSessionManager;
 import android.media.session.PlaybackState;
 import android.media.session.PlaybackState.Actions;
 import android.os.Bundle;
@@ -45,16 +43,8 @@
 import java.util.stream.Collectors;
 
 /**
- * View-model for playback UI components. This abstractions provides a simplified view of
- * {@link MediaSession} and {@link MediaSessionManager} data and events.
- *
- * <p>
- * It automatically determines the foreground media app (the one that would normally
- * receive playback events) and exposes metadata and events from such app, or when a different app
- * becomes foreground.
- * <p>
- * This requires the android.Manifest.permission.MEDIA_CONTENT_CONTROL
- * permission be held by the calling app.
+ * Wrapper of {@link MediaSession}. It provides access to media session events and extended
+ * information on the currently playing item metadata.
  */
 public class PlaybackModel {
     private static final String TAG = "PlaybackModel";
@@ -62,56 +52,14 @@
     private static final String ACTION_SET_RATING =
             "com.android.car.media.common.ACTION_SET_RATING";
     private static final String EXTRA_SET_HEART = "com.android.car.media.common.EXTRA_SET_HEART";
-    private static final String PLAYBACK_MODEL_SHARED_PREFS =
-            "com.android.car.media.PLAYBACK_MODEL";
-    private static final String PLAYBACK_MODEL_ACTIVE_PACKAGE_NAME_KEY =
-            "active_packagename";
 
-    private final MediaSessionManager mMediaSessionManager;
     private final Handler mHandler = new Handler();
     @Nullable
+    private final Context mContext;
+    private final List<PlaybackObserver> mObservers = new ArrayList<>();
     private MediaController mMediaController;
-    private Context mContext;
-    private List<PlaybackObserver> mObservers = new ArrayList<>();
-    private final MediaSessionUpdater mMediaSessionUpdater = new MediaSessionUpdater();
     private MediaSource mMediaSource;
     private boolean mIsStarted;
-    private SharedPreferences mSharedPreferences;
-
-    /**
-     * Temporary work-around to bug b/76017849.
-     * MediaSessionManager is not notifying media session priority changes.
-     * As a work-around we subscribe to playback state changes on all controllers to detect
-     * potential priority changes.
-     * This might cause a few unnecessary checks, but selecting the top-most controller is a
-     * cheap operation.
-     */
-    private class MediaSessionUpdater {
-        private List<MediaController> mControllers = new ArrayList<>();
-
-        private MediaController.Callback mCallback = new MediaController.Callback() {
-            @Override
-            public void onPlaybackStateChanged(PlaybackState state) {
-                selectMediaController(mMediaSessionManager.getActiveSessions(null));
-            }
-
-            @Override
-            public void onSessionDestroyed() {
-                selectMediaController(mMediaSessionManager.getActiveSessions(null));
-            }
-        };
-
-        void setControllersByPackageName(List<MediaController> newControllers) {
-            for (MediaController oldController : mControllers) {
-                oldController.unregisterCallback(mCallback);
-            }
-            for (MediaController newController : newControllers) {
-                newController.registerCallback(mCallback);
-            }
-            mControllers.clear();
-            mControllers.addAll(newControllers);
-        }
-    }
 
     /**
      * An observer of this model
@@ -151,119 +99,29 @@
         }
     };
 
-    private MediaSessionManager.OnActiveSessionsChangedListener mSessionChangeListener =
-            this::selectMediaController;
+    /**
+     * Creates a {@link PlaybackModel}
+     */
+    public PlaybackModel(@NonNull Context context) {
+       this(context, null);
+    }
 
     /**
-     * Creates a {@link PlaybackModel}. This instance is going to be inactive until
-     * {@link #start()} method is invoked.
+     * Creates a {@link PlaybackModel} wrapping to the given media controller
      */
-    public PlaybackModel(Context context) {
+    public PlaybackModel(@NonNull Context context, @Nullable MediaController controller) {
         mContext = context;
-        mMediaSessionManager = mContext.getSystemService(MediaSessionManager.class);
-        mSharedPreferences = mContext.getSharedPreferences(PLAYBACK_MODEL_SHARED_PREFS,
-                Context.MODE_PRIVATE);
+        changeMediaController(controller);
     }
 
     /**
      * Sets the {@link MediaController} wrapped by this model.
      */
-    public void setMediaController(MediaController controller) {
-        changeMediaController(controller);
+    public void setMediaController(@Nullable MediaController mediaController) {
+        changeMediaController(mediaController);
     }
 
-    /**
-     * Selects one of the provided controllers as the "currently playing" one.
-     */
-    private void selectMediaController(List<MediaController> controllers) {
-        if (Log.isLoggable(TAG, Log.DEBUG)) {
-            dump("Selecting a media controller from: ", controllers);
-        }
-        changeMediaController(getTopMostController(controllers));
-        mMediaSessionUpdater.setControllersByPackageName(controllers);
-    }
-
-    private void dump(String title, List<MediaController> controllers) {
-        Log.d(TAG, title + " (total: " + controllers.size() + ")");
-        for (MediaController controller : controllers) {
-            String stateName = getStateName(controller.getPlaybackState() != null
-                    ? controller.getPlaybackState().getState()
-                    : PlaybackState.STATE_NONE);
-            Log.d(TAG, String.format("\t%s: %s",
-                    controller.getPackageName(),
-                    stateName));
-        }
-    }
-
-    private String getStateName(@PlaybackState.State int state) {
-        switch (state) {
-            case PlaybackState.STATE_NONE:
-                return "NONE";
-            case PlaybackState.STATE_STOPPED:
-                return "STOPPED";
-            case PlaybackState.STATE_PAUSED:
-                return "PAUSED";
-            case PlaybackState.STATE_PLAYING:
-                return "PLAYING";
-            case PlaybackState.STATE_FAST_FORWARDING:
-                return "FORWARDING";
-            case PlaybackState.STATE_REWINDING:
-                return "REWINDING";
-            case PlaybackState.STATE_BUFFERING:
-                return "BUFFERING";
-            case PlaybackState.STATE_ERROR:
-                return "ERROR";
-            case PlaybackState.STATE_CONNECTING:
-                return "CONNECTING";
-            case PlaybackState.STATE_SKIPPING_TO_PREVIOUS:
-                return "SKIPPING_TO_PREVIOUS";
-            case PlaybackState.STATE_SKIPPING_TO_NEXT:
-                return "SKIPPING_TO_NEXT";
-            case PlaybackState.STATE_SKIPPING_TO_QUEUE_ITEM:
-                return "SKIPPING_TO_QUEUE_ITEM";
-            default:
-                return "UNKNOWN";
-        }
-    }
-
-    /**
-     * @return the controller most likely to be the currently active one, out of the list of
-     * active controllers repoted by {@link MediaSessionManager}. It does so by picking the first
-     * one (in order of priority) which an active state as reported by
-     * {@link MediaController#getPlaybackState()}
-     */
-    private MediaController getTopMostController(List<MediaController> controllers) {
-        if (controllers != null && controllers.size() > 0) {
-            for (MediaController candidate : controllers) {
-                @PlaybackState.State int state = candidate.getPlaybackState() != null
-                        ? candidate.getPlaybackState().getState()
-                        : PlaybackState.STATE_NONE;
-                if (state == PlaybackState.STATE_BUFFERING
-                        || state == PlaybackState.STATE_CONNECTING
-                        || state == PlaybackState.STATE_FAST_FORWARDING
-                        || state == PlaybackState.STATE_PLAYING
-                        || state == PlaybackState.STATE_REWINDING
-                        || state == PlaybackState.STATE_SKIPPING_TO_NEXT
-                        || state == PlaybackState.STATE_SKIPPING_TO_PREVIOUS
-                        || state == PlaybackState.STATE_SKIPPING_TO_QUEUE_ITEM) {
-                    return candidate;
-                }
-            }
-            // If no source is active, we go for the last known source
-            String packageName = getLastKnownActivePackageName();
-            if (packageName != null) {
-                for (MediaController candidate : controllers) {
-                    if (candidate.getPackageName().equals(packageName)) {
-                        return candidate;
-                    }
-                }
-            }
-            return controllers.get(0);
-        }
-        return null;
-    }
-
-    private void changeMediaController(MediaController mediaController) {
+    private void changeMediaController(@Nullable MediaController mediaController) {
         if (Log.isLoggable(TAG, Log.DEBUG)) {
             Log.d(TAG, "New media controller: " + (mediaController != null
                     ? mediaController.getPackageName() : null));
@@ -278,40 +136,35 @@
             mMediaController.unregisterCallback(mCallback);
         }
         mMediaController = mediaController;
-        setLastKnownActivePackageName(mMediaController != null
-                ? mMediaController.getPackageName()
-                : null);
-        if (mMediaController != null) {
-            mMediaSource = new MediaSource(mContext, mMediaController.getPackageName());
+        mMediaSource = mMediaController != null
+            ? new MediaSource(mContext, mMediaController.getPackageName()) : null;
+        if (mMediaController != null && mIsStarted) {
             mMediaController.registerCallback(mCallback);
-        } else {
-            mMediaSource = null;
         }
-        notify(PlaybackObserver::onSourceChanged);
+        if (mIsStarted) {
+            notify(PlaybackObserver::onSourceChanged);
+        }
     }
 
     /**
-     * Starts following changes on the list of active media sources. If any changes happen, all
-     * observers registered through {@link #registerObserver(PlaybackObserver)} will be notified.
-     * <p>
-     * Calling this method might cause an immediate {@link PlaybackObserver#onSourceChanged()}
-     * event in case the current media source is different than the last known one.
+     * Starts following changes on the playback state of the given source. If any changes happen,
+     * all observers registered through {@link #registerObserver(PlaybackObserver)} will be
+     * notified.
      */
     private void start() {
-        mMediaSessionManager.addOnActiveSessionsChangedListener(mSessionChangeListener, null);
-        selectMediaController(mMediaSessionManager.getActiveSessions(null));
+        if (mMediaController != null) {
+            mMediaController.registerCallback(mCallback);
+        }
         mIsStarted = true;
     }
 
     /**
-     * Stops following changes on the list of active media sources. This method could cause an
-     * immediate {@link PlaybackObserver#onSourceChanged()} event if a media source was already
-     * connected.
+     * Stops following changes on the list of active media sources.
      */
     private void stop() {
-        mMediaSessionUpdater.setControllersByPackageName(new ArrayList<>());
-        mMediaSessionManager.removeOnActiveSessionsChangedListener(mSessionChangeListener);
-        changeMediaController(null);
+        if (mMediaController != null) {
+            mMediaController.unregisterCallback(mCallback);
+        }
         mIsStarted = false;
     }
 
@@ -326,20 +179,20 @@
 
     /**
      * @return a {@link MediaSource} providing access to metadata of the currently playing media
-     * source, or NULL if no media source has an active session. Changes on this value will
-     * be notified through {@link PlaybackObserver#onSourceChanged()}
+     * source, or NULL if the media source has no active session.
      */
     @Nullable
     public MediaSource getMediaSource() {
-        if (mIsStarted) {
-            return mMediaSource;
-        }
+        return mMediaSource;
+    }
 
-        MediaController controller = getTopMostController(mMediaSessionManager
-                .getActiveSessions(null));
-        return controller != null
-                ? new MediaSource(mContext, controller.getPackageName())
-                : null;
+    /**
+     * @return a {@link MediaController} that can be used to control this media source, or NULL
+     * if the media source has no active session.
+     */
+    @Nullable
+    public MediaController getMediaController() {
+        return mMediaController;
     }
 
     /**
@@ -399,7 +252,6 @@
         if (mMediaController != null) {
             mMediaController.getTransportControls().skipToPrevious();
         }
-
     }
 
     /**
@@ -445,9 +297,7 @@
             cntrl.sendCustomAction(action, extras);
         }
 
-        if (mMediaController != null) {
-            mMediaController.getTransportControls().sendCustomAction(action, extras);
-        }
+        mMediaController.getTransportControls().sendCustomAction(action, extras);
     }
 
     /**
@@ -743,14 +593,4 @@
             return null;
         }
     }
-
-    private String getLastKnownActivePackageName() {
-        return mSharedPreferences.getString(PLAYBACK_MODEL_ACTIVE_PACKAGE_NAME_KEY, null);
-    }
-
-    private void setLastKnownActivePackageName(String packageName) {
-        mSharedPreferences.edit()
-                .putString(PLAYBACK_MODEL_ACTIVE_PACKAGE_NAME_KEY, packageName)
-                .apply();
-    }
 }