M7350v1_en_gpl

This commit is contained in:
T
2024-09-09 08:52:07 +00:00
commit f9cc65cfda
65988 changed files with 26357421 additions and 0 deletions

View File

@ -0,0 +1,455 @@
/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.providers.settings;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Arrays;
import java.util.zip.CRC32;
import android.app.backup.BackupDataInput;
import android.app.backup.BackupDataOutput;
import android.app.backup.BackupAgentHelper;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.net.wifi.WifiManager;
import android.os.FileUtils;
import android.os.ParcelFileDescriptor;
import android.os.Process;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.Log;
/**
* Performs backup and restore of the System and Secure settings.
* List of settings that are backed up are stored in the Settings.java file
*/
public class SettingsBackupAgent extends BackupAgentHelper {
private static final boolean DEBUG = false;
private static final String KEY_SYSTEM = "system";
private static final String KEY_SECURE = "secure";
private static final String KEY_LOCALE = "locale";
// Versioning of the state file. Increment this version
// number any time the set of state items is altered.
private static final int STATE_VERSION = 1;
private static final int STATE_SYSTEM = 0;
private static final int STATE_SECURE = 1;
private static final int STATE_LOCALE = 2;
private static final int STATE_WIFI = 3;
private static final int STATE_SIZE = 4; // The number of state items
private static String[] sortedSystemKeys = null;
private static String[] sortedSecureKeys = null;
private static final byte[] EMPTY_DATA = new byte[0];
private static final String TAG = "SettingsBackupAgent";
private static final int COLUMN_NAME = 1;
private static final int COLUMN_VALUE = 2;
private static final String[] PROJECTION = {
Settings.NameValueTable._ID,
Settings.NameValueTable.NAME,
Settings.NameValueTable.VALUE
};
private static final String FILE_WIFI_SUPPLICANT = "/data/misc/wifi/wpa_supplicant.conf";
private static final String FILE_WIFI_SUPPLICANT_TEMPLATE =
"/system/etc/wifi/wpa_supplicant.conf";
// the key to store the WIFI data under, should be sorted as last, so restore happens last.
// use very late unicode character to quasi-guarantee last sort position.
private static final String KEY_WIFI_SUPPLICANT = "\uffedWIFI";
private SettingsHelper mSettingsHelper;
public void onCreate() {
mSettingsHelper = new SettingsHelper(this);
super.onCreate();
}
@Override
public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
ParcelFileDescriptor newState) throws IOException {
byte[] systemSettingsData = getSystemSettings();
byte[] secureSettingsData = getSecureSettings();
byte[] locale = mSettingsHelper.getLocaleData();
byte[] wifiData = getWifiSupplicant(FILE_WIFI_SUPPLICANT);
long[] stateChecksums = readOldChecksums(oldState);
stateChecksums[STATE_SYSTEM] =
writeIfChanged(stateChecksums[STATE_SYSTEM], KEY_SYSTEM, systemSettingsData, data);
stateChecksums[STATE_SECURE] =
writeIfChanged(stateChecksums[STATE_SECURE], KEY_SECURE, secureSettingsData, data);
stateChecksums[STATE_LOCALE] =
writeIfChanged(stateChecksums[STATE_LOCALE], KEY_LOCALE, locale, data);
stateChecksums[STATE_WIFI] =
writeIfChanged(stateChecksums[STATE_WIFI], KEY_WIFI_SUPPLICANT, wifiData, data);
writeNewChecksums(stateChecksums, newState);
}
@Override
public void onRestore(BackupDataInput data, int appVersionCode,
ParcelFileDescriptor newState) throws IOException {
while (data.readNextHeader()) {
final String key = data.getKey();
final int size = data.getDataSize();
if (KEY_SYSTEM.equals(key)) {
restoreSettings(data, Settings.System.CONTENT_URI);
mSettingsHelper.applyAudioSettings();
} else if (KEY_SECURE.equals(key)) {
restoreSettings(data, Settings.Secure.CONTENT_URI);
} else if (KEY_WIFI_SUPPLICANT.equals(key)) {
int retainedWifiState = enableWifi(false);
restoreWifiSupplicant(FILE_WIFI_SUPPLICANT, data);
FileUtils.setPermissions(FILE_WIFI_SUPPLICANT,
FileUtils.S_IRUSR | FileUtils.S_IWUSR |
FileUtils.S_IRGRP | FileUtils.S_IWGRP,
Process.myUid(), Process.WIFI_UID);
// retain the previous WIFI state.
enableWifi(retainedWifiState == WifiManager.WIFI_STATE_ENABLED ||
retainedWifiState == WifiManager.WIFI_STATE_ENABLING);
} else if (KEY_LOCALE.equals(key)) {
byte[] localeData = new byte[size];
data.readEntityData(localeData, 0, size);
mSettingsHelper.setLocaleData(localeData);
} else {
data.skipEntityData();
}
}
}
private long[] readOldChecksums(ParcelFileDescriptor oldState) throws IOException {
long[] stateChecksums = new long[STATE_SIZE];
DataInputStream dataInput = new DataInputStream(
new FileInputStream(oldState.getFileDescriptor()));
try {
int stateVersion = dataInput.readInt();
if (stateVersion == STATE_VERSION) {
for (int i = 0; i < STATE_SIZE; i++) {
stateChecksums[i] = dataInput.readLong();
}
}
} catch (EOFException eof) {
// With the default 0 checksum we'll wind up forcing a backup of
// any unhandled data sets, which is appropriate.
}
dataInput.close();
return stateChecksums;
}
private void writeNewChecksums(long[] checksums, ParcelFileDescriptor newState)
throws IOException {
DataOutputStream dataOutput = new DataOutputStream(
new FileOutputStream(newState.getFileDescriptor()));
dataOutput.writeInt(STATE_VERSION);
for (int i = 0; i < STATE_SIZE; i++) {
dataOutput.writeLong(checksums[i]);
}
dataOutput.close();
}
private long writeIfChanged(long oldChecksum, String key, byte[] data,
BackupDataOutput output) {
CRC32 checkSummer = new CRC32();
checkSummer.update(data);
long newChecksum = checkSummer.getValue();
if (oldChecksum == newChecksum) {
return oldChecksum;
}
try {
output.writeEntityHeader(key, data.length);
output.writeEntityData(data, data.length);
} catch (IOException ioe) {
// Bail
}
return newChecksum;
}
private byte[] getSystemSettings() {
Cursor sortedCursor = getContentResolver().query(Settings.System.CONTENT_URI, PROJECTION,
null, null, Settings.NameValueTable.NAME);
// Copy and sort the array
if (sortedSystemKeys == null) {
sortedSystemKeys = copyAndSort(Settings.System.SETTINGS_TO_BACKUP);
}
byte[] result = extractRelevantValues(sortedCursor, sortedSystemKeys);
sortedCursor.close();
return result;
}
private byte[] getSecureSettings() {
Cursor sortedCursor = getContentResolver().query(Settings.Secure.CONTENT_URI, PROJECTION,
null, null, Settings.NameValueTable.NAME);
// Copy and sort the array
if (sortedSecureKeys == null) {
sortedSecureKeys = copyAndSort(Settings.Secure.SETTINGS_TO_BACKUP);
}
byte[] result = extractRelevantValues(sortedCursor, sortedSecureKeys);
sortedCursor.close();
return result;
}
private void restoreSettings(BackupDataInput data, Uri contentUri) {
if (DEBUG) Log.i(TAG, "restoreSettings: " + contentUri);
String[] whitelist = null;
if (contentUri.equals(Settings.Secure.CONTENT_URI)) {
whitelist = Settings.Secure.SETTINGS_TO_BACKUP;
} else if (contentUri.equals(Settings.System.CONTENT_URI)) {
whitelist = Settings.System.SETTINGS_TO_BACKUP;
}
ContentValues cv = new ContentValues(2);
byte[] settings = new byte[data.getDataSize()];
try {
data.readEntityData(settings, 0, settings.length);
} catch (IOException ioe) {
Log.e(TAG, "Couldn't read entity data");
return;
}
int pos = 0;
while (pos < settings.length) {
int length = readInt(settings, pos);
pos += 4;
String settingName = length > 0? new String(settings, pos, length) : null;
pos += length;
length = readInt(settings, pos);
pos += 4;
String settingValue = length > 0? new String(settings, pos, length) : null;
pos += length;
if (!TextUtils.isEmpty(settingName) && !TextUtils.isEmpty(settingValue)) {
//Log.i(TAG, "Restore " + settingName + " = " + settingValue);
// Only restore settings in our list of known-acceptable data
if (invalidSavedSetting(whitelist, settingName)) {
continue;
}
if (mSettingsHelper.restoreValue(settingName, settingValue)) {
cv.clear();
cv.put(Settings.NameValueTable.NAME, settingName);
cv.put(Settings.NameValueTable.VALUE, settingValue);
getContentResolver().insert(contentUri, cv);
}
}
}
}
// Returns 'true' if the given setting is one that we refuse to restore
private boolean invalidSavedSetting(String[] knownNames, String candidate) {
// no filter? allow everything
if (knownNames == null) {
return false;
}
// whitelisted setting? allow it
for (String name : knownNames) {
if (name.equals(candidate)) {
return false;
}
}
// refuse everything else
if (DEBUG) Log.v(TAG, "Ignoring restore datum: " + candidate);
return true;
}
private String[] copyAndSort(String[] keys) {
String[] sortedKeys = new String[keys.length];
System.arraycopy(keys, 0, sortedKeys, 0, keys.length);
Arrays.sort(sortedKeys);
return sortedKeys;
}
/**
* Given a cursor sorted by key name and a set of keys sorted by name,
* extract the required keys and values and write them to a byte array.
* @param sortedCursor
* @param sortedKeys
* @return
*/
byte[] extractRelevantValues(Cursor sortedCursor, String[] sortedKeys) {
byte[][] values = new byte[sortedKeys.length * 2][]; // keys and values
if (!sortedCursor.moveToFirst()) {
Log.e(TAG, "Couldn't read from the cursor");
return new byte[0];
}
int keyIndex = 0;
int totalSize = 0;
while (!sortedCursor.isAfterLast()) {
String name = sortedCursor.getString(COLUMN_NAME);
while (sortedKeys[keyIndex].compareTo(name.toString()) < 0) {
keyIndex++;
if (keyIndex == sortedKeys.length) break;
}
if (keyIndex < sortedKeys.length && name.equals(sortedKeys[keyIndex])) {
String value = sortedCursor.getString(COLUMN_VALUE);
byte[] nameBytes = name.toString().getBytes();
totalSize += 4 + nameBytes.length;
values[keyIndex * 2] = nameBytes;
byte[] valueBytes;
if (TextUtils.isEmpty(value)) {
valueBytes = null;
totalSize += 4;
} else {
valueBytes = value.toString().getBytes();
totalSize += 4 + valueBytes.length;
//Log.i(TAG, "Backing up " + name + " = " + value);
}
values[keyIndex * 2 + 1] = valueBytes;
keyIndex++;
}
if (keyIndex == sortedKeys.length || !sortedCursor.moveToNext()) {
break;
}
}
byte[] result = new byte[totalSize];
int pos = 0;
for (int i = 0; i < sortedKeys.length * 2; i++) {
if (values[i] != null) {
pos = writeInt(result, pos, values[i].length);
pos = writeBytes(result, pos, values[i]);
}
}
return result;
}
private byte[] getWifiSupplicant(String filename) {
try {
File file = new File(filename);
if (file.exists()) {
BufferedReader br = new BufferedReader(new FileReader(file));
StringBuffer relevantLines = new StringBuffer();
boolean started = false;
String line;
while ((line = br.readLine()) != null) {
if (!started && line.startsWith("network")) {
started = true;
}
if (started) {
relevantLines.append(line).append("\n");
}
}
if (relevantLines.length() > 0) {
return relevantLines.toString().getBytes();
} else {
return EMPTY_DATA;
}
} else {
return EMPTY_DATA;
}
} catch (IOException ioe) {
Log.w(TAG, "Couldn't backup " + filename);
return EMPTY_DATA;
}
}
private void restoreWifiSupplicant(String filename, BackupDataInput data) {
byte[] bytes = new byte[data.getDataSize()];
if (bytes.length <= 0) return;
try {
data.readEntityData(bytes, 0, bytes.length);
File supplicantFile = new File(FILE_WIFI_SUPPLICANT);
if (supplicantFile.exists()) supplicantFile.delete();
copyWifiSupplicantTemplate();
FileOutputStream fos = new FileOutputStream(filename, true);
fos.write("\n".getBytes());
fos.write(bytes);
} catch (IOException ioe) {
Log.w(TAG, "Couldn't restore " + filename);
}
}
private void copyWifiSupplicantTemplate() {
try {
BufferedReader br = new BufferedReader(new FileReader(FILE_WIFI_SUPPLICANT_TEMPLATE));
BufferedWriter bw = new BufferedWriter(new FileWriter(FILE_WIFI_SUPPLICANT));
char[] temp = new char[1024];
int size;
while ((size = br.read(temp)) > 0) {
bw.write(temp, 0, size);
}
bw.close();
br.close();
} catch (IOException ioe) {
Log.w(TAG, "Couldn't copy wpa_supplicant file");
}
}
/**
* Write an int in BigEndian into the byte array.
* @param out byte array
* @param pos current pos in array
* @param value integer to write
* @return the index after adding the size of an int (4)
*/
private int writeInt(byte[] out, int pos, int value) {
out[pos + 0] = (byte) ((value >> 24) & 0xFF);
out[pos + 1] = (byte) ((value >> 16) & 0xFF);
out[pos + 2] = (byte) ((value >> 8) & 0xFF);
out[pos + 3] = (byte) ((value >> 0) & 0xFF);
return pos + 4;
}
private int writeBytes(byte[] out, int pos, byte[] value) {
System.arraycopy(value, 0, out, pos, value.length);
return pos + value.length;
}
private int readInt(byte[] in, int pos) {
int result =
((in[pos ] & 0xFF) << 24) |
((in[pos + 1] & 0xFF) << 16) |
((in[pos + 2] & 0xFF) << 8) |
((in[pos + 3] & 0xFF) << 0);
return result;
}
private int enableWifi(boolean enable) {
WifiManager wfm = (WifiManager) getSystemService(Context.WIFI_SERVICE);
if (wfm != null) {
int state = wfm.getWifiState();
wfm.setWifiEnabled(enable);
return state;
}
return WifiManager.WIFI_STATE_UNKNOWN;
}
}

