1/* 2 * Copyright (C) 2016 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 17package com.android.tv.dvr.ui; 18 19import android.content.Context; 20import android.content.Intent; 21import android.content.res.Resources; 22import android.graphics.Bitmap; 23import android.graphics.drawable.BitmapDrawable; 24import android.graphics.drawable.Drawable; 25import android.media.tv.TvContentRating; 26import android.media.tv.TvInputManager; 27import android.net.Uri; 28import android.os.Bundle; 29import android.support.annotation.Nullable; 30import android.support.v17.leanback.app.DetailsFragment; 31import android.support.v17.leanback.widget.ArrayObjectAdapter; 32import android.support.v17.leanback.widget.ClassPresenterSelector; 33import android.support.v17.leanback.widget.DetailsOverviewRow; 34import android.support.v17.leanback.widget.DetailsOverviewRowPresenter; 35import android.support.v17.leanback.widget.OnActionClickedListener; 36import android.support.v17.leanback.widget.PresenterSelector; 37import android.support.v17.leanback.widget.SparseArrayObjectAdapter; 38import android.support.v17.leanback.widget.VerticalGridView; 39import android.text.Spannable; 40import android.text.SpannableString; 41import android.text.TextUtils; 42import android.text.style.TextAppearanceSpan; 43import android.widget.Toast; 44 45import com.android.tv.R; 46import com.android.tv.TvApplication; 47import com.android.tv.data.BaseProgram; 48import com.android.tv.data.Channel; 49import com.android.tv.data.ChannelDataManager; 50import com.android.tv.dialog.PinDialogFragment; 51import com.android.tv.dvr.DvrPlaybackActivity; 52import com.android.tv.dvr.RecordedProgram; 53import com.android.tv.parental.ParentalControlSettings; 54import com.android.tv.util.ImageLoader; 55import com.android.tv.util.ToastUtils; 56import com.android.tv.util.Utils; 57 58import java.io.File; 59 60abstract class DvrDetailsFragment extends DetailsFragment { 61 private static final int LOAD_LOGO_IMAGE = 1; 62 private static final int LOAD_BACKGROUND_IMAGE = 2; 63 64 protected DetailsViewBackgroundHelper mBackgroundHelper; 65 private ArrayObjectAdapter mRowsAdapter; 66 private DetailsOverviewRow mDetailsOverview; 67 68 @Override 69 public void onCreate(Bundle savedInstanceState) { 70 super.onCreate(savedInstanceState); 71 if (!onLoadRecordingDetails(getArguments())) { 72 getActivity().finish(); 73 return; 74 } 75 mBackgroundHelper = new DetailsViewBackgroundHelper(getActivity()); 76 setupAdapter(); 77 onCreateInternal(); 78 } 79 80 @Override 81 public void onStart() { 82 super.onStart(); 83 // TODO: remove the workaround of b/30401180. 84 VerticalGridView container = (VerticalGridView) getActivity() 85 .findViewById(R.id.container_list); 86 // Need to manually modify offset. Please refer DetailsFragment.setVerticalGridViewLayout. 87 container.setItemAlignmentOffset(0); 88 container.setWindowAlignmentOffset( 89 getResources().getDimensionPixelSize(R.dimen.lb_details_rows_align_top)); 90 } 91 92 private void setupAdapter() { 93 DetailsOverviewRowPresenter rowPresenter = new DetailsOverviewRowPresenter( 94 new DetailsContentPresenter(getActivity())); 95 rowPresenter.setBackgroundColor(getResources().getColor(R.color.common_tv_background, 96 null)); 97 rowPresenter.setSharedElementEnterTransition(getActivity(), 98 DvrDetailsActivity.SHARED_ELEMENT_NAME); 99 rowPresenter.setOnActionClickedListener(onCreateOnActionClickedListener()); 100 mRowsAdapter = new ArrayObjectAdapter(onCreatePresenterSelector(rowPresenter)); 101 setAdapter(mRowsAdapter); 102 } 103 104 /** 105 * Returns details views' rows adapter. 106 */ 107 protected ArrayObjectAdapter getRowsAdapter() { 108 return mRowsAdapter; 109 } 110 111 /** 112 * Sets details overview. 113 */ 114 protected void setDetailsOverviewRow(DetailsContent detailsContent) { 115 mDetailsOverview = new DetailsOverviewRow(detailsContent); 116 mDetailsOverview.setActionsAdapter(onCreateActionsAdapter()); 117 mRowsAdapter.add(mDetailsOverview); 118 onLoadLogoAndBackgroundImages(detailsContent); 119 } 120 121 /** 122 * Creates and returns presenter selector will be used by rows adaptor. 123 */ 124 protected PresenterSelector onCreatePresenterSelector( 125 DetailsOverviewRowPresenter rowPresenter) { 126 ClassPresenterSelector presenterSelector = new ClassPresenterSelector(); 127 presenterSelector.addClassPresenter(DetailsOverviewRow.class, rowPresenter); 128 return presenterSelector; 129 } 130 131 /** 132 * Does customized initialization of subclasses. Since {@link #onCreate(Bundle)} might finish 133 * activity early when it cannot fetch valid recordings, subclasses' onCreate method should not 134 * do anything after calling {@link #onCreate(Bundle)}. If there's something subclasses have to 135 * do after the super class did onCreate, it should override this method and put the codes here. 136 */ 137 protected void onCreateInternal() { } 138 139 /** 140 * Updates actions of details overview. 141 */ 142 protected void updateActions() { 143 mDetailsOverview.setActionsAdapter(onCreateActionsAdapter()); 144 } 145 146 /** 147 * Loads recording details according to the arguments the fragment got. 148 * 149 * @return false if cannot find valid recordings, else return true. If the return value 150 * is false, the detail activity and fragment will be ended. 151 */ 152 abstract boolean onLoadRecordingDetails(Bundle args); 153 154 /** 155 * Creates actions users can interact with and their adaptor for this fragment. 156 */ 157 abstract SparseArrayObjectAdapter onCreateActionsAdapter(); 158 159 /** 160 * Creates actions listeners to implement the behavior of the fragment after users click some 161 * action buttons. 162 */ 163 abstract OnActionClickedListener onCreateOnActionClickedListener(); 164 165 /** 166 * Returns program title with episode number. If the program is null, returns channel name. 167 */ 168 protected CharSequence getTitleFromProgram(BaseProgram program, Channel channel) { 169 String titleWithEpisodeNumber = program.getTitleWithEpisodeNumber(getContext()); 170 SpannableString title = titleWithEpisodeNumber == null ? null 171 : new SpannableString(titleWithEpisodeNumber); 172 if (TextUtils.isEmpty(title)) { 173 title = new SpannableString(channel != null ? channel.getDisplayName() 174 : getContext().getResources().getString( 175 R.string.no_program_information)); 176 } else { 177 String programTitle = program.getTitle(); 178 title.setSpan(new TextAppearanceSpan(getContext(), 179 R.style.text_appearance_card_view_episode_number), programTitle == null ? 0 180 : programTitle.length(), title.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); 181 } 182 return title; 183 } 184 185 /** 186 * Loads logo and background images for detail fragments. 187 */ 188 protected void onLoadLogoAndBackgroundImages(DetailsContent detailsContent) { 189 Drawable logoDrawable = null; 190 Drawable backgroundDrawable = null; 191 if (TextUtils.isEmpty(detailsContent.getLogoImageUri())) { 192 logoDrawable = getContext().getResources() 193 .getDrawable(R.drawable.dvr_default_poster, null); 194 mDetailsOverview.setImageDrawable(logoDrawable); 195 } 196 if (TextUtils.isEmpty(detailsContent.getBackgroundImageUri())) { 197 backgroundDrawable = getContext().getResources() 198 .getDrawable(R.drawable.dvr_default_poster, null); 199 mBackgroundHelper.setBackground(backgroundDrawable); 200 } 201 if (logoDrawable != null && backgroundDrawable != null) { 202 return; 203 } 204 if (logoDrawable == null && backgroundDrawable == null 205 && detailsContent.getLogoImageUri().equals( 206 detailsContent.getBackgroundImageUri())) { 207 ImageLoader.loadBitmap(getContext(), detailsContent.getLogoImageUri(), 208 new MyImageLoaderCallback(this, LOAD_LOGO_IMAGE | LOAD_BACKGROUND_IMAGE, 209 getContext())); 210 return; 211 } 212 if (logoDrawable == null) { 213 int imageWidth = getResources().getDimensionPixelSize(R.dimen.dvr_details_poster_width); 214 int imageHeight = getResources() 215 .getDimensionPixelSize(R.dimen.dvr_details_poster_height); 216 ImageLoader.loadBitmap(getContext(), detailsContent.getLogoImageUri(), 217 imageWidth, imageHeight, 218 new MyImageLoaderCallback(this, LOAD_LOGO_IMAGE, getContext())); 219 } 220 if (backgroundDrawable == null) { 221 ImageLoader.loadBitmap(getContext(), detailsContent.getBackgroundImageUri(), 222 new MyImageLoaderCallback(this, LOAD_BACKGROUND_IMAGE, getContext())); 223 } 224 } 225 226 protected void startPlayback(RecordedProgram recordedProgram, long seekTimeMs) { 227 if (Utils.isInBundledPackageSet(recordedProgram.getPackageName()) && 228 !isDataUriAccessible(recordedProgram.getDataUri())) { 229 // Since cleaning RecordedProgram from forgotten storage will take some time, 230 // ignore playback until cleaning is finished. 231 ToastUtils.show(getContext(), 232 getContext().getResources().getString(R.string.dvr_toast_recording_deleted), 233 Toast.LENGTH_SHORT); 234 return; 235 } 236 ParentalControlSettings parental = TvApplication.getSingletons(getActivity()) 237 .getTvInputManagerHelper().getParentalControlSettings(); 238 if (!parental.isParentalControlsEnabled()) { 239 launchPlaybackActivity(recordedProgram, seekTimeMs, false); 240 return; 241 } 242 ChannelDataManager channelDataManager = 243 TvApplication.getSingletons(getActivity()).getChannelDataManager(); 244 Channel channel = channelDataManager.getChannel(recordedProgram.getChannelId()); 245 if (channel != null && channel.isLocked()) { 246 checkPinToPlay(recordedProgram, seekTimeMs); 247 return; 248 } 249 String ratingString = recordedProgram.getContentRating(); 250 if (TextUtils.isEmpty(ratingString)) { 251 launchPlaybackActivity(recordedProgram, seekTimeMs, false); 252 return; 253 } 254 String[] ratingList = ratingString.split(","); 255 TvContentRating[] programRatings = new TvContentRating[ratingList.length]; 256 for (int i = 0; i < ratingList.length; i++) { 257 programRatings[i] = TvContentRating.unflattenFromString(ratingList[i]); 258 } 259 TvContentRating blockRatings = parental.getBlockedRating(programRatings); 260 if (blockRatings != null) { 261 checkPinToPlay(recordedProgram, seekTimeMs); 262 } else { 263 launchPlaybackActivity(recordedProgram, seekTimeMs, false); 264 } 265 } 266 267 private boolean isDataUriAccessible(Uri dataUri) { 268 if (dataUri == null || dataUri.getPath() == null) { 269 return false; 270 } 271 try { 272 File recordedProgramPath = new File(dataUri.getPath()); 273 if (recordedProgramPath.exists()) { 274 return true; 275 } 276 } catch (SecurityException e) { 277 } 278 return false; 279 } 280 281 private void checkPinToPlay(RecordedProgram recordedProgram, long seekTimeMs) { 282 new PinDialogFragment(PinDialogFragment.PIN_DIALOG_TYPE_UNLOCK_PROGRAM, 283 new PinDialogFragment.ResultListener() { 284 @Override 285 public void done(boolean success) { 286 if (success) { 287 launchPlaybackActivity(recordedProgram, seekTimeMs, true); 288 } 289 } 290 }).show(getActivity().getFragmentManager(), PinDialogFragment.DIALOG_TAG); 291 } 292 293 private void launchPlaybackActivity(RecordedProgram mRecordedProgram, long seekTimeMs, 294 boolean pinChecked) { 295 Intent intent = new Intent(getActivity(), DvrPlaybackActivity.class); 296 intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, mRecordedProgram.getId()); 297 if (seekTimeMs != TvInputManager.TIME_SHIFT_INVALID_TIME) { 298 intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_SEEK_TIME, seekTimeMs); 299 } 300 intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_PIN_CHECKED, pinChecked); 301 getActivity().startActivity(intent); 302 } 303 304 private static class MyImageLoaderCallback extends 305 ImageLoader.ImageLoaderCallback<DvrDetailsFragment> { 306 private final Context mContext; 307 private final int mLoadType; 308 309 public MyImageLoaderCallback(DvrDetailsFragment fragment, 310 int loadType, Context context) { 311 super(fragment); 312 mLoadType = loadType; 313 mContext = context; 314 } 315 316 @Override 317 public void onBitmapLoaded(DvrDetailsFragment fragment, 318 @Nullable Bitmap bitmap) { 319 Drawable drawable; 320 int loadType = mLoadType; 321 if (bitmap == null) { 322 Resources res = mContext.getResources(); 323 drawable = res.getDrawable(R.drawable.dvr_default_poster, null); 324 if ((loadType & LOAD_BACKGROUND_IMAGE) != 0 && !fragment.isDetached()) { 325 loadType &= ~LOAD_BACKGROUND_IMAGE; 326 fragment.mBackgroundHelper.setBackgroundColor( 327 res.getColor(R.color.dvr_detail_default_background)); 328 fragment.mBackgroundHelper.setScrim( 329 res.getColor(R.color.dvr_detail_default_background_scrim)); 330 } 331 } else { 332 drawable = new BitmapDrawable(mContext.getResources(), bitmap); 333 } 334 if (!fragment.isDetached()) { 335 if ((loadType & LOAD_LOGO_IMAGE) != 0) { 336 fragment.mDetailsOverview.setImageDrawable(drawable); 337 } 338 if ((loadType & LOAD_BACKGROUND_IMAGE) != 0) { 339 fragment.mBackgroundHelper.setBackground(drawable); 340 } 341 } 342 } 343 } 344} 345