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