| /* |
| * Copyright (C) 2015 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.tv.util; |
| |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.SharedPreferences; |
| import android.content.pm.PackageManager; |
| import android.content.pm.PackageManager.NameNotFoundException; |
| import android.media.tv.TvContract; |
| import android.media.tv.TvInputInfo; |
| import android.media.tv.TvInputManager; |
| import android.preference.PreferenceManager; |
| import android.support.annotation.Nullable; |
| import android.support.annotation.UiThread; |
| import android.text.TextUtils; |
| import android.util.ArraySet; |
| import android.util.Log; |
| import com.android.tv.TvSingletons; |
| import com.android.tv.common.SoftPreconditions; |
| import com.android.tv.common.dagger.annotations.ApplicationContext; |
| import com.android.tv.common.singletons.HasTvInputId; |
| import com.android.tv.data.ChannelDataManager; |
| import com.android.tv.data.api.Channel; |
| import com.android.tv.tunerinputcontroller.BuiltInTunerManager; |
| import com.google.common.base.Optional; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.Set; |
| import javax.inject.Inject; |
| import javax.inject.Singleton; |
| |
| /** A utility class related to input setup. */ |
| @Singleton |
| public class SetupUtils { |
| private static final String TAG = "SetupUtils"; |
| private static final boolean DEBUG = false; |
| |
| // Known inputs are inputs which are shown in SetupView before. When a new input is installed, |
| // the input will not be included in "PREF_KEY_KNOWN_INPUTS". |
| private static final String PREF_KEY_KNOWN_INPUTS = "known_inputs"; |
| // Set up inputs are inputs whose setup activity has been launched and finished successfully. |
| private static final String PREF_KEY_SET_UP_INPUTS = "set_up_inputs"; |
| // Recognized inputs means that the user already knows the inputs are installed. |
| private static final String PREF_KEY_RECOGNIZED_INPUTS = "recognized_inputs"; |
| private static final String PREF_KEY_IS_FIRST_TUNE = "is_first_tune"; |
| |
| private final Context mContext; |
| private final SharedPreferences mSharedPreferences; |
| private final Set<String> mKnownInputs; |
| private final Set<String> mSetUpInputs; |
| private final Set<String> mRecognizedInputs; |
| private boolean mIsFirstTune; |
| private final Optional<String> mOptionalTunerInputId; |
| |
| @Inject |
| public SetupUtils( |
| @ApplicationContext Context context, |
| Optional<BuiltInTunerManager> optionalBuiltInTunerManager) { |
| mContext = context; |
| mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); |
| mSetUpInputs = new ArraySet<>(); |
| mSetUpInputs.addAll( |
| mSharedPreferences.getStringSet(PREF_KEY_SET_UP_INPUTS, Collections.emptySet())); |
| mKnownInputs = new ArraySet<>(); |
| mKnownInputs.addAll( |
| mSharedPreferences.getStringSet(PREF_KEY_KNOWN_INPUTS, Collections.emptySet())); |
| mRecognizedInputs = new ArraySet<>(); |
| mRecognizedInputs.addAll( |
| mSharedPreferences.getStringSet(PREF_KEY_RECOGNIZED_INPUTS, mKnownInputs)); |
| mIsFirstTune = mSharedPreferences.getBoolean(PREF_KEY_IS_FIRST_TUNE, true); |
| mOptionalTunerInputId = |
| optionalBuiltInTunerManager.transform(HasTvInputId::getEmbeddedTunerInputId); |
| } |
| |
| /** Additional work after the setup of TV input. */ |
| public void onTvInputSetupFinished( |
| final String inputId, @Nullable final Runnable postRunnable) { |
| // When TIS adds several channels, ChannelDataManager.Listener.onChannelList |
| // Updated() can be called several times. In this case, it is hard to detect |
| // which one is the last callback. To reduce error prune, we update channel |
| // list again and make all channels of {@code inputId} browsable. |
| onSetupDone(inputId); |
| final ChannelDataManager manager = |
| TvSingletons.getSingletons(mContext).getChannelDataManager(); |
| if (!manager.isDbLoadFinished()) { |
| manager.addListener( |
| new ChannelDataManager.Listener() { |
| @Override |
| public void onLoadFinished() { |
| manager.removeListener(this); |
| updateChannelsAfterSetup(mContext, inputId, postRunnable); |
| } |
| |
| @Override |
| public void onChannelListUpdated() {} |
| |
| @Override |
| public void onChannelBrowsableChanged() {} |
| }); |
| } else { |
| updateChannelsAfterSetup(mContext, inputId, postRunnable); |
| } |
| } |
| |
| private static void updateChannelsAfterSetup( |
| Context context, final String inputId, final Runnable postRunnable) { |
| TvSingletons tvSingletons = TvSingletons.getSingletons(context); |
| final ChannelDataManager manager = tvSingletons.getChannelDataManager(); |
| manager.updateChannels( |
| () -> { |
| Channel firstChannelForInput = null; |
| boolean browsableChanged = false; |
| for (Channel channel : manager.getChannelList()) { |
| if (channel.getInputId().equals(inputId)) { |
| if (!channel.isBrowsable()) { |
| manager.updateBrowsable(channel.getId(), true, true); |
| browsableChanged = true; |
| } |
| if (firstChannelForInput == null) { |
| firstChannelForInput = channel; |
| } |
| } |
| } |
| if (firstChannelForInput != null) { |
| Utils.setLastWatchedChannel(context, firstChannelForInput); |
| } |
| if (browsableChanged) { |
| manager.notifyChannelBrowsableChanged(); |
| manager.applyUpdatedValuesToDb(); |
| } |
| if (postRunnable != null) { |
| postRunnable.run(); |
| } |
| }); |
| } |
| |
| /** Marks the channels in newly installed inputs browsable. */ |
| @UiThread |
| public void markNewChannelsBrowsable() { |
| Set<String> newInputsWithChannels = new HashSet<>(); |
| TvSingletons singletons = TvSingletons.getSingletons(mContext); |
| TvInputManagerHelper tvInputManagerHelper = singletons.getTvInputManagerHelper(); |
| ChannelDataManager channelDataManager = singletons.getChannelDataManager(); |
| SoftPreconditions.checkState(channelDataManager.isDbLoadFinished()); |
| for (TvInputInfo input : tvInputManagerHelper.getTvInputInfos(true, true)) { |
| String inputId = input.getId(); |
| if (!isSetupDone(inputId) && channelDataManager.getChannelCountForInput(inputId) > 0) { |
| onSetupDone(inputId); |
| newInputsWithChannels.add(inputId); |
| if (DEBUG) { |
| Log.d( |
| TAG, |
| "New input " |
| + inputId |
| + " has " |
| + channelDataManager.getChannelCountForInput(inputId) |
| + " channels"); |
| } |
| } |
| } |
| if (!newInputsWithChannels.isEmpty()) { |
| for (Channel channel : channelDataManager.getChannelList()) { |
| if (newInputsWithChannels.contains(channel.getInputId())) { |
| channelDataManager.updateBrowsable(channel.getId(), true); |
| } |
| } |
| channelDataManager.applyUpdatedValuesToDb(); |
| } |
| } |
| |
| public boolean isFirstTune() { |
| return mIsFirstTune; |
| } |
| |
| /** Returns true, if the input with {@code inputId} is newly installed. */ |
| public boolean isNewInput(String inputId) { |
| return !mKnownInputs.contains(inputId); |
| } |
| |
| /** |
| * Marks an input with {@code inputId} as a known input. Once it is marked, {@link #isNewInput} |
| * will return false. |
| */ |
| public void markAsKnownInput(String inputId) { |
| mKnownInputs.add(inputId); |
| mRecognizedInputs.add(inputId); |
| mSharedPreferences |
| .edit() |
| .putStringSet(PREF_KEY_KNOWN_INPUTS, mKnownInputs) |
| .putStringSet(PREF_KEY_RECOGNIZED_INPUTS, mRecognizedInputs) |
| .apply(); |
| } |
| |
| /** Returns {@code true}, if {@code inputId}'s setup has been done before. */ |
| public boolean isSetupDone(String inputId) { |
| boolean done = mSetUpInputs.contains(inputId); |
| if (DEBUG) { |
| Log.d(TAG, "isSetupDone: (input=" + inputId + ", result= " + done + ")"); |
| } |
| return done; |
| } |
| |
| /** Returns true, if there is any newly installed input. */ |
| public boolean hasNewInput(TvInputManagerHelper inputManager) { |
| for (TvInputInfo input : inputManager.getTvInputInfos(true, true)) { |
| if (isNewInput(input.getId())) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** Checks whether the given input is already recognized by the user or not. */ |
| private boolean isRecognizedInput(String inputId) { |
| return mRecognizedInputs.contains(inputId); |
| } |
| |
| /** |
| * Marks all the inputs as recognized inputs. Once it is marked, {@link #isRecognizedInput} will |
| * return {@code true}. |
| */ |
| public void markAllInputsRecognized(TvInputManagerHelper inputManager) { |
| for (TvInputInfo input : inputManager.getTvInputInfos(true, true)) { |
| mRecognizedInputs.add(input.getId()); |
| } |
| mSharedPreferences |
| .edit() |
| .putStringSet(PREF_KEY_RECOGNIZED_INPUTS, mRecognizedInputs) |
| .apply(); |
| } |
| |
| /** Checks whether there are any unrecognized inputs. */ |
| public boolean hasUnrecognizedInput(TvInputManagerHelper inputManager) { |
| for (TvInputInfo input : inputManager.getTvInputInfos(true, true)) { |
| if (!isRecognizedInput(input.getId())) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Grants permission for writing EPG data to all verified packages. |
| * |
| * @param context The Context used for granting permission. |
| */ |
| public static void grantEpgPermissionToSetUpPackages(Context context) { |
| // Find all already-verified packages. |
| Set<String> setUpPackages = new HashSet<>(); |
| SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); |
| for (String input : |
| sp.getStringSet(PREF_KEY_SET_UP_INPUTS, Collections.<String>emptySet())) { |
| if (!TextUtils.isEmpty(input)) { |
| ComponentName componentName = ComponentName.unflattenFromString(input); |
| if (componentName != null) { |
| setUpPackages.add(componentName.getPackageName()); |
| } |
| } |
| } |
| |
| for (String packageName : setUpPackages) { |
| grantEpgPermission(context, packageName); |
| } |
| } |
| |
| /** |
| * Grants permission for writing EPG data to a given package. |
| * |
| * @param context The Context used for granting permission. |
| * @param packageName The name of the package to give permission. |
| */ |
| public static void grantEpgPermission(Context context, String packageName) { |
| if (DEBUG) { |
| Log.d( |
| TAG, |
| "grantEpgPermission(context=" + context + ", packageName=" + packageName + ")"); |
| } |
| try { |
| int modeFlags = |
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION |
| | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION; |
| context.grantUriPermission(packageName, TvContract.Channels.CONTENT_URI, modeFlags); |
| context.grantUriPermission(packageName, TvContract.Programs.CONTENT_URI, modeFlags); |
| } catch (SecurityException e) { |
| Log.e( |
| TAG, |
| "Either TvProvider does not allow granting of Uri permissions or the app" |
| + " does not have permission.", |
| e); |
| } |
| } |
| |
| /** |
| * Called when TV app is launched. Once it is called, {@link #isFirstTune} will return false. |
| */ |
| public void onTuned() { |
| if (!mIsFirstTune) { |
| return; |
| } |
| mIsFirstTune = false; |
| mSharedPreferences.edit().putBoolean(PREF_KEY_IS_FIRST_TUNE, false).apply(); |
| } |
| |
| /** Called when input list is changed. It mainly handles input removals. */ |
| public void onInputListUpdated(TvInputManager manager) { |
| // mRecognizedInputs > mKnownInputs > mSetUpInputs. |
| Set<String> removedInputList = new HashSet<>(mRecognizedInputs); |
| for (TvInputInfo input : manager.getTvInputList()) { |
| removedInputList.remove(input.getId()); |
| } |
| // A USB tuner device can be temporarily unplugged. We do not remove the USB tuner input |
| // from the known inputs so that the input won't appear as a new input whenever the user |
| // plugs in the USB tuner device again. |
| if (mOptionalTunerInputId.isPresent()) { |
| removedInputList.remove(mOptionalTunerInputId.get()); |
| } |
| |
| if (!removedInputList.isEmpty()) { |
| boolean inputPackageDeleted = false; |
| for (String input : removedInputList) { |
| try { |
| // Just after booting, input list from TvInputManager are not reliable. |
| // So we need to double-check package existence. b/29034900 |
| mContext.getPackageManager() |
| .getPackageInfo( |
| ComponentName.unflattenFromString(input).getPackageName(), |
| PackageManager.GET_ACTIVITIES); |
| Log.i(TAG, "TV input (" + input + ") is removed but package is not deleted"); |
| } catch (NameNotFoundException e) { |
| Log.i(TAG, "TV input (" + input + ") and its package are removed"); |
| mRecognizedInputs.remove(input); |
| mSetUpInputs.remove(input); |
| mKnownInputs.remove(input); |
| inputPackageDeleted = true; |
| } |
| } |
| if (inputPackageDeleted) { |
| mSharedPreferences |
| .edit() |
| .putStringSet(PREF_KEY_SET_UP_INPUTS, mSetUpInputs) |
| .putStringSet(PREF_KEY_KNOWN_INPUTS, mKnownInputs) |
| .putStringSet(PREF_KEY_RECOGNIZED_INPUTS, mRecognizedInputs) |
| .apply(); |
| } |
| } |
| } |
| |
| /** |
| * Called when an setup is done. Once it is called, {@link #isSetupDone} returns {@code true} |
| * for {@code inputId}. |
| */ |
| private void onSetupDone(String inputId) { |
| SoftPreconditions.checkState(inputId != null); |
| if (DEBUG) Log.d(TAG, "onSetupDone: input=" + inputId); |
| if (!mRecognizedInputs.contains(inputId)) { |
| Log.i(TAG, "An unrecognized input's setup has been done. inputId=" + inputId); |
| mRecognizedInputs.add(inputId); |
| mSharedPreferences |
| .edit() |
| .putStringSet(PREF_KEY_RECOGNIZED_INPUTS, mRecognizedInputs) |
| .apply(); |
| } |
| if (!mKnownInputs.contains(inputId)) { |
| Log.i(TAG, "An unknown input's setup has been done. inputId=" + inputId); |
| mKnownInputs.add(inputId); |
| mSharedPreferences.edit().putStringSet(PREF_KEY_KNOWN_INPUTS, mKnownInputs).apply(); |
| } |
| if (!mSetUpInputs.contains(inputId)) { |
| mSetUpInputs.add(inputId); |
| mSharedPreferences.edit().putStringSet(PREF_KEY_SET_UP_INPUTS, mSetUpInputs).apply(); |
| } |
| } |
| } |