SetupSourcesFragment.java revision 2c1070775c30662ded2b01cc7d7138436d9a2af2
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.Context; 20import android.graphics.Typeface; 21import android.media.tv.TvInputInfo; 22import android.media.tv.TvInputManager.TvInputCallback; 23import android.os.Bundle; 24import android.support.annotation.NonNull; 25import android.support.v17.leanback.widget.GuidanceStylist.Guidance; 26import android.support.v17.leanback.widget.GuidedAction; 27import android.support.v17.leanback.widget.GuidedActionsStylist; 28import android.support.v17.leanback.widget.VerticalGridView; 29import android.view.LayoutInflater; 30import android.view.View; 31import android.view.View.AccessibilityDelegate; 32import android.view.ViewGroup; 33import android.view.accessibility.AccessibilityEvent; 34import android.view.accessibility.AccessibilityNodeInfo; 35import android.widget.TextView; 36import com.android.tv.R; 37import com.android.tv.TvSingletons; 38import com.android.tv.common.ui.setup.SetupGuidedStepFragment; 39import com.android.tv.common.ui.setup.SetupMultiPaneFragment; 40import com.android.tv.data.ChannelDataManager; 41import com.android.tv.data.TvInputNewComparator; 42import com.android.tv.ui.GuidedActionsStylistWithDivider; 43import com.android.tv.util.SetupUtils; 44import com.android.tv.util.TvInputManagerHelper; 45import java.util.ArrayList; 46import java.util.Collections; 47import java.util.List; 48 49/** A fragment for channel source info/setup. */ 50public class SetupSourcesFragment extends SetupMultiPaneFragment { 51 /** The action category for the actions which is fired from this fragment. */ 52 public static final String ACTION_CATEGORY = "com.android.tv.onboarding.SetupSourcesFragment"; 53 /** An action to open the merchant collection. */ 54 public static final int ACTION_ONLINE_STORE = 1; 55 /** 56 * An action to show the setup activity of TV input. 57 * 58 * <p>This action is not added to the action list. This is sent outside of the fragment. Use 59 * {@link #ACTION_PARAM_KEY_INPUT_ID} to get the input ID from the parameter. 60 */ 61 public static final int ACTION_SETUP_INPUT = 2; 62 63 /** 64 * The key for the action parameter which contains the TV input ID. It's used for the action 65 * {@link #ACTION_SETUP_INPUT}. 66 */ 67 public static final String ACTION_PARAM_KEY_INPUT_ID = "input_id"; 68 69 private static final String SETUP_TRACKER_LABEL = "Setup fragment"; 70 71 @Override 72 public View onCreateView( 73 LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 74 View view = super.onCreateView(inflater, container, savedInstanceState); 75 TvSingletons.getSingletons(getActivity()).getTracker().sendScreenView(SETUP_TRACKER_LABEL); 76 return view; 77 } 78 79 @Override 80 protected void onEnterTransitionEnd() { 81 SetupGuidedStepFragment f = getContentFragment(); 82 if (f instanceof ContentFragment) { 83 // If the enter transition is canceled quickly, the child fragment can be null because 84 // the fragment is added asynchronously. 85 ((ContentFragment) f).executePendingAction(); 86 } 87 } 88 89 @Override 90 protected SetupGuidedStepFragment onCreateContentFragment() { 91 SetupGuidedStepFragment f = new ContentFragment(); 92 Bundle arguments = new Bundle(); 93 arguments.putBoolean(SetupGuidedStepFragment.KEY_THREE_PANE, true); 94 f.setArguments(arguments); 95 return f; 96 } 97 98 @Override 99 protected String getActionCategory() { 100 return ACTION_CATEGORY; 101 } 102 103 public static class ContentFragment extends SetupGuidedStepFragment { 104 // ACTION_ONLINE_STORE is defined in the outer class. 105 private static final int ACTION_HEADER = 3; 106 private static final int ACTION_INPUT_START = 4; 107 108 private static final int PENDING_ACTION_NONE = 0; 109 private static final int PENDING_ACTION_INPUT_CHANGED = 1; 110 private static final int PENDING_ACTION_CHANNEL_CHANGED = 2; 111 112 private TvInputManagerHelper mInputManager; 113 private ChannelDataManager mChannelDataManager; 114 private SetupUtils mSetupUtils; 115 private List<TvInputInfo> mInputs; 116 private int mKnownInputStartIndex; 117 private int mDoneInputStartIndex; 118 119 private SetupSourcesFragment mParentFragment; 120 121 private String mNewlyAddedInputId; 122 123 private int mPendingAction = PENDING_ACTION_NONE; 124 125 private final TvInputCallback mInputCallback = 126 new TvInputCallback() { 127 @Override 128 public void onInputAdded(String inputId) { 129 handleInputChanged(); 130 } 131 132 @Override 133 public void onInputRemoved(String inputId) { 134 handleInputChanged(); 135 } 136 137 @Override 138 public void onInputUpdated(String inputId) { 139 handleInputChanged(); 140 } 141 142 @Override 143 public void onTvInputInfoUpdated(TvInputInfo inputInfo) { 144 handleInputChanged(); 145 } 146 147 private void handleInputChanged() { 148 // The actions created while enter transition is running will not be 149 // included in the 150 // fragment transition. 151 if (mParentFragment.isEnterTransitionRunning()) { 152 mPendingAction = PENDING_ACTION_INPUT_CHANGED; 153 return; 154 } 155 buildInputs(); 156 updateActions(); 157 } 158 }; 159 160 private final ChannelDataManager.Listener mChannelDataManagerListener = 161 new ChannelDataManager.Listener() { 162 @Override 163 public void onLoadFinished() { 164 handleChannelChanged(); 165 } 166 167 @Override 168 public void onChannelListUpdated() { 169 handleChannelChanged(); 170 } 171 172 @Override 173 public void onChannelBrowsableChanged() { 174 handleChannelChanged(); 175 } 176 177 private void handleChannelChanged() { 178 // The actions created while enter transition is running will not be 179 // included in the 180 // fragment transition. 181 if (mParentFragment.isEnterTransitionRunning()) { 182 if (mPendingAction != PENDING_ACTION_INPUT_CHANGED) { 183 mPendingAction = PENDING_ACTION_CHANNEL_CHANGED; 184 } 185 return; 186 } 187 updateActions(); 188 } 189 }; 190 191 @Override 192 public void onCreate(Bundle savedInstanceState) { 193 Context context = getActivity(); 194 TvSingletons singletons = TvSingletons.getSingletons(context); 195 mInputManager = singletons.getTvInputManagerHelper(); 196 mChannelDataManager = singletons.getChannelDataManager(); 197 mSetupUtils = singletons.getSetupUtils(); 198 buildInputs(); 199 mInputManager.addCallback(mInputCallback); 200 mChannelDataManager.addListener(mChannelDataManagerListener); 201 super.onCreate(savedInstanceState); 202 mParentFragment = (SetupSourcesFragment) getParentFragment(); 203 singletons 204 .getTunerInputController() 205 .executeNetworkTunerDiscoveryAsyncTask(getContext()); 206 } 207 208 @Override 209 public void onDestroy() { 210 super.onDestroy(); 211 mChannelDataManager.removeListener(mChannelDataManagerListener); 212 mInputManager.removeCallback(mInputCallback); 213 } 214 215 @NonNull 216 @Override 217 public Guidance onCreateGuidance(Bundle savedInstanceState) { 218 String title = getString(R.string.setup_sources_text); 219 String description = getString(R.string.setup_sources_description); 220 return new Guidance(title, description, null, null); 221 } 222 223 @Override 224 public GuidedActionsStylist onCreateActionsStylist() { 225 return new SetupSourceGuidedActionsStylist(); 226 } 227 228 @Override 229 public void onCreateActions( 230 @NonNull List<GuidedAction> actions, Bundle savedInstanceState) { 231 createActionsInternal(actions); 232 } 233 234 private void buildInputs() { 235 List<TvInputInfo> oldInputs = mInputs; 236 mInputs = mInputManager.getTvInputInfos(true, true); 237 // Get newly installed input ID. 238 if (oldInputs != null) { 239 List<TvInputInfo> newList = new ArrayList<>(mInputs); 240 for (TvInputInfo input : oldInputs) { 241 newList.remove(input); 242 } 243 if (newList.size() > 0 && mSetupUtils.isNewInput(newList.get(0).getId())) { 244 mNewlyAddedInputId = newList.get(0).getId(); 245 } else { 246 mNewlyAddedInputId = null; 247 } 248 } 249 Collections.sort(mInputs, new TvInputNewComparator(mSetupUtils, mInputManager)); 250 mKnownInputStartIndex = 0; 251 mDoneInputStartIndex = 0; 252 for (TvInputInfo input : mInputs) { 253 if (mSetupUtils.isNewInput(input.getId())) { 254 mSetupUtils.markAsKnownInput(input.getId()); 255 ++mKnownInputStartIndex; 256 } 257 if (!mSetupUtils.isSetupDone(input.getId())) { 258 ++mDoneInputStartIndex; 259 } 260 } 261 } 262 263 private void updateActions() { 264 List<GuidedAction> actions = new ArrayList<>(); 265 createActionsInternal(actions); 266 setActions(actions); 267 } 268 269 private void createActionsInternal(List<GuidedAction> actions) { 270 int newPosition = -1; 271 int position = 0; 272 if (mDoneInputStartIndex > 0) { 273 // Need a "New" category 274 actions.add( 275 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( 287 new GuidedAction.Builder(getActivity()) 288 .id(ACTION_HEADER) 289 .title(null) 290 .description(getString(R.string.setup_category_done)) 291 .focusable(false) 292 .infoOnly(true) 293 .build()); 294 } 295 TvInputInfo input = mInputs.get(i); 296 String inputId = input.getId(); 297 String description; 298 int channelCount = mChannelDataManager.getChannelCountForInput(inputId); 299 if (mSetupUtils.isSetupDone(inputId) || channelCount > 0) { 300 if (channelCount == 0) { 301 description = getString(R.string.setup_input_no_channels); 302 } else { 303 description = 304 getResources() 305 .getQuantityString( 306 R.plurals.setup_input_channels, 307 channelCount, 308 channelCount); 309 } 310 } else if (i >= mKnownInputStartIndex) { 311 description = getString(R.string.setup_input_setup_now); 312 } else { 313 description = getString(R.string.setup_input_new); 314 } 315 ++position; 316 if (input.getId().equals(mNewlyAddedInputId)) { 317 newPosition = position; 318 } 319 actions.add( 320 new GuidedAction.Builder(getActivity()) 321 .id(ACTION_INPUT_START + i) 322 .title(input.loadLabel(getActivity()).toString()) 323 .description(description) 324 .build()); 325 } 326 if (mInputs.size() > 0) { 327 // Divider 328 ++position; 329 actions.add(GuidedActionsStylistWithDivider.createDividerAction(getContext())); 330 } 331 // online store action 332 ++position; 333 actions.add( 334 new GuidedAction.Builder(getActivity()) 335 .id(ACTION_ONLINE_STORE) 336 .title(getString(R.string.setup_store_action_title)) 337 .description(getString(R.string.setup_store_action_description)) 338 .icon(R.drawable.ic_store) 339 .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_ONLINE_STORE) { 355 mParentFragment.onActionClick(ACTION_CATEGORY, (int) action.getId()); 356 return; 357 } 358 int index = (int) action.getId() - ACTION_INPUT_START; 359 if (index >= 0) { 360 TvInputInfo input = mInputs.get(index); 361 Bundle params = new Bundle(); 362 params.putString(ACTION_PARAM_KEY_INPUT_ID, input.getId()); 363 mParentFragment.onActionClick(ACTION_CATEGORY, ACTION_SETUP_INPUT, params); 364 } 365 } 366 367 void executePendingAction() { 368 switch (mPendingAction) { 369 case PENDING_ACTION_INPUT_CHANGED: 370 buildInputs(); 371 // Fall through 372 case PENDING_ACTION_CHANNEL_CHANGED: 373 updateActions(); 374 break; 375 default: // fall out 376 } 377 mPendingAction = PENDING_ACTION_NONE; 378 } 379 380 private class SetupSourceGuidedActionsStylist extends GuidedActionsStylistWithDivider { 381 private static final float ALPHA_CATEGORY = 1.0f; 382 private static final float ALPHA_INPUT_DESCRIPTION = 0.5f; 383 384 @Override 385 public void onBindViewHolder(ViewHolder vh, GuidedAction action) { 386 super.onBindViewHolder(vh, action); 387 TextView descriptionView = vh.getDescriptionView(); 388 if (descriptionView != null) { 389 if (action.getId() == ACTION_HEADER) { 390 descriptionView.setAlpha(ALPHA_CATEGORY); 391 descriptionView.setTextColor( 392 getResources().getColor(R.color.setup_category, null)); 393 descriptionView.setTypeface( 394 Typeface.create(getString(R.string.condensed_font), 0)); 395 } else { 396 descriptionView.setAlpha(ALPHA_INPUT_DESCRIPTION); 397 descriptionView.setTextColor( 398 getResources() 399 .getColor(R.color.common_setup_input_description, null)); 400 descriptionView.setTypeface(Typeface.create(getString(R.string.font), 0)); 401 } 402 } 403 if (!getAccessibilityMode() || findActionPositionById(action.getId()) == 0) { 404 return; 405 } 406 vh.itemView.setAccessibilityDelegate( 407 new AccessibilityDelegate() { 408 @Override 409 public boolean performAccessibilityAction(View host, int action, 410 Bundle args) { 411 if ((action == AccessibilityNodeInfo.ACTION_FOCUS 412 || action == AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS) 413 && getFromContentFragment()) { 414 // block the action and make the first action view accessibility 415 // focused 416 View view = getActionItemView(0); 417 if (view != null) { 418 view.sendAccessibilityEvent( 419 AccessibilityEvent.TYPE_VIEW_FOCUSED); 420 setFromContentFragment(false); 421 return true; 422 } 423 } 424 return super.performAccessibilityAction(host, action, args); 425 } 426 }); 427 } 428 } 429 } 430} 431