View File

@ -0,0 +1,193 @@
/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.providers.settings;
import java.util.Locale;
import android.app.ActivityManagerNative;
import android.app.IActivityManager;
import android.app.backup.BackupDataInput;
import android.app.backup.IBackupManager;
import android.content.ContentResolver;
import android.content.Context;
import android.content.IContentService;
import android.content.res.Configuration;
import android.location.LocationManager;
import android.media.AudioManager;
import android.os.IPowerManager;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.Log;
public class SettingsHelper {
private static final String TAG = "SettingsHelper";
private Context mContext;
private AudioManager mAudioManager;
private IContentService mContentService;
private IPowerManager mPowerManager;
private boolean mSilent;
private boolean mVibrate;
public SettingsHelper(Context context) {
mContext = context;
mAudioManager = (AudioManager) context
.getSystemService(Context.AUDIO_SERVICE);
mContentService = ContentResolver.getContentService();
mPowerManager = IPowerManager.Stub.asInterface(
ServiceManager.getService("power"));
}
/**
* Sets the property via a call to the appropriate API, if any, and returns
* whether or not the setting should be saved to the database as well.
* @param name the name of the setting
* @param value the string value of the setting
* @return whether to continue with writing the value to the database. In
* some cases the data will be written by the call to the appropriate API,
* and in some cases the property value needs to be modified before setting.
*/
public boolean restoreValue(String name, String value) {
if (Settings.System.SCREEN_BRIGHTNESS.equals(name)) {
setBrightness(Integer.parseInt(value));
} else if (Settings.System.SOUND_EFFECTS_ENABLED.equals(name)) {
setSoundEffects(Integer.parseInt(value) == 1);
} else if (Settings.Secure.LOCATION_PROVIDERS_ALLOWED.equals(name)) {
setGpsLocation(value);
return false;
} else if (Settings.Secure.BACKUP_AUTO_RESTORE.equals(name)) {
setAutoRestore(Integer.parseInt(value) == 1);
}
return true;
}
private void setAutoRestore(boolean enabled) {
try {
IBackupManager bm = IBackupManager.Stub.asInterface(
ServiceManager.getService(Context.BACKUP_SERVICE));
if (bm != null) {
bm.setAutoRestore(enabled);
}
} catch (RemoteException e) {}
}
private void setGpsLocation(String value) {
final String GPS = LocationManager.GPS_PROVIDER;
boolean enabled =
GPS.equals(value) ||
value.startsWith(GPS + ",") ||
value.endsWith("," + GPS) ||
value.contains("," + GPS + ",");
Settings.Secure.setLocationProviderEnabled(
mContext.getContentResolver(), GPS, enabled);
}
private void setSoundEffects(boolean enable) {
if (enable) {
mAudioManager.loadSoundEffects();
} else {
mAudioManager.unloadSoundEffects();
}
}
private void setBrightness(int brightness) {
try {
IPowerManager power = IPowerManager.Stub.asInterface(
ServiceManager.getService("power"));
if (power != null) {
power.setBacklightBrightness(brightness);
}
} catch (RemoteException doe) {
}
}
private void setRingerMode() {
if (mSilent) {
mAudioManager.setRingerMode(mVibrate ? AudioManager.RINGER_MODE_VIBRATE :
AudioManager.RINGER_MODE_SILENT);
} else {
mAudioManager.setRingerMode(AudioManager.RINGER_MODE_NORMAL);
mAudioManager.setVibrateSetting(AudioManager.VIBRATE_TYPE_RINGER,
mVibrate ? AudioManager.VIBRATE_SETTING_ON
: AudioManager.VIBRATE_SETTING_OFF);
}
}
byte[] getLocaleData() {
Configuration conf = mContext.getResources().getConfiguration();
final Locale loc = conf.locale;
String localeString = loc.getLanguage();
String country = loc.getCountry();
if (!TextUtils.isEmpty(country)) {
localeString += "_" + country;
}
return localeString.getBytes();
}
/**
* Sets the locale specified. Input data is the equivalent of "ll_cc".getBytes(), where
* "ll" is the language code and "cc" is the country code.
* @param data the locale string in bytes.
*/
void setLocaleData(byte[] data) {
// Check if locale was set by the user:
Configuration conf = mContext.getResources().getConfiguration();
Locale loc = conf.locale;
// TODO: The following is not working as intended because the network is forcing a locale
// change after registering. Need to find some other way to detect if the user manually
// changed the locale
if (conf.userSetLocale) return; // Don't change if user set it in the SetupWizard
final String[] availableLocales = mContext.getAssets().getLocales();
String localeCode = new String(data);
String language = new String(data, 0, 2);
String country = data.length > 4 ? new String(data, 3, 2) : "";
loc = null;
for (int i = 0; i < availableLocales.length; i++) {
if (availableLocales[i].equals(localeCode)) {
loc = new Locale(language, country);
break;
}
}
if (loc == null) return; // Couldn't find the saved locale in this version of the software
try {
IActivityManager am = ActivityManagerNative.getDefault();
Configuration config = am.getConfiguration();
config.locale = loc;
// indicate this isn't some passing default - the user wants this remembered
config.userSetLocale = true;
am.updateConfiguration(config);
} catch (RemoteException e) {
// Intentionally left blank
}
}
/**
* Informs the audio service of changes to the settings so that
* they can be re-read and applied.
*/
void applyAudioSettings() {
AudioManager am = new AudioManager(mContext);
am.reloadAudioSettings();
}
}

