From e2c39e35ba2cce95b97ec747dd66f793ecd86ea4 Mon Sep 17 00:00:00 2001
From: Martijn Brekhof
Date: Tue, 14 Nov 2017 08:30:56 +0100
Subject: [PATCH] Redesigned playing movies on device running Kore
* Implemented a new widget "fabspeeddial"
* Provides user with two options to play the media item. One
option to play the item on Kodi, one to play it on the remote.
* Replaced deprecated FAB button from
com.melnykov:floatingactionbutton:1.3.0 with the FAB button from
the design library
* Implemented a busy indicator (pulsate) when fab button is clicked
and JSON API method is still pending
* Added a setting to allow the user to disable local playback and
revert back to the old behaviour
* Refactored AbstractFragmentInfo
* Replaced RelativeLayout by CoordinatorLayout to support
hiding/showing the FAB button when scrolling
* Replaced the tree view observer to fade out art view when scrolling
with a behavior for the CoordinaterLayout
* Removed empty theme file for v19
* Refactored HostConnection to allow new activities to attach its
callbacks to any pending ApiMethod. This is required to support device
configuration changes.
---
app/build.gradle | 2 +-
app/src/main/java/org/xbmc/kore/Settings.java | 4 +
.../org/xbmc/kore/jsonrpc/HostConnection.java | 102 ++-
.../xbmc/kore/ui/AbstractInfoFragment.java | 122 +--
.../xbmc/kore/ui/MovieDetailsFragment.java | 721 ------------------
.../animators/ChangeImageFadeAnimation.java | 102 +++
.../kore/ui/animators/PulsateAnimation.java | 104 +++
.../ui/behaviors/FABSpeedDialBehavior.java | 54 ++
.../FadeOutOnVerticalScrollBehavior.java | 51 ++
.../ui/sections/addon/AddonInfoFragment.java | 11 +-
.../ui/sections/audio/AlbumInfoFragment.java | 8 +-
.../ui/sections/audio/ArtistInfoFragment.java | 9 +-
.../audio/MusicVideoInfoFragment.java | 8 +-
.../ui/sections/video/MovieInfoFragment.java | 17 +-
.../video/TVShowEpisodeInfoFragment.java | 40 +-
.../ui/sections/video/TVShowInfoFragment.java | 4 +-
.../fabspeeddial/DialActionButton.java | 206 +++++
.../ui/widgets/fabspeeddial/FABSpeedDial.java | 362 +++++++++
app/src/main/res/color/fabspeeddial.xml | 8 +
.../ic_cellphone_android_white_24dp.xml | 8 +
.../main/res/drawable/ic_plus_white_24dp.xml | 8 +
.../res/drawable/rounded_corners_shape.xml | 6 +
.../main/res/layout/dial_action_button.xml | 24 +
app/src/main/res/layout/fab_speed_dial.xml | 31 +
app/src/main/res/layout/fragment_info.xml | 37 +-
app/src/main/res/values-v19/themes.xml | 23 -
app/src/main/res/values/attr.xml | 9 +-
app/src/main/res/values/colors.xml | 2 +
app/src/main/res/values/strings.xml | 3 +
app/src/main/res/values/styles.xml | 10 +-
app/src/main/res/values/themes.xml | 10 +-
app/src/main/res/xml/preferences.xml | 6 +
32 files changed, 1225 insertions(+), 887 deletions(-)
delete mode 100644 app/src/main/java/org/xbmc/kore/ui/MovieDetailsFragment.java
create mode 100644 app/src/main/java/org/xbmc/kore/ui/animators/ChangeImageFadeAnimation.java
create mode 100644 app/src/main/java/org/xbmc/kore/ui/animators/PulsateAnimation.java
create mode 100644 app/src/main/java/org/xbmc/kore/ui/behaviors/FABSpeedDialBehavior.java
create mode 100644 app/src/main/java/org/xbmc/kore/ui/behaviors/FadeOutOnVerticalScrollBehavior.java
create mode 100644 app/src/main/java/org/xbmc/kore/ui/widgets/fabspeeddial/DialActionButton.java
create mode 100644 app/src/main/java/org/xbmc/kore/ui/widgets/fabspeeddial/FABSpeedDial.java
create mode 100644 app/src/main/res/color/fabspeeddial.xml
create mode 100644 app/src/main/res/drawable/ic_cellphone_android_white_24dp.xml
create mode 100644 app/src/main/res/drawable/ic_plus_white_24dp.xml
create mode 100644 app/src/main/res/drawable/rounded_corners_shape.xml
create mode 100644 app/src/main/res/layout/dial_action_button.xml
create mode 100644 app/src/main/res/layout/fab_speed_dial.xml
delete mode 100644 app/src/main/res/values-v19/themes.xml
diff --git a/app/build.gradle b/app/build.gradle
index a4ea54bf6..ed97622f9 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -116,6 +116,7 @@ dependencies {
compile "com.android.support:cardview-v7:${supportLibVersion}"
compile "com.android.support:preference-v14:${supportLibVersion}"
compile "com.android.support:support-v13:${supportLibVersion}"
+ compile "com.android.support:design:${supportLibVersion}"
compile 'com.fasterxml.jackson.core:jackson-databind:2.5.2'
compile 'com.jakewharton:butterknife:6.1.0'
@@ -124,7 +125,6 @@ dependencies {
compile 'de.greenrobot:eventbus:2.4.0'
compile 'org.jmdns:jmdns:3.5.1'
compile 'com.astuetz:pagerslidingtabstrip:1.0.1'
- compile 'com.melnykov:floatingactionbutton:1.3.0'
compile 'at.blogc:expandabletextview:1.0.3'
compile 'com.sothree.slidinguppanel:library:3.3.1'
diff --git a/app/src/main/java/org/xbmc/kore/Settings.java b/app/src/main/java/org/xbmc/kore/Settings.java
index 6f9628b6a..6fa971a69 100644
--- a/app/src/main/java/org/xbmc/kore/Settings.java
+++ b/app/src/main/java/org/xbmc/kore/Settings.java
@@ -155,6 +155,10 @@ public static String getNavDrawerItemsPrefKey(int hostId) {
public static final String KEY_PREF_SINGLE_COLUMN = "pref_single_multi_column";
public static final boolean DEFAULT_PREF_SINGLE_COLUMN = false;
+ // Switch to remote
+ public static final String KEY_PREF_DISABLE_LOCAL_PLAY = "pref_disable_local_play";
+ public static final boolean DEFAULT_PREF_DISABLE_LOCAL_PLAY = false;
+
/**
* Determines the bit flags used by {@link DownloadManager.Request} to correspond to the enabled network connections
* from the settings screen.
diff --git a/app/src/main/java/org/xbmc/kore/jsonrpc/HostConnection.java b/app/src/main/java/org/xbmc/kore/jsonrpc/HostConnection.java
index cb9dccfeb..f61d6fd0f 100644
--- a/app/src/main/java/org/xbmc/kore/jsonrpc/HostConnection.java
+++ b/app/src/main/java/org/xbmc/kore/jsonrpc/HostConnection.java
@@ -15,6 +15,7 @@
*/
package org.xbmc.kore.jsonrpc;
+import android.annotation.SuppressLint;
import android.os.Handler;
import android.os.Process;
import android.text.TextUtils;
@@ -281,11 +282,13 @@ public void unregisterApplicationNotificationsObserver(ApplicationNotificationsO
}
/**
- * Calls the a method on the server
+ * Calls the given method on the server
* This call is always asynchronous. The results will be posted, through the
* {@link ApiCallback callback} parameter, on the specified {@link android.os.Handler}.
- *
- * @param method Method object that represents the methood too call
+ *
+ * If you need to update the callback and handler (e.g. due to a device configuration change)
+ * use {@link #updateClientCallback(int, ApiCallback, Handler)}
+ * @param method Method object that represents the methood too call
* @param callback {@link ApiCallback} to post the response to
* @param handler {@link Handler} to invoke callbacks on. When null, the
* callbacks are invoked on the same thread as the request.
@@ -297,13 +300,21 @@ public void execute(final ApiMethod method, final ApiCallback callback
LogUtils.LOGD(TAG, "Starting method execute. Method: " + method.getMethodName() +
" on host: " + hostInfo.getJsonRpcHttpEndpoint());
+ if (protocol == PROTOCOL_TCP) {
+ /**
+ * Do not call this from the runnable below as it may cause a race condition
+ * with {@link #updateClientCallback(int, ApiCallback, Handler)}
+ */
+ // Save this method/callback for any later response
+ addClientCallback(method, callback, handler);
+ }
+
// Launch background thread
Runnable command = new Runnable() {
@Override
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
if (protocol == PROTOCOL_HTTP) {
-// executeThroughHttp(method, callback, handler);
executeThroughOkHttp(method, callback, handler);
} else {
executeThroughTcp(method, callback, handler);
@@ -342,6 +353,67 @@ public Future execute(ApiMethod method) {
return ApiFuture.from(this, method);
}
+ /**
+ * Updates the client callback for the given {@link ApiMethod} if it is still pending.
+ * This can be used when the activity or fragment has been destroyed and recreated and
+ * you are still interested in the result of any pending {@link ApiMethod}
+ * @param methodId for which a new callback needs to be attached
+ * @param callback new callback that needs to be called for the new activity or fragment
+ * @param handler used to execute the callback on the UI thread
+ * @param result type
+ * @return true if the {@link ApiMethod} was still pending, false otherwise.
+ */
+ @SuppressWarnings("unchecked")
+ public boolean updateClientCallback(final int methodId, final ApiCallback callback,
+ final Handler handler) {
+
+ if (getProtocol() == PROTOCOL_HTTP)
+ return false;
+
+ synchronized (clientCallbacks) {
+ String id = String.valueOf(methodId);
+ if (clientCallbacks.containsKey(id)) {
+ clientCallbacks.put(id, new MethodCallInfo<>((ApiMethod) clientCallbacks.get(id).method,
+ callback, handler));
+ return true;
+ }
+ return false;
+ }
+ }
+
+ /**
+ * Stores the method and callback to handle asynchronous responses.
+ * Note this is only needed for requests over TCP.
+ * @param method
+ * @param callback
+ * @param handler
+ * @param
+ */
+ private void addClientCallback(final ApiMethod method, final ApiCallback callback,
+ final Handler handler) {
+
+ if (getProtocol() == PROTOCOL_HTTP)
+ return;
+
+ String methodId = String.valueOf(method.getId());
+
+ synchronized (clientCallbacks) {
+ if (clientCallbacks.containsKey(methodId)) {
+ if ((handler != null) && (callback != null)) {
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ callback.onError(ApiException.API_METHOD_WITH_SAME_ID_ALREADY_EXECUTING,
+ "A method with the same Id is already executing");
+ }
+ });
+ }
+ return;
+ }
+ clientCallbacks.put(methodId, new MethodCallInfo(method, callback, handler));
+ }
+ }
+
/**
* Sends the JSON RPC request through HTTP (using OkHttp library)
*/
@@ -355,7 +427,6 @@ private void executeThroughOkHttp(final ApiMethod method, final ApiCallba
.url(hostInfo.getJsonRpcHttpEndpoint())
.post(RequestBody.create(MEDIA_TYPE_JSON, jsonRequest))
.build();
- LogUtils.LOGD(TAG, "Sending request via OkHttp: " + jsonRequest);
Response response = sendOkHttpRequest(client, request);
final T result = method.resultFromJson(parseJsonResponse(handleOkHttpResponse(response)));
@@ -517,25 +588,7 @@ private ObjectNode parseJsonResponse(String response) throws ApiException {
private void executeThroughTcp(final ApiMethod method, final ApiCallback callback,
final Handler handler) {
String methodId = String.valueOf(method.getId());
- try {
- // Save this method/callback for later response
- // Check if a method with this id is already running and raise an error if so
- synchronized (clientCallbacks) {
- if (clientCallbacks.containsKey(methodId)) {
- if (callback != null) {
- postOrRunNow(handler, new Runnable() {
- @Override
- public void run() {
- callback.onError(ApiException.API_METHOD_WITH_SAME_ID_ALREADY_EXECUTING,
- "A method with the same Id is already executing");
- }
- });
- }
- return;
- }
- clientCallbacks.put(methodId, new MethodCallInfo(method, callback, handler));
- }
-
+ try {
// TODO: Validate if this shouldn't be enclosed by a synchronized.
if (socket == null) {
// Open connection to the server and setup reader thread
@@ -843,6 +896,7 @@ public void run() {
});
}
}
+
clientCallbacks.clear();
}
}
diff --git a/app/src/main/java/org/xbmc/kore/ui/AbstractInfoFragment.java b/app/src/main/java/org/xbmc/kore/ui/AbstractInfoFragment.java
index 28c08c84b..8cbe02b8e 100644
--- a/app/src/main/java/org/xbmc/kore/ui/AbstractInfoFragment.java
+++ b/app/src/main/java/org/xbmc/kore/ui/AbstractInfoFragment.java
@@ -17,11 +17,14 @@
package org.xbmc.kore.ui;
import android.Manifest;
+import android.annotation.SuppressLint;
import android.annotation.TargetApi;
+import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.content.res.TypedArray;
+import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.preference.PreferenceManager;
@@ -30,6 +33,7 @@
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.content.ContextCompat;
+import android.support.v4.widget.NestedScrollView;
import android.support.v4.widget.SwipeRefreshLayout;
import android.text.TextUtils;
import android.util.DisplayMetrics;
@@ -39,17 +43,12 @@
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
-import android.view.ViewTreeObserver;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
-import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
-import com.melnykov.fab.FloatingActionButton;
-import com.melnykov.fab.ObservableScrollView;
-
import org.xbmc.kore.R;
import org.xbmc.kore.Settings;
import org.xbmc.kore.host.HostInfo;
@@ -60,6 +59,7 @@
import org.xbmc.kore.service.library.LibrarySyncService;
import org.xbmc.kore.service.library.SyncUtils;
import org.xbmc.kore.ui.generic.RefreshItem;
+import org.xbmc.kore.ui.widgets.fabspeeddial.FABSpeedDial;
import org.xbmc.kore.utils.LogUtils;
import org.xbmc.kore.utils.SharedElementTransition;
import org.xbmc.kore.utils.UIUtils;
@@ -79,9 +79,11 @@ abstract public class AbstractInfoFragment extends AbstractFragment
SharedElementTransition.SharedElement {
private static final String TAG = LogUtils.makeLogTag(AbstractInfoFragment.class);
+ private static final String BUNDLE_KEY_APIMETHOD_PENDING = "pending_apimethod";
+
// Detail views
@InjectView(R.id.swipe_refresh_layout) SwipeRefreshLayout swipeRefreshLayout;
- @InjectView(R.id.media_panel) ScrollView panelScrollView;
+ @InjectView(R.id.media_panel) NestedScrollView panelScrollView;
@InjectView(R.id.art) ImageView artImageView;
@InjectView(R.id.poster) ImageView posterImageView;
@InjectView(R.id.media_title) TextView titleTextView;
@@ -101,7 +103,7 @@ abstract public class AbstractInfoFragment extends AbstractFragment
@InjectView(R.id.media_description) ExpandableTextView descriptionExpandableTextView;
@InjectView(R.id.media_description_container) LinearLayout descriptionContainer;
@InjectView(R.id.show_all) ImageView expansionImage;
- @InjectView(R.id.fab) ImageButton fabButton;
+ @InjectView(R.id.fab) FABSpeedDial fabButton;
@InjectView(R.id.exit_transition_view) View exitTransitionView;
private HostManager hostManager;
@@ -109,6 +111,7 @@ abstract public class AbstractInfoFragment extends AbstractFragment
private ServiceConnection serviceConnection;
private RefreshItem refreshItem;
private boolean expandDescription;
+ private int methodId;
/**
* Handler on which to post RPC callbacks
@@ -144,17 +147,7 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa
ViewGroup root = (ViewGroup) inflater.inflate(R.layout.fragment_info, container, false);
ButterKnife.inject(this, root);
- // Setup dim the fanart when scroll changes. Full dim on 4 * iconSize dp
Resources resources = getActivity().getResources();
- final int pixelsToTransparent = 4 * resources.getDimensionPixelSize(R.dimen.default_icon_size);
- panelScrollView.getViewTreeObserver().addOnScrollChangedListener(new ViewTreeObserver.OnScrollChangedListener() {
- @Override
- public void onScrollChanged() {
- float y = panelScrollView.getScrollY();
- float newAlpha = Math.min(1, Math.max(0, 1 - (y / pixelsToTransparent)));
- artImageView.setAlpha(newAlpha);
- }
- });
DataHolder dataHolder = getDataHolder();
@@ -171,9 +164,6 @@ public void onScrollChanged() {
swipeRefreshLayout.setEnabled(false);
}
- FloatingActionButton fab = (FloatingActionButton)fabButton;
- fab.attachToScrollView((ObservableScrollView) panelScrollView);
-
if(Utils.isLollipopOrLater()) {
posterImageView.setTransitionName(dataHolder.getPosterTransitionName());
}
@@ -207,6 +197,14 @@ public void onScrollChanged() {
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
setHasOptionsMenu(true);
+
+ if (savedInstanceState != null) {
+ int methodId = savedInstanceState.getInt(BUNDLE_KEY_APIMETHOD_PENDING);
+
+ fabButton.enableBusyAnimation(HostManager.getInstance(getContext()).getConnection()
+ .updateClientCallback(methodId, createPlayItemOnKodiCallback(),
+ callbackHandler));
+ }
}
@Override
@@ -245,6 +243,13 @@ public void onStop() {
SyncUtils.disconnectFromLibrarySyncService(getActivity(), serviceConnection);
}
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+
+ outState.putInt(BUNDLE_KEY_APIMETHOD_PENDING, methodId);
+ }
+
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
@@ -294,37 +299,24 @@ protected void setFabButtonState(boolean enable) {
}
}
- protected void fabActionPlayItem(PlaylistType.Item item) {
+ protected void playItemLocally(String url, String type) {
+ Uri uri = Uri.parse(url);
+ Intent intent = new Intent(Intent.ACTION_VIEW, uri);
+ intent.setDataAndType(uri, type);
+ startActivity(intent);
+ }
+
+ protected void playItemOnKodi(PlaylistType.Item item) {
if (item == null) {
Toast.makeText(getActivity(), R.string.no_item_available_to_play, Toast.LENGTH_SHORT).show();
return;
}
+ fabButton.enableBusyAnimation(true);
Player.Open action = new Player.Open(item);
- action.execute(HostManager.getInstance(getActivity()).getConnection(), new ApiCallback() {
- @Override
- public void onSuccess(String result) {
- if (!isAdded()) return;
- // Check whether we should switch to the remote
- boolean switchToRemote = PreferenceManager
- .getDefaultSharedPreferences(getActivity())
- .getBoolean(Settings.KEY_PREF_SWITCH_TO_REMOTE_AFTER_MEDIA_START,
- Settings.DEFAULT_PREF_SWITCH_TO_REMOTE_AFTER_MEDIA_START);
- if (switchToRemote) {
- int cx = (fabButton.getLeft() + fabButton.getRight()) / 2;
- int cy = (fabButton.getTop() + fabButton.getBottom()) / 2;
- UIUtils.switchToRemoteWithAnimation(getActivity(), cx, cy, exitTransitionView);
- }
- }
-
- @Override
- public void onError(int errorCode, String description) {
- if (!isAdded()) return;
- // Got an error, show toast
- Toast.makeText(getActivity(), R.string.unable_to_connect_to_xbmc, Toast.LENGTH_SHORT)
- .show();
- }
- }, callbackHandler);
+ methodId = action.getId();
+ action.execute(HostManager.getInstance(getActivity()).getConnection(),
+ createPlayItemOnKodiCallback(), callbackHandler);
}
@Override
@@ -366,6 +358,7 @@ protected HostInfo getHostInfo() {
/**
* Call this when you are ready to provide the titleTextView, undertitle, details, descriptionExpandableTextView, etc. etc.
*/
+ @SuppressLint("StringFormatInvalid")
protected void updateView(DataHolder dataHolder) {
titleTextView.setText(dataHolder.getTitle());
underTitleTextView.setText(dataHolder.getUnderTitle());
@@ -497,7 +490,7 @@ protected void setOnPinClickedListener(final View.OnClickListener listener) {
/**
* Uses colors to show to the user the item has been downloaded
- * @param state true if item has been watched/listened too, false otherwise
+ * @param state true if item has been downloaded, false otherwise
*/
protected void setDownloadButtonState(boolean state) {
UIUtils.highlightImageView(getActivity(), downloadButton, state);
@@ -558,6 +551,41 @@ protected void setExpandDescription(boolean expandDescription) {
this.expandDescription = expandDescription;
}
+ public FABSpeedDial getFabButton() {
+ return fabButton;
+ }
+
+ private ApiCallback createPlayItemOnKodiCallback() {
+ return new ApiCallback() {
+ @Override
+ public void onSuccess(String result) {
+ if (!isAdded()) return;
+ fabButton.enableBusyAnimation(false);
+
+ // Check whether we should switch to the remote
+ boolean switchToRemote = PreferenceManager
+ .getDefaultSharedPreferences(getActivity())
+ .getBoolean(Settings.KEY_PREF_SWITCH_TO_REMOTE_AFTER_MEDIA_START,
+ Settings.DEFAULT_PREF_SWITCH_TO_REMOTE_AFTER_MEDIA_START);
+ if (switchToRemote) {
+ int cx = (fabButton.getLeft() + fabButton.getRight()) / 2;
+ int cy = (fabButton.getTop() + fabButton.getBottom()) / 2;
+ UIUtils.switchToRemoteWithAnimation(getActivity(), cx, cy, exitTransitionView);
+ }
+ }
+
+ @Override
+ public void onError(int errorCode, String description) {
+ if (!isAdded()) return;
+ fabButton.enableBusyAnimation(false);
+
+ // Got an error, show toast
+ Toast.makeText(getActivity(), R.string.unable_to_connect_to_xbmc, Toast.LENGTH_SHORT)
+ .show();
+ }
+ };
+ }
+
abstract protected AbstractAdditionalInfoFragment getAdditionalInfoFragment();
/**
@@ -586,5 +614,5 @@ protected void setExpandDescription(boolean expandDescription) {
* Called when the fab button is available
* @return true to enable the Floating Action Button, false otherwise
*/
- abstract protected boolean setupFAB(ImageButton FAB);
+ abstract protected boolean setupFAB(FABSpeedDial FAB);
}
diff --git a/app/src/main/java/org/xbmc/kore/ui/MovieDetailsFragment.java b/app/src/main/java/org/xbmc/kore/ui/MovieDetailsFragment.java
deleted file mode 100644
index 745a55e44..000000000
--- a/app/src/main/java/org/xbmc/kore/ui/MovieDetailsFragment.java
+++ /dev/null
@@ -1,721 +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 org.xbmc.kore.ui;
-
-import android.annotation.TargetApi;
-import android.app.AlertDialog;
-import android.content.DialogInterface;
-import android.content.Intent;
-import android.content.res.Resources;
-import android.content.res.TypedArray;
-import android.database.Cursor;
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.Handler;
-import android.preference.PreferenceManager;
-import android.provider.BaseColumns;
-import android.support.v4.app.LoaderManager;
-import android.support.v4.content.CursorLoader;
-import android.support.v4.content.Loader;
-import android.support.v4.widget.SwipeRefreshLayout;
-import android.text.TextUtils;
-import android.util.DisplayMetrics;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewTreeObserver;
-import android.widget.GridLayout;
-import android.widget.ImageButton;
-import android.widget.ImageView;
-import android.widget.ScrollView;
-import android.widget.TextView;
-import android.widget.Toast;
-
-import com.melnykov.fab.FloatingActionButton;
-import com.melnykov.fab.ObservableScrollView;
-
-import org.xbmc.kore.R;
-import org.xbmc.kore.Settings;
-import org.xbmc.kore.host.HostInfo;
-import org.xbmc.kore.jsonrpc.ApiCallback;
-import org.xbmc.kore.jsonrpc.event.MediaSyncEvent;
-import org.xbmc.kore.jsonrpc.method.Player;
-import org.xbmc.kore.jsonrpc.method.Playlist;
-import org.xbmc.kore.jsonrpc.method.VideoLibrary;
-import org.xbmc.kore.jsonrpc.type.PlaylistType;
-import org.xbmc.kore.jsonrpc.type.VideoType;
-import org.xbmc.kore.provider.MediaContract;
-import org.xbmc.kore.service.library.LibrarySyncService;
-import org.xbmc.kore.utils.FileDownloadHelper;
-import org.xbmc.kore.utils.LogUtils;
-import org.xbmc.kore.utils.UIUtils;
-import org.xbmc.kore.utils.Utils;
-
-import java.io.File;
-import java.util.ArrayList;
-
-import butterknife.ButterKnife;
-import butterknife.InjectView;
-import butterknife.OnClick;
-
-/**
- * Presents movie details
- */
-public class MovieDetailsFragment extends AbstractDetailsFragment
- implements LoaderManager.LoaderCallbacks {
- private static final String TAG = LogUtils.makeLogTag(MovieDetailsFragment.class);
-
- public static final String BUNDLE_KEY_MOVIETITLE = "movie_title";
- public static final String BUNDLE_KEY_MOVIEPLOT = "movie_plot";
- public static final String BUNDLE_KEY_MOVIEID = "movie_id";
- public static final String POSTER_TRANS_NAME = "POSTER_TRANS_NAME";
- public static final String BUNDLE_KEY_MOVIEGENRES = "movie_genres";
- public static final String BUNDLE_KEY_MOVIEYEAR = "movie_year";
- public static final String BUNDLE_KEY_MOVIERUNTIME = "movie_runtime";
- public static final String BUNDLE_KEY_MOVIERATING = "movie_rating";
- // Loader IDs
- private static final int LOADER_MOVIE = 0,
- LOADER_CAST = 1;
-
- /**
- * Handler on which to post RPC callbacks
- */
- private Handler callbackHandler = new Handler();
-
- // Displayed movie id
- private int movieId = -1;
- private String movieTitle;
-
- private ArrayList castArrayList;
-
- // Info for downloading the movie
- private FileDownloadHelper.MovieInfo movieDownloadInfo = null;
-
- // Controls whether a automatic sync refresh has been issued for this show
- private static boolean hasIssuedOutdatedRefresh = false;
-
- @InjectView(R.id.swipe_refresh_layout) SwipeRefreshLayout swipeRefreshLayout;
-
- @InjectView(R.id.exit_transition_view) View exitTransitionView;
- // Buttons
- @InjectView(R.id.fab) ImageButton fabButton;
- @InjectView(R.id.add_to_playlist) ImageButton addToPlaylistButton;
- @InjectView(R.id.go_to_imdb) ImageButton imdbButton;
- @InjectView(R.id.download) ImageButton downloadButton;
- @InjectView(R.id.seen) ImageButton seenButton;
- @InjectView(R.id.local_play) ImageButton localPlayButton;
-
- // Detail views
- @InjectView(R.id.media_panel) ScrollView mediaPanel;
-
- @InjectView(R.id.art) ImageView mediaArt;
- @InjectView(R.id.poster) ImageView mediaPoster;
-
- @InjectView(R.id.media_title) TextView mediaTitle;
- @InjectView(R.id.media_undertitle) TextView mediaUndertitle;
-
- @InjectView(R.id.rating) TextView mediaRating;
- @InjectView(R.id.max_rating) TextView mediaMaxRating;
- @InjectView(R.id.year) TextView mediaYear;
- @InjectView(R.id.genres) TextView mediaGenres;
- @InjectView(R.id.rating_votes) TextView mediaRatingVotes;
-
- @InjectView(R.id.media_description) TextView mediaDescription;
- @InjectView(R.id.directors) TextView mediaDirectors;
- @InjectView(R.id.cast_list) GridLayout videoCastList;
-
- /**
- * Create a new instance of this, initialized to show the movie movieId
- */
- @TargetApi(21)
- public static MovieDetailsFragment newInstance(MovieListFragment.ViewHolder vh) {
- MovieDetailsFragment fragment = new MovieDetailsFragment();
-
- Bundle args = new Bundle();
- args.putInt(BUNDLE_KEY_MOVIEID, vh.movieId);
- args.putString(BUNDLE_KEY_MOVIETITLE, vh.movieTitle);
- args.putString(BUNDLE_KEY_MOVIEPLOT, vh.movieTagline);
- args.putString(BUNDLE_KEY_MOVIEGENRES, vh.movieGenres);
- args.putInt(BUNDLE_KEY_MOVIEYEAR, vh.movieYear);
- args.putInt(BUNDLE_KEY_MOVIERUNTIME, vh.movieRuntime);
- args.putDouble(BUNDLE_KEY_MOVIERATING, vh.movieRating);
- if( Utils.isLollipopOrLater()) {
- args.putString(POSTER_TRANS_NAME, vh.artView.getTransitionName());
- }
-
- fragment.setArguments(args);
- return fragment;
- }
-
- @TargetApi(21)
- @Override
- protected View createView(LayoutInflater inflater, ViewGroup container) {
- Bundle bundle = getArguments();
- movieId = bundle.getInt(BUNDLE_KEY_MOVIEID, -1);
-
- if (movieId == -1) {
- // There's nothing to show
- return null;
- }
-
- ViewGroup root = (ViewGroup) inflater.inflate(R.layout.fragment_movie_details, container, false);
- ButterKnife.inject(this, root);
-
- //UIUtils.setSwipeRefreshLayoutColorScheme(swipeRefreshLayout);
-
- // Setup dim the fanart when scroll changes. Full dim on 4 * iconSize dp
- Resources resources = getActivity().getResources();
- final int pixelsToTransparent = 4 * resources.getDimensionPixelSize(R.dimen.default_icon_size);
- mediaPanel.getViewTreeObserver().addOnScrollChangedListener(new ViewTreeObserver.OnScrollChangedListener() {
- @Override
- public void onScrollChanged() {
- float y = mediaPanel.getScrollY();
- float newAlpha = Math.min(1, Math.max(0, 1 - (y / pixelsToTransparent)));
- mediaArt.setAlpha(newAlpha);
- }
- });
-
- FloatingActionButton fab = (FloatingActionButton)fabButton;
- fab.attachToScrollView((ObservableScrollView) mediaPanel);
-
- if(Utils.isLollipopOrLater()) {
- mediaPoster.setTransitionName(getArguments().getString(POSTER_TRANS_NAME));
- }
-
- mediaTitle.setText(bundle.getString(BUNDLE_KEY_MOVIETITLE));
- mediaUndertitle.setText(bundle.getString(BUNDLE_KEY_MOVIEPLOT));
- mediaGenres.setText(bundle.getString(BUNDLE_KEY_MOVIEGENRES));
- setMediaYear(bundle.getInt(BUNDLE_KEY_MOVIERUNTIME), bundle.getInt(BUNDLE_KEY_MOVIEYEAR));
- setMediaRating(bundle.getDouble(BUNDLE_KEY_MOVIERATING));
-
- // Pad main content view to overlap with bottom system bar
-// UIUtils.setPaddingForSystemBars(getActivity(), mediaPanel, false, false, true);
-// mediaPanel.setClipToPadding(false);
-
- return root;
- }
-
- @Override
- protected String getSyncType() {
- return LibrarySyncService.SYNC_SINGLE_MOVIE;
- }
-
- @Override
- protected String getSyncID() {
- return LibrarySyncService.SYNC_MOVIEID;
- }
-
- @Override
- protected int getSyncItemID() {
- return movieId;
- }
-
- @Override
- protected SwipeRefreshLayout getSwipeRefreshLayout() {
- return swipeRefreshLayout;
- }
-
- @Override
- public void onActivityCreated (Bundle savedInstanceState) {
- super.onActivityCreated(savedInstanceState);
-
- hasIssuedOutdatedRefresh = false;
-
- // Start the loaders
- getLoaderManager().initLoader(LOADER_MOVIE, null, this);
- getLoaderManager().initLoader(LOADER_CAST, null, this);
- }
-
- @Override
- public void onResume() {
- // Force the exit view to invisible
- exitTransitionView.setVisibility(View.INVISIBLE);
- //As we make mediaPoster invisible in onStop() we need to make it visible here.
- mediaPoster.setVisibility(View.VISIBLE);
- super.onResume();
- }
-
- @Override
- public void onStop() {
- //For some reason poster is included in the bottom slide animation, by making it invisible it is not noticeable for the user
- mediaPoster.setVisibility(View.INVISIBLE);
- super.onStop();
- }
-
- @Override
- protected void onSyncProcessEnded(MediaSyncEvent event) {
- if (event.status == MediaSyncEvent.STATUS_SUCCESS) {
- getLoaderManager().restartLoader(LOADER_MOVIE, null, this);
- getLoaderManager().restartLoader(LOADER_CAST, null, this);
- }
- }
-
- /**
- * Loader callbacks
- */
- /** {@inheritDoc} */
- @Override
- public Loader onCreateLoader(int i, Bundle bundle) {
- Uri uri;
- switch (i) {
- case LOADER_MOVIE:
- uri = MediaContract.Movies.buildMovieUri(getHostInfo().getId(), movieId);
- return new CursorLoader(getActivity(), uri,
- MovieDetailsQuery.PROJECTION, null, null, null);
- case LOADER_CAST:
- uri = MediaContract.MovieCast.buildMovieCastListUri(getHostInfo().getId(), movieId);
- return new CursorLoader(getActivity(), uri,
- MovieCastListQuery.PROJECTION, null, null, MovieCastListQuery.SORT);
- default:
- return null;
- }
- }
-
- /** {@inheritDoc} */
- @Override
- public void onLoadFinished(Loader cursorLoader, Cursor cursor) {
- if (cursor != null && cursor.getCount() > 0) {
- switch (cursorLoader.getId()) {
- case LOADER_MOVIE:
- displayMovieDetails(cursor);
- checkOutdatedMovieDetails(cursor);
- break;
- case LOADER_CAST:
- displayCastList(cursor);
- break;
- }
- }
- }
-
- /** {@inheritDoc} */
- @Override
- public void onLoaderReset(Loader cursorLoader) {
- // Release loader's data
- }
-
- /**
- * Callbacks for button bar
- */
- @OnClick(R.id.fab)
- public void onFabClicked(View v) {
- PlaylistType.Item item = new PlaylistType.Item();
- item.movieid = movieId;
- Player.Open action = new Player.Open(item);
- action.execute(getHostManager().getConnection(), new ApiCallback() {
- @Override
- public void onSuccess(String result) {
- if (!isAdded()) return;
- // Check whether we should switch to the remote
- boolean switchToRemote = PreferenceManager
- .getDefaultSharedPreferences(getActivity())
- .getBoolean(Settings.KEY_PREF_SWITCH_TO_REMOTE_AFTER_MEDIA_START,
- Settings.DEFAULT_PREF_SWITCH_TO_REMOTE_AFTER_MEDIA_START);
- if (switchToRemote) {
- int cx = (fabButton.getLeft() + fabButton.getRight()) / 2;
- int cy = (fabButton.getTop() + fabButton.getBottom()) / 2;
- UIUtils.switchToRemoteWithAnimation(getActivity(), cx, cy, exitTransitionView);
- }
- }
-
- @Override
- public void onError(int errorCode, String description) {
- if (!isAdded()) return;
- // Got an error, show toast
- Toast.makeText(getActivity(), R.string.unable_to_connect_to_xbmc, Toast.LENGTH_SHORT)
- .show();
- }
- }, callbackHandler);
- }
-
- @OnClick(R.id.add_to_playlist)
- public void onAddToPlaylistClicked(View v) {
- Playlist.GetPlaylists getPlaylists = new Playlist.GetPlaylists();
-
- getPlaylists.execute(getHostManager().getConnection(), new ApiCallback>() {
- @Override
- public void onSuccess(ArrayList result) {
- if (!isAdded()) return;
- // Ok, loop through the playlists, looking for the video one
- int videoPlaylistId = -1;
- for (PlaylistType.GetPlaylistsReturnType playlist : result) {
- if (playlist.type.equals(PlaylistType.GetPlaylistsReturnType.VIDEO)) {
- videoPlaylistId = playlist.playlistid;
- break;
- }
- }
- // If found, add to playlist
- if (videoPlaylistId != -1) {
- PlaylistType.Item item = new PlaylistType.Item();
- item.movieid = movieId;
- Playlist.Add action = new Playlist.Add(videoPlaylistId, item);
- action.execute(getHostManager().getConnection(), new ApiCallback() {
- @Override
- public void onSuccess(String result) {
- if (!isAdded()) return;
- // Got an error, show toast
- Toast.makeText(getActivity(), R.string.item_added_to_playlist, Toast.LENGTH_SHORT)
- .show();
- }
-
- @Override
- public void onError(int errorCode, String description) {
- if (!isAdded()) return;
- // Got an error, show toast
- Toast.makeText(getActivity(), R.string.unable_to_connect_to_xbmc, Toast.LENGTH_SHORT)
- .show();
- }
- }, callbackHandler);
- } else {
- Toast.makeText(getActivity(), R.string.no_suitable_playlist, Toast.LENGTH_SHORT)
- .show();
- }
- }
-
- @Override
- public void onError(int errorCode, String description) {
- if (!isAdded()) return;
- // Got an error, show toast
- Toast.makeText(getActivity(), R.string.unable_to_connect_to_xbmc, Toast.LENGTH_SHORT)
- .show();
- }
- }, callbackHandler);
- }
-
- @OnClick(R.id.go_to_imdb)
- public void onImdbClicked(View v) {
- String imdbNumber = (String)v.getTag();
-
- if (imdbNumber != null) {
- Utils.openImdbForMovie(getActivity(), imdbNumber);
- }
- }
-
- @OnClick(R.id.seen)
- public void onSeenClicked(View v) {
- // Set the playcount
- Integer playcount = (Integer)v.getTag();
- int newPlaycount = (playcount > 0) ? 0 : 1;
-
- VideoLibrary.SetMovieDetails action =
- new VideoLibrary.SetMovieDetails(movieId, newPlaycount, null);
- action.execute(getHostManager().getConnection(), new ApiCallback() {
- @Override
- public void onSuccess(String result) {
- if (!isAdded()) return;
- // Force a refresh, but don't show a message
- startSync(true);
- }
-
- @Override
- public void onError(int errorCode, String description) { }
- }, callbackHandler);
-
- // Change the button, to provide imeddiate feedback, even if it isn't yet stored in the db
- // (will be properly updated and refreshed after the refresh callback ends)
- setupSeenButton(newPlaycount);
- }
-
- @OnClick(R.id.local_play)
- public void onLocalPlayClicked(View v) {
- if (movieDownloadInfo == null) {
- // Nothing to play on local
- Toast.makeText(getActivity(), R.string.no_files_to_play, Toast.LENGTH_SHORT).show();
- return;
- }
-
- String videoUrl = movieDownloadInfo.getMediaUrl(getHostInfo());
- Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(videoUrl));
- intent.setDataAndType(Uri.parse(videoUrl), "video/*");
- startActivity(intent);
- }
-
- @Override
- protected void onDownload() {
- if (movieDownloadInfo == null) {
- // Nothing to download
- Toast.makeText(getActivity(), R.string.no_files_to_download, Toast.LENGTH_SHORT).show();
- return;
- }
-
- DialogInterface.OnClickListener noopClickListener =
- new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) { }
- };
-
- // Check if the directory exists and whether to overwrite it
- File file = new File(movieDownloadInfo.getAbsoluteFilePath());
- if (file.exists()) {
- AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
- builder.setTitle(R.string.download)
- .setMessage(R.string.download_file_exists)
- .setPositiveButton(R.string.overwrite,
- new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- FileDownloadHelper.downloadFiles(getActivity(), getHostInfo(),
- movieDownloadInfo, FileDownloadHelper.OVERWRITE_FILES,
- callbackHandler);
- }
- })
- .setNeutralButton(R.string.download_with_new_name,
- new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- FileDownloadHelper.downloadFiles(getActivity(), getHostInfo(),
- movieDownloadInfo, FileDownloadHelper.DOWNLOAD_WITH_NEW_NAME,
- callbackHandler);
- }
- })
- .setNegativeButton(android.R.string.cancel, noopClickListener)
- .show();
- } else {
- // Confirm that the user really wants to download the file
- AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
- builder.setTitle(R.string.download)
- .setMessage(R.string.confirm_movie_download)
- .setPositiveButton(android.R.string.ok,
- new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- FileDownloadHelper.downloadFiles(getActivity(), getHostInfo(),
- movieDownloadInfo, FileDownloadHelper.OVERWRITE_FILES,
- callbackHandler);
- }
- })
- .setNegativeButton(android.R.string.cancel, noopClickListener)
- .show();
- }
- }
-
- /**
- * Display the movie details
- *
- * @param cursor Cursor with the data
- */
- private void displayMovieDetails(Cursor cursor) {
- LogUtils.LOGD(TAG, "Refreshing movie details");
- cursor.moveToFirst();
- movieTitle = cursor.getString(MovieDetailsQuery.TITLE);
- mediaTitle.setText(movieTitle);
- mediaUndertitle.setText(cursor.getString(MovieDetailsQuery.TAGLINE));
-
- setMediaYear(cursor.getInt(MovieDetailsQuery.RUNTIME) / 60, cursor.getInt(MovieDetailsQuery.YEAR));
-
- mediaGenres.setText(cursor.getString(MovieDetailsQuery.GENRES));
-
- double rating = cursor.getDouble(MovieDetailsQuery.RATING);
- if (rating > 0) {
- mediaRating.setVisibility(View.VISIBLE);
- mediaMaxRating.setVisibility(View.VISIBLE);
- mediaRatingVotes.setVisibility(View.VISIBLE);
- setMediaRating(rating);
- String votes = cursor.getString(MovieDetailsQuery.VOTES);
- mediaRatingVotes.setText((TextUtils.isEmpty(votes)) ?
- "" : String.format(getString(R.string.votes), votes));
- } else {
- mediaRating.setVisibility(View.INVISIBLE);
- mediaMaxRating.setVisibility(View.INVISIBLE);
- mediaRatingVotes.setVisibility(View.INVISIBLE);
- }
-
- mediaDescription.setText(cursor.getString(MovieDetailsQuery.PLOT));
- mediaDirectors.setText(cursor.getString(MovieDetailsQuery.DIRECTOR));
-
- // IMDB button
- imdbButton.setTag(cursor.getString(MovieDetailsQuery.IMDBNUMBER));
-
- setupSeenButton(cursor.getInt(MovieDetailsQuery.PLAYCOUNT));
-
- // Images
- Resources resources = getActivity().getResources();
- DisplayMetrics displayMetrics = new DisplayMetrics();
- getActivity().getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
-
- int posterWidth = resources.getDimensionPixelOffset(R.dimen.now_playing_poster_width);
- int posterHeight = resources.getDimensionPixelOffset(R.dimen.now_playing_poster_height);
- UIUtils.loadImageWithCharacterAvatar(getActivity(), getHostManager(),
- cursor.getString(MovieDetailsQuery.THUMBNAIL), movieTitle,
- mediaPoster, posterWidth, posterHeight);
- int artHeight = resources.getDimensionPixelOffset(R.dimen.now_playing_art_height);
- UIUtils.loadImageIntoImageview(getHostManager(),
- cursor.getString(MovieDetailsQuery.FANART),
- mediaArt, displayMetrics.widthPixels, artHeight);
-
- // Setup movie download info
- movieDownloadInfo = new FileDownloadHelper.MovieInfo(
- movieTitle, cursor.getString(MovieDetailsQuery.FILE));
-
- // Check if downloaded file exists
- if (movieDownloadInfo.downloadFileExists()) {
- Resources.Theme theme = getActivity().getTheme();
- TypedArray styledAttributes = theme.obtainStyledAttributes(new int[]{
- R.attr.colorAccent});
- downloadButton.setColorFilter(
- styledAttributes.getColor(0,
- getActivity().getResources().getColor(R.color.accent_default)));
- styledAttributes.recycle();
- } else {
- downloadButton.clearColorFilter();
- }
- }
-
- private void setMediaRating(double rating) {
- mediaRating.setText(String.format("%01.01f", rating));
- mediaMaxRating.setText(getString(R.string.max_rating_video));
- }
-
- private void setMediaYear(int runtime, int year) {
- String durationYear = runtime > 0 ?
- String.format(getString(R.string.minutes_abbrev), String.valueOf(runtime)) +
- " | " + year :
- String.valueOf(year);
- mediaYear.setText(durationYear);
- }
-
- private void setupSeenButton(int playcount) {
- // Seen button
- if (playcount > 0) {
- Resources.Theme theme = getActivity().getTheme();
- TypedArray styledAttributes = theme.obtainStyledAttributes(new int[] {
- R.attr.colorAccent});
- seenButton.setColorFilter(styledAttributes.getColor(0,
- getActivity().getResources().getColor(R.color.accent_default)));
- styledAttributes.recycle();
- } else {
- seenButton.clearColorFilter();
- }
- // Save the playcount
- seenButton.setTag(playcount);
- }
-
- /**
- * Display the cast details
- *
- * @param cursor Cursor with the data
- */
- private void displayCastList(Cursor cursor) {
- // Transform the cursor into a List
-
- if (cursor.moveToFirst()) {
- castArrayList = new ArrayList(cursor.getCount());
- do {
- castArrayList.add(new VideoType.Cast(cursor.getString(MovieCastListQuery.NAME),
- cursor.getInt(MovieCastListQuery.ORDER),
- cursor.getString(MovieCastListQuery.ROLE),
- cursor.getString(MovieCastListQuery.THUMBNAIL)));
- } while (cursor.moveToNext());
-
- UIUtils.setupCastInfo(getActivity(), castArrayList, videoCastList,
- AllCastActivity.buildLaunchIntent(getActivity(), movieTitle, castArrayList));
- }
- }
-
- /**
- * Checks wether we should refresh the movie details with the info on XBMC
- * The details will be updated if the last update is older than what is configured in the
- * settings
- *
- * @param cursor Cursor with the data
- */
- private void checkOutdatedMovieDetails(Cursor cursor) {
- if (hasIssuedOutdatedRefresh)
- return;
-
- cursor.moveToFirst();
- long lastUpdated = cursor.getLong(MovieDetailsQuery.UPDATED);
-
- if (System.currentTimeMillis() > lastUpdated + Settings.DB_UPDATE_INTERVAL) {
- // Trigger a silent refresh
- hasIssuedOutdatedRefresh = true;
- startSync(true);
- }
- }
-
- /**
- * Returns the shared element if visible
- * @return View if visible, null otherwise
- */
- public View getSharedElement() {
- if (UIUtils.isViewInBounds(mediaPanel, mediaPoster)) {
- return mediaPoster;
- }
-
- return null;
- }
-
- /**
- * Movie details query parameters.
- */
- private interface MovieDetailsQuery {
- String[] PROJECTION = {
- BaseColumns._ID,
- MediaContract.Movies.TITLE,
- MediaContract.Movies.TAGLINE,
- MediaContract.Movies.THUMBNAIL,
- MediaContract.Movies.FANART,
- MediaContract.Movies.YEAR,
- MediaContract.Movies.GENRES,
- MediaContract.Movies.RUNTIME,
- MediaContract.Movies.RATING,
- MediaContract.Movies.VOTES,
- MediaContract.Movies.PLOT,
- MediaContract.Movies.PLAYCOUNT,
- MediaContract.Movies.DIRECTOR,
- MediaContract.Movies.IMDBNUMBER,
- MediaContract.Movies.FILE,
- MediaContract.SyncColumns.UPDATED,
- };
-
- final int ID = 0;
- final int TITLE = 1;
- final int TAGLINE = 2;
- final int THUMBNAIL = 3;
- final int FANART = 4;
- final int YEAR = 5;
- final int GENRES = 6;
- final int RUNTIME = 7;
- final int RATING = 8;
- final int VOTES = 9;
- final int PLOT = 10;
- final int PLAYCOUNT = 11;
- final int DIRECTOR = 12;
- final int IMDBNUMBER = 13;
- final int FILE = 14;
- final int UPDATED = 15;
- }
-
- /**
- * Movie cast list query parameters.
- */
- public interface MovieCastListQuery {
- String[] PROJECTION = {
- BaseColumns._ID,
- MediaContract.MovieCast.NAME,
- MediaContract.MovieCast.ORDER,
- MediaContract.MovieCast.ROLE,
- MediaContract.MovieCast.THUMBNAIL,
- };
-
- String SORT = MediaContract.MovieCast.ORDER + " ASC";
-
- final int ID = 0;
- final int NAME = 1;
- final int ORDER = 2;
- final int ROLE = 3;
- final int THUMBNAIL = 4;
- }
-}
diff --git a/app/src/main/java/org/xbmc/kore/ui/animators/ChangeImageFadeAnimation.java b/app/src/main/java/org/xbmc/kore/ui/animators/ChangeImageFadeAnimation.java
new file mode 100644
index 000000000..ea98c5457
--- /dev/null
+++ b/app/src/main/java/org/xbmc/kore/ui/animators/ChangeImageFadeAnimation.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2017 Martijn Brekhof. 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 org.xbmc.kore.ui.animators;
+
+import android.animation.Animator;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.NonNull;
+import android.support.design.widget.FloatingActionButton;
+
+import org.xbmc.kore.utils.LogUtils;
+
+public class ChangeImageFadeAnimation {
+
+ private Drawable fadeOutImage;
+ private Drawable fadeInImage;
+ private Drawable animatedImage;
+ private FloatingActionButton imageHolder;
+
+ private ValueAnimator fadeOutAnimator;
+
+ private ChangeImageFadeAnimation() {
+
+ }
+
+ public ChangeImageFadeAnimation(@NonNull FloatingActionButton imageHolder,
+ @NonNull Drawable fadeOutImage, @NonNull Drawable fadeInImage) {
+ this.fadeOutImage = fadeOutImage.getConstantState().newDrawable();
+ this.fadeOutImage.mutate();
+ this.fadeInImage = fadeInImage.getConstantState().newDrawable();
+ this.fadeInImage.mutate();
+
+ this.imageHolder = imageHolder;
+ setupAnimation();
+ }
+
+ public void cancel() {
+ fadeOutAnimator.cancel();
+ }
+
+ public void start() {
+ fadeOutAnimator.start();
+ }
+
+ private void setupAnimation() {
+ fadeOutAnimator = new ValueAnimator();
+ fadeOutAnimator.setIntValues(255, 0);
+ fadeOutAnimator.setDuration(500);
+ final ValueAnimator fadeInAnimator = new ValueAnimator();
+ fadeInAnimator.setIntValues(0, 255);
+ fadeInAnimator.setDuration(500);
+ animatedImage = fadeOutImage;
+
+ ValueAnimator.AnimatorUpdateListener updateListener = new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ animatedImage.setAlpha((int) animation.getAnimatedValue());
+ }
+ };
+ fadeInAnimator.addUpdateListener(updateListener);
+ fadeOutAnimator.addUpdateListener(updateListener);
+
+ fadeOutAnimator.addListener(new Animator.AnimatorListener() {
+ @Override
+ public void onAnimationStart(Animator animator) {
+
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ animatedImage = fadeInImage;
+ animatedImage.setAlpha(0);
+ imageHolder.setImageDrawable(animatedImage);
+ fadeInAnimator.start();
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animator) {
+
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animator) {
+
+ }
+ });
+ }
+}
diff --git a/app/src/main/java/org/xbmc/kore/ui/animators/PulsateAnimation.java b/app/src/main/java/org/xbmc/kore/ui/animators/PulsateAnimation.java
new file mode 100644
index 000000000..afcf722fa
--- /dev/null
+++ b/app/src/main/java/org/xbmc/kore/ui/animators/PulsateAnimation.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2017 Martijn Brekhof. 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 org.xbmc.kore.ui.animators;
+
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.ArgbEvaluator;
+import android.animation.ValueAnimator;
+import android.graphics.PorterDuff;
+import android.view.View;
+
+public class PulsateAnimation {
+
+ private View view;
+ private AnimatorSet animatorSet;
+ private boolean stopAnimation;
+ private int startColor;
+ private int endColor;
+
+ private PulsateAnimation() {
+
+ }
+
+ public PulsateAnimation(View v, int startColor, int endColor) {
+ view = v;
+ this.startColor = startColor;
+ this.endColor = endColor;
+
+ setupAnimation();
+ }
+
+ public void start() {
+ stopAnimation = false;
+ animatorSet.start();
+ }
+
+ public void stop() {
+ stopAnimation = true;
+ }
+
+ public boolean isRunning() {
+ return animatorSet.isRunning();
+ }
+
+ private void setupAnimation() {
+ animatorSet = new AnimatorSet();
+
+ //Creates an animation that first changes color from startColor to endColor and
+ //afterwards changes color from endColor to startColor
+ animatorSet.playSequentially(createValueAnimator(startColor, endColor),
+ createValueAnimator(endColor, startColor));
+
+ animatorSet.addListener(new Animator.AnimatorListener() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (!stopAnimation)
+ animatorSet.start();
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {
+
+ }
+ });
+ }
+
+ private ValueAnimator createValueAnimator(int startColor, int endColor) {
+ ValueAnimator valueAnimator = new ValueAnimator();
+ valueAnimator.setDuration(1000);
+ valueAnimator.setIntValues(startColor, endColor);
+ valueAnimator.setEvaluator(new ArgbEvaluator());
+ valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animator) {
+ int color = (int) animator.getAnimatedValue();
+ view.getBackground().setColorFilter(color, PorterDuff.Mode.SRC_IN);
+ }
+ });
+ return valueAnimator;
+ }
+}
diff --git a/app/src/main/java/org/xbmc/kore/ui/behaviors/FABSpeedDialBehavior.java b/app/src/main/java/org/xbmc/kore/ui/behaviors/FABSpeedDialBehavior.java
new file mode 100644
index 000000000..7fbd6b617
--- /dev/null
+++ b/app/src/main/java/org/xbmc/kore/ui/behaviors/FABSpeedDialBehavior.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2017 Martijn Brekhof. 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 org.xbmc.kore.ui.behaviors;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.support.design.widget.CoordinatorLayout;
+import android.support.v4.view.ViewCompat;
+import android.util.AttributeSet;
+import android.view.View;
+
+public class FABSpeedDialBehavior extends CoordinatorLayout.Behavior {
+
+ private boolean hide;
+
+ public FABSpeedDialBehavior(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View directTargetChild, @NonNull View target, int axes, int type) {
+ //Make sure we respond to vertical scroll events
+ return axes == ViewCompat.SCROLL_AXIS_VERTICAL ||
+ super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target,
+ axes, type);
+ }
+
+ @Override
+ public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull final View child, @NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
+ super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type);
+
+ if (dyConsumed > 0 && !hide) {
+ hide = true;
+ ViewCompat.animate(child).translationY(child.getHeight());
+ } else if (dyConsumed < 0 && hide) {
+ hide = false;
+ ViewCompat.animate(child).translationY(0);
+ }
+ }
+}
diff --git a/app/src/main/java/org/xbmc/kore/ui/behaviors/FadeOutOnVerticalScrollBehavior.java b/app/src/main/java/org/xbmc/kore/ui/behaviors/FadeOutOnVerticalScrollBehavior.java
new file mode 100644
index 000000000..194a8726f
--- /dev/null
+++ b/app/src/main/java/org/xbmc/kore/ui/behaviors/FadeOutOnVerticalScrollBehavior.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2017 Martijn Brekhof. 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 org.xbmc.kore.ui.behaviors;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.support.design.widget.CoordinatorLayout;
+import android.support.v4.view.ViewCompat;
+import android.util.AttributeSet;
+import android.view.View;
+
+public class FadeOutOnVerticalScrollBehavior extends CoordinatorLayout.Behavior {
+
+ private int maxScroll = 0;
+ private int currentScroll;
+
+ public FadeOutOnVerticalScrollBehavior(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View directTargetChild, @NonNull View target, int axes, int type) {
+ if ( axes == ViewCompat.SCROLL_AXIS_VERTICAL ) {
+ if (maxScroll == 0)
+ maxScroll = child.getHeight();
+
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
+ currentScroll += dyConsumed;
+ child.setAlpha((float) ((maxScroll - currentScroll) / (double) maxScroll));
+ }
+}
diff --git a/app/src/main/java/org/xbmc/kore/ui/sections/addon/AddonInfoFragment.java b/app/src/main/java/org/xbmc/kore/ui/sections/addon/AddonInfoFragment.java
index 07f0b3ffa..f80192be5 100644
--- a/app/src/main/java/org/xbmc/kore/ui/sections/addon/AddonInfoFragment.java
+++ b/app/src/main/java/org/xbmc/kore/ui/sections/addon/AddonInfoFragment.java
@@ -20,7 +20,6 @@
import android.os.Bundle;
import android.os.Handler;
import android.view.View;
-import android.widget.ImageButton;
import android.widget.Toast;
import org.xbmc.kore.R;
@@ -30,6 +29,7 @@
import org.xbmc.kore.ui.AbstractAdditionalInfoFragment;
import org.xbmc.kore.ui.AbstractInfoFragment;
import org.xbmc.kore.ui.generic.RefreshItem;
+import org.xbmc.kore.ui.widgets.fabspeeddial.FABSpeedDial;
import org.xbmc.kore.utils.LogUtils;
import java.util.Collections;
@@ -86,20 +86,23 @@ protected boolean setupMediaActionBar() {
}
@Override
- protected boolean setupFAB(ImageButton FAB) {
- FAB.setOnClickListener(new View.OnClickListener() {
+ protected boolean setupFAB(final FABSpeedDial FAB) {
+ FAB.setOnFabClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
+ FAB.enableBusyAnimation(true);
Addons.ExecuteAddon action = new Addons.ExecuteAddon(addonId);
action.execute(getHostManager().getConnection(), new ApiCallback() {
@Override
public void onSuccess(String result) {
- // Do nothing
+ if (!isAdded()) return;
+ FAB.enableBusyAnimation(false);
}
@Override
public void onError(int errorCode, String description) {
if (!isAdded()) return;
+ FAB.enableBusyAnimation(false);
// Got an error, show toast
Toast.makeText(getActivity(), R.string.unable_to_connect_to_xbmc, Toast.LENGTH_SHORT)
.show();
diff --git a/app/src/main/java/org/xbmc/kore/ui/sections/audio/AlbumInfoFragment.java b/app/src/main/java/org/xbmc/kore/ui/sections/audio/AlbumInfoFragment.java
index 2b1851c9f..a44f58812 100644
--- a/app/src/main/java/org/xbmc/kore/ui/sections/audio/AlbumInfoFragment.java
+++ b/app/src/main/java/org/xbmc/kore/ui/sections/audio/AlbumInfoFragment.java
@@ -26,7 +26,6 @@
import android.support.v4.content.Loader;
import android.text.TextUtils;
import android.view.View;
-import android.widget.ImageButton;
import android.widget.Toast;
import org.xbmc.kore.R;
@@ -40,6 +39,7 @@
import org.xbmc.kore.ui.AbstractAdditionalInfoFragment;
import org.xbmc.kore.ui.AbstractInfoFragment;
import org.xbmc.kore.ui.generic.RefreshItem;
+import org.xbmc.kore.ui.widgets.fabspeeddial.FABSpeedDial;
import org.xbmc.kore.utils.FileDownloadHelper;
import org.xbmc.kore.utils.LogUtils;
import org.xbmc.kore.utils.UIUtils;
@@ -172,13 +172,13 @@ public void onClick(View view) {
}
@Override
- protected boolean setupFAB(ImageButton FAB) {
- FAB.setOnClickListener(new View.OnClickListener() {
+ protected boolean setupFAB(FABSpeedDial FAB) {
+ FAB.setOnFabClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
PlaylistType.Item item = new PlaylistType.Item();
item.albumid = getDataHolder().getId();
- fabActionPlayItem(item);
+ playItemOnKodi(item);
}
});
return true;
diff --git a/app/src/main/java/org/xbmc/kore/ui/sections/audio/ArtistInfoFragment.java b/app/src/main/java/org/xbmc/kore/ui/sections/audio/ArtistInfoFragment.java
index fd77db4c8..cecdded72 100644
--- a/app/src/main/java/org/xbmc/kore/ui/sections/audio/ArtistInfoFragment.java
+++ b/app/src/main/java/org/xbmc/kore/ui/sections/audio/ArtistInfoFragment.java
@@ -26,7 +26,6 @@
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.view.View;
-import android.widget.ImageButton;
import org.xbmc.kore.jsonrpc.event.MediaSyncEvent;
import org.xbmc.kore.jsonrpc.type.PlaylistType;
@@ -34,10 +33,10 @@
import org.xbmc.kore.provider.MediaDatabase;
import org.xbmc.kore.provider.MediaProvider;
import org.xbmc.kore.service.library.LibrarySyncService;
-import org.xbmc.kore.service.library.SyncMusic;
import org.xbmc.kore.ui.AbstractAdditionalInfoFragment;
import org.xbmc.kore.ui.AbstractInfoFragment;
import org.xbmc.kore.ui.generic.RefreshItem;
+import org.xbmc.kore.ui.widgets.fabspeeddial.FABSpeedDial;
import org.xbmc.kore.utils.FileDownloadHelper;
import org.xbmc.kore.utils.LogUtils;
import org.xbmc.kore.utils.MediaPlayerUtils;
@@ -98,13 +97,13 @@ public void onClick(View view) {
}
@Override
- protected boolean setupFAB(ImageButton FAB) {
- FAB.setOnClickListener(new View.OnClickListener() {
+ protected boolean setupFAB(FABSpeedDial FAB) {
+ FAB.setOnFabClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
PlaylistType.Item item = new PlaylistType.Item();
item.artistid = getDataHolder().getId();
- fabActionPlayItem(item);
+ playItemOnKodi(item);
}
});
return true;
diff --git a/app/src/main/java/org/xbmc/kore/ui/sections/audio/MusicVideoInfoFragment.java b/app/src/main/java/org/xbmc/kore/ui/sections/audio/MusicVideoInfoFragment.java
index f5545dee3..2d74e0f3b 100644
--- a/app/src/main/java/org/xbmc/kore/ui/sections/audio/MusicVideoInfoFragment.java
+++ b/app/src/main/java/org/xbmc/kore/ui/sections/audio/MusicVideoInfoFragment.java
@@ -27,7 +27,6 @@
import android.support.v4.content.Loader;
import android.support.v7.app.AlertDialog;
import android.view.View;
-import android.widget.ImageButton;
import android.widget.Toast;
import org.xbmc.kore.R;
@@ -40,6 +39,7 @@
import org.xbmc.kore.ui.AbstractAdditionalInfoFragment;
import org.xbmc.kore.ui.AbstractInfoFragment;
import org.xbmc.kore.ui.generic.RefreshItem;
+import org.xbmc.kore.ui.widgets.fabspeeddial.FABSpeedDial;
import org.xbmc.kore.utils.FileDownloadHelper;
import org.xbmc.kore.utils.LogUtils;
import org.xbmc.kore.utils.UIUtils;
@@ -107,13 +107,13 @@ public void onClick(View view) {
}
@Override
- protected boolean setupFAB(ImageButton FAB) {
- FAB.setOnClickListener(new View.OnClickListener() {
+ protected boolean setupFAB(FABSpeedDial FAB) {
+ FAB.setOnFabClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
PlaylistType.Item item = new PlaylistType.Item();
item.musicvideoid = getDataHolder().getId();
- fabActionPlayItem(item);
+ playItemOnKodi(item);
}
});
return true;
diff --git a/app/src/main/java/org/xbmc/kore/ui/sections/video/MovieInfoFragment.java b/app/src/main/java/org/xbmc/kore/ui/sections/video/MovieInfoFragment.java
index 77458a84d..3554e970c 100644
--- a/app/src/main/java/org/xbmc/kore/ui/sections/video/MovieInfoFragment.java
+++ b/app/src/main/java/org/xbmc/kore/ui/sections/video/MovieInfoFragment.java
@@ -28,7 +28,6 @@
import android.support.v7.app.AlertDialog;
import android.text.TextUtils;
import android.view.View;
-import android.widget.ImageButton;
import android.widget.Toast;
import org.xbmc.kore.R;
@@ -44,6 +43,7 @@
import org.xbmc.kore.ui.AbstractInfoFragment;
import org.xbmc.kore.ui.generic.CastFragment;
import org.xbmc.kore.ui.generic.RefreshItem;
+import org.xbmc.kore.ui.widgets.fabspeeddial.FABSpeedDial;
import org.xbmc.kore.utils.FileDownloadHelper;
import org.xbmc.kore.utils.LogUtils;
import org.xbmc.kore.utils.Utils;
@@ -241,13 +241,18 @@ public void onError(int errorCode, String description) {
}
@Override
- protected boolean setupFAB(ImageButton FAB) {
- FAB.setOnClickListener(new View.OnClickListener() {
+ protected boolean setupFAB(final FABSpeedDial FAB) {
+ FAB.setOnDialItemClickListener(new FABSpeedDial.DialListener() {
@Override
- public void onClick(View v) {
+ public void onLocalPlayClicked() {
+ playItemLocally(movieDownloadInfo.getMediaUrl(getHostInfo()), "video/*");
+ }
+
+ @Override
+ public void onRemotePlayClicked() {
PlaylistType.Item item = new PlaylistType.Item();
item.movieid = getDataHolder().getId();
- fabActionPlayItem(item);
+ playItemOnKodi(item);
}
});
return true;
@@ -332,6 +337,8 @@ public void onLoadFinished(Loader cursorLoader, Cursor cursor) {
break;
}
}
+
+ getFabButton().enableLocalPlay(movieDownloadInfo != null);
}
/** {@inheritDoc} */
diff --git a/app/src/main/java/org/xbmc/kore/ui/sections/video/TVShowEpisodeInfoFragment.java b/app/src/main/java/org/xbmc/kore/ui/sections/video/TVShowEpisodeInfoFragment.java
index 1c8a429c4..dc028d451 100644
--- a/app/src/main/java/org/xbmc/kore/ui/sections/video/TVShowEpisodeInfoFragment.java
+++ b/app/src/main/java/org/xbmc/kore/ui/sections/video/TVShowEpisodeInfoFragment.java
@@ -28,7 +28,6 @@
import android.support.v7.app.AlertDialog;
import android.text.TextUtils;
import android.view.View;
-import android.widget.ImageButton;
import org.xbmc.kore.R;
import org.xbmc.kore.jsonrpc.ApiCallback;
@@ -40,6 +39,7 @@
import org.xbmc.kore.ui.AbstractAdditionalInfoFragment;
import org.xbmc.kore.ui.AbstractInfoFragment;
import org.xbmc.kore.ui.generic.RefreshItem;
+import org.xbmc.kore.ui.widgets.fabspeeddial.FABSpeedDial;
import org.xbmc.kore.utils.FileDownloadHelper;
import org.xbmc.kore.utils.LogUtils;
import org.xbmc.kore.utils.Utils;
@@ -134,13 +134,18 @@ public void onError(int errorCode, String description) { }
}
@Override
- protected boolean setupFAB(ImageButton FAB) {
- FAB.setOnClickListener(new View.OnClickListener() {
+ protected boolean setupFAB(FABSpeedDial FAB) {
+ FAB.setOnDialItemClickListener(new FABSpeedDial.DialListener() {
@Override
- public void onClick(View v) {
+ public void onLocalPlayClicked() {
+ playItemLocally(fileDownloadHelper.getMediaUrl(getHostInfo()), "video/*");
+ }
+
+ @Override
+ public void onRemotePlayClicked() {
PlaylistType.Item item = new PlaylistType.Item();
item.episodeid = getDataHolder().getId();
- fabActionPlayItem(item);
+ playItemOnKodi(item);
}
});
return true;
@@ -198,10 +203,10 @@ public void onLoadFinished(Loader cursorLoader, Cursor cursor) {
director = getActivity().getResources().getString(R.string.directors) + " " + director;
}
int runtime = cursor.getInt(EpisodeDetailsQuery.RUNTIME) / 60;
- String durationPremiered = runtime > 0 ?
- String.format(getString(R.string.minutes_abbrev), String.valueOf(runtime)) +
- " | " + cursor.getString(EpisodeDetailsQuery.FIRSTAIRED) :
- cursor.getString(EpisodeDetailsQuery.FIRSTAIRED);
+ String durationPremiered = runtime > 0 ?
+ String.format(getString(R.string.minutes_abbrev), String.valueOf(runtime)) +
+ " | " + cursor.getString(EpisodeDetailsQuery.FIRSTAIRED) :
+ cursor.getString(EpisodeDetailsQuery.FIRSTAIRED);
String season = String.format(getString(R.string.season_episode),
cursor.getInt(EpisodeDetailsQuery.SEASON),
cursor.getInt(EpisodeDetailsQuery.EPISODE));
@@ -228,6 +233,8 @@ public void onLoadFinished(Loader cursorLoader, Cursor cursor) {
break;
}
}
+
+ getFabButton().enableLocalPlay(fileDownloadHelper != null);
}
/** {@inheritDoc} */
@@ -237,13 +244,6 @@ public void onLoaderReset(Loader cursorLoader) {
}
private void downloadEpisode() {
- final FileDownloadHelper.TVShowInfo tvshowDownloadInfo = new FileDownloadHelper.TVShowInfo(
- cursor.getString(EpisodeDetailsQuery.SHOWTITLE),
- cursor.getInt(EpisodeDetailsQuery.SEASON),
- cursor.getInt(EpisodeDetailsQuery.EPISODE),
- cursor.getString(EpisodeDetailsQuery.TITLE),
- cursor.getString(EpisodeDetailsQuery.FILE));
-
DialogInterface.OnClickListener noopClickListener =
new DialogInterface.OnClickListener() {
@Override
@@ -251,7 +251,7 @@ public void onClick(DialogInterface dialog, int which) { }
};
// Check if the directory exists and whether to overwrite it
- File file = new File(tvshowDownloadInfo.getAbsoluteFilePath());
+ File file = new File(fileDownloadHelper.getAbsoluteFilePath());
if (file.exists()) {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle(R.string.download)
@@ -261,7 +261,7 @@ public void onClick(DialogInterface dialog, int which) { }
@Override
public void onClick(DialogInterface dialog, int which) {
FileDownloadHelper.downloadFiles(getActivity(), getHostInfo(),
- tvshowDownloadInfo, FileDownloadHelper.OVERWRITE_FILES,
+ fileDownloadHelper, FileDownloadHelper.OVERWRITE_FILES,
callbackHandler);
}
})
@@ -270,7 +270,7 @@ public void onClick(DialogInterface dialog, int which) {
@Override
public void onClick(DialogInterface dialog, int which) {
FileDownloadHelper.downloadFiles(getActivity(), getHostInfo(),
- tvshowDownloadInfo, FileDownloadHelper.DOWNLOAD_WITH_NEW_NAME,
+ fileDownloadHelper, FileDownloadHelper.DOWNLOAD_WITH_NEW_NAME,
callbackHandler);
}
})
@@ -286,7 +286,7 @@ public void onClick(DialogInterface dialog, int which) {
@Override
public void onClick(DialogInterface dialog, int which) {
FileDownloadHelper.downloadFiles(getActivity(), getHostInfo(),
- tvshowDownloadInfo, FileDownloadHelper.OVERWRITE_FILES,
+ fileDownloadHelper, FileDownloadHelper.OVERWRITE_FILES,
callbackHandler);
}
})
diff --git a/app/src/main/java/org/xbmc/kore/ui/sections/video/TVShowInfoFragment.java b/app/src/main/java/org/xbmc/kore/ui/sections/video/TVShowInfoFragment.java
index 9c811d801..222849979 100644
--- a/app/src/main/java/org/xbmc/kore/ui/sections/video/TVShowInfoFragment.java
+++ b/app/src/main/java/org/xbmc/kore/ui/sections/video/TVShowInfoFragment.java
@@ -22,7 +22,6 @@
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
-import android.widget.ImageButton;
import org.xbmc.kore.R;
import org.xbmc.kore.Settings;
@@ -32,6 +31,7 @@
import org.xbmc.kore.ui.AbstractAdditionalInfoFragment;
import org.xbmc.kore.ui.AbstractInfoFragment;
import org.xbmc.kore.ui.generic.RefreshItem;
+import org.xbmc.kore.ui.widgets.fabspeeddial.FABSpeedDial;
import org.xbmc.kore.utils.LogUtils;
/**
@@ -72,7 +72,7 @@ protected boolean setupMediaActionBar() {
}
@Override
- protected boolean setupFAB(ImageButton FAB) {
+ protected boolean setupFAB(FABSpeedDial FAB) {
return false;
}
diff --git a/app/src/main/java/org/xbmc/kore/ui/widgets/fabspeeddial/DialActionButton.java b/app/src/main/java/org/xbmc/kore/ui/widgets/fabspeeddial/DialActionButton.java
new file mode 100644
index 000000000..a0605c5c8
--- /dev/null
+++ b/app/src/main/java/org/xbmc/kore/ui/widgets/fabspeeddial/DialActionButton.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright 2017 Martijn Brekhof. 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 org.xbmc.kore.ui.widgets.fabspeeddial;
+
+import android.animation.Animator;
+import android.animation.TimeInterpolator;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Interpolator;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
+import android.support.design.widget.FloatingActionButton;
+import android.support.v7.content.res.AppCompatResources;
+import android.support.v7.widget.AppCompatTextView;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.LinearLayout;
+
+import org.xbmc.kore.R;
+import org.xbmc.kore.utils.Utils;
+
+import butterknife.ButterKnife;
+import butterknife.InjectView;
+
+public class DialActionButton extends LinearLayout {
+ @InjectView(R.id.dial_label) AppCompatTextView label;
+ @InjectView(R.id.dial_action_button) FloatingActionButton button;
+
+ private View anchorView;
+ private boolean isHiding;
+ private TimeInterpolator showInterpolator;
+ private TimeInterpolator hideInterpolator;
+
+ public DialActionButton(Context context) {
+ this(context, null, 0);
+ }
+
+ public DialActionButton(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public DialActionButton(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+
+ initializeView(context, attrs, defStyleAttr);
+ }
+
+ public void setShowInterpolator(TimeInterpolator showInterpolator) {
+ this.showInterpolator = showInterpolator;
+ }
+
+ public void setHideInterpolator(TimeInterpolator hideInterpolator) {
+ this.hideInterpolator = hideInterpolator;
+ }
+
+ /**
+ * Sets the View from which the DialActionButtons should appear or disappear.
+ * It uses the anchorView's animation duration to set the duration for
+ * the DialActionButton.
+ *
+ * Use {@link #setShowInterpolator(TimeInterpolator)} and
+ * {@link #setHideInterpolator(TimeInterpolator)} to set the appropriate interpolators
+ * for this DialActionButton
+ * @param anchorView
+ */
+ public void setAnchorView(View anchorView) {
+ this.anchorView = anchorView;
+
+ //Initialize animation
+ long anim_duration = anchorView.animate().getDuration();
+
+ label.setAlpha(0f);
+ label.animate().setDuration(anim_duration);
+ label.setScaleX(0f);
+ label.setScaleY(0f);
+
+ animate().setDuration(anim_duration);
+ animate().setListener(new Animator.AnimatorListener() {
+ @Override
+ public void onAnimationStart(Animator animator) {
+
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ if (isHiding) {
+ setVisibility(View.INVISIBLE);
+ }
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animator) {
+
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animator) {
+
+ }
+ });
+ }
+
+ public void show() {
+ isHiding = false;
+
+ setVisibility(View.VISIBLE);
+
+ if (anchorView != null) {
+ setY(anchorView.getY());
+ animate().translationY(0);
+ animate().setInterpolator(showInterpolator);
+
+ label.animate().setInterpolator(showInterpolator);
+ label.setX(anchorView.getX());
+ label.animate().translationX(0);
+ label.animate().alpha(1f);
+ label.animate().scaleX(1f);
+ label.animate().scaleY(1f);
+ }
+ }
+
+ public void hide() {
+ if (isHiding)
+ return;
+
+ if (anchorView == null) {
+ setVisibility(View.GONE);
+ } else {
+ isHiding = true;
+ animate().setInterpolator(hideInterpolator);
+ animate().translationY(anchorView.getY() - getY());
+
+ label.animate().setInterpolator(hideInterpolator);
+ label.animate().translationX(anchorView.getX() - label.getX());
+ label.animate().alpha(0f);
+ label.animate().scaleX(0f);
+ label.animate().scaleY(0f);
+ }
+ }
+
+ public Drawable getDrawable() {
+ return button.getDrawable();
+ }
+
+ public AppCompatTextView getLabel() {
+ return label;
+ }
+
+ public void setColorFilter(int color) {
+ button.getBackground().setColorFilter(color, PorterDuff.Mode.SRC_IN);
+ label.getBackground().setColorFilter(color, PorterDuff.Mode.SRC_IN);
+ }
+
+ private void initializeView(Context context, AttributeSet attrs, int defStyleAttr) {
+ LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View view = inflater.inflate(R.layout.dial_action_button, this);
+ ButterKnife.inject(view);
+
+ // Make sure shadow is not clipped
+ setClipToPadding(false);
+
+ // Make sure translation animations do not cause clipping
+ // by parent view group when moving outside its boundaries.
+ // For example, when using the overshoot interpolator.
+ setClipChildren(false);
+
+ Resources.Theme theme = getContext().getTheme();
+ TypedArray typedArray = theme.obtainStyledAttributes(attrs, new int[]{android.R.attr.text,
+ R.attr.iconFABDial},
+ defStyleAttr,
+ 0);
+ String text = typedArray.getString(0);
+
+ if (text != null) {
+ label.setText(text);
+ } else {
+ label.setVisibility(View.GONE);
+ }
+
+ TypedValue typedValue = new TypedValue();
+ typedArray.getValue(1, typedValue);
+ button.setImageResource(typedValue.resourceId);
+
+ typedArray.recycle();
+
+ ColorStateList colorStateList = AppCompatResources.getColorStateList(context, R.color.fabspeeddial);
+ button.setBackgroundTintList(colorStateList);
+ }
+}
diff --git a/app/src/main/java/org/xbmc/kore/ui/widgets/fabspeeddial/FABSpeedDial.java b/app/src/main/java/org/xbmc/kore/ui/widgets/fabspeeddial/FABSpeedDial.java
new file mode 100644
index 000000000..07d289a3e
--- /dev/null
+++ b/app/src/main/java/org/xbmc/kore/ui/widgets/fabspeeddial/FABSpeedDial.java
@@ -0,0 +1,362 @@
+/*
+ * Copyright 2017 Martijn Brekhof. 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 org.xbmc.kore.ui.widgets.fabspeeddial;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.preference.PreferenceManager;
+import android.support.annotation.Nullable;
+import android.support.design.widget.FloatingActionButton;
+import android.support.v7.content.res.AppCompatResources;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.OvershootInterpolator;
+import android.widget.LinearLayout;
+
+import org.xbmc.kore.R;
+import org.xbmc.kore.Settings;
+import org.xbmc.kore.ui.animators.ChangeImageFadeAnimation;
+import org.xbmc.kore.ui.animators.PulsateAnimation;
+
+import butterknife.ButterKnife;
+import butterknife.InjectView;
+
+/**
+ * The Floating Action Button Speed Dial uses a {@link FloatingActionButton} and can
+ * optionally show a speed dial menu. To enable the speed dials add a listener
+ * for the dials using {@link #setOnDialItemClickListener(DialListener)}.
+ *
+ * The icons for the FAB needs to be set through your theme:
+ *
+ * - org.xbmc.kore.R.attr.iconFABDefault sets the icon when the dials are disabled
+ * - org.xbmc.kore.R.attr.iconFABDialsOpenClose sets the icon when the dials are enabled
+ *
+ *
+ *
+ *
+ * The background color can be set through your theme:
+ *
+ * - org.xbmc.kore.R.attr.fabColorNormal sets the default color
+ * - org.xbmc.kore.R.attr.fabColorPressed sets the pressed state color
+ * - org.xbmc.kore.R.attr.fabColorFocus sets the focus state color
+ *
+ *
+ */
+public class FABSpeedDial extends LinearLayout {
+ @InjectView(R.id.fabspeeddial) FloatingActionButton FABMain;
+ @InjectView(R.id.play_local) DialActionButton FABPlayLocal;
+ @InjectView(R.id.play_remote) DialActionButton FABPlayRemote;
+
+ private final String BUNDLE_KEY_EXPANDED = "expanded";
+ private final String BUNDLE_KEY_PARENT = "parent";
+ private final String BUNDLE_KEY_DIALCLICKED = "dialclicked";
+
+ private PulsateAnimation busyAnimation;
+ private DialActionButton dialSelected;
+ private boolean dialsVisible;
+ private boolean dialsEnabled;
+
+ private Drawable iconFABDefault;
+ private Drawable iconFABOpenClose;
+
+ private OvershootInterpolator showDialsInterpolator = new OvershootInterpolator();
+ private AccelerateInterpolator hideDialsInterpolator = new AccelerateInterpolator();
+
+ public interface DialListener {
+ void onLocalPlayClicked();
+ void onRemotePlayClicked();
+ }
+
+ private DialListener dialListener;
+ private OnClickListener fabListener;
+
+ public FABSpeedDial(Context context) {
+ this(context, null);
+ }
+
+ public FABSpeedDial(Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public FABSpeedDial(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ initializeView(context);
+ }
+
+ @TargetApi(21)
+ public FABSpeedDial(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ initializeView(context);
+ }
+
+ /**
+ * Enables/disables the speed dials. This means that if enabled,
+ * the dials will be shown if the user pressed the FAB button.
+ * @param enable true to enable the dials, false to disable
+ * @param animate true to use animation to change FAB icon, false to instantly change the FAB icon
+ */
+ public void enableSpeedDials(boolean enable, boolean animate) {
+ if (dialsEnabled == enable)
+ return;
+
+ dialsEnabled = enable;
+
+ changeFABIcon(animate);
+ }
+
+ /**
+ * Add listener to handle dial button click events.
+ *
+ * Note: adding a listener for the dials also enables the speed dials if
+ * user didn't disable usage in settings
+ * @param dialListener
+ */
+ public void setOnDialItemClickListener(DialListener dialListener) {
+ this.dialListener = dialListener;
+
+ // Disable speed dials if user disabled it through settings
+ boolean disable = PreferenceManager
+ .getDefaultSharedPreferences(getContext())
+ .getBoolean(Settings.KEY_PREF_DISABLE_LOCAL_PLAY,
+ Settings.DEFAULT_PREF_DISABLE_LOCAL_PLAY);
+
+ enableSpeedDials(!disable, false);
+ }
+
+ /**
+ * Add listener to handle FAB click events.
+ *
+ * Note: if the speed dials are enabled this won't be called
+ * when the FAB button is pressed.
+ * @param fabListener
+ */
+ public void setOnFabClickListener(OnClickListener fabListener) {
+ this.fabListener = fabListener;
+ }
+
+ /**
+ * WARNING: Do not use this to set a listener for the FAB button.
+ * Use {@link #setOnFabClickListener(OnClickListener)}
+ * instead.
+ *
+ * {@inheritDoc}
+ * @param l
+ */
+ @Override
+ public void setOnClickListener(@Nullable OnClickListener l) {
+ super.setOnClickListener(l);
+ }
+
+ /**
+ * Enables/disables the FAB button and starts/stops the busy animation
+ * @param enable true to disable the FAB button and start the busy animation, false to enable
+ * the FAB button and stop the busy animation.
+ */
+ public void enableBusyAnimation(boolean enable) {
+ if (enable) {
+ busyAnimation.start();
+ if (dialSelected != null) {
+ changeFABIcon(FABMain.getDrawable(), dialSelected.getDrawable());
+ }
+ FABMain.setEnabled(false);
+ } else {
+ busyAnimation.stop();
+ if (dialSelected != null) {
+ changeFABIcon(true);
+ dialSelected = null;
+ }
+ FABMain.setEnabled(true);
+ }
+ }
+
+ public boolean busyAnimationIsEnabled() {
+ return busyAnimation.isRunning();
+ }
+
+ public void enableLocalPlay(boolean enable) {
+ FABPlayLocal.setEnabled(enable);
+ }
+
+ public void showDials(boolean show) {
+ dialsVisible = show;
+
+ if (show) {
+ FABMain.animate().setInterpolator(showDialsInterpolator);
+ FABMain.animate().rotation(-45f);
+ FABPlayLocal.show();
+ FABPlayRemote.show();
+ } else {
+ FABMain.animate().setInterpolator(hideDialsInterpolator);
+ FABMain.animate().rotation(0f);
+ FABPlayLocal.hide();
+ FABPlayRemote.hide();
+ }
+ }
+
+ @Nullable
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ Bundle bundle = new Bundle();
+ bundle.putParcelable(BUNDLE_KEY_PARENT, super.onSaveInstanceState());
+ bundle.putBoolean(BUNDLE_KEY_EXPANDED, dialsVisible);
+ if (dialSelected != null) {
+ bundle.putCharSequence(BUNDLE_KEY_DIALCLICKED, dialSelected.getLabel().getText());
+ }
+ return bundle;
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Parcelable state) {
+ if (state != null) {
+ Bundle bundle = (Bundle) state;
+
+ super.onRestoreInstanceState(bundle.getParcelable(BUNDLE_KEY_PARENT));
+ showDials(bundle.getBoolean(BUNDLE_KEY_EXPANDED));
+
+ CharSequence charSequence = bundle.getCharSequence(BUNDLE_KEY_DIALCLICKED);
+ if ((charSequence != null) && (! charSequence.equals(FABPlayLocal.getLabel().getText()))) {
+ dialSelected = FABPlayRemote;
+
+ enableBusyAnimation(true);
+ }
+ }
+ }
+
+ private void initializeView(Context context) {
+ LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View view = inflater.inflate(R.layout.fab_speed_dial, this);
+
+ ButterKnife.inject(view);
+
+ // Makes sure shadow is not clipped
+ setClipToPadding(false);
+
+ // Makes sure translation animations do not cause clipping
+ // by parent view group when moving outside its boundaries.
+ // For example, when using the overshoot interpolator.
+ setClipChildren(false);
+
+ setupListeners();
+
+ setupFABIcon(context);
+ setupDial(FABPlayLocal);
+ setupDial(FABPlayRemote);
+ }
+
+ private void setupFABIcon(Context context) {
+ TypedValue tv = new TypedValue();
+
+ context.getTheme().resolveAttribute(R.attr.iconFABDialsOpenClose, tv, false);
+ iconFABOpenClose = AppCompatResources.getDrawable(context, tv.data);
+ context.getTheme().resolveAttribute(R.attr.iconFABDefault, tv, false);
+ iconFABDefault = AppCompatResources.getDrawable(context, tv.data);
+
+ FABMain.setImageDrawable(dialsEnabled ? iconFABOpenClose : iconFABDefault);
+
+ ColorStateList colorStateList = AppCompatResources.getColorStateList(context, R.color.fabspeeddial);
+ int fabColorNormal = colorStateList.getColorForState(new int[] {android.R.attr.state_enabled},
+ R.attr.colorPrimaryDark);
+ int fabColorPressed = colorStateList.getColorForState(new int[] {android.R.attr.state_pressed},
+ R.attr.colorPrimary);
+
+ busyAnimation = new PulsateAnimation(FABMain, fabColorNormal, fabColorPressed);
+
+ FABMain.setBackgroundTintList(colorStateList);
+ }
+
+ private void setupDial(DialActionButton dialActionButton) {
+ dialActionButton.setAnchorView(FABMain);
+ dialActionButton.setShowInterpolator(showDialsInterpolator);
+ dialActionButton.setHideInterpolator(hideDialsInterpolator);
+ }
+
+ private void setupListeners() {
+ FABMain.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (dialsEnabled) {
+ showDials(!FABPlayLocal.isShown());
+ } else {
+ if (fabListener != null) {
+ fabListener.onClick(v);
+ } else if (dialListener != null) {
+ /**
+ * We take remote play as default and we try to fallback if dev misconfigured
+ * the FAB in {@link org.xbmc.kore.ui.AbstractInfoFragment#setupFAB(FABSpeedDial)}.
+ * This is also needed to support disabling local playback through settings.
+ */
+ dialListener.onRemotePlayClicked();
+ }
+ }
+ }
+ });
+
+ FABPlayLocal.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ dialSelected = FABPlayLocal;
+ if (dialListener != null) {
+ dialListener.onLocalPlayClicked();
+ showDials(false);
+ }
+ }
+ });
+
+
+ FABPlayRemote.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ dialSelected = FABPlayRemote;
+ if (dialListener != null) {
+ dialListener.onRemotePlayClicked();
+ showDials(false);
+ }
+ }
+ });
+ }
+
+ private ChangeImageFadeAnimation changeImageFadeAnimation;
+
+ private void changeFABIcon(final Drawable from, final Drawable to) {
+ // Cancel previous animation if any
+ if (changeImageFadeAnimation != null)
+ changeImageFadeAnimation.cancel();
+
+ changeImageFadeAnimation = new ChangeImageFadeAnimation(FABMain, from, to);
+ changeImageFadeAnimation.start();
+ }
+
+ /**
+ * Changes the FAB icon to its default value.
+ * @param animate true to use an animation to change the icon, false to change it instantly
+ */
+ private void changeFABIcon(boolean animate) {
+ Drawable drawable = dialsEnabled ? iconFABOpenClose : iconFABDefault;
+
+ if (animate) {
+ changeFABIcon(FABMain.getDrawable(), drawable);
+ } else {
+ FABMain.setImageDrawable(drawable);
+ }
+ }
+}
diff --git a/app/src/main/res/color/fabspeeddial.xml b/app/src/main/res/color/fabspeeddial.xml
new file mode 100644
index 000000000..18fbb6333
--- /dev/null
+++ b/app/src/main/res/color/fabspeeddial.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_cellphone_android_white_24dp.xml b/app/src/main/res/drawable/ic_cellphone_android_white_24dp.xml
new file mode 100644
index 000000000..1637dd452
--- /dev/null
+++ b/app/src/main/res/drawable/ic_cellphone_android_white_24dp.xml
@@ -0,0 +1,8 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_plus_white_24dp.xml b/app/src/main/res/drawable/ic_plus_white_24dp.xml
new file mode 100644
index 000000000..0c615ff63
--- /dev/null
+++ b/app/src/main/res/drawable/ic_plus_white_24dp.xml
@@ -0,0 +1,8 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/rounded_corners_shape.xml b/app/src/main/res/drawable/rounded_corners_shape.xml
new file mode 100644
index 000000000..b68c7083f
--- /dev/null
+++ b/app/src/main/res/drawable/rounded_corners_shape.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/dial_action_button.xml b/app/src/main/res/layout/dial_action_button.xml
new file mode 100644
index 000000000..a1232d5c3
--- /dev/null
+++ b/app/src/main/res/layout/dial_action_button.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fab_speed_dial.xml b/app/src/main/res/layout/fab_speed_dial.xml
new file mode 100644
index 000000000..1ce75d06c
--- /dev/null
+++ b/app/src/main/res/layout/fab_speed_dial.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_info.xml b/app/src/main/res/layout/fragment_info.xml
index 33553400a..5428578a5 100644
--- a/app/src/main/res/layout/fragment_info.xml
+++ b/app/src/main/res/layout/fragment_info.xml
@@ -15,11 +15,12 @@
limitations under the License.
-->
-
+ android:layout_height="match_parent"
+ android:clipChildren="false">
-
-
-
-
+
-
@@ -277,4 +268,4 @@
android:layout_height="match_parent"
android:background="?attr/fabColorNormal"
android:visibility="invisible"/>
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/src/main/res/values-v19/themes.xml b/app/src/main/res/values-v19/themes.xml
deleted file mode 100644
index 63b996b9f..000000000
--- a/app/src/main/res/values-v19/themes.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/values/attr.xml b/app/src/main/res/values/attr.xml
index 8f45760d5..0831d6913 100644
--- a/app/src/main/res/values/attr.xml
+++ b/app/src/main/res/values/attr.xml
@@ -31,6 +31,7 @@
+
@@ -50,6 +51,7 @@
+
@@ -114,7 +116,12 @@
+
+
+
+
-
\ No newline at end of file
+
+
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index be8effcd4..e83717fb2 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -23,6 +23,7 @@
@color/orange_A400
@color/orange_A700
+ @color/orange_A100
#ffffffff
#88ffffff
@@ -32,6 +33,7 @@
#8a000000
#0f85a5
+ #17cdff
#0a5b71
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index eb8a14be9..1a1928721 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -423,5 +423,8 @@
0:00
Expand/Collapse
+ Play Locally
+ Disable local playback support
+ Disables support for playing media locally on the device running Kore.
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index b2b08f8c9..9f0bb1ac7 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -14,7 +14,8 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-
+
+
+