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 17 package com.android.vending.sectool.v1; 18 19 import android.content.ContentResolver; 20 import android.database.ContentObserver; 21 import android.database.Cursor; 22 import android.net.Uri; 23 import android.os.Handler; 24 import android.os.Looper; 25 import android.util.Log; 26 27 import java.util.HashMap; 28 import java.util.Map; 29 import java.util.TreeMap; 30 import 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 */ 36 public 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 ensureCacheInitializedLocked(final ContentResolver cr)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 */ getString(ContentResolver cr, String key, String defValue)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 */ getString(ContentResolver cr, String key)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 */ getInt(ContentResolver cr, String key, int defValue)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 */ getLong(ContentResolver cr, String key, long defValue)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 getBoolean(ContentResolver cr, String key, boolean defValue)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 */ getStringsByPrefix(ContentResolver cr, String... prefixes)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 */ getVersionToken(ContentResolver cr)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