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
17package com.android.tv.onboarding;
18
19import android.content.Context;
20import android.graphics.Typeface;
21import android.media.tv.TvInputInfo;
22import android.media.tv.TvInputManager.TvInputCallback;
23import android.os.Bundle;
24import android.support.annotation.NonNull;
25import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
26import android.support.v17.leanback.widget.GuidedAction;
27import android.support.v17.leanback.widget.GuidedActionsStylist;
28import android.support.v17.leanback.widget.VerticalGridView;
29import android.view.LayoutInflater;
30import android.view.View;
31import android.view.ViewGroup;
32import android.widget.TextView;
33
34import com.android.tv.ApplicationSingletons;
35import com.android.tv.R;
36import com.android.tv.TvApplication;
37import com.android.tv.common.ui.setup.SetupGuidedStepFragment;
38import com.android.tv.common.ui.setup.SetupMultiPaneFragment;
39import com.android.tv.data.ChannelDataManager;
40import com.android.tv.data.TvInputNewComparator;
41import com.android.tv.ui.GuidedActionsStylistWithDivider;
42import com.android.tv.util.SetupUtils;
43import com.android.tv.util.TvInputManagerHelper;
44
45import java.util.ArrayList;
46import java.util.Collections;
47import java.util.List;
48
49/**
50 * A fragment for channel source info/setup.
51 */
52public class SetupSourcesFragment extends SetupMultiPaneFragment {
53    /**
54     * The action category for the actions which is fired from this fragment.
55     */
56    public static final String ACTION_CATEGORY =
57            "com.android.tv.onboarding.SetupSourcesFragment";
58    /**
59     * An action to open the merchant collection.
60     */
61    public static final int ACTION_ONLINE_STORE = 1;
62    /**
63     * An action to show the setup activity of TV input.
64     * <p>
65     * This action is not added to the action list. This is sent outside of the fragment.
66     * Use {@link #ACTION_PARAM_KEY_INPUT_ID} to get the input ID from the parameter.
67     */
68    public static final int ACTION_SETUP_INPUT = 2;
69
70    /**
71     * The key for the action parameter which contains the TV input ID. It's used for the action
72     * {@link #ACTION_SETUP_INPUT}.
73     */
74    public static final String ACTION_PARAM_KEY_INPUT_ID = "input_id";
75
76    private static final String SETUP_TRACKER_LABEL = "Setup fragment";
77
78    @Override
79    public View onCreateView(LayoutInflater inflater, ViewGroup container,
80            Bundle savedInstanceState) {
81        View view = super.onCreateView(inflater, container, savedInstanceState);
82        TvApplication.getSingletons(getActivity()).getTracker().sendScreenView(SETUP_TRACKER_LABEL);
83        return view;
84    }
85
86    @Override
87    protected void onEnterTransitionEnd() {
88        SetupGuidedStepFragment f = getContentFragment();
89        if (f instanceof ContentFragment) {
90            // If the enter transition is canceled quickly, the child fragment can be null because
91            // the fragment is added asynchronously.
92            ((ContentFragment) f).executePendingAction();
93        }
94    }
95
96    @Override
97    protected SetupGuidedStepFragment onCreateContentFragment() {
98        SetupGuidedStepFragment f = new ContentFragment();
99        Bundle arguments = new Bundle();
100        arguments.putBoolean(SetupGuidedStepFragment.KEY_THREE_PANE, true);
101        f.setArguments(arguments);
102        return f;
103    }
104
105    @Override
106    protected String getActionCategory() {
107        return ACTION_CATEGORY;
108    }
109
110    public static class ContentFragment extends SetupGuidedStepFragment {
111        // ACTION_ONLINE_STORE is defined in the outer class.
112        private static final int ACTION_HEADER = 3;
113        private static final int ACTION_INPUT_START = 4;
114
115        private static final int PENDING_ACTION_NONE = 0;
116        private static final int PENDING_ACTION_INPUT_CHANGED = 1;
117        private static final int PENDING_ACTION_CHANNEL_CHANGED = 2;
118
119        private TvInputManagerHelper mInputManager;
120        private ChannelDataManager mChannelDataManager;
121        private SetupUtils mSetupUtils;
122        private List<TvInputInfo> mInputs;
123        private int mKnownInputStartIndex;
124        private int mDoneInputStartIndex;
125
126        private SetupSourcesFragment mParentFragment;
127
128        private String mNewlyAddedInputId;
129
130        private int mPendingAction = PENDING_ACTION_NONE;
131
132        private final TvInputCallback mInputCallback = new TvInputCallback() {
133            @Override
134            public void onInputAdded(String inputId) {
135                handleInputChanged();
136            }
137
138            @Override
139            public void onInputRemoved(String inputId) {
140                handleInputChanged();
141            }
142
143            @Override
144            public void onInputUpdated(String inputId) {
145                handleInputChanged();
146            }
147
148            @Override
149            public void onTvInputInfoUpdated(TvInputInfo inputInfo) {
150                handleInputChanged();
151            }
152
153            private void handleInputChanged() {
154                // The actions created while enter transition is running will not be included in the
155                // fragment transition.
156                if (mParentFragment.isEnterTransitionRunning()) {
157                    mPendingAction = PENDING_ACTION_INPUT_CHANGED;
158                    return;
159                }
160                buildInputs();
161                updateActions();
162            }
163        };
164
165        private final ChannelDataManager.Listener mChannelDataManagerListener
166                = new ChannelDataManager.Listener() {
167            @Override
168            public void onLoadFinished() {
169                handleChannelChanged();
170            }
171
172            @Override
173            public void onChannelListUpdated() {
174                handleChannelChanged();
175            }
176
177            @Override
178            public void onChannelBrowsableChanged() {
179                handleChannelChanged();
180            }
181
182            private void handleChannelChanged() {
183                // The actions created while enter transition is running will not be included in the
184                // fragment transition.
185                if (mParentFragment.isEnterTransitionRunning()) {
186                    if (mPendingAction != PENDING_ACTION_INPUT_CHANGED) {
187                        mPendingAction = PENDING_ACTION_CHANNEL_CHANGED;
188                    }
189                    return;
190                }
191                updateActions();
192            }
193        };
194
195        @Override
196        public void onCreate(Bundle savedInstanceState) {
197            Context context = getActivity();
198            ApplicationSingletons app = TvApplication.getSingletons(context);
199            mInputManager = app.getTvInputManagerHelper();
200            mChannelDataManager = app.getChannelDataManager();
201            mSetupUtils = SetupUtils.getInstance(context);
202            buildInputs();
203            mInputManager.addCallback(mInputCallback);
204            mChannelDataManager.addListener(mChannelDataManagerListener);
205            super.onCreate(savedInstanceState);
206            mParentFragment = (SetupSourcesFragment) getParentFragment();
207        }
208
209        @Override
210        public void onDestroy() {
211            super.onDestroy();
212            mChannelDataManager.removeListener(mChannelDataManagerListener);
213            mInputManager.removeCallback(mInputCallback);
214        }
215
216        @NonNull
217        @Override
218        public Guidance onCreateGuidance(Bundle savedInstanceState) {
219            String title = getString(R.string.setup_sources_text);
220            String description = getString(R.string.setup_sources_description);
221            return new Guidance(title, description, null, null);
222        }
223
224        @Override
225        public GuidedActionsStylist onCreateActionsStylist() {
226            return new SetupSourceGuidedActionsStylist();
227        }
228
229        @Override
230        public void onCreateActions(@NonNull List<GuidedAction> actions,
231                Bundle savedInstanceState) {
232            createActionsInternal(actions);
233        }
234
235        private void buildInputs() {
236            List<TvInputInfo> oldInputs = mInputs;
237            mInputs = mInputManager.getTvInputInfos(true, true);
238            // Get newly installed input ID.
239            if (oldInputs != null) {
240                List<TvInputInfo> newList = new ArrayList<>(mInputs);
241                for (TvInputInfo input : oldInputs) {
242                    newList.remove(input);
243                }
244                if (newList.size() > 0 && mSetupUtils.isNewInput(newList.get(0).getId())) {
245                    mNewlyAddedInputId = newList.get(0).getId();
246                } else {
247                    mNewlyAddedInputId = null;
248                }
249            }
250            Collections.sort(mInputs, new TvInputNewComparator(mSetupUtils, mInputManager));
251            mKnownInputStartIndex = 0;
252            mDoneInputStartIndex = 0;
253            for (TvInputInfo input : mInputs) {
254                if (mSetupUtils.isNewInput(input.getId())) {
255                    mSetupUtils.markAsKnownInput(input.getId());
256                    ++mKnownInputStartIndex;
257                }
258                if (!mSetupUtils.isSetupDone(input.getId())) {
259                    ++mDoneInputStartIndex;
260                }
261            }
262        }
263
264        private void updateActions() {
265            List<GuidedAction> actions = new ArrayList<>();
266            createActionsInternal(actions);
267            setActions(actions);
268        }
269
270        private void createActionsInternal(List<GuidedAction> actions) {
271            int newPosition = -1;
272            int position = 0;
273            if (mDoneInputStartIndex > 0) {
274                // Need a "New" category
275                actions.add(new GuidedAction.Builder(getActivity())
276                        .id(ACTION_HEADER)
277                        .title(null)
278                        .description(getString(R.string.setup_category_new))
279                        .focusable(false)
280                        .infoOnly(true)
281                        .build());
282            }
283            for (int i = 0; i < mInputs.size(); ++i) {
284                if (i == mDoneInputStartIndex) {
285                    ++position;
286                    actions.add(new GuidedAction.Builder(getActivity())
287                            .id(ACTION_HEADER)
288                            .title(null)
289                            .description(getString(R.string.setup_category_done))
290                            .focusable(false)
291                            .infoOnly(true)
292                            .build());
293                }
294                TvInputInfo input = mInputs.get(i);
295                String inputId = input.getId();
296                String description;
297                int channelCount = mChannelDataManager.getChannelCountForInput(inputId);
298                if (mSetupUtils.isSetupDone(inputId) || channelCount > 0) {
299                    if (channelCount == 0) {
300                        description = getString(R.string.setup_input_no_channels);
301                    } else {
302                        description = getResources().getQuantityString(
303                                R.plurals.setup_input_channels, channelCount, channelCount);
304                    }
305                } else if (i >= mKnownInputStartIndex) {
306                    description = getString(R.string.setup_input_setup_now);
307                } else {
308                    description = getString(R.string.setup_input_new);
309                }
310                ++position;
311                if (input.getId().equals(mNewlyAddedInputId)) {
312                    newPosition = position;
313                }
314                actions.add(new GuidedAction.Builder(getActivity())
315                        .id(ACTION_INPUT_START + i)
316                        .title(input.loadLabel(getActivity()).toString())
317                        .description(description)
318                        .build());
319            }
320            if (mInputs.size() > 0) {
321                // Divider
322                ++position;
323                actions.add(GuidedActionsStylistWithDivider.createDividerAction(getContext()));
324            }
325            // online store action
326            ++position;
327            actions.add(new GuidedAction.Builder(getActivity())
328                    .id(ACTION_ONLINE_STORE)
329                    .title(getString(R.string.setup_store_action_title))
330                    .description(getString(R.string.setup_store_action_description))
331                    .icon(R.drawable.ic_store)
332                    .build());
333
334            if (newPosition != -1) {
335                VerticalGridView gridView = getGuidedActionsStylist().getActionsGridView();
336                gridView.setSelectedPosition(newPosition);
337            }
338        }
339
340        @Override
341        protected String getActionCategory() {
342            return ACTION_CATEGORY;
343        }
344
345        @Override
346        public void onGuidedActionClicked(GuidedAction action) {
347            if (action.getId() == ACTION_ONLINE_STORE) {
348                mParentFragment.onActionClick(ACTION_CATEGORY, (int) action.getId());
349                return;
350            }
351            int index = (int) action.getId() - ACTION_INPUT_START;
352            if (index >= 0) {
353                TvInputInfo input = mInputs.get(index);
354                Bundle params = new Bundle();
355                params.putString(ACTION_PARAM_KEY_INPUT_ID, input.getId());
356                mParentFragment.onActionClick(ACTION_CATEGORY, ACTION_SETUP_INPUT, params);
357            }
358        }
359
360        void executePendingAction() {
361            switch (mPendingAction) {
362                case PENDING_ACTION_INPUT_CHANGED:
363                    buildInputs();
364                    // Fall through
365                case PENDING_ACTION_CHANNEL_CHANGED:
366                    updateActions();
367                    break;
368            }
369            mPendingAction = PENDING_ACTION_NONE;
370        }
371
372        private class SetupSourceGuidedActionsStylist extends GuidedActionsStylistWithDivider {
373            private static final float ALPHA_CATEGORY = 1.0f;
374            private static final float ALPHA_INPUT_DESCRIPTION = 0.5f;
375
376            @Override
377            public void onBindViewHolder(ViewHolder vh, GuidedAction action) {
378                super.onBindViewHolder(vh, action);
379                TextView descriptionView = vh.getDescriptionView();
380                if (descriptionView != null) {
381                    if (action.getId() == ACTION_HEADER) {
382                        descriptionView.setAlpha(ALPHA_CATEGORY);
383                        descriptionView.setTextColor(getResources().getColor(R.color.setup_category,
384                                null));
385                        descriptionView.setTypeface(Typeface.create(
386                                getString(R.string.condensed_font), 0));
387                    } else {
388                        descriptionView.setAlpha(ALPHA_INPUT_DESCRIPTION);
389                        descriptionView.setTextColor(getResources().getColor(
390                                R.color.common_setup_input_description, null));
391                        descriptionView.setTypeface(Typeface.create(getString(R.string.font), 0));
392                    }
393                }
394            }
395        }
396    }
397}
398