• 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 import com.android.tv.R;
35 import com.android.tv.TvSingletons;
36 import com.android.tv.common.SoftPreconditions;
37 import com.android.tv.common.dagger.annotations.ApplicationContext;
38 import com.android.tv.common.singletons.HasTvInputId;
39 import com.android.tv.common.util.CommonUtils;
40 import com.android.tv.data.ChannelDataManager;
41 import com.android.tv.data.api.Channel;
42 import com.android.tv.tunerinputcontroller.BuiltInTunerManager;
43 import com.google.common.base.Optional;
44 
45 import java.util.Arrays;
46 import java.util.Collections;
47 import java.util.HashSet;
48 import java.util.Set;
49 import javax.inject.Inject;
50 import javax.inject.Singleton;
51 
52 /** A utility class related to input setup. */
53 @Singleton
54 public class SetupUtils {
55     private static final String TAG = "SetupUtils";
56     private static final boolean DEBUG = false;
57 
58     // Known inputs are inputs which are shown in SetupView before. When a new input is installed,
59     // the input will not be included in "PREF_KEY_KNOWN_INPUTS".
60     private static final String PREF_KEY_KNOWN_INPUTS = "known_inputs";
61     // Set up inputs are inputs whose setup activity has been launched and finished successfully.
62     private static final String PREF_KEY_SET_UP_INPUTS = "set_up_inputs";
63     // Recognized inputs means that the user already knows the inputs are installed.
64     private static final String PREF_KEY_RECOGNIZED_INPUTS = "recognized_inputs";
65     private static final String PREF_KEY_IS_FIRST_TUNE = "is_first_tune";
66 
67     private final Context mContext;
68     private final SharedPreferences mSharedPreferences;
69     private final Set<String> mKnownInputs;
70     private final Set<String> mSetUpInputs;
71     private final Set<String> mRecognizedInputs;
72     private boolean mIsFirstTune;
73     private final Optional<String> mOptionalTunerInputId;
74 
75     @Inject
SetupUtils( @pplicationContext Context context, Optional<BuiltInTunerManager> optionalBuiltInTunerManager)76     public SetupUtils(
77             @ApplicationContext Context context,
78             Optional<BuiltInTunerManager> optionalBuiltInTunerManager) {
79         mContext = context;
80         mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
81         mSetUpInputs = new ArraySet<>();
82         mSetUpInputs.addAll(
83                 mSharedPreferences.getStringSet(PREF_KEY_SET_UP_INPUTS, Collections.emptySet()));
84         mKnownInputs = new ArraySet<>();
85         mKnownInputs.addAll(
86                 mSharedPreferences.getStringSet(PREF_KEY_KNOWN_INPUTS, Collections.emptySet()));
87         mRecognizedInputs = new ArraySet<>();
88         mRecognizedInputs.addAll(
89                 mSharedPreferences.getStringSet(PREF_KEY_RECOGNIZED_INPUTS, mKnownInputs));
90         mIsFirstTune = mSharedPreferences.getBoolean(PREF_KEY_IS_FIRST_TUNE, true);
91         mOptionalTunerInputId =
92                 optionalBuiltInTunerManager.transform(HasTvInputId::getEmbeddedTunerInputId);
93     }
94 
95     /** Additional work after the setup of TV input. */
onTvInputSetupFinished( final String inputId, @Nullable final Runnable postRunnable)96     public void onTvInputSetupFinished(
97             final String inputId, @Nullable final Runnable postRunnable) {
98         // When TIS adds several channels, ChannelDataManager.Listener.onChannelList
99         // Updated() can be called several times. In this case, it is hard to detect
100         // which one is the last callback. To reduce error prune, we update channel
101         // list again and make all channels of {@code inputId} browsable.
102         onSetupDone(inputId);
103         final ChannelDataManager manager =
104                 TvSingletons.getSingletons(mContext).getChannelDataManager();
105         if (!manager.isDbLoadFinished()) {
106             manager.addListener(
107                     new ChannelDataManager.Listener() {
108                         @Override
109                         public void onLoadFinished() {
110                             manager.removeListener(this);
111                             updateChannelsAfterSetup(mContext, inputId, postRunnable);
112                         }
113 
114                         @Override
115                         public void onChannelListUpdated() {}
116 
117                         @Override
118                         public void onChannelBrowsableChanged() {}
119                     });
120         } else {
121             updateChannelsAfterSetup(mContext, inputId, postRunnable);
122         }
123     }
124 
updateChannelsAfterSetup( Context context, final String inputId, final Runnable postRunnable)125     private static void updateChannelsAfterSetup(
126             Context context, final String inputId, final Runnable postRunnable) {
127         TvSingletons tvSingletons = TvSingletons.getSingletons(context);
128         final ChannelDataManager manager = tvSingletons.getChannelDataManager();
129         manager.updateChannels(
130                 () -> {
131                     Channel firstChannelForInput = null;
132                     boolean browsableChanged = false;
133                     for (Channel channel : manager.getChannelList()) {
134                         if (channel.getInputId().equals(inputId)) {
135                             if (!channel.isBrowsable()) {
136                                 manager.updateBrowsable(channel.getId(), true, true);
137                                 browsableChanged = true;
138                             }
139                             if (firstChannelForInput == null) {
140                                 firstChannelForInput = channel;
141                             }
142                         }
143                     }
144                     if (firstChannelForInput != null) {
145                         Utils.setLastWatchedChannel(context, firstChannelForInput);
146                     }
147                     if (browsableChanged) {
148                         manager.notifyChannelBrowsableChanged();
149                         manager.applyUpdatedValuesToDb();
150                     }
151                     if (postRunnable != null) {
152                         postRunnable.run();
153                     }
154                 });
155     }
156 
157     /** Marks the channels in newly installed inputs browsable. */
158     @UiThread
markNewChannelsBrowsable()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 
isFirstTune()191     public boolean isFirstTune() {
192         return mIsFirstTune;
193     }
194 
195     /** Returns true, if the input with {@code inputId} is newly installed. */
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
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. */
isSetupDone(String inputId)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. */
hasNewInput(TvInputManagerHelper inputManager)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. */
isRecognizedInput(String inputId)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      */
markAllInputsRecognized(TvInputManagerHelper inputManager)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. */
hasUnrecognizedInput(TvInputManagerHelper inputManager)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      */
grantEpgPermissionToSetUpPackages(Context context)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      */
grantEpgPermission(Context context, String packageName)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 TV app is launched. Once it is called, {@link #isFirstTune} will return false.
315      */
onTuned()316     public void onTuned() {
317         if (!mIsFirstTune) {
318             return;
319         }
320         mIsFirstTune = false;
321         mSharedPreferences.edit().putBoolean(PREF_KEY_IS_FIRST_TUNE, false).apply();
322     }
323 
324     /** Called when input list is changed. It mainly handles input removals. */
onInputListUpdated(TvInputManager manager)325     public void onInputListUpdated(TvInputManager manager) {
326         // mRecognizedInputs > mKnownInputs > mSetUpInputs.
327         Set<String> removedInputList = new HashSet<>(mRecognizedInputs);
328         for (TvInputInfo input : manager.getTvInputList()) {
329             removedInputList.remove(input.getId());
330         }
331         // A USB tuner device can be temporarily unplugged. We do not remove the USB tuner input
332         // from the known inputs so that the input won't appear as a new input whenever the user
333         // plugs in the USB tuner device again.
334         if (mOptionalTunerInputId.isPresent()) {
335             removedInputList.remove(mOptionalTunerInputId.get());
336         }
337 
338         if (!removedInputList.isEmpty()) {
339             boolean inputPackageDeleted = false;
340             for (String input : removedInputList) {
341                 try {
342                     // Just after booting, input list from TvInputManager are not reliable.
343                     // So we need to double-check package existence. b/29034900
344                     mContext.getPackageManager()
345                             .getPackageInfo(
346                                     ComponentName.unflattenFromString(input).getPackageName(),
347                                     PackageManager.GET_ACTIVITIES);
348                     Log.i(TAG, "TV input (" + input + ") is removed but package is not deleted");
349                 } catch (NameNotFoundException e) {
350                     Log.i(TAG, "TV input (" + input + ") and its package are removed");
351                     mRecognizedInputs.remove(input);
352                     mSetUpInputs.remove(input);
353                     mKnownInputs.remove(input);
354                     inputPackageDeleted = true;
355                 }
356             }
357             if (inputPackageDeleted) {
358                 mSharedPreferences
359                         .edit()
360                         .putStringSet(PREF_KEY_SET_UP_INPUTS, mSetUpInputs)
361                         .putStringSet(PREF_KEY_KNOWN_INPUTS, mKnownInputs)
362                         .putStringSet(PREF_KEY_RECOGNIZED_INPUTS, mRecognizedInputs)
363                         .apply();
364             }
365         }
366     }
367 
368     /**
369      * Create a Intent to launch setup activity for {@code inputId}. The setup activity defined
370      * in the overlayable resources precedes the one defined in the corresponding TV input service.
371      */
372     @Nullable
createSetupIntent(Context context, TvInputInfo input)373     public Intent createSetupIntent(Context context, TvInputInfo input) {
374         String[] componentStrings = context.getResources()
375                 .getStringArray(R.array.setup_ComponentNames);
376 
377         if (componentStrings != null) {
378             for (String component : componentStrings) {
379                 String[] split = component.split("#");
380                 if (split.length != 2) {
381                     Log.w(TAG, "Invalid component item: " + Arrays.toString(split));
382                     continue;
383                 }
384 
385                 final String inputId = split[0].trim();
386                 if (inputId.equals(input.getId())) {
387                     final String flattenedComponentName = split[1].trim();
388                     final ComponentName componentName = ComponentName
389                             .unflattenFromString(flattenedComponentName);
390                     if (componentName == null) {
391                         Log.w(TAG, "Failed to unflatten component: " + flattenedComponentName);
392                         continue;
393                     }
394 
395                     final Intent overlaySetupIntent = new Intent(Intent.ACTION_MAIN);
396                     overlaySetupIntent.setComponent(componentName);
397                     overlaySetupIntent.putExtra(TvInputInfo.EXTRA_INPUT_ID, inputId);
398 
399                     PackageManager pm = context.getPackageManager();
400                     if (overlaySetupIntent.resolveActivityInfo(pm, 0) == null) {
401                         Log.w(TAG, "unable to find component" + flattenedComponentName);
402                         continue;
403                     }
404 
405                     Log.i(TAG, "overlay input id: " + inputId
406                             + " to setup activity: " + flattenedComponentName);
407                     return CommonUtils.createSetupIntent(overlaySetupIntent, inputId);
408                 }
409             }
410         }
411         return CommonUtils.createSetupIntent(input);
412     }
413 
414     /**
415      * Called when an setup is done. Once it is called, {@link #isSetupDone} returns {@code true}
416      * for {@code inputId}.
417      */
onSetupDone(String inputId)418     private void onSetupDone(String inputId) {
419         SoftPreconditions.checkState(inputId != null);
420         if (DEBUG) Log.d(TAG, "onSetupDone: input=" + inputId);
421         if (!mRecognizedInputs.contains(inputId)) {
422             Log.i(TAG, "An unrecognized input's setup has been done. inputId=" + inputId);
423             mRecognizedInputs.add(inputId);
424             mSharedPreferences
425                     .edit()
426                     .putStringSet(PREF_KEY_RECOGNIZED_INPUTS, mRecognizedInputs)
427                     .apply();
428         }
429         if (!mKnownInputs.contains(inputId)) {
430             Log.i(TAG, "An unknown input's setup has been done. inputId=" + inputId);
431             mKnownInputs.add(inputId);
432             mSharedPreferences.edit().putStringSet(PREF_KEY_KNOWN_INPUTS, mKnownInputs).apply();
433         }
434         if (!mSetUpInputs.contains(inputId)) {
435             mSetUpInputs.add(inputId);
436             mSharedPreferences.edit().putStringSet(PREF_KEY_SET_UP_INPUTS, mSetUpInputs).apply();
437         }
438     }
439 }
440