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.util; 18 19 import android.content.ComponentName; 20 import android.content.Context; 21 import android.content.Intent; 22 import android.content.SharedPreferences; 23 import android.content.pm.PackageManager; 24 import android.content.pm.PackageManager.NameNotFoundException; 25 import android.media.tv.TvContract; 26 import android.media.tv.TvInputInfo; 27 import android.media.tv.TvInputManager; 28 import android.preference.PreferenceManager; 29 import android.support.annotation.Nullable; 30 import android.support.annotation.UiThread; 31 import android.text.TextUtils; 32 import android.util.ArraySet; 33 import android.util.Log; 34 import com.android.tv.R; 35 import com.android.tv.TvSingletons; 36 import com.android.tv.common.SoftPreconditions; 37 import com.android.tv.common.dagger.annotations.ApplicationContext; 38 import com.android.tv.common.singletons.HasTvInputId; 39 import com.android.tv.common.util.CommonUtils; 40 import com.android.tv.data.ChannelDataManager; 41 import com.android.tv.data.api.Channel; 42 import com.android.tv.tunerinputcontroller.BuiltInTunerManager; 43 import com.google.common.base.Optional; 44 45 import java.util.Arrays; 46 import java.util.Collections; 47 import java.util.HashSet; 48 import java.util.Set; 49 import javax.inject.Inject; 50 import javax.inject.Singleton; 51 52 /** A utility class related to input setup. */ 53 @Singleton 54 public class SetupUtils { 55 private static final String TAG = "SetupUtils"; 56 private static final boolean DEBUG = false; 57 58 // Known inputs are inputs which are shown in SetupView before. When a new input is installed, 59 // the input will not be included in "PREF_KEY_KNOWN_INPUTS". 60 private static final String PREF_KEY_KNOWN_INPUTS = "known_inputs"; 61 // Set up inputs are inputs whose setup activity has been launched and finished successfully. 62 private static final String PREF_KEY_SET_UP_INPUTS = "set_up_inputs"; 63 // Recognized inputs means that the user already knows the inputs are installed. 64 private static final String PREF_KEY_RECOGNIZED_INPUTS = "recognized_inputs"; 65 private static final String PREF_KEY_IS_FIRST_TUNE = "is_first_tune"; 66 67 private final Context mContext; 68 private final SharedPreferences mSharedPreferences; 69 private final Set<String> mKnownInputs; 70 private final Set<String> mSetUpInputs; 71 private final Set<String> mRecognizedInputs; 72 private boolean mIsFirstTune; 73 private final Optional<String> mOptionalTunerInputId; 74 75 @Inject SetupUtils( @pplicationContext Context context, Optional<BuiltInTunerManager> optionalBuiltInTunerManager)76 public SetupUtils( 77 @ApplicationContext Context context, 78 Optional<BuiltInTunerManager> optionalBuiltInTunerManager) { 79 mContext = context; 80 mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); 81 mSetUpInputs = new ArraySet<>(); 82 mSetUpInputs.addAll( 83 mSharedPreferences.getStringSet(PREF_KEY_SET_UP_INPUTS, Collections.emptySet())); 84 mKnownInputs = new ArraySet<>(); 85 mKnownInputs.addAll( 86 mSharedPreferences.getStringSet(PREF_KEY_KNOWN_INPUTS, Collections.emptySet())); 87 mRecognizedInputs = new ArraySet<>(); 88 mRecognizedInputs.addAll( 89 mSharedPreferences.getStringSet(PREF_KEY_RECOGNIZED_INPUTS, mKnownInputs)); 90 mIsFirstTune = mSharedPreferences.getBoolean(PREF_KEY_IS_FIRST_TUNE, true); 91 mOptionalTunerInputId = 92 optionalBuiltInTunerManager.transform(HasTvInputId::getEmbeddedTunerInputId); 93 } 94 95 /** Additional work after the setup of TV input. */ onTvInputSetupFinished( final String inputId, @Nullable final Runnable postRunnable)96 public void onTvInputSetupFinished( 97 final String inputId, @Nullable final Runnable postRunnable) { 98 // When TIS adds several channels, ChannelDataManager.Listener.onChannelList 99 // Updated() can be called several times. In this case, it is hard to detect 100 // which one is the last callback. To reduce error prune, we update channel 101 // list again and make all channels of {@code inputId} browsable. 102 onSetupDone(inputId); 103 final ChannelDataManager manager = 104 TvSingletons.getSingletons(mContext).getChannelDataManager(); 105 if (!manager.isDbLoadFinished()) { 106 manager.addListener( 107 new ChannelDataManager.Listener() { 108 @Override 109 public void onLoadFinished() { 110 manager.removeListener(this); 111 updateChannelsAfterSetup(mContext, inputId, postRunnable); 112 } 113 114 @Override 115 public void onChannelListUpdated() {} 116 117 @Override 118 public void onChannelBrowsableChanged() {} 119 }); 120 } else { 121 updateChannelsAfterSetup(mContext, inputId, postRunnable); 122 } 123 } 124 updateChannelsAfterSetup( Context context, final String inputId, final Runnable postRunnable)125 private static void updateChannelsAfterSetup( 126 Context context, final String inputId, final Runnable postRunnable) { 127 TvSingletons tvSingletons = TvSingletons.getSingletons(context); 128 final ChannelDataManager manager = tvSingletons.getChannelDataManager(); 129 manager.updateChannels( 130 () -> { 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 /** Marks the channels in newly installed inputs browsable. */ 158 @UiThread markNewChannelsBrowsable()159 public void markNewChannelsBrowsable() { 160 Set<String> newInputsWithChannels = new HashSet<>(); 161 TvSingletons singletons = TvSingletons.getSingletons(mContext); 162 TvInputManagerHelper tvInputManagerHelper = singletons.getTvInputManagerHelper(); 163 ChannelDataManager channelDataManager = singletons.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 isFirstTune()191 public boolean isFirstTune() { 192 return mIsFirstTune; 193 } 194 195 /** Returns true, if the input with {@code inputId} is newly installed. */ isNewInput(String inputId)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 */ markAsKnownInput(String inputId)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. */ isSetupDone(String inputId)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. */ hasNewInput(TvInputManagerHelper inputManager)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. */ isRecognizedInput(String inputId)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 */ markAllInputsRecognized(TvInputManagerHelper inputManager)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. */ hasUnrecognizedInput(TvInputManagerHelper inputManager)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 */ grantEpgPermissionToSetUpPackages(Context context)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 */ grantEpgPermission(Context context, String packageName)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 TV app is launched. Once it is called, {@link #isFirstTune} will return false. 315 */ onTuned()316 public void onTuned() { 317 if (!mIsFirstTune) { 318 return; 319 } 320 mIsFirstTune = false; 321 mSharedPreferences.edit().putBoolean(PREF_KEY_IS_FIRST_TUNE, false).apply(); 322 } 323 324 /** Called when input list is changed. It mainly handles input removals. */ onInputListUpdated(TvInputManager manager)325 public void onInputListUpdated(TvInputManager manager) { 326 // mRecognizedInputs > mKnownInputs > mSetUpInputs. 327 Set<String> removedInputList = new HashSet<>(mRecognizedInputs); 328 for (TvInputInfo input : manager.getTvInputList()) { 329 removedInputList.remove(input.getId()); 330 } 331 // A USB tuner device can be temporarily unplugged. We do not remove the USB tuner input 332 // from the known inputs so that the input won't appear as a new input whenever the user 333 // plugs in the USB tuner device again. 334 if (mOptionalTunerInputId.isPresent()) { 335 removedInputList.remove(mOptionalTunerInputId.get()); 336 } 337 338 if (!removedInputList.isEmpty()) { 339 boolean inputPackageDeleted = false; 340 for (String input : removedInputList) { 341 try { 342 // Just after booting, input list from TvInputManager are not reliable. 343 // So we need to double-check package existence. b/29034900 344 mContext.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 * Create a Intent to launch setup activity for {@code inputId}. The setup activity defined 370 * in the overlayable resources precedes the one defined in the corresponding TV input service. 371 */ 372 @Nullable createSetupIntent(Context context, TvInputInfo input)373 public Intent createSetupIntent(Context context, TvInputInfo input) { 374 String[] componentStrings = context.getResources() 375 .getStringArray(R.array.setup_ComponentNames); 376 377 if (componentStrings != null) { 378 for (String component : componentStrings) { 379 String[] split = component.split("#"); 380 if (split.length != 2) { 381 Log.w(TAG, "Invalid component item: " + Arrays.toString(split)); 382 continue; 383 } 384 385 final String inputId = split[0].trim(); 386 if (inputId.equals(input.getId())) { 387 final String flattenedComponentName = split[1].trim(); 388 final ComponentName componentName = ComponentName 389 .unflattenFromString(flattenedComponentName); 390 if (componentName == null) { 391 Log.w(TAG, "Failed to unflatten component: " + flattenedComponentName); 392 continue; 393 } 394 395 final Intent overlaySetupIntent = new Intent(Intent.ACTION_MAIN); 396 overlaySetupIntent.setComponent(componentName); 397 overlaySetupIntent.putExtra(TvInputInfo.EXTRA_INPUT_ID, inputId); 398 399 PackageManager pm = context.getPackageManager(); 400 if (overlaySetupIntent.resolveActivityInfo(pm, 0) == null) { 401 Log.w(TAG, "unable to find component" + flattenedComponentName); 402 continue; 403 } 404 405 Log.i(TAG, "overlay input id: " + inputId 406 + " to setup activity: " + flattenedComponentName); 407 return CommonUtils.createSetupIntent(overlaySetupIntent, inputId); 408 } 409 } 410 } 411 return CommonUtils.createSetupIntent(input); 412 } 413 414 /** 415 * Called when an setup is done. Once it is called, {@link #isSetupDone} returns {@code true} 416 * for {@code inputId}. 417 */ onSetupDone(String inputId)418 private void onSetupDone(String inputId) { 419 SoftPreconditions.checkState(inputId != null); 420 if (DEBUG) Log.d(TAG, "onSetupDone: input=" + inputId); 421 if (!mRecognizedInputs.contains(inputId)) { 422 Log.i(TAG, "An unrecognized input's setup has been done. inputId=" + inputId); 423 mRecognizedInputs.add(inputId); 424 mSharedPreferences 425 .edit() 426 .putStringSet(PREF_KEY_RECOGNIZED_INPUTS, mRecognizedInputs) 427 .apply(); 428 } 429 if (!mKnownInputs.contains(inputId)) { 430 Log.i(TAG, "An unknown input's setup has been done. inputId=" + inputId); 431 mKnownInputs.add(inputId); 432 mSharedPreferences.edit().putStringSet(PREF_KEY_KNOWN_INPUTS, mKnownInputs).apply(); 433 } 434 if (!mSetUpInputs.contains(inputId)) { 435 mSetUpInputs.add(inputId); 436 mSharedPreferences.edit().putStringSet(PREF_KEY_SET_UP_INPUTS, mSetUpInputs).apply(); 437 } 438 } 439 } 440