1 /*
2  * Copyright (C) 2017 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 package com.android.tv.common;
17 
18 import android.content.ContentResolver;
19 import android.content.ContentValues;
20 import android.content.Context;
21 import android.content.SharedPreferences;
22 import android.database.ContentObserver;
23 import android.database.Cursor;
24 import android.os.AsyncTask;
25 import android.os.Bundle;
26 import android.os.Handler;
27 import android.support.annotation.GuardedBy;
28 import android.support.annotation.IntDef;
29 import android.support.annotation.MainThread;
30 import android.util.Log;
31 import com.android.tv.common.CommonPreferenceProvider.Preferences;
32 import com.android.tv.common.util.CommonUtils;
33 import java.lang.annotation.Retention;
34 import java.lang.annotation.RetentionPolicy;
35 import java.util.HashMap;
36 import java.util.Map;
37 
38 /** A helper class for setting/getting common preferences across applications. */
39 public class CommonPreferences {
40     private static final String TAG = "CommonPreferences";
41 
42     private static final String PREFS_KEY_LAUNCH_SETUP = "launch_setup";
43     private static final String PREFS_KEY_STORE_TS_STREAM = "store_ts_stream";
44     private static final String PREFS_KEY_TRICKPLAY_SETTING = "trickplay_setting";
45     private static final String PREFS_KEY_LAST_POSTAL_CODE = "last_postal_code";
46 
47     private static final Map<String, Class> sPref2TypeMapping = new HashMap<>();
48 
49     static {
sPref2TypeMapping.put(PREFS_KEY_TRICKPLAY_SETTING, int.class)50         sPref2TypeMapping.put(PREFS_KEY_TRICKPLAY_SETTING, int.class);
sPref2TypeMapping.put(PREFS_KEY_STORE_TS_STREAM, boolean.class)51         sPref2TypeMapping.put(PREFS_KEY_STORE_TS_STREAM, boolean.class);
sPref2TypeMapping.put(PREFS_KEY_LAUNCH_SETUP, boolean.class)52         sPref2TypeMapping.put(PREFS_KEY_LAUNCH_SETUP, boolean.class);
sPref2TypeMapping.put(PREFS_KEY_LAST_POSTAL_CODE, String.class)53         sPref2TypeMapping.put(PREFS_KEY_LAST_POSTAL_CODE, String.class);
54     }
55 
56     private static final String SHARED_PREFS_NAME =
57             CommonConstants.BASE_PACKAGE + ".common.preferences";
58 
59     @IntDef({TRICKPLAY_SETTING_NOT_SET, TRICKPLAY_SETTING_DISABLED, TRICKPLAY_SETTING_ENABLED})
60     @Retention(RetentionPolicy.SOURCE)
61     public @interface TrickplaySetting {}
62 
63     /** Trickplay setting is not changed by a user. Trickplay will be enabled in this case. */
64     public static final int TRICKPLAY_SETTING_NOT_SET = -1;
65 
66     /** Trickplay setting is disabled. */
67     public static final int TRICKPLAY_SETTING_DISABLED = 0;
68 
69     /** Trickplay setting is enabled. */
70     public static final int TRICKPLAY_SETTING_ENABLED = 1;
71 
72     @GuardedBy("CommonPreferences.class")
73     private static final Bundle sPreferenceValues = new Bundle();
74 
75     private static LoadPreferencesTask sLoadPreferencesTask;
76     private static ContentObserver sContentObserver;
77     private static CommonPreferencesChangedListener sPreferencesChangedListener = null;
78 
79     protected static boolean sInitialized;
80 
81     /** Listeners for CommonPreferences change. */
82     public interface CommonPreferencesChangedListener {
onCommonPreferencesChanged()83         void onCommonPreferencesChanged();
84     }
85 
86     /** Initializes the common preferences. */
87     @MainThread
initialize(final Context context)88     public static void initialize(final Context context) {
89         if (sInitialized) {
90             return;
91         }
92         sInitialized = true;
93         if (useContentProvider(context)) {
94             loadPreferences(context);
95             sContentObserver =
96                     new ContentObserver(new Handler()) {
97                         @Override
98                         public void onChange(boolean selfChange) {
99                             loadPreferences(context);
100                         }
101                     };
102             context.getContentResolver()
103                     .registerContentObserver(Preferences.CONTENT_URI, true, sContentObserver);
104         } else {
105             new AsyncTask<Void, Void, Void>() {
106                 @Override
107                 protected Void doInBackground(Void... params) {
108                     getSharedPreferences(context);
109                     return null;
110                 }
111             }.execute();
112         }
113     }
114 
115     /** Releases the resources. */
release(Context context)116     public static synchronized void release(Context context) {
117         if (useContentProvider(context) && sContentObserver != null) {
118             context.getContentResolver().unregisterContentObserver(sContentObserver);
119         }
120         setCommonPreferencesChangedListener(null);
121     }
122 
123     /** Sets the listener for CommonPreferences change. */
setCommonPreferencesChangedListener( CommonPreferencesChangedListener listener)124     public static void setCommonPreferencesChangedListener(
125             CommonPreferencesChangedListener listener) {
126         sPreferencesChangedListener = listener;
127     }
128 
129     /**
130      * Loads the preferences from database.
131      *
132      * <p>This preferences is used across processes, so the preferences should be loaded again when
133      * the databases changes.
134      */
135     @MainThread
loadPreferences(Context context)136     public static void loadPreferences(Context context) {
137         if (sLoadPreferencesTask != null
138                 && sLoadPreferencesTask.getStatus() != AsyncTask.Status.FINISHED) {
139             sLoadPreferencesTask.cancel(true);
140         }
141         sLoadPreferencesTask = new LoadPreferencesTask(context);
142         sLoadPreferencesTask.execute();
143     }
144 
useContentProvider(Context context)145     private static boolean useContentProvider(Context context) {
146         // If TIS is a part of LC, it should use ContentProvider to resolve multiple process access.
147         return CommonUtils.isPackagedWithLiveChannels(context);
148     }
149 
shouldShowSetupActivity(Context context)150     public static synchronized boolean shouldShowSetupActivity(Context context) {
151         SoftPreconditions.checkState(sInitialized);
152         if (useContentProvider(context)) {
153             return sPreferenceValues.getBoolean(PREFS_KEY_LAUNCH_SETUP);
154         } else {
155             return getSharedPreferences(context).getBoolean(PREFS_KEY_LAUNCH_SETUP, false);
156         }
157     }
158 
setShouldShowSetupActivity(Context context, boolean need)159     public static synchronized void setShouldShowSetupActivity(Context context, boolean need) {
160         if (useContentProvider(context)) {
161             setPreference(context, PREFS_KEY_LAUNCH_SETUP, need);
162         } else {
163             getSharedPreferences(context).edit().putBoolean(PREFS_KEY_LAUNCH_SETUP, need).apply();
164         }
165     }
166 
167     @TrickplaySetting
getTrickplaySetting(Context context)168     public static synchronized int getTrickplaySetting(Context context) {
169         SoftPreconditions.checkState(sInitialized);
170         if (useContentProvider(context)) {
171             return sPreferenceValues.getInt(PREFS_KEY_TRICKPLAY_SETTING, TRICKPLAY_SETTING_NOT_SET);
172         } else {
173             return getSharedPreferences(context)
174                     .getInt(PREFS_KEY_TRICKPLAY_SETTING, TRICKPLAY_SETTING_NOT_SET);
175         }
176     }
177 
setTrickplaySetting( Context context, @TrickplaySetting int trickplaySetting)178     public static synchronized void setTrickplaySetting(
179             Context context, @TrickplaySetting int trickplaySetting) {
180         SoftPreconditions.checkState(sInitialized);
181         SoftPreconditions.checkArgument(trickplaySetting != TRICKPLAY_SETTING_NOT_SET);
182         if (useContentProvider(context)) {
183             setPreference(context, PREFS_KEY_TRICKPLAY_SETTING, trickplaySetting);
184         } else {
185             getSharedPreferences(context)
186                     .edit()
187                     .putInt(PREFS_KEY_TRICKPLAY_SETTING, trickplaySetting)
188                     .apply();
189         }
190     }
191 
getStoreTsStream(Context context)192     public static synchronized boolean getStoreTsStream(Context context) {
193         SoftPreconditions.checkState(sInitialized);
194         if (useContentProvider(context)) {
195             return sPreferenceValues.getBoolean(PREFS_KEY_STORE_TS_STREAM, false);
196         } else {
197             return getSharedPreferences(context).getBoolean(PREFS_KEY_STORE_TS_STREAM, false);
198         }
199     }
200 
setStoreTsStream(Context context, boolean shouldStore)201     public static synchronized void setStoreTsStream(Context context, boolean shouldStore) {
202         if (useContentProvider(context)) {
203             setPreference(context, PREFS_KEY_STORE_TS_STREAM, shouldStore);
204         } else {
205             getSharedPreferences(context)
206                     .edit()
207                     .putBoolean(PREFS_KEY_STORE_TS_STREAM, shouldStore)
208                     .apply();
209         }
210     }
211 
getLastPostalCode(Context context)212     public static synchronized String getLastPostalCode(Context context) {
213         SoftPreconditions.checkState(sInitialized);
214         if (useContentProvider(context)) {
215             return sPreferenceValues.getString(PREFS_KEY_LAST_POSTAL_CODE);
216         } else {
217             return getSharedPreferences(context).getString(PREFS_KEY_LAST_POSTAL_CODE, null);
218         }
219     }
220 
setLastPostalCode(Context context, String postalCode)221     public static synchronized void setLastPostalCode(Context context, String postalCode) {
222         if (useContentProvider(context)) {
223             setPreference(context, PREFS_KEY_LAST_POSTAL_CODE, postalCode);
224         } else {
225             getSharedPreferences(context)
226                     .edit()
227                     .putString(PREFS_KEY_LAST_POSTAL_CODE, postalCode)
228                     .apply();
229         }
230     }
231 
getSharedPreferences(Context context)232     protected static SharedPreferences getSharedPreferences(Context context) {
233         return context.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE);
234     }
235 
setPreference(Context context, String key, String value)236     private static synchronized void setPreference(Context context, String key, String value) {
237         sPreferenceValues.putString(key, value);
238         savePreference(context, key, value);
239     }
240 
setPreference(Context context, String key, int value)241     private static synchronized void setPreference(Context context, String key, int value) {
242         sPreferenceValues.putInt(key, value);
243         savePreference(context, key, Integer.toString(value));
244     }
245 
setPreference(Context context, String key, long value)246     private static synchronized void setPreference(Context context, String key, long value) {
247         sPreferenceValues.putLong(key, value);
248         savePreference(context, key, Long.toString(value));
249     }
250 
setPreference(Context context, String key, boolean value)251     private static synchronized void setPreference(Context context, String key, boolean value) {
252         sPreferenceValues.putBoolean(key, value);
253         savePreference(context, key, Boolean.toString(value));
254     }
255 
savePreference( final Context context, final String key, final String value)256     private static void savePreference(
257             final Context context, final String key, final String value) {
258         new AsyncTask<Void, Void, Void>() {
259             @Override
260             protected Void doInBackground(Void... params) {
261                 ContentResolver resolver = context.getContentResolver();
262                 ContentValues values = new ContentValues();
263                 values.put(Preferences.COLUMN_KEY, key);
264                 values.put(Preferences.COLUMN_VALUE, value);
265                 try {
266                     resolver.insert(Preferences.CONTENT_URI, values);
267                 } catch (Exception e) {
268                     SoftPreconditions.warn(
269                             TAG, "setPreference", e, "Error writing preference values");
270                 }
271                 return null;
272             }
273         }.execute();
274     }
275 
276     private static class LoadPreferencesTask extends AsyncTask<Void, Void, Bundle> {
277         private final Context mContext;
278 
LoadPreferencesTask(Context context)279         private LoadPreferencesTask(Context context) {
280             mContext = context;
281         }
282 
283         @Override
doInBackground(Void... params)284         protected Bundle doInBackground(Void... params) {
285             Bundle bundle = new Bundle();
286             ContentResolver resolver = mContext.getContentResolver();
287             String[] projection = new String[] {Preferences.COLUMN_KEY, Preferences.COLUMN_VALUE};
288             try (Cursor cursor =
289                     resolver.query(Preferences.CONTENT_URI, projection, null, null, null)) {
290                 if (cursor != null) {
291                     while (!isCancelled() && cursor.moveToNext()) {
292                         String key = cursor.getString(0);
293                         String value = cursor.getString(1);
294                         Class prefClass = sPref2TypeMapping.get(key);
295                         if (prefClass == int.class) {
296                             try {
297                                 bundle.putInt(key, Integer.parseInt(value));
298                             } catch (NumberFormatException e) {
299                                 Log.w(TAG, "Invalid format, key=" + key + ", value=" + value);
300                             }
301                         } else if (prefClass == long.class) {
302                             try {
303                                 bundle.putLong(key, Long.parseLong(value));
304                             } catch (NumberFormatException e) {
305                                 Log.w(TAG, "Invalid format, key=" + key + ", value=" + value);
306                             }
307                         } else if (prefClass == boolean.class) {
308                             bundle.putBoolean(key, Boolean.parseBoolean(value));
309                         } else {
310                             bundle.putString(key, value);
311                         }
312                     }
313                 }
314             } catch (Exception e) {
315                 SoftPreconditions.warn(TAG, "getPreference", e, "Error querying preference values");
316                 return null;
317             }
318             return bundle;
319         }
320 
321         @Override
onPostExecute(Bundle bundle)322         protected void onPostExecute(Bundle bundle) {
323             synchronized (CommonPreferences.class) {
324                 if (bundle != null) {
325                     sPreferenceValues.putAll(bundle);
326                 }
327             }
328             if (sPreferencesChangedListener != null) {
329                 sPreferencesChangedListener.onCommonPreferencesChanged();
330             }
331         }
332     }
333 }
334