From d0223c6eab8f58799c8c3ff0e67f5974c237e01c Mon Sep 17 00:00:00 2001 From: Carey Metcalfe Date: Fri, 13 Oct 2017 19:54:41 -0400 Subject: Fix crash on startup when building without environment variables - Adds a check for the `CAST_APPLICATION_ID` variable in `CastCompat`. If it's null, casting will be disabled. - Without a `PASTEBIN_DEV_KEY` sending logs would result in a generic "Failed to gather logs" message. This commit changes the message to something more accurate. --- app/src/main/java/github/daneren2005/dsub/fragments/MainFragment.java | 4 ++++ .../main/java/github/daneren2005/dsub/util/EnvironmentVariables.java | 4 ++-- app/src/main/java/github/daneren2005/dsub/util/compat/CastCompat.java | 3 +++ 3 files changed, 9 insertions(+), 2 deletions(-) (limited to 'app/src/main/java/github') diff --git a/app/src/main/java/github/daneren2005/dsub/fragments/MainFragment.java b/app/src/main/java/github/daneren2005/dsub/fragments/MainFragment.java index 82e50b76..e7c7f1fb 100644 --- a/app/src/main/java/github/daneren2005/dsub/fragments/MainFragment.java +++ b/app/src/main/java/github/daneren2005/dsub/fragments/MainFragment.java @@ -270,6 +270,10 @@ public class MainFragment extends SelectRecyclerFragment { } private void getLogs() { + if (EnvironmentVariables.PASTEBIN_DEV_KEY == null) { + Util.toast(context, "No PASTEBIN_DEV_KEY configured - can't upload logs"); + return; + } try { final PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); new LoadingTask(context) { diff --git a/app/src/main/java/github/daneren2005/dsub/util/EnvironmentVariables.java b/app/src/main/java/github/daneren2005/dsub/util/EnvironmentVariables.java index 710d5232..8af74f07 100644 --- a/app/src/main/java/github/daneren2005/dsub/util/EnvironmentVariables.java +++ b/app/src/main/java/github/daneren2005/dsub/util/EnvironmentVariables.java @@ -16,6 +16,6 @@ package github.daneren2005.dsub.util; public final class EnvironmentVariables { - public static final String PASTEBIN_DEV_KEY = ""; - public static final String CAST_APPLICATION_ID = ""; + public static final String PASTEBIN_DEV_KEY = null; + public static final String CAST_APPLICATION_ID = null; } diff --git a/app/src/main/java/github/daneren2005/dsub/util/compat/CastCompat.java b/app/src/main/java/github/daneren2005/dsub/util/compat/CastCompat.java index 415106db..08bac263 100644 --- a/app/src/main/java/github/daneren2005/dsub/util/compat/CastCompat.java +++ b/app/src/main/java/github/daneren2005/dsub/util/compat/CastCompat.java @@ -27,6 +27,9 @@ import github.daneren2005.dsub.util.EnvironmentVariables; public final class CastCompat { static { + if (EnvironmentVariables.CAST_APPLICATION_ID == null) { + throw new RuntimeException("CAST_APPLICATION_ID not provided"); + } try { Class.forName("com.google.android.gms.cast.CastDevice"); } catch (Exception ex) { -- cgit v1.2.3 From 28336265e822ab4b2ff1697fd5066fc0cfd88861 Mon Sep 17 00:00:00 2001 From: Carey Metcalfe Date: Fri, 13 Oct 2017 22:34:23 -0400 Subject: Use product flavours to enable builds without proprietary libraries This commit uses build flavours to provide two different builds: A 'floss' build and a 'google' build. - The 'floss' build builds a basic version of the app with no dependencies on proprietary libraries. - The 'google' build adds Chromecast support and a few other extras provided by Google Play Services --- app/build.gradle | 12 +- .../daneren2005/dsub/util/compat/GoogleCompat.java | 31 ++ app/src/google/AndroidManifest.xml | 18 + .../dsub/service/ChromeCastController.java | 506 ++++++++++++++++++++ .../daneren2005/dsub/util/compat/GoogleCompat.java | 61 +++ app/src/main/AndroidManifest.xml | 11 - .../dsub/service/ChromeCastController.java | 507 --------------------- .../daneren2005/dsub/service/RESTMusicService.java | 6 +- .../daneren2005/dsub/util/MediaRouteManager.java | 29 +- .../daneren2005/dsub/util/compat/CastCompat.java | 56 --- 10 files changed, 635 insertions(+), 602 deletions(-) create mode 100644 app/src/floss/java/github/daneren2005/dsub/util/compat/GoogleCompat.java create mode 100644 app/src/google/AndroidManifest.xml create mode 100644 app/src/google/java/github/daneren2005/dsub/service/ChromeCastController.java create mode 100644 app/src/google/java/github/daneren2005/dsub/util/compat/GoogleCompat.java delete mode 100644 app/src/main/java/github/daneren2005/dsub/service/ChromeCastController.java delete mode 100644 app/src/main/java/github/daneren2005/dsub/util/compat/CastCompat.java (limited to 'app/src/main/java/github') diff --git a/app/build.gradle b/app/build.gradle index 26afb5ea..47604723 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -28,6 +28,16 @@ android { } } + productFlavors { + floss { + // FLOSS build (no proprietary libraries) + } + google { + // FLOSS build + Google libraries + // Adds ChromeCast support + } + } + packagingOptions { exclude 'META-INF/beans.xml' } @@ -51,7 +61,7 @@ dependencies { compile 'com.android.support:mediarouter-v7:24.2.+' compile 'com.android.support:recyclerview-v7:24.2.+' compile 'com.android.support:design:24.2.+' - compile 'com.google.android.gms:play-services-cast:8.1.0' + googleCompile 'com.google.android.gms:play-services-cast:8.1.0' compile 'com.sothree.slidinguppanel:library:3.0.0' compile 'de.hdodenhof:circleimageview:1.2.1' compile 'com.shehabic.droppy:Droppy:0.5.1@aar' diff --git a/app/src/floss/java/github/daneren2005/dsub/util/compat/GoogleCompat.java b/app/src/floss/java/github/daneren2005/dsub/util/compat/GoogleCompat.java new file mode 100644 index 00000000..08db4f1c --- /dev/null +++ b/app/src/floss/java/github/daneren2005/dsub/util/compat/GoogleCompat.java @@ -0,0 +1,31 @@ +package github.daneren2005.dsub.util.compat; + +import android.content.Context; +import android.support.v7.media.MediaRouter; + +import github.daneren2005.dsub.service.DownloadService; +import github.daneren2005.dsub.service.RemoteController; + + +// Provides stubs for Google-related functionality +public final class GoogleCompat { + + public static boolean playServicesAvailable(Context context) { + return false; + } + + public static void installProvider(Context context) throws Exception { + } + + public static boolean castAvailable() { + return false; + } + + public static RemoteController getController(DownloadService downloadService, MediaRouter.RouteInfo info) { + return null; + } + + public static String getCastControlCategory() { + return null; + } +} diff --git a/app/src/google/AndroidManifest.xml b/app/src/google/AndroidManifest.xml new file mode 100644 index 00000000..14827d43 --- /dev/null +++ b/app/src/google/AndroidManifest.xml @@ -0,0 +1,18 @@ + + + + + + + + + diff --git a/app/src/google/java/github/daneren2005/dsub/service/ChromeCastController.java b/app/src/google/java/github/daneren2005/dsub/service/ChromeCastController.java new file mode 100644 index 00000000..b2405705 --- /dev/null +++ b/app/src/google/java/github/daneren2005/dsub/service/ChromeCastController.java @@ -0,0 +1,506 @@ +/* + This file is part of Subsonic. + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + Copyright 2014 (C) Scott Jackson +*/ + +package github.daneren2005.dsub.service; + +import android.content.SharedPreferences; +import android.net.Uri; +import android.os.Bundle; +import android.util.Log; + +import com.google.android.gms.cast.ApplicationMetadata; +import com.google.android.gms.cast.Cast; +import com.google.android.gms.cast.CastDevice; +import com.google.android.gms.cast.CastStatusCodes; +import com.google.android.gms.cast.MediaInfo; +import com.google.android.gms.cast.MediaMetadata; +import com.google.android.gms.cast.MediaStatus; +import com.google.android.gms.cast.RemoteMediaPlayer; +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.ResultCallback; +import com.google.android.gms.common.api.Status; +import com.google.android.gms.common.images.WebImage; + +import java.io.File; +import java.io.IOException; + +import github.daneren2005.dsub.R; +import github.daneren2005.dsub.domain.MusicDirectory; +import github.daneren2005.dsub.domain.PlayerState; +import github.daneren2005.dsub.domain.RemoteControlState; +import github.daneren2005.dsub.util.Constants; +import github.daneren2005.dsub.util.EnvironmentVariables; +import github.daneren2005.dsub.util.FileUtil; +import github.daneren2005.dsub.util.Util; +import github.daneren2005.serverproxy.FileProxy; +import github.daneren2005.serverproxy.ServerProxy; +import github.daneren2005.serverproxy.WebProxy; + +/** + * Created by owner on 2/9/14. + */ +public class ChromeCastController extends RemoteController { + private static final String TAG = ChromeCastController.class.getSimpleName(); + + private CastDevice castDevice; + private GoogleApiClient apiClient; + + private boolean applicationStarted = false; + private boolean waitingForReconnect = false; + private boolean error = false; + private boolean ignoreNextPaused = false; + private String sessionId; + private boolean isStopping = false; + private Runnable afterUpdateComplete = null; + + private RemoteMediaPlayer mediaPlayer; + private double gain = 0.5; + + public ChromeCastController(DownloadService downloadService, CastDevice castDevice) { + super(downloadService); + this.castDevice = castDevice; + } + + @Override + public void create(boolean playing, int seconds) { + downloadService.setPlayerState(PlayerState.PREPARING); + + ConnectionCallbacks connectionCallbacks = new ConnectionCallbacks(playing, seconds); + ConnectionFailedListener connectionFailedListener = new ConnectionFailedListener(); + Cast.Listener castClientListener = new Cast.Listener() { + @Override + public void onApplicationStatusChanged() { + if (apiClient != null && apiClient.isConnected()) { + Log.i(TAG, "onApplicationStatusChanged: " + Cast.CastApi.getApplicationStatus(apiClient)); + } + } + + @Override + public void onVolumeChanged() { + if (apiClient != null && applicationStarted) { + try { + gain = Cast.CastApi.getVolume(apiClient); + } catch (Exception e) { + Log.w(TAG, "Failed to get volume"); + } + } + } + + @Override + public void onApplicationDisconnected(int errorCode) { + shutdownInternal(); + } + + }; + + Cast.CastOptions.Builder apiOptionsBuilder = Cast.CastOptions.builder(castDevice, castClientListener).setVerboseLoggingEnabled(true); + apiClient = new GoogleApiClient.Builder(downloadService).useDefaultAccount() + .addApi(Cast.API, apiOptionsBuilder.build()) + .addConnectionCallbacks(connectionCallbacks) + .addOnConnectionFailedListener(connectionFailedListener) + .build(); + + apiClient.connect(); + } + + @Override + public void start() { + if(error) { + error = false; + Log.w(TAG, "Attempting to restart song"); + startSong(downloadService.getCurrentPlaying(), true, 0); + return; + } + + try { + mediaPlayer.play(apiClient); + } catch(Exception e) { + Log.e(TAG, "Failed to start"); + } + } + + @Override + public void stop() { + try { + mediaPlayer.pause(apiClient); + } catch(Exception e) { + Log.e(TAG, "Failed to pause"); + } + } + + @Override + public void shutdown() { + try { + if(mediaPlayer != null && !error) { + mediaPlayer.stop(apiClient); + } + } catch(Exception e) { + Log.e(TAG, "Failed to stop mediaPlayer", e); + } + + try { + if(apiClient != null) { + Cast.CastApi.stopApplication(apiClient); + Cast.CastApi.removeMessageReceivedCallbacks(apiClient, mediaPlayer.getNamespace()); + mediaPlayer = null; + applicationStarted = false; + } + } catch(Exception e) { + Log.e(TAG, "Failed to shutdown application", e); + } + + if(apiClient != null && apiClient.isConnected()) { + apiClient.disconnect(); + } + apiClient = null; + + if(proxy != null) { + proxy.stop(); + proxy = null; + } + } + + private void shutdownInternal() { + // This will call this.shutdown() indirectly + downloadService.setRemoteEnabled(RemoteControlState.LOCAL, null); + } + + @Override + public void updatePlaylist() { + if(downloadService.getCurrentPlaying() == null) { + startSong(null, false, 0); + } + } + + @Override + public void changePosition(int seconds) { + try { + mediaPlayer.seek(apiClient, seconds * 1000L); + } catch(Exception e) { + Log.e(TAG, "FAiled to seek to " + seconds); + } + } + + @Override + public void changeTrack(int index, DownloadFile song) { + startSong(song, true, 0); + } + + @Override + public void setVolume(int volume) { + gain = volume / 10.0; + + try { + Cast.CastApi.setVolume(apiClient, gain); + } catch(Exception e) { + Log.e(TAG, "Failed to the volume"); + } + } + @Override + public void updateVolume(boolean up) { + double delta = up ? 0.1 : -0.1; + gain += delta; + gain = Math.max(gain, 0.0); + gain = Math.min(gain, 1.0); + + try { + Cast.CastApi.setVolume(apiClient, gain); + } catch(Exception e) { + Log.e(TAG, "Failed to the volume"); + } + } + @Override + public double getVolume() { + return Cast.CastApi.getVolume(apiClient); + } + + @Override + public int getRemotePosition() { + if(mediaPlayer != null) { + return (int) (mediaPlayer.getApproximateStreamPosition() / 1000L); + } else { + return 0; + } + } + + @Override + public int getRemoteDuration() { + if(mediaPlayer != null) { + return (int) (mediaPlayer.getStreamDuration() / 1000L); + } else { + return 0; + } + } + + void startSong(final DownloadFile currentPlaying, final boolean autoStart, final int position) { + if(currentPlaying == null) { + try { + if (mediaPlayer != null && !error && !isStopping) { + isStopping = true; + mediaPlayer.stop(apiClient).setResultCallback(new ResultCallback() { + @Override + public void onResult(RemoteMediaPlayer.MediaChannelResult mediaChannelResult) { + isStopping = false; + + if(afterUpdateComplete != null) { + afterUpdateComplete.run(); + afterUpdateComplete = null; + } + } + }); + } + } catch(Exception e) { + // Just means it didn't need to be stopped + } + downloadService.setPlayerState(PlayerState.IDLE); + return; + } else if(isStopping) { + afterUpdateComplete = new Runnable() { + @Override + public void run() { + startSong(currentPlaying, autoStart, position); + } + }; + return; + } + + downloadService.setPlayerState(PlayerState.PREPARING); + MusicDirectory.Entry song = currentPlaying.getSong(); + + try { + MusicService musicService = MusicServiceFactory.getMusicService(downloadService); + String url = getStreamUrl(musicService, currentPlaying); + + // Setup song/video information + MediaMetadata meta = new MediaMetadata(song.isVideo() ? MediaMetadata.MEDIA_TYPE_MOVIE : MediaMetadata.MEDIA_TYPE_MUSIC_TRACK); + meta.putString(MediaMetadata.KEY_TITLE, song.getTitle()); + if(song.getTrack() != null) { + meta.putInt(MediaMetadata.KEY_TRACK_NUMBER, song.getTrack()); + } + if(!song.isVideo()) { + meta.putString(MediaMetadata.KEY_ARTIST, song.getArtist()); + meta.putString(MediaMetadata.KEY_ALBUM_ARTIST, song.getArtist()); + meta.putString(MediaMetadata.KEY_ALBUM_TITLE, song.getAlbum()); + + if(castDevice.hasCapability(CastDevice.CAPABILITY_VIDEO_OUT)) { + if (proxy == null || proxy instanceof WebProxy) { + String coverArt = musicService.getCoverArtUrl(downloadService, song); + + // If proxy is going, it is a web proxy + if (proxy != null) { + coverArt = proxy.getPublicAddress(coverArt); + } + + meta.addImage(new WebImage(Uri.parse(coverArt))); + } else { + File coverArtFile = FileUtil.getAlbumArtFile(downloadService, song); + if (coverArtFile != null && coverArtFile.exists()) { + String coverArt = proxy.getPublicAddress(coverArtFile.getPath()); + meta.addImage(new WebImage(Uri.parse(coverArt))); + } + } + } + } + + String contentType; + if(song.isVideo()) { + contentType = "application/x-mpegURL"; + } + else if(song.getTranscodedContentType() != null) { + contentType = song.getTranscodedContentType(); + } else if(song.getContentType() != null) { + contentType = song.getContentType(); + } else { + contentType = "audio/mpeg"; + } + + // Load it into a MediaInfo wrapper + MediaInfo mediaInfo = new MediaInfo.Builder(url) + .setContentType(contentType) + .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) + .setMetadata(meta) + .build(); + + if(autoStart) { + ignoreNextPaused = true; + } + + ResultCallback callback = new ResultCallback() { + @Override + public void onResult(RemoteMediaPlayer.MediaChannelResult result) { + if (result.getStatus().isSuccess()) { + // Handled in other handler + } else if(result.getStatus().getStatusCode() == CastStatusCodes.REPLACED) { + Log.w(TAG, "Request was replaced: " + currentPlaying.toString()); + } else { + Log.e(TAG, "Failed to load: " + result.getStatus().toString()); + failedLoad(); + } + } + }; + + if(position > 0) { + mediaPlayer.load(apiClient, mediaInfo, autoStart, position * 1000L).setResultCallback(callback); + } else { + mediaPlayer.load(apiClient, mediaInfo, autoStart).setResultCallback(callback); + } + } catch (IllegalStateException e) { + Log.e(TAG, "Problem occurred with media during loading", e); + failedLoad(); + } catch (Exception e) { + Log.e(TAG, "Problem opening media during loading", e); + failedLoad(); + } + } + + private void failedLoad() { + Util.toast(downloadService, downloadService.getResources().getString(R.string.download_failed_to_load)); + downloadService.setPlayerState(PlayerState.STOPPED); + error = true; + } + + + private class ConnectionCallbacks implements GoogleApiClient.ConnectionCallbacks { + private boolean isPlaying; + private int position; + private ResultCallback resultCallback; + + ConnectionCallbacks(boolean isPlaying, int position) { + this.isPlaying = isPlaying; + this.position = position; + + resultCallback = new ResultCallback() { + @Override + public void onResult(Cast.ApplicationConnectionResult result) { + Status status = result.getStatus(); + if (status.isSuccess()) { + ApplicationMetadata applicationMetadata = result.getApplicationMetadata(); + sessionId = result.getSessionId(); + String applicationStatus = result.getApplicationStatus(); + boolean wasLaunched = result.getWasLaunched(); + + applicationStarted = true; + setupChannel(); + } else { + shutdownInternal(); + } + } + }; + } + + @Override + public void onConnected(Bundle connectionHint) { + if (waitingForReconnect) { + Log.i(TAG, "Reconnecting"); + reconnectApplication(); + } else { + launchApplication(); + } + } + + @Override + public void onConnectionSuspended(int cause) { + Log.w(TAG, "Connection suspended"); + isPlaying = downloadService.getPlayerState() == PlayerState.STARTED; + position = getRemotePosition(); + waitingForReconnect = true; + } + + void launchApplication() { + try { + Cast.CastApi.launchApplication(apiClient, EnvironmentVariables.CAST_APPLICATION_ID, false).setResultCallback(resultCallback); + } catch (Exception e) { + Log.e(TAG, "Failed to launch application", e); + } + } + void reconnectApplication() { + try { + Cast.CastApi.joinApplication(apiClient, EnvironmentVariables.CAST_APPLICATION_ID, sessionId).setResultCallback(resultCallback); + } catch (Exception e) { + Log.e(TAG, "Failed to reconnect application", e); + } + } + void setupChannel() { + if(!waitingForReconnect) { + mediaPlayer = new RemoteMediaPlayer(); + mediaPlayer.setOnStatusUpdatedListener(new RemoteMediaPlayer.OnStatusUpdatedListener() { + @Override + public void onStatusUpdated() { + MediaStatus mediaStatus = mediaPlayer.getMediaStatus(); + if (mediaStatus == null) { + return; + } + + switch (mediaStatus.getPlayerState()) { + case MediaStatus.PLAYER_STATE_PLAYING: + if (ignoreNextPaused) { + ignoreNextPaused = false; + } + downloadService.setPlayerState(PlayerState.STARTED); + break; + case MediaStatus.PLAYER_STATE_PAUSED: + if (!ignoreNextPaused) { + downloadService.setPlayerState(PlayerState.PAUSED); + } + break; + case MediaStatus.PLAYER_STATE_BUFFERING: + downloadService.setPlayerState(PlayerState.PREPARING); + break; + case MediaStatus.PLAYER_STATE_IDLE: + if (mediaStatus.getIdleReason() == MediaStatus.IDLE_REASON_FINISHED) { + if(downloadService.getPlayerState() != PlayerState.PREPARING) { + downloadService.onSongCompleted(); + } + } else if (mediaStatus.getIdleReason() == MediaStatus.IDLE_REASON_INTERRUPTED) { + if (downloadService.getPlayerState() != PlayerState.PREPARING) { + downloadService.setPlayerState(PlayerState.PREPARING); + } + } else if (mediaStatus.getIdleReason() == MediaStatus.IDLE_REASON_ERROR) { + Log.e(TAG, "Idle due to unknown error"); + downloadService.onSongCompleted(); + } else { + Log.w(TAG, "Idle reason: " + mediaStatus.getIdleReason()); + downloadService.setPlayerState(PlayerState.IDLE); + } + break; + } + } + }); + } + + try { + Cast.CastApi.setMessageReceivedCallbacks(apiClient, mediaPlayer.getNamespace(), mediaPlayer); + } catch (Exception e) { + Log.e(TAG, "Exception while creating channel", e); + } + + if(!waitingForReconnect) { + DownloadFile currentPlaying = downloadService.getCurrentPlaying(); + startSong(currentPlaying, isPlaying, position); + } + if(waitingForReconnect) { + waitingForReconnect = false; + } + } + } + + private class ConnectionFailedListener implements GoogleApiClient.OnConnectionFailedListener { + @Override + public void onConnectionFailed(ConnectionResult result) { + shutdownInternal(); + } + } +} diff --git a/app/src/google/java/github/daneren2005/dsub/util/compat/GoogleCompat.java b/app/src/google/java/github/daneren2005/dsub/util/compat/GoogleCompat.java new file mode 100644 index 00000000..63992b0f --- /dev/null +++ b/app/src/google/java/github/daneren2005/dsub/util/compat/GoogleCompat.java @@ -0,0 +1,61 @@ +package github.daneren2005.dsub.util.compat; + +import android.content.Context; +import android.support.v7.media.MediaRouter; +import android.util.Log; + +import com.google.android.gms.cast.CastDevice; +import com.google.android.gms.cast.CastMediaControlIntent; +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.GoogleApiAvailability; +import com.google.android.gms.security.ProviderInstaller; + +import github.daneren2005.dsub.service.ChromeCastController; +import github.daneren2005.dsub.service.DownloadService; +import github.daneren2005.dsub.service.RemoteController; +import github.daneren2005.dsub.util.EnvironmentVariables; + +public final class GoogleCompat { + + private static final String TAG = GoogleCompat.class.getSimpleName(); + + public static boolean playServicesAvailable(Context context){ + int result = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context); + if(result != ConnectionResult.SUCCESS){ + Log.w(TAG, "No play services, failed with result: " + result); + return false; + } + return true; + } + + public static void installProvider(Context context) throws Exception{ + ProviderInstaller.installIfNeeded(context); + } + + public static boolean castAvailable() { + if (EnvironmentVariables.CAST_APPLICATION_ID == null) { + Log.w(TAG, "CAST_APPLICATION_ID not provided"); + return false; + } + try { + Class.forName("com.google.android.gms.cast.CastDevice"); + } catch (Exception ex) { + Log.w(TAG, "Chromecast library not available"); + return false; + } + return true; + } + + public static RemoteController getController(DownloadService downloadService, MediaRouter.RouteInfo info) { + CastDevice device = CastDevice.getFromBundle(info.getExtras()); + if(device != null) { + return new ChromeCastController(downloadService, device); + } else { + return null; + } + } + + public static String getCastControlCategory() { + return CastMediaControlIntent.categoryForCast(EnvironmentVariables.CAST_APPLICATION_ID); + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 37643a21..06f79977 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -243,18 +243,7 @@ - - - - - - diff --git a/app/src/main/java/github/daneren2005/dsub/service/ChromeCastController.java b/app/src/main/java/github/daneren2005/dsub/service/ChromeCastController.java deleted file mode 100644 index f9e2bfb1..00000000 --- a/app/src/main/java/github/daneren2005/dsub/service/ChromeCastController.java +++ /dev/null @@ -1,507 +0,0 @@ -/* - This file is part of Subsonic. - Subsonic is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - Subsonic is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - You should have received a copy of the GNU General Public License - along with Subsonic. If not, see . - Copyright 2014 (C) Scott Jackson -*/ - -package github.daneren2005.dsub.service; - -import android.content.SharedPreferences; -import android.net.Uri; -import android.os.Bundle; -import android.util.Log; - -import com.google.android.gms.cast.ApplicationMetadata; -import com.google.android.gms.cast.Cast; -import com.google.android.gms.cast.CastDevice; -import com.google.android.gms.cast.CastStatusCodes; -import com.google.android.gms.cast.MediaInfo; -import com.google.android.gms.cast.MediaMetadata; -import com.google.android.gms.cast.MediaStatus; -import com.google.android.gms.cast.RemoteMediaPlayer; -import com.google.android.gms.common.ConnectionResult; -import com.google.android.gms.common.api.GoogleApiClient; -import com.google.android.gms.common.api.ResultCallback; -import com.google.android.gms.common.api.Status; -import com.google.android.gms.common.images.WebImage; - -import java.io.File; -import java.io.IOException; - -import github.daneren2005.dsub.R; -import github.daneren2005.dsub.domain.MusicDirectory; -import github.daneren2005.dsub.domain.PlayerState; -import github.daneren2005.dsub.domain.RemoteControlState; -import github.daneren2005.dsub.util.Constants; -import github.daneren2005.dsub.util.EnvironmentVariables; -import github.daneren2005.dsub.util.FileUtil; -import github.daneren2005.dsub.util.Util; -import github.daneren2005.dsub.util.compat.CastCompat; -import github.daneren2005.serverproxy.FileProxy; -import github.daneren2005.serverproxy.ServerProxy; -import github.daneren2005.serverproxy.WebProxy; - -/** - * Created by owner on 2/9/14. - */ -public class ChromeCastController extends RemoteController { - private static final String TAG = ChromeCastController.class.getSimpleName(); - - private CastDevice castDevice; - private GoogleApiClient apiClient; - - private boolean applicationStarted = false; - private boolean waitingForReconnect = false; - private boolean error = false; - private boolean ignoreNextPaused = false; - private String sessionId; - private boolean isStopping = false; - private Runnable afterUpdateComplete = null; - - private RemoteMediaPlayer mediaPlayer; - private double gain = 0.5; - - public ChromeCastController(DownloadService downloadService, CastDevice castDevice) { - super(downloadService); - this.castDevice = castDevice; - } - - @Override - public void create(boolean playing, int seconds) { - downloadService.setPlayerState(PlayerState.PREPARING); - - ConnectionCallbacks connectionCallbacks = new ConnectionCallbacks(playing, seconds); - ConnectionFailedListener connectionFailedListener = new ConnectionFailedListener(); - Cast.Listener castClientListener = new Cast.Listener() { - @Override - public void onApplicationStatusChanged() { - if (apiClient != null && apiClient.isConnected()) { - Log.i(TAG, "onApplicationStatusChanged: " + Cast.CastApi.getApplicationStatus(apiClient)); - } - } - - @Override - public void onVolumeChanged() { - if (apiClient != null && applicationStarted) { - try { - gain = Cast.CastApi.getVolume(apiClient); - } catch (Exception e) { - Log.w(TAG, "Failed to get volume"); - } - } - } - - @Override - public void onApplicationDisconnected(int errorCode) { - shutdownInternal(); - } - - }; - - Cast.CastOptions.Builder apiOptionsBuilder = Cast.CastOptions.builder(castDevice, castClientListener).setVerboseLoggingEnabled(true); - apiClient = new GoogleApiClient.Builder(downloadService).useDefaultAccount() - .addApi(Cast.API, apiOptionsBuilder.build()) - .addConnectionCallbacks(connectionCallbacks) - .addOnConnectionFailedListener(connectionFailedListener) - .build(); - - apiClient.connect(); - } - - @Override - public void start() { - if(error) { - error = false; - Log.w(TAG, "Attempting to restart song"); - startSong(downloadService.getCurrentPlaying(), true, 0); - return; - } - - try { - mediaPlayer.play(apiClient); - } catch(Exception e) { - Log.e(TAG, "Failed to start"); - } - } - - @Override - public void stop() { - try { - mediaPlayer.pause(apiClient); - } catch(Exception e) { - Log.e(TAG, "Failed to pause"); - } - } - - @Override - public void shutdown() { - try { - if(mediaPlayer != null && !error) { - mediaPlayer.stop(apiClient); - } - } catch(Exception e) { - Log.e(TAG, "Failed to stop mediaPlayer", e); - } - - try { - if(apiClient != null) { - Cast.CastApi.stopApplication(apiClient); - Cast.CastApi.removeMessageReceivedCallbacks(apiClient, mediaPlayer.getNamespace()); - mediaPlayer = null; - applicationStarted = false; - } - } catch(Exception e) { - Log.e(TAG, "Failed to shutdown application", e); - } - - if(apiClient != null && apiClient.isConnected()) { - apiClient.disconnect(); - } - apiClient = null; - - if(proxy != null) { - proxy.stop(); - proxy = null; - } - } - - private void shutdownInternal() { - // This will call this.shutdown() indirectly - downloadService.setRemoteEnabled(RemoteControlState.LOCAL, null); - } - - @Override - public void updatePlaylist() { - if(downloadService.getCurrentPlaying() == null) { - startSong(null, false, 0); - } - } - - @Override - public void changePosition(int seconds) { - try { - mediaPlayer.seek(apiClient, seconds * 1000L); - } catch(Exception e) { - Log.e(TAG, "FAiled to seek to " + seconds); - } - } - - @Override - public void changeTrack(int index, DownloadFile song) { - startSong(song, true, 0); - } - - @Override - public void setVolume(int volume) { - gain = volume / 10.0; - - try { - Cast.CastApi.setVolume(apiClient, gain); - } catch(Exception e) { - Log.e(TAG, "Failed to the volume"); - } - } - @Override - public void updateVolume(boolean up) { - double delta = up ? 0.1 : -0.1; - gain += delta; - gain = Math.max(gain, 0.0); - gain = Math.min(gain, 1.0); - - try { - Cast.CastApi.setVolume(apiClient, gain); - } catch(Exception e) { - Log.e(TAG, "Failed to the volume"); - } - } - @Override - public double getVolume() { - return Cast.CastApi.getVolume(apiClient); - } - - @Override - public int getRemotePosition() { - if(mediaPlayer != null) { - return (int) (mediaPlayer.getApproximateStreamPosition() / 1000L); - } else { - return 0; - } - } - - @Override - public int getRemoteDuration() { - if(mediaPlayer != null) { - return (int) (mediaPlayer.getStreamDuration() / 1000L); - } else { - return 0; - } - } - - void startSong(final DownloadFile currentPlaying, final boolean autoStart, final int position) { - if(currentPlaying == null) { - try { - if (mediaPlayer != null && !error && !isStopping) { - isStopping = true; - mediaPlayer.stop(apiClient).setResultCallback(new ResultCallback() { - @Override - public void onResult(RemoteMediaPlayer.MediaChannelResult mediaChannelResult) { - isStopping = false; - - if(afterUpdateComplete != null) { - afterUpdateComplete.run(); - afterUpdateComplete = null; - } - } - }); - } - } catch(Exception e) { - // Just means it didn't need to be stopped - } - downloadService.setPlayerState(PlayerState.IDLE); - return; - } else if(isStopping) { - afterUpdateComplete = new Runnable() { - @Override - public void run() { - startSong(currentPlaying, autoStart, position); - } - }; - return; - } - - downloadService.setPlayerState(PlayerState.PREPARING); - MusicDirectory.Entry song = currentPlaying.getSong(); - - try { - MusicService musicService = MusicServiceFactory.getMusicService(downloadService); - String url = getStreamUrl(musicService, currentPlaying); - - // Setup song/video information - MediaMetadata meta = new MediaMetadata(song.isVideo() ? MediaMetadata.MEDIA_TYPE_MOVIE : MediaMetadata.MEDIA_TYPE_MUSIC_TRACK); - meta.putString(MediaMetadata.KEY_TITLE, song.getTitle()); - if(song.getTrack() != null) { - meta.putInt(MediaMetadata.KEY_TRACK_NUMBER, song.getTrack()); - } - if(!song.isVideo()) { - meta.putString(MediaMetadata.KEY_ARTIST, song.getArtist()); - meta.putString(MediaMetadata.KEY_ALBUM_ARTIST, song.getArtist()); - meta.putString(MediaMetadata.KEY_ALBUM_TITLE, song.getAlbum()); - - if(castDevice.hasCapability(CastDevice.CAPABILITY_VIDEO_OUT)) { - if (proxy == null || proxy instanceof WebProxy) { - String coverArt = musicService.getCoverArtUrl(downloadService, song); - - // If proxy is going, it is a web proxy - if (proxy != null) { - coverArt = proxy.getPublicAddress(coverArt); - } - - meta.addImage(new WebImage(Uri.parse(coverArt))); - } else { - File coverArtFile = FileUtil.getAlbumArtFile(downloadService, song); - if (coverArtFile != null && coverArtFile.exists()) { - String coverArt = proxy.getPublicAddress(coverArtFile.getPath()); - meta.addImage(new WebImage(Uri.parse(coverArt))); - } - } - } - } - - String contentType; - if(song.isVideo()) { - contentType = "application/x-mpegURL"; - } - else if(song.getTranscodedContentType() != null) { - contentType = song.getTranscodedContentType(); - } else if(song.getContentType() != null) { - contentType = song.getContentType(); - } else { - contentType = "audio/mpeg"; - } - - // Load it into a MediaInfo wrapper - MediaInfo mediaInfo = new MediaInfo.Builder(url) - .setContentType(contentType) - .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) - .setMetadata(meta) - .build(); - - if(autoStart) { - ignoreNextPaused = true; - } - - ResultCallback callback = new ResultCallback() { - @Override - public void onResult(RemoteMediaPlayer.MediaChannelResult result) { - if (result.getStatus().isSuccess()) { - // Handled in other handler - } else if(result.getStatus().getStatusCode() == CastStatusCodes.REPLACED) { - Log.w(TAG, "Request was replaced: " + currentPlaying.toString()); - } else { - Log.e(TAG, "Failed to load: " + result.getStatus().toString()); - failedLoad(); - } - } - }; - - if(position > 0) { - mediaPlayer.load(apiClient, mediaInfo, autoStart, position * 1000L).setResultCallback(callback); - } else { - mediaPlayer.load(apiClient, mediaInfo, autoStart).setResultCallback(callback); - } - } catch (IllegalStateException e) { - Log.e(TAG, "Problem occurred with media during loading", e); - failedLoad(); - } catch (Exception e) { - Log.e(TAG, "Problem opening media during loading", e); - failedLoad(); - } - } - - private void failedLoad() { - Util.toast(downloadService, downloadService.getResources().getString(R.string.download_failed_to_load)); - downloadService.setPlayerState(PlayerState.STOPPED); - error = true; - } - - - private class ConnectionCallbacks implements GoogleApiClient.ConnectionCallbacks { - private boolean isPlaying; - private int position; - private ResultCallback resultCallback; - - ConnectionCallbacks(boolean isPlaying, int position) { - this.isPlaying = isPlaying; - this.position = position; - - resultCallback = new ResultCallback() { - @Override - public void onResult(Cast.ApplicationConnectionResult result) { - Status status = result.getStatus(); - if (status.isSuccess()) { - ApplicationMetadata applicationMetadata = result.getApplicationMetadata(); - sessionId = result.getSessionId(); - String applicationStatus = result.getApplicationStatus(); - boolean wasLaunched = result.getWasLaunched(); - - applicationStarted = true; - setupChannel(); - } else { - shutdownInternal(); - } - } - }; - } - - @Override - public void onConnected(Bundle connectionHint) { - if (waitingForReconnect) { - Log.i(TAG, "Reconnecting"); - reconnectApplication(); - } else { - launchApplication(); - } - } - - @Override - public void onConnectionSuspended(int cause) { - Log.w(TAG, "Connection suspended"); - isPlaying = downloadService.getPlayerState() == PlayerState.STARTED; - position = getRemotePosition(); - waitingForReconnect = true; - } - - void launchApplication() { - try { - Cast.CastApi.launchApplication(apiClient, EnvironmentVariables.CAST_APPLICATION_ID, false).setResultCallback(resultCallback); - } catch (Exception e) { - Log.e(TAG, "Failed to launch application", e); - } - } - void reconnectApplication() { - try { - Cast.CastApi.joinApplication(apiClient, EnvironmentVariables.CAST_APPLICATION_ID, sessionId).setResultCallback(resultCallback); - } catch (Exception e) { - Log.e(TAG, "Failed to reconnect application", e); - } - } - void setupChannel() { - if(!waitingForReconnect) { - mediaPlayer = new RemoteMediaPlayer(); - mediaPlayer.setOnStatusUpdatedListener(new RemoteMediaPlayer.OnStatusUpdatedListener() { - @Override - public void onStatusUpdated() { - MediaStatus mediaStatus = mediaPlayer.getMediaStatus(); - if (mediaStatus == null) { - return; - } - - switch (mediaStatus.getPlayerState()) { - case MediaStatus.PLAYER_STATE_PLAYING: - if (ignoreNextPaused) { - ignoreNextPaused = false; - } - downloadService.setPlayerState(PlayerState.STARTED); - break; - case MediaStatus.PLAYER_STATE_PAUSED: - if (!ignoreNextPaused) { - downloadService.setPlayerState(PlayerState.PAUSED); - } - break; - case MediaStatus.PLAYER_STATE_BUFFERING: - downloadService.setPlayerState(PlayerState.PREPARING); - break; - case MediaStatus.PLAYER_STATE_IDLE: - if (mediaStatus.getIdleReason() == MediaStatus.IDLE_REASON_FINISHED) { - if(downloadService.getPlayerState() != PlayerState.PREPARING) { - downloadService.onSongCompleted(); - } - } else if (mediaStatus.getIdleReason() == MediaStatus.IDLE_REASON_INTERRUPTED) { - if (downloadService.getPlayerState() != PlayerState.PREPARING) { - downloadService.setPlayerState(PlayerState.PREPARING); - } - } else if (mediaStatus.getIdleReason() == MediaStatus.IDLE_REASON_ERROR) { - Log.e(TAG, "Idle due to unknown error"); - downloadService.onSongCompleted(); - } else { - Log.w(TAG, "Idle reason: " + mediaStatus.getIdleReason()); - downloadService.setPlayerState(PlayerState.IDLE); - } - break; - } - } - }); - } - - try { - Cast.CastApi.setMessageReceivedCallbacks(apiClient, mediaPlayer.getNamespace(), mediaPlayer); - } catch (Exception e) { - Log.e(TAG, "Exception while creating channel", e); - } - - if(!waitingForReconnect) { - DownloadFile currentPlaying = downloadService.getCurrentPlaying(); - startSong(currentPlaying, isPlaying, position); - } - if(waitingForReconnect) { - waitingForReconnect = false; - } - } - } - - private class ConnectionFailedListener implements GoogleApiClient.OnConnectionFailedListener { - @Override - public void onConnectionFailed(ConnectionResult result) { - shutdownInternal(); - } - } -} diff --git a/app/src/main/java/github/daneren2005/dsub/service/RESTMusicService.java b/app/src/main/java/github/daneren2005/dsub/service/RESTMusicService.java index 657ac4a9..a4987b09 100644 --- a/app/src/main/java/github/daneren2005/dsub/service/RESTMusicService.java +++ b/app/src/main/java/github/daneren2005/dsub/service/RESTMusicService.java @@ -42,8 +42,6 @@ import android.net.NetworkInfo; import android.util.Base64; import android.util.Log; -import com.google.android.gms.security.ProviderInstaller; - import github.daneren2005.dsub.R; import github.daneren2005.dsub.domain.*; import github.daneren2005.dsub.fragments.MainFragment; @@ -81,6 +79,8 @@ import github.daneren2005.dsub.util.FileUtil; import github.daneren2005.dsub.util.ProgressListener; import github.daneren2005.dsub.util.SongDBHandler; import github.daneren2005.dsub.util.Util; +import github.daneren2005.dsub.util.compat.GoogleCompat; + import java.io.*; import java.util.Map; import java.util.zip.GZIPInputStream; @@ -1875,7 +1875,7 @@ public class RESTMusicService implements MusicService { private HttpURLConnection getConnectionDirect(Context context, String url, Map headers, int minNetworkTimeout) throws Exception { if(!hasInstalledGoogleSSL) { try { - ProviderInstaller.installIfNeeded(context); + GoogleCompat.installProvider(context); } catch(Exception e) { // Just continue on anyways, doesn't really harm anything if this fails Log.w(TAG, "Failed to update to use Google Play SSL", e); diff --git a/app/src/main/java/github/daneren2005/dsub/util/MediaRouteManager.java b/app/src/main/java/github/daneren2005/dsub/util/MediaRouteManager.java index 73ec6aec..e19cc156 100644 --- a/app/src/main/java/github/daneren2005/dsub/util/MediaRouteManager.java +++ b/app/src/main/java/github/daneren2005/dsub/util/MediaRouteManager.java @@ -19,10 +19,6 @@ import android.os.Build; import android.support.v7.media.MediaRouteProvider; import android.support.v7.media.MediaRouteSelector; import android.support.v7.media.MediaRouter; -import android.util.Log; - -import com.google.android.gms.common.ConnectionResult; -import com.google.android.gms.common.GooglePlayServicesUtil; import java.util.ArrayList; import java.util.List; @@ -32,7 +28,7 @@ import github.daneren2005.dsub.provider.DLNARouteProvider; import github.daneren2005.dsub.provider.JukeboxRouteProvider; import github.daneren2005.dsub.service.DownloadService; import github.daneren2005.dsub.service.RemoteController; -import github.daneren2005.dsub.util.compat.CastCompat; +import github.daneren2005.dsub.util.compat.GoogleCompat; import static android.support.v7.media.MediaRouter.RouteInfo; @@ -50,25 +46,10 @@ public class MediaRouteManager extends MediaRouter.Callback { private List onlineProviders = new ArrayList(); private DLNARouteProvider dlnaProvider; - static { - try { - CastCompat.checkAvailable(); - castAvailable = true; - } catch(Throwable t) { - castAvailable = false; - } - } - public MediaRouteManager(DownloadService downloadService) { this.downloadService = downloadService; router = MediaRouter.getInstance(downloadService); - - // Check if play services is available - int result = GooglePlayServicesUtil.isGooglePlayServicesAvailable(downloadService); - if(result != ConnectionResult.SUCCESS){ - Log.w(TAG, "No play services, failed with result: " + result); - castAvailable = false; - } + castAvailable = GoogleCompat.playServicesAvailable(downloadService) && GoogleCompat.castAvailable(); addProviders(); buildSelector(); @@ -83,7 +64,7 @@ public class MediaRouteManager extends MediaRouter.Callback { @Override public void onRouteSelected(MediaRouter router, RouteInfo info) { if(castAvailable) { - RemoteController controller = CastCompat.getController(downloadService, info); + RemoteController controller = GoogleCompat.getController(downloadService, info); if(controller != null) { downloadService.setRemoteEnabled(RemoteControlState.CHROMECAST, controller); } @@ -137,7 +118,7 @@ public class MediaRouteManager extends MediaRouter.Callback { } public RemoteController getRemoteController(RouteInfo info) { if(castAvailable) { - return CastCompat.getController(downloadService, info); + return GoogleCompat.getController(downloadService, info); } else { return null; } @@ -170,7 +151,7 @@ public class MediaRouteManager extends MediaRouter.Callback { builder.addControlCategory(JukeboxRouteProvider.CATEGORY_JUKEBOX_ROUTE); } if(castAvailable) { - builder.addControlCategory(CastCompat.getCastControlCategory()); + builder.addControlCategory(GoogleCompat.getCastControlCategory()); } if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { builder.addControlCategory(DLNARouteProvider.CATEGORY_DLNA); diff --git a/app/src/main/java/github/daneren2005/dsub/util/compat/CastCompat.java b/app/src/main/java/github/daneren2005/dsub/util/compat/CastCompat.java deleted file mode 100644 index 08bac263..00000000 --- a/app/src/main/java/github/daneren2005/dsub/util/compat/CastCompat.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - This file is part of Subsonic. - Subsonic is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - Subsonic is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - You should have received a copy of the GNU General Public License - along with Subsonic. If not, see . - Copyright 2014 (C) Scott Jackson -*/ - -package github.daneren2005.dsub.util.compat; - -import android.support.v7.media.MediaRouter; - -import com.google.android.gms.cast.CastDevice; -import com.google.android.gms.cast.CastMediaControlIntent; - -import github.daneren2005.dsub.service.ChromeCastController; -import github.daneren2005.dsub.service.DownloadService; -import github.daneren2005.dsub.service.RemoteController; -import github.daneren2005.dsub.util.EnvironmentVariables; - -public final class CastCompat { - static { - if (EnvironmentVariables.CAST_APPLICATION_ID == null) { - throw new RuntimeException("CAST_APPLICATION_ID not provided"); - } - try { - Class.forName("com.google.android.gms.cast.CastDevice"); - } catch (Exception ex) { - throw new RuntimeException(ex); - } - } - - public static void checkAvailable() throws Throwable { - // Calling here forces class initialization. - } - - public static RemoteController getController(DownloadService downloadService, MediaRouter.RouteInfo info) { - CastDevice device = CastDevice.getFromBundle(info.getExtras()); - if(device != null) { - return new ChromeCastController(downloadService, device); - } else { - return null; - } - } - - public static String getCastControlCategory() { - return CastMediaControlIntent.categoryForCast(EnvironmentVariables.CAST_APPLICATION_ID); - } -} -- cgit v1.2.3