1 /* 2 * Copyright (C) 2018 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; 18 19 import android.content.Context; 20 import android.content.res.Resources; 21 import android.graphics.Bitmap; 22 import android.graphics.drawable.BitmapDrawable; 23 import android.graphics.drawable.Drawable; 24 import android.os.Bundle; 25 import android.support.annotation.Nullable; 26 import android.support.v17.leanback.app.DetailsFragment; 27 import android.support.v17.leanback.widget.Action; 28 import android.support.v17.leanback.widget.ArrayObjectAdapter; 29 import android.support.v17.leanback.widget.ClassPresenterSelector; 30 import android.support.v17.leanback.widget.DetailsOverviewRow; 31 import android.support.v17.leanback.widget.DetailsOverviewRowPresenter; 32 import android.support.v17.leanback.widget.OnActionClickedListener; 33 import android.support.v17.leanback.widget.PresenterSelector; 34 import android.support.v17.leanback.widget.SparseArrayObjectAdapter; 35 import android.support.v17.leanback.widget.VerticalGridView; 36 import android.text.TextUtils; 37 import com.android.tv.R; 38 import com.android.tv.TvSingletons; 39 import com.android.tv.common.feature.CommonFeatures; 40 import com.android.tv.data.Program; 41 import com.android.tv.data.api.Channel; 42 import com.android.tv.dvr.DvrDataManager; 43 import com.android.tv.dvr.DvrManager; 44 import com.android.tv.dvr.DvrScheduleManager; 45 import com.android.tv.dvr.data.ScheduledRecording; 46 import com.android.tv.dvr.ui.DvrUiHelper; 47 import com.android.tv.dvr.ui.browse.ActionPresenterSelector; 48 import com.android.tv.dvr.ui.browse.DetailsContent; 49 import com.android.tv.dvr.ui.browse.DetailsContentPresenter; 50 import com.android.tv.dvr.ui.browse.DetailsViewBackgroundHelper; 51 import com.android.tv.util.images.ImageLoader; 52 53 /** A fragment shows the details of a Program */ 54 public class ProgramDetailsFragment extends DetailsFragment 55 implements DvrDataManager.ScheduledRecordingListener, 56 DvrScheduleManager.OnConflictStateChangeListener { 57 private static final int LOAD_LOGO_IMAGE = 1; 58 private static final int LOAD_BACKGROUND_IMAGE = 2; 59 60 private static final int ACTION_VIEW_SCHEDULE = 1; 61 private static final int ACTION_CANCEL = 2; 62 private static final int ACTION_SCHEDULE_RECORDING = 3; 63 64 protected DetailsViewBackgroundHelper mBackgroundHelper; 65 private ArrayObjectAdapter mRowsAdapter; 66 private DetailsOverviewRow mDetailsOverview; 67 private Program mProgram; 68 private String mInputId; 69 private ScheduledRecording mScheduledRecording; 70 private DvrManager mDvrManager; 71 private DvrDataManager mDvrDataManager; 72 private DvrScheduleManager mDvrScheduleManager; 73 74 @Override onCreate(Bundle savedInstanceState)75 public void onCreate(Bundle savedInstanceState) { 76 super.onCreate(savedInstanceState); 77 if (!onLoadDetails(getArguments())) { 78 getActivity().finish(); 79 } 80 } 81 82 @Override onDestroy()83 public void onDestroy() { 84 mDvrDataManager.removeScheduledRecordingListener(this); 85 mDvrScheduleManager.removeOnConflictStateChangeListener(this); 86 super.onDestroy(); 87 } 88 89 @Override onStart()90 public void onStart() { 91 super.onStart(); 92 VerticalGridView container = 93 (VerticalGridView) getActivity().findViewById(R.id.container_list); 94 // Need to manually modify offset. Please refer DetailsFragment.setVerticalGridViewLayout. 95 container.setItemAlignmentOffset(0); 96 container.setWindowAlignmentOffset( 97 getResources().getDimensionPixelSize(R.dimen.lb_details_rows_align_top)); 98 } 99 setupAdapter()100 private void setupAdapter() { 101 DetailsOverviewRowPresenter rowPresenter = 102 new DetailsOverviewRowPresenter(new DetailsContentPresenter(getActivity())); 103 rowPresenter.setBackgroundColor( 104 getResources().getColor(R.color.common_tv_background, null)); 105 rowPresenter.setSharedElementEnterTransition( 106 getActivity(), DetailsActivity.SHARED_ELEMENT_NAME); 107 rowPresenter.setOnActionClickedListener(onCreateOnActionClickedListener()); 108 mRowsAdapter = new ArrayObjectAdapter(onCreatePresenterSelector(rowPresenter)); 109 setAdapter(mRowsAdapter); 110 } 111 112 /** Sets details overview. */ setDetailsOverviewRow(DetailsContent detailsContent)113 protected void setDetailsOverviewRow(DetailsContent detailsContent) { 114 mDetailsOverview = new DetailsOverviewRow(detailsContent); 115 mDetailsOverview.setActionsAdapter(onCreateActionsAdapter()); 116 mRowsAdapter.add(mDetailsOverview); 117 onLoadLogoAndBackgroundImages(detailsContent); 118 } 119 120 /** Creates and returns presenter selector will be used by rows adaptor. */ onCreatePresenterSelector( DetailsOverviewRowPresenter rowPresenter)121 protected PresenterSelector onCreatePresenterSelector( 122 DetailsOverviewRowPresenter rowPresenter) { 123 ClassPresenterSelector presenterSelector = new ClassPresenterSelector(); 124 presenterSelector.addClassPresenter(DetailsOverviewRow.class, rowPresenter); 125 return presenterSelector; 126 } 127 128 /** Updates actions of details overview. */ updateActions()129 protected void updateActions() { 130 mDetailsOverview.setActionsAdapter(onCreateActionsAdapter()); 131 } 132 133 /** 134 * Loads program details according to the arguments the fragment got. 135 * 136 * @return false if cannot find valid programs, else return true. If the return value is false, 137 * the detail activity and fragment will be ended. 138 */ onLoadDetails(Bundle args)139 private boolean onLoadDetails(Bundle args) { 140 Program program = args.getParcelable(DetailsActivity.PROGRAM); 141 long channelId = args.getLong(DetailsActivity.CHANNEL_ID); 142 String inputId = args.getString(DetailsActivity.INPUT_ID); 143 if (program != null && channelId != Channel.INVALID_ID && !TextUtils.isEmpty(inputId)) { 144 mProgram = program; 145 mInputId = inputId; 146 TvSingletons singletons = TvSingletons.getSingletons(getContext()); 147 mDvrDataManager = singletons.getDvrDataManager(); 148 mDvrManager = singletons.getDvrManager(); 149 mDvrScheduleManager = singletons.getDvrScheduleManager(); 150 mScheduledRecording = 151 mDvrDataManager.getScheduledRecordingForProgramId(program.getId()); 152 mBackgroundHelper = new DetailsViewBackgroundHelper(getActivity()); 153 setupAdapter(); 154 setDetailsOverviewRow(DetailsContent.createFromProgram(getContext(), mProgram)); 155 mDvrDataManager.addScheduledRecordingListener(this); 156 mDvrScheduleManager.addOnConflictStateChangeListener(this); 157 return true; 158 } 159 return false; 160 } 161 getScheduleIconId()162 private int getScheduleIconId() { 163 if (mDvrManager.isConflicting(mScheduledRecording)) { 164 return R.drawable.ic_warning_white_32dp; 165 } else { 166 return R.drawable.ic_schedule_32dp; 167 } 168 } 169 170 /** Creates actions users can interact with and their adaptor for this fragment. */ onCreateActionsAdapter()171 private SparseArrayObjectAdapter onCreateActionsAdapter() { 172 SparseArrayObjectAdapter adapter = 173 new SparseArrayObjectAdapter(new ActionPresenterSelector()); 174 Resources res = getResources(); 175 if (mScheduledRecording != null) { 176 adapter.set( 177 ACTION_VIEW_SCHEDULE, 178 new Action( 179 ACTION_VIEW_SCHEDULE, 180 res.getString(R.string.dvr_detail_view_schedule), 181 null, 182 res.getDrawable(getScheduleIconId()))); 183 adapter.set( 184 ACTION_CANCEL, 185 new Action( 186 ACTION_CANCEL, 187 res.getString(R.string.dvr_detail_cancel_recording), 188 null, 189 res.getDrawable(R.drawable.ic_dvr_cancel_32dp))); 190 } else if (CommonFeatures.DVR.isEnabled(getActivity()) 191 && mDvrManager.isProgramRecordable(mProgram)) { 192 adapter.set( 193 ACTION_SCHEDULE_RECORDING, 194 new Action( 195 ACTION_SCHEDULE_RECORDING, 196 res.getString(R.string.dvr_detail_schedule_recording), 197 null, 198 res.getDrawable(R.drawable.ic_schedule_32dp))); 199 } 200 return adapter; 201 } 202 203 /** 204 * Creates actions listeners to implement the behavior of the fragment after users click some 205 * action buttons. 206 */ onCreateOnActionClickedListener()207 private OnActionClickedListener onCreateOnActionClickedListener() { 208 return new OnActionClickedListener() { 209 @Override 210 public void onActionClicked(Action action) { 211 long actionId = action.getId(); 212 if (actionId == ACTION_VIEW_SCHEDULE) { 213 DvrUiHelper.startSchedulesActivity(getContext(), mScheduledRecording); 214 } else if (actionId == ACTION_CANCEL) { 215 mDvrManager.removeScheduledRecording(mScheduledRecording); 216 } else if (actionId == ACTION_SCHEDULE_RECORDING) { 217 DvrUiHelper.checkStorageStatusAndShowErrorMessage( 218 getActivity(), 219 mInputId, 220 () -> 221 DvrUiHelper.requestRecordingFutureProgram( 222 getActivity(), mProgram, false)); 223 } 224 } 225 }; 226 } 227 228 /** Loads logo and background images for detail fragments. */ 229 protected void onLoadLogoAndBackgroundImages(DetailsContent detailsContent) { 230 Drawable logoDrawable = null; 231 Drawable backgroundDrawable = null; 232 if (TextUtils.isEmpty(detailsContent.getLogoImageUri())) { 233 logoDrawable = 234 getContext().getResources().getDrawable(R.drawable.dvr_default_poster, null); 235 mDetailsOverview.setImageDrawable(logoDrawable); 236 } 237 if (TextUtils.isEmpty(detailsContent.getBackgroundImageUri())) { 238 backgroundDrawable = 239 getContext().getResources().getDrawable(R.drawable.dvr_default_poster, null); 240 mBackgroundHelper.setBackground(backgroundDrawable); 241 } 242 if (logoDrawable != null && backgroundDrawable != null) { 243 return; 244 } 245 if (logoDrawable == null 246 && backgroundDrawable == null 247 && detailsContent 248 .getLogoImageUri() 249 .equals(detailsContent.getBackgroundImageUri())) { 250 ImageLoader.loadBitmap( 251 getContext(), 252 detailsContent.getLogoImageUri(), 253 new MyImageLoaderCallback( 254 this, LOAD_LOGO_IMAGE | LOAD_BACKGROUND_IMAGE, getContext())); 255 return; 256 } 257 if (logoDrawable == null) { 258 int imageWidth = getResources().getDimensionPixelSize(R.dimen.dvr_details_poster_width); 259 int imageHeight = 260 getResources().getDimensionPixelSize(R.dimen.dvr_details_poster_height); 261 ImageLoader.loadBitmap( 262 getContext(), 263 detailsContent.getLogoImageUri(), 264 imageWidth, 265 imageHeight, 266 new MyImageLoaderCallback(this, LOAD_LOGO_IMAGE, getContext())); 267 } 268 if (backgroundDrawable == null) { 269 ImageLoader.loadBitmap( 270 getContext(), 271 detailsContent.getBackgroundImageUri(), 272 new MyImageLoaderCallback(this, LOAD_BACKGROUND_IMAGE, getContext())); 273 } 274 } 275 276 @Override 277 public void onScheduledRecordingAdded(ScheduledRecording... scheduledRecordings) { 278 for (ScheduledRecording recording : scheduledRecordings) { 279 if (recording.getProgramId() == mProgram.getId()) { 280 mScheduledRecording = recording; 281 updateActions(); 282 return; 283 } 284 } 285 } 286 287 @Override 288 public void onScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings) { 289 if (mScheduledRecording == null) { 290 return; 291 } 292 for (ScheduledRecording recording : scheduledRecordings) { 293 if (recording.getId() == mScheduledRecording.getId()) { 294 mScheduledRecording = null; 295 updateActions(); 296 return; 297 } 298 } 299 } 300 301 @Override 302 public void onScheduledRecordingStatusChanged(ScheduledRecording... scheduledRecordings) { 303 if (mScheduledRecording == null) { 304 return; 305 } 306 for (ScheduledRecording recording : scheduledRecordings) { 307 if (recording.getId() == mScheduledRecording.getId()) { 308 mScheduledRecording = recording; 309 updateActions(); 310 return; 311 } 312 } 313 } 314 315 @Override 316 public void onConflictStateChange(boolean conflict, ScheduledRecording... scheduledRecordings) { 317 onScheduledRecordingStatusChanged(scheduledRecordings); 318 } 319 320 private static class MyImageLoaderCallback 321 extends ImageLoader.ImageLoaderCallback<ProgramDetailsFragment> { 322 private final Context mContext; 323 private final int mLoadType; 324 325 public MyImageLoaderCallback( 326 ProgramDetailsFragment fragment, int loadType, Context context) { 327 super(fragment); 328 mLoadType = loadType; 329 mContext = context; 330 } 331 332 @Override 333 public void onBitmapLoaded(ProgramDetailsFragment fragment, @Nullable Bitmap bitmap) { 334 Drawable drawable; 335 int loadType = mLoadType; 336 if (bitmap == null) { 337 Resources res = mContext.getResources(); 338 drawable = res.getDrawable(R.drawable.dvr_default_poster, null); 339 if ((loadType & LOAD_BACKGROUND_IMAGE) != 0 && !fragment.isDetached()) { 340 loadType &= ~LOAD_BACKGROUND_IMAGE; 341 fragment.mBackgroundHelper.setBackgroundColor( 342 res.getColor(R.color.dvr_detail_default_background)); 343 fragment.mBackgroundHelper.setScrim( 344 res.getColor(R.color.dvr_detail_default_background_scrim)); 345 } 346 } else { 347 drawable = new BitmapDrawable(mContext.getResources(), bitmap); 348 } 349 if (!fragment.isDetached()) { 350 if ((loadType & LOAD_LOGO_IMAGE) != 0) { 351 fragment.mDetailsOverview.setImageDrawable(drawable); 352 } 353 if ((loadType & LOAD_BACKGROUND_IMAGE) != 0) { 354 fragment.mBackgroundHelper.setBackground(drawable); 355 } 356 } 357 } 358 } 359 } 360