Merge pull request #2 from SyncedSynapse/master

Merge new commits
This commit is contained in:
jonas2515 2015-02-16 09:38:44 +01:00
commit 1dda7e9600
30 changed files with 1426 additions and 242 deletions

29
CHANGELOG.md Normal file
View File

@ -0,0 +1,29 @@
Changelog
---------
Version 1.0.1
-------------
- Fixed bug with In-app purchase key that was crashing Settings screen
Version 1.0.0
-------------
- New options to sort movies and tv shows
- Bulgarian translation (by NEOhidra)
- German translation (by jonas2515)
Version 0.9.2
-------------
- Added new actions in remote: update/clean library and toggle fullscreen
- French translation (thanks Kowalski!)
- Bug fixes and visual tweaks
Version 0.9.1
-------------
- Improved library sync;
- Automatically switch to remove after media start;
- Visual tweaks.
Version 0.9.0
-------------
- First version

View File

@ -35,6 +35,7 @@ Credits
- French - Kowalski
- Bulgarian - NEOhidra
- German - jonas2515
- Italian - Enrico Strocchi
Links
-----

View File

@ -8,8 +8,8 @@ android {
applicationId "com.syncedsynapse.kore2"
minSdkVersion 15
targetSdkVersion 21
versionCode 5
versionName "1.0.0"
versionCode 6
versionName "1.0.1"
buildConfigField("String", "IAP_KEY", "\"${rootProject.property("IAP_KEY")}\"")
}

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

