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