• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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;
18 
19 import android.content.Context;
20 import android.content.Intent;
21 import android.content.res.Resources;
22 import android.graphics.Bitmap;
23 import android.graphics.drawable.BitmapDrawable;
24 import android.graphics.drawable.Drawable;
25 import android.media.tv.TvContentRating;
26 import android.media.tv.TvInputManager;
27 import android.net.Uri;
28 import android.os.Bundle;
29 import android.support.annotation.Nullable;
30 import android.support.v17.leanback.app.DetailsFragment;
31 import android.support.v17.leanback.widget.ArrayObjectAdapter;
32 import android.support.v17.leanback.widget.ClassPresenterSelector;
33 import android.support.v17.leanback.widget.DetailsOverviewRow;
34 import android.support.v17.leanback.widget.DetailsOverviewRowPresenter;
35 import android.support.v17.leanback.widget.OnActionClickedListener;
36 import android.support.v17.leanback.widget.PresenterSelector;
37 import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
38 import android.support.v17.leanback.widget.VerticalGridView;
39 import android.text.Spannable;
40 import android.text.SpannableString;
41 import android.text.TextUtils;
42 import android.text.style.TextAppearanceSpan;
43 import android.widget.Toast;
44 
45 import com.android.tv.R;
46 import com.android.tv.TvApplication;
47 import com.android.tv.data.BaseProgram;
48 import com.android.tv.data.Channel;
49 import com.android.tv.data.ChannelDataManager;
50 import com.android.tv.dialog.PinDialogFragment;
51 import com.android.tv.dvr.DvrPlaybackActivity;
52 import com.android.tv.dvr.RecordedProgram;
53 import com.android.tv.parental.ParentalControlSettings;
54 import com.android.tv.util.ImageLoader;
55 import com.android.tv.util.ToastUtils;
56 import com.android.tv.util.Utils;
57 
58 import java.io.File;
59 
60 abstract 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
onCreate(Bundle savedInstanceState)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
onStart()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 
setupAdapter()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      */
getRowsAdapter()107     protected ArrayObjectAdapter getRowsAdapter() {
108         return  mRowsAdapter;
109     }
110 
111     /**
112      * Sets details overview.
113      */
setDetailsOverviewRow(DetailsContent detailsContent)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      */
onCreatePresenterSelector( DetailsOverviewRowPresenter rowPresenter)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      */
onCreateInternal()137     protected void onCreateInternal() { }
138 
139     /**
140      * Updates actions of details overview.
141      */
updateActions()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      */
onLoadRecordingDetails(Bundle args)152     abstract boolean onLoadRecordingDetails(Bundle args);
153 
154     /**
155      * Creates actions users can interact with and their adaptor for this fragment.
156      */
onCreateActionsAdapter()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      */
onCreateOnActionClickedListener()163     abstract OnActionClickedListener onCreateOnActionClickedListener();
164 
165     /**
166      * Returns program title with episode number. If the program is null, returns channel name.
167      */
getTitleFromProgram(BaseProgram program, Channel channel)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      */
onLoadLogoAndBackgroundImages(DetailsContent detailsContent)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 
startPlayback(RecordedProgram recordedProgram, long seekTimeMs)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 
isDataUriAccessible(Uri dataUri)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 
checkPinToPlay(RecordedProgram recordedProgram, long seekTimeMs)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 
launchPlaybackActivity(RecordedProgram mRecordedProgram, long seekTimeMs, boolean pinChecked)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 
MyImageLoaderCallback(DvrDetailsFragment fragment, int loadType, Context context)309         public MyImageLoaderCallback(DvrDetailsFragment fragment,
310                 int loadType, Context context) {
311             super(fragment);
312             mLoadType = loadType;
313             mContext = context;
314         }
315 
316         @Override
onBitmapLoaded(DvrDetailsFragment fragment, @Nullable Bitmap bitmap)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