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     // Whether to mark new channels to browsable.
67     private static final boolean MARK_NEW_CHANNELS_BROWSABLE = false;
68 
69     private final Context mContext;
70     private final SharedPreferences mSharedPreferences;
71     private final Set<String> mKnownInputs;
72     private final Set<String> mSetUpInputs;
73     private final Set<String> mRecognizedInputs;
74     private boolean mIsFirstTune;
75     private final Optional<String> mOptionalTunerInputId;
76 
77     @Inject
SetupUtils( @pplicationContext Context context, Optional<BuiltInTunerManager> optionalBuiltInTunerManager)78     public SetupUtils(
79             @ApplicationContext Context context,
80             Optional<BuiltInTunerManager> optionalBuiltInTunerManager) {
81         mContext = context;
82         mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
83         mSetUpInputs = new ArraySet<>();
84         mSetUpInputs.addAll(
85                 mSharedPreferences.getStringSet(PREF_KEY_SET_UP_INPUTS, Collections.emptySet()));
86         mKnownInputs = new ArraySet<>();
87         mKnownInputs.addAll(
88                 mSharedPreferences.getStringSet(PREF_KEY_KNOWN_INPUTS, Collections.emptySet()));
89         mRecognizedInputs = new ArraySet<>();
90         mRecognizedInputs.addAll(
91                 mSharedPreferences.getStringSet(PREF_KEY_RECOGNIZED_INPUTS, mKnownInputs));
92         mIsFirstTune = mSharedPreferences.getBoolean(PREF_KEY_IS_FIRST_TUNE, true);
93         mOptionalTunerInputId =
94                 optionalBuiltInTunerManager.transform(HasTvInputId::getEmbeddedTunerInputId);
95     }
96 
97     /** Additional work after the setup of TV input. */
onTvInputSetupFinished( final String inputId, @Nullable final Runnable postRunnable)98     public void onTvInputSetupFinished(
99             final String inputId, @Nullable final Runnable postRunnable) {
100         // When TIS adds several channels, ChannelDataManager.Listener.onChannelList
101         // Updated() can be called several times. In this case, it is hard to detect
102         // which one is the last callback. To reduce error prune, we update channel
103         // list again and make all channels of {@code inputId} browsable.
104         onSetupDone(inputId);
105         final ChannelDataManager manager =
106                 TvSingletons.getSingletons(mContext).getChannelDataManager();
107         if (!manager.isDbLoadFinished()) {
108             manager.addListener(
109                     new ChannelDataManager.Listener() {
110                         @Override
111                         public void onLoadFinished() {
112                             manager.removeListener(this);
113                             updateChannelsAfterSetup(mContext, inputId, postRunnable);
114                         }
115 
116                         @Override
117                         public void onChannelListUpdated() {}
118 
119                         @Override
120                         public void onChannelBrowsableChanged() {}
121                     });
122         } else {
123             updateChannelsAfterSetup(mContext, inputId, postRunnable);
124         }
125     }
126 
updateChannelsAfterSetup( Context context, final String inputId, final Runnable postRunnable)127     private static void updateChannelsAfterSetup(
128             Context context, final String inputId, final Runnable postRunnable) {
129         TvSingletons tvSingletons = TvSingletons.getSingletons(context);
130         final ChannelDataManager manager = tvSingletons.getChannelDataManager();
131         manager.updateChannels(
132                 () -> {
133                     Channel firstChannelForInput = null;
134                     boolean browsableChanged = false;
135                     for (Channel channel : manager.getChannelList()) {
136                         if (channel.getInputId().equals(inputId)) {
137                             if (!channel.isBrowsable() && MARK_NEW_CHANNELS_BROWSABLE) {
138                                 manager.updateBrowsable(channel.getId(), true, true);
139                                 browsableChanged = true;
140                             }
141                             if (firstChannelForInput == null && channel.isBrowsable()) {
142                                 firstChannelForInput = channel;
143                             }
144                         }
145                     }
146                     if (firstChannelForInput != null) {
147                         Utils.setLastWatchedChannel(context, firstChannelForInput);
148                     }
149                     if (browsableChanged) {
150                         manager.notifyChannelBrowsableChanged();
151                         manager.applyUpdatedValuesToDb();
152                     }
153                     if (postRunnable != null) {
154                         postRunnable.run();
155                     }
156                 });
157     }
158 
159     /** Marks the channels in newly installed inputs browsable if enabled. */
160     @UiThread
markNewChannelsBrowsableIfEnabled()161     public void markNewChannelsBrowsableIfEnabled() {
162         // TODO(b/288499376): Handle browsable field for channels added outside Live TV app in a
163         // better way.
164         if (!MARK_NEW_CHANNELS_BROWSABLE) {
165             return;
166         }
167 
168         Set<String> newInputsWithChannels = new HashSet<>();
169         TvSingletons singletons = TvSingletons.getSingletons(mContext);
170         TvInputManagerHelper tvInputManagerHelper = singletons.getTvInputManagerHelper();
171         ChannelDataManager channelDataManager = singletons.getChannelDataManager();
172         SoftPreconditions.checkState(channelDataManager.isDbLoadFinished());
173         for (TvInputInfo input : tvInputManagerHelper.getTvInputInfos(true, true)) {
174             String inputId = input.getId();
175             if (!isSetupDone(inputId) && channelDataManager.getChannelCountForInput(inputId) > 0) {
176                 onSetupDone(inputId);
177                 newInputsWithChannels.add(inputId);
178                 if (DEBUG) {
179                     Log.d(
180                             TAG,
181                             "New input "
182                                     + inputId
183                                     + " has "
184                                     + channelDataManager.getChannelCountForInput(inputId)
185                                     + " channels");
186                 }
187             }
188         }
189         if (!newInputsWithChannels.isEmpty()) {
190             for (Channel channel : channelDataManager.getChannelList()) {
191                 if (newInputsWithChannels.contains(channel.getInputId())) {
192                     channelDataManager.updateBrowsable(channel.getId(), true);
193                 }
194             }
195             channelDataManager.applyUpdatedValuesToDb();
196         }
197     }
198 
isFirstTune()199     public boolean isFirstTune() {
200         return mIsFirstTune;
201     }
202 
203     /** Returns true, if the input with {@code inputId} is newly installed. */
isNewInput(String inputId)204     public boolean isNewInput(String inputId) {
205         return !mKnownInputs.contains(inputId);
206     }
207 
208     /**
209      * Marks an input with {@code inputId} as a known input. Once it is marked, {@link #isNewInput}
210      * will return false.
211      */
markAsKnownInput(String inputId)212     public void markAsKnownInput(String inputId) {
213         mKnownInputs.add(inputId);
214         mRecognizedInputs.add(inputId);
215         mSharedPreferences
216                 .edit()
217                 .putStringSet(PREF_KEY_KNOWN_INPUTS, mKnownInputs)
218                 .putStringSet(PREF_KEY_RECOGNIZED_INPUTS, mRecognizedInputs)
219                 .apply();
220     }
221 
222     /** Returns {@code true}, if {@code inputId}'s setup has been done before. */
isSetupDone(String inputId)223     public boolean isSetupDone(String inputId) {
224         boolean done = mSetUpInputs.contains(inputId);
225         if (DEBUG) {
226             Log.d(TAG, "isSetupDone: (input=" + inputId + ", result= " + done + ")");
227         }
228         return done;
229     }
230 
231     /** Returns true, if there is any newly installed input. */
hasNewInput(TvInputManagerHelper inputManager)232     public boolean hasNewInput(TvInputManagerHelper inputManager) {
233         for (TvInputInfo input : inputManager.getTvInputInfos(true, true)) {
234             if (isNewInput(input.getId())) {
235                 return true;
236             }
237         }
238         return false;
239     }
240 
241     /** Checks whether the given input is already recognized by the user or not. */
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
255                 .edit()
256                 .putStringSet(PREF_KEY_RECOGNIZED_INPUTS, mRecognizedInputs)
257                 .apply();
258     }
259 
260     /** Checks whether there are any unrecognized inputs. */
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 :
280                 sp.getStringSet(PREF_KEY_SET_UP_INPUTS, 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(
303                     TAG,
304                     "grantEpgPermission(context=" + context + ", packageName=" + packageName + ")");
305         }
306         try {
307             int modeFlags =
308                     Intent.FLAG_GRANT_WRITE_URI_PERMISSION
309                             | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION;
310             context.grantUriPermission(packageName, TvContract.Channels.CONTENT_URI, modeFlags);
311             context.grantUriPermission(packageName, TvContract.Programs.CONTENT_URI, modeFlags);
312         } catch (SecurityException e) {
313             Log.e(
314                     TAG,
315                     "Either TvProvider does not allow granting of Uri permissions or the app"
316                             + " does not have permission.",
317                     e);
318         }
319     }
320 
321     /**
322      * Called when TV app is launched. Once it is called, {@link #isFirstTune} will return false.
323      */
onTuned()324     public void onTuned() {
325         if (!mIsFirstTune) {
326             return;
327         }
328         mIsFirstTune = false;
329         mSharedPreferences.edit().putBoolean(PREF_KEY_IS_FIRST_TUNE, false).apply();
330     }
331 
332     /** Called when input list is changed. It mainly handles input removals. */
onInputListUpdated(TvInputManager manager)333     public void onInputListUpdated(TvInputManager manager) {
334         // mRecognizedInputs > mKnownInputs > mSetUpInputs.
335         Set<String> removedInputList = new HashSet<>(mRecognizedInputs);
336         for (TvInputInfo input : manager.getTvInputList()) {
337             removedInputList.remove(input.getId());
338         }
339         // A USB tuner device can be temporarily unplugged. We do not remove the USB tuner input
340         // from the known inputs so that the input won't appear as a new input whenever the user
341         // plugs in the USB tuner device again.
342         if (mOptionalTunerInputId.isPresent()) {
343             removedInputList.remove(mOptionalTunerInputId.get());
344         }
345 
346         if (!removedInputList.isEmpty()) {
347             boolean inputPackageDeleted = false;
348             for (String input : removedInputList) {
349                 try {
350                     // Just after booting, input list from TvInputManager are not reliable.
351                     // So we need to double-check package existence. b/29034900
352                     mContext.getPackageManager()
353                             .getPackageInfo(
354                                     ComponentName.unflattenFromString(input).getPackageName(),
355                                     PackageManager.GET_ACTIVITIES);
356                     Log.i(TAG, "TV input (" + input + ") is removed but package is not deleted");
357                 } catch (NameNotFoundException e) {
358                     Log.i(TAG, "TV input (" + input + ") and its package are removed");
359                     mRecognizedInputs.remove(input);
360                     mSetUpInputs.remove(input);
361                     mKnownInputs.remove(input);
362                     inputPackageDeleted = true;
363                 }
364             }
365             if (inputPackageDeleted) {
366                 mSharedPreferences
367                         .edit()
368                         .putStringSet(PREF_KEY_SET_UP_INPUTS, mSetUpInputs)
369                         .putStringSet(PREF_KEY_KNOWN_INPUTS, mKnownInputs)
370                         .putStringSet(PREF_KEY_RECOGNIZED_INPUTS, mRecognizedInputs)
371                         .apply();
372             }
373         }
374     }
375 
376     /**
377      * Create a Intent to launch setup activity for {@code inputId}. The setup activity defined
378      * in the overlayable resources precedes the one defined in the corresponding TV input service.
379      */
380     @Nullable
createSetupIntent(Context context, TvInputInfo input)381     public Intent createSetupIntent(Context context, TvInputInfo input) {
382         String[] componentStrings = context.getResources()
383                 .getStringArray(R.array.setup_ComponentNames);
384 
385         if (componentStrings != null) {
386             for (String component : componentStrings) {
387                 String[] split = component.split("#");
388                 if (split.length != 2) {
389                     Log.w(TAG, "Invalid component item: " + Arrays.toString(split));
390                     continue;
391                 }
392 
393                 final String inputId = split[0].trim();
394                 if (inputId.equals(input.getId())) {
395                     final String flattenedComponentName = split[1].trim();
396                     final ComponentName componentName = ComponentName
397                             .unflattenFromString(flattenedComponentName);
398                     if (componentName == null) {
399                         Log.w(TAG, "Failed to unflatten component: " + flattenedComponentName);
400                         continue;
401                     }
402 
403                     final Intent overlaySetupIntent = new Intent(Intent.ACTION_MAIN);
404                     overlaySetupIntent.setComponent(componentName);
405                     overlaySetupIntent.putExtra(TvInputInfo.EXTRA_INPUT_ID, inputId);
406 
407                     PackageManager pm = context.getPackageManager();
408                     if (overlaySetupIntent.resolveActivityInfo(pm, 0) == null) {
409                         Log.w(TAG, "unable to find component" + flattenedComponentName);
410                         continue;
411                     }
412 
413                     Log.i(TAG, "overlay input id: " + inputId
414                             + " to setup activity: " + flattenedComponentName);
415                     return CommonUtils.createSetupIntent(overlaySetupIntent, inputId);
416                 }
417             }
418         }
419         return CommonUtils.createSetupIntent(input);
420     }
421 
422     /**
423      * Called when an setup is done. Once it is called, {@link #isSetupDone} returns {@code true}
424      * for {@code inputId}.
425      */
onSetupDone(String inputId)426     private void onSetupDone(String inputId) {
427         SoftPreconditions.checkState(inputId != null);
428         if (DEBUG) Log.d(TAG, "onSetupDone: input=" + inputId);
429         if (!mRecognizedInputs.contains(inputId)) {
430             Log.i(TAG, "An unrecognized input's setup has been done. inputId=" + inputId);
431             mRecognizedInputs.add(inputId);
432             mSharedPreferences
433                     .edit()
434                     .putStringSet(PREF_KEY_RECOGNIZED_INPUTS, mRecognizedInputs)
435                     .apply();
436         }
437         if (!mKnownInputs.contains(inputId)) {
438             Log.i(TAG, "An unknown input's setup has been done. inputId=" + inputId);
439             mKnownInputs.add(inputId);
440             mSharedPreferences.edit().putStringSet(PREF_KEY_KNOWN_INPUTS, mKnownInputs).apply();
441         }
442         if (!mSetUpInputs.contains(inputId)) {
443             mSetUpInputs.add(inputId);
444             mSharedPreferences.edit().putStringSet(PREF_KEY_SET_UP_INPUTS, mSetUpInputs).apply();
445         }
446     }
447 }
448