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