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.ActivityNotFoundException;
20 import android.content.ComponentName;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.graphics.Typeface;
24 import android.graphics.drawable.Drawable;
25 import android.media.tv.TvInputInfo;
26 import android.media.tv.TvInputManager.TvInputCallback;
27 import android.os.Bundle;
28 import android.support.annotation.NonNull;
29 import android.support.v17.leanback.widget.GuidanceStylist.Guidance;
30 import android.support.v17.leanback.widget.GuidedAction;
31 import android.support.v17.leanback.widget.GuidedActionsStylist;
32 import android.support.v17.leanback.widget.VerticalGridView;
33 import android.view.LayoutInflater;
34 import android.view.View;
35 import android.view.ViewGroup;
36 import android.widget.ImageView;
37 import android.widget.TextView;
38 import android.widget.Toast;
39 
40 import com.android.tv.ApplicationSingletons;
41 import com.android.tv.Features;
42 import com.android.tv.R;
43 import com.android.tv.SetupPassthroughActivity;
44 import com.android.tv.TvApplication;
45 import com.android.tv.common.TvCommonUtils;
46 import com.android.tv.common.ui.setup.SetupGuidedStepFragment;
47 import com.android.tv.common.ui.setup.SetupMultiPaneFragment;
48 import com.android.tv.data.ChannelDataManager;
49 import com.android.tv.data.TvInputNewComparator;
50 import com.android.tv.util.SetupUtils;
51 import com.android.tv.util.TvInputManagerHelper;
52 import com.android.tv.util.Utils;
53 
54 import java.util.ArrayList;
55 import java.util.Collections;
56 import java.util.List;
57 
58 /**
59  * A fragment for channel source info/setup.
60  */
61 public 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
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)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
onEnterTransitionEnd()83     protected void onEnterTransitionEnd() {
84         if (mContentFragment != null) {
85             mContentFragment.executePendingAction();
86         }
87     }
88 
89     @Override
onCreateContentFragment()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
getActionCategory()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      */
setInputSetupRunnable(InputSetupRunnable runnable)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          */
runInputSetup(TvInputInfo input)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 
setParentFragment(SetupSourcesFragment parentFragment)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
onCreate(Bundle savedInstanceState)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
onDestroy()227         public void onDestroy() {
228             super.onDestroy();
229             mChannelDataManager.removeListener(mChannelDataManagerListener);
230             mInputManager.removeCallback(mInputCallback);
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_description);
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(@NonNull List<GuidedAction> actions,
248                 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(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
getActionCategory()348         protected String getActionCategory() {
349             return ACTION_CATEGORY;
350         }
351 
352         @Override
onGuidedActionClicked(GuidedAction action)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
onActivityResult(int requestCode, int resultCode, Intent data)384         public void onActivityResult(int requestCode, int resultCode, Intent data) {
385             updateActions();
386         }
387 
executePendingAction()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
getItemViewType(GuidedAction action)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
onProvideItemLayoutId(int viewType)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
onBindViewHolder(ViewHolder vh, GuidedAction action)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