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