Add MediaSourceViewModel.

Adds a ViewModel for obtaining and selecting MediaSources. SimpleMediaSource is a copy of MediaSource, and will be renamed once all references to the old class are removed. ActiveMediaControllerLiveData is a heavily modified copy of ActiveMediaSourceManager. MediaSourcesLiveData is modeled after MediaSourcesManager.

Test: Full tests for LiveDatas and ViewModels in this CL. Also fixes test breakage in PlaybackViewModelTest.
Change-Id: Icd8072c4c9aaa618a08f96afb39977f10a655775
diff --git a/car-media-common/src/com/android/car/media/common/source/ActiveMediaControllerLiveData.java b/car-media-common/src/com/android/car/media/common/source/ActiveMediaControllerLiveData.java
new file mode 100644
index 0000000..cfec8d4
--- /dev/null
+++ b/car-media-common/src/com/android/car/media/common/source/ActiveMediaControllerLiveData.java
@@ -0,0 +1,259 @@
+/*
+ * 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.source;
+
+import android.annotation.NonNull;
+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.util.Log;
+
+import androidx.annotation.VisibleForTesting;
+import androidx.lifecycle.LiveData;
+
+import com.android.car.media.common.playback.PlaybackStateAnnotations;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * 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.
+ */
+class ActiveMediaControllerLiveData extends LiveData<MediaController> {
+    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 MediaSessionUpdater mMediaSessionUpdater = new MediaSessionUpdater();
+    private final 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(@Nullable PlaybackState state) {
+                selectMediaController(mMediaSessionManager.getActiveSessions(null));
+            }
+
+            @Override
+            public void onSessionDestroyed() {
+                selectMediaController(mMediaSessionManager.getActiveSessions(null));
+            }
+        };
+
+        void registerCallbacks(List<MediaController> newControllers) {
+            for (MediaController oldController : mControllers) {
+                oldController.unregisterCallback(mCallback);
+            }
+            for (MediaController newController : newControllers) {
+                newController.registerCallback(mCallback);
+            }
+            mControllers.clear();
+            mControllers.addAll(newControllers);
+        }
+    }
+
+    private MediaSessionManager.OnActiveSessionsChangedListener mSessionChangeListener =
+            this::selectMediaController;
+
+    ActiveMediaControllerLiveData(Context context) {
+        this(Objects.requireNonNull(context.getSystemService(MediaSessionManager.class)),
+                context.getSharedPreferences(PLAYBACK_MODEL_SHARED_PREFS, Context.MODE_PRIVATE));
+    }
+
+    @VisibleForTesting
+    ActiveMediaControllerLiveData(@NonNull MediaSessionManager mediaSessionManager,
+            @NonNull SharedPreferences preferences) {
+        mMediaSessionManager = mediaSessionManager;
+        mSharedPreferences = preferences;
+    }
+
+    /**
+     * 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), true);
+        mMediaSessionUpdater.registerCallbacks(controllers);
+    }
+
+    private void dump(@SuppressWarnings("SameParameterValue") 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(@PlaybackStateAnnotations.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";
+        }
+    }
+
+    /**
+     * Returns the controller most likely to be the currently active one, out of the list of active
+     * controllers reported by {@link MediaSessionManager}. It does so by picking the first one (in
+     * order of priority) with an active state as reported by
+     * {@link MediaController#getPlaybackState()}
+     */
+    @Nullable
+    private MediaController getTopMostController(List<MediaController> controllers) {
+        if (controllers != null && controllers.size() > 0) {
+            for (MediaController candidate : controllers) {
+                @PlaybackStateAnnotations.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, boolean persist) {
+        if (Log.isLoggable(TAG, Log.DEBUG)) {
+            Log.d(TAG, "New media controller: " + (mediaController != null
+                    ? mediaController.getPackageName() : null));
+        }
+        if ((mediaController == null && getValue() == null)
+                || (mediaController != null && getValue() != null
+                && mediaController.getPackageName().equals(getValue().getPackageName()))) {
+            // If no change, do nothing.
+            return;
+        }
+        postValue(mediaController);
+        if (persist) {
+            setLastKnownActivePackageName(mediaController != null
+                    ? mediaController.getPackageName()
+                    : null);
+        }
+    }
+
+    @Override
+    protected void onActive() {
+        mMediaSessionManager.addOnActiveSessionsChangedListener(mSessionChangeListener, null);
+        selectMediaController(mMediaSessionManager.getActiveSessions(null));
+    }
+
+    @Override
+    protected void onInactive() {
+        mMediaSessionUpdater.registerCallbacks(new ArrayList<>());
+        mMediaSessionManager.removeOnActiveSessionsChangedListener(mSessionChangeListener);
+        changeMediaController(null, false);
+    }
+
+
+    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.
+     */
+    @Nullable
+    MediaController getControllerForPackage(String packageName) {
+        List<MediaController> controllers = mMediaSessionManager.getActiveSessions(null);
+        for (MediaController controller : controllers) {
+            if (controller.getPackageName().equals(packageName)) {
+                return controller;
+            }
+        }
+        return null;
+    }
+
+}
diff --git a/car-media-common/src/com/android/car/media/common/source/MediaBrowserConnector.java b/car-media-common/src/com/android/car/media/common/source/MediaBrowserConnector.java
new file mode 100644
index 0000000..2b93713
--- /dev/null
+++ b/car-media-common/src/com/android/car/media/common/source/MediaBrowserConnector.java
@@ -0,0 +1,134 @@
+/*
+ * 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.source;
+
+import android.annotation.NonNull;
+import android.content.ComponentName;
+import android.content.Context;
+import android.media.browse.MediaBrowser;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.VisibleForTesting;
+import androidx.lifecycle.LiveData;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * A LiveData that emits a MediaBrowserState for the current connection status. Attempts to maintain
+ * a connection while active.
+ */
+
+class MediaBrowserConnector extends LiveData<MediaBrowserConnector.MediaBrowserState> {
+
+    /**
+     * Contains the {@link MediaBrowser} for a {@link MediaBrowserConnector} and its associated
+     * connection status.
+     */
+    public static class MediaBrowserState {
+        public final MediaBrowser mMediaBrowser;
+
+        @ConnectionState
+        public final int mConnectionState;
+
+        MediaBrowserState(MediaBrowser mediaBrowser, @ConnectionState int connectionState) {
+            mMediaBrowser = mediaBrowser;
+            mConnectionState = connectionState;
+        }
+    }
+
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(value = {ConnectionState.DISCONNECTED, ConnectionState.CONNECTING,
+            ConnectionState.CONNECTED, ConnectionState.CONNECTION_FAILED})
+    public @interface ConnectionState {
+        int DISCONNECTED = 0;
+        int CONNECTING = 1;
+        int CONNECTED = 2;
+        int CONNECTION_FAILED = 3;
+    }
+
+    private final MediaBrowser mBrowser;
+
+    /**
+     * Create a new MediaBrowserConnector for the specified component.
+     *
+     * @param context       The Context with which to build the MediaBrowser.
+     * @param browseService The ComponentName of the media browser service.
+     * @see MediaBrowser#MediaBrowser(Context, ComponentName, MediaBrowser.ConnectionCallback,
+     * android.os.Bundle)
+     */
+    MediaBrowserConnector(@NonNull Context context,
+            @NonNull ComponentName browseService) {
+        mBrowser = createMediaBrowser(context, browseService,
+                new MediaBrowser.ConnectionCallback() {
+                    @Override
+                    public void onConnected() {
+                        setValue(new MediaBrowserState(mBrowser, ConnectionState.CONNECTED));
+                    }
+
+                    @Override
+                    public void onConnectionFailed() {
+                        setValue(
+                                new MediaBrowserState(mBrowser, ConnectionState.CONNECTION_FAILED));
+                    }
+
+                    @Override
+                    public void onConnectionSuspended() {
+                        setValue(new MediaBrowserState(mBrowser, ConnectionState.DISCONNECTED));
+                    }
+                });
+    }
+
+    /**
+     * Instantiate the MediaBrowser this MediaBrowserConnector will connect with.
+     */
+    @VisibleForTesting()
+    protected MediaBrowser createMediaBrowser(@NonNull Context context,
+            @NonNull ComponentName browseService,
+            @NonNull MediaBrowser.ConnectionCallback callback) {
+        return new MediaBrowser(context, browseService, callback, null);
+    }
+
+    @Override
+    protected void onActive() {
+        super.onActive();
+        if (mBrowser.isConnected()) {
+            setValue(new MediaBrowserState(mBrowser, ConnectionState.CONNECTED));
+        } else {
+            try {
+                connect();
+            } catch (IllegalStateException ex) {
+                // Ignore: MediaBrowse could be in an intermediate state (not connected, but not
+                // disconnected either.). In this situation, trying to connect again can throw
+                // this exception, but there is no way to know without trying.
+            }
+        }
+    }
+
+    private void connect() {
+        setValue(new MediaBrowserState(mBrowser, ConnectionState.CONNECTING));
+        mBrowser.connect();
+    }
+
+    @Override
+    protected void onInactive() {
+        // TODO(b/77640010): Review MediaBrowse disconnection.
+        // Some media sources are not responding correctly to MediaBrowser#disconnect(). We
+        // are keeping the connection going.
+        //   mBrowser.disconnect();
+    }
+}
diff --git a/car-media-common/src/com/android/car/media/common/source/MediaSourceColors.java b/car-media-common/src/com/android/car/media/common/source/MediaSourceColors.java
index 0de155c..1db676a 100644
--- a/car-media-common/src/com/android/car/media/common/source/MediaSourceColors.java
+++ b/car-media-common/src/com/android/car/media/common/source/MediaSourceColors.java
@@ -76,6 +76,11 @@
             mContext = context;
         }
 
