1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.tv.ui.sidepanel;
18 
19 import android.app.Fragment;
20 import android.content.Context;
21 import android.graphics.drawable.RippleDrawable;
22 import android.os.Bundle;
23 import android.support.v17.leanback.widget.VerticalGridView;
24 import android.support.v7.widget.RecyclerView;
25 import android.view.KeyEvent;
26 import android.view.LayoutInflater;
27 import android.view.View;
28 import android.view.ViewGroup;
29 import android.widget.TextView;
30 
31 import com.android.tv.MainActivity;
32 import com.android.tv.R;
33 import com.android.tv.TvApplication;
34 import com.android.tv.analytics.DurationTimer;
35 import com.android.tv.analytics.HasTrackerLabel;
36 import com.android.tv.analytics.Tracker;
37 import com.android.tv.data.ChannelDataManager;
38 import com.android.tv.data.ProgramDataManager;
39 import com.android.tv.util.SystemProperties;
40 
41 import java.util.List;
42 
43 public abstract class SideFragment extends Fragment implements HasTrackerLabel {
44     public static final int INVALID_POSITION = -1;
45 
46     private static final int RECYCLED_VIEW_POOL_SIZE = 7;
47     private static final int[] PRELOADED_VIEW_IDS = {
48         R.layout.option_item_radio_button,
49         R.layout.option_item_channel_lock,
50         R.layout.option_item_check_box,
51         R.layout.option_item_channel_check
52     };
53 
54     private static RecyclerView.RecycledViewPool sRecycledViewPool;
55 
56     private VerticalGridView mListView;
57     private ItemAdapter mAdapter;
58     private SideFragmentListener mListener;
59     private ChannelDataManager mChannelDataManager;
60     private ProgramDataManager mProgramDataManager;
61     private Tracker mTracker;
62     private final DurationTimer mSidePanelDurationTimer = new DurationTimer();
63 
64     private final int mHideKey;
65     private final int mDebugHideKey;
66 
SideFragment()67     public SideFragment() {
68         this(KeyEvent.KEYCODE_UNKNOWN, KeyEvent.KEYCODE_UNKNOWN);
69     }
70 
71     /**
72      * @param hideKey the KeyCode used to hide the fragment
73      * @param debugHideKey the KeyCode used to hide the fragment if
74      *            {@link SystemProperties#USE_DEBUG_KEYS}.
75      */
SideFragment(int hideKey, int debugHideKey)76     public SideFragment(int hideKey, int debugHideKey) {
77         mHideKey = hideKey;
78         mDebugHideKey = debugHideKey;
79     }
80 
81     @Override
onAttach(Context context)82     public void onAttach(Context context) {
83         super.onAttach(context);
84         mChannelDataManager = getMainActivity().getChannelDataManager();
85         mProgramDataManager = getMainActivity().getProgramDataManager();
86         mTracker = TvApplication.getSingletons(context).getTracker();
87     }
88 
89     @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)90     public View onCreateView(LayoutInflater inflater, ViewGroup container,
91             Bundle savedInstanceState) {
92         if (sRecycledViewPool == null) {
93             // sRecycledViewPool should be initialized by calling preloadRecycledViews()
94             // before the entering animation of this fragment starts,
95             // because it takes long time and if it is called after the animation starts (e.g. here)
96             // it can affect the animation.
97             throw new IllegalStateException("The RecyclerView pool has not been initialized.");
98         }
99         View view = inflater.inflate(getFragmentLayoutResourceId(), container, false);
100 
101         TextView textView = (TextView) view.findViewById(R.id.side_panel_title);
102         textView.setText(getTitle());
103 
104         mListView = (VerticalGridView) view.findViewById(R.id.side_panel_list);
105         mListView.setRecycledViewPool(sRecycledViewPool);
106 
107         mAdapter = new ItemAdapter(inflater, getItemList());
108         mListView.setAdapter(mAdapter);
109         mListView.requestFocus();
110 
111         return view;
112     }
113 
114     @Override
onResume()115     public void onResume() {
116         super.onResume();
117         mTracker.sendShowSidePanel(this);
118         mTracker.sendScreenView(this.getTrackerLabel());
119         mSidePanelDurationTimer.start();
120     }
121 
122     @Override
onPause()123     public void onPause() {
124         mTracker.sendHideSidePanel(this, mSidePanelDurationTimer.reset());
125         super.onPause();
126     }
127 
128     @Override
onDetach()129     public void onDetach() {
130         mTracker = null;
131         super.onDetach();
132     }
133 
isHideKeyForThisPanel(int keyCode)134     public final boolean isHideKeyForThisPanel(int keyCode) {
135         boolean debugKeysEnabled = SystemProperties.USE_DEBUG_KEYS.getValue();
136         return mHideKey != KeyEvent.KEYCODE_UNKNOWN &&
137                 (mHideKey == keyCode || (debugKeysEnabled && mDebugHideKey == keyCode));
138     }
139 
140     @Override
onDestroyView()141     public void onDestroyView() {
142         super.onDestroyView();
143         mListView.swapAdapter(null, true);
144         if (mListener != null) {
145             mListener.onSideFragmentViewDestroyed();
146         }
147     }
148 
setListener(SideFragmentListener listener)149     public final void setListener(SideFragmentListener listener) {
150         mListener = listener;
151     }
152 
setSelectedPosition(int position)153     protected void setSelectedPosition(int position) {
154         mListView.setSelectedPosition(position);
155     }
156 
getSelectedPosition()157     protected int getSelectedPosition() {
158         return mListView.getSelectedPosition();
159     }
160 
setItems(List<Item> items)161     public void setItems(List<Item> items) {
162         mAdapter.reset(items);
163     }
164 
closeFragment()165     protected void closeFragment() {
166         getMainActivity().getOverlayManager().getSideFragmentManager().popSideFragment();
167     }
168 
getMainActivity()169     protected MainActivity getMainActivity() {
170         return (MainActivity) getActivity();
171     }
172 
getChannelDataManager()173     protected ChannelDataManager getChannelDataManager() {
174         return mChannelDataManager;
175     }
176 
getProgramDataManager()177     protected ProgramDataManager getProgramDataManager() {
178         return mProgramDataManager;
179     }
180 
notifyDataSetChanged()181     protected void notifyDataSetChanged() {
182         mAdapter.notifyDataSetChanged();
183     }
184 
185     /*
186      * HACK: The following methods bypass the updating mechanism of RecyclerView.Adapter and
187      * directly updates each item. This works around a bug in the base libraries where calling
188      * Adapter.notifyItemsChanged() causes the VerticalGridView to lose track of displayed item
189      * position.
190      */
191 
notifyItemChanged(int position)192     protected void notifyItemChanged(int position) {
193         notifyItemChanged(mAdapter.getItem(position));
194     }
195 
notifyItemChanged(Item item)196     protected void notifyItemChanged(Item item) {
197         item.notifyUpdated();
198     }
199 
200     /**
201      * Notifies all items of ItemAdapter has changed without structural changes.
202      */
notifyItemsChanged()203     protected void notifyItemsChanged() {
204         notifyItemsChanged(0, mAdapter.getItemCount());
205     }
206 
207     /**
208      * Notifies some items of ItemAdapter has changed starting from position
209      * <code>positionStart</code> to the end without structural changes.
210      */
notifyItemsChanged(int positionStart)211     protected void notifyItemsChanged(int positionStart) {
212         notifyItemsChanged(positionStart, mAdapter.getItemCount() - positionStart);
213     }
214 
notifyItemsChanged(int positionStart, int itemCount)215     protected void notifyItemsChanged(int positionStart, int itemCount) {
216         while (itemCount-- != 0) {
217             notifyItemChanged(positionStart++);
218         }
219     }
220 
221     /*
222      * END HACK
223      */
224 
getFragmentLayoutResourceId()225     protected int getFragmentLayoutResourceId() {
226         return R.layout.option_fragment;
227     }
228 
getTitle()229     protected abstract String getTitle();
230     @Override
getTrackerLabel()231     public abstract String getTrackerLabel();
getItemList()232     protected abstract List<Item> getItemList();
233 
234     public interface SideFragmentListener {
onSideFragmentViewDestroyed()235         void onSideFragmentViewDestroyed();
236     }
237 
238     /**
239      * Preloads the view holders.
240      */
preloadRecycledViews(Context context)241     public static void preloadRecycledViews(Context context) {
242         if (sRecycledViewPool != null) {
243             return;
244         }
245         sRecycledViewPool = new RecyclerView.RecycledViewPool();
246         LayoutInflater inflater =
247                 (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
248         for (int id : PRELOADED_VIEW_IDS) {
249             sRecycledViewPool.setMaxRecycledViews(id, RECYCLED_VIEW_POOL_SIZE);
250             for (int j = 0; j < RECYCLED_VIEW_POOL_SIZE; ++j) {
251                 ItemAdapter.ViewHolder viewHolder = new ItemAdapter.ViewHolder(
252                         inflater.inflate(id, null, false));
253                 sRecycledViewPool.putRecycledView(viewHolder);
254             }
255         }
256     }
257 
258     /**
259      * Releases the pre-loaded view holders.
260      */
releasePreloadedRecycledViews()261     public static void releasePreloadedRecycledViews() {
262         sRecycledViewPool = null;
263     }
264 
265     private static class ItemAdapter extends RecyclerView.Adapter<ItemAdapter.ViewHolder> {
266         private final LayoutInflater mLayoutInflater;
267         private List<Item> mItems;
268 
ItemAdapter(LayoutInflater layoutInflater, List<Item> items)269         private ItemAdapter(LayoutInflater layoutInflater, List<Item> items) {
270             mLayoutInflater = layoutInflater;
271             mItems = items;
272         }
273 
reset(List<Item> items)274         private void reset(List<Item> items) {
275             mItems = items;
276             notifyDataSetChanged();
277         }
278 
279         @Override
onCreateViewHolder(ViewGroup parent, int viewType)280         public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
281             return new ViewHolder(mLayoutInflater.inflate(viewType, parent, false));
282         }
283 
284         @Override
onBindViewHolder(ViewHolder holder, int position)285         public void onBindViewHolder(ViewHolder holder, int position) {
286             holder.onBind(this, getItem(position));
287         }
288 
289         @Override
onViewRecycled(ViewHolder holder)290         public void onViewRecycled(ViewHolder holder) {
291             holder.onUnbind();
292         }
293 
294         @Override
getItemViewType(int position)295         public int getItemViewType(int position) {
296             return getItem(position).getResourceId();
297         }
298 
299         @Override
getItemCount()300         public int getItemCount() {
301             return mItems == null ? 0 : mItems.size();
302         }
303 
getItem(int position)304         private Item getItem(int position) {
305             return mItems.get(position);
306         }
307 
clearRadioGroup(Item item)308         private void clearRadioGroup(Item item) {
309             int position = mItems.indexOf(item);
310             for (int i = position - 1; i >= 0; --i) {
311                 if ((item = mItems.get(i)) instanceof RadioButtonItem) {
312                     ((RadioButtonItem) item).setChecked(false);
313                 } else {
314                     break;
315                 }
316             }
317             for (int i = position + 1; i < mItems.size(); ++i) {
318                 if ((item = mItems.get(i)) instanceof RadioButtonItem) {
319                     ((RadioButtonItem) item).setChecked(false);
320                 } else {
321                     break;
322                 }
323             }
324         }
325 
326         private static class ViewHolder extends RecyclerView.ViewHolder
327                 implements View.OnClickListener, View.OnFocusChangeListener {
328             private ItemAdapter mAdapter;
329             public Item mItem;
330 
ViewHolder(View view)331             private ViewHolder(View view) {
332                 super(view);
333                 itemView.setOnClickListener(this);
334                 itemView.setOnFocusChangeListener(this);
335             }
336 
onBind(ItemAdapter adapter, Item item)337             public void onBind(ItemAdapter adapter, Item item) {
338                 mAdapter = adapter;
339                 mItem = item;
340                 mItem.onBind(itemView);
341                 mItem.onUpdate();
342             }
343 
onUnbind()344             public void onUnbind() {
345                 mItem.onUnbind();
346                 mItem = null;
347                 mAdapter = null;
348             }
349 
350             @Override
onClick(View view)351             public void onClick(View view) {
352                 if (mItem instanceof RadioButtonItem) {
353                     mAdapter.clearRadioGroup(mItem);
354                 }
355                 if (view.getBackground() instanceof RippleDrawable) {
356                     view.postDelayed(new Runnable() {
357                         @Override
358                         public void run() {
359                             if (mItem != null) {
360                                 mItem.onSelected();
361                             }
362                         }
363                     }, view.getResources().getInteger(R.integer.side_panel_ripple_anim_duration));
364                 } else {
365                     mItem.onSelected();
366                 }
367             }
368 
369             @Override
onFocusChange(View view, boolean focusGained)370             public void onFocusChange(View view, boolean focusGained) {
371                 if (focusGained) {
372                     mItem.onFocused();
373                 }
374             }
375         }
376     }
377 }
378