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