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