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