• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
SetupUtils(TvApplication tvApplication)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      */
getInstance(Context context)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      */
onTvInputSetupFinished(final String inputId, @Nullable final Runnable postRunnable)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 
updateChannelsAfterSetup(Context context, final String inputId, final Runnable postRunnable)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
markNewChannelsBrowsable()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 
isFirstTune()194     public boolean isFirstTune() {
195         return mIsFirstTune;
196     }
197 
198     /**
199      * Returns true, if the input with {@code inputId} is newly installed.
200      */
isNewInput(String inputId)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      */
markAsKnownInput(String inputId)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      */
isSetupDone(String inputId)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      */
hasNewInput(TvInputManagerHelper inputManager)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      */
isRecognizedInput(String inputId)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      */
markAllInputsRecognized(TvInputManagerHelper inputManager)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      */
hasUnrecognizedInput(TvInputManagerHelper inputManager)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      */
grantEpgPermissionToSetUpPackages(Context context)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      */
grantEpgPermission(Context context, String packageName)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      */
onTuned()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      */
onInputListUpdated(TvInputManager manager)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      */
onSetupDone(String inputId)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 }