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