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