blob: 52b3e3e81fb9231c544221d8efeb4e14543d9777 [file] [log] [blame]
/*
* 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();
}
}
}