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