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