Home | History | Annotate | Download | only in util
      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 
     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      */
     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      */
    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 
    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
    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 
    185     public boolean isFirstTune() {
    186         return mIsFirstTune;
    187     }
    188 
    189     /**
    190      * Returns true, if the input with {@code inputId} is newly installed.
    191      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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