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