1 /*
2  * Copyright (C) 2018 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.ui;
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.os.Bundle;
25 import android.support.annotation.Nullable;
26 import android.text.TextUtils;
27 
28 import androidx.leanback.app.DetailsFragment;
29 import androidx.leanback.widget.Action;
30 import androidx.leanback.widget.ArrayObjectAdapter;
31 import androidx.leanback.widget.ClassPresenterSelector;
32 import androidx.leanback.widget.DetailsOverviewRow;
33 import androidx.leanback.widget.DetailsOverviewRowPresenter;
34 import androidx.leanback.widget.OnActionClickedListener;
35 import androidx.leanback.widget.PresenterSelector;
36 import androidx.leanback.widget.SparseArrayObjectAdapter;
37 import androidx.leanback.widget.VerticalGridView;
38 
39 import com.android.tv.R;
40 import com.android.tv.TvSingletons;
41 import com.android.tv.common.feature.CommonFeatures;
42 import com.android.tv.common.flags.DvrFlags;
43 import com.android.tv.data.ProgramImpl;
44 import com.android.tv.data.api.Channel;
45 import com.android.tv.dvr.DvrDataManager;
46 import com.android.tv.dvr.DvrManager;
47 import com.android.tv.dvr.DvrScheduleManager;
48 import com.android.tv.dvr.data.ScheduledRecording;
49 import com.android.tv.dvr.ui.DvrUiHelper;
50 import com.android.tv.dvr.ui.browse.ActionPresenterSelector;
51 import com.android.tv.dvr.ui.browse.DetailsContent;
52 import com.android.tv.dvr.ui.browse.DetailsContentPresenter;
53 import com.android.tv.dvr.ui.browse.DetailsViewBackgroundHelper;
54 import com.android.tv.util.images.ImageLoader;
55 
56 import javax.inject.Inject;
57 import dagger.android.AndroidInjection;
58 
59 /** A fragment shows the details of a Program */
60 public class ProgramDetailsFragment extends DetailsFragment
61         implements DvrDataManager.ScheduledRecordingListener,
62                 DvrScheduleManager.OnConflictStateChangeListener {
63     private static final int LOAD_LOGO_IMAGE = 1;
64     private static final int LOAD_BACKGROUND_IMAGE = 2;
65 
66     private static final int ACTION_VIEW_SCHEDULE = 1;
67     private static final int ACTION_CANCEL = 2;
68     private static final int ACTION_SCHEDULE_RECORDING = 3;
69 
70     protected DetailsViewBackgroundHelper mBackgroundHelper;
71     private ArrayObjectAdapter mRowsAdapter;
72     private DetailsOverviewRow mDetailsOverview;
73     private ProgramImpl mProgram;
74     private String mInputId;
75     private ScheduledRecording mScheduledRecording;
76     private DvrManager mDvrManager;
77     private DvrDataManager mDvrDataManager;
78     private DvrScheduleManager mDvrScheduleManager;
79     @Inject DvrFlags mDvrFlags;
80 
81     @Override
onCreate(Bundle savedInstanceState)82     public void onCreate(Bundle savedInstanceState) {
83         super.onCreate(savedInstanceState);
84         if (!onLoadDetails(getArguments())) {
85             getActivity().finish();
86         }
87     }
88 
89     @Override
onAttach(Context context)90     public void onAttach(Context context) {
91         AndroidInjection.inject(this);
92         super.onAttach(context);
93     }
94 
95     @Override
onDestroy()96     public void onDestroy() {
97         mDvrDataManager.removeScheduledRecordingListener(this);
98         mDvrScheduleManager.removeOnConflictStateChangeListener(this);
99         super.onDestroy();
100     }
101 
102     @Override
onStart()103     public void onStart() {
104         super.onStart();
105         VerticalGridView container =
106                 (VerticalGridView) getActivity().findViewById(R.id.container_list);
107         // Need to manually modify offset. Please refer DetailsFragment.setVerticalGridViewLayout.
108         container.setItemAlignmentOffset(0);
109         container.setWindowAlignmentOffset(
110                 getResources().getDimensionPixelSize(R.dimen.lb_details_rows_align_top));
111     }
112 
setupAdapter()113     private void setupAdapter() {
114         DetailsOverviewRowPresenter rowPresenter =
115                 new DetailsOverviewRowPresenter(new DetailsContentPresenter(getActivity()));
116         rowPresenter.setBackgroundColor(
117                 getResources().getColor(R.color.common_tv_background, null));
118         rowPresenter.setSharedElementEnterTransition(
119                 getActivity(), DetailsActivity.SHARED_ELEMENT_NAME);
120         rowPresenter.setOnActionClickedListener(onCreateOnActionClickedListener());
121         mRowsAdapter = new ArrayObjectAdapter(onCreatePresenterSelector(rowPresenter));
122         setAdapter(mRowsAdapter);
123     }
124 
125     /** Sets details overview. */
setDetailsOverviewRow(DetailsContent detailsContent)126     protected void setDetailsOverviewRow(DetailsContent detailsContent) {
127         mDetailsOverview = new DetailsOverviewRow(detailsContent);
128         mDetailsOverview.setActionsAdapter(onCreateActionsAdapter());
129         mRowsAdapter.add(mDetailsOverview);
130         onLoadLogoAndBackgroundImages(detailsContent);
131     }
132 
133     /** Creates and returns presenter selector will be used by rows adaptor. */
onCreatePresenterSelector( DetailsOverviewRowPresenter rowPresenter)134     protected PresenterSelector onCreatePresenterSelector(
135             DetailsOverviewRowPresenter rowPresenter) {
136         ClassPresenterSelector presenterSelector = new ClassPresenterSelector();
137         presenterSelector.addClassPresenter(DetailsOverviewRow.class, rowPresenter);
138         return presenterSelector;
139     }
140 
141     /** Updates actions of details overview. */
updateActions()142     protected void updateActions() {
143         mDetailsOverview.setActionsAdapter(onCreateActionsAdapter());
144     }
145 
146     /**
147      * Loads program details according to the arguments the fragment got.
148      *
149      * @return false if cannot find valid programs, else return true. If the return value is false,
150      *     the detail activity and fragment will be ended.
151      */
onLoadDetails(Bundle args)152     private boolean onLoadDetails(Bundle args) {
153         ProgramImpl program = args.getParcelable(DetailsActivity.PROGRAM);
154         long channelId = args.getLong(DetailsActivity.CHANNEL_ID);
155         String inputId = args.getString(DetailsActivity.INPUT_ID);
156         if (program != null && channelId != Channel.INVALID_ID && !TextUtils.isEmpty(inputId)) {
157             mProgram = program;
158             mInputId = inputId;
159             TvSingletons singletons = TvSingletons.getSingletons(getContext());
160             mDvrDataManager = singletons.getDvrDataManager();
161             mDvrManager = singletons.getDvrManager();
162             mDvrScheduleManager = singletons.getDvrScheduleManager();
163             mScheduledRecording =
164                     mDvrDataManager.getScheduledRecordingForProgramId(program.getId());
165             mBackgroundHelper = new DetailsViewBackgroundHelper(getActivity());
166             setupAdapter();
167             setDetailsOverviewRow(DetailsContent.createFromProgram(getContext(), mProgram));
168             mDvrDataManager.addScheduledRecordingListener(this);
169             mDvrScheduleManager.addOnConflictStateChangeListener(this);
170             return true;
171         }
172         return false;
173     }
174 
getScheduleIconId()175     private int getScheduleIconId() {
176         if (mDvrManager.isConflicting(mScheduledRecording)) {
177             return R.drawable.ic_warning_white_32dp;
178         } else {
179             return R.drawable.ic_schedule_32dp;
180         }
181     }
182 
183     /** Creates actions users can interact with and their adaptor for this fragment. */
onCreateActionsAdapter()184     private SparseArrayObjectAdapter onCreateActionsAdapter() {
185         SparseArrayObjectAdapter adapter =
186                 new SparseArrayObjectAdapter(new ActionPresenterSelector());
187         Resources res = getResources();
188         if (mScheduledRecording != null) {
189             adapter.set(
190                     ACTION_VIEW_SCHEDULE,
191                     new Action(
192                             ACTION_VIEW_SCHEDULE,
193                             res.getString(R.string.dvr_detail_view_schedule),
194                             null,
195                             res.getDrawable(getScheduleIconId())));
196             adapter.set(
197                     ACTION_CANCEL,
198                     new Action(
199                             ACTION_CANCEL,
200                             res.getString(R.string.dvr_detail_cancel_recording),
201                             null,
202                             res.getDrawable(R.drawable.ic_dvr_cancel_32dp)));
203         } else if (CommonFeatures.DVR.isEnabled(getActivity())
204                 && mDvrManager.isProgramRecordable(mProgram)) {
205             adapter.set(
206                     ACTION_SCHEDULE_RECORDING,
207                     new Action(
208                             ACTION_SCHEDULE_RECORDING,
209                             res.getString(R.string.dvr_detail_schedule_recording),
210                             null,
211                             res.getDrawable(R.drawable.ic_schedule_32dp)));
212         }
213         return adapter;
214     }
215 
216     /**
217      * Creates actions listeners to implement the behavior of the fragment after users click some
218      * action buttons.
219      */
onCreateOnActionClickedListener()220     private OnActionClickedListener onCreateOnActionClickedListener() {
221         return new OnActionClickedListener() {
222             @Override
223             public void onActionClicked(Action action) {
224                 long actionId = action.getId();
225                 if (actionId == ACTION_VIEW_SCHEDULE) {
226                     DvrUiHelper.startSchedulesActivity(getContext(), mScheduledRecording);
227                 } else if (actionId == ACTION_CANCEL) {
228                     mDvrManager.removeScheduledRecording(mScheduledRecording);
229                 } else if (actionId == ACTION_SCHEDULE_RECORDING) {
230                     if (!mProgram.isEpisodic() && mDvrFlags.startEarlyEndLateEnabled()) {
231                         DvrUiHelper.startRecordingSettingsActivity(getContext(), mProgram);
232                     } else {
233                         DvrUiHelper.checkStorageStatusAndShowErrorMessage(
234                                 getActivity(),
235                                 mInputId,
236                                 () ->
237                                         DvrUiHelper.requestRecordingFutureProgram(
238                                                 getActivity(), mProgram, false));
239                     }
240                 }
241             }
242         };
243     }
244 
245     /** Loads logo and background images for detail fragments. */
246     protected void onLoadLogoAndBackgroundImages(DetailsContent detailsContent) {
247         Drawable logoDrawable = null;
248         Drawable backgroundDrawable = null;
249         if (TextUtils.isEmpty(detailsContent.getLogoImageUri())) {
250             logoDrawable =
251                     getContext().getResources().getDrawable(R.drawable.dvr_default_poster, null);
252             mDetailsOverview.setImageDrawable(logoDrawable);
253         }
254         if (TextUtils.isEmpty(detailsContent.getBackgroundImageUri())) {
255             backgroundDrawable =
256                     getContext().getResources().getDrawable(R.drawable.dvr_default_poster, null);
257             mBackgroundHelper.setBackground(backgroundDrawable);
258         }
259         if (logoDrawable != null && backgroundDrawable != null) {
260             return;
261         }
262         if (logoDrawable == null
263                 && backgroundDrawable == null
264                 && detailsContent
265                         .getLogoImageUri()
266                         .equals(detailsContent.getBackgroundImageUri())) {
267             ImageLoader.loadBitmap(
268                     getContext(),
269                     detailsContent.getLogoImageUri(),
270                     new MyImageLoaderCallback(
271                             this, LOAD_LOGO_IMAGE | LOAD_BACKGROUND_IMAGE, getContext()));
272             return;
273         }
274         if (logoDrawable == null) {
275             int imageWidth = getResources().getDimensionPixelSize(R.dimen.dvr_details_poster_width);
276             int imageHeight =
277                     getResources().getDimensionPixelSize(R.dimen.dvr_details_poster_height);
278             ImageLoader.loadBitmap(
279                     getContext(),
280                     detailsContent.getLogoImageUri(),
281                     imageWidth,
282                     imageHeight,
283                     new MyImageLoaderCallback(this, LOAD_LOGO_IMAGE, getContext()));
284         }
285         if (backgroundDrawable == null) {
286             ImageLoader.loadBitmap(
287                     getContext(),
288                     detailsContent.getBackgroundImageUri(),
289                     new MyImageLoaderCallback(this, LOAD_BACKGROUND_IMAGE, getContext()));
290         }
291     }
292 
293     @Override
294     public void onScheduledRecordingAdded(ScheduledRecording... scheduledRecordings) {
295         for (ScheduledRecording recording : scheduledRecordings) {
296             if (recording.getProgramId() == mProgram.getId()) {
297                 mScheduledRecording = recording;
298                 updateActions();
299                 return;
300             }
301         }
302     }
303 
304     @Override
305     public void onScheduledRecordingRemoved(ScheduledRecording... scheduledRecordings) {
306         if (mScheduledRecording == null) {
307             return;
308         }
309         for (ScheduledRecording recording : scheduledRecordings) {
310             if (recording.getId() == mScheduledRecording.getId()) {
311                 mScheduledRecording = null;
312                 updateActions();
313                 return;
314             }
315         }
316     }
317 
318     @Override
319     public void onScheduledRecordingStatusChanged(ScheduledRecording... scheduledRecordings) {
320         if (mScheduledRecording == null) {
321             return;
322         }
323         for (ScheduledRecording recording : scheduledRecordings) {
324             if (recording.getId() == mScheduledRecording.getId()) {
325                 mScheduledRecording = recording;
326                 updateActions();
327                 return;
328             }
329         }
330     }
331 
332     @Override
333     public void onConflictStateChange(boolean conflict, ScheduledRecording... scheduledRecordings) {
334         onScheduledRecordingStatusChanged(scheduledRecordings);
335     }
336 
337     private static class MyImageLoaderCallback
338             extends ImageLoader.ImageLoaderCallback<ProgramDetailsFragment> {
339         private final Context mContext;
340         private final int mLoadType;
341 
342         public MyImageLoaderCallback(
343                 ProgramDetailsFragment fragment, int loadType, Context context) {
344             super(fragment);
345             mLoadType = loadType;
346             mContext = context;
347         }
348 
349         @Override
350         public void onBitmapLoaded(ProgramDetailsFragment fragment, @Nullable Bitmap bitmap) {
351             Drawable drawable;
352             int loadType = mLoadType;
353             if (bitmap == null) {
354                 Resources res = mContext.getResources();
355                 drawable = res.getDrawable(R.drawable.dvr_default_poster, null);
356                 if ((loadType & LOAD_BACKGROUND_IMAGE) != 0 && !fragment.isDetached()) {
357                     loadType &= ~LOAD_BACKGROUND_IMAGE;
358                     fragment.mBackgroundHelper.setBackgroundColor(
359                             res.getColor(R.color.dvr_detail_default_background));
360                     fragment.mBackgroundHelper.setScrim(
361                             res.getColor(R.color.dvr_detail_default_background_scrim));
362                 }
363             } else {
364                 drawable = new BitmapDrawable(mContext.getResources(), bitmap);
365             }
366             if (!fragment.isDetached()) {
367                 if ((loadType & LOAD_LOGO_IMAGE) != 0) {
368                     fragment.mDetailsOverview.setImageDrawable(drawable);
369                 }
370                 if ((loadType & LOAD_BACKGROUND_IMAGE) != 0) {
371                     fragment.mBackgroundHelper.setBackground(drawable);
372                 }
373             }
374         }
375     }
376 }
377