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.TvSingletons;
35 import com.android.tv.common.SoftPreconditions;
36 import com.android.tv.common.dagger.annotations.ApplicationContext;
37 import com.android.tv.common.singletons.HasTvInputId;
38 import com.android.tv.data.ChannelDataManager;
39 import com.android.tv.data.api.Channel;
40 import com.android.tv.tunerinputcontroller.BuiltInTunerManager;
41 import com.google.common.base.Optional;
42 import java.util.Collections;
43 import java.util.HashSet;
44 import java.util.Set;
45 import javax.inject.Inject;
46 import javax.inject.Singleton;
47 
48 /** A utility class related to input setup. */
49 @Singleton
50 public class SetupUtils {
51     private static final String TAG = "SetupUtils";
52     private static final boolean DEBUG = false;
53 
54     // Known inputs are inputs which are shown in SetupView before. When a new input is installed,
55     // the input will not be included in "PREF_KEY_KNOWN_INPUTS".
56     private static final String PREF_KEY_KNOWN_INPUTS = "known_inputs";
57     // Set up inputs are inputs whose setup activity has been launched and finished successfully.
58     private static final String PREF_KEY_SET_UP_INPUTS = "set_up_inputs";
59     // Recognized inputs means that the user already knows the inputs are installed.
60     private static final String PREF_KEY_RECOGNIZED_INPUTS = "recognized_inputs";
61     private static final String PREF_KEY_IS_FIRST_TUNE = "is_first_tune";
62 
63     private final Context mContext;
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 Optional<String> mOptionalTunerInputId;
70 
71     @Inject
SetupUtils( @pplicationContext Context context, Optional<BuiltInTunerManager> optionalBuiltInTunerManager)72     public SetupUtils(
73             @ApplicationContext Context context,
74             Optional<BuiltInTunerManager> optionalBuiltInTunerManager) {
75         mContext = context;
76         mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
77         mSetUpInputs = new ArraySet<>();
78         mSetUpInputs.addAll(
79                 mSharedPreferences.getStringSet(PREF_KEY_SET_UP_INPUTS, Collections.emptySet()));
80         mKnownInputs = new ArraySet<>();
81         mKnownInputs.addAll(
82                 mSharedPreferences.getStringSet(PREF_KEY_KNOWN_INPUTS, Collections.emptySet()));
83         mRecognizedInputs = new ArraySet<>();
84         mRecognizedInputs.addAll(
85                 mSharedPreferences.getStringSet(PREF_KEY_RECOGNIZED_INPUTS, mKnownInputs));
86         mIsFirstTune = mSharedPreferences.getBoolean(PREF_KEY_IS_FIRST_TUNE, true);
87         mOptionalTunerInputId =
88                 optionalBuiltInTunerManager.transform(HasTvInputId::getEmbeddedTunerInputId);
89     }
90 
91     /** Additional work after the setup of TV input. */
onTvInputSetupFinished( final String inputId, @Nullable final Runnable postRunnable)92     public void onTvInputSetupFinished(
93             final String inputId, @Nullable final Runnable postRunnable) {
94         // When TIS adds several channels, ChannelDataManager.Listener.onChannelList
95         // Updated() can be called several times. In this case, it is hard to detect
96         // which one is the last callback. To reduce error prune, we update channel
97         // list again and make all channels of {@code inputId} browsable.
98         onSetupDone(inputId);
99         final ChannelDataManager manager =
100                 TvSingletons.getSingletons(mContext).getChannelDataManager();
101         if (!manager.isDbLoadFinished()) {
102             manager.addListener(
103                     new ChannelDataManager.Listener() {
104                         @Override
105                         public void onLoadFinished() {
106                             manager.removeListener(this);
107                             updateChannelsAfterSetup(mContext, inputId, postRunnable);
108                         }
109 
110                         @Override
111                         public void onChannelListUpdated() {}
112 
113                         @Override
114                         public void onChannelBrowsableChanged() {}
115                     });
116         } else {
117             updateChannelsAfterSetup(mContext, inputId, postRunnable);
118         }
119     }
120 
updateChannelsAfterSetup( Context context, final String inputId, final Runnable postRunnable)121     private static void updateChannelsAfterSetup(
122             Context context, final String inputId, final Runnable postRunnable) {
123         TvSingletons tvSingletons = TvSingletons.getSingletons(context);
124         final ChannelDataManager manager = tvSingletons.getChannelDataManager();
125         manager.updateChannels(
126                 () -> {
127                     Channel firstChannelForInput = null;
128                     boolean browsableChanged = false;
129                     for (Channel channel : manager.getChannelList()) {
130                         if (channel.getInputId().equals(inputId)) {
131                             if (!channel.isBrowsable()) {
132                                 manager.updateBrowsable(channel.getId(), true, true);
133                                 browsableChanged = true;
134                             }
135                             if (firstChannelForInput == null) {
136                                 firstChannelForInput = channel;
137                             }
138                         }
139                     }
140                     if (firstChannelForInput != null) {
141                         Utils.setLastWatchedChannel(context, firstChannelForInput);
142                     }
143                     if (browsableChanged) {
144                         manager.notifyChannelBrowsableChanged();
145                         manager.applyUpdatedValuesToDb();
146                     }
147                     if (postRunnable != null) {
148                         postRunnable.run();
149                     }
150                 });
151     }
152 
153     /** Marks the channels in newly installed inputs browsable. */
154     @UiThread
markNewChannelsBrowsable()155     public void markNewChannelsBrowsable() {
156         Set<String> newInputsWithChannels = new HashSet<>();
157         TvSingletons singletons = TvSingletons.getSingletons(mContext);
158         TvInputManagerHelper tvInputManagerHelper = singletons.getTvInputManagerHelper();
159         ChannelDataManager channelDataManager = singletons.getChannelDataManager();
160         SoftPreconditions.checkState(channelDataManager.isDbLoadFinished());
161         for (TvInputInfo input : tvInputManagerHelper.getTvInputInfos(true, true)) {
162             String inputId = input.getId();
163             if (!isSetupDone(inputId) && channelDataManager.getChannelCountForInput(inputId) > 0) {
164                 onSetupDone(inputId);
165                 newInputsWithChannels.add(inputId);
166                 if (DEBUG) {
167                     Log.d(
168                             TAG,
169                             "New input "
170                                     + inputId
171                                     + " has "
172                                     + channelDataManager.getChannelCountForInput(inputId)
173                                     + " channels");
174                 }
175             }
176         }
177         if (!newInputsWithChannels.isEmpty()) {
178             for (Channel channel : channelDataManager.getChannelList()) {
179                 if (newInputsWithChannels.contains(channel.getInputId())) {
180                     channelDataManager.updateBrowsable(channel.getId(), true);
181                 }
182             }
183             channelDataManager.applyUpdatedValuesToDb();
184         }
185     }
186 
isFirstTune()187     public boolean isFirstTune() {
188         return mIsFirstTune;
189     }
190 
191     /** Returns true, if the input with {@code inputId} is newly installed. */
isNewInput(String inputId)192     public boolean isNewInput(String inputId) {
193         return !mKnownInputs.contains(inputId);
194     }
195 
196     /**
197      * Marks an input with {@code inputId} as a known input. Once it is marked, {@link #isNewInput}
198      * will return false.
199      */
markAsKnownInput(String inputId)200     public void markAsKnownInput(String inputId) {
201         mKnownInputs.add(inputId);
202         mRecognizedInputs.add(inputId);
203         mSharedPreferences
204                 .edit()
205                 .putStringSet(PREF_KEY_KNOWN_INPUTS, mKnownInputs)
206                 .putStringSet(PREF_KEY_RECOGNIZED_INPUTS, mRecognizedInputs)
207                 .apply();
208     }
209 
210     /** Returns {@code true}, if {@code inputId}'s setup has been done before. */
isSetupDone(String inputId)211     public boolean isSetupDone(String inputId) {
212         boolean done = mSetUpInputs.contains(inputId);
213         if (DEBUG) {
214             Log.d(TAG, "isSetupDone: (input=" + inputId + ", result= " + done + ")");
215         }
216         return done;
217     }
218 
219     /** Returns true, if there is any newly installed input. */
hasNewInput(TvInputManagerHelper inputManager)220     public boolean hasNewInput(TvInputManagerHelper inputManager) {
221         for (TvInputInfo input : inputManager.getTvInputInfos(true, true)) {
222             if (isNewInput(input.getId())) {
223                 return true;
224             }
225         }
226         return false;
227     }
228 
229     /** Checks whether the given input is already recognized by the user or not. */
isRecognizedInput(String inputId)230     private boolean isRecognizedInput(String inputId) {
231         return mRecognizedInputs.contains(inputId);
232     }
233 
234     /**
235      * Marks all the inputs as recognized inputs. Once it is marked, {@link #isRecognizedInput} will
236      * return {@code true}.
237      */
markAllInputsRecognized(TvInputManagerHelper inputManager)238     public void markAllInputsRecognized(TvInputManagerHelper inputManager) {
239         for (TvInputInfo input : inputManager.getTvInputInfos(true, true)) {
240             mRecognizedInputs.add(input.getId());
241         }
242         mSharedPreferences
243                 .edit()
244                 .putStringSet(PREF_KEY_RECOGNIZED_INPUTS, mRecognizedInputs)
245                 .apply();
246     }
247 
248     /** Checks whether there are any unrecognized inputs. */
hasUnrecognizedInput(TvInputManagerHelper inputManager)249     public boolean hasUnrecognizedInput(TvInputManagerHelper inputManager) {
250         for (TvInputInfo input : inputManager.getTvInputInfos(true, true)) {
251             if (!isRecognizedInput(input.getId())) {
252                 return true;
253             }
254         }
255         return false;
256     }
257 
258     /**
259      * Grants permission for writing EPG data to all verified packages.
260      *
261      * @param context The Context used for granting permission.
262      */
grantEpgPermissionToSetUpPackages(Context context)263     public static void grantEpgPermissionToSetUpPackages(Context context) {
264         // Find all already-verified packages.
265         Set<String> setUpPackages = new HashSet<>();
266         SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
267         for (String input :
268                 sp.getStringSet(PREF_KEY_SET_UP_INPUTS, Collections.<String>emptySet())) {
269             if (!TextUtils.isEmpty(input)) {
270                 ComponentName componentName = ComponentName.unflattenFromString(input);
271                 if (componentName != null) {
272                     setUpPackages.add(componentName.getPackageName());
273                 }
274             }
275         }
276 
277         for (String packageName : setUpPackages) {
278             grantEpgPermission(context, packageName);
279         }
280     }
281 
282     /**
283      * Grants permission for writing EPG data to a given package.
284      *
285      * @param context The Context used for granting permission.
286      * @param packageName The name of the package to give permission.
287      */
grantEpgPermission(Context context, String packageName)288     public static void grantEpgPermission(Context context, String packageName) {
289         if (DEBUG) {
290             Log.d(
291                     TAG,
292                     "grantEpgPermission(context=" + context + ", packageName=" + packageName + ")");
293         }
294         try {
295             int modeFlags =
296                     Intent.FLAG_GRANT_WRITE_URI_PERMISSION
297                             | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION;
298             context.grantUriPermission(packageName, TvContract.Channels.CONTENT_URI, modeFlags);
299             context.grantUriPermission(packageName, TvContract.Programs.CONTENT_URI, modeFlags);
300         } catch (SecurityException e) {
301             Log.e(
302                     TAG,
303                     "Either TvProvider does not allow granting of Uri permissions or the app"
304                             + " does not have permission.",
305                     e);
306         }
307     }
308 
309     /**
310      * Called when TV app is launched. Once it is called, {@link #isFirstTune} will return false.
311      */
onTuned()312     public void onTuned() {
313         if (!mIsFirstTune) {
314             return;
315         }
316         mIsFirstTune = false;
317         mSharedPreferences.edit().putBoolean(PREF_KEY_IS_FIRST_TUNE, false).apply();
318     }
319 
320     /** Called when input list is changed. It mainly handles input removals. */
onInputListUpdated(TvInputManager manager)321     public void onInputListUpdated(TvInputManager manager) {
322         // mRecognizedInputs > mKnownInputs > mSetUpInputs.
323         Set<String> removedInputList = new HashSet<>(mRecognizedInputs);
324         for (TvInputInfo input : manager.getTvInputList()) {
325             removedInputList.remove(input.getId());
326         }
327         // A USB tuner device can be temporarily unplugged. We do not remove the USB tuner input
328         // from the known inputs so that the input won't appear as a new input whenever the user
329         // plugs in the USB tuner device again.
330         if (mOptionalTunerInputId.isPresent()) {
331             removedInputList.remove(mOptionalTunerInputId.get());
332         }
333 
334         if (!removedInputList.isEmpty()) {
335             boolean inputPackageDeleted = false;
336             for (String input : removedInputList) {
337                 try {
338                     // Just after booting, input list from TvInputManager are not reliable.
339                     // So we need to double-check package existence. b/29034900
340                     mContext.getPackageManager()
341                             .getPackageInfo(
342                                     ComponentName.unflattenFromString(input).getPackageName(),
343                                     PackageManager.GET_ACTIVITIES);
344                     Log.i(TAG, "TV input (" + input + ") is removed but package is not deleted");
345                 } catch (NameNotFoundException e) {
346                     Log.i(TAG, "TV input (" + input + ") and its package are removed");
347                     mRecognizedInputs.remove(input);
348                     mSetUpInputs.remove(input);
349                     mKnownInputs.remove(input);
350                     inputPackageDeleted = true;
351                 }
352             }
353             if (inputPackageDeleted) {
354                 mSharedPreferences
355                         .edit()
356                         .putStringSet(PREF_KEY_SET_UP_INPUTS, mSetUpInputs)
357                         .putStringSet(PREF_KEY_KNOWN_INPUTS, mKnownInputs)
358                         .putStringSet(PREF_KEY_RECOGNIZED_INPUTS, mRecognizedInputs)
359                         .apply();
360             }
361         }
362     }
363 
364     /**
365      * Called when an setup is done. Once it is called, {@link #isSetupDone} returns {@code true}
366      * for {@code inputId}.
367      */
onSetupDone(String inputId)368     private void onSetupDone(String inputId) {
369         SoftPreconditions.checkState(inputId != null);
370         if (DEBUG) Log.d(TAG, "onSetupDone: input=" + inputId);
371         if (!mRecognizedInputs.contains(inputId)) {
372             Log.i(TAG, "An unrecognized input's setup has been done. inputId=" + inputId);
373             mRecognizedInputs.add(inputId);
374             mSharedPreferences
375                     .edit()
376                     .putStringSet(PREF_KEY_RECOGNIZED_INPUTS, mRecognizedInputs)
377                     .apply();
378         }
379         if (!mKnownInputs.contains(inputId)) {
380             Log.i(TAG, "An unknown input's setup has been done. inputId=" + inputId);
381             mKnownInputs.add(inputId);
382             mSharedPreferences.edit().putStringSet(PREF_KEY_KNOWN_INPUTS, mKnownInputs).apply();
383         }
384         if (!mSetUpInputs.contains(inputId)) {
385             mSetUpInputs.add(inputId);
386             mSharedPreferences.edit().putStringSet(PREF_KEY_SET_UP_INPUTS, mSetUpInputs).apply();
387         }
388     }
389 }
390