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 
SetupUtils(TvApplication tvApplication)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      */
getInstance(Context context)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      */
onTvInputSetupFinished(final String inputId, @Nullable final Runnable postRunnable)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 
updateChannelBrowsable(Context context, final String inputId, final Runnable postRunnable)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
markNewChannelsBrowsable()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 
isFirstTune()189     public boolean isFirstTune() {
190         return mIsFirstTune;
191     }
192 
193     /**
194      * Returns true, if the input with {@code inputId} is newly installed.
195      */
isNewInput(String inputId)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      */
markAsKnownInput(String inputId)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      */
isSetupDone(String inputId)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      */
hasNewInput(TvInputManagerHelper inputManager)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      */
isRecognizedInput(String inputId)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      */
markAllInputsRecognized(TvInputManagerHelper inputManager)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      */
hasUnrecognizedInput(TvInputManagerHelper inputManager)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      */
grantEpgPermissionToSetUpPackages(Context context)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      */
grantEpgPermission(Context context, String packageName)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      */
onTuned()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      */
onInputListUpdated(TvInputManager manager)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      */
onSetupDone(String inputId)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