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