/* * Copyright (C) 2014 The Android Open Source Project * * 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 tv.emby.embyatv.browsing; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.Handler; import android.support.v17.leanback.app.BackgroundManager; import android.support.v17.leanback.app.BrowseFragment; import android.support.v17.leanback.widget.ArrayObjectAdapter; import android.support.v17.leanback.widget.HeaderItem; import android.support.v17.leanback.widget.ListRow; import android.support.v17.leanback.widget.OnItemViewClickedListener; import android.support.v17.leanback.widget.OnItemViewSelectedListener; import android.support.v17.leanback.widget.Presenter; import android.support.v17.leanback.widget.Row; import android.support.v17.leanback.widget.RowPresenter; import android.util.DisplayMetrics; import android.view.Gravity; import android.view.KeyEvent; import android.view.View; import android.view.ViewGroup; import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.TextView; import com.bumptech.glide.Glide; import com.bumptech.glide.request.animation.GlideAnimation; import com.bumptech.glide.request.target.SimpleTarget; import java.util.ArrayList; import java.util.List; import java.util.Timer; import java.util.TimerTask; import mediabrowser.apiinteraction.EmptyResponse; import tv.emby.embyatv.R; import tv.emby.embyatv.TvApp; import tv.emby.embyatv.base.BaseActivity; import tv.emby.embyatv.base.CustomMessage; import tv.emby.embyatv.base.IKeyListener; import tv.emby.embyatv.base.IMessageListener; import tv.emby.embyatv.itemhandling.AudioQueueItem; import tv.emby.embyatv.itemhandling.BaseRowItem; import tv.emby.embyatv.itemhandling.ItemLauncher; import tv.emby.embyatv.itemhandling.ItemRowAdapter; import tv.emby.embyatv.presentation.CardPresenter; import tv.emby.embyatv.presentation.PositionableListRowPresenter; import tv.emby.embyatv.presentation.ThemeManager; import tv.emby.embyatv.querying.QueryType; import tv.emby.embyatv.querying.ViewQuery; import tv.emby.embyatv.ui.ClockUserView; import tv.emby.embyatv.ui.ItemPanel; import tv.emby.embyatv.util.KeyProcessor; import tv.emby.embyatv.util.Utils; public class StdBrowseFragment extends BrowseFragment implements IRowLoader { private static final String TAG = "StdBrowseFragment"; private static final int BACKGROUND_UPDATE_DELAY = 100; protected String MainTitle; protected boolean ShowBadge = true; protected boolean ShowInfoPanel = true; protected TvApp mApplication; protected BaseActivity mActivity; protected BaseRowItem mCurrentItem; protected ListRow mCurrentRow; protected CompositeClickedListener mClickedListener = new CompositeClickedListener(); protected CompositeSelectedListener mSelectedListener = new CompositeSelectedListener(); protected ArrayObjectAdapter mRowsAdapter; private Drawable mDefaultBackground; private SimpleTarget<Bitmap> mBackgroundTarget; private DisplayMetrics mMetrics; private Timer mBackgroundTimer; private final Handler mHandler = new Handler(); private String mBackgroundUrl; protected ArrayList<BrowseRowDef> mRows = new ArrayList<>(); CardPresenter mCardPresenter; protected boolean justLoaded = true; private ItemPanel mItemPanel; private Animation fadeInPanel; private Animation fadeOutPanel; @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); mApplication = TvApp.getApplication(); if (getActivity() instanceof BaseActivity) mActivity = (BaseActivity)getActivity(); prepareBackgroundManager(); setupUIElements(); setupQueries(this); setupEventListeners(); } protected void setupQueries(IRowLoader rowLoader) { rowLoader.loadRows(mRows); } @Override public void onDestroy() { super.onDestroy(); if (null != mBackgroundTimer) { mBackgroundTimer.cancel(); } } @Override public void onPause() { super.onPause(); } @Override public void onResume() { super.onResume(); // set fastLane (or headers) background color setBrandColor(ThemeManager.getBrandColor()); // set info panel option ShowInfoPanel = mApplication.getPrefs().getBoolean("pref_enable_info_panel", true); //React to deletion if (getActivity() != null && !getActivity().isFinishing() && mCurrentRow != null && mCurrentItem != null && mCurrentItem.getItemId() != null && mCurrentItem.getItemId().equals(TvApp.getApplication().getLastDeletedItemId())) { ((ItemRowAdapter)mCurrentRow.getAdapter()).remove(mCurrentItem); TvApp.getApplication().setLastDeletedItemId(null); } if (!justLoaded) { //Re-retrieve anything that needs it but delay slightly so we don't take away gui landing if (mRowsAdapter != null) { refreshCurrentItem(); refreshRows(); } } else { justLoaded = false; } } protected void refreshRows() { new Handler().postDelayed(new Runnable() { @Override public void run() { if (mActivity.isFinishing()) return; for (int i = 0; i < mRowsAdapter.size(); i++) { if (mRowsAdapter.get(i) instanceof ListRow) { if (((ListRow) mRowsAdapter.get(i)).getAdapter() instanceof ItemRowAdapter && !mActivity.isFinishing()) { ((ItemRowAdapter) ((ListRow) mRowsAdapter.get(i)).getAdapter()).ReRetrieveIfNeeded(); } } } } },1500); } public void loadRows(List<BrowseRowDef> rows) { mRowsAdapter = new ArrayObjectAdapter(new PositionableListRowPresenter()); mCardPresenter = new CardPresenter(); for (BrowseRowDef def : rows) { HeaderItem header = new HeaderItem(def.getHeaderText()); ItemRowAdapter rowAdapter; switch (def.getQueryType()) { case NextUp: rowAdapter = new ItemRowAdapter(def.getNextUpQuery(), true, mCardPresenter, mRowsAdapter); break; case Season: rowAdapter = new ItemRowAdapter(def.getSeasonQuery(), mCardPresenter, mRowsAdapter); break; case Upcoming: rowAdapter = new ItemRowAdapter(def.getUpcomingQuery(), mCardPresenter, mRowsAdapter); break; case Views: rowAdapter = new ItemRowAdapter(new ViewQuery(), mCardPresenter, mRowsAdapter); break; case SimilarSeries: rowAdapter = new ItemRowAdapter(def.getSimilarQuery(), QueryType.SimilarSeries, mCardPresenter, mRowsAdapter); break; case SimilarMovies: rowAdapter = new ItemRowAdapter(def.getSimilarQuery(), QueryType.SimilarMovies, mCardPresenter, mRowsAdapter); break; case Persons: rowAdapter = new ItemRowAdapter(def.getPersonsQuery(), def.getChunkSize(), mCardPresenter, mRowsAdapter); break; case LiveTvChannel: rowAdapter = new ItemRowAdapter(def.getTvChannelQuery(), 40, mCardPresenter, mRowsAdapter); break; case LiveTvProgram: rowAdapter = new ItemRowAdapter(def.getProgramQuery(), mCardPresenter, mRowsAdapter); break; case LiveTvRecording: rowAdapter = new ItemRowAdapter(def.getRecordingQuery(), mCardPresenter, mRowsAdapter); break; case LiveTvRecordingGroup: rowAdapter = new ItemRowAdapter(def.getRecordingGroupQuery(), mCardPresenter, mRowsAdapter); break; default: rowAdapter = new ItemRowAdapter(def.getQuery(), def.getChunkSize(), def.getPreferParentThumb(), def.isStaticHeight(), mCardPresenter, mRowsAdapter, def.getQueryType()); break; } rowAdapter.setReRetrieveTriggers(def.getChangeTriggers()); ListRow row = new ListRow(header, rowAdapter); mRowsAdapter.add(row); rowAdapter.setRow(row); rowAdapter.Retrieve(); } addAdditionalRows(mRowsAdapter); setAdapter(mRowsAdapter); } protected void addAdditionalRows(ArrayObjectAdapter rowAdapter) { } private void prepareBackgroundManager() { final BackgroundManager backgroundManager = BackgroundManager.getInstance(getActivity()); backgroundManager.attach(getActivity().getWindow()); mDefaultBackground = getResources().getDrawable(R.drawable.moviebg); mMetrics = new DisplayMetrics(); getActivity().getWindowManager().getDefaultDisplay().getMetrics(mMetrics); mBackgroundTarget = new SimpleTarget<Bitmap>(mMetrics.widthPixels, mMetrics.heightPixels) { @Override public void onResourceReady(Bitmap resource, GlideAnimation<? super Bitmap> glideAnimation) { backgroundManager.setBitmap(resource); } }; } protected void setupUIElements() { if (ShowBadge) setBadgeDrawable(getActivity().getResources().getDrawable(R.drawable.logob400)); setTitle(MainTitle); // Badge, when set, takes precedent over title setHeadersState(HEADERS_ENABLED); setHeadersTransitionOnBackEnabled(true); // move the badge/title to the left to make way for our clock/user bug ImageView badge = (ImageView) getActivity().findViewById(R.id.title_badge); if (badge != null) { FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) badge.getLayoutParams(); lp.rightMargin = Utils.convertDpToPixel(getActivity(), 120); lp.width = Utils.convertDpToPixel(getActivity(), 250); badge.setLayoutParams(lp); } TextView title = (TextView) getActivity().findViewById(R.id.title_text); if (title != null) { FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) title.getLayoutParams(); lp.rightMargin = Utils.convertDpToPixel(getActivity(), 120); lp.width = Utils.convertDpToPixel(getActivity(), 250); title.setLayoutParams(lp); } ViewGroup root = (ViewGroup) getActivity().findViewById(android.R.id.content); // add item panel mItemPanel = new ItemPanel(getActivity()); FrameLayout.LayoutParams panelLayout = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, Utils.convertDpToPixel(TvApp.getApplication(), 145)); panelLayout.gravity = Gravity.BOTTOM; panelLayout.bottomMargin = -10; mItemPanel.setLayoutParams(panelLayout); root.addView(mItemPanel); mItemPanel.setVisibility(View.INVISIBLE); // and add the clock element FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); ClockUserView clock = new ClockUserView(getActivity()); layoutParams.gravity = Gravity.TOP | Gravity.RIGHT; layoutParams.rightMargin = Utils.convertDpToPixel(getActivity(), 40); layoutParams.topMargin = Utils.convertDpToPixel(getActivity(), 20); clock.setLayoutParams(layoutParams); root.addView(clock); // load item panel animation fadeInPanel = AnimationUtils.loadAnimation(mActivity, R.anim.abc_fade_in); fadeInPanel.setAnimationListener(new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { } @Override public void onAnimationEnd(Animation animation) { mItemPanel.setVisibility(View.VISIBLE); } @Override public void onAnimationRepeat(Animation animation) { } }); fadeOutPanel = AnimationUtils.loadAnimation(mActivity, R.anim.abc_fade_out); fadeOutPanel.setAnimationListener(new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { } @Override public void onAnimationEnd(Animation animation) { mItemPanel.setVisibility(View.INVISIBLE); } @Override public void onAnimationRepeat(Animation animation) { } }); // set search icon color setSearchAffordanceColor(getResources().getColor(R.color.search_opaque)); } private Runnable showItemPanel = new Runnable() { @Override public void run() { if (mCurrentItem != null && !mCurrentItem.isFolder() && (mCurrentItem.getRuntimeTicks() > 0 || (mCurrentItem.getSummary() != null && mCurrentItem.getSummary().length() > 0))) { mItemPanel.setItem(mCurrentItem); mItemPanel.startAnimation(fadeInPanel); mHandler.removeCallbacks(hideItemPanel); mHandler.postDelayed(hideItemPanel, 20000); } else { mItemPanel.setVisibility(View.INVISIBLE); } } }; private Runnable hideItemPanel = new Runnable() { @Override public void run() { mItemPanel.startAnimation(fadeOutPanel); } }; protected void setupEventListeners() { setOnSearchClickedListener(new View.OnClickListener() { @Override public void onClick(View view) { TvApp.getApplication().showSearch(getActivity(), false); } }); setOnItemViewClickedListener(mClickedListener); mClickedListener.registerListener(new ItemViewClickedListener()); setOnItemViewSelectedListener(mSelectedListener); mSelectedListener.registerListener(new ItemViewSelectedListener()); if (mActivity != null) { mActivity.registerKeyListener(new IKeyListener() { @Override public boolean onKeyUp(int key, KeyEvent event) { return KeyProcessor.HandleKey(key, mCurrentItem, mActivity); } }); mActivity.registerMessageListener(new IMessageListener() { @Override public void onMessageReceived(CustomMessage message) { switch (message) { case RefreshCurrentItem: refreshCurrentItem(); break; } } }); } } private void refreshCurrentItem() { if (mCurrentItem != null && !mCurrentItem.getType().equals("UserView") && !mCurrentItem.getType().equals("CollectionFolder")) { TvApp.getApplication().getLogger().Debug("Refresh item "+mCurrentItem.getFullName()); mCurrentItem.refresh(new EmptyResponse() { @Override public void onResponse() { ItemRowAdapter adapter = (ItemRowAdapter) ((ListRow)mCurrentRow).getAdapter(); adapter.notifyArrayItemRangeChanged(adapter.indexOf(mCurrentItem), 1); } }); } } private final class ItemViewClickedListener implements OnItemViewClickedListener { @Override public void onItemClicked(final Presenter.ViewHolder itemViewHolder, Object item, RowPresenter.ViewHolder rowViewHolder, Row row) { if (!(item instanceof BaseRowItem)) return; ItemLauncher.launch((BaseRowItem) item, (ItemRowAdapter) ((ListRow)row).getAdapter(), ((BaseRowItem)item).getIndex(), getActivity()); } } private final class ItemViewSelectedListener implements OnItemViewSelectedListener { @Override public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item, RowPresenter.ViewHolder rowViewHolder, Row row) { if (ShowInfoPanel) { // cancel any delayed showing and hide item panel mHandler.removeCallbacks(showItemPanel); mItemPanel.setVisibility(View.INVISIBLE); } if (!(item instanceof BaseRowItem)) { mCurrentItem = null; mHandler.removeCallbacks(hideItemPanel); //fill in default background mBackgroundUrl = null; startBackgroundTimer(); return; } else { mCurrentItem = (BaseRowItem)item; if (!isShowingHeaders() && ShowInfoPanel && !(item instanceof AudioQueueItem)) { // delay show the item panel mHandler.postDelayed(showItemPanel, 1000); } } mCurrentRow = (ListRow) row; BaseRowItem rowItem = (BaseRowItem) item; //mApplication.getLogger().Debug("Selected Item "+rowItem.getIndex() + " type: "+ (rowItem.getItemType().equals(BaseRowItem.ItemType.BaseItem) ? rowItem.getBaseItem().getType() : "other")); ItemRowAdapter adapter = (ItemRowAdapter) ((ListRow)row).getAdapter(); adapter.loadMoreItemsIfNeeded(rowItem.getIndex()); mBackgroundUrl = rowItem.getBackdropImageUrl(); startBackgroundTimer(); } } protected void setDefaultBackground(Drawable background) { mDefaultBackground = background; } protected void setDefaultBackground(int resourceId) { mDefaultBackground = getResources().getDrawable(resourceId); } protected void updateBackground(String url) { Glide.with(getActivity()) .load(url) .asBitmap() .override(mMetrics.widthPixels, mMetrics.heightPixels) .centerCrop() .error(mDefaultBackground) .into(mBackgroundTarget); } protected void updateBackground(Drawable drawable) { BackgroundManager.getInstance(getActivity()).setDrawable(drawable); } protected void clearBackground() { BackgroundManager.getInstance(getActivity()).setDrawable(mDefaultBackground); } private void startBackgroundTimer() { if (null != mBackgroundTimer) { mBackgroundTimer.cancel(); } mBackgroundTimer = new Timer(); mBackgroundTimer.schedule(new UpdateBackgroundTask(), BACKGROUND_UPDATE_DELAY); } private class UpdateBackgroundTask extends TimerTask { @Override public void run() { mHandler.post(new Runnable() { @Override public void run() { if (mBackgroundUrl != null) { updateBackground(mBackgroundUrl); } else { updateBackground(mDefaultBackground); } } }); } } }