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.media.tv.TvInputManager;
21 import android.os.Bundle;
22 import android.support.v17.leanback.app.GuidedStepFragment;
23 import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
24 import android.support.v17.leanback.widget.GuidedAction;
25 import android.support.v17.leanback.widget.GuidedActionsStylist;
26 import android.text.TextUtils;
27 import android.view.ViewGroup.LayoutParams;
28 import android.widget.Toast;
29 
30 import com.android.tv.R;
31 import com.android.tv.TvApplication;
32 import com.android.tv.common.SoftPreconditions;
33 import com.android.tv.dvr.DvrDataManager;
34 import com.android.tv.dvr.DvrManager;
35 import com.android.tv.dvr.DvrWatchedPositionManager;
36 import com.android.tv.dvr.RecordedProgram;
37 import com.android.tv.dvr.SeriesRecording;
38 import com.android.tv.ui.GuidedActionsStylistWithDivider;
39 
40 import java.util.ArrayList;
41 import java.util.Collections;
42 import java.util.HashSet;
43 import java.util.List;
44 import java.util.Set;
45 import java.util.concurrent.TimeUnit;
46 
47 /**
48  * Fragment for DVR series recording settings.
49  */
50 public class SeriesDeletionFragment extends GuidedStepFragment {
51     private static final long WATCHED_TIME_UNIT_THRESHOLD = TimeUnit.MINUTES.toMillis(2);
52 
53     // Since recordings' IDs are used as its check actions' IDs, which are random positive numbers,
54     // negative values are used by other actions to prevent duplicated IDs.
55     private static final long ACTION_ID_SELECT_WATCHED = -110;
56     private static final long ACTION_ID_SELECT_ALL = -111;
57     private static final long ACTION_ID_DELETE = -112;
58 
59     private DvrDataManager mDvrDataManager;
60     private DvrWatchedPositionManager mDvrWatchedPositionManager;
61     private List<RecordedProgram> mRecordings;
62     private final Set<Long> mWatchedRecordings = new HashSet<>();
63     private boolean mAllSelected;
64     private long mSeriesRecordingId;
65     private int mOneLineActionHeight;
66 
67     @Override
onAttach(Context context)68     public void onAttach(Context context) {
69         super.onAttach(context);
70         mSeriesRecordingId = getArguments()
71                 .getLong(DvrSeriesDeletionActivity.SERIES_RECORDING_ID, -1);
72         SoftPreconditions.checkArgument(mSeriesRecordingId != -1);
73         mDvrDataManager = TvApplication.getSingletons(context).getDvrDataManager();
74         mDvrWatchedPositionManager =
75                 TvApplication.getSingletons(context).getDvrWatchedPositionManager();
76         mRecordings = mDvrDataManager.getRecordedPrograms(mSeriesRecordingId);
77         mOneLineActionHeight = getResources().getDimensionPixelSize(
78                 R.dimen.dvr_settings_one_line_action_container_height);
79         if (mRecordings.isEmpty()) {
80             Toast.makeText(getActivity(), getString(R.string.dvr_series_deletion_no_recordings),
81                     Toast.LENGTH_LONG).show();
82             finishGuidedStepFragments();
83             return;
84         }
85         Collections.sort(mRecordings, RecordedProgram.EPISODE_COMPARATOR);
86     }
87 
88     @Override
onCreateGuidance(Bundle savedInstanceState)89     public Guidance onCreateGuidance(Bundle savedInstanceState) {
90         String breadcrumb = null;
91         SeriesRecording series = mDvrDataManager.getSeriesRecording(mSeriesRecordingId);
92         if (series != null) {
93             breadcrumb = series.getTitle();
94         }
95         return new Guidance(getString(R.string.dvr_series_deletion_title),
96                 getString(R.string.dvr_series_deletion_description), breadcrumb, null);
97     }
98 
99     @Override
onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState)100     public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) {
101         actions.add(new GuidedAction.Builder(getActivity())
102                 .id(ACTION_ID_SELECT_WATCHED)
103                 .title(getString(R.string.dvr_series_select_watched))
104                 .build());
105         actions.add(new GuidedAction.Builder(getActivity())
106                 .id(ACTION_ID_SELECT_ALL)
107                 .title(getString(R.string.dvr_series_select_all))
108                 .build());
109         actions.add(GuidedActionsStylistWithDivider.createDividerAction(getContext()));
110         for (RecordedProgram recording : mRecordings) {
111             long watchedPositionMs =
112                     mDvrWatchedPositionManager.getWatchedPosition(recording.getId());
113             String title = recording.getEpisodeDisplayTitle(getContext());
114             if (TextUtils.isEmpty(title)) {
115                 title = TextUtils.isEmpty(recording.getTitle()) ?
116                         getString(R.string.channel_banner_no_title) : recording.getTitle();
117             }
118             String description;
119             if (watchedPositionMs != TvInputManager.TIME_SHIFT_INVALID_TIME) {
120                 description = getWatchedString(watchedPositionMs, recording.getDurationMillis());
121                 mWatchedRecordings.add(recording.getId());
122             } else {
123                 description = getString(R.string.dvr_series_never_watched);
124             }
125             actions.add(new GuidedAction.Builder(getActivity())
126                     .id(recording.getId())
127                     .title(title)
128                     .description(description)
129                     .checkSetId(GuidedAction.CHECKBOX_CHECK_SET_ID)
130                     .build());
131         }
132     }
133 
134     @Override
onCreateButtonActions(List<GuidedAction> actions, Bundle savedInstanceState)135     public void onCreateButtonActions(List<GuidedAction> actions, Bundle savedInstanceState) {
136         actions.add(new GuidedAction.Builder(getActivity())
137                 .id(ACTION_ID_DELETE)
138                 .title(getString(R.string.dvr_detail_delete))
139                 .build());
140         actions.add(new GuidedAction.Builder(getActivity())
141                 .clickAction(GuidedAction.ACTION_ID_CANCEL)
142                 .build());
143     }
144 
145     @Override
onGuidedActionClicked(GuidedAction action)146     public void onGuidedActionClicked(GuidedAction action) {
147         long actionId = action.getId();
148         if (actionId == ACTION_ID_DELETE) {
149             List<Long> idsToDelete = new ArrayList<>();
150             for (GuidedAction guidedAction : getActions()) {
151                 if (guidedAction.getCheckSetId() == GuidedAction.CHECKBOX_CHECK_SET_ID
152                         && guidedAction.isChecked()) {
153                     idsToDelete.add(guidedAction.getId());
154                 }
155             }
156             if (!idsToDelete.isEmpty()) {
157                 DvrManager dvrManager = TvApplication.getSingletons(getActivity()).getDvrManager();
158                 dvrManager.removeRecordedPrograms(idsToDelete);
159             }
160             Toast.makeText(getContext(), getResources().getQuantityString(
161                     R.plurals.dvr_msg_episodes_deleted, idsToDelete.size(), idsToDelete.size(),
162                     mRecordings.size()), Toast.LENGTH_LONG).show();
163             finishGuidedStepFragments();
164         } else if (actionId == GuidedAction.ACTION_ID_CANCEL) {
165             finishGuidedStepFragments();
166         } else if (actionId == ACTION_ID_SELECT_WATCHED) {
167             for (GuidedAction guidedAction : getActions()) {
168                 if (guidedAction.getCheckSetId() == GuidedAction.CHECKBOX_CHECK_SET_ID) {
169                     long recordingId = guidedAction.getId();
170                     if (mWatchedRecordings.contains(recordingId)) {
171                         guidedAction.setChecked(true);
172                     } else {
173                         guidedAction.setChecked(false);
174                     }
175                     notifyActionChanged(findActionPositionById(recordingId));
176                 }
177             }
178             mAllSelected = updateSelectAllState();
179         } else if (actionId == ACTION_ID_SELECT_ALL) {
180             mAllSelected = !mAllSelected;
181             for (GuidedAction guidedAction : getActions()) {
182                 if (guidedAction.getCheckSetId() == GuidedAction.CHECKBOX_CHECK_SET_ID) {
183                     guidedAction.setChecked(mAllSelected);
184                     notifyActionChanged(findActionPositionById(guidedAction.getId()));
185                 }
186             }
187             updateSelectAllState(action, mAllSelected);
188         } else {
189             mAllSelected = updateSelectAllState();
190         }
191     }
192 
193     @Override
onCreateButtonActionsStylist()194     public GuidedActionsStylist onCreateButtonActionsStylist() {
195         return new DvrGuidedActionsStylist(true);
196     }
197 
198     @Override
onCreateActionsStylist()199     public GuidedActionsStylist onCreateActionsStylist() {
200         return new GuidedActionsStylistWithDivider() {
201             @Override
202             public void onBindViewHolder(ViewHolder vh, GuidedAction action) {
203                 super.onBindViewHolder(vh, action);
204                 if (action.getId() == ACTION_DIVIDER) {
205                     return;
206                 }
207                 LayoutParams lp = vh.itemView.getLayoutParams();
208                 if (action.getCheckSetId() != GuidedAction.CHECKBOX_CHECK_SET_ID) {
209                     lp.height = mOneLineActionHeight;
210                 } else {
211                     vh.itemView.setLayoutParams(
212                             new LayoutParams(lp.width, LayoutParams.WRAP_CONTENT));
213                 }
214             }
215         };
216     }
217 
218     private String getWatchedString(long watchedPositionMs, long durationMs) {
219         if (durationMs > WATCHED_TIME_UNIT_THRESHOLD) {
220             return getResources().getString(R.string.dvr_series_watched_info_minutes,
221                     Math.max(1, TimeUnit.MILLISECONDS.toMinutes(watchedPositionMs)),
222                     TimeUnit.MILLISECONDS.toMinutes(durationMs));
223         } else {
224             return getResources().getString(R.string.dvr_series_watched_info_seconds,
225                     Math.max(1, TimeUnit.MILLISECONDS.toSeconds(watchedPositionMs)),
226                     TimeUnit.MILLISECONDS.toSeconds(durationMs));
227         }
228     }
229 
230     private boolean updateSelectAllState() {
231         for (GuidedAction guidedAction : getActions()) {
232             if (guidedAction.getCheckSetId() == GuidedAction.CHECKBOX_CHECK_SET_ID) {
233                 if (!guidedAction.isChecked()) {
234                     if (mAllSelected) {
235                         updateSelectAllState(findActionById(ACTION_ID_SELECT_ALL), false);
236                     }
237                     return false;
238                 }
239             }
240         }
241         if (!mAllSelected) {
242             updateSelectAllState(findActionById(ACTION_ID_SELECT_ALL), true);
243         }
244         return true;
245     }
246 
247     private void updateSelectAllState(GuidedAction selectAll, boolean select) {
248         selectAll.setTitle(select ? getString(R.string.dvr_series_deselect_all)
249                 : getString(R.string.dvr_series_select_all));
250         notifyActionChanged(findActionPositionById(ACTION_ID_SELECT_ALL));
251     }
252 }
253