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