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 
17 package com.android.tv.onboarding;
18 
19 import android.content.Context;
20 import android.graphics.Typeface;
21 import android.media.tv.TvInputInfo;
22 import android.media.tv.TvInputManager.TvInputCallback;
23 import android.os.Bundle;
24 import android.support.annotation.NonNull;
25 import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
26 import android.support.v17.leanback.widget.GuidedAction;
27 import android.support.v17.leanback.widget.GuidedActionsStylist;
28 import android.support.v17.leanback.widget.VerticalGridView;
29 import android.view.LayoutInflater;
30 import android.view.View;
31 import android.view.ViewGroup;
32 import android.widget.TextView;
33 
34 import com.android.tv.ApplicationSingletons;
35 import com.android.tv.R;
36 import com.android.tv.TvApplication;
37 import com.android.tv.common.ui.setup.SetupGuidedStepFragment;
38 import com.android.tv.common.ui.setup.SetupMultiPaneFragment;
39 import com.android.tv.data.ChannelDataManager;
40 import com.android.tv.data.TvInputNewComparator;
41 import com.android.tv.ui.GuidedActionsStylistWithDivider;
42 import com.android.tv.util.SetupUtils;
43 import com.android.tv.util.TvInputManagerHelper;
44 
45 import java.util.ArrayList;
46 import java.util.Collections;
47 import java.util.List;
48 
49 /**
50  * A fragment for channel source info/setup.
51  */
52 public 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
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)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
onEnterTransitionEnd()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
onCreateContentFragment()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
getActionCategory()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
onCreate(Bundle savedInstanceState)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
onDestroy()210         public void onDestroy() {
211             super.onDestroy();
212             mChannelDataManager.removeListener(mChannelDataManagerListener);
213             mInputManager.removeCallback(mInputCallback);
214         }
215 
216         @NonNull
217         @Override
onCreateGuidance(Bundle savedInstanceState)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
onCreateActionsStylist()225         public GuidedActionsStylist onCreateActionsStylist() {
226             return new SetupSourceGuidedActionsStylist();
227         }
228 
229         @Override
onCreateActions(@onNull List<GuidedAction> actions, Bundle savedInstanceState)230         public void onCreateActions(@NonNull List<GuidedAction> actions,
231                 Bundle savedInstanceState) {
232             createActionsInternal(actions);
233         }
234 
buildInputs()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 
updateActions()264         private void updateActions() {
265             List<GuidedAction> actions = new ArrayList<>();
266             createActionsInternal(actions);
267             setActions(actions);
268         }
269 
createActionsInternal(List<GuidedAction> actions)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
getActionCategory()341         protected String getActionCategory() {
342             return ACTION_CATEGORY;
343         }
344 
345         @Override
onGuidedActionClicked(GuidedAction action)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 
executePendingAction()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
onBindViewHolder(ViewHolder vh, GuidedAction action)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