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