@ -50,48 +50,64 @@ public class Settings {
// Maximum pictures to show on cast list (-1 to show all)
public static final int DEFAULT_MAX_CAST_PICTURES = 12;
// Sort orders
public static final int SORT_BY_NAME = 0,
SORT_BY_DATE_ADDED = 1;
/**
* Default Shared Preferences keys.
* Preferences keys.
* These settings are automatically managed by the Preferences mechanism.
* Make sure these are the same as in preferences.xml
*/
// Theme
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 DEFAULT_PREF_THEME = "0";
// Switch to remote
public static final String KEY_PREF_SWITCH_TO_REMOTE_AFTER_MEDIA_START = "pref_switch_to_remote_after_media_start";
public static final boolean DEFAULT_PREF_SWITCH_TO_REMOTE_AFTER_MEDIA_START = true;
// Show notifications
public static final String KEY_PREF_SHOW_NOTIFICATION = "pref_show_notification";
public static final boolean DEFAULT_PREF_SHOW_NOTIFICATION = false;
// Other keys used in preferences.xml
public static final String KEY_PREF_ABOUT = "pref_about";
public static final String KEY_PREF_COFFEE = "pref_coffee";
// Filter watched movies on movie list
public static final String KEY_PREF_MOVIES_FILTER_HIDE_WATCHED = "movies_filter_hide_watched";
public static final boolean DEFAULT_PREF_MOVIES_FILTER_HIDE_WATCHED = false;
// Sort order on movies
public static final String KEY_PREF_MOVIES_SORT_ORDER = "movies_sort_order";
public static final int DEFAULT_PREF_MOVIES_SORT_ORDER = SORT_BY_NAME;
// Ignore articles on movie sorting
public static final String KEY_PREF_MOVIES_IGNORE_PREFIXES = "movies_ignore_prefixes";
public static final boolean DEFAULT_PREF_MOVIES_IGNORE_PREFIXES = false;
// Filter watched tv shows on tvshow list
public static final String KEY_PREF_TVSHOWS_FILTER_HIDE_WATCHED = "tvshows_filter_hide_watched";
public static final String KEY_PREF_TVSHOW_EPISODES_FILTER_HIDE_WATCHED = "tvshow_episodes_filter_hide_watched";
// Sort order on tv shows
public static final String KEY_PREF_TVSHOWS_SORT_ORDER = "tvshows_sort_order";
// Ignore articles on tv show sorting
public static final String KEY_PREF_TVSHOWS_IGNORE_PREFIXES = "tvshows_ignore_prefixes";
// 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_MOVIES_FILTER_HIDE_WATCHED = false;
public static final boolean DEFAULT_PREF_TVSHOWS_FILTER_HIDE_WATCHED = false;
// Filter watched episodes on episodes list
public static final String KEY_PREF_TVSHOW_EPISODES_FILTER_HIDE_WATCHED = "tvshow_episodes_filter_hide_watched";
public static final boolean DEFAULT_PREF_TVSHOW_EPISODES_FILTER_HIDE_WATCHED = false;
// Sort orders
public static final int SORT_BY_NAME = 0,
SORT_BY_DATE_ADDED = 1;
public static final int DEFAULT_PREF_MOVIES_SORT_ORDER = SORT_BY_NAME;
// Sort order on tv shows
public static final String KEY_PREF_TVSHOWS_SORT_ORDER = "tvshows_sort_order";
public static final int DEFAULT_PREF_TVSHOWS_SORT_ORDER = SORT_BY_NAME;
public static final boolean DEFAULT_PREF_MOVIES_IGNORE_PREFIXES = false;
// Ignore articles on tv show sorting
public static final String KEY_PREF_TVSHOWS_IGNORE_PREFIXES = "tvshows_ignore_prefixes";
public static final boolean DEFAULT_PREF_TVSHOWS_IGNORE_PREFIXES = false;
// Use hardware volume keys to control volume
public static final String KEY_PREF_USE_HARDWARE_VOLUME_KEYS = "pref_use_hardware_volume_keys";
public static final boolean DEFAULT_PREF_USE_HARDWARE_VOLUME_KEYS = true;
// Singleton instance
private static Settings instance = null;

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

@ -15,7 +15,7 @@
*/
package com.syncedsynapse.kore2.jsonrpc;
import android.os.*;
import android.os.Handler;
import android.os.Process;
import android.util.Base64;
@ -24,7 +24,8 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.syncedsynapse.kore2.host.HostInfo;
import com.syncedsynapse.kore2.jsonrpc.notification.*;
import com.syncedsynapse.kore2.jsonrpc.notification.Input;
import com.syncedsynapse.kore2.jsonrpc.notification.Player;
import com.syncedsynapse.kore2.jsonrpc.notification.System;
import com.syncedsynapse.kore2.utils.LogUtils;
@ -37,7 +38,6 @@ import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.ProtocolException;
import java.net.Socket;
import java.net.SocketException;
import java.net.URL;
import java.util.HashMap;
import java.util.concurrent.ExecutorService;
@ -134,31 +134,33 @@ public class HostConnection {
private ExecutorService executorService;
private final int connectionTimeout;
private final int connectTimeout;
private static final int DEFAULT_TIMEOUT = 10000; // ms
private static final int DEFAULT_CONNECT_TIMEOUT = 5000; // ms
private static final int TCP_READ_TIMEOUT = 30000; // ms
/**
* Creates a new host connection
* @param hostInfo Host info object
*/
public HostConnection(final HostInfo hostInfo) {
this(hostInfo, DEFAULT_TIMEOUT);
this(hostInfo, DEFAULT_CONNECT_TIMEOUT);
}
/**
* Creates a new host connection
* @param hostInfo Host info object
* @param connectionTimeout Connection timeout in ms
* @param connectTimeout Connection timeout in ms
*/
public HostConnection(final HostInfo hostInfo, int connectionTimeout) {
public HostConnection(final HostInfo hostInfo, int connectTimeout) {
this.hostInfo = hostInfo;
// Start with the default host protocol
this.protocol = hostInfo.getProtocol();
// Create a single threaded executor
this.executorService = Executors.newSingleThreadExecutor();
// Set timeout
this.connectionTimeout = connectionTimeout;
this.connectTimeout = connectTimeout;
}
/**
@ -356,8 +358,8 @@ public class HostConnection {
// LogUtils.LOGD(TAG, "Opening HTTP connection.");
HttpURLConnection connection = (HttpURLConnection) new URL(hostInfo.getJsonRpcHttpEndpoint()).openConnection();
connection.setRequestMethod("POST");
connection.setConnectTimeout(connectionTimeout);
//connection.setReadTimeout(connectionTimeout);
connection.setConnectTimeout(connectTimeout);
//connection.setReadTimeout(connectTimeout);
connection.setRequestProperty("Content-Type", "application/json");
connection.setDoOutput(true);
@ -482,10 +484,6 @@ public class HostConnection {
*/
private <T> void executeThroughTcp(final ApiMethod<T> method, final ApiCallback<T> callback,
final Handler handler) {
// TODO: We're going to create a background listener thread.
// Also create a thread that periodically checks if the connection should be shutdown
// based on not having activity. Use android timer or Thread.sleep
String methodId = String.valueOf(method.getId());
try {
// Save this method/callback for later response
@ -534,13 +532,11 @@ public class HostConnection {
Socket socket = new Socket();
final InetSocketAddress address = new InetSocketAddress(hostInfo.getAddress(), hostInfo.getTcpPort());
socket.setSoTimeout(0); // No read timeout. Read should block
socket.connect(address, connectionTimeout);
// We're setting a read timeout on the socket, so no need to explicitly close it
socket.setSoTimeout(TCP_READ_TIMEOUT);
socket.connect(address, connectTimeout);
return socket;
} catch (SocketException e) {
LogUtils.LOGW(TAG, "Failed to open TCP connection to host: " + hostInfo.getAddress());
throw new ApiException(ApiException.IO_EXCEPTION_WHILE_CONNECTING, e);
} catch (IOException e) {
LogUtils.LOGW(TAG, "Failed to open TCP connection to host: " + hostInfo.getAddress());
throw new ApiException(ApiException.IO_EXCEPTION_WHILE_CONNECTING, e);

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

@ -605,8 +605,7 @@ public class LibrarySyncService extends Service {
@Override
public void onSucess(VideoType.DetailsTVShow result) {
deleteTVShows(contentResolver, hostId, tvshowId);
List<VideoType.DetailsTVShow> tvShows =
new ArrayList<VideoType.DetailsTVShow>(1);
List<VideoType.DetailsTVShow> tvShows = new ArrayList<>(1);
tvShows.add(result);
insertTVShowsAndGetDetails(orchestrator, hostConnection, callbackHandler,
contentResolver, tvShows);
@ -908,8 +907,7 @@ public class LibrarySyncService extends Service {
final HostConnection hostConnection,
final Handler callbackHandler,
final ContentResolver contentResolver) {
chainCallSyncArtists(orchestrator, hostConnection, callbackHandler, contentResolver,
0, new ArrayList<AudioType.DetailsArtist>());
chainCallSyncArtists(orchestrator, hostConnection, callbackHandler, contentResolver, 0);
}
private final static String getArtistsProperties[] = {
@ -932,8 +930,7 @@ public class LibrarySyncService extends Service {
final HostConnection hostConnection,
final Handler callbackHandler,
final ContentResolver contentResolver,
final int startIdx,
final List<AudioType.DetailsArtist> allResults) {
final int startIdx) {
// Artists->Genres->Albums->Songs
// Only gets album artists (first parameter)
ListType.Limits limits = new ListType.Limits(startIdx, startIdx + LIMIT_SYNC_ARTISTS);
@ -941,26 +938,27 @@ public class LibrarySyncService extends Service {
action.execute(hostConnection, new ApiCallback<List<AudioType.DetailsArtist>>() {
@Override
public void onSucess(List<AudioType.DetailsArtist> result) {
allResults.addAll(result);
if (result == null) result = new ArrayList<>(0); // Safeguard
// First delete all music info
if (startIdx == 0) deleteMusicInfo(contentResolver, hostId);
// Insert artists
ContentValues artistValuesBatch[] = new ContentValues[result.size()];
for (int i = 0; i < result.size(); i++) {
AudioType.DetailsArtist artist = result.get(i);
artistValuesBatch[i] = SyncUtils.contentValuesFromArtist(hostId, artist);
}
contentResolver.bulkInsert(MediaContract.Artists.CONTENT_URI, artistValuesBatch);
if (result.size() == LIMIT_SYNC_ARTISTS) {
// Max limit returned, there may be some more
LogUtils.LOGD(TAG, "chainCallSyncArtists: More results on media center, recursing.");
result = null; // Help the GC?
chainCallSyncArtists(orchestrator, hostConnection, callbackHandler, contentResolver,
startIdx + LIMIT_SYNC_ARTISTS, allResults);
startIdx + LIMIT_SYNC_ARTISTS);
} else {
// Ok, we have all the shows, insert them
// Ok, we have all the artists, proceed
LogUtils.LOGD(TAG, "chainCallSyncArtists: Got all results, continuing");
// First delete all music info
deleteMusicInfo(contentResolver, hostId);
ContentValues artistValuesBatch[] = new ContentValues[allResults.size()];
for (int i = 0; i < allResults.size(); i++) {
AudioType.DetailsArtist artist = allResults.get(i);
artistValuesBatch[i] = SyncUtils.contentValuesFromArtist(hostId, artist);
}
// Insert the artists and continue the syncing
contentResolver.bulkInsert(MediaContract.Artists.CONTENT_URI, artistValuesBatch);
chainCallSyncGenres(orchestrator, hostConnection, callbackHandler, contentResolver);
}
}
@ -1008,6 +1006,7 @@ public class LibrarySyncService extends Service {
action.execute(hostConnection, new ApiCallback<List<LibraryType.DetailsGenre>>() {
@Override
public void onSucess(List<LibraryType.DetailsGenre> result) {
if (result == null) result = new ArrayList<>(0); // Safeguard
ContentValues genresValuesBatch[] = new ContentValues[result.size()];
for (int i = 0; i < result.size(); i++) {
@ -1015,11 +1014,9 @@ public class LibrarySyncService extends Service {
genresValuesBatch[i] = SyncUtils.contentValuesFromAudioGenre(hostId, genre);
}
// Insert the genres
// Insert the genres and proceed to albums
contentResolver.bulkInsert(MediaContract.AudioGenres.CONTENT_URI, genresValuesBatch);
chainCallSyncAlbums(orchestrator, hostConnection, callbackHandler, contentResolver,
0, new ArrayList<AudioType.DetailsAlbum>());
chainCallSyncAlbums(orchestrator, hostConnection, callbackHandler, contentResolver, 0);
}
@Override
@ -1052,8 +1049,7 @@ public class LibrarySyncService extends Service {
final HostConnection hostConnection,
final Handler callbackHandler,
final ContentResolver contentResolver,
final int startIdx,
final List<AudioType.DetailsAlbum> allResults) {
final int startIdx) {
final long albumSyncStartTime = System.currentTimeMillis();
// Albums->Songs
ListType.Limits limits = new ListType.Limits(startIdx, startIdx + LIMIT_SYNC_ALBUMS);
@ -1061,68 +1057,60 @@ public class LibrarySyncService extends Service {
action.execute(hostConnection, new ApiCallback<List<AudioType.DetailsAlbum>>() {
@Override
public void onSucess(List<AudioType.DetailsAlbum> result) {
allResults.addAll(result);
if (result == null) result = new ArrayList<>(0); // Safeguard
// Insert the partial results
ContentValues albumValuesBatch[] = new ContentValues[result.size()];
int artistsCount = 0, genresCount = 0;
for (int i = 0; i < result.size(); i++) {
AudioType.DetailsAlbum album = result.get(i);
albumValuesBatch[i] = SyncUtils.contentValuesFromAlbum(hostId, album);
artistsCount += album.artistid.size();
genresCount += album.genreid.size();
}
contentResolver.bulkInsert(MediaContract.Albums.CONTENT_URI, albumValuesBatch);
LogUtils.LOGD(TAG, "Finished inserting albums in: " +
(System.currentTimeMillis() - albumSyncStartTime));
// Iterate on each album, collect the artists and the genres and insert them
ContentValues albumArtistsValuesBatch[] = new ContentValues[artistsCount];
ContentValues albumGenresValuesBatch[] = new ContentValues[genresCount];
int artistCount = 0, genreCount = 0;
for (AudioType.DetailsAlbum album : result) {
for (int artistId : album.artistid) {
albumArtistsValuesBatch[artistCount] = new ContentValues();
albumArtistsValuesBatch[artistCount].put(MediaContract.AlbumArtists.HOST_ID, hostId);
albumArtistsValuesBatch[artistCount].put(MediaContract.AlbumArtists.ALBUMID, album.albumid);
albumArtistsValuesBatch[artistCount].put(MediaContract.AlbumArtists.ARTISTID, artistId);
artistCount++;
}
for (int genreId : album.genreid) {
albumGenresValuesBatch[genreCount] = new ContentValues();
albumGenresValuesBatch[genreCount].put(MediaContract.AlbumGenres.HOST_ID, hostId);
albumGenresValuesBatch[genreCount].put(MediaContract.AlbumGenres.ALBUMID, album.albumid);
albumGenresValuesBatch[genreCount].put(MediaContract.AlbumGenres.GENREID, genreId);
genreCount++;
}
}
contentResolver.bulkInsert(MediaContract.AlbumArtists.CONTENT_URI, albumArtistsValuesBatch);
contentResolver.bulkInsert(MediaContract.AlbumGenres.CONTENT_URI, albumGenresValuesBatch);
LogUtils.LOGD(TAG, "Finished inserting artists and genres in: " +
(System.currentTimeMillis() - albumSyncStartTime));
if (result.size() == LIMIT_SYNC_ALBUMS) {
// Max limit returned, there may be some more
LogUtils.LOGD(TAG, "chainCallSyncAlbums: More results on media center, recursing.");
result = null; // Help the GC?
chainCallSyncAlbums(orchestrator, hostConnection, callbackHandler, contentResolver,
startIdx + LIMIT_SYNC_ALBUMS, allResults);
startIdx + LIMIT_SYNC_ALBUMS);
} else {
// Ok, we have all the shows, insert them
// Ok, we have all the albums, proceed to songs
LogUtils.LOGD(TAG, "chainCallSyncAlbums: Got all results, continuing");
ContentValues albumValuesBatch[] = new ContentValues[allResults.size()];
int artistsCount = 0;
int genresCount = 0;
for (int i = 0; i < allResults.size(); i++) {
AudioType.DetailsAlbum album = allResults.get(i);
albumValuesBatch[i] = SyncUtils.contentValuesFromAlbum(hostId, album);
artistsCount += album.artistid.size();
genresCount += album.genreid.size();
}
LogUtils.LOGD(TAG, "Finished parsing albums in: " +
(System.currentTimeMillis() - albumSyncStartTime));
// Insert the albums
contentResolver.bulkInsert(MediaContract.Albums.CONTENT_URI, albumValuesBatch);
LogUtils.LOGD(TAG, "Finished inserting albums in: " +
(System.currentTimeMillis() - albumSyncStartTime));
// Iterate on each album, collect the artists and the genres and insert them
ContentValues albumArtistsValuesBatch[] = new ContentValues[artistsCount];
ContentValues albumGenresValuesBatch[] = new ContentValues[genresCount];
int artistCount = 0, genreCount = 0;
for (AudioType.DetailsAlbum album : allResults) {
for (int artistId : album.artistid) {
albumArtistsValuesBatch[artistCount] = new ContentValues();
albumArtistsValuesBatch[artistCount].put(MediaContract.AlbumArtists.HOST_ID, hostId);
albumArtistsValuesBatch[artistCount].put(MediaContract.AlbumArtists.ALBUMID, album.albumid);
albumArtistsValuesBatch[artistCount].put(MediaContract.AlbumArtists.ARTISTID, artistId);
artistCount++;
}
for (int genreId : album.genreid) {
albumGenresValuesBatch[genreCount] = new ContentValues();
albumGenresValuesBatch[genreCount].put(MediaContract.AlbumGenres.HOST_ID, hostId);
albumGenresValuesBatch[genreCount].put(MediaContract.AlbumGenres.ALBUMID, album.albumid);
albumGenresValuesBatch[genreCount].put(MediaContract.AlbumGenres.GENREID, genreId);
genreCount++;
}
}
LogUtils.LOGD(TAG, "Finished parsing artists and genres in: " +
(System.currentTimeMillis() - albumSyncStartTime));
contentResolver.bulkInsert(MediaContract.AlbumArtists.CONTENT_URI, albumArtistsValuesBatch);
contentResolver.bulkInsert(MediaContract.AlbumGenres.CONTENT_URI, albumGenresValuesBatch);
LogUtils.LOGD(TAG, "Finished inserting artists and genres in: " +
(System.currentTimeMillis() - albumSyncStartTime));
chainCallSyncSongs(orchestrator, hostConnection, callbackHandler, contentResolver,
0, new ArrayList<AudioType.DetailsSong>());
chainCallSyncSongs(orchestrator, hostConnection, callbackHandler, contentResolver, 0);
}
}
@ -1160,34 +1148,31 @@ public class LibrarySyncService extends Service {
final HostConnection hostConnection,
final Handler callbackHandler,
final ContentResolver contentResolver,
final int startIdx,
final List<AudioType.DetailsSong> allResults) {
final int startIdx) {
// Songs
ListType.Limits limits = new ListType.Limits(startIdx, startIdx + LIMIT_SYNC_SONGS);
AudioLibrary.GetSongs action = new AudioLibrary.GetSongs(limits, getSongsProperties);
action.execute(hostConnection, new ApiCallback<List<AudioType.DetailsSong>>() {
@Override
public void onSucess(List<AudioType.DetailsSong> result) {
allResults.addAll(result);
if (result == null) result = new ArrayList<>(0); // Safeguard
// Save partial results to DB
ContentValues songValuesBatch[] = new ContentValues[result.size()];
for (int i = 0; i < result.size(); i++) {
AudioType.DetailsSong song = result.get(i);
songValuesBatch[i] = SyncUtils.contentValuesFromSong(hostId, song);
}
contentResolver.bulkInsert(MediaContract.Songs.CONTENT_URI, songValuesBatch);
if (result.size() == LIMIT_SYNC_SONGS) {
// Max limit returned, there may be some more
LogUtils.LOGD(TAG, "chainCallSyncSongs: More results on media center, recursing.");
result = null; // Help the GC?
chainCallSyncSongs(orchestrator, hostConnection, callbackHandler, contentResolver,
startIdx + LIMIT_SYNC_SONGS, allResults);
startIdx + LIMIT_SYNC_SONGS);
} else {
// Ok, we have all the songs, insert them
LogUtils.LOGD(TAG, "chainCallSyncSongs: Got all results, continuing");
ContentValues songValuesBatch[] = new ContentValues[allResults.size()];
for (int i = 0; i < allResults.size(); i++) {
AudioType.DetailsSong song = allResults.get(i);
songValuesBatch[i] = SyncUtils.contentValuesFromSong(hostId, song);
}
// Insert the songs
contentResolver.bulkInsert(MediaContract.Songs.CONTENT_URI, songValuesBatch);
orchestrator.syncItemFinished();
}
}

View File

@ -0,0 +1,358 @@
/*
* 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.graphics.Bitmap;
import android.graphics.drawable.Drawable;
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.squareup.picasso.Picasso;
import com.squareup.picasso.Target;
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.CharacterDrawable;
import com.syncedsynapse.kore2.utils.LogUtils;
import com.syncedsynapse.kore2.utils.UIUtils;
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();
}
// Picasso target that will be used to load images
private static Target picassoTarget = null;
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
private void buildNotification(PlayerType.GetActivePlayersReturnType getActivePlayerResult,
PlayerType.PropertyValue getPropertiesResult,
ListType.ItemsAll getItemResult) {
final 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
final 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);
final 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);
final 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);
final Notification notification = builder
.setSmallIcon(smallIcon)
.setShowWhen(false)
.setOngoing(true)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setCategory(NotificationCompat.CATEGORY_TRANSPORT)
.setContentIntent(mRemoteStartPendingIntent)
.setContent(collapsedRV)
.build();
// This is a convoluted way of loading the image and showing the
// notification, but it's what works with Picasso and is efficient.
// Here's what's going on:
//
// 1. The image is loaded asynchronously into a Target, and only after
// it is loaded is the notification shown. Using targets is a lot more
// efficient than letting Picasso load it directly into the
// notification imageview, which causes a lot of flickering
//
// 2. The target needs to be static, because Picasso only keeps a weak
// reference to it, so we need to keed a strong reference and reset it
// to null when we're done. We also need to check if it is not null in
// case a previous request hasn't finished yet.
//
// 3. We can only show the notification after the bitmap is loaded into
// the target, so it is done in the callback
//
// 4. We specifically resize the image to the same dimensions used in
// the remote, so that Picasso reuses it in the remote and here from the cache
Resources resources = this.getResources();
final int posterWidth = resources.getDimensionPixelOffset(R.dimen.now_playing_poster_width);
final int posterHeight = isVideo?
resources.getDimensionPixelOffset(R.dimen.now_playing_poster_height):
posterWidth;
if (picassoTarget == null ) {
picassoTarget = new Target() {
@Override
public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {
showNotification(bitmap);
}
@Override
public void onBitmapFailed(Drawable errorDrawable) {
CharacterDrawable avatarDrawable = UIUtils.getCharacterAvatar(NotificationService.this, title);
showNotification(Utils.drawableToBitmap(avatarDrawable, posterWidth, posterHeight));
}
@Override
public void onPrepareLoad(Drawable placeHolderDrawable) { }
private void showNotification(Bitmap bitmap) {
collapsedRV.setImageViewBitmap(R.id.icon, bitmap);
if (Utils.isJellybeanOrLater()) {
notification.bigContentView = expandedRV;
expandedRV.setImageViewBitmap(expandedIconResId, bitmap);
}
NotificationManager notificationManager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.notify(NOTIFICATION_ID, notification);
picassoTarget = null;
}
};
// Load the image
HostManager hostManager = HostManager.getInstance(this);
hostManager.getPicasso()
.load(hostManager.getHostInfo().getImageUrl(poster))
.resize(posterWidth, posterHeight)
.into(picassoTarget);
}
}
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

@ -35,7 +35,7 @@ import com.syncedsynapse.kore2.utils.Utils;
* Controls the presentation of Addons information (list, details)
* All the information is presented by specific fragments
*/
public class AddonsActivity extends HostConnectionActivity
public class AddonsActivity extends BaseActivity
implements AddonListFragment.OnAddonSelectedListener {
private static final String TAG = LogUtils.makeLogTag(AddonsActivity.class);

View File

@ -1,47 +0,0 @@
/*
* 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.ui;
import android.os.Bundle;
import com.syncedsynapse.kore2.host.HostManager;
import com.syncedsynapse.kore2.jsonrpc.HostConnection;
/**
* This activity manages the closing of the {@link HostConnection} singleton provided by
* {@link HostManager}.
* All activities that plan to use the {@link HostConnection}, or their fragments do,
* should inherit from this class to make sure the connection is closed onPause/
*/
public class HostConnectionActivity extends BaseActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
public void onPause() {
super.onPause();
// Disconnect from the connections used in the fragments
HostConnection connection = HostManager.getInstance(this).getConnection();
if (connection != null) {
connection.disconnect();
}
}
}

View File

@ -35,7 +35,7 @@ import com.syncedsynapse.kore2.utils.Utils;
* Controls the presentation of Movies information (list, details)
* All the information is presented by specific fragments
*/
public class MoviesActivity extends HostConnectionActivity
public class MoviesActivity extends BaseActivity
implements MovieListFragment.OnMovieSelectedListener {
private static final String TAG = LogUtils.makeLogTag(MoviesActivity.class);

View File

@ -34,7 +34,7 @@ import com.syncedsynapse.kore2.utils.Utils;
* Controls the presentation of Music information (list, details)
* All the information is presented by specific fragments
*/
public class MusicActivity extends HostConnectionActivity
public class MusicActivity extends BaseActivity
implements ArtistListFragment.OnArtistSelectedListener,
AlbumListFragment.OnAlbumSelectedListener,
AudioGenresListFragment.OnAudioGenreSelectedListener,

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

@ -24,6 +24,7 @@ import android.support.v4.view.ViewPager;
import android.support.v4.widget.DrawerLayout;
import android.support.v7.widget.Toolbar;
import android.text.TextUtils;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.ViewTreeObserver;
@ -31,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;
@ -39,8 +41,10 @@ import com.syncedsynapse.kore2.jsonrpc.method.AudioLibrary;
import com.syncedsynapse.kore2.jsonrpc.method.Input;
import com.syncedsynapse.kore2.jsonrpc.method.System;
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;
@ -51,7 +55,7 @@ import butterknife.ButterKnife;
import butterknife.InjectView;
public class RemoteActivity extends HostConnectionActivity
public class RemoteActivity extends BaseActivity
implements HostConnectionObserver.PlayerEventsObserver,
NowPlayingFragment.NowPlayingListener,
SendTextDialogFragment.SendTextDialogListener {
@ -127,9 +131,9 @@ public class RemoteActivity extends HostConnectionActivity
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
@ -139,6 +143,36 @@ public class RemoteActivity extends HostConnectionActivity
hostConnectionObserver = null;
}
/**
* Override hardware volume keys and send to Kodi
*/
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
// Check whether we should intercept this
boolean useVolumeKeys = PreferenceManager
.getDefaultSharedPreferences(this)
.getBoolean(Settings.KEY_PREF_USE_HARDWARE_VOLUME_KEYS,
Settings.DEFAULT_PREF_USE_HARDWARE_VOLUME_KEYS);
if (useVolumeKeys) {
int action = event.getAction();
int keyCode = event.getKeyCode();
switch (keyCode) {
case KeyEvent.KEYCODE_VOLUME_UP:
if (action == KeyEvent.ACTION_DOWN) {
new Application.SetVolume(GlobalType.IncrementDecrement.INCREMENT).execute(hostManager.getConnection(), null, null);
}
return true;
case KeyEvent.KEYCODE_VOLUME_DOWN:
if (action == KeyEvent.ACTION_DOWN) {
new Application.SetVolume(GlobalType.IncrementDecrement.DECREMENT).execute(hostManager.getConnection(), null, null);
}
return true;
}
}
return super.dispatchKeyEvent(event);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
if (!navigationDrawerFragment.isDrawerOpen()) {
@ -273,7 +307,7 @@ public class RemoteActivity extends HostConnectionActivity
/**
* Sets or clear the image background
* @param url
* @param url Image url
*/
private void setImageViewBackground(String url) {
if (url != null) {
@ -331,6 +365,16 @@ public class RemoteActivity extends HostConnectionActivity
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,
@ -365,6 +409,8 @@ public class RemoteActivity extends HostConnectionActivity
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

@ -35,7 +35,7 @@ import com.syncedsynapse.kore2.utils.Utils;
* Controls the presentation of TV Shows information (list, details)
* All the information is presented by specific fragments
*/
public class TVShowsActivity extends HostConnectionActivity
public class TVShowsActivity extends BaseActivity
implements TVShowListFragment.OnTVShowSelectedListener,
TVShowEpisodeListFragment.OnEpisodeSelectedListener {
private static final String TAG = LogUtils.makeLogTag(TVShowsActivity.class);

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

@ -129,22 +129,8 @@ public class UIUtils {
String imageUrl, String stringAvatar,
ImageView imageView,
int imageWidth, int imageHeight) {
// Load character avatar
if (characterAvatarColors == null) {
characterAvatarColors = context.getResources()
.obtainTypedArray(R.array.character_avatar_colors);
}
char charAvatar = TextUtils.isEmpty(stringAvatar) ?
' ' : stringAvatar.charAt(0);
avatarColorsIdx = TextUtils.isEmpty(stringAvatar) ? 0 :
Math.max(Character.getNumericValue(stringAvatar.charAt(0)) +
Character.getNumericValue(stringAvatar.charAt(stringAvatar.length() - 1)) +
stringAvatar.length(), 0) % characterAvatarColors.length();
int color = characterAvatarColors.getColor(avatarColorsIdx, 0xff000000);
CharacterDrawable avatarDrawable = new CharacterDrawable(charAvatar, color);
// avatarColorsIdx = randomGenerator.nextInt(characterAvatarColors.length());
CharacterDrawable avatarDrawable = getCharacterAvatar(context, stringAvatar);
if (TextUtils.isEmpty(imageUrl)) {
imageView.setImageDrawable(avatarDrawable);
return;
@ -166,6 +152,31 @@ public class UIUtils {
}
}
/**
* Returns a CharacterDrawable that is suitable to use as an avatar
* @param context Context
* @param str String to use to create the avatar
* @return Character avatar to use in a image view
*/
public static CharacterDrawable getCharacterAvatar(Context context, String str) {
// Load character avatar
if (characterAvatarColors == null) {
characterAvatarColors = context
.getResources()
.obtainTypedArray(R.array.character_avatar_colors);
}
char charAvatar = TextUtils.isEmpty(str) ?
' ' : str.charAt(0);
avatarColorsIdx = TextUtils.isEmpty(str) ? 0 :
Math.max(Character.getNumericValue(str.charAt(0)) +
Character.getNumericValue(str.charAt(str.length() - 1)) +
str.length(), 0) % characterAvatarColors.length();
int color = characterAvatarColors.getColor(avatarColorsIdx, 0xff000000);
// avatarColorsIdx = randomGenerator.nextInt(characterAvatarColors.length());
return new CharacterDrawable(charAvatar, color);
}
/**
* Sets play/pause button icon on a ImageView, based on speed
* @param context Activity

View File

@ -17,6 +17,10 @@ package com.syncedsynapse.kore2.utils;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.text.TextUtils;
@ -131,4 +135,23 @@ public class Utils {
context.startActivity(intent);
}
}
/**
* Converts a drawable to a bitmap
* @param drawable Drawable to convert
* @return Bitmap
*/
public static Bitmap drawableToBitmap (Drawable drawable, int width, int height) {
if (drawable instanceof BitmapDrawable) {
return ((BitmapDrawable)drawable).getBitmap();
}
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);
return bitmap;
}
}

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

@ -0,0 +1,296 @@
<?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.
-->
<resources>
<string name="app_name">Kore</string>
<string name="settings">Parametri</string>
<string name="action_options">Opzioni</string>
<string name="loading">Caricamento…</string>
<!-- Navigation drawer tips -->
<string name="navigation_drawer_open">Apri il riquadro di navigazione</string>
<string name="navigation_drawer_close">Chiudi il riquadro di navigazione</string>
<string name="xbmc_media_center">Media Center</string>
<string name="home">Home</string>
<string name="movies">Film</string>
<string name="tv_shows">Programmi TV</string>
<string name="music">Musica</string>
<string name="pictures">Immagini</string>
<string name="addons">Estensioni</string>
<string name="no_xbmc_configured">Media center non configurato</string>
<string name="add_xbmc">Aggiungi Media Center</string>
<string name="xbmc_quit">Media center in chiusura.</string>
<string name="wol_sent">Wake up inviato al media center.</string>
<string name="power">Alimentazione</string>
<string name="quit">Esci</string>
<string name="suspend">Sospensione</string>
<string name="shutdown">Spegni</string>
<string name="send">Invia</string>
<string name="send_text">Invia testo al media center</string>
<string name="text_to_send">Testo da inviare</string>
<string name="finish_after_send">Chiudi dopo l\'invio</string>
<string name="library_actions">Gestione libreria</string>
<string name="clean_video_library">Pulisci libreria video</string>
<string name="clean_audio_library">Pulisci libreria audio</string>
<string name="update_video_library">Aggiorna libreria video</string>
<string name="update_audio_library">Aggiorna libreria audio</string>
<string name="toggle_fullscreen">Passa a schermo intero</string>
<string name="connected_to">Connesso a %1$s</string>
<string name="connecting">Connessione…</string>
<string name="connecting_to">Connessione a %1$s (%2$s)…</string>
<!-- String used in add host wizard -->
<string name="wizard_welcome">Benvenuto</string>
<string name="wizard_welcome_message"><![CDATA[
Cominciamo aggiungendo un media center. Assicurati che il tuo Kodi/XBMC sia in esecuzione, correttamente configurato e sulla stessa rete del tuo dispositivo.<br/><br/>
Puoi trovare aiuto per la configurazione <a href="http://syncedsynapse.com/kore/kore-faq/>qui</a>.<br/><br/>
Quando sei pronto premi su <b><i>Avanti</i></b>.
]]></string>
<string name="wizard_search_message"><![CDATA[
Ricerca dei media center sulla tua rete locale…<br/>
]]></string>
<string name="wizard_search_no_host_found"><![CDATA[
Non riesco a trovare media center sulla tua rete.<br/>Se hai bisogno di aiuto per la configurazione, controlla <a href="http://syncedsynapse.com/kore/kore-faq/>qui</a>.<br/><br/>
Clicca su <i>Ricerca</i> per cercare ancora o <i>Avanti</i> per la configurazione manuale.
]]></string>
<string name="wizard_search_host_found"><![CDATA[
Ho trovato questi media centers sulla tua rete.<br/><br/>Selezionane uno da aggiungere o premi <i>Avanti</i> per aggiungerne uno manualmente.
]]></string>
<string name="searching">Ricerca…</string>
<string name="no_xbmc_found">Nessun media center trovato</string>
<string name="xbmc_found">Media center trovato</string>
<string name="wizard_manual_configuration">Configurazione manuale</string>
<string name="wizard_manual_configuration_message">Inserisci la configurazione del tuo media center:</string>
<string name="wizard_manual_configuration_message_advanced">Configurazione avanzata (lascia vuoto per la configurazione predefinita)</string>
<string name="wizard_xbmc_name">Nome del Media center</string>
<string name="wizard_xbmc_ip">Indirizzo</string>
<string name="wizard_xbmc_port">Porta</string>
<string name="wizard_xbmc_username">Nome utente</string>
<string name="wizard_xbmc_password">Password</string>
<string name="wizard_xbmc_tcp_port">Porta TCP (9090)</string>
<string name="wizard_xbmc_mac_address">Indirizzo MAC</string>
<string name="wizard_xbmc_wol_port">Porta WoL (9)</string>
<string name="wizard_no_name_specified">Indica un nome per questo media center, così potrai identificarlo in seguito.</string>
<string name="wizard_no_address_specified">Specifica l\'indirizzo di questo media center, così posso trovarlo.</string>
<string name="wizard_invalid_http_port_specified">Specifica una porta HTTP valida per questo media center, così posso trovarlo.</string>
<string name="wizard_invalid_tcp_port_specified">Specifica una porta TCP valida per questo media center, così posso trovarlo.</string>
<string name="wizard_connecting_to_xbmc_title">Connessione a %1$s…</string>
<string name="wizard_connecting_to_xbmc_message">Per favore aspetta mentre provo a collegarmi al tuo media center…</string>
<string name="wizard_empty_authentication">Kodi/XBMC richiede un\'autenticazione.\nPer favore specifica un nome utente e una password.</string>
<string name="wizard_incorrect_authentication">Nome utente e/o password non validi.\nPer favore controlla le tue credenziali.</string>
<string name="wizard_success_connecting">Connesso a Kodi/XBMC.</string>
<string name="wizard_error_connecting">Non posso collegarmi a Kodi/XBMC.\nPer favore verifica la configurazione.</string>
<string name="wizard_done">Tutto fatto!</string>
<string name="wizard_done_message"><![CDATA[
Il tuo media center è configurato.<br/>
Ora potrai usare il telecomando per controllarlo. La libreria si sta sincronizzando e dovrebbe essere disponibile fra poco.<br/><br/>
Premi <b><i>Finito</i></b> per cominciare a usare il telecomando.
]]></string>
<string name="play">Play</string>
<string name="pause">Pausa</string>
<string name="stop">Arresta</string>
<string name="fast_forward">Avanti Veloce</string>
<string name="rewind">Indietro</string>
<string name="repeat">Ripeti</string>
<string name="shuffle">Mescola</string>
<string name="volume_up">Aumenta Volume</string>
<string name="volume_down">Riduci Volume</string>
<string name="volume_mute">Muto</string>
<string name="subtitles">Sottotitoli</string>
<string name="audiostreams">Audio</string>
<string name="no_audiostream">Nessun audio disponibile</string>
<string name="download_subtitle">Scarica sottotitoli</string>
<string name="none">Nessuno</string>
<string name="left">Sinistra</string>
<string name="right">Destra</string>
<string name="up">Su</string>
<string name="down">Giù</string>
<string name="select">Select</string>
<string name="info">Info</string>
<string name="codec_info">Codec</string>
<string name="osd">Menu</string>
<string name="back">Indietro</string>
<string name="previous">Precedente</string>
<string name="next">Prossimo</string>
<string name="finish">Finito</string>
<string name="test_connection">Test</string>
<string name="search_again">Cerca ancora</string>
<string name="remove">Rimuovi</string>
<string name="edit">Modifica</string>
<string name="wake_up">Wake up</string>
<string name="edit_xbmc">Modifica media center</string>
<string name="delete_xbmc">Cancella media center</string>
<string name="delete_xbmc_confirm">Sei sicuro di voler cancellare questo media center?</string>
<string name="connecting_to_xbmc">Connessione…</string>
<string name="unable_to_connect_to_xbmc">Impossibile connettersi al media center</string>
<string name="connected_to_xbmc">Connesso</string>
<string name="xbmc_available">Disponibile</string>
<string name="xbmc_unavailable">Non disponibile</string>
<string name="nothing_playing">Niente in play</string>
<!-- Main view tabs -->
<string name="now_playing">Ora in play</string>
<string name="remote">Telecomando</string>
<string name="playlist">Playlist</string>
<string name="season_episode_abbrev">s%1$02de%2$02d</string>
<string name="season_episode">Stagione %1$02d | Episodio %2$02d</string>
<string name="season_number">Stagione %1$02d</string>
<string name="episode_number">%1$d</string>
<string name="votes">%1$s voti</string>
<string name="max_rating_video">/10</string>
<string name="max_rating_music">/5</string>
<string name="fanart">Fanart</string>
<string name="poster">Poster</string>
<string name="thumbnail">Thumbnail</string>
<string name="error_getting_properties">Non posso leggere i parametri di Kodi/XBMC.\nMessaggio di errore:
%1$s.</string>
<string name="error_executing_subtitles">Non posso eseguire l\'estensione per i sottotitoli.\nMessaggio di errore: %1$s.</string>
<string name="error_getting_addon_info">Non posso acquisire informazioni sulle estensioni.\nMessaggio di errore:
%1$s.</string>
<string name="directors">Registi:</string>
<string name="studio">Studio:</string>
<string name="cast">Cast</string>
<string name="additional_cast">Cast secondario</string>
<string name="cast_list_text">%1$s as %2$s</string>
<string name="general_error_executing_action">Errore durante l\'operazione: %1$s</string>
<string name="error_getting_playlist">Errore nella lettura della playlist</string>
<string name="error_message">Messaggio di errore: %1$s</string>
<string name="playlist_empty">Playlist vuota</string>
<string name="clear_playlist">Pulisci playlist</string>
<string name="no_movies_found_refresh">Nessun film trovato\n\nSwipe in giù per aggiornare</string>
<string name="no_tvshows_found_refresh">Nessun programma TV trovato\n\nSwipe in giù per aggiornare</string>
<string name="no_episodes_found">Nessun episodio trovato</string>
<string name="no_artists_found_refresh">Nessun artista trovato\n\nSwipe in giù per aggiornare</string>
<string name="no_albums_found_refresh">Nessim album trovato\n\nSwipe in giù per aggiornare</string>
<string name="no_genres_found_refresh">Nessun genere trovato\n\nSwipe down to refresh</string>
<string name="no_addons_found_refresh">Nessuna estensione trovata o non connesso\n\nSwipe in giù per aggiornare</string>
<string name="no_music_videos_found_refresh">Nessun video trovato\n\nSwipe in giù per aggiornare</string>
<string name="pull_to_refresh">Premi per aggiornare</string>
<string name="minutes_abbrev">%1$s min</string>
<string name="sync_successful">Sincronizzazione avvenuto con successo</string>
<string name="error_while_syncing">Si è verificato un errore durante la sincronizzazione: %1$s</string>
<string name="action_search">Cerca</string>
<string name="action_search_movies">Cerca Film</string>
<string name="action_search_tvshows">Cerca spettacoli TV</string>
<string name="action_search_albums">Cerca album</string>
<string name="action_search_artists">Cerca per artista</string>
<string name="action_search_genres">Cerca per genere</string>
<string name="action_search_music_videos">Cerca video</string>
<string name="add_to_playlist">Aggiungi alla playlist</string>
<string name="item_added_to_playlist">Aggiunto alla playlist</string>
<string name="no_suitable_playlist">Nessuna playlist disponibile per aggiungere il media.</string>
<string name="imdb">IMDb</string>
<string name="seen">Visto</string>
<string name="download">Scaricato</string>
<string name="download_file_exists">File già esistente.\nVuoi sovrascriverlo o scaricarlo con un nuovo nome?</string>
<string name="download_dir_exists">Cartella download esistente.\nSe esistono file con lo stesso nome, vuoi sovrascriverli o scaricarli con un nuovo nome?</string>
<string name="overwrite">Sovrascrivi</string>
<string name="download_with_new_name">Nuovo nome</string>
<string name="download_file_description">Scaricato dal tuo media center</string>
<string name="num_episodes">%1$d episodi | %2$d non visti</string>
<string name="premiered">Premiere: %1$s</string>
<string name="tvshow_overview">Riepilogo</string>
<string name="tvshow_episodes">Episodi</string>
<string name="artists">Artisti</string>
<string name="albums">Album</string>
<string name="genres">Generi</string>
<string name="music_videos">Video</string>
<string name="no_files_to_download">Nessun file da scaricare.</string>
<string name="error_getting_file_information">Non posso ricevere informazioni per scaricare il file %1$s.</string>
<string name="author">Autore:</string>
<string name="version">Versione:</string>
<string name="enable_disable">Abilita/Disabilita Estensioni</string>
<string name="addon_enabled">Estensioni abilitate</string>
<string name="addon_disabled">Estensioni disabilitate</string>
<!-- Filters on list menus -->
<string name="hide_watched">Nascondi già visti</string>
<string name="sort_order">Ordina</string>
<string name="sort_by_name">Per nome</string>
<string name="sort_by_date_added">Per data di inserimento</string>
<string name="ignore_prefixes">Ignora prefissi</string>
<!-- Preferences strings -->
<string name="theme">Tema</string>
<string name="theme_night">Notte</string>
<string name="theme_day">Giorno</string>
<string name="theme_mist">Nebbia</string>
<string name="theme_solarized">Solarized</string>
<string name="theme_solarized_dark">Solarized Dark</string>
<string name="switch_to_remote">Switch to remote after media start</string>
<string name="about">About</string>
<string name="about_desc"><![CDATA[
\u00A9 2015 Synced Synapse.<br><br>
Per favore votaci su <b><a href="market://details?id=com.syncedsynapse.kore2">Google Play</a></b><br><br>
Se ti serve aiuto, controlla le <b><a href="http://syncedsynapse.com/kore/kore-faq/">FAQ</a></b> o
segui Kore <b><a href="https://plus.google.com/u/0/communities/110340113064213296333">Google+ Community</a></b>
]]></string>
<!-- String for coffee -->
<string name="buy_me_coffee">Offrimi un caffè</string>
<string name="expresso_please">Un espresso, per favore. Grazie!</string>
<string name="thanks_for_coffe">Grazie per il caffè!</string>
<string name="remove_coffee_message">Premi per nascondere questa impostazione</string>
<string name="buy_coffee_to_unlock_themes">Per favore offrimi un caffè per sbloccare più temi</string>
<string name="error_setting_up_billing">Non risco ad accedere al Google Play Billing Service:\n%s</string>
<string name="error_querying_inventory">Errore durante l\'interrogazione dell\'inventario.</string>
<string name="error_during_purchased">Si è un errore durante l\'acquisto.</string>
<string name="purchase_thanks">Grazie per il tuo supporto!</string>
</resources>

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,8 @@
<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="use_hardware_volume_keys">Use volume keys to control volume</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,16 @@
android:title="@string/switch_to_remote"
android:defaultValue="true"/>
<CheckBoxPreference
android:key="pref_show_notification"
android:title="@string/show_notification"
android:defaultValue="false"/>
<CheckBoxPreference
android:key="pref_use_hardware_volume_keys"
android:title="@string/use_hardware_volume_keys"
android:defaultValue="true"/>
<Preference
android:key="pref_about"
android:title="@string/about"/>