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 // Whether to mark new channels to browsable. 67 private static final boolean MARK_NEW_CHANNELS_BROWSABLE = false; 68 69 private final Context mContext; 70 private final SharedPreferences mSharedPreferences; 71 private final Set<String> mKnownInputs; 72 private final Set<String> mSetUpInputs; 73 private final Set<String> mRecognizedInputs; 74 private boolean mIsFirstTune; 75 private final Optional<String> mOptionalTunerInputId; 76 77 @Inject SetupUtils( @pplicationContext Context context, Optional<BuiltInTunerManager> optionalBuiltInTunerManager)78 public SetupUtils( 79 @ApplicationContext Context context, 80 Optional<BuiltInTunerManager> optionalBuiltInTunerManager) { 81 mContext = context; 82 mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); 83 mSetUpInputs = new ArraySet<>(); 84 mSetUpInputs.addAll( 85 mSharedPreferences.getStringSet(PREF_KEY_SET_UP_INPUTS, Collections.emptySet())); 86 mKnownInputs = new ArraySet<>(); 87 mKnownInputs.addAll( 88 mSharedPreferences.getStringSet(PREF_KEY_KNOWN_INPUTS, Collections.emptySet())); 89 mRecognizedInputs = new ArraySet<>(); 90 mRecognizedInputs.addAll( 91 mSharedPreferences.getStringSet(PREF_KEY_RECOGNIZED_INPUTS, mKnownInputs)); 92 mIsFirstTune = mSharedPreferences.getBoolean(PREF_KEY_IS_FIRST_TUNE, true); 93 mOptionalTunerInputId = 94 optionalBuiltInTunerManager.transform(HasTvInputId::getEmbeddedTunerInputId); 95 } 96 97 /** Additional work after the setup of TV input. */ onTvInputSetupFinished( final String inputId, @Nullable final Runnable postRunnable)98 public void onTvInputSetupFinished( 99 final String inputId, @Nullable final Runnable postRunnable) { 100 // When TIS adds several channels, ChannelDataManager.Listener.onChannelList 101 // Updated() can be called several times. In this case, it is hard to detect 102 // which one is the last callback. To reduce error prune, we update channel 103 // list again and make all channels of {@code inputId} browsable. 104 onSetupDone(inputId); 105 final ChannelDataManager manager = 106 TvSingletons.getSingletons(mContext).getChannelDataManager(); 107 if (!manager.isDbLoadFinished()) { 108 manager.addListener( 109 new ChannelDataManager.Listener() { 110 @Override 111 public void onLoadFinished() { 112 manager.removeListener(this); 113 updateChannelsAfterSetup(mContext, inputId, postRunnable); 114 } 115 116 @Override 117 public void onChannelListUpdated() {} 118 119 @Override 120 public void onChannelBrowsableChanged() {} 121 }); 122 } else { 123 updateChannelsAfterSetup(mContext, inputId, postRunnable); 124 } 125 } 126 updateChannelsAfterSetup( Context context, final String inputId, final Runnable postRunnable)127 private static void updateChannelsAfterSetup( 128 Context context, final String inputId, final Runnable postRunnable) { 129 TvSingletons tvSingletons = TvSingletons.getSingletons(context); 130 final ChannelDataManager manager = tvSingletons.getChannelDataManager(); 131 manager.updateChannels( 132 () -> { 133 Channel firstChannelForInput = null; 134 boolean browsableChanged = false; 135 for (Channel channel : manager.getChannelList()) { 136 if (channel.getInputId().equals(inputId)) { 137 if (!channel.isBrowsable() && MARK_NEW_CHANNELS_BROWSABLE) { 138 manager.updateBrowsable(channel.getId(), true, true); 139 browsableChanged = true; 140 } 141 if (firstChannelForInput == null && channel.isBrowsable()) { 142 firstChannelForInput = channel; 143 } 144 } 145 } 146 if (firstChannelForInput != null) { 147 Utils.setLastWatchedChannel(context, firstChannelForInput); 148 } 149 if (browsableChanged) { 150 manager.notifyChannelBrowsableChanged(); 151 manager.applyUpdatedValuesToDb(); 152 } 153 if (postRunnable != null) { 154 postRunnable.run(); 155 } 156 }); 157 } 158 159 /** Marks the channels in newly installed inputs browsable if enabled. */ 160 @UiThread markNewChannelsBrowsableIfEnabled()161 public void markNewChannelsBrowsableIfEnabled() { 162 // TODO(b/288499376): Handle browsable field for channels added outside Live TV app in a 163 // better way. 164 if (!MARK_NEW_CHANNELS_BROWSABLE) { 165 return; 166 } 167 168 Set<String> newInputsWithChannels = new HashSet<>(); 169 TvSingletons singletons = TvSingletons.getSingletons(mContext); 170 TvInputManagerHelper tvInputManagerHelper = singletons.getTvInputManagerHelper(); 171 ChannelDataManager channelDataManager = singletons.getChannelDataManager(); 172 SoftPreconditions.checkState(channelDataManager.isDbLoadFinished()); 173 for (TvInputInfo input : tvInputManagerHelper.getTvInputInfos(true, true)) { 174 String inputId = input.getId(); 175 if (!isSetupDone(inputId) && channelDataManager.getChannelCountForInput(inputId) > 0) { 176 onSetupDone(inputId); 177 newInputsWithChannels.add(inputId); 178 if (DEBUG) { 179 Log.d( 180 TAG, 181 "New input " 182 + inputId 183 + " has " 184 + channelDataManager.getChannelCountForInput(inputId) 185 + " channels"); 186 } 187 } 188 } 189 if (!newInputsWithChannels.isEmpty()) { 190 for (Channel channel : channelDataManager.getChannelList()) { 191 if (newInputsWithChannels.contains(channel.getInputId())) { 192 channelDataManager.updateBrowsable(channel.getId(), true); 193 } 194 } 195 channelDataManager.applyUpdatedValuesToDb(); 196 } 197 } 198 isFirstTune()199 public boolean isFirstTune() { 200 return mIsFirstTune; 201 } 202 203 /** Returns true, if the input with {@code inputId} is newly installed. */ isNewInput(String inputId)204 public boolean isNewInput(String inputId) { 205 return !mKnownInputs.contains(inputId); 206 } 207 208 /** 209 * Marks an input with {@code inputId} as a known input. Once it is marked, {@link #isNewInput} 210 * will return false. 211 */ markAsKnownInput(String inputId)212 public void markAsKnownInput(String inputId) { 213 mKnownInputs.add(inputId); 214 mRecognizedInputs.add(inputId); 215 mSharedPreferences 216 .edit() 217 .putStringSet(PREF_KEY_KNOWN_INPUTS, mKnownInputs) 218 .putStringSet(PREF_KEY_RECOGNIZED_INPUTS, mRecognizedInputs) 219 .apply(); 220 } 221 222 /** Returns {@code true}, if {@code inputId}'s setup has been done before. */ isSetupDone(String inputId)223 public boolean isSetupDone(String inputId) { 224 boolean done = mSetUpInputs.contains(inputId); 225 if (DEBUG) { 226 Log.d(TAG, "isSetupDone: (input=" + inputId + ", result= " + done + ")"); 227 } 228 return done; 229 } 230 231 /** Returns true, if there is any newly installed input. */ hasNewInput(TvInputManagerHelper inputManager)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 /** Checks whether the given input is already recognized by the user or not. */ isRecognizedInput(String inputId)242 private boolean isRecognizedInput(String inputId) { 243 return mRecognizedInputs.contains(inputId); 244 } 245 246 /** 247 * Marks all the inputs as recognized inputs. Once it is marked, {@link #isRecognizedInput} will 248 * return {@code true}. 249 */ markAllInputsRecognized(TvInputManagerHelper inputManager)250 public void markAllInputsRecognized(TvInputManagerHelper inputManager) { 251 for (TvInputInfo input : inputManager.getTvInputInfos(true, true)) { 252 mRecognizedInputs.add(input.getId()); 253 } 254 mSharedPreferences 255 .edit() 256 .putStringSet(PREF_KEY_RECOGNIZED_INPUTS, mRecognizedInputs) 257 .apply(); 258 } 259 260 /** Checks whether there are any unrecognized inputs. */ hasUnrecognizedInput(TvInputManagerHelper inputManager)261 public boolean hasUnrecognizedInput(TvInputManagerHelper inputManager) { 262 for (TvInputInfo input : inputManager.getTvInputInfos(true, true)) { 263 if (!isRecognizedInput(input.getId())) { 264 return true; 265 } 266 } 267 return false; 268 } 269 270 /** 271 * Grants permission for writing EPG data to all verified packages. 272 * 273 * @param context The Context used for granting permission. 274 */ grantEpgPermissionToSetUpPackages(Context context)275 public static void grantEpgPermissionToSetUpPackages(Context context) { 276 // Find all already-verified packages. 277 Set<String> setUpPackages = new HashSet<>(); 278 SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); 279 for (String input : 280 sp.getStringSet(PREF_KEY_SET_UP_INPUTS, Collections.<String>emptySet())) { 281 if (!TextUtils.isEmpty(input)) { 282 ComponentName componentName = ComponentName.unflattenFromString(input); 283 if (componentName != null) { 284 setUpPackages.add(componentName.getPackageName()); 285 } 286 } 287 } 288 289 for (String packageName : setUpPackages) { 290 grantEpgPermission(context, packageName); 291 } 292 } 293 294 /** 295 * Grants permission for writing EPG data to a given package. 296 * 297 * @param context The Context used for granting permission. 298 * @param packageName The name of the package to give permission. 299 */ grantEpgPermission(Context context, String packageName)300 public static void grantEpgPermission(Context context, String packageName) { 301 if (DEBUG) { 302 Log.d( 303 TAG, 304 "grantEpgPermission(context=" + context + ", packageName=" + packageName + ")"); 305 } 306 try { 307 int modeFlags = 308 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( 314 TAG, 315 "Either TvProvider does not allow granting of Uri permissions or the app" 316 + " does not have permission.", 317 e); 318 } 319 } 320 321 /** 322 * Called when TV app is launched. Once it is called, {@link #isFirstTune} will return false. 323 */ onTuned()324 public void onTuned() { 325 if (!mIsFirstTune) { 326 return; 327 } 328 mIsFirstTune = false; 329 mSharedPreferences.edit().putBoolean(PREF_KEY_IS_FIRST_TUNE, false).apply(); 330 } 331 332 /** Called when input list is changed. It mainly handles input removals. */ onInputListUpdated(TvInputManager manager)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 if (mOptionalTunerInputId.isPresent()) { 343 removedInputList.remove(mOptionalTunerInputId.get()); 344 } 345 346 if (!removedInputList.isEmpty()) { 347 boolean inputPackageDeleted = false; 348 for (String input : removedInputList) { 349 try { 350 // Just after booting, input list from TvInputManager are not reliable. 351 // So we need to double-check package existence. b/29034900 352 mContext.getPackageManager() 353 .getPackageInfo( 354 ComponentName.unflattenFromString(input).getPackageName(), 355 PackageManager.GET_ACTIVITIES); 356 Log.i(TAG, "TV input (" + input + ") is removed but package is not deleted"); 357 } catch (NameNotFoundException e) { 358 Log.i(TAG, "TV input (" + input + ") and its package are removed"); 359 mRecognizedInputs.remove(input); 360 mSetUpInputs.remove(input); 361 mKnownInputs.remove(input); 362 inputPackageDeleted = true; 363 } 364 } 365 if (inputPackageDeleted) { 366 mSharedPreferences 367 .edit() 368 .putStringSet(PREF_KEY_SET_UP_INPUTS, mSetUpInputs) 369 .putStringSet(PREF_KEY_KNOWN_INPUTS, mKnownInputs) 370 .putStringSet(PREF_KEY_RECOGNIZED_INPUTS, mRecognizedInputs) 371 .apply(); 372 } 373 } 374 } 375 376 /** 377 * Create a Intent to launch setup activity for {@code inputId}. The setup activity defined 378 * in the overlayable resources precedes the one defined in the corresponding TV input service. 379 */ 380 @Nullable createSetupIntent(Context context, TvInputInfo input)381 public Intent createSetupIntent(Context context, TvInputInfo input) { 382 String[] componentStrings = context.getResources() 383 .getStringArray(R.array.setup_ComponentNames); 384 385 if (componentStrings != null) { 386 for (String component : componentStrings) { 387 String[] split = component.split("#"); 388 if (split.length != 2) { 389 Log.w(TAG, "Invalid component item: " + Arrays.toString(split)); 390 continue; 391 } 392 393 final String inputId = split[0].trim(); 394 if (inputId.equals(input.getId())) { 395 final String flattenedComponentName = split[1].trim(); 396 final ComponentName componentName = ComponentName 397 .unflattenFromString(flattenedComponentName); 398 if (componentName == null) { 399 Log.w(TAG, "Failed to unflatten component: " + flattenedComponentName); 400 continue; 401 } 402 403 final Intent overlaySetupIntent = new Intent(Intent.ACTION_MAIN); 404 overlaySetupIntent.setComponent(componentName); 405 overlaySetupIntent.putExtra(TvInputInfo.EXTRA_INPUT_ID, inputId); 406 407 PackageManager pm = context.getPackageManager(); 408 if (overlaySetupIntent.resolveActivityInfo(pm, 0) == null) { 409 Log.w(TAG, "unable to find component" + flattenedComponentName); 410 continue; 411 } 412 413 Log.i(TAG, "overlay input id: " + inputId 414 + " to setup activity: " + flattenedComponentName); 415 return CommonUtils.createSetupIntent(overlaySetupIntent, inputId); 416 } 417 } 418 } 419 return CommonUtils.createSetupIntent(input); 420 } 421 422 /** 423 * Called when an setup is done. Once it is called, {@link #isSetupDone} returns {@code true} 424 * for {@code inputId}. 425 */ onSetupDone(String inputId)426 private void onSetupDone(String inputId) { 427 SoftPreconditions.checkState(inputId != null); 428 if (DEBUG) Log.d(TAG, "onSetupDone: input=" + inputId); 429 if (!mRecognizedInputs.contains(inputId)) { 430 Log.i(TAG, "An unrecognized input's setup has been done. inputId=" + inputId); 431 mRecognizedInputs.add(inputId); 432 mSharedPreferences 433 .edit() 434 .putStringSet(PREF_KEY_RECOGNIZED_INPUTS, mRecognizedInputs) 435 .apply(); 436 } 437 if (!mKnownInputs.contains(inputId)) { 438 Log.i(TAG, "An unknown input's setup has been done. inputId=" + inputId); 439 mKnownInputs.add(inputId); 440 mSharedPreferences.edit().putStringSet(PREF_KEY_KNOWN_INPUTS, mKnownInputs).apply(); 441 } 442 if (!mSetUpInputs.contains(inputId)) { 443 mSetUpInputs.add(inputId); 444 mSharedPreferences.edit().putStringSet(PREF_KEY_SET_UP_INPUTS, mSetUpInputs).apply(); 445 } 446 } 447 } 448