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