SetupSourcesFragment.java revision 3a72b93e554bd22a5c64e71a6956d9604ce05108
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.ActivityNotFoundException; 20import android.content.ComponentName; 21import android.content.Context; 22import android.content.Intent; 23import android.graphics.Typeface; 24import android.graphics.drawable.Drawable; 25import android.media.tv.TvInputInfo; 26import android.media.tv.TvInputManager.TvInputCallback; 27import android.os.Bundle; 28import android.support.annotation.NonNull; 29import android.support.v17.leanback.widget.GuidanceStylist.Guidance; 30import android.support.v17.leanback.widget.GuidedAction; 31import android.support.v17.leanback.widget.GuidedActionsStylist; 32import android.support.v17.leanback.widget.VerticalGridView; 33import android.view.ContextThemeWrapper; 34import android.view.LayoutInflater; 35import android.view.View; 36import android.view.ViewGroup; 37import android.widget.ImageView; 38import android.widget.TextView; 39import android.widget.Toast; 40 41import com.android.tv.ApplicationSingletons; 42import com.android.tv.Features; 43import com.android.tv.R; 44import com.android.tv.SetupPassthroughActivity; 45import com.android.tv.TvApplication; 46import com.android.tv.common.TvCommonUtils; 47import com.android.tv.common.ui.setup.SetupGuidedStepFragment; 48import com.android.tv.common.ui.setup.SetupMultiPaneFragment; 49import com.android.tv.data.ChannelDataManager; 50import com.android.tv.data.TvInputNewComparator; 51import com.android.tv.util.SetupUtils; 52import com.android.tv.util.TvInputManagerHelper; 53import com.android.tv.util.Utils; 54 55import java.util.ArrayList; 56import java.util.Collections; 57import java.util.List; 58 59/** 60 * A fragment for channel source info/setup. 61 */ 62public class SetupSourcesFragment extends SetupMultiPaneFragment { 63 public static final String ACTION_CATEGORY = 64 "com.android.tv.onboarding.SetupSourcesFragment"; 65 public static final int ACTION_PLAY_STORE = 1; 66 67 public static final int DEFAULT_THEME = -1; 68 69 private static final String SETUP_TRACKER_LABEL = "Setup fragment"; 70 71 private static int sTheme = DEFAULT_THEME; 72 73 private InputSetupRunnable mInputSetupRunnable; 74 75 private ContentFragment mContentFragment; 76 77 @Override 78 public View onCreateView(LayoutInflater inflater, ViewGroup container, 79 Bundle savedInstanceState) { 80 LayoutInflater localInflater = inflater; 81 if (sTheme != -1) { 82 ContextThemeWrapper themeWrapper = new ContextThemeWrapper(getActivity(), sTheme); 83 localInflater = inflater.cloneInContext(themeWrapper); 84 } 85 View view = super.onCreateView(localInflater, container, savedInstanceState); 86 TvApplication.getSingletons(getActivity()).getTracker().sendScreenView(SETUP_TRACKER_LABEL); 87 return view; 88 } 89 90 @Override 91 protected void onEnterTransitionEnd() { 92 if (mContentFragment != null) { 93 mContentFragment.executePendingAction(); 94 } 95 } 96 97 @Override 98 protected SetupGuidedStepFragment onCreateContentFragment() { 99 mContentFragment = new ContentFragment(); 100 Bundle arguments = new Bundle(); 101 arguments.putBoolean(SetupGuidedStepFragment.KEY_THREE_PANE, true); 102 mContentFragment.setArguments(arguments); 103 mContentFragment.setParentFragment(this); 104 return mContentFragment; 105 } 106 107 @Override 108 protected String getActionCategory() { 109 return ACTION_CATEGORY; 110 } 111 112 /** 113 * Sets the custom theme dynamically. 114 */ 115 public static void setTheme(int theme) { 116 sTheme = theme; 117 } 118 119 /** 120 * Call this method to run customized input setup. 121 * 122 * @param runnable runnable to be called when the input setup is necessary. 123 */ 124 public void setInputSetupRunnable(InputSetupRunnable runnable) { 125 mInputSetupRunnable = runnable; 126 } 127 128 /** 129 * Interface for the customized input setup. 130 */ 131 public interface InputSetupRunnable { 132 /** 133 * Called for the input setup. 134 * 135 * @param input TV input for setup. 136 */ 137 void runInputSetup(TvInputInfo input); 138 } 139 140 public static class ContentFragment extends SetupGuidedStepFragment { 141 private static final int REQUEST_CODE_START_SETUP_ACTIVITY = 1; 142 143 // ACTION_PLAY_STORE is defined in the outer class. 144 private static final int ACTION_DIVIDER = 2; 145 private static final int ACTION_HEADER = 3; 146 private static final int ACTION_INPUT_START = 4; 147 148 private static final int PENDING_ACTION_NONE = 0; 149 private static final int PENDING_ACTION_INPUT_CHANGED = 1; 150 private static final int PENDING_ACTION_CHANNEL_CHANGED = 2; 151 152 private TvInputManagerHelper mInputManager; 153 private ChannelDataManager mChannelDataManager; 154 private SetupUtils mSetupUtils; 155 private List<TvInputInfo> mInputs; 156 private int mKnownInputStartIndex; 157 private int mDoneInputStartIndex; 158 159 private SetupSourcesFragment mParentFragment; 160 161 private String mNewlyAddedInputId; 162 163 private int mPendingAction = PENDING_ACTION_NONE; 164 165 private final TvInputCallback mInputCallback = new TvInputCallback() { 166 @Override 167 public void onInputAdded(String inputId) { 168 handleInputChanged(); 169 } 170 171 @Override 172 public void onInputRemoved(String inputId) { 173 handleInputChanged(); 174 } 175 176 private void handleInputChanged() { 177 // The actions created while enter transition is running will not be included in the 178 // fragment transition. 179 if (mParentFragment.isEnterTransitionRunning()) { 180 mPendingAction = PENDING_ACTION_INPUT_CHANGED; 181 return; 182 } 183 buildInputs(); 184 updateActions(); 185 } 186 }; 187 188 void setParentFragment(SetupSourcesFragment parentFragment) { 189 mParentFragment = parentFragment; 190 } 191 192 private final ChannelDataManager.Listener mChannelDataManagerListener 193 = new ChannelDataManager.Listener() { 194 @Override 195 public void onLoadFinished() { 196 handleChannelChanged(); 197 } 198 199 @Override 200 public void onChannelListUpdated() { 201 handleChannelChanged(); 202 } 203 204 @Override 205 public void onChannelBrowsableChanged() { 206 handleChannelChanged(); 207 } 208 209 private void handleChannelChanged() { 210 // The actions created while enter transition is running will not be included in the 211 // fragment transition. 212 if (mParentFragment.isEnterTransitionRunning()) { 213 if (mPendingAction != PENDING_ACTION_INPUT_CHANGED) { 214 mPendingAction = PENDING_ACTION_CHANNEL_CHANGED; 215 } 216 return; 217 } 218 updateActions(); 219 } 220 }; 221 222 @Override 223 public void onCreate(Bundle savedInstanceState) { 224 // TODO: Handle USB TV tuner differently. 225 Context context = getActivity(); 226 ApplicationSingletons app = TvApplication.getSingletons(context); 227 mInputManager = app.getTvInputManagerHelper(); 228 mChannelDataManager = app.getChannelDataManager(); 229 mSetupUtils = SetupUtils.getInstance(context); 230 buildInputs(); 231 mInputManager.addCallback(mInputCallback); 232 mChannelDataManager.addListener(mChannelDataManagerListener); 233 super.onCreate(savedInstanceState); 234 } 235 236 @Override 237 public void onDestroy() { 238 super.onDestroy(); 239 mChannelDataManager.removeListener(mChannelDataManagerListener); 240 mInputManager.removeCallback(mInputCallback); 241 } 242 243 @NonNull 244 @Override 245 public Guidance onCreateGuidance(Bundle savedInstanceState) { 246 String title = getString(R.string.setup_sources_text); 247 String description = getString(R.string.setup_sources_description); 248 return new Guidance(title, description, null, null); 249 } 250 251 @Override 252 public GuidedActionsStylist onCreateActionsStylist() { 253 return new SetupSourceGuidedActionsStylist(); 254 } 255 256 @Override 257 public void onCreateActions(@NonNull List<GuidedAction> actions, 258 Bundle savedInstanceState) { 259 createActionsInternal(actions); 260 } 261 262 private void buildInputs() { 263 List<TvInputInfo> oldInputs = mInputs; 264 mInputs = mInputManager.getTvInputInfos(true, true); 265 // Get newly installed input ID. 266 if (oldInputs != null) { 267 List<TvInputInfo> newList = new ArrayList<>(mInputs); 268 for (TvInputInfo input : oldInputs) { 269 newList.remove(input); 270 } 271 if (newList.size() > 0 && mSetupUtils.isNewInput(newList.get(0).getId())) { 272 mNewlyAddedInputId = newList.get(0).getId(); 273 } else { 274 mNewlyAddedInputId = null; 275 } 276 } 277 Collections.sort(mInputs, new TvInputNewComparator(mSetupUtils, mInputManager)); 278 mKnownInputStartIndex = 0; 279 mDoneInputStartIndex = 0; 280 for (TvInputInfo input : mInputs) { 281 if (mSetupUtils.isNewInput(input.getId())) { 282 mSetupUtils.markAsKnownInput(input.getId()); 283 ++mKnownInputStartIndex; 284 } 285 if (!mSetupUtils.isSetupDone(input.getId())) { 286 ++mDoneInputStartIndex; 287 } 288 } 289 } 290 291 private void updateActions() { 292 List<GuidedAction> actions = new ArrayList<>(); 293 createActionsInternal(actions); 294 setActions(actions); 295 } 296 297 private void createActionsInternal(List<GuidedAction> actions) { 298 int newPosition = -1; 299 int position = 0; 300 if (mDoneInputStartIndex > 0) { 301 // Need a "New" category 302 actions.add(new GuidedAction.Builder(getActivity()).id(ACTION_HEADER) 303 .title(null).description(getString(R.string.setup_category_new)) 304 .focusable(false).build()); 305 } 306 for (int i = 0; i < mInputs.size(); ++i) { 307 if (i == mDoneInputStartIndex) { 308 ++position; 309 actions.add(new GuidedAction.Builder(getActivity()).id(ACTION_HEADER) 310 .title(null).description(getString(R.string.setup_category_done)) 311 .focusable(false).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 = getResources().getQuantityString( 322 R.plurals.setup_input_channels, channelCount, channelCount); 323 } 324 } else if (i >= mKnownInputStartIndex) { 325 description = getString(R.string.setup_input_setup_now); 326 } else { 327 description = getString(R.string.setup_input_new); 328 } 329 ++position; 330 if (input.getId().equals(mNewlyAddedInputId)) { 331 newPosition = position; 332 } 333 actions.add(new GuidedAction.Builder(getActivity()).id(ACTION_INPUT_START + i) 334 .title(input.loadLabel(getActivity()).toString()).description(description) 335 .build()); 336 } 337 if (Features.ONBOARDING_PLAY_STORE.isEnabled(getActivity())) { 338 if (mInputs.size() > 0) { 339 // Divider 340 ++position; 341 actions.add(new GuidedAction.Builder(getActivity()).id(ACTION_DIVIDER) 342 .title(null).description(null).focusable(false).build()); 343 } 344 // Play store action 345 ++position; 346 actions.add(new GuidedAction.Builder(getActivity()).id(ACTION_PLAY_STORE) 347 .title(getString(R.string.setup_play_store_action_title)) 348 .description(getString(R.string.setup_play_store_action_description)) 349 .icon(R.drawable.ic_playstore).build()); 350 } 351 if (newPosition != -1) { 352 VerticalGridView gridView = getGuidedActionsStylist().getActionsGridView(); 353 gridView.setSelectedPosition(newPosition); 354 } 355 } 356 357 @Override 358 protected String getActionCategory() { 359 return ACTION_CATEGORY; 360 } 361 362 @Override 363 public void onGuidedActionClicked(GuidedAction action) { 364 if (action.getId() == ACTION_PLAY_STORE) { 365 mParentFragment.onActionClick(ACTION_CATEGORY, (int) action.getId()); 366 return; 367 } 368 TvInputInfo input = mInputs.get((int) action.getId() - ACTION_INPUT_START); 369 if (mParentFragment.mInputSetupRunnable != null) { 370 mParentFragment.mInputSetupRunnable.runInputSetup(input); 371 return; 372 } 373 Intent intent = TvCommonUtils.createSetupIntent(input); 374 if (intent == null) { 375 Toast.makeText(getActivity(), R.string.msg_no_setup_activity, Toast.LENGTH_SHORT) 376 .show(); 377 return; 378 } 379 // Even though other app can handle the intent, the setup launched by Live channels 380 // should go through Live channels SetupPassthroughActivity. 381 intent.setComponent(new ComponentName(getActivity(), SetupPassthroughActivity.class)); 382 try { 383 // Now we know that the user intends to set up this input. Grant permission for 384 // writing EPG data. 385 SetupUtils.grantEpgPermission(getActivity(), input.getServiceInfo().packageName); 386 startActivityForResult(intent, REQUEST_CODE_START_SETUP_ACTIVITY); 387 } catch (ActivityNotFoundException e) { 388 Toast.makeText(getActivity(), getString(R.string.msg_unable_to_start_setup_activity, 389 input.loadLabel(getActivity())), Toast.LENGTH_SHORT).show(); 390 } 391 } 392 393 @Override 394 public void onActivityResult(int requestCode, int resultCode, Intent data) { 395 updateActions(); 396 } 397 398 @Override 399 public int onProvideTheme() { 400 return sTheme == DEFAULT_THEME ? super.onProvideTheme() : sTheme; 401 } 402 403 void executePendingAction() { 404 switch (mPendingAction) { 405 case PENDING_ACTION_INPUT_CHANGED: 406 buildInputs(); 407 // Fall through 408 case PENDING_ACTION_CHANNEL_CHANGED: 409 updateActions(); 410 break; 411 } 412 mPendingAction = PENDING_ACTION_NONE; 413 } 414 415 private class SetupSourceGuidedActionsStylist extends GuidedActionsStylist { 416 private static final int VIEW_TYPE_DIVIDER = 1; 417 418 private static final float ALPHA_CATEGORY = 1.0f; 419 private static final float ALPHA_INPUT_DESCRIPTION = 0.5f; 420 421 @Override 422 public int getItemViewType(GuidedAction action) { 423 if (action.getId() == ACTION_DIVIDER) { 424 return VIEW_TYPE_DIVIDER; 425 } 426 return super.getItemViewType(action); 427 } 428 429 @Override 430 public int onProvideItemLayoutId(int viewType) { 431 if (viewType == VIEW_TYPE_DIVIDER) { 432 return R.layout.onboarding_item_divider; 433 } 434 return super.onProvideItemLayoutId(viewType); 435 } 436 437 @Override 438 public void onBindViewHolder(ViewHolder vh, GuidedAction action) { 439 super.onBindViewHolder(vh, action); 440 TextView descriptionView = vh.getDescriptionView(); 441 if (descriptionView != null) { 442 if (action.getId() == ACTION_HEADER) { 443 descriptionView.setAlpha(ALPHA_CATEGORY); 444 descriptionView.setTextColor(Utils.getColor(getResources(), 445 R.color.setup_category)); 446 descriptionView.setTypeface(Typeface.create( 447 getString(R.string.condensed_font), 0)); 448 } else { 449 descriptionView.setAlpha(ALPHA_INPUT_DESCRIPTION); 450 descriptionView.setTextColor(Utils.getColor(getResources(), 451 R.color.common_setup_input_description)); 452 descriptionView.setTypeface(Typeface.create(getString(R.string.font), 0)); 453 } 454 } 455 // Workaround for b/26473407. 456 ImageView iconView = vh.getIconView(); 457 if (iconView != null) { 458 Drawable icon = action.getIcon(); 459 if (icon != null) { 460 // setImageDrawable resets the drawable's level unless we set the view level 461 // first. 462 iconView.setImageLevel(icon.getLevel()); 463 iconView.setImageDrawable(icon); 464 iconView.setVisibility(View.VISIBLE); 465 } else { 466 iconView.setVisibility(View.GONE); 467 } 468 } 469 } 470 } 471 } 472} 473