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 import com.android.tv.R; 34 import com.android.tv.TvSingletons; 35 import com.android.tv.common.ui.setup.SetupGuidedStepFragment; 36 import com.android.tv.common.ui.setup.SetupMultiPaneFragment; 37 import com.android.tv.data.ChannelDataManager; 38 import com.android.tv.data.TvInputNewComparator; 39 import com.android.tv.ui.GuidedActionsStylistWithDivider; 40 import com.android.tv.util.SetupUtils; 41 import com.android.tv.util.TvInputManagerHelper; 42 import java.util.ArrayList; 43 import java.util.Collections; 44 import java.util.List; 45 46 /** A fragment for channel source info/setup. */ 47 public class SetupSourcesFragment extends SetupMultiPaneFragment { 48 /** The action category for the actions which is fired from this fragment. */ 49 public static final String ACTION_CATEGORY = "com.android.tv.onboarding.SetupSourcesFragment"; 50 /** An action to open the merchant collection. */ 51 public static final int ACTION_ONLINE_STORE = 1; 52 /** 53 * An action to show the setup activity of TV input. 54 * 55 * <p>This action is not added to the action list. This is sent outside of the fragment. Use 56 * {@link #ACTION_PARAM_KEY_INPUT_ID} to get the input ID from the parameter. 57 */ 58 public static final int ACTION_SETUP_INPUT = 2; 59 60 /** 61 * The key for the action parameter which contains the TV input ID. It's used for the action 62 * {@link #ACTION_SETUP_INPUT}. 63 */ 64 public static final String ACTION_PARAM_KEY_INPUT_ID = "input_id"; 65 66 private static final String SETUP_TRACKER_LABEL = "Setup fragment"; 67 68 @Override onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)69 public View onCreateView( 70 LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 71 View view = super.onCreateView(inflater, container, savedInstanceState); 72 TvSingletons.getSingletons(getActivity()).getTracker().sendScreenView(SETUP_TRACKER_LABEL); 73 return view; 74 } 75 76 @Override onEnterTransitionEnd()77 protected void onEnterTransitionEnd() { 78 SetupGuidedStepFragment f = getContentFragment(); 79 if (f instanceof ContentFragment) { 80 // If the enter transition is canceled quickly, the child fragment can be null because 81 // the fragment is added asynchronously. 82 ((ContentFragment) f).executePendingAction(); 83 } 84 } 85 86 @Override onCreateContentFragment()87 protected SetupGuidedStepFragment onCreateContentFragment() { 88 SetupGuidedStepFragment f = new ContentFragment(); 89 Bundle arguments = new Bundle(); 90 arguments.putBoolean(SetupGuidedStepFragment.KEY_THREE_PANE, true); 91 f.setArguments(arguments); 92 return f; 93 } 94 95 @Override getActionCategory()96 protected String getActionCategory() { 97 return ACTION_CATEGORY; 98 } 99 100 public static class ContentFragment extends SetupGuidedStepFragment { 101 // ACTION_ONLINE_STORE is defined in the outer class. 102 private static final int ACTION_HEADER = 3; 103 private static final int ACTION_INPUT_START = 4; 104 105 private static final int PENDING_ACTION_NONE = 0; 106 private static final int PENDING_ACTION_INPUT_CHANGED = 1; 107 private static final int PENDING_ACTION_CHANNEL_CHANGED = 2; 108 109 private TvInputManagerHelper mInputManager; 110 private ChannelDataManager mChannelDataManager; 111 private SetupUtils mSetupUtils; 112 private List<TvInputInfo> mInputs; 113 private int mKnownInputStartIndex; 114 private int mDoneInputStartIndex; 115 116 private SetupSourcesFragment mParentFragment; 117 118 private String mNewlyAddedInputId; 119 120 private int mPendingAction = PENDING_ACTION_NONE; 121 122 private final TvInputCallback mInputCallback = 123 new TvInputCallback() { 124 @Override 125 public void onInputAdded(String inputId) { 126 handleInputChanged(); 127 } 128 129 @Override 130 public void onInputRemoved(String inputId) { 131 handleInputChanged(); 132 } 133 134 @Override 135 public void onInputUpdated(String inputId) { 136 handleInputChanged(); 137 } 138 139 @Override 140 public void onTvInputInfoUpdated(TvInputInfo inputInfo) { 141 handleInputChanged(); 142 } 143 144 private void handleInputChanged() { 145 // The actions created while enter transition is running will not be 146 // included in the 147 // fragment transition. 148 if (mParentFragment.isEnterTransitionRunning()) { 149 mPendingAction = PENDING_ACTION_INPUT_CHANGED; 150 return; 151 } 152 buildInputs(); 153 updateActions(); 154 } 155 }; 156 157 private final ChannelDataManager.Listener mChannelDataManagerListener = 158 new ChannelDataManager.Listener() { 159 @Override 160 public void onLoadFinished() { 161 handleChannelChanged(); 162 } 163 164 @Override 165 public void onChannelListUpdated() { 166 handleChannelChanged(); 167 } 168 169 @Override 170 public void onChannelBrowsableChanged() { 171 handleChannelChanged(); 172 } 173 174 private void handleChannelChanged() { 175 // The actions created while enter transition is running will not be 176 // included in the 177 // fragment transition. 178 if (mParentFragment.isEnterTransitionRunning()) { 179 if (mPendingAction != PENDING_ACTION_INPUT_CHANGED) { 180 mPendingAction = PENDING_ACTION_CHANNEL_CHANGED; 181 } 182 return; 183 } 184 updateActions(); 185 } 186 }; 187 188 @Override onCreate(Bundle savedInstanceState)189 public void onCreate(Bundle savedInstanceState) { 190 Context context = getActivity(); 191 TvSingletons singletons = TvSingletons.getSingletons(context); 192 mInputManager = singletons.getTvInputManagerHelper(); 193 mChannelDataManager = singletons.getChannelDataManager(); 194 mSetupUtils = singletons.getSetupUtils(); 195 buildInputs(); 196 mInputManager.addCallback(mInputCallback); 197 mChannelDataManager.addListener(mChannelDataManagerListener); 198 super.onCreate(savedInstanceState); 199 mParentFragment = (SetupSourcesFragment) getParentFragment(); 200 if (singletons.getBuiltInTunerManager().isPresent()) { 201 singletons 202 .getBuiltInTunerManager() 203 .get() 204 .getTunerInputController() 205 .executeNetworkTunerDiscoveryAsyncTask(getContext()); 206 } 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( 231 @NonNull List<GuidedAction> actions, 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( 276 new GuidedAction.Builder(getActivity()) 277 .id(ACTION_HEADER) 278 .title(null) 279 .description(getString(R.string.setup_category_new)) 280 .focusable(false) 281 .infoOnly(true) 282 .build()); 283 } 284 for (int i = 0; i < mInputs.size(); ++i) { 285 if (i == mDoneInputStartIndex) { 286 ++position; 287 actions.add( 288 new GuidedAction.Builder(getActivity()) 289 .id(ACTION_HEADER) 290 .title(null) 291 .description(getString(R.string.setup_category_done)) 292 .focusable(false) 293 .infoOnly(true) 294 .build()); 295 } 296 TvInputInfo input = mInputs.get(i); 297 String inputId = input.getId(); 298 String description; 299 int channelCount = mChannelDataManager.getChannelCountForInput(inputId); 300 if (mSetupUtils.isSetupDone(inputId) || channelCount > 0) { 301 if (channelCount == 0) { 302 description = getString(R.string.setup_input_no_channels); 303 } else { 304 description = 305 getResources() 306 .getQuantityString( 307 R.plurals.setup_input_channels, 308 channelCount, 309 channelCount); 310 } 311 } else if (i >= mKnownInputStartIndex) { 312 description = getString(R.string.setup_input_setup_now); 313 } else { 314 description = getString(R.string.setup_input_new); 315 } 316 ++position; 317 if (input.getId().equals(mNewlyAddedInputId)) { 318 newPosition = position; 319 } 320 actions.add( 321 new GuidedAction.Builder(getActivity()) 322 .id(ACTION_INPUT_START + i) 323 .title(input.loadLabel(getActivity()).toString()) 324 .description(description) 325 .build()); 326 } 327 if (mInputs.size() > 0) { 328 // Divider 329 ++position; 330 actions.add(GuidedActionsStylistWithDivider.createDividerAction(getContext())); 331 } 332 // online store action 333 ++position; 334 actions.add( 335 new GuidedAction.Builder(getActivity()) 336 .id(ACTION_ONLINE_STORE) 337 .title(getString(R.string.setup_store_action_title)) 338 .description(getString(R.string.setup_store_action_description)) 339 .icon(R.drawable.ic_app_store) 340 .build()); 341 342 if (newPosition != -1) { 343 VerticalGridView gridView = getGuidedActionsStylist().getActionsGridView(); 344 gridView.setSelectedPosition(newPosition); 345 } 346 } 347 348 @Override getActionCategory()349 protected String getActionCategory() { 350 return ACTION_CATEGORY; 351 } 352 353 @Override onGuidedActionClicked(GuidedAction action)354 public void onGuidedActionClicked(GuidedAction action) { 355 if (action.getId() == ACTION_ONLINE_STORE) { 356 mParentFragment.onActionClick(ACTION_CATEGORY, (int) action.getId()); 357 return; 358 } 359 int index = (int) action.getId() - ACTION_INPUT_START; 360 if (index >= 0) { 361 TvInputInfo input = mInputs.get(index); 362 Bundle params = new Bundle(); 363 params.putString(ACTION_PARAM_KEY_INPUT_ID, input.getId()); 364 mParentFragment.onActionClick(ACTION_CATEGORY, ACTION_SETUP_INPUT, params); 365 } 366 } 367 executePendingAction()368 void executePendingAction() { 369 switch (mPendingAction) { 370 case PENDING_ACTION_INPUT_CHANGED: 371 buildInputs(); 372 // Fall through 373 case PENDING_ACTION_CHANNEL_CHANGED: 374 updateActions(); 375 break; 376 default: // fall out 377 } 378 mPendingAction = PENDING_ACTION_NONE; 379 } 380 381 private class SetupSourceGuidedActionsStylist extends GuidedActionsStylistWithDivider { 382 private static final float ALPHA_CATEGORY = 1.0f; 383 private static final float ALPHA_INPUT_DESCRIPTION = 0.5f; 384 385 @Override onBindViewHolder(ViewHolder vh, GuidedAction action)386 public void onBindViewHolder(ViewHolder vh, GuidedAction action) { 387 super.onBindViewHolder(vh, action); 388 TextView descriptionView = vh.getDescriptionView(); 389 if (descriptionView != null) { 390 if (action.getId() == ACTION_HEADER) { 391 descriptionView.setAlpha(ALPHA_CATEGORY); 392 descriptionView.setTextColor( 393 getResources().getColor(R.color.setup_category, null)); 394 descriptionView.setTypeface( 395 Typeface.create(getString(R.string.condensed_font), 0)); 396 } else { 397 descriptionView.setAlpha(ALPHA_INPUT_DESCRIPTION); 398 descriptionView.setTextColor( 399 getResources() 400 .getColor(R.color.common_setup_input_description, null)); 401 descriptionView.setTypeface(Typeface.create(getString(R.string.font), 0)); 402 } 403 } 404 setAccessibilityDelegate(vh, action); 405 } 406 } 407 } 408 } 409