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