diff --git a/.gitignore b/.gitignore index 6265e60a..25ed3657 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ gen/ bin/ build/ .gradle/ +.idea/ user.gradle local.properties .directory diff --git a/play-services-core/build.gradle b/play-services-core/build.gradle index 14ad88de..2a7ff302 100644 --- a/play-services-core/build.gradle +++ b/play-services-core/build.gradle @@ -23,15 +23,27 @@ def useMapbox() { } dependencies { + implementation 'com.android.support:multidex:1.0.3' implementation "com.android.support:support-v4:$supportLibraryVersion" implementation "com.android.support:appcompat-v7:$supportLibraryVersion" + implementation "com.android.support:mediarouter-v7:$supportLibraryVersion" implementation "com.squareup.wire:wire-runtime:1.6.1" implementation "com.takisoft.fix:preference-v7:$supportLibraryVersion.0" implementation "de.hdodenhof:circleimageview:1.3.0" implementation "org.conscrypt:conscrypt-android:2.0.0" + // 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" + + // Specified manually due to + // https://github.com/vitalidze/chromecast-java-api-v2/issues/91 + api "org.slf4j:slf4j-api:1.7.25" + api "uk.uuid.slf4j:slf4j-android:1.7.25-1" implementation project(':microg-ui-tools') implementation project(':play-services-api') + implementation project(':play-services-cast-api') implementation project(':play-services-wearable') implementation project(':unifiednlp-base') implementation project(':wearable-lib') @@ -76,6 +88,8 @@ android { minSdkVersion androidMinSdk() targetSdkVersion androidTargetSdk() + multiDexEnabled true + ndk { abiFilters "armeabi", "armeabi-v7a", "arm64-v8a", "x86", "x86_64" } @@ -102,6 +116,10 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } + + packagingOptions { + exclude 'META-INF/ASL2.0' + } } if (file('user.gradle').exists()) { diff --git a/play-services-core/src/main/AndroidManifest.xml b/play-services-core/src/main/AndroidManifest.xml index 0f0e3887..d7d9e60f 100644 --- a/play-services-core/src/main/AndroidManifest.xml +++ b/play-services-core/src/main/AndroidManifest.xml @@ -92,13 +92,22 @@ - + + android:label="@string/gms_app_name" + android:name="android.support.multidex.MultiDexApplication"> @@ -392,6 +401,14 @@ + + + + + + + + + + + + + + @@ -619,7 +642,6 @@ - diff --git a/play-services-core/src/main/java/com/google/android/gms/cast/framework/internal/CastContextImpl.java b/play-services-core/src/main/java/com/google/android/gms/cast/framework/internal/CastContextImpl.java new file mode 100644 index 00000000..30dd6a22 --- /dev/null +++ b/play-services-core/src/main/java/com/google/android/gms/cast/framework/internal/CastContextImpl.java @@ -0,0 +1,154 @@ +/* + * 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.framework.internal; + +import android.content.Context; +import android.os.Bundle; +import android.os.IBinder; +import android.os.RemoteException; +import android.support.v7.media.MediaControlIntent; +import android.support.v7.media.MediaRouteSelector; +import android.support.v7.media.MediaRouter; +import android.util.Log; + +import com.google.android.gms.cast.CastMediaControlIntent; +import com.google.android.gms.cast.framework.CastOptions; +import com.google.android.gms.cast.framework.IAppVisibilityListener; +import com.google.android.gms.cast.framework.ICastContext; +import com.google.android.gms.cast.framework.IDiscoveryManager; +import com.google.android.gms.cast.framework.ISessionManager; +import com.google.android.gms.cast.framework.ISessionProvider; +import com.google.android.gms.dynamic.IObjectWrapper; +import com.google.android.gms.dynamic.ObjectWrapper; + +import java.util.Map; +import java.util.HashMap; + +public class CastContextImpl extends ICastContext.Stub { + private static final String TAG = CastContextImpl.class.getSimpleName(); + + private SessionManagerImpl sessionManager; + private DiscoveryManagerImpl discoveryManager; + + private Context context; + private CastOptions options; + private IMediaRouter router; + private Map sessionProviders = new HashMap(); + public ISessionProvider defaultSessionProvider; + + private MediaRouteSelector mergedSelector; + + public CastContextImpl(IObjectWrapper context, CastOptions options, IMediaRouter router, Map sessionProviders) throws RemoteException { + this.context = (Context) ObjectWrapper.unwrap(context); + this.options = options; + this.router = router; + for (Map.Entry entry : sessionProviders.entrySet()) { + this.sessionProviders.put(entry.getKey(), ISessionProvider.Stub.asInterface(entry.getValue())); + } + + String receiverApplicationId = options.getReceiverApplicationId(); + String defaultCategory = CastMediaControlIntent.categoryForCast(receiverApplicationId); + + this.defaultSessionProvider = this.sessionProviders.get(defaultCategory); + + // TODO: This should incorporate passed options + this.mergedSelector = new MediaRouteSelector.Builder() + .addControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO) + .addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK) + .addControlCategory(defaultCategory) + .build(); + } + + @Override + public Bundle getMergedSelectorAsBundle() throws RemoteException { + return this.mergedSelector.asBundle(); + } + + @Override + public void addVisibilityChangeListener(IAppVisibilityListener listener) { + Log.d(TAG, "unimplemented Method: addVisibilityChangeListener"); + } + + @Override + public void removeVisibilityChangeListener(IAppVisibilityListener listener) { + Log.d(TAG, "unimplemented Method: removeVisibilityChangeListener"); + } + + @Override + public boolean isApplicationVisible() throws RemoteException { + Log.d(TAG, "unimplemented Method: isApplicationVisible"); + return true; + } + + @Override + public SessionManagerImpl getSessionManagerImpl() { + if (this.sessionManager == null) { + this.sessionManager = new SessionManagerImpl(this); + } + return this.sessionManager; + } + + @Override + public IDiscoveryManager getDiscoveryManagerImpl() throws RemoteException { + if (this.discoveryManager == null) { + this.discoveryManager = new DiscoveryManagerImpl(this); + } + return this.discoveryManager; + } + + @Override + public void destroy() throws RemoteException { + Log.d(TAG, "unimplemented Method: destroy"); + } + + @Override + public void onActivityResumed(IObjectWrapper activity) throws RemoteException { + Log.d(TAG, "unimplemented Method: onActivityResumed"); + + } + + @Override + public void onActivityPaused(IObjectWrapper activity) throws RemoteException { + Log.d(TAG, "unimplemented Method: onActivityPaused"); + } + + @Override + public void setReceiverApplicationId(String receiverApplicationId, Map sessionProvidersByCategory) throws RemoteException { + Log.d(TAG, "unimplemented Method: setReceiverApplicationId"); + } + + public Context getContext() { + return this.context; + } + + public IMediaRouter getRouter() { + return this.router; + } + + public MediaRouteSelector getMergedSelector() { + return this.mergedSelector; + } + + public CastOptions getOptions() { + return this.options; + } + + @Override + public IObjectWrapper getWrappedThis() throws RemoteException { + return ObjectWrapper.wrap(this); + } +} diff --git a/play-services-core/src/main/java/com/google/android/gms/cast/framework/internal/CastDynamiteModuleImpl.java b/play-services-core/src/main/java/com/google/android/gms/cast/framework/internal/CastDynamiteModuleImpl.java index 374deded..71bd3345 100644 --- a/play-services-core/src/main/java/com/google/android/gms/cast/framework/internal/CastDynamiteModuleImpl.java +++ b/play-services-core/src/main/java/com/google/android/gms/cast/framework/internal/CastDynamiteModuleImpl.java @@ -16,7 +16,9 @@ package com.google.android.gms.cast.framework.internal; +import android.content.Context; import android.os.RemoteException; +import android.support.v7.media.MediaRouter; import android.util.Log; import com.google.android.gms.cast.framework.CastOptions; @@ -27,6 +29,10 @@ import com.google.android.gms.cast.framework.IReconnectionService; import com.google.android.gms.cast.framework.ISession; import com.google.android.gms.cast.framework.ISessionProxy; import com.google.android.gms.cast.framework.media.CastMediaOptions; +import com.google.android.gms.cast.framework.internal.CastContextImpl; +import com.google.android.gms.cast.framework.internal.CastSessionImpl; +import com.google.android.gms.cast.framework.internal.MediaRouterCallbackImpl; +import com.google.android.gms.cast.framework.internal.SessionImpl; import com.google.android.gms.cast.framework.media.IMediaNotificationService; import com.google.android.gms.cast.framework.media.internal.IFetchBitmapTask; import com.google.android.gms.cast.framework.media.internal.IFetchBitmapTaskProgressPublisher; @@ -38,21 +44,18 @@ public class CastDynamiteModuleImpl extends ICastDynamiteModule.Stub { private static final String TAG = CastDynamiteModuleImpl.class.getSimpleName(); @Override - public ICastContext newCastContextImpl(IObjectWrapper context, CastOptions options, IMediaRouter router, Map map) throws RemoteException { - Log.d(TAG, "unimplemented Method: newCastContextImpl"); - return null; + public ICastContext newCastContextImpl(IObjectWrapper context, CastOptions options, IMediaRouter router, Map sessionProviders) throws RemoteException { + return new CastContextImpl(context, options, router, sessionProviders); } @Override - public ISession newSessionImpl(String s1, String s2, ISessionProxy proxy) throws RemoteException { - Log.d(TAG, "unimplemented Method: newSessionImpl"); - return null; + public ISession newSessionImpl(String category, String sessionId, ISessionProxy proxy) throws RemoteException { + return new SessionImpl(category, sessionId, proxy); } @Override public ICastSession newCastSessionImpl(CastOptions options, IObjectWrapper session, ICastConnectionController controller) throws RemoteException { - Log.d(TAG, "unimplemented Method: newCastSessionImpl"); - return null; + return new CastSessionImpl(options, session, controller); } @Override diff --git a/play-services-core/src/main/java/com/google/android/gms/cast/framework/internal/CastSessionImpl.java b/play-services-core/src/main/java/com/google/android/gms/cast/framework/internal/CastSessionImpl.java new file mode 100644 index 00000000..cc63b949 --- /dev/null +++ b/play-services-core/src/main/java/com/google/android/gms/cast/framework/internal/CastSessionImpl.java @@ -0,0 +1,79 @@ +/* + * 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.framework.internal; + +import com.google.android.gms.cast.framework.ICastSession; + +import android.os.Bundle; +import android.os.RemoteException; +import android.util.Log; + +import com.google.android.gms.cast.ApplicationMetadata; +import com.google.android.gms.cast.framework.CastOptions; +import com.google.android.gms.cast.framework.ICastConnectionController; +import com.google.android.gms.common.api.Status; +import com.google.android.gms.dynamic.IObjectWrapper; +import com.google.android.gms.dynamic.ObjectWrapper; + +public class CastSessionImpl extends ICastSession.Stub { + private static final String TAG = CastSessionImpl.class.getSimpleName(); + private CastOptions options; + private SessionImpl session; + private ICastConnectionController controller; + + public CastSessionImpl(CastOptions options, IObjectWrapper session, ICastConnectionController controller) throws RemoteException { + this.options = options; + this.session = (SessionImpl) ObjectWrapper.unwrap(session); + this.controller = controller; + + this.session.setCastSession(this); + } + + public void launchApplication() throws RemoteException { + this.controller.launchApplication(this.options.getReceiverApplicationId(), this.options.getLaunchOptions()); + } + + @Override + public void onConnected(Bundle routeInfoExtra) throws RemoteException { + this.controller.launchApplication(this.options.getReceiverApplicationId(), this.options.getLaunchOptions()); + } + + @Override + public void onConnectionSuspended(int reason) { + Log.d(TAG, "unimplemented Method: onConnectionSuspended"); + } + + @Override + public void onConnectionFailed(Status status) { + Log.d(TAG, "unimplemented Method: onConnectionFailed"); + } + + @Override + public void onApplicationConnectionSuccess(ApplicationMetadata applicationMetadata, String applicationStatus, String sessionId, boolean wasLaunched) { + this.session.onApplicationConnectionSuccess(applicationMetadata, applicationStatus, sessionId, wasLaunched); + } + + @Override + public void onApplicationConnectionFailure(int statusCode) { + this.session.onApplicationConnectionFailure(statusCode); + } + + @Override + public void disconnectFromDevice(boolean boolean1, int int1) { + Log.d(TAG, "unimplemented Method: disconnectFromDevice"); + } +} diff --git a/play-services-core/src/main/java/com/google/android/gms/cast/framework/internal/DiscoveryManagerImpl.java b/play-services-core/src/main/java/com/google/android/gms/cast/framework/internal/DiscoveryManagerImpl.java new file mode 100644 index 00000000..b0e7b51c --- /dev/null +++ b/play-services-core/src/main/java/com/google/android/gms/cast/framework/internal/DiscoveryManagerImpl.java @@ -0,0 +1,68 @@ +/* + * 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.framework.internal; + +import android.os.RemoteException; +import android.util.Log; + +import com.google.android.gms.cast.framework.IDiscoveryManager; +import com.google.android.gms.cast.framework.IDiscoveryManagerListener; +import com.google.android.gms.cast.framework.internal.CastContextImpl; +import com.google.android.gms.dynamic.IObjectWrapper; +import com.google.android.gms.dynamic.ObjectWrapper; + +import java.util.Set; +import java.util.HashSet; + +public class DiscoveryManagerImpl extends IDiscoveryManager.Stub { + private static final String TAG = DiscoveryManagerImpl.class.getSimpleName(); + + private CastContextImpl castContextImpl; + + private Set discoveryManagerListeners = new HashSet(); + + public DiscoveryManagerImpl(CastContextImpl castContextImpl) { + this.castContextImpl = castContextImpl; + } + + @Override + public void startDiscovery() { + Log.d(TAG, "unimplemented Method: startDiscovery"); + } + + @Override + public void stopDiscovery() { + Log.d(TAG, "unimplemented Method: stopDiscovery"); + } + + @Override + public void addDiscoveryManagerListener(IDiscoveryManagerListener listener) { + Log.d(TAG, "unimplemented Method: addDiscoveryManagerListener"); + this.discoveryManagerListeners.add(listener); + } + + @Override + public void removeDiscoveryManagerListener(IDiscoveryManagerListener listener) { + Log.d(TAG, "unimplemented Method: removeDiscoveryManagerListener"); + this.discoveryManagerListeners.remove(listener); + } + + @Override + public IObjectWrapper getWrappedThis() throws RemoteException { + return ObjectWrapper.wrap(this); + } +} diff --git a/play-services-core/src/main/java/com/google/android/gms/cast/framework/internal/MediaRouterCallbackImpl.java b/play-services-core/src/main/java/com/google/android/gms/cast/framework/internal/MediaRouterCallbackImpl.java new file mode 100644 index 00000000..8bf933f3 --- /dev/null +++ b/play-services-core/src/main/java/com/google/android/gms/cast/framework/internal/MediaRouterCallbackImpl.java @@ -0,0 +1,70 @@ +/* + * 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.framework.internal; + +import android.content.Intent; +import android.os.Bundle; +import android.os.RemoteException; +import android.util.Log; + +import com.google.android.gms.cast.CastDevice; +import com.google.android.gms.cast.framework.ISession; +import com.google.android.gms.dynamic.IObjectWrapper; +import com.google.android.gms.dynamic.ObjectWrapper; + +import android.support.v7.media.MediaControlIntent; + +public class MediaRouterCallbackImpl extends IMediaRouterCallback.Stub { + private static final String TAG = MediaRouterCallbackImpl.class.getSimpleName(); + + private CastContextImpl castContext; + + public MediaRouterCallbackImpl(CastContextImpl castContext) { + this.castContext = castContext; + } + + @Override + public void onRouteAdded(String routeId, Bundle extras) { + Log.d(TAG, "unimplemented Method: onRouteAdded"); + } + @Override + public void onRouteChanged(String routeId, Bundle extras) { + Log.d(TAG, "unimplemented Method: onRouteChanged"); + } + @Override + public void onRouteRemoved(String routeId, Bundle extras) { + Log.d(TAG, "unimplemented Method: onRouteRemoved"); + } + @Override + public void onRouteSelected(String routeId, Bundle extras) throws RemoteException { + CastDevice castDevice = CastDevice.getFromBundle(extras); + + SessionImpl session = (SessionImpl) ObjectWrapper.unwrap(this.castContext.defaultSessionProvider.getSession(null)); + Bundle routeInfoExtras = this.castContext.getRouter().getRouteInfoExtrasById(routeId); + if (routeInfoExtras != null) { + session.start(this.castContext, castDevice, routeId, routeInfoExtras); + } + } + @Override + public void unknown(String routeId, Bundle extras) { + Log.d(TAG, "unimplemented Method: unknown"); + } + @Override + public void onRouteUnselected(String routeId, Bundle extras, int reason) { + Log.d(TAG, "unimplemented Method: onRouteUnselected"); + } +} diff --git a/play-services-core/src/main/java/com/google/android/gms/cast/framework/internal/SessionImpl.java b/play-services-core/src/main/java/com/google/android/gms/cast/framework/internal/SessionImpl.java new file mode 100644 index 00000000..954405d8 --- /dev/null +++ b/play-services-core/src/main/java/com/google/android/gms/cast/framework/internal/SessionImpl.java @@ -0,0 +1,197 @@ +/* + * 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.framework.internal; + +import android.os.Bundle; +import android.os.RemoteException; +import android.util.Log; + + +import com.google.android.gms.cast.ApplicationMetadata; +import com.google.android.gms.cast.CastDevice; +import com.google.android.gms.cast.framework.ISession; +import com.google.android.gms.cast.framework.ISessionProxy; +import com.google.android.gms.dynamic.IObjectWrapper; +import com.google.android.gms.dynamic.ObjectWrapper; + +public class SessionImpl extends ISession.Stub { + private static final String TAG = SessionImpl.class.getSimpleName(); + + private String category; + private String sessionId; + private ISessionProxy proxy; + + private CastSessionImpl castSession; + + private CastContextImpl castContext; + private CastDevice castDevice; + private Bundle routeInfoExtra; + + private boolean mIsConnecting = false; + private boolean mIsConnected = false; + private String routeId = null; + + public SessionImpl(String category, String sessionId, ISessionProxy proxy) { + this.category = category; + this.sessionId = sessionId; + this.proxy = proxy; + } + + public void start(CastContextImpl castContext, CastDevice castDevice, String routeId, Bundle routeInfoExtra) throws RemoteException { + this.castContext = castContext; + this.castDevice = castDevice; + this.routeInfoExtra = routeInfoExtra; + this.routeId = routeId; + + this.mIsConnecting = true; + this.mIsConnected = false; + this.castContext.getSessionManagerImpl().onSessionStarting(this); + this.proxy.start(routeInfoExtra); + } + + public void onApplicationConnectionSuccess(ApplicationMetadata applicationMetadata, String applicationStatus, String sessionId, boolean wasLaunched) { + this.mIsConnecting = false; + this.mIsConnected = true; + this.castContext.getSessionManagerImpl().onSessionStarted(this, sessionId); + try { + this.castContext.getRouter().selectRouteById(this.getRouteId()); + } catch (RemoteException ex) { + Log.e(TAG, "Error calling selectRouteById: " + ex.getMessage()); + } + } + + public void onApplicationConnectionFailure(int statusCode) { + this.mIsConnecting = false; + this.mIsConnected = false; + this.routeId = null; + this.castContext = null; + this.castDevice = null; + this.routeInfoExtra = null; + this.castContext.getSessionManagerImpl().onSessionStartFailed(this, statusCode); + try { + this.castContext.getRouter().selectDefaultRoute(); + } catch (RemoteException ex) { + Log.e(TAG, "Error calling selectDefaultRoute: " + ex.getMessage()); + } + } + + public void onRouteSelected(Bundle extras) { + } + + public CastSessionImpl getCastSession() { + return this.castSession; + } + + public void setCastSession(CastSessionImpl castSession) { + this.castSession = castSession; + } + + public ISessionProxy getSessionProxy() { + return this.proxy; + } + + public IObjectWrapper getWrappedSession() throws RemoteException { + if (this.proxy == null) { + return ObjectWrapper.wrap(null); + } + return this.proxy.getWrappedSession(); + } + + @Override + public String getCategory() { + return this.category; + } + + @Override + public String getSessionId() { + return this.sessionId; + } + + @Override + public String getRouteId() { + return this.routeId; + } + + @Override + public boolean isConnected() { + return this.mIsConnected; + } + + @Override + public boolean isConnecting() { + return this.mIsConnecting; + } + + @Override + public boolean isDisconnecting() { + Log.d(TAG, "unimplemented Method: isDisconnecting"); + return false; + } + + @Override + public boolean isDisconnected() { + Log.d(TAG, "unimplemented Method: isDisconnected"); + return false; + } + + @Override + public boolean isResuming() { + Log.d(TAG, "unimplemented Method: isResuming"); + return false; + } + + @Override + public boolean isSuspended() { + Log.d(TAG, "unimplemented Method: isSuspended"); + return false; + } + + @Override + public void notifySessionStarted(String sessionId) { + Log.d(TAG, "unimplemented Method: notifySessionStarted"); + } + + @Override + public void notifyFailedToStartSession(int error) { + Log.d(TAG, "unimplemented Method: notifyFailedToStartSession"); + } + + @Override + public void notifySessionEnded(int error) { + Log.d(TAG, "unimplemented Method: notifySessionEnded"); + } + + @Override + public void notifySessionResumed(boolean wasSuspended) { + Log.d(TAG, "unimplemented Method: notifySessionResumed"); + } + + @Override + public void notifyFailedToResumeSession(int error) { + Log.d(TAG, "unimplemented Method: notifyFailedToResumeSession"); + } + + @Override + public void notifySessionSuspended(int reason) { + Log.d(TAG, "unimplemented Method: notifySessionSuspended"); + } + + @Override + public IObjectWrapper getWrappedObject() { + return ObjectWrapper.wrap(this); + } +} diff --git a/play-services-core/src/main/java/com/google/android/gms/cast/framework/internal/SessionManagerImpl.java b/play-services-core/src/main/java/com/google/android/gms/cast/framework/internal/SessionManagerImpl.java new file mode 100644 index 00000000..d10f8b21 --- /dev/null +++ b/play-services-core/src/main/java/com/google/android/gms/cast/framework/internal/SessionManagerImpl.java @@ -0,0 +1,230 @@ +/* + * 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.framework.internal; + +import android.os.Bundle; +import android.os.RemoteException; +import android.util.Log; + +import com.google.android.gms.cast.framework.CastState; +import com.google.android.gms.cast.framework.ICastStateListener; +import com.google.android.gms.cast.framework.ISession; +import com.google.android.gms.cast.framework.ISessionManager; +import com.google.android.gms.cast.framework.ISessionManagerListener; +import com.google.android.gms.cast.framework.internal.CastContextImpl; +import com.google.android.gms.cast.framework.internal.SessionImpl; +import com.google.android.gms.dynamic.IObjectWrapper; +import com.google.android.gms.dynamic.ObjectWrapper; + +import java.util.Set; +import java.util.HashSet; + +import java.util.Map; +import java.util.HashMap; + +public class SessionManagerImpl extends ISessionManager.Stub { + private static final String TAG = SessionManagerImpl.class.getSimpleName(); + + private CastContextImpl castContext; + + private Set sessionManagerListeners = new HashSet(); + private Set castStateListeners = new HashSet(); + + private Map routeSessions = new HashMap(); + + private SessionImpl currentSession; + + private int castState = CastState.NO_DEVICES_AVAILABLE; + + public SessionManagerImpl(CastContextImpl castContext) { + this.castContext = castContext; + } + + @Override + public IObjectWrapper getWrappedCurrentSession() throws RemoteException { + if (this.currentSession == null) { + return ObjectWrapper.wrap(null); + } + return this.currentSession.getWrappedSession(); + } + + @Override + public void endCurrentSession(boolean b, boolean stopCasting) throws RemoteException { + Log.d(TAG, "unimplemented Method: endCurrentSession"); + } + + @Override + public void addSessionManagerListener(ISessionManagerListener listener) { + Log.d(TAG, "unimplemented Method: addSessionManagerListener"); + this.sessionManagerListeners.add(listener); + } + + @Override + public void removeSessionManagerListener(ISessionManagerListener listener) { + Log.d(TAG, "unimplemented Method: removeSessionManagerListener"); + this.sessionManagerListeners.remove(listener); + } + + @Override + public void addCastStateListener(ICastStateListener listener) { + Log.d(TAG, "unimplemented Method: addCastStateListener"); + this.castStateListeners.add(listener); + } + + @Override + public void removeCastStateListener(ICastStateListener listener) { + Log.d(TAG, "unimplemented Method: removeCastStateListener"); + this.castStateListeners.remove(listener); + } + + @Override + public IObjectWrapper getWrappedThis() throws RemoteException { + return ObjectWrapper.wrap(this); + } + + @Override + public int getCastState() { + return this.castState; + } + + @Override + public void startSession(Bundle params) { + Log.d(TAG, "unimplemented Method: startSession"); + String routeId = params.getString("CAST_INTENT_TO_CAST_ROUTE_ID_KEY"); + String sessionId = params.getString("CAST_INTENT_TO_CAST_SESSION_ID_KEY"); + } + + public void onRouteSelected(String routeId, Bundle extras) { + Log.d(TAG, "unimplemented Method: onRouteSelected: " + routeId); + } + + private void setCastState(int castState) { + this.castState = castState; + this.onCastStateChanged(); + } + + public void onCastStateChanged() { + for (ICastStateListener listener : this.castStateListeners) { + try { + listener.onCastStateChanged(this.castState); + } catch (RemoteException e) { + Log.d(TAG, "Remote exception calling onCastStateChanged: " + e.getMessage()); + } + } + } + + public void onSessionStarting(SessionImpl session) { + this.setCastState(CastState.CONNECTING); + for (ISessionManagerListener listener : this.sessionManagerListeners) { + try { + listener.onSessionStarting(session.getSessionProxy().getWrappedSession()); + } catch (RemoteException e) { + Log.d(TAG, "Remote exception calling onSessionStarting: " + e.getMessage()); + } + } + } + + public void onSessionStartFailed(SessionImpl session, int error) { + this.currentSession = null; + this.setCastState(CastState.NOT_CONNECTED); + for (ISessionManagerListener listener : this.sessionManagerListeners) { + try { + listener.onSessionStartFailed(session.getSessionProxy().getWrappedSession(), error); + } catch (RemoteException e) { + Log.d(TAG, "Remote exception calling onSessionStartFailed: " + e.getMessage()); + } + } + } + + public void onSessionStarted(SessionImpl session, String sessionId) { + this.currentSession = session; + this.setCastState(CastState.CONNECTED); + for (ISessionManagerListener listener : this.sessionManagerListeners) { + try { + listener.onSessionStarted(session.getSessionProxy().getWrappedSession(), sessionId); + } catch (RemoteException e) { + Log.d(TAG, "Remote exception calling onSessionStarted: " + e.getMessage()); + } + } + } + + public void onSessionResumed(SessionImpl session, boolean wasSuspended) { + this.setCastState(CastState.CONNECTED); + for (ISessionManagerListener listener : this.sessionManagerListeners) { + try { + listener.onSessionResumed(session.getSessionProxy().getWrappedSession(), wasSuspended); + } catch (RemoteException e) { + Log.d(TAG, "Remote exception calling onSessionResumed: " + e.getMessage()); + } + } + } + + public void onSessionEnding(SessionImpl session) { + for (ISessionManagerListener listener : this.sessionManagerListeners) { + try { + listener.onSessionEnding(session.getSessionProxy().getWrappedSession()); + } catch (RemoteException e) { + Log.d(TAG, "Remote exception calling onSessionEnding: " + e.getMessage()); + } + } + } + + public void onSessionEnded(SessionImpl session, int error) { + this.currentSession = null; + this.setCastState(CastState.NOT_CONNECTED); + for (ISessionManagerListener listener : this.sessionManagerListeners) { + try { + listener.onSessionEnded(session.getSessionProxy().getWrappedSession(), error); + } catch (RemoteException e) { + Log.d(TAG, "Remote exception calling onSessionEnded: " + e.getMessage()); + } + } + } + + public void onSessionResuming(SessionImpl session, String sessionId) { + for (ISessionManagerListener listener : this.sessionManagerListeners) { + try { + listener.onSessionResuming(session.getSessionProxy().getWrappedSession(), sessionId); + } catch (RemoteException e) { + Log.d(TAG, "Remote exception calling onSessionResuming: " + e.getMessage()); + } + } + } + + public void onSessionResumeFailed(SessionImpl session, int error) { + this.currentSession = null; + this.setCastState(CastState.NOT_CONNECTED); + for (ISessionManagerListener listener : this.sessionManagerListeners) { + try { + listener.onSessionResumeFailed(session.getSessionProxy().getWrappedSession(), error); + } catch (RemoteException e) { + Log.d(TAG, "Remote exception calling onSessionResumeFailed: " + e.getMessage()); + } + } + } + + public void onSessionSuspended(SessionImpl session, int reason) { + this.setCastState(CastState.NOT_CONNECTED); + for (ISessionManagerListener listener : this.sessionManagerListeners) { + try { + listener.onSessionSuspended(session.getSessionProxy().getWrappedSession(), reason); + } catch (RemoteException e) { + Log.d(TAG, "Remote exception calling onSessionSuspended: " + e.getMessage()); + } + } + } +} diff --git a/play-services-core/src/main/java/com/google/android/gms/cast/media/CastMediaRouteProviderService.java b/play-services-core/src/main/java/com/google/android/gms/cast/media/CastMediaRouteProviderService.java new file mode 100644 index 00000000..47d18154 --- /dev/null +++ b/play-services-core/src/main/java/com/google/android/gms/cast/media/CastMediaRouteProviderService.java @@ -0,0 +1,32 @@ +/* + * 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.support.v7.media.MediaRouteProviderService; +import android.support.v7.media.MediaRouteProvider; +import android.util.Log; + +public class CastMediaRouteProviderService extends MediaRouteProviderService { + private static final String TAG = CastMediaRouteProviderService.class.getSimpleName(); + + @Override + public MediaRouteProvider onCreateMediaRouteProvider() { + return new CastMediaRouteProvider(this); + } +} diff --git a/play-services-core/src/main/java/org/microg/gms/AbstractGmsServiceBroker.java b/play-services-core/src/main/java/org/microg/gms/AbstractGmsServiceBroker.java index 255cd967..da1def3e 100644 --- a/play-services-core/src/main/java/org/microg/gms/AbstractGmsServiceBroker.java +++ b/play-services-core/src/main/java/org/microg/gms/AbstractGmsServiceBroker.java @@ -179,7 +179,7 @@ public abstract class AbstractGmsServiceBroker extends IGmsServiceBroker.Stub { @Override public void getCastService(IGmsCallbacks callback, int versionCode, String packageName, IBinder binder, Bundle params) throws RemoteException { - throw new IllegalArgumentException("Cast service not supported"); + callGetService(GmsService.CAST, callback, versionCode, packageName, params); } @Deprecated diff --git a/play-services-core/src/main/java/org/microg/gms/cast/CastDeviceControllerImpl.java b/play-services-core/src/main/java/org/microg/gms/cast/CastDeviceControllerImpl.java new file mode 100644 index 00000000..e93e3c13 --- /dev/null +++ b/play-services-core/src/main/java/org/microg/gms/cast/CastDeviceControllerImpl.java @@ -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(); + metadata.namespaces = new ArrayList(); + 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()); + } + } + } +} diff --git a/play-services-core/src/main/java/org/microg/gms/cast/CastDeviceControllerService.java b/play-services-core/src/main/java/org/microg/gms/cast/CastDeviceControllerService.java new file mode 100644 index 00000000..d494a012 --- /dev/null +++ b/play-services-core/src/main/java/org/microg/gms/cast/CastDeviceControllerService.java @@ -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); + } +} diff --git a/play-services-core/src/main/java/org/microg/gms/cast/CastMediaRouteController.java b/play-services-core/src/main/java/org/microg/gms/cast/CastMediaRouteController.java new file mode 100644 index 00000000..4bb034bd --- /dev/null +++ b/play-services-core/src/main/java/org/microg/gms/cast/CastMediaRouteController.java @@ -0,0 +1,96 @@ +/* + * 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.support.v7.media.MediaControlIntent; +import android.support.v7.media.MediaRouteDescriptor; +import android.support.v7.media.MediaRouteDiscoveryRequest; +import android.support.v7.media.MediaRouteProvider; +import android.support.v7.media.MediaRouteProviderDescriptor; +import android.support.v7.media.MediaRouter; +import android.util.Log; + +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); + } +} diff --git a/play-services-core/src/main/java/org/microg/gms/cast/CastMediaRouteProvider.java b/play-services-core/src/main/java/org/microg/gms/cast/CastMediaRouteProvider.java new file mode 100644 index 00000000..7c5ba7c2 --- /dev/null +++ b/play-services-core/src/main/java/org/microg/gms/cast/CastMediaRouteProvider.java @@ -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.Build; +import android.os.Bundle; +import android.os.AsyncTask; +import android.os.Handler; +import android.support.v7.media.MediaControlIntent; +import android.support.v7.media.MediaRouteDescriptor; +import android.support.v7.media.MediaRouteDiscoveryRequest; +import android.support.v7.media.MediaRouteProvider; +import android.support.v7.media.MediaRouteProviderDescriptor; +import android.support.v7.media.MediaRouter; +import android.util.Log; + +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 castDevices = new HashMap(); + private Map serviceCastIds = new HashMap(); + + private NsdManager mNsdManager; + private NsdManager.DiscoveryListener mDiscoveryListener; + + private List customCategories = new ArrayList(); + + private enum State { + NOT_DISCOVERING, + DISCOVERY_REQUESTED, + DISCOVERING, + DISCOVERY_STOP_REQUESTED, + } + private State state = State.NOT_DISCOVERING; + + private static final ArrayList 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 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 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()); + } +} diff --git a/proguard.flags b/proguard.flags index 198869bb..6be46b9c 100644 --- a/proguard.flags +++ b/proguard.flags @@ -14,6 +14,7 @@ -dontwarn org.oscim.tiling.source.OkHttpEngine$OkHttpFactory -dontwarn com.caverock.androidsvg.** -dontwarn org.slf4j.** +-dontwarn org.codehaus.jackson.** # Disable ProGuard Notes, they won't help here -dontnote @@ -46,3 +47,8 @@ -keep public class com.squareup.wire.Message -keep public class * extends com.squareup.wire.Message -keep public class * extends com.squareup.wire.Message$Builder { public (...); } + +# Proguard configuration for Jackson 1.x +-keepclassmembers class * { + @org.codehaus.jackson.annotate.* *; +}