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