VancedMicroG/play-services-core/src/main/java/org/microg/gms/cast/CastMediaRouteProvider.java

345 lines
13 KiB
Java

/*
* Copyright (C) 2013-2017 microG Project Team
*
* 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 org.microg.gms.cast;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.IntentFilter;
import android.net.nsd.NsdManager;
import android.net.nsd.NsdServiceInfo;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import androidx.mediarouter.media.MediaControlIntent;
import androidx.mediarouter.media.MediaRouteDescriptor;
import androidx.mediarouter.media.MediaRouteDiscoveryRequest;
import androidx.mediarouter.media.MediaRouteProvider;
import androidx.mediarouter.media.MediaRouteProviderDescriptor;
import androidx.mediarouter.media.MediaRouter;
import com.google.android.gms.cast.CastDevice;
import com.google.android.gms.cast.CastMediaControlIntent;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class CastMediaRouteProvider extends MediaRouteProvider {
private static final String TAG = CastMediaRouteProvider.class.getSimpleName();
private Map<String, CastDevice> castDevices = new HashMap<>();
private Map<String, String> serviceCastIds = new HashMap<>();
private NsdManager mNsdManager;
private NsdManager.DiscoveryListener mDiscoveryListener;
private List<String> customCategories = new ArrayList<>();
private enum State {
NOT_DISCOVERING,
DISCOVERY_REQUESTED,
DISCOVERING,
DISCOVERY_STOP_REQUESTED,
}
private State state = State.NOT_DISCOVERING;
private static final ArrayList<IntentFilter> BASE_CONTROL_FILTERS = new ArrayList<>();
static {
IntentFilter filter;
filter = new IntentFilter();
filter.addCategory(CastMediaControlIntent.CATEGORY_CAST);
BASE_CONTROL_FILTERS.add(filter);
filter = new IntentFilter();
filter.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
filter.addAction(MediaControlIntent.ACTION_PLAY);
filter.addDataScheme("http");
filter.addDataScheme("https");
String[] types = {
"image/jpeg",
"image/pjpeg",
"image/jpg",
"image/webp",
"image/png",
"image/gif",
"image/bmp",
"image/vnd.microsoft.icon",
"image/x-icon",
"image/x-xbitmap",
"audio/wav",
"audio/x-wav",
"audio/mp3",
"audio/x-mp3",
"audio/x-m4a",
"audio/mpeg",
"audio/webm",
"audio/ogg",
"audio/x-matroska",
"video/mp4",
"video/x-m4v",
"video/mp2t",
"video/webm",
"video/ogg",
"video/x-matroska",
"application/x-mpegurl",
"application/vnd.apple.mpegurl",
"application/dash+xml",
"application/vnd.ms-sstr+xml",
};
for (String type : types) {
try {
filter.addDataType(type);
} catch (IntentFilter.MalformedMimeTypeException ex) {
Log.e(TAG, "Error adding filter type " + type);
}
}
BASE_CONTROL_FILTERS.add(filter);
filter = new IntentFilter();
filter.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
filter.addAction(MediaControlIntent.ACTION_PAUSE);
BASE_CONTROL_FILTERS.add(filter);
filter = new IntentFilter();
filter.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
filter.addAction(MediaControlIntent.ACTION_RESUME);
BASE_CONTROL_FILTERS.add(filter);
filter = new IntentFilter();
filter.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
filter.addAction(MediaControlIntent.ACTION_STOP);
BASE_CONTROL_FILTERS.add(filter);
filter = new IntentFilter();
filter.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
filter.addAction(MediaControlIntent.ACTION_SEEK);
BASE_CONTROL_FILTERS.add(filter);
filter = new IntentFilter();
filter.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
filter.addAction(MediaControlIntent.ACTION_GET_STATUS);
BASE_CONTROL_FILTERS.add(filter);
filter = new IntentFilter();
filter.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
filter.addAction(MediaControlIntent.ACTION_START_SESSION);
BASE_CONTROL_FILTERS.add(filter);
filter = new IntentFilter();
filter.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
filter.addAction(MediaControlIntent.ACTION_GET_SESSION_STATUS);
BASE_CONTROL_FILTERS.add(filter);
filter = new IntentFilter();
filter.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
filter.addAction(MediaControlIntent.ACTION_END_SESSION);
BASE_CONTROL_FILTERS.add(filter);
filter = new IntentFilter();
filter.addCategory(CastMediaControlIntent.CATEGORY_CAST_REMOTE_PLAYBACK);
filter.addAction(CastMediaControlIntent.ACTION_SYNC_STATUS);
BASE_CONTROL_FILTERS.add(filter);
filter = new IntentFilter();
filter.addCategory(CastMediaControlIntent.CATEGORY_CAST_REMOTE_PLAYBACK);
filter.addAction(CastMediaControlIntent.ACTION_SYNC_STATUS);
BASE_CONTROL_FILTERS.add(filter);
}
@SuppressLint("NewApi")
public CastMediaRouteProvider(Context context) {
super(context);
if (android.os.Build.VERSION.SDK_INT < 16) {
Log.i(TAG, "Cast discovery disabled. Android SDK version 16 or higher required.");
return;
}
mNsdManager = (NsdManager)context.getSystemService(Context.NSD_SERVICE);
mDiscoveryListener = new NsdManager.DiscoveryListener() {
@Override
public void onDiscoveryStarted(String regType) {
CastMediaRouteProvider.this.state = State.DISCOVERING;
}
@Override
public void onServiceFound(NsdServiceInfo service) {
mNsdManager.resolveService(service, new NsdManager.ResolveListener() {
@Override
public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) {
if (errorCode == NsdManager.FAILURE_ALREADY_ACTIVE) {
return;
}
Log.e(TAG, "DiscoveryListener Resolve failed. Error code " + errorCode);
}
@Override
public void onServiceResolved(NsdServiceInfo serviceInfo) {
String name = serviceInfo.getServiceName();
InetAddress host = serviceInfo.getHost();
int port = serviceInfo.getPort();
Map<String, byte[]> attributes = serviceInfo.getAttributes();
if (attributes == null) {
Log.e(TAG, "Error getting service attributes from DNS-SD response");
return;
}
try {
String id = new String(attributes.get("id"), StandardCharsets.UTF_8);
String deviceVersion = new String(attributes.get("ve"), StandardCharsets.UTF_8);
String friendlyName = new String(attributes.get("fn"), StandardCharsets.UTF_8);
String modelName = new String(attributes.get("md"), StandardCharsets.UTF_8);
String iconPath = new String(attributes.get("ic"), StandardCharsets.UTF_8);
int status = Integer.parseInt(new String(attributes.get("st"), StandardCharsets.UTF_8));
onChromeCastDiscovered(id, name, host, port, deviceVersion, friendlyName, modelName, iconPath, status);
} catch (NullPointerException ex) {
Log.e(TAG, "Error getting cast details from DNS-SD response", ex);
}
}
});
}
@Override
public void onServiceLost(NsdServiceInfo serviceInfo) {
String name = serviceInfo.getServiceName();
onChromeCastLost(name);
}
@Override
public void onDiscoveryStopped(String serviceType) {
CastMediaRouteProvider.this.state = State.NOT_DISCOVERING;
}
@Override
public void onStartDiscoveryFailed(String serviceType, int errorCode) {
CastMediaRouteProvider.this.state = State.NOT_DISCOVERING;
}
@Override
public void onStopDiscoveryFailed(String serviceType, int errorCode) {
CastMediaRouteProvider.this.state = State.DISCOVERING;
}
};
}
private void onChromeCastDiscovered(
String id, String name, InetAddress host, int port, String
deviceVersion, String friendlyName, String modelName, String
iconPath, int status) {
if (!this.castDevices.containsKey(id)) {
// TODO: Capabilities
int capabilities = CastDevice.CAPABILITY_VIDEO_OUT | CastDevice.CAPABILITY_AUDIO_OUT;
CastDevice castDevice = new CastDevice(id, name, host, port, deviceVersion, friendlyName, modelName, iconPath, status, capabilities);
this.castDevices.put(id, castDevice);
this.serviceCastIds.put(name, id);
}
publishRoutesInMainThread();
}
private void onChromeCastLost(String name) {
String id = this.serviceCastIds.remove(name);
if (id != null) {
this.castDevices.remove(id);
}
publishRoutesInMainThread();
}
@SuppressLint("NewApi")
@Override
public void onDiscoveryRequestChanged(MediaRouteDiscoveryRequest request) {
if (android.os.Build.VERSION.SDK_INT < 16) {
return;
}
if (request != null && request.isValid() && request.isActiveScan()) {
if (request.getSelector() != null) {
for (String category : request.getSelector().getControlCategories()) {
if (CastMediaControlIntent.isCategoryForCast(category)) {
this.customCategories.add(category);
}
}
}
if (this.state == State.NOT_DISCOVERING) {
mNsdManager.discoverServices("_googlecast._tcp.", NsdManager.PROTOCOL_DNS_SD, mDiscoveryListener);
this.state = State.DISCOVERY_REQUESTED;
}
} else {
if (this.state == State.DISCOVERING) {
mNsdManager.stopServiceDiscovery(mDiscoveryListener);
this.state = State.DISCOVERY_STOP_REQUESTED;
}
}
}
@Override
public RouteController onCreateRouteController(String routeId) {
CastDevice castDevice = this.castDevices.get(routeId);
if (castDevice == null) {
return null;
}
return new CastMediaRouteController(this, routeId, castDevice.getAddress());
}
private void publishRoutesInMainThread() {
Handler mainHandler = new Handler(this.getContext().getMainLooper());
mainHandler.post(this::publishRoutes);
}
private void publishRoutes() {
MediaRouteProviderDescriptor.Builder builder = new MediaRouteProviderDescriptor.Builder();
for (CastDevice castDevice : this.castDevices.values()) {
ArrayList<IntentFilter> controlFilters = new ArrayList<>(BASE_CONTROL_FILTERS);
// Include any app-specific control filters that have been requested.
// TODO: Do we need to check with the device?
for (String category : this.customCategories) {
IntentFilter filter = new IntentFilter();
filter.addCategory(category);
controlFilters.add(filter);
}
Bundle extras = new Bundle();
castDevice.putInBundle(extras);
MediaRouteDescriptor route = new MediaRouteDescriptor.Builder(
castDevice.getDeviceId(),
castDevice.getFriendlyName())
.setDescription(castDevice.getModelName())
.addControlFilters(controlFilters)
.setDeviceType(MediaRouter.RouteInfo.DEVICE_TYPE_TV)
.setPlaybackType(MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE)
.setVolumeHandling(MediaRouter.RouteInfo.PLAYBACK_VOLUME_FIXED)
.setVolumeMax(20)
.setVolume(0)
.setEnabled(true)
.setExtras(extras)
.setConnectionState(MediaRouter.RouteInfo.CONNECTION_STATE_DISCONNECTED)
.build();
builder.addRoute(route);
}
this.setDescriptor(builder.build());
}
}