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.annotation.TargetApi; 20 import android.app.FragmentManager; 21 import android.content.Context; 22 import android.os.Build; 23 import android.os.Bundle; 24 import android.util.LongSparseArray; 25 26 import androidx.leanback.app.GuidedStepFragment; 27 import androidx.leanback.widget.GuidanceStylist.Guidance; 28 import androidx.leanback.widget.GuidedAction; 29 import androidx.leanback.widget.GuidedActionsStylist; 30 31 import com.android.tv.R; 32 import com.android.tv.TvSingletons; 33 import com.android.tv.data.ChannelDataManager; 34 import com.android.tv.data.ChannelImpl; 35 import com.android.tv.data.api.Channel; 36 import com.android.tv.data.api.Program; 37 import com.android.tv.dvr.DvrDataManager; 38 import com.android.tv.dvr.DvrManager; 39 import com.android.tv.dvr.data.ScheduledRecording; 40 import com.android.tv.dvr.data.SeasonEpisodeNumber; 41 import com.android.tv.dvr.data.SeriesRecording; 42 import com.android.tv.dvr.data.SeriesRecording.ChannelOption; 43 import com.android.tv.dvr.recorder.SeriesRecordingScheduler; 44 45 import java.util.ArrayList; 46 import java.util.Collections; 47 import java.util.HashSet; 48 import java.util.List; 49 import java.util.Set; 50 51 /** Fragment for DVR series recording settings. */ 52 @TargetApi(Build.VERSION_CODES.N) 53 @SuppressWarnings("AndroidApiChecker") // TODO(b/32513850) remove when error prone is updated 54 public class DvrSeriesSettingsFragment extends GuidedStepFragment 55 implements DvrDataManager.SeriesRecordingListener { 56 private static final String TAG = "SeriesSettingsFragment"; 57 58 private static final long ACTION_ID_PRIORITY = 10; 59 private static final long ACTION_ID_CHANNEL = 11; 60 61 private static final long SUB_ACTION_ID_CHANNEL_ALL = 102; 62 // Each channel's action id = SUB_ACTION_ID_CHANNEL_ONE_BASE + channel id 63 private static final long SUB_ACTION_ID_CHANNEL_ONE_BASE = 500; 64 65 private DvrDataManager mDvrDataManager; 66 private SeriesRecording mSeriesRecording; 67 private long mSeriesRecordingId; 68 @ChannelOption int mChannelOption; 69 private long mSelectedChannelId; 70 private int mBackStackCount; 71 private boolean mShowViewScheduleOptionInDialog; 72 private Program mCurrentProgram; 73 74 private String mFragmentTitle; 75 private String mSeriesRecordingTitle; 76 private String mProrityActionTitle; 77 private String mProrityActionHighestText; 78 private String mProrityActionLowestText; 79 private String mChannelsActionTitle; 80 private String mChannelsActionAllText; 81 private LongSparseArray<Channel> mId2Channel = new LongSparseArray<>(); 82 private List<Channel> mChannels = new ArrayList<>(); 83 private List<Program> mPrograms; 84 85 private GuidedAction mPriorityGuidedAction; 86 private GuidedAction mChannelsGuidedAction; 87 88 @Override onAttach(Context context)89 public void onAttach(Context context) { 90 super.onAttach(context); 91 mBackStackCount = getFragmentManager().getBackStackEntryCount(); 92 mDvrDataManager = TvSingletons.getSingletons(context).getDvrDataManager(); 93 mSeriesRecordingId = getArguments().getLong(DvrSeriesSettingsActivity.SERIES_RECORDING_ID); 94 mSeriesRecording = mDvrDataManager.getSeriesRecording(mSeriesRecordingId); 95 if (mSeriesRecording == null) { 96 getActivity().finish(); 97 return; 98 } 99 mSeriesRecordingTitle = mSeriesRecording.getTitle(); 100 mShowViewScheduleOptionInDialog = 101 getArguments() 102 .getBoolean(DvrSeriesSettingsActivity.SHOW_VIEW_SCHEDULE_OPTION_IN_DIALOG); 103 mCurrentProgram = getArguments().getParcelable(DvrSeriesSettingsActivity.CURRENT_PROGRAM); 104 mDvrDataManager.addSeriesRecordingListener(this); 105 mPrograms = 106 (List<Program>) BigArguments.getArgument(DvrSeriesSettingsActivity.PROGRAM_LIST); 107 BigArguments.reset(); 108 if (mPrograms == null) { 109 getActivity().finish(); 110 return; 111 } 112 Set<Long> channelIds = new HashSet<>(); 113 ChannelDataManager channelDataManager = 114 TvSingletons.getSingletons(context).getChannelDataManager(); 115 for (Program program : mPrograms) { 116 long channelId = program.getChannelId(); 117 if (channelIds.add(channelId)) { 118 Channel channel = channelDataManager.getChannel(channelId); 119 if (channel != null) { 120 mId2Channel.put(channel.getId(), channel); 121 mChannels.add(channel); 122 } 123 } 124 } 125 mChannelOption = mSeriesRecording.getChannelOption(); 126 mSelectedChannelId = Channel.INVALID_ID; 127 if (mChannelOption == SeriesRecording.OPTION_CHANNEL_ONE) { 128 Channel channel = channelDataManager.getChannel(mSeriesRecording.getChannelId()); 129 if (channel != null) { 130 mSelectedChannelId = channel.getId(); 131 } else { 132 mChannelOption = SeriesRecording.OPTION_CHANNEL_ALL; 133 } 134 } 135 mChannels.sort(ChannelImpl.CHANNEL_NUMBER_COMPARATOR); 136 mFragmentTitle = getString(R.string.dvr_series_settings_title); 137 mProrityActionTitle = getString(R.string.dvr_series_settings_priority); 138 mProrityActionHighestText = getString(R.string.dvr_series_settings_priority_highest); 139 mProrityActionLowestText = getString(R.string.dvr_series_settings_priority_lowest); 140 mChannelsActionTitle = getString(R.string.dvr_series_settings_channels); 141 mChannelsActionAllText = getString(R.string.dvr_series_settings_channels_all); 142 } 143 144 @Override onResume()145 public void onResume() { 146 super.onResume(); 147 // To avoid the order of series's priority has changed, but series doesn't get update. 148 updatePriorityGuidedAction(); 149 } 150 151 @Override onDetach()152 public void onDetach() { 153 super.onDetach(); 154 mDvrDataManager.removeSeriesRecordingListener(this); 155 } 156 157 @Override onDestroy()158 public void onDestroy() { 159 if (getFragmentManager().getBackStackEntryCount() == mBackStackCount 160 && getArguments() 161 .getBoolean(DvrSeriesSettingsActivity.REMOVE_EMPTY_SERIES_RECORDING)) { 162 mDvrDataManager.checkAndRemoveEmptySeriesRecording(mSeriesRecordingId); 163 } 164 super.onDestroy(); 165 } 166 167 @Override onCreateGuidance(Bundle savedInstanceState)168 public Guidance onCreateGuidance(Bundle savedInstanceState) { 169 String title = mFragmentTitle; 170 return new Guidance(title, null, mSeriesRecordingTitle, null); 171 } 172 173 @Override onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState)174 public void onCreateActions(List<GuidedAction> actions, Bundle savedInstanceState) { 175 mPriorityGuidedAction = 176 new GuidedAction.Builder(getActivity()) 177 .id(ACTION_ID_PRIORITY) 178 .title(mProrityActionTitle) 179 .build(); 180 actions.add(mPriorityGuidedAction); 181 182 mChannelsGuidedAction = 183 new GuidedAction.Builder(getActivity()) 184 .id(ACTION_ID_CHANNEL) 185 .title(mChannelsActionTitle) 186 .subActions(buildChannelSubAction()) 187 .build(); 188 actions.add(mChannelsGuidedAction); 189 updateChannelsGuidedAction(false); 190 } 191 192 @Override onCreateButtonActions(List<GuidedAction> actions, Bundle savedInstanceState)193 public void onCreateButtonActions(List<GuidedAction> actions, Bundle savedInstanceState) { 194 actions.add( 195 new GuidedAction.Builder(getActivity()) 196 .clickAction(GuidedAction.ACTION_ID_OK) 197 .build()); 198 actions.add( 199 new GuidedAction.Builder(getActivity()) 200 .clickAction(GuidedAction.ACTION_ID_CANCEL) 201 .build()); 202 } 203 204 @Override onGuidedActionClicked(GuidedAction action)205 public void onGuidedActionClicked(GuidedAction action) { 206 long actionId = action.getId(); 207 if (actionId == GuidedAction.ACTION_ID_OK) { 208 if (mChannelOption != mSeriesRecording.getChannelOption() 209 || mSeriesRecording.isStopped() 210 || (mChannelOption == SeriesRecording.OPTION_CHANNEL_ONE 211 && mSeriesRecording.getChannelId() != mSelectedChannelId)) { 212 SeriesRecording.Builder builder = 213 SeriesRecording.buildFrom(mSeriesRecording) 214 .setChannelOption(mChannelOption) 215 .setState(SeriesRecording.STATE_SERIES_NORMAL); 216 if (mSelectedChannelId != Channel.INVALID_ID) { 217 builder.setChannelId(mSelectedChannelId); 218 } 219 DvrManager dvrManager = TvSingletons.getSingletons(getContext()).getDvrManager(); 220 dvrManager.updateSeriesRecording(builder.build()); 221 if (mCurrentProgram != null 222 && (mChannelOption == SeriesRecording.OPTION_CHANNEL_ALL 223 || mSelectedChannelId == mCurrentProgram.getChannelId())) { 224 dvrManager.addSchedule(mCurrentProgram); 225 } 226 updateSchedulesToSeries(); 227 showConfirmDialog(); 228 } else { 229 showConfirmDialog(); 230 } 231 } else if (actionId == GuidedAction.ACTION_ID_CANCEL) { 232 finishGuidedStepFragments(); 233 } else if (actionId == ACTION_ID_PRIORITY) { 234 FragmentManager fragmentManager = getFragmentManager(); 235 DvrPrioritySettingsFragment fragment = new DvrPrioritySettingsFragment(); 236 Bundle args = new Bundle(); 237 args.putLong( 238 DvrPrioritySettingsFragment.COME_FROM_SERIES_RECORDING_ID, 239 mSeriesRecording.getId()); 240 fragment.setArguments(args); 241 GuidedStepFragment.add(fragmentManager, fragment, R.id.dvr_settings_view_frame); 242 } 243 } 244 245 @Override onSubGuidedActionClicked(GuidedAction action)246 public boolean onSubGuidedActionClicked(GuidedAction action) { 247 long actionId = action.getId(); 248 if (actionId == SUB_ACTION_ID_CHANNEL_ALL) { 249 mChannelOption = SeriesRecording.OPTION_CHANNEL_ALL; 250 mSelectedChannelId = Channel.INVALID_ID; 251 updateChannelsGuidedAction(true); 252 return true; 253 } else if (actionId > SUB_ACTION_ID_CHANNEL_ONE_BASE) { 254 mChannelOption = SeriesRecording.OPTION_CHANNEL_ONE; 255 mSelectedChannelId = actionId - SUB_ACTION_ID_CHANNEL_ONE_BASE; 256 updateChannelsGuidedAction(true); 257 return true; 258 } 259 return false; 260 } 261 262 @Override onCreateButtonActionsStylist()263 public GuidedActionsStylist onCreateButtonActionsStylist() { 264 return new DvrGuidedActionsStylist(true); 265 } 266 updateChannelsGuidedAction(boolean notifyActionChanged)267 private void updateChannelsGuidedAction(boolean notifyActionChanged) { 268 if (mChannelOption == SeriesRecording.OPTION_CHANNEL_ALL) { 269 mChannelsGuidedAction.setDescription(mChannelsActionAllText); 270 } else if (mId2Channel.get(mSelectedChannelId) != null) { 271 mChannelsGuidedAction.setDescription( 272 mId2Channel.get(mSelectedChannelId).getDisplayText()); 273 } 274 if (notifyActionChanged) { 275 notifyActionChanged(findActionPositionById(ACTION_ID_CHANNEL)); 276 } 277 } 278 updatePriorityGuidedAction()279 private void updatePriorityGuidedAction() { 280 int totalSeriesCount = 0; 281 int priorityOrder = 0; 282 for (SeriesRecording seriesRecording : mDvrDataManager.getSeriesRecordings()) { 283 if (seriesRecording.getState() == SeriesRecording.STATE_SERIES_NORMAL 284 || seriesRecording.getId() == mSeriesRecording.getId()) { 285 ++totalSeriesCount; 286 } 287 if (seriesRecording.getState() == SeriesRecording.STATE_SERIES_NORMAL 288 && seriesRecording.getId() != mSeriesRecording.getId() 289 && seriesRecording.getPriority() > mSeriesRecording.getPriority()) { 290 ++priorityOrder; 291 } 292 } 293 if (priorityOrder == 0) { 294 mPriorityGuidedAction.setDescription(mProrityActionHighestText); 295 } else if (priorityOrder >= totalSeriesCount - 1) { 296 mPriorityGuidedAction.setDescription(mProrityActionLowestText); 297 } else { 298 mPriorityGuidedAction.setDescription( 299 getString(R.string.dvr_series_settings_priority_rank, priorityOrder + 1)); 300 } 301 notifyActionChanged(findActionPositionById(ACTION_ID_PRIORITY)); 302 } 303 updateSchedulesToSeries()304 private void updateSchedulesToSeries() { 305 List<Program> recordingCandidates = new ArrayList<>(); 306 Set<SeasonEpisodeNumber> scheduledEpisodes = new HashSet<>(); 307 for (ScheduledRecording r : mDvrDataManager.getScheduledRecordings(mSeriesRecordingId)) { 308 if (r.getState() != ScheduledRecording.STATE_RECORDING_FAILED 309 && r.getState() != ScheduledRecording.STATE_RECORDING_CLIPPED) { 310 scheduledEpisodes.add( 311 new SeasonEpisodeNumber( 312 r.getSeriesRecordingId(), 313 r.getSeasonNumber(), 314 r.getEpisodeNumber())); 315 } 316 } 317 for (Program program : mPrograms) { 318 // Removes current programs and scheduled episodes out, matches the channel option. 319 if (program.getStartTimeUtcMillis() >= System.currentTimeMillis() 320 && mSeriesRecording.matchProgram(program) 321 && !scheduledEpisodes.contains( 322 new SeasonEpisodeNumber( 323 mSeriesRecordingId, 324 program.getSeasonNumber(), 325 program.getEpisodeNumber()))) { 326 recordingCandidates.add(program); 327 } 328 } 329 if (recordingCandidates.isEmpty()) { 330 return; 331 } 332 List<Program> programsToSchedule = 333 SeriesRecordingScheduler.pickOneProgramPerEpisode( 334 mDvrDataManager, 335 Collections.singletonList(mSeriesRecording), 336 recordingCandidates) 337 .get(mSeriesRecordingId); 338 if (!programsToSchedule.isEmpty()) { 339 TvSingletons.getSingletons(getContext()) 340 .getDvrManager() 341 .addScheduleToSeriesRecording(mSeriesRecording, programsToSchedule); 342 } 343 } 344 buildChannelSubAction()345 private List<GuidedAction> buildChannelSubAction() { 346 List<GuidedAction> channelSubActions = new ArrayList<>(); 347 channelSubActions.add( 348 new GuidedAction.Builder(getActivity()) 349 .id(SUB_ACTION_ID_CHANNEL_ALL) 350 .title(mChannelsActionAllText) 351 .build()); 352 for (Channel channel : mChannels) { 353 channelSubActions.add( 354 new GuidedAction.Builder(getActivity()) 355 .id(SUB_ACTION_ID_CHANNEL_ONE_BASE + channel.getId()) 356 .title(channel.getDisplayText()) 357 .build()); 358 } 359 return channelSubActions; 360 } 361 showConfirmDialog()362 private void showConfirmDialog() { 363 DvrUiHelper.startSeriesScheduledDialogActivity( 364 getContext(), mSeriesRecording, mShowViewScheduleOptionInDialog, mPrograms); 365 finishGuidedStepFragments(); 366 } 367 368 @Override onSeriesRecordingAdded(SeriesRecording... seriesRecordings)369 public void onSeriesRecordingAdded(SeriesRecording... seriesRecordings) {} 370 371 @Override onSeriesRecordingRemoved(SeriesRecording... seriesRecordings)372 public void onSeriesRecordingRemoved(SeriesRecording... seriesRecordings) { 373 for (SeriesRecording series : seriesRecordings) { 374 if (series.getId() == mSeriesRecording.getId()) { 375 finishGuidedStepFragments(); 376 return; 377 } 378 } 379 } 380 381 @Override onSeriesRecordingChanged(SeriesRecording... seriesRecordings)382 public void onSeriesRecordingChanged(SeriesRecording... seriesRecordings) { 383 for (SeriesRecording seriesRecording : seriesRecordings) { 384 if (seriesRecording.getId() == mSeriesRecordingId) { 385 mSeriesRecording = seriesRecording; 386 updatePriorityGuidedAction(); 387 return; 388 } 389 } 390 } 391 } 392