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