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 @@