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