View File

@ -0,0 +1,854 @@
/*
* Copyright (C) 2007 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.providers.settings;
import java.io.FileNotFoundException;
import java.io.UnsupportedEncodingException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import android.app.backup.BackupManager;
import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.res.AssetFileDescriptor;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteQueryBuilder;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.FileObserver;
import android.os.ParcelFileDescriptor;
import android.os.SystemProperties;
import android.provider.DrmStore;
import android.provider.MediaStore;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.Log;
public class SettingsProvider extends ContentProvider {
private static final String TAG = "SettingsProvider";
private static final boolean LOCAL_LOGV = false;
private static final String TABLE_FAVORITES = "favorites";
private static final String TABLE_OLD_FAVORITES = "old_favorites";
private static final String[] COLUMN_VALUE = new String[] { "value" };
// Cache for settings, access-ordered for acting as LRU.
// Guarded by themselves.
private static final int MAX_CACHE_ENTRIES = 200;
private static final SettingsCache sSystemCache = new SettingsCache("system");
private static final SettingsCache sSecureCache = new SettingsCache("secure");
// The count of how many known (handled by SettingsProvider)
// database mutations are currently being handled. Used by
// sFileObserver to not reload the database when it's ourselves
// modifying it.
private static final AtomicInteger sKnownMutationsInFlight = new AtomicInteger(0);
// Over this size we don't reject loading or saving settings but
// we do consider them broken/malicious and don't keep them in
// memory at least:
private static final int MAX_CACHE_ENTRY_SIZE = 500;
private static final Bundle NULL_SETTING = Bundle.forPair("value", null);
// Used as a sentinel value in an instance equality test when we
// want to cache the existence of a key, but not store its value.
private static final Bundle TOO_LARGE_TO_CACHE_MARKER = Bundle.forPair("_dummy", null);
protected DatabaseHelper mOpenHelper;
private BackupManager mBackupManager;
/**
* Decode a content URL into the table, projection, and arguments
* used to access the corresponding database rows.
*/
private static class SqlArguments {
public String table;
public final String where;
public final String[] args;
/** Operate on existing rows. */
SqlArguments(Uri url, String where, String[] args) {
if (url.getPathSegments().size() == 1) {
this.table = url.getPathSegments().get(0);
if (!DatabaseHelper.isValidTable(this.table)) {
throw new IllegalArgumentException("Bad root path: " + this.table);
}
this.where = where;
this.args = args;
} else if (url.getPathSegments().size() != 2) {
throw new IllegalArgumentException("Invalid URI: " + url);
} else if (!TextUtils.isEmpty(where)) {
throw new UnsupportedOperationException("WHERE clause not supported: " + url);
} else {
this.table = url.getPathSegments().get(0);
if (!DatabaseHelper.isValidTable(this.table)) {
throw new IllegalArgumentException("Bad root path: " + this.table);
}
if ("system".equals(this.table) || "secure".equals(this.table)) {
this.where = Settings.NameValueTable.NAME + "=?";
this.args = new String[] { url.getPathSegments().get(1) };
} else {
this.where = "_id=" + ContentUris.parseId(url);
this.args = null;
}
}
}
/** Insert new rows (no where clause allowed). */
SqlArguments(Uri url) {
if (url.getPathSegments().size() == 1) {
this.table = url.getPathSegments().get(0);
if (!DatabaseHelper.isValidTable(this.table)) {
throw new IllegalArgumentException("Bad root path: " + this.table);
}
this.where = null;
this.args = null;
} else {
throw new IllegalArgumentException("Invalid URI: " + url);
}
}
}
/**
* Get the content URI of a row added to a table.
* @param tableUri of the entire table
* @param values found in the row
* @param rowId of the row
* @return the content URI for this particular row
*/
private Uri getUriFor(Uri tableUri, ContentValues values, long rowId) {
if (tableUri.getPathSegments().size() != 1) {
throw new IllegalArgumentException("Invalid URI: " + tableUri);
}
String table = tableUri.getPathSegments().get(0);
if ("system".equals(table) || "secure".equals(table)) {
String name = values.getAsString(Settings.NameValueTable.NAME);
return Uri.withAppendedPath(tableUri, name);
} else {
return ContentUris.withAppendedId(tableUri, rowId);
}
}
/**
* Send a notification when a particular content URI changes.
* Modify the system property used to communicate the version of
* this table, for tables which have such a property. (The Settings
* contract class uses these to provide client-side caches.)
* @param uri to send notifications for
*/
private void sendNotify(Uri uri) {
// Update the system property *first*, so if someone is listening for
// a notification and then using the contract class to get their data,
// the system property will be updated and they'll get the new data.
boolean backedUpDataChanged = false;
String property = null, table = uri.getPathSegments().get(0);
if (table.equals("system")) {
property = Settings.System.SYS_PROP_SETTING_VERSION;
backedUpDataChanged = true;
} else if (table.equals("secure")) {
property = Settings.Secure.SYS_PROP_SETTING_VERSION;
backedUpDataChanged = true;
}
if (property != null) {
long version = SystemProperties.getLong(property, 0) + 1;
if (LOCAL_LOGV) Log.v(TAG, "property: " + property + "=" + version);
SystemProperties.set(property, Long.toString(version));
}
// Inform the backup manager about a data change
if (backedUpDataChanged) {
mBackupManager.dataChanged();
}
// Now send the notification through the content framework.
String notify = uri.getQueryParameter("notify");
if (notify == null || "true".equals(notify)) {
getContext().getContentResolver().notifyChange(uri, null);
if (LOCAL_LOGV) Log.v(TAG, "notifying: " + uri);
} else {
if (LOCAL_LOGV) Log.v(TAG, "notification suppressed: " + uri);
}
}
/**
* Make sure the caller has permission to write this data.
* @param args supplied by the caller
* @throws SecurityException if the caller is forbidden to write.
*/
private void checkWritePermissions(SqlArguments args) {
if ("secure".equals(args.table) &&
getContext().checkCallingOrSelfPermission(
android.Manifest.permission.WRITE_SECURE_SETTINGS) !=
PackageManager.PERMISSION_GRANTED) {
throw new SecurityException(
String.format("Permission denial: writing to secure settings requires %1$s",
android.Manifest.permission.WRITE_SECURE_SETTINGS));
}
}
// FileObserver for external modifications to the database file.
// Note that this is for platform developers only with
// userdebug/eng builds who should be able to tinker with the
// sqlite database out from under the SettingsProvider, which is
// normally the exclusive owner of the database. But we keep this
// enabled all the time to minimize development-vs-user
// differences in testing.
private static SettingsFileObserver sObserverInstance;
private class SettingsFileObserver extends FileObserver {
private final AtomicBoolean mIsDirty = new AtomicBoolean(false);
private final String mPath;
public SettingsFileObserver(String path) {
super(path, FileObserver.CLOSE_WRITE |
FileObserver.CREATE | FileObserver.DELETE |
FileObserver.MOVED_TO | FileObserver.MODIFY);
mPath = path;
}
public void onEvent(int event, String path) {
int modsInFlight = sKnownMutationsInFlight.get();
if (modsInFlight > 0) {
// our own modification.
return;
}
Log.d(TAG, "external modification to " + mPath + "; event=" + event);
if (!mIsDirty.compareAndSet(false, true)) {
// already handled. (we get a few update events
// during an sqlite write)
return;
}
Log.d(TAG, "updating our caches for " + mPath);
fullyPopulateCaches();
mIsDirty.set(false);
}
}
@Override
public boolean onCreate() {
mOpenHelper = new DatabaseHelper(getContext());
mBackupManager = new BackupManager(getContext());
if (!ensureAndroidIdIsSet()) {
return false;
}
// Watch for external modifications to the database file,
// keeping our cache in sync.
// It's kinda lame to call mOpenHelper.getReadableDatabase()
// during onCreate(), but since ensureAndroidIdIsSet has
// already done it above and initialized/upgraded the
// database, might as well just use it...
SQLiteDatabase db = mOpenHelper.getReadableDatabase();
sObserverInstance = new SettingsFileObserver(db.getPath());
sObserverInstance.startWatching();
startAsyncCachePopulation();
return true;
}
private void startAsyncCachePopulation() {
new Thread("populate-settings-caches") {
public void run() {
fullyPopulateCaches();
}
}.start();
}
private void fullyPopulateCaches() {
fullyPopulateCache("secure", sSecureCache);
fullyPopulateCache("system", sSystemCache);
}
// Slurp all values (if sane in number & size) into cache.
private void fullyPopulateCache(String table, SettingsCache cache) {
SQLiteDatabase db = mOpenHelper.getReadableDatabase();
Cursor c = db.query(
table,
new String[] { Settings.NameValueTable.NAME, Settings.NameValueTable.VALUE },
null, null, null, null, null,
"" + (MAX_CACHE_ENTRIES + 1) /* limit */);
try {
synchronized (cache) {
cache.clear();
cache.setFullyMatchesDisk(true); // optimistic
int rows = 0;
while (c.moveToNext()) {
rows++;
String name = c.getString(0);
String value = c.getString(1);
cache.populate(name, value);
}
if (rows > MAX_CACHE_ENTRIES) {
// Somewhat redundant, as removeEldestEntry() will
// have already done this, but to be explicit:
cache.setFullyMatchesDisk(false);
Log.d(TAG, "row count exceeds max cache entries for table " + table);
}
Log.d(TAG, "cache for settings table '" + table + "' rows=" + rows + "; fullycached=" +
cache.fullyMatchesDisk());
}
} finally {
c.close();
}
}
private boolean ensureAndroidIdIsSet() {
final Cursor c = query(Settings.Secure.CONTENT_URI,
new String[] { Settings.NameValueTable.VALUE },
Settings.NameValueTable.NAME + "=?",
new String[] { Settings.Secure.ANDROID_ID }, null);
try {
final String value = c.moveToNext() ? c.getString(0) : null;
if (value == null) {
final SecureRandom random = new SecureRandom();
final String newAndroidIdValue = Long.toHexString(random.nextLong());
Log.d(TAG, "Generated and saved new ANDROID_ID [" + newAndroidIdValue + "]");
final ContentValues values = new ContentValues();
values.put(Settings.NameValueTable.NAME, Settings.Secure.ANDROID_ID);
values.put(Settings.NameValueTable.VALUE, newAndroidIdValue);
final Uri uri = insert(Settings.Secure.CONTENT_URI, values);
if (uri == null) {
return false;
}
}
return true;
} finally {
c.close();
}
}
/**
* Fast path that avoids the use of chatty remoted Cursors.
*/
@Override
public Bundle call(String method, String request, Bundle args) {
if (Settings.CALL_METHOD_GET_SYSTEM.equals(method)) {
return lookupValue("system", sSystemCache, request);
}
if (Settings.CALL_METHOD_GET_SECURE.equals(method)) {
return lookupValue("secure", sSecureCache, request);
}
return null;
}
// Looks up value 'key' in 'table' and returns either a single-pair Bundle,
// possibly with a null value, or null on failure.
private Bundle lookupValue(String table, SettingsCache cache, String key) {
synchronized (cache) {
if (cache.containsKey(key)) {
Bundle value = cache.get(key);
if (value != TOO_LARGE_TO_CACHE_MARKER) {
return value;
}
// else we fall through and read the value from disk
} else if (cache.fullyMatchesDisk()) {
// Fast path (very common). Don't even try touch disk
// if we know we've slurped it all in. Trying to
// touch the disk would mean waiting for yaffs2 to
// give us access, which could takes hundreds of
// milliseconds. And we're very likely being called
// from somebody's UI thread...
return NULL_SETTING;
}
}
SQLiteDatabase db = mOpenHelper.getReadableDatabase();
Cursor cursor = null;
try {
cursor = db.query(table, COLUMN_VALUE, "name=?", new String[]{key},
null, null, null, null);
if (cursor != null && cursor.getCount() == 1) {
cursor.moveToFirst();
return cache.putIfAbsent(key, cursor.getString(0));
}
} catch (SQLiteException e) {
Log.w(TAG, "settings lookup error", e);
return null;
} finally {
if (cursor != null) cursor.close();
}
cache.putIfAbsent(key, null);
return NULL_SETTING;
}
@Override
public Cursor query(Uri url, String[] select, String where, String[] whereArgs, String sort) {
SqlArguments args = new SqlArguments(url, where, whereArgs);
SQLiteDatabase db = mOpenHelper.getReadableDatabase();
// The favorites table was moved from this provider to a provider inside Home
// Home still need to query this table to upgrade from pre-cupcake builds
// However, a cupcake+ build with no data does not contain this table which will
// cause an exception in the SQL stack. The following line is a special case to
// let the caller of the query have a chance to recover and avoid the exception
if (TABLE_FAVORITES.equals(args.table)) {
return null;
} else if (TABLE_OLD_FAVORITES.equals(args.table)) {
args.table = TABLE_FAVORITES;
Cursor cursor = db.rawQuery("PRAGMA table_info(favorites);", null);
if (cursor != null) {
boolean exists = cursor.getCount() > 0;
cursor.close();
if (!exists) return null;
} else {
return null;
}
}
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
qb.setTables(args.table);
Cursor ret = qb.query(db, select, args.where, args.args, null, null, sort);
ret.setNotificationUri(getContext().getContentResolver(), url);
return ret;
}
@Override
public String getType(Uri url) {
// If SqlArguments supplies a where clause, then it must be an item
// (because we aren't supplying our own where clause).
SqlArguments args = new SqlArguments(url, null, null);
if (TextUtils.isEmpty(args.where)) {
return "vnd.android.cursor.dir/" + args.table;
} else {
return "vnd.android.cursor.item/" + args.table;
}
}
@Override
public int bulkInsert(Uri uri, ContentValues[] values) {
SqlArguments args = new SqlArguments(uri);
if (TABLE_FAVORITES.equals(args.table)) {
return 0;
}
checkWritePermissions(args);
SettingsCache cache = SettingsCache.forTable(args.table);
sKnownMutationsInFlight.incrementAndGet();
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
db.beginTransaction();
try {
int numValues = values.length;
for (int i = 0; i < numValues; i++) {
if (db.insert(args.table, null, values[i]) < 0) return 0;
SettingsCache.populate(cache, values[i]);
if (LOCAL_LOGV) Log.v(TAG, args.table + " <- " + values[i]);
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
sKnownMutationsInFlight.decrementAndGet();
}
sendNotify(uri);
return values.length;
}
/*
* Used to parse changes to the value of Settings.Secure.LOCATION_PROVIDERS_ALLOWED.
* This setting contains a list of the currently enabled location providers.
* But helper functions in android.providers.Settings can enable or disable
* a single provider by using a "+" or "-" prefix before the provider name.
*
* @returns whether the database needs to be updated or not, also modifying
* 'initialValues' if needed.
*/
private boolean parseProviderList(Uri url, ContentValues initialValues) {
String value = initialValues.getAsString(Settings.Secure.VALUE);
String newProviders = null;
if (value != null && value.length() > 1) {
char prefix = value.charAt(0);
if (prefix == '+' || prefix == '-') {
// skip prefix
value = value.substring(1);
// read list of enabled providers into "providers"
String providers = "";
String[] columns = {Settings.Secure.VALUE};
String where = Settings.Secure.NAME + "=\'" + Settings.Secure.LOCATION_PROVIDERS_ALLOWED + "\'";
Cursor cursor = query(url, columns, where, null, null);
if (cursor != null && cursor.getCount() == 1) {
try {
cursor.moveToFirst();
providers = cursor.getString(0);
} finally {
cursor.close();
}
}
int index = providers.indexOf(value);
int end = index + value.length();
// check for commas to avoid matching on partial string
if (index > 0 && providers.charAt(index - 1) != ',') index = -1;
if (end < providers.length() && providers.charAt(end) != ',') index = -1;
if (prefix == '+' && index < 0) {
// append the provider to the list if not present
if (providers.length() == 0) {
newProviders = value;
} else {
newProviders = providers + ',' + value;
}
} else if (prefix == '-' && index >= 0) {
// remove the provider from the list if present
// remove leading or trailing comma
if (index > 0) {
index--;
} else if (end < providers.length()) {
end++;
}
newProviders = providers.substring(0, index);
if (end < providers.length()) {
newProviders += providers.substring(end);
}
} else {
// nothing changed, so no need to update the database
return false;
}
if (newProviders != null) {
initialValues.put(Settings.Secure.VALUE, newProviders);
}
}
}
return true;
}
@Override
public Uri insert(Uri url, ContentValues initialValues) {
SqlArguments args = new SqlArguments(url);
if (TABLE_FAVORITES.equals(args.table)) {
return null;
}
checkWritePermissions(args);
// Special case LOCATION_PROVIDERS_ALLOWED.
// Support enabling/disabling a single provider (using "+" or "-" prefix)
String name = initialValues.getAsString(Settings.Secure.NAME);
if (Settings.Secure.LOCATION_PROVIDERS_ALLOWED.equals(name)) {
if (!parseProviderList(url, initialValues)) return null;
}
SettingsCache cache = SettingsCache.forTable(args.table);
String value = initialValues.getAsString(Settings.NameValueTable.VALUE);
if (SettingsCache.isRedundantSetValue(cache, name, value)) {
return Uri.withAppendedPath(url, name);
}
sKnownMutationsInFlight.incrementAndGet();
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
final long rowId = db.insert(args.table, null, initialValues);
sKnownMutationsInFlight.decrementAndGet();
if (rowId <= 0) return null;
SettingsCache.populate(cache, initialValues); // before we notify
if (LOCAL_LOGV) Log.v(TAG, args.table + " <- " + initialValues);
url = getUriFor(url, initialValues, rowId);
sendNotify(url);
return url;
}
@Override
public int delete(Uri url, String where, String[] whereArgs) {
SqlArguments args = new SqlArguments(url, where, whereArgs);
if (TABLE_FAVORITES.equals(args.table)) {
return 0;
} else if (TABLE_OLD_FAVORITES.equals(args.table)) {
args.table = TABLE_FAVORITES;
}
checkWritePermissions(args);
sKnownMutationsInFlight.incrementAndGet();
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
int count = db.delete(args.table, args.where, args.args);
sKnownMutationsInFlight.decrementAndGet();
if (count > 0) {
SettingsCache.invalidate(args.table); // before we notify
sendNotify(url);
}
startAsyncCachePopulation();
if (LOCAL_LOGV) Log.v(TAG, args.table + ": " + count + " row(s) deleted");
return count;
}
@Override
public int update(Uri url, ContentValues initialValues, String where, String[] whereArgs) {
SqlArguments args = new SqlArguments(url, where, whereArgs);
if (TABLE_FAVORITES.equals(args.table)) {
return 0;
}
checkWritePermissions(args);
sKnownMutationsInFlight.incrementAndGet();
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
int count = db.update(args.table, initialValues, args.where, args.args);
sKnownMutationsInFlight.decrementAndGet();
if (count > 0) {
SettingsCache.invalidate(args.table); // before we notify
sendNotify(url);
}
startAsyncCachePopulation();
if (LOCAL_LOGV) Log.v(TAG, args.table + ": " + count + " row(s) <- " + initialValues);
return count;
}
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
/*
* When a client attempts to openFile the default ringtone or
* notification setting Uri, we will proxy the call to the current
* default ringtone's Uri (if it is in the DRM or media provider).
*/
int ringtoneType = RingtoneManager.getDefaultType(uri);
// Above call returns -1 if the Uri doesn't match a default type
if (ringtoneType != -1) {
Context context = getContext();
// Get the current value for the default sound
Uri soundUri = RingtoneManager.getActualDefaultRingtoneUri(context, ringtoneType);
if (soundUri != null) {
// Only proxy the openFile call to drm or media providers
String authority = soundUri.getAuthority();
boolean isDrmAuthority = authority.equals(DrmStore.AUTHORITY);
if (isDrmAuthority || authority.equals(MediaStore.AUTHORITY)) {
if (isDrmAuthority) {
try {
// Check DRM access permission here, since once we
// do the below call the DRM will be checking our
// permission, not our caller's permission
DrmStore.enforceAccessDrmPermission(context);
} catch (SecurityException e) {
throw new FileNotFoundException(e.getMessage());
}
}
return context.getContentResolver().openFileDescriptor(soundUri, mode);
}
}
}
return super.openFile(uri, mode);
}
@Override
public AssetFileDescriptor openAssetFile(Uri uri, String mode) throws FileNotFoundException {
/*
* When a client attempts to openFile the default ringtone or
* notification setting Uri, we will proxy the call to the current
* default ringtone's Uri (if it is in the DRM or media provider).
*/
int ringtoneType = RingtoneManager.getDefaultType(uri);
// Above call returns -1 if the Uri doesn't match a default type
if (ringtoneType != -1) {
Context context = getContext();
// Get the current value for the default sound
Uri soundUri = RingtoneManager.getActualDefaultRingtoneUri(context, ringtoneType);
if (soundUri != null) {
// Only proxy the openFile call to drm or media providers
String authority = soundUri.getAuthority();
boolean isDrmAuthority = authority.equals(DrmStore.AUTHORITY);
if (isDrmAuthority || authority.equals(MediaStore.AUTHORITY)) {
if (isDrmAuthority) {
try {
// Check DRM access permission here, since once we
// do the below call the DRM will be checking our
// permission, not our caller's permission
DrmStore.enforceAccessDrmPermission(context);
} catch (SecurityException e) {
throw new FileNotFoundException(e.getMessage());
}
}
ParcelFileDescriptor pfd = null;
try {
pfd = context.getContentResolver().openFileDescriptor(soundUri, mode);
return new AssetFileDescriptor(pfd, 0, -1);
} catch (FileNotFoundException ex) {
// fall through and open the fallback ringtone below
}
}
try {
return super.openAssetFile(soundUri, mode);
} catch (FileNotFoundException ex) {
// Since a non-null Uri was specified, but couldn't be opened,
// fall back to the built-in ringtone.
return context.getResources().openRawResourceFd(
com.android.internal.R.raw.fallbackring);
}
}
// no need to fall through and have openFile() try again, since we
// already know that will fail.
throw new FileNotFoundException(); // or return null ?
}
// Note that this will end up calling openFile() above.
return super.openAssetFile(uri, mode);
}
/**
* In-memory LRU Cache of system and secure settings, along with
* associated helper functions to keep cache coherent with the
* database.
*/
private static final class SettingsCache extends LinkedHashMap<String, Bundle> {
private final String mCacheName;
private boolean mCacheFullyMatchesDisk = false; // has the whole database slurped.
public SettingsCache(String name) {
super(MAX_CACHE_ENTRIES, 0.75f /* load factor */, true /* access ordered */);
mCacheName = name;
}
/**
* Is the whole database table slurped into this cache?
*/
public boolean fullyMatchesDisk() {
synchronized (this) {
return mCacheFullyMatchesDisk;
}
}
public void setFullyMatchesDisk(boolean value) {
synchronized (this) {
mCacheFullyMatchesDisk = value;
}
}
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
if (size() <= MAX_CACHE_ENTRIES) {
return false;
}
synchronized (this) {
mCacheFullyMatchesDisk = false;
}
return true;
}
/**
* Atomic cache population, conditional on size of value and if
* we lost a race.
*
* @returns a Bundle to send back to the client from call(), even
* if we lost the race.
*/
public Bundle putIfAbsent(String key, String value) {
Bundle bundle = (value == null) ? NULL_SETTING : Bundle.forPair("value", value);
if (value == null || value.length() <= MAX_CACHE_ENTRY_SIZE) {
synchronized (this) {
if (!containsKey(key)) {
put(key, bundle);
}
}
}
return bundle;
}
public static SettingsCache forTable(String tableName) {
if ("system".equals(tableName)) {
return SettingsProvider.sSystemCache;
}
if ("secure".equals(tableName)) {
return SettingsProvider.sSecureCache;
}
return null;
}
/**
* Populates a key in a given (possibly-null) cache.
*/
public static void populate(SettingsCache cache, ContentValues contentValues) {
if (cache == null) {
return;
}
String name = contentValues.getAsString(Settings.NameValueTable.NAME);
if (name == null) {
Log.w(TAG, "null name populating settings cache.");
return;
}
String value = contentValues.getAsString(Settings.NameValueTable.VALUE);
cache.populate(name, value);
}
public void populate(String name, String value) {
synchronized (this) {
if (value == null || value.length() <= MAX_CACHE_ENTRY_SIZE) {
put(name, Bundle.forPair(Settings.NameValueTable.VALUE, value));
} else {
put(name, TOO_LARGE_TO_CACHE_MARKER);
}
}
}
/**
* Used for wiping a whole cache on deletes when we're not
* sure what exactly was deleted or changed.
*/
public static void invalidate(String tableName) {
SettingsCache cache = SettingsCache.forTable(tableName);
if (cache == null) {
return;
}
synchronized (cache) {
cache.clear();
cache.mCacheFullyMatchesDisk = false;
}
}
/**
* For suppressing duplicate/redundant settings inserts early,
* checking our cache first (but without faulting it in),
* before going to sqlite with the mutation.
*/
public static boolean isRedundantSetValue(SettingsCache cache, String name, String value) {
if (cache == null) return false;
synchronized (cache) {
Bundle bundle = cache.get(name);
if (bundle == null) return false;
String oldValue = bundle.getPairValue();
if (oldValue == null && value == null) return true;
if ((oldValue == null) != (value == null)) return false;
return oldValue.equals(value);
}
}
}
}