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