SetupUtils.java revision 3dfa929b24f38ac7836450176d88ceab41dc6ac5
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.util; 18 19import android.content.ComponentName; 20import android.content.Context; 21import android.content.Intent; 22import android.content.SharedPreferences; 23import android.content.pm.PackageManager; 24import android.content.pm.PackageManager.NameNotFoundException; 25import android.media.tv.TvContract; 26import android.media.tv.TvInputInfo; 27import android.media.tv.TvInputManager; 28import android.preference.PreferenceManager; 29import android.support.annotation.Nullable; 30import android.support.annotation.UiThread; 31import android.text.TextUtils; 32import android.util.ArraySet; 33import android.util.Log; 34 35import com.android.tv.ApplicationSingletons; 36import com.android.tv.TvApplication; 37import com.android.tv.common.SoftPreconditions; 38import com.android.tv.data.Channel; 39import com.android.tv.data.ChannelDataManager; 40import com.android.tv.data.epg.EpgFetcher; 41import com.android.tv.experiments.Experiments; 42import com.android.tv.tuner.tvinput.TunerTvInputService; 43 44import java.util.Collections; 45import java.util.HashSet; 46import java.util.Set; 47 48/** 49 * A utility class related to input setup. 50 */ 51public class SetupUtils { 52 private static final String TAG = "SetupUtils"; 53 private static final boolean DEBUG = false; 54 55 // Known inputs are inputs which are shown in SetupView before. When a new input is installed, 56 // the input will not be included in "PREF_KEY_KNOWN_INPUTS". 57 private static final String PREF_KEY_KNOWN_INPUTS = "known_inputs"; 58 // Set up inputs are inputs whose setup activity has been launched and finished successfully. 59 private static final String PREF_KEY_SET_UP_INPUTS = "set_up_inputs"; 60 // Recognized inputs means that the user already knows the inputs are installed. 61 private static final String PREF_KEY_RECOGNIZED_INPUTS = "recognized_inputs"; 62 private static final String PREF_KEY_IS_FIRST_TUNE = "is_first_tune"; 63 private static SetupUtils sSetupUtils; 64 65 private final TvApplication mTvApplication; 66 private final SharedPreferences mSharedPreferences; 67 private final Set<String> mKnownInputs; 68 private final Set<String> mSetUpInputs; 69 private final Set<String> mRecognizedInputs; 70 private boolean mIsFirstTune; 71 private final String mTunerInputId; 72 73 private SetupUtils(TvApplication tvApplication) { 74 mTvApplication = tvApplication; 75 mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(tvApplication); 76 mSetUpInputs = new ArraySet<>(); 77 mSetUpInputs.addAll(mSharedPreferences.getStringSet(PREF_KEY_SET_UP_INPUTS, 78 Collections.emptySet())); 79 mKnownInputs = new ArraySet<>(); 80 mKnownInputs.addAll(mSharedPreferences.getStringSet(PREF_KEY_KNOWN_INPUTS, 81 Collections.emptySet())); 82 mRecognizedInputs = new ArraySet<>(); 83 mRecognizedInputs.addAll(mSharedPreferences.getStringSet(PREF_KEY_RECOGNIZED_INPUTS, 84 mKnownInputs)); 85 mIsFirstTune = mSharedPreferences.getBoolean(PREF_KEY_IS_FIRST_TUNE, true); 86 mTunerInputId = TvContract.buildInputId(new ComponentName(tvApplication, 87 TunerTvInputService.class)); 88 } 89 90 /** 91 * Gets an instance of {@link SetupUtils}. 92 */ 93 public static SetupUtils getInstance(Context context) { 94 if (sSetupUtils != null) { 95 return sSetupUtils; 96 } 97 sSetupUtils = new SetupUtils((TvApplication) context.getApplicationContext()); 98 return sSetupUtils; 99 } 100 101 /** 102 * Additional work after the setup of TV input. 103 */ 104 public void onTvInputSetupFinished(final String inputId, 105 @Nullable final Runnable postRunnable) { 106 // When TIS adds several channels, ChannelDataManager.Listener.onChannelList 107 // Updated() can be called several times. In this case, it is hard to detect 108 // which one is the last callback. To reduce error prune, we update channel 109 // list again and make all channels of {@code inputId} browsable. 110 onSetupDone(inputId); 111 final ChannelDataManager manager = mTvApplication.getChannelDataManager(); 112 if (!manager.isDbLoadFinished()) { 113 manager.addListener(new ChannelDataManager.Listener() { 114 @Override 115 public void onLoadFinished() { 116 manager.removeListener(this); 117 updateChannelsAfterSetup(mTvApplication, inputId, postRunnable); 118 } 119 120 @Override 121 public void onChannelListUpdated() { } 122 123 @Override 124 public void onChannelBrowsableChanged() { } 125 }); 126 } else { 127 updateChannelsAfterSetup(mTvApplication, inputId, postRunnable); 128 } 129 } 130 131 private static void updateChannelsAfterSetup(Context context, final String inputId, 132 final Runnable postRunnable) { 133 ApplicationSingletons appSingletons = TvApplication.getSingletons(context); 134 final ChannelDataManager manager = appSingletons.getChannelDataManager(); 135 manager.updateChannels(new Runnable() { 136 @Override 137 public void run() { 138 Channel firstChannelForInput = null; 139 boolean browsableChanged = false; 140 for (Channel channel : manager.getChannelList()) { 141 if (channel.getInputId().equals(inputId)) { 142 if (!channel.isBrowsable()) { 143 manager.updateBrowsable(channel.getId(), true, true); 144 browsableChanged = true; 145 } 146 if (firstChannelForInput == null) { 147 firstChannelForInput = channel; 148 } 149 } 150 } 151 if (firstChannelForInput != null) { 152 Utils.setLastWatchedChannel(context, firstChannelForInput); 153 } 154 if (browsableChanged) { 155 manager.notifyChannelBrowsableChanged(); 156 manager.applyUpdatedValuesToDb(); 157 } 158 if (postRunnable != null) { 159 postRunnable.run(); 160 } 161 } 162 }); 163 } 164 165 /** 166 * Marks the channels in newly installed inputs browsable. 167 */ 168 @UiThread 169 public void markNewChannelsBrowsable() { 170 Set<String> newInputsWithChannels = new HashSet<>(); 171 TvInputManagerHelper tvInputManagerHelper = mTvApplication.getTvInputManagerHelper(); 172 ChannelDataManager channelDataManager = mTvApplication.getChannelDataManager(); 173 SoftPreconditions.checkState(channelDataManager.isDbLoadFinished()); 174 for (TvInputInfo input : tvInputManagerHelper.getTvInputInfos(true, true)) { 175 String inputId = input.getId(); 176 if (!isSetupDone(inputId) && channelDataManager.getChannelCountForInput(inputId) > 0) { 177 onSetupDone(inputId); 178 newInputsWithChannels.add(inputId); 179 if (DEBUG) { 180 Log.d(TAG, "New input " + inputId + " has " 181 + channelDataManager.getChannelCountForInput(inputId) 182 + " channels"); 183 } 184 } 185 } 186 if (!newInputsWithChannels.isEmpty()) { 187 for (Channel channel : channelDataManager.getChannelList()) { 188 if (newInputsWithChannels.contains(channel.getInputId())) { 189 channelDataManager.updateBrowsable(channel.getId(), true); 190 } 191 } 192 channelDataManager.applyUpdatedValuesToDb(); 193 } 194 } 195 196 public boolean isFirstTune() { 197 return mIsFirstTune; 198 } 199 200 /** 201 * Returns true, if the input with {@code inputId} is newly installed. 202 */ 203 public boolean isNewInput(String inputId) { 204 return !mKnownInputs.contains(inputId); 205 } 206 207 /** 208 * Marks an input with {@code inputId} as a known input. Once it is marked, {@link #isNewInput} 209 * will return false. 210 */ 211 public void markAsKnownInput(String inputId) { 212 mKnownInputs.add(inputId); 213 mRecognizedInputs.add(inputId); 214 mSharedPreferences.edit().putStringSet(PREF_KEY_KNOWN_INPUTS, mKnownInputs) 215 .putStringSet(PREF_KEY_RECOGNIZED_INPUTS, mRecognizedInputs).apply(); 216 } 217 218 /** 219 * Returns {@code true}, if {@code inputId}'s setup has been done before. 220 */ 221 public boolean isSetupDone(String inputId) { 222 boolean done = mSetUpInputs.contains(inputId); 223 if (DEBUG) { 224 Log.d(TAG, "isSetupDone: (input=" + inputId + ", result= " + done + ")"); 225 } 226 return done; 227 } 228 229 /** 230 * Returns true, if there is any newly installed input. 231 */ 232 public boolean hasNewInput(TvInputManagerHelper inputManager) { 233 for (TvInputInfo input : inputManager.getTvInputInfos(true, true)) { 234 if (isNewInput(input.getId())) { 235 return true; 236 } 237 } 238 return false; 239 } 240 241 /** 242 * Checks whether the given input is already recognized by the user or not. 243 */ 244 private boolean isRecognizedInput(String inputId) { 245 return mRecognizedInputs.contains(inputId); 246 } 247 248 /** 249 * Marks all the inputs as recognized inputs. Once it is marked, {@link #isRecognizedInput} will 250 * return {@code true}. 251 */ 252 public void markAllInputsRecognized(TvInputManagerHelper inputManager) { 253 for (TvInputInfo input : inputManager.getTvInputInfos(true, true)) { 254 mRecognizedInputs.add(input.getId()); 255 } 256 mSharedPreferences.edit().putStringSet(PREF_KEY_RECOGNIZED_INPUTS, mRecognizedInputs) 257 .apply(); 258 } 259 260 /** 261 * Checks whether there are any unrecognized inputs. 262 */ 263 public boolean hasUnrecognizedInput(TvInputManagerHelper inputManager) { 264 for (TvInputInfo input : inputManager.getTvInputInfos(true, true)) { 265 if (!isRecognizedInput(input.getId())) { 266 return true; 267 } 268 } 269 return false; 270 } 271 272 /** 273 * Grants permission for writing EPG data to all verified packages. 274 * 275 * @param context The Context used for granting permission. 276 */ 277 public static void grantEpgPermissionToSetUpPackages(Context context) { 278 // Find all already-verified packages. 279 Set<String> setUpPackages = new HashSet<>(); 280 SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); 281 for (String input : sp.getStringSet(PREF_KEY_SET_UP_INPUTS, 282 Collections.<String>emptySet())) { 283 if (!TextUtils.isEmpty(input)) { 284 ComponentName componentName = ComponentName.unflattenFromString(input); 285 if (componentName != null) { 286 setUpPackages.add(componentName.getPackageName()); 287 } 288 } 289 } 290 291 for (String packageName : setUpPackages) { 292 grantEpgPermission(context, packageName); 293 } 294 } 295 296 /** 297 * Grants permission for writing EPG data to a given package. 298 * 299 * @param context The Context used for granting permission. 300 * @param packageName The name of the package to give permission. 301 */ 302 public static void grantEpgPermission(Context context, String packageName) { 303 if (DEBUG) { 304 Log.d(TAG, "grantEpgPermission(context=" + context + ", packageName=" + packageName 305 + ")"); 306 } 307 try { 308 int modeFlags = Intent.FLAG_GRANT_WRITE_URI_PERMISSION 309 | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION; 310 context.grantUriPermission(packageName, TvContract.Channels.CONTENT_URI, modeFlags); 311 context.grantUriPermission(packageName, TvContract.Programs.CONTENT_URI, modeFlags); 312 } catch (SecurityException e) { 313 Log.e(TAG, "Either TvProvider does not allow granting of Uri permissions or the app" 314 + " does not have permission.", e); 315 } 316 } 317 318 /** 319 * Called when Live channels app is launched. Once it is called, {@link 320 * #isFirstTune} will return false. 321 */ 322 public void onTuned() { 323 if (!mIsFirstTune) { 324 return; 325 } 326 mIsFirstTune = false; 327 mSharedPreferences.edit().putBoolean(PREF_KEY_IS_FIRST_TUNE, false).apply(); 328 } 329 330 /** 331 * Called when input list is changed. It mainly handles input removals. 332 */ 333 public void onInputListUpdated(TvInputManager manager) { 334 // mRecognizedInputs > mKnownInputs > mSetUpInputs. 335 Set<String> removedInputList = new HashSet<>(mRecognizedInputs); 336 for (TvInputInfo input : manager.getTvInputList()) { 337 removedInputList.remove(input.getId()); 338 } 339 // A USB tuner device can be temporarily unplugged. We do not remove the USB tuner input 340 // from the known inputs so that the input won't appear as a new input whenever the user 341 // plugs in the USB tuner device again. 342 removedInputList.remove(mTunerInputId); 343 344 if (!removedInputList.isEmpty()) { 345 boolean inputPackageDeleted = false; 346 for (String input : removedInputList) { 347 try { 348 // Just after booting, input list from TvInputManager are not reliable. 349 // So we need to double-check package existence. b/29034900 350 mTvApplication.getPackageManager().getPackageInfo( 351 ComponentName.unflattenFromString(input) 352 .getPackageName(), PackageManager.GET_ACTIVITIES); 353 Log.i(TAG, "TV input (" + input + ") is removed but package is not deleted"); 354 } catch (NameNotFoundException e) { 355 Log.i(TAG, "TV input (" + input + ") and its package are removed"); 356 mRecognizedInputs.remove(input); 357 mSetUpInputs.remove(input); 358 mKnownInputs.remove(input); 359 inputPackageDeleted = true; 360 } 361 } 362 if (inputPackageDeleted) { 363 mSharedPreferences.edit().putStringSet(PREF_KEY_SET_UP_INPUTS, mSetUpInputs) 364 .putStringSet(PREF_KEY_KNOWN_INPUTS, mKnownInputs) 365 .putStringSet(PREF_KEY_RECOGNIZED_INPUTS, mRecognizedInputs).apply(); 366 } 367 } 368 } 369 370 /** 371 * Called when an setup is done. Once it is called, {@link #isSetupDone} returns {@code true} 372 * for {@code inputId}. 373 */ 374 private void onSetupDone(String inputId) { 375 SoftPreconditions.checkState(inputId != null); 376 if (DEBUG) Log.d(TAG, "onSetupDone: input=" + inputId); 377 if (!mRecognizedInputs.contains(inputId)) { 378 Log.i(TAG, "An unrecognized input's setup has been done. inputId=" + inputId); 379 mRecognizedInputs.add(inputId); 380 mSharedPreferences.edit().putStringSet(PREF_KEY_RECOGNIZED_INPUTS, mRecognizedInputs) 381 .apply(); 382 } 383 if (!mKnownInputs.contains(inputId)) { 384 Log.i(TAG, "An unknown input's setup has been done. inputId=" + inputId); 385 mKnownInputs.add(inputId); 386 mSharedPreferences.edit().putStringSet(PREF_KEY_KNOWN_INPUTS, mKnownInputs).apply(); 387 } 388 if (!mSetUpInputs.contains(inputId)) { 389 mSetUpInputs.add(inputId); 390 mSharedPreferences.edit().putStringSet(PREF_KEY_SET_UP_INPUTS, mSetUpInputs).apply(); 391 } 392 // Start fetching program guide data for internal tuners. 393 Context context = mTvApplication.getApplicationContext(); 394 if (Utils.isInternalTvInput(context, inputId)) { 395 EpgFetcher.getInstance(context).startImmediately(true); 396 } 397 } 398}