aboutsummaryrefslogtreecommitdiff
path: root/app/src/google
diff options
context:
space:
mode:
authorCarey Metcalfe <carey@cmetcalfe.ca>2017-10-13 22:34:23 -0400
committerCarey Metcalfe <carey@cmetcalfe.ca>2017-10-13 23:02:15 -0400
commit28336265e822ab4b2ff1697fd5066fc0cfd88861 (patch)
treefff3daff98137c530a4d0eb2291acd5fc6709ef0 /app/src/google
parentd0223c6eab8f58799c8c3ff0e67f5974c237e01c (diff)
downloaddsub-28336265e822ab4b2ff1697fd5066fc0cfd88861.tar.gz
dsub-28336265e822ab4b2ff1697fd5066fc0cfd88861.tar.bz2
dsub-28336265e822ab4b2ff1697fd5066fc0cfd88861.zip
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
Diffstat (limited to 'app/src/google')
-rw-r--r--app/src/google/AndroidManifest.xml18
-rw-r--r--app/src/google/java/github/daneren2005/dsub/service/ChromeCastController.java506
-rw-r--r--app/src/google/java/github/daneren2005/dsub/util/compat/GoogleCompat.java61
3 files changed, 585 insertions, 0 deletions
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="github.daneren2005.dsub">
+
+ <meta-data
+ android:name="com.google.android.backup.api_key"
+ android:value="AEdPqrEAAAAIUhOMtwa_eG-f0oYUHnetl_Cz7cO9zae8ZXOK5w" />
+ <meta-data
+ android:name="com.google.android.gms.version"
+ android:value="@integer/google_play_services_version" />
+ <meta-data
+ android:name="com.google.android.gms.car.application"
+ android:resource="@xml/auto_app_description" />
+ <meta-data
+ android:name="com.google.android.gms.car.notification.SmallIcon"
+ android:resource="@drawable/stat_notify_playing" />
+
+</manifest>
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 <http://www.gnu.org/licenses/>.
+ 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<RemoteMediaPlayer.MediaChannelResult>() {
+ @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<RemoteMediaPlayer.MediaChannelResult>() {
+ @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<Cast.ApplicationConnectionResult> resultCallback;
+
+ ConnectionCallbacks(boolean isPlaying, int position) {
+ this.isPlaying = isPlaying;
+ this.position = position;
+
+ resultCallback = new ResultCallback<Cast.ApplicationConnectionResult>() {
+ @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);
+ }
+}