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