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.graphics.drawable.Drawable; 20 import android.media.tv.TvInputInfo; 21 import android.os.Bundle; 22 import android.support.annotation.NonNull; 23 import android.support.annotation.Nullable; 24 import android.util.Log; 25 import android.view.LayoutInflater; 26 import android.view.View; 27 import android.view.ViewGroup; 28 29 import androidx.leanback.widget.GuidanceStylist.Guidance; 30 import androidx.leanback.widget.GuidedAction; 31 32 import com.android.tv.MainActivity; 33 import com.android.tv.R; 34 import com.android.tv.TvSingletons; 35 import com.android.tv.common.SoftPreconditions; 36 import com.android.tv.data.api.Channel; 37 import com.android.tv.data.api.Program; 38 import com.android.tv.dvr.data.ScheduledRecording; 39 import com.android.tv.dvr.recorder.ConflictChecker; 40 import com.android.tv.dvr.recorder.ConflictChecker.OnUpcomingConflictChangeListener; 41 import com.android.tv.util.Utils; 42 43 import java.util.ArrayList; 44 import java.util.Collections; 45 import java.util.HashSet; 46 import java.util.List; 47 48 public abstract class DvrConflictFragment extends DvrGuidedStepFragment { 49 private static final String TAG = "DvrConflictFragment"; 50 private static final boolean DEBUG = false; 51 52 private static final int ACTION_DELETE_CONFLICT = 1; 53 private static final int ACTION_CANCEL = 2; 54 private static final int ACTION_VIEW_SCHEDULES = 3; 55 56 // The program count which will be listed in the description. This is the number of the 57 // program strings in R.plurals.dvr_program_conflict_dialog_description_many. 58 private static final int LISTED_PROGRAM_COUNT = 2; 59 60 protected List<ScheduledRecording> mConflicts; 61 setConflicts(List<ScheduledRecording> conflicts)62 void setConflicts(List<ScheduledRecording> conflicts) { 63 mConflicts = conflicts; 64 } 65 getConflicts()66 List<ScheduledRecording> getConflicts() { 67 return mConflicts; 68 } 69 70 @Override onProvideTheme()71 public int onProvideTheme() { 72 return R.style.Theme_TV_Dvr_Conflict_GuidedStep; 73 } 74 75 @Override onCreateActions(@onNull List<GuidedAction> actions, Bundle savedInstanceState)76 public void onCreateActions(@NonNull List<GuidedAction> actions, Bundle savedInstanceState) { 77 actions.add( 78 new GuidedAction.Builder(getContext()) 79 .clickAction(GuidedAction.ACTION_ID_OK) 80 .build()); 81 actions.add( 82 new GuidedAction.Builder(getContext()) 83 .id(ACTION_VIEW_SCHEDULES) 84 .title(R.string.dvr_action_view_schedules) 85 .build()); 86 } 87 88 @Override onTrackedGuidedActionClicked(GuidedAction action)89 public void onTrackedGuidedActionClicked(GuidedAction action) { 90 if (action.getId() == ACTION_VIEW_SCHEDULES) { 91 DvrUiHelper.startSchedulesActivityForOneTimeRecordingConflict( 92 getContext(), getConflicts()); 93 } 94 dismissDialog(); 95 // Finish the Recording setting Activity on dismissal. 96 if (getActivity() instanceof DvrRecordingSettingsActivity) { 97 getActivity().finish(); 98 } 99 } 100 101 @Override getTrackerLabelForGuidedAction(GuidedAction action)102 public String getTrackerLabelForGuidedAction(GuidedAction action) { 103 long actionId = getId(); 104 if (actionId == ACTION_VIEW_SCHEDULES) { 105 return "view-schedules"; 106 } else { 107 return super.getTrackerLabelForGuidedAction(action); 108 } 109 } 110 getConflictDescription()111 String getConflictDescription() { 112 List<String> titles = new ArrayList<>(); 113 HashSet<String> titleSet = new HashSet<>(); 114 for (ScheduledRecording schedule : getConflicts()) { 115 String scheduleTitle = getScheduleTitle(schedule); 116 if (scheduleTitle != null && !titleSet.contains(scheduleTitle)) { 117 titles.add(scheduleTitle); 118 titleSet.add(scheduleTitle); 119 } 120 } 121 switch (titles.size()) { 122 case 0: 123 Log.i( 124 TAG, 125 "Conflict has been resolved by any reason. Maybe input might have" 126 + " been deleted."); 127 return null; 128 case 1: 129 return getResources() 130 .getString( 131 R.string.dvr_program_conflict_dialog_description_1, titles.get(0)); 132 case 2: 133 return getResources() 134 .getString( 135 R.string.dvr_program_conflict_dialog_description_2, 136 titles.get(0), 137 titles.get(1)); 138 case 3: 139 return getResources() 140 .getString( 141 R.string.dvr_program_conflict_dialog_description_3, 142 titles.get(0), 143 titles.get(1)); 144 default: 145 return getResources() 146 .getQuantityString( 147 R.plurals.dvr_program_conflict_dialog_description_many, 148 titles.size() - LISTED_PROGRAM_COUNT, 149 titles.get(0), 150 titles.get(1), 151 titles.size() - LISTED_PROGRAM_COUNT); 152 } 153 } 154 155 @Nullable getScheduleTitle(ScheduledRecording schedule)156 private String getScheduleTitle(ScheduledRecording schedule) { 157 if (schedule.getType() == ScheduledRecording.TYPE_TIMED) { 158 Channel channel = 159 TvSingletons.getSingletons(getContext()) 160 .getChannelDataManager() 161 .getChannel(schedule.getChannelId()); 162 if (channel != null) { 163 return channel.getDisplayName(); 164 } else { 165 return null; 166 } 167 } else { 168 return schedule.getProgramTitle(); 169 } 170 } 171 172 /** A fragment to show the program conflict. */ 173 public static class DvrProgramConflictFragment extends DvrConflictFragment { 174 private Program mProgram; 175 176 @Override onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)177 public View onCreateView( 178 LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 179 Bundle args = getArguments(); 180 if (args != null) { 181 mProgram = args.getParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM); 182 } 183 SoftPreconditions.checkArgument(mProgram != null); 184 TvInputInfo input = Utils.getTvInputInfoForProgram(getContext(), mProgram); 185 SoftPreconditions.checkNotNull(input); 186 List<ScheduledRecording> conflicts = null; 187 if (input != null) { 188 conflicts = 189 TvSingletons.getSingletons(getContext()) 190 .getDvrManager() 191 .getConflictingSchedules(mProgram); 192 } 193 if (conflicts == null) { 194 conflicts = Collections.emptyList(); 195 } 196 if (conflicts.isEmpty()) { 197 dismissDialog(); 198 } 199 setConflicts(conflicts); 200 return super.onCreateView(inflater, container, savedInstanceState); 201 } 202 203 @NonNull 204 @Override onCreateGuidance(Bundle savedInstanceState)205 public Guidance onCreateGuidance(Bundle savedInstanceState) { 206 String title = getResources().getString(R.string.dvr_program_conflict_dialog_title); 207 String descriptionPrefix = 208 getString( 209 R.string.dvr_program_conflict_dialog_description_prefix, 210 mProgram.getTitle()); 211 String description = getConflictDescription(); 212 if (description == null) { 213 dismissDialog(); 214 } 215 Drawable icon = getResources().getDrawable(R.drawable.quantum_ic_error_white_48, null); 216 return new Guidance(title, descriptionPrefix + " " + description, null, icon); 217 } 218 219 @Override getTrackerPrefix()220 public String getTrackerPrefix() { 221 return "DvrProgramConflictFragment"; 222 } 223 } 224 225 /** A fragment to show the channel recording conflict. */ 226 public static class DvrChannelRecordConflictFragment extends DvrConflictFragment { 227 private Channel mChannel; 228 private long mStartTimeMs; 229 private long mEndTimeMs; 230 231 @Override onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)232 public View onCreateView( 233 LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 234 Bundle args = getArguments(); 235 long channelId = args.getLong(DvrHalfSizedDialogFragment.KEY_CHANNEL_ID); 236 mChannel = 237 TvSingletons.getSingletons(getContext()) 238 .getChannelDataManager() 239 .getChannel(channelId); 240 SoftPreconditions.checkArgument(mChannel != null); 241 TvInputInfo input = Utils.getTvInputInfoForChannelId(getContext(), mChannel.getId()); 242 SoftPreconditions.checkNotNull(input); 243 List<ScheduledRecording> conflicts = null; 244 if (input != null) { 245 mStartTimeMs = args.getLong(DvrHalfSizedDialogFragment.KEY_START_TIME_MS); 246 mEndTimeMs = args.getLong(DvrHalfSizedDialogFragment.KEY_END_TIME_MS); 247 conflicts = 248 TvSingletons.getSingletons(getContext()) 249 .getDvrManager() 250 .getConflictingSchedules( 251 mChannel.getId(), mStartTimeMs, mEndTimeMs); 252 } 253 if (conflicts == null) { 254 conflicts = Collections.emptyList(); 255 } 256 if (conflicts.isEmpty()) { 257 dismissDialog(); 258 } 259 setConflicts(conflicts); 260 return super.onCreateView(inflater, container, savedInstanceState); 261 } 262 263 @NonNull 264 @Override onCreateGuidance(Bundle savedInstanceState)265 public Guidance onCreateGuidance(Bundle savedInstanceState) { 266 String title = getResources().getString(R.string.dvr_channel_conflict_dialog_title); 267 String descriptionPrefix = 268 getString( 269 R.string.dvr_channel_conflict_dialog_description_prefix, 270 mChannel.getDisplayName()); 271 String description = getConflictDescription(); 272 if (description == null) { 273 dismissDialog(); 274 } 275 Drawable icon = getResources().getDrawable(R.drawable.quantum_ic_error_white_48, null); 276 return new Guidance(title, descriptionPrefix + " " + description, null, icon); 277 } 278 279 @Override getTrackerPrefix()280 public String getTrackerPrefix() { 281 return "DvrChannelRecordConflictFragment"; 282 } 283 } 284 285 /** 286 * A fragment to show the channel watching conflict. 287 * 288 * <p>This fragment is automatically closed when there are no upcoming conflicts. 289 */ 290 public static class DvrChannelWatchConflictFragment extends DvrConflictFragment 291 implements OnUpcomingConflictChangeListener { 292 private long mChannelId; 293 294 @Override onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)295 public View onCreateView( 296 LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 297 Bundle args = getArguments(); 298 if (args != null) { 299 mChannelId = args.getLong(DvrHalfSizedDialogFragment.KEY_CHANNEL_ID); 300 } 301 SoftPreconditions.checkArgument(mChannelId != Channel.INVALID_ID); 302 ConflictChecker checker = ((MainActivity) getContext()).getDvrConflictChecker(); 303 List<ScheduledRecording> conflicts = null; 304 if (checker != null) { 305 checker.addOnUpcomingConflictChangeListener(this); 306 conflicts = checker.getUpcomingConflicts(); 307 if (DEBUG) Log.d(TAG, "onCreateView: upcoming conflicts: " + conflicts); 308 if (conflicts.isEmpty()) { 309 dismissDialog(); 310 } 311 } 312 if (conflicts == null) { 313 if (DEBUG) Log.d(TAG, "onCreateView: There's no conflict."); 314 conflicts = Collections.emptyList(); 315 } 316 if (conflicts.isEmpty()) { 317 dismissDialog(); 318 } 319 setConflicts(conflicts); 320 return super.onCreateView(inflater, container, savedInstanceState); 321 } 322 323 @NonNull 324 @Override onCreateGuidance(Bundle savedInstanceState)325 public Guidance onCreateGuidance(Bundle savedInstanceState) { 326 String title = 327 getResources().getString(R.string.dvr_epg_channel_watch_conflict_dialog_title); 328 String description = 329 getResources() 330 .getString(R.string.dvr_epg_channel_watch_conflict_dialog_description); 331 return new Guidance(title, description, null, null); 332 } 333 334 @Override onCreateActions( @onNull List<GuidedAction> actions, Bundle savedInstanceState)335 public void onCreateActions( 336 @NonNull List<GuidedAction> actions, Bundle savedInstanceState) { 337 actions.add( 338 new GuidedAction.Builder(getContext()) 339 .id(ACTION_DELETE_CONFLICT) 340 .title(R.string.dvr_action_delete_schedule) 341 .build()); 342 actions.add( 343 new GuidedAction.Builder(getContext()) 344 .id(ACTION_CANCEL) 345 .title(R.string.dvr_action_record_program) 346 .build()); 347 } 348 349 @Override onTrackedGuidedActionClicked(GuidedAction action)350 public void onTrackedGuidedActionClicked(GuidedAction action) { 351 if (action.getId() == ACTION_CANCEL) { 352 ConflictChecker checker = ((MainActivity) getContext()).getDvrConflictChecker(); 353 if (checker != null) { 354 checker.setCheckedConflictsForChannel(mChannelId, getConflicts()); 355 } 356 } else if (action.getId() == ACTION_DELETE_CONFLICT) { 357 for (ScheduledRecording schedule : mConflicts) { 358 if (schedule.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) { 359 getDvrManager().stopRecording(schedule); 360 } else { 361 getDvrManager().removeScheduledRecording(schedule); 362 } 363 } 364 } 365 super.onGuidedActionClicked(action); 366 } 367 368 @Override getTrackerPrefix()369 public String getTrackerPrefix() { 370 return "DvrChannelWatchConflictFragment"; 371 } 372 373 @Override getTrackerLabelForGuidedAction(GuidedAction action)374 public String getTrackerLabelForGuidedAction(GuidedAction action) { 375 long actionId = action.getId(); 376 if (actionId == ACTION_CANCEL) { 377 return "cancel"; 378 } else if (actionId == ACTION_DELETE_CONFLICT) { 379 return "delete"; 380 } else { 381 return super.getTrackerLabelForGuidedAction(action); 382 } 383 } 384 385 @Override onDetach()386 public void onDetach() { 387 ConflictChecker checker = ((MainActivity) getContext()).getDvrConflictChecker(); 388 if (checker != null) { 389 checker.removeOnUpcomingConflictChangeListener(this); 390 } 391 super.onDetach(); 392 } 393 394 @Override onUpcomingConflictChange()395 public void onUpcomingConflictChange() { 396 ConflictChecker checker = ((MainActivity) getContext()).getDvrConflictChecker(); 397 if (checker == null || checker.getUpcomingConflicts().isEmpty()) { 398 if (DEBUG) Log.d(TAG, "onUpcomingConflictChange: There's no conflict."); 399 dismissDialog(); 400 } 401 } 402 } 403 } 404