+        /** Extract colors for (@code mediaSource} and create a MediaSourceColors for it */
+        public MediaSourceColors extractColors(@NonNull SimpleMediaSource mediaSource) {
+            return extractColors(mediaSource.getPackageName());
+        }
+
         /** Extract colors for {@code packageName} and create a MediaSourceColors for it */
         public MediaSourceColors extractColors(@NonNull String packageName) {
             TypedArray ta = null;
diff --git a/car-media-common/src/com/android/car/media/common/source/MediaSourceViewModel.java b/car-media-common/src/com/android/car/media/common/source/MediaSourceViewModel.java
new file mode 100644
index 0000000..22cedaa
--- /dev/null
+++ b/car-media-common/src/com/android/car/media/common/source/MediaSourceViewModel.java
@@ -0,0 +1,245 @@
+/*
+ * 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.source;
+
+import static androidx.lifecycle.Transformations.map;
+import static androidx.lifecycle.Transformations.switchMap;
+
+import static com.android.car.arch.common.LiveDataFunctions.coalesceNull;
+import static com.android.car.arch.common.LiveDataFunctions.combine;
+import static com.android.car.arch.common.LiveDataFunctions.mapNonNull;
+import static com.android.car.arch.common.LiveDataFunctions.nullLiveData;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UiThread;
+import android.app.Application;
+import android.content.ComponentName;
+import android.media.browse.MediaBrowser;
+import android.media.session.MediaController;
+import android.media.session.MediaSession;
+
+import androidx.annotation.VisibleForTesting;
+import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+
+import com.android.car.media.common.source.MediaBrowserConnector.ConnectionState;
+import com.android.car.media.common.source.MediaBrowserConnector.MediaBrowserState;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Contains observable data needed for displaying playback and browse UI
+ */
+public class MediaSourceViewModel extends AndroidViewModel {
+
+    private final InputFactory mInputFactory;
+
+    private final LiveData<List<SimpleMediaSource>> mMediaSources;
+
+    private final LiveData<Boolean> mHasMediaSources;
+
+    private final MutableLiveData<SimpleMediaSource> mSelectedMediaSource = new MutableLiveData<>();
+
+    private final LiveData<MediaBrowser> mConnectedMediaBrowser;
+
+    // Media controller for selected media source.
+    private final LiveData<MediaController> mMediaController;
+
+    // Media controller for active media source, may not be the same as selected media source.
+    private final LiveData<MediaController> mActiveMediaController;
+
+    private final LiveData<Boolean> mIsCurrentMediaSourcePlaying;
+
+    /**
+     * Factory for creating dependencies. Can be swapped out for testing.
+     */
+    @VisibleForTesting
+    interface InputFactory {
+        LiveData<List<SimpleMediaSource>> createMediaSources();
+
+        LiveData<MediaBrowserState> createMediaBrowserConnector(
+                @NonNull ComponentName browseService);
+
+        LiveData<MediaController> createActiveMediaController();
+
+        MediaController getControllerForPackage(String packageName);
+
+        MediaController getControllerForSession(@Nullable MediaSession.Token session);
+    }
+
+    /**
+     * Create a new instance of MediaSourceViewModel
+     *
+     * @see AndroidViewModel
+     */
+    public MediaSourceViewModel(@NonNull Application application) {
+        this(application, new InputFactory() {
+
+            private ActiveMediaControllerLiveData mActiveMediaControllerLiveData =
+                    new ActiveMediaControllerLiveData(application);
+
+            @Override
+            public LiveData<List<SimpleMediaSource>> createMediaSources() {
+                return new MediaSourcesLiveData(application);
+            }
+
+            @Override
+            public LiveData<MediaBrowserState> createMediaBrowserConnector(
+                    @NonNull ComponentName browseService) {
+                return new MediaBrowserConnector(application, browseService);
+            }
+
+            @Override
+            public LiveData<MediaController> createActiveMediaController() {
+                return mActiveMediaControllerLiveData;
+            }
+
+            @Override
+            public MediaController getControllerForPackage(String packageName) {
+                return mActiveMediaControllerLiveData.getControllerForPackage(packageName);
+            }
+
+            @Override
+            public MediaController getControllerForSession(@Nullable MediaSession.Token token) {
+                if (token == null) return null;
+                return new MediaController(application, token);
+            }
+        });
+    }
+
+    @VisibleForTesting
+    MediaSourceViewModel(@NonNull Application application, @NonNull InputFactory inputFactory) {
+        super(application);
+
+        mInputFactory = inputFactory;
+
+        mActiveMediaController = inputFactory.createActiveMediaController();
+
+        mMediaSources = inputFactory.createMediaSources();
+        mHasMediaSources = map(mMediaSources, sources -> sources != null && !sources.isEmpty());
+
+        LiveData<MediaBrowserState> mediaBrowserState = switchMap(mSelectedMediaSource,
+                (mediaSource) -> {
+                    if (mediaSource == null) {
+                        return nullLiveData();
+                    }
+                    ComponentName browseService = mediaSource.getBrowseServiceComponentName();
+                    if (browseService == null) {
+                        return nullLiveData();
+                    }
+                    return inputFactory.createMediaBrowserConnector(browseService);
+                });
+        mConnectedMediaBrowser = map(mediaBrowserState,
+                state -> state != null && (state.mConnectionState == ConnectionState.CONNECTED)
+                        ? state.mMediaBrowser : null);
+
+        LiveData<MediaController> controllerFromActiveControllers = mapNonNull(mSelectedMediaSource,
+                source -> getControllerForPackage(source.getPackageName()));
+        LiveData<MediaController> controllerFromMediaBrowser = mapNonNull(mConnectedMediaBrowser,
+                browser -> inputFactory.getControllerForSession(browser.getSessionToken()));
+        mMediaController = coalesceNull(
+                // Prefer fetching MediaController from MediaSessionManager's active controller
+                // list.
+                controllerFromActiveControllers,
+                // If that isn't found, try to connect to the browse service and create a
+                // controller from there.
+                controllerFromMediaBrowser);
+
+        mIsCurrentMediaSourcePlaying = combine(mActiveMediaController, mSelectedMediaSource,
+                (mediaController, mediaSource) ->
+                        mediaController != null && mediaSource != null
+                                && Objects.equals(mediaController.getPackageName(),
+                                mediaSource.getPackageName()));
+    }
+
+    /**
+     * Returns a live list of all MediaSources that can be selected for playback
+     */
+    public LiveData<List<SimpleMediaSource>> getMediaSources() {
+        return mMediaSources;
+    }
+
+    /**
+     * Returns a LiveData that emits whether there are any media sources that can be selected for
+     * playback
+     */
+    public LiveData<Boolean> hasMediaSources() {
+        return mHasMediaSources;
+    }
+
+    /**
+     * Returns a LiveData that emits the MediaSource that is to be browsed or displayed.
+     */
+    public LiveData<SimpleMediaSource> getSelectedMediaSource() {
+        return mSelectedMediaSource;
+    }
+
+    /**
+     * Set the MediaSource that is to be browsed or displayed. If a browse service is available, a
+     * connection may be made and provided through {@link #getConnectedMediaBrowser()}.
+     */
+    @UiThread
+    public void setSelectedMediaSource(@Nullable SimpleMediaSource mediaSource) {
+        mSelectedMediaSource.setValue(mediaSource);
+    }
+
+    /**
+     * Returns a LiveData that emits the currently connected MediaBrowser. Emits {@code null} if no
+     * MediaSource is set, if the MediaSource does not support browsing, or if the MediaBrowser is
+     * not connected. Observing the LiveData will attempt to connect to a media browse session if
+     * possible.
+     */
+    public LiveData<MediaBrowser> getConnectedMediaBrowser() {
+        return mConnectedMediaBrowser;
+    }
+
+    /**
+     * Returns a LiveData that emits a {@link MediaController} that allows controlling this media
+     * source, or emits {@code null} if the media source doesn't support browsing or the browser is
+     * not connected.
+     */
+    public LiveData<MediaController> getMediaController() {
+        return mMediaController;
+    }
+
+    /**
+     * Returns a LiveData that emits a {@link MediaController} for the active media source. Note
+     * that this may not be from the selected media source.
+     */
+    public LiveData<MediaController> getActiveMediaController() {
+        return mActiveMediaController;
+    }
+
+    /**
+     * Returns a MediaController for the specified package if an active MediaSession is available.
+     */
+    @Nullable
+    private MediaController getControllerForPackage(String packageName) {
+        return mInputFactory.getControllerForPackage(packageName);
+    }
+
+    /**
+     * Emits {@code true} iff the selected media source is the active media source
+     */
+    public LiveData<Boolean> isCurrentMediaSourcePlaying() {
+        return mIsCurrentMediaSourcePlaying;
+    }
+
+}
diff --git a/car-media-common/src/com/android/car/media/common/source/MediaSourcesLiveData.java b/car-media-common/src/com/android/car/media/common/source/MediaSourcesLiveData.java
new file mode 100644
index 0000000..7634f9f
--- /dev/null
+++ b/car-media-common/src/com/android/car/media/common/source/MediaSourcesLiveData.java
@@ -0,0 +1,126 @@
+/*
+ * 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.source;
+
+import android.annotation.NonNull;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.service.media.MediaBrowserService;
+import android.util.Log;
+
+import androidx.lifecycle.LiveData;
+
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * A LiveData that provides access to the list of all possible media sources that can be selected
+ * to be played.
+ */
+class MediaSourcesLiveData extends LiveData<List<SimpleMediaSource>> {
+
+    private static final String TAG = "MediaSourcesLiveData";
+    private final Context mContext;
+
+    private final BroadcastReceiver mAppInstallUninstallReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            updateMediaSources();
+        }
+    };
+
+    MediaSourcesLiveData(@NonNull Context context) {
+        mContext = context;
+    }
+
+    @Override
+    protected void onActive() {
+        super.onActive();
+        updateMediaSources();
+        registerBroadcastReceiver();
+    }
+
+    @Override
+    protected void onInactive() {
+        super.onInactive();
+        unregisterBroadcastReceiver();
+    }
+
+    private void registerBroadcastReceiver() {
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(Intent.ACTION_PACKAGE_ADDED);
+        filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+        filter.addDataScheme("package");
+        mContext.getApplicationContext().registerReceiver(mAppInstallUninstallReceiver, filter);
+    }
+
+    private void unregisterBroadcastReceiver() {
+        mContext.getApplicationContext().unregisterReceiver(mAppInstallUninstallReceiver);
+    }
+
+    private void updateMediaSources() {
+        List<SimpleMediaSource> mediaSources = getPackageNames().stream()
+                .filter(Objects::nonNull)
+                .map(packageName -> new SimpleMediaSource(mContext, packageName))
+                .filter(mediaSource -> {
+                    if (mediaSource.getName() == null) {
+                        Log.w(TAG, "Found media source without name: "
+                                + mediaSource.getPackageName());
+                        return false;
+                    }
+                    return true;
+                })
+                .sorted(Comparator.comparing(mediaSource -> mediaSource.getName().toString()))
+                .collect(Collectors.toList());
+        setValue(mediaSources);
+    }
+
+    /**
+     * Generates a set of all possible apps to choose from, including the ones that are just
+     * media services.
+     */
+    private Set<String> getPackageNames() {
+        PackageManager packageManager = mContext.getPackageManager();
+        Intent intent = new Intent(Intent.ACTION_MAIN, null);
+        intent.addCategory(Intent.CATEGORY_APP_MUSIC);
+
+        Intent mediaIntent = new Intent();
+        mediaIntent.setAction(MediaBrowserService.SERVICE_INTERFACE);
+
+        List<ResolveInfo> availableActivities = packageManager.queryIntentActivities(intent, 0);
+        List<ResolveInfo> mediaServices = packageManager.queryIntentServices(mediaIntent,
+                PackageManager.GET_RESOLVED_FILTER);
+
+        Set<String> apps = new HashSet<>();
+        for (ResolveInfo info : mediaServices) {
+            apps.add(info.serviceInfo.packageName);
+        }
+        for (ResolveInfo info : availableActivities) {
+            apps.add(info.activityInfo.packageName);
+        }
+        return apps;
+    }
+
+}
diff --git a/car-media-common/src/com/android/car/media/common/source/SimpleMediaSource.java b/car-media-common/src/com/android/car/media/common/source/SimpleMediaSource.java
new file mode 100644
index 0000000..5268135
--- /dev/null
+++ b/car-media-common/src/com/android/car/media/common/source/SimpleMediaSource.java
@@ -0,0 +1,254 @@
+/*
+ * 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.source;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.service.media.MediaBrowserService;
+import android.util.Log;
+
+import com.android.car.media.common.MediaSource;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * This represents a source of media content. It provides convenient methods to access media source
+ * metadata, such as primary color and application name.
+ */
+// TODO (keyboardr): Rename to MediaSource once other MediaSource is removed
+public class SimpleMediaSource {
+    private static final String TAG = "MediaSource";
+
+    /**
+     * Custom media sources which should not be templatized.
+     */
+    private static final Set<String> CUSTOM_MEDIA_SOURCES = new HashSet<>();
+    static {
+        CUSTOM_MEDIA_SOURCES.add("com.android.car.radio");
+    }
+
+    private final String mPackageName;
+    @Nullable
+    private final String mBrowseServiceClassName;
+    private final Context mContext;
+    private CharSequence mName;
+
+    /**
+     * Creates a {@link SimpleMediaSource} for the given application package name
+     */
+    public SimpleMediaSource(@NonNull Context context, @NonNull String packageName) {
+        mContext = context;
+        mPackageName = packageName;
+        mBrowseServiceClassName = getBrowseServiceClassName(packageName);
+        extractComponentInfo(mPackageName, mBrowseServiceClassName);
+    }
+
+    /**
+     * Returns a MediaSource equivalent to the one represented by this instance.
+     */
+    // TODO (keyboardr): Remove this method once SimpleMediaSource and MediaSource are merged
+    public MediaSource toMediaSource() {
+        return new MediaSource(mContext, mPackageName);
+    }
+
+    /**
+     * @return the classname corresponding to a {@link MediaBrowserService} in the media source, or
+     * null if the media source doesn't implement {@link MediaBrowserService}. A non-null result
+     * doesn't imply that this service is accessible. The consumer code should attempt to connect
+     * and handle rejections gracefully.
+     */
+    @Nullable
+    private String getBrowseServiceClassName(@NonNull String packageName) {
+        PackageManager packageManager = mContext.getPackageManager();
+        Intent intent = new Intent();
+        intent.setAction(MediaBrowserService.SERVICE_INTERFACE);
+        intent.setPackage(packageName);
+        List<ResolveInfo> resolveInfos = packageManager.queryIntentServices(intent,
+                PackageManager.GET_RESOLVED_FILTER);
+        if (resolveInfos == null || resolveInfos.isEmpty()) {
+            return null;
+        }
+        return resolveInfos.get(0).serviceInfo.name;
+    }
+
+
+
+    private void extractComponentInfo(@NonNull String packageName,
+            @Nullable String browseServiceClassName) {
+        try {
+            ApplicationInfo applicationInfo =
+                    mContext.getPackageManager().getApplicationInfo(packageName,
+                            PackageManager.GET_META_DATA);
+            ServiceInfo serviceInfo = browseServiceClassName != null
+                    ? mContext.getPackageManager().getServiceInfo(
+                    new ComponentName(packageName, browseServiceClassName),
+                    PackageManager.GET_META_DATA)
+                    : null;
+
+            // Get the proper app name, check service label, then application label.
+            if (serviceInfo != null && serviceInfo.labelRes != 0) {
+                mName = serviceInfo.loadLabel(mContext.getPackageManager());
+            } else {
+                mName = applicationInfo.loadLabel(mContext.getPackageManager());
+            }
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.w(TAG, "Unable to update media client package attributes.", e);
+        }
+    }
+
+    /**
+     * @return media source human readable name.
+     */
+    public CharSequence getName() {
+        return mName;
+    }
+
+    /**
+     * @return the package name that identifies this media source.
+     */
+    public String getPackageName() {
+        return mPackageName;
+    }
+
+    /**
+     * @return a {@link ComponentName} referencing this media source's {@link MediaBrowserService},
+     * or NULL if this media source doesn't implement such service.
+     */
+    @Nullable
+    public ComponentName getBrowseServiceComponentName() {
+        if (mBrowseServiceClassName != null) {
+            return new ComponentName(mPackageName, mBrowseServiceClassName);
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Returns this media source's icon as a {@link Drawable}
+     */
+    public Drawable getPackageIcon() {
+        try {
+            return mContext.getPackageManager().getApplicationIcon(getPackageName());
+        } catch (PackageManager.NameNotFoundException e) {
+            return null;
+        }
+    }
+
+    /**
+     * Returns this media source's icon cropped to a circle.
+     */
+    public Bitmap getRoundPackageIcon() {
+        Drawable packageIcon = getPackageIcon();
+        return packageIcon != null
+                ? getRoundCroppedBitmap(drawableToBitmap(getPackageIcon()))
+                : null;
+    }
+
+    /**
+     * Returns {@code true} iff this media source should not be templatized.
+     */
+    public boolean isCustom() {
+        return CUSTOM_MEDIA_SOURCES.contains(mPackageName);
+    }
+
+    /**
+     * Returns {@code true} iff this media source has a browse service to connect to.
+     */
+    public boolean isBrowsable() {
+        return mBrowseServiceClassName != null;
+    }
+
+    private Bitmap drawableToBitmap(Drawable drawable) {
+        Bitmap bitmap = null;
+
+        if (drawable instanceof BitmapDrawable) {
+            BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
+            if (bitmapDrawable.getBitmap() != null) {
+                return bitmapDrawable.getBitmap();
+            }
+        }
+
+        if (drawable.getIntrinsicWidth() <= 0 || drawable.getIntrinsicHeight() <= 0) {
+            bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
+        } else {
+            bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(),
+                    drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
+        }
+
+        Canvas canvas = new Canvas(bitmap);
+        drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
+        drawable.draw(canvas);
+        return bitmap;
+    }
+
+    private Bitmap getRoundCroppedBitmap(Bitmap bitmap) {
+        Bitmap output = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(),
+                Bitmap.Config.ARGB_8888);
+        Canvas canvas = new Canvas(output);
+
+        final int color = 0xff424242;
+        final Paint paint = new Paint();
+        final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
+
+        paint.setAntiAlias(true);
+        canvas.drawARGB(0, 0, 0, 0);
+        paint.setColor(color);
+        canvas.drawCircle(bitmap.getWidth() / 2, bitmap.getHeight() / 2,
+                bitmap.getWidth() / 2f, paint);
+        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
+        canvas.drawBitmap(bitmap, rect, rect, paint);
+        return output;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        SimpleMediaSource that = (SimpleMediaSource) o;
+        return Objects.equals(mPackageName, that.mPackageName)
+                && Objects.equals(mBrowseServiceClassName, that.mBrowseServiceClassName);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mPackageName, mBrowseServiceClassName);
+    }
+
+    @Override
+    @NonNull
+    public String toString() {
+        return getPackageName();
+    }
+}
diff --git a/car-media-common/tests/robotests/src/com/android/car/media/common/playback/PlaybackViewModelTest.java b/car-media-common/tests/robotests/src/com/android/car/media/common/playback/PlaybackViewModelTest.java
index 0e3e0c3..1b31468 100644
--- a/car-media-common/tests/robotests/src/com/android/car/media/common/playback/PlaybackViewModelTest.java
+++ b/car-media-common/tests/robotests/src/com/android/car/media/common/playback/PlaybackViewModelTest.java
@@ -25,13 +25,13 @@
 import static org.mockito.Mockito.when;
 import static org.robolectric.RuntimeEnvironment.application;
 
-import android.annotation.NonNull;
 import android.media.MediaDescription;
 import android.media.MediaMetadata;
 import android.media.session.MediaController;
 import android.media.session.MediaSession;
 import android.media.session.PlaybackState;
 
+import androidx.annotation.NonNull;
 import androidx.arch.core.executor.testing.InstantTaskExecutorRule;
 
 import com.android.car.arch.common.testing.CaptureObserver;
@@ -70,6 +70,8 @@
     @Mock
     public MediaMetadata mMediaMetadata;
     @Mock
+    public MediaDescription mMediaDescription;
+    @Mock
     public PlaybackState mPlaybackState;
     @Captor
     private ArgumentCaptor<MediaController.Callback> mCapturedCallback;
@@ -79,6 +81,7 @@
     @Before
     public void setUp() {
         doNothing().when(mMediaController).registerCallback(mCapturedCallback.capture());
+        when(mMediaMetadata.getDescription()).thenReturn(mMediaDescription);
         mPlaybackViewModel = new PlaybackViewModel(application);
         mPlaybackViewModel.setMediaController(dataOf(mMediaController));
     }
@@ -95,21 +98,23 @@
     public void testGetMetadata() {
         CaptureObserver<MediaItemMetadata> observer = new CaptureObserver<>();
         mPlaybackViewModel.getMetadata().observe(mLifecycleOwner, observer);
+        observer.reset();
 
-        assertThat(observer.hasBeenNotified()).isFalse();
         assertThat(mCapturedCallback.getValue()).isNotNull();
         mCapturedCallback.getValue().onMetadataChanged(mMediaMetadata);
 
         assertThat(observer.hasBeenNotified()).isTrue();
-        assertThat(observer.getObservedValue()).isEqualTo(mMediaMetadata);
+        MediaItemMetadata observedValue = observer.getObservedValue();
+        assertThat(observedValue).isNotNull();
+        assertThat(observedValue).isEqualTo(new MediaItemMetadata(mMediaMetadata));
     }
 
     @Test
     public void testGetPlaybackState() {
         CaptureObserver<PlaybackState> observer = new CaptureObserver<>();
         mPlaybackViewModel.getPlaybackState().observe(mLifecycleOwner, observer);
+        observer.reset();
 
-        assertThat(observer.hasBeenNotified()).isFalse();
         assertThat(mCapturedCallback.getValue()).isNotNull();
         mCapturedCallback.getValue().onPlaybackStateChanged(mPlaybackState);
 
@@ -125,9 +130,8 @@
         List<MediaSession.QueueItem> queue = Collections.singletonList(queueItem);
         CaptureObserver<List<MediaItemMetadata>> observer = new CaptureObserver<>();
         mPlaybackViewModel.getQueue().observe(mLifecycleOwner, observer);
+        observer.reset();
 
-        assertThat(observer.hasBeenNotified()).isFalse();
-        assertThat(mCapturedCallback.getValue()).isNotNull();
         mCapturedCallback.getValue().onQueueChanged(queue);
 
         assertThat(observer.hasBeenNotified()).isTrue();
@@ -144,9 +148,10 @@
     public void testGetHasQueue_null() {
         CaptureObserver<Boolean> observer = new CaptureObserver<>();
         mPlaybackViewModel.hasQueue().observe(mLifecycleOwner, observer);
+        mCapturedCallback.getValue().onQueueChanged(
+                Collections.singletonList(createQueueItem("title", 1)));
+        observer.reset();
 
-        assertThat(observer.hasBeenNotified()).isFalse();
-        assertThat(mCapturedCallback.getValue()).isNotNull();
         mCapturedCallback.getValue().onQueueChanged(null);
 
         assertThat(observer.hasBeenNotified()).isTrue();
@@ -158,9 +163,10 @@
         List<MediaSession.QueueItem> queue = Collections.emptyList();
         CaptureObserver<Boolean> observer = new CaptureObserver<>();
         mPlaybackViewModel.hasQueue().observe(mLifecycleOwner, observer);
+        mCapturedCallback.getValue().onQueueChanged(
+                Collections.singletonList(createQueueItem("title", 1)));
+        observer.reset();
 
-        assertThat(observer.hasBeenNotified()).isFalse();
-        assertThat(mCapturedCallback.getValue()).isNotNull();
         mCapturedCallback.getValue().onQueueChanged(queue);
 
         assertThat(observer.hasBeenNotified()).isTrue();
@@ -172,9 +178,8 @@
         List<MediaSession.QueueItem> queue = Collections.singletonList(createQueueItem("title", 1));
         CaptureObserver<Boolean> observer = new CaptureObserver<>();
         mPlaybackViewModel.hasQueue().observe(mLifecycleOwner, observer);
+        observer.reset();
 
-        assertThat(observer.hasBeenNotified()).isFalse();
-        assertThat(mCapturedCallback.getValue()).isNotNull();
         mCapturedCallback.getValue().onQueueChanged(queue);
 
         assertThat(observer.hasBeenNotified()).isTrue();
diff --git a/car-media-common/tests/robotests/src/com/android/car/media/common/source/ActiveMediaControllerLiveDataTest.java b/car-media-common/tests/robotests/src/com/android/car/media/common/source/ActiveMediaControllerLiveDataTest.java
new file mode 100644
index 0000000..89a8d11
--- /dev/null
+++ b/car-media-common/tests/robotests/src/com/android/car/media/common/source/ActiveMediaControllerLiveDataTest.java
@@ -0,0 +1,297 @@
+/*
+ * 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.source;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.robolectric.RuntimeEnvironment.application;
+
+import android.content.SharedPreferences;
+import android.media.session.MediaController;
+import android.media.session.MediaSessionManager;
+import android.media.session.MediaSessionManager.OnActiveSessionsChangedListener;
+import android.media.session.PlaybackState;
+import android.preference.PreferenceManager;
+
+import androidx.annotation.Nullable;
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule;
+import androidx.lifecycle.Lifecycle;
+
+import com.android.car.arch.common.testing.CaptureObserver;
+import com.android.car.arch.common.testing.TestLifecycleOwner;
+import com.android.car.media.common.TestConfig;
+import com.android.car.media.common.playback.PlaybackStateAnnotations;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
+public class ActiveMediaControllerLiveDataTest {
+
+    private static final String TEST_PACKAGE_1 = "package1";
+    private static final String TEST_PACKAGE_2 = "package2";
+    @Rule
+    public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+    @Rule
+    public final InstantTaskExecutorRule mTaskExecutorRule = new InstantTaskExecutorRule();
+    @Rule
+    public final TestLifecycleOwner mLifecycleOwner = new TestLifecycleOwner();
+
+    @Mock
+    public MediaSessionManager mMediaSessionManager;
+    @Mock
+    public MediaController mFirstMediaController;
+    @Mock
+    public MediaController mSecondMediaController;
+    @Captor
+    public ArgumentCaptor<OnActiveSessionsChangedListener> mSessionChangeListenerCaptor;
+    @Captor
+    public ArgumentCaptor<MediaController.Callback> mFirstControllerCallbackCaptor;
+    @Captor
+    public ArgumentCaptor<MediaController.Callback> mSecondControllerCallbackCaptor;
+
+    private List<MediaController> mMediaControllerList;
+    private SharedPreferences mSharedPreferences;
+    private ActiveMediaControllerLiveData mLiveData;
+
+
+    @Before
+    public void setUp() {
+        mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(
+                application);
+        mSharedPreferences.edit().clear().apply();
+
+        mMediaControllerList = new ArrayList<>();
+        mMediaControllerList.add(mFirstMediaController);
+        mMediaControllerList.add(mSecondMediaController);
+
+        setControllerState(mFirstMediaController, PlaybackState.STATE_PLAYING);
+        when(mFirstMediaController.getPackageName()).thenReturn(TEST_PACKAGE_1);
+        doNothing().when(mFirstMediaController)
+                .registerCallback(mFirstControllerCallbackCaptor.capture());
+        setControllerState(mSecondMediaController, PlaybackState.STATE_PLAYING);
+        when(mSecondMediaController.getPackageName()).thenReturn(TEST_PACKAGE_2);
+        doNothing().when(mSecondMediaController)
+                .registerCallback(mSecondControllerCallbackCaptor.capture());
+
+        doNothing().when(mMediaSessionManager).addOnActiveSessionsChangedListener(
+                mSessionChangeListenerCaptor.capture(), any());
+        doAnswer(invocation -> {
+            OnActiveSessionsChangedListener argument = invocation.getArgument(0);
+            if (argument == mSessionChangeListenerCaptor.getValue()) {
+                mSessionChangeListenerCaptor =
+                        ArgumentCaptor.forClass(OnActiveSessionsChangedListener.class);
+            }
+            return null;
+        }).when(mMediaSessionManager).removeOnActiveSessionsChangedListener(any());
+
+        mLiveData = new ActiveMediaControllerLiveData(mMediaSessionManager, mSharedPreferences);
+    }
+
+    private void doOnActiveObservation(CaptureObserver<MediaController> observer) {
+        when(mMediaSessionManager.getActiveSessions(any())).thenReturn(mMediaControllerList);
+
+        mLiveData.observe(mLifecycleOwner, observer);
+    }
+
+    private void doOnUpdateObservation(CaptureObserver<MediaController> observer) {
+        mLiveData.observe(mLifecycleOwner, observer);
+        observer.reset();
+        mSessionChangeListenerCaptor.getValue().onActiveSessionsChanged(mMediaControllerList);
+    }
+
+    @Test
+    public void testChooseFirstController_onActive() {
+        CaptureObserver<MediaController> observer = new CaptureObserver<>();
+
+        doOnActiveObservation(observer);
+
+        assertObservedController(observer, mFirstMediaController);
+    }
+
+    @Test
+    public void testChooseFirstController_onUpdate() {
+        CaptureObserver<MediaController> observer = new CaptureObserver<>();
+
+        doOnUpdateObservation(observer);
+
+        assertObservedController(observer, mFirstMediaController);
+    }
+
+    @Test
+    public void testPickPlayingController_onActive() {
+        CaptureObserver<MediaController> observer = new CaptureObserver<>();
+        setControllerState(mFirstMediaController, PlaybackState.STATE_PAUSED);
+
+        doOnActiveObservation(observer);
+
+        assertObservedController(observer, mSecondMediaController);
+    }
+
+    @Test
+    public void testPickPlayingController_onUpdate() {
+        CaptureObserver<MediaController> observer = new CaptureObserver<>();
+        setControllerState(mFirstMediaController, PlaybackState.STATE_PAUSED);
+
+        doOnUpdateObservation(observer);
+
+        assertObservedController(observer, mSecondMediaController);
+    }
+
+    @Test
+    public void testUsedLastWhenAllPaused_onActive() {
+        CaptureObserver<MediaController> observer = new CaptureObserver<>();
+        setControllerState(mFirstMediaController, PlaybackState.STATE_PAUSED);
+        setControllerState(mSecondMediaController, PlaybackState.STATE_PAUSED);
+        setLastObservedController(mSecondMediaController);
+
+        doOnActiveObservation(observer);
+
+        assertObservedController(observer, mSecondMediaController);
+    }
+
+    @Test
+    public void testUsedLastWhenAllPaused_onUpdate() {
+        CaptureObserver<MediaController> observer = new CaptureObserver<>();
+        setControllerState(mFirstMediaController, PlaybackState.STATE_PAUSED);
+        setControllerState(mSecondMediaController, PlaybackState.STATE_PAUSED);
+        setLastObservedController(mSecondMediaController);
+
+        doOnUpdateObservation(observer);
+
+        assertObservedController(observer, mSecondMediaController);
+    }
+
+    @Test
+    public void testPlaybackStateChangedTriggersUpdate() {
+        CaptureObserver<MediaController> observer = new CaptureObserver<>();
+        doOnActiveObservation(observer);
+        observer.reset();
+        setControllerState(mFirstMediaController, PlaybackState.STATE_PAUSED);
+
+        mFirstControllerCallbackCaptor.getValue()
+                .onPlaybackStateChanged(mFirstMediaController.getPlaybackState());
+
+        assertObservedController(observer, mSecondMediaController);
+    }
+
+    @Test
+    public void testSessionDestroyedTriggersUpdate() {
+        CaptureObserver<MediaController> observer = new CaptureObserver<>();
+        doOnActiveObservation(observer);
+        observer.reset();
+        setControllerState(mFirstMediaController, PlaybackState.STATE_NONE);
+
+        mFirstControllerCallbackCaptor.getValue().onSessionDestroyed();
+
+        assertObservedController(observer, mSecondMediaController);
+    }
+
+    @Test
+    public void testGetControllerForPackage() {
+        // Ensure LiveData is active
+        CaptureObserver<MediaController> observer = new CaptureObserver<>();
+        doOnActiveObservation(observer);
+
+        assertThat(mLiveData.getControllerForPackage(TEST_PACKAGE_2)).isSameAs(
+                mSecondMediaController);
+    }
+
+    @Test
+    public void testGetControllerForPackage_noMatch() {
+        // Ensure LiveData is active
+        CaptureObserver<MediaController> observer = new CaptureObserver<>();
+        doOnActiveObservation(observer);
+
+        assertThat(mLiveData.getControllerForPackage("")).isNull();
+    }
+
+    @Test
+    public void testUnregisterOnInactive() {
+        CaptureObserver<MediaController> observer = new CaptureObserver<>();
+        doOnActiveObservation(observer);
+
+        // Need to hold reference to captor since it will be swapped out when listener is
+        // unregistered.
+        ArgumentCaptor<OnActiveSessionsChangedListener> oldArgumentCaptor =
+                mSessionChangeListenerCaptor;
+        mLifecycleOwner.markState(Lifecycle.State.DESTROYED);
+
+        verify(mMediaSessionManager)
+                .removeOnActiveSessionsChangedListener(oldArgumentCaptor.getValue());
+    }
+
+    private void setLastObservedController(@Nullable MediaController mediaController) {
+        PlaybackState oldControllerState =
+                mediaController == null ? null : mediaController.getPlaybackState();
+        if (mediaController != null) {
+            setControllerState(mediaController, PlaybackState.STATE_PLAYING);
+        }
+
+        MediaSessionManager mediaSessionManager = mock(MediaSessionManager.class);
+        List<MediaController> mediaControllers =
+                mediaController == null ? Collections.emptyList()
+                        : Collections.singletonList(mediaController);
+        when(mediaSessionManager.getActiveSessions(any())).thenReturn(mediaControllers);
+
+        // Use another instance of ActiveMediaControllerLiveData (not the one under test) to
+        // inject the desired value into SharedPreferences.
+        ActiveMediaControllerLiveData injectorData =
+                new ActiveMediaControllerLiveData(mediaSessionManager, mSharedPreferences);
+
+        CaptureObserver<MediaController> observer = new CaptureObserver<>();
+        injectorData.observe(mLifecycleOwner, observer);
+        injectorData.removeObserver(observer);
+
+        if (mediaController != null) {
+            when(mediaController.getPlaybackState()).thenReturn(oldControllerState);
+        }
+    }
+
+    private void setControllerState(MediaController mediaController,
+            @PlaybackStateAnnotations.State int state) {
+        when(mediaController.getPlaybackState())
+                .thenReturn(
+                        new PlaybackState.Builder().setState(state, 0, 0).build());
+    }
+
+    private void assertObservedController(CaptureObserver<MediaController> observer,
+            MediaController mediaController) {
+        assertThat(observer.hasBeenNotified()).isTrue();
+        assertThat(observer.getObservedValue()).isSameAs(mediaController);
+    }
+}
diff --git a/car-media-common/tests/robotests/src/com/android/car/media/common/source/MediaBrowserConnectorTest.java b/car-media-common/tests/robotests/src/com/android/car/media/common/source/MediaBrowserConnectorTest.java
new file mode 100644
index 0000000..d5cbac0
--- /dev/null
+++ b/car-media-common/tests/robotests/src/com/android/car/media/common/source/MediaBrowserConnectorTest.java
@@ -0,0 +1,187 @@
+/*
+ * 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.source;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.robolectric.RuntimeEnvironment.application;
+
+import android.annotation.NonNull;
+import android.content.ComponentName;
+import android.content.Context;
+import android.media.browse.MediaBrowser;
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule;
+
+import com.android.car.arch.common.testing.CaptureObserver;
+import com.android.car.arch.common.testing.TestLifecycleOwner;
+import com.android.car.media.common.TestConfig;
+import com.android.car.media.common.source.MediaBrowserConnector.ConnectionState;
+import com.android.car.media.common.source.MediaBrowserConnector.MediaBrowserState;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
+public class MediaBrowserConnectorTest {
+
+    @Rule
+    public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+    @Rule
+    public final InstantTaskExecutorRule mTaskExecutorRule = new InstantTaskExecutorRule();
+    @Rule
+    public final TestLifecycleOwner mLifecycleOwner = new TestLifecycleOwner();
+
+    @Mock
+    public MediaBrowser mMediaBrowser;
+
+    private MediaBrowserConnector mLiveData;
+    private MediaBrowser.ConnectionCallback mConnectionCallback;
+
+    @Before
+    public void setUp() {
+        mLiveData = new MediaBrowserConnector(application, new ComponentName("", "")) {
+            @Override
+            protected MediaBrowser createMediaBrowser(@NonNull Context context,
+                    @NonNull ComponentName browseService,
+                    @NonNull MediaBrowser.ConnectionCallback callback) {
+                mConnectionCallback = callback;
+                return mMediaBrowser;
+            }
+        };
+    }
+
+    @Test
+    public void testConnectOnActive() {
+        CaptureObserver<MediaBrowserState> observer = new CaptureObserver<>();
+        when(mMediaBrowser.isConnected()).thenReturn(false);
+
+        mLiveData.observe(mLifecycleOwner, observer);
+
+        verify(mMediaBrowser).connect();
+        assertThat(observer.hasBeenNotified()).isTrue();
+        MediaBrowserState observedValue = observer.getObservedValue();
+        assertThat(observedValue).isNotNull();
+        assertThat(observedValue.mConnectionState).isEqualTo(ConnectionState.CONNECTING);
+    }
+
+    @Test
+    public void testAlreadyConnectedOnActive() {
+        CaptureObserver<MediaBrowserState> observer = new CaptureObserver<>();
+        when(mMediaBrowser.isConnected()).thenReturn(true);
+
+        mLiveData.observe(mLifecycleOwner, observer);
+
+        verify(mMediaBrowser, never()).connect();
+        assertThat(observer.hasBeenNotified()).isTrue();
+        MediaBrowserState observedValue = observer.getObservedValue();
+        assertThat(observedValue).isNotNull();
+        assertThat(observedValue.mMediaBrowser).isSameAs(mMediaBrowser);
+        assertThat(observedValue.mConnectionState).isEqualTo(ConnectionState.CONNECTED);
+    }
+
+    @Test
+    public void testExceptionOnConnect() {
+        CaptureObserver<MediaBrowserState> observer = new CaptureObserver<>();
+        when(mMediaBrowser.isConnected()).thenReturn(false);
+        setConnectionAction(() -> {
+            throw new IllegalStateException("expected");
+        });
+
+        mLiveData.observe(mLifecycleOwner, observer);
+
+        verify(mMediaBrowser).connect();
+        assertThat(observer.hasBeenNotified()).isTrue();
+        MediaBrowserState observedValue = observer.getObservedValue();
+        assertThat(observedValue).isNotNull();
+        assertThat(observedValue.mConnectionState).isEqualTo(ConnectionState.CONNECTING);
+    }
+
+    @Test
+    public void testConnectionCallback_onConnected() {
+        CaptureObserver<MediaBrowserState> observer = new CaptureObserver<>();
+        when(mMediaBrowser.isConnected()).thenReturn(false);
+        setConnectionAction(() -> {
+            observer.reset();
+            mConnectionCallback.onConnected();
+        });
+
+        mLiveData.observe(mLifecycleOwner, observer);
+
+        assertThat(observer.hasBeenNotified()).isTrue();
+        MediaBrowserState observedValue = observer.getObservedValue();
+        assertThat(observedValue).isNotNull();
+        assertThat(observedValue.mMediaBrowser).isSameAs(mMediaBrowser);
+        assertThat(observedValue.mConnectionState).isEqualTo(ConnectionState.CONNECTED);
+    }
+
+    @Test
+    public void testConnectionCallback_onConnectionFailed() {
+        CaptureObserver<MediaBrowserState> observer = new CaptureObserver<>();
+        when(mMediaBrowser.isConnected()).thenReturn(false);
+        setConnectionAction(() -> {
+            observer.reset();
+            mConnectionCallback.onConnectionFailed();
+        });
+
+        mLiveData.observe(mLifecycleOwner, observer);
+
+        assertThat(observer.hasBeenNotified()).isTrue();
+        MediaBrowserState observedValue = observer.getObservedValue();
+        assertThat(observedValue).isNotNull();
+        assertThat(observedValue.mConnectionState).isEqualTo(ConnectionState.CONNECTION_FAILED);
+    }
+
+    @Test
+    public void testConnectionCallback_onConnectionSuspended() {
+        CaptureObserver<MediaBrowserState> observer = new CaptureObserver<>();
+        when(mMediaBrowser.isConnected()).thenReturn(false);
+        setConnectionAction(() -> {
+            mConnectionCallback.onConnected();
+            observer.reset();
+            mConnectionCallback.onConnectionSuspended();
+        });
+
+        mLiveData.observe(mLifecycleOwner, observer);
+
+        assertThat(observer.hasBeenNotified()).isTrue();
+        MediaBrowserState observedValue = observer.getObservedValue();
+        assertThat(observedValue).isNotNull();
+        assertThat(observedValue.mConnectionState).isEqualTo(ConnectionState.DISCONNECTED);
+    }
+
+    private void setConnectionAction(@NonNull Runnable action) {
+        doAnswer(invocation -> {
+            action.run();
+            return null;
+        }).when(mMediaBrowser).connect();
+    }
+
+
+}
diff --git a/car-media-common/tests/robotests/src/com/android/car/media/common/source/MediaSourceViewModelTest.java b/car-media-common/tests/robotests/src/com/android/car/media/common/source/MediaSourceViewModelTest.java
new file mode 100644
index 0000000..6481755
--- /dev/null
+++ b/car-media-common/tests/robotests/src/com/android/car/media/common/source/MediaSourceViewModelTest.java
@@ -0,0 +1,238 @@
+/*
+ * 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.source;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+import static org.robolectric.RuntimeEnvironment.application;
+
+import android.annotation.NonNull;
+import android.content.ComponentName;
+import android.media.browse.MediaBrowser;
+import android.media.session.MediaController;
+import android.media.session.MediaSession;
+
+import androidx.annotation.Nullable;
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+
+import com.android.car.arch.common.testing.CaptureObserver;
+import com.android.car.arch.common.testing.TestLifecycleOwner;
+import com.android.car.media.common.TestConfig;
+import com.android.car.media.common.source.MediaBrowserConnector.MediaBrowserState;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+import java.util.Collections;
+import java.util.List;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
+public class MediaSourceViewModelTest {
+
+    private static final String SESSION_CONTROLLER_PACKAGE_NAME = "session";
+    private static final String PACKAGE_CONTROLLER_PACKAGE_NAME = "package";
+    @Rule
+    public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+    @Rule
+    public final InstantTaskExecutorRule mTaskExecutorRule = new InstantTaskExecutorRule();
+    @Rule
+    public final TestLifecycleOwner mLifecycleOwner = new TestLifecycleOwner();
+
+    @Mock
+    public SimpleMediaSource mMediaSource;
+    @Mock
+    public MediaBrowser mMediaBrowser;
+    @Mock
+    public MediaController mMediaControllerForPackage;
+    @Mock
+    public MediaController mMediaControllerForSession;
+
+    private final MutableLiveData<List<SimpleMediaSource>> mMediaSources = new MutableLiveData<>();
+    private final MutableLiveData<MediaBrowserState> mMediaBrowserState = new MutableLiveData<>();
+    private final MutableLiveData<MediaController> mActiveMediaController = new MutableLiveData<>();
+
+    private MediaSourceViewModel mViewModel;
+
+    private ComponentName mRequestedBrowseService;
+
+    @Before
+    public void setUp() {
+        when(mMediaControllerForPackage.getPackageName()).thenReturn(
+                PACKAGE_CONTROLLER_PACKAGE_NAME);
+        when(mMediaControllerForSession.getPackageName()).thenReturn(
+                SESSION_CONTROLLER_PACKAGE_NAME);
+        mRequestedBrowseService = null;
+        mViewModel = new MediaSourceViewModel(application, new MediaSourceViewModel.InputFactory() {
+            @Override
+            public LiveData<List<SimpleMediaSource>> createMediaSources() {
+                return mMediaSources;
+            }
+
+            @Override
+            public LiveData<MediaBrowserState> createMediaBrowserConnector(
+                    @NonNull ComponentName browseService) {
+                mRequestedBrowseService = browseService;
+                return mMediaBrowserState;
+            }
+
+            @Override
+            public LiveData<MediaController> createActiveMediaController() {
+                return mActiveMediaController;
+            }
+
+            @Override
+            public MediaController getControllerForPackage(String packageName) {
+                return mMediaControllerForPackage;
+            }
+
+            @Override
+            public MediaController getControllerForSession(@Nullable MediaSession.Token token) {
+                return mMediaControllerForSession;
+            }
+        });
+        mViewModel.setSelectedMediaSource(mMediaSource);
+    }
+
+
+    @Test
+    public void testGetMediaSources() {
+        assertThat(mViewModel.getMediaSources()).isSameAs(mMediaSources);
+    }
+
+    @Test
+    public void testHasMediaSources() {
+        CaptureObserver<Boolean> observer = new CaptureObserver<>();
+        mMediaSources.setValue(Collections.singletonList(mMediaSource));
+
+        mViewModel.hasMediaSources().observe(mLifecycleOwner, observer);
+
+        assertThat(observer.hasBeenNotified()).isTrue();
+        assertThat(observer.getObservedValue()).isTrue();
+        observer.reset();
+
+        mMediaSources.setValue(Collections.emptyList());
+
+        assertThat(observer.hasBeenNotified()).isTrue();
+        assertThat(observer.getObservedValue()).isFalse();
+        observer.reset();
+
+        mMediaSources.setValue(null);
+
+        assertThat(observer.hasBeenNotified()).isTrue();
+        assertThat(observer.getObservedValue()).isFalse();
+    }
+
+    @Test
+    public void testGetSelectedMediaSource() {
+        CaptureObserver<SimpleMediaSource> observer = new CaptureObserver<>();
+
+        mViewModel.getSelectedMediaSource().observe(mLifecycleOwner, observer);
+
+        assertThat(observer.getObservedValue()).isSameAs(mMediaSource);
+    }
+
+    @Test
+    public void testGetMediaController_fromActiveSession() {
+        CaptureObserver<MediaController> observer = new CaptureObserver<>();
+
+        mViewModel.getMediaController().observe(mLifecycleOwner, observer);
+
+        assertThat(observer.getObservedValue()).isSameAs(mMediaControllerForPackage);
+        assertThat(mRequestedBrowseService).isNull();
+    }
+
+    @Test
+    public void testGetMediaController_noActiveSession() {
+        CaptureObserver<MediaController> observer = new CaptureObserver<>();
+        mMediaControllerForPackage = null;
+        ComponentName testComponent = new ComponentName("test", "test");
+        when(mMediaSource.getBrowseServiceComponentName()).thenReturn(testComponent);
+        mMediaBrowserState.setValue(new MediaBrowserState(mMediaBrowser,
+                MediaBrowserConnector.ConnectionState.CONNECTED));
+
+        mViewModel.getMediaController().observe(mLifecycleOwner, observer);
+
+        assertThat(observer.getObservedValue()).isSameAs(mMediaControllerForSession);
+        assertThat(mRequestedBrowseService).isEqualTo(testComponent);
+    }
+
+    @Test
+    public void testGetMediaController_noActiveSession_noBrowseService() {
+        CaptureObserver<MediaController> observer = new CaptureObserver<>();
+        mMediaControllerForPackage = null;
+        when(mMediaSource.getBrowseServiceComponentName()).thenReturn(null);
+        mMediaBrowserState.setValue(new MediaBrowserState(mMediaBrowser,
+                MediaBrowserConnector.ConnectionState.CONNECTED));
+
+        mViewModel.getMediaController().observe(mLifecycleOwner, observer);
+
+        assertThat(observer.hasBeenNotified()).isTrue();
+        assertThat(observer.getObservedValue()).isNull();
+        assertThat(mRequestedBrowseService).isEqualTo(null);
+    }
+
+    @Test
+    public void testGetMediaController_noActiveSession_notConnected() {
+        CaptureObserver<MediaController> observer = new CaptureObserver<>();
+        mMediaControllerForPackage = null;
+        ComponentName testComponent = new ComponentName("test", "test");
+        when(mMediaSource.getBrowseServiceComponentName()).thenReturn(testComponent);
+        mMediaBrowserState.setValue(new MediaBrowserState(mMediaBrowser,
+                MediaBrowserConnector.ConnectionState.CONNECTING));
+
+        mViewModel.getMediaController().observe(mLifecycleOwner, observer);
+
+        assertThat(observer.hasBeenNotified()).isTrue();
+        assertThat(observer.getObservedValue()).isNull();
+        assertThat(mRequestedBrowseService).isEqualTo(testComponent);
+    }
+
+    @Test
+    public void testGetActiveMediaController() {
+        assertThat(mViewModel.getActiveMediaController()).isSameAs(mActiveMediaController);
+    }
+
+    @Test
+    public void testIsCurrentMediaSourcePlaying() {
+        CaptureObserver<Boolean> observer = new CaptureObserver<>();
+        ComponentName testComponent = new ComponentName("test", "test");
+        when(mMediaSource.getBrowseServiceComponentName()).thenReturn(testComponent);
+        when(mMediaSource.getPackageName()).thenReturn(SESSION_CONTROLLER_PACKAGE_NAME);
+        mMediaBrowserState.setValue(new MediaBrowserState(mMediaBrowser,
+                MediaBrowserConnector.ConnectionState.CONNECTED));
+        mActiveMediaController.setValue(mMediaControllerForSession);
+
+        mViewModel.isCurrentMediaSourcePlaying().observe(mLifecycleOwner, observer);
+
+        assertThat(observer.getObservedValue()).isTrue();
+
+        mActiveMediaController.setValue(mMediaControllerForPackage);
+
+        assertThat(observer.getObservedValue()).isFalse();
+    }
+}
diff --git a/car-media-common/tests/robotests/src/com/android/car/media/common/source/MediaSourcesLiveDataTest.java b/car-media-common/tests/robotests/src/com/android/car/media/common/source/MediaSourcesLiveDataTest.java
new file mode 100644
index 0000000..01f4190
--- /dev/null
+++ b/car-media-common/tests/robotests/src/com/android/car/media/common/source/MediaSourcesLiveDataTest.java
@@ -0,0 +1,246 @@
+/*
+ * 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.source;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.robolectric.RuntimeEnvironment.application;
+import static org.robolectric.Shadows.shadowOf;
+
+import android.annotation.NonNull;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.os.Bundle;
+import android.service.media.MediaBrowserService;
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule;
+
+import com.android.car.arch.common.testing.CaptureObserver;
+import com.android.car.arch.common.testing.TestLifecycleOwner;
+import com.android.car.media.common.TestConfig;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowApplication;
+import org.robolectric.shadows.ShadowPackageManager;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
+public class MediaSourcesLiveDataTest {
+
+    @Rule
+    public final MockitoRule mMockitoRule = MockitoJUnit.rule();
+    @Rule
+    public final InstantTaskExecutorRule mTaskExecutorRule = new InstantTaskExecutorRule();
+    @Rule
+    public final TestLifecycleOwner mLifecycleOwner = new TestLifecycleOwner();
+
+    private static final String TEST_ACTIVITY_PACKAGE_1 = "activity_package1";
+    private static final String TEST_ACTIVITY_PACKAGE_2 = "activity_package2";
+    private static final String TEST_SERVICE_PACKAGE_1 = "service_package1";
+    private static final String TEST_SERVICE_PACKAGE_2 = "service_package2";
+    private static final String TEST_SERVICE_PACKAGE_WITH_METADATA = "service_package3";
+
+    private Intent mActivityIntent;
+    private Intent mServiceIntent;
+
+    @Before
+    public void setUp() {
+        mActivityIntent = new Intent(Intent.ACTION_MAIN, null);
+        mActivityIntent.addCategory(Intent.CATEGORY_APP_MUSIC);
+
+        mServiceIntent = new Intent(MediaBrowserService.SERVICE_INTERFACE);
+
+        ShadowPackageManager packageManager = shadowOf(application.getPackageManager());
+
+        List<ResolveInfo> activityResolveInfo = buildActivityResolveInfo();
+        List<ResolveInfo> allActivityResolveInfo = new ArrayList<>(activityResolveInfo);
+        allActivityResolveInfo.add(newActivityResolveInfo(TEST_ACTIVITY_PACKAGE_2));
+        for (ResolveInfo info : allActivityResolveInfo) {
+            PackageInfo packageInfo = new PackageInfo();
+            packageInfo.activities = new ActivityInfo[]{info.activityInfo};
+            packageInfo.packageName = info.activityInfo.packageName;
+            packageInfo.applicationInfo = info.activityInfo.applicationInfo;
+            packageManager.addPackage(packageInfo);
+        }
+        List<ResolveInfo> serviceResolveInfo = buildServiceResolveInfo();
+        List<ResolveInfo> allServiceResolveInfo = new ArrayList<>(serviceResolveInfo);
+        allServiceResolveInfo.add(newServiceResolveInfo(TEST_SERVICE_PACKAGE_2));
+        for (ResolveInfo info : allServiceResolveInfo) {
+            PackageInfo packageInfo = new PackageInfo();
+            packageInfo.services = new ServiceInfo[]{info.serviceInfo};
+            packageInfo.packageName = info.serviceInfo.packageName;
+            packageInfo.applicationInfo = info.serviceInfo.applicationInfo;
+            packageManager.addPackage(packageInfo);
+        }
+        setPackageManagerResolveInfos(activityResolveInfo, serviceResolveInfo);
+    }
+
+    @Test
+    public void testGetAppsOnActive() {
+        CaptureObserver<List<SimpleMediaSource>> observer = new CaptureObserver<>();
+        MediaSourcesLiveData liveData = new MediaSourcesLiveData(application);
+
+        liveData.observe(mLifecycleOwner, observer);
+        assertThat(observer.hasBeenNotified()).isTrue();
+        List<SimpleMediaSource> observedValue = observer.getObservedValue();
+        assertThat(observedValue).isNotNull();
+        assertThat(
+                observedValue.stream().map(SimpleMediaSource::getPackageName)
+                        .collect(Collectors.toList()))
+                .containsExactly(TEST_ACTIVITY_PACKAGE_1, TEST_SERVICE_PACKAGE_1,
+                        TEST_SERVICE_PACKAGE_WITH_METADATA);
+    }
+
+    @Test
+    public void testGetAppsOnPackageAdded() {
+        CaptureObserver<List<SimpleMediaSource>> observer = new CaptureObserver<>();
+        MediaSourcesLiveData liveData = new MediaSourcesLiveData(application);
+        liveData.observe(mLifecycleOwner, observer);
+        observer.reset();
+
+        List<ResolveInfo> activityResolveInfo = buildActivityResolveInfo();
+        activityResolveInfo.add(newActivityResolveInfo(TEST_ACTIVITY_PACKAGE_2));
+        List<ResolveInfo> serviceResolveInfo = buildServiceResolveInfo();
+        serviceResolveInfo.add(newServiceResolveInfo(TEST_SERVICE_PACKAGE_2));
+        setPackageManagerResolveInfos(activityResolveInfo, serviceResolveInfo);
+
+        Intent packageAdded = new Intent(Intent.ACTION_PACKAGE_ADDED);
+        ShadowApplication.getInstance().getRegisteredReceivers().stream()
+                .filter(wrapper -> wrapper.intentFilter.hasAction(Intent.ACTION_PACKAGE_ADDED))
+                .map(wrapper -> wrapper.broadcastReceiver)
+                .forEach(broadcastReceiver -> broadcastReceiver.onReceive(application,
+                        packageAdded));
+
+        assertThat(observer.hasBeenNotified()).isTrue();
+        List<SimpleMediaSource> observedValue = observer.getObservedValue();
+        assertThat(observedValue).isNotNull();
+        assertThat(
+                observedValue.stream().map(SimpleMediaSource::getPackageName)
+                        .collect(Collectors.toList()))
+                .containsExactly(TEST_ACTIVITY_PACKAGE_1, TEST_ACTIVITY_PACKAGE_2,
+                        TEST_SERVICE_PACKAGE_1, TEST_SERVICE_PACKAGE_2,
+                        TEST_SERVICE_PACKAGE_WITH_METADATA);
+    }
+
+    @Test
+    public void testGetAppsOnPackageRemoved() {
+        CaptureObserver<List<SimpleMediaSource>> observer = new CaptureObserver<>();
+        MediaSourcesLiveData liveData = new MediaSourcesLiveData(application);
+        liveData.observe(mLifecycleOwner, observer);
+        observer.reset();
+
+        List<ResolveInfo> activityResolveInfo = buildActivityResolveInfo();
+        List<ResolveInfo> serviceResolveInfo = buildServiceResolveInfo();
+        serviceResolveInfo.remove(0);
+        setPackageManagerResolveInfos(activityResolveInfo, serviceResolveInfo);
+
+        Intent packageRemoved = new Intent(Intent.ACTION_PACKAGE_REMOVED);
+        ShadowApplication.getInstance().getRegisteredReceivers().stream()
+                .filter(wrapper -> wrapper.intentFilter.hasAction(Intent.ACTION_PACKAGE_REMOVED))
+                .map(wrapper -> wrapper.broadcastReceiver)
+                .forEach(broadcastReceiver ->
+                        broadcastReceiver.onReceive(application, packageRemoved));
+
+        assertThat(observer.hasBeenNotified()).isTrue();
+        List<SimpleMediaSource> observedValue = observer.getObservedValue();
+        assertThat(observedValue).isNotNull();
+        assertThat(
+                observedValue.stream().map(SimpleMediaSource::getPackageName)
+                        .collect(Collectors.toList()))
+                .containsExactly(TEST_ACTIVITY_PACKAGE_1, TEST_SERVICE_PACKAGE_WITH_METADATA);
+    }
+
+    @NonNull
+    private List<ResolveInfo> buildActivityResolveInfo() {
+        List<ResolveInfo> activityResolveInfo = new ArrayList<>();
+        activityResolveInfo.add(newActivityResolveInfo(TEST_ACTIVITY_PACKAGE_1));
+        return activityResolveInfo;
+    }
+
+    @NonNull
+    private List<ResolveInfo> buildServiceResolveInfo() {
+        List<ResolveInfo> serviceResolveInfo = new ArrayList<>();
+        serviceResolveInfo.add(newServiceResolveInfo(TEST_SERVICE_PACKAGE_1));
+        ResolveInfo withMetadata = newServiceResolveInfo(TEST_SERVICE_PACKAGE_WITH_METADATA);
+        withMetadata.serviceInfo.applicationInfo.metaData = new Bundle();
+        serviceResolveInfo.add(withMetadata);
+        return serviceResolveInfo;
+    }
+
+    private void setPackageManagerResolveInfos(List<ResolveInfo> activityResolveInfo,
+            List<ResolveInfo> serviceResolveInfo) {
+        ShadowPackageManager packageManager = shadowOf(application.getPackageManager());
+        packageManager.removeResolveInfosForIntent(mActivityIntent, TEST_ACTIVITY_PACKAGE_1);
+        packageManager.removeResolveInfosForIntent(mActivityIntent, TEST_ACTIVITY_PACKAGE_2);
+        packageManager.removeResolveInfosForIntent(mServiceIntent, TEST_SERVICE_PACKAGE_1);
+        packageManager.removeResolveInfosForIntent(mServiceIntent, TEST_SERVICE_PACKAGE_2);
+        packageManager.removeResolveInfosForIntent(mServiceIntent,
+                TEST_SERVICE_PACKAGE_WITH_METADATA);
+        packageManager.addResolveInfoForIntent(mActivityIntent, activityResolveInfo);
+
+        packageManager.addResolveInfoForIntent(mServiceIntent, serviceResolveInfo);
+        for (ResolveInfo info : serviceResolveInfo) {
+            Intent intent = new Intent(mServiceIntent);
+            intent.setPackage(info.serviceInfo.packageName);
+            packageManager.addResolveInfoForIntent(intent, info);
+        }
+    }
+
+
+    private ResolveInfo newActivityResolveInfo(String packageName) {
+        ResolveInfo resolveInfo = new ResolveInfo();
+        ActivityInfo activityInfo = new ActivityInfo();
+        activityInfo.packageName = packageName;
+        activityInfo.name = "activity";
+        activityInfo.nonLocalizedLabel = "Activity Label " + packageName;
+        ApplicationInfo applicationInfo = new ApplicationInfo();
+        applicationInfo.packageName = packageName;
+        applicationInfo.nonLocalizedLabel = "Activity Label " + packageName;
+        activityInfo.applicationInfo = applicationInfo;
+        resolveInfo.activityInfo = activityInfo;
+        return resolveInfo;
+    }
+
+    private ResolveInfo newServiceResolveInfo(String packageName) {
+        ResolveInfo resolveInfo = new ResolveInfo();
+        ServiceInfo serviceInfo = new ServiceInfo();
+        serviceInfo.packageName = packageName;
+        serviceInfo.name = "service";
+        serviceInfo.nonLocalizedLabel = "Service Label " + packageName;
+        ApplicationInfo applicationInfo = new ApplicationInfo();
+        applicationInfo.packageName = packageName;
+        applicationInfo.nonLocalizedLabel = "Service Label " + packageName;
+        serviceInfo.applicationInfo = applicationInfo;
+        resolveInfo.serviceInfo = serviceInfo;
+        return resolveInfo;
+    }
+}