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