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