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