blob: 45081b5d27abf8af43eaea0b629b5ea0f7fd0042 [file] [log] [blame]
/*
* 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.dataOf;
import static com.android.car.arch.common.LiveDataFunctions.nullLiveData;
import static com.android.car.arch.common.LiveDataFunctions.pair;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.Application;
import android.content.ComponentName;
import android.content.ContentValues;
import android.content.Context;
import android.database.ContentObserver;
import android.database.Cursor;
import android.media.session.MediaController;
import android.media.session.MediaSessionManager;
import android.os.Handler;
import android.os.RemoteException;
import android.support.v4.media.MediaBrowserCompat;
import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
import androidx.arch.core.util.Function;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModelProviders;
import com.android.car.media.common.MediaConstants;
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.function.BiFunction;
import java.util.stream.Collectors;
/**
* Contains observable data needed for displaying playback and browse UI
*/
public class MediaSourceViewModel extends AndroidViewModel {
private static final String TAG = "MediaSourceViewModel";
private final MutableLiveData<MediaSource> mPrimaryMediaSource = dataOf(null);
private final LiveData<MediaBrowserCompat> mConnectedMediaBrowser;
// Media controller for selected media source.
private final LiveData<MediaControllerCompat> mMediaController;
/**
* Factory for creating dependencies. Can be swapped out for testing.
*/
@VisibleForTesting
interface InputFactory {
LiveData<MediaBrowserState> createMediaBrowserConnector(
@NonNull ComponentName browseService);
List<MediaControllerCompat> getActiveMediaControllers();
MediaControllerCompat getControllerForSession(@Nullable MediaSessionCompat.Token session);
MediaSource getSelectedSourceFromContentProvider();
}
/** Returns the MediaSourceViewModel tied to the given activity. */
public static MediaSourceViewModel get(@NonNull FragmentActivity activity) {
return ViewModelProviders.of(activity).get(MediaSourceViewModel.class);
}
/**
* Create a new instance of MediaSourceViewModel
*
* @see AndroidViewModel
*/
public MediaSourceViewModel(@NonNull Application application) {
this(application, new InputFactory() {
private final MediaSessionManager mMediaSessionManager =
application.getSystemService(MediaSessionManager.class);
@Override
public LiveData<MediaBrowserState> createMediaBrowserConnector(
@NonNull ComponentName browseService) {
return new MediaBrowserConnector(application, browseService);
}
@Override
public List<MediaControllerCompat> getActiveMediaControllers() {
return toCompatList(mMediaSessionManager.getActiveSessions(null), application);
}
@Override
public MediaControllerCompat getControllerForSession(
@Nullable MediaSessionCompat.Token token) {
if (token == null) return null;
try {
return new MediaControllerCompat(application, token);
} catch (RemoteException e) {
Log.e(TAG, "Couldn't get MediaControllerCompat", e);
return null;
}
}
@Override
public MediaSource getSelectedSourceFromContentProvider() {
Cursor cursor = application.getContentResolver().query(
MediaConstants.URI_MEDIA_SOURCE, null, null, null);
try {
if (cursor != null && cursor.moveToFirst()) {
return new MediaSource(application, cursor.getString(0));
}
return null;
} finally {
if (cursor != null) {
cursor.close();
}
}
}
});
}
@VisibleForTesting
MediaSourceViewModel(@NonNull Application application, @NonNull InputFactory inputFactory) {
super(application);
mPrimaryMediaSource.setValue(inputFactory.getSelectedSourceFromContentProvider());
application.getContentResolver().registerContentObserver(
MediaConstants.URI_MEDIA_SOURCE, false,
new ContentObserver(new Handler()) {
@Override
public void onChange(boolean selfChange) {
mPrimaryMediaSource.setValue(
inputFactory.getSelectedSourceFromContentProvider());
}
});
LiveData<MediaBrowserState> mediaBrowserState = switchMap(mPrimaryMediaSource,
(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);
mMediaController = map(pair(mPrimaryMediaSource, mConnectedMediaBrowser),
(pair) -> {
MediaSource mediaSource = pair.first;
MediaBrowserCompat browser = pair.second;
// Prefer fetching MediaController from MediaSessionManager's active controller
// list. Otherwise use controller from MediaBrowser (which requires connecting
// to it).
if (mediaSource != null) {
List<MediaControllerCompat> controllers =
inputFactory.getActiveMediaControllers();
String packageName = mediaSource.getPackageName();
for (MediaControllerCompat controller : controllers) {
if (controller != null && packageName.equals(
controller.getPackageName())) {
return controller;
}
}
}
if (browser != null) {
return inputFactory.getControllerForSession(browser.getSessionToken());
}
return null;
});
}
private static List<MediaControllerCompat> toCompatList(List<MediaController> mediaControllers,
Context context) {
return mediaControllers.stream().map(
controller -> toCompatController(controller, context)).collect(Collectors.toList());
}
private static MediaControllerCompat toCompatController(MediaController mediaController,
Context context) {
MediaSessionCompat.Token token =
MediaSessionCompat.Token.fromToken(mediaController.getSessionToken());
MediaControllerCompat controllerCompat = null;
try {
controllerCompat = new MediaControllerCompat(context, token);
} catch (RemoteException e) {
Log.e(TAG, "Couldn't get MediaController", e);
}
return controllerCompat;
}
/**
* Returns a LiveData that emits the MediaSource that is to be browsed or displayed.
*/
public LiveData<MediaSource> getPrimaryMediaSource() {
return mPrimaryMediaSource;
}
/**
* Updates the primary media source, and notifies content provider of new source
*/
public void setPrimaryMediaSource(MediaSource mediaSource) {
ContentValues values = new ContentValues();
values.put(MediaConstants.KEY_PACKAGE_NAME, mediaSource.getPackageName());
getApplication().getContentResolver().update(
MediaConstants.URI_MEDIA_SOURCE, values, null, null);
}
/**
* 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<MediaBrowserCompat> 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<MediaControllerCompat> getMediaController() {
return mMediaController;
}
private static class LoopbackFunction<P, V> implements Function<P, V> {
private final BiFunction<P, V, V> mFunction;
private V mLastValue;
private LoopbackFunction(BiFunction<P, V, V> function) {
mFunction = function;
}
@Override
public V apply(P param) {
mLastValue = mFunction.apply(param, mLastValue);
return mLastValue;
}
}
}