From 412931b8db08ba40f871635cc4b6c4ba15331357 Mon Sep 17 00:00:00 2001 From: Martijn Brekhof Date: Fri, 30 Dec 2016 09:27:24 +0100 Subject: [PATCH] Refactored AbstractDetailsFragment This introduces the MVC model to details fragments. It moves as much view and control code out of the concrete fragments into the abstract classes. * Added UML class and sequence diagrams under doc/diagrams to clarify the new setup * Introduces new abstract classes * AbstractFragment class to hold the DataHolder * AbstractInfoFragment class to display media information * AbstractAddtionalInfoFragment class to allow *InfoFragments to add additional UI elements and propagate refresh requests. See for an example TVShowInfoFragment which adds TVShowProgressFragment to display next episodes and season progression. * Introduces new RefreshItem class to encapsulate all refresh functionality from AbstractDetailsFragment * Introduces new SharedElementTransition utility class to encapsulate all shared element transition code * Introduces new CastFragment class to encapsulate all code for displaying casts reducing code duplication * Introduces DataHolder class to replace passing the ViewHolder from the *ListFragment to the *DetailsFragment or *InfoFragment * Refactored AbstractDetailsFragment into two classes: o AbstractDetailsFragment: for fragments requiring a tab bar o AbstractInfoFragment: for fragments showing media information We used to use DetailsFragments for both fragments that show generic info about some media item and fragments that hold all details for some media item. For example, artist details showed artist info and used tabs to show artist albums and songs as well. Now Details fragments are used to show all details, Info fragments only show media item information like description, title, rating, etc. * Moved swiperefreshlayout code from AbstractCursorListFragment to AbstractListFragment --- .../ui/RestoreSearchQueryViewPagerTest.java | 4 +- .../ui/AbstractAdditionalInfoFragment.java | 21 + .../kore/ui/AbstractCursorListFragment.java | 50 +- .../xbmc/kore/ui/AbstractDetailsFragment.java | 321 -------- .../org/xbmc/kore/ui/AbstractFragment.java | 183 +++++ .../xbmc/kore/ui/AbstractInfoFragment.java | 597 ++++++++++++++ .../xbmc/kore/ui/AbstractListFragment.java | 20 +- .../xbmc/kore/ui/AbstractTabsFragment.java | 101 +++ .../xbmc/kore/ui/generic/CastFragment.java | 193 +++++ .../org/xbmc/kore/ui/generic/RefreshItem.java | 187 +++++ .../sections/addon/AddonDetailsFragment.java | 364 +-------- .../ui/sections/addon/AddonInfoFragment.java | 194 +++++ .../addon/AddonListContainerFragment.java | 90 +-- .../ui/sections/addon/AddonListFragment.java | 218 ++--- .../sections/addon/AddonOverviewFragment.java | 145 ---- .../ui/sections/addon/AddonsActivity.java | 104 +-- .../sections/audio/AlbumDetailsFragment.java | 677 ---------------- .../ui/sections/audio/AlbumInfoFragment.java | 266 +++++++ .../ui/sections/audio/AlbumListFragment.java | 86 +- .../audio/AlbumSongsListFragment.java | 418 ++++++++++ .../sections/audio/ArtistDetailsFragment.java | 98 +-- .../ui/sections/audio/ArtistInfoFragment.java | 255 ++++++ .../ui/sections/audio/ArtistListFragment.java | 44 +- .../audio/ArtistOverviewFragment.java | 405 ---------- .../kore/ui/sections/audio/MusicActivity.java | 199 ++--- .../audio/MusicVideoDetailsFragment.java | 519 ------------ .../audio/MusicVideoInfoFragment.java | 322 ++++++++ .../audio/MusicVideoListFragment.java | 69 +- .../ui/sections/audio/SongsListFragment.java | 162 +++- .../sections/file/MediaFileListFragment.java | 5 + .../ui/sections/video/AllCastActivity.java | 12 +- .../sections/video/MovieDetailsFragment.java | 705 ----------------- .../ui/sections/video/MovieInfoFragment.java | 411 ++++++++++ .../ui/sections/video/MovieListFragment.java | 100 ++- .../ui/sections/video/MoviesActivity.java | 98 +-- .../sections/video/TVShowDetailsFragment.java | 745 ------------------ .../video/TVShowEpisodeDetailsFragment.java | 600 -------------- .../video/TVShowEpisodeInfoFragment.java | 341 ++++++++ .../video/TVShowEpisodeListFragment.java | 34 +- .../ui/sections/video/TVShowInfoFragment.java | 210 +++++ .../ui/sections/video/TVShowListFragment.java | 90 +-- .../video/TVShowProgressFragment.java | 394 +++++++++ .../ui/sections/video/TVShowsActivity.java | 108 +-- .../kore/utils/SharedElementTransition.java | 118 +++ .../java/org/xbmc/kore/utils/UIUtils.java | 14 +- .../main/java/org/xbmc/kore/utils/Utils.java | 68 ++ .../res/layout/fragment_addon_details.xml | 196 ----- .../res/layout/fragment_album_details.xml | 241 ------ .../res/layout/fragment_artist_details.xml | 149 ---- app/src/main/res/layout/fragment_cast.xml | 39 + .../res/layout/fragment_episode_details.xml | 274 ------- ...nt_movie_details.xml => fragment_info.xml} | 234 +++--- .../layout/fragment_music_video_details.xml | 197 ----- .../layout/fragment_tvshow_episodes_list.xml | 45 -- .../res/layout/fragment_tvshow_overview.xml | 244 ------ .../res/layout/fragment_tvshow_progress.xml | 71 ++ app/src/main/res/layout/list_item_episode.xml | 2 + app/src/main/res/values-sw600dp/dimens.xml | 9 - app/src/main/res/values/dimens.xml | 15 +- app/src/main/res/values/strings.xml | 5 + app/src/main/res/values/styles.xml | 1 - doc/diagrams/audio.puml | 105 --- doc/diagrams/class/org.xbmc.kore.ui.puml | 180 +++++ doc/diagrams/org.xbmc.kore.ui.puml | 126 --- .../org.xbmc.kore.ui.abstractcursorlist.puml | 98 +++ .../org.xbmc.kore.ui.abstractinfo.puml | 113 +++ doc/mockups/detailsfragment.svg | 358 +++++++++ 67 files changed, 5914 insertions(+), 7153 deletions(-) create mode 100644 app/src/main/java/org/xbmc/kore/ui/AbstractAdditionalInfoFragment.java delete mode 100644 app/src/main/java/org/xbmc/kore/ui/AbstractDetailsFragment.java create mode 100644 app/src/main/java/org/xbmc/kore/ui/AbstractFragment.java create mode 100644 app/src/main/java/org/xbmc/kore/ui/AbstractInfoFragment.java create mode 100644 app/src/main/java/org/xbmc/kore/ui/AbstractTabsFragment.java create mode 100644 app/src/main/java/org/xbmc/kore/ui/generic/CastFragment.java create mode 100644 app/src/main/java/org/xbmc/kore/ui/generic/RefreshItem.java create mode 100644 app/src/main/java/org/xbmc/kore/ui/sections/addon/AddonInfoFragment.java delete mode 100644 app/src/main/java/org/xbmc/kore/ui/sections/addon/AddonOverviewFragment.java delete mode 100644 app/src/main/java/org/xbmc/kore/ui/sections/audio/AlbumDetailsFragment.java create mode 100644 app/src/main/java/org/xbmc/kore/ui/sections/audio/AlbumInfoFragment.java create mode 100644 app/src/main/java/org/xbmc/kore/ui/sections/audio/AlbumSongsListFragment.java create mode 100644 app/src/main/java/org/xbmc/kore/ui/sections/audio/ArtistInfoFragment.java delete mode 100644 app/src/main/java/org/xbmc/kore/ui/sections/audio/ArtistOverviewFragment.java delete mode 100644 app/src/main/java/org/xbmc/kore/ui/sections/audio/MusicVideoDetailsFragment.java create mode 100644 app/src/main/java/org/xbmc/kore/ui/sections/audio/MusicVideoInfoFragment.java delete mode 100644 app/src/main/java/org/xbmc/kore/ui/sections/video/MovieDetailsFragment.java create mode 100644 app/src/main/java/org/xbmc/kore/ui/sections/video/MovieInfoFragment.java delete mode 100644 app/src/main/java/org/xbmc/kore/ui/sections/video/TVShowDetailsFragment.java delete mode 100644 app/src/main/java/org/xbmc/kore/ui/sections/video/TVShowEpisodeDetailsFragment.java create mode 100644 app/src/main/java/org/xbmc/kore/ui/sections/video/TVShowEpisodeInfoFragment.java create mode 100644 app/src/main/java/org/xbmc/kore/ui/sections/video/TVShowInfoFragment.java create mode 100644 app/src/main/java/org/xbmc/kore/ui/sections/video/TVShowProgressFragment.java create mode 100644 app/src/main/java/org/xbmc/kore/utils/SharedElementTransition.java delete mode 100644 app/src/main/res/layout/fragment_addon_details.xml delete mode 100644 app/src/main/res/layout/fragment_album_details.xml delete mode 100644 app/src/main/res/layout/fragment_artist_details.xml create mode 100644 app/src/main/res/layout/fragment_cast.xml delete mode 100644 app/src/main/res/layout/fragment_episode_details.xml rename app/src/main/res/layout/{fragment_movie_details.xml => fragment_info.xml} (60%) delete mode 100644 app/src/main/res/layout/fragment_music_video_details.xml delete mode 100644 app/src/main/res/layout/fragment_tvshow_episodes_list.xml delete mode 100644 app/src/main/res/layout/fragment_tvshow_overview.xml create mode 100644 app/src/main/res/layout/fragment_tvshow_progress.xml delete mode 100644 doc/diagrams/audio.puml create mode 100644 doc/diagrams/class/org.xbmc.kore.ui.puml delete mode 100644 doc/diagrams/org.xbmc.kore.ui.puml create mode 100644 doc/diagrams/sequence/org.xbmc.kore.ui.abstractcursorlist.puml create mode 100644 doc/diagrams/sequence/org.xbmc.kore.ui.abstractinfo.puml create mode 100644 doc/mockups/detailsfragment.svg diff --git a/app/src/androidTest/java/org/xbmc/kore/tests/ui/RestoreSearchQueryViewPagerTest.java b/app/src/androidTest/java/org/xbmc/kore/tests/ui/RestoreSearchQueryViewPagerTest.java index 13a2182..0baea49 100644 --- a/app/src/androidTest/java/org/xbmc/kore/tests/ui/RestoreSearchQueryViewPagerTest.java +++ b/app/src/androidTest/java/org/xbmc/kore/tests/ui/RestoreSearchQueryViewPagerTest.java @@ -49,8 +49,8 @@ public class RestoreSearchQueryViewPagerTest { private final int ARTIST_SEARCH_QUERY_LIST_SIZE = 2; private final String ALBUMS_SEARCH_QUERY = "tes"; private final int ALBUM_SEARCH_QUERY_LIST_SIZE = 3; - private final int ARTIST_COMPLETE_LIST_SIZE = 228; - private final int ALBUM_COMPLETE_LIST_SIZE = 234; + private final int ARTIST_COMPLETE_LIST_SIZE = 229; + private final int ALBUM_COMPLETE_LIST_SIZE = 235; private LoaderIdlingResource loaderIdlingResource; diff --git a/app/src/main/java/org/xbmc/kore/ui/AbstractAdditionalInfoFragment.java b/app/src/main/java/org/xbmc/kore/ui/AbstractAdditionalInfoFragment.java new file mode 100644 index 0000000..7ad7e81 --- /dev/null +++ b/app/src/main/java/org/xbmc/kore/ui/AbstractAdditionalInfoFragment.java @@ -0,0 +1,21 @@ +/* + * 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; + +abstract public class AbstractAdditionalInfoFragment extends AbstractFragment { + abstract protected void refresh(); +} diff --git a/app/src/main/java/org/xbmc/kore/ui/AbstractCursorListFragment.java b/app/src/main/java/org/xbmc/kore/ui/AbstractCursorListFragment.java index da6821b..ae08d36 100644 --- a/app/src/main/java/org/xbmc/kore/ui/AbstractCursorListFragment.java +++ b/app/src/main/java/org/xbmc/kore/ui/AbstractCursorListFragment.java @@ -27,7 +27,6 @@ import android.support.v4.app.LoaderManager; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; import android.support.v4.view.MenuItemCompat; -import android.support.v4.widget.SwipeRefreshLayout; import android.support.v7.widget.SearchView; import android.text.TextUtils; import android.view.LayoutInflater; @@ -55,19 +54,15 @@ import de.greenrobot.event.EventBus; public abstract class AbstractCursorListFragment extends AbstractListFragment implements LoaderManager.LoaderCallbacks, SyncUtils.OnServiceListener, - SearchView.OnQueryTextListener, - SwipeRefreshLayout.OnRefreshListener { + SearchView.OnQueryTextListener { private static final String TAG = LogUtils.makeLogTag(AbstractCursorListFragment.class); private final String BUNDLE_KEY_SEARCH_QUERY = "search_query"; private ServiceConnection serviceConnection; - private HostInfo hostInfo; private EventBus bus; - private CursorAdapter adapter; - // Loader IDs private static final int LOADER = 0; @@ -89,13 +84,6 @@ public abstract class AbstractCursorListFragment extends AbstractListFragment View root = super.onCreateView(inflater, container, savedInstanceState); bus = EventBus.getDefault(); - HostManager hostManager = HostManager.getInstance(getActivity()); - hostInfo = hostManager.getHostInfo(); - - swipeRefreshLayout.setEnabled(true); - swipeRefreshLayout.setOnRefreshListener(this); - - adapter = (CursorAdapter) getAdapter(); if (savedInstanceState != null) { savedSearchFilter = savedInstanceState.getString(BUNDLE_KEY_SEARCH_QUERY); @@ -177,22 +165,6 @@ public abstract class AbstractCursorListFragment extends AbstractListFragment return super.onOptionsItemSelected(item); } - /** - * Swipe refresh layout callback - */ - /** {@inheritDoc} */ - @Override - public void onRefresh() { - if (hostInfo != null) { - showRefreshAnimation(); - onSwipeRefresh(); - } else { - swipeRefreshLayout.setRefreshing(false); - Toast.makeText(getActivity(), R.string.no_xbmc_configured, Toast.LENGTH_SHORT) - .show(); - } - } - /** * Should return the {@link LibrarySyncService} SyncType that * this list initiates @@ -237,7 +209,7 @@ public abstract class AbstractCursorListFragment extends AbstractListFragment } if (event.syncType.equals(getListSyncType())) { - swipeRefreshLayout.setRefreshing(false); + hideRefreshAnimation(); if (event.status == MediaSyncEvent.STATUS_SUCCESS) { refreshList(); if (!silentSync) { @@ -273,10 +245,10 @@ public abstract class AbstractCursorListFragment extends AbstractListFragment this.supportsSearch = supportsSearch; } - protected void onSwipeRefresh() { - LogUtils.LOGD(TAG, "Swipe, starting sync for: " + getListSyncType()); - // Start the syncing process - Intent syncIntent = new Intent(this.getActivity(), LibrarySyncService.class); + @Override + public void onRefresh() { + showRefreshAnimation(); + Intent syncIntent = new Intent(this.getActivity(), LibrarySyncService.class); syncIntent.putExtra(getListSyncType(), true); String syncID = getSyncID(); @@ -327,16 +299,18 @@ public abstract class AbstractCursorListFragment extends AbstractListFragment @Override public void onLoadFinished(Loader cursorLoader, Cursor cursor) { loaderLoading = false; - adapter.swapCursor(cursor); - // To prevent the empty text from appearing on the first load, set it now - emptyView.setText(getString(R.string.swipe_down_to_refresh)); + ((CursorAdapter) getAdapter()).swapCursor(cursor); + if (TextUtils.isEmpty(searchFilter)) { + // To prevent the empty text from appearing on the first load, set it now + emptyView.setText(getString(R.string.swipe_down_to_refresh)); + } loaderLoading = false; } /** {@inheritDoc} */ @Override public void onLoaderReset(Loader cursorLoader) { - adapter.swapCursor(null); + ((CursorAdapter) getAdapter()).swapCursor(null); } /** diff --git a/app/src/main/java/org/xbmc/kore/ui/AbstractDetailsFragment.java b/app/src/main/java/org/xbmc/kore/ui/AbstractDetailsFragment.java deleted file mode 100644 index f272fc2..0000000 --- a/app/src/main/java/org/xbmc/kore/ui/AbstractDetailsFragment.java +++ /dev/null @@ -1,321 +0,0 @@ -/* - * Copyright 2015 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; - -import android.Manifest; -import android.content.Intent; -import android.content.ServiceConnection; -import android.content.pm.PackageManager; -import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v13.app.FragmentCompat; -import android.support.v4.app.Fragment; -import android.support.v4.content.ContextCompat; -import android.support.v4.widget.SwipeRefreshLayout; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Toast; - -import org.xbmc.kore.R; -import org.xbmc.kore.Settings; -import org.xbmc.kore.host.HostInfo; -import org.xbmc.kore.host.HostManager; -import org.xbmc.kore.jsonrpc.ApiException; -import org.xbmc.kore.jsonrpc.event.MediaSyncEvent; -import org.xbmc.kore.service.library.LibrarySyncService; -import org.xbmc.kore.service.library.SyncUtils; -import org.xbmc.kore.utils.LogUtils; -import org.xbmc.kore.utils.UIUtils; -import org.xbmc.kore.utils.Utils; - -import butterknife.OnClick; -import butterknife.Optional; -import de.greenrobot.event.EventBus; - -abstract public class AbstractDetailsFragment extends Fragment - implements SwipeRefreshLayout.OnRefreshListener, - SyncUtils.OnServiceListener { - private static final String TAG = LogUtils.makeLogTag(AbstractDetailsFragment.class); - - private HostManager hostManager; - private HostInfo hostInfo; - private EventBus bus; - private String syncType; - - private SwipeRefreshLayout swipeRefreshLayout; - - private ServiceConnection serviceConnection; - - // Used to hide the refresh animation when a silent refresh is issued - private boolean silentRefresh = false; - - abstract protected View createView(LayoutInflater inflater, ViewGroup container); - - /** - * Should return {@link LibrarySyncService} SyncType that - * this fragment initiates - * @return {@link LibrarySyncService} SyncType - */ - abstract protected String getSyncType(); - - /** - * Should return the {@link LibrarySyncService} syncID if this fragment - * synchronizes a single item. The itemId that should be synced must returned by {@link #getSyncItemID()} - * @return {@link LibrarySyncService} SyncID - */ - abstract protected String getSyncID(); - - /** - * Should return the item ID for SyncID returned by {@link #getSyncID()} - * @return -1 if not used. - */ - abstract protected int getSyncItemID(); - - /** - * Should return the SwipeRefreshLayout if the fragment's view uses one. - * Used to notify the user if a sync for synctype returned by {@link #getSyncType()} - * is currently in progress - * @return - */ - abstract protected SwipeRefreshLayout getSwipeRefreshLayout(); - - /** - * When the view created in {@link #createView(LayoutInflater, ViewGroup)} contains a button - * with resource identifier R.id.download this will be called to initiate the download - */ - abstract protected void onDownload(); - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - bus = EventBus.getDefault(); - hostManager = HostManager.getInstance(getActivity()); - hostInfo = hostManager.getHostInfo(); - syncType = getSyncType(); - } - - @Nullable - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - if (container == null) { - // We're not being shown or there's nothing to show - return null; - } - - View view = createView(inflater, container); - if( view != null ) { - swipeRefreshLayout = getSwipeRefreshLayout(); - if( swipeRefreshLayout != null ) { - swipeRefreshLayout.setOnRefreshListener(this); - } - } - - return view; - } - - @Override - public void onActivityCreated(@Nullable Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - setHasOptionsMenu(true); - } - - @Override - public void onStart() { - super.onStart(); - serviceConnection = SyncUtils.connectToLibrarySyncService(getActivity(), this); - } - - @Override - public void onResume() { - bus.register(this); - super.onResume(); - } - - @Override - public void onPause() { - bus.unregister(this); - super.onPause(); - } - - @Override - public void onStop() { - super.onStop(); - SyncUtils.disconnectFromLibrarySyncService(getActivity(), serviceConnection); - } - - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - inflater.inflate(R.menu.refresh_item, menu); - super.onCreateOptionsMenu(menu, inflater); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.action_refresh: - onRefresh(); - } - return super.onOptionsItemSelected(item); - } - - @Optional - @OnClick(R.id.download) - public void onDownloadClicked(View v) { - boolean hasStoragePermission = - ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED; - - if (!hasStoragePermission) { - requestPermissions(new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, - Utils.PERMISSION_REQUEST_WRITE_STORAGE); - return; - } - - if (Settings.allowedDownloadNetworkTypes(getActivity()) != 0) { - onDownload(); - } else { - Toast.makeText(getActivity(), R.string.no_connection_type_selected, Toast.LENGTH_SHORT).show(); - } - } - - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) { - switch (requestCode) { - case Utils.PERMISSION_REQUEST_WRITE_STORAGE: - // If request is cancelled, the result arrays are empty. - if ((grantResults.length > 0) && - (grantResults[0] == PackageManager.PERMISSION_GRANTED)) { - onDownloadClicked(null); - } else { - Toast.makeText(getActivity(), R.string.write_storage_permission_denied, Toast.LENGTH_SHORT) - .show(); - } - break; - } - } - - - protected void startSync(boolean silentRefresh) { - this.silentRefresh = silentRefresh; - LogUtils.LOGD(TAG, "Starting syc. Silent? " + silentRefresh); - - if (getHostInfo() != null) { - if ((swipeRefreshLayout != null) && (!silentRefresh)) { - UIUtils.showRefreshAnimation(swipeRefreshLayout); - } - // Start the syncing process - Intent syncIntent = new Intent(this.getActivity(), LibrarySyncService.class); - - if (syncType != null) { - syncIntent.putExtra(syncType, true); - } - - String syncID = getSyncID(); - int itemId = getSyncItemID(); - if ((syncID != null) && (itemId != -1)) { - syncIntent.putExtra(syncID, itemId); - } - - Bundle syncExtras = new Bundle(); - syncExtras.putBoolean(LibrarySyncService.SILENT_SYNC, silentRefresh); - syncIntent.putExtra(LibrarySyncService.SYNC_EXTRAS, syncExtras); - - getActivity().startService(syncIntent); - } else { - if (swipeRefreshLayout != null) { - swipeRefreshLayout.setRefreshing(false); - } - Toast.makeText(getActivity(), R.string.no_xbmc_configured, Toast.LENGTH_SHORT) - .show(); - } - } - - /** - * Swipe refresh layout callback - */ - /** {@inheritDoc} */ - @Override - public void onRefresh () { - startSync(false); - } - - /** - * Event bus post. Called when the syncing process ended - * - * @param event Refreshes data - */ - public void onEventMainThread(MediaSyncEvent event) { - if ((syncType == null) || (! event.syncType.equals(syncType))) - return; - - boolean silentSync = false; - if (event.syncExtras != null) { - silentSync = event.syncExtras.getBoolean(LibrarySyncService.SILENT_SYNC, false); - } - - - if( swipeRefreshLayout != null ) { - swipeRefreshLayout.setRefreshing(false); - } - onSyncProcessEnded(event); - if (event.status == MediaSyncEvent.STATUS_SUCCESS) { - if (!silentSync) { - Toast.makeText(getActivity(), - R.string.sync_successful, Toast.LENGTH_SHORT) - .show(); - } - } else if (!silentSync) { - String msg = (event.errorCode == ApiException.API_ERROR) ? - String.format(getString(R.string.error_while_syncing), event.errorMessage) : - getString(R.string.unable_to_connect_to_xbmc); - Toast.makeText(getActivity(), msg, Toast.LENGTH_SHORT).show(); - } - } - - @Override - public void onServiceConnected(LibrarySyncService librarySyncService) { - if (syncType == null) - return; - - if (!silentRefresh && - (swipeRefreshLayout != null) && - SyncUtils.isLibrarySyncing(librarySyncService, - HostManager.getInstance(getActivity()).getHostInfo(), - syncType)) { - LogUtils.LOGD(TAG, "Showing refresh animation"); - UIUtils.showRefreshAnimation(swipeRefreshLayout); - } - } - - /** - * Called when sync process for type set through {@link #getSyncType()} ends - * @param event - */ - abstract protected void onSyncProcessEnded(MediaSyncEvent event); - - protected HostManager getHostManager() { - return hostManager; - } - - protected HostInfo getHostInfo() { - return hostInfo; - } -} diff --git a/app/src/main/java/org/xbmc/kore/ui/AbstractFragment.java b/app/src/main/java/org/xbmc/kore/ui/AbstractFragment.java new file mode 100644 index 0000000..a13713a --- /dev/null +++ b/app/src/main/java/org/xbmc/kore/ui/AbstractFragment.java @@ -0,0 +1,183 @@ +/* + * 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; + +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; + +public class AbstractFragment extends Fragment { + + private AbstractInfoFragment.DataHolder dataHolder; + + public void setDataHolder(AbstractInfoFragment.DataHolder dataHolder) { + this.dataHolder = dataHolder; + Bundle bundle = getArguments(); + if (bundle == null) { + setArguments(dataHolder.getBundle()); + } else { + bundle.putAll(dataHolder.getBundle()); + } + } + + public AbstractInfoFragment.DataHolder getDataHolder() { + return dataHolder; + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if( this.dataHolder == null ) { + this.dataHolder = new AbstractInfoFragment.DataHolder(-1); + } + + this.dataHolder.setBundle(getArguments()); + } + + public static class DataHolder { + static final String POSTER_TRANS_NAME = "POSTER_TRANS_NAME"; + static final String BUNDLE_KEY_ID = "id"; + static final String BUNDLE_KEY_TITLE = "title"; + static final String BUNDLE_KEY_UNDERTITLE = "undertitle"; + static final String BUNDLE_KEY_DESCRIPTION = "description"; + static final String BUNDLE_KEY_DETAILS = "details"; + static final String BUNDLE_KEY_POSTERURL = "poster"; + static final String BUNDLE_KEY_FANARTURL = "fanart"; + static final String BUNDLE_KEY_SQUAREPOSTER = "squareposter"; + static final String BUNDLE_KEY_RATING = "rating"; + static final String BUNDLE_KEY_MAXRATING = "maxrating"; + static final String BUNDLE_KEY_VOTES = "votes"; + + private Bundle bundle; + + private DataHolder() {} + + public DataHolder(Bundle bundle) { + setBundle(bundle); + } + + public DataHolder(int itemId) { + bundle = new Bundle(); + bundle.putInt(BUNDLE_KEY_ID, itemId); + } + + public void setBundle(Bundle bundle) { + this.bundle = bundle; + } + + public void setPosterTransitionName(String posterTransitionName) { + bundle.putString(POSTER_TRANS_NAME, posterTransitionName); + } + + public void setSquarePoster(boolean squarePoster) { + bundle.putBoolean(BUNDLE_KEY_SQUAREPOSTER, squarePoster); + } + + public void setRating(double rating) { + bundle.putDouble(BUNDLE_KEY_RATING, rating); + } + + public void setMaxRating(int maxRating) { + bundle.putInt(BUNDLE_KEY_MAXRATING, maxRating); + } + + public void setVotes(int votes) { + bundle.putInt(BUNDLE_KEY_VOTES, votes); + } + + public void setPosterUrl(String posterUrl) { + bundle.putString(BUNDLE_KEY_POSTERURL, posterUrl); + } + + public void setTitle(String title) { + bundle.putString(BUNDLE_KEY_TITLE, title); + } + + public void setUndertitle(String underTitle) { + bundle.putString(BUNDLE_KEY_UNDERTITLE, underTitle); + } + + public void setDescription(String description) { + bundle.putString(BUNDLE_KEY_DESCRIPTION, description); + } + + public void setDetails(String details) { + bundle.putString(BUNDLE_KEY_DETAILS, details); + } + + public void setFanArtUrl(String fanArtUrl) { + bundle.putString(BUNDLE_KEY_FANARTURL, fanArtUrl); + } + + public void setId(int id) { + bundle.putInt(BUNDLE_KEY_ID, id); + } + + public String getPosterTransitionName() { + return bundle.getString(POSTER_TRANS_NAME); + } + + public boolean getSquarePoster() { + return bundle.getBoolean(BUNDLE_KEY_SQUAREPOSTER); + } + + public double getRating() { + return bundle.getDouble(BUNDLE_KEY_RATING); + } + + public int getMaxRating() { + return bundle.getInt(BUNDLE_KEY_MAXRATING); + } + + public int getVotes() { + return bundle.getInt(BUNDLE_KEY_VOTES); + } + + public String getPosterUrl() { + return bundle.getString(BUNDLE_KEY_POSTERURL); + } + + public String getTitle() { + return bundle.getString(BUNDLE_KEY_TITLE); + } + + public String getUnderTitle() { + return bundle.getString(BUNDLE_KEY_UNDERTITLE); + } + + public String getDescription() { + return bundle.getString(BUNDLE_KEY_DESCRIPTION); + } + + public String getDetails() { + return bundle.getString(BUNDLE_KEY_DETAILS); + } + + public String getFanArtUrl() { + return bundle.getString(BUNDLE_KEY_FANARTURL); + } + + public int getId() { + return bundle.getInt(BUNDLE_KEY_ID); + } + + public Bundle getBundle() { + return bundle; + } + } +} diff --git a/app/src/main/java/org/xbmc/kore/ui/AbstractInfoFragment.java b/app/src/main/java/org/xbmc/kore/ui/AbstractInfoFragment.java new file mode 100644 index 0000000..16a3cd1 --- /dev/null +++ b/app/src/main/java/org/xbmc/kore/ui/AbstractInfoFragment.java @@ -0,0 +1,597 @@ +/* + * Copyright 2015 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; + +import android.Manifest; +import android.annotation.TargetApi; +import android.content.ServiceConnection; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.os.Bundle; +import android.os.Handler; +import android.preference.PreferenceManager; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.content.ContextCompat; +import android.support.v4.widget.SwipeRefreshLayout; +import android.text.TextUtils; +import android.util.DisplayMetrics; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +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; +import org.xbmc.kore.host.HostManager; +import org.xbmc.kore.jsonrpc.ApiCallback; +import org.xbmc.kore.jsonrpc.method.Player; +import org.xbmc.kore.jsonrpc.type.PlaylistType; +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.utils.LogUtils; +import org.xbmc.kore.utils.SharedElementTransition; +import org.xbmc.kore.utils.UIUtils; +import org.xbmc.kore.utils.Utils; + +import java.util.Locale; + +import at.blogc.android.views.ExpandableTextView; +import butterknife.ButterKnife; +import butterknife.InjectView; + +import static android.view.View.GONE; + +abstract public class AbstractInfoFragment extends AbstractFragment + implements SwipeRefreshLayout.OnRefreshListener, + SyncUtils.OnServiceListener, + SharedElementTransition.SharedElement { + private static final String TAG = LogUtils.makeLogTag(AbstractInfoFragment.class); + + // Detail views + @InjectView(R.id.swipe_refresh_layout) SwipeRefreshLayout swipeRefreshLayout; + @InjectView(R.id.media_panel) ScrollView panelScrollView; + @InjectView(R.id.art) ImageView artImageView; + @InjectView(R.id.poster) ImageView posterImageView; + @InjectView(R.id.media_title) TextView titleTextView; + @InjectView(R.id.media_undertitle) TextView underTitleTextView; + @InjectView(R.id.rating_container) LinearLayout ratingContainer; + @InjectView(R.id.rating) TextView ratingTextView; + @InjectView(R.id.rating_votes) TextView ratingVotesTextView; + @InjectView(R.id.max_rating) TextView maxRatingTextView; + @InjectView(R.id.media_details_right) TextView detailsRightTextView; + @InjectView(R.id.media_details) LinearLayout mediaDetailsContainer; + @InjectView(R.id.media_action_download) ImageButton downloadButton; + @InjectView(R.id.media_action_pin_unpin) ImageButton pinUnpinButton; + @InjectView(R.id.media_action_add_to_playlist) ImageButton addToPlaylistButton; + @InjectView(R.id.media_action_seen) ImageButton seenButton; + @InjectView(R.id.media_action_go_to_imdb) ImageButton imdbButton; + @InjectView(R.id.media_actions_bar) LinearLayout mediaActionsBar; + @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.exit_transition_view) View exitTransitionView; + + private HostManager hostManager; + private HostInfo hostInfo; + private ServiceConnection serviceConnection; + private RefreshItem refreshItem; + private boolean expandDescription; + + /** + * Handler on which to post RPC callbacks + */ + private Handler callbackHandler = new Handler(); + + /** + * Use {@link #setDataHolder(DataHolder)} + * to provide the required info after creating a new instance of this Fragment + */ + public AbstractInfoFragment() { + super(); + } + + + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + hostManager = HostManager.getInstance(getActivity()); + hostInfo = hostManager.getHostInfo(); + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + if (container == null) { + // We're not being shown or there's nothing to show + return null; + } + + 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(); + + if(!dataHolder.getSquarePoster()) { + posterImageView.getLayoutParams().width = + resources.getDimensionPixelSize(R.dimen.detail_poster_width_nonsquare); + posterImageView.getLayoutParams().height = + resources.getDimensionPixelSize(R.dimen.detail_poster_height_nonsquare); + } + + if(getRefreshItem() != null) { + swipeRefreshLayout.setOnRefreshListener(this); + } else { + swipeRefreshLayout.setEnabled(false); + } + + FloatingActionButton fab = (FloatingActionButton)fabButton; + fab.attachToScrollView((ObservableScrollView) panelScrollView); + + if(Utils.isLollipopOrLater()) { + posterImageView.setTransitionName(dataHolder.getPosterTransitionName()); + } + + if (savedInstanceState == null) { + FragmentManager fragmentManager = getChildFragmentManager(); + Fragment fragment = fragmentManager.findFragmentById(R.id.media_additional_info); + if (fragment == null) { + fragment = getAdditionalInfoFragment(); + if (fragment != null) { + fragmentManager.beginTransaction() + .add(R.id.media_additional_info, fragment) + .commit(); + } + } + } + + if(setupMediaActionBar()) { + mediaActionsBar.setVisibility(View.VISIBLE); + } + + if(setupFAB(fabButton)) { + fabButton.setVisibility(View.VISIBLE); + } + + updateView(dataHolder); + return root; + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + setHasOptionsMenu(true); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.refresh_item, menu); + super.onCreateOptionsMenu(menu, inflater); + } + + @Override + public void onStart() { + super.onStart(); + serviceConnection = SyncUtils.connectToLibrarySyncService(getActivity(), this); + } + + @Override + public void onResume() { + // Force the exit view to invisible + exitTransitionView.setVisibility(View.INVISIBLE); + if ( refreshItem != null ) { + refreshItem.register(); + } + super.onResume(); + } + + @Override + public void onPause() { + if ( refreshItem != null ) { + refreshItem.unregister(); + } + super.onPause(); + } + + @Override + public void onStop() { + super.onStop(); + SyncUtils.disconnectFromLibrarySyncService(getActivity(), serviceConnection); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.action_refresh: + onRefresh(); + } + return super.onOptionsItemSelected(item); + } + + /** + * Swipe refresh layout callback + */ + /** {@inheritDoc} */ + @Override + public void onRefresh () { + if (getRefreshItem() == null) { + Toast.makeText(getActivity(), R.string.Refreshing_not_implemented_for_this_item, + Toast.LENGTH_SHORT).show(); + swipeRefreshLayout.setRefreshing(false); + return; + } + + refreshItem.setSwipeRefreshLayout(swipeRefreshLayout); + refreshItem.startSync(false); + } + + @Override + public void onServiceConnected(LibrarySyncService librarySyncService) { + if (getRefreshItem() == null) { + return; + } + + if (SyncUtils.isLibrarySyncing(librarySyncService, + HostManager.getInstance(getActivity()).getHostInfo(), + refreshItem.getSyncType())) { + UIUtils.showRefreshAnimation(swipeRefreshLayout); + refreshItem.setSwipeRefreshLayout(swipeRefreshLayout); + refreshItem.register(); + } + } + + protected void setFabButtonState(boolean enable) { + if(enable) { + fabButton.setVisibility(View.VISIBLE); + } else { + fabButton.setVisibility(GONE); + } + } + + protected void fabActionPlayItem(PlaylistType.Item item) { + if (item == null) { + Toast.makeText(getActivity(), R.string.no_item_available_to_play, Toast.LENGTH_SHORT).show(); + return; + } + + 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); + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) { + switch (requestCode) { + case Utils.PERMISSION_REQUEST_WRITE_STORAGE: + // If request is cancelled, the result arrays are empty. + if ((grantResults.length > 0) && + (grantResults[0] == PackageManager.PERMISSION_GRANTED)) { + downloadButton.performClick(); + } else { + Toast.makeText(getActivity(), R.string.write_storage_permission_denied, Toast.LENGTH_SHORT) + .show(); + } + break; + } + } + + @Override + @TargetApi(21) + public boolean isSharedElementVisible() { + return UIUtils.isViewInBounds(panelScrollView, posterImageView); + } + + protected void refreshAdditionInfoFragment() { + Fragment fragment = getChildFragmentManager().findFragmentById(R.id.media_additional_info); + if (fragment != null) + ((AbstractAdditionalInfoFragment) fragment).refresh(); + } + + protected HostManager getHostManager() { + return hostManager; + } + + protected HostInfo getHostInfo() { + return hostInfo; + } + + /** + * Call this when you are ready to provide the titleTextView, undertitle, details, descriptionExpandableTextView, etc. etc. + */ + protected void updateView(DataHolder dataHolder) { + titleTextView.setText(dataHolder.getTitle()); + underTitleTextView.setText(dataHolder.getUnderTitle()); + detailsRightTextView.setText(dataHolder.getDetails()); + + if (!TextUtils.isEmpty(dataHolder.getDescription())) { + Resources.Theme theme = getActivity().getTheme(); + TypedArray styledAttributes = theme.obtainStyledAttributes(new int[]{ + R.attr.iconExpand, + R.attr.iconCollapse + }); + final int iconCollapseResId = + styledAttributes.getResourceId(styledAttributes.getIndex(0), R.drawable.ic_expand_less_white_24dp); + final int iconExpandResId = + styledAttributes.getResourceId(styledAttributes.getIndex(1), R.drawable.ic_expand_more_white_24dp); + styledAttributes.recycle(); + descriptionContainer.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + descriptionExpandableTextView.toggle(); + expansionImage.setImageResource(descriptionExpandableTextView.isExpanded() ? iconCollapseResId : iconExpandResId); + } + }); + descriptionExpandableTextView.setText(dataHolder.getDescription()); + if (expandDescription) { + descriptionExpandableTextView.expand(); + expansionImage.setImageResource(iconExpandResId); + } + descriptionContainer.setVisibility(View.VISIBLE); + } else { + descriptionContainer.setVisibility(GONE); + } + + // Images + DisplayMetrics displayMetrics = new DisplayMetrics(); + getActivity().getWindowManager().getDefaultDisplay().getMetrics(displayMetrics); + + Resources resources = getActivity().getResources(); + + if (dataHolder.getPosterUrl() != null) { + int posterWidth; + int posterHeight; + if (dataHolder.getSquarePoster()) { + posterWidth = resources.getDimensionPixelOffset(R.dimen.detail_poster_width_square); + posterHeight = resources.getDimensionPixelOffset(R.dimen.detail_poster_height_square); + } else { + posterWidth = resources.getDimensionPixelOffset(R.dimen.detail_poster_width_nonsquare); + posterHeight = resources.getDimensionPixelOffset(R.dimen.detail_poster_height_nonsquare); + } + + UIUtils.loadImageWithCharacterAvatar(getActivity(), hostManager, + dataHolder.getPosterUrl(), dataHolder.getTitle(), + posterImageView, posterWidth, posterHeight); + } else { + posterImageView.setVisibility(GONE); + int padding = getActivity().getResources().getDimensionPixelSize(R.dimen.default_padding); + titleTextView.setPadding(padding, padding, 0, 0); + underTitleTextView.setPadding(padding, padding, 0, 0); + } + + int artHeight = resources.getDimensionPixelOffset(R.dimen.detail_art_height); + int artWidth = displayMetrics.widthPixels; + + UIUtils.loadImageIntoImageview(hostManager, + TextUtils.isEmpty(dataHolder.getFanArtUrl()) ? + dataHolder.getPosterUrl() : dataHolder.getFanArtUrl(), + artImageView, artWidth, artHeight); + + if (dataHolder.getRating() > 0) { + ratingTextView.setText(String.format(Locale.getDefault(), "%01.01f", dataHolder.getRating())); + if (dataHolder.getMaxRating() > 0) { + maxRatingTextView.setText(String.format(getString(R.string.max_rating), + String.valueOf(dataHolder.getMaxRating()))); + } + if (dataHolder.getVotes() > 0 ) { + ratingVotesTextView.setText(String.format(getString(R.string.votes), + String.valueOf(dataHolder.getVotes()))); + } + ratingContainer.setVisibility(View.VISIBLE); + } else if (TextUtils.isEmpty(dataHolder.getDetails())) { + mediaDetailsContainer.setVisibility(View.GONE); + } + } + + /** + * Setting a listener for downloads will add the download button to the UI + * @param listener to be called when user clicks the download button. Note that the View passed + * into onClick from {@link android.view.View.OnClickListener} will be null + * when the user is asked for storage permissions + */ + protected void setOnDownloadListener(final View.OnClickListener listener) { + downloadButton.setVisibility(View.VISIBLE); + downloadButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + if (checkStoragePermission()) { + if (Settings.allowedDownloadNetworkTypes(getActivity()) != 0) { + listener.onClick(view); + setButtonState(downloadButton, true); + } else { + Toast.makeText(getActivity(), R.string.no_connection_type_selected, Toast.LENGTH_SHORT).show(); + } + } + } + }); + } + + protected void setOnAddToPlaylistListener(View.OnClickListener listener) { + addToPlaylistButton.setVisibility(View.VISIBLE); + addToPlaylistButton.setOnClickListener(listener); + } + + protected void setOnGoToImdbListener(View.OnClickListener listener) { + imdbButton.setVisibility(View.VISIBLE); + imdbButton.setOnClickListener(listener); + } + + /** + * Use {@link #setSeenButtonState(boolean)} to set the state of the seen button + * @param listener + */ + protected void setOnSeenListener(final View.OnClickListener listener) { + setupToggleButton(seenButton, listener); + } + + protected void setOnPinClickedListener(final View.OnClickListener listener) { + setupToggleButton(pinUnpinButton, 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 + */ + protected void setDownloadButtonState(boolean state) { + setButtonState(downloadButton, state); + } + + /** + * Uses colors to show the seen state to the user + * @param state true if item has been watched/listened too, false otherwise + */ + protected void setSeenButtonState(boolean state) { + setToggleButtonState(seenButton, state); + } + + protected void setPinButtonState(boolean state) { + setToggleButtonState(pinUnpinButton, state); + } + + private void setButtonState(ImageButton button, boolean state) { + if (state) { + UIUtils.highlightImageView(getActivity(), button); + } else { + button.clearColorFilter(); + } + } + + private void setToggleButtonState(ImageButton button, boolean state) { + setButtonState(button, state); + button.setTag(state); + } + + private void setupToggleButton(final ImageButton button, final View.OnClickListener listener) { + button.setVisibility(View.VISIBLE); + button.setTag(false); + button.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + listener.onClick(view); + // Boldly invert the state. We depend on the observer to correct the state + // if Kodi or other service didn't honour our request + setToggleButtonState(button, ! (boolean) button.getTag()); + } + }); + } + + private boolean checkStoragePermission() { + boolean hasStoragePermission = + ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED; + + if (!hasStoragePermission) { + requestPermissions(new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, + Utils.PERMISSION_REQUEST_WRITE_STORAGE); + return false; + } + + return true; + } + + protected RefreshItem getRefreshItem() { + if (refreshItem == null) { + refreshItem = createRefreshItem(); + } + return refreshItem; + } + + protected void setExpandDescription(boolean expandDescription) { + this.expandDescription = expandDescription; + } + + abstract protected AbstractAdditionalInfoFragment getAdditionalInfoFragment(); + + /** + * Called when user commands the information to be renewed. Either through a swipe down + * or a menu call. + *
+ * Note, that {@link AbstractAdditionalInfoFragment#refresh()} will be called for an + * additional fragment, if available, automatically. + * @return + */ + abstract protected RefreshItem createRefreshItem(); + + /** + * Called when the media action bar actions are available and + * you can use {@link #setOnAddToPlaylistListener(View.OnClickListener)}, + * {@link #setOnSeenListener(View.OnClickListener)}, + * {@link #setOnDownloadListener(View.OnClickListener)}, + * {@link #setOnGoToImdbListener(View.OnClickListener)}, + * and {@link #setOnPinClickedListener(View.OnClickListener)} to enable + * one or more actions. + * @return true if media action bar should be visible, false otherwise + */ + abstract protected boolean setupMediaActionBar(); + + /** + * Called when the fab button is available + * @return true to enable the Floating Action Button, false otherwise + */ + abstract protected boolean setupFAB(ImageButton FAB); +} diff --git a/app/src/main/java/org/xbmc/kore/ui/AbstractListFragment.java b/app/src/main/java/org/xbmc/kore/ui/AbstractListFragment.java index 92cbe31..58dad0f 100644 --- a/app/src/main/java/org/xbmc/kore/ui/AbstractListFragment.java +++ b/app/src/main/java/org/xbmc/kore/ui/AbstractListFragment.java @@ -43,7 +43,8 @@ import org.xbmc.kore.utils.Utils; import butterknife.ButterKnife; import butterknife.InjectView; -public abstract class AbstractListFragment extends Fragment { +public abstract class AbstractListFragment extends Fragment implements + SwipeRefreshLayout.OnRefreshListener { private static final String TAG = LogUtils.makeLogTag(AbstractListFragment.class); private BaseAdapter adapter; @@ -58,6 +59,12 @@ public abstract class AbstractListFragment extends Fragment { abstract protected AdapterView.OnItemClickListener createOnItemClickListener(); abstract protected BaseAdapter createAdapter(); + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + adapter = createAdapter(); + } + @TargetApi(16) @Nullable @Override @@ -65,13 +72,10 @@ public abstract class AbstractListFragment extends Fragment { ViewGroup root = (ViewGroup) inflater.inflate(R.layout.fragment_generic_media_list, container, false); ButterKnife.inject(this, root); - swipeRefreshLayout.setEnabled(false); + swipeRefreshLayout.setOnRefreshListener(this); gridView.setEmptyView(emptyView); gridView.setOnItemClickListener(createOnItemClickListener()); - - // Configure the adapter and start the loader - adapter = createAdapter(); gridView.setAdapter(adapter); if (savedInstanceState != null) { @@ -99,7 +103,7 @@ public abstract class AbstractListFragment extends Fragment { gridView.getViewTreeObserver().removeGlobalOnLayoutListener(this); } - //Make sure menu is updated if it was already created + //Make sure menu is update d if it was already created getActivity().invalidateOptionsMenu(); } }); @@ -184,6 +188,10 @@ public abstract class AbstractListFragment extends Fragment { }); } + public void hideRefreshAnimation() { + swipeRefreshLayout.setRefreshing(false); + } + public BaseAdapter getAdapter() { return adapter; } diff --git a/app/src/main/java/org/xbmc/kore/ui/AbstractTabsFragment.java b/app/src/main/java/org/xbmc/kore/ui/AbstractTabsFragment.java new file mode 100644 index 0000000..2adfbc3 --- /dev/null +++ b/app/src/main/java/org/xbmc/kore/ui/AbstractTabsFragment.java @@ -0,0 +1,101 @@ +/* + * Copyright 2015 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; + +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v4.view.ViewPager; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import com.astuetz.PagerSlidingTabStrip; + +import org.xbmc.kore.R; +import org.xbmc.kore.utils.LogUtils; +import org.xbmc.kore.utils.SharedElementTransition; +import org.xbmc.kore.utils.TabsAdapter; +import org.xbmc.kore.utils.UIUtils; + +import butterknife.ButterKnife; +import butterknife.InjectView; + +abstract public class AbstractTabsFragment extends AbstractFragment + implements SharedElementTransition.SharedElement { + private static final String TAG = LogUtils.makeLogTag(AbstractTabsFragment.class); + + @InjectView(R.id.pager_tab_strip) PagerSlidingTabStrip pagerTabStrip; + @InjectView(R.id.pager) ViewPager viewPager; + + /** + * Use {@link #setDataHolder(AbstractInfoFragment.DataHolder)} to provide the required info + * after creating a new instance of this Fragment + */ + public AbstractTabsFragment() { + super(); + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + if (container == null) { + // We're not being shown or there's nothing to show + return null; + } + + ViewGroup root = (ViewGroup) inflater.inflate(R.layout.fragment_default_view_pager, container, false); + ButterKnife.inject(this, root); + + viewPager.setAdapter(createTabsAdapter(getDataHolder())); + pagerTabStrip.setViewPager(viewPager); + + return root; + } + + @Override + public void onActivityCreated (Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + setHasOptionsMenu(false); + } + + @Override + public boolean isSharedElementVisible() { + View view = getView(); + if (view == null) + return false; + + //Note: this works as R.id.poster is only used in *InfoFragment. + //If the same id is used in other fragments in the TabsAdapter we + //need to check which fragment is currently displayed + View artView = view.findViewById(R.id.poster); + View scrollView = view.findViewById(R.id.media_panel); + if (( artView != null ) && + ( scrollView != null ) && + UIUtils.isViewInBounds(scrollView, artView)) { + return true; + } + + return false; + } + + /** + * Called to get the TabsAdapter that should be connected to the ViewPager + * @param dataHolder the data passed to the *DetailsFragment + * @return + */ + abstract protected TabsAdapter createTabsAdapter(AbstractInfoFragment.DataHolder dataHolder); +} diff --git a/app/src/main/java/org/xbmc/kore/ui/generic/CastFragment.java b/app/src/main/java/org/xbmc/kore/ui/generic/CastFragment.java new file mode 100644 index 0000000..38ee6a5 --- /dev/null +++ b/app/src/main/java/org/xbmc/kore/ui/generic/CastFragment.java @@ -0,0 +1,193 @@ +/* + * 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.generic; + +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.provider.BaseColumns; +import android.support.annotation.Nullable; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.CursorLoader; +import android.support.v4.content.Loader; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.GridLayout; + +import org.xbmc.kore.R; +import org.xbmc.kore.host.HostManager; +import org.xbmc.kore.jsonrpc.type.VideoType; +import org.xbmc.kore.provider.MediaContract; +import org.xbmc.kore.ui.AbstractAdditionalInfoFragment; +import org.xbmc.kore.ui.sections.video.AllCastActivity; +import org.xbmc.kore.utils.LogUtils; +import org.xbmc.kore.utils.UIUtils; + +import java.util.ArrayList; + +public class CastFragment extends AbstractAdditionalInfoFragment implements LoaderManager.LoaderCallbacks { + private static final String TAG = LogUtils.makeLogTag(CastFragment.class); + + private static final String BUNDLE_ITEMID = "itemid"; + private static final String BUNDLE_TITLE = "title"; + private static final String BUNDLE_LOADER_TYPE = "loadertype"; + + public static enum TYPE { + TVSHOW, + MOVIE + } + + public void setArgs(int itemId, String title, TYPE type) { + Bundle bundle = new Bundle(); + bundle.putInt(BUNDLE_ITEMID, itemId); + bundle.putString(BUNDLE_TITLE, title); + bundle.putInt(BUNDLE_LOADER_TYPE, type.ordinal()); + setArguments(bundle); + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_cast, container, false); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + Bundle bundle = getArguments(); + getLoaderManager().initLoader(bundle.getInt(BUNDLE_LOADER_TYPE), null, this); + } + + @Override + public Loader onCreateLoader(int id, Bundle args) { + int hostId = HostManager.getInstance(getActivity()).getHostInfo().getId(); + + Uri uri; + int itemId = getArguments().getInt(BUNDLE_ITEMID); + + if (id == TYPE.MOVIE.ordinal()) { + uri = MediaContract.MovieCast.buildMovieCastListUri(hostId, itemId); + return new CursorLoader(getActivity(), uri, + MovieCastListQuery.PROJECTION, null, null, MovieCastListQuery.SORT); + } else { + uri = MediaContract.TVShowCast.buildTVShowCastListUri(hostId, itemId); + return new CursorLoader(getActivity(), uri, + TVShowCastListQuery.PROJECTION, null, null, TVShowCastListQuery.SORT); + } + } + + @Override + public void onLoadFinished(Loader loader, Cursor cursor) { + if (! cursor.moveToFirst()) { + return; + } + + ArrayList castArrayList; + + int id = getArguments().getInt(BUNDLE_LOADER_TYPE); + if (id == TYPE.MOVIE.ordinal()) { + castArrayList = createMovieCastList(cursor); + } else { + castArrayList = createTVShowCastList(cursor); + + } + + UIUtils.setupCastInfo(getActivity(), castArrayList, + (GridLayout) getView().findViewById(R.id.cast_list), + AllCastActivity.buildLaunchIntent(getActivity(), + getArguments().getString(BUNDLE_TITLE), + castArrayList)); + } + + @Override + public void onLoaderReset(Loader loader) { + + } + + private ArrayList createMovieCastList(Cursor cursor) { + ArrayList 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()); + + return castArrayList; + } + + private ArrayList createTVShowCastList(Cursor cursor) { + ArrayList castArrayList = new ArrayList<>(cursor.getCount()); + do { + castArrayList.add(new VideoType.Cast(cursor.getString(TVShowCastListQuery.NAME), + cursor.getInt(TVShowCastListQuery.ORDER), + cursor.getString(TVShowCastListQuery.ROLE), + cursor.getString(TVShowCastListQuery.THUMBNAIL))); + } while (cursor.moveToNext()); + + return castArrayList; + } + + @Override + public void refresh() { + getLoaderManager().restartLoader(getArguments().getInt(BUNDLE_LOADER_TYPE), + null, this); + } + + /** + * 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"; + + int ID = 0; + int NAME = 1; + int ORDER = 2; + int ROLE = 3; + int THUMBNAIL = 4; + } + + /** + * Movie cast list query parameters. + */ + public interface TVShowCastListQuery { + String[] PROJECTION = { + BaseColumns._ID, + MediaContract.MovieCast.NAME, + MediaContract.MovieCast.ORDER, + MediaContract.MovieCast.ROLE, + MediaContract.MovieCast.THUMBNAIL, + }; + + String SORT = MediaContract.TVShowCast.ORDER + " ASC"; + + int ID = 0; + int NAME = 1; + int ORDER = 2; + int ROLE = 3; + int THUMBNAIL = 4; + } +} diff --git a/app/src/main/java/org/xbmc/kore/ui/generic/RefreshItem.java b/app/src/main/java/org/xbmc/kore/ui/generic/RefreshItem.java new file mode 100644 index 0000000..1cfd643 --- /dev/null +++ b/app/src/main/java/org/xbmc/kore/ui/generic/RefreshItem.java @@ -0,0 +1,187 @@ +/* + * 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.generic; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.support.v4.widget.SwipeRefreshLayout; +import android.widget.Toast; + +import org.xbmc.kore.R; +import org.xbmc.kore.host.HostInfo; +import org.xbmc.kore.host.HostManager; +import org.xbmc.kore.jsonrpc.ApiException; +import org.xbmc.kore.jsonrpc.event.MediaSyncEvent; +import org.xbmc.kore.service.library.LibrarySyncService; +import org.xbmc.kore.utils.LogUtils; +import org.xbmc.kore.utils.UIUtils; + +import de.greenrobot.event.EventBus; + +public class RefreshItem { + private final String TAG = LogUtils.makeLogTag(RefreshItem.class); + + public interface RefreshItemListener { + void onSyncProcessEnded(MediaSyncEvent event); + } + + private RefreshItemListener listener; + + private String syncType; + private String syncID; + private int itemId; + private Context context; + private SwipeRefreshLayout swipeRefreshLayout; + + private RefreshItem() {} + + /** + * RefreshItem can be used to refresh the information for one or more items. + * If you want to sync a single item you will need to use {@link #setSyncItem(String, int)} + * to set the item that needs to be refreshed. + * @param context + * @param syncType {@link LibrarySyncService} SyncType + */ + public RefreshItem(Context context, String syncType) { + if (syncType == null) { + throw new IllegalArgumentException("Argument syncType can not be null"); + } + this.syncType = syncType; + this.context = context; + } + + /** + * Sets the item that needs to be refreshed. Only required if you want to refresh a single item. + * @param syncID {@link LibrarySyncService} syncID if you want to refresh a single item. + * @param itemId the item ID of the single item (set with syncID) you want to refresh. + */ + public void setSyncItem(String syncID, int itemId) { + this.syncID = syncID; + this.itemId = itemId; + } + + /** + * @return {@link LibrarySyncService} SyncType + */ + public String getSyncType() { + return syncType; + } + + /** + * Specifiy a listener if you want to be notified when the synchronization has finished. + * @param listener + */ + public void setListener(RefreshItemListener listener) { + this.listener = listener; + } + + /** + * If you use a SwipeRefreshLayout you can let RefreshItem manage the refresh animation + * by passing in the reference to SwipeRefreshLayout in your View + * @param swipeRefreshLayout + */ + public void setSwipeRefreshLayout(SwipeRefreshLayout swipeRefreshLayout) { + this.swipeRefreshLayout = swipeRefreshLayout; + } + + public void startSync(boolean silentRefresh) { + LogUtils.LOGD(TAG, "Starting sync. Silent? " + silentRefresh); + + HostInfo hostInfo = HostManager.getInstance(context).getHostInfo(); + + if (hostInfo != null) { + register(); + + if ((swipeRefreshLayout != null) && (!silentRefresh)) { + UIUtils.showRefreshAnimation(swipeRefreshLayout); + } + // Start the syncing process + Intent syncIntent = new Intent(context, LibrarySyncService.class); + + syncIntent.putExtra(syncType, true); + + if ((syncID != null) && (itemId != -1)) { + syncIntent.putExtra(syncID, itemId); + } + + Bundle syncExtras = new Bundle(); + syncExtras.putBoolean(LibrarySyncService.SILENT_SYNC, silentRefresh); + syncIntent.putExtra(LibrarySyncService.SYNC_EXTRAS, syncExtras); + + context.startService(syncIntent); + } else { + if (swipeRefreshLayout != null) { + swipeRefreshLayout.setRefreshing(false); + } + Toast.makeText(context, R.string.no_xbmc_configured, Toast.LENGTH_SHORT) + .show(); + } + } + + /** + * Event bus post. Called when the syncing process ended + * + * @param event Refreshes data + */ + public void onEventMainThread(MediaSyncEvent event) { + unregister(); + + if (! event.syncType.equals(syncType)) + return; + + boolean silentRefresh = false; + if (event.syncExtras != null) { + silentRefresh = event.syncExtras.getBoolean(LibrarySyncService.SILENT_SYNC, false); + } + + if( swipeRefreshLayout != null ) { + swipeRefreshLayout.setRefreshing(false); + } + + if (listener != null) { + listener.onSyncProcessEnded(event); + } + + if (event.status == MediaSyncEvent.STATUS_SUCCESS) { + if (!silentRefresh) { + Toast.makeText(context, + R.string.sync_successful, Toast.LENGTH_SHORT) + .show(); + } + } else if (!silentRefresh) { + String msg = (event.errorCode == ApiException.API_ERROR) ? + String.format(context.getString(R.string.error_while_syncing), event.errorMessage) : + context.getString(R.string.unable_to_connect_to_xbmc); + Toast.makeText(context, msg, Toast.LENGTH_SHORT).show(); + } + } + + public void unregister() { + EventBus eventBus = EventBus.getDefault(); + if ( eventBus.isRegistered(this) ) { + eventBus.unregister(this); + } + } + + public void register() { + EventBus eventBus = EventBus.getDefault(); + if ( ! eventBus.isRegistered(this) ) { + eventBus.register(this); + } + } +} diff --git a/app/src/main/java/org/xbmc/kore/ui/sections/addon/AddonDetailsFragment.java b/app/src/main/java/org/xbmc/kore/ui/sections/addon/AddonDetailsFragment.java index c360377..b40de79 100644 --- a/app/src/main/java/org/xbmc/kore/ui/sections/addon/AddonDetailsFragment.java +++ b/app/src/main/java/org/xbmc/kore/ui/sections/addon/AddonDetailsFragment.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 Synced Synapse. All rights reserved. + * Copyright 2016 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. @@ -15,363 +15,43 @@ */ package org.xbmc.kore.ui.sections.addon; -import android.annotation.TargetApi; -import android.content.Context; -import android.content.SharedPreferences; -import android.content.res.Resources; -import android.content.res.TypedArray; import android.os.Bundle; -import android.os.Handler; -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.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.host.HostInfo; -import org.xbmc.kore.host.HostManager; -import org.xbmc.kore.jsonrpc.ApiCallback; -import org.xbmc.kore.jsonrpc.method.Addons; -import org.xbmc.kore.jsonrpc.type.AddonType; +import org.xbmc.kore.ui.AbstractTabsFragment; +import org.xbmc.kore.ui.AbstractInfoFragment; +import org.xbmc.kore.ui.sections.file.MediaFileListFragment; import org.xbmc.kore.utils.LogUtils; -import org.xbmc.kore.utils.UIUtils; -import org.xbmc.kore.utils.Utils; +import org.xbmc.kore.utils.TabsAdapter; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; - -import butterknife.ButterKnife; -import butterknife.InjectView; -import butterknife.OnClick; - -/** - * Presents addon details - */ -public class AddonDetailsFragment extends SharedElementFragment { +public class AddonDetailsFragment extends AbstractTabsFragment { private static final String TAG = LogUtils.makeLogTag(AddonDetailsFragment.class); - public static final String BUNDLE_KEY_ADDONID = "addon_id"; - public static final String POSTER_TRANS_NAME = "POSTER_TRANS_NAME"; - public static final String BUNDLE_KEY_NAME = "name"; - public static final String BUNDLE_KEY_AUTHOR = "author"; - public static final String BUNDLE_KEY_SUMMARY = "summary"; - public static final String BUNDLE_KEY_VERSION = "version"; - public static final String BUNDLE_KEY_DESCRIPTION = "description"; - public static final String BUNDLE_KEY_FANART = "fanart"; - public static final String BUNDLE_KEY_POSTER = "poster"; - public static final String BUNDLE_KEY_ENABLED = "enabled"; - public static final String BUNDLE_KEY_BROWSABLE = "browsable"; + public Bundle contentArgs(Bundle details) { + AbstractInfoFragment.DataHolder dataHolder = new AbstractInfoFragment.DataHolder(details); + String name = dataHolder.getTitle(); + String path = details.getString(AddonInfoFragment.BUNDLE_KEY_ADDONID); - private HostManager hostManager; - private HostInfo hostInfo; - - /** - * Handler on which to post RPC callbacks - */ - private Handler callbackHandler = new Handler(); - - // Displayed addon id - private String addonId; - - // Buttons - @InjectView(R.id.fab) ImageButton fabButton; - @InjectView(R.id.enable_disable) ImageButton enabledButton; - @InjectView(R.id.pin_unpin) ImageView pinButton; - - // 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.author) TextView mediaAuthor; - @InjectView(R.id.version) TextView mediaVersion; - - @InjectView(R.id.media_description) TextView mediaDescription; - - /** - * Create a new instance of this, initialized to show the addon addonId - */ - @TargetApi(21) - public static AddonDetailsFragment newInstance(AddonListFragment.ViewHolder vh) { - AddonDetailsFragment fragment = new AddonDetailsFragment(); - - Bundle args = new Bundle(); - args.putString(BUNDLE_KEY_ADDONID, vh.addonId); - args.putString(BUNDLE_KEY_NAME, vh.addonName); - args.putString(BUNDLE_KEY_AUTHOR, vh.author); - args.putString(BUNDLE_KEY_VERSION, vh.version); - args.putString(BUNDLE_KEY_SUMMARY, vh.summary); - args.putString(BUNDLE_KEY_DESCRIPTION, vh.description); - args.putString(BUNDLE_KEY_FANART, vh.fanart); - args.putString(BUNDLE_KEY_POSTER, vh.poster); - args.putBoolean(BUNDLE_KEY_ENABLED, vh.enabled); - args.putBoolean(BUNDLE_KEY_BROWSABLE, vh.browsable); - - if( Utils.isLollipopOrLater()) { - args.putString(POSTER_TRANS_NAME, vh.artView.getTransitionName()); - } - fragment.setArguments(args); - return fragment; - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - } - - @TargetApi(21) - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - Bundle bundle = getArguments(); - addonId = bundle.getString(BUNDLE_KEY_ADDONID, null); - - if ((container == null) || (addonId == null)) { - // We're not being shown or there's nothing to show - return null; - } - - ViewGroup root = (ViewGroup) inflater.inflate(R.layout.fragment_addon_details, container, false); - ButterKnife.inject(this, root); - - hostManager = HostManager.getInstance(getActivity()); - hostInfo = hostManager.getHostInfo(); - - // 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(bundle.getString(POSTER_TRANS_NAME)); - } - - mediaTitle.setText(bundle.getString(BUNDLE_KEY_NAME)); - mediaUndertitle.setText(bundle.getString(BUNDLE_KEY_SUMMARY)); - mediaAuthor.setText(bundle.getString(BUNDLE_KEY_AUTHOR)); - mediaVersion.setText(bundle.getString(BUNDLE_KEY_VERSION)); - mediaDescription.setText(bundle.getString(BUNDLE_KEY_DESCRIPTION)); - - setImages(bundle.getString(BUNDLE_KEY_POSTER), bundle.getString(BUNDLE_KEY_FANART)); - - setupEnableButton(bundle.getBoolean(BUNDLE_KEY_ENABLED, false)); - if (bundle.getBoolean(BUNDLE_KEY_BROWSABLE, true)) - updatePinButton(); - - // Pad main content view to overlap with bottom system bar -// UIUtils.setPaddingForSystemBars(getActivity(), mediaPanel, false, false, true); -// mediaPanel.setClipToPadding(false); - - return root; + MediaFileListFragment.FileLocation rootPath = new MediaFileListFragment.FileLocation(name, "plugin://" + path, true); + rootPath.setRootDir(true); + details.putParcelable(MediaFileListFragment.ROOT_PATH, rootPath); + details.putBoolean(MediaFileListFragment.DELAY_LOAD, true); + return details; } @Override public void onActivityCreated (Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); setHasOptionsMenu(false); - updateEnabledButton(); } @Override - public void onResume() { - super.onResume(); - } - - @Override - public void onPause() { - super.onPause(); - } - - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); -// outState.putInt(ADDONID, addonId); - } - - /** - * Callbacks for button bar - */ - @OnClick(R.id.fab) - public void onFabClicked(View v) { - Addons.ExecuteAddon action = new Addons.ExecuteAddon(addonId); - action.execute(hostManager.getConnection(), new ApiCallback() { - @Override - public void onSuccess(String result) { - // Do nothing - } - - @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.enable_disable) - public void onEnabledClicked(View v) { - final Boolean isEnabled = (v.getTag() == null)? false : (Boolean)v.getTag(); - - Addons.SetAddonEnabled action = new Addons.SetAddonEnabled(addonId, !isEnabled); - action.execute(hostManager.getConnection(), new ApiCallback() { - @Override - public void onSuccess(String result) { - if (!isAdded()) return; - int messageResId = (!isEnabled) ? R.string.addon_enabled : R.string.addon_disabled; - Toast.makeText(getActivity(), messageResId, Toast.LENGTH_SHORT).show(); - setupEnableButton(!isEnabled); - } - - @Override - public void onError(int errorCode, String description) { - if (!isAdded()) return; - Toast.makeText(getActivity(), - String.format(getString(R.string.general_error_executing_action), description), - Toast.LENGTH_SHORT) - .show(); - } - }, callbackHandler); - } - - private void setImages(String poster, String fanart) { - Resources resources = getActivity().getResources(); - DisplayMetrics displayMetrics = new DisplayMetrics(); - getActivity().getWindowManager().getDefaultDisplay().getMetrics(displayMetrics); - int artHeight = resources.getDimensionPixelOffset(R.dimen.now_playing_art_height), - artWidth = displayMetrics.widthPixels; - int posterWidth = resources.getDimensionPixelOffset(R.dimen.addondetail_poster_width); - int posterHeight = resources.getDimensionPixelOffset(R.dimen.addondetail_poster_height); - - UIUtils.loadImageIntoImageview(hostManager, - TextUtils.isEmpty(fanart)? poster : fanart, - mediaArt, artWidth, artHeight); - UIUtils.loadImageIntoImageview(hostManager, - poster, - mediaPoster, posterWidth, posterHeight); - - } - - private void setupEnableButton(boolean enabled) { - // Enabled button - if (enabled) { - Resources.Theme theme = getActivity().getTheme(); - TypedArray styledAttributes = theme.obtainStyledAttributes(new int[]{ - R.attr.colorAccent}); - enabledButton.setColorFilter(styledAttributes.getColor(0, - getActivity().getResources().getColor(R.color.accent_default))); - styledAttributes.recycle(); - - fabButton.setVisibility(View.VISIBLE); - } else { - enabledButton.clearColorFilter(); - fabButton.setVisibility(View.GONE); - } - enabledButton.setTag(enabled); - } - - /** - * Returns the shared element if visible - * @return View if visible, null otherwise - */ - @Override - public View getSharedElement() { - if (UIUtils.isViewInBounds(mediaPanel, mediaPoster)) { - return mediaPoster; - } - - return null; - } - - private void updateEnabledButton() { - // Get the addon details, this is done asyhnchronously - String[] properties = new String[] { - AddonType.Fields.ENABLED - }; - Addons.GetAddonDetails action = new Addons.GetAddonDetails(addonId, properties); - action.execute(hostManager.getConnection(), new ApiCallback() { - @Override - public void onSuccess(AddonType.Details result) { - if (!isAdded()) return; - setupEnableButton(result.enabled); - } - - @Override - public void onError(int errorCode, String description) { - if (!isAdded()) return; - Toast.makeText(getActivity(), - String.format(getString(R.string.error_getting_addon_info), description), - Toast.LENGTH_SHORT).show(); - } - }, callbackHandler); - } - - @OnClick(R.id.pin_unpin) - public void onPinClicked(View v) { - final boolean isBookmarked = (v.getTag() == null)? true : !(Boolean)v.getTag(); - - String name = mediaTitle.getText().toString(); - String path = addonId; - - SharedPreferences prefs = getActivity().getSharedPreferences("addons", Context.MODE_PRIVATE); - Set bookmarks = new HashSet<>(prefs.getStringSet("bookmarked", Collections.emptySet())); - if (isBookmarked) - bookmarks.add(path); - else - bookmarks.remove(path); - prefs.edit() - .putStringSet("bookmarked", bookmarks) - .putString("name_" + path, name) - .apply(); - Toast.makeText(getActivity(), isBookmarked? R.string.addon_pinned : R.string.addon_unpinned, Toast.LENGTH_SHORT).show(); - setupPinButton(isBookmarked); - } - - private void setupPinButton(boolean bookmarked) { - Resources.Theme theme = getActivity().getTheme(); - TypedArray styledAttributes = - theme.obtainStyledAttributes(new int[] {R.attr.defaultButtonColorFilter, R.attr.colorAccent}); - Resources resources = getActivity().getResources(); - // Bookmarked button - if (bookmarked) { - pinButton.setColorFilter(styledAttributes.getColor(styledAttributes.getIndex(1), - resources.getColor(R.color.accent_default))); - } else { - pinButton.setColorFilter(styledAttributes.getColor(styledAttributes.getIndex(0), - resources.getColor(R.color.white))); - } - styledAttributes.recycle(); - pinButton.setTag(bookmarked); - pinButton.setVisibility(View.VISIBLE); - } - - private void updatePinButton() { - SharedPreferences prefs = getActivity().getSharedPreferences("addons", Context.MODE_PRIVATE); - Set bookmarked = prefs.getStringSet("bookmarked", Collections.emptySet()); - setupPinButton(bookmarked.contains(addonId)); + protected TabsAdapter createTabsAdapter(AbstractInfoFragment.DataHolder dataHolder) { + long baseFragmentId = 1000; + Bundle args = getArguments(); + return new TabsAdapter(getActivity(), getChildFragmentManager()) + .addTab(AddonInfoFragment.class, args, R.string.addon_overview, baseFragmentId++) + .addTab(MediaFileListFragment.class, contentArgs(args), R.string.addon_content, baseFragmentId++) + ; } } 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 new file mode 100644 index 0000000..07f0b3f --- /dev/null +++ b/app/src/main/java/org/xbmc/kore/ui/sections/addon/AddonInfoFragment.java @@ -0,0 +1,194 @@ +/* + * 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.sections.addon; + +import android.content.Context; +import android.content.SharedPreferences; +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; +import org.xbmc.kore.jsonrpc.ApiCallback; +import org.xbmc.kore.jsonrpc.method.Addons; +import org.xbmc.kore.jsonrpc.type.AddonType; +import org.xbmc.kore.ui.AbstractAdditionalInfoFragment; +import org.xbmc.kore.ui.AbstractInfoFragment; +import org.xbmc.kore.ui.generic.RefreshItem; +import org.xbmc.kore.utils.LogUtils; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * Presents addon details + */ +public class AddonInfoFragment extends AbstractInfoFragment { + private static final String TAG = LogUtils.makeLogTag(AddonInfoFragment.class); + + public static final String BUNDLE_KEY_ADDONID = "addonid"; + public static final String BUNDLE_KEY_BROWSABLE = "browsable"; + + /** + * Handler on which to post RPC callbacks + */ + private Handler callbackHandler = new Handler(); + + private String addonId; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + addonId = getDataHolder().getBundle().getString(BUNDLE_KEY_ADDONID, null); + } + + @Override + public void onActivityCreated (Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + setHasOptionsMenu(false); + } + + @Override + protected AbstractAdditionalInfoFragment getAdditionalInfoFragment() { + return null; + } + + @Override + protected RefreshItem createRefreshItem() { + return null; + } + + @Override + protected boolean setupMediaActionBar() { + boolean browsable = getDataHolder().getBundle().getBoolean(BUNDLE_KEY_BROWSABLE, true); + if (browsable) { + setupPinButton(); + } + + setupEnabledButton(); + return true; + } + + @Override + protected boolean setupFAB(ImageButton FAB) { + FAB.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Addons.ExecuteAddon action = new Addons.ExecuteAddon(addonId); + action.execute(getHostManager().getConnection(), new ApiCallback() { + @Override + public void onSuccess(String result) { + // Do nothing + } + + @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); + } + }); + return true; + } + + private void setupEnabledButton() { + setOnSeenListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + final Boolean isEnabled = (v.getTag() == null)? false : (Boolean)v.getTag(); + + Addons.SetAddonEnabled action = new Addons.SetAddonEnabled(addonId, !isEnabled); + action.execute(getHostManager().getConnection(), new ApiCallback() { + @Override + public void onSuccess(String result) { + if (!isAdded()) return; + int messageResId = (!isEnabled) ? R.string.addon_enabled : R.string.addon_disabled; + Toast.makeText(getActivity(), messageResId, Toast.LENGTH_SHORT).show(); + setSeenButtonState(!isEnabled); + setFabButtonState(!isEnabled); + } + + @Override + public void onError(int errorCode, String description) { + if (!isAdded()) return; + Toast.makeText(getActivity(), + String.format(getString(R.string.general_error_executing_action), description), + Toast.LENGTH_SHORT) + .show(); + } + }, callbackHandler); + } + }); + + // Get the addon details, this is done asyhnchronously + String[] properties = new String[] { + AddonType.Fields.ENABLED + }; + Addons.GetAddonDetails action = new Addons.GetAddonDetails( + addonId, properties); + action.execute(getHostManager().getConnection(), new ApiCallback() { + @Override + public void onSuccess(AddonType.Details result) { + if (!isAdded()) return; + setSeenButtonState(result.enabled); + setFabButtonState(result.enabled); + } + + @Override + public void onError(int errorCode, String description) { + if (!isAdded()) return; + Toast.makeText(getActivity(), + String.format(getString(R.string.error_getting_addon_info), description), + Toast.LENGTH_SHORT).show(); + } + }, callbackHandler); + } + + private void setupPinButton() { + setOnPinClickedListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + final boolean isBookmarked = (view.getTag() == null) ? true : !(Boolean) view.getTag(); + + String name = getDataHolder().getTitle(); + String path = addonId; + + SharedPreferences prefs = getActivity().getSharedPreferences("addons", Context.MODE_PRIVATE); + Set bookmarks = new HashSet<>(prefs.getStringSet("bookmarked", Collections.emptySet())); + if (isBookmarked) + bookmarks.add(path); + else + bookmarks.remove(path); + prefs.edit() + .putStringSet("bookmarked", bookmarks) + .putString("name_" + path, name) + .apply(); + Toast.makeText(getActivity(), isBookmarked ? R.string.addon_pinned : R.string.addon_unpinned, Toast.LENGTH_SHORT).show(); + setPinButtonState(!isBookmarked); + } + }); + + SharedPreferences prefs = getActivity().getSharedPreferences("addons", Context.MODE_PRIVATE); + Set bookmarked = prefs.getStringSet("bookmarked", Collections.emptySet()); + setPinButtonState(bookmarked.contains(addonId)); + } +} diff --git a/app/src/main/java/org/xbmc/kore/ui/sections/addon/AddonListContainerFragment.java b/app/src/main/java/org/xbmc/kore/ui/sections/addon/AddonListContainerFragment.java index e9d32a3..b1ff9f6 100644 --- a/app/src/main/java/org/xbmc/kore/ui/sections/addon/AddonListContainerFragment.java +++ b/app/src/main/java/org/xbmc/kore/ui/sections/addon/AddonListContainerFragment.java @@ -18,96 +18,44 @@ package org.xbmc.kore.ui.sections.addon; import android.content.Context; import android.content.SharedPreferences; import android.os.Bundle; -import android.support.v4.app.Fragment; -import android.support.v4.view.ViewPager; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import com.astuetz.PagerSlidingTabStrip; import org.xbmc.kore.R; +import org.xbmc.kore.ui.AbstractTabsFragment; +import org.xbmc.kore.ui.AbstractInfoFragment; import org.xbmc.kore.ui.sections.file.MediaFileListFragment; import org.xbmc.kore.utils.LogUtils; import org.xbmc.kore.utils.TabsAdapter; -import org.xbmc.kore.utils.UIUtils; import java.util.Collections; import java.util.Set; -import butterknife.ButterKnife; -import butterknife.InjectView; - -/** - * Container for the TV Show overview and Episodes list - */ -public class AddonListContainerFragment extends Fragment { +public class AddonListContainerFragment extends AbstractTabsFragment { private static final String TAG = LogUtils.makeLogTag(AddonListContainerFragment.class); - private TabsAdapter tabsAdapter; - - @InjectView(R.id.pager_tab_strip) PagerSlidingTabStrip pagerTabStrip; - @InjectView(R.id.pager) ViewPager viewPager; - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - if (container == null) { - // We're not being shown or there's nothing to show - return null; - } - - ViewGroup root = (ViewGroup) inflater.inflate(R.layout.fragment_default_view_pager, container, false); - ButterKnife.inject(this, root); - - tabsAdapter = new TabsAdapter(getActivity(), getChildFragmentManager()); - SharedPreferences prefs = getActivity().getSharedPreferences("addons", Context.MODE_PRIVATE); - Set bookmarked = prefs.getStringSet("bookmarked", Collections.emptySet()); - long baseFragmentId = 70 + bookmarked.size() * 100; - tabsAdapter.addTab(AddonListFragment.class, new Bundle(), R.string.addons, baseFragmentId); - for (String path: bookmarked) { - String name = prefs.getString("name_" + path, "Content"); - Bundle addon = new Bundle(); - addon.putString(AddonDetailsFragment.BUNDLE_KEY_NAME, name); - addon.putParcelable(MediaFileListFragment.ROOT_PATH, new MediaFileListFragment.FileLocation(name, "plugin://" + path, true)); - tabsAdapter.addTab(MediaFileListFragment.class, addon, name, ++baseFragmentId); - } - viewPager.setAdapter(tabsAdapter); - pagerTabStrip.setViewPager(viewPager); - - return root; - } - @Override public void onActivityCreated (Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); setHasOptionsMenu(false); } - public Fragment getCurrentTabFragment() { - return tabsAdapter.getItem(viewPager.getCurrentItem()); - } + @Override + protected TabsAdapter createTabsAdapter(AbstractInfoFragment.DataHolder dataHolder) { + Bundle arguments = dataHolder.getBundle(); + if (arguments == null) + arguments = new Bundle(); - public View getSharedElement() { - View view = getView(); - if (view == null) - return null; - - //Note: this works as R.id.poster is only used in TVShowOverviewFragment. - //If the same id is used in other fragments in the TabsAdapter we - //need to check which fragment is currently displayed - View artView = view.findViewById(R.id.poster); - View scrollView = view.findViewById(R.id.media_panel); - if (( artView != null ) && - ( scrollView != null ) && - UIUtils.isViewInBounds(scrollView, artView)) { - return artView; + TabsAdapter tabsAdapter = new TabsAdapter(getActivity(), getChildFragmentManager()); + SharedPreferences prefs = getActivity().getSharedPreferences("addons", Context.MODE_PRIVATE); + Set bookmarked = prefs.getStringSet("bookmarked", Collections.emptySet()); + long baseFragmentId = 70 + bookmarked.size() * 100; + tabsAdapter.addTab(AddonListFragment.class, new Bundle(), R.string.addons, baseFragmentId); + for (String path: bookmarked) { + String name = prefs.getString("name_" + path, "Content"); + arguments.putParcelable(MediaFileListFragment.ROOT_PATH, + new MediaFileListFragment.FileLocation(name, "plugin://" + path, true)); + tabsAdapter.addTab(MediaFileListFragment.class, arguments, name, ++baseFragmentId); } - return null; + return tabsAdapter; } } diff --git a/app/src/main/java/org/xbmc/kore/ui/sections/addon/AddonListFragment.java b/app/src/main/java/org/xbmc/kore/ui/sections/addon/AddonListFragment.java index dc87b06..90328ec 100644 --- a/app/src/main/java/org/xbmc/kore/ui/sections/addon/AddonListFragment.java +++ b/app/src/main/java/org/xbmc/kore/ui/sections/addon/AddonListFragment.java @@ -21,14 +21,12 @@ import android.content.Context; import android.content.res.Resources; import android.os.Bundle; import android.os.Handler; -import android.support.v4.app.Fragment; -import android.support.v4.widget.SwipeRefreshLayout; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.ArrayAdapter; -import android.widget.GridView; +import android.widget.BaseAdapter; import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; @@ -38,6 +36,8 @@ import org.xbmc.kore.host.HostManager; import org.xbmc.kore.jsonrpc.ApiCallback; import org.xbmc.kore.jsonrpc.method.Addons; import org.xbmc.kore.jsonrpc.type.AddonType; +import org.xbmc.kore.ui.AbstractInfoFragment; +import org.xbmc.kore.ui.AbstractListFragment; import org.xbmc.kore.utils.LogUtils; import org.xbmc.kore.utils.UIUtils; import org.xbmc.kore.utils.Utils; @@ -46,58 +46,27 @@ import java.util.Collections; import java.util.Comparator; import java.util.List; -import butterknife.ButterKnife; -import butterknife.InjectView; - /** * Fragment that presents the movie list */ -public class AddonListFragment extends Fragment - implements SwipeRefreshLayout.OnRefreshListener { +public class AddonListFragment extends AbstractListFragment { private static final String TAG = LogUtils.makeLogTag(AddonListFragment.class); public interface OnAddonSelectedListener { - public void onAddonSelected(ViewHolder vh); + void onAddonSelected(ViewHolder vh); } // Activity listener private OnAddonSelectedListener listenerActivity; - private HostManager hostManager; - - @InjectView(R.id.list) GridView addonsGridView; - @InjectView(R.id.swipe_refresh_layout) SwipeRefreshLayout swipeRefreshLayout; - @InjectView(android.R.id.empty) TextView emptyView; - /** * Handler on which to post RPC callbacks */ private Handler callbackHandler = new Handler(); - private AddonsAdapter adapter = null; - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - ViewGroup root = (ViewGroup) inflater.inflate(R.layout.fragment_generic_media_list, container, false); - ButterKnife.inject(this, root); - - hostManager = HostManager.getInstance(getActivity()); - - swipeRefreshLayout.setOnRefreshListener(this); - - emptyView.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - onRefresh(); - } - }); - addonsGridView.setEmptyView(emptyView); - addonsGridView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + protected AdapterView.OnItemClickListener createOnItemClickListener() { + return new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView parent, View view, int position, long id) { // Get the movie id from the tag @@ -105,27 +74,21 @@ public class AddonListFragment extends Fragment // Notify the activity listenerActivity.onAddonSelected(tag); } - }); - - if (adapter == null) { - adapter = new AddonsAdapter(getActivity(), R.layout.grid_item_addon); - } - addonsGridView.setAdapter(adapter); - - // Pad main content view to overlap with bottom system bar -// UIUtils.setPaddingForSystemBars(getActivity(), addonsGridView, false, false, true); -// addonsGridView.setClipToPadding(false); - - return root; + }; } + @Override + protected BaseAdapter createAdapter() { + return new AddonsAdapter(getActivity(), R.layout.grid_item_addon); + } + @Override public void onActivityCreated (Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); setHasOptionsMenu(false); - if (adapter.getCount() == 0) + if (getAdapter().getCount() == 0) callGetAddonsAndSetup(); } @@ -145,26 +108,12 @@ public class AddonListFragment extends Fragment listenerActivity = null; } - @Override - public void onResume() { - super.onResume(); - } - - @Override - public void onPause() { - super.onPause(); - } - - /** - * Swipe refresh layout callback - */ - /** {@inheritDoc} */ @Override public void onRefresh () { - if (hostManager.getHostInfo() != null) { + if (HostManager.getInstance(getActivity()).getHostInfo() != null) { callGetAddonsAndSetup(); } else { - swipeRefreshLayout.setRefreshing(false); + hideRefreshAnimation(); Toast.makeText(getActivity(), R.string.no_xbmc_configured, Toast.LENGTH_SHORT) .show(); } @@ -179,7 +128,9 @@ public class AddonListFragment extends Fragment * Get the addons list and setup the gridview */ private void callGetAddonsAndSetup() { - swipeRefreshLayout.setRefreshing(true); + final AddonsAdapter adapter = (AddonsAdapter) getAdapter(); + + showRefreshAnimation(); // Get the addon list, this is done asyhnchronously String[] properties = new String[] { AddonType.Fields.NAME, AddonType.Fields.VERSION, AddonType.Fields.SUMMARY, @@ -189,54 +140,60 @@ public class AddonListFragment extends Fragment AddonType.Fields.RATING, AddonType.Fields.ENABLED }; Addons.GetAddons action = new Addons.GetAddons(properties); - action.execute(hostManager.getConnection(), new ApiCallback>() { - @Override - public void onSuccess(List result) { - if (!isAdded()) return; - for (AddonType.Details addon : result) { - String regex = "\\[.*?\\]"; - addon.name = addon.name.replaceAll(regex, ""); - addon.description = addon.description.replaceAll(regex, ""); - addon.summary = addon.summary.replaceAll(regex, ""); - addon.author = addon.author.replaceAll(regex, ""); - } - Collections.sort(result, new AddonNameComparator()); - adapter.clear(); - for (AddonType.Details addon : result) { - if (addon.type.equals(AddonType.Types.UNKNOWN) || - addon.type.equals(AddonType.Types.XBMC_PYTHON_PLUGINSOURCE) || - addon.type.equals(AddonType.Types.XBMC_PYTHON_SCRIPT) || - addon.type.equals(AddonType.Types.XBMC_ADDON_AUDIO) || - addon.type.equals(AddonType.Types.XBMC_ADDON_EXECUTABLE) || - addon.type.equals(AddonType.Types.XBMC_ADDON_VIDEO) || - addon.type.equals(AddonType.Types.XBMC_ADDON_IMAGE)) { - adapter.add(addon); - } - } - // To prevent the empty text from appearing on the first load, set it now - emptyView.setText(getString(R.string.no_addons_found_refresh)); - adapter.notifyDataSetChanged(); - swipeRefreshLayout.setRefreshing(false); - } + action.execute(HostManager.getInstance(getActivity()).getConnection(), + new ApiCallback>() { + @Override + public void onSuccess(List result) { + if (!isAdded()) return; - @Override - public void onError(int errorCode, String description) { - if (!isAdded()) return; + for (AddonType.Details addon : result) { + String regex = "\\[.*?\\]"; + addon.name = addon.name.replaceAll(regex, ""); + addon.description = addon.description.replaceAll(regex, ""); + addon.summary = addon.summary.replaceAll(regex, ""); + addon.author = addon.author.replaceAll(regex, ""); + } + Collections.sort(result, new AddonNameComparator()); - // To prevent the empty text from appearing on the first load, set it now - emptyView.setText(getString(R.string.no_addons_found_refresh)); - Toast.makeText(getActivity(), - String.format(getString(R.string.error_getting_addon_info), description), - Toast.LENGTH_SHORT).show(); - swipeRefreshLayout.setRefreshing(false); - } - }, callbackHandler); + adapter.clear(); + for (AddonType.Details addon : result) { + if (addon.type.equals(AddonType.Types.UNKNOWN) || + addon.type.equals(AddonType.Types.XBMC_PYTHON_PLUGINSOURCE) || + addon.type.equals(AddonType.Types.XBMC_PYTHON_SCRIPT) || + addon.type.equals(AddonType.Types.XBMC_ADDON_AUDIO) || + addon.type.equals(AddonType.Types.XBMC_ADDON_EXECUTABLE) || + addon.type.equals(AddonType.Types.XBMC_ADDON_VIDEO) || + addon.type.equals(AddonType.Types.XBMC_ADDON_IMAGE)) { + adapter.add(addon); + } + } + + adapter.notifyDataSetChanged(); + hideRefreshAnimation(); + + if(adapter.getCount() == 0) { + getEmptyView().setText(R.string.no_addons_found_refresh); + } + } + + @Override + public void onError(int errorCode, String description) { + if (!isAdded()) return; + + Toast.makeText(getActivity(), + String.format(getString(R.string.error_getting_addon_info), description), + Toast.LENGTH_SHORT).show(); + hideRefreshAnimation(); + } + }, callbackHandler); } private class AddonsAdapter extends ArrayAdapter { private HostManager hostManager; private int artWidth, artHeight; + private String author; + private String version; public AddonsAdapter(Context context, int resource) { super(context, resource); @@ -246,8 +203,11 @@ public class AddonListFragment extends Fragment // Use the same dimensions as in the details fragment, so that it hits Picasso's cache when // the user transitions to that fragment, avoiding another call and imediatelly showing the image Resources resources = context.getResources(); - artWidth = resources.getDimensionPixelOffset(R.dimen.addondetail_poster_width);; - artHeight = resources.getDimensionPixelOffset(R.dimen.addondetail_poster_height);; + artWidth = resources.getDimensionPixelOffset(R.dimen.detail_poster_width_square); + artHeight = resources.getDimensionPixelOffset(R.dimen.detail_poster_height_square); + + author = context.getString(R.string.author); + version = context.getString(R.string.version); } /** {@inheritDoc} */ @@ -269,27 +229,26 @@ public class AddonListFragment extends Fragment final ViewHolder viewHolder = (ViewHolder)convertView.getTag(); AddonType.Details addonDetails = this.getItem(position); - // Save the movie id - viewHolder.addonId = addonDetails.addonid; - viewHolder.addonName = addonDetails.name; - viewHolder.author = addonDetails.author; - viewHolder.description = addonDetails.description; - viewHolder.summary = addonDetails.summary; - viewHolder.version = addonDetails.version; - viewHolder.fanart = addonDetails.fanart; - viewHolder.poster = addonDetails.thumbnail; - viewHolder.enabled = addonDetails.enabled; - viewHolder.browsable = AddonType.Types.XBMC_PYTHON_PLUGINSOURCE.equals(addonDetails.type); + viewHolder.dataHolder.setTitle(addonDetails.name); + viewHolder.dataHolder.setDescription(addonDetails.description); + viewHolder.dataHolder.setUndertitle(addonDetails.summary); + viewHolder.dataHolder.setFanArtUrl(addonDetails.fanart); + viewHolder.dataHolder.setPosterUrl(addonDetails.thumbnail); + viewHolder.dataHolder.setDetails(author + " " + addonDetails.author + "\n" + + version + " " +addonDetails.version); + viewHolder.dataHolder.getBundle().putString(AddonInfoFragment.BUNDLE_KEY_ADDONID, addonDetails.addonid); + viewHolder.dataHolder.getBundle().putBoolean(AddonInfoFragment.BUNDLE_KEY_BROWSABLE, + AddonType.Types.XBMC_PYTHON_PLUGINSOURCE.equals(addonDetails.type)); - viewHolder.titleView.setText(viewHolder.addonName); + viewHolder.titleView.setText(viewHolder.dataHolder.getTitle()); viewHolder.detailsView.setText(addonDetails.summary); UIUtils.loadImageWithCharacterAvatar(getContext(), hostManager, - addonDetails.thumbnail, viewHolder.addonName, + addonDetails.thumbnail, viewHolder.dataHolder.getTitle(), viewHolder.artView, artWidth, artHeight); if(Utils.isLollipopOrLater()) { - viewHolder.artView.setTransitionName("a"+viewHolder.addonId); + viewHolder.artView.setTransitionName("a"+addonDetails.addonid); } return convertView; } @@ -303,15 +262,6 @@ public class AddonListFragment extends Fragment TextView detailsView; ImageView artView; - String addonId; - String addonName; - String summary; - String author; - String version; - String description; - String fanart; - String poster; - Boolean enabled; - Boolean browsable; + AbstractInfoFragment.DataHolder dataHolder = new AbstractInfoFragment.DataHolder(0); } } diff --git a/app/src/main/java/org/xbmc/kore/ui/sections/addon/AddonOverviewFragment.java b/app/src/main/java/org/xbmc/kore/ui/sections/addon/AddonOverviewFragment.java deleted file mode 100644 index 52f0481..0000000 --- a/app/src/main/java/org/xbmc/kore/ui/sections/addon/AddonOverviewFragment.java +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright 2016 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.sections.addon; - -import android.annotation.TargetApi; -import android.os.Bundle; -import android.support.v4.app.Fragment; -import android.support.v4.view.ViewPager; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import com.astuetz.PagerSlidingTabStrip; - -import org.xbmc.kore.R; -import org.xbmc.kore.ui.sections.file.MediaFileListFragment; -import org.xbmc.kore.utils.LogUtils; -import org.xbmc.kore.utils.TabsAdapter; -import org.xbmc.kore.utils.UIUtils; -import org.xbmc.kore.utils.Utils; - -import butterknife.ButterKnife; -import butterknife.InjectView; - -/** - * Container for the TV Show overview and Episodes list - */ -public class AddonOverviewFragment extends SharedElementFragment { - private static final String TAG = LogUtils.makeLogTag(AddonOverviewFragment.class); - - private TabsAdapter tabsAdapter; - - @InjectView(R.id.pager_tab_strip) PagerSlidingTabStrip pagerTabStrip; - @InjectView(R.id.pager) ViewPager viewPager; - - /** - * Create a new instance of this, initialized to show the addon addonId - */ - @TargetApi(21) - public static AddonOverviewFragment newInstance(AddonListFragment.ViewHolder vh) { - AddonOverviewFragment fragment = new AddonOverviewFragment(); - - Bundle args = new Bundle(); - args.putString(AddonDetailsFragment.BUNDLE_KEY_ADDONID, vh.addonId); - args.putString(AddonDetailsFragment.BUNDLE_KEY_NAME, vh.addonName); - args.putString(AddonDetailsFragment.BUNDLE_KEY_AUTHOR, vh.author); - args.putString(AddonDetailsFragment.BUNDLE_KEY_VERSION, vh.version); - args.putString(AddonDetailsFragment.BUNDLE_KEY_SUMMARY, vh.summary); - args.putString(AddonDetailsFragment.BUNDLE_KEY_DESCRIPTION, vh.description); - args.putString(AddonDetailsFragment.BUNDLE_KEY_FANART, vh.fanart); - args.putString(AddonDetailsFragment.BUNDLE_KEY_POSTER, vh.poster); - args.putBoolean(AddonDetailsFragment.BUNDLE_KEY_ENABLED, vh.enabled); - args.putBoolean(AddonDetailsFragment.BUNDLE_KEY_BROWSABLE, vh.browsable); - - if( Utils.isLollipopOrLater()) { - args.putString(AddonDetailsFragment.POSTER_TRANS_NAME, vh.artView.getTransitionName()); - } - fragment.setArguments(args); - return fragment; - } - - public Bundle contentArgs(Bundle details) { - String name = details.getString(AddonDetailsFragment.BUNDLE_KEY_NAME, "Content"); - String path = details.getString(AddonDetailsFragment.BUNDLE_KEY_ADDONID); - - Bundle content = new Bundle(); - content.putString(AddonDetailsFragment.BUNDLE_KEY_NAME, name); - MediaFileListFragment.FileLocation rootPath = new MediaFileListFragment.FileLocation(name, "plugin://" + path, true); - rootPath.setRootDir(true); - content.putParcelable(MediaFileListFragment.ROOT_PATH, rootPath); - content.putBoolean(MediaFileListFragment.DELAY_LOAD, true); - return content; - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - Bundle args = getArguments(); - - if ((container == null) || (args == null)) { - // We're not being shown or there's nothing to show - return null; - } - - ViewGroup root = (ViewGroup) inflater.inflate(R.layout.fragment_default_view_pager, container, false); - ButterKnife.inject(this, root); - - long baseFragmentId = 1000; - tabsAdapter = new TabsAdapter(getActivity(), getChildFragmentManager()) - .addTab(AddonDetailsFragment.class, args, R.string.addon_overview, baseFragmentId++) - .addTab(MediaFileListFragment.class, contentArgs(args), R.string.addon_content, baseFragmentId++) - ; - viewPager.setAdapter(tabsAdapter); - pagerTabStrip.setViewPager(viewPager); - - return root; - } - - @Override - public void onActivityCreated (Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - setHasOptionsMenu(false); - } - - public Fragment getCurrentTabFragment() { - return tabsAdapter.getItem(viewPager.getCurrentItem()); - } - - @Override - public View getSharedElement() { - View view = getView(); - if (view == null) - return null; - - //Note: this works as R.id.poster is only used in TVShowOverviewFragment. - //If the same id is used in other fragments in the TabsAdapter we - //need to check which fragment is currently displayed - View artView = view.findViewById(R.id.poster); - View scrollView = view.findViewById(R.id.media_panel); - if (( artView != null ) && - ( scrollView != null ) && - UIUtils.isViewInBounds(scrollView, artView)) { - return artView; - } - - return null; - } -} diff --git a/app/src/main/java/org/xbmc/kore/ui/sections/addon/AddonsActivity.java b/app/src/main/java/org/xbmc/kore/ui/sections/addon/AddonsActivity.java index afb5bb9..f300b32 100644 --- a/app/src/main/java/org/xbmc/kore/ui/sections/addon/AddonsActivity.java +++ b/app/src/main/java/org/xbmc/kore/ui/sections/addon/AddonsActivity.java @@ -18,27 +18,24 @@ package org.xbmc.kore.ui.sections.addon; import android.annotation.TargetApi; import android.content.Intent; import android.os.Bundle; +import android.support.v4.app.Fragment; import android.support.v4.app.FragmentTransaction; import android.support.v4.widget.DrawerLayout; import android.support.v7.app.ActionBar; import android.support.v7.widget.Toolbar; -import android.transition.Transition; -import android.transition.TransitionInflater; import android.view.Menu; import android.view.MenuItem; -import android.view.View; import android.view.Window; import org.xbmc.kore.R; +import org.xbmc.kore.ui.AbstractFragment; import org.xbmc.kore.ui.BaseActivity; import org.xbmc.kore.ui.generic.NavigationDrawerFragment; import org.xbmc.kore.ui.sections.remote.RemoteActivity; import org.xbmc.kore.utils.LogUtils; +import org.xbmc.kore.utils.SharedElementTransition; import org.xbmc.kore.utils.Utils; -import java.util.List; -import java.util.Map; - /** * Controls the presentation of Addons information (list, details) * All the information is presented by specific fragments @@ -49,13 +46,14 @@ public class AddonsActivity extends BaseActivity public static final String ADDONID = "addon_id"; public static final String ADDONTITLE = "addon_title"; + public static final String LISTFRAGMENT_TAG = "addonlist"; private String selectedAddonId; private String selectedAddonTitle; private NavigationDrawerFragment navigationDrawerFragment; - private boolean clearSharedElements; + private SharedElementTransition sharedElementTransition = new SharedElementTransition(); @TargetApi(21) @Override @@ -72,54 +70,28 @@ public class AddonsActivity extends BaseActivity .findFragmentById(R.id.navigation_drawer); navigationDrawerFragment.setUp(R.id.navigation_drawer, (DrawerLayout) findViewById(R.id.drawer_layout)); + Fragment fragment; if (savedInstanceState == null) { - AddonListContainerFragment addonListFragment = new AddonListContainerFragment(); + fragment = new AddonListContainerFragment(); - // Setup animations - if (Utils.isLollipopOrLater()) { - //Fade added to prevent shared element from disappearing very shortly at the start of the transition. - Transition fade = TransitionInflater - .from(this) - .inflateTransition(android.R.transition.fade); - addonListFragment.setExitTransition(fade); - addonListFragment.setReenterTransition(fade); - addonListFragment.setSharedElementReturnTransition(TransitionInflater.from( - this).inflateTransition(R.transition.change_image)); - - android.support.v4.app.SharedElementCallback seCallback = new android.support.v4.app.SharedElementCallback() { - @Override - public void onMapSharedElements(List names, Map sharedElements) { - if (clearSharedElements) { - names.clear(); - sharedElements.clear(); - clearSharedElements = false; - } - } - }; - addonListFragment.setExitSharedElementCallback(seCallback); - } getSupportFragmentManager() .beginTransaction() - .add(R.id.fragment_container, addonListFragment) + .add(R.id.fragment_container, fragment, LISTFRAGMENT_TAG) .commit(); } else { + fragment = getSupportFragmentManager().findFragmentByTag(LISTFRAGMENT_TAG); + selectedAddonId = savedInstanceState.getString(ADDONID, null); selectedAddonTitle = savedInstanceState.getString(ADDONTITLE, null); } + if (Utils.isLollipopOrLater()) { + sharedElementTransition.setupExitTransition(this, fragment); + } + setupActionBar(selectedAddonTitle); } - @Override - public void onResume() { - super.onResume(); - } - - @Override - public void onPause() { - super.onPause(); - } - @Override protected void onSaveInstanceState (Bundle outState) { super.onSaveInstanceState(outState); @@ -205,48 +177,26 @@ public class AddonsActivity extends BaseActivity */ @TargetApi(21) public void onAddonSelected(AddonListFragment.ViewHolder vh) { - selectedAddonId = vh.addonId; - selectedAddonTitle = vh.addonName; + Bundle bundle = vh.dataHolder.getBundle(); + selectedAddonId = bundle.getString(AddonInfoFragment.BUNDLE_KEY_ADDONID); + selectedAddonTitle = vh.dataHolder.getTitle(); // Replace list fragment - final SharedElementFragment addonDetailsFragment = - vh.browsable - ? AddonOverviewFragment.newInstance(vh) - : AddonDetailsFragment.newInstance(vh) + final AbstractFragment addonDetailsFragment = + bundle.getBoolean(AddonInfoFragment.BUNDLE_KEY_BROWSABLE) + ? new AddonDetailsFragment() + : new AddonInfoFragment() ; + addonDetailsFragment.setDataHolder(vh.dataHolder); + vh.dataHolder.setSquarePoster(true); + vh.dataHolder.setPosterTransitionName(vh.artView.getTransitionName()); + FragmentTransaction fragTrans = getSupportFragmentManager().beginTransaction(); // Set up transitions if (Utils.isLollipopOrLater()) { - android.support.v4.app.SharedElementCallback seCallback = new android.support.v4.app.SharedElementCallback() { - @Override - public void onMapSharedElements(List names, Map sharedElements) { - //On returning onMapSharedElements for the exiting fragment is called before the onMapSharedElements - // for the reentering fragment. We use this to determine if we are returning and if - // we should clear the shared element lists. Note that, clearing must be done in the reentering fragment - // as this is called last. Otherwise it the app will crash during transition setup. Not sure, but might - // be a v4 support package bug. - if (addonDetailsFragment.isVisible()) { - View sharedView = addonDetailsFragment.getSharedElement(); - if (sharedView == null) { // shared element not visible - clearSharedElements = true; - } - } - } - }; - addonDetailsFragment.setEnterSharedElementCallback(seCallback); - - addonDetailsFragment.setEnterTransition(TransitionInflater - .from(this) - .inflateTransition(R.transition.media_details)); - addonDetailsFragment.setReturnTransition(null); - - Transition changeImageTransition = TransitionInflater.from( - this).inflateTransition(R.transition.change_image); - addonDetailsFragment.setSharedElementReturnTransition(changeImageTransition); - addonDetailsFragment.setSharedElementEnterTransition(changeImageTransition); - - fragTrans.addSharedElement(vh.artView, vh.artView.getTransitionName()); + sharedElementTransition.setupEnterTransition(this, fragTrans, addonDetailsFragment, + vh.artView); } else { fragTrans.setCustomAnimations(R.anim.fragment_details_enter, 0, R.anim.fragment_list_popenter, 0); diff --git a/app/src/main/java/org/xbmc/kore/ui/sections/audio/AlbumDetailsFragment.java b/app/src/main/java/org/xbmc/kore/ui/sections/audio/AlbumDetailsFragment.java deleted file mode 100644 index 65125d0..0000000 --- a/app/src/main/java/org/xbmc/kore/ui/sections/audio/AlbumDetailsFragment.java +++ /dev/null @@ -1,677 +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.sections.audio; - -import android.annotation.TargetApi; -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.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.PopupMenu; -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.host.HostManager; -import org.xbmc.kore.jsonrpc.ApiCallback; -import org.xbmc.kore.jsonrpc.ApiMethod; -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.type.PlaylistType; -import org.xbmc.kore.provider.MediaContract; -import org.xbmc.kore.ui.AbstractDetailsFragment; -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.util.ArrayList; -import java.util.Locale; - -import at.blogc.android.views.ExpandableTextView; -import butterknife.ButterKnife; -import butterknife.InjectView; -import butterknife.OnClick; - -/** - * Presents movie details - */ -public class AlbumDetailsFragment extends AbstractDetailsFragment - implements LoaderManager.LoaderCallbacks { - private static final String TAG = LogUtils.makeLogTag(AlbumDetailsFragment.class); - - public static final String BUNDLE_KEY_ALBUMID = "album_id"; - public static final String POSTER_TRANS_NAME = "POSTER_TRANS_NAME"; - public static final String BUNDLE_KEY_ALBUMARTIST = "album_artist"; - public static final String BUNDLE_KEY_ALBUMTITLE = "album_title"; - public static final String BUNDLE_KEY_ALBUMGENRE = "album_genre"; - public static final String BUNDLE_KEY_ALBUMYEAR = "album_year"; - public static final String BUNDLE_KEY_ALBUMRATING = "album_rating"; - - // Loader IDs - private static final int LOADER_ALBUM = 0, - LOADER_SONGS = 1; - - private HostManager hostManager; - private HostInfo hostInfo; - - /** - * Handler on which to post RPC callbacks - */ - private Handler callbackHandler = new Handler(); - - // Displayed album id - private int albumId = -1; - - // Album information - private String albumDisplayArtist; - private String albumTitle; - private ArrayList songInfoList = null; - - @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.download) ImageButton downloadButton; - - // 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.media_description_container) LinearLayout mediaDescriptionContainer; - @InjectView(R.id.media_description) ExpandableTextView mediaDescription; - @InjectView(R.id.show_all) ImageView mediaShowAll; - - @InjectView(R.id.song_list) LinearLayout songListView; - - /** - * Create a new instance of this, initialized to show the album albumId - */ - @TargetApi(21) - public static AlbumDetailsFragment newInstance(AlbumListFragment.ViewHolder vh) { - AlbumDetailsFragment fragment = new AlbumDetailsFragment(); - - Bundle args = new Bundle(); - args.putInt(BUNDLE_KEY_ALBUMID, vh.albumId); - args.putString(BUNDLE_KEY_ALBUMTITLE, vh.albumTitle); - args.putString(BUNDLE_KEY_ALBUMARTIST, vh.albumArtist); - args.putString(BUNDLE_KEY_ALBUMGENRE, vh.albumGenre); - args.putInt(BUNDLE_KEY_ALBUMYEAR, vh.albumYear); - args.putDouble(BUNDLE_KEY_ALBUMRATING, vh.albumRating); - - 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) { - albumId = getArguments().getInt(BUNDLE_KEY_ALBUMID, -1); - - if ((container == null) || (albumId == -1)) { - // We're not being shown or there's nothing to show - return null; - } - - ViewGroup root = (ViewGroup) inflater.inflate(R.layout.fragment_album_details, container, false); - ButterKnife.inject(this, root); - - hostManager = HostManager.getInstance(getActivity()); - hostInfo = hostManager.getHostInfo(); - - // 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); - - Bundle bundle = getArguments(); - - if(Utils.isLollipopOrLater()) { - mediaPoster.setTransitionName(bundle.getString(POSTER_TRANS_NAME)); - } - - mediaTitle.setText(bundle.getString(BUNDLE_KEY_ALBUMTITLE)); - mediaUndertitle.setText(bundle.getString(BUNDLE_KEY_ALBUMARTIST)); - setMediaYear(bundle.getString(BUNDLE_KEY_ALBUMGENRE), bundle.getInt(BUNDLE_KEY_ALBUMYEAR)); - setMediaRating(bundle.getDouble(BUNDLE_KEY_ALBUMRATING)); - - // 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 null; - } - - @Override - protected String getSyncID() { - return null; - } - - @Override - protected int getSyncItemID() { - return 0; - } - - @Override - protected SwipeRefreshLayout getSwipeRefreshLayout() { - return null; - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - } - - @Override - public void onActivityCreated (Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - - // Start the loaders - getLoaderManager().initLoader(LOADER_ALBUM, null, this); - - setHasOptionsMenu(false); - } - - @Override - public void onResume() { - // Force the exit view to invisible - exitTransitionView.setVisibility(View.INVISIBLE); - super.onResume(); - } - - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - } - - /** - * Loader callbacks - */ - /** {@inheritDoc} */ - @Override - public Loader onCreateLoader(int i, Bundle bundle) { - Uri uri; - switch (i) { - case LOADER_ALBUM: - uri = MediaContract.Albums.buildAlbumUri(hostInfo.getId(), albumId); - return new CursorLoader(getActivity(), uri, - AlbumDetailsQuery.PROJECTION, null, null, null); - case LOADER_SONGS: - uri = MediaContract.Songs.buildAlbumSongsListUri(hostInfo.getId(), albumId); - return new CursorLoader(getActivity(), uri, - AlbumSongsListQuery.PROJECTION, null, null, AlbumSongsListQuery.SORT); - default: - return null; - } - } - - /** {@inheritDoc} */ - @Override - public void onLoadFinished(Loader cursorLoader, Cursor cursor) { - if (cursor != null && cursor.getCount() > 0) { - switch (cursorLoader.getId()) { - case LOADER_ALBUM: - displayAlbumDetails(cursor); - getLoaderManager().initLoader(LOADER_SONGS, null, this); - break; - case LOADER_SONGS: - displaySongsList(cursor); - break; - } - } - } - - /** {@inheritDoc} */ - @Override - public void onLoaderReset(Loader cursorLoader) { - // Release loader's data - } - - /** - * Callbacks for button bar - */ - private ApiCallback defaultStringActionCallback = ApiMethod.getDefaultActionCallback(); - - @OnClick(R.id.fab) - public void onFabClicked(View v) { - PlaylistType.Item item = new PlaylistType.Item(); - item.albumid = albumId; - Player.Open action = new Player.Open(item); - action.execute(hostManager.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) { - addToPlaylist(TYPE_ALBUM, albumId); - } - - @Override - protected void onDownload() { - UIUtils.downloadSongs(getActivity(), songInfoList, hostInfo, callbackHandler); - } - - @Override - protected void onSyncProcessEnded(MediaSyncEvent event) { - - } - - /** - * Display the album details - * - * @param cursor Cursor with the data - */ - private void displayAlbumDetails(Cursor cursor) { - final Resources resources = getActivity().getResources(); - - cursor.moveToFirst(); - albumTitle = cursor.getString(AlbumDetailsQuery.TITLE); - albumDisplayArtist = cursor.getString(AlbumDetailsQuery.DISPLAYARTIST); - mediaTitle.setText(albumTitle); - mediaUndertitle.setText(albumDisplayArtist); - - setMediaYear(cursor.getString(AlbumDetailsQuery.GENRE), cursor.getInt(AlbumDetailsQuery.YEAR)); - - double rating = cursor.getDouble(AlbumDetailsQuery.RATING); - if (rating > 0) { - mediaRating.setVisibility(View.VISIBLE); - mediaMaxRating.setVisibility(View.VISIBLE); - setMediaRating(rating); - } else { - mediaRating.setVisibility(View.GONE); - mediaMaxRating.setVisibility(View.GONE); - } - - String description = cursor.getString(AlbumDetailsQuery.DESCRIPTION); - if (!TextUtils.isEmpty(description)) { - mediaDescription.setVisibility(View.VISIBLE); - mediaDescription.setText(description); - - Resources.Theme theme = getActivity().getTheme(); - TypedArray styledAttributes = theme.obtainStyledAttributes(new int[] { - R.attr.iconExpand, - R.attr.iconCollapse - }); - final int iconCollapseResId = - styledAttributes.getResourceId(styledAttributes.getIndex(0), R.drawable.ic_expand_less_white_24dp); - final int iconExpandResId = - styledAttributes.getResourceId(styledAttributes.getIndex(1), R.drawable.ic_expand_more_white_24dp); - styledAttributes.recycle(); - mediaDescriptionContainer.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - mediaDescription.toggle(); - mediaShowAll.setImageResource(mediaDescription.isExpanded() ? iconCollapseResId: iconExpandResId); - } - }); - } else { - mediaDescriptionContainer.setVisibility(View.GONE); - } - - // Images - DisplayMetrics displayMetrics = new DisplayMetrics(); - getActivity().getWindowManager().getDefaultDisplay().getMetrics(displayMetrics); - - String fanart = cursor.getString(AlbumDetailsQuery.FANART), - poster = cursor.getString(AlbumDetailsQuery.THUMBNAIL); - - int artHeight = resources.getDimensionPixelOffset(R.dimen.now_playing_art_height), - artWidth = displayMetrics.widthPixels; - int posterWidth = resources.getDimensionPixelOffset(R.dimen.albumdetail_poster_width); - int posterHeight = resources.getDimensionPixelOffset(R.dimen.albumdetail_poster_heigth); - UIUtils.loadImageWithCharacterAvatar(getActivity(), hostManager, - poster, albumTitle, - mediaPoster, posterWidth, posterHeight); - UIUtils.loadImageIntoImageview(hostManager, - TextUtils.isEmpty(fanart)? poster : fanart, - mediaArt, artWidth, artHeight); - } - - private void setMediaRating(double rating) { - mediaRating.setText(String.format(Locale.getDefault(), "%01.01f", rating)); - mediaMaxRating.setText(getString(R.string.max_rating_music)); - } - - private void setMediaYear(String genres, int year) { - String label = (year > 0) ? - (!TextUtils.isEmpty(genres) ? - genres + " | " + String.valueOf(year) : - String.valueOf(year)) : - genres; - mediaYear.setText(label); - } - - /** - * Starts playing the song on XBMC - * @param songId song to play - */ - private void playSong(int songId) { - PlaylistType.Item item = new PlaylistType.Item(); - item.songid = songId; - Player.Open action = new Player.Open(item); - action.execute(hostManager.getConnection(), defaultStringActionCallback, callbackHandler); - } - - private int TYPE_ALBUM = 0, - TYPE_SONG = 1; - /** - * Adds an album or a song to the audio playlist - * @param type Album or song - * @param id albumId or songId - */ - private void addToPlaylist(final int type, final int id) { - Playlist.GetPlaylists getPlaylists = new Playlist.GetPlaylists(); - - getPlaylists.execute(hostManager.getConnection(), new ApiCallback>() { - @Override - public void onSuccess(ArrayList result) { - if (!isAdded()) return; - // Ok, loop through the playlists, looking for the audio one - int audioPlaylistId = -1; - for (PlaylistType.GetPlaylistsReturnType playlist : result) { - if (playlist.type.equals(PlaylistType.GetPlaylistsReturnType.AUDIO)) { - audioPlaylistId = playlist.playlistid; - break; - } - } - // If found, add to playlist - if (audioPlaylistId != -1) { - PlaylistType.Item item = new PlaylistType.Item(); - if (type == TYPE_ALBUM) { - item.albumid = id; - } else { - item.songid = id; - } - Playlist.Add action = new Playlist.Add(audioPlaylistId, item); - action.execute(hostManager.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); - } - - View.OnClickListener songClickListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - playSong(((FileDownloadHelper.SongInfo)v.getTag()).songId); - } - }; - - private View.OnClickListener songItemMenuClickListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - final FileDownloadHelper.SongInfo songInfo = ((FileDownloadHelper.SongInfo)v.getTag()); - final int songId = songInfo.songId; - - final PopupMenu popupMenu = new PopupMenu(getActivity(), v); - popupMenu.getMenuInflater().inflate(R.menu.song_item, popupMenu.getMenu()); - popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { - @Override - public boolean onMenuItemClick(MenuItem item) { - switch (item.getItemId()) { - case R.id.action_play_song: - playSong(songId); - return true; - case R.id.action_add_to_playlist: - addToPlaylist(TYPE_SONG, songId); - return true; - case R.id.download: - ArrayList songInfoList = new ArrayList<>(); - songInfoList.add(songInfo); - UIUtils.downloadSongs(getActivity(), songInfoList, - hostInfo, callbackHandler); - return true; - } - return false; - } - }); - popupMenu.show(); - } - }; - - /** - * Display the songs - * - * @param cursor Cursor with the data - */ - private void displaySongsList(Cursor cursor) { - if (cursor.moveToFirst()) { - songInfoList = new ArrayList<>(cursor.getCount()); - do { - View songView = LayoutInflater.from(getActivity()) - .inflate(R.layout.list_item_song, songListView, false); - TextView songTitle = (TextView)songView.findViewById(R.id.song_title); - TextView trackNumber = (TextView)songView.findViewById(R.id.track_number); - TextView details = (TextView)songView.findViewById(R.id.details); - ImageView contextMenu = (ImageView)songView.findViewById(R.id.list_context_menu); - - String artist = cursor.getString(AlbumSongsListQuery.ARTIST); - - // Add this song to the list - FileDownloadHelper.SongInfo songInfo = new FileDownloadHelper.SongInfo( - artist, - albumTitle, - cursor.getInt(AlbumSongsListQuery.SONGID), - cursor.getInt(AlbumSongsListQuery.TRACK), - cursor.getString(AlbumSongsListQuery.TITLE), - cursor.getString(AlbumSongsListQuery.FILE)); - songInfoList.add(songInfo); - - songTitle.setText(songInfo.title); - - - trackNumber.setText(String.valueOf(songInfo.track)); - - String duration = UIUtils.formatTime(cursor.getInt(AlbumSongsListQuery.DURATION)); - String detailsText = TextUtils.isEmpty(artist) ? duration : duration + " | " + artist; - details.setText(detailsText); - - contextMenu.setTag(songInfo); - contextMenu.setOnClickListener(songItemMenuClickListener); - - songView.setTag(songInfo); - songView.setOnClickListener(songClickListener); - songListView.addView(songView); - } while (cursor.moveToNext()); - - if (!songInfoList.isEmpty()) { - // Check if download dir exists - FileDownloadHelper.SongInfo songInfo = new FileDownloadHelper.SongInfo - (albumDisplayArtist, albumTitle, 0, 0, null, null); - if (songInfo.downloadDirectoryExists()) { - 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(); - } - } - } - } - - /** - * Returns the shared element if visible - * @return View if visible, null otherwise - */ - public View getSharedElement() { - if (UIUtils.isViewInBounds(mediaPanel, mediaPoster)) { - return mediaPoster; - } - - return null; - } - - /** - * Album details query parameters. - */ - public interface AlbumDetailsQuery { - String[] PROJECTION = { - BaseColumns._ID, - MediaContract.Albums.TITLE, - MediaContract.Albums.DISPLAYARTIST, - MediaContract.Albums.THUMBNAIL, - MediaContract.Albums.FANART, - MediaContract.Albums.YEAR, - MediaContract.Albums.GENRE, - MediaContract.Albums.ALBUMLABEL, - MediaContract.Albums.DESCRIPTION, - MediaContract.Albums.RATING, - }; - - int ID = 0; - int TITLE = 1; - int DISPLAYARTIST = 2; - int THUMBNAIL = 3; - int FANART = 4; - int YEAR = 5; - int GENRE = 6; - int ALBUMLABEL = 7; - int DESCRIPTION = 8; - int RATING = 9; - } - - /** - * Album songs list query parameters. - */ - public interface AlbumSongsListQuery { - String[] PROJECTION = { - BaseColumns._ID, - MediaContract.Songs.TITLE, - MediaContract.Songs.TRACK, - MediaContract.Songs.DURATION, - MediaContract.Songs.FILE, - MediaContract.Songs.SONGID, - MediaContract.Songs.DISPLAYARTIST, - MediaContract.Songs.DISC - }; - - String SORT = MediaContract.Songs.DISC + " ASC, " + MediaContract.Songs.TRACK + " ASC"; - - int ID = 0; - int TITLE = 1; - int TRACK = 2; - int DURATION = 3; - int FILE = 4; - int SONGID = 5; - int ARTIST = 6; - int DISC = 7; - } -} 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 new file mode 100644 index 0000000..2b1851c --- /dev/null +++ b/app/src/main/java/org/xbmc/kore/ui/sections/audio/AlbumInfoFragment.java @@ -0,0 +1,266 @@ +/* + * 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.sections.audio; + +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.provider.BaseColumns; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.CursorLoader; +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; +import org.xbmc.kore.host.HostManager; +import org.xbmc.kore.jsonrpc.ApiCallback; +import org.xbmc.kore.jsonrpc.event.MediaSyncEvent; +import org.xbmc.kore.jsonrpc.method.Playlist; +import org.xbmc.kore.jsonrpc.type.PlaylistType; +import org.xbmc.kore.provider.MediaContract; +import org.xbmc.kore.service.library.LibrarySyncService; +import org.xbmc.kore.ui.AbstractAdditionalInfoFragment; +import org.xbmc.kore.ui.AbstractInfoFragment; +import org.xbmc.kore.ui.generic.RefreshItem; +import org.xbmc.kore.utils.FileDownloadHelper; +import org.xbmc.kore.utils.LogUtils; +import org.xbmc.kore.utils.UIUtils; + +import java.util.ArrayList; + +/** + * Presents album details + */ +public class AlbumInfoFragment extends AbstractInfoFragment + implements LoaderManager.LoaderCallbacks { + private static final String TAG = LogUtils.makeLogTag(AlbumInfoFragment.class); + + private static final int LOADER_ALBUM = 0; + + private Handler callbackHandler = new Handler(); + private AlbumSongsListFragment albumSongsListFragment; + + @Override + public void onActivityCreated (Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + getLoaderManager().initLoader(LOADER_ALBUM, null, this); + + setHasOptionsMenu(false); + + + } + + /** + * Loader callbacks + */ + /** {@inheritDoc} */ + @Override + public Loader onCreateLoader(int i, Bundle bundle) { + Uri uri; + int albumId = getDataHolder().getId(); + int id = HostManager.getInstance(getActivity()).getHostInfo().getId(); + switch (i) { + case LOADER_ALBUM: + uri = MediaContract.Albums.buildAlbumUri(id, albumId); + return new CursorLoader(getActivity(), uri, + AlbumDetailsQuery.PROJECTION, null, null, null); + default: + return null; + } + } + + /** {@inheritDoc} */ + @Override + public void onLoadFinished(Loader cursorLoader, Cursor cursor) { + if (cursor != null && cursor.getCount() > 0) { + switch (cursorLoader.getId()) { + case LOADER_ALBUM: + cursor.moveToFirst(); + + DataHolder dataHolder = getDataHolder(); + + dataHolder.setRating(cursor.getDouble(AlbumDetailsQuery.RATING)); + dataHolder.setTitle(cursor.getString(AlbumDetailsQuery.TITLE)); + dataHolder.setUndertitle(cursor.getString(AlbumDetailsQuery.DISPLAYARTIST)); + dataHolder.setDescription(cursor.getString(AlbumDetailsQuery.DESCRIPTION)); + dataHolder.setFanArtUrl(cursor.getString(AlbumInfoFragment.AlbumDetailsQuery.FANART)); + dataHolder.setPosterUrl(cursor.getString(AlbumInfoFragment.AlbumDetailsQuery.THUMBNAIL)); + + int year = cursor.getInt(AlbumDetailsQuery.YEAR); + String genres = cursor.getString(AlbumDetailsQuery.GENRE); + dataHolder.setDetails ( (year > 0) ? + (!TextUtils.isEmpty(genres) ? + genres + " | " + String.valueOf(year) : + String.valueOf(year)) : + genres + ); + + FileDownloadHelper.SongInfo songInfo = new FileDownloadHelper.SongInfo + (dataHolder.getUnderTitle(), dataHolder.getTitle(), 0, 0, null, null); + setDownloadButtonState(songInfo.downloadDirectoryExists()); + + updateView(dataHolder); + break; + } + } + } + + /** {@inheritDoc} */ + @Override + public void onLoaderReset(Loader cursorLoader) { + // Release loader's data + } + + @Override + protected AbstractAdditionalInfoFragment getAdditionalInfoFragment() { + DataHolder dataHolder = getDataHolder(); + albumSongsListFragment = new AlbumSongsListFragment(); + albumSongsListFragment.setAlbum(dataHolder.getId(), dataHolder.getTitle()); + return albumSongsListFragment; + } + + @Override + protected RefreshItem createRefreshItem() { + RefreshItem refreshItem = new RefreshItem(getActivity(), LibrarySyncService.SYNC_ALL_MUSIC); + refreshItem.setListener(new RefreshItem.RefreshItemListener() { + @Override + public void onSyncProcessEnded(MediaSyncEvent event) { + if (event.status == MediaSyncEvent.STATUS_SUCCESS) + getLoaderManager().restartLoader(LOADER_ALBUM, null, AlbumInfoFragment.this); + } + }); + return refreshItem; + } + + @Override + protected boolean setupMediaActionBar() { + setOnDownloadListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + UIUtils.downloadSongs(getActivity(), albumSongsListFragment.getSongInfoList(), + getHostInfo(), callbackHandler); + } + }); + + setOnAddToPlaylistListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + addToPlaylist(); + } + }); + + return true; + } + + @Override + protected boolean setupFAB(ImageButton FAB) { + FAB.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + PlaylistType.Item item = new PlaylistType.Item(); + item.albumid = getDataHolder().getId(); + fabActionPlayItem(item); + } + }); + return true; + } + + private void addToPlaylist() { + Playlist.GetPlaylists getPlaylists = new Playlist.GetPlaylists(); + + getPlaylists.execute(HostManager.getInstance(getActivity()).getConnection(), new ApiCallback>() { + @Override + public void onSuccess(ArrayList result) { + if (!isAdded()) return; + // Ok, loop through the playlists, looking for the audio one + int audioPlaylistId = -1; + for (PlaylistType.GetPlaylistsReturnType playlist : result) { + if (playlist.type.equals(PlaylistType.GetPlaylistsReturnType.AUDIO)) { + audioPlaylistId = playlist.playlistid; + break; + } + } + // If found, add to playlist + if (audioPlaylistId != -1) { + PlaylistType.Item item = new PlaylistType.Item(); + item.albumid = getDataHolder().getId(); + Playlist.Add action = new Playlist.Add(audioPlaylistId, item); + action.execute(HostManager.getInstance(getActivity()).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); + } + + /** + * Album details query parameters. + */ + public interface AlbumDetailsQuery { + String[] PROJECTION = { + BaseColumns._ID, + MediaContract.Albums.TITLE, + MediaContract.Albums.DISPLAYARTIST, + MediaContract.Albums.THUMBNAIL, + MediaContract.Albums.FANART, + MediaContract.Albums.YEAR, + MediaContract.Albums.GENRE, + MediaContract.Albums.DESCRIPTION, + MediaContract.Albums.RATING, + }; + + int ID = 0; + int TITLE = 1; + int DISPLAYARTIST = 2; + int THUMBNAIL = 3; + int FANART = 4; + int YEAR = 5; + int GENRE = 6; + int DESCRIPTION = 7; + int RATING = 8; + } +} diff --git a/app/src/main/java/org/xbmc/kore/ui/sections/audio/AlbumListFragment.java b/app/src/main/java/org/xbmc/kore/ui/sections/audio/AlbumListFragment.java index 745e286..63824df 100644 --- a/app/src/main/java/org/xbmc/kore/ui/sections/audio/AlbumListFragment.java +++ b/app/src/main/java/org/xbmc/kore/ui/sections/audio/AlbumListFragment.java @@ -47,6 +47,7 @@ import org.xbmc.kore.provider.MediaContract; import org.xbmc.kore.provider.MediaDatabase; import org.xbmc.kore.service.library.LibrarySyncService; import org.xbmc.kore.ui.AbstractCursorListFragment; +import org.xbmc.kore.ui.AbstractInfoFragment; import org.xbmc.kore.utils.LogUtils; import org.xbmc.kore.utils.MediaPlayerUtils; import org.xbmc.kore.utils.UIUtils; @@ -59,7 +60,7 @@ public class AlbumListFragment extends AbstractCursorListFragment { private static final String TAG = LogUtils.makeLogTag(AlbumListFragment.class); public interface OnAlbumSelectedListener { - public void onAlbumSelected(ViewHolder vh); + public void onAlbumSelected(ViewHolder viewHolder); } public static final String BUNDLE_KEY_GENREID = "genreid", @@ -75,27 +76,23 @@ public class AlbumListFragment extends AbstractCursorListFragment { protected String getListSyncType() { return LibrarySyncService.SYNC_ALL_MUSIC; } /** - * Create a new instance of this, initialized to show albums of genres + * Use this to display all albums for a specific artist + * @param artistId */ - public static AlbumListFragment newInstanceForGenre(final int genreId) { - AlbumListFragment fragment = new AlbumListFragment(); - + public void setArtist(int artistId) { Bundle args = new Bundle(); - args.putInt(BUNDLE_KEY_GENREID, genreId); - fragment.setArguments(args); - return fragment; + args.putInt(BUNDLE_KEY_ARTISTID, artistId); + setArguments(args); } /** - * Create a new instance of this, initialized to show albums of artists + * Use this to display all albums for a specific genre + * @param genreId */ - public static AlbumListFragment newInstanceForArtist(final int artistId) { - AlbumListFragment fragment = new AlbumListFragment(); - + public void setGenre(int genreId) { Bundle args = new Bundle(); - args.putInt(BUNDLE_KEY_ARTISTID, artistId); - fragment.setArguments(args); - return fragment; + args.putInt(BUNDLE_KEY_GENREID, genreId); + setArguments(args); } @Override @@ -204,7 +201,7 @@ public class AlbumListFragment extends AbstractCursorListFragment { } return new CursorLoader(getActivity(), uri, - AlbumListQuery.PROJECTION, selection, selectionArgs, sortOrderStr); + AlbumListQuery.PROJECTION, selection, selectionArgs, sortOrderStr); } @Override @@ -249,21 +246,21 @@ public class AlbumListFragment extends AbstractCursorListFragment { MediaContract.Albums.THUMBNAIL, MediaContract.Albums.YEAR, MediaContract.Albums.RATING, - }; + }; String SORT_BY_ALBUM = MediaDatabase.sortCommonTokens(MediaContract.Albums.TITLE) + " ASC"; String SORT_BY_ARTIST = MediaDatabase.sortCommonTokens(MediaContract.Albums.DISPLAYARTIST) + " ASC"; String SORT_BY_ARTIST_YEAR = MediaDatabase.sortCommonTokens(MediaContract.Albums.DISPLAYARTIST) + " ASC, " + MediaContract.Albums.YEAR + " ASC"; - final int ID = 0; - final int ALBUMID = 1; - final int TITLE = 2; - final int DISPLAYARTIST = 3; - final int GENRE = 4; - final int THUMBNAIL = 5; - final int YEAR = 6; - final int RATING = 7; + int ID = 0; + int ALBUMID = 1; + int TITLE = 2; + int DISPLAYARTIST = 3; + int GENRE = 4; + int THUMBNAIL = 5; + int YEAR = 6; + int RATING = 7; } private class AlbumsAdapter extends CursorAdapter { @@ -279,8 +276,8 @@ public class AlbumListFragment extends AbstractCursorListFragment { // Use the same dimensions as in the details fragment, so that it hits Picasso's cache when // the user transitions to that fragment, avoiding another call and imediatelly showing the image Resources resources = context.getResources(); - artWidth = (int) resources.getDimensionPixelOffset(R.dimen.albumdetail_poster_width); - artHeight = (int) resources.getDimensionPixelOffset(R.dimen.albumdetail_poster_heigth); + artWidth = resources.getDimensionPixelOffset(R.dimen.detail_poster_width_square); + artHeight = resources.getDimensionPixelOffset(R.dimen.detail_poster_height_square); } /** {@inheritDoc} */ @@ -306,34 +303,33 @@ public class AlbumListFragment extends AbstractCursorListFragment { public void bindView(View view, Context context, Cursor cursor) { final ViewHolder viewHolder = (ViewHolder)view.getTag(); - viewHolder.albumId = cursor.getInt(AlbumListQuery.ALBUMID); - viewHolder.albumTitle = cursor.getString(AlbumListQuery.TITLE); - viewHolder.albumArtist = cursor.getString(AlbumListQuery.DISPLAYARTIST); - viewHolder.albumGenre = cursor.getString(AlbumListQuery.GENRE); - viewHolder.albumYear = cursor.getInt(AlbumListQuery.YEAR); - viewHolder.albumRating = cursor.getDouble(AlbumListQuery.RATING); + viewHolder.dataHolder.setId(cursor.getInt(AlbumListQuery.ALBUMID)); + viewHolder.dataHolder.setTitle(cursor.getString(AlbumListQuery.TITLE)); + viewHolder.dataHolder.setUndertitle(cursor.getString(AlbumListQuery.DISPLAYARTIST)); - viewHolder.titleView.setText(viewHolder.albumTitle); - viewHolder.artistView.setText(cursor.getString(AlbumListQuery.DISPLAYARTIST)); + viewHolder.titleView.setText(viewHolder.dataHolder.getTitle()); + viewHolder.artistView.setText(viewHolder.dataHolder.getUnderTitle()); int year = cursor.getInt(AlbumListQuery.YEAR); String genres = cursor.getString(AlbumListQuery.GENRE); String desc = (genres != null) ? ((year > 0) ? genres + " | " + year : genres) : String.valueOf(year); + viewHolder.dataHolder.setDescription(desc); viewHolder.genresView.setText(desc); - String thumbnail = cursor.getString(AlbumListQuery.THUMBNAIL); + viewHolder.dataHolder.setPosterUrl(cursor.getString(AlbumListQuery.THUMBNAIL)); UIUtils.loadImageWithCharacterAvatar(context, hostManager, - thumbnail, viewHolder.albumTitle, - viewHolder.artView, artWidth, artHeight); + viewHolder.dataHolder.getPosterUrl(), + viewHolder.dataHolder.getTitle(), + viewHolder.artView, artWidth, artHeight); // For the popupmenu ImageView contextMenu = (ImageView)view.findViewById(R.id.list_context_menu); contextMenu.setTag(viewHolder); contextMenu.setOnClickListener(albumlistItemMenuClickListener); - if(Utils.isLollipopOrLater()) { - viewHolder.artView.setTransitionName("a"+viewHolder.albumId); + if (Utils.isLollipopOrLater()) { + viewHolder.artView.setTransitionName("al"+viewHolder.dataHolder.getId()); } } } @@ -346,13 +342,7 @@ public class AlbumListFragment extends AbstractCursorListFragment { TextView artistView; TextView genresView; ImageView artView; - - int albumId; - String albumTitle; - String albumArtist; - int albumYear; - String albumGenre; - double albumRating; + AbstractInfoFragment.DataHolder dataHolder = new AbstractInfoFragment.DataHolder(0); } private View.OnClickListener albumlistItemMenuClickListener = new View.OnClickListener() { @@ -361,7 +351,7 @@ public class AlbumListFragment extends AbstractCursorListFragment { final ViewHolder viewHolder = (ViewHolder)v.getTag(); final PlaylistType.Item playListItem = new PlaylistType.Item(); - playListItem.albumid = viewHolder.albumId; + playListItem.albumid = viewHolder.dataHolder.getId(); final PopupMenu popupMenu = new PopupMenu(getActivity(), v); popupMenu.getMenuInflater().inflate(R.menu.musiclist_item, popupMenu.getMenu()); diff --git a/app/src/main/java/org/xbmc/kore/ui/sections/audio/AlbumSongsListFragment.java b/app/src/main/java/org/xbmc/kore/ui/sections/audio/AlbumSongsListFragment.java new file mode 100644 index 0000000..c8f6132 --- /dev/null +++ b/app/src/main/java/org/xbmc/kore/ui/sections/audio/AlbumSongsListFragment.java @@ -0,0 +1,418 @@ +/* + * 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.sections.audio; + +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.provider.BaseColumns; +import android.support.annotation.Nullable; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.CursorLoader; +import android.support.v4.content.Loader; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CursorAdapter; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.PopupMenu; +import android.widget.TextView; +import android.widget.Toast; + +import org.xbmc.kore.R; +import org.xbmc.kore.host.HostInfo; +import org.xbmc.kore.host.HostManager; +import org.xbmc.kore.jsonrpc.ApiCallback; +import org.xbmc.kore.jsonrpc.ApiMethod; +import org.xbmc.kore.jsonrpc.method.Player; +import org.xbmc.kore.jsonrpc.method.Playlist; +import org.xbmc.kore.jsonrpc.type.PlaylistType; +import org.xbmc.kore.provider.MediaContract; +import org.xbmc.kore.ui.AbstractAdditionalInfoFragment; +import org.xbmc.kore.utils.FileDownloadHelper; +import org.xbmc.kore.utils.LogUtils; +import org.xbmc.kore.utils.MediaPlayerUtils; +import org.xbmc.kore.utils.UIUtils; + +import java.util.ArrayList; + +import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; + +/** + * Fragment that presents the songs list + */ +public class AlbumSongsListFragment extends AbstractAdditionalInfoFragment + implements LoaderManager.LoaderCallbacks { + private static final String TAG = LogUtils.makeLogTag(AlbumSongsListFragment.class); + + public static final String BUNDLE_KEY_ALBUMID = "albumid"; + public static final String BUNDLE_KEY_ALBUMTITLE = "albumtitle"; + + private static final int LOADER = 0; + + private int albumId = -1; + private String albumTitle = ""; + + private Handler callbackHandler = new Handler(); + + private ArrayList songInfoList; + + /** + * Use this to display all songs for a specific album + * @param albumId + */ + public void setAlbum(int albumId, String albumTitle) { + Bundle args = new Bundle(); + args.putInt(BUNDLE_KEY_ALBUMID, albumId); + args.putString(BUNDLE_KEY_ALBUMTITLE, albumTitle); + setArguments(args); + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + Bundle arguments = getArguments(); + if (arguments != null) { + albumId = arguments.getInt(BUNDLE_KEY_ALBUMID, -1); + albumTitle = arguments.getString(BUNDLE_KEY_ALBUMTITLE, ""); + } + + getLoaderManager().initLoader(LOADER, null, this); + super.onCreate(savedInstanceState); + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + LinearLayout linearLayout = new LinearLayout(getActivity()); + linearLayout.setOrientation(LinearLayout.VERTICAL); + ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT); + linearLayout.setLayoutParams(lp); + return linearLayout; + } + + @Override + public Loader onCreateLoader(int id, Bundle args) { + Uri uri; + HostInfo hostInfo = HostManager.getInstance(getActivity()).getHostInfo(); + int hostId = hostInfo != null ? hostInfo.getId() : -1; + + uri = MediaContract.Songs.buildAlbumSongsListUri(hostId, albumId); + + return new CursorLoader(getActivity(), uri, AlbumSongsListQuery.PROJECTION, null, + null, AlbumSongsListQuery.SORT); + } + + @Override + public void onLoadFinished(Loader loader, Cursor data) { + if (! data.moveToFirst()) { + Toast.makeText(getActivity(), R.string.no_songs_found_refresh, + Toast.LENGTH_SHORT).show(); + return; + } + displaySongs(data); + } + + @Override + public void onLoaderReset(Loader loader) { + + } + + /** + * Returns all songs displayed by this fragment + * @return + */ + public ArrayList getSongInfoList() { + return songInfoList; + } + + private void displaySongs(Cursor cursor) { + songInfoList = new ArrayList<>(cursor.getCount()); + LinearLayout listView = (LinearLayout) getView(); + do { + View songView = LayoutInflater.from(getActivity()) + .inflate(R.layout.list_item_song, listView, false); + TextView songTitle = (TextView)songView.findViewById(R.id.song_title); + TextView trackNumber = (TextView)songView.findViewById(R.id.track_number); + TextView details = (TextView)songView.findViewById(R.id.details); + ImageView contextMenu = (ImageView)songView.findViewById(R.id.list_context_menu); + + String artist = cursor.getString(AlbumSongsListQuery.ARTIST); + + // Add this song to the list + FileDownloadHelper.SongInfo songInfo = new FileDownloadHelper.SongInfo( + artist, + albumTitle, + cursor.getInt(AlbumSongsListQuery.SONGID), + cursor.getInt(AlbumSongsListQuery.TRACK), + cursor.getString(AlbumSongsListQuery.TITLE), + cursor.getString(AlbumSongsListQuery.FILE)); + songInfoList.add(songInfo); + + songTitle.setText(songInfo.title); + + + trackNumber.setText(String.valueOf(songInfo.track)); + + String duration = UIUtils.formatTime(cursor.getInt(AlbumSongsListQuery.DURATION)); + String detailsText = TextUtils.isEmpty(artist) ? duration : duration + " | " + artist; + details.setText(detailsText); + + contextMenu.setTag(songInfo); + contextMenu.setOnClickListener(songItemMenuClickListener); + + songView.setTag(songInfo); + songView.setOnClickListener(songClickListener); + listView.addView(songView); + } while (cursor.moveToNext()); + } + + View.OnClickListener songClickListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + playSong(((FileDownloadHelper.SongInfo)v.getTag()).songId); + } + }; + + private ApiCallback defaultStringActionCallback = ApiMethod.getDefaultActionCallback(); + + private void playSong(int songId) { + PlaylistType.Item item = new PlaylistType.Item(); + item.songid = songId; + Player.Open action = new Player.Open(item); + action.execute(HostManager.getInstance(getActivity()).getConnection(), + defaultStringActionCallback, callbackHandler); + } + + private View.OnClickListener songItemMenuClickListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + final FileDownloadHelper.SongInfo songInfo = ((FileDownloadHelper.SongInfo)v.getTag()); + final int songId = songInfo.songId; + + final PopupMenu popupMenu = new PopupMenu(getActivity(), v); + popupMenu.getMenuInflater().inflate(R.menu.song_item, popupMenu.getMenu()); + popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + switch (item.getItemId()) { + case R.id.action_play_song: + playSong(songId); + return true; + case R.id.action_add_to_playlist: + addToPlaylist(songId); + return true; + case R.id.download: + ArrayList songInfoList = new ArrayList<>(); + songInfoList.add(songInfo); + UIUtils.downloadSongs(getActivity(), songInfoList, + HostManager.getInstance(getActivity()).getHostInfo(), callbackHandler); + return true; + } + return false; + } + }); + popupMenu.show(); + } + }; + + private void addToPlaylist(final int id) { + Playlist.GetPlaylists getPlaylists = new Playlist.GetPlaylists(); + + getPlaylists.execute(HostManager.getInstance(getActivity()).getConnection(), new ApiCallback>() { + @Override + public void onSuccess(ArrayList result) { + if (!isAdded()) return; + // Ok, loop through the playlists, looking for the audio one + int audioPlaylistId = -1; + for (PlaylistType.GetPlaylistsReturnType playlist : result) { + if (playlist.type.equals(PlaylistType.GetPlaylistsReturnType.AUDIO)) { + audioPlaylistId = playlist.playlistid; + break; + } + } + // If found, add to playlist + if (audioPlaylistId != -1) { + PlaylistType.Item item = new PlaylistType.Item(); + item.songid = id; + Playlist.Add action = new Playlist.Add(audioPlaylistId, item); + action.execute(HostManager.getInstance(getActivity()).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); + } + + @Override + public void refresh() { + getLoaderManager().restartLoader(LOADER, null, this); + } + + /** + * Album songs list query parameters. + */ + public interface AlbumSongsListQuery { + String[] PROJECTION = { + BaseColumns._ID, + MediaContract.Songs.TITLE, + MediaContract.Songs.TRACK, + MediaContract.Songs.DURATION, + MediaContract.Songs.FILE, + MediaContract.Songs.SONGID, + MediaContract.Songs.DISPLAYARTIST, + MediaContract.Songs.DISC + }; + + String SORT = MediaContract.Songs.DISC + " ASC, " + MediaContract.Songs.TRACK + " ASC"; + + int ID = 0; + int TITLE = 1; + int TRACK = 2; + int DURATION = 3; + int FILE = 4; + int SONGID = 5; + int ARTIST = 6; + int DISC = 7; + } + + private class AlbumSongsAdapter extends CursorAdapter { + + public AlbumSongsAdapter(Context context) { + super(context, null, false); + } + + @Override + public View newView(Context context, Cursor cursor, ViewGroup viewGroup) { + View view = LayoutInflater.from(context) + .inflate(R.layout.list_item_song, viewGroup, false); + // Setup View holder pattern + ViewHolder viewHolder = new ViewHolder(); + viewHolder.trackNumber = (TextView)view.findViewById(R.id.track_number); + viewHolder.title = (TextView)view.findViewById(R.id.song_title); + viewHolder.details = (TextView)view.findViewById(R.id.details); + viewHolder.contextMenu = (ImageView)view.findViewById(R.id.list_context_menu); + viewHolder.songInfo = new FileDownloadHelper.SongInfo(); + + view.setTag(viewHolder); + + return view; + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + String artist = cursor.getString(AlbumSongsListQuery.ARTIST); + + ViewHolder vh = (ViewHolder) view.getTag(); + + vh.title.setText(cursor.getString(AlbumSongsListQuery.TITLE)); + + vh.songInfo.artist = artist; + vh.songInfo.album = albumTitle; + vh.songInfo.songId = cursor.getInt(AlbumSongsListQuery.SONGID); + vh.songInfo.title = cursor.getString(AlbumSongsListQuery.TITLE); + vh.songInfo.fileName = cursor.getString(AlbumSongsListQuery.FILE); + vh.songInfo.track = cursor.getInt(AlbumSongsListQuery.TRACK); + + vh.trackNumber.setText(String.valueOf(vh.songInfo.track)); + + String duration = UIUtils.formatTime(cursor.getInt(AlbumSongsListQuery.DURATION)); + String detailsText = TextUtils.isEmpty(artist) ? duration : duration + " | " + artist; + vh.details.setText(detailsText); + + vh.contextMenu.setTag(vh); + vh.contextMenu.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(final View v) { + showPopupMenu(v); + } + }); + } + } + + /** + * View holder pattern + */ + public static class ViewHolder { + TextView title; + TextView details; + TextView trackNumber; + ImageView contextMenu; + + FileDownloadHelper.SongInfo songInfo; + } + + private void showPopupMenu(View v) { + final ViewHolder viewHolder = (ViewHolder) v.getTag(); + + final PlaylistType.Item playListItem = new PlaylistType.Item(); + + final PopupMenu popupMenu = new PopupMenu(getActivity(), v); + popupMenu.getMenuInflater().inflate(R.menu.song_item, popupMenu.getMenu()); + popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + switch (item.getItemId()) { + case R.id.action_play_song: + MediaPlayerUtils.play(AlbumSongsListFragment.this, playListItem); + return true; + case R.id.action_add_to_playlist: + MediaPlayerUtils.queue(AlbumSongsListFragment.this, playListItem, PlaylistType.GetPlaylistsReturnType.AUDIO); + return true; + case R.id.download: + ArrayList songInfoList = new ArrayList<>(); + songInfoList.add(viewHolder.songInfo); + UIUtils.downloadSongs(getActivity(), + songInfoList, + HostManager.getInstance(getActivity()).getHostInfo(), + callbackHandler); + } + return false; + } + }); + popupMenu.show(); + } +} diff --git a/app/src/main/java/org/xbmc/kore/ui/sections/audio/ArtistDetailsFragment.java b/app/src/main/java/org/xbmc/kore/ui/sections/audio/ArtistDetailsFragment.java index 1e02707..98e71cb 100644 --- a/app/src/main/java/org/xbmc/kore/ui/sections/audio/ArtistDetailsFragment.java +++ b/app/src/main/java/org/xbmc/kore/ui/sections/audio/ArtistDetailsFragment.java @@ -15,108 +15,32 @@ */ package org.xbmc.kore.ui.sections.audio; -import android.annotation.TargetApi; import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.v4.app.Fragment; -import android.support.v4.view.ViewPager; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import com.astuetz.PagerSlidingTabStrip; import org.xbmc.kore.R; +import org.xbmc.kore.ui.AbstractTabsFragment; +import org.xbmc.kore.ui.AbstractInfoFragment; import org.xbmc.kore.utils.LogUtils; import org.xbmc.kore.utils.TabsAdapter; -import org.xbmc.kore.utils.UIUtils; -import org.xbmc.kore.utils.Utils; -import butterknife.ButterKnife; -import butterknife.InjectView; - -public class ArtistDetailsFragment extends Fragment { +public class ArtistDetailsFragment extends AbstractTabsFragment { private static final String TAG = LogUtils.makeLogTag(ArtistDetailsFragment.class); - @InjectView(R.id.pager_tab_strip) PagerSlidingTabStrip pagerTabStrip; - @InjectView(R.id.pager) ViewPager viewPager; - - /** - * Create a new instance of this, initialized to show tvshowId - */ - @TargetApi(21) - public static ArtistDetailsFragment newInstance(ArtistListFragment.ViewHolder vh) { - ArtistDetailsFragment fragment = new ArtistDetailsFragment(); - - Bundle args = new Bundle(); - args.putInt(ArtistOverviewFragment.BUNDLE_KEY_ARTISTID, vh.artistId); - args.putInt(AlbumListFragment.BUNDLE_KEY_ARTISTID, vh.artistId); - args.putString(ArtistOverviewFragment.BUNDLE_KEY_TITLE, vh.artistName); - args.putString(ArtistOverviewFragment.BUNDLE_KEY_FANART, vh.fanart); - args.putString(ArtistOverviewFragment.BUNDLE_KEY_DESCRIPTION, vh.description); - args.putString(ArtistOverviewFragment.BUNDLE_KEY_GENRE, vh.genres); - args.putString(ArtistOverviewFragment.BUNDLE_KEY_POSTER, vh.poster); - - if( Utils.isLollipopOrLater()) { - args.putString(ArtistOverviewFragment.POSTER_TRANS_NAME, vh.artView.getTransitionName()); - } - fragment.setArguments(args); - return fragment; - } - - @Nullable @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - Bundle arguments = getArguments(); - int id = arguments.getInt(ArtistOverviewFragment.BUNDLE_KEY_ARTISTID, -1); + protected TabsAdapter createTabsAdapter(AbstractInfoFragment.DataHolder dataHolder) { + Bundle arguments = dataHolder.getBundle(); + int itemId = dataHolder.getId(); + long baseFragmentId = itemId * 10; - arguments.putInt(SongsListFragment.BUNDLE_KEY_ARTISTID, id); + arguments.putInt(AlbumListFragment.BUNDLE_KEY_ARTISTID, itemId); + arguments.putInt(SongsListFragment.BUNDLE_KEY_ARTISTID, itemId); - if ((container == null) || (id == -1)) { - // We're not being shown or there's nothing to show - return null; - } - - ViewGroup root = (ViewGroup) inflater.inflate(R.layout.fragment_default_view_pager, container, false); - ButterKnife.inject(this, root); - - long baseFragmentId = id * 10; - TabsAdapter tabsAdapter = new TabsAdapter(getActivity(), getChildFragmentManager()) - .addTab(ArtistOverviewFragment.class, arguments, R.string.info, + return new TabsAdapter(getActivity(), getChildFragmentManager()) + .addTab(ArtistInfoFragment.class, arguments, R.string.info, baseFragmentId) .addTab(AlbumListFragment.class, arguments, R.string.albums, baseFragmentId + 1) .addTab(SongsListFragment.class, arguments, R.string.songs, baseFragmentId + 2); - - viewPager.setAdapter(tabsAdapter); - pagerTabStrip.setViewPager(viewPager); - - return root; - } - - @Override - public void onActivityCreated (Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - setHasOptionsMenu(false); - } - - public View getSharedElement() { - View view = getView(); - if (view == null) - return null; - - //Note: this works as R.id.poster is only used in ArtistShowOverviewFragment. - //If the same id is used in other fragments in the TabsAdapter we - //need to check which fragment is currently displayed - View artView = view.findViewById(R.id.poster); - View scrollView = view.findViewById(R.id.media_panel); - if (( artView != null ) && - ( scrollView != null ) && - UIUtils.isViewInBounds(scrollView, artView)) { - return artView; - } - - return null; } } 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 new file mode 100644 index 0000000..fd77db4 --- /dev/null +++ b/app/src/main/java/org/xbmc/kore/ui/sections/audio/ArtistInfoFragment.java @@ -0,0 +1,255 @@ +/* + * 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.sections.audio; + +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.provider.BaseColumns; +import android.support.annotation.Nullable; +import android.support.v4.app.LoaderManager; +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; +import org.xbmc.kore.provider.MediaContract; +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.utils.FileDownloadHelper; +import org.xbmc.kore.utils.LogUtils; +import org.xbmc.kore.utils.MediaPlayerUtils; +import org.xbmc.kore.utils.UIUtils; + +import java.util.ArrayList; + +public class ArtistInfoFragment extends AbstractInfoFragment + implements LoaderManager.LoaderCallbacks { + private static final String TAG = LogUtils.makeLogTag(ArtistInfoFragment.class); + + // Loader IDs + private static final int LOADER_ARTIST = 0, + LOADER_SONGS = 1; + + /** + * Handler on which to post RPC callbacks + */ + private Handler callbackHandler = new Handler(); + + @Override + protected AbstractAdditionalInfoFragment getAdditionalInfoFragment() { + return null; + } + + @Override + protected RefreshItem createRefreshItem() { + RefreshItem refreshItem = new RefreshItem(getActivity(), LibrarySyncService.SYNC_ALL_MUSIC); + refreshItem.setListener(new RefreshItem.RefreshItemListener() { + @Override + public void onSyncProcessEnded(MediaSyncEvent event) { + if (event.status == MediaSyncEvent.STATUS_SUCCESS) + getLoaderManager().restartLoader(LOADER_ARTIST, null, ArtistInfoFragment.this); + } + }); + return refreshItem; + } + + @Override + protected boolean setupMediaActionBar() { + setOnAddToPlaylistListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + final PlaylistType.Item playListItem = new PlaylistType.Item(); + playListItem.artistid = getDataHolder().getId(); + MediaPlayerUtils.queue(ArtistInfoFragment.this, playListItem, PlaylistType.GetPlaylistsReturnType.AUDIO); + } + }); + + setOnDownloadListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + getLoaderManager().initLoader(LOADER_SONGS, null, ArtistInfoFragment.this); + } + }); + + return true; + } + + @Override + protected boolean setupFAB(ImageButton FAB) { + FAB.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + PlaylistType.Item item = new PlaylistType.Item(); + item.artistid = getDataHolder().getId(); + fabActionPlayItem(item); + } + }); + return true; + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setExpandDescription(true); + } + + @Override + public void onActivityCreated (Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + getLoaderManager().initLoader(LOADER_ARTIST, null, this); + + setHasOptionsMenu(false); + } + + @Override + public void onPause() { + //Make sure loader is not reloaded for albums and songs when we return + //These loaders should only be activated by the user pressing the download button + getLoaderManager().destroyLoader(LOADER_SONGS); + super.onPause(); + } + + /** + * Loader callbacks + */ + /** {@inheritDoc} */ + @Override + public Loader onCreateLoader(int i, Bundle bundle) { + Uri uri; + switch (i) { + case LOADER_ARTIST: + uri = MediaContract.Artists.buildArtistUri(getHostInfo().getId(), getDataHolder().getId()); + return new CursorLoader(getActivity(), uri, + DetailsQuery.PROJECTION, null, null, null); + case LOADER_SONGS: + uri = MediaContract.Songs.buildArtistSongsListUri(getHostInfo().getId(), getDataHolder().getId()); + return new CursorLoader(getActivity(), uri, + SongsListQuery.PROJECTION, null, null, SongsListQuery.SORT); + default: + return null; + } + } + + /** {@inheritDoc} */ + @Override + public void onLoadFinished(Loader cursorLoader, Cursor cursor) { + if (cursor != null && cursor.getCount() > 0) { + switch (cursorLoader.getId()) { + case LOADER_ARTIST: + cursor.moveToFirst(); + + FileDownloadHelper.SongInfo songInfo = new FileDownloadHelper.SongInfo( + cursor.getString(DetailsQuery.ARTIST),null, -1, -1, null, null); + setDownloadButtonState(songInfo.downloadDirectoryExists()); + + DataHolder dataHolder = getDataHolder(); + dataHolder.setTitle(cursor.getString(DetailsQuery.ARTIST)); + dataHolder.setUndertitle(cursor.getString(DetailsQuery.GENRE)); + dataHolder.setDescription(cursor.getString(DetailsQuery.DESCRIPTION)); + dataHolder.setPosterUrl(cursor.getString(DetailsQuery.THUMBNAIL)); + dataHolder.setFanArtUrl(cursor.getString(DetailsQuery.FANART)); + updateView(dataHolder); + break; + case LOADER_SONGS: + final ArrayList songInfoList = new ArrayList<>(cursor.getCount()); + if (cursor.moveToFirst()) { + do { + songInfoList.add(createSongInfo(cursor)); + } while (cursor.moveToNext()); + } + + UIUtils.downloadSongs(getActivity(), songInfoList, getHostInfo(), callbackHandler); + } + } + } + + /** {@inheritDoc} */ + @Override + public void onLoaderReset(Loader cursorLoader) { + // Release loader's data + } + + + private FileDownloadHelper.SongInfo createSongInfo(Cursor cursor) { + return new FileDownloadHelper.SongInfo( + cursor.getString(SongsListQuery.DISPLAYARTIST), + cursor.getString(SongsListQuery.ALBUMTITLE), + cursor.getInt(SongsListQuery.SONGID), + cursor.getInt(SongsListQuery.TRACK), + cursor.getString(SongsListQuery.TITLE), + cursor.getString(SongsListQuery.FILE)); + } + + private interface DetailsQuery { + String[] PROJECTION = { + BaseColumns._ID, + MediaContract.Artists.ARTISTID, + MediaContract.Artists.ARTIST, + MediaContract.Artists.GENRE, + MediaContract.Artists.THUMBNAIL, + MediaContract.Artists.DESCRIPTION, + MediaContract.Artists.FANART + }; + + int ID = 0; + int ARTISTID = 1; + int ARTIST = 2; + int GENRE = 3; + int THUMBNAIL = 4; + int DESCRIPTION = 5; + int FANART = 6; + } + + /** + * Song list query parameters. + */ + private interface SongsListQuery { + String[] PROJECTION = { + MediaDatabase.Tables.SONGS + "." + BaseColumns._ID, + MediaProvider.Qualified.SONGS_TITLE, + MediaProvider.Qualified.SONGS_TRACK, + MediaProvider.Qualified.SONGS_DURATION, + MediaProvider.Qualified.SONGS_FILE, + MediaProvider.Qualified.SONGS_SONGID, + MediaProvider.Qualified.SONGS_ALBUMID, + MediaProvider.Qualified.ALBUMS_TITLE, + MediaProvider.Qualified.SONGS_DISPLAYARTIST + }; + + String SORT = MediaContract.Songs.TRACK + " ASC"; + + int ID = 0; + int TITLE = 1; + int TRACK = 2; + int DURATION = 3; + int FILE = 4; + int SONGID = 5; + int ALBUMID = 6; + int ALBUMTITLE = 7; + int DISPLAYARTIST = 8; + } +} diff --git a/app/src/main/java/org/xbmc/kore/ui/sections/audio/ArtistListFragment.java b/app/src/main/java/org/xbmc/kore/ui/sections/audio/ArtistListFragment.java index 16f8e07..e2914b7 100644 --- a/app/src/main/java/org/xbmc/kore/ui/sections/audio/ArtistListFragment.java +++ b/app/src/main/java/org/xbmc/kore/ui/sections/audio/ArtistListFragment.java @@ -41,6 +41,7 @@ import org.xbmc.kore.provider.MediaContract; import org.xbmc.kore.provider.MediaDatabase; import org.xbmc.kore.service.library.LibrarySyncService; import org.xbmc.kore.ui.AbstractCursorListFragment; +import org.xbmc.kore.ui.AbstractInfoFragment; import org.xbmc.kore.utils.LogUtils; import org.xbmc.kore.utils.MediaPlayerUtils; import org.xbmc.kore.utils.UIUtils; @@ -145,8 +146,8 @@ public class ArtistListFragment extends AbstractCursorListFragment { // Get the art dimensions Resources resources = context.getResources(); - artWidth = (int)(resources.getDimension(R.dimen.albumdetail_poster_width)); - artHeight = (int)(resources.getDimension(R.dimen.albumdetail_poster_heigth)); + artWidth = (int)(resources.getDimension(R.dimen.detail_poster_width_square)); + artHeight = (int)(resources.getDimension(R.dimen.detail_poster_height_square)); } /** {@inheritDoc} */ @@ -160,8 +161,9 @@ public class ArtistListFragment extends AbstractCursorListFragment { viewHolder.nameView = (TextView)view.findViewById(R.id.name); viewHolder.genresView = (TextView)view.findViewById(R.id.genres); viewHolder.artView = (ImageView)view.findViewById(R.id.art); - + viewHolder.contextMenu = (ImageView)view.findViewById(R.id.list_context_menu); view.setTag(viewHolder); + return view; } @@ -172,27 +174,25 @@ public class ArtistListFragment extends AbstractCursorListFragment { final ViewHolder viewHolder = (ViewHolder)view.getTag(); // Save the movie id - viewHolder.artistId = cursor.getInt(ArtistListQuery.ARTISTID); - viewHolder.artistName = cursor.getString(ArtistListQuery.ARTIST); - viewHolder.genres = cursor.getString(ArtistListQuery.GENRE); - viewHolder.description = cursor.getString(ArtistListQuery.DESCRIPTION); - viewHolder.fanart = cursor.getString(ArtistListQuery.FANART); + viewHolder.dataHolder.setId(cursor.getInt(ArtistListQuery.ARTISTID)); + viewHolder.dataHolder.setTitle(cursor.getString(ArtistListQuery.ARTIST)); + viewHolder.dataHolder.setUndertitle(cursor.getString(ArtistListQuery.GENRE)); + viewHolder.dataHolder.setDescription(cursor.getString(ArtistListQuery.DESCRIPTION)); + viewHolder.dataHolder.setFanArtUrl(cursor.getString(ArtistListQuery.FANART)); - viewHolder.nameView.setText(viewHolder.artistName); + viewHolder.nameView.setText(cursor.getString(ArtistListQuery.ARTIST)); viewHolder.genresView.setText(cursor.getString(ArtistListQuery.GENRE)); - viewHolder.poster = cursor.getString(ArtistListQuery.THUMBNAIL); + viewHolder.dataHolder.setPosterUrl(cursor.getString(ArtistListQuery.THUMBNAIL)); UIUtils.loadImageWithCharacterAvatar(context, hostManager, - viewHolder.poster, viewHolder.artistName, + viewHolder.dataHolder.getPosterUrl(), viewHolder.dataHolder.getTitle(), viewHolder.artView, artWidth, artHeight); - // For the popupmenu - ImageView contextMenu = (ImageView)view.findViewById(R.id.list_context_menu); - contextMenu.setTag(viewHolder); - contextMenu.setOnClickListener(artistlistItemMenuClickListener); + viewHolder.contextMenu.setTag(viewHolder); + viewHolder.contextMenu.setOnClickListener(artistlistItemMenuClickListener); - if(Utils.isLollipopOrLater()) { - viewHolder.artView.setTransitionName("a"+viewHolder.artistId); + if (Utils.isLollipopOrLater()) { + viewHolder.artView.setTransitionName("ar"+viewHolder.dataHolder.getId()); } } } @@ -204,13 +204,9 @@ public class ArtistListFragment extends AbstractCursorListFragment { TextView nameView; TextView genresView; ImageView artView; + ImageView contextMenu; - int artistId; - String artistName; - String genres; - String description; - String fanart; - String poster; + AbstractInfoFragment.DataHolder dataHolder = new AbstractInfoFragment.DataHolder(0); } private View.OnClickListener artistlistItemMenuClickListener = new View.OnClickListener() { @@ -219,7 +215,7 @@ public class ArtistListFragment extends AbstractCursorListFragment { final ViewHolder viewHolder = (ViewHolder)v.getTag(); final PlaylistType.Item playListItem = new PlaylistType.Item(); - playListItem.artistid = viewHolder.artistId; + playListItem.artistid = viewHolder.dataHolder.getId(); final PopupMenu popupMenu = new PopupMenu(getActivity(), v); popupMenu.getMenuInflater().inflate(R.menu.musiclist_item, popupMenu.getMenu()); diff --git a/app/src/main/java/org/xbmc/kore/ui/sections/audio/ArtistOverviewFragment.java b/app/src/main/java/org/xbmc/kore/ui/sections/audio/ArtistOverviewFragment.java deleted file mode 100644 index 27b3680..0000000 --- a/app/src/main/java/org/xbmc/kore/ui/sections/audio/ArtistOverviewFragment.java +++ /dev/null @@ -1,405 +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.sections.audio; - -import android.annotation.TargetApi; -import android.content.DialogInterface; -import android.content.res.Resources; -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.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.host.HostManager; -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.type.PlaylistType; -import org.xbmc.kore.provider.MediaContract; -import org.xbmc.kore.provider.MediaDatabase; -import org.xbmc.kore.provider.MediaProvider; -import org.xbmc.kore.ui.AbstractDetailsFragment; -import org.xbmc.kore.utils.FileDownloadHelper; -import org.xbmc.kore.utils.LogUtils; -import org.xbmc.kore.utils.MediaPlayerUtils; -import org.xbmc.kore.utils.UIUtils; -import org.xbmc.kore.utils.Utils; - -import java.util.ArrayList; - -import butterknife.ButterKnife; -import butterknife.InjectView; -import butterknife.OnClick; - -public class ArtistOverviewFragment extends AbstractDetailsFragment - implements LoaderManager.LoaderCallbacks { - private static final String TAG = LogUtils.makeLogTag(ArtistOverviewFragment.class); - - public static final String BUNDLE_KEY_ARTISTID = "id"; - public static final String POSTER_TRANS_NAME = "POSTER_TRANS_NAME"; - public static final String BUNDLE_KEY_TITLE = "title"; - public static final String BUNDLE_KEY_GENRE = "genre"; - public static final String BUNDLE_KEY_DESCRIPTION = "description"; - public static final String BUNDLE_KEY_FANART = "fanart"; - public static final String BUNDLE_KEY_POSTER = "poster"; - - // Loader IDs - private static final int LOADER_ARTIST = 0, - LOADER_SONGS = 1; - - private HostManager hostManager; - private HostInfo hostInfo; - - /** - * Handler on which to post RPC callbacks - */ - private Handler callbackHandler = new Handler(); - - private int artistId = -1; - - private String artistTitle; - - @InjectView(R.id.exit_transition_view) View exitTransitionView; - // Buttons - @InjectView(R.id.fab) ImageButton fabButton; - - // 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.media_description) TextView mediaDescription; - - @TargetApi(21) - @Override - protected View createView(LayoutInflater inflater, ViewGroup container) { - Bundle bundle = getArguments(); - artistId = bundle.getInt(BUNDLE_KEY_ARTISTID, -1); - - if ((container == null) || (artistId == -1)) { - // We're not being shown or there's nothing to show - return null; - } - - ViewGroup root = (ViewGroup) inflater.inflate(R.layout.fragment_artist_details, container, false); - ButterKnife.inject(this, root); - - hostManager = HostManager.getInstance(getActivity()); - hostInfo = hostManager.getHostInfo(); - - // 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) fabButton).attachToScrollView((ObservableScrollView) mediaPanel); - - if(Utils.isLollipopOrLater()) { - mediaPoster.setTransitionName(bundle.getString(POSTER_TRANS_NAME)); - } - - artistTitle = bundle.getString(BUNDLE_KEY_TITLE); - - mediaTitle.setText(artistTitle); - - mediaUndertitle.setText(bundle.getString(BUNDLE_KEY_GENRE)); - mediaDescription.setText(bundle.getString(BUNDLE_KEY_DESCRIPTION)); - setArt(bundle.getString(BUNDLE_KEY_POSTER), bundle.getString(BUNDLE_KEY_FANART)); - - return root; - } - - @Override - protected String getSyncType() { - return null; - } - - @Override - protected String getSyncID() { - return null; - } - - @Override - protected int getSyncItemID() { - return 0; - } - - @Override - protected SwipeRefreshLayout getSwipeRefreshLayout() { - return null; - } - - @Override - protected void onDownload() { - getLoaderManager().initLoader(LOADER_SONGS, null, this); - } - - @Override - public void onActivityCreated (Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - setHasOptionsMenu(false); - } - - @Override - public void onResume() { - // Force the exit view to invisible - exitTransitionView.setVisibility(View.INVISIBLE); - super.onResume(); - } - - @Override - public void onPause() { - //Make sure loader is not reloaded for albums and songs when we return - //These loaders should only be activated by the user pressing the download button - getLoaderManager().destroyLoader(LOADER_SONGS); - super.onPause(); - } - - @Override - protected void onSyncProcessEnded(MediaSyncEvent event) { - - } - - /** - * Loader callbacks - */ - /** {@inheritDoc} */ - @Override - public Loader onCreateLoader(int i, Bundle bundle) { - Uri uri; - switch (i) { - case LOADER_ARTIST: - uri = MediaContract.Artists.buildArtistUri(hostInfo.getId(), artistId); - return new CursorLoader(getActivity(), uri, - DetailsQuery.PROJECTION, null, null, null); - case LOADER_SONGS: - uri = MediaContract.Songs.buildArtistSongsListUri(hostInfo.getId(), artistId); - return new CursorLoader(getActivity(), uri, - SongsListQuery.PROJECTION, null, null, SongsListQuery.SORT); - default: - return null; - } - } - - /** {@inheritDoc} */ - @Override - public void onLoadFinished(Loader cursorLoader, Cursor cursor) { - if (cursor != null && cursor.getCount() > 0) { - switch (cursorLoader.getId()) { - case LOADER_ARTIST: - displayArtistDetails(cursor); - break; - case LOADER_SONGS: - downloadSongs(cursor); - } - } - } - - /** {@inheritDoc} */ - @Override - public void onLoaderReset(Loader cursorLoader) { - // Release loader's data - } - - @OnClick(R.id.fab) - public void onFabClicked(View v) { - PlaylistType.Item item = new PlaylistType.Item(); - item.artistid = artistId; - Player.Open action = new Player.Open(item); - action.execute(hostManager.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) { - final PlaylistType.Item playListItem = new PlaylistType.Item(); - playListItem.artistid = artistId; - MediaPlayerUtils.queue(this, playListItem, PlaylistType.GetPlaylistsReturnType.AUDIO); - } - - private FileDownloadHelper.SongInfo createSongInfo(Cursor cursor) { - return new FileDownloadHelper.SongInfo( - cursor.getString(SongsListQuery.DISPLAYARTIST), - cursor.getString(SongsListQuery.ALBUMTITLE), - cursor.getInt(SongsListQuery.SONGID), - cursor.getInt(SongsListQuery.TRACK), - cursor.getString(SongsListQuery.TITLE), - cursor.getString(SongsListQuery.FILE)); - } - - private void downloadSongs(Cursor cursor) { - DialogInterface.OnClickListener noopClickListener = - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { } - }; - - final ArrayList songInfoList = new ArrayList<>(cursor.getCount()); - if (cursor.moveToFirst()) { - do { - FileDownloadHelper.SongInfo songInfo = createSongInfo(cursor); - if (songInfo != null) { - songInfoList.add(songInfo); - } - } while (cursor.moveToNext()); - } - - UIUtils.downloadSongs(getActivity(), songInfoList, hostInfo, callbackHandler); - } - - private void displayArtistDetails(Cursor cursor) { - cursor.moveToFirst(); - artistTitle = cursor.getString(DetailsQuery.ARTIST); - mediaTitle.setText(artistTitle); - mediaUndertitle.setText(cursor.getString(DetailsQuery.GENRE)); - mediaDescription.setText(cursor.getString(DetailsQuery.DESCRIPTION)); - setArt(cursor.getString(DetailsQuery.THUMBNAIL), cursor.getString(DetailsQuery.FANART)); - } - - private void setArt(String poster, String fanart) { - final Resources resources = getActivity().getResources(); - - // Images - DisplayMetrics displayMetrics = new DisplayMetrics(); - getActivity().getWindowManager().getDefaultDisplay().getMetrics(displayMetrics); - - int artHeight = resources.getDimensionPixelOffset(R.dimen.now_playing_art_height), - artWidth = displayMetrics.widthPixels; - int posterWidth = resources.getDimensionPixelOffset(R.dimen.albumdetail_poster_width); - int posterHeight = resources.getDimensionPixelOffset(R.dimen.albumdetail_poster_heigth); - UIUtils.loadImageWithCharacterAvatar(getActivity(), hostManager, - poster, artistTitle, - mediaPoster, posterWidth, posterHeight); - UIUtils.loadImageIntoImageview(hostManager, - TextUtils.isEmpty(fanart) ? poster : fanart, - mediaArt, artWidth, artHeight); - } - - /** - * Returns the shared element if visible - * @return View if visible, null otherwise - */ - public View getSharedElement() { - if (UIUtils.isViewInBounds(mediaPanel, mediaPoster)) { - return mediaPoster; - } - - return null; - } - - private interface DetailsQuery { - String[] PROJECTION = { - BaseColumns._ID, - MediaContract.Artists.ARTISTID, - MediaContract.Artists.ARTIST, - MediaContract.Artists.GENRE, - MediaContract.Artists.THUMBNAIL, - MediaContract.Artists.DESCRIPTION, - MediaContract.Artists.FANART - }; - - int ID = 0; - int ARTISTID = 1; - int ARTIST = 2; - int GENRE = 3; - int THUMBNAIL = 4; - int DESCRIPTION = 5; - int FANART = 6; - } - - /** - * Song list query parameters. - */ - private interface SongsListQuery { - String[] PROJECTION = { - MediaDatabase.Tables.SONGS + "." + BaseColumns._ID, - MediaProvider.Qualified.SONGS_TITLE, - MediaProvider.Qualified.SONGS_TRACK, - MediaProvider.Qualified.SONGS_DURATION, - MediaProvider.Qualified.SONGS_FILE, - MediaProvider.Qualified.SONGS_SONGID, - MediaProvider.Qualified.SONGS_ALBUMID, - MediaProvider.Qualified.ALBUMS_TITLE, - MediaProvider.Qualified.SONGS_DISPLAYARTIST - }; - - String SORT = MediaContract.Songs.TRACK + " ASC"; - - int ID = 0; - int TITLE = 1; - int TRACK = 2; - int DURATION = 3; - int FILE = 4; - int SONGID = 5; - int ALBUMID = 6; - int ALBUMTITLE = 7; - int DISPLAYARTIST = 8; - } -} diff --git a/app/src/main/java/org/xbmc/kore/ui/sections/audio/MusicActivity.java b/app/src/main/java/org/xbmc/kore/ui/sections/audio/MusicActivity.java index d0f6044..f6cf9a4 100644 --- a/app/src/main/java/org/xbmc/kore/ui/sections/audio/MusicActivity.java +++ b/app/src/main/java/org/xbmc/kore/ui/sections/audio/MusicActivity.java @@ -18,36 +18,32 @@ package org.xbmc.kore.ui.sections.audio; import android.annotation.TargetApi; import android.content.Intent; import android.os.Bundle; +import android.support.v4.app.Fragment; import android.support.v4.app.FragmentTransaction; -import android.support.v4.app.SharedElementCallback; import android.support.v4.widget.DrawerLayout; import android.support.v7.app.ActionBar; import android.support.v7.widget.Toolbar; -import android.transition.Transition; -import android.transition.TransitionInflater; import android.view.Menu; import android.view.MenuItem; -import android.view.View; import org.xbmc.kore.R; +import org.xbmc.kore.ui.AbstractInfoFragment; import org.xbmc.kore.ui.BaseActivity; import org.xbmc.kore.ui.generic.NavigationDrawerFragment; import org.xbmc.kore.ui.sections.remote.RemoteActivity; import org.xbmc.kore.utils.LogUtils; +import org.xbmc.kore.utils.SharedElementTransition; import org.xbmc.kore.utils.Utils; -import java.util.List; -import java.util.Map; - /** * Controls the presentation of Music information (list, details) * All the information is presented by specific fragments */ public class MusicActivity extends BaseActivity implements ArtistListFragment.OnArtistSelectedListener, - AlbumListFragment.OnAlbumSelectedListener, - AudioGenresListFragment.OnAudioGenreSelectedListener, - MusicVideoListFragment.OnMusicVideoSelectedListener { + AlbumListFragment.OnAlbumSelectedListener, + AudioGenresListFragment.OnAudioGenreSelectedListener, + MusicVideoListFragment.OnMusicVideoSelectedListener { private static final String TAG = LogUtils.makeLogTag(MusicActivity.class); public static final String ALBUMID = "album_id"; @@ -58,6 +54,7 @@ public class MusicActivity extends BaseActivity public static final String GENRETITLE = "genre_title"; public static final String MUSICVIDEOID = "music_video_id"; public static final String MUSICVIDEOTITLE = "music_video_title"; + public static final String LISTFRAGMENT_TAG = "musiclist"; private int selectedAlbumId = -1; private int selectedArtistId = -1; @@ -70,9 +67,8 @@ public class MusicActivity extends BaseActivity private NavigationDrawerFragment navigationDrawerFragment; - private MusicListFragment musicListFragment; + private SharedElementTransition sharedElementTransition = new SharedElementTransition(); - private boolean clearSharedElements; @TargetApi(21) @Override @@ -85,34 +81,17 @@ public class MusicActivity extends BaseActivity .findFragmentById(R.id.navigation_drawer); navigationDrawerFragment.setUp(R.id.navigation_drawer, (DrawerLayout) findViewById(R.id.drawer_layout)); + Fragment fragment; if (savedInstanceState == null) { - musicListFragment = new MusicListFragment(); + fragment = new MusicListFragment(); - // Setup animations - if (Utils.isLollipopOrLater()) { - musicListFragment.setExitTransition(null); - musicListFragment.setReenterTransition(TransitionInflater - .from(this) - .inflateTransition(android.R.transition.fade)); - musicListFragment.setExitSharedElementCallback(new SharedElementCallback() { - @Override - public void onMapSharedElements(List names, Map sharedElements) { - // Clearing must be done in the reentering fragment - // as this is called last. Otherwise, the app will crash during transition setup. Not sure, but might - // be a v4 support package bug. - if (clearSharedElements) { - names.clear(); - sharedElements.clear(); - clearSharedElements = false; - } - } - }); - } getSupportFragmentManager() .beginTransaction() - .add(R.id.fragment_container, musicListFragment) + .add(R.id.fragment_container, fragment, LISTFRAGMENT_TAG) .commit(); } else { + fragment = getSupportFragmentManager().findFragmentByTag(LISTFRAGMENT_TAG); + selectedAlbumId = savedInstanceState.getInt(ALBUMID, -1); selectedArtistId = savedInstanceState.getInt(ARTISTID, -1); selectedGenreId = savedInstanceState.getInt(GENREID, -1); @@ -123,6 +102,10 @@ public class MusicActivity extends BaseActivity selectedMusicVideoTitle = savedInstanceState.getString(MUSICVIDEOTITLE, null); } + if (Utils.isLollipopOrLater()) { + sharedElementTransition.setupExitTransition(this, fragment); + } + setupActionBar(selectedAlbumTitle, selectedArtistName, selectedGenreTitle, selectedMusicVideoTitle); // // Setup system bars and content padding, allowing averlap with the bottom bar @@ -131,16 +114,6 @@ public class MusicActivity extends BaseActivity // UIUtils.setPaddingForSystemBars(this, findViewById(R.id.drawer_layout), true, true, true); } - @Override - public void onResume() { - super.onResume(); - } - - @Override - public void onPause() { - super.onPause(); - } - @Override protected void onSaveInstanceState (Bundle outState) { super.onSaveInstanceState(outState); @@ -270,44 +243,28 @@ public class MusicActivity extends BaseActivity } @TargetApi(21) - public void onArtistSelected(ArtistListFragment.ViewHolder viewHolder) { - selectedArtistId = viewHolder.artistId; - selectedArtistName = viewHolder.artistName; + public void onArtistSelected(ArtistListFragment.ViewHolder vh) { + selectedArtistId = vh.dataHolder.getId(); + selectedArtistName = vh.dataHolder.getTitle(); // Replace list fragment - final ArtistDetailsFragment artistDetailsFragment = ArtistDetailsFragment.newInstance(viewHolder); + final ArtistDetailsFragment artistDetailsFragment = new ArtistDetailsFragment(); + artistDetailsFragment.setDataHolder(vh.dataHolder); + vh.dataHolder.setSquarePoster(true); FragmentTransaction fragTrans = getSupportFragmentManager().beginTransaction(); // Setup animations if (Utils.isLollipopOrLater()) { - android.support.v4.app.SharedElementCallback seCallback = new android.support.v4.app.SharedElementCallback() { - @Override - public void onMapSharedElements(List names, Map sharedElements) { - View sharedView = artistDetailsFragment.getSharedElement(); - if (sharedView == null) { // shared element not visible - clearSharedElements = true; - } - } - }; - artistDetailsFragment.setEnterSharedElementCallback(seCallback); - - artistDetailsFragment.setEnterTransition(TransitionInflater - .from(this) - .inflateTransition(R.transition.media_details)); - artistDetailsFragment.setReturnTransition(null); - - Transition changeImageTransition = TransitionInflater.from( - this).inflateTransition(R.transition.change_image); - artistDetailsFragment.setSharedElementReturnTransition(changeImageTransition); - artistDetailsFragment.setSharedElementEnterTransition(changeImageTransition); - fragTrans.addSharedElement(viewHolder.artView, viewHolder.artView.getTransitionName()); + vh.dataHolder.setPosterTransitionName(vh.artView.getTransitionName()); + sharedElementTransition.setupEnterTransition(this, fragTrans, artistDetailsFragment, + vh.artView); } else { fragTrans.setCustomAnimations(R.anim.fragment_details_enter, 0, R.anim.fragment_list_popenter, 0); } fragTrans.replace(R.id.fragment_container, artistDetailsFragment) - .addToBackStack(null) - .commit(); + .addToBackStack(null) + .commit(); navigationDrawerFragment.animateDrawerToggle(true); setupActionBar(null, selectedArtistName, null, null); @@ -315,52 +272,27 @@ public class MusicActivity extends BaseActivity @TargetApi(21) public void onAlbumSelected(AlbumListFragment.ViewHolder vh) { - selectedAlbumId = vh.albumId; - selectedAlbumTitle = vh.albumTitle; + selectedAlbumId = vh.dataHolder.getId(); + selectedAlbumTitle = vh.dataHolder.getTitle(); // Replace list fragment - final AlbumDetailsFragment albumDetailsFragment = AlbumDetailsFragment.newInstance(vh); + final AbstractInfoFragment albumInfoFragment = new AlbumInfoFragment(); + vh.dataHolder.setSquarePoster(true); + albumInfoFragment.setDataHolder(vh.dataHolder); FragmentTransaction fragTrans = getSupportFragmentManager().beginTransaction(); // Set up transitions if (Utils.isLollipopOrLater()) { - android.support.v4.app.SharedElementCallback seCallback = new android.support.v4.app.SharedElementCallback() { - @Override - public void onMapSharedElements(List names, Map sharedElements) { - //On returning onMapSharedElements for the exiting fragment is called before the onMapSharedElements - // for the reentering fragment. We use this to determine if we are returning and if - // we should clear the shared element lists. Note that, clearing must be done in the reentering fragment - // as this is called last. Otherwise it the app will crash during transition setup. Not sure, but might - // be a v4 support package bug. - if (albumDetailsFragment.isVisible()) { - View sharedView = albumDetailsFragment.getSharedElement(); - if (sharedView == null) { // shared element not visible - clearSharedElements = true; - } - } - } - }; - albumDetailsFragment.setEnterSharedElementCallback(seCallback); - - albumDetailsFragment.setEnterTransition(TransitionInflater - .from(this) - .inflateTransition(R.transition.media_details)); - albumDetailsFragment.setReturnTransition(null); - - Transition changeImageTransition = TransitionInflater.from( - this).inflateTransition(R.transition.change_image); - albumDetailsFragment.setSharedElementReturnTransition(changeImageTransition); - albumDetailsFragment.setSharedElementEnterTransition(changeImageTransition); - - fragTrans.addSharedElement(vh.artView, vh.artView.getTransitionName()); + vh.dataHolder.setPosterTransitionName(vh.artView.getTransitionName()); + sharedElementTransition.setupEnterTransition(this, fragTrans, albumInfoFragment, vh.artView); } else { fragTrans.setCustomAnimations(R.anim.fragment_details_enter, 0, - R.anim.fragment_list_popenter, 0); + R.anim.fragment_list_popenter, 0); } - fragTrans.replace(R.id.fragment_container, albumDetailsFragment) - .addToBackStack(null) - .commit(); + fragTrans.replace(R.id.fragment_container, albumInfoFragment) + .addToBackStack(null) + .commit(); setupActionBar(selectedAlbumTitle, null, null, null); } @@ -369,7 +301,9 @@ public class MusicActivity extends BaseActivity selectedGenreTitle = genreTitle; // Replace list fragment - AlbumListFragment albumListFragment = AlbumListFragment.newInstanceForGenre(genreId); + AlbumListFragment albumListFragment = new AlbumListFragment(); + albumListFragment.setGenre(genreId); + getSupportFragmentManager() .beginTransaction() .setCustomAnimations(R.anim.fragment_details_enter, 0, R.anim.fragment_list_popenter, 0) @@ -381,53 +315,28 @@ public class MusicActivity extends BaseActivity @TargetApi(21) public void onMusicVideoSelected(MusicVideoListFragment.ViewHolder vh) { - selectedMusicVideoId = vh.musicVideoId; - selectedMusicVideoTitle = vh.musicVideoTitle; + selectedMusicVideoId = vh.dataHolder.getId(); + selectedMusicVideoTitle = vh.dataHolder.getTitle(); // Replace list fragment - final MusicVideoDetailsFragment detailsFragment = MusicVideoDetailsFragment.newInstance(vh); + final MusicVideoInfoFragment musicVideoInfoFragment = new MusicVideoInfoFragment(); + vh.dataHolder.setSquarePoster(true); + musicVideoInfoFragment.setDataHolder(vh.dataHolder); + FragmentTransaction fragTrans = getSupportFragmentManager().beginTransaction(); // Set up transitions if (Utils.isLollipopOrLater()) { - android.support.v4.app.SharedElementCallback seCallback = new android.support.v4.app.SharedElementCallback() { - @Override - public void onMapSharedElements(List names, Map sharedElements) { - //On returning onMapSharedElements for the exiting fragment is called before the onMapSharedElements - // for the reentering fragment. We use this to determine if we are returning and if - // we should clear the shared element lists. Note that, clearing must be done in the reentering fragment - // as this is called last. Otherwise it the app will crash during transition setup. Not sure, but might - // be a v4 support package bug. - if (detailsFragment.isVisible()) { - View sharedView = detailsFragment.getSharedElement(); - if (sharedView == null) { // shared element not visible - LogUtils.LOGD(TAG, "onMusicVideoSelected: setting clearedSharedElements to true"); - clearSharedElements = true; - } - } - } - }; - detailsFragment.setEnterSharedElementCallback(seCallback); - - detailsFragment.setEnterTransition(TransitionInflater - .from(this) - .inflateTransition(R.transition.media_details)); - detailsFragment.setReturnTransition(null); - - Transition changeImageTransition = TransitionInflater.from( - this).inflateTransition(R.transition.change_image); - detailsFragment.setSharedElementReturnTransition(changeImageTransition); - detailsFragment.setSharedElementEnterTransition(changeImageTransition); - - fragTrans.addSharedElement(vh.artView, vh.artView.getTransitionName()); + vh.dataHolder.setPosterTransitionName(vh.artView.getTransitionName()); + sharedElementTransition.setupEnterTransition(this, fragTrans, musicVideoInfoFragment, vh.artView); } else { fragTrans.setCustomAnimations(R.anim.fragment_details_enter, 0, - R.anim.fragment_list_popenter, 0); + R.anim.fragment_list_popenter, 0); } - fragTrans.replace(R.id.fragment_container, detailsFragment) - .addToBackStack(null) - .commit(); + fragTrans.replace(R.id.fragment_container, musicVideoInfoFragment) + .addToBackStack(null) + .commit(); setupActionBar(null, null, null, selectedMusicVideoTitle); } } diff --git a/app/src/main/java/org/xbmc/kore/ui/sections/audio/MusicVideoDetailsFragment.java b/app/src/main/java/org/xbmc/kore/ui/sections/audio/MusicVideoDetailsFragment.java deleted file mode 100644 index 4dba943..0000000 --- a/app/src/main/java/org/xbmc/kore/ui/sections/audio/MusicVideoDetailsFragment.java +++ /dev/null @@ -1,519 +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.sections.audio; - -import android.annotation.TargetApi; -import android.content.DialogInterface; -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.support.v7.app.AlertDialog; -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.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.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.type.PlaylistType; -import org.xbmc.kore.provider.MediaContract; -import org.xbmc.kore.service.library.LibrarySyncService; -import org.xbmc.kore.ui.AbstractDetailsFragment; -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 music videos details - */ -public class MusicVideoDetailsFragment extends AbstractDetailsFragment - implements LoaderManager.LoaderCallbacks { - private static final String TAG = LogUtils.makeLogTag(MusicVideoDetailsFragment.class); - - public static final String BUNDLE_KEY_ID = "music_video_id"; - public static final String BUNDLE_KEY_ALBUM = "album"; - public static final String POSTER_TRANS_NAME = "POSTER_TRANS_NAME"; - public static final String BUNDLE_KEY_ARTIST = "artist"; - public static final String BUNDLE_KEY_TITLE = "title"; - public static final String BUNDLE_KEY_GENRES = "genre"; - public static final String BUNDLE_KEY_YEAR = "year"; - public static final String BUNDLE_KEY_PLOT = "plot"; - public static final String BUNDLE_KEY_RUNTIME = "runtime"; - - // Loader IDs - private static final int LOADER_MUSIC_VIDEO = 0; - - /** - * Handler on which to post RPC callbacks - */ - private Handler callbackHandler = new Handler(); - - // Displayed music video id - private int musicVideoId = -1; - - // Info for downloading the music video - private FileDownloadHelper.MusicVideoInfo musicVideoDownloadInfo = null; - - // Controls whether the finished refreshing message is shown - private boolean showRefreshStatusMessage = true; - - @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.download) ImageButton downloadButton; - - // 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.year) TextView mediaYear; - @InjectView(R.id.genres) TextView mediaGenres; - - @InjectView(R.id.media_description) TextView mediaDescription; - - /** - * Create a new instance of this, initialized to show the video musicVideoId - */ - @TargetApi(21) - public static MusicVideoDetailsFragment newInstance(MusicVideoListFragment.ViewHolder vh) { - MusicVideoDetailsFragment fragment = new MusicVideoDetailsFragment(); - - Bundle args = new Bundle(); - args.putInt(BUNDLE_KEY_ID, vh.musicVideoId); - args.putString(BUNDLE_KEY_TITLE, vh.musicVideoTitle); - args.putString(BUNDLE_KEY_ALBUM, vh.album); - args.putString(BUNDLE_KEY_ARTIST, vh.artist); - args.putString(BUNDLE_KEY_GENRES, vh.genres); - args.putString(BUNDLE_KEY_PLOT, vh.plot); - args.putInt(BUNDLE_KEY_RUNTIME, vh.runtime); - args.putInt(BUNDLE_KEY_YEAR, vh.year); - - 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(); - musicVideoId = bundle.getInt(BUNDLE_KEY_ID, -1); - - if (musicVideoId == -1) { - // There's nothing to show - return null; - } - - ViewGroup root = (ViewGroup) inflater.inflate(R.layout.fragment_music_video_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); - - mediaTitle.setText(bundle.getString(BUNDLE_KEY_TITLE)); - setMediaUndertitle(bundle.getString(BUNDLE_KEY_ARTIST), bundle.getString(BUNDLE_KEY_ALBUM)); - setMediaYear(bundle.getInt(BUNDLE_KEY_RUNTIME), bundle.getInt(BUNDLE_KEY_YEAR)); - mediaGenres.setText(bundle.getString(BUNDLE_KEY_GENRES)); - mediaDescription.setText(bundle.getString(BUNDLE_KEY_PLOT)); - - if(Utils.isLollipopOrLater()) { - mediaPoster.setTransitionName(bundle.getString(POSTER_TRANS_NAME)); - } - - // 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_ALL_MUSIC_VIDEOS; - } - - @Override - protected String getSyncID() { - return null; - } - - @Override - protected int getSyncItemID() { - return -1; - } - - @Override - protected SwipeRefreshLayout getSwipeRefreshLayout() { - return swipeRefreshLayout; - } - - @Override - public void onActivityCreated (Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - - // Start the loaders - getLoaderManager().initLoader(LOADER_MUSIC_VIDEO, null, this); - } - - @Override - public void onResume() { - // Force the exit view to invisible - exitTransitionView.setVisibility(View.INVISIBLE); - super.onResume(); - } - - @Override - protected void onSyncProcessEnded(MediaSyncEvent event) { - if (event.status == MediaSyncEvent.STATUS_SUCCESS) { - getLoaderManager().restartLoader(LOADER_MUSIC_VIDEO, null, this); - } - } - - /** - * Loader callbacks - */ - /** {@inheritDoc} */ - @Override - public Loader onCreateLoader(int i, Bundle bundle) { - Uri uri; - switch (i) { - case LOADER_MUSIC_VIDEO: - uri = MediaContract.MusicVideos.buildMusicVideoUri(getHostInfo().getId(), musicVideoId); - return new CursorLoader(getActivity(), uri, - MusicVideoDetailsQuery.PROJECTION, null, null, null); - default: - return null; - } - } - - /** {@inheritDoc} */ - @Override - public void onLoadFinished(Loader cursorLoader, Cursor cursor) { - if (cursor != null && cursor.getCount() > 0) { - switch (cursorLoader.getId()) { - case LOADER_MUSIC_VIDEO: - displayMusicVideoDetails(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.musicvideoid = musicVideoId; - 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.musicvideoid = musicVideoId; - 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); - } - - @Override - protected void onDownload() { - if (musicVideoDownloadInfo == null) { - // Nothing to download - Toast.makeText(getActivity(), R.string.no_files_to_download, Toast.LENGTH_SHORT).show(); - return; - } - - // Check if the directory exists and whether to overwrite it - File file = new File(musicVideoDownloadInfo.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(), - musicVideoDownloadInfo, 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(), - musicVideoDownloadInfo, FileDownloadHelper.DOWNLOAD_WITH_NEW_NAME, - callbackHandler); - } - }) - .setNegativeButton(android.R.string.cancel, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - // Nothing to do - } - }) - .show(); - } else { - FileDownloadHelper.downloadFiles(getActivity(), getHostInfo(), - musicVideoDownloadInfo, FileDownloadHelper.DOWNLOAD_WITH_NEW_NAME, - callbackHandler); - } - } - - /** - * Display the video details - * - * @param cursor Cursor with the data - */ - private void displayMusicVideoDetails(Cursor cursor) { - cursor.moveToFirst(); - String musicVideoTitle = cursor.getString(MusicVideoDetailsQuery.TITLE); - mediaTitle.setText(musicVideoTitle); - - setMediaUndertitle(cursor.getString(MusicVideoDetailsQuery.ARTIST), cursor.getString(MusicVideoDetailsQuery.ALBUM)); - - setMediaYear(cursor.getInt(MusicVideoDetailsQuery.RUNTIME), cursor.getInt(MusicVideoDetailsQuery.YEAR)); - - mediaGenres.setText(cursor.getString(MusicVideoDetailsQuery.GENRES)); - - mediaDescription.setText(cursor.getString(MusicVideoDetailsQuery.PLOT)); - - // Images - Resources resources = getActivity().getResources(); - DisplayMetrics displayMetrics = new DisplayMetrics(); - getActivity().getWindowManager().getDefaultDisplay().getMetrics(displayMetrics); - - String fanart = cursor.getString(MusicVideoDetailsQuery.FANART), - poster = cursor.getString(MusicVideoDetailsQuery.THUMBNAIL); - - int artHeight = resources.getDimensionPixelOffset(R.dimen.now_playing_art_height), - artWidth = displayMetrics.widthPixels; - int posterWidth = resources.getDimensionPixelOffset(R.dimen.musicvideodetail_poster_width); - int posterHeight = resources.getDimensionPixelOffset(R.dimen.musicvideodetail_poster_width); - UIUtils.loadImageWithCharacterAvatar(getActivity(), getHostManager(), - poster, musicVideoTitle, - mediaPoster, posterWidth, posterHeight); - UIUtils.loadImageIntoImageview(getHostManager(), - TextUtils.isEmpty(fanart)? poster : fanart, - mediaArt, artWidth, artHeight); - - // Setup download info - musicVideoDownloadInfo = new FileDownloadHelper.MusicVideoInfo( - musicVideoTitle, cursor.getString(MusicVideoDetailsQuery.FILE)); - - // Check if downloaded file exists - if (musicVideoDownloadInfo.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 setMediaUndertitle(String artist, String album) { - mediaUndertitle.setText(artist + " | " + album); - } - - private void setMediaYear(int runtime, int year) { - String durationYear = runtime > 0 ? - UIUtils.formatTime(runtime) + " | " + - String.valueOf(year) : - String.valueOf(year); - mediaYear.setText(durationYear); - } - - /** - * Returns the shared element if visible - * @return View if visible, null otherwise - */ - public View getSharedElement() { - if (UIUtils.isViewInBounds(mediaPanel, mediaPoster)) { - return mediaPoster; - } - - return null; - } - - /** - * Video details query parameters. - */ - private interface MusicVideoDetailsQuery { - String[] PROJECTION = { - BaseColumns._ID, - MediaContract.MusicVideos.TITLE, - MediaContract.MusicVideos.ALBUM, - MediaContract.MusicVideos.ARTIST, - MediaContract.MusicVideos.THUMBNAIL, - MediaContract.MusicVideos.FANART, - MediaContract.MusicVideos.YEAR, - MediaContract.MusicVideos.GENRES, - MediaContract.MusicVideos.RUNTIME, - MediaContract.MusicVideos.PLOT, - MediaContract.MusicVideos.FILE, - }; - - final int ID = 0; - final int TITLE = 1; - final int ALBUM = 2; - final int ARTIST = 3; - final int THUMBNAIL =4; - final int FANART = 5; - final int YEAR = 6; - final int GENRES = 7; - final int RUNTIME = 8; - final int PLOT = 9; - final int FILE = 10; - } -} 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 new file mode 100644 index 0000000..f5545de --- /dev/null +++ b/app/src/main/java/org/xbmc/kore/ui/sections/audio/MusicVideoInfoFragment.java @@ -0,0 +1,322 @@ +/* + * 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.sections.audio; + +import android.content.DialogInterface; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.provider.BaseColumns; +import android.support.annotation.Nullable; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.CursorLoader; +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; +import org.xbmc.kore.jsonrpc.ApiCallback; +import org.xbmc.kore.jsonrpc.event.MediaSyncEvent; +import org.xbmc.kore.jsonrpc.method.Playlist; +import org.xbmc.kore.jsonrpc.type.PlaylistType; +import org.xbmc.kore.provider.MediaContract; +import org.xbmc.kore.service.library.LibrarySyncService; +import org.xbmc.kore.ui.AbstractAdditionalInfoFragment; +import org.xbmc.kore.ui.AbstractInfoFragment; +import org.xbmc.kore.ui.generic.RefreshItem; +import org.xbmc.kore.utils.FileDownloadHelper; +import org.xbmc.kore.utils.LogUtils; +import org.xbmc.kore.utils.UIUtils; + +import java.io.File; +import java.util.ArrayList; + +/** + * Presents music videos details + */ +public class MusicVideoInfoFragment extends AbstractInfoFragment + implements LoaderManager.LoaderCallbacks { + private static final String TAG = LogUtils.makeLogTag(MusicVideoInfoFragment.class); + + // Loader IDs + private static final int LOADER_MUSIC_VIDEO = 0; + + // /** +// * Handler on which to post RPC callbacks +// */ + private Handler callbackHandler = new Handler(); + + private Cursor cursor; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setExpandDescription(true); + } + + @Override + protected RefreshItem createRefreshItem() { + RefreshItem refreshItem = new RefreshItem(getActivity(), + LibrarySyncService.SYNC_ALL_MUSIC_VIDEOS); + refreshItem.setListener(new RefreshItem.RefreshItemListener() { + @Override + public void onSyncProcessEnded(MediaSyncEvent event) { + if (event.status == MediaSyncEvent.STATUS_SUCCESS) { + getLoaderManager().restartLoader(LOADER_MUSIC_VIDEO, null, + MusicVideoInfoFragment.this); + } + } + }); + + return refreshItem; + } + + @Override + protected boolean setupMediaActionBar() { + setOnAddToPlaylistListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + addToPlaylist(getDataHolder().getId()); + } + }); + + setOnDownloadListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + download(); + } + }); + + return true; + } + + @Override + protected boolean setupFAB(ImageButton FAB) { + FAB.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + PlaylistType.Item item = new PlaylistType.Item(); + item.musicvideoid = getDataHolder().getId(); + fabActionPlayItem(item); + } + }); + return true; + } + + @Override + public void onActivityCreated (Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + // Start the loaders + getLoaderManager().initLoader(LOADER_MUSIC_VIDEO, null, this); + } + + /** + * Loader callbacks + */ + /** {@inheritDoc} */ + @Override + public Loader onCreateLoader(int i, Bundle bundle) { + Uri uri; + switch (i) { + case LOADER_MUSIC_VIDEO: + uri = MediaContract.MusicVideos.buildMusicVideoUri(getHostInfo().getId(), + getDataHolder().getId()); + return new CursorLoader(getActivity(), uri, + MusicVideoDetailsQuery.PROJECTION, null, null, null); + default: + return null; + } + } + + /** {@inheritDoc} */ + @Override + public void onLoadFinished(Loader cursorLoader, Cursor cursor) { + if (cursor != null && cursor.getCount() > 0) { + switch (cursorLoader.getId()) { + case LOADER_MUSIC_VIDEO: + this.cursor = cursor; + cursor.moveToFirst(); + DataHolder dataHolder = getDataHolder(); + + dataHolder.setFanArtUrl(cursor.getString(MusicVideoDetailsQuery.FANART)); + dataHolder.setPosterUrl(cursor.getString(MusicVideoDetailsQuery.THUMBNAIL)); + + int runtime = cursor.getInt(MusicVideoDetailsQuery.RUNTIME); + int year = cursor.getInt(MusicVideoDetailsQuery.YEAR); + String details = runtime > 0 ? + UIUtils.formatTime(runtime) + " | " + + String.valueOf(year) : + String.valueOf(year); + dataHolder.setDetails(details + "\n" + cursor.getString(MusicVideoDetailsQuery.GENRES)); + + dataHolder.setTitle(cursor.getString(MusicVideoDetailsQuery.TITLE)); + dataHolder.setUndertitle(cursor.getString(MusicVideoDetailsQuery.ARTIST) + + " | " + + cursor.getString(MusicVideoDetailsQuery.ALBUM)); + dataHolder.setDescription(cursor.getString(MusicVideoDetailsQuery.PLOT)); + + FileDownloadHelper.MusicVideoInfo musicVideoDownloadInfo = new FileDownloadHelper.MusicVideoInfo( + dataHolder.getTitle(), cursor.getString(MusicVideoDetailsQuery.FILE)); + setDownloadButtonState(musicVideoDownloadInfo.downloadFileExists()); + + updateView(dataHolder); + break; + } + } + } + + /** {@inheritDoc} */ + @Override + public void onLoaderReset(Loader cursorLoader) { + // Release loader's data + } + + public void addToPlaylist(final int itemId) { + 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.musicvideoid = itemId; + 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); + } + + protected void download() { + final FileDownloadHelper.MusicVideoInfo musicVideoDownloadInfo = new FileDownloadHelper.MusicVideoInfo( + getDataHolder().getTitle(), cursor.getString(MusicVideoDetailsQuery.FILE)); + + // Check if the directory exists and whether to overwrite it + File file = new File(musicVideoDownloadInfo.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(), + musicVideoDownloadInfo, 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(), + musicVideoDownloadInfo, FileDownloadHelper.DOWNLOAD_WITH_NEW_NAME, + callbackHandler); + } + }) + .setNegativeButton(android.R.string.cancel, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + // Nothing to do + } + }) + .show(); + } else { + FileDownloadHelper.downloadFiles(getActivity(), getHostInfo(), + musicVideoDownloadInfo, FileDownloadHelper.DOWNLOAD_WITH_NEW_NAME, + callbackHandler); + } + } + + @Override + protected AbstractAdditionalInfoFragment getAdditionalInfoFragment() { + return null; + } + + /** + * Video details query parameters. + */ + private interface MusicVideoDetailsQuery { + String[] PROJECTION = { + BaseColumns._ID, + MediaContract.MusicVideos.TITLE, + MediaContract.MusicVideos.ALBUM, + MediaContract.MusicVideos.ARTIST, + MediaContract.MusicVideos.THUMBNAIL, + MediaContract.MusicVideos.FANART, + MediaContract.MusicVideos.YEAR, + MediaContract.MusicVideos.GENRES, + MediaContract.MusicVideos.RUNTIME, + MediaContract.MusicVideos.PLOT, + MediaContract.MusicVideos.FILE, + }; + + int ID = 0; + int TITLE = 1; + int ALBUM = 2; + int ARTIST = 3; + int THUMBNAIL =4; + int FANART = 5; + int YEAR = 6; + int GENRES = 7; + int RUNTIME = 8; + int PLOT = 9; + int FILE = 10; + } +} diff --git a/app/src/main/java/org/xbmc/kore/ui/sections/audio/MusicVideoListFragment.java b/app/src/main/java/org/xbmc/kore/ui/sections/audio/MusicVideoListFragment.java index ba575d6..15a65e1 100644 --- a/app/src/main/java/org/xbmc/kore/ui/sections/audio/MusicVideoListFragment.java +++ b/app/src/main/java/org/xbmc/kore/ui/sections/audio/MusicVideoListFragment.java @@ -38,6 +38,7 @@ import org.xbmc.kore.provider.MediaContract; import org.xbmc.kore.provider.MediaDatabase; import org.xbmc.kore.service.library.LibrarySyncService; import org.xbmc.kore.ui.AbstractCursorListFragment; +import org.xbmc.kore.ui.AbstractInfoFragment; import org.xbmc.kore.utils.LogUtils; import org.xbmc.kore.utils.UIUtils; import org.xbmc.kore.utils.Utils; @@ -124,16 +125,16 @@ public class MusicVideoListFragment extends AbstractCursorListFragment { String SORT = MediaDatabase.sortCommonTokens(MediaContract.MusicVideos.TITLE) + " ASC"; - final int ID = 0; - final int MUSICVIDEOID = 1; - final int TITLE = 2; - final int ARTIST = 3; - final int ALBUM = 4; - final int THUMBNAIL = 5; - final int RUNTIME = 6; - final int GENRES = 7; - final int YEAR = 8; - final int PLOT = 9; + int ID = 0; + int MUSICVIDEOID = 1; + int TITLE = 2; + int ARTIST = 3; + int ALBUM = 4; + int THUMBNAIL = 5; + int RUNTIME = 6; + int GENRES = 7; + int YEAR = 8; + int PLOT = 9; } private static class MusicVideosAdapter extends CursorAdapter { @@ -147,8 +148,8 @@ public class MusicVideoListFragment extends AbstractCursorListFragment { // Get the art dimensions Resources resources = context.getResources(); - artHeight = resources.getDimensionPixelOffset(R.dimen.musicvideodetail_poster_heigth); - artWidth = resources.getDimensionPixelOffset(R.dimen.musicvideodetail_poster_width); + artHeight = resources.getDimensionPixelOffset(R.dimen.detail_poster_width_square); + artWidth = resources.getDimensionPixelOffset(R.dimen.detail_poster_height_square); } /** {@inheritDoc} */ @@ -175,31 +176,32 @@ public class MusicVideoListFragment extends AbstractCursorListFragment { final ViewHolder viewHolder = (ViewHolder)view.getTag(); // Save the movie id - viewHolder.musicVideoId = cursor.getInt(MusicVideosListQuery.MUSICVIDEOID); - viewHolder.musicVideoTitle = cursor.getString(MusicVideosListQuery.TITLE); - viewHolder.album = cursor.getString(MusicVideosListQuery.ALBUM); - viewHolder.artist = cursor.getString(MusicVideosListQuery.ARTIST); - viewHolder.genres = cursor.getString(MusicVideosListQuery.GENRES); - viewHolder.plot = cursor.getString(MusicVideosListQuery.PLOT); - viewHolder.runtime = cursor.getInt(MusicVideosListQuery.RUNTIME); - viewHolder.year = cursor.getInt(MusicVideosListQuery.YEAR); + viewHolder.dataHolder.setId(cursor.getInt(MusicVideosListQuery.MUSICVIDEOID)); + viewHolder.dataHolder.setTitle(cursor.getString(MusicVideosListQuery.TITLE)); - viewHolder.titleView.setText(viewHolder.musicVideoTitle); - String artistAlbum = viewHolder.artist + " | " + - viewHolder.album; + viewHolder.titleView.setText(viewHolder.dataHolder.getTitle()); + String artistAlbum = cursor.getString(MusicVideosListQuery.ARTIST) + " | " + + cursor.getString(MusicVideosListQuery.ALBUM); viewHolder.artistAlbumView.setText(artistAlbum); + viewHolder.dataHolder.setUndertitle(artistAlbum); + int runtime = cursor.getInt(MusicVideosListQuery.RUNTIME); + String genres = cursor.getString(MusicVideosListQuery.GENRES); String durationGenres = - viewHolder.runtime > 0 ? - UIUtils.formatTime(viewHolder.runtime) + " | " + viewHolder.genres : - viewHolder.genres; + runtime > 0 ? + UIUtils.formatTime(runtime) + " | " + genres : + genres; viewHolder.durationGenresView.setText(durationGenres); - UIUtils.loadImageWithCharacterAvatar(context, hostManager, - cursor.getString(MusicVideosListQuery.THUMBNAIL), viewHolder.musicVideoTitle, + viewHolder.dataHolder.setDetails(durationGenres); + + String posterUrl = cursor.getString(MusicVideosListQuery.THUMBNAIL); + viewHolder.dataHolder.setPosterUrl(posterUrl); + UIUtils.loadImageWithCharacterAvatar(context, hostManager, posterUrl + , viewHolder.dataHolder.getTitle(), viewHolder.artView, artWidth, artHeight); if(Utils.isLollipopOrLater()) { - viewHolder.artView.setTransitionName("a"+viewHolder.musicVideoId); + viewHolder.artView.setTransitionName("a"+viewHolder.dataHolder.getId()); } } } @@ -213,13 +215,6 @@ public class MusicVideoListFragment extends AbstractCursorListFragment { TextView durationGenresView; ImageView artView; - int musicVideoId; - String musicVideoTitle; - String artist; - String album; - int runtime; - String genres; - int year; - String plot; + AbstractInfoFragment.DataHolder dataHolder = new AbstractInfoFragment.DataHolder(0); } } diff --git a/app/src/main/java/org/xbmc/kore/ui/sections/audio/SongsListFragment.java b/app/src/main/java/org/xbmc/kore/ui/sections/audio/SongsListFragment.java index df1d863..0fcecaa 100644 --- a/app/src/main/java/org/xbmc/kore/ui/sections/audio/SongsListFragment.java +++ b/app/src/main/java/org/xbmc/kore/ui/sections/audio/SongsListFragment.java @@ -15,7 +15,6 @@ */ package org.xbmc.kore.ui.sections.audio; -import android.annotation.TargetApi; import android.app.Activity; import android.content.Context; import android.content.res.Resources; @@ -23,6 +22,8 @@ import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.os.Handler; +import android.provider.BaseColumns; +import android.support.annotation.Nullable; import android.support.v4.content.CursorLoader; import android.support.v4.view.MenuItemCompat; import android.support.v7.widget.SearchView; @@ -61,17 +62,46 @@ public class SongsListFragment extends AbstractCursorListFragment { private static final String TAG = LogUtils.makeLogTag(SongsListFragment.class); public static final String BUNDLE_KEY_ARTISTID = "artistid"; + public static final String BUNDLE_KEY_ALBUMID = "albumid"; + public static final String BUNDLE_KEY_ALBUMTITLE = "albumtitle"; private int artistId = -1; + private int albumId = -1; + private String albumTitle = ""; private Handler callbackHandler = new Handler(); + /** + * Use this to display all songs for a specific artist + * @param artistId + */ + public void setArtist(int artistId) { + Bundle args = new Bundle(); + args.putInt(BUNDLE_KEY_ARTISTID, artistId); + setArguments(args); + } + + /** + * Use this to display all songs for a specific album + * @param albumId + */ + public void setAlbum(int albumId, String albumTitle) { + Bundle args = new Bundle(); + args.putInt(BUNDLE_KEY_ALBUMID, albumId); + args.putString(BUNDLE_KEY_ALBUMTITLE, albumTitle); + setArguments(args); + } + @Override protected String getListSyncType() { return LibrarySyncService.SYNC_ALL_MUSIC; } @Override protected CursorAdapter createAdapter() { - return new SongsAdapter(getActivity()); + if (albumId != -1 ) { + return new AlbumSongsAdapter(getActivity()); + } else { + return new SongsAdapter(getActivity()); + } } @Override @@ -86,9 +116,11 @@ public class SongsListFragment extends AbstractCursorListFragment { HostInfo hostInfo = HostManager.getInstance(getActivity()).getHostInfo(); int hostId = hostInfo != null ? hostInfo.getId() : -1; - if (artistId != -1) { + if (artistId != -1) { // get songs for artist uri = MediaContract.Songs.buildArtistSongsListUri(hostId, artistId); - } else { + } else if (albumId != -1) { + uri = MediaContract.Songs.buildAlbumSongsListUri(hostId, albumId); + } else { // get all songs uri = MediaContract.Songs.buildSongsListUri(hostId); } @@ -100,8 +132,13 @@ public class SongsListFragment extends AbstractCursorListFragment { selectionArgs = new String[] {"%" + searchFilter + "%"}; } - return new CursorLoader(getActivity(), uri, - SongsListQuery.PROJECTION, selection, selectionArgs, SongsListQuery.SORT); + if (albumId != -1) { + return new CursorLoader(getActivity(), uri, + AlbumSongsListQuery.PROJECTION, selection, selectionArgs, AlbumSongsListQuery.SORT); + } else { + return new CursorLoader(getActivity(), uri, + SongsListQuery.PROJECTION, selection, selectionArgs, SongsListQuery.SORT); + } } @Override @@ -111,13 +148,14 @@ public class SongsListFragment extends AbstractCursorListFragment { } @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - Bundle args = getArguments(); - if (args != null) { - artistId = getArguments().getInt(BUNDLE_KEY_ARTISTID, -1); + public void onCreate(@Nullable Bundle savedInstanceState) { + Bundle arguments = getArguments(); + if (arguments != null) { + artistId = arguments.getInt(BUNDLE_KEY_ARTISTID, -1); + albumId = arguments.getInt(BUNDLE_KEY_ALBUMID, -1); + albumTitle = arguments.getString(BUNDLE_KEY_ALBUMTITLE, ""); } - - return super.onCreateView(inflater, container, savedInstanceState); + super.onCreate(savedInstanceState); } @Override @@ -152,7 +190,7 @@ public class SongsListFragment extends AbstractCursorListFragment { MediaProvider.Qualified.ALBUMS_GENRE, MediaProvider.Qualified.ALBUMS_YEAR, MediaProvider.Qualified.ALBUMS_THUMBNAIL - }; + }; String SORT = MediaDatabase.sortCommonTokens(MediaProvider.Qualified.SONGS_TITLE) + " ASC"; @@ -169,6 +207,33 @@ public class SongsListFragment extends AbstractCursorListFragment { int THUMBNAIL = 10; } + /** + * Album songs list query parameters. + */ + public interface AlbumSongsListQuery { + String[] PROJECTION = { + BaseColumns._ID, + MediaContract.Songs.TITLE, + MediaContract.Songs.TRACK, + MediaContract.Songs.DURATION, + MediaContract.Songs.FILE, + MediaContract.Songs.SONGID, + MediaContract.Songs.DISPLAYARTIST, + MediaContract.Songs.DISC + }; + + String SORT = MediaContract.Songs.DISC + " ASC, " + MediaContract.Songs.TRACK + " ASC"; + + int ID = 0; + int TITLE = 1; + int TRACK = 2; + int DURATION = 3; + int FILE = 4; + int SONGID = 5; + int ARTIST = 6; + int DISC = 7; + } + private class SongsAdapter extends CursorAdapter { private HostManager hostManager; @@ -182,14 +247,14 @@ public class SongsListFragment extends AbstractCursorListFragment { // Use the same dimensions as in the details fragment, so that it hits Picasso's cache when // the user transitions to that fragment, avoiding another call and imediatelly showing the image Resources resources = context.getResources(); - artWidth = (int) resources.getDimensionPixelOffset(R.dimen.albumdetail_poster_width); - artHeight = (int) resources.getDimensionPixelOffset(R.dimen.albumdetail_poster_heigth); + artWidth = resources.getDimensionPixelOffset(R.dimen.detail_poster_width_square); + artHeight = resources.getDimensionPixelOffset(R.dimen.detail_poster_height_square); } /** {@inheritDoc} */ @Override - public View newView(Context context, final Cursor cursor, ViewGroup parent) { - final View view = LayoutInflater.from(context) + public View newView(Context context, Cursor cursor, ViewGroup parent) { + View view = LayoutInflater.from(context) .inflate(R.layout.grid_item_song, parent, false); // Setup View holder pattern @@ -199,13 +264,13 @@ public class SongsListFragment extends AbstractCursorListFragment { viewHolder.art = (ImageView)view.findViewById(R.id.art); viewHolder.artist = (TextView)view.findViewById(R.id.artist); viewHolder.songInfo = new FileDownloadHelper.SongInfo(); + viewHolder.contextMenu = (ImageView)view.findViewById(R.id.list_context_menu); view.setTag(viewHolder); return view; } /** {@inheritDoc} */ - @TargetApi(21) @Override public void bindView(View view, Context context, Cursor cursor) { final ViewHolder viewHolder = (ViewHolder)view.getTag(); @@ -243,9 +308,62 @@ public class SongsListFragment extends AbstractCursorListFragment { viewHolder.songInfo.track = cursor.getInt(SongsListQuery.TRACK); // For the popupmenu - ImageView contextMenu = (ImageView)view.findViewById(R.id.list_context_menu); - contextMenu.setTag(viewHolder); - contextMenu.setOnClickListener(new View.OnClickListener() { + viewHolder.contextMenu.setTag(viewHolder); + viewHolder.contextMenu.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(final View v) { + showPopupMenu(v); + } + }); + } + } + + private class AlbumSongsAdapter extends CursorAdapter { + + public AlbumSongsAdapter(Context context) { + super(context, null, false); + } + + @Override + public View newView(Context context, Cursor cursor, ViewGroup viewGroup) { + View view = LayoutInflater.from(context) + .inflate(R.layout.list_item_song, viewGroup, false); + // Setup View holder pattern + ViewHolder viewHolder = new ViewHolder(); + viewHolder.trackNumber = (TextView)view.findViewById(R.id.track_number); + viewHolder.title = (TextView)view.findViewById(R.id.song_title); + viewHolder.details = (TextView)view.findViewById(R.id.details); + viewHolder.contextMenu = (ImageView)view.findViewById(R.id.list_context_menu); + viewHolder.songInfo = new FileDownloadHelper.SongInfo(); + + view.setTag(viewHolder); + + return view; + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + String artist = cursor.getString(AlbumSongsListQuery.ARTIST); + + ViewHolder vh = (ViewHolder) view.getTag(); + + vh.title.setText(cursor.getString(AlbumSongsListQuery.TITLE)); + + vh.songInfo.artist = artist; + vh.songInfo.album = albumTitle; + vh.songInfo.songId = cursor.getInt(SongsListQuery.SONGID); + vh.songInfo.title = cursor.getString(SongsListQuery.TITLE); + vh.songInfo.fileName = cursor.getString(SongsListQuery.FILE); + vh.songInfo.track = cursor.getInt(SongsListQuery.TRACK); + + vh.trackNumber.setText(String.valueOf(vh.songInfo.track)); + + String duration = UIUtils.formatTime(cursor.getInt(AlbumSongsListQuery.DURATION)); + String detailsText = TextUtils.isEmpty(artist) ? duration : duration + " | " + artist; + vh.details.setText(detailsText); + + vh.contextMenu.setTag(vh); + vh.contextMenu.setOnClickListener(new View.OnClickListener() { @Override public void onClick(final View v) { showPopupMenu(v); @@ -262,6 +380,8 @@ public class SongsListFragment extends AbstractCursorListFragment { TextView title; TextView details; TextView artist; + TextView trackNumber; + ImageView contextMenu; FileDownloadHelper.SongInfo songInfo; } diff --git a/app/src/main/java/org/xbmc/kore/ui/sections/file/MediaFileListFragment.java b/app/src/main/java/org/xbmc/kore/ui/sections/file/MediaFileListFragment.java index 09ffe8d..d06abd6 100644 --- a/app/src/main/java/org/xbmc/kore/ui/sections/file/MediaFileListFragment.java +++ b/app/src/main/java/org/xbmc/kore/ui/sections/file/MediaFileListFragment.java @@ -474,6 +474,11 @@ public class MediaFileListFragment extends AbstractListFragment { return p; } + @Override + public void onRefresh() { + + } + private class MediaFileListAdapter extends BaseAdapter implements ListAdapter { Context ctx; diff --git a/app/src/main/java/org/xbmc/kore/ui/sections/video/AllCastActivity.java b/app/src/main/java/org/xbmc/kore/ui/sections/video/AllCastActivity.java index 9036ca6..008c7de 100644 --- a/app/src/main/java/org/xbmc/kore/ui/sections/video/AllCastActivity.java +++ b/app/src/main/java/org/xbmc/kore/ui/sections/video/AllCastActivity.java @@ -110,7 +110,7 @@ public class AllCastActivity extends BaseActivity { @Override public void onItemClick(AdapterView parent, View view, int position, long id) { // Get the name from the tag - Utils.openImdbForPerson(AllCastActivity.this, (String) ((ViewHolder)view.getTag()).castName); + Utils.openImdbForPerson(AllCastActivity.this, ((ViewHolder)view.getTag()).castName); } }); @@ -120,16 +120,6 @@ public class AllCastActivity extends BaseActivity { setupActionBar(movie_tvshow_title); } - @Override - public void onResume() { - super.onResume(); - } - - @Override - public void onPause() { - super.onPause(); - } - @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); diff --git a/app/src/main/java/org/xbmc/kore/ui/sections/video/MovieDetailsFragment.java b/app/src/main/java/org/xbmc/kore/ui/sections/video/MovieDetailsFragment.java deleted file mode 100644 index 1c72c2d..0000000 --- a/app/src/main/java/org/xbmc/kore/ui/sections/video/MovieDetailsFragment.java +++ /dev/null @@ -1,705 +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.sections.video; - -import android.annotation.TargetApi; -import android.content.DialogInterface; -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.support.v7.app.AlertDialog; -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.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.ui.AbstractDetailsFragment; -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; - - // 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); - } - - @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/sections/video/MovieInfoFragment.java b/app/src/main/java/org/xbmc/kore/ui/sections/video/MovieInfoFragment.java new file mode 100644 index 0000000..77458a8 --- /dev/null +++ b/app/src/main/java/org/xbmc/kore/ui/sections/video/MovieInfoFragment.java @@ -0,0 +1,411 @@ +/* + * 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.sections.video; + +import android.content.DialogInterface; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.provider.BaseColumns; +import android.support.annotation.Nullable; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.CursorLoader; +import android.support.v4.content.Loader; +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; +import org.xbmc.kore.Settings; +import org.xbmc.kore.jsonrpc.ApiCallback; +import org.xbmc.kore.jsonrpc.event.MediaSyncEvent; +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.provider.MediaContract; +import org.xbmc.kore.service.library.LibrarySyncService; +import org.xbmc.kore.ui.AbstractAdditionalInfoFragment; +import org.xbmc.kore.ui.AbstractInfoFragment; +import org.xbmc.kore.ui.generic.CastFragment; +import org.xbmc.kore.ui.generic.RefreshItem; +import org.xbmc.kore.utils.FileDownloadHelper; +import org.xbmc.kore.utils.LogUtils; +import org.xbmc.kore.utils.Utils; + +import java.io.File; +import java.util.ArrayList; + +/** + * Presents movie details + */ +public class MovieInfoFragment extends AbstractInfoFragment + implements LoaderManager.LoaderCallbacks { + private static final String TAG = LogUtils.makeLogTag(MovieInfoFragment.class); + + // Loader IDs + private static final int LOADER_MOVIE = 0; + + /** + * Handler on which to post RPC callbacks + */ + private Handler callbackHandler = new Handler(); + + // Controls whether a automatic sync refresh has been issued for this show + private static boolean hasIssuedOutdatedRefresh = false; + + private Cursor cursor; + private FileDownloadHelper.MovieInfo movieDownloadInfo; + + @Override + protected RefreshItem createRefreshItem() { + RefreshItem refreshItem = new RefreshItem(getActivity(), + LibrarySyncService.SYNC_SINGLE_MOVIE); + refreshItem.setSyncItem(LibrarySyncService.SYNC_MOVIEID, getDataHolder().getId()); + refreshItem.setListener(new RefreshItem.RefreshItemListener() { + @Override + public void onSyncProcessEnded(MediaSyncEvent event) { + if (event.status == MediaSyncEvent.STATUS_SUCCESS) { + getLoaderManager().restartLoader(LOADER_MOVIE, null, MovieInfoFragment.this); + } + } + }); + + return refreshItem; + } + + @Override + protected boolean setupMediaActionBar() { + setOnDownloadListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + 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(); + } + + } + }); + + setOnSeenListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + // Set the playcount + int playcount = cursor.getInt(MovieDetailsQuery.PLAYCOUNT); + int newPlaycount = (playcount > 0) ? 0 : 1; + + VideoLibrary.SetMovieDetails action = + new VideoLibrary.SetMovieDetails(getDataHolder().getId(), 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 + getRefreshItem().startSync(true); + } + + @Override + public void onError(int errorCode, String description) { } + }, callbackHandler); + } + }); + + setOnGoToImdbListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + String imdbNumber = cursor.getString(MovieDetailsQuery.IMDBNUMBER); + + if (imdbNumber != null) { + Utils.openImdbForMovie(getActivity(), imdbNumber); + } + } + }); + + setOnAddToPlaylistListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + 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 = getDataHolder().getId(); + 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); + } + }); + + return true; + } + + @Override + protected boolean setupFAB(ImageButton FAB) { + FAB.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + PlaylistType.Item item = new PlaylistType.Item(); + item.movieid = getDataHolder().getId(); + fabActionPlayItem(item); + } + }); + return true; + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setExpandDescription(true); + } + + @Override + public void onActivityCreated (Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + hasIssuedOutdatedRefresh = false; + + // Start the loaders + getLoaderManager().initLoader(LOADER_MOVIE, 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(), getDataHolder().getId()); + return new CursorLoader(getActivity(), uri, + MovieDetailsQuery.PROJECTION, null, null, null); + default: + return null; + } + } + + /** {@inheritDoc} */ + @Override + public void onLoadFinished(Loader cursorLoader, Cursor cursor) { + if (cursor != null && cursor.getCount() > 0) { + switch (cursorLoader.getId()) { + case LOADER_MOVIE: + cursor.moveToFirst(); + this.cursor = cursor; + + DataHolder dataHolder = getDataHolder(); + dataHolder.setFanArtUrl(cursor.getString(MovieDetailsQuery.FANART)); + dataHolder.setPosterUrl(cursor.getString(MovieDetailsQuery.THUMBNAIL)); + dataHolder.setRating(cursor.getDouble(MovieDetailsQuery.RATING)); + dataHolder.setMaxRating(10); + dataHolder.setVotes(cursor.getInt(MovieDetailsQuery.VOTES)); + + String director = cursor.getString(MovieDetailsQuery.DIRECTOR); + if (!TextUtils.isEmpty(director)) { + director = getString(R.string.directors) + " " + director; + } + + int runtime = cursor.getInt(MovieDetailsQuery.RUNTIME) / 60; + String year = String.valueOf(cursor.getInt(MovieDetailsQuery.YEAR)); + String durationYear = runtime > 0 ? + String.format(getString(R.string.minutes_abbrev), String.valueOf(runtime)) + + " | " + year : + String.valueOf(year); + + dataHolder.setDetails(durationYear + + "\n" + + cursor.getString(MovieDetailsQuery.GENRES) + +"\n" + + director); + + dataHolder.setTitle(cursor.getString(MovieDetailsQuery.TITLE)); + dataHolder.setUndertitle(cursor.getString(MovieDetailsQuery.TAGLINE)); + dataHolder.setDescription(cursor.getString(MovieDetailsQuery.PLOT)); + movieDownloadInfo = new FileDownloadHelper.MovieInfo( + dataHolder.getTitle(), cursor.getString(MovieDetailsQuery.FILE)); + setDownloadButtonState(movieDownloadInfo.downloadDirectoryExists()); + setSeenButtonState(cursor.getInt(MovieDetailsQuery.PLAYCOUNT) > 0); + updateView(dataHolder); + checkOutdatedMovieDetails(cursor); + break; + } + } + } + + /** {@inheritDoc} */ + @Override + public void onLoaderReset(Loader cursorLoader) { + // Release loader's data + } + + @Override + protected AbstractAdditionalInfoFragment getAdditionalInfoFragment() { + CastFragment castFragment = new CastFragment(); + castFragment.setArgs(getDataHolder().getId(), getDataHolder().getTitle(), + CastFragment.TYPE.MOVIE); + return castFragment; + } + + /** + * 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; + + long lastUpdated = cursor.getLong(MovieDetailsQuery.UPDATED); + + if (System.currentTimeMillis() > lastUpdated + Settings.DB_UPDATE_INTERVAL) { + // Trigger a silent refresh + hasIssuedOutdatedRefresh = true; + getRefreshItem().startSync(true); + } + } + + /** + * 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, + }; + + int ID = 0; + int TITLE = 1; + int TAGLINE = 2; + int THUMBNAIL = 3; + int FANART = 4; + int YEAR = 5; + int GENRES = 6; + int RUNTIME = 7; + int RATING = 8; + int VOTES = 9; + int PLOT = 10; + int PLAYCOUNT = 11; + int DIRECTOR = 12; + int IMDBNUMBER = 13; + int FILE = 14; + int UPDATED = 15; + } +} diff --git a/app/src/main/java/org/xbmc/kore/ui/sections/video/MovieListFragment.java b/app/src/main/java/org/xbmc/kore/ui/sections/video/MovieListFragment.java index f0a37c8..bdfc3e9 100644 --- a/app/src/main/java/org/xbmc/kore/ui/sections/video/MovieListFragment.java +++ b/app/src/main/java/org/xbmc/kore/ui/sections/video/MovieListFragment.java @@ -44,6 +44,7 @@ import org.xbmc.kore.provider.MediaContract; import org.xbmc.kore.provider.MediaDatabase; import org.xbmc.kore.service.library.LibrarySyncService; import org.xbmc.kore.ui.AbstractCursorListFragment; +import org.xbmc.kore.ui.AbstractInfoFragment; import org.xbmc.kore.utils.LogUtils; import org.xbmc.kore.utils.UIUtils; import org.xbmc.kore.utils.Utils; @@ -95,7 +96,7 @@ public class MovieListFragment extends AbstractCursorListFragment { if (selection.length() != 0) selection.append(" AND "); selection.append(MediaContract.MoviesColumns.PLAYCOUNT) - .append("=0"); + .append("=0"); } String sortOrderStr; @@ -120,7 +121,7 @@ public class MovieListFragment extends AbstractCursorListFragment { } return new CursorLoader(getActivity(), uri, - MovieListQuery.PROJECTION, selection.toString(), selectionArgs, sortOrderStr); + MovieListQuery.PROJECTION, selection.toString(), selectionArgs, sortOrderStr); } @Override @@ -197,43 +198,43 @@ public class MovieListFragment extends AbstractCursorListFragment { case R.id.action_hide_watched: item.setChecked(!item.isChecked()); preferences.edit() - .putBoolean(Settings.KEY_PREF_MOVIES_FILTER_HIDE_WATCHED, item.isChecked()) - .apply(); + .putBoolean(Settings.KEY_PREF_MOVIES_FILTER_HIDE_WATCHED, item.isChecked()) + .apply(); refreshList(); break; case R.id.action_ignore_prefixes: item.setChecked(!item.isChecked()); preferences.edit() - .putBoolean(Settings.KEY_PREF_MOVIES_IGNORE_PREFIXES, item.isChecked()) - .apply(); + .putBoolean(Settings.KEY_PREF_MOVIES_IGNORE_PREFIXES, item.isChecked()) + .apply(); refreshList(); break; case R.id.action_sort_by_name: item.setChecked(true); preferences.edit() - .putInt(Settings.KEY_PREF_MOVIES_SORT_ORDER, Settings.SORT_BY_NAME) - .apply(); + .putInt(Settings.KEY_PREF_MOVIES_SORT_ORDER, Settings.SORT_BY_NAME) + .apply(); refreshList(); break; case R.id.action_sort_by_year: item.setChecked(true); preferences.edit() - .putInt(Settings.KEY_PREF_MOVIES_SORT_ORDER, Settings.SORT_BY_YEAR) - .apply(); + .putInt(Settings.KEY_PREF_MOVIES_SORT_ORDER, Settings.SORT_BY_YEAR) + .apply(); refreshList(); break; case R.id.action_sort_by_rating: item.setChecked(true); preferences.edit() - .putInt(Settings.KEY_PREF_MOVIES_SORT_ORDER, Settings.SORT_BY_RATING) - .apply(); + .putInt(Settings.KEY_PREF_MOVIES_SORT_ORDER, Settings.SORT_BY_RATING) + .apply(); refreshList(); break; case R.id.action_sort_by_date_added: item.setChecked(true); preferences.edit() - .putInt(Settings.KEY_PREF_MOVIES_SORT_ORDER, Settings.SORT_BY_DATE_ADDED) - .apply(); + .putInt(Settings.KEY_PREF_MOVIES_SORT_ORDER, Settings.SORT_BY_DATE_ADDED) + .apply(); refreshList(); break; case R.id.action_sort_by_last_played: @@ -246,8 +247,8 @@ public class MovieListFragment extends AbstractCursorListFragment { case R.id.action_sort_by_length: item.setChecked(true); preferences.edit() - .putInt(Settings.KEY_PREF_MOVIES_SORT_ORDER, Settings.SORT_BY_LENGTH) - .apply(); + .putInt(Settings.KEY_PREF_MOVIES_SORT_ORDER, Settings.SORT_BY_LENGTH) + .apply(); refreshList(); break; default: @@ -271,7 +272,7 @@ public class MovieListFragment extends AbstractCursorListFragment { MediaContract.Movies.RUNTIME, MediaContract.Movies.RATING, MediaContract.Movies.TAGLINE, - }; + }; String SORT_BY_NAME = MediaContract.Movies.TITLE + " ASC"; @@ -307,22 +308,21 @@ public class MovieListFragment extends AbstractCursorListFragment { // the user transitions to that fragment, avoiding another call and imediatelly showing the image Resources resources = context.getResources(); artWidth = (int)(resources.getDimension(R.dimen.now_playing_poster_width) / - UIUtils.IMAGE_RESIZE_FACTOR); + UIUtils.IMAGE_RESIZE_FACTOR); artHeight = (int)(resources.getDimension(R.dimen.now_playing_poster_height) / - UIUtils.IMAGE_RESIZE_FACTOR); + UIUtils.IMAGE_RESIZE_FACTOR); } /** {@inheritDoc} */ @Override public View newView(Context context, final Cursor cursor, ViewGroup parent) { final View view = LayoutInflater.from(context) - .inflate(R.layout.grid_item_movie, parent, false); + .inflate(R.layout.grid_item_movie, parent, false); // Setup View holder pattern ViewHolder viewHolder = new ViewHolder(); viewHolder.titleView = (TextView)view.findViewById(R.id.title); viewHolder.detailsView = (TextView)view.findViewById(R.id.details); -// viewHolder.yearView = (TextView)view.findViewById(R.id.year); viewHolder.durationView = (TextView)view.findViewById(R.id.duration); viewHolder.artView = (ImageView)view.findViewById(R.id.art); @@ -337,32 +337,37 @@ public class MovieListFragment extends AbstractCursorListFragment { final ViewHolder viewHolder = (ViewHolder)view.getTag(); // Save the movie id - viewHolder.movieId = cursor.getInt(MovieListQuery.MOVIEID); - viewHolder.movieTitle = cursor.getString(MovieListQuery.TITLE); - viewHolder.movieTagline = cursor.getString(MovieListQuery.TAGLINE); - viewHolder.movieYear = cursor.getInt(MovieListQuery.YEAR); - viewHolder.movieRating = cursor.getDouble(MovieListQuery.RATING); + viewHolder.dataHolder.setId(cursor.getInt(MovieListQuery.MOVIEID)); + viewHolder.dataHolder.setTitle(cursor.getString(MovieListQuery.TITLE)); + viewHolder.dataHolder.setUndertitle(cursor.getString(MovieListQuery.TAGLINE)); - viewHolder.titleView.setText(viewHolder.movieTitle); + int movieYear = cursor.getInt(MovieListQuery.YEAR); + viewHolder.dataHolder.setRating(cursor.getDouble(MovieListQuery.RATING)); + viewHolder.dataHolder.setMaxRating(10); - viewHolder.movieGenres = cursor.getString(MovieListQuery.GENRES); - String details = TextUtils.isEmpty(viewHolder.movieTagline) ? - viewHolder.movieGenres : - viewHolder.movieTagline; + viewHolder.titleView.setText(viewHolder.dataHolder.getTitle()); + + String genres = cursor.getString(MovieListQuery.GENRES); + String details = TextUtils.isEmpty(viewHolder.dataHolder.getUnderTitle()) ? + genres : viewHolder.dataHolder.getUnderTitle(); viewHolder.detailsView.setText(details); -// viewHolder.yearView.setText(String.valueOf(cursor.getInt(MovieListQuery.YEAR))); - viewHolder.movieRuntime = cursor.getInt(MovieListQuery.RUNTIME) / 60; - String duration = viewHolder.movieRuntime > 0 ? - String.format(context.getString(R.string.minutes_abbrev), String.valueOf(viewHolder.movieRuntime)) + - " | " + viewHolder.movieYear : - String.valueOf(viewHolder.movieYear); - viewHolder.durationView.setText(duration); - UIUtils.loadImageWithCharacterAvatar(context, hostManager, - cursor.getString(MovieListQuery.THUMBNAIL), viewHolder.movieTitle, - viewHolder.artView, artWidth, artHeight); - if(Utils.isLollipopOrLater()) { - viewHolder.artView.setTransitionName("a"+viewHolder.movieId); + int runtime = cursor.getInt(MovieListQuery.RUNTIME) / 60; + String duration = runtime > 0 ? + String.format(context.getString(R.string.minutes_abbrev), String.valueOf(runtime)) + + " | " + movieYear : + String.valueOf(movieYear); + viewHolder.durationView.setText(duration); + viewHolder.dataHolder.setDetails(duration + "\n" + details); + + viewHolder.dataHolder.setPosterUrl(cursor.getString(MovieListQuery.THUMBNAIL)); + UIUtils.loadImageWithCharacterAvatar(context, hostManager, + viewHolder.dataHolder.getPosterUrl(), + viewHolder.dataHolder.getTitle(), + viewHolder.artView, artWidth, artHeight); + + if (Utils.isLollipopOrLater()) { + viewHolder.artView.setTransitionName("a" + viewHolder.dataHolder.getId()); } } } @@ -373,16 +378,9 @@ public class MovieListFragment extends AbstractCursorListFragment { public static class ViewHolder { TextView titleView; TextView detailsView; - // TextView yearView; TextView durationView; ImageView artView; - int movieId; - String movieTitle; - String movieTagline; - int movieYear; - int movieRuntime; - String movieGenres; - double movieRating; + AbstractInfoFragment.DataHolder dataHolder = new AbstractInfoFragment.DataHolder(0); } } diff --git a/app/src/main/java/org/xbmc/kore/ui/sections/video/MoviesActivity.java b/app/src/main/java/org/xbmc/kore/ui/sections/video/MoviesActivity.java index 6d09231..53ab9de 100644 --- a/app/src/main/java/org/xbmc/kore/ui/sections/video/MoviesActivity.java +++ b/app/src/main/java/org/xbmc/kore/ui/sections/video/MoviesActivity.java @@ -18,15 +18,13 @@ package org.xbmc.kore.ui.sections.video; import android.annotation.TargetApi; import android.content.Intent; import android.os.Bundle; +import android.support.v4.app.Fragment; import android.support.v4.app.FragmentTransaction; import android.support.v4.widget.DrawerLayout; import android.support.v7.app.ActionBar; import android.support.v7.widget.Toolbar; -import android.transition.Transition; -import android.transition.TransitionInflater; import android.view.Menu; import android.view.MenuItem; -import android.view.View; import android.view.Window; import org.xbmc.kore.R; @@ -34,11 +32,9 @@ import org.xbmc.kore.ui.BaseActivity; import org.xbmc.kore.ui.generic.NavigationDrawerFragment; import org.xbmc.kore.ui.sections.remote.RemoteActivity; import org.xbmc.kore.utils.LogUtils; +import org.xbmc.kore.utils.SharedElementTransition; import org.xbmc.kore.utils.Utils; -import java.util.List; -import java.util.Map; - /** * Controls the presentation of Movies information (list, details) * All the information is presented by specific fragments @@ -49,15 +45,14 @@ public class MoviesActivity extends BaseActivity public static final String MOVIEID = "movie_id"; public static final String MOVIETITLE = "movie_title"; + public static final String LISTFRAGMENT_TAG = "movielist"; private int selectedMovieId = -1; private String selectedMovieTitle; private NavigationDrawerFragment navigationDrawerFragment; - private boolean clearSharedElements; - - private MovieListFragment movieListFragment; + private SharedElementTransition sharedElementTransition = new SharedElementTransition(); @TargetApi(21) @Override @@ -74,54 +69,28 @@ public class MoviesActivity extends BaseActivity .findFragmentById(R.id.navigation_drawer); navigationDrawerFragment.setUp(R.id.navigation_drawer, (DrawerLayout) findViewById(R.id.drawer_layout)); + Fragment fragment; if (savedInstanceState == null) { - movieListFragment = new MovieListFragment(); + fragment = new MovieListFragment(); - // Setup animations - if (Utils.isLollipopOrLater()) { - //Fade added to prevent shared element from disappearing very shortly at the start of the transition. - Transition fade = TransitionInflater - .from(this) - .inflateTransition(android.R.transition.fade); - movieListFragment.setExitTransition(fade); - movieListFragment.setReenterTransition(fade); - movieListFragment.setSharedElementReturnTransition(TransitionInflater.from( - this).inflateTransition(R.transition.change_image)); - - android.support.v4.app.SharedElementCallback seCallback = new android.support.v4.app.SharedElementCallback() { - @Override - public void onMapSharedElements(List names, Map sharedElements) { - if (clearSharedElements) { - names.clear(); - sharedElements.clear(); - clearSharedElements = false; - } - } - }; - movieListFragment.setExitSharedElementCallback(seCallback); - } getSupportFragmentManager() .beginTransaction() - .add(R.id.fragment_container, movieListFragment) + .add(R.id.fragment_container, fragment, LISTFRAGMENT_TAG) .commit(); } else { + fragment = getSupportFragmentManager().findFragmentByTag(LISTFRAGMENT_TAG); + selectedMovieId = savedInstanceState.getInt(MOVIEID, -1); selectedMovieTitle = savedInstanceState.getString(MOVIETITLE, null); } + if (Utils.isLollipopOrLater()) { + sharedElementTransition.setupExitTransition(this, fragment); + } + setupActionBar(selectedMovieTitle); } - @Override - public void onResume() { - super.onResume(); - } - - @Override - public void onPause() { - super.onPause(); - } - @Override protected void onSaveInstanceState (Bundle outState) { super.onSaveInstanceState(outState); @@ -208,47 +177,24 @@ public class MoviesActivity extends BaseActivity */ @TargetApi(21) public void onMovieSelected(MovieListFragment.ViewHolder vh) { - selectedMovieTitle = vh.movieTitle; - selectedMovieId = vh.movieId; + selectedMovieTitle = vh.dataHolder.getTitle(); + selectedMovieId = vh.dataHolder.getId(); + + final MovieInfoFragment movieInfoFragment = new MovieInfoFragment(); + movieInfoFragment.setDataHolder(vh.dataHolder); - final MovieDetailsFragment movieDetailsFragment = MovieDetailsFragment.newInstance(vh); FragmentTransaction fragTrans = getSupportFragmentManager().beginTransaction(); // Set up transitions if (Utils.isLollipopOrLater()) { - android.support.v4.app.SharedElementCallback seCallback = new android.support.v4.app.SharedElementCallback() { - @Override - public void onMapSharedElements(List names, Map sharedElements) { - //On returning onMapSharedElements for the exiting fragment is called before the onMapSharedElements - // for the reentering fragment. We use this to determine if we are returning and if - // we should clear the shared element lists. Note that, clearing must be done in the reentering fragment - // as this is called last. Otherwise it the app will crash during transition setup. Not sure, but might - // be a v4 support package bug. - if (movieDetailsFragment.isVisible()) { - View sharedView = movieDetailsFragment.getSharedElement(); - if (sharedView == null) { // shared element not visible - clearSharedElements = true; - } - } - } - }; - movieDetailsFragment.setEnterSharedElementCallback(seCallback); - movieDetailsFragment.setEnterTransition( - TransitionInflater.from(this) - .inflateTransition(R.transition.media_details)); - movieDetailsFragment.setReturnTransition(null); - - Transition changeImageTransition = - TransitionInflater.from(this).inflateTransition(R.transition.change_image); - movieDetailsFragment.setSharedElementReturnTransition(changeImageTransition); - movieDetailsFragment.setSharedElementEnterTransition(changeImageTransition); - - fragTrans.addSharedElement(vh.artView, vh.artView.getTransitionName()); + vh.dataHolder.setPosterTransitionName(vh.artView.getTransitionName()); + sharedElementTransition.setupEnterTransition(this, fragTrans, movieInfoFragment, + vh.artView); } else { fragTrans.setCustomAnimations(R.anim.fragment_details_enter, 0, R.anim.fragment_list_popenter, 0); } - fragTrans.replace(R.id.fragment_container, movieDetailsFragment) + fragTrans.replace(R.id.fragment_container, movieInfoFragment) .addToBackStack(null) .commit(); diff --git a/app/src/main/java/org/xbmc/kore/ui/sections/video/TVShowDetailsFragment.java b/app/src/main/java/org/xbmc/kore/ui/sections/video/TVShowDetailsFragment.java deleted file mode 100644 index dd2eff5..0000000 --- a/app/src/main/java/org/xbmc/kore/ui/sections/video/TVShowDetailsFragment.java +++ /dev/null @@ -1,745 +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.sections.video; - -import android.annotation.TargetApi; -import android.content.Context; -import android.content.res.Resources; -import android.content.res.TypedArray; -import android.database.Cursor; -import android.net.Uri; -import android.os.Bundle; -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.util.DisplayMetrics; -import android.view.LayoutInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewTreeObserver; -import android.widget.GridLayout; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.PopupMenu; -import android.widget.ProgressBar; -import android.widget.ScrollView; -import android.widget.TextView; - -import org.xbmc.kore.R; -import org.xbmc.kore.Settings; -import org.xbmc.kore.host.HostManager; -import org.xbmc.kore.jsonrpc.event.MediaSyncEvent; -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.ui.AbstractDetailsFragment; -import org.xbmc.kore.utils.LogUtils; -import org.xbmc.kore.utils.MediaPlayerUtils; -import org.xbmc.kore.utils.UIUtils; -import org.xbmc.kore.utils.Utils; - -import java.util.ArrayList; - -import at.blogc.android.views.ExpandableTextView; -import butterknife.ButterKnife; -import butterknife.InjectView; - -/** - * Presents a TV Show overview - */ -public class TVShowDetailsFragment extends AbstractDetailsFragment - implements LoaderManager.LoaderCallbacks { - private static final String TAG = LogUtils.makeLogTag(TVShowDetailsFragment.class); - - private static final int NEXT_EPISODES_COUNT = 2; - - public static final String POSTER_TRANS_NAME = "POSTER_TRANS_NAME"; - public static final String BUNDLE_KEY_TVSHOWID = "tvshow_id"; - public static final String BUNDLE_KEY_TITLE = "title"; - public static final String BUNDLE_KEY_PREMIERED = "premiered"; - public static final String BUNDLE_KEY_STUDIO = "studio"; - public static final String BUNDLE_KEY_EPISODE = "episode"; - public static final String BUNDLE_KEY_WATCHEDEPISODES = "watchedepisodes"; - public static final String BUNDLE_KEY_RATING = "rating"; - public static final String BUNDLE_KEY_PLOT = "plot"; - public static final String BUNDLE_KEY_GENRES = "genres"; - - public interface TVShowDetailsActionListener { - void onSeasonSelected(int tvshowId, int season); - void onNextEpisodeSelected(int episodeId); - } - - // Activity listener - private TVShowDetailsActionListener listenerActivity; - - // Loader IDs - private static final int LOADER_TVSHOW = 0, - LOADER_NEXT_EPISODES = 1, - LOADER_SEASONS = 2, - LOADER_CAST = 3; - - // Displayed movie id - private int tvshowId = -1; - private String tvshowTitle; - - // 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; - -// // Buttons -// @InjectView(R.id.go_to_imdb) ImageButton imdbButton; - - // 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.premiered) TextView mediaPremiered; - @InjectView(R.id.genres) TextView mediaGenres; - - @InjectView(R.id.media_description) ExpandableTextView mediaDescription; - @InjectView(R.id.cast_list) GridLayout videoCastList; - - @InjectView(R.id.next_episode_title) TextView nextEpisodeTitle; - @InjectView(R.id.next_episode_list) GridLayout nextEpisodeList; - - @InjectView(R.id.seasons_title) TextView seasonsListTitle; - @InjectView(R.id.seasons_list) GridLayout seasonsList; - - @InjectView(R.id.media_description_container) LinearLayout mediaDescriptionContainer; - @InjectView(R.id.show_all) ImageView mediaShowAll; - - /** - * Create a new instance of this, initialized to show tvshowId - */ - @TargetApi(21) - public static TVShowDetailsFragment newInstance(TVShowListFragment.ViewHolder vh) { - TVShowDetailsFragment fragment = new TVShowDetailsFragment(); - - Bundle args = new Bundle(); - args.putInt(BUNDLE_KEY_TVSHOWID, vh.tvshowId); - args.putInt(BUNDLE_KEY_EPISODE, vh.episode); - args.putString(BUNDLE_KEY_GENRES, vh.genres); - args.putString(BUNDLE_KEY_PLOT, vh.plot); - args.putString(BUNDLE_KEY_PREMIERED, vh.premiered); - args.putDouble(BUNDLE_KEY_RATING, vh.rating); - args.putString(BUNDLE_KEY_STUDIO, vh.studio); - args.putString(BUNDLE_KEY_TITLE, vh.tvshowTitle); - args.putInt(BUNDLE_KEY_WATCHEDEPISODES, vh.watchedEpisodes); - if( Utils.isLollipopOrLater()) { - args.putString(POSTER_TRANS_NAME, vh.artView.getTransitionName()); - } - fragment.setArguments(args); - return fragment; - } - - @Override - @TargetApi(21) - protected View createView(LayoutInflater inflater, ViewGroup container) { - Bundle bundle = getArguments(); - tvshowId = bundle.getInt(BUNDLE_KEY_TVSHOWID, -1); - - if (tvshowId == -1) { - // There's nothing to show - return null; - } - - ViewGroup root = (ViewGroup) inflater.inflate(R.layout.fragment_tvshow_overview, 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); - } - }); - - tvshowTitle = bundle.getString(BUNDLE_KEY_TITLE); - - mediaTitle.setText(tvshowTitle); - setMediaUndertitle(bundle.getInt(BUNDLE_KEY_EPISODE), bundle.getInt(BUNDLE_KEY_WATCHEDEPISODES)); - setMediaPremiered(bundle.getString(BUNDLE_KEY_PREMIERED), bundle.getString(BUNDLE_KEY_STUDIO)); - mediaGenres.setText(bundle.getString(BUNDLE_KEY_GENRES)); - setMediaRating(bundle.getDouble(BUNDLE_KEY_RATING)); - mediaDescription.setText(bundle.getString(BUNDLE_KEY_PLOT)); - - if(Utils.isLollipopOrLater()) { - mediaPoster.setTransitionName(getArguments().getString(POSTER_TRANS_NAME)); - } - // 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_TVSHOW; - } - - @Override - protected String getSyncID() { - return LibrarySyncService.SYNC_TVSHOWID; - } - - @Override - protected int getSyncItemID() { - return tvshowId; - } - - @Override - protected SwipeRefreshLayout getSwipeRefreshLayout() { - return swipeRefreshLayout; - } - - @Override - protected void onDownload() { } - - @Override - public void onActivityCreated (Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - - hasIssuedOutdatedRefresh = false; - - // Start the loaders - getLoaderManager().initLoader(LOADER_TVSHOW, null, this); - getLoaderManager().initLoader(LOADER_NEXT_EPISODES, null, this); - getLoaderManager().initLoader(LOADER_SEASONS, null, this); - getLoaderManager().initLoader(LOADER_CAST, null, this); - } - - @Override - public void onAttach(Context activity) { - super.onAttach(activity); - try { - listenerActivity = (TVShowDetailsActionListener) activity; - } catch (ClassCastException e) { - throw new ClassCastException(activity.toString() + " must implement TVShowDetailsActionListener"); - } - } - - @Override - public void onDetach() { - super.onDetach(); - listenerActivity = null; - } - - @Override - protected void onSyncProcessEnded(MediaSyncEvent event) { - if (event.status == MediaSyncEvent.STATUS_SUCCESS) { - getLoaderManager().restartLoader(LOADER_TVSHOW, null, this); - getLoaderManager().restartLoader(LOADER_NEXT_EPISODES, null, this); - getLoaderManager().restartLoader(LOADER_SEASONS, 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_TVSHOW: - uri = MediaContract.TVShows.buildTVShowUri(getHostInfo().getId(), tvshowId); - return new CursorLoader(getActivity(), uri, - TVShowDetailsQuery.PROJECTION, null, null, null); - case LOADER_NEXT_EPISODES: - // Load seasons - uri = MediaContract.Episodes.buildTVShowEpisodesListUri(getHostInfo().getId(), tvshowId, NEXT_EPISODES_COUNT); - String selection = MediaContract.EpisodesColumns.PLAYCOUNT + "=0"; - return new CursorLoader(getActivity(), uri, - NextEpisodesListQuery.PROJECTION, selection, null, NextEpisodesListQuery.SORT); - case LOADER_SEASONS: - // Load seasons - uri = MediaContract.Seasons.buildTVShowSeasonsListUri(getHostInfo().getId(), tvshowId); - return new CursorLoader(getActivity(), uri, - SeasonsListQuery.PROJECTION, null, null, SeasonsListQuery.SORT); - case LOADER_CAST: - uri = MediaContract.TVShowCast.buildTVShowCastListUri(getHostInfo().getId(), tvshowId); - return new CursorLoader(getActivity(), uri, - TVShowCastListQuery.PROJECTION, null, null, TVShowCastListQuery.SORT); - default: - return null; - } - } - - /** {@inheritDoc} */ - @Override - public void onLoadFinished(Loader cursorLoader, Cursor cursor) { - LogUtils.LOGD(TAG, "onLoadFinished"); - if (cursor != null) { - switch (cursorLoader.getId()) { - case LOADER_TVSHOW: - displayTVShowDetails(cursor); - checkOutdatedTVShowDetails(cursor); - break; - case LOADER_NEXT_EPISODES: - displayNextEpisodeList(cursor); - break; - case LOADER_SEASONS: - displaySeasonList(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.go_to_imdb) -// public void onImdbClicked(View v) { -// String imdbNumber = (String)v.getTag(); -// -// if (imdbNumber != null) { -// Utils.openImdbForMovie(getActivity(), imdbNumber); -// } -// } - - /** - * Display the tv show details - * - * @param cursor Cursor with the data - */ - private void displayTVShowDetails(Cursor cursor) { - LogUtils.LOGD(TAG, "displayTVShowDetails"); - cursor.moveToFirst(); - tvshowTitle = cursor.getString(TVShowDetailsQuery.TITLE); - mediaTitle.setText(tvshowTitle); - int numEpisodes = cursor.getInt(TVShowDetailsQuery.EPISODE), - watchedEpisodes = cursor.getInt(TVShowDetailsQuery.WATCHEDEPISODES); - setMediaUndertitle(numEpisodes, watchedEpisodes); - - setMediaPremiered(cursor.getString(TVShowDetailsQuery.PREMIERED), cursor.getString(TVShowDetailsQuery.STUDIO)); - - mediaGenres.setText(cursor.getString(TVShowDetailsQuery.GENRES)); - - setMediaRating(cursor.getDouble(TVShowDetailsQuery.RATING)); - - mediaDescription.setText(cursor.getString(TVShowDetailsQuery.PLOT)); - - Resources resources = getActivity().getResources(); - TypedArray styledAttributes = getActivity().getTheme().obtainStyledAttributes(new int[] { - R.attr.iconExpand, - R.attr.iconCollapse - }); - final int iconCollapseResId = - styledAttributes.getResourceId(styledAttributes.getIndex(0), R.drawable.ic_expand_less_white_24dp); - final int iconExpandResId = - styledAttributes.getResourceId(styledAttributes.getIndex(1), R.drawable.ic_expand_more_white_24dp); - styledAttributes.recycle(); - mediaDescriptionContainer.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - mediaDescription.toggle(); - mediaShowAll.setImageResource(mediaDescription.isExpanded() ? iconCollapseResId: iconExpandResId); - } - }); - - -// // IMDB button -// imdbButton.setTag(cursor.getString(TVShowDetailsQuery.IMDBNUMBER)); - - // Images - 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(TVShowDetailsQuery.THUMBNAIL), tvshowTitle, - mediaPoster, posterWidth, posterHeight); - int artHeight = resources.getDimensionPixelOffset(R.dimen.now_playing_art_height); - UIUtils.loadImageIntoImageview(getHostManager(), - cursor.getString(TVShowDetailsQuery.FANART), - mediaArt, displayMetrics.widthPixels, artHeight); - } - - private void setMediaUndertitle(int numEpisodes, int watchedEpisodes) { - String episodes = String.format(getString(R.string.num_episodes), - numEpisodes, numEpisodes - watchedEpisodes); - mediaUndertitle.setText(episodes); - } - - private void setMediaPremiered(String premiered, String studio) { - mediaPremiered.setText(String.format(getString(R.string.premiered), premiered) + " | " + studio); - } - - private void setMediaRating(double rating) { - if (rating > 0) { - mediaRating.setVisibility(View.VISIBLE); - mediaMaxRating.setVisibility(View.VISIBLE); - mediaRating.setText(String.format("%01.01f", rating)); - mediaMaxRating.setText(getString(R.string.max_rating_video)); - } else { - mediaRating.setVisibility(View.INVISIBLE); - mediaMaxRating.setVisibility(View.INVISIBLE); - } - } - - /** - * Display the cast details - * - * @param cursor Cursor with the data - */ - private void displayCastList(Cursor cursor) { - // Transform the cursor into a List - if (cursor.moveToFirst()) { - ArrayList castArrayList = new ArrayList<>(cursor.getCount()); - do { - castArrayList.add(new VideoType.Cast(cursor.getString(TVShowCastListQuery.NAME), - cursor.getInt(TVShowCastListQuery.ORDER), - cursor.getString(TVShowCastListQuery.ROLE), - cursor.getString(TVShowCastListQuery.THUMBNAIL))); - } while (cursor.moveToNext()); - - UIUtils.setupCastInfo(getActivity(), castArrayList, videoCastList, - AllCastActivity.buildLaunchIntent(getActivity(), tvshowTitle, castArrayList)); - } - } - - private View.OnClickListener contextlistItemMenuClickListener = new View.OnClickListener() { - @Override - public void onClick(final View v) { - final PlaylistType.Item playListItem = new PlaylistType.Item(); - playListItem.episodeid = (int)v.getTag(); - - final PopupMenu popupMenu = new PopupMenu(getActivity(), v); - popupMenu.getMenuInflater().inflate(R.menu.musiclist_item, popupMenu.getMenu()); - popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { - @Override - public boolean onMenuItemClick(MenuItem item) { - switch (item.getItemId()) { - case R.id.action_play: - MediaPlayerUtils.play(TVShowDetailsFragment.this, playListItem); - return true; - case R.id.action_queue: - MediaPlayerUtils.queue(TVShowDetailsFragment.this, playListItem, PlaylistType.GetPlaylistsReturnType.VIDEO); - return true; - } - return false; - } - }); - popupMenu.show(); - } - }; - - /** - * Display next episode list - * - * @param cursor Cursor with the data - */ - private void displayNextEpisodeList(Cursor cursor) { - if (cursor.moveToFirst()) { - nextEpisodeTitle.setVisibility(View.VISIBLE); - nextEpisodeList.setVisibility(View.VISIBLE); - - HostManager hostManager = HostManager.getInstance(getActivity()); - - View.OnClickListener episodeClickListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - listenerActivity.onNextEpisodeSelected((int)v.getTag()); - } - }; - - // Get the art dimensions - Resources resources = getActivity().getResources(); - int artWidth = (int)(resources.getDimension(R.dimen.episodelist_art_width) / - UIUtils.IMAGE_RESIZE_FACTOR); - int artHeight = (int)(resources.getDimension(R.dimen.episodelist_art_heigth) / - UIUtils.IMAGE_RESIZE_FACTOR); - - nextEpisodeList.removeAllViews(); - do { - int episodeId = cursor.getInt(NextEpisodesListQuery.EPISODEID); - String title = cursor.getString(NextEpisodesListQuery.TITLE); - String seasonEpisode = String.format(getString(R.string.season_episode), - cursor.getInt(NextEpisodesListQuery.SEASON), - cursor.getInt(NextEpisodesListQuery.EPISODE)); - int runtime = cursor.getInt(NextEpisodesListQuery.RUNTIME) / 60; - String duration = runtime > 0 ? - String.format(getString(R.string.minutes_abbrev), String.valueOf(runtime)) + - " | " + cursor.getString(NextEpisodesListQuery.FIRSTAIRED) : - cursor.getString(NextEpisodesListQuery.FIRSTAIRED); - String thumbnail = cursor.getString(NextEpisodesListQuery.THUMBNAIL); - - View episodeView = LayoutInflater.from(getActivity()) - .inflate(R.layout.list_item_next_episode, nextEpisodeList, false); - - ImageView artView = (ImageView)episodeView.findViewById(R.id.art); - TextView titleView = (TextView)episodeView.findViewById(R.id.title); - TextView detailsView = (TextView)episodeView.findViewById(R.id.details); - TextView durationView = (TextView)episodeView.findViewById(R.id.duration); - - titleView.setText(title); - detailsView.setText(seasonEpisode); - durationView.setText(duration); - - UIUtils.loadImageWithCharacterAvatar(getActivity(), hostManager, - thumbnail, title, - artView, artWidth, artHeight); - episodeView.setTag(episodeId); - episodeView.setOnClickListener(episodeClickListener); - - // For the popupmenu - ImageView contextMenu = (ImageView)episodeView.findViewById(R.id.list_context_menu); - contextMenu.setTag(episodeId); - contextMenu.setOnClickListener(contextlistItemMenuClickListener); - - nextEpisodeList.addView(episodeView); - } while (cursor.moveToNext()); - } else { - // No episodes, hide views - nextEpisodeTitle.setVisibility(View.GONE); - nextEpisodeList.setVisibility(View.GONE); - } - } - - /** - * Display the seasons list - * - * @param cursor Cursor with the data - */ - private void displaySeasonList(Cursor cursor) { - if (cursor.moveToFirst()) { - seasonsListTitle.setVisibility(View.VISIBLE); - seasonsList.setVisibility(View.VISIBLE); - - HostManager hostManager = HostManager.getInstance(getActivity()); - - View.OnClickListener seasonListClickListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - listenerActivity.onSeasonSelected(tvshowId, (int)v.getTag()); - } - }; - - // Get the art dimensions - Resources resources = getActivity().getResources(); - int artWidth = (int)(resources.getDimension(R.dimen.seasonlist_art_width) / - UIUtils.IMAGE_RESIZE_FACTOR); - int artHeight = (int)(resources.getDimension(R.dimen.seasonlist_art_heigth) / - UIUtils.IMAGE_RESIZE_FACTOR); - - seasonsList.removeAllViews(); - do { - int seasonNumber = cursor.getInt(SeasonsListQuery.SEASON); - String thumbnail = cursor.getString(SeasonsListQuery.THUMBNAIL); - int numEpisodes = cursor.getInt(SeasonsListQuery.EPISODE); - int watchedEpisodes = cursor.getInt(SeasonsListQuery.WATCHEDEPISODES); - - View seasonView = LayoutInflater.from(getActivity()).inflate(R.layout.grid_item_season, seasonsList, false); - - ImageView seasonPictureView = (ImageView) seasonView.findViewById(R.id.art); - TextView seasonNumberView = (TextView) seasonView.findViewById(R.id.season); - TextView seasonEpisodesView = (TextView) seasonView.findViewById(R.id.episodes); - ProgressBar seasonProgressBar = (ProgressBar) seasonView.findViewById(R.id.season_progress_bar); - - seasonNumberView.setText(String.format(getActivity().getString(R.string.season_number), seasonNumber)); - seasonEpisodesView.setText(String.format(getActivity().getString(R.string.num_episodes), - numEpisodes, numEpisodes - watchedEpisodes)); - seasonProgressBar.setMax(numEpisodes); - seasonProgressBar.setProgress(watchedEpisodes); - - UIUtils.loadImageWithCharacterAvatar(getActivity(), hostManager, - thumbnail, - String.valueOf(seasonNumber), - seasonPictureView, artWidth, artHeight); - - seasonView.setTag(seasonNumber); - seasonView.setOnClickListener(seasonListClickListener); - seasonsList.addView(seasonView); - } while (cursor.moveToNext()); - } else { - // No seasons, hide views - seasonsListTitle.setVisibility(View.GONE); - seasonsList.setVisibility(View.GONE); - } - } - - /** - * Checks wether we should refresh the TV Show 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 checkOutdatedTVShowDetails(Cursor cursor) { - if (hasIssuedOutdatedRefresh) - return; - - cursor.moveToFirst(); - long lastUpdated = cursor.getLong(TVShowDetailsQuery.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; - } - - /** - * TV Show details query parameters. - */ - private interface TVShowDetailsQuery { - String[] PROJECTION = { - BaseColumns._ID, - MediaContract.TVShows.TITLE, - MediaContract.TVShows.THUMBNAIL, - MediaContract.TVShows.FANART, - MediaContract.TVShows.PREMIERED, - MediaContract.TVShows.STUDIO, - MediaContract.TVShows.EPISODE, - MediaContract.TVShows.WATCHEDEPISODES, - MediaContract.TVShows.RATING, - MediaContract.TVShows.PLOT, - MediaContract.TVShows.PLAYCOUNT, - MediaContract.TVShows.IMDBNUMBER, - MediaContract.TVShows.GENRES, - MediaContract.SyncColumns.UPDATED, - }; - - int ID = 0; - int TITLE = 1; - int THUMBNAIL = 2; - int FANART = 3; - int PREMIERED = 4; - int STUDIO = 5; - int EPISODE = 6; - int WATCHEDEPISODES = 7; - int RATING = 8; - int PLOT = 9; - int PLAYCOUNT = 10; - int IMDBNUMBER = 11; - int GENRES = 12; - int UPDATED = 13; - } - - /** - * Next episodes list query parameters. - */ - private interface NextEpisodesListQuery { - String[] PROJECTION = { - BaseColumns._ID, - MediaContract.Episodes.EPISODEID, - MediaContract.Episodes.SEASON, - MediaContract.Episodes.EPISODE, - MediaContract.Episodes.THUMBNAIL, - MediaContract.Episodes.PLAYCOUNT, - MediaContract.Episodes.TITLE, - MediaContract.Episodes.RUNTIME, - MediaContract.Episodes.FIRSTAIRED, - }; - - String SORT = MediaContract.Episodes.EPISODEID + " ASC"; - - int ID = 0; - int EPISODEID = 1; - int SEASON = 2; - int EPISODE = 3; - int THUMBNAIL = 4; - int PLAYCOUNT = 5; - int TITLE = 6; - int RUNTIME = 7; - int FIRSTAIRED = 8; - } - - /** - * Seasons list query parameters. - */ - private interface SeasonsListQuery { - String[] PROJECTION = { - BaseColumns._ID, - MediaContract.Seasons.SEASON, - MediaContract.Seasons.THUMBNAIL, - MediaContract.Seasons.EPISODE, - MediaContract.Seasons.WATCHEDEPISODES - }; - - String SORT = MediaContract.Seasons.SEASON + " ASC"; - - int ID = 0; - int SEASON = 1; - int THUMBNAIL = 2; - int EPISODE = 3; - int WATCHEDEPISODES = 4; - } - - /** - * Movie cast list query parameters. - */ - public interface TVShowCastListQuery { - String[] PROJECTION = { - BaseColumns._ID, - MediaContract.TVShowCast.NAME, - MediaContract.TVShowCast.ORDER, - MediaContract.TVShowCast.ROLE, - MediaContract.TVShowCast.THUMBNAIL, - }; - - String SORT = MediaContract.TVShowCast.ORDER + " ASC"; - - int ID = 0; - int NAME = 1; - int ORDER = 2; - int ROLE = 3; - int THUMBNAIL = 4; - } -} diff --git a/app/src/main/java/org/xbmc/kore/ui/sections/video/TVShowEpisodeDetailsFragment.java b/app/src/main/java/org/xbmc/kore/ui/sections/video/TVShowEpisodeDetailsFragment.java deleted file mode 100644 index 2b381e5..0000000 --- a/app/src/main/java/org/xbmc/kore/ui/sections/video/TVShowEpisodeDetailsFragment.java +++ /dev/null @@ -1,600 +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.sections.video; - -import android.content.DialogInterface; -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.support.v7.app.AlertDialog; -import android.util.DisplayMetrics; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewTreeObserver; -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.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.provider.MediaContract; -import org.xbmc.kore.service.library.LibrarySyncService; -import org.xbmc.kore.ui.AbstractDetailsFragment; -import org.xbmc.kore.utils.FileDownloadHelper; -import org.xbmc.kore.utils.LogUtils; -import org.xbmc.kore.utils.UIUtils; - -import java.io.File; -import java.util.ArrayList; - -import butterknife.ButterKnife; -import butterknife.InjectView; -import butterknife.OnClick; - -/** - * Presents movie details - */ -public class TVShowEpisodeDetailsFragment extends AbstractDetailsFragment - implements LoaderManager.LoaderCallbacks { - private static final String TAG = LogUtils.makeLogTag(TVShowEpisodeDetailsFragment.class); - - public static final String TVSHOWID = "tvshow_id"; - public static final String EPISODEID = "episode_id"; - - // Loader IDs - private static final int LOADER_EPISODE = 0; -// private static final int LOADER_CAST = 1; - - /** - * Handler on which to post RPC callbacks - */ - private Handler callbackHandler = new Handler(); - - // Displayed episode - private int tvshowId = -1; - private int episodeId = -1; - - // Info for downloading the episode - private FileDownloadHelper.TVShowInfo tvshowDownloadInfo = null; - - @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.download) ImageButton downloadButton; - @InjectView(R.id.seen) ImageButton seenButton; - - // 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.premiered) TextView mediaPremiered; - @InjectView(R.id.season) TextView mediaSeason; - - @InjectView(R.id.media_description) TextView mediaDescription; - @InjectView(R.id.directors) TextView mediaDirectors; -// @InjectView(R.id.cast_list) GridLayout videoCastList; -// @InjectView(R.id.additional_cast_list) TextView videoAdditionalCastList; -// @InjectView(R.id.additional_cast_title) TextView videoAdditionalCastTitle; - - /** - * Create a new instance of this, initialized to show the episode episodeId - */ - public static TVShowEpisodeDetailsFragment newInstance(final int tvshowId, final int episodeId) { - TVShowEpisodeDetailsFragment fragment = new TVShowEpisodeDetailsFragment(); - - Bundle args = new Bundle(); - args.putInt(TVSHOWID, tvshowId); - args.putInt(EPISODEID, episodeId); - fragment.setArguments(args); - return fragment; - } - - @Override - protected View createView(LayoutInflater inflater, ViewGroup container) { - tvshowId = getArguments().getInt(TVSHOWID, -1); - episodeId = getArguments().getInt(EPISODEID, -1); - - if(episodeId == -1) { - // There's nothing to show - return null; - } - - ViewGroup root = (ViewGroup) inflater.inflate(R.layout.fragment_episode_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); - - // 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_TVSHOW; - } - - @Override - protected String getSyncID() { - return LibrarySyncService.SYNC_TVSHOWID; - } - - @Override - protected int getSyncItemID() { - return tvshowId; - } - - @Override - protected SwipeRefreshLayout getSwipeRefreshLayout() { - return swipeRefreshLayout; - } - - @Override - public void onActivityCreated (Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - - // Start the loaders - getLoaderManager().initLoader(LOADER_EPISODE, null, this); -// getLoaderManager().initLoader(LOADER_CAST, null, this); - } - - @Override - public void onResume() { - // Force the exit view to invisible - exitTransitionView.setVisibility(View.INVISIBLE); - super.onResume(); - } - - @Override - protected void onSyncProcessEnded(MediaSyncEvent event) { - if (event.status == MediaSyncEvent.STATUS_SUCCESS) { - getLoaderManager().restartLoader(LOADER_EPISODE, null, this); - } - } - - /** - * Loader callbacks - */ - /** {@inheritDoc} */ - @Override - public Loader onCreateLoader(int i, Bundle bundle) { - Uri uri; - switch (i) { - case LOADER_EPISODE: - uri = MediaContract.Episodes.buildTVShowEpisodeUri(getHostInfo().getId(), tvshowId, episodeId); - return new CursorLoader(getActivity(), uri, - EpisodeDetailsQuery.PROJECTION, null, null, null); -// case LOADER_CAST: -// uri = MediaContract.MovieCast.buildMovieCastListUri(hostInfo.getId(), episodeId); -// return new CursorLoader(getActivity(), uri, -// MovieCastListQuery.PROJECTION, null, null, MovieCastListQuery.SORT_BY_NAME_IGNORE_ARTICLES); - default: - return null; - } - } - - /** {@inheritDoc} */ - @Override - public void onLoadFinished(Loader cursorLoader, Cursor cursor) { - if (cursor != null && cursor.getCount() > 0) { - switch (cursorLoader.getId()) { - case LOADER_EPISODE: - displayEpisodeDetails(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.episodeid = episodeId; - 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.episodeid = episodeId; - 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.seen) - public void onSeenClicked(View v) { - // Set the playcount - Integer playcount = (Integer)v.getTag(); - int newPlaycount = (playcount > 0) ? 0 : 1; - - VideoLibrary.SetEpisodeDetails action = - new VideoLibrary.SetEpisodeDetails(episodeId, newPlaycount, null); - action.execute(getHostManager().getConnection(), new ApiCallback() { - @Override - public void onSuccess(String result) { - // Force a refresh, but don't show a message - if (!isAdded()) return; - 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); - } - - @Override - protected void onDownload() { - if (tvshowDownloadInfo == 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(tvshowDownloadInfo.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(), - tvshowDownloadInfo, 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(), - tvshowDownloadInfo, 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_episode_download) - .setPositiveButton(android.R.string.ok, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - FileDownloadHelper.downloadFiles(getActivity(), getHostInfo(), - tvshowDownloadInfo, FileDownloadHelper.OVERWRITE_FILES, - callbackHandler); - } - }) - .setNegativeButton(android.R.string.cancel, noopClickListener) - .show(); - } - } - - /** - * Display the episode details - * - * @param cursor Cursor with the data - */ - private void displayEpisodeDetails(Cursor cursor) { - cursor.moveToFirst(); - mediaTitle.setText(cursor.getString(EpisodeDetailsQuery.TITLE)); - mediaUndertitle.setText(cursor.getString(EpisodeDetailsQuery.SHOWTITLE)); - - 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); - mediaPremiered.setText(durationPremiered); - String season = String.format(getString(R.string.season_episode), - cursor.getInt(EpisodeDetailsQuery.SEASON), - cursor.getInt(EpisodeDetailsQuery.EPISODE)); - mediaSeason.setText(season); - - double rating = cursor.getDouble(EpisodeDetailsQuery.RATING); - if (rating > 0) { - mediaRating.setVisibility(View.VISIBLE); - mediaMaxRating.setVisibility(View.VISIBLE); - mediaRating.setText(String.format("%01.01f", rating)); - mediaMaxRating.setText(getString(R.string.max_rating_video)); - } else { - mediaRating.setVisibility(View.INVISIBLE); - mediaMaxRating.setVisibility(View.INVISIBLE); - } - - mediaDescription.setText(cursor.getString(EpisodeDetailsQuery.PLOT)); - mediaDirectors.setText(cursor.getString(EpisodeDetailsQuery.DIRECTOR)); - - setupSeenButton(cursor.getInt(EpisodeDetailsQuery.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.loadImageIntoImageview(hostManager, -// cursor.getString(EpisodeDetailsQuery.THUMBNAIL), -// mediaPoster, posterWidth, posterHeight); - int artHeight = resources.getDimensionPixelOffset(R.dimen.now_playing_art_height); - UIUtils.loadImageIntoImageview(getHostManager(), - cursor.getString(EpisodeDetailsQuery.THUMBNAIL), - mediaArt, displayMetrics.widthPixels, artHeight); - - // Setup movie download info - tvshowDownloadInfo = new FileDownloadHelper.TVShowInfo( - cursor.getString(EpisodeDetailsQuery.SHOWTITLE), - cursor.getInt(EpisodeDetailsQuery.SEASON), - cursor.getInt(EpisodeDetailsQuery.EPISODE), - cursor.getString(EpisodeDetailsQuery.TITLE), - cursor.getString(EpisodeDetailsQuery.FILE)); - - // Check if downloaded file exists - downloadButton.setVisibility(View.VISIBLE); - if (tvshowDownloadInfo.downloadFileExists()) { - Resources.Theme theme = getActivity().getTheme(); - TypedArray styledAttributes = theme.obtainStyledAttributes(new int[]{ - R.attr.colorAccent}); - downloadButton.setColorFilter( - styledAttributes.getColor(0, getResources().getColor(R.color.accent_default))); - styledAttributes.recycle(); - } else { - downloadButton.clearColorFilter(); - } - } - - 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, 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()) { -// List castList = new ArrayList(cursor.getCount()); -// do { -// castList.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(), castList, videoCastList, -// videoAdditionalCastTitle, videoAdditionalCastList); -// } -// } -// - /** - * Episode details query parameters. - */ - private interface EpisodeDetailsQuery { - String[] PROJECTION = { - BaseColumns._ID, - MediaContract.Episodes.TITLE, - MediaContract.Episodes.SHOWTITLE, - MediaContract.Episodes.SEASON, - MediaContract.Episodes.EPISODE, - MediaContract.Episodes.THUMBNAIL, - MediaContract.Episodes.FANART, - MediaContract.Episodes.FIRSTAIRED, - MediaContract.Episodes.RUNTIME, - MediaContract.Episodes.RATING, - MediaContract.Episodes.PLOT, - MediaContract.Episodes.PLAYCOUNT, - MediaContract.Episodes.DIRECTOR, - MediaContract.Episodes.WRITER, - MediaContract.Episodes.FILE, - }; - - final int ID = 0; - final int TITLE = 1; - final int SHOWTITLE = 2; - final int SEASON = 3; - final int EPISODE = 4; - final int THUMBNAIL = 5; - final int FANART = 6; - final int FIRSTAIRED = 7; - final int RUNTIME = 8; - final int RATING = 9; - final int PLOT = 10; - final int PLAYCOUNT = 11; - final int DIRECTOR = 12; - final int WRITER = 13; - final int FILE = 14; - } - -// /** -// * Movie cast list query parameters. -// */ -// private interface MovieCastListQuery { -// String[] PROJECTION = { -// MediaContract.MovieCast.ID, -// MediaContract.MovieCast.NAME, -// MediaContract.MovieCast.ORDER, -// MediaContract.MovieCast.ROLE, -// MediaContract.MovieCast.THUMBNAIL, -// }; -// -// String SORT_BY_NAME_IGNORE_ARTICLES = 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/sections/video/TVShowEpisodeInfoFragment.java b/app/src/main/java/org/xbmc/kore/ui/sections/video/TVShowEpisodeInfoFragment.java new file mode 100644 index 0000000..1c8a429 --- /dev/null +++ b/app/src/main/java/org/xbmc/kore/ui/sections/video/TVShowEpisodeInfoFragment.java @@ -0,0 +1,341 @@ +/* + * 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.sections.video; + +import android.content.DialogInterface; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.provider.BaseColumns; +import android.support.annotation.Nullable; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.CursorLoader; +import android.support.v4.content.Loader; +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; +import org.xbmc.kore.jsonrpc.event.MediaSyncEvent; +import org.xbmc.kore.jsonrpc.method.VideoLibrary; +import org.xbmc.kore.jsonrpc.type.PlaylistType; +import org.xbmc.kore.provider.MediaContract; +import org.xbmc.kore.service.library.LibrarySyncService; +import org.xbmc.kore.ui.AbstractAdditionalInfoFragment; +import org.xbmc.kore.ui.AbstractInfoFragment; +import org.xbmc.kore.ui.generic.RefreshItem; +import org.xbmc.kore.utils.FileDownloadHelper; +import org.xbmc.kore.utils.LogUtils; +import org.xbmc.kore.utils.Utils; + +import java.io.File; + +/** + * Presents movie details + */ +public class TVShowEpisodeInfoFragment extends AbstractInfoFragment + implements LoaderManager.LoaderCallbacks { + private static final String TAG = LogUtils.makeLogTag(TVShowEpisodeInfoFragment.class); + + public static final String BUNDLE_KEY_TVSHOWID = "tvshow_id"; + + // Loader IDs + private static final int LOADER_EPISODE = 0; + + /** + * Handler on which to post RPC callbacks + */ + private Handler callbackHandler = new Handler(); + + // Displayed episode + private int tvshowId = -1; + + private Cursor cursor; + + FileDownloadHelper.TVShowInfo fileDownloadHelper; + + public void setTvshowId(int tvshowId) { + getDataHolder().getBundle().putInt(BUNDLE_KEY_TVSHOWID, tvshowId); + } + + @Override + protected RefreshItem createRefreshItem() { + RefreshItem refreshItem = new RefreshItem(getActivity(), + LibrarySyncService.SYNC_SINGLE_TVSHOW); + refreshItem.setSyncItem(LibrarySyncService.SYNC_TVSHOWID, tvshowId); + refreshItem.setListener(new RefreshItem.RefreshItemListener() { + @Override + public void onSyncProcessEnded(MediaSyncEvent event) { + if (event.status == MediaSyncEvent.STATUS_SUCCESS) { + getLoaderManager().restartLoader(LOADER_EPISODE, null, + TVShowEpisodeInfoFragment.this); + } + } + }); + return refreshItem; + } + + @Override + protected boolean setupMediaActionBar() { + setOnDownloadListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + downloadEpisode(); + } + }); + + setOnAddToPlaylistListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + Utils.addToPlaylist(TVShowEpisodeInfoFragment.this, getDataHolder().getId(), + PlaylistType.GetPlaylistsReturnType.VIDEO); + } + }); + + setOnSeenListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + Integer playcount = cursor.getInt(EpisodeDetailsQuery.PLAYCOUNT); + int newPlaycount = (playcount > 0) ? 0 : 1; + + VideoLibrary.SetEpisodeDetails action = + new VideoLibrary.SetEpisodeDetails(getDataHolder().getId(), newPlaycount, null); + action.execute(getHostManager().getConnection(), new ApiCallback() { + @Override + public void onSuccess(String result) { + // Force a refresh, but don't show a message + if (!isAdded()) return; + getRefreshItem().startSync(true); + } + + @Override + public void onError(int errorCode, String description) { } + }, callbackHandler); + } + }); + + return true; + } + + @Override + protected boolean setupFAB(ImageButton FAB) { + FAB.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + PlaylistType.Item item = new PlaylistType.Item(); + item.episodeid = getDataHolder().getId(); + fabActionPlayItem(item); + } + }); + return true; + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + this.tvshowId = getArguments().getInt(BUNDLE_KEY_TVSHOWID, -1); + } + + @Override + public void onActivityCreated (Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + getLoaderManager().initLoader(LOADER_EPISODE, null, this); + } + + /** + * Loader callbacks + */ + /** {@inheritDoc} */ + @Override + public Loader onCreateLoader(int i, Bundle bundle) { + Uri uri; + switch (i) { + case LOADER_EPISODE: + uri = MediaContract.Episodes.buildTVShowEpisodeUri(getHostInfo().getId(), tvshowId, + getDataHolder().getId()); + return new CursorLoader(getActivity(), uri, + EpisodeDetailsQuery.PROJECTION, null, null, null); + default: + return null; + } + } + + /** {@inheritDoc} */ + @Override + public void onLoadFinished(Loader cursorLoader, Cursor cursor) { + if (cursor != null && cursor.getCount() > 0) { + switch (cursorLoader.getId()) { + case LOADER_EPISODE: + cursor.moveToFirst(); + this.cursor = cursor; + + DataHolder dataHolder = getDataHolder(); + + dataHolder.setPosterUrl(cursor.getString(EpisodeDetailsQuery.THUMBNAIL)); + + dataHolder.setRating(cursor.getDouble(EpisodeDetailsQuery.RATING)); + dataHolder.setMaxRating(10); + + String director = cursor.getString(EpisodeDetailsQuery.DIRECTOR); + if (!TextUtils.isEmpty(director)) { + 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 season = String.format(getString(R.string.season_episode), + cursor.getInt(EpisodeDetailsQuery.SEASON), + cursor.getInt(EpisodeDetailsQuery.EPISODE)); + + dataHolder.setDetails(durationPremiered + "\n" + season + "\n" + director); + + fileDownloadHelper = new FileDownloadHelper.TVShowInfo( + cursor.getString(EpisodeDetailsQuery.SHOWTITLE), + cursor.getInt(EpisodeDetailsQuery.SEASON), + cursor.getInt(EpisodeDetailsQuery.EPISODE), + cursor.getString(EpisodeDetailsQuery.TITLE), + cursor.getString(EpisodeDetailsQuery.FILE)); + + setDownloadButtonState(fileDownloadHelper.downloadFileExists()); + + setSeenButtonState(cursor.getInt(EpisodeDetailsQuery.PLAYCOUNT) > 0); + + getDataHolder().setTitle(cursor.getString(EpisodeDetailsQuery.TITLE)); + getDataHolder().setUndertitle(cursor.getString(EpisodeDetailsQuery.SHOWTITLE)); + setExpandDescription(true); + getDataHolder().setDescription(cursor.getString(EpisodeDetailsQuery.PLOT)); + + updateView(dataHolder); + break; + } + } + } + + /** {@inheritDoc} */ + @Override + public void onLoaderReset(Loader cursorLoader) { + // Release loader's data + } + + 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 + public void onClick(DialogInterface dialog, int which) { } + }; + + // Check if the directory exists and whether to overwrite it + File file = new File(tvshowDownloadInfo.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(), + tvshowDownloadInfo, 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(), + tvshowDownloadInfo, 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_episode_download) + .setPositiveButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + FileDownloadHelper.downloadFiles(getActivity(), getHostInfo(), + tvshowDownloadInfo, FileDownloadHelper.OVERWRITE_FILES, + callbackHandler); + } + }) + .setNegativeButton(android.R.string.cancel, noopClickListener) + .show(); + } + } + + @Override + protected AbstractAdditionalInfoFragment getAdditionalInfoFragment() { + return null; + } + + /** + * Episode details query parameters. + */ + private interface EpisodeDetailsQuery { + String[] PROJECTION = { + BaseColumns._ID, + MediaContract.Episodes.TITLE, + MediaContract.Episodes.SHOWTITLE, + MediaContract.Episodes.SEASON, + MediaContract.Episodes.EPISODE, + MediaContract.Episodes.THUMBNAIL, + MediaContract.Episodes.FANART, + MediaContract.Episodes.FIRSTAIRED, + MediaContract.Episodes.RUNTIME, + MediaContract.Episodes.RATING, + MediaContract.Episodes.PLOT, + MediaContract.Episodes.PLAYCOUNT, + MediaContract.Episodes.DIRECTOR, + MediaContract.Episodes.WRITER, + MediaContract.Episodes.FILE, + }; + + int ID = 0; + int TITLE = 1; + int SHOWTITLE = 2; + int SEASON = 3; + int EPISODE = 4; + int THUMBNAIL = 5; + int FANART = 6; + int FIRSTAIRED = 7; + int RUNTIME = 8; + int RATING = 9; + int PLOT = 10; + int PLAYCOUNT = 11; + int DIRECTOR = 12; + int WRITER = 13; + int FILE = 14; + } +} diff --git a/app/src/main/java/org/xbmc/kore/ui/sections/video/TVShowEpisodeListFragment.java b/app/src/main/java/org/xbmc/kore/ui/sections/video/TVShowEpisodeListFragment.java index e6a46b1..b88d799 100644 --- a/app/src/main/java/org/xbmc/kore/ui/sections/video/TVShowEpisodeListFragment.java +++ b/app/src/main/java/org/xbmc/kore/ui/sections/video/TVShowEpisodeListFragment.java @@ -33,10 +33,10 @@ import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import android.widget.CursorAdapter; import android.widget.ImageView; import android.widget.PopupMenu; import android.widget.TextView; -import android.widget.CursorAdapter; import org.xbmc.kore.R; import org.xbmc.kore.Settings; @@ -46,10 +46,10 @@ import org.xbmc.kore.jsonrpc.type.PlaylistType; import org.xbmc.kore.provider.MediaContract; import org.xbmc.kore.service.library.LibrarySyncService; import org.xbmc.kore.ui.AbstractCursorListFragment; +import org.xbmc.kore.ui.AbstractInfoFragment; import org.xbmc.kore.utils.LogUtils; import org.xbmc.kore.utils.MediaPlayerUtils; import org.xbmc.kore.utils.UIUtils; -import org.xbmc.kore.utils.Utils; /** * Presents a list of episodes for a TV show season @@ -58,7 +58,7 @@ public class TVShowEpisodeListFragment extends AbstractCursorListFragment { private static final String TAG = LogUtils.makeLogTag(TVShowEpisodeListFragment.class); public interface OnEpisodeSelectedListener { - void onEpisodeSelected(EpisodeViewHolder vh); + void onEpisodeSelected(int tvshowId, ViewHolder dataHolder); } public static final String TVSHOWID = "tvshow_id"; @@ -109,9 +109,9 @@ public class TVShowEpisodeListFragment extends AbstractCursorListFragment { @Override protected void onListItemClicked(View view) { // Get the movie id from the tag - EpisodeViewHolder tag = (EpisodeViewHolder) view.getTag(); + ViewHolder tag = (ViewHolder) view.getTag(); // Notify the activity - listenerActivity.onEpisodeSelected(tag); + listenerActivity.onEpisodeSelected(tvshowId, tag); } @@ -253,7 +253,7 @@ public class TVShowEpisodeListFragment extends AbstractCursorListFragment { .inflate(R.layout.list_item_episode, parent, false); // Setup View holder pattern - EpisodeViewHolder viewHolder = new EpisodeViewHolder(); + ViewHolder viewHolder = new ViewHolder(); viewHolder.titleView = (TextView)view.findViewById(R.id.title); viewHolder.detailsView = (TextView)view.findViewById(R.id.details); viewHolder.episodenumberView = (TextView)view.findViewById(R.id.episode_number); @@ -269,15 +269,11 @@ public class TVShowEpisodeListFragment extends AbstractCursorListFragment { @TargetApi(21) @Override public void bindView(View view, Context context, Cursor cursor) { - final EpisodeViewHolder viewHolder = (EpisodeViewHolder)view.getTag(); + final ViewHolder viewHolder = (ViewHolder)view.getTag(); // Save the episode id - viewHolder.episodeId = cursor.getInt(EpisodesListQuery.EPISODEID); - viewHolder.title = cursor.getString(EpisodesListQuery.TITLE); - - if(Utils.isLollipopOrLater()) { - viewHolder.artView.setTransitionName("a"+viewHolder.episodeId); - } + viewHolder.dataHolder.setId(cursor.getInt(EpisodesListQuery.EPISODEID)); + viewHolder.dataHolder.setTitle(cursor.getString(EpisodesListQuery.TITLE)); viewHolder.episodenumberView.setText( String.format(context.getString(R.string.episode_number), @@ -298,7 +294,8 @@ public class TVShowEpisodeListFragment extends AbstractCursorListFragment { } UIUtils.loadImageWithCharacterAvatar(context, hostManager, - cursor.getString(EpisodesListQuery.THUMBNAIL), viewHolder.title, + cursor.getString(EpisodesListQuery.THUMBNAIL), + viewHolder.dataHolder.getTitle(), viewHolder.artView, artWidth, artHeight); // For the popupmenu @@ -311,7 +308,7 @@ public class TVShowEpisodeListFragment extends AbstractCursorListFragment { /** * View holder pattern, only for episodes */ - public static class EpisodeViewHolder { + public static class ViewHolder { TextView titleView; TextView detailsView; TextView episodenumberView; @@ -319,17 +316,16 @@ public class TVShowEpisodeListFragment extends AbstractCursorListFragment { ImageView checkmarkView; ImageView artView; - int episodeId; - String title; + AbstractInfoFragment.DataHolder dataHolder = new AbstractInfoFragment.DataHolder(0); } private View.OnClickListener contextlistItemMenuClickListener = new View.OnClickListener() { @Override public void onClick(final View v) { - final EpisodeViewHolder viewHolder = (EpisodeViewHolder)v.getTag(); + final ViewHolder viewHolder = (ViewHolder)v.getTag(); final PlaylistType.Item playListItem = new PlaylistType.Item(); - playListItem.episodeid = viewHolder.episodeId; + playListItem.episodeid = viewHolder.dataHolder.getId(); final PopupMenu popupMenu = new PopupMenu(getActivity(), v); popupMenu.getMenuInflater().inflate(R.menu.musiclist_item, popupMenu.getMenu()); 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 new file mode 100644 index 0000000..9c811d8 --- /dev/null +++ b/app/src/main/java/org/xbmc/kore/ui/sections/video/TVShowInfoFragment.java @@ -0,0 +1,210 @@ +/* + * 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.sections.video; + +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.provider.BaseColumns; +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; +import org.xbmc.kore.jsonrpc.event.MediaSyncEvent; +import org.xbmc.kore.provider.MediaContract; +import org.xbmc.kore.service.library.LibrarySyncService; +import org.xbmc.kore.ui.AbstractAdditionalInfoFragment; +import org.xbmc.kore.ui.AbstractInfoFragment; +import org.xbmc.kore.ui.generic.RefreshItem; +import org.xbmc.kore.utils.LogUtils; + +/** + * Presents a TV Show overview + */ +public class TVShowInfoFragment extends AbstractInfoFragment + implements LoaderManager.LoaderCallbacks { + private static final String TAG = LogUtils.makeLogTag(TVShowInfoFragment.class); + + // Loader IDs + private static final int LOADER_TVSHOW = 0; + + // Controls whether a automatic sync refresh has been issued for this show + private static boolean hasIssuedOutdatedRefresh = false; + + @Override + protected RefreshItem createRefreshItem() { + RefreshItem refreshItem = new RefreshItem(getActivity(), + LibrarySyncService.SYNC_SINGLE_TVSHOW); + refreshItem.setSyncItem(LibrarySyncService.SYNC_TVSHOWID, getDataHolder().getId()); + refreshItem.setListener(new RefreshItem.RefreshItemListener() { + @Override + public void onSyncProcessEnded(MediaSyncEvent event) { + if (event.status == MediaSyncEvent.STATUS_SUCCESS) { + getLoaderManager().restartLoader(LOADER_TVSHOW, null, + TVShowInfoFragment.this); + refreshAdditionInfoFragment(); + } + } + }); + + return refreshItem; + } + + @Override + protected boolean setupMediaActionBar() { + return false; + } + + @Override + protected boolean setupFAB(ImageButton FAB) { + return false; + } + + @Override + protected AbstractAdditionalInfoFragment getAdditionalInfoFragment() { + TVShowProgressFragment tvShowProgressFragment = new TVShowProgressFragment(); + tvShowProgressFragment.setArgs(getDataHolder().getId(), getDataHolder().getTitle()); + return tvShowProgressFragment; + } + + @Override + public void onActivityCreated (Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + hasIssuedOutdatedRefresh = false; + + getLoaderManager().initLoader(LOADER_TVSHOW, null, this); + } + + /** + * Loader callbacks + */ + /** {@inheritDoc} */ + @Override + public Loader onCreateLoader(int i, Bundle bundle) { + Uri uri = MediaContract.TVShows.buildTVShowUri(getHostInfo().getId(), getDataHolder().getId()); + return new CursorLoader(getActivity(), uri, + TVShowDetailsQuery.PROJECTION, null, null, null); + + } + + /** {@inheritDoc} */ + @Override + public void onLoadFinished(Loader cursorLoader, Cursor cursor) { + if (cursor != null) { + switch (cursorLoader.getId()) { + case LOADER_TVSHOW: + cursor.moveToFirst(); + + DataHolder dataHolder = getDataHolder(); + + dataHolder.setFanArtUrl(cursor.getString(TVShowDetailsQuery.FANART)); + + dataHolder.setPosterUrl(cursor.getString(TVShowDetailsQuery.THUMBNAIL)); + dataHolder.setRating(cursor.getDouble(TVShowDetailsQuery.RATING)); + dataHolder.setMaxRating(10); + + String premiered = cursor.getString(TVShowDetailsQuery.PREMIERED); + String studio = cursor.getString(TVShowDetailsQuery.STUDIO); + + dataHolder.setDetails(String.format(getString(R.string.premiered), premiered) + " | " + studio + + "\n" + + cursor.getString(TVShowDetailsQuery.GENRES)); + + dataHolder.setTitle(cursor.getString(TVShowDetailsQuery.TITLE)); + + int numEpisodes = cursor.getInt(TVShowDetailsQuery.EPISODE), + watchedEpisodes = cursor.getInt(TVShowDetailsQuery.WATCHEDEPISODES); + + dataHolder.setUndertitle(String.format(getString(R.string.num_episodes), + numEpisodes, numEpisodes - watchedEpisodes)); + + dataHolder.setDescription(cursor.getString(TVShowDetailsQuery.PLOT)); + + updateView(dataHolder); + checkOutdatedTVShowDetails(cursor); + break; + } + } + } + + /** {@inheritDoc} */ + @Override + public void onLoaderReset(Loader cursorLoader) { + // Release loader's data + } + + /** + * Checks wether we should refresh the TV Show 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 checkOutdatedTVShowDetails(Cursor cursor) { + if (hasIssuedOutdatedRefresh) + return; + + cursor.moveToFirst(); + long lastUpdated = cursor.getLong(TVShowDetailsQuery.UPDATED); + + if (System.currentTimeMillis() > lastUpdated + Settings.DB_UPDATE_INTERVAL) { + // Trigger a silent refresh + hasIssuedOutdatedRefresh = true; + getRefreshItem().startSync(true); + } + } + + /** + * TV Show details query parameters. + */ + private interface TVShowDetailsQuery { + String[] PROJECTION = { + BaseColumns._ID, + MediaContract.TVShows.TITLE, + MediaContract.TVShows.THUMBNAIL, + MediaContract.TVShows.FANART, + MediaContract.TVShows.PREMIERED, + MediaContract.TVShows.STUDIO, + MediaContract.TVShows.EPISODE, + MediaContract.TVShows.WATCHEDEPISODES, + MediaContract.TVShows.RATING, + MediaContract.TVShows.PLOT, + MediaContract.TVShows.PLAYCOUNT, + MediaContract.TVShows.IMDBNUMBER, + MediaContract.TVShows.GENRES, + MediaContract.SyncColumns.UPDATED, + }; + + int ID = 0; + int TITLE = 1; + int THUMBNAIL = 2; + int FANART = 3; + int PREMIERED = 4; + int STUDIO = 5; + int EPISODE = 6; + int WATCHEDEPISODES = 7; + int RATING = 8; + int PLOT = 9; + int PLAYCOUNT = 10; + int IMDBNUMBER = 11; + int GENRES = 12; + int UPDATED = 13; + } +} diff --git a/app/src/main/java/org/xbmc/kore/ui/sections/video/TVShowListFragment.java b/app/src/main/java/org/xbmc/kore/ui/sections/video/TVShowListFragment.java index ec3c8e5..6ea165e 100644 --- a/app/src/main/java/org/xbmc/kore/ui/sections/video/TVShowListFragment.java +++ b/app/src/main/java/org/xbmc/kore/ui/sections/video/TVShowListFragment.java @@ -44,6 +44,7 @@ import org.xbmc.kore.provider.MediaContract; import org.xbmc.kore.provider.MediaDatabase; import org.xbmc.kore.service.library.LibrarySyncService; import org.xbmc.kore.ui.AbstractCursorListFragment; +import org.xbmc.kore.ui.AbstractInfoFragment; import org.xbmc.kore.utils.LogUtils; import org.xbmc.kore.utils.UIUtils; import org.xbmc.kore.utils.Utils; @@ -96,8 +97,8 @@ public class TVShowListFragment extends AbstractCursorListFragment { if (selection.length() != 0) selection.append(" AND "); selection.append(MediaContract.TVShowsColumns.WATCHEDEPISODES) - .append("!=") - .append(MediaContract.TVShowsColumns.EPISODE); + .append("!=") + .append(MediaContract.TVShowsColumns.EPISODE); } String sortOrderStr; @@ -121,8 +122,8 @@ public class TVShowListFragment extends AbstractCursorListFragment { return new CursorLoader(getActivity(), uri, - TVShowListQuery.PROJECTION, selection.toString(), - selectionArgs, sortOrderStr); + TVShowListQuery.PROJECTION, selection.toString(), + selectionArgs, sortOrderStr); } @Override @@ -194,43 +195,43 @@ public class TVShowListFragment extends AbstractCursorListFragment { case R.id.action_hide_watched: item.setChecked(!item.isChecked()); preferences.edit() - .putBoolean(Settings.KEY_PREF_TVSHOWS_FILTER_HIDE_WATCHED, item.isChecked()) - .apply(); + .putBoolean(Settings.KEY_PREF_TVSHOWS_FILTER_HIDE_WATCHED, item.isChecked()) + .apply(); refreshList(); break; case R.id.action_ignore_prefixes: item.setChecked(!item.isChecked()); preferences.edit() - .putBoolean(Settings.KEY_PREF_TVSHOWS_IGNORE_PREFIXES, item.isChecked()) - .apply(); + .putBoolean(Settings.KEY_PREF_TVSHOWS_IGNORE_PREFIXES, item.isChecked()) + .apply(); refreshList(); break; case R.id.action_sort_by_name: item.setChecked(true); preferences.edit() - .putInt(Settings.KEY_PREF_TVSHOWS_SORT_ORDER, Settings.SORT_BY_NAME) - .apply(); + .putInt(Settings.KEY_PREF_TVSHOWS_SORT_ORDER, Settings.SORT_BY_NAME) + .apply(); refreshList(); break; case R.id.action_sort_by_year: item.setChecked(true); preferences.edit() - .putInt(Settings.KEY_PREF_TVSHOWS_SORT_ORDER, Settings.SORT_BY_YEAR) - .apply(); + .putInt(Settings.KEY_PREF_TVSHOWS_SORT_ORDER, Settings.SORT_BY_YEAR) + .apply(); refreshList(); break; case R.id.action_sort_by_rating: item.setChecked(true); preferences.edit() - .putInt(Settings.KEY_PREF_TVSHOWS_SORT_ORDER, Settings.SORT_BY_RATING) - .apply(); + .putInt(Settings.KEY_PREF_TVSHOWS_SORT_ORDER, Settings.SORT_BY_RATING) + .apply(); refreshList(); break; case R.id.action_sort_by_date_added: item.setChecked(true); preferences.edit() - .putInt(Settings.KEY_PREF_TVSHOWS_SORT_ORDER, Settings.SORT_BY_DATE_ADDED) - .apply(); + .putInt(Settings.KEY_PREF_TVSHOWS_SORT_ORDER, Settings.SORT_BY_DATE_ADDED) + .apply(); refreshList(); break; case R.id.action_sort_by_last_played: @@ -266,7 +267,7 @@ public class TVShowListFragment extends AbstractCursorListFragment { MediaContract.TVShows.PLAYCOUNT, MediaContract.TVShows.IMDBNUMBER, MediaContract.TVShows.GENRES, - }; + }; String SORT_BY_NAME = MediaContract.TVShows.TITLE + " ASC"; String SORT_BY_YEAR = MediaContract.TVShows.PREMIERED + " ASC"; @@ -305,22 +306,21 @@ public class TVShowListFragment extends AbstractCursorListFragment { // the user transitions to that fragment, avoiding another call and imediatelly showing the image Resources resources = context.getResources(); artWidth = (int)(resources.getDimension(R.dimen.now_playing_poster_width) / - UIUtils.IMAGE_RESIZE_FACTOR); + UIUtils.IMAGE_RESIZE_FACTOR); artHeight = (int)(resources.getDimension(R.dimen.now_playing_poster_height) / - UIUtils.IMAGE_RESIZE_FACTOR); + UIUtils.IMAGE_RESIZE_FACTOR); } /** {@inheritDoc} */ @Override public View newView(Context context, final Cursor cursor, ViewGroup parent) { final View view = LayoutInflater.from(context) - .inflate(R.layout.grid_item_tvshow, parent, false); + .inflate(R.layout.grid_item_tvshow, parent, false); // Setup View holder pattern ViewHolder viewHolder = new ViewHolder(); viewHolder.titleView = (TextView)view.findViewById(R.id.title); viewHolder.detailsView = (TextView)view.findViewById(R.id.details); -// viewHolder.yearView = (TextView)view.findViewById(R.id.year); viewHolder.premieredView = (TextView)view.findViewById(R.id.premiered); viewHolder.artView = (ImageView)view.findViewById(R.id.art); @@ -335,31 +335,32 @@ public class TVShowListFragment extends AbstractCursorListFragment { final ViewHolder viewHolder = (ViewHolder)view.getTag(); // Save the movie id - viewHolder.tvshowId = cursor.getInt(TVShowListQuery.TVSHOWID); - viewHolder.tvshowTitle = cursor.getString(TVShowListQuery.TITLE); - viewHolder.episode = cursor.getInt(TVShowListQuery.EPISODE); - viewHolder.genres = cursor.getString(TVShowListQuery.GENRES); - viewHolder.plot = cursor.getString(TVShowListQuery.PLOT); - viewHolder.premiered = cursor.getString(TVShowListQuery.PREMIERED); - viewHolder.rating = cursor.getInt(TVShowListQuery.RATING); - viewHolder.studio = cursor.getString(TVShowListQuery.STUDIO); - viewHolder.watchedEpisodes = cursor.getInt(TVShowListQuery.WATCHEDEPISODES); + viewHolder.dataHolder.setId(cursor.getInt(TVShowListQuery.TVSHOWID)); + viewHolder.dataHolder.setTitle(cursor.getString(TVShowListQuery.TITLE)); + viewHolder.dataHolder.setDescription(cursor.getString(TVShowListQuery.PLOT)); + viewHolder.dataHolder.setRating(cursor.getInt(TVShowListQuery.RATING)); + int episode = cursor.getInt(TVShowListQuery.EPISODE); + int watchedEpisodes = cursor.getInt(TVShowListQuery.WATCHEDEPISODES); - if(Utils.isLollipopOrLater()) { - viewHolder.artView.setTransitionName("a"+viewHolder.tvshowId); - } - - viewHolder.titleView.setText(viewHolder.tvshowTitle); + viewHolder.titleView.setText(viewHolder.dataHolder.getTitle()); String details = String.format(context.getString(R.string.num_episodes), - viewHolder.episode, viewHolder.episode - viewHolder.watchedEpisodes); + episode, episode - watchedEpisodes); viewHolder.detailsView.setText(details); + viewHolder.dataHolder.setUndertitle(details); String premiered = String.format(context.getString(R.string.premiered), - viewHolder.premiered); + cursor.getString(TVShowListQuery.PREMIERED)); viewHolder.premieredView.setText(premiered); + viewHolder.dataHolder.setDetails(premiered); + viewHolder.dataHolder.setPosterUrl(cursor.getString(TVShowListQuery.THUMBNAIL)); UIUtils.loadImageWithCharacterAvatar(context, hostManager, - cursor.getString(TVShowListQuery.THUMBNAIL), viewHolder.tvshowTitle, - viewHolder.artView, artWidth, artHeight); + viewHolder.dataHolder.getPosterUrl(), + viewHolder.dataHolder.getTitle(), + viewHolder.artView, artWidth, artHeight); + + if (Utils.isLollipopOrLater()) { + viewHolder.artView.setTransitionName("a" + viewHolder.dataHolder.getId()); + } } } @@ -369,18 +370,9 @@ public class TVShowListFragment extends AbstractCursorListFragment { public static class ViewHolder { TextView titleView; TextView detailsView; - // TextView yearView; TextView premieredView; ImageView artView; - int tvshowId; - String tvshowTitle; - String premiered; - String studio; - int episode; - int watchedEpisodes; - double rating; - String plot; - String genres; + AbstractInfoFragment.DataHolder dataHolder = new AbstractInfoFragment.DataHolder(0); } } diff --git a/app/src/main/java/org/xbmc/kore/ui/sections/video/TVShowProgressFragment.java b/app/src/main/java/org/xbmc/kore/ui/sections/video/TVShowProgressFragment.java new file mode 100644 index 0000000..5217996 --- /dev/null +++ b/app/src/main/java/org/xbmc/kore/ui/sections/video/TVShowProgressFragment.java @@ -0,0 +1,394 @@ +/* + * 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.sections.video; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.Resources; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.provider.BaseColumns; +import android.support.annotation.Nullable; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.CursorLoader; +import android.support.v4.content.Loader; +import android.support.v7.widget.PopupMenu; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.GridLayout; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.TextView; + +import org.xbmc.kore.R; +import org.xbmc.kore.host.HostManager; +import org.xbmc.kore.jsonrpc.type.PlaylistType; +import org.xbmc.kore.provider.MediaContract; +import org.xbmc.kore.ui.AbstractAdditionalInfoFragment; +import org.xbmc.kore.ui.AbstractInfoFragment; +import org.xbmc.kore.ui.generic.CastFragment; +import org.xbmc.kore.utils.LogUtils; +import org.xbmc.kore.utils.MediaPlayerUtils; +import org.xbmc.kore.utils.UIUtils; + +public class TVShowProgressFragment extends AbstractAdditionalInfoFragment implements LoaderManager.LoaderCallbacks { + private static final String TAG = LogUtils.makeLogTag(TVShowProgressFragment.class); + + public static final String BUNDLE_ITEM_ID = "itemid"; + public static final String BUNDLE_TITLE = "title"; + + private static final int NEXT_EPISODES_COUNT = 2; + private int itemId = -1; + private CastFragment castFragment; + + public static final int LOADER_NEXT_EPISODES = 1, + LOADER_SEASONS = 2; + + public interface TVShowProgressActionListener { + void onSeasonSelected(int tvshowId, int season); + void onNextEpisodeSelected(int tvshowId, AbstractInfoFragment.DataHolder dataHolder); + } + + // Activity listener + private TVShowProgressActionListener listenerActivity; + + public void setArgs(int itemId, String showTitle) { + Bundle bundle = new Bundle(); + bundle.putInt(BUNDLE_ITEM_ID, itemId); + bundle.putString(BUNDLE_TITLE, showTitle); + setArguments(bundle); + } + + @Override + public void refresh() { + getLoaderManager().restartLoader(LOADER_NEXT_EPISODES, null, this); + getLoaderManager().restartLoader(LOADER_SEASONS, null, this); + castFragment.refresh(); + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + + try { + listenerActivity = (TVShowProgressActionListener) context; + } catch (ClassCastException e) { + throw new ClassCastException(context.toString() + " must implement TVShowProgressActionListener"); + } + } + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + Bundle arguments = getArguments(); + if (arguments == null) { + throw new IllegalStateException("Use setArgs to set required item id"); + } + this.itemId = arguments.getInt(BUNDLE_ITEM_ID); + String title = arguments.getString(BUNDLE_TITLE); + + View view = inflater.inflate(R.layout.fragment_tvshow_progress, container, false); + + castFragment = new CastFragment(); + castFragment.setArgs(this.itemId, title, CastFragment.TYPE.TVSHOW); + getActivity().getSupportFragmentManager() + .beginTransaction() + .add(R.id.cast_fragment, castFragment) + .commit(); + + return view; + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + getLoaderManager().initLoader(LOADER_NEXT_EPISODES, null, this); + getLoaderManager().initLoader(LOADER_SEASONS, null, this); + } + + @Override + public void onDetach() { + super.onDetach(); + listenerActivity = null; + } + + @Override + public Loader onCreateLoader(int id, Bundle args) { + Uri uri; + + int hostId = HostManager.getInstance(getActivity()).getHostInfo().getId(); + switch (id) { + case LOADER_NEXT_EPISODES: + // Load seasons + uri = MediaContract.Episodes.buildTVShowEpisodesListUri(hostId, itemId, NEXT_EPISODES_COUNT); + String selection = MediaContract.EpisodesColumns.PLAYCOUNT + "=0"; + return new CursorLoader(getActivity(), uri, + NextEpisodesListQuery.PROJECTION, selection, null, NextEpisodesListQuery.SORT); + case LOADER_SEASONS: + // Load seasons + uri = MediaContract.Seasons.buildTVShowSeasonsListUri(hostId, itemId); + return new CursorLoader(getActivity(), uri, + SeasonsListQuery.PROJECTION, null, null, SeasonsListQuery.SORT); + default: + return null; + } + } + + @Override + public void onLoadFinished(Loader loader, Cursor data) { + if (data != null) { + switch (loader.getId()) { + case LOADER_NEXT_EPISODES: + displayNextEpisodeList(data); + break; + case LOADER_SEASONS: + displaySeasonList(data); + break; + } + } + } + + @Override + public void onLoaderReset(Loader loader) { + + } + + /** + * Display next episode list + * + * @param cursor Cursor with the data + */ + @TargetApi(21) + private void displayNextEpisodeList(Cursor cursor) { + TextView nextEpisodeTitle = (TextView) getActivity().findViewById(R.id.next_episode_title); + GridLayout nextEpisodeList = (GridLayout) getActivity().findViewById(R.id.next_episode_list); + + if (cursor.moveToFirst()) { + nextEpisodeTitle.setVisibility(View.VISIBLE); + nextEpisodeList.setVisibility(View.VISIBLE); + + HostManager hostManager = HostManager.getInstance(getActivity()); + + View.OnClickListener episodeClickListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + AbstractInfoFragment.DataHolder vh = (AbstractInfoFragment.DataHolder) v.getTag(); + listenerActivity.onNextEpisodeSelected(itemId, vh); + } + }; + + // Get the art dimensions + Resources resources = getActivity().getResources(); + int artWidth = (int)(resources.getDimension(R.dimen.detail_poster_width_square) / + UIUtils.IMAGE_RESIZE_FACTOR); + int artHeight = (int)(resources.getDimension(R.dimen.detail_poster_height_square) / + UIUtils.IMAGE_RESIZE_FACTOR); + + nextEpisodeList.removeAllViews(); + do { + int episodeId = cursor.getInt(NextEpisodesListQuery.EPISODEID); + String title = cursor.getString(NextEpisodesListQuery.TITLE); + String seasonEpisode = String.format(getString(R.string.season_episode), + cursor.getInt(NextEpisodesListQuery.SEASON), + cursor.getInt(NextEpisodesListQuery.EPISODE)); + int runtime = cursor.getInt(NextEpisodesListQuery.RUNTIME) / 60; + String duration = runtime > 0 ? + String.format(getString(R.string.minutes_abbrev), String.valueOf(runtime)) + + " | " + cursor.getString(NextEpisodesListQuery.FIRSTAIRED) : + cursor.getString(NextEpisodesListQuery.FIRSTAIRED); + String thumbnail = cursor.getString(NextEpisodesListQuery.THUMBNAIL); + + View episodeView = LayoutInflater.from(getActivity()) + .inflate(R.layout.list_item_next_episode, nextEpisodeList, false); + + ImageView artView = (ImageView)episodeView.findViewById(R.id.art); + TextView titleView = (TextView)episodeView.findViewById(R.id.title); + TextView detailsView = (TextView)episodeView.findViewById(R.id.details); + TextView durationView = (TextView)episodeView.findViewById(R.id.duration); + + titleView.setText(title); + detailsView.setText(seasonEpisode); + durationView.setText(duration); + + UIUtils.loadImageWithCharacterAvatar(getActivity(), hostManager, + thumbnail, title, + artView, artWidth, artHeight); + + AbstractInfoFragment.DataHolder vh = new AbstractInfoFragment.DataHolder(episodeId); + vh.setTitle(title); + vh.setUndertitle(seasonEpisode); + episodeView.setTag(vh); + episodeView.setOnClickListener(episodeClickListener); + + // For the popupmenu + ImageView contextMenu = (ImageView)episodeView.findViewById(R.id.list_context_menu); + contextMenu.setTag(episodeId); + contextMenu.setOnClickListener(contextlistItemMenuClickListener); + + nextEpisodeList.addView(episodeView); + } while (cursor.moveToNext()); + } else { + // No episodes, hide views + nextEpisodeTitle.setVisibility(View.GONE); + nextEpisodeList.setVisibility(View.GONE); + } + } + + /** + * Display the seasons list + * + * @param cursor Cursor with the data + */ + private void displaySeasonList(Cursor cursor) { + TextView seasonsListTitle = (TextView) getActivity().findViewById(R.id.seasons_title); + GridLayout seasonsList = (GridLayout) getActivity().findViewById(R.id.seasons_list); + + if (cursor.moveToFirst()) { + seasonsListTitle.setVisibility(View.VISIBLE); + seasonsList.setVisibility(View.VISIBLE); + + HostManager hostManager = HostManager.getInstance(getActivity()); + + View.OnClickListener seasonListClickListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + listenerActivity.onSeasonSelected(itemId, (int)v.getTag()); + } + }; + + // Get the art dimensions + Resources resources = getActivity().getResources(); + int artWidth = (int)(resources.getDimension(R.dimen.seasonlist_art_width) / + UIUtils.IMAGE_RESIZE_FACTOR); + int artHeight = (int)(resources.getDimension(R.dimen.seasonlist_art_heigth) / + UIUtils.IMAGE_RESIZE_FACTOR); + + seasonsList.removeAllViews(); + do { + int seasonNumber = cursor.getInt(SeasonsListQuery.SEASON); + String thumbnail = cursor.getString(SeasonsListQuery.THUMBNAIL); + int numEpisodes = cursor.getInt(SeasonsListQuery.EPISODE); + int watchedEpisodes = cursor.getInt(SeasonsListQuery.WATCHEDEPISODES); + + View seasonView = LayoutInflater.from(getActivity()).inflate(R.layout.grid_item_season, seasonsList, false); + + ImageView seasonPictureView = (ImageView) seasonView.findViewById(R.id.art); + TextView seasonNumberView = (TextView) seasonView.findViewById(R.id.season); + TextView seasonEpisodesView = (TextView) seasonView.findViewById(R.id.episodes); + ProgressBar seasonProgressBar = (ProgressBar) seasonView.findViewById(R.id.season_progress_bar); + + seasonNumberView.setText(String.format(getActivity().getString(R.string.season_number), seasonNumber)); + seasonEpisodesView.setText(String.format(getActivity().getString(R.string.num_episodes), + numEpisodes, numEpisodes - watchedEpisodes)); + seasonProgressBar.setMax(numEpisodes); + seasonProgressBar.setProgress(watchedEpisodes); + + UIUtils.loadImageWithCharacterAvatar(getActivity(), hostManager, + thumbnail, + String.valueOf(seasonNumber), + seasonPictureView, artWidth, artHeight); + + seasonView.setTag(seasonNumber); + seasonView.setOnClickListener(seasonListClickListener); + seasonsList.addView(seasonView); + } while (cursor.moveToNext()); + } else { + // No seasons, hide views + seasonsListTitle.setVisibility(View.GONE); + seasonsList.setVisibility(View.GONE); + } + } + + private View.OnClickListener contextlistItemMenuClickListener = new View.OnClickListener() { + @Override + public void onClick(final View v) { + final PlaylistType.Item playListItem = new PlaylistType.Item(); + playListItem.episodeid = (int)v.getTag(); + + final PopupMenu popupMenu = new PopupMenu(getActivity(), v); + popupMenu.getMenuInflater().inflate(R.menu.musiclist_item, popupMenu.getMenu()); + popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + switch (item.getItemId()) { + case R.id.action_play: + MediaPlayerUtils.play(TVShowProgressFragment.this, playListItem); + return true; + case R.id.action_queue: + MediaPlayerUtils.queue(TVShowProgressFragment.this, playListItem, PlaylistType.GetPlaylistsReturnType.VIDEO); + return true; + } + return false; + } + }); + popupMenu.show(); + } + }; + + /** + * Next episodes list query parameters. + */ + private interface NextEpisodesListQuery { + String[] PROJECTION = { + BaseColumns._ID, + MediaContract.Episodes.EPISODEID, + MediaContract.Episodes.SEASON, + MediaContract.Episodes.EPISODE, + MediaContract.Episodes.THUMBNAIL, + MediaContract.Episodes.PLAYCOUNT, + MediaContract.Episodes.TITLE, + MediaContract.Episodes.RUNTIME, + MediaContract.Episodes.FIRSTAIRED, + }; + + String SORT = MediaContract.Episodes.EPISODEID + " ASC"; + + int ID = 0; + int EPISODEID = 1; + int SEASON = 2; + int EPISODE = 3; + int THUMBNAIL = 4; + int PLAYCOUNT = 5; + int TITLE = 6; + int RUNTIME = 7; + int FIRSTAIRED = 8; + } + + /** + * Seasons list query parameters. + */ + private interface SeasonsListQuery { + String[] PROJECTION = { + BaseColumns._ID, + MediaContract.Seasons.SEASON, + MediaContract.Seasons.THUMBNAIL, + MediaContract.Seasons.EPISODE, + MediaContract.Seasons.WATCHEDEPISODES + }; + + String SORT = MediaContract.Seasons.SEASON + " ASC"; + + int ID = 0; + int SEASON = 1; + int THUMBNAIL = 2; + int EPISODE = 3; + int WATCHEDEPISODES = 4; + } +} diff --git a/app/src/main/java/org/xbmc/kore/ui/sections/video/TVShowsActivity.java b/app/src/main/java/org/xbmc/kore/ui/sections/video/TVShowsActivity.java index 5e2a6b7..830421d 100644 --- a/app/src/main/java/org/xbmc/kore/ui/sections/video/TVShowsActivity.java +++ b/app/src/main/java/org/xbmc/kore/ui/sections/video/TVShowsActivity.java @@ -18,34 +18,32 @@ package org.xbmc.kore.ui.sections.video; import android.annotation.TargetApi; import android.content.Intent; import android.os.Bundle; +import android.support.v4.app.Fragment; import android.support.v4.app.FragmentTransaction; import android.support.v4.widget.DrawerLayout; import android.support.v7.app.ActionBar; import android.support.v7.widget.Toolbar; -import android.transition.Transition; import android.transition.TransitionInflater; import android.view.Menu; import android.view.MenuItem; -import android.view.View; import android.view.Window; import org.xbmc.kore.R; +import org.xbmc.kore.ui.AbstractInfoFragment; import org.xbmc.kore.ui.BaseActivity; import org.xbmc.kore.ui.generic.NavigationDrawerFragment; import org.xbmc.kore.ui.sections.remote.RemoteActivity; import org.xbmc.kore.utils.LogUtils; +import org.xbmc.kore.utils.SharedElementTransition; import org.xbmc.kore.utils.Utils; -import java.util.List; -import java.util.Map; - /** * Controls the presentation of TV Shows information (list, details) * All the information is presented by specific fragments */ public class TVShowsActivity extends BaseActivity implements TVShowListFragment.OnTVShowSelectedListener, - TVShowDetailsFragment.TVShowDetailsActionListener, + TVShowProgressFragment.TVShowProgressActionListener, TVShowEpisodeListFragment.OnEpisodeSelectedListener { private static final String TAG = LogUtils.makeLogTag(TVShowsActivity.class); @@ -54,6 +52,7 @@ public class TVShowsActivity extends BaseActivity public static final String EPISODEID = "episode_id"; public static final String SEASON = "season"; public static final String SEASONTITLE = "season_title"; + public static final String LISTFRAGMENT_TAG = "tvshowlist"; private int selectedTVShowId = -1; private String selectedTVShowTitle = null; @@ -61,7 +60,7 @@ public class TVShowsActivity extends BaseActivity private String selectedSeasonTitle = null; private int selectedEpisodeId = -1; - private boolean clearSharedElements; + private SharedElementTransition sharedElementTransition = new SharedElementTransition(); private NavigationDrawerFragment navigationDrawerFragment; @@ -80,38 +79,17 @@ public class TVShowsActivity extends BaseActivity .findFragmentById(R.id.navigation_drawer); navigationDrawerFragment.setUp(R.id.navigation_drawer, (DrawerLayout) findViewById(R.id.drawer_layout)); + Fragment fragment; if (savedInstanceState == null) { - TVShowListFragment tvshowListFragment = new TVShowListFragment(); + fragment = new TVShowListFragment(); - // Setup animations - if (Utils.isLollipopOrLater()) { - //Fade added to prevent shared element from disappearing very shortly at the start of the transition. - Transition fade = TransitionInflater - .from(this) - .inflateTransition(android.R.transition.fade); - tvshowListFragment.setExitTransition(fade); - tvshowListFragment.setReenterTransition(fade); - - tvshowListFragment.setSharedElementReturnTransition(TransitionInflater.from( - this).inflateTransition(R.transition.change_image)); - - android.support.v4.app.SharedElementCallback seCallback = new android.support.v4.app.SharedElementCallback() { - @Override - public void onMapSharedElements(List names, Map sharedElements) { - if (clearSharedElements) { - names.clear(); - sharedElements.clear(); - clearSharedElements = false; - } - } - }; - tvshowListFragment.setExitSharedElementCallback(seCallback); - } getSupportFragmentManager() .beginTransaction() - .add(R.id.fragment_container, tvshowListFragment) + .add(R.id.fragment_container, fragment, LISTFRAGMENT_TAG) .commit(); } else { + fragment = getSupportFragmentManager().findFragmentByTag(LISTFRAGMENT_TAG); + selectedTVShowId = savedInstanceState.getInt(TVSHOWID, -1); selectedTVShowTitle = savedInstanceState.getString(TVSHOWTITLE, null); selectedEpisodeId = savedInstanceState.getInt(EPISODEID, -1); @@ -119,6 +97,10 @@ public class TVShowsActivity extends BaseActivity selectedSeasonTitle = savedInstanceState.getString(SEASONTITLE, null); } + if (Utils.isLollipopOrLater()) { + sharedElementTransition.setupExitTransition(this, fragment); + } + setupActionBar(selectedTVShowTitle); // // Setup system bars and content padding, allowing averlap with the bottom bar @@ -238,43 +220,19 @@ public class TVShowsActivity extends BaseActivity */ @TargetApi(21) public void onTVShowSelected(TVShowListFragment.ViewHolder vh) { - selectedTVShowId = vh.tvshowId; - selectedTVShowTitle = vh.tvshowTitle; + selectedTVShowId = vh.dataHolder.getId(); + selectedTVShowTitle = vh.dataHolder.getTitle(); // Replace list fragment - final TVShowDetailsFragment tvshowDetailsFragment = TVShowDetailsFragment.newInstance(vh); + final TVShowInfoFragment tvshowDetailsFragment = new TVShowInfoFragment(); + tvshowDetailsFragment.setDataHolder(vh.dataHolder); + FragmentTransaction fragTrans = getSupportFragmentManager().beginTransaction(); // Set up transitions if (Utils.isLollipopOrLater()) { - android.support.v4.app.SharedElementCallback seCallback = new android.support.v4.app.SharedElementCallback() { - @Override - public void onMapSharedElements(List names, Map sharedElements) { - //On returning onMapSharedElements for the exiting fragment is called before the onMapSharedElements - // for the reentering fragment. We use this to determine if we are returning and if - // we should clear the shared element lists. Note that, clearing must be done in the reentering fragment - // as this is called last. Otherwise it the app will crash during transition setup. Not sure, but might - // be a v4 support package bug. - if (tvshowDetailsFragment.isVisible()) { - View sharedView = tvshowDetailsFragment.getSharedElement(); - if (sharedView == null) { // shared element not visible - clearSharedElements = true; - } - } - } - }; - tvshowDetailsFragment.setEnterSharedElementCallback(seCallback); - - tvshowDetailsFragment.setEnterTransition( - TransitionInflater.from(this) - .inflateTransition(R.transition.media_details)); - tvshowDetailsFragment.setReturnTransition(null); - Transition changeImageTransition = - TransitionInflater.from(this) - .inflateTransition(R.transition.change_image); - tvshowDetailsFragment.setSharedElementReturnTransition(changeImageTransition); - tvshowDetailsFragment.setSharedElementEnterTransition(changeImageTransition); - fragTrans.addSharedElement(vh.artView, vh.artView.getTransitionName()); + vh.dataHolder.setPosterTransitionName(vh.artView.getTransitionName()); + sharedElementTransition.setupEnterTransition(this, fragTrans, tvshowDetailsFragment, vh.artView); } else { fragTrans.setCustomAnimations(R.anim.fragment_details_enter, 0, R.anim.fragment_list_popenter, 0); } @@ -308,15 +266,16 @@ public class TVShowsActivity extends BaseActivity /** * Callback from tvshow details when a episode is selected - * @param episodeId episode id */ @TargetApi(21) - public void onNextEpisodeSelected(int episodeId) { - selectedEpisodeId = episodeId; + public void onNextEpisodeSelected(int tvshowId, + AbstractInfoFragment.DataHolder dh) { + selectedEpisodeId = dh.getId(); // Replace list fragment - TVShowEpisodeDetailsFragment fragment = - TVShowEpisodeDetailsFragment.newInstance(selectedTVShowId, selectedEpisodeId); + TVShowEpisodeInfoFragment fragment = new TVShowEpisodeInfoFragment(); + fragment.setDataHolder(dh); + fragment.setTvshowId(tvshowId); FragmentTransaction fragTrans = getSupportFragmentManager().beginTransaction(); // Set up transitions @@ -336,15 +295,16 @@ public class TVShowsActivity extends BaseActivity /** * Callback from tvshow episodes list when a episode is selected - * @param vh view holder */ @TargetApi(21) - public void onEpisodeSelected(TVShowEpisodeListFragment.EpisodeViewHolder vh) { - selectedEpisodeId = vh.episodeId; + public void onEpisodeSelected(int tvshowId, + TVShowEpisodeListFragment.ViewHolder viewHolder) { + selectedEpisodeId = viewHolder.dataHolder.getId(); // Replace list fragment - TVShowEpisodeDetailsFragment fragment = - TVShowEpisodeDetailsFragment.newInstance(selectedTVShowId, selectedEpisodeId); + TVShowEpisodeInfoFragment fragment = new TVShowEpisodeInfoFragment(); + fragment.setDataHolder(viewHolder.dataHolder); + fragment.setTvshowId(tvshowId); FragmentTransaction fragTrans = getSupportFragmentManager().beginTransaction(); // Set up transitions diff --git a/app/src/main/java/org/xbmc/kore/utils/SharedElementTransition.java b/app/src/main/java/org/xbmc/kore/utils/SharedElementTransition.java new file mode 100644 index 0000000..3d84ae2 --- /dev/null +++ b/app/src/main/java/org/xbmc/kore/utils/SharedElementTransition.java @@ -0,0 +1,118 @@ +/* + * 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.utils; + +import android.annotation.TargetApi; +import android.content.Context; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentTransaction; +import android.support.v4.app.SharedElementCallback; +import android.transition.Transition; +import android.transition.TransitionInflater; +import android.view.View; + +import org.xbmc.kore.R; + +import java.util.List; +import java.util.Map; + +public class SharedElementTransition { + private static final String TAG = LogUtils.makeLogTag(SharedElementTransition.class); + + public interface SharedElement { + + /** + * Returns if the shared element if visible + * @return true if visible, false otherwise + */ + boolean isSharedElementVisible(); + } + + private boolean clearSharedElements; + + /** + * Sets up the transition for the exiting fragment + * @param fragment + */ + @TargetApi(21) + public void setupExitTransition(Context context, Fragment fragment) { + Transition fade = TransitionInflater + .from(context) + .inflateTransition(android.R.transition.fade); + fragment.setExitTransition(fade); + fragment.setReenterTransition(fade); + + fragment.setExitSharedElementCallback(new SharedElementCallback() { + @Override + public void onMapSharedElements(List names, Map sharedElements) { + // Clearing must be done in the reentering fragment + // as this is called last. Otherwise, the app will crash during transition setup. Not sure, but might + // be a v4 support package bug. + if (clearSharedElements) { + names.clear(); + sharedElements.clear(); + clearSharedElements = false; + } + } + }); + } + + /** + * Sets up the transition for the entering fragment + * @param fragmentTransaction + * @param fragment entering fragment + * @param sharedElement must have the transition name set + */ + @TargetApi(21) + public void setupEnterTransition(Context context, + FragmentTransaction fragmentTransaction, + final Fragment fragment, + View sharedElement) { + if (!(fragment instanceof SharedElement)) { + LogUtils.LOGD(TAG, "Enter transition fragment must implement SharedElement interface"); + return; + } + + android.support.v4.app.SharedElementCallback seCallback = new android.support.v4.app.SharedElementCallback() { + @Override + public void onMapSharedElements(List names, Map sharedElements) { + // On returning, onMapSharedElements for the exiting fragment is called before the onMapSharedElements + // for the reentering fragment. We use this to determine if we are returning and if + // we should clear the shared element lists. Note that, clearing must be done in the reentering fragment + // as this is called last. Otherwise, the app will crash during transition setup. Not sure, but might + // be a v4 support package bug. + if (fragment.isVisible() && (!((SharedElement) fragment).isSharedElementVisible())) { + // shared element not visible + clearSharedElements = true; + } + } + }; + fragment.setEnterSharedElementCallback(seCallback); + + fragment.setEnterTransition(TransitionInflater + .from(context) + .inflateTransition(R.transition.media_details)); + fragment.setReturnTransition(null); + + Transition changeImageTransition = TransitionInflater.from( + context).inflateTransition(R.transition.change_image); + fragment.setSharedElementReturnTransition(changeImageTransition); + fragment.setSharedElementEnterTransition(changeImageTransition); + + fragmentTransaction.addSharedElement(sharedElement, sharedElement.getTransitionName()); + } +} diff --git a/app/src/main/java/org/xbmc/kore/utils/UIUtils.java b/app/src/main/java/org/xbmc/kore/utils/UIUtils.java index 953a801..d83cdea 100644 --- a/app/src/main/java/org/xbmc/kore/utils/UIUtils.java +++ b/app/src/main/java/org/xbmc/kore/utils/UIUtils.java @@ -468,7 +468,7 @@ public class UIUtils { } /** - * Returns true if {@param view} is contained within {@param container}'s bounds. + * Returns true if {@param view} is visible within {@param container}'s bounds. */ public static boolean isViewInBounds(@NonNull View container, @NonNull View view) { Rect containerBounds = new Rect(); @@ -497,7 +497,7 @@ public class UIUtils { // Check if any file exists and whether to overwrite it boolean someFilesExist = false; for (FileDownloadHelper.SongInfo songInfo : songInfoList) { - File file = new File(songInfoList.get(0).getAbsoluteFilePath()); + File file = new File(songInfo.getAbsoluteFilePath()); if (file.exists()) { someFilesExist = true; break; @@ -560,4 +560,14 @@ public class UIUtils { } } } + + public static void highlightImageView(Context context, ImageView view) { + Resources.Theme theme = context.getTheme(); + TypedArray styledAttributes = theme.obtainStyledAttributes(new int[]{ + R.attr.colorAccent}); + view.setColorFilter( + styledAttributes.getColor(0, + context.getResources().getColor(R.color.accent_default))); + styledAttributes.recycle(); + } } diff --git a/app/src/main/java/org/xbmc/kore/utils/Utils.java b/app/src/main/java/org/xbmc/kore/utils/Utils.java index 5883ab5..c95f6fe 100644 --- a/app/src/main/java/org/xbmc/kore/utils/Utils.java +++ b/app/src/main/java/org/xbmc/kore/utils/Utils.java @@ -23,8 +23,19 @@ import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Build; +import android.os.Handler; +import android.support.v4.app.Fragment; import android.text.TextUtils; +import android.widget.Toast; +import org.xbmc.kore.R; +import org.xbmc.kore.host.HostManager; +import org.xbmc.kore.jsonrpc.ApiCallback; +import org.xbmc.kore.jsonrpc.HostConnection; +import org.xbmc.kore.jsonrpc.method.Playlist; +import org.xbmc.kore.jsonrpc.type.PlaylistType; + +import java.util.ArrayList; import java.util.List; /** @@ -173,4 +184,61 @@ public class Utils { return bitmap; } + + public static void addToPlaylist(final Fragment fragment, final int itemId, final String playlistId) { + Playlist.GetPlaylists getPlaylists = new Playlist.GetPlaylists(); + + final Context context = fragment.getContext(); + final HostConnection hostConnection = HostManager.getInstance(context).getConnection(); + final Handler callbackHandler = new Handler(); + + getPlaylists.execute(hostConnection, new ApiCallback>() { + @Override + public void onSuccess(ArrayList result) { + if (!fragment.isAdded()) return; + // Ok, loop through the playlists, looking for the video one + int videoPlaylistId = -1; + for (PlaylistType.GetPlaylistsReturnType playlist : result) { + if (playlist.type.equals(playlistId)) { + videoPlaylistId = playlist.playlistid; + break; + } + } + // If found, add to playlist + if (videoPlaylistId != -1) { + PlaylistType.Item item = new PlaylistType.Item(); + item.episodeid = itemId; + Playlist.Add action = new Playlist.Add(videoPlaylistId, item); + action.execute(hostConnection, new ApiCallback() { + @Override + public void onSuccess(String result) { + if (!fragment.isAdded()) return; + // Got an error, show toast + Toast.makeText(context, R.string.item_added_to_playlist, Toast.LENGTH_SHORT) + .show(); + } + + @Override + public void onError(int errorCode, String description) { + if (!fragment.isAdded()) return; + // Got an error, show toast + Toast.makeText(context, R.string.unable_to_connect_to_xbmc, Toast.LENGTH_SHORT) + .show(); + } + }, callbackHandler); + } else { + Toast.makeText(context, R.string.no_suitable_playlist, Toast.LENGTH_SHORT) + .show(); + } + } + + @Override + public void onError(int errorCode, String description) { + if (!fragment.isAdded()) return; + // Got an error, show toast + Toast.makeText(context, R.string.unable_to_connect_to_xbmc, Toast.LENGTH_SHORT) + .show(); + } + }, callbackHandler); + } } diff --git a/app/src/main/res/layout/fragment_addon_details.xml b/app/src/main/res/layout/fragment_addon_details.xml deleted file mode 100644 index 5d72643..0000000 --- a/app/src/main/res/layout/fragment_addon_details.xml +++ /dev/null @@ -1,196 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_album_details.xml b/app/src/main/res/layout/fragment_album_details.xml deleted file mode 100644 index ffb6dfe..0000000 --- a/app/src/main/res/layout/fragment_album_details.xml +++ /dev/null @@ -1,241 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_artist_details.xml b/app/src/main/res/layout/fragment_artist_details.xml deleted file mode 100644 index 888ad1f..0000000 --- a/app/src/main/res/layout/fragment_artist_details.xml +++ /dev/null @@ -1,149 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/fragment_cast.xml b/app/src/main/res/layout/fragment_cast.xml new file mode 100644 index 0000000..a2702ce --- /dev/null +++ b/app/src/main/res/layout/fragment_cast.xml @@ -0,0 +1,39 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_episode_details.xml b/app/src/main/res/layout/fragment_episode_details.xml deleted file mode 100644 index f0e22a8..0000000 --- a/app/src/main/res/layout/fragment_episode_details.xml +++ /dev/null @@ -1,274 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_movie_details.xml b/app/src/main/res/layout/fragment_info.xml similarity index 60% rename from app/src/main/res/layout/fragment_movie_details.xml rename to app/src/main/res/layout/fragment_info.xml index 782b39e..3b51d7a 100644 --- a/app/src/main/res/layout/fragment_movie_details.xml +++ b/app/src/main/res/layout/fragment_info.xml @@ -40,18 +40,15 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:layout_alignParentStart="true" - android:layout_alignParentLeft="true"> + android:layout_alignParentLeft="true" + android:transitionGroup="true"> - - + android:layout_marginRight="@dimen/remote_content_hmargin"> - - - - - - - + android:background="?attr/contentBackgroundColor" + android:visibility="gone"> + + android:contentDescription="@string/add_to_playlist" + android:visibility="gone"/> - + android:visibility="gone"/> + + android:contentDescription="@string/seen" + android:visibility="gone"/> - - - + + + + + + + + + + + - - - - - - - - - - - - + android:orientation="vertical" + android:background="?attr/contentBackgroundColor"> + + + - - - + android:layout_marginTop="@dimen/default_padding"/> - + app:fab_colorPressed="?attr/fabColorPressed" + android:visibility="gone"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_tvshow_episodes_list.xml b/app/src/main/res/layout/fragment_tvshow_episodes_list.xml deleted file mode 100644 index fed861d..0000000 --- a/app/src/main/res/layout/fragment_tvshow_episodes_list.xml +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/layout/fragment_tvshow_overview.xml b/app/src/main/res/layout/fragment_tvshow_overview.xml deleted file mode 100644 index 20f7e65..0000000 --- a/app/src/main/res/layout/fragment_tvshow_overview.xml +++ /dev/null @@ -1,244 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_tvshow_progress.xml b/app/src/main/res/layout/fragment_tvshow_progress.xml new file mode 100644 index 0000000..fe3fe3e --- /dev/null +++ b/app/src/main/res/layout/fragment_tvshow_progress.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_item_episode.xml b/app/src/main/res/layout/list_item_episode.xml index b252d2a..b930ebe 100644 --- a/app/src/main/res/layout/list_item_episode.xml +++ b/app/src/main/res/layout/list_item_episode.xml @@ -44,6 +44,7 @@ android:layout_height="wrap_content" style="@style/TextAppearance.Medialist.Title" android:paddingRight="@dimen/small_padding" + android:paddingEnd="@dimen/small_padding" android:layout_alignTop="@id/art" android:layout_toRightOf="@id/art" android:layout_toEndOf="@id/art"/> @@ -94,6 +95,7 @@ android:layout_below="@id/title" style="@style/TextAppearance.Medialist.Details" android:paddingLeft="@dimen/small_padding" + android:paddingStart="@dimen/small_padding" android:paddingTop="0dp" android:paddingBottom="0dp" android:gravity="center_vertical"/> diff --git a/app/src/main/res/values-sw600dp/dimens.xml b/app/src/main/res/values-sw600dp/dimens.xml index 4b02d73..d7b6750 100644 --- a/app/src/main/res/values-sw600dp/dimens.xml +++ b/app/src/main/res/values-sw600dp/dimens.xml @@ -54,24 +54,15 @@ 88dp 88dp - 136dp - 136dp - 88dp 88dp 88dp 88dp - 146dp - 146dp - 88dp 88dp - 146dp - 146dp - 88dp 88dp diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 18df0b1..8baa248 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -95,8 +95,13 @@ 74dp 74dp - 112dp - 112dp + 216dp + + 112dp + 112dp + + 112dp + 160dp 74dp 74dp @@ -104,15 +109,9 @@ 74dp 74dp - 112dp - 112dp - 74dp 74dp - 112dp - 112dp - 52dp 76dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a0097e8..ad1a30f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -195,6 +195,7 @@ %1$d %1$s votes + /%1$s /10 /5 @@ -400,5 +401,9 @@ By artist By artist and year muted + Error: no item available to play. + Refreshing not implemented for this item + No songs found\\n\\nSwipe down to refresh + Not implemented diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 07e401e..adb8c24 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -172,7 +172,6 @@