SetupSourcesFragment.java revision 3a72b93e554bd22a5c64e71a6956d9604ce05108
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.ActivityNotFoundException;
20import android.content.ComponentName;
21import android.content.Context;
22import android.content.Intent;
23import android.graphics.Typeface;
24import android.graphics.drawable.Drawable;
25import android.media.tv.TvInputInfo;
26import android.media.tv.TvInputManager.TvInputCallback;
27import android.os.Bundle;
28import android.support.annotation.NonNull;
29import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
30import android.support.v17.leanback.widget.GuidedAction;
31import android.support.v17.leanback.widget.GuidedActionsStylist;
32import android.support.v17.leanback.widget.VerticalGridView;
33import android.view.ContextThemeWrapper;
34import android.view.LayoutInflater;
35import android.view.View;
36import android.view.ViewGroup;
37import android.widget.ImageView;
38import android.widget.TextView;
39import android.widget.Toast;
40
41import com.android.tv.ApplicationSingletons;
42import com.android.tv.Features;
43import com.android.tv.R;
44import com.android.tv.SetupPassthroughActivity;
45import com.android.tv.TvApplication;
46import com.android.tv.common.TvCommonUtils;
47import com.android.tv.common.ui.setup.SetupGuidedStepFragment;
48import com.android.tv.common.ui.setup.SetupMultiPaneFragment;
49import com.android.tv.data.ChannelDataManager;
50import com.android.tv.data.TvInputNewComparator;
51import com.android.tv.util.SetupUtils;
52import com.android.tv.util.TvInputManagerHelper;
53import com.android.tv.util.Utils;
54
55import java.util.ArrayList;
56import java.util.Collections;
57import java.util.List;
58
59/**
60 * A fragment for channel source info/setup.
61 */
62public class SetupSourcesFragment extends SetupMultiPaneFragment {
63    public static final String ACTION_CATEGORY =
64            "com.android.tv.onboarding.SetupSourcesFragment";
65    public static final int ACTION_PLAY_STORE = 1;
66
67    public static final int DEFAULT_THEME = -1;
68
69    private static final String SETUP_TRACKER_LABEL = "Setup fragment";
70
71    private static int sTheme = DEFAULT_THEME;
72
73    private InputSetupRunnable mInputSetupRunnable;
74
75    private ContentFragment mContentFragment;
76
77    @Override
78    public View onCreateView(LayoutInflater inflater, ViewGroup container,
79            Bundle savedInstanceState) {
80        LayoutInflater localInflater = inflater;
81        if (sTheme != -1) {
82            ContextThemeWrapper themeWrapper = new ContextThemeWrapper(getActivity(), sTheme);
83            localInflater = inflater.cloneInContext(themeWrapper);
84        }
85        View view = super.onCreateView(localInflater, container, savedInstanceState);
86        TvApplication.getSingletons(getActivity()).getTracker().sendScreenView(SETUP_TRACKER_LABEL);
87        return view;
88    }
89
90    @Override
91    protected void onEnterTransitionEnd() {
92        if (mContentFragment != null) {
93            mContentFragment.executePendingAction();
94        }
95    }
96
97    @Override
98    protected SetupGuidedStepFragment onCreateContentFragment() {
99        mContentFragment = new ContentFragment();
100        Bundle arguments = new Bundle();
101        arguments.putBoolean(SetupGuidedStepFragment.KEY_THREE_PANE, true);
102        mContentFragment.setArguments(arguments);
103        mContentFragment.setParentFragment(this);
104        return mContentFragment;
105    }
106
107    @Override
108    protected String getActionCategory() {
109        return ACTION_CATEGORY;
110    }
111
112    /**
113     * Sets the custom theme dynamically.
114     */
115    public static void setTheme(int theme) {
116        sTheme = theme;
117    }
118
119    /**
120     * Call this method to run customized input setup.
121     *
122     * @param runnable runnable to be called when the input setup is necessary.
123     */
124    public void setInputSetupRunnable(InputSetupRunnable runnable) {
125        mInputSetupRunnable = runnable;
126    }
127
128    /**
129     * Interface for the customized input setup.
130     */
131    public interface InputSetupRunnable {
132        /**
133         * Called for the input setup.
134         *
135         * @param input TV input for setup.
136         */
137        void runInputSetup(TvInputInfo input);
138    }
139
140    public static class ContentFragment extends SetupGuidedStepFragment {
141        private static final int REQUEST_CODE_START_SETUP_ACTIVITY = 1;
142
143        // ACTION_PLAY_STORE is defined in the outer class.
144        private static final int ACTION_DIVIDER = 2;
145        private static final int ACTION_HEADER = 3;
146        private static final int ACTION_INPUT_START = 4;
147
148        private static final int PENDING_ACTION_NONE = 0;
149        private static final int PENDING_ACTION_INPUT_CHANGED = 1;
150        private static final int PENDING_ACTION_CHANNEL_CHANGED = 2;
151
152        private TvInputManagerHelper mInputManager;
153        private ChannelDataManager mChannelDataManager;
154        private SetupUtils mSetupUtils;
155        private List<TvInputInfo> mInputs;
156        private int mKnownInputStartIndex;
157        private int mDoneInputStartIndex;
158
159        private SetupSourcesFragment mParentFragment;
160
161        private String mNewlyAddedInputId;
162
163        private int mPendingAction = PENDING_ACTION_NONE;
164
165        private final TvInputCallback mInputCallback = new TvInputCallback() {
166            @Override
167            public void onInputAdded(String inputId) {
168                handleInputChanged();
169            }
170
171            @Override
172            public void onInputRemoved(String inputId) {
173                handleInputChanged();
174            }
175
176            private void handleInputChanged() {
177                // The actions created while enter transition is running will not be included in the
178                // fragment transition.
179                if (mParentFragment.isEnterTransitionRunning()) {
180                    mPendingAction = PENDING_ACTION_INPUT_CHANGED;
181                    return;
182                }
183                buildInputs();
184                updateActions();
185            }
186        };
187
188        void setParentFragment(SetupSourcesFragment parentFragment) {
189            mParentFragment = parentFragment;
190        }
191
192        private final ChannelDataManager.Listener mChannelDataManagerListener
193                = new ChannelDataManager.Listener() {
194            @Override
195            public void onLoadFinished() {
196                handleChannelChanged();
197            }
198
199            @Override
200            public void onChannelListUpdated() {
201                handleChannelChanged();
202            }
203
204            @Override
205            public void onChannelBrowsableChanged() {
206                handleChannelChanged();
207            }
208
209            private void handleChannelChanged() {
210                // The actions created while enter transition is running will not be included in the
211                // fragment transition.
212                if (mParentFragment.isEnterTransitionRunning()) {
213                    if (mPendingAction != PENDING_ACTION_INPUT_CHANGED) {
214                        mPendingAction = PENDING_ACTION_CHANNEL_CHANGED;
215                    }
216                    return;
217                }
218                updateActions();
219            }
220        };
221
222        @Override
223        public void onCreate(Bundle savedInstanceState) {
224            // TODO: Handle USB TV tuner differently.
225            Context context = getActivity();
226            ApplicationSingletons app = TvApplication.getSingletons(context);
227            mInputManager = app.getTvInputManagerHelper();
228            mChannelDataManager = app.getChannelDataManager();
229            mSetupUtils = SetupUtils.getInstance(context);
230            buildInputs();
231            mInputManager.addCallback(mInputCallback);
232            mChannelDataManager.addListener(mChannelDataManagerListener);
233            super.onCreate(savedInstanceState);
234        }
235
236        @Override
237        public void onDestroy() {
238            super.onDestroy();
239            mChannelDataManager.removeListener(mChannelDataManagerListener);
240            mInputManager.removeCallback(mInputCallback);
241        }
242
243        @NonNull
244        @Override
245        public Guidance onCreateGuidance(Bundle savedInstanceState) {
246            String title = getString(R.string.setup_sources_text);
247            String description = getString(R.string.setup_sources_description);
248            return new Guidance(title, description, null, null);
249        }
250
251        @Override
252        public GuidedActionsStylist onCreateActionsStylist() {
253            return new SetupSourceGuidedActionsStylist();
254        }
255
256        @Override
257        public void onCreateActions(@NonNull List<GuidedAction> actions,
258                Bundle savedInstanceState) {
259            createActionsInternal(actions);
260        }
261
262        private void buildInputs() {
263            List<TvInputInfo> oldInputs = mInputs;
264            mInputs = mInputManager.getTvInputInfos(true, true);
265            // Get newly installed input ID.
266            if (oldInputs != null) {
267                List<TvInputInfo> newList = new ArrayList<>(mInputs);
268                for (TvInputInfo input : oldInputs) {
269                    newList.remove(input);
270                }
271                if (newList.size() > 0 && mSetupUtils.isNewInput(newList.get(0).getId())) {
272                    mNewlyAddedInputId = newList.get(0).getId();
273                } else {
274                    mNewlyAddedInputId = null;
275                }
276            }
277            Collections.sort(mInputs, new TvInputNewComparator(mSetupUtils, mInputManager));
278            mKnownInputStartIndex = 0;
279            mDoneInputStartIndex = 0;
280            for (TvInputInfo input : mInputs) {
281                if (mSetupUtils.isNewInput(input.getId())) {
282                    mSetupUtils.markAsKnownInput(input.getId());
283                    ++mKnownInputStartIndex;
284                }
285                if (!mSetupUtils.isSetupDone(input.getId())) {
286                    ++mDoneInputStartIndex;
287                }
288            }
289        }
290
291        private void updateActions() {
292            List<GuidedAction> actions = new ArrayList<>();
293            createActionsInternal(actions);
294            setActions(actions);
295        }
296
297        private void createActionsInternal(List<GuidedAction> actions) {
298            int newPosition = -1;
299            int position = 0;
300            if (mDoneInputStartIndex > 0) {
301                // Need a "New" category
302                actions.add(new GuidedAction.Builder(getActivity()).id(ACTION_HEADER)
303                        .title(null).description(getString(R.string.setup_category_new))
304                        .focusable(false).build());
305            }
306            for (int i = 0; i < mInputs.size(); ++i) {
307                if (i == mDoneInputStartIndex) {
308                    ++position;
309                    actions.add(new GuidedAction.Builder(getActivity()).id(ACTION_HEADER)
310                            .title(null).description(getString(R.string.setup_category_done))
311                            .focusable(false).build());
312                }
313                TvInputInfo input = mInputs.get(i);
314                String inputId = input.getId();
315                String description;
316                int channelCount = mChannelDataManager.getChannelCountForInput(inputId);
317                if (mSetupUtils.isSetupDone(inputId) || channelCount > 0) {
318                    if (channelCount == 0) {
319                        description = getString(R.string.setup_input_no_channels);
320                    } else {
321                        description = getResources().getQuantityString(
322                                R.plurals.setup_input_channels, channelCount, channelCount);
323                    }
324                } else if (i >= mKnownInputStartIndex) {
325                    description = getString(R.string.setup_input_setup_now);
326                } else {
327                    description = getString(R.string.setup_input_new);
328                }
329                ++position;
330                if (input.getId().equals(mNewlyAddedInputId)) {
331                    newPosition = position;
332                }
333                actions.add(new GuidedAction.Builder(getActivity()).id(ACTION_INPUT_START + i)
334                        .title(input.loadLabel(getActivity()).toString()).description(description)
335                        .build());
336            }
337            if (Features.ONBOARDING_PLAY_STORE.isEnabled(getActivity())) {
338                if (mInputs.size() > 0) {
339                    // Divider
340                    ++position;
341                    actions.add(new GuidedAction.Builder(getActivity()).id(ACTION_DIVIDER)
342                            .title(null).description(null).focusable(false).build());
343                }
344                // Play store action
345                ++position;
346                actions.add(new GuidedAction.Builder(getActivity()).id(ACTION_PLAY_STORE)
347                        .title(getString(R.string.setup_play_store_action_title))
348                        .description(getString(R.string.setup_play_store_action_description))
349                        .icon(R.drawable.ic_playstore).build());
350            }
351            if (newPosition != -1) {
352                VerticalGridView gridView = getGuidedActionsStylist().getActionsGridView();
353                gridView.setSelectedPosition(newPosition);
354            }
355        }
356
357        @Override
358        protected String getActionCategory() {
359            return ACTION_CATEGORY;
360        }
361
362        @Override
363        public void onGuidedActionClicked(GuidedAction action) {
364            if (action.getId() == ACTION_PLAY_STORE) {
365                mParentFragment.onActionClick(ACTION_CATEGORY, (int) action.getId());
366                return;
367            }
368            TvInputInfo input = mInputs.get((int) action.getId() - ACTION_INPUT_START);
369            if (mParentFragment.mInputSetupRunnable != null) {
370                mParentFragment.mInputSetupRunnable.runInputSetup(input);
371                return;
372            }
373            Intent intent = TvCommonUtils.createSetupIntent(input);
374            if (intent == null) {
375                Toast.makeText(getActivity(), R.string.msg_no_setup_activity, Toast.LENGTH_SHORT)
376                        .show();
377                return;
378            }
379            // Even though other app can handle the intent, the setup launched by Live channels
380            // should go through Live channels SetupPassthroughActivity.
381            intent.setComponent(new ComponentName(getActivity(), SetupPassthroughActivity.class));
382            try {
383                // Now we know that the user intends to set up this input. Grant permission for
384                // writing EPG data.
385                SetupUtils.grantEpgPermission(getActivity(), input.getServiceInfo().packageName);
386                startActivityForResult(intent, REQUEST_CODE_START_SETUP_ACTIVITY);
387            } catch (ActivityNotFoundException e) {
388                Toast.makeText(getActivity(), getString(R.string.msg_unable_to_start_setup_activity,
389                        input.loadLabel(getActivity())), Toast.LENGTH_SHORT).show();
390            }
391        }
392
393        @Override
394        public void onActivityResult(int requestCode, int resultCode, Intent data) {
395            updateActions();
396        }
397
398        @Override
399        public int onProvideTheme() {
400            return sTheme == DEFAULT_THEME ? super.onProvideTheme() : sTheme;
401        }
402
403        void executePendingAction() {
404            switch (mPendingAction) {
405                case PENDING_ACTION_INPUT_CHANGED:
406                    buildInputs();
407                    // Fall through
408                case PENDING_ACTION_CHANNEL_CHANGED:
409                    updateActions();
410                    break;
411            }
412            mPendingAction = PENDING_ACTION_NONE;
413        }
414
415        private class SetupSourceGuidedActionsStylist extends GuidedActionsStylist {
416            private static final int VIEW_TYPE_DIVIDER = 1;
417
418            private static final float ALPHA_CATEGORY = 1.0f;
419            private static final float ALPHA_INPUT_DESCRIPTION = 0.5f;
420
421            @Override
422            public int getItemViewType(GuidedAction action) {
423                if (action.getId() == ACTION_DIVIDER) {
424                    return VIEW_TYPE_DIVIDER;
425                }
426                return super.getItemViewType(action);
427            }
428
429            @Override
430            public int onProvideItemLayoutId(int viewType) {
431                if (viewType == VIEW_TYPE_DIVIDER) {
432                    return R.layout.onboarding_item_divider;
433                }
434                return super.onProvideItemLayoutId(viewType);
435            }
436
437            @Override
438            public void onBindViewHolder(ViewHolder vh, GuidedAction action) {
439                super.onBindViewHolder(vh, action);
440                TextView descriptionView = vh.getDescriptionView();
441                if (descriptionView != null) {
442                    if (action.getId() == ACTION_HEADER) {
443                        descriptionView.setAlpha(ALPHA_CATEGORY);
444                        descriptionView.setTextColor(Utils.getColor(getResources(),
445                                R.color.setup_category));
446                        descriptionView.setTypeface(Typeface.create(
447                                getString(R.string.condensed_font), 0));
448                    } else {
449                        descriptionView.setAlpha(ALPHA_INPUT_DESCRIPTION);
450                        descriptionView.setTextColor(Utils.getColor(getResources(),
451                                R.color.common_setup_input_description));
452                        descriptionView.setTypeface(Typeface.create(getString(R.string.font), 0));
453                    }
454                }
455                // Workaround for b/26473407.
456                ImageView iconView = vh.getIconView();
457                if (iconView != null) {
458                    Drawable icon = action.getIcon();
459                    if (icon != null) {
460                        // setImageDrawable resets the drawable's level unless we set the view level
461                        // first.
462                        iconView.setImageLevel(icon.getLevel());
463                        iconView.setImageDrawable(icon);
464                        iconView.setVisibility(View.VISIBLE);
465                    } else {
466                        iconView.setVisibility(View.GONE);
467                    }
468                }
469            }
470        }
471    }
472}
473