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.support.annotation.VisibleForTesting; 32 import android.text.TextUtils; 33 import android.util.ArraySet; 34 import android.util.Log; 35 import com.android.tv.TvSingletons; 36 import com.android.tv.common.BaseApplication; 37 import com.android.tv.common.SoftPreconditions; 38 import com.android.tv.data.ChannelDataManager; 39 import com.android.tv.data.api.Channel; 40 import java.util.Collections; 41 import java.util.HashSet; 42 import java.util.Set; 43 44 /** A utility class related to input setup. */ 45 public 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 58 private final Context mContext; 59 private final SharedPreferences mSharedPreferences; 60 private final Set<String> mKnownInputs; 61 private final Set<String> mSetUpInputs; 62 private final Set<String> mRecognizedInputs; 63 private boolean mIsFirstTune; 64 private final String mTunerInputId; 65 66 @VisibleForTesting 67 protected SetupUtils(Context context) { 68 mContext = context; 69 mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); 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 = BaseApplication.getSingletons(context).getEmbeddedTunerInputId(); 81 } 82 83 /** 84 * Creates an instance of {@link SetupUtils}. 85 * 86 * <p><b>WARNING</b> this should only be called by the top level application. 87 */ 88 public static SetupUtils createForTvSingletons(Context context) { 89 return new SetupUtils(context.getApplicationContext()); 90 } 91 92 /** Additional work after the setup of TV input. */ 93 public void onTvInputSetupFinished( 94 final String inputId, @Nullable final Runnable postRunnable) { 95 // When TIS adds several channels, ChannelDataManager.Listener.onChannelList 96 // Updated() can be called several times. In this case, it is hard to detect 97 // which one is the last callback. To reduce error prune, we update channel 98 // list again and make all channels of {@code inputId} browsable. 99 onSetupDone(inputId); 100 final ChannelDataManager manager = 101 TvSingletons.getSingletons(mContext).getChannelDataManager(); 102 if (!manager.isDbLoadFinished()) { 103 manager.addListener( 104 new ChannelDataManager.Listener() { 105 @Override 106 public void onLoadFinished() { 107 manager.removeListener(this); 108 updateChannelsAfterSetup(mContext, inputId, postRunnable); 109 } 110 111 @Override 112 public void onChannelListUpdated() {} 113 114 @Override 115 public void onChannelBrowsableChanged() {} 116 }); 117 } else { 118 updateChannelsAfterSetup(mContext, inputId, postRunnable); 119 } 120 } 121 122 private static void updateChannelsAfterSetup( 123 Context context, final String inputId, final Runnable postRunnable) { 124 TvSingletons tvSingletons = TvSingletons.getSingletons(context); 125 final ChannelDataManager manager = tvSingletons.getChannelDataManager(); 126 manager.updateChannels( 127 new Runnable() { 128 @Override 129 public void run() { 130 Channel firstChannelForInput = null; 131 boolean browsableChanged = false; 132 for (Channel channel : manager.getChannelList()) { 133 if (channel.getInputId().equals(inputId)) { 134 if (!channel.isBrowsable()) { 135 manager.updateBrowsable(channel.getId(), true, true); 136 browsableChanged = true; 137 } 138 if (firstChannelForInput == null) { 139 firstChannelForInput = channel; 140 } 141 } 142 } 143 if (firstChannelForInput != null) { 144 Utils.setLastWatchedChannel(context, firstChannelForInput); 145 } 146 if (browsableChanged) { 147 manager.notifyChannelBrowsableChanged(); 148 manager.applyUpdatedValuesToDb(); 149 } 150 if (postRunnable != null) { 151 postRunnable.run(); 152 } 153 } 154 }); 155 } 156 157 /** Marks the channels in newly installed inputs browsable. */ 158 @UiThread 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 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 mContext.getPackageManager() 344 .getPackageInfo( 345 ComponentName.unflattenFromString(input).getPackageName(), 346 PackageManager.GET_ACTIVITIES); 347 Log.i(TAG, "TV input (" + input + ") is removed but package is not deleted"); 348 } catch (NameNotFoundException e) { 349 Log.i(TAG, "TV input (" + input + ") and its package are removed"); 350 mRecognizedInputs.remove(input); 351 mSetUpInputs.remove(input); 352 mKnownInputs.remove(input); 353 inputPackageDeleted = true; 354 } 355 } 356 if (inputPackageDeleted) { 357 mSharedPreferences 358 .edit() 359 .putStringSet(PREF_KEY_SET_UP_INPUTS, mSetUpInputs) 360 .putStringSet(PREF_KEY_KNOWN_INPUTS, mKnownInputs) 361 .putStringSet(PREF_KEY_RECOGNIZED_INPUTS, mRecognizedInputs) 362 .apply(); 363 } 364 } 365 } 366 367 /** 368 * Called when an setup is done. Once it is called, {@link #isSetupDone} returns {@code true} 369 * for {@code inputId}. 370 */ 371 private void onSetupDone(String inputId) { 372 SoftPreconditions.checkState(inputId != null); 373 if (DEBUG) Log.d(TAG, "onSetupDone: input=" + inputId); 374 if (!mRecognizedInputs.contains(inputId)) { 375 Log.i(TAG, "An unrecognized input's setup has been done. inputId=" + inputId); 376 mRecognizedInputs.add(inputId); 377 mSharedPreferences 378 .edit() 379 .putStringSet(PREF_KEY_RECOGNIZED_INPUTS, mRecognizedInputs) 380 .apply(); 381 } 382 if (!mKnownInputs.contains(inputId)) { 383 Log.i(TAG, "An unknown input's setup has been done. inputId=" + inputId); 384 mKnownInputs.add(inputId); 385 mSharedPreferences.edit().putStringSet(PREF_KEY_KNOWN_INPUTS, mKnownInputs).apply(); 386 } 387 if (!mSetUpInputs.contains(inputId)) { 388 mSetUpInputs.add(inputId); 389 mSharedPreferences.edit().putStringSet(PREF_KEY_SET_UP_INPUTS, mSetUpInputs).apply(); 390 } 391 } 392 } 393