1/*
2 * Copyright (C) 2009 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
17package com.android.vending.sectool.v1;
18
19import android.content.ContentResolver;
20import android.database.ContentObserver;
21import android.database.Cursor;
22import android.net.Uri;
23import android.os.Handler;
24import android.os.Looper;
25import android.util.Log;
26
27import java.util.HashMap;
28import java.util.Map;
29import java.util.TreeMap;
30import java.util.regex.Pattern;
31
32/**
33 * Gservices provides access to a key-value store that is can be
34 * updated remote (by the google checkin service).
35 */
36public class Gservices {
37    public static final String TAG = "Gservices";
38
39    public static final String OVERRIDE_ACTION =
40        "com.google.gservices.intent.action.GSERVICES_OVERRIDE";
41    public static final String CHANGED_ACTION =
42        "com.google.gservices.intent.action.GSERVICES_CHANGED";
43
44    public final static Uri CONTENT_URI =
45        Uri.parse("content://com.google.android.gsf.gservices");
46    public final static Uri CONTENT_PREFIX_URI =
47        Uri.parse("content://com.google.android.gsf.gservices/prefix");
48
49    public static final Pattern TRUE_PATTERN =
50        Pattern.compile("^(1|true|t|on|yes|y)$", Pattern.CASE_INSENSITIVE);
51    public static final Pattern FALSE_PATTERN =
52        Pattern.compile("^(0|false|f|off|no|n)$", Pattern.CASE_INSENSITIVE);
53
54    private static ContentResolver sResolver;
55    private static HashMap<String, String> sCache;
56    private static Object sVersionToken;
57
58    private static void ensureCacheInitializedLocked(final ContentResolver cr) {
59        if (sCache == null) {
60            sCache = new HashMap<String, String>();
61            sVersionToken = new Object();
62            sResolver = cr;
63
64            // Create a thread to host a Handler for ContentObserver callbacks.
65            // The callback will clear the cache to force the resolver to be consulted
66            // on future gets. The version is also updated.
67            new Thread() {
68                public void run() {
69                    Looper.prepare();
70                    cr.registerContentObserver(CONTENT_URI, true,
71                        new ContentObserver(new Handler(Looper.myLooper())) {
72                            public void onChange(boolean selfChange) {
73                                synchronized (Gservices.class) {
74                                    sCache.clear();
75                                    sVersionToken = new Object();
76                                }
77                            } });
78                    Looper.loop();
79                }
80            }.start();
81        }
82    }
83
84    /**
85     * Look up a key in the database.
86     * @param cr to access the database with
87     * @param key to look up in the table
88     * @param defValue the value to return if the value from the database is null
89     * @return the corresponding value, or defValue if not present
90     */
91    public static String getString(ContentResolver cr, String key, String defValue) {
92        final Object version;
93        synchronized (Gservices.class) {
94            ensureCacheInitializedLocked(cr);
95            version = sVersionToken;
96            if (sCache.containsKey(key)) {
97                String value = sCache.get(key);
98                return (value != null) ? value : defValue;
99            }
100        }
101        Cursor cursor = sResolver.query(CONTENT_URI, null, null, new String[]{ key }, null);
102        if (cursor == null) return defValue;
103
104        try {
105            cursor.moveToFirst();
106            String value = cursor.getString(1);
107            synchronized (Gservices.class) {
108                // There is a chance that the version change, and thus the cache clearing,
109                // happened after the query, meaning the value we got could be stale. Don't
110                // store it in the cache in this case.
111                if (version == sVersionToken) {
112                    sCache.put(key, value);
113                }
114            }
115            return (value != null) ? value : defValue;
116        } finally {
117            cursor.close();
118        }
119    }
120
121    /**
122     * Look up a key in the database.
123     * @param cr to access the database with
124     * @param key to look up in the table
125     * @return the corresponding value, or null if not present
126     */
127    public static String getString(ContentResolver cr, String key) {
128        return getString(cr, key, null);
129    }
130
131    /**
132     * Look up the value for key in the database, convert it to an int
133     * using Integer.parseInt and return it. If it is null or if a
134     * NumberFormatException is caught during the conversion then
135     * return defValue.
136     */
137    public static int getInt(ContentResolver cr, String key, int defValue) {
138        String valString = getString(cr, key);
139        int value;
140        try {
141            value = valString != null ? Integer.parseInt(valString) : defValue;
142        } catch (NumberFormatException e) {
143            value = defValue;
144        }
145        return value;
146    }
147
148    /**
149     * Look up the value for key in the database, convert it to a long
150     * using Long.parseLong and return it. If it is null or if a
151     * NumberFormatException is caught during the conversion then
152     * return defValue.
153     */
154    public static long getLong(ContentResolver cr, String key, long defValue) {
155        String valString = getString(cr, key);
156        long value;
157        try {
158            value = valString != null ? Long.parseLong(valString) : defValue;
159        } catch (NumberFormatException e) {
160            value = defValue;
161        }
162        return value;
163    }
164
165    public static boolean getBoolean(ContentResolver cr, String key, boolean defValue) {
166        String valString = getString(cr, key);
167        if (valString == null || valString.equals("")) {
168            return defValue;
169        } else if (TRUE_PATTERN.matcher(valString).matches()) {
170            return true;
171        } else if (FALSE_PATTERN.matcher(valString).matches()) {
172            return false;
173        } else {
174            // Log a possible app bug
175            Log.w(TAG, "attempt to read gservices key " + key + " (value \"" +
176                  valString + "\") as boolean");
177            return defValue;
178        }
179    }
180
181    /**
182     * Look up values for all keys beginning with any of the given prefixes.
183     *
184     * @return a Map<String, String> of the matching key-value pairs.
185     */
186    public static Map<String, String> getStringsByPrefix(ContentResolver cr,
187                                                         String... prefixes) {
188        Cursor c = cr.query(CONTENT_PREFIX_URI, null, null, prefixes, null);
189        TreeMap<String, String> out = new TreeMap<String, String>();
190        if (c == null) return out;
191
192        try {
193            while (c.moveToNext()) {
194                out.put(c.getString(0), c.getString(1));
195            }
196        } finally {
197            c.close();
198        }
199        return out;
200    }
201
202    /**
203     * Returns a token that represents the current version of the data within gservices
204     * @param cr the ContentResolver that Gservices should use to fill its cache
205     * @return an Object that represents the current version of the Gservices values.
206     */
207    public static Object getVersionToken(ContentResolver cr) {
208        synchronized (Gservices.class) {
209            // Even though we don't need the cache itself, we need the cache version, so we make
210            // that the cache has been initialized before we return its version.
211            ensureCacheInitializedLocked(cr);
212            return sVersionToken;
213        }
214    }
215}
216