Added now playing notifications

This commit is contained in:
Synced Synapse 2015-02-15 19:11:32 +00:00
parent 8d8dd8c6c2
commit 24ef1d5ff6
17 changed files with 832 additions and 38 deletions

View File

@ -42,6 +42,10 @@
<!-- Services -->
<service android:name="com.syncedsynapse.kore2.service.LibrarySyncService"
android:exported="false"/>
<service android:name="com.syncedsynapse.kore2.service.NotificationService"
android:exported="false"/>
<service android:name="com.syncedsynapse.kore2.service.IntentActionsService"
android:exported="false"/>
</application>

View File

@ -58,6 +58,8 @@ public class Settings {
public static final String KEY_PREF_THEME = "pref_theme";
public static final String KEY_PREF_SWITCH_TO_REMOTE_AFTER_MEDIA_START =
"pref_switch_to_remote_after_media_start";
public static final String KEY_PREF_SHOW_NOTIFICATION =
"pref_show_notification";
public static final String KEY_PREF_ABOUT = "pref_about";
public static final String KEY_PREF_COFFEE = "pref_coffee";
@ -79,6 +81,7 @@ public class Settings {
// Defaults for the preferences
public static final String DEFAULT_PREF_THEME = "0";
public static final boolean DEFAULT_PREF_SWITCH_TO_REMOTE_AFTER_MEDIA_START = true;
public static final boolean DEFAULT_PREF_SHOW_NOTIFICATION = false;
public static final boolean DEFAULT_PREF_MOVIES_FILTER_HIDE_WATCHED = false;
public static final boolean DEFAULT_PREF_TVSHOWS_FILTER_HIDE_WATCHED = false;
public static final boolean DEFAULT_PREF_TVSHOW_EPISODES_FILTER_HIDE_WATCHED = false;

View File

@ -107,6 +107,11 @@ public class HostConnectionObserver
* Notifies that XBMC has requested input
*/
public void inputOnInputRequested(String title, String type, String value);
/**
* Notifies the observer that it this is stopping
*/
public void observerOnStopObserving();
}
/**
@ -191,7 +196,7 @@ public class HostConnectionObserver
* Registers a new observer that will be notified about player events
* @param observer Observer
*/
public synchronized void registerPlayerObserver(PlayerEventsObserver observer) {
public void registerPlayerObserver(PlayerEventsObserver observer, boolean replyImmediately) {
if (this.connection == null)
return;
@ -199,6 +204,8 @@ public class HostConnectionObserver
playerEventsObservers.add(observer);
// observerHandlerMap.put(observer, new Handler());
if (replyImmediately) replyWithLastResult(observer);
if (playerEventsObservers.size() == 1) {
// If this is the first observer, start checking through HTTP or register us
// as a connection observer, which we will pass to the "real" observer
@ -218,8 +225,7 @@ public class HostConnectionObserver
* Unregisters a previously registered observer
* @param observer Observer to unregister
*/
public synchronized void unregisterPlayerObserver(PlayerEventsObserver observer) {
// Remove this observer and its associated handler
public void unregisterPlayerObserver(PlayerEventsObserver observer) {
playerEventsObservers.remove(observer);
// observerHandlerMap.remove(observer);
@ -244,8 +250,10 @@ public class HostConnectionObserver
/**
* Unregisters all observers
*/
public void unregisterAllObservers() {
// observerHandlerMap.clear();
public void stopObserving() {
for (final PlayerEventsObserver observer : playerEventsObservers)
observer.observerOnStopObserving();
playerEventsObservers.clear();
if (connection.getProtocol() == HostConnection.PROTOCOL_TCP) {
@ -293,25 +301,33 @@ public class HostConnectionObserver
* The {@link HostConnection.SystemNotificationsObserver} interface methods
*/
public void onQuit(System.OnQuit notification) {
for (final PlayerEventsObserver observer : playerEventsObservers) {
// Copy list to prevent ConcurrentModificationExceptions
List<PlayerEventsObserver> allObservers = new ArrayList<>(playerEventsObservers);
for (final PlayerEventsObserver observer : allObservers) {
observer.systemOnQuit();
}
}
public void onRestart(System.OnRestart notification) {
for (final PlayerEventsObserver observer : playerEventsObservers) {
// Copy list to prevent ConcurrentModificationExceptions
List<PlayerEventsObserver> allObservers = new ArrayList<>(playerEventsObservers);
for (final PlayerEventsObserver observer : allObservers) {
observer.systemOnQuit();
}
}
public void onSleep(System.OnSleep notification) {
for (final PlayerEventsObserver observer : playerEventsObservers) {
// Copy list to prevent ConcurrentModificationExceptions
List<PlayerEventsObserver> allObservers = new ArrayList<>(playerEventsObservers);
for (final PlayerEventsObserver observer : allObservers) {
observer.systemOnQuit();
}
}
public void onInputRequested(Input.OnInputRequested notification) {
for (final PlayerEventsObserver observer : playerEventsObservers) {
// Copy list to prevent ConcurrentModificationExceptions
List<PlayerEventsObserver> allObservers = new ArrayList<>(playerEventsObservers);
for (final PlayerEventsObserver observer : allObservers) {
observer.inputOnInputRequested(notification.title, notification.type, notification.value);
}
}
@ -467,7 +483,9 @@ public class HostConnectionObserver
lastErrorCode = errorCode;
lastErrorDescription = description;
forceReply = false;
for (final PlayerEventsObserver observer : observers) {
// Copy list to prevent ConcurrentModificationExceptions
List<PlayerEventsObserver> allObservers = new ArrayList<>(observers);
for (final PlayerEventsObserver observer : allObservers) {
notifyConnectionError(errorCode, description, observer);
}
}
@ -503,7 +521,9 @@ public class HostConnectionObserver
(lastCallResult != PlayerEventsObserver.PLAYER_IS_STOPPED)) {
lastCallResult = PlayerEventsObserver.PLAYER_IS_STOPPED;
forceReply = false;
for (final PlayerEventsObserver observer : observers) {
// Copy list to prevent ConcurrentModificationExceptions
List<PlayerEventsObserver> allObservers = new ArrayList<>(observers);
for (final PlayerEventsObserver observer : allObservers) {
notifyNothingIsPlaying(observer);
}
}
@ -543,7 +563,9 @@ public class HostConnectionObserver
lastGetPropertiesResult = getPropertiesResult;
lastGetItemResult = getItemResult;
forceReply = false;
for (final PlayerEventsObserver observer : observers) {
// Copy list to prevent ConcurrentModificationExceptions
List<PlayerEventsObserver> allObservers = new ArrayList<>(observers);
for (final PlayerEventsObserver observer : allObservers) {
notifySomethingIsPlaying(getActivePlayersResult, getPropertiesResult, getItemResult, observer);
}
}

View File

@ -22,8 +22,8 @@ import android.net.Uri;
import com.squareup.picasso.Picasso;
import com.syncedsynapse.kore2.Settings;
import com.syncedsynapse.kore2.provider.MediaContract;
import com.syncedsynapse.kore2.jsonrpc.HostConnection;
import com.syncedsynapse.kore2.provider.MediaContract;
import com.syncedsynapse.kore2.utils.BasicAuthPicassoDownloader;
import com.syncedsynapse.kore2.utils.LogUtils;
@ -375,7 +375,7 @@ public class HostManager {
*/
private void releaseCurrentHost() {
if (currentHostConnectionObserver != null) {
currentHostConnectionObserver.unregisterAllObservers();
currentHostConnectionObserver.stopObserving();
currentHostConnectionObserver = null;
}

View File

@ -0,0 +1,90 @@
/*
* Copyright 2015 Synced Synapse. All rights reserved.
*
* 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.syncedsynapse.kore2.service;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import com.syncedsynapse.kore2.host.HostManager;
import com.syncedsynapse.kore2.jsonrpc.HostConnection;
import com.syncedsynapse.kore2.jsonrpc.method.Player;
import com.syncedsynapse.kore2.jsonrpc.type.GlobalType;
import com.syncedsynapse.kore2.utils.LogUtils;
/**
* Service that implements some player actions
* Used to support the notifications actions
*/
public class IntentActionsService extends Service {
public static final String TAG = LogUtils.makeLogTag(IntentActionsService.class);
public static final String EXTRA_PLAYER_ID = "extra_player_id";
public static final String ACTION_PLAY_PAUSE = "play_pause",
ACTION_REWIND = "rewind",
ACTION_FAST_FORWARD = "fast_forward",
ACTION_PREVIOUS = "previous",
ACTION_NEXT = "next";
@Override
public void onCreate() { }
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// We won't create a new thread because the request to the host are
// already done in a separate thread. Just fire the request and forget
HostConnection hostConnection = HostManager.getInstance(this).getConnection();
String action = intent.getAction();
int playerId = intent.getIntExtra(EXTRA_PLAYER_ID, -1);
if ((hostConnection != null) && (playerId != -1)) {
switch (action) {
case ACTION_PLAY_PAUSE:
hostConnection.execute(
new Player.PlayPause(playerId),
null, null);
break;
case ACTION_REWIND:
hostConnection.execute(
new Player.SetSpeed(playerId, GlobalType.IncrementDecrement.DECREMENT),
null, null);
break;
case ACTION_FAST_FORWARD:
hostConnection.execute(
new Player.SetSpeed(playerId, GlobalType.IncrementDecrement.INCREMENT),
null, null);
break;
case ACTION_PREVIOUS:
hostConnection.execute(
new Player.GoTo(playerId, Player.GoTo.PREVIOUS),
null, null);
break;
case ACTION_NEXT:
hostConnection.execute(
new Player.GoTo(playerId, Player.GoTo.NEXT),
null, null);
break;
}
}
return START_NOT_STICKY;
}
@Override
public IBinder onBind(Intent intent) { return null; }
}

View File

@ -0,0 +1,313 @@
/*
* Copyright 2015 Synced Synapse. All rights reserved.
*
* 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.syncedsynapse.kore2.service;
import android.annotation.TargetApi;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.os.Build;
import android.os.IBinder;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.TaskStackBuilder;
import android.view.View;
import android.widget.RemoteViews;
import com.syncedsynapse.kore2.R;
import com.syncedsynapse.kore2.host.HostConnectionObserver;
import com.syncedsynapse.kore2.host.HostManager;
import com.syncedsynapse.kore2.jsonrpc.type.ListType;
import com.syncedsynapse.kore2.jsonrpc.type.PlayerType;
import com.syncedsynapse.kore2.ui.RemoteActivity;
import com.syncedsynapse.kore2.utils.LogUtils;
import com.syncedsynapse.kore2.utils.Utils;
/**
* This service maintains a notification in the notification area while
* something is playing, and keeps running while it is playing.
* This service stops itself as soon as the playing stops or there's no
* connection. Thus, this should only be started if something is already
* playing, otherwise it will shutdown automatically.
* It doesn't try to mirror Kodi's state at all times, because that would
* imply running at all times which can be resource consuming.
*
* A {@link HostConnectionObserver} singleton is used to keep track of Kodi's
* state. This singleton should be the same as used in the app's activities
*/
public class NotificationService extends Service
implements HostConnectionObserver.PlayerEventsObserver {
public static final String TAG = LogUtils.makeLogTag(NotificationService.class);
private static final int NOTIFICATION_ID = 1;
private HostConnectionObserver mHostConnectionObserver = null;
private PendingIntent mRemoteStartPendingIntent;
@Override
public void onCreate() {
// We do not create any thread because all the works is supposed to
// be done on the main thread, so that the connection observer
// can be shared with the app, and notify it on the UI thread
LogUtils.LOGD(TAG, "onCreate");
// Create the intent to start the remote when the user taps the notification
TaskStackBuilder stackBuilder = TaskStackBuilder.create(this);
stackBuilder.addParentStack(RemoteActivity.class);
stackBuilder.addNextIntent(new Intent(this, RemoteActivity.class));
mRemoteStartPendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
LogUtils.LOGD(TAG, "onStartCommand");
// Get the connection observer here, not on create to check if
// there has been a change in hosts, and if so unregister the previous one
HostConnectionObserver connectionObserver = HostManager.getInstance(this).getHostConnectionObserver();
// If we are already initialized and the same host, exit
if (mHostConnectionObserver == connectionObserver) {
LogUtils.LOGD(TAG, "Already initialized");
return START_NOT_STICKY;
}
// If there's a change in hosts, unregister from the previous one
if (mHostConnectionObserver != null) {
mHostConnectionObserver.unregisterPlayerObserver(this);
}
// Register us on the connection observer
mHostConnectionObserver = connectionObserver;
mHostConnectionObserver.registerPlayerObserver(this, true);
// If we get killed, after returning from here, don't restart
return START_NOT_STICKY;
}
@Override
public IBinder onBind(Intent intent) {
// We don't provide binding, so return null
return null;
}
/**
* HostConnectionObserver.PlayerEventsObserver interface callbacks
*/
public void playerOnPlay(PlayerType.GetActivePlayersReturnType getActivePlayerResult,
PlayerType.PropertyValue getPropertiesResult,
ListType.ItemsAll getItemResult) {
buildNotification(getActivePlayerResult, getPropertiesResult, getItemResult);
}
public void playerOnPause(PlayerType.GetActivePlayersReturnType getActivePlayerResult,
PlayerType.PropertyValue getPropertiesResult,
ListType.ItemsAll getItemResult) {
buildNotification(getActivePlayerResult, getPropertiesResult, getItemResult);
}
public void playerOnStop() {
removeNotification();
// Stop service
LogUtils.LOGD(TAG, "Shutting down notification service - Player stopped");
mHostConnectionObserver.unregisterPlayerObserver(this);
stopSelf();
}
public void playerNoResultsYet() {
removeNotification();
}
public void playerOnConnectionError(int errorCode, String description) {
removeNotification();
// Stop service
LogUtils.LOGD(TAG, "Shutting down notification service - Connection error");
mHostConnectionObserver.unregisterPlayerObserver(this);
stopSelf();
}
public void systemOnQuit() {
removeNotification();
// Stop service
LogUtils.LOGD(TAG, "Shutting down notification service - System quit");
mHostConnectionObserver.unregisterPlayerObserver(this);
stopSelf();
}
// Ignore this
public void inputOnInputRequested(String title, String type, String value) { }
public void observerOnStopObserving() {
// Called when the user changes host
removeNotification();
LogUtils.LOGD(TAG, "Shutting down notification service - System quit");
stopSelf();
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
private void buildNotification(PlayerType.GetActivePlayersReturnType getActivePlayerResult,
PlayerType.PropertyValue getPropertiesResult,
ListType.ItemsAll getItemResult) {
String title, underTitle, poster;
int smallIcon, playPauseIcon, rewindIcon, ffIcon;
boolean isVideo = ((getItemResult.type.equals(ListType.ItemsAll.TYPE_MOVIE)) ||
(getItemResult.type.equals(ListType.ItemsAll.TYPE_EPISODE)));
switch (getItemResult.type) {
case ListType.ItemsAll.TYPE_MOVIE:
title = getItemResult.title;
underTitle = getItemResult.tagline;
poster = getItemResult.thumbnail;
smallIcon = R.drawable.ic_movie_white_24dp;
break;
case ListType.ItemsAll.TYPE_EPISODE:
title = getItemResult.title;
String seasonEpisode = String.format(getString(R.string.season_episode_abbrev),
getItemResult.season, getItemResult.episode);
underTitle = String.format("%s | %s", getItemResult.showtitle, seasonEpisode);
poster = getItemResult.art.poster;
smallIcon = R.drawable.ic_tv_white_24dp;
break;
case ListType.ItemsAll.TYPE_SONG:
title = getItemResult.title;
underTitle = getItemResult.displayartist + " - " + getItemResult.album;
poster = getItemResult.thumbnail;
smallIcon = R.drawable.ic_headset_white_24dp;
break;
case ListType.ItemsAll.TYPE_MUSIC_VIDEO:
title = getItemResult.title;
underTitle = Utils.listStringConcat(getItemResult.artist, ", ") + " - " + getItemResult.album;
poster = getItemResult.thumbnail;
smallIcon = R.drawable.ic_headset_white_24dp;
break;
default:
// We don't know what this is, forget it
return;
}
switch (getPropertiesResult.speed) {
case 1:
playPauseIcon = R.drawable.ic_pause_white_24dp;
break;
default:
playPauseIcon = R.drawable.ic_play_arrow_white_24dp;
break;
}
// Create the actions, depending on the type of media
PendingIntent rewindPendingItent, ffPendingItent, playPausePendingIntent;
playPausePendingIntent = buildActionPendingIntent(getActivePlayerResult.playerid, IntentActionsService.ACTION_PLAY_PAUSE);
if (getItemResult.type.equals(ListType.ItemsAll.TYPE_SONG)) {
rewindPendingItent = buildActionPendingIntent(getActivePlayerResult.playerid, IntentActionsService.ACTION_PREVIOUS);
rewindIcon = R.drawable.ic_skip_previous_white_24dp;
ffPendingItent = buildActionPendingIntent(getActivePlayerResult.playerid, IntentActionsService.ACTION_NEXT);
ffIcon = R.drawable.ic_skip_next_white_24dp;
} else {
rewindPendingItent = buildActionPendingIntent(getActivePlayerResult.playerid, IntentActionsService.ACTION_REWIND);
rewindIcon = R.drawable.ic_fast_rewind_white_24dp;
ffPendingItent = buildActionPendingIntent(getActivePlayerResult.playerid, IntentActionsService.ACTION_FAST_FORWARD);
ffIcon = R.drawable.ic_fast_forward_white_24dp;
}
// Setup the collpased and expanded notifications
RemoteViews collapsedRV = new RemoteViews(this.getPackageName(), R.layout.notification_colapsed);
collapsedRV.setImageViewResource(R.id.rewind, rewindIcon);
collapsedRV.setOnClickPendingIntent(R.id.rewind, rewindPendingItent);
collapsedRV.setImageViewResource(R.id.play, playPauseIcon);
collapsedRV.setOnClickPendingIntent(R.id.play, playPausePendingIntent);
collapsedRV.setImageViewResource(R.id.fast_forward, ffIcon);
collapsedRV.setOnClickPendingIntent(R.id.fast_forward, ffPendingItent);
collapsedRV.setTextViewText(R.id.title, title);
collapsedRV.setTextViewText(R.id.text2, underTitle);
RemoteViews expandedRV = new RemoteViews(this.getPackageName(), R.layout.notification_expanded);
expandedRV.setImageViewResource(R.id.rewind, rewindIcon);
expandedRV.setOnClickPendingIntent(R.id.rewind, rewindPendingItent);
expandedRV.setImageViewResource(R.id.play, playPauseIcon);
expandedRV.setOnClickPendingIntent(R.id.play, playPausePendingIntent);
expandedRV.setImageViewResource(R.id.fast_forward, ffIcon);
expandedRV.setOnClickPendingIntent(R.id.fast_forward, ffPendingItent);
expandedRV.setTextViewText(R.id.title, title);
expandedRV.setTextViewText(R.id.text2, underTitle);
int expandedIconResId;
if (isVideo) {
expandedIconResId = R.id.icon_slim;
expandedRV.setViewVisibility(R.id.icon_slim, View.VISIBLE);
expandedRV.setViewVisibility(R.id.icon_square, View.GONE);
} else {
expandedIconResId = R.id.icon_square;
expandedRV.setViewVisibility(R.id.icon_slim, View.GONE);
expandedRV.setViewVisibility(R.id.icon_square, View.VISIBLE);
}
// Build the notification
NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
Notification notification = builder
.setSmallIcon(smallIcon)
.setShowWhen(false)
.setOngoing(true)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setCategory(NotificationCompat.CATEGORY_TRANSPORT)
.setContentIntent(mRemoteStartPendingIntent)
.setContent(collapsedRV)
.build();
// Load images. Use the same dimensions as the remote to hit the cache both times
Resources resources = this.getResources();
int posterWidth = resources.getDimensionPixelOffset(R.dimen.now_playing_poster_width);
int posterHeight = isVideo?
resources.getDimensionPixelOffset(R.dimen.now_playing_poster_height):
posterWidth;
HostManager hostManager = HostManager.getInstance(this);
hostManager.getPicasso()
.load(hostManager.getHostInfo().getImageUrl(poster))
.resize(posterWidth, posterHeight)
.into(collapsedRV, R.id.icon, NOTIFICATION_ID, notification);
if (Utils.isJellybeanOrLater()) {
notification.bigContentView = expandedRV;
hostManager.getPicasso()
.load(hostManager.getHostInfo().getImageUrl(poster))
.resize(posterWidth, posterHeight)
.into(expandedRV, expandedIconResId, NOTIFICATION_ID, notification);
}
NotificationManager notificationManager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.notify(NOTIFICATION_ID, notification);
}
private PendingIntent buildActionPendingIntent(int playerId, String action) {
LogUtils.LOGD(TAG, "Build action: " + action);
Intent intent = new Intent(this, IntentActionsService.class)
.setAction(action)
.putExtra(IntentActionsService.EXTRA_PLAYER_ID, playerId);
return PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
}
private void removeNotification() {
NotificationManager notificationManager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel(NOTIFICATION_ID);
}
}

View File

@ -214,15 +214,12 @@ public class NowPlayingFragment extends Fragment
public void onActivityCreated (Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
setHasOptionsMenu(false);
// Get last result from host observer, so that we update the UI accordingly
// One of the PlayerEventsObserver callbacks will be called if there's a result available
hostConnectionObserver.replyWithLastResult(this);
}
@Override
public void onResume() {
super.onResume();
hostConnectionObserver.registerPlayerObserver(this);
hostConnectionObserver.registerPlayerObserver(this, true);
}
@Override
@ -591,6 +588,7 @@ public class NowPlayingFragment extends Fragment
// Ignore this
public void inputOnInputRequested(String title, String type, String value) {}
public void observerOnStopObserving() {}
/**
* Sets whats playing information

View File

@ -144,15 +144,12 @@ public class PlaylistFragment extends Fragment
super.onActivityCreated(savedInstanceState);
// We have options
setHasOptionsMenu(true);
// Get last result from host observer, so that we update the UI accordingly
// One of the PlayerEventsObserver callbacks will be called if there's a result available
hostConnectionObserver.replyWithLastResult(this);
}
@Override
public void onResume() {
super.onResume();
hostConnectionObserver.registerPlayerObserver(this);
hostConnectionObserver.registerPlayerObserver(this, true);
}
@Override
@ -369,6 +366,7 @@ public class PlaylistFragment extends Fragment
// Ignore this
public void inputOnInputRequested(String title, String type, String value) {}
public void observerOnStopObserving() {}
/**
* Starts the call chain to display the playlist

View File

@ -32,6 +32,7 @@ import android.widget.ImageView;
import android.widget.Toast;
import com.syncedsynapse.kore2.R;
import com.syncedsynapse.kore2.Settings;
import com.syncedsynapse.kore2.host.HostConnectionObserver;
import com.syncedsynapse.kore2.host.HostManager;
import com.syncedsynapse.kore2.jsonrpc.ApiCallback;
@ -43,6 +44,7 @@ import com.syncedsynapse.kore2.jsonrpc.method.VideoLibrary;
import com.syncedsynapse.kore2.jsonrpc.type.GlobalType;
import com.syncedsynapse.kore2.jsonrpc.type.ListType;
import com.syncedsynapse.kore2.jsonrpc.type.PlayerType;
import com.syncedsynapse.kore2.service.NotificationService;
import com.syncedsynapse.kore2.ui.hosts.AddHostActivity;
import com.syncedsynapse.kore2.ui.views.CirclePageIndicator;
import com.syncedsynapse.kore2.utils.LogUtils;
@ -129,9 +131,9 @@ public class RemoteActivity extends BaseActivity
public void onResume() {
super.onResume();
hostConnectionObserver = hostManager.getHostConnectionObserver();
hostConnectionObserver.registerPlayerObserver(this);
// Get last result
hostConnectionObserver.replyWithLastResult(this);
hostConnectionObserver.registerPlayerObserver(this, true);
// Force a refresh, mainly to update the time elapsed on the fragments
hostConnectionObserver.forceRefreshResults();
}
@Override
@ -298,7 +300,7 @@ public class RemoteActivity extends BaseActivity
/**
* Sets or clear the image background
* @param url
* @param url Image url
*/
private void setImageViewBackground(String url) {
if (url != null) {
@ -356,6 +358,16 @@ public class RemoteActivity extends BaseActivity
setImageViewBackground(imageUrl);
}
lastImageUrl = imageUrl;
// Check whether we should show a notification
boolean showNotification = PreferenceManager
.getDefaultSharedPreferences(this)
.getBoolean(Settings.KEY_PREF_SHOW_NOTIFICATION, Settings.DEFAULT_PREF_SHOW_NOTIFICATION);
if (showNotification) {
// Let's start the notification service
LogUtils.LOGD(TAG, "Starting notification service");
startService(new Intent(this, NotificationService.class));
}
}
public void playerOnPause(PlayerType.GetActivePlayersReturnType getActivePlayerResult,
@ -390,6 +402,8 @@ public class RemoteActivity extends BaseActivity
dialog.show(getSupportFragmentManager(), null);
}
public void observerOnStopObserving() {}
/**
* Now playing fragment listener
*/

View File

@ -168,15 +168,12 @@ public class RemoteFragment extends Fragment
public void onActivityCreated (Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
setHasOptionsMenu(false);
// Get last result from host observer, so that we update the UI accordingly
// One of the PlayerEventsObserver callbacks will be called if there's a result available
hostConnectionObserver.replyWithLastResult(this);
}
@Override
public void onResume() {
super.onResume();
hostConnectionObserver.registerPlayerObserver(this);
hostConnectionObserver.registerPlayerObserver(this, true);
}
@Override
@ -339,6 +336,7 @@ public class RemoteFragment extends Fragment
// Ignore this
public void inputOnInputRequested(String title, String type, String value) {}
public void observerOnStopObserving() {}
/**
* Sets whats playing information

View File

@ -16,12 +16,10 @@
package com.syncedsynapse.kore2.utils;
import com.syncedsynapse.kore2.BuildConfig;
import com.syncedsynapse.kore2.host.HostConnectionObserver;
import com.syncedsynapse.kore2.jsonrpc.HostConnection;
import android.util.Log;
import com.syncedsynapse.kore2.BuildConfig;
import java.util.Arrays;
import java.util.List;
@ -33,8 +31,8 @@ public class LogUtils {
// TODO: Remove this later
private static final List<String> doNotLogTags = Arrays.asList(
HostConnection.TAG,
HostConnectionObserver.TAG
// HostConnection.TAG
// HostConnectionObserver.TAG
);
public static String makeLogTag(String str) {

View File

@ -0,0 +1,100 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2015 Synced Synapse. All rights reserved.
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.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/status_bar_latest_event_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="@color/background_floating_material_dark">
<ImageView android:id="@+id/icon"
android:layout_width="@dimen/notification_art_default_width"
android:layout_height="@dimen/notification_art_default_height"
android:layout_weight="0"
android:padding="12dp"
android:scaleType="centerInside"
android:contentDescription="@string/poster"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="fill_vertical"
android:minHeight="@dimen/notification_height"
android:orientation="vertical">
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="@dimen/small_padding"
android:textAppearance="@style/TextAppearance.Notification.Title"
android:singleLine="true"
android:ellipsize="marquee"
android:fadingEdge="horizontal"/>
<TextView
android:id="@+id/text2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.Notification.Details"
android:singleLine="true"
android:fadingEdge="horizontal"
android:ellipsize="marquee"/>
</LinearLayout>
<LinearLayout
android:id="@+id/media_actions"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginRight="6dp"
android:layout_marginEnd="6dp"
android:layout_gravity="center_vertical|end"
android:orientation="horizontal"
android:layoutDirection="ltr">
<ImageButton
android:id="@+id/rewind"
style="@style/Widget.Button.Borderless"
android:layout_width="@dimen/default_icon_size"
android:layout_height="match_parent"
android:layout_marginLeft="2dp"
android:layout_marginRight="2dp"
android:layout_weight="1"
android:gravity="center"
android:contentDescription="@string/rewind"/>
<ImageButton
android:id="@+id/play"
style="@style/Widget.Button.Borderless"
android:layout_width="@dimen/default_icon_size"
android:layout_height="match_parent"
android:layout_marginLeft="2dp"
android:layout_marginRight="2dp"
android:layout_weight="1"
android:gravity="center"
android:contentDescription="@string/play"/>
<ImageButton
android:id="@+id/fast_forward"
style="@style/Widget.Button.Borderless"
android:layout_width="@dimen/default_icon_size"
android:layout_height="match_parent"
android:layout_marginLeft="2dp"
android:layout_marginRight="2dp"
android:layout_weight="1"
android:gravity="center"
android:contentDescription="@string/fast_forward"/>
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,224 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2015 Synced Synapse. All rights reserved.
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.
-->
<!--<LinearLayout-->
<!--xmlns:android="http://schemas.android.com/apk/res/android"-->
<!--android:id="@+id/status_bar_latest_event_content"-->
<!--android:layout_width="match_parent"-->
<!--android:layout_height="wrap_content"-->
<!--android:orientation="horizontal"-->
<!--android:background="@color/background_floating_material_dark">-->
<!--<ImageView android:id="@+id/icon_square"-->
<!--android:layout_width="@dimen/notification_expanded_art_default_height"-->
<!--android:layout_height="@dimen/notification_expanded_art_default_height"-->
<!--android:scaleType="centerCrop"-->
<!--android:contentDescription="@string/poster"-->
<!--android:visibility="gone"/>-->
<!--<ImageView android:id="@+id/icon_slim"-->
<!--android:layout_width="@dimen/notification_expanded_art_slim_width"-->
<!--android:layout_height="@dimen/notification_expanded_art_default_height"-->
<!--android:scaleType="centerCrop"-->
<!--android:contentDescription="@string/poster"-->
<!--android:visibility="gone"/>-->
<!--<LinearLayout-->
<!--android:layout_width="0dp"-->
<!--android:layout_height="wrap_content"-->
<!--android:layout_weight="1"-->
<!--android:layout_gravity="fill_vertical"-->
<!--android:minHeight="@dimen/notification_expanded_height"-->
<!--android:orientation="vertical">-->
<!--<TextView-->
<!--android:id="@+id/title"-->
<!--android:layout_width="wrap_content"-->
<!--android:layout_height="wrap_content"-->
<!--android:paddingTop="@dimen/small_padding"-->
<!--android:layout_marginLeft="12dp"-->
<!--android:layout_marginStart="12dp"-->
<!--android:layout_marginRight="12dp"-->
<!--android:layout_marginEnd="12dp"-->
<!--android:textAppearance="@style/TextAppearance.Notification.Title"-->
<!--android:singleLine="true"-->
<!--android:ellipsize="marquee"-->
<!--android:fadingEdge="horizontal"/>-->
<!--<TextView-->
<!--android:id="@+id/text2"-->
<!--android:layout_width="wrap_content"-->
<!--android:layout_height="0dp"-->
<!--android:layout_weight="1"-->
<!--android:layout_marginLeft="12dp"-->
<!--android:layout_marginStart="12dp"-->
<!--android:layout_marginRight="12dp"-->
<!--android:layout_marginEnd="12dp"-->
<!--android:textAppearance="@style/TextAppearance.Notification.Details"-->
<!--android:maxLines="2"-->
<!--android:fadingEdge="horizontal"-->
<!--android:ellipsize="marquee"/>-->
<!--<ImageView-->
<!--android:layout_width="match_parent"-->
<!--android:layout_height="1dp"-->
<!--android:id="@+id/action_divider"-->
<!--android:background="#29ffffff"/>-->
<!--<LinearLayout-->
<!--android:id="@+id/media_actions"-->
<!--android:layout_width="match_parent"-->
<!--android:layout_height="@dimen/default_icon_size"-->
<!--android:layout_marginStart="12dp"-->
<!--android:layout_marginEnd="12dp"-->
<!--android:orientation="horizontal"-->
<!--android:layoutDirection="ltr">-->
<!--<ImageButton-->
<!--android:id="@+id/rewind"-->
<!--style="@style/Widget.Button.Borderless"-->
<!--android:layout_width="@dimen/default_icon_size"-->
<!--android:layout_height="match_parent"-->
<!--android:layout_marginLeft="2dp"-->
<!--android:layout_marginRight="2dp"-->
<!--android:layout_weight="1"-->
<!--android:gravity="center"-->
<!--android:contentDescription="@string/rewind"/>-->
<!--<ImageButton-->
<!--android:id="@+id/play"-->
<!--style="@style/Widget.Button.Borderless"-->
<!--android:layout_width="@dimen/default_icon_size"-->
<!--android:layout_height="match_parent"-->
<!--android:layout_marginLeft="2dp"-->
<!--android:layout_marginRight="2dp"-->
<!--android:layout_weight="1"-->
<!--android:gravity="center"-->
<!--android:contentDescription="@string/play"/>-->
<!--<ImageButton-->
<!--android:id="@+id/fast_forward"-->
<!--style="@style/Widget.Button.Borderless"-->
<!--android:layout_width="@dimen/default_icon_size"-->
<!--android:layout_height="match_parent"-->
<!--android:layout_marginLeft="2dp"-->
<!--android:layout_marginRight="2dp"-->
<!--android:layout_weight="1"-->
<!--android:gravity="center"-->
<!--android:contentDescription="@string/fast_forward"/>-->
<!--</LinearLayout>-->
<!--</LinearLayout>-->
<!--</LinearLayout>-->
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/status_bar_latest_event_content"
android:layout_width="match_parent"
android:layout_height="128dp"
android:background="@color/background_floating_material_dark">
<FrameLayout
android:id="@+id/icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView android:id="@+id/icon_square"
android:layout_width="@dimen/notification_expanded_art_default_height"
android:layout_height="@dimen/notification_expanded_art_default_height"
android:scaleType="centerCrop"
android:contentDescription="@string/poster"
android:visibility="gone"/>
<ImageView android:id="@+id/icon_slim"
android:layout_width="@dimen/notification_expanded_art_slim_width"
android:layout_height="@dimen/notification_expanded_art_default_height"
android:scaleType="centerCrop"
android:contentDescription="@string/poster"
android:visibility="gone"/>
</FrameLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="12dp"
android:layout_marginStart="12dp"
android:layout_marginRight="12dp"
android:layout_marginEnd="12dp"
android:layout_toRightOf="@id/icon"
android:layout_toEndOf="@id/icon"
android:orientation="vertical">
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="@dimen/small_padding"
android:textAppearance="@style/TextAppearance.Notification.Title"
android:singleLine="true"
android:ellipsize="marquee"
android:fadingEdge="horizontal"/>
<TextView
android:id="@+id/text2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.Notification.Details"
android:maxLines="2"
android:fadingEdge="horizontal"
android:ellipsize="marquee"/>
</LinearLayout>
<LinearLayout
android:id="@+id/media_actions"
android:layout_width="match_parent"
android:layout_height="@dimen/default_icon_size"
android:layout_toRightOf="@id/icon"
android:layout_toEndOf="@id/icon"
android:layout_alignParentBottom="true"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:orientation="horizontal"
android:layoutDirection="ltr">
<ImageButton
android:id="@+id/rewind"
style="@style/Widget.Button.Borderless"
android:layout_width="@dimen/default_icon_size"
android:layout_height="match_parent"
android:layout_marginLeft="2dp"
android:layout_marginRight="2dp"
android:layout_weight="1"
android:gravity="center"/>
<ImageButton
android:id="@+id/play"
style="@style/Widget.Button.Borderless"
android:layout_width="@dimen/default_icon_size"
android:layout_height="match_parent"
android:layout_marginLeft="2dp"
android:layout_marginRight="2dp"
android:layout_weight="1"
android:gravity="center"/>
<ImageButton
android:id="@+id/fast_forward"
style="@style/Widget.Button.Borderless"
android:layout_width="@dimen/default_icon_size"
android:layout_height="match_parent"
android:layout_marginLeft="2dp"
android:layout_marginRight="2dp"
android:layout_weight="1"
android:gravity="center"/>
</LinearLayout>
<ImageView
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_toRightOf="@id/icon"
android:layout_toEndOf="@id/icon"
android:layout_above="@id/media_actions"
android:id="@+id/action_divider"
android:background="#29ffffff"/>
</RelativeLayout>

View File

@ -114,4 +114,16 @@
<dimen name="addondetail_poster_width">112dp</dimen>
<dimen name="addondetail_poster_heigth">112dp</dimen>
<!-- Notification -->
<dimen name="notification_height">64dp</dimen>
<dimen name="notification_expanded_height">128dp</dimen>
<dimen name="notification_art_default_height">64dp</dimen>
<dimen name="notification_art_default_width">64dp</dimen>
<dimen name="notification_art_slim_width">42dp</dimen>
<dimen name="notification_expanded_art_default_height">128dp</dimen>
<dimen name="notification_expanded_art_default_width">128dp</dimen>
<dimen name="notification_expanded_art_slim_width">84dp</dimen>
</resources>

View File

@ -270,7 +270,7 @@
<string name="theme_solarized_dark">Solarized Dark</string>
<string name="switch_to_remote">Switch to remote after media start</string>
<string name="show_notification">Show notification while playing</string>
<string name="about">About</string>
<string name="about_desc"><![CDATA[

View File

@ -66,7 +66,7 @@
<item name="android:paddingBottom">@dimen/large_padding</item>
<item name="android:paddingLeft">@dimen/large_padding</item>
<item name="android:paddingRight">@dimen/large_padding</item>
<item name="android:maxLines">1</item>
<item name="android:maxLines">2</item>
<item name="android:ellipsize">end</item>
</style>
@ -282,5 +282,20 @@
<!--<item name="android:maxLines">2</item>-->
<item name="android:ellipsize">end</item>
</style>
<style name="TextAppearance.Notification">
</style>
<style name="TextAppearance.Notification.Title">
<item name="android:textAppearance">@style/TextAppearance.AppCompat.Title</item>
<item name="android:textColor">@color/primary_text_default_material_dark</item>
<item name="android:textSize">@dimen/text_size_large</item>
</style>
<style name="TextAppearance.Notification.Details">
<item name="android:textAppearance">@style/TextAppearance.AppCompat.Body1</item>
<item name="android:textColor">@color/secondary_text_default_material_dark</item>
<item name="android:textSize">@dimen/text_size_medium</item>
</style>
</resources>

View File

@ -34,6 +34,11 @@
android:title="@string/switch_to_remote"
android:defaultValue="true"/>
<CheckBoxPreference
android:key="pref_show_notification"
android:title="@string/show_notification"
android:defaultValue="true"/>
<Preference
android:key="pref_about"
android:title="@string/about"/>