1 /*
2  * Copyright (C) 2015 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.Activity;
21 import android.app.ProgressDialog;
22 import android.content.Context;
23 import android.content.DialogInterface;
24 import android.content.Intent;
25 import android.media.tv.TvInputManager;
26 import android.os.Build;
27 import android.os.Bundle;
28 import android.support.annotation.MainThread;
29 import android.support.annotation.NonNull;
30 import android.support.annotation.Nullable;
31 import android.support.v4.app.ActivityOptionsCompat;
32 import android.text.Html;
33 import android.text.Spannable;
34 import android.text.SpannableString;
35 import android.text.SpannableStringBuilder;
36 import android.text.Spanned;
37 import android.text.TextUtils;
38 import android.text.style.TextAppearanceSpan;
39 import android.widget.ImageView;
40 import android.widget.Toast;
41 
42 import com.android.tv.MainActivity;
43 import com.android.tv.R;
44 import com.android.tv.TvSingletons;
45 import com.android.tv.common.SoftPreconditions;
46 import com.android.tv.common.recording.RecordingStorageStatusManager;
47 import com.android.tv.common.util.CommonUtils;
48 import com.android.tv.data.api.BaseProgram;
49 import com.android.tv.data.api.Channel;
50 import com.android.tv.data.api.Program;
51 import com.android.tv.dialog.HalfSizedDialogFragment;
52 import com.android.tv.dvr.DvrManager;
53 import com.android.tv.dvr.data.RecordedProgram;
54 import com.android.tv.dvr.data.ScheduledRecording;
55 import com.android.tv.dvr.data.SeriesRecording;
56 import com.android.tv.dvr.provider.EpisodicProgramLoadTask;
57 import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrAlreadyRecordedDialogFragment;
58 import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrAlreadyScheduledDialogFragment;
59 import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrChannelRecordDurationOptionDialogFragment;
60 import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrChannelWatchConflictDialogFragment;
61 import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrInsufficientSpaceErrorDialogFragment;
62 import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrMissingStorageErrorDialogFragment;
63 import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrNoFreeSpaceErrorDialogFragment;
64 import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrProgramConflictDialogFragment;
65 import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrScheduleDialogFragment;
66 import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrSmallSizedStorageErrorDialogFragment;
67 import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrStopRecordingDialogFragment;
68 import com.android.tv.dvr.ui.DvrHalfSizedDialogFragment.DvrWriteStoragePermissionRationaleDialogFragment;
69 import com.android.tv.dvr.ui.browse.DvrBrowseActivity;
70 import com.android.tv.dvr.ui.list.DvrHistoryActivity;
71 import com.android.tv.dvr.ui.list.DvrSchedulesActivity;
72 import com.android.tv.dvr.ui.list.DvrSchedulesFragment;
73 import com.android.tv.dvr.ui.list.DvrSeriesSchedulesFragment;
74 import com.android.tv.dvr.ui.playback.DvrPlaybackActivity;
75 import com.android.tv.ui.DetailsActivity;
76 import com.android.tv.util.ToastUtils;
77 import com.android.tv.util.Utils;
78 
79 import com.google.common.collect.ImmutableList;
80 import java.util.ArrayList;
81 import java.util.Collections;
82 import java.util.List;
83 import java.util.Set;
84 
85 /** A helper class for DVR UI. */
86 @MainThread
87 @TargetApi(Build.VERSION_CODES.N)
88 public class DvrUiHelper {
89     private static final String TAG = "DvrUiHelper";
90 
91     private static ProgressDialog sProgressDialog = null;
92 
93     /**
94      * Checks if the storage status is good for recording and shows error messages if needed.
95      *
96      * @param recordingRequestRunnable if the storage status is OK to record or users choose to
97      *     perform the operation anyway, this Runnable will run.
98      */
checkStorageStatusAndShowErrorMessage( Activity activity, String inputId, Runnable recordingRequestRunnable)99     public static void checkStorageStatusAndShowErrorMessage(
100             Activity activity, String inputId, Runnable recordingRequestRunnable) {
101         if (CommonUtils.isBundledInput(inputId)) {
102             switch (TvSingletons.getSingletons(activity)
103                     .getRecordingStorageStatusManager()
104                     .getDvrStorageStatus()) {
105                 case RecordingStorageStatusManager.STORAGE_STATUS_TOTAL_CAPACITY_TOO_SMALL:
106                     showDvrSmallSizedStorageErrorDialog(activity);
107                     return;
108                 case RecordingStorageStatusManager.STORAGE_STATUS_MISSING:
109                     showDvrMissingStorageErrorDialog(activity);
110                     return;
111                 case RecordingStorageStatusManager.STORAGE_STATUS_FREE_SPACE_INSUFFICIENT:
112                     showDvrNoFreeSpaceErrorDialog(activity, recordingRequestRunnable);
113                     return;
114                 default: // fall out
115             }
116         }
117         recordingRequestRunnable.run();
118     }
119 
120     /** Shows the schedule dialog. */
showScheduleDialog( Activity activity, Program program, boolean addCurrentProgramToSeries)121     public static void showScheduleDialog(
122             Activity activity, Program program, boolean addCurrentProgramToSeries) {
123         if (SoftPreconditions.checkNotNull(program) == null) {
124             return;
125         }
126         Bundle args = new Bundle();
127         args.putParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM, program.toParcelable());
128         args.putBoolean(
129                 DvrScheduleFragment.KEY_ADD_CURRENT_PROGRAM_TO_SERIES, addCurrentProgramToSeries);
130         showDialogFragment(activity, new DvrScheduleDialogFragment(), args, true, true);
131     }
132 
133     /** Shows the recording duration options dialog. */
showChannelRecordDurationOptions(Activity activity, Channel channel)134     public static void showChannelRecordDurationOptions(Activity activity, Channel channel) {
135         if (SoftPreconditions.checkNotNull(channel) == null) {
136             return;
137         }
138         Bundle args = new Bundle();
139         args.putLong(DvrHalfSizedDialogFragment.KEY_CHANNEL_ID, channel.getId());
140         showDialogFragment(activity, new DvrChannelRecordDurationOptionDialogFragment(), args);
141     }
142 
143     /** Shows the dialog which says that the new schedule conflicts with others. */
showScheduleConflictDialog(Activity activity, Program program)144     public static void showScheduleConflictDialog(Activity activity, Program program) {
145         if (program == null) {
146             return;
147         }
148         Bundle args = new Bundle();
149         args.putParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM, program.toParcelable());
150         showDialogFragment(activity, new DvrProgramConflictDialogFragment(), args, false, true);
151     }
152 
153     /** Shows the conflict dialog for the channel watching. */
showChannelWatchConflictDialog(MainActivity activity, Channel channel)154     public static void showChannelWatchConflictDialog(MainActivity activity, Channel channel) {
155         if (channel == null) {
156             return;
157         }
158         Bundle args = new Bundle();
159         args.putLong(DvrHalfSizedDialogFragment.KEY_CHANNEL_ID, channel.getId());
160         showDialogFragment(activity, new DvrChannelWatchConflictDialogFragment(), args);
161     }
162 
163     /** Shows DVR insufficient space error dialog. */
showDvrInsufficientSpaceErrorDialog( MainActivity activity, Set<String> failedScheduledRecordingInfoSet)164     public static void showDvrInsufficientSpaceErrorDialog(
165             MainActivity activity, Set<String> failedScheduledRecordingInfoSet) {
166         Bundle args = new Bundle();
167         ArrayList<String> failedScheduledRecordingInfoArray =
168                 new ArrayList<>(failedScheduledRecordingInfoSet);
169         args.putStringArrayList(
170                 DvrInsufficientSpaceErrorFragment.FAILED_SCHEDULED_RECORDING_INFOS,
171                 failedScheduledRecordingInfoArray);
172         showDialogFragment(activity, new DvrInsufficientSpaceErrorDialogFragment(), args);
173         Utils.clearRecordingFailedReason(
174                 activity, TvInputManager.RECORDING_ERROR_INSUFFICIENT_SPACE);
175         Utils.clearFailedScheduledRecordingInfoSet(activity);
176     }
177 
178     /**
179      * Shows DVR no free space error dialog.
180      *
181      * @param recordingRequestRunnable the recording request to be executed when users choose {@link
182      *     DvrGuidedStepFragment#ACTION_RECORD_ANYWAY}.
183      */
showDvrNoFreeSpaceErrorDialog( Activity activity, Runnable recordingRequestRunnable)184     public static void showDvrNoFreeSpaceErrorDialog(
185             Activity activity, Runnable recordingRequestRunnable) {
186         DvrHalfSizedDialogFragment fragment = new DvrNoFreeSpaceErrorDialogFragment();
187         fragment.setOnActionClickListener(
188                 new HalfSizedDialogFragment.OnActionClickListener() {
189                     @Override
190                     public void onActionClick(long actionId) {
191                         if (actionId == DvrGuidedStepFragment.ACTION_RECORD_ANYWAY) {
192                             recordingRequestRunnable.run();
193                         } else if (actionId == DvrGuidedStepFragment.ACTION_DELETE_RECORDINGS) {
194                             Intent intent = new Intent(activity, DvrBrowseActivity.class);
195                             activity.startActivity(intent);
196                         }
197                     }
198                 });
199         showDialogFragment(activity, fragment, null);
200     }
201 
202     /** Shows DVR missing storage error dialog. */
showDvrMissingStorageErrorDialog(Activity activity)203     private static void showDvrMissingStorageErrorDialog(Activity activity) {
204         showDialogFragment(activity, new DvrMissingStorageErrorDialogFragment(), null);
205     }
206 
207     /** Shows DVR small sized storage error dialog. */
showDvrSmallSizedStorageErrorDialog(Activity activity)208     public static void showDvrSmallSizedStorageErrorDialog(Activity activity) {
209         showDialogFragment(activity, new DvrSmallSizedStorageErrorDialogFragment(), null);
210     }
211 
212     /** Shows stop recording dialog. */
showStopRecordingDialog( Activity activity, long channelId, int reason, HalfSizedDialogFragment.OnActionClickListener listener)213     public static void showStopRecordingDialog(
214             Activity activity,
215             long channelId,
216             int reason,
217             HalfSizedDialogFragment.OnActionClickListener listener) {
218         Bundle args = new Bundle();
219         args.putLong(DvrHalfSizedDialogFragment.KEY_CHANNEL_ID, channelId);
220         args.putInt(DvrStopRecordingFragment.KEY_REASON, reason);
221         DvrHalfSizedDialogFragment fragment = new DvrStopRecordingDialogFragment();
222         fragment.setOnActionClickListener(listener);
223         showDialogFragment(activity, fragment, args);
224     }
225 
226     /** Shows "already scheduled" dialog. */
showAlreadyScheduleDialog(Activity activity, Program program)227     public static void showAlreadyScheduleDialog(Activity activity, Program program) {
228         if (program == null) {
229             return;
230         }
231         Bundle args = new Bundle();
232         args.putParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM, program.toParcelable());
233         showDialogFragment(activity, new DvrAlreadyScheduledDialogFragment(), args, false, true);
234     }
235 
236     /** Shows "already recorded" dialog. */
showAlreadyRecordedDialog(Activity activity, Program program)237     public static void showAlreadyRecordedDialog(Activity activity, Program program) {
238         if (program == null) {
239             return;
240         }
241         Bundle args = new Bundle();
242         args.putParcelable(DvrHalfSizedDialogFragment.KEY_PROGRAM, program.toParcelable());
243         showDialogFragment(activity, new DvrAlreadyRecordedDialogFragment(), args, false, true);
244     }
245 
246     /** Shows program information dialog. */
showWriteStoragePermissionRationaleDialog(Activity activity)247     public static void showWriteStoragePermissionRationaleDialog(Activity activity) {
248         showDialogFragment(
249                 activity,
250                 new DvrWriteStoragePermissionRationaleDialogFragment(),
251                 new Bundle(),
252                 false,
253                 false);
254     }
255 
256     /**
257      * Handle the request of recording a current program. It will handle creating schedules and
258      * shows the proper dialog and toast message respectively for timed-recording and program
259      * recording cases.
260      *
261      * @param addProgramToSeries denotes whether the program to be recorded should be added into the
262      *     series recording when users choose to record the entire series.
263      */
requestRecordingCurrentProgram( Activity activity, Channel channel, Program program, boolean addProgramToSeries)264     public static void requestRecordingCurrentProgram(
265             Activity activity, Channel channel, Program program, boolean addProgramToSeries) {
266         if (program == null) {
267             DvrUiHelper.showChannelRecordDurationOptions(activity, channel);
268         } else if (DvrUiHelper.handleCreateSchedule(activity, program, addProgramToSeries)) {
269             String msg =
270                     activity.getString(
271                             R.string.dvr_msg_current_program_scheduled,
272                             program.getTitle(),
273                             Utils.toTimeString(program.getEndTimeUtcMillis(), false));
274             Toast.makeText(activity, msg, Toast.LENGTH_SHORT).show();
275         }
276     }
277 
278     /**
279      * Handle the request of recording a future program. It will handle creating schedules and shows
280      * the proper toast message.
281      *
282      * @param addProgramToSeries denotes whether the program to be recorded should be added into the
283      *     series recording when users choose to record the entire series.
284      */
requestRecordingFutureProgram( Activity activity, Program program, boolean addProgramToSeries)285     public static void requestRecordingFutureProgram(
286             Activity activity, Program program, boolean addProgramToSeries) {
287         if (DvrUiHelper.handleCreateSchedule(activity, program, addProgramToSeries)) {
288             String msg = activity.getString(R.string.dvr_msg_program_scheduled, program.getTitle());
289             ToastUtils.show(activity, msg, Toast.LENGTH_SHORT);
290         }
291     }
292 
293     /**
294      * Handles the action to create the new schedule. It returns {@code true} if the schedule is
295      * added and there's no additional UI, otherwise {@code false}.
296      */
handleCreateSchedule( Activity activity, Program program, boolean addProgramToSeries)297     private static boolean handleCreateSchedule(
298             Activity activity, Program program, boolean addProgramToSeries) {
299         if (program == null) {
300             return false;
301         }
302         DvrManager dvrManager = TvSingletons.getSingletons(activity).getDvrManager();
303         if (!program.isEpisodic()) {
304             // One time recording.
305             dvrManager.addSchedule(program);
306             if (!dvrManager.getConflictingSchedules(program).isEmpty()) {
307                 DvrUiHelper.showScheduleConflictDialog(activity, program);
308                 return false;
309             }
310         } else {
311             // Show recorded program rather than the schedule.
312             RecordedProgram recordedProgram =
313                     dvrManager.getRecordedProgram(
314                             program.getTitle(),
315                             program.getSeasonNumber(),
316                             program.getEpisodeNumber());
317             if (recordedProgram != null) {
318                 DvrUiHelper.showAlreadyRecordedDialog(activity, program);
319                 return false;
320             }
321             ScheduledRecording duplicate =
322                     dvrManager.getScheduledRecording(
323                             program.getTitle(),
324                             program.getSeasonNumber(),
325                             program.getEpisodeNumber());
326             if (duplicate != null
327                     && (duplicate.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED
328                             || duplicate.getState()
329                                     == ScheduledRecording.STATE_RECORDING_IN_PROGRESS)) {
330                 DvrUiHelper.showAlreadyScheduleDialog(activity, program);
331                 return false;
332             }
333             SeriesRecording seriesRecording = dvrManager.getSeriesRecording(program);
334             if (seriesRecording == null || seriesRecording.isStopped()) {
335                 DvrUiHelper.showScheduleDialog(activity, program, addProgramToSeries);
336                 return false;
337             } else {
338                 // Just add the schedule.
339                 dvrManager.addSchedule(program);
340             }
341         }
342         return true;
343     }
344 
showDialogFragment( Activity activity, DvrHalfSizedDialogFragment dialogFragment, Bundle args)345     private static void showDialogFragment(
346             Activity activity, DvrHalfSizedDialogFragment dialogFragment, Bundle args) {
347         showDialogFragment(activity, dialogFragment, args, false, false);
348     }
349 
showDialogFragment( Activity activity, DvrHalfSizedDialogFragment dialogFragment, Bundle args, boolean keepSidePanelHistory, boolean keepProgramGuide)350     private static void showDialogFragment(
351             Activity activity,
352             DvrHalfSizedDialogFragment dialogFragment,
353             Bundle args,
354             boolean keepSidePanelHistory,
355             boolean keepProgramGuide) {
356         dialogFragment.setArguments(args);
357         if (activity instanceof MainActivity) {
358             ((MainActivity) activity)
359                     .getOverlayManager()
360                     .showDialogFragment(
361                             DvrHalfSizedDialogFragment.DIALOG_TAG,
362                             dialogFragment,
363                             keepSidePanelHistory,
364                             keepProgramGuide);
365         } else {
366             dialogFragment.show(
367                     activity.getFragmentManager(), DvrHalfSizedDialogFragment.DIALOG_TAG);
368         }
369     }
370 
371     /** Checks whether channel watch conflict dialog is open or not. */
isChannelWatchConflictDialogShown(MainActivity activity)372     public static boolean isChannelWatchConflictDialogShown(MainActivity activity) {
373         return activity.getOverlayManager().getCurrentDialog()
374                 instanceof DvrChannelWatchConflictDialogFragment;
375     }
376 
getEarliestScheduledRecording( List<ScheduledRecording> recordings)377     private static ScheduledRecording getEarliestScheduledRecording(
378             List<ScheduledRecording> recordings) {
379         ScheduledRecording earlistScheduledRecording = null;
380         if (!recordings.isEmpty()) {
381             Collections.sort(
382                     recordings, ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR);
383             earlistScheduledRecording = recordings.get(0);
384         }
385         return earlistScheduledRecording;
386     }
387 
388     /**
389      * Launches DVR playback activity for the give recorded program.
390      *
391      * @param programId the ID of the recorded program going to be played.
392      * @param seekTimeMs the seek position to initial playback.
393      * @param pinChecked {@code true} if the pin code for parental controls has already been
394      *     verified, otherwise {@code false}.
395      */
startPlaybackActivity( Context context, long programId, long seekTimeMs, boolean pinChecked)396     public static void startPlaybackActivity(
397             Context context, long programId, long seekTimeMs, boolean pinChecked) {
398         Intent intent = new Intent(context, DvrPlaybackActivity.class);
399         intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_ID, programId);
400         if (seekTimeMs != TvInputManager.TIME_SHIFT_INVALID_TIME) {
401             intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_SEEK_TIME, seekTimeMs);
402         }
403         intent.putExtra(Utils.EXTRA_KEY_RECORDED_PROGRAM_PIN_CHECKED, pinChecked);
404         context.startActivity(intent);
405     }
406 
407     /** Shows the schedules activity to resolve the tune conflict. */
startSchedulesActivityForTuneConflict(Context context, Channel channel)408     public static void startSchedulesActivityForTuneConflict(Context context, Channel channel) {
409         if (channel == null) {
410             return;
411         }
412         List<ScheduledRecording> conflicts =
413                 TvSingletons.getSingletons(context)
414                         .getDvrManager()
415                         .getConflictingSchedulesForTune(channel.getId());
416         startSchedulesActivity(context, getEarliestScheduledRecording(conflicts));
417     }
418 
419     /** Shows the schedules activity to resolve the one time recording conflict. */
startSchedulesActivityForOneTimeRecordingConflict( Context context, List<ScheduledRecording> conflicts)420     public static void startSchedulesActivityForOneTimeRecordingConflict(
421             Context context, List<ScheduledRecording> conflicts) {
422         startSchedulesActivity(context, getEarliestScheduledRecording(conflicts));
423     }
424 
425     /** Shows the schedules activity with full schedule. */
startDvrHistoryActivity(Context context)426     public static void startDvrHistoryActivity(Context context) {
427         Intent intent = new Intent(context, DvrHistoryActivity.class);
428         context.startActivity(intent);
429     }
430 
431     /** Shows the schedules activity with full schedule. */
startSchedulesActivity( Context context, ScheduledRecording focusedScheduledRecording)432     public static void startSchedulesActivity(
433             Context context, ScheduledRecording focusedScheduledRecording) {
434         Intent intent = new Intent(context, DvrSchedulesActivity.class);
435         intent.putExtra(
436                 DvrSchedulesActivity.KEY_SCHEDULES_TYPE, DvrSchedulesActivity.TYPE_FULL_SCHEDULE);
437         if (focusedScheduledRecording != null) {
438             intent.putExtra(
439                     DvrSchedulesFragment.SCHEDULES_KEY_SCHEDULED_RECORDING,
440                     focusedScheduledRecording);
441         }
442         context.startActivity(intent);
443     }
444 
445     /** Shows the schedules activity for series recording. */
startSchedulesActivityForSeries( Context context, SeriesRecording seriesRecording)446     public static void startSchedulesActivityForSeries(
447             Context context, SeriesRecording seriesRecording) {
448         Intent intent = new Intent(context, DvrSchedulesActivity.class);
449         intent.putExtra(
450                 DvrSchedulesActivity.KEY_SCHEDULES_TYPE, DvrSchedulesActivity.TYPE_SERIES_SCHEDULE);
451         intent.putExtra(
452                 DvrSeriesSchedulesFragment.SERIES_SCHEDULES_KEY_SERIES_RECORDING, seriesRecording);
453         context.startActivity(intent);
454     }
455 
456     /**
457      * Shows the series settings activity.
458      *
459      * @param programs list of programs which belong to the series.
460      */
startSeriesSettingsActivity( Context context, long seriesRecordingId, @Nullable List<Program> programs, boolean removeEmptySeriesSchedule, boolean isWindowTranslucent, boolean showViewScheduleOptionInDialog, @Nullable Program currentProgram)461     public static void startSeriesSettingsActivity(
462             Context context,
463             long seriesRecordingId,
464             @Nullable List<Program> programs,
465             boolean removeEmptySeriesSchedule,
466             boolean isWindowTranslucent,
467             boolean showViewScheduleOptionInDialog,
468             @Nullable Program currentProgram) {
469         SeriesRecording series =
470                 TvSingletons.getSingletons(context)
471                         .getDvrDataManager()
472                         .getSeriesRecording(seriesRecordingId);
473         if (series == null) {
474             return;
475         }
476         if (programs != null) {
477             startSeriesSettingsActivityInternal(
478                     context,
479                     seriesRecordingId,
480                     programs,
481                     removeEmptySeriesSchedule,
482                     isWindowTranslucent,
483                     showViewScheduleOptionInDialog,
484                     currentProgram);
485         } else {
486             EpisodicProgramLoadTask episodicProgramLoadTask =
487                     new EpisodicProgramLoadTask(context, series) {
488                         @Override
489                         protected void onPostExecute(List<Program> loadedPrograms) {
490                             if (sProgressDialog != null) {
491                                 sProgressDialog.dismiss();
492                                 sProgressDialog = null;
493                             }
494                             startSeriesSettingsActivityInternal(
495                                     context,
496                                     seriesRecordingId,
497                                     loadedPrograms == null
498                                             ? ImmutableList.of()
499                                             : loadedPrograms,
500                                     removeEmptySeriesSchedule,
501                                     isWindowTranslucent,
502                                     showViewScheduleOptionInDialog,
503                                     currentProgram);
504                         }
505                     }.setLoadCurrentProgram(true)
506                             .setLoadDisallowedProgram(true)
507                             .setLoadScheduledEpisode(true)
508                             .setIgnoreChannelOption(true);
509             sProgressDialog =
510                     ProgressDialog.show(
511                             context,
512                             null,
513                             context.getString(
514                                     R.string.dvr_series_progress_message_reading_programs),
515                             true,
516                             true,
517                             new DialogInterface.OnCancelListener() {
518                                 @Override
519                                 public void onCancel(DialogInterface dialogInterface) {
520                                     episodicProgramLoadTask.cancel(true);
521                                     sProgressDialog = null;
522                                 }
523                             });
524             episodicProgramLoadTask.execute();
525         }
526     }
527 
528     /**
529      * Shows the episode recording settings activity.
530      *
531      * @param program Program to be recorded
532      */
startRecordingSettingsActivity( Context context, Program program)533     public static void startRecordingSettingsActivity(
534             Context context,
535             Program program) {
536         if (program != null) {
537             Intent intent = new Intent(context, DvrRecordingSettingsActivity.class);
538             intent.putExtra(DvrRecordingSettingsActivity.IS_WINDOW_TRANSLUCENT, true);
539             intent.putExtra(DvrRecordingSettingsActivity.PROGRAM, program.toParcelable());
540             context.startActivity(intent);
541         }
542     }
543 
startSeriesSettingsActivityInternal( Context context, long seriesRecordingId, @NonNull List<Program> programs, boolean removeEmptySeriesSchedule, boolean isWindowTranslucent, boolean showViewScheduleOptionInDialog, @Nullable Program currentProgram)544     private static void startSeriesSettingsActivityInternal(
545             Context context,
546             long seriesRecordingId,
547             @NonNull List<Program> programs,
548             boolean removeEmptySeriesSchedule,
549             boolean isWindowTranslucent,
550             boolean showViewScheduleOptionInDialog,
551             @Nullable Program currentProgram) {
552         SoftPreconditions.checkState(
553                 programs != null, TAG, "Start series settings activity but programs is null");
554         Intent intent = new Intent(context, DvrSeriesSettingsActivity.class);
555         intent.putExtra(DvrSeriesSettingsActivity.SERIES_RECORDING_ID, seriesRecordingId);
556         BigArguments.reset();
557         BigArguments.setArgument(DvrSeriesSettingsActivity.PROGRAM_LIST, programs);
558         intent.putExtra(
559                 DvrSeriesSettingsActivity.REMOVE_EMPTY_SERIES_RECORDING, removeEmptySeriesSchedule);
560         intent.putExtra(DvrSeriesSettingsActivity.IS_WINDOW_TRANSLUCENT, isWindowTranslucent);
561         intent.putExtra(
562                 DvrSeriesSettingsActivity.SHOW_VIEW_SCHEDULE_OPTION_IN_DIALOG,
563                 showViewScheduleOptionInDialog);
564         if (currentProgram != null) {
565             intent.putExtra(DvrSeriesSettingsActivity.CURRENT_PROGRAM, currentProgram.toParcelable());
566         }
567         context.startActivity(intent);
568     }
569 
570     /** Shows "series recording scheduled" dialog activity. */
startSeriesScheduledDialogActivity( Context context, SeriesRecording seriesRecording, boolean showViewScheduleOptionInDialog, List<Program> programs)571     public static void startSeriesScheduledDialogActivity(
572             Context context,
573             SeriesRecording seriesRecording,
574             boolean showViewScheduleOptionInDialog,
575             List<Program> programs) {
576         if (seriesRecording == null) {
577             return;
578         }
579         Intent intent = new Intent(context, DvrSeriesScheduledDialogActivity.class);
580         intent.putExtra(
581                 DvrSeriesScheduledDialogActivity.SERIES_RECORDING_ID, seriesRecording.getId());
582         intent.putExtra(
583                 DvrSeriesScheduledDialogActivity.SHOW_VIEW_SCHEDULE_OPTION,
584                 showViewScheduleOptionInDialog);
585         BigArguments.reset();
586         BigArguments.setArgument(
587                 DvrSeriesScheduledFragment.SERIES_SCHEDULED_KEY_PROGRAMS, programs);
588         context.startActivity(intent);
589     }
590 
591     /**
592      * Shows the details activity for the DVR items. The type of DVR items may be {@link
593      * ScheduledRecording}, {@link RecordedProgram}, or {@link SeriesRecording}.
594      */
startDetailsActivity( Activity activity, Object dvrItem, @Nullable ImageView imageView, boolean hideViewSchedule)595     public static void startDetailsActivity(
596             Activity activity,
597             Object dvrItem,
598             @Nullable ImageView imageView,
599             boolean hideViewSchedule) {
600         if (dvrItem == null) {
601             return;
602         }
603         Intent intent = new Intent(activity, DetailsActivity.class);
604         long recordingId;
605         int viewType;
606         if (dvrItem instanceof ScheduledRecording) {
607             ScheduledRecording schedule = (ScheduledRecording) dvrItem;
608             recordingId = schedule.getId();
609             if (schedule.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED) {
610                 viewType = DetailsActivity.SCHEDULED_RECORDING_VIEW;
611             } else if (schedule.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS) {
612                 viewType = DetailsActivity.CURRENT_RECORDING_VIEW;
613             } else if (schedule.getState() == ScheduledRecording.STATE_RECORDING_FINISHED
614                     && schedule.getRecordedProgramId() != null) {
615                 recordingId = schedule.getRecordedProgramId();
616                 viewType = DetailsActivity.RECORDED_PROGRAM_VIEW;
617             } else if (schedule.getState() == ScheduledRecording.STATE_RECORDING_FAILED) {
618                 viewType = DetailsActivity.SCHEDULED_RECORDING_VIEW;
619                 hideViewSchedule = true;
620             } else {
621                 return;
622             }
623         } else if (dvrItem instanceof RecordedProgram) {
624             recordingId = ((RecordedProgram) dvrItem).getId();
625             viewType = DetailsActivity.RECORDED_PROGRAM_VIEW;
626         } else if (dvrItem instanceof SeriesRecording) {
627             recordingId = ((SeriesRecording) dvrItem).getId();
628             viewType = DetailsActivity.SERIES_RECORDING_VIEW;
629         } else {
630             return;
631         }
632         intent.putExtra(DetailsActivity.RECORDING_ID, recordingId);
633         intent.putExtra(DetailsActivity.DETAILS_VIEW_TYPE, viewType);
634         intent.putExtra(DetailsActivity.HIDE_VIEW_SCHEDULE, hideViewSchedule);
635         Bundle bundle = null;
636         if (imageView != null) {
637             bundle =
638                     ActivityOptionsCompat.makeSceneTransitionAnimation(
639                                     activity, imageView, DetailsActivity.SHARED_ELEMENT_NAME)
640                             .toBundle();
641         }
642         activity.startActivity(intent, bundle);
643     }
644 
645     /** Shows the cancel all dialog for series schedules list. */
showCancelAllSeriesRecordingDialog( DvrSchedulesActivity activity, SeriesRecording seriesRecording)646     public static void showCancelAllSeriesRecordingDialog(
647             DvrSchedulesActivity activity, SeriesRecording seriesRecording) {
648         DvrStopSeriesRecordingDialogFragment dvrStopSeriesRecordingDialogFragment =
649                 new DvrStopSeriesRecordingDialogFragment();
650         Bundle arguments = new Bundle();
651         arguments.putParcelable(
652                 DvrStopSeriesRecordingFragment.KEY_SERIES_RECORDING, seriesRecording);
653         dvrStopSeriesRecordingDialogFragment.setArguments(arguments);
654         dvrStopSeriesRecordingDialogFragment.show(
655                 activity.getFragmentManager(), DvrStopSeriesRecordingDialogFragment.DIALOG_TAG);
656     }
657 
658     /** Shows the series deletion activity. */
startSeriesDeletionActivity(Context context, long seriesRecordingId)659     public static void startSeriesDeletionActivity(Context context, long seriesRecordingId) {
660         Intent intent = new Intent(context, DvrSeriesDeletionActivity.class);
661         intent.putExtra(DvrSeriesDeletionActivity.SERIES_RECORDING_ID, seriesRecordingId);
662         context.startActivity(intent);
663     }
664 
showAddScheduleToast( Context context, String title, long startTimeMs, long endTimeMs)665     public static void showAddScheduleToast(
666             Context context, String title, long startTimeMs, long endTimeMs) {
667         String msg =
668                 (startTimeMs > System.currentTimeMillis())
669                         ? context.getString(R.string.dvr_msg_program_scheduled, title)
670                         : context.getString(
671                                 R.string.dvr_msg_current_program_scheduled,
672                                 title,
673                                 Utils.toTimeString(endTimeMs, false));
674         Toast.makeText(context, msg, Toast.LENGTH_SHORT).show();
675     }
676 
677     /** Returns the styled schedule's title with its season and episode number. */
getStyledTitleWithEpisodeNumber( Context context, ScheduledRecording schedule, int episodeNumberStyleResId)678     public static CharSequence getStyledTitleWithEpisodeNumber(
679             Context context, ScheduledRecording schedule, int episodeNumberStyleResId) {
680         return getStyledTitleWithEpisodeNumber(
681                 context,
682                 schedule.getProgramTitle(),
683                 schedule.getSeasonNumber(),
684                 schedule.getEpisodeNumber(),
685                 episodeNumberStyleResId);
686     }
687 
688     /** Returns the styled program's title with its season and episode number. */
getStyledTitleWithEpisodeNumber( Context context, BaseProgram program, int episodeNumberStyleResId)689     public static CharSequence getStyledTitleWithEpisodeNumber(
690             Context context, BaseProgram program, int episodeNumberStyleResId) {
691         return getStyledTitleWithEpisodeNumber(
692                 context,
693                 program.getTitle(),
694                 program.getSeasonNumber(),
695                 program.getEpisodeNumber(),
696                 episodeNumberStyleResId);
697     }
698 
699     @NonNull
getStyledTitleWithEpisodeNumber( Context context, String title, String seasonNumber, String episodeNumber, int episodeNumberStyleResId)700     public static CharSequence getStyledTitleWithEpisodeNumber(
701             Context context,
702             String title,
703             String seasonNumber,
704             String episodeNumber,
705             int episodeNumberStyleResId) {
706         if (TextUtils.isEmpty(title)) {
707             return "";
708         }
709         SpannableStringBuilder builder;
710         if (TextUtils.isEmpty(seasonNumber) || seasonNumber.equals("0")) {
711             Spanned temp =
712                     TextUtils.isEmpty(episodeNumber)
713                             ? SpannableStringBuilder.valueOf(title)
714                             : Html.fromHtml(
715                                     context.getString(
716                                             R.string.program_title_with_episode_number_no_season,
717                                             title,
718                                             episodeNumber));
719             builder = SpannableStringBuilder.valueOf(temp);
720         } else {
721             builder =
722                     SpannableStringBuilder.valueOf(
723                             Html.fromHtml(
724                                     context.getString(
725                                             R.string.program_title_with_episode_number,
726                                             title,
727                                             seasonNumber,
728                                             episodeNumber)));
729         }
730         Object[] spans = builder.getSpans(0, builder.length(), Object.class);
731         if (spans.length > 0) {
732             if (episodeNumberStyleResId != 0) {
733                 builder.setSpan(
734                         new TextAppearanceSpan(context, episodeNumberStyleResId),
735                         builder.getSpanStart(spans[0]),
736                         builder.getSpanEnd(spans[0]),
737                         Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
738             }
739             builder.removeSpan(spans[0]);
740         }
741         return new SpannableString(builder);
742     }
743 }
744