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