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