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 androidx.leanback.app.GuidedStepFragment;
23 import androidx.leanback.widget.GuidanceStylist.Guidance;
24 import androidx.leanback.widget.GuidedAction;
25 import androidx.leanback.widget.GuidedActionsStylist;
26 import android.text.TextUtils;
27 import android.view.ViewGroup.LayoutParams;
28 import android.widget.Toast;
29 import com.android.tv.R;
30 import com.android.tv.TvSingletons;
31 import com.android.tv.common.SoftPreconditions;
32 import com.android.tv.common.util.PermissionUtils;
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.data.RecordedProgram;
37 import com.android.tv.dvr.data.SeriesRecording;
38 import com.android.tv.ui.GuidedActionsStylistWithDivider;
39 import com.android.tv.util.Utils;
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 /** Fragment for DVR series recording settings. */
48 public class DvrSeriesDeletionFragment extends GuidedStepFragment {
49     private static final long WATCHED_TIME_UNIT_THRESHOLD = TimeUnit.MINUTES.toMillis(2);
50 
51     // Since recordings' IDs are used as its check actions' IDs, which are random positive numbers,
52     // negative values are used by other actions to prevent duplicated IDs.
53     private static final long ACTION_ID_SELECT_WATCHED = -110;
54     private static final long ACTION_ID_SELECT_ALL = -111;
55     private static final long ACTION_ID_DELETE = -112;
56 
57     private DvrManager mDvrManager;
58     private DvrDataManager mDvrDataManager;
59     private DvrWatchedPositionManager mDvrWatchedPositionManager;
60     private List<RecordedProgram> mRecordings;
61     private final Set<Long> mWatchedRecordings = new HashSet<>();
62     private final List<Long> mIdsToDelete = new ArrayList<>();
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 =
71                 getArguments().getLong(DvrSeriesDeletionActivity.SERIES_RECORDING_ID, -1);
72         SoftPreconditions.checkArgument(mSeriesRecordingId != -1);
73         TvSingletons singletons = TvSingletons.getSingletons(context);
74         mDvrManager = singletons.getDvrManager();
75         mDvrDataManager = singletons.getDvrDataManager();
76         mDvrWatchedPositionManager = singletons.getDvrWatchedPositionManager();
77         mRecordings = mDvrDataManager.getRecordedPrograms(mSeriesRecordingId);
78         mOneLineActionHeight =
79                 getResources()
80                         .getDimensionPixelSize(
81                                 R.dimen.dvr_settings_one_line_action_container_height);
82         if (mRecordings.isEmpty()) {
83             Toast.makeText(
84                             getActivity(),
85                             getString(R.string.dvr_series_deletion_no_recordings),
86                             Toast.LENGTH_LONG)
87                     .show();
88             finishGuidedStepFragments();
89             return;
90         }
91         Collections.sort(mRecordings, RecordedProgram.EPISODE_COMPARATOR);
92     }
93 
94     @Override
onCreateGuidance(Bundle savedInstanceState)95     public Guidance onCreateGuidance(Bundle savedInstanceState) {
96         String breadcrumb = null;
97         SeriesRecording series = mDvrDataManager.getSeriesRecording(mSeriesRecordingId);
98         if (series != null) {
99             breadcrumb = series.getTitle();
100         }
101         return new Guidance(
102                 getString(R.string.dvr_series_deletion_title),
103                 getString(R.string.dvr_series_deletion_description),
104                 breadcrumb,
105                 null);
106     }
107 
108     @Override
onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState)109     public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) {
110         actions.add(
111                 new GuidedAction.Builder(getActivity())
112                         .id(ACTION_ID_SELECT_WATCHED)
113                         .title(getString(R.string.dvr_series_select_watched))
114                         .build());
115         actions.add(
116                 new GuidedAction.Builder(getActivity())
117                         .id(ACTION_ID_SELECT_ALL)
118                         .title(getString(R.string.dvr_series_select_all))
119                         .build());
120         actions.add(GuidedActionsStylistWithDivider.createDividerAction(getContext()));
121         for (RecordedProgram recording : mRecordings) {
122             long watchedPositionMs =
123                     mDvrWatchedPositionManager.getWatchedPosition(recording.getId());
124             String title = recording.getEpisodeDisplayTitle(getContext());
125             if (TextUtils.isEmpty(title)) {
126                 title =
127                         TextUtils.isEmpty(recording.getTitle())
128                                 ? getString(R.string.channel_banner_no_title)
129                                 : recording.getTitle();
130             }
131             String description;
132             if (watchedPositionMs != TvInputManager.TIME_SHIFT_INVALID_TIME) {
133                 description = getWatchedString(watchedPositionMs, recording.getDurationMillis());
134                 mWatchedRecordings.add(recording.getId());
135             } else {
136                 description = getString(R.string.dvr_series_never_watched);
137             }
138             actions.add(
139                     new GuidedAction.Builder(getActivity())
140                             .id(recording.getId())
141                             .title(title)
142                             .description(description)
143                             .checkSetId(GuidedAction.CHECKBOX_CHECK_SET_ID)
144                             .build());
145         }
146     }
147 
148     @Override
onCreateButtonActions(List<GuidedAction> actions, Bundle savedInstanceState)149     public void onCreateButtonActions(List<GuidedAction> actions, Bundle savedInstanceState) {
150         actions.add(
151                 new GuidedAction.Builder(getActivity())
152                         .id(ACTION_ID_DELETE)
153                         .title(getString(R.string.dvr_detail_delete))
154                         .build());
155         actions.add(
156                 new GuidedAction.Builder(getActivity())
157                         .clickAction(GuidedAction.ACTION_ID_CANCEL)
158                         .build());
159     }
160 
161     @Override
onGuidedActionClicked(GuidedAction action)162     public void onGuidedActionClicked(GuidedAction action) {
163         long actionId = action.getId();
164         if (actionId == ACTION_ID_DELETE) {
165             delete();
166         } else if (actionId == GuidedAction.ACTION_ID_CANCEL) {
167             finishGuidedStepFragments();
168         } else if (actionId == ACTION_ID_SELECT_WATCHED) {
169             for (GuidedAction guidedAction : getActions()) {
170                 if (guidedAction.getCheckSetId() == GuidedAction.CHECKBOX_CHECK_SET_ID) {
171                     long recordingId = guidedAction.getId();
172                     if (mWatchedRecordings.contains(recordingId)) {
173                         guidedAction.setChecked(true);
174                     } else {
175                         guidedAction.setChecked(false);
176                     }
177                     notifyActionChanged(findActionPositionById(recordingId));
178                 }
179             }
180             mAllSelected = updateSelectAllState();
181         } else if (actionId == ACTION_ID_SELECT_ALL) {
182             mAllSelected = !mAllSelected;
183             for (GuidedAction guidedAction : getActions()) {
184                 if (guidedAction.getCheckSetId() == GuidedAction.CHECKBOX_CHECK_SET_ID) {
185                     guidedAction.setChecked(mAllSelected);
186                     notifyActionChanged(findActionPositionById(guidedAction.getId()));
187                 }
188             }
189             updateSelectAllState(action, mAllSelected);
190         } else {
191             mAllSelected = updateSelectAllState();
192         }
193     }
194 
195     @Override
onCreateButtonActionsStylist()196     public GuidedActionsStylist onCreateButtonActionsStylist() {
197         return new DvrGuidedActionsStylist(true);
198     }
199 
200     @Override
onCreateActionsStylist()201     public GuidedActionsStylist onCreateActionsStylist() {
202         return new GuidedActionsStylistWithDivider() {
203             @Override
204             public void onBindViewHolder(ViewHolder vh, GuidedAction action) {
205                 super.onBindViewHolder(vh, action);
206                 if (action.getId() == ACTION_DIVIDER) {
207                     return;
208                 }
209                 LayoutParams lp = vh.itemView.getLayoutParams();
210                 if (action.getCheckSetId() != GuidedAction.CHECKBOX_CHECK_SET_ID) {
211                     lp.height = mOneLineActionHeight;
212                 } else {
213                     vh.itemView.setLayoutParams(
214                             new LayoutParams(lp.width, LayoutParams.WRAP_CONTENT));
215                 }
216             }
217         };
218     }
219 
220     private void delete() {
221         mIdsToDelete.clear();
222         for (GuidedAction guidedAction : getActions()) {
223             if (guidedAction.getCheckSetId() == GuidedAction.CHECKBOX_CHECK_SET_ID
224                     && guidedAction.isChecked()) {
225                 mIdsToDelete.add(guidedAction.getId());
226             }
227         }
228         ((DvrSeriesDeletionActivity) getActivity()).setIdsToDelete(mIdsToDelete);
229         if (!PermissionUtils.hasWriteExternalStorage(getContext())
230                 && doesAnySelectedRecordedProgramNeedWritePermission()) {
231             DvrUiHelper.showWriteStoragePermissionRationaleDialog(getActivity());
232         } else {
233             deleteSelectedIds();
234         }
235     }
236 
237     private boolean doesAnySelectedRecordedProgramNeedWritePermission() {
238         for (RecordedProgram r : mRecordings) {
239             if (mIdsToDelete.contains(r.getId())
240                     && DvrManager.isFile(r.getDataUri())
241                     && !DvrManager.isFromBundledInput(r)) {
242                 return true;
243             }
244         }
245         return false;
246     }
247 
248     private void deleteSelectedIds() {
249         if (!mIdsToDelete.isEmpty()) {
250             mDvrManager.removeRecordedPrograms(mIdsToDelete, true);
251         }
252         Toast.makeText(
253                         getContext(),
254                         getResources()
255                                 .getQuantityString(
256                                         R.plurals.dvr_msg_episodes_deleted,
257                                         mIdsToDelete.size(),
258                                         mIdsToDelete.size(),
259                                         mRecordings.size()),
260                         Toast.LENGTH_LONG)
261                 .show();
262         finishGuidedStepFragments();
263     }
264 
265     private String getWatchedString(long watchedPositionMs, long durationMs) {
266         if (durationMs > WATCHED_TIME_UNIT_THRESHOLD) {
267             return getResources()
268                     .getString(
269                             R.string.dvr_series_watched_info_minutes,
270                             Math.max(1, Utils.getRoundOffMinsFromMs(watchedPositionMs)),
271                             Utils.getRoundOffMinsFromMs(durationMs));
272         } else {
273             return getResources()
274                     .getString(
275                             R.string.dvr_series_watched_info_seconds,
276                             Math.max(1, TimeUnit.MILLISECONDS.toSeconds(watchedPositionMs)),
277                             TimeUnit.MILLISECONDS.toSeconds(durationMs));
278         }
279     }
280 
281     private boolean updateSelectAllState() {
282         for (GuidedAction guidedAction : getActions()) {
283             if (guidedAction.getCheckSetId() == GuidedAction.CHECKBOX_CHECK_SET_ID) {
284                 if (!guidedAction.isChecked()) {
285                     if (mAllSelected) {
286                         updateSelectAllState(findActionById(ACTION_ID_SELECT_ALL), false);
287                     }
288                     return false;
289                 }
290             }
291         }
292         if (!mAllSelected) {
293             updateSelectAllState(findActionById(ACTION_ID_SELECT_ALL), true);
294         }
295         return true;
296     }
297 
298     private void updateSelectAllState(GuidedAction selectAll, boolean select) {
299         selectAll.setTitle(
300                 select
301                         ? getString(R.string.dvr_series_deselect_all)
302                         : getString(R.string.dvr_series_select_all));
303         notifyActionChanged(findActionPositionById(ACTION_ID_SELECT_ALL));
304     }
305 }
306