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.media.tv.TvContract;
24 import android.media.tv.TvInputInfo;
25 import android.media.tv.TvInputManager;
26 import android.os.Build;
27 import android.preference.PreferenceManager;
28 import android.support.annotation.Nullable;
29 import android.support.annotation.UiThread;
30 import android.text.TextUtils;
31 import android.util.ArraySet;
32 import android.util.Log;
33 
34 import com.android.tv.ApplicationSingletons;
35 import com.android.tv.TvApplication;
36 import com.android.tv.common.SoftPreconditions;
37 import com.android.tv.data.Channel;
38 import com.android.tv.data.ChannelDataManager;
39 
40 import java.util.Collections;
41 import java.util.HashSet;
42 import java.util.Set;
43 
44 /**
45  * A utility class related to input setup.
46  */
47 public class SetupUtils {
48     private static final String TAG = "SetupUtils";
49     private static final boolean DEBUG = false;
50 
51     // Known inputs are inputs which are shown in SetupView before. When a new input is installed,
52     // the input will not be included in "PREF_KEY_KNOWN_INPUTS".
53     private static final String PREF_KEY_KNOWN_INPUTS = "known_inputs";
54     // Set up inputs are inputs whose setup activity has been launched and finished successfully.
55     private static final String PREF_KEY_SET_UP_INPUTS = "set_up_inputs";
56     // Recognized inputs means that the user already knows the inputs are installed.
57     private static final String PREF_KEY_RECOGNIZED_INPUTS = "recognized_inputs";
58     private static final String PREF_KEY_IS_FIRST_TUNE = "is_first_tune";
59     private static SetupUtils sSetupUtils;
60 
61     private final TvApplication mTvApplication;
62     private final SharedPreferences mSharedPreferences;
63     private final Set<String> mKnownInputs;
64     private final Set<String> mSetUpInputs;
65     private final Set<String> mRecognizedInputs;
66     private boolean mIsFirstTune;
67     private final String mUsbTunerInputId;
68 
SetupUtils(TvApplication tvApplication)69     private SetupUtils(TvApplication tvApplication) {
70         mTvApplication = tvApplication;
71         mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(tvApplication);
72         mSetUpInputs = new ArraySet<>();
73         mSetUpInputs.addAll(mSharedPreferences.getStringSet(PREF_KEY_SET_UP_INPUTS,
74                 Collections.<String>emptySet()));
75         mKnownInputs = new ArraySet<>();
76         mKnownInputs.addAll(mSharedPreferences.getStringSet(PREF_KEY_KNOWN_INPUTS,
77                 Collections.<String>emptySet()));
78         mRecognizedInputs = new ArraySet<>();
79         mRecognizedInputs.addAll(mSharedPreferences.getStringSet(PREF_KEY_RECOGNIZED_INPUTS,
80                 mKnownInputs));
81         mIsFirstTune = mSharedPreferences.getBoolean(PREF_KEY_IS_FIRST_TUNE, true);
82         mUsbTunerInputId = TvContract.buildInputId(new ComponentName(tvApplication,
83                 com.android.usbtuner.tvinput.UsbTunerTvInputService.class));
84     }
85 
86     /**
87      * Gets an instance of {@link SetupUtils}.
88      */
getInstance(Context context)89     public static SetupUtils getInstance(Context context) {
90         if (sSetupUtils != null) {
91             return sSetupUtils;
92         }
93         sSetupUtils = new SetupUtils((TvApplication) context.getApplicationContext());
94         return sSetupUtils;
95     }
96 
97     /**
98      * Additional work after the setup of TV input.
99      */
onTvInputSetupFinished(final String inputId, @Nullable final Runnable postRunnable)100     public void onTvInputSetupFinished(final String inputId,
101             @Nullable final Runnable postRunnable) {
102         // When TIS adds several channels, ChannelDataManager.Listener.onChannelList
103         // Updated() can be called several times. In this case, it is hard to detect
104         // which one is the last callback. To reduce error prune, we update channel
105         // list again and make all channels of {@code inputId} browsable.
106         onSetupDone(inputId);
107         final ChannelDataManager manager = mTvApplication.getChannelDataManager();
108         if (!manager.isDbLoadFinished()) {
109             manager.addListener(new ChannelDataManager.Listener() {
110                 @Override
111                 public void onLoadFinished() {
112                     manager.removeListener(this);
113                     updateChannelBrowsable(mTvApplication, inputId, postRunnable);
114                 }
115 
116                 @Override
117                 public void onChannelListUpdated() { }
118 
119                 @Override
120                 public void onChannelBrowsableChanged() { }
121             });
122         } else {
123             updateChannelBrowsable(mTvApplication, inputId, postRunnable);
124         }
125     }
126 
updateChannelBrowsable(Context context, final String inputId, final Runnable postRunnable)127     private static void updateChannelBrowsable(Context context, final String inputId,
128             final Runnable postRunnable) {
129         ApplicationSingletons appSingletons = TvApplication.getSingletons(context);
130         final ChannelDataManager manager = appSingletons.getChannelDataManager();
131         manager.updateChannels(new Runnable() {
132             @Override
133             public void run() {
134                 boolean browsableChanged = false;
135                 for (Channel channel : manager.getChannelList()) {
136                     if (channel.getInputId().equals(inputId)) {
137                         if (!channel.isBrowsable()) {
138                             manager.updateBrowsable(channel.getId(), true, true);
139                             browsableChanged = true;
140                         }
141                     }
142                 }
143                 if (browsableChanged) {
144                     manager.notifyChannelBrowsableChanged();
145                     manager.applyUpdatedValuesToDb();
146                 }
147                 if (postRunnable != null) {
148                     postRunnable.run();
149                 }
150             }
151         });
152     }
153 
154     /**
155      * Marks the channels in newly installed inputs browsable.
156      */
157     @UiThread
markNewChannelsBrowsable()158     public void markNewChannelsBrowsable() {
159         Set<String> newInputsWithChannels = new HashSet<>();
160         TvInputManagerHelper tvInputManagerHelper = mTvApplication.getTvInputManagerHelper();
161         ChannelDataManager channelDataManager = mTvApplication.getChannelDataManager();
162         SoftPreconditions.checkState(channelDataManager.isDbLoadFinished());
163         for (TvInputInfo input : tvInputManagerHelper.getTvInputInfos(true, true)) {
164             String inputId = input.getId();
165             if (!isSetupDone(inputId) && channelDataManager.getChannelCountForInput(inputId) > 0) {
166                 onSetupDone(inputId);
167                 newInputsWithChannels.add(inputId);
168                 if (DEBUG) {
169                     Log.d(TAG, "New input " + inputId + " has "
170                             + channelDataManager.getChannelCountForInput(inputId)
171                             + " channels");
172                 }
173             }
174         }
175         if (!newInputsWithChannels.isEmpty()) {
176             for (Channel channel : channelDataManager.getChannelList()) {
177                 if (newInputsWithChannels.contains(channel.getInputId())) {
178                     channelDataManager.updateBrowsable(channel.getId(), true);
179                 }
180             }
181             channelDataManager.applyUpdatedValuesToDb();
182         }
183     }
184 
isFirstTune()185     public boolean isFirstTune() {
186         return mIsFirstTune;
187     }
188 
189     /**
190      * Returns true, if the input with {@code inputId} is newly installed.
191      */
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.edit().putStringSet(PREF_KEY_KNOWN_INPUTS, mKnownInputs)
204                 .putStringSet(PREF_KEY_RECOGNIZED_INPUTS, mRecognizedInputs).apply();
205     }
206 
207     /**
208      * Returns {@code true}, if {@code inputId}'s setup has been done before.
209      */
isSetupDone(String inputId)210     public boolean isSetupDone(String inputId) {
211         boolean done = mSetUpInputs.contains(inputId);
212         if (DEBUG) {
213             Log.d(TAG, "isSetupDone: (input=" + inputId + ", result= " + done + ")");
214         }
215         return done;
216     }
217 
218     /**
219      * Returns true, if there is any newly installed input.
220      */
hasNewInput(TvInputManagerHelper inputManager)221     public boolean hasNewInput(TvInputManagerHelper inputManager) {
222         for (TvInputInfo input : inputManager.getTvInputInfos(true, true)) {
223             if (isNewInput(input.getId())) {
224                 return true;
225             }
226         }
227         return false;
228     }
229 
230     /**
231      * Checks whether the given input is already recognized by the user or not.
232      */
isRecognizedInput(String inputId)233     private boolean isRecognizedInput(String inputId) {
234         return mRecognizedInputs.contains(inputId);
235     }
236 
237     /**
238      * Marks all the inputs as recognized inputs. Once it is marked, {@link #isRecognizedInput} will
239      * return {@code true}.
240      */
markAllInputsRecognized(TvInputManagerHelper inputManager)241     public void markAllInputsRecognized(TvInputManagerHelper inputManager) {
242         for (TvInputInfo input : inputManager.getTvInputInfos(true, true)) {
243             mRecognizedInputs.add(input.getId());
244         }
245         mSharedPreferences.edit().putStringSet(PREF_KEY_RECOGNIZED_INPUTS, mRecognizedInputs)
246                 .apply();
247     }
248 
249     /**
250      * Checks whether there are any unrecognized inputs.
251      */
hasUnrecognizedInput(TvInputManagerHelper inputManager)252     public boolean hasUnrecognizedInput(TvInputManagerHelper inputManager) {
253         for (TvInputInfo input : inputManager.getTvInputInfos(true, true)) {
254             if (!isRecognizedInput(input.getId())) {
255                 return true;
256             }
257         }
258         return false;
259     }
260 
261     /**
262      * Grants permission for writing EPG data to all verified packages.
263      *
264      * @param context The Context used for granting permission.
265      */
grantEpgPermissionToSetUpPackages(Context context)266     public static void grantEpgPermissionToSetUpPackages(Context context) {
267         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
268             // Can't grant permission.
269             return;
270         }
271 
272         // Find all already-verified packages.
273         Set<String> setUpPackages = new HashSet<>();
274         SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
275         for (String input : sp.getStringSet(PREF_KEY_SET_UP_INPUTS, Collections.EMPTY_SET)) {
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         // TvProvider allows granting of Uri permissions starting from MNC.
297         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
298             if (DEBUG) {
299                 Log.d(TAG, "grantEpgPermission(context=" + context + ", packageName=" + packageName
300                         + ")");
301             }
302             try {
303                 int modeFlags = Intent.FLAG_GRANT_WRITE_URI_PERMISSION
304                         | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION;
305                 context.grantUriPermission(packageName, TvContract.Channels.CONTENT_URI, modeFlags);
306                 context.grantUriPermission(packageName, TvContract.Programs.CONTENT_URI, modeFlags);
307             } catch (SecurityException e) {
308                 Log.e(TAG, "Either TvProvider does not allow granting of Uri permissions or the app"
309                         + " does not have permission.", e);
310             }
311         }
312     }
313 
314     /**
315      * Called when Live channels app is launched. Once it is called, {@link
316      * #isFirstTune} will return false.
317      */
onTuned()318     public void onTuned() {
319         if (!mIsFirstTune) {
320             return;
321         }
322         mIsFirstTune = false;
323         mSharedPreferences.edit().putBoolean(PREF_KEY_IS_FIRST_TUNE, false).apply();
324     }
325 
326     /**
327      * Called when input list is changed. It mainly handles input removals.
328      */
onInputListUpdated(TvInputManager manager)329     public void onInputListUpdated(TvInputManager manager) {
330         // mRecognizedInputs > mKnownInputs > mSetUpInputs.
331         Set<String> removedInputList = new HashSet<>(mRecognizedInputs);
332         for (TvInputInfo input : manager.getTvInputList()) {
333             removedInputList.remove(input.getId());
334         }
335         // A USB tuner device can be temporarily unplugged. We do not remove the USB tuner input
336         // from the known inputs so that the input won't appear as a new input whenever the user
337         // plugs in the USB tuner device again.
338         removedInputList.remove(mUsbTunerInputId);
339 
340         if (!removedInputList.isEmpty()) {
341             for (String input : removedInputList) {
342                 mRecognizedInputs.remove(input);
343                 mSetUpInputs.remove(input);
344                 mKnownInputs.remove(input);
345             }
346             mSharedPreferences.edit().putStringSet(PREF_KEY_SET_UP_INPUTS, mSetUpInputs)
347                     .putStringSet(PREF_KEY_KNOWN_INPUTS, mKnownInputs)
348                     .putStringSet(PREF_KEY_RECOGNIZED_INPUTS, mRecognizedInputs).apply();
349         }
350     }
351 
352     /**
353      * Called when an setup is done. Once it is called, {@link #isSetupDone} returns {@code true}
354      * for {@code inputId}.
355      */
onSetupDone(String inputId)356     public void onSetupDone(String inputId) {
357         SoftPreconditions.checkState(inputId != null);
358         if (DEBUG) Log.d(TAG, "onSetupDone: input=" + inputId);
359         if (!mRecognizedInputs.contains(inputId)) {
360             Log.i(TAG, "An unrecognized input's setup has been done. inputId=" + inputId);
361             mRecognizedInputs.add(inputId);
362             mSharedPreferences.edit().putStringSet(PREF_KEY_RECOGNIZED_INPUTS, mRecognizedInputs)
363                     .apply();
364         }
365         if (!mKnownInputs.contains(inputId)) {
366             Log.i(TAG, "An unknown input's setup has been done. inputId=" + inputId);
367             mKnownInputs.add(inputId);
368             mSharedPreferences.edit().putStringSet(PREF_KEY_KNOWN_INPUTS, mKnownInputs).apply();
369         }
370         if (!mSetUpInputs.contains(inputId)) {
371             mSetUpInputs.add(inputId);
372             mSharedPreferences.edit().putStringSet(PREF_KEY_SET_UP_INPUTS, mSetUpInputs).apply();
373         }
374     }
375 }
376