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