Repo Created

This commit is contained in:
Fr4nz D13trich 2025-11-15 17:44:12 +01:00
parent eb305e2886
commit a8c22c65db
4784 changed files with 329907 additions and 2 deletions

View file

@ -0,0 +1,61 @@
/*
* SPDX-FileCopyrightText: 2022 microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'maven-publish'
apply plugin: 'signing'
dependencies {
implementation project(':play-services-cast')
implementation project(':play-services-base-core')
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVersion"
// TODO: Switch to upstream once raw requests are merged
// https://github.com/vitalidze/chromecast-java-api-v2/pull/99
// implementation "su.litvak.chromecast:api-v2:0.10.4"
implementation "info.armills.chromecast-java-api-v2:api-v2-raw-request:0.10.4-raw-request-1"
}
android {
namespace "org.microg.gms.cast.core"
compileSdkVersion androidCompileSdk
buildToolsVersion "$androidBuildVersionTools"
defaultConfig {
versionName version
minSdkVersion androidMinSdk
targetSdkVersion androidTargetSdk
}
buildFeatures {
dataBinding = true
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
lintOptions {
disable 'MissingTranslation'
}
compileOptions {
sourceCompatibility = 1.8
targetCompatibility = 1.8
}
kotlinOptions {
jvmTarget = 1.8
}
}
apply from: '../../gradle/publish-android.gradle'
description = 'microG service implementation for play-services-cast'

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2022 microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<service android:name="com.google.android.gms.cast.media.CastMediaRouteProviderService">
<intent-filter>
<action android:name="android.media.MediaRouteProviderService" />
</intent-filter>
</service>
<service android:name="org.microg.gms.cast.CastDeviceControllerService">
<intent-filter>
<action android:name="com.google.android.gms.cast.service.BIND_CAST_DEVICE_CONTROLLER_SERVICE" />
</intent-filter>
</service>
</application>
</manifest>

View file

@ -0,0 +1,33 @@
/*
* 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 com.google.android.gms.cast.media;
import org.microg.gms.cast.CastMediaRouteProvider;
import android.util.Log;
import androidx.mediarouter.media.MediaRouteProvider;
import androidx.mediarouter.media.MediaRouteProviderService;
public class CastMediaRouteProviderService extends MediaRouteProviderService {
private static final String TAG = CastMediaRouteProviderService.class.getSimpleName();
@Override
public MediaRouteProvider onCreateMediaRouteProvider() {
return new CastMediaRouteProvider(this);
}
}

View file

@ -0,0 +1,328 @@
/*
* 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 java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.os.Parcel;
import android.os.RemoteException;
import android.util.Base64;
import android.util.Log;
import com.google.android.gms.cast.ApplicationMetadata;
import com.google.android.gms.cast.ApplicationStatus;
import com.google.android.gms.cast.CastDevice;
import com.google.android.gms.cast.CastDeviceStatus;
import com.google.android.gms.cast.JoinOptions;
import com.google.android.gms.cast.LaunchOptions;
import com.google.android.gms.cast.internal.ICastDeviceController;
import com.google.android.gms.cast.internal.ICastDeviceControllerListener;
import com.google.android.gms.common.api.CommonStatusCodes;
import com.google.android.gms.common.api.Status;
import com.google.android.gms.common.images.WebImage;
import com.google.android.gms.common.internal.BinderWrapper;
import com.google.android.gms.common.internal.GetServiceRequest;
import su.litvak.chromecast.api.v2.Application;
import su.litvak.chromecast.api.v2.ChromeCast;
import su.litvak.chromecast.api.v2.Namespace;
import su.litvak.chromecast.api.v2.ChromeCastConnectionEventListener;
import su.litvak.chromecast.api.v2.ChromeCastSpontaneousEventListener;
import su.litvak.chromecast.api.v2.ChromeCastRawMessageListener;
import su.litvak.chromecast.api.v2.ChromeCastConnectionEvent;
import su.litvak.chromecast.api.v2.ChromeCastSpontaneousEvent;
import su.litvak.chromecast.api.v2.ChromeCastRawMessage;
import su.litvak.chromecast.api.v2.AppEvent;
public class CastDeviceControllerImpl extends ICastDeviceController.Stub implements
ChromeCastConnectionEventListener,
ChromeCastSpontaneousEventListener,
ChromeCastRawMessageListener,
ICastDeviceControllerListener
{
private static final String TAG = "GmsCastDeviceController";
private Context context;
private String packageName;
private CastDevice castDevice;
boolean notificationEnabled;
long castFlags;
ICastDeviceControllerListener listener;
ChromeCast chromecast;
String sessionId = null;
public CastDeviceControllerImpl(Context context, String packageName, Bundle extras) {
this.context = context;
this.packageName = packageName;
extras.setClassLoader(BinderWrapper.class.getClassLoader());
this.castDevice = CastDevice.getFromBundle(extras);
this.notificationEnabled = extras.getBoolean("com.google.android.gms.cast.EXTRA_CAST_FRAMEWORK_NOTIFICATION_ENABLED");
this.castFlags = extras.getLong("com.google.android.gms.cast.EXTRA_CAST_FLAGS");
BinderWrapper listenerWrapper = (BinderWrapper)extras.get("listener");
if (listenerWrapper != null) {
this.listener = ICastDeviceControllerListener.Stub.asInterface(listenerWrapper.binder);
}
this.chromecast = new ChromeCast(this.castDevice.getAddress());
this.chromecast.registerListener(this);
this.chromecast.registerRawMessageListener(this);
this.chromecast.registerConnectionListener(this);
}
@Override
public void connectionEventReceived(ChromeCastConnectionEvent event) {
if (!event.isConnected()) {
this.onDisconnected(CommonStatusCodes.SUCCESS);
}
}
protected ApplicationMetadata createMetadataFromApplication(Application app) {
if (app == null) {
return null;
}
ApplicationMetadata metadata = new ApplicationMetadata();
metadata.applicationId = app.id;
metadata.name = app.name;
Log.d(TAG, "unimplemented: ApplicationMetadata.images");
Log.d(TAG, "unimplemented: ApplicationMetadata.senderAppLaunchUri");
metadata.images = new ArrayList<WebImage>();
metadata.namespaces = new ArrayList<String>();
for(Namespace namespace : app.namespaces) {
metadata.namespaces.add(namespace.name);
}
metadata.senderAppIdentifier = this.context.getPackageName();
return metadata;
}
@Override
public void spontaneousEventReceived(ChromeCastSpontaneousEvent event) {
switch (event.getType()) {
case MEDIA_STATUS:
break;
case STATUS:
su.litvak.chromecast.api.v2.Status status = (su.litvak.chromecast.api.v2.Status)event.getData();
Application app = status.getRunningApp();
ApplicationMetadata metadata = this.createMetadataFromApplication(app);
if (app != null) {
this.onApplicationStatusChanged(new ApplicationStatus(app.statusText));
}
int activeInputState = status.activeInput ? 1 : 0;
int standbyState = status.standBy ? 1 : 0;
this.onDeviceStatusChanged(new CastDeviceStatus(status.volume.level, status.volume.muted, activeInputState, metadata, standbyState));
break;
case APPEVENT:
break;
case CLOSE:
this.onApplicationDisconnected(CommonStatusCodes.SUCCESS);
break;
default:
break;
}
}
@Override
public void rawMessageReceived(ChromeCastRawMessage message, Long requestId) {
switch (message.getPayloadType()) {
case STRING:
String response = message.getPayloadUtf8();
if (requestId == null) {
this.onTextMessageReceived(message.getNamespace(), response);
} else {
this.onSendMessageSuccess(response, requestId);
this.onTextMessageReceived(message.getNamespace(), response);
}
break;
case BINARY:
byte[] payload = message.getPayloadBinary();
this.onBinaryMessageReceived(message.getNamespace(), payload);
break;
}
}
@Override
public void disconnect() {
try {
this.chromecast.disconnect();
} catch (IOException e) {
Log.e(TAG, "Error disconnecting chromecast: " + e.getMessage());
return;
}
}
@Override
public void sendMessage(String namespace, String message, long requestId) {
try {
this.chromecast.sendRawRequest(namespace, message, requestId);
} catch (IOException e) {
Log.w(TAG, "Error sending cast message: " + e.getMessage());
this.onSendMessageFailure("", requestId, CommonStatusCodes.NETWORK_ERROR);
return;
}
}
@Override
public void stopApplication(String sessionId) {
try {
this.chromecast.stopSession(sessionId);
} catch (IOException e) {
Log.w(TAG, "Error sending cast message: " + e.getMessage());
return;
}
this.sessionId = null;
}
@Override
public void registerNamespace(String namespace) {
Log.d(TAG, "unimplemented Method: registerNamespace");
}
@Override
public void unregisterNamespace(String namespace) {
Log.d(TAG, "unimplemented Method: unregisterNamespace");
}
@Override
public void launchApplication(String applicationId, LaunchOptions launchOptions) {
Application app = null;
try {
app = this.chromecast.launchApp(applicationId);
} catch (IOException e) {
Log.w(TAG, "Error launching cast application: " + e.getMessage());
this.onApplicationConnectionFailure(CommonStatusCodes.NETWORK_ERROR);
return;
}
this.sessionId = app.sessionId;
ApplicationMetadata metadata = this.createMetadataFromApplication(app);
this.onApplicationConnectionSuccess(metadata, app.statusText, app.sessionId, true);
}
@Override
public void joinApplication(String applicationId, String sessionId, JoinOptions joinOptions) {
Log.d(TAG, "unimplemented Method: joinApplication");
this.launchApplication(applicationId, new LaunchOptions());
}
public void onDisconnected(int reason) {
if (this.listener != null) {
try {
this.listener.onDisconnected(reason);
} catch (RemoteException ex) {
Log.e(TAG, "Error calling onDisconnected: " + ex.getMessage());
}
}
}
public void onApplicationConnectionSuccess(ApplicationMetadata applicationMetadata, String applicationStatus, String sessionId, boolean wasLaunched) {
if (this.listener != null) {
try {
this.listener.onApplicationConnectionSuccess(applicationMetadata, applicationStatus, sessionId, wasLaunched);
} catch (RemoteException ex) {
Log.e(TAG, "Error calling onApplicationConnectionSuccess: " + ex.getMessage());
}
}
}
public void onApplicationConnectionFailure(int statusCode) {
if (this.listener != null) {
try {
this.listener.onApplicationConnectionFailure(statusCode);
} catch (RemoteException ex) {
Log.e(TAG, "Error calling onApplicationConnectionFailure: " + ex.getMessage());
}
}
}
public void onTextMessageReceived(String namespace, String message) {
if (this.listener != null) {
try {
this.listener.onTextMessageReceived(namespace, message);
} catch (RemoteException ex) {
Log.e(TAG, "Error calling onTextMessageReceived: " + ex.getMessage());
}
}
}
public void onBinaryMessageReceived(String namespace, byte[] data) {
if (this.listener != null) {
try {
this.listener.onBinaryMessageReceived(namespace, data);
} catch (RemoteException ex) {
Log.e(TAG, "Error calling onBinaryMessageReceived: " + ex.getMessage());
}
}
}
public void onApplicationDisconnected(int paramInt) {
Log.d(TAG, "unimplemented Method: onApplicationDisconnected");
if (this.listener != null) {
try {
this.listener.onApplicationDisconnected(paramInt);
} catch (RemoteException ex) {
Log.e(TAG, "Error calling onApplicationDisconnected: " + ex.getMessage());
}
}
}
public void onSendMessageFailure(String response, long requestId, int statusCode) {
if (this.listener != null) {
try {
this.listener.onSendMessageFailure(response, requestId, statusCode);
} catch (RemoteException ex) {
Log.e(TAG, "Error calling onSendMessageFailure: " + ex.getMessage());
}
}
}
public void onSendMessageSuccess(String response, long requestId) {
if (this.listener != null) {
try {
this.listener.onSendMessageSuccess(response, requestId);
} catch (RemoteException ex) {
Log.e(TAG, "Error calling onSendMessageSuccess: " + ex.getMessage());
}
}
}
public void onApplicationStatusChanged(ApplicationStatus applicationStatus) {
if (this.listener != null) {
try {
this.listener.onApplicationStatusChanged(applicationStatus);
} catch (RemoteException ex) {
Log.e(TAG, "Error calling onApplicationStatusChanged: " + ex.getMessage());
}
}
}
public void onDeviceStatusChanged(CastDeviceStatus deviceStatus) {
if (this.listener != null) {
try {
this.listener.onDeviceStatusChanged(deviceStatus);
} catch (RemoteException ex) {
Log.e(TAG, "Error calling onDeviceStatusChanged: " + ex.getMessage());
}
}
}
}

View file

@ -0,0 +1,50 @@
/*
* 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.os.IBinder;
import android.os.RemoteException;
import android.os.Parcel;
import android.util.ArrayMap;
import android.util.Log;
import com.google.android.gms.cast.CastDevice;
import com.google.android.gms.cast.internal.ICastDeviceControllerListener;
import com.google.android.gms.common.internal.GetServiceRequest;
import com.google.android.gms.common.internal.BinderWrapper;
import com.google.android.gms.common.internal.IGmsCallbacks;
import org.microg.gms.BaseService;
import org.microg.gms.common.GmsService;
import su.litvak.chromecast.api.v2.ChromeCast;
import su.litvak.chromecast.api.v2.ChromeCasts;
import su.litvak.chromecast.api.v2.Status;
import su.litvak.chromecast.api.v2.ChromeCastsListener;
public class CastDeviceControllerService extends BaseService {
private static final String TAG = CastDeviceControllerService.class.getSimpleName();
public CastDeviceControllerService() {
super("GmsCastDeviceControllerSvc", GmsService.CAST);
}
@Override
public void handleServiceRequest(IGmsCallbacks callback, GetServiceRequest request, GmsService service) throws RemoteException {
callback.onPostInitComplete(0, new CastDeviceControllerImpl(this, request.packageName, request.extras), null);
}
}

View file

@ -0,0 +1,93 @@
/*
* 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.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.Uri;
import android.os.Bundle;
import android.os.AsyncTask;
import android.os.Handler;
import android.util.Log;
import androidx.mediarouter.media.MediaRouteProvider;
import androidx.mediarouter.media.MediaRouter;
import com.google.android.gms.common.images.WebImage;
import com.google.android.gms.cast.CastDevice;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Inet4Address;
import java.net.UnknownHostException;
import java.io.IOException;
import java.lang.Thread;
import java.lang.Runnable;
import java.util.ArrayList;
import java.util.Map;
import java.util.HashMap;
import su.litvak.chromecast.api.v2.ChromeCast;
import su.litvak.chromecast.api.v2.ChromeCasts;
import su.litvak.chromecast.api.v2.Status;
import su.litvak.chromecast.api.v2.ChromeCastsListener;
public class CastMediaRouteController extends MediaRouteProvider.RouteController {
private static final String TAG = CastMediaRouteController.class.getSimpleName();
private CastMediaRouteProvider provider;
private String routeId;
private ChromeCast chromecast;
public CastMediaRouteController(CastMediaRouteProvider provider, String routeId, String address) {
super();
this.provider = provider;
this.routeId = routeId;
this.chromecast = new ChromeCast(address);
}
public boolean onControlRequest(Intent intent, MediaRouter.ControlRequestCallback callback) {
Log.d(TAG, "unimplemented Method: onControlRequest: " + this.routeId);
return false;
}
public void onRelease() {
Log.d(TAG, "unimplemented Method: onRelease: " + this.routeId);
}
public void onSelect() {
Log.d(TAG, "unimplemented Method: onSelect: " + this.routeId);
}
public void onSetVolume(int volume) {
Log.d(TAG, "unimplemented Method: onSetVolume: " + this.routeId);
}
public void onUnselect() {
Log.d(TAG, "unimplemented Method: onUnselect: " + this.routeId);
}
public void onUnselect(int reason) {
Log.d(TAG, "unimplemented Method: onUnselect: " + this.routeId);
}
public void onUpdateVolume(int delta) {
Log.d(TAG, "unimplemented Method: onUpdateVolume: " + this.routeId);
}
}

View file

@ -0,0 +1,359 @@
/*
* 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.Uri;
import android.net.nsd.NsdManager;
import android.net.nsd.NsdServiceInfo;
import android.os.Bundle;
import android.os.AsyncTask;
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.common.images.WebImage;
import com.google.android.gms.cast.CastDevice;
import com.google.android.gms.cast.CastMediaControlIntent;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Inet4Address;
import java.net.UnknownHostException;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.Thread;
import java.lang.Runnable;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.util.HashMap;
public class CastMediaRouteProvider extends MediaRouteProvider {
private static final String TAG = CastMediaRouteProvider.class.getSimpleName();
private Map<String, CastDevice> castDevices = new HashMap<String, CastDevice>();
private Map<String, String> serviceCastIds = new HashMap<String, String>();
private NsdManager mNsdManager;
private NsdManager.DiscoveryListener mDiscoveryListener;
private List<String> customCategories = new ArrayList<String>();
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<IntentFilter>();
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.getApplicationContext().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"), "UTF-8");
String deviceVersion = new String(attributes.get("ve"), "UTF-8");
String friendlyName = new String(attributes.get("fn"), "UTF-8");
String modelName = new String(attributes.get("md"), "UTF-8");
String iconPath = new String(attributes.get("ic"), "UTF-8");
int status = Integer.parseInt(new String(attributes.get("st"), "UTF-8"));
onChromeCastDiscovered(id, name, host, port, deviceVersion, friendlyName, modelName, iconPath, status);
} catch (UnsupportedEncodingException | NullPointerException ex) {
Log.e(TAG, "Error getting cast details from DNS-SD response", ex);
return;
}
}
});
}
@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(new Runnable() {
@Override
public void run() {
publishRoutes();
}
});
}
private void publishRoutes() {
MediaRouteProviderDescriptor.Builder builder = new MediaRouteProviderDescriptor.Builder();
for (CastDevice castDevice : this.castDevices.values()) {
ArrayList<IntentFilter> controlFilters = new ArrayList<IntentFilter>(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());
}
}