/* * 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); }