diff options
author | Jean-Baptiste Queru <jbq@google.com> | 2009-07-25 19:52:22 -0700 |
---|---|---|
committer | Jean-Baptiste Queru <jbq@google.com> | 2009-07-25 21:15:25 -0700 |
commit | 2af1b3db3d4f687d008db74b150f149e956b4bc6 (patch) | |
tree | 39d7d5bf15667c01f9b6dfe02bdd0e7fa36cd303 /packages | |
parent | 8ecb36eec61f119f500a805b82438aadb3396a19 (diff) | |
parent | cf4550c3198d6b3d92cdc52707fe70d7cc0caa9f (diff) | |
download | frameworks_base-2af1b3db3d4f687d008db74b150f149e956b4bc6.zip frameworks_base-2af1b3db3d4f687d008db74b150f149e956b4bc6.tar.gz frameworks_base-2af1b3db3d4f687d008db74b150f149e956b4bc6.tar.bz2 |
Merge korg/donut into korg/master
Diffstat (limited to 'packages')
35 files changed, 4593 insertions, 84 deletions
diff --git a/packages/SettingsProvider/AndroidManifest.xml b/packages/SettingsProvider/AndroidManifest.xml index 4abc337..d84572b 100644 --- a/packages/SettingsProvider/AndroidManifest.xml +++ b/packages/SettingsProvider/AndroidManifest.xml @@ -2,18 +2,14 @@ package="com.android.providers.settings" android:sharedUserId="android.uid.system"> - <!-- Permission to write Gservices in SettingsProvider --> - <permission android:name="android.permission.WRITE_GSERVICES" - android:label="@string/permlab_writeGservices" - android:description="@string/permdesc_writeGservices" - android:protectionLevel="signature" /> - <application android:allowClearUserData="false" - android:label="Settings Storage" + android:label="@string/app_label" + android:process="system" + android:backupAgent="SettingsBackupAgent" android:icon="@drawable/ic_launcher_settings"> <provider android:name="SettingsProvider" android:authorities="settings" - android:process="system" android:multiprocess="false" + android:multiprocess="false" android:writePermission="android.permission.WRITE_SETTINGS" android:initOrder="100" /> </application> diff --git a/packages/SettingsProvider/res/values/defaults.xml b/packages/SettingsProvider/res/values/defaults.xml index 275ff3a..f8adaa1 100644 --- a/packages/SettingsProvider/res/values/defaults.xml +++ b/packages/SettingsProvider/res/values/defaults.xml @@ -31,11 +31,17 @@ <bool name="def_bluetooth_on">false</bool> <bool name="def_install_non_market_apps">false</bool> - <!-- Comma-separated list of providers. --> - <string name="def_location_providers_allowed">network</string> + <!-- Comma-separated list of location providers. + Network location is off by default because it requires + user opt-in via Setup Wizard or Settings. + --> + <string name="def_location_providers_allowed">gps</string> <!-- 0 == mobile, 1 == wifi. --> <integer name="def_network_preference">1</integer> <bool name="def_usb_mass_storage_enabled">true</bool> <bool name="def_wifi_on">false</bool> <bool name="def_networks_available_notification_on">true</bool> + + <bool name="def_backup_enabled">false</bool> + <string name="def_backup_transport"></string> </resources> diff --git a/packages/SettingsProvider/res/values/strings.xml b/packages/SettingsProvider/res/values/strings.xml index 8a00091..9ca575e 100644 --- a/packages/SettingsProvider/res/values/strings.xml +++ b/packages/SettingsProvider/res/values/strings.xml @@ -17,7 +17,6 @@ */ --> <resources> - <string name="permlab_writeGservices">Write Gservices settings.</string> - <string name="permdesc_writeGservices">Allows the application to - change the settings in Gservices.</string> + <!-- Name of the activity for Settings storage. --> + <string name="app_label">Settings Storage</string> </resources> diff --git a/packages/SettingsProvider/src/com/android/providers/settings/DatabaseHelper.java b/packages/SettingsProvider/src/com/android/providers/settings/DatabaseHelper.java index c6f54a3..fa5b8c4 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/DatabaseHelper.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/DatabaseHelper.java @@ -38,6 +38,7 @@ import android.util.Config; import android.util.Log; import android.util.Xml; import com.android.internal.util.XmlUtils; +import com.android.internal.telephony.RILConstants; import com.android.internal.widget.LockPatternUtils; import com.android.internal.widget.LockPatternView; @@ -63,8 +64,8 @@ public class DatabaseHelper extends SQLiteOpenHelper { private static final String TAG = "SettingsProvider"; private static final String DATABASE_NAME = "settings.db"; - private static final int DATABASE_VERSION = 34; - + private static final int DATABASE_VERSION = 35; + private Context mContext; public DatabaseHelper(Context context) { @@ -80,7 +81,7 @@ public class DatabaseHelper extends SQLiteOpenHelper { ");"); db.execSQL("CREATE INDEX secureIndex1 ON secure (name);"); } - + @Override public void onCreate(SQLiteDatabase db) { db.execSQL("CREATE TABLE system (" + @@ -133,7 +134,7 @@ public class DatabaseHelper extends SQLiteOpenHelper { public void onUpgrade(SQLiteDatabase db, int oldVersion, int currentVersion) { Log.w(TAG, "Upgrading settings database from version " + oldVersion + " to " + currentVersion); - + int upgradeVersion = oldVersion; // Pattern for upgrade blocks: @@ -142,7 +143,7 @@ public class DatabaseHelper extends SQLiteOpenHelper { // .. your upgrade logic.. // upgradeVersion = [the DATABASE_VERSION you set] // } - + if (upgradeVersion == 20) { /* * Version 21 is part of the volume control refresh. There is no @@ -155,7 +156,7 @@ public class DatabaseHelper extends SQLiteOpenHelper { upgradeVersion = 21; } - + if (upgradeVersion < 22) { upgradeVersion = 22; // Upgrade the lock gesture storage location and format @@ -175,7 +176,8 @@ public class DatabaseHelper extends SQLiteOpenHelper { // Shortcuts, applications, folders db.execSQL("UPDATE favorites SET spanX=1, spanY=1 WHERE itemType<=0"); // Photo frames, clocks - db.execSQL("UPDATE favorites SET spanX=2, spanY=2 WHERE itemType=1000 or itemType=1002"); + db.execSQL( + "UPDATE favorites SET spanX=2, spanY=2 WHERE itemType=1000 or itemType=1002"); // Search boxes db.execSQL("UPDATE favorites SET spanX=4, spanY=1 WHERE itemType=1001"); db.setTransactionSuccessful(); @@ -184,7 +186,7 @@ public class DatabaseHelper extends SQLiteOpenHelper { } upgradeVersion = 24; } - + if (upgradeVersion == 24) { db.beginTransaction(); try { @@ -211,7 +213,7 @@ public class DatabaseHelper extends SQLiteOpenHelper { } upgradeVersion = 26; } - + if (upgradeVersion == 26) { // This introduces the new secure settings table. db.beginTransaction(); @@ -223,12 +225,12 @@ public class DatabaseHelper extends SQLiteOpenHelper { } upgradeVersion = 27; } - + if (upgradeVersion == 27) { // Copy settings values from 'system' to 'secure' and delete them from 'system' SQLiteStatement insertStmt = null; SQLiteStatement deleteStmt = null; - + db.beginTransaction(); try { insertStmt = @@ -269,11 +271,11 @@ public class DatabaseHelper extends SQLiteOpenHelper { Settings.Secure.WIFI_WATCHDOG_PING_DELAY_MS, Settings.Secure.WIFI_WATCHDOG_PING_TIMEOUT_MS, }; - + for (String setting : settingsToMove) { insertStmt.bindString(1, setting); insertStmt.execute(); - + deleteStmt.bindString(1, setting); deleteStmt.execute(); } @@ -289,7 +291,7 @@ public class DatabaseHelper extends SQLiteOpenHelper { } upgradeVersion = 28; } - + if (upgradeVersion == 28 || upgradeVersion == 29) { // Note: The upgrade to 28 was flawed since it didn't delete the old // setting first before inserting. Combining 28 and 29 with the @@ -311,10 +313,10 @@ public class DatabaseHelper extends SQLiteOpenHelper { } finally { db.endTransaction(); } - + upgradeVersion = 30; } - + if (upgradeVersion == 30) { /* * Upgrade 31 clears the title for all quick launch shortcuts so the @@ -371,7 +373,7 @@ public class DatabaseHelper extends SQLiteOpenHelper { } upgradeVersion = 33; } - + if (upgradeVersion == 33) { // Set the default zoom controls to: tap-twice to bring up +/- db.beginTransaction(); @@ -384,6 +386,20 @@ public class DatabaseHelper extends SQLiteOpenHelper { upgradeVersion = 34; } + if (upgradeVersion == 34) { + db.beginTransaction(); + try { + SQLiteStatement stmt = db.compileStatement("INSERT OR IGNORE INTO secure(name,value)" + + " VALUES(?,?);"); + loadSecure35Settings(stmt); + stmt.close(); + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + upgradeVersion = 35; + } + if (upgradeVersion != currentVersion) { Log.w(TAG, "Got stuck trying to upgrade from version " + upgradeVersion + ", must wipe the settings provider"); @@ -403,7 +419,7 @@ public class DatabaseHelper extends SQLiteOpenHelper { } private void upgradeLockPatternLocation(SQLiteDatabase db) { - Cursor c = db.query("system", new String[] {"_id", "value"}, "name='lock_pattern'", + Cursor c = db.query("system", new String[] {"_id", "value"}, "name='lock_pattern'", null, null, null, null); if (c.getCount() > 0) { c.moveToFirst(); @@ -412,7 +428,7 @@ public class DatabaseHelper extends SQLiteOpenHelper { // Convert lock pattern try { LockPatternUtils lpu = new LockPatternUtils(mContext.getContentResolver()); - List<LockPatternView.Cell> cellPattern = + List<LockPatternView.Cell> cellPattern = LockPatternUtils.stringToPattern(lockPattern); lpu.saveLockPattern(cellPattern); } catch (IllegalArgumentException e) { @@ -540,12 +556,12 @@ public class DatabaseHelper extends SQLiteOpenHelper { AudioManager.RINGER_MODE_NORMAL); loadVibrateSetting(db, false); - + // By default, only the ring/notification and system streams are affected loadSetting(stmt, Settings.System.MODE_RINGER_STREAMS_AFFECTED, (1 << AudioManager.STREAM_RING) | (1 << AudioManager.STREAM_NOTIFICATION) | (1 << AudioManager.STREAM_SYSTEM)); - + loadSetting(stmt, Settings.System.MUTE_STREAMS_AFFECTED, ((1 << AudioManager.STREAM_MUSIC) | (1 << AudioManager.STREAM_RING) | @@ -559,7 +575,7 @@ public class DatabaseHelper extends SQLiteOpenHelper { if (deleteOld) { db.execSQL("DELETE FROM system WHERE name='" + Settings.System.VIBRATE_ON + "'"); } - + SQLiteStatement stmt = db.compileStatement("INSERT OR IGNORE INTO system(name,value)" + " VALUES(?,?);"); @@ -574,77 +590,89 @@ public class DatabaseHelper extends SQLiteOpenHelper { private void loadSettings(SQLiteDatabase db) { loadSystemSettings(db); - loadSecureSettings(db); + loadSecureSettings(db); } - + private void loadSystemSettings(SQLiteDatabase db) { SQLiteStatement stmt = db.compileStatement("INSERT OR IGNORE INTO system(name,value)" + " VALUES(?,?);"); - + Resources r = mContext.getResources(); + loadBooleanSetting(stmt, Settings.System.DIM_SCREEN, R.bool.def_dim_screen); - loadSetting(stmt, Settings.System.STAY_ON_WHILE_PLUGGED_IN, + loadSetting(stmt, Settings.System.STAY_ON_WHILE_PLUGGED_IN, "1".equals(SystemProperties.get("ro.kernel.qemu")) ? 1 : 0); loadIntegerSetting(stmt, Settings.System.SCREEN_OFF_TIMEOUT, R.integer.def_screen_off_timeout); - + + // Set default cdma emergency tone + loadSetting(stmt, Settings.System.EMERGENCY_TONE, 0); + + // Set default cdma call auto retry + loadSetting(stmt, Settings.System.CALL_AUTO_RETRY, 0); + + // Set default cdma DTMF type + loadSetting(stmt, Settings.System.DTMF_TONE_TYPE_WHEN_DIALING, 0); + + // Set default hearing aid + loadSetting(stmt, Settings.System.HEARING_AID, 0); + + // Set default tty mode + loadSetting(stmt, Settings.System.TTY_MODE, 0); + loadBooleanSetting(stmt, Settings.System.AIRPLANE_MODE_ON, R.bool.def_airplane_mode_on); - + loadStringSetting(stmt, Settings.System.AIRPLANE_MODE_RADIOS, R.string.def_airplane_mode_radios); - + loadBooleanSetting(stmt, Settings.System.AUTO_TIME, R.bool.def_auto_time); // Sync time to NITZ - + loadIntegerSetting(stmt, Settings.System.SCREEN_BRIGHTNESS, R.integer.def_screen_brightness); - + loadDefaultAnimationSettings(stmt); loadBooleanSetting(stmt, Settings.System.ACCELEROMETER_ROTATION, R.bool.def_accelerometer_rotation); - - // Default date format based on build - loadSetting(stmt, Settings.System.DATE_FORMAT, - SystemProperties.get("ro.com.android.dateformat", - "MM-dd-yyyy")); + stmt.close(); } - + private void loadDefaultAnimationSettings(SQLiteStatement stmt) { loadFractionSetting(stmt, Settings.System.WINDOW_ANIMATION_SCALE, R.fraction.def_window_animation_scale, 1); loadFractionSetting(stmt, Settings.System.TRANSITION_ANIMATION_SCALE, R.fraction.def_window_transition_scale, 1); } - + private void loadSecureSettings(SQLiteDatabase db) { SQLiteStatement stmt = db.compileStatement("INSERT OR IGNORE INTO secure(name,value)" + " VALUES(?,?);"); - + loadBooleanSetting(stmt, Settings.Secure.BLUETOOTH_ON, R.bool.def_bluetooth_on); - + // Data roaming default, based on build - loadSetting(stmt, Settings.Secure.DATA_ROAMING, + loadSetting(stmt, Settings.Secure.DATA_ROAMING, "true".equalsIgnoreCase( - SystemProperties.get("ro.com.android.dataroaming", - "false")) ? 1 : 0); - + SystemProperties.get("ro.com.android.dataroaming", + "false")) ? 1 : 0); + loadBooleanSetting(stmt, Settings.Secure.INSTALL_NON_MARKET_APPS, R.bool.def_install_non_market_apps); - + loadStringSetting(stmt, Settings.Secure.LOCATION_PROVIDERS_ALLOWED, R.string.def_location_providers_allowed); - + loadIntegerSetting(stmt, Settings.Secure.NETWORK_PREFERENCE, R.integer.def_network_preference); - + loadBooleanSetting(stmt, Settings.Secure.USB_MASS_STORAGE_ENABLED, R.bool.def_usb_mass_storage_enabled); - + loadBooleanSetting(stmt, Settings.Secure.WIFI_ON, R.bool.def_wifi_on); loadBooleanSetting(stmt, Settings.Secure.WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON, @@ -655,37 +683,60 @@ public class DatabaseHelper extends SQLiteOpenHelper { loadSetting(stmt, Settings.Secure.WIFI_WATCHDOG_WATCH_LIST, wifiWatchList); } + // Set the preferred network mode to 0 = Global, CDMA default + int type = SystemProperties.getInt("ro.telephony.default_network", + RILConstants.PREFERRED_NETWORK_MODE); + loadSetting(stmt, Settings.Secure.PREFERRED_NETWORK_MODE, type); + + // Enable or disable Cell Broadcast SMS + loadSetting(stmt, Settings.Secure.CDMA_CELL_BROADCAST_SMS, + RILConstants.CDMA_CELL_BROADCAST_SMS_DISABLED); + + // Set the preferred cdma subscription to 0 = Subscription from RUIM, when available + loadSetting(stmt, Settings.Secure.PREFERRED_CDMA_SUBSCRIPTION, + RILConstants.PREFERRED_CDMA_SUBSCRIPTION); + // Don't do this. The SystemServer will initialize ADB_ENABLED from a // persistent system property instead. //loadSetting(stmt, Settings.Secure.ADB_ENABLED, 0); - + // Allow mock locations default, based on build - loadSetting(stmt, Settings.Secure.ALLOW_MOCK_LOCATION, + loadSetting(stmt, Settings.Secure.ALLOW_MOCK_LOCATION, "1".equals(SystemProperties.get("ro.allow.mock.location")) ? 1 : 0); + + loadSecure35Settings(stmt); stmt.close(); } + private void loadSecure35Settings(SQLiteStatement stmt) { + loadBooleanSetting(stmt, Settings.Secure.BACKUP_ENABLED, + R.bool.def_backup_enabled); + + loadStringSetting(stmt, Settings.Secure.BACKUP_TRANSPORT, + R.string.def_backup_transport); + } + private void loadSetting(SQLiteStatement stmt, String key, Object value) { stmt.bindString(1, key); stmt.bindString(2, value.toString()); stmt.execute(); } - + private void loadStringSetting(SQLiteStatement stmt, String key, int resid) { loadSetting(stmt, key, mContext.getResources().getString(resid)); } - + private void loadBooleanSetting(SQLiteStatement stmt, String key, int resid) { loadSetting(stmt, key, mContext.getResources().getBoolean(resid) ? "1" : "0"); } - + private void loadIntegerSetting(SQLiteStatement stmt, String key, int resid) { loadSetting(stmt, key, Integer.toString(mContext.getResources().getInteger(resid))); } - + private void loadFractionSetting(SQLiteStatement stmt, String key, int resid, int base) { loadSetting(stmt, key, Float.toString(mContext.getResources().getFraction(resid, base, base))); diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java new file mode 100644 index 0000000..b6bc8a5 --- /dev/null +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java @@ -0,0 +1,332 @@ +/* + * 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.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; + +import android.backup.BackupDataInput; +import android.backup.BackupDataOutput; +import android.backup.BackupHelperAgent; +import android.bluetooth.BluetoothDevice; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.media.AudioManager; +import android.net.Uri; +import android.net.wifi.WifiManager; +import android.os.ParcelFileDescriptor; +import android.os.RemoteException; +import android.os.ServiceManager; +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 BackupHelperAgent { + + private static final String KEY_SYSTEM = "system"; + private static final String KEY_SECURE = "secure"; + private static final String KEY_SYNC = "sync_providers"; + private static final String KEY_LOCALE = "locale"; + + 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_ID = 0; + 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_BT_ROOT = "/data/misc/hcid/"; + + 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[] syncProviders = mSettingsHelper.getSyncProviders(); + byte[] locale = mSettingsHelper.getLocaleData(); + + data.writeEntityHeader(KEY_SYSTEM, systemSettingsData.length); + data.writeEntityData(systemSettingsData, systemSettingsData.length); + + data.writeEntityHeader(KEY_SECURE, secureSettingsData.length); + data.writeEntityData(secureSettingsData, secureSettingsData.length); + + data.writeEntityHeader(KEY_SYNC, syncProviders.length); + data.writeEntityData(syncProviders, syncProviders.length); + + data.writeEntityHeader(KEY_LOCALE, locale.length); + data.writeEntityData(locale, locale.length); + + backupFile(FILE_WIFI_SUPPLICANT, data); + } + + @Override + public void onRestore(BackupDataInput data, int appVersionCode, + ParcelFileDescriptor newState) throws IOException { + + enableWifi(false); + enableBluetooth(false); + + while (data.readNextHeader()) { + final String key = data.getKey(); + final int size = data.getDataSize(); + if (KEY_SYSTEM.equals(key)) { + restoreSettings(data, Settings.System.CONTENT_URI); + } else if (KEY_SECURE.equals(key)) { + restoreSettings(data, Settings.Secure.CONTENT_URI); +// TODO: Re-enable WIFI restore when we figure out a solution for the permissions +// } else if (FILE_WIFI_SUPPLICANT.equals(key)) { +// restoreFile(FILE_WIFI_SUPPLICANT, data); + } else if (KEY_SYNC.equals(key)) { + mSettingsHelper.setSyncProviders(data); + } else if (KEY_LOCALE.equals(key)) { + byte[] localeData = new byte[size]; + data.readEntityData(localeData, 0, size); + mSettingsHelper.setLocaleData(localeData); + } else { + data.skipEntityData(); + } + } + } + + 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) { + 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); + if (mSettingsHelper.restoreValue(settingName, settingValue)) { + cv.clear(); + cv.put(Settings.NameValueTable.NAME, settingName); + cv.put(Settings.NameValueTable.VALUE, settingValue); + getContentResolver().insert(contentUri, cv); + } + } + } + } + + 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 void backupFile(String filename, BackupDataOutput data) { + try { + File file = new File(filename); + if (file.exists()) { + byte[] bytes = new byte[(int) file.length()]; + FileInputStream fis = new FileInputStream(file); + int offset = 0; + int got = 0; + do { + got = fis.read(bytes, offset, bytes.length - offset); + if (got > 0) offset += got; + } while (offset < bytes.length && got > 0); + data.writeEntityHeader(filename, bytes.length); + data.writeEntityData(bytes, bytes.length); + } else { + data.writeEntityHeader(filename, 0); + data.writeEntityData(EMPTY_DATA, 0); + } + } catch (IOException ioe) { + Log.w(TAG, "Couldn't backup " + filename); + } + } + + private void restoreFile(String filename, BackupDataInput data) { + byte[] bytes = new byte[data.getDataSize()]; + if (bytes.length <= 0) return; + try { + data.readEntityData(bytes, 0, bytes.length); + FileOutputStream fos = new FileOutputStream(filename); + fos.write(bytes); + } catch (IOException ioe) { + Log.w(TAG, "Couldn't restore " + filename); + } + } + + /** + * 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 void enableWifi(boolean enable) { + WifiManager wfm = (WifiManager) getSystemService(Context.WIFI_SERVICE); + if (wfm != null) { + wfm.setWifiEnabled(enable); + } + } + + private void enableBluetooth(boolean enable) { + BluetoothDevice bt = (BluetoothDevice) getSystemService(Context.BLUETOOTH_SERVICE); + if (bt != null) { + if (!enable) { + bt.disable(); + } else { + bt.enable(); + } + } + } +} diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java new file mode 100644 index 0000000..2c5775a --- /dev/null +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java @@ -0,0 +1,198 @@ +/* + * 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.backup.BackupDataInput; +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.IHardwareService; +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 static final String[] PROVIDERS = { "gmail-ls", "calendar", "contacts" }; + + private boolean mSilent; + private boolean mVibrate; + + public SettingsHelper(Context context) { + mContext = context; + mAudioManager = (AudioManager) context + .getSystemService(Context.AUDIO_SERVICE); + mContentService = ContentResolver.getContentService(); + } + + /** + * 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; + } + return true; + } + + 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 { + IHardwareService hardware = IHardwareService.Stub + .asInterface(ServiceManager.getService("hardware")); + if (hardware != null) { + hardware.setBacklights(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[] getSyncProviders() { + byte[] sync = new byte[1 + PROVIDERS.length]; + try { + sync[0] = (byte) (mContentService.getListenForNetworkTickles() ? 1 : 0); + for (int i = 0; i < PROVIDERS.length; i++) { + sync[i + 1] = (byte) + (mContentService.getSyncProviderAutomatically(PROVIDERS[i]) ? 1 : 0); + } + } catch (RemoteException re) { + Log.w(TAG, "Unable to backup sync providers"); + return sync; + } + return sync; + } + + void setSyncProviders(BackupDataInput backup) { + byte[] sync = new byte[backup.getDataSize()]; + + try { + backup.readEntityData(sync, 0, sync.length); + mContentService.setListenForNetworkTickles(sync[0] == 1); + for (int i = 0; i < PROVIDERS.length; i++) { + mContentService.setSyncProviderAutomatically(PROVIDERS[i], sync[i + 1] > 0); + } + } catch (RemoteException re) { + Log.w(TAG, "Unable to restore sync providers"); + } catch (java.io.IOException ioe) { + Log.w(TAG, "Unable to read sync settings"); + } + } + + 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; + 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 + } + + } +} diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java index 8d52070..2abf8b3 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java @@ -16,6 +16,9 @@ package com.android.providers.settings; +import java.io.FileNotFoundException; + +import android.backup.BackupManager; import android.content.ContentProvider; import android.content.ContentUris; import android.content.ContentValues; @@ -27,6 +30,7 @@ import android.database.sqlite.SQLiteQueryBuilder; import android.media.RingtoneManager; import android.net.Uri; import android.os.ParcelFileDescriptor; +import android.os.ServiceManager; import android.os.SystemProperties; import android.provider.DrmStore; import android.provider.MediaStore; @@ -34,8 +38,6 @@ import android.provider.Settings; import android.text.TextUtils; import android.util.Log; -import java.io.FileNotFoundException; - public class SettingsProvider extends ContentProvider { private static final String TAG = "SettingsProvider"; private static final boolean LOCAL_LOGV = false; @@ -43,7 +45,9 @@ public class SettingsProvider extends ContentProvider { private static final String TABLE_FAVORITES = "favorites"; private static final String TABLE_OLD_FAVORITES = "old_favorites"; - protected DatabaseHelper mOpenHelper; + private DatabaseHelper mOpenHelper; + + private BackupManager mBackupManager; /** * Decode a content URL into the table, projection, and arguments @@ -137,6 +141,8 @@ public class SettingsProvider extends ContentProvider { SystemProperties.set(property, Long.toString(version)); } + // Inform the backup manager about a data change + mBackupManager.dataChanged(); // Now send the notification through the content framework. String notify = uri.getQueryParameter("notify"); @@ -158,20 +164,25 @@ public class SettingsProvider extends ContentProvider { getContext().checkCallingOrSelfPermission( android.Manifest.permission.WRITE_SECURE_SETTINGS) != PackageManager.PERMISSION_GRANTED) { - throw new SecurityException("Cannot write secure settings table"); - + throw new SecurityException( + String.format("Permission denial: writing to secure settings requires %1$s", + android.Manifest.permission.WRITE_SECURE_SETTINGS)); + // TODO: Move gservices into its own provider so we don't need this nonsense. } else if ("gservices".equals(args.table) && getContext().checkCallingOrSelfPermission( android.Manifest.permission.WRITE_GSERVICES) != PackageManager.PERMISSION_GRANTED) { - throw new SecurityException("Cannot write gservices table"); + throw new SecurityException( + String.format("Permission denial: writing to gservices settings requires %1$s", + android.Manifest.permission.WRITE_GSERVICES)); } } @Override public boolean onCreate() { mOpenHelper = new DatabaseHelper(getContext()); + mBackupManager = new BackupManager(getContext()); return true; } @@ -244,6 +255,72 @@ public class SettingsProvider extends ContentProvider { 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. + */ + 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 and trailing commas + if (index > 0) index--; + 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); @@ -252,6 +329,13 @@ public class SettingsProvider extends ContentProvider { } 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; + } + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); final long rowId = db.insert(args.table, null, initialValues); if (rowId <= 0) return null; diff --git a/packages/SubscribedFeedsProvider/AndroidManifest.xml b/packages/SubscribedFeedsProvider/AndroidManifest.xml index 6ecda48..ca00a9b 100644 --- a/packages/SubscribedFeedsProvider/AndroidManifest.xml +++ b/packages/SubscribedFeedsProvider/AndroidManifest.xml @@ -10,7 +10,7 @@ <application android:process="system" android:allowClearUserData="false" android:icon="@drawable/app_icon" - android:label="Sync Feeds"> + android:label="@string/app_label"> <uses-library android:name="com.google.android.gtalkservice" /> <provider android:name="SubscribedFeedsProvider" android:authorities="subscribedfeeds" android:syncable="false" diff --git a/packages/SubscribedFeedsProvider/res/values/strings.xml b/packages/SubscribedFeedsProvider/res/values/strings.xml new file mode 100644 index 0000000..072571d --- /dev/null +++ b/packages/SubscribedFeedsProvider/res/values/strings.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<resources> + <!-- Title of the feed synchronization activity. --> + <string name="app_label">Sync Feeds</string> +</resources> + diff --git a/packages/SubscribedFeedsProvider/src/com/android/providers/subscribedfeeds/SubscribedFeedsIntentService.java b/packages/SubscribedFeedsProvider/src/com/android/providers/subscribedfeeds/SubscribedFeedsIntentService.java index df599c7..8b3bedf 100644 --- a/packages/SubscribedFeedsProvider/src/com/android/providers/subscribedfeeds/SubscribedFeedsIntentService.java +++ b/packages/SubscribedFeedsProvider/src/com/android/providers/subscribedfeeds/SubscribedFeedsIntentService.java @@ -9,7 +9,6 @@ import android.util.Log; import android.util.Config; import android.util.EventLog; import android.app.IntentService; -import android.provider.Sync; import android.provider.SubscribedFeeds; import android.provider.SyncConstValue; import android.database.Cursor; @@ -17,7 +16,7 @@ import android.database.sqlite.SQLiteFullException; import android.app.AlarmManager; import android.app.PendingIntent; import android.os.Bundle; -import android.os.Debug; +import android.os.RemoteException; import android.text.TextUtils; import android.net.Uri; @@ -105,10 +104,6 @@ public class SubscribedFeedsIntentService extends IntentService { private void handleTickle(Context context, String account, String feed) { Cursor c = null; - Sync.Settings.QueryMap syncSettings = - new Sync.Settings.QueryMap(context.getContentResolver(), - false /* don't keep updated */, - null /* not needed since keep updated is false */); final String where = SubscribedFeeds.Feeds._SYNC_ACCOUNT + "= ? " + "and " + SubscribedFeeds.Feeds.FEED + "= ?"; try { @@ -124,9 +119,14 @@ public class SubscribedFeedsIntentService extends IntentService { String authority = c.getString(c.getColumnIndexOrThrow( SubscribedFeeds.Feeds.AUTHORITY)); EventLog.writeEvent(LOG_TICKLE, authority); - if (!syncSettings.getSyncProviderAutomatically(authority)) { - Log.d(TAG, "supressing tickle since provider " + authority - + " is configured to not sync automatically"); + try { + if (!ContentResolver.getContentService() + .getSyncProviderAutomatically(authority)) { + Log.d(TAG, "supressing tickle since provider " + authority + + " is configured to not sync automatically"); + continue; + } + } catch (RemoteException e) { continue; } Uri uri = Uri.parse("content://" + authority); @@ -137,7 +137,6 @@ public class SubscribedFeedsIntentService extends IntentService { } } finally { if (c != null) c.deactivate(); - syncSettings.close(); } } diff --git a/packages/TtsService/Android.mk b/packages/TtsService/Android.mk new file mode 100644 index 0000000..2737fb4 --- /dev/null +++ b/packages/TtsService/Android.mk @@ -0,0 +1,13 @@ +LOCAL_PATH:= $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_MODULE_TAGS := user + +LOCAL_SRC_FILES := $(call all-subdir-java-files) \ + +LOCAL_PACKAGE_NAME := TtsService +LOCAL_CERTIFICATE := platform + +include $(BUILD_PACKAGE) + +include $(call all-makefiles-under,$(LOCAL_PATH)) diff --git a/packages/TtsService/AndroidManifest.xml b/packages/TtsService/AndroidManifest.xml new file mode 100755 index 0000000..bd17ba0 --- /dev/null +++ b/packages/TtsService/AndroidManifest.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="android.tts"> + <application android:label="TTS Service"> + <service android:enabled="true" + android:name=".TtsService" + android:label="TTS Service"> + <intent-filter> + <action android:name="android.intent.action.START_TTS_SERVICE"/> + <category android:name="android.intent.category.TTS"/> + </intent-filter> + </service> + </application> + <uses-permission android:name="android.permission.INTERNET"/> + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> +</manifest> diff --git a/packages/TtsService/MODULE_LICENSE_APACHE2 b/packages/TtsService/MODULE_LICENSE_APACHE2 new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/packages/TtsService/MODULE_LICENSE_APACHE2 diff --git a/packages/TtsService/NOTICE b/packages/TtsService/NOTICE new file mode 100644 index 0000000..64aaa8d --- /dev/null +++ b/packages/TtsService/NOTICE @@ -0,0 +1,190 @@ + + Copyright (c) 2009, 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. + + 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. + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + diff --git a/packages/TtsService/jni/Android.mk b/packages/TtsService/jni/Android.mk new file mode 100755 index 0000000..665d6d2 --- /dev/null +++ b/packages/TtsService/jni/Android.mk @@ -0,0 +1,31 @@ +LOCAL_PATH:= $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_SRC_FILES:= \ + android_tts_SynthProxy.cpp + +LOCAL_C_INCLUDES += \ + $(JNI_H_INCLUDE) + +LOCAL_SHARED_LIBRARIES := \ + libandroid_runtime \ + libnativehelper \ + libmedia \ + libutils \ + libcutils + +ifeq ($(TARGET_SIMULATOR),true) + LOCAL_LDLIBS += -ldl +else + LOCAL_SHARED_LIBRARIES += libdl +endif + + +LOCAL_MODULE:= libttssynthproxy + +LOCAL_ARM_MODE := arm + +LOCAL_PRELINK_MODULE := false + +include $(BUILD_SHARED_LIBRARY) + diff --git a/packages/TtsService/jni/android_tts_SynthProxy.cpp b/packages/TtsService/jni/android_tts_SynthProxy.cpp new file mode 100644 index 0000000..1958ba9 --- /dev/null +++ b/packages/TtsService/jni/android_tts_SynthProxy.cpp @@ -0,0 +1,778 @@ +/* + * Copyright (C) 2009 Google Inc. + * + * 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. + */ +#define LOG_NDEBUG 0 + +#include <stdio.h> +#include <unistd.h> + +#define LOG_TAG "SynthProxy" + +#include <utils/Log.h> +#include <nativehelper/jni.h> +#include <nativehelper/JNIHelp.h> +#include <android_runtime/AndroidRuntime.h> +#include <tts/TtsEngine.h> +#include <media/AudioTrack.h> + +#include <dlfcn.h> + +#define DEFAULT_TTS_RATE 16000 +#define DEFAULT_TTS_FORMAT AudioSystem::PCM_16_BIT +#define DEFAULT_TTS_NB_CHANNELS 1 +#define DEFAULT_TTS_BUFFERSIZE 1024 + +#define USAGEMODE_PLAY_IMMEDIATELY 0 +#define USAGEMODE_WRITE_TO_FILE 1 + +using namespace android; + +// ---------------------------------------------------------------------------- +struct fields_t { + jfieldID synthProxyFieldJniData; + jclass synthProxyClass; + jmethodID synthProxyMethodPost; +}; + +struct afterSynthData_t { + jint jniStorage; + int usageMode; + FILE* outputFile; +}; + +// ---------------------------------------------------------------------------- +static fields_t javaTTSFields; + +// ---------------------------------------------------------------------------- +class SynthProxyJniStorage { + public : + //jclass tts_class; + jobject tts_ref; + TtsEngine* mNativeSynthInterface; + AudioTrack* mAudioOut; + uint32_t mSampleRate; + AudioSystem::audio_format mAudFormat; + int mNbChannels; + int8_t * mBuffer; + size_t mBufferSize; + + SynthProxyJniStorage() { + //tts_class = NULL; + tts_ref = NULL; + mNativeSynthInterface = NULL; + mAudioOut = NULL; + mSampleRate = DEFAULT_TTS_RATE; + mAudFormat = DEFAULT_TTS_FORMAT; + mNbChannels = DEFAULT_TTS_NB_CHANNELS; + mBufferSize = DEFAULT_TTS_BUFFERSIZE; + mBuffer = new int8_t[mBufferSize]; + } + + ~SynthProxyJniStorage() { + killAudio(); + if (mNativeSynthInterface) { + mNativeSynthInterface->shutdown(); + mNativeSynthInterface = NULL; + } + delete mBuffer; + } + + void killAudio() { + if (mAudioOut) { + mAudioOut->stop(); + delete mAudioOut; + mAudioOut = NULL; + } + } + + void createAudioOut(uint32_t rate, AudioSystem::audio_format format, + int channel) { + mSampleRate = rate; + mAudFormat = format; + mNbChannels = channel; + + // TODO use the TTS stream type + int streamType = AudioSystem::MUSIC; + + // retrieve system properties to ensure successful creation of the + // AudioTrack object for playback + int afSampleRate; + if (AudioSystem::getOutputSamplingRate(&afSampleRate, streamType) != NO_ERROR) { + afSampleRate = 44100; + } + int afFrameCount; + if (AudioSystem::getOutputFrameCount(&afFrameCount, streamType) != NO_ERROR) { + afFrameCount = 2048; + } + uint32_t afLatency; + if (AudioSystem::getOutputLatency(&afLatency, streamType) != NO_ERROR) { + afLatency = 500; + } + uint32_t minBufCount = afLatency / ((1000 * afFrameCount)/afSampleRate); + if (minBufCount < 2) minBufCount = 2; + int minFrameCount = (afFrameCount * rate * minBufCount)/afSampleRate; + + mAudioOut = new AudioTrack(streamType, rate, format, channel, + minFrameCount > 4096 ? minFrameCount : 4096, + 0, 0, 0, 0); // not using an AudioTrack callback + + if (mAudioOut->initCheck() != NO_ERROR) { + LOGI("AudioTrack error"); + delete mAudioOut; + mAudioOut = NULL; + } else { + //LOGI("AudioTrack OK"); + mAudioOut->start(); + LOGI("AudioTrack started"); + } + } +}; + + +// ---------------------------------------------------------------------------- +void prepAudioTrack(SynthProxyJniStorage* pJniData, + uint32_t rate, AudioSystem::audio_format format, int channel) +{ + // Don't bother creating a new audiotrack object if the current + // object is already set. + if ( pJniData->mAudioOut && + (rate == pJniData->mSampleRate) && + (format == pJniData->mAudFormat) && + (channel == pJniData->mNbChannels) ){ + return; + } + if (pJniData->mAudioOut){ + pJniData->killAudio(); + } + pJniData->createAudioOut(rate, format, channel); +} + + +// ---------------------------------------------------------------------------- +/* + * Callback from TTS engine. + * Directly speaks using AudioTrack or write to file + */ +static tts_callback_status ttsSynthDoneCB(void *& userdata, uint32_t rate, + AudioSystem::audio_format format, int channel, + int8_t *&wav, size_t &bufferSize, tts_synth_status status) { + //LOGV("ttsSynthDoneCallback: %d bytes", bufferSize); + + if (userdata == NULL){ + LOGE("userdata == NULL"); + return TTS_CALLBACK_HALT; + } + afterSynthData_t* pForAfter = (afterSynthData_t*)userdata; + SynthProxyJniStorage* pJniData = (SynthProxyJniStorage*)(pForAfter->jniStorage); + + if (pForAfter->usageMode == USAGEMODE_PLAY_IMMEDIATELY){ + //LOGV("Direct speech"); + + if (wav == NULL) { + delete pForAfter; + LOGI("Null: speech has completed"); + } + + if (bufferSize > 0) { + prepAudioTrack(pJniData, rate, format, channel); + if (pJniData->mAudioOut) { + pJniData->mAudioOut->write(wav, bufferSize); + //LOGV("AudioTrack wrote: %d bytes", bufferSize); + } else { + LOGE("Can't play, null audiotrack"); + } + } + } else if (pForAfter->usageMode == USAGEMODE_WRITE_TO_FILE) { + LOGV("Save to file"); + if (wav == NULL) { + delete pForAfter; + LOGV("Null: speech has completed"); + return TTS_CALLBACK_HALT; + } + if (bufferSize > 0){ + fwrite(wav, 1, bufferSize, pForAfter->outputFile); + } + } + // Future update: + // For sync points in the speech, call back into the SynthProxy class through the + // javaTTSFields.synthProxyMethodPost methode to notify + // playback has completed if the synthesis is done or if a marker has been reached. + + if (status == TTS_SYNTH_DONE) { + // this struct was allocated in the original android_tts_SynthProxy_speak call, + // all processing matching this call is now done. + LOGV("Speech synthesis done."); + if (pForAfter->usageMode == USAGEMODE_PLAY_IMMEDIATELY) { + // only delete for direct playback. When writing to a file, we still have work to do + // in android_tts_SynthProxy_synthesizeToFile. The struct will be deleted there. + delete pForAfter; + pForAfter = NULL; + } + return TTS_CALLBACK_HALT; + } + + // we don't update the wav (output) parameter as we'll let the next callback + // write at the same location, we've consumed the data already, but we need + // to update bufferSize to let the TTS engine know how much it can write the + // next time it calls this function. + bufferSize = pJniData->mBufferSize; + + return TTS_CALLBACK_CONTINUE; +} + + +// ---------------------------------------------------------------------------- +static void +android_tts_SynthProxy_native_setup(JNIEnv *env, jobject thiz, + jobject weak_this, jstring nativeSoLib) +{ + SynthProxyJniStorage* pJniStorage = new SynthProxyJniStorage(); + + prepAudioTrack(pJniStorage, + DEFAULT_TTS_RATE, DEFAULT_TTS_FORMAT, DEFAULT_TTS_NB_CHANNELS); + + const char *nativeSoLibNativeString = + env->GetStringUTFChars(nativeSoLib, 0); + + void *engine_lib_handle = dlopen(nativeSoLibNativeString, + RTLD_NOW | RTLD_LOCAL); + if (engine_lib_handle==NULL) { + LOGI("engine_lib_handle==NULL"); + // TODO report error so the TTS can't be used + } else { + TtsEngine *(*get_TtsEngine)() = + reinterpret_cast<TtsEngine* (*)()>(dlsym(engine_lib_handle, "getTtsEngine")); + + pJniStorage->mNativeSynthInterface = (*get_TtsEngine)(); + + if (pJniStorage->mNativeSynthInterface) { + pJniStorage->mNativeSynthInterface->init(ttsSynthDoneCB); + } + } + + // we use a weak reference so the SynthProxy object can be garbage collected. + pJniStorage->tts_ref = env->NewGlobalRef(weak_this); + + // save the JNI resources so we can use them (and free them) later + env->SetIntField(thiz, javaTTSFields.synthProxyFieldJniData, + (int)pJniStorage); + + env->ReleaseStringUTFChars(nativeSoLib, nativeSoLibNativeString); +} + + +static void +android_tts_SynthProxy_native_finalize(JNIEnv *env, jobject thiz, jint jniData) +{ + if (jniData) { + SynthProxyJniStorage* pSynthData = (SynthProxyJniStorage*)jniData; + delete pSynthData; + } +} + + +static int +android_tts_SynthProxy_isLanguageAvailable(JNIEnv *env, jobject thiz, jint jniData, + jstring language, jstring country, jstring variant) +{ + int result = TTS_LANG_NOT_SUPPORTED; + + if (jniData == 0) { + LOGE("android_tts_SynthProxy_isLanguageAvailable(): invalid JNI data"); + return result; + } + + SynthProxyJniStorage* pSynthData = (SynthProxyJniStorage*)jniData; + const char *langNativeString = env->GetStringUTFChars(language, 0); + const char *countryNativeString = env->GetStringUTFChars(country, 0); + const char *variantNativeString = env->GetStringUTFChars(variant, 0); + + if (pSynthData->mNativeSynthInterface) { + result = pSynthData->mNativeSynthInterface->isLanguageAvailable(langNativeString, + countryNativeString, variantNativeString); + } + env->ReleaseStringUTFChars(language, langNativeString); + env->ReleaseStringUTFChars(country, countryNativeString); + env->ReleaseStringUTFChars(variant, variantNativeString); + return result; +} + + +static int +android_tts_SynthProxy_setLanguage(JNIEnv *env, jobject thiz, jint jniData, + jstring language, jstring country, jstring variant) +{ + int result = TTS_LANG_NOT_SUPPORTED; + + if (jniData == 0) { + LOGE("android_tts_SynthProxy_setLanguage(): invalid JNI data"); + return result; + } + + SynthProxyJniStorage* pSynthData = (SynthProxyJniStorage*)jniData; + const char *langNativeString = env->GetStringUTFChars(language, 0); + const char *countryNativeString = env->GetStringUTFChars(country, 0); + const char *variantNativeString = env->GetStringUTFChars(variant, 0); + + if (pSynthData->mNativeSynthInterface) { + result = pSynthData->mNativeSynthInterface->setLanguage(langNativeString, + countryNativeString, variantNativeString); + } + env->ReleaseStringUTFChars(language, langNativeString); + env->ReleaseStringUTFChars(country, countryNativeString); + env->ReleaseStringUTFChars(variant, variantNativeString); + return result; +} + + +static int +android_tts_SynthProxy_loadLanguage(JNIEnv *env, jobject thiz, jint jniData, + jstring language, jstring country, jstring variant) +{ + int result = TTS_LANG_NOT_SUPPORTED; + + if (jniData == 0) { + LOGE("android_tts_SynthProxy_loadLanguage(): invalid JNI data"); + return result; + } + + SynthProxyJniStorage* pSynthData = (SynthProxyJniStorage*)jniData; + const char *langNativeString = env->GetStringUTFChars(language, 0); + const char *countryNativeString = env->GetStringUTFChars(country, 0); + const char *variantNativeString = env->GetStringUTFChars(variant, 0); + + if (pSynthData->mNativeSynthInterface) { + result = pSynthData->mNativeSynthInterface->loadLanguage(langNativeString, + countryNativeString, variantNativeString); + } + env->ReleaseStringUTFChars(language, langNativeString); + env->ReleaseStringUTFChars(country, countryNativeString); + env->ReleaseStringUTFChars(variant, variantNativeString); + + return result; +} + + +static int +android_tts_SynthProxy_setSpeechRate(JNIEnv *env, jobject thiz, jint jniData, + jint speechRate) +{ + int result = TTS_FAILURE; + + if (jniData == 0) { + LOGE("android_tts_SynthProxy_setSpeechRate(): invalid JNI data"); + return result; + } + + int bufSize = 10; + char buffer [bufSize]; + sprintf(buffer, "%d", speechRate); + + SynthProxyJniStorage* pSynthData = (SynthProxyJniStorage*)jniData; + LOGI("setting speech rate to %d", speechRate); + + if (pSynthData->mNativeSynthInterface) { + result = pSynthData->mNativeSynthInterface->setProperty("rate", buffer, bufSize); + } + + return result; +} + + +static int +android_tts_SynthProxy_setPitch(JNIEnv *env, jobject thiz, jint jniData, + jint pitch) +{ + int result = TTS_FAILURE; + + if (jniData == 0) { + LOGE("android_tts_SynthProxy_setPitch(): invalid JNI data"); + return result; + } + + int bufSize = 10; + char buffer [bufSize]; + sprintf(buffer, "%d", pitch); + + SynthProxyJniStorage* pSynthData = (SynthProxyJniStorage*)jniData; + LOGI("setting pitch to %d", pitch); + + if (pSynthData->mNativeSynthInterface) { + result = pSynthData->mNativeSynthInterface->setProperty("pitch", buffer, bufSize); + } + + return result; +} + + +static int +android_tts_SynthProxy_synthesizeToFile(JNIEnv *env, jobject thiz, jint jniData, + jstring textJavaString, jstring filenameJavaString) +{ + int result = TTS_FAILURE; + + if (jniData == 0) { + LOGE("android_tts_SynthProxy_synthesizeToFile(): invalid JNI data"); + return result; + } + + SynthProxyJniStorage* pSynthData = (SynthProxyJniStorage*)jniData; + if (!pSynthData->mNativeSynthInterface) { + LOGE("android_tts_SynthProxy_synthesizeToFile(): invalid engine handle"); + return result; + } + + // Retrieve audio parameters before writing the file header + AudioSystem::audio_format encoding = DEFAULT_TTS_FORMAT; + uint32_t rate = DEFAULT_TTS_RATE; + int channels = DEFAULT_TTS_NB_CHANNELS; + pSynthData->mNativeSynthInterface->setAudioFormat(encoding, rate, channels); + + if ((encoding != AudioSystem::PCM_16_BIT) && (encoding != AudioSystem::PCM_8_BIT)) { + LOGE("android_tts_SynthProxy_synthesizeToFile(): engine uses invalid format"); + return result; + } + + const char *filenameNativeString = + env->GetStringUTFChars(filenameJavaString, 0); + const char *textNativeString = env->GetStringUTFChars(textJavaString, 0); + + afterSynthData_t* pForAfter = new (afterSynthData_t); + pForAfter->jniStorage = jniData; + pForAfter->usageMode = USAGEMODE_WRITE_TO_FILE; + + pForAfter->outputFile = fopen(filenameNativeString, "wb"); + + if (pForAfter->outputFile == NULL) { + LOGE("android_tts_SynthProxy_synthesizeToFile(): error creating output file"); + delete pForAfter; + return result; + } + + // Write 44 blank bytes for WAV header, then come back and fill them in + // after we've written the audio data + char header[44]; + fwrite(header, 1, 44, pForAfter->outputFile); + + unsigned int unique_identifier; + + result = pSynthData->mNativeSynthInterface->synthesizeText(textNativeString, + pSynthData->mBuffer, pSynthData->mBufferSize, (void *)pForAfter); + + long filelen = ftell(pForAfter->outputFile); + + int samples = (((int)filelen) - 44) / 2; + header[0] = 'R'; + header[1] = 'I'; + header[2] = 'F'; + header[3] = 'F'; + ((uint32_t *)(&header[4]))[0] = filelen - 8; + header[8] = 'W'; + header[9] = 'A'; + header[10] = 'V'; + header[11] = 'E'; + + header[12] = 'f'; + header[13] = 'm'; + header[14] = 't'; + header[15] = ' '; + + ((uint32_t *)(&header[16]))[0] = 16; // size of fmt + + int sampleSizeInByte = (encoding == AudioSystem::PCM_16_BIT ? 2 : 1); + + ((unsigned short *)(&header[20]))[0] = 1; // format + ((unsigned short *)(&header[22]))[0] = channels; // channels + ((uint32_t *)(&header[24]))[0] = rate; // samplerate + ((uint32_t *)(&header[28]))[0] = rate * sampleSizeInByte * channels;// byterate + ((unsigned short *)(&header[32]))[0] = sampleSizeInByte * channels; // block align + ((unsigned short *)(&header[34]))[0] = sampleSizeInByte * 8; // bits per sample + + header[36] = 'd'; + header[37] = 'a'; + header[38] = 't'; + header[39] = 'a'; + + ((uint32_t *)(&header[40]))[0] = samples * 2; // size of data + + // Skip back to the beginning and rewrite the header + fseek(pForAfter->outputFile, 0, SEEK_SET); + fwrite(header, 1, 44, pForAfter->outputFile); + + fflush(pForAfter->outputFile); + fclose(pForAfter->outputFile); + + delete pForAfter; + pForAfter = NULL; + + env->ReleaseStringUTFChars(textJavaString, textNativeString); + env->ReleaseStringUTFChars(filenameJavaString, filenameNativeString); + + return result; +} + + +static int +android_tts_SynthProxy_speak(JNIEnv *env, jobject thiz, jint jniData, + jstring textJavaString) +{ + int result = TTS_FAILURE; + + if (jniData == 0) { + LOGE("android_tts_SynthProxy_speak(): invalid JNI data"); + return result; + } + + SynthProxyJniStorage* pSynthData = (SynthProxyJniStorage*)jniData; + + if (pSynthData->mAudioOut) { + pSynthData->mAudioOut->stop(); + pSynthData->mAudioOut->start(); + } + + afterSynthData_t* pForAfter = new (afterSynthData_t); + pForAfter->jniStorage = jniData; + pForAfter->usageMode = USAGEMODE_PLAY_IMMEDIATELY; + + if (pSynthData->mNativeSynthInterface) { + const char *textNativeString = env->GetStringUTFChars(textJavaString, 0); + result = pSynthData->mNativeSynthInterface->synthesizeText(textNativeString, + pSynthData->mBuffer, pSynthData->mBufferSize, (void *)pForAfter); + env->ReleaseStringUTFChars(textJavaString, textNativeString); + } + + return result; +} + + +static int +android_tts_SynthProxy_stop(JNIEnv *env, jobject thiz, jint jniData) +{ + int result = TTS_FAILURE; + + if (jniData == 0) { + LOGE("android_tts_SynthProxy_stop(): invalid JNI data"); + return result; + } + + SynthProxyJniStorage* pSynthData = (SynthProxyJniStorage*)jniData; + + if (pSynthData->mNativeSynthInterface) { + result = pSynthData->mNativeSynthInterface->stop(); + } + if (pSynthData->mAudioOut) { + pSynthData->mAudioOut->stop(); + } + + return result; +} + + +static void +android_tts_SynthProxy_shutdown(JNIEnv *env, jobject thiz, jint jniData) +{ + if (jniData == 0) { + LOGE("android_tts_SynthProxy_shutdown(): invalid JNI data"); + return; + } + + SynthProxyJniStorage* pSynthData = (SynthProxyJniStorage*)jniData; + if (pSynthData->mNativeSynthInterface) { + pSynthData->mNativeSynthInterface->shutdown(); + pSynthData->mNativeSynthInterface = NULL; + } +} + + +// TODO add buffer format +static void +android_tts_SynthProxy_playAudioBuffer(JNIEnv *env, jobject thiz, jint jniData, + int bufferPointer, int bufferSize) +{ +LOGI("android_tts_SynthProxy_playAudioBuffer"); + if (jniData == 0) { + LOGE("android_tts_SynthProxy_playAudioBuffer(): invalid JNI data"); + return; + } + + SynthProxyJniStorage* pSynthData = (SynthProxyJniStorage*)jniData; + short* wav = (short*) bufferPointer; + pSynthData->mAudioOut->write(wav, bufferSize); + //LOGI("AudioTrack wrote: %d bytes", bufferSize); +} + + +static jobjectArray +android_tts_SynthProxy_getLanguage(JNIEnv *env, jobject thiz, jint jniData) +{ + if (jniData == 0) { + LOGE("android_tts_SynthProxy_getLanguage(): invalid JNI data"); + return NULL; + } + + SynthProxyJniStorage* pSynthData = (SynthProxyJniStorage*)jniData; + + if (pSynthData->mNativeSynthInterface) { + size_t bufSize = 100; + char lang[bufSize]; + char country[bufSize]; + char variant[bufSize]; + memset(lang, 0, bufSize); + memset(country, 0, bufSize); + memset(variant, 0, bufSize); + jobjectArray retLocale = (jobjectArray)env->NewObjectArray(3, + env->FindClass("java/lang/String"), env->NewStringUTF("")); + pSynthData->mNativeSynthInterface->getLanguage(lang, country, variant); + env->SetObjectArrayElement(retLocale, 0, env->NewStringUTF(lang)); + env->SetObjectArrayElement(retLocale, 1, env->NewStringUTF(country)); + env->SetObjectArrayElement(retLocale, 2, env->NewStringUTF(variant)); + return retLocale; + } else { + return NULL; + } +} + + +JNIEXPORT int JNICALL +android_tts_SynthProxy_getRate(JNIEnv *env, jobject thiz, jint jniData) +{ + if (jniData == 0) { + LOGE("android_tts_SynthProxy_getRate(): invalid JNI data"); + return 0; + } + + SynthProxyJniStorage* pSynthData = (SynthProxyJniStorage*)jniData; + size_t bufSize = 100; + + char buf[bufSize]; + memset(buf, 0, bufSize); + // TODO check return codes + if (pSynthData->mNativeSynthInterface) { + pSynthData->mNativeSynthInterface->getProperty("rate", buf, &bufSize); + } + return atoi(buf); +} + +// Dalvik VM type signatures +static JNINativeMethod gMethods[] = { + { "native_stop", + "(I)I", + (void*)android_tts_SynthProxy_stop + }, + { "native_speak", + "(ILjava/lang/String;)I", + (void*)android_tts_SynthProxy_speak + }, + { "native_synthesizeToFile", + "(ILjava/lang/String;Ljava/lang/String;)I", + (void*)android_tts_SynthProxy_synthesizeToFile + }, + { "native_isLanguageAvailable", + "(ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;)I", + (void*)android_tts_SynthProxy_isLanguageAvailable + }, + { "native_setLanguage", + "(ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;)I", + (void*)android_tts_SynthProxy_setLanguage + }, + { "native_loadLanguage", + "(ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;)I", + (void*)android_tts_SynthProxy_loadLanguage + }, + { "native_setSpeechRate", + "(II)I", + (void*)android_tts_SynthProxy_setSpeechRate + }, + { "native_setPitch", + "(II)I", + (void*)android_tts_SynthProxy_setPitch + }, + { "native_playAudioBuffer", + "(III)V", + (void*)android_tts_SynthProxy_playAudioBuffer + }, + { "native_getLanguage", + "(I)[Ljava/lang/String;", + (void*)android_tts_SynthProxy_getLanguage + }, + { "native_getRate", + "(I)I", + (void*)android_tts_SynthProxy_getRate + }, + { "native_shutdown", + "(I)V", + (void*)android_tts_SynthProxy_shutdown + }, + { "native_setup", + "(Ljava/lang/Object;Ljava/lang/String;)V", + (void*)android_tts_SynthProxy_native_setup + }, + { "native_finalize", + "(I)V", + (void*)android_tts_SynthProxy_native_finalize + } +}; + +#define SP_JNIDATA_FIELD_NAME "mJniData" +#define SP_POSTSPEECHSYNTHESIZED_METHOD_NAME "postNativeSpeechSynthesizedInJava" + +static const char* const kClassPathName = "android/tts/SynthProxy"; + +jint JNI_OnLoad(JavaVM* vm, void* reserved) +{ + JNIEnv* env = NULL; + jint result = -1; + jclass clazz; + + if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) { + LOGE("ERROR: GetEnv failed\n"); + goto bail; + } + assert(env != NULL); + + clazz = env->FindClass(kClassPathName); + if (clazz == NULL) { + LOGE("Can't find %s", kClassPathName); + goto bail; + } + + javaTTSFields.synthProxyClass = clazz; + javaTTSFields.synthProxyFieldJniData = NULL; + javaTTSFields.synthProxyMethodPost = NULL; + + javaTTSFields.synthProxyFieldJniData = env->GetFieldID(clazz, + SP_JNIDATA_FIELD_NAME, "I"); + if (javaTTSFields.synthProxyFieldJniData == NULL) { + LOGE("Can't find %s.%s field", kClassPathName, SP_JNIDATA_FIELD_NAME); + goto bail; + } + + javaTTSFields.synthProxyMethodPost = env->GetStaticMethodID(clazz, + SP_POSTSPEECHSYNTHESIZED_METHOD_NAME, "(Ljava/lang/Object;II)V"); + if (javaTTSFields.synthProxyMethodPost == NULL) { + LOGE("Can't find %s.%s method", kClassPathName, SP_POSTSPEECHSYNTHESIZED_METHOD_NAME); + goto bail; + } + + if (jniRegisterNativeMethods( + env, kClassPathName, gMethods, NELEM(gMethods)) < 0) + goto bail; + + /* success -- return valid version number */ + result = JNI_VERSION_1_4; + + bail: + return result; +} diff --git a/packages/TtsService/src/android/tts/SynthProxy.java b/packages/TtsService/src/android/tts/SynthProxy.java new file mode 100755 index 0000000..bb16b14 --- /dev/null +++ b/packages/TtsService/src/android/tts/SynthProxy.java @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2009 Google Inc. + * + * 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 android.tts; + +import android.util.Log; +import java.lang.ref.WeakReference; + +/** + * @hide + * + * The SpeechSynthesis class provides a high-level api to create and play + * synthesized speech. This class is used internally to talk to a native + * TTS library that implements the interface defined in + * frameworks/base/include/tts/TtsEngine.h + * + */ +@SuppressWarnings("unused") +public class SynthProxy { + + // + // External API + // + + /** + * Constructor; pass the location of the native TTS .so to use. + */ + public SynthProxy(String nativeSoLib) { + Log.e("TTS is loading", nativeSoLib); + native_setup(new WeakReference<SynthProxy>(this), nativeSoLib); + } + + /** + * Stops and clears the AudioTrack. + */ + public int stop() { + return native_stop(mJniData); + } + + /** + * Synthesize speech and speak it directly using AudioTrack. + */ + public int speak(String text) { + return native_speak(mJniData, text); + } + + /** + * Synthesize speech to a file. The current implementation writes a valid + * WAV file to the given path, assuming it is writable. Something like + * "/sdcard/???.wav" is recommended. + */ + public int synthesizeToFile(String text, String filename) { + return native_synthesizeToFile(mJniData, text, filename); + } + + /** + * Queries for language support. + * Return codes are defined in android.speech.tts.TextToSpeech + */ + public int isLanguageAvailable(String language, String country, String variant) { + return native_isLanguageAvailable(mJniData, language, country, variant); + } + + /** + * Sets the language. + */ + public int setLanguage(String language, String country, String variant) { + return native_setLanguage(mJniData, language, country, variant); + } + + /** + * Loads the language: it's not set, but prepared for use later. + */ + public int loadLanguage(String language, String country, String variant) { + return native_loadLanguage(mJniData, language, country, variant); + } + + /** + * Sets the speech rate. + */ + public final int setSpeechRate(int speechRate) { + return native_setSpeechRate(mJniData, speechRate); + } + + /** + * Sets the pitch of the synthesized voice. + */ + public final int setPitch(int pitch) { + return native_setPitch(mJniData, pitch); + } + + /** + * Plays the given audio buffer. + */ + public void playAudioBuffer(int bufferPointer, int bufferSize) { + native_playAudioBuffer(mJniData, bufferPointer, bufferSize); + } + + /** + * Returns the currently set language, country and variant information. + */ + public String[] getLanguage() { + return native_getLanguage(mJniData); + } + + /** + * Gets the currently set rate. + */ + public int getRate() { + return native_getRate(mJniData); + } + + /** + * Shuts down the native synthesizer. + */ + public void shutdown() { + native_shutdown(mJniData); + } + + // + // Internal + // + + protected void finalize() { + native_finalize(mJniData); + mJniData = 0; + } + + static { + System.loadLibrary("ttssynthproxy"); + } + + private final static String TAG = "SynthProxy"; + + /** + * Accessed by native methods + */ + private int mJniData = 0; + + private native final void native_setup(Object weak_this, + String nativeSoLib); + + private native final void native_finalize(int jniData); + + private native final int native_stop(int jniData); + + private native final int native_speak(int jniData, String text); + + private native final int native_synthesizeToFile(int jniData, String text, String filename); + + private native final int native_isLanguageAvailable(int jniData, String language, + String country, String variant); + + private native final int native_setLanguage(int jniData, String language, String country, + String variant); + + private native final int native_loadLanguage(int jniData, String language, String country, + String variant); + + private native final int native_setSpeechRate(int jniData, int speechRate); + + private native final int native_setPitch(int jniData, int speechRate); + + // TODO add buffer format + private native final void native_playAudioBuffer(int jniData, int bufferPointer, int bufferSize); + + private native final String[] native_getLanguage(int jniData); + + private native final int native_getRate(int jniData); + + private native final void native_shutdown(int jniData); + + + /** + * Callback from the C layer + */ + @SuppressWarnings("unused") + private static void postNativeSpeechSynthesizedInJava(Object tts_ref, + int bufferPointer, int bufferSize) { + + Log.i("TTS plugin debug", "bufferPointer: " + bufferPointer + + " bufferSize: " + bufferSize); + + SynthProxy nativeTTS = (SynthProxy)((WeakReference)tts_ref).get(); + // TODO notify TTS service of synthesis/playback completion, + // method definition to be changed. + } +} diff --git a/packages/TtsService/src/android/tts/TtsService.java b/packages/TtsService/src/android/tts/TtsService.java new file mode 100755 index 0000000..a713edf --- /dev/null +++ b/packages/TtsService/src/android/tts/TtsService.java @@ -0,0 +1,936 @@ +/* + * Copyright (C) 2009 Google Inc. + * + * 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 android.tts; + +import android.app.Service; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.media.MediaPlayer; +import android.media.MediaPlayer.OnCompletionListener; +import android.net.Uri; +import android.os.IBinder; +import android.os.RemoteCallbackList; +import android.os.RemoteException; +import android.preference.PreferenceManager; +import android.speech.tts.ITts.Stub; +import android.speech.tts.ITtsCallback; +import android.speech.tts.TextToSpeech; +import android.util.Log; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Locale; +import java.util.concurrent.locks.ReentrantLock; + +/** + * @hide Synthesizes speech from text. This is implemented as a service so that + * other applications can call the TTS without needing to bundle the TTS + * in the build. + * + */ +public class TtsService extends Service implements OnCompletionListener { + + private static class SpeechItem { + public static final int TEXT = 0; + public static final int EARCON = 1; + public static final int SILENCE = 2; + public static final int TEXT_TO_FILE = 3; + public String mText = ""; + public ArrayList<String> mParams = null; + public int mType = TEXT; + public long mDuration = 0; + public String mFilename = null; + + public SpeechItem(String text, ArrayList<String> params, int itemType) { + mText = text; + mParams = params; + mType = itemType; + } + + public SpeechItem(long silenceTime) { + mDuration = silenceTime; + mType = SILENCE; + } + + public SpeechItem(String text, ArrayList<String> params, int itemType, String filename) { + mText = text; + mParams = params; + mType = itemType; + mFilename = filename; + } + + } + + /** + * Contains the information needed to access a sound resource; the name of + * the package that contains the resource and the resID of the resource + * within that package. + */ + private static class SoundResource { + public String mSourcePackageName = null; + public int mResId = -1; + public String mFilename = null; + + public SoundResource(String packageName, int id) { + mSourcePackageName = packageName; + mResId = id; + mFilename = null; + } + + public SoundResource(String file) { + mSourcePackageName = null; + mResId = -1; + mFilename = file; + } + } + + private static final int MAX_SPEECH_ITEM_CHAR_LENGTH = 4000; + private static final int MAX_FILENAME_LENGTH = 250; + + private static final String ACTION = "android.intent.action.START_TTS_SERVICE"; + private static final String CATEGORY = "android.intent.category.TTS"; + private static final String PKGNAME = "android.tts"; + + final RemoteCallbackList<android.speech.tts.ITtsCallback> mCallbacks = new RemoteCallbackList<ITtsCallback>(); + + private Boolean mIsSpeaking; + private ArrayList<SpeechItem> mSpeechQueue; + private HashMap<String, SoundResource> mEarcons; + private HashMap<String, SoundResource> mUtterances; + private MediaPlayer mPlayer; + private TtsService mSelf; + + private ContentResolver mResolver; + + private final ReentrantLock speechQueueLock = new ReentrantLock(); + private final ReentrantLock synthesizerLock = new ReentrantLock(); + + private SynthProxy nativeSynth; + @Override + public void onCreate() { + super.onCreate(); + //Log.i("TTS", "TTS starting"); + + mResolver = getContentResolver(); + + String soLibPath = "/system/lib/libttspico.so"; + nativeSynth = new SynthProxy(soLibPath); + + mSelf = this; + mIsSpeaking = false; + + mEarcons = new HashMap<String, SoundResource>(); + mUtterances = new HashMap<String, SoundResource>(); + + mSpeechQueue = new ArrayList<SpeechItem>(); + mPlayer = null; + + setDefaultSettings(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + // Don't hog the media player + cleanUpPlayer(); + + nativeSynth.shutdown(); + + // Unregister all callbacks. + mCallbacks.kill(); + } + + + private void setDefaultSettings() { + setLanguage(this.getDefaultLanguage(), getDefaultCountry(), getDefaultLocVariant()); + + // speech rate + setSpeechRate(getDefaultRate()); + } + + + private boolean isDefaultEnforced() { + return (android.provider.Settings.Secure.getInt(mResolver, + android.provider.Settings.Secure.TTS_USE_DEFAULTS, + TextToSpeech.Engine.FALLBACK_TTS_USE_DEFAULTS) + == 1 ); + } + + + private int getDefaultRate() { + return android.provider.Settings.Secure.getInt(mResolver, + android.provider.Settings.Secure.TTS_DEFAULT_RATE, + TextToSpeech.Engine.FALLBACK_TTS_DEFAULT_RATE); + } + + + private String getDefaultLanguage() { + String defaultLang = android.provider.Settings.Secure.getString(mResolver, + android.provider.Settings.Secure.TTS_DEFAULT_LANG); + if (defaultLang == null) { + // no setting found, use the current Locale to determine the default language + return Locale.getDefault().getISO3Language(); + } else { + return defaultLang; + } + } + + + private String getDefaultCountry() { + String defaultCountry = android.provider.Settings.Secure.getString(mResolver, + android.provider.Settings.Secure.TTS_DEFAULT_COUNTRY); + if (defaultCountry == null) { + // no setting found, use the current Locale to determine the default country + return Locale.getDefault().getISO3Country(); + } else { + return defaultCountry; + } + } + + + private String getDefaultLocVariant() { + String defaultVar = android.provider.Settings.Secure.getString(mResolver, + android.provider.Settings.Secure.TTS_DEFAULT_VARIANT); + if (defaultVar == null) { + // no setting found, use the current Locale to determine the default variant + return Locale.getDefault().getVariant(); + } else { + return defaultVar; + } + } + + + private int setSpeechRate(int rate) { + if (isDefaultEnforced()) { + return nativeSynth.setSpeechRate(getDefaultRate()); + } else { + return nativeSynth.setSpeechRate(rate); + } + } + + + private int setPitch(int pitch) { + return nativeSynth.setPitch(pitch); + } + + + private int isLanguageAvailable(String lang, String country, String variant) { + //Log.v("TTS", "TtsService.isLanguageAvailable(" + lang + ", " + country + ", " +variant+")"); + return nativeSynth.isLanguageAvailable(lang, country, variant); + } + + + private String[] getLanguage() { + return nativeSynth.getLanguage(); + } + + + private int setLanguage(String lang, String country, String variant) { + //Log.v("TTS", "TtsService.setLanguage(" + lang + ", " + country + ", " + variant + ")"); + if (isDefaultEnforced()) { + return nativeSynth.setLanguage(getDefaultLanguage(), getDefaultCountry(), + getDefaultLocVariant()); + } else { + return nativeSynth.setLanguage(lang, country, variant); + } + } + + + /** + * Adds a sound resource to the TTS. + * + * @param text + * The text that should be associated with the sound resource + * @param packageName + * The name of the package which has the sound resource + * @param resId + * The resource ID of the sound within its package + */ + private void addSpeech(String text, String packageName, int resId) { + mUtterances.put(text, new SoundResource(packageName, resId)); + } + + /** + * Adds a sound resource to the TTS. + * + * @param text + * The text that should be associated with the sound resource + * @param filename + * The filename of the sound resource. This must be a complete + * path like: (/sdcard/mysounds/mysoundbite.mp3). + */ + private void addSpeech(String text, String filename) { + mUtterances.put(text, new SoundResource(filename)); + } + + /** + * Adds a sound resource to the TTS as an earcon. + * + * @param earcon + * The text that should be associated with the sound resource + * @param packageName + * The name of the package which has the sound resource + * @param resId + * The resource ID of the sound within its package + */ + private void addEarcon(String earcon, String packageName, int resId) { + mEarcons.put(earcon, new SoundResource(packageName, resId)); + } + + /** + * Adds a sound resource to the TTS as an earcon. + * + * @param earcon + * The text that should be associated with the sound resource + * @param filename + * The filename of the sound resource. This must be a complete + * path like: (/sdcard/mysounds/mysoundbite.mp3). + */ + private void addEarcon(String earcon, String filename) { + mEarcons.put(earcon, new SoundResource(filename)); + } + + /** + * Speaks the given text using the specified queueing mode and parameters. + * + * @param text + * The text that should be spoken + * @param queueMode + * 0 for no queue (interrupts all previous utterances), 1 for + * queued + * @param params + * An ArrayList of parameters. This is not implemented for all + * engines. + */ + private int speak(String text, int queueMode, ArrayList<String> params) { + if (queueMode == 0) { + stop(); + } + mSpeechQueue.add(new SpeechItem(text, params, SpeechItem.TEXT)); + if (!mIsSpeaking) { + processSpeechQueue(); + } + return TextToSpeech.TTS_SUCCESS; + } + + /** + * Plays the earcon using the specified queueing mode and parameters. + * + * @param earcon + * The earcon that should be played + * @param queueMode + * 0 for no queue (interrupts all previous utterances), 1 for + * queued + * @param params + * An ArrayList of parameters. This is not implemented for all + * engines. + */ + private int playEarcon(String earcon, int queueMode, + ArrayList<String> params) { + if (queueMode == 0) { + stop(); + } + mSpeechQueue.add(new SpeechItem(earcon, params, SpeechItem.EARCON)); + if (!mIsSpeaking) { + processSpeechQueue(); + } + return TextToSpeech.TTS_SUCCESS; + } + + /** + * Stops all speech output and removes any utterances still in the queue. + */ + private int stop() { + Log.i("TTS", "Stopping"); + mSpeechQueue.clear(); + + int result = nativeSynth.stop(); + mIsSpeaking = false; + if (mPlayer != null) { + try { + mPlayer.stop(); + } catch (IllegalStateException e) { + // Do nothing, the player is already stopped. + } + } + Log.i("TTS", "Stopped"); + return result; + } + + public void onCompletion(MediaPlayer arg0) { + processSpeechQueue(); + } + + private int playSilence(long duration, int queueMode, + ArrayList<String> params) { + if (queueMode == 0) { + stop(); + } + mSpeechQueue.add(new SpeechItem(duration)); + if (!mIsSpeaking) { + processSpeechQueue(); + } + return TextToSpeech.TTS_SUCCESS; + } + + private void silence(final long duration) { + class SilenceThread implements Runnable { + public void run() { + try { + Thread.sleep(duration); + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + processSpeechQueue(); + } + } + } + Thread slnc = (new Thread(new SilenceThread())); + slnc.setPriority(Thread.MIN_PRIORITY); + slnc.start(); + } + + private void speakInternalOnly(final String text, + final ArrayList<String> params) { + class SynthThread implements Runnable { + public void run() { + boolean synthAvailable = false; + try { + synthAvailable = synthesizerLock.tryLock(); + if (!synthAvailable) { + Thread.sleep(100); + Thread synth = (new Thread(new SynthThread())); + synth.setPriority(Thread.MIN_PRIORITY); + synth.start(); + return; + } + if (params != null){ + String language = ""; + String country = ""; + String variant = ""; + for (int i = 0; i < params.size() - 1; i = i + 2){ + String param = params.get(i); + if (param.equals(TextToSpeech.Engine.TTS_KEY_PARAM_RATE)){ + setSpeechRate(Integer.parseInt(params.get(i+1))); + } else if (param.equals(TextToSpeech.Engine.TTS_KEY_PARAM_LANGUAGE)){ + language = params.get(i+1); + } else if (param.equals(TextToSpeech.Engine.TTS_KEY_PARAM_COUNTRY)){ + country = params.get(i+1); + } else if (param.equals(TextToSpeech.Engine.TTS_KEY_PARAM_VARIANT)){ + variant = params.get(i+1); + } + } + if (language.length() > 0){ + setLanguage(language, country, variant); + } + } + nativeSynth.speak(text); + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + // This check is needed because finally will always run; + // even if the + // method returns somewhere in the try block. + if (synthAvailable) { + synthesizerLock.unlock(); + } + processSpeechQueue(); + } + } + } + Thread synth = (new Thread(new SynthThread())); + synth.setPriority(Thread.MIN_PRIORITY); + synth.start(); + } + + private void synthToFileInternalOnly(final String text, + final ArrayList<String> params, final String filename) { + class SynthThread implements Runnable { + public void run() { + Log.i("TTS", "Synthesizing to " + filename); + boolean synthAvailable = false; + try { + synthAvailable = synthesizerLock.tryLock(); + if (!synthAvailable) { + Thread.sleep(100); + Thread synth = (new Thread(new SynthThread())); + synth.setPriority(Thread.MIN_PRIORITY); + synth.start(); + return; + } + if (params != null){ + String language = ""; + String country = ""; + String variant = ""; + for (int i = 0; i < params.size() - 1; i = i + 2){ + String param = params.get(i); + if (param.equals(TextToSpeech.Engine.TTS_KEY_PARAM_RATE)){ + setSpeechRate(Integer.parseInt(params.get(i+1))); + } else if (param.equals(TextToSpeech.Engine.TTS_KEY_PARAM_LANGUAGE)){ + language = params.get(i+1); + } else if (param.equals(TextToSpeech.Engine.TTS_KEY_PARAM_COUNTRY)){ + country = params.get(i+1); + } else if (param.equals(TextToSpeech.Engine.TTS_KEY_PARAM_VARIANT)){ + variant = params.get(i+1); + } + } + if (language.length() > 0){ + setLanguage(language, country, variant); + } + } + nativeSynth.synthesizeToFile(text, filename); + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + // This check is needed because finally will always run; + // even if the + // method returns somewhere in the try block. + if (synthAvailable) { + synthesizerLock.unlock(); + } + processSpeechQueue(); + } + } + } + Thread synth = (new Thread(new SynthThread())); + synth.setPriority(Thread.MIN_PRIORITY); + synth.start(); + } + + private SoundResource getSoundResource(SpeechItem speechItem) { + SoundResource sr = null; + String text = speechItem.mText; + if (speechItem.mType == SpeechItem.SILENCE) { + // Do nothing if this is just silence + } else if (speechItem.mType == SpeechItem.EARCON) { + sr = mEarcons.get(text); + } else { + sr = mUtterances.get(text); + } + return sr; + } + + private void broadcastTtsQueueProcessingCompleted(){ + Intent i = new Intent(Intent.ACTION_TTS_QUEUE_PROCESSING_COMPLETED); + sendBroadcast(i); + } + + private void dispatchSpeechCompletedCallbacks(String mark) { + Log.i("TTS callback", "dispatch started"); + // Broadcast to all clients the new value. + final int N = mCallbacks.beginBroadcast(); + for (int i = 0; i < N; i++) { + try { + mCallbacks.getBroadcastItem(i).markReached(mark); + } catch (RemoteException e) { + // The RemoteCallbackList will take care of removing + // the dead object for us. + } + } + mCallbacks.finishBroadcast(); + Log.i("TTS callback", "dispatch completed to " + N); + } + + private SpeechItem splitCurrentTextIfNeeded(SpeechItem currentSpeechItem){ + if (currentSpeechItem.mText.length() < MAX_SPEECH_ITEM_CHAR_LENGTH){ + return currentSpeechItem; + } else { + ArrayList<SpeechItem> splitItems = new ArrayList<SpeechItem>(); + int start = 0; + int end = start + MAX_SPEECH_ITEM_CHAR_LENGTH - 1; + String splitText; + SpeechItem splitItem; + while (end < currentSpeechItem.mText.length()){ + splitText = currentSpeechItem.mText.substring(start, end); + splitItem = new SpeechItem(splitText, null, SpeechItem.TEXT); + splitItems.add(splitItem); + start = end; + end = start + MAX_SPEECH_ITEM_CHAR_LENGTH - 1; + } + splitText = currentSpeechItem.mText.substring(start); + splitItem = new SpeechItem(splitText, null, SpeechItem.TEXT); + splitItems.add(splitItem); + mSpeechQueue.remove(0); + for (int i = splitItems.size() - 1; i >= 0; i--){ + mSpeechQueue.add(0, splitItems.get(i)); + } + return mSpeechQueue.get(0); + } + } + + private void processSpeechQueue() { + boolean speechQueueAvailable = false; + try { + speechQueueAvailable = speechQueueLock.tryLock(); + if (!speechQueueAvailable) { + return; + } + if (mSpeechQueue.size() < 1) { + mIsSpeaking = false; + broadcastTtsQueueProcessingCompleted(); + return; + } + + SpeechItem currentSpeechItem = mSpeechQueue.get(0); + mIsSpeaking = true; + SoundResource sr = getSoundResource(currentSpeechItem); + // Synth speech as needed - synthesizer should call + // processSpeechQueue to continue running the queue + Log.i("TTS processing: ", currentSpeechItem.mText); + if (sr == null) { + if (currentSpeechItem.mType == SpeechItem.TEXT) { + currentSpeechItem = splitCurrentTextIfNeeded(currentSpeechItem); + speakInternalOnly(currentSpeechItem.mText, + currentSpeechItem.mParams); + } else if (currentSpeechItem.mType == SpeechItem.TEXT_TO_FILE) { + synthToFileInternalOnly(currentSpeechItem.mText, + currentSpeechItem.mParams, currentSpeechItem.mFilename); + } else { + // This is either silence or an earcon that was missing + silence(currentSpeechItem.mDuration); + } + } else { + cleanUpPlayer(); + if (sr.mSourcePackageName == PKGNAME) { + // Utterance is part of the TTS library + mPlayer = MediaPlayer.create(this, sr.mResId); + } else if (sr.mSourcePackageName != null) { + // Utterance is part of the app calling the library + Context ctx; + try { + ctx = this.createPackageContext(sr.mSourcePackageName, + 0); + } catch (NameNotFoundException e) { + e.printStackTrace(); + mSpeechQueue.remove(0); // Remove it from the queue and + // move on + mIsSpeaking = false; + return; + } + mPlayer = MediaPlayer.create(ctx, sr.mResId); + } else { + // Utterance is coming from a file + mPlayer = MediaPlayer.create(this, Uri.parse(sr.mFilename)); + } + + // Check if Media Server is dead; if it is, clear the queue and + // give up for now - hopefully, it will recover itself. + if (mPlayer == null) { + mSpeechQueue.clear(); + mIsSpeaking = false; + return; + } + mPlayer.setOnCompletionListener(this); + try { + mPlayer.start(); + } catch (IllegalStateException e) { + mSpeechQueue.clear(); + mIsSpeaking = false; + cleanUpPlayer(); + return; + } + } + if (mSpeechQueue.size() > 0) { + mSpeechQueue.remove(0); + } + } finally { + // This check is needed because finally will always run; even if the + // method returns somewhere in the try block. + if (speechQueueAvailable) { + speechQueueLock.unlock(); + } + } + } + + private void cleanUpPlayer() { + if (mPlayer != null) { + mPlayer.release(); + mPlayer = null; + } + } + + /** + * Synthesizes the given text to a file using the specified parameters. + * + * @param text + * The String of text that should be synthesized + * @param params + * An ArrayList of parameters. The first element of this array + * controls the type of voice to use. + * @param filename + * The string that gives the full output filename; it should be + * something like "/sdcard/myappsounds/mysound.wav". + * @return A boolean that indicates if the synthesis succeeded + */ + private boolean synthesizeToFile(String text, ArrayList<String> params, + String filename) { + // Don't allow a filename that is too long + if (filename.length() > MAX_FILENAME_LENGTH) { + return false; + } + // Don't allow anything longer than the max text length; since this + // is synthing to a file, don't even bother splitting it. + if (text.length() >= MAX_SPEECH_ITEM_CHAR_LENGTH){ + return false; + } + mSpeechQueue.add(new SpeechItem(text, params, SpeechItem.TEXT_TO_FILE, filename)); + if (!mIsSpeaking) { + processSpeechQueue(); + } + return true; + } + + @Override + public IBinder onBind(Intent intent) { + if (ACTION.equals(intent.getAction())) { + for (String category : intent.getCategories()) { + if (category.equals(CATEGORY)) { + return mBinder; + } + } + } + return null; + } + + private final android.speech.tts.ITts.Stub mBinder = new Stub() { + + public void registerCallback(ITtsCallback cb) { + if (cb != null) + mCallbacks.register(cb); + } + + public void unregisterCallback(ITtsCallback cb) { + if (cb != null) + mCallbacks.unregister(cb); + } + + /** + * Speaks the given text using the specified queueing mode and + * parameters. + * + * @param text + * The text that should be spoken + * @param queueMode + * 0 for no queue (interrupts all previous utterances), 1 for + * queued + * @param params + * An ArrayList of parameters. The first element of this + * array controls the type of voice to use. + */ + public int speak(String text, int queueMode, String[] params) { + ArrayList<String> speakingParams = new ArrayList<String>(); + if (params != null) { + speakingParams = new ArrayList<String>(Arrays.asList(params)); + } + return mSelf.speak(text, queueMode, speakingParams); + } + + /** + * Plays the earcon using the specified queueing mode and parameters. + * + * @param earcon + * The earcon that should be played + * @param queueMode + * 0 for no queue (interrupts all previous utterances), 1 for + * queued + * @param params + * An ArrayList of parameters. + */ + public int playEarcon(String earcon, int queueMode, String[] params) { + ArrayList<String> speakingParams = new ArrayList<String>(); + if (params != null) { + speakingParams = new ArrayList<String>(Arrays.asList(params)); + } + return mSelf.playEarcon(earcon, queueMode, speakingParams); + } + + /** + * Plays the silence using the specified queueing mode and parameters. + * + * @param duration + * The duration of the silence that should be played + * @param queueMode + * 0 for no queue (interrupts all previous utterances), 1 for + * queued + * @param params + * An ArrayList of parameters. + */ + public int playSilence(long duration, int queueMode, String[] params) { + ArrayList<String> speakingParams = new ArrayList<String>(); + if (params != null) { + speakingParams = new ArrayList<String>(Arrays.asList(params)); + } + return mSelf.playSilence(duration, queueMode, speakingParams); + } + + /** + * Stops all speech output and removes any utterances still in the + * queue. + */ + public int stop() { + return mSelf.stop(); + } + + /** + * Returns whether or not the TTS is speaking. + * + * @return Boolean to indicate whether or not the TTS is speaking + */ + public boolean isSpeaking() { + return (mSelf.mIsSpeaking && (mSpeechQueue.size() < 1)); + } + + /** + * Adds a sound resource to the TTS. + * + * @param text + * The text that should be associated with the sound resource + * @param packageName + * The name of the package which has the sound resource + * @param resId + * The resource ID of the sound within its package + */ + public void addSpeech(String text, String packageName, int resId) { + mSelf.addSpeech(text, packageName, resId); + } + + /** + * Adds a sound resource to the TTS. + * + * @param text + * The text that should be associated with the sound resource + * @param filename + * The filename of the sound resource. This must be a + * complete path like: (/sdcard/mysounds/mysoundbite.mp3). + */ + public void addSpeechFile(String text, String filename) { + mSelf.addSpeech(text, filename); + } + + /** + * Adds a sound resource to the TTS as an earcon. + * + * @param earcon + * The text that should be associated with the sound resource + * @param packageName + * The name of the package which has the sound resource + * @param resId + * The resource ID of the sound within its package + */ + public void addEarcon(String earcon, String packageName, int resId) { + mSelf.addEarcon(earcon, packageName, resId); + } + + /** + * Adds a sound resource to the TTS as an earcon. + * + * @param earcon + * The text that should be associated with the sound resource + * @param filename + * The filename of the sound resource. This must be a + * complete path like: (/sdcard/mysounds/mysoundbite.mp3). + */ + public void addEarconFile(String earcon, String filename) { + mSelf.addEarcon(earcon, filename); + } + + /** + * Sets the speech rate for the TTS. Note that this will only have an + * effect on synthesized speech; it will not affect pre-recorded speech. + * + * @param speechRate + * The speech rate that should be used + */ + public int setSpeechRate(int speechRate) { + return mSelf.setSpeechRate(speechRate); + } + + /** + * Sets the pitch for the TTS. Note that this will only have an + * effect on synthesized speech; it will not affect pre-recorded speech. + * + * @param pitch + * The pitch that should be used for the synthesized voice + */ + public int setPitch(int pitch) { + return mSelf.setPitch(pitch); + } + + /** + * Returns the level of support for the specified language. + * + * @param lang the three letter ISO language code. + * @param country the three letter ISO country code. + * @param variant the variant code associated with the country and language pair. + * @return one of TTS_LANG_NOT_SUPPORTED, TTS_LANG_MISSING_DATA, TTS_LANG_AVAILABLE, + * TTS_LANG_COUNTRY_AVAILABLE, TTS_LANG_COUNTRY_VAR_AVAILABLE as defined in + * android.speech.tts.TextToSpeech. + */ + public int isLanguageAvailable(String lang, String country, String variant) { + return mSelf.isLanguageAvailable(lang, country, variant); + } + + /** + * Returns the currently set language / country / variant strings representing the + * language used by the TTS engine. + * @return null is no language is set, or an array of 3 string containing respectively + * the language, country and variant. + */ + public String[] getLanguage() { + return mSelf.getLanguage(); + } + + /** + * Sets the speech rate for the TTS, which affects the synthesized voice. + * + * @param lang the three letter ISO language code. + * @param country the three letter ISO country code. + * @param variant the variant code associated with the country and language pair. + */ + public int setLanguage(String lang, String country, String variant) { + return mSelf.setLanguage(lang, country, variant); + } + + /** + * Synthesizes the given text to a file using the specified + * parameters. + * + * @param text + * The String of text that should be synthesized + * @param params + * An ArrayList of parameters. The first element of this + * array controls the type of voice to use. + * @param filename + * The string that gives the full output filename; it should + * be something like "/sdcard/myappsounds/mysound.wav". + * @return A boolean that indicates if the synthesis succeeded + */ + public boolean synthesizeToFile(String text, String[] params, + String filename) { + ArrayList<String> speakingParams = new ArrayList<String>(); + if (params != null) { + speakingParams = new ArrayList<String>(Arrays.asList(params)); + } + return mSelf.synthesizeToFile(text, speakingParams, filename); + } + + }; + +} diff --git a/packages/VpnServices/Android.mk b/packages/VpnServices/Android.mk new file mode 100644 index 0000000..eb27ed5 --- /dev/null +++ b/packages/VpnServices/Android.mk @@ -0,0 +1,16 @@ +LOCAL_PATH:= $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_MODULE_TAGS := user + +LOCAL_SRC_FILES := $(call all-subdir-java-files) + +LOCAL_JAVA_LIBRARIES := + +LOCAL_PACKAGE_NAME := VpnServices +LOCAL_CERTIFICATE := platform + +include $(BUILD_PACKAGE) + +######################## +include $(call all-makefiles-under,$(LOCAL_PATH)) diff --git a/packages/VpnServices/AndroidManifest.xml b/packages/VpnServices/AndroidManifest.xml new file mode 100644 index 0000000..6092e30 --- /dev/null +++ b/packages/VpnServices/AndroidManifest.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.server.vpn" + android:sharedUserId="android.uid.system" + > + <application android:label="@string/app_label"> + + <service android:name=".VpnServiceBinder" android:process=":remote"> + <intent-filter> + <!-- These are the interfaces supported by the service, which + you can bind to. --> + <action android:name="android.net.vpn.IVpnService" /> + <!-- This is an action code you can use to select the service + without explicitly supplying the implementation class. --> + <action android:name="android.net.vpn.SERVICE" /> + </intent-filter> + </service> + + </application> + + <uses-permission android:name="android.permission.INTERNET"></uses-permission> +</manifest> diff --git a/packages/VpnServices/MODULE_LICENSE_APACHE2 b/packages/VpnServices/MODULE_LICENSE_APACHE2 new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/packages/VpnServices/MODULE_LICENSE_APACHE2 diff --git a/packages/VpnServices/NOTICE b/packages/VpnServices/NOTICE new file mode 100644 index 0000000..c5b1efa --- /dev/null +++ b/packages/VpnServices/NOTICE @@ -0,0 +1,190 @@ + + Copyright (c) 2005-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. + + 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. + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + diff --git a/packages/VpnServices/res/drawable/vpn_connected.png b/packages/VpnServices/res/drawable/vpn_connected.png Binary files differnew file mode 100644 index 0000000..65fc6db --- /dev/null +++ b/packages/VpnServices/res/drawable/vpn_connected.png diff --git a/packages/VpnServices/res/drawable/vpn_disconnected.png b/packages/VpnServices/res/drawable/vpn_disconnected.png Binary files differnew file mode 100644 index 0000000..2440c69 --- /dev/null +++ b/packages/VpnServices/res/drawable/vpn_disconnected.png diff --git a/packages/VpnServices/res/values/strings.xml b/packages/VpnServices/res/values/strings.xml new file mode 100755 index 0000000..074655e --- /dev/null +++ b/packages/VpnServices/res/values/strings.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- Title for the VPN Services activity. --> + <string name="app_label">VPN Services</string> + + <string name="vpn_notification_title_connected">%s VPN connected</string> + <string name="vpn_notification_title_disconnected">%s VPN disconnected</string> + <string name="vpn_notification_hint_disconnected">Touch to reconnect to a VPN.</string> +</resources> + diff --git a/packages/VpnServices/src/com/android/server/vpn/AndroidServiceProxy.java b/packages/VpnServices/src/com/android/server/vpn/AndroidServiceProxy.java new file mode 100644 index 0000000..7dd9d9e --- /dev/null +++ b/packages/VpnServices/src/com/android/server/vpn/AndroidServiceProxy.java @@ -0,0 +1,243 @@ +/* + * Copyright (C) 2009, 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.server.vpn; + +import android.net.LocalSocket; +import android.net.LocalSocketAddress; +import android.os.SystemProperties; +import android.util.Log; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * Proxy to start, stop and interact with an Android service defined in init.rc. + * The android service is expected to accept connection through Unix domain + * socket. When the proxy successfully starts the service, it will establish a + * socket connection with the service. The socket serves two purposes: (1) send + * commands to the service; (2) for the proxy to know whether the service is + * alive. + * + * After the service receives commands from the proxy, it should return either + * 0 if the service will close the socket (and the proxy will re-establish + * another connection immediately after), or 1 if the socket is remained alive. + */ +public class AndroidServiceProxy extends ProcessProxy { + private static final int WAITING_TIME = 15; // sec + + private static final String SVC_STATE_CMD_PREFIX = "init.svc."; + private static final String SVC_START_CMD = "ctl.start"; + private static final String SVC_STOP_CMD = "ctl.stop"; + private static final String SVC_STATE_RUNNING = "running"; + private static final String SVC_STATE_STOPPED = "stopped"; + + private static final int END_OF_ARGUMENTS = 255; + + private String mServiceName; + private String mSocketName; + private LocalSocket mKeepaliveSocket; + private boolean mControlSocketInUse; + private Integer mSocketResult = null; + private String mTag; + + /** + * Creates a proxy with the service name. + * @param serviceName the service name + */ + public AndroidServiceProxy(String serviceName) { + mServiceName = serviceName; + mSocketName = serviceName; + mTag = "SProxy_" + serviceName; + } + + @Override + public String getName() { + return "Service " + mServiceName; + } + + @Override + public synchronized void stop() { + if (isRunning()) setResultAndCloseControlSocket(-1); + SystemProperties.set(SVC_STOP_CMD, mServiceName); + } + + /** + * Sends a command with arguments to the service through the control socket. + */ + public void sendCommand(String ...args) throws IOException { + OutputStream out = getControlSocketOutput(); + for (String arg : args) outputString(out, arg); + out.write(END_OF_ARGUMENTS); + out.flush(); + checkSocketResult(); + } + + /** + * {@inheritDoc} + * The method returns when the service exits. + */ + @Override + protected void performTask() throws IOException { + String svc = mServiceName; + Log.d(mTag, "+++++ Execute: " + svc); + SystemProperties.set(SVC_START_CMD, svc); + + boolean success = blockUntil(SVC_STATE_RUNNING, WAITING_TIME); + + if (success) { + Log.d(mTag, "----- Running: " + svc + ", create keepalive socket"); + LocalSocket s = mKeepaliveSocket = createServiceSocket(); + setState(ProcessState.RUNNING); + + if (s == null) { + // no socket connection, stop hosting the service + stop(); + return; + } + try { + for (;;) { + InputStream in = s.getInputStream(); + int data = in.read(); + if (data >= 0) { + Log.d(mTag, "got data from keepalive socket: " + data); + + if (data == 0) { + // re-establish the connection: + // synchronized here so that checkSocketResult() + // returns when new mKeepaliveSocket is available for + // next cmd + synchronized (this) { + setResultAndCloseControlSocket((byte) data); + s = mKeepaliveSocket = createServiceSocket(); + } + } else { + // keep the socket + setSocketResult(data); + } + } else { + // service is gone + if (mControlSocketInUse) setSocketResult(-1); + break; + } + } + Log.d(mTag, "keepalive connection closed"); + } catch (IOException e) { + Log.d(mTag, "keepalive socket broken: " + e.getMessage()); + } + + // Wait 5 seconds for the service to exit + success = blockUntil(SVC_STATE_STOPPED, 5); + Log.d(mTag, "stopping " + svc + ", success? " + success); + } else { + setState(ProcessState.STOPPED); + throw new IOException("cannot start service: " + svc); + } + } + + private LocalSocket createServiceSocket() throws IOException { + LocalSocket s = new LocalSocket(); + LocalSocketAddress a = new LocalSocketAddress(mSocketName, + LocalSocketAddress.Namespace.RESERVED); + + // try a few times in case the service has not listen()ed + IOException excp = null; + for (int i = 0; i < 10; i++) { + try { + s.connect(a); + return s; + } catch (IOException e) { + Log.d(mTag, "service not yet listen()ing; try again"); + excp = e; + sleep(500); + } + } + throw excp; + } + + private OutputStream getControlSocketOutput() throws IOException { + if (mKeepaliveSocket != null) { + mControlSocketInUse = true; + mSocketResult = null; + return mKeepaliveSocket.getOutputStream(); + } else { + throw new IOException("no control socket available"); + } + } + + private synchronized void checkSocketResult() throws IOException { + try { + // will be notified when the result comes back from service + if (mSocketResult == null) wait(); + } catch (InterruptedException e) { + Log.d(mTag, "checkSocketResult(): " + e); + } finally { + mControlSocketInUse = false; + if ((mSocketResult == null) || (mSocketResult < 0)) { + throw new IOException("socket error, result from service: " + + mSocketResult); + } + } + } + + private synchronized void setSocketResult(int result) { + if (mControlSocketInUse) { + mSocketResult = result; + notifyAll(); + } + } + + private void setResultAndCloseControlSocket(int result) { + setSocketResult(result); + try { + mKeepaliveSocket.shutdownInput(); + mKeepaliveSocket.shutdownOutput(); + mKeepaliveSocket.close(); + } catch (IOException e) { + Log.e(mTag, "close keepalive socket", e); + } finally { + mKeepaliveSocket = null; + } + } + + /** + * Waits for the process to be in the expected state. The method returns + * false if after the specified duration (in seconds), the process is still + * not in the expected state. + */ + private boolean blockUntil(String expectedState, int waitTime) { + String cmd = SVC_STATE_CMD_PREFIX + mServiceName; + int sleepTime = 200; // ms + int n = waitTime * 1000 / sleepTime; + for (int i = 0; i < n; i++) { + if (expectedState.equals(SystemProperties.get(cmd))) { + Log.d(mTag, mServiceName + " is " + expectedState + " after " + + (i * sleepTime) + " msec"); + break; + } + sleep(sleepTime); + } + return expectedState.equals(SystemProperties.get(cmd)); + } + + private void outputString(OutputStream out, String s) throws IOException { + byte[] bytes = s.getBytes(); + out.write(bytes.length); + out.write(bytes); + out.flush(); + } +} diff --git a/packages/VpnServices/src/com/android/server/vpn/L2tpIpsecPskService.java b/packages/VpnServices/src/com/android/server/vpn/L2tpIpsecPskService.java new file mode 100644 index 0000000..6abf81c --- /dev/null +++ b/packages/VpnServices/src/com/android/server/vpn/L2tpIpsecPskService.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2009, 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.server.vpn; + +import android.net.vpn.L2tpIpsecPskProfile; + +import java.io.IOException; + +/** + * The service that manages the preshared key based L2TP-over-IPSec VPN + * connection. + */ +class L2tpIpsecPskService extends VpnService<L2tpIpsecPskProfile> { + private static final String IPSEC_DAEMON = "racoon"; + + @Override + protected void connect(String serverIp, String username, String password) + throws IOException { + String hostIp = getHostIp(); + L2tpIpsecPskProfile p = getProfile(); + + // IPSEC + AndroidServiceProxy ipsecService = startService(IPSEC_DAEMON); + ipsecService.sendCommand(hostIp, serverIp, L2tpService.L2TP_PORT, + p.getPresharedKey()); + + sleep(2000); // 2 seconds + + // L2TP + MtpdHelper.sendCommand(this, L2tpService.L2TP_DAEMON, serverIp, + L2tpService.L2TP_PORT, + (p.isSecretEnabled() ? p.getSecretString() : null), + username, password); + } +} diff --git a/packages/VpnServices/src/com/android/server/vpn/L2tpIpsecService.java b/packages/VpnServices/src/com/android/server/vpn/L2tpIpsecService.java new file mode 100644 index 0000000..825953c --- /dev/null +++ b/packages/VpnServices/src/com/android/server/vpn/L2tpIpsecService.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2009, 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.server.vpn; + +import android.net.vpn.L2tpIpsecProfile; +import android.security.CertTool; + +import java.io.IOException; + +/** + * The service that manages the certificate based L2TP-over-IPSec VPN connection. + */ +class L2tpIpsecService extends VpnService<L2tpIpsecProfile> { + private static final String IPSEC_DAEMON = "racoon"; + + @Override + protected void connect(String serverIp, String username, String password) + throws IOException { + String hostIp = getHostIp(); + + // IPSEC + AndroidServiceProxy ipsecService = startService(IPSEC_DAEMON); + ipsecService.sendCommand(hostIp, serverIp, L2tpService.L2TP_PORT, + getUserkeyPath(), getUserCertPath(), getCaCertPath()); + + sleep(2000); // 2 seconds + + // L2TP + L2tpIpsecProfile p = getProfile(); + MtpdHelper.sendCommand(this, L2tpService.L2TP_DAEMON, serverIp, + L2tpService.L2TP_PORT, + (p.isSecretEnabled() ? p.getSecretString() : null), + username, password); + } + + private String getCaCertPath() { + return CertTool.getInstance().getCaCertificate( + getProfile().getCaCertificate()); + } + + private String getUserCertPath() { + return CertTool.getInstance().getUserCertificate( + getProfile().getUserCertificate()); + } + + private String getUserkeyPath() { + return CertTool.getInstance().getUserPrivateKey( + getProfile().getUserCertificate()); + } +} diff --git a/packages/VpnServices/src/com/android/server/vpn/L2tpService.java b/packages/VpnServices/src/com/android/server/vpn/L2tpService.java new file mode 100644 index 0000000..9273f35 --- /dev/null +++ b/packages/VpnServices/src/com/android/server/vpn/L2tpService.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2009, 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.server.vpn; + +import android.net.vpn.L2tpProfile; + +import java.io.IOException; + +/** + * The service that manages the L2TP VPN connection. + */ +class L2tpService extends VpnService<L2tpProfile> { + static final String L2TP_DAEMON = "l2tp"; + static final String L2TP_PORT = "1701"; + + @Override + protected void connect(String serverIp, String username, String password) + throws IOException { + L2tpProfile p = getProfile(); + MtpdHelper.sendCommand(this, L2TP_DAEMON, serverIp, L2TP_PORT, + (p.isSecretEnabled() ? p.getSecretString() : null), + username, password); + } +} diff --git a/packages/VpnServices/src/com/android/server/vpn/MtpdHelper.java b/packages/VpnServices/src/com/android/server/vpn/MtpdHelper.java new file mode 100644 index 0000000..16d253a --- /dev/null +++ b/packages/VpnServices/src/com/android/server/vpn/MtpdHelper.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2009, 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.server.vpn; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; + +/** + * A helper class for sending commands to the MTP daemon (mtpd). + */ +class MtpdHelper { + private static final String MTPD_SERVICE = "mtpd"; + private static final String VPN_LINKNAME = "vpn"; + private static final String PPP_ARGS_SEPARATOR = ""; + + static void sendCommand(VpnService<?> vpnService, String protocol, + String serverIp, String port, String secret, String username, + String password) throws IOException { + ArrayList<String> args = new ArrayList<String>(); + args.addAll(Arrays.asList(protocol, serverIp, port)); + if (secret != null) args.add(secret); + args.add(PPP_ARGS_SEPARATOR); + addPppArguments(vpnService, args, serverIp, username, password); + + AndroidServiceProxy mtpd = vpnService.startService(MTPD_SERVICE); + mtpd.sendCommand(args.toArray(new String[args.size()])); + } + + private static void addPppArguments(VpnService<?> vpnService, + ArrayList<String> args, String serverIp, String username, + String password) throws IOException { + args.addAll(Arrays.asList( + "linkname", VPN_LINKNAME, + "name", username, + "password", password, + "ipparam", serverIp + "@" + vpnService.getGatewayIp(), + "refuse-eap", "nodefaultroute", "usepeerdns", + "idle", "1800", + "mtu", "1400", + "mru", "1400")); + } + + private MtpdHelper() { + } +} diff --git a/packages/VpnServices/src/com/android/server/vpn/NormalProcessProxy.java b/packages/VpnServices/src/com/android/server/vpn/NormalProcessProxy.java new file mode 100644 index 0000000..f0bbc34 --- /dev/null +++ b/packages/VpnServices/src/com/android/server/vpn/NormalProcessProxy.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2009, 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.server.vpn; + +import android.util.Log; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.IOException; + +/** + * Proxy to perform a command with arguments. + */ +public class NormalProcessProxy extends ProcessProxy { + private Process mProcess; + private String[] mArgs; + private String mTag; + + /** + * Creates a proxy with the arguments. + * @param args the argument list with the first one being the command + */ + public NormalProcessProxy(String ...args) { + if ((args == null) || (args.length == 0)) { + throw new IllegalArgumentException(); + } + mArgs = args; + mTag = "PProxy_" + getName(); + } + + @Override + public String getName() { + return mArgs[0]; + } + + @Override + public synchronized void stop() { + if (isStopped()) return; + getHostThread().interrupt(); + // TODO: not sure how to reliably kill a process + mProcess.destroy(); + setState(ProcessState.STOPPING); + } + + @Override + protected void performTask() throws IOException, InterruptedException { + String[] args = mArgs; + Log.d(mTag, "+++++ Execute: " + getEntireCommand()); + ProcessBuilder pb = new ProcessBuilder(args); + setState(ProcessState.RUNNING); + Process p = mProcess = pb.start(); + BufferedReader reader = new BufferedReader(new InputStreamReader( + p.getInputStream())); + while (true) { + String line = reader.readLine(); + if ((line == null) || isStopping()) break; + Log.d(mTag, line); + } + + Log.d(mTag, "----- p.waitFor(): " + getName()); + p.waitFor(); + Log.d(mTag, "----- Done: " + getName()); + } + + private CharSequence getEntireCommand() { + String[] args = mArgs; + StringBuilder sb = new StringBuilder(args[0]); + for (int i = 1; i < args.length; i++) sb.append(' ').append(args[i]); + return sb; + } +} diff --git a/packages/VpnServices/src/com/android/server/vpn/PptpService.java b/packages/VpnServices/src/com/android/server/vpn/PptpService.java new file mode 100644 index 0000000..01362a5 --- /dev/null +++ b/packages/VpnServices/src/com/android/server/vpn/PptpService.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2009, 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.server.vpn; + +import android.net.vpn.PptpProfile; + +import java.io.IOException; + +/** + * The service that manages the PPTP VPN connection. + */ +class PptpService extends VpnService<PptpProfile> { + static final String PPTP_DAEMON = "pptp"; + static final String PPTP_PORT = "1723"; + @Override + protected void connect(String serverIp, String username, String password) + throws IOException { + MtpdHelper.sendCommand(this, PPTP_DAEMON, serverIp, PPTP_PORT, null, + username, password); + } + +} diff --git a/packages/VpnServices/src/com/android/server/vpn/ProcessProxy.java b/packages/VpnServices/src/com/android/server/vpn/ProcessProxy.java new file mode 100644 index 0000000..50fbf4b --- /dev/null +++ b/packages/VpnServices/src/com/android/server/vpn/ProcessProxy.java @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2009, 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.server.vpn; + +import android.os.ConditionVariable; + +import java.io.IOException; + +/** + * A proxy class that spawns a process to accomplish a certain task. + */ +public abstract class ProcessProxy { + /** + * Defines the interface to call back when the process is finished or an + * error occurs during execution. + */ + public static interface Callback { + /** + * Called when the process is finished. + * @param proxy the proxy that hosts the process + */ + void done(ProcessProxy proxy); + + /** + * Called when some error occurs. + * @param proxy the proxy that hosts the process + */ + void error(ProcessProxy proxy, Throwable error); + } + + protected enum ProcessState { + STOPPED, STARTING, RUNNING, STOPPING, ERROR + } + + private ProcessState mState = ProcessState.STOPPED; + private ConditionVariable mLock = new ConditionVariable(); + private Thread mThread; + + /** + * Returns the name of the process. + */ + public abstract String getName(); + + /** + * Starts the process with a callback. + * @param callback the callback to get notified when the process is finished + * or an error occurs during execution + * @throws IOException when the process is already running or failed to + * start + */ + public synchronized void start(final Callback callback) throws IOException { + if (!isStopped()) { + throw new IOException("Process is already running: " + this); + } + mLock.close(); + setState(ProcessState.STARTING); + Thread thread = new Thread(new Runnable() { + public void run() { + try { + performTask(); + setState(ProcessState.STOPPED); + mLock.open(); + if (callback != null) callback.done(ProcessProxy.this); + } catch (Throwable e) { + setState(ProcessState.ERROR); + if (callback != null) callback.error(ProcessProxy.this, e); + } finally { + mThread = null; + } + } + }); + thread.setPriority(Thread.MIN_PRIORITY); + thread.start(); + mThread = thread; + if (!waitUntilRunning()) { + throw new IOException("Failed to start the process: " + this); + } + } + + /** + * Starts the process. + * @throws IOException when the process is already running or failed to + * start + */ + public synchronized void start() throws IOException { + start(null); + if (!waitUntilDone()) { + throw new IOException("Failed to complete the process: " + this); + } + } + + /** + * Returns the thread that hosts the process. + */ + public Thread getHostThread() { + return mThread; + } + + /** + * Blocks the current thread until the hosted process is finished. + * + * @return true if the process is finished normally; false if an error + * occurs + */ + public boolean waitUntilDone() { + while (!mLock.block(1000)) { + if (isStopped() || isInError()) break; + } + return isStopped(); + } + + /** + * Blocks the current thread until the hosted process is running. + * + * @return true if the process is running normally; false if the process + * is in another state + */ + private boolean waitUntilRunning() { + for (;;) { + if (!isStarting()) break; + } + return isRunning(); + } + + /** + * Stops and destroys the process. + */ + public abstract void stop(); + + /** + * Checks whether the process is finished. + * @return true if the process is stopped + */ + public boolean isStopped() { + return (mState == ProcessState.STOPPED); + } + + /** + * Checks whether the process is being stopped. + * @return true if the process is being stopped + */ + public boolean isStopping() { + return (mState == ProcessState.STOPPING); + } + + /** + * Checks whether the process is being started. + * @return true if the process is being started + */ + public boolean isStarting() { + return (mState == ProcessState.STARTING); + } + + /** + * Checks whether the process is running. + * @return true if the process is running + */ + public boolean isRunning() { + return (mState == ProcessState.RUNNING); + } + + /** + * Checks whether some error has occurred and the process is stopped. + * @return true if some error has occurred and the process is stopped + */ + public boolean isInError() { + return (mState == ProcessState.ERROR); + } + + /** + * Performs the actual task. Subclasses must make sure that the method + * is blocked until the task is done or an error occurs. + */ + protected abstract void performTask() + throws IOException, InterruptedException; + + /** + * Sets the process state. + * @param state the new state to be in + */ + protected void setState(ProcessState state) { + mState = state; + } + + /** + * Makes the current thread sleep for the specified time. + * @param msec time to sleep in miliseconds + */ + protected void sleep(int msec) { + try { + Thread.currentThread().sleep(msec); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } +} diff --git a/packages/VpnServices/src/com/android/server/vpn/VpnService.java b/packages/VpnServices/src/com/android/server/vpn/VpnService.java new file mode 100644 index 0000000..a60788a --- /dev/null +++ b/packages/VpnServices/src/com/android/server/vpn/VpnService.java @@ -0,0 +1,520 @@ +/* + * Copyright (C) 2009, 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.server.vpn; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.net.NetworkUtils; +import android.net.vpn.VpnManager; +import android.net.vpn.VpnProfile; +import android.net.vpn.VpnState; +import android.os.SystemProperties; +import android.text.TextUtils; +import android.util.Log; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.Socket; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; + +/** + * The service base class for managing a type of VPN connection. + */ +abstract class VpnService<E extends VpnProfile> { + private static final int NOTIFICATION_ID = 1; + + private static final String DNS1 = "net.dns1"; + private static final String DNS2 = "net.dns2"; + private static final String VPN_DNS1 = "vpn.dns1"; + private static final String VPN_DNS2 = "vpn.dns2"; + private static final String VPN_UP = "vpn.up"; + private static final String VPN_IS_UP = "1"; + private static final String VPN_IS_DOWN = "0"; + + private static final String REMOTE_IP = "net.ipremote"; + private static final String DNS_DOMAIN_SUFFICES = "net.dns.search"; + + private final String TAG = VpnService.class.getSimpleName(); + + E mProfile; + VpnServiceBinder mContext; + + private VpnState mState = VpnState.IDLE; + private boolean mInError; + + // connection settings + private String mOriginalDns1; + private String mOriginalDns2; + private String mVpnDns1 = ""; + private String mVpnDns2 = ""; + private String mOriginalDomainSuffices; + private String mHostIp; + + private long mStartTime; // VPN connection start time + + // for helping managing multiple Android services + private ServiceHelper mServiceHelper = new ServiceHelper(); + + // for helping showing, updating notification + private NotificationHelper mNotification = new NotificationHelper(); + + /** + * Establishes a VPN connection with the specified username and password. + */ + protected abstract void connect(String serverIp, String username, + String password) throws IOException; + + /** + * Tears down the VPN connection. The base class simply terminates all the + * Android services. A subclass may need to do some clean-up before that. + */ + protected void disconnect() { + } + + /** + * Starts an Android service defined in init.rc. + */ + protected AndroidServiceProxy startService(String serviceName) + throws IOException { + return mServiceHelper.startService(serviceName); + } + + /** + * Returns the VPN profile associated with the connection. + */ + protected E getProfile() { + return mProfile; + } + + /** + * Returns the host IP for establishing the VPN connection. + */ + protected String getHostIp() throws IOException { + if (mHostIp == null) mHostIp = reallyGetHostIp(); + return mHostIp; + } + + /** + * Returns the IP address of the specified host name. + */ + protected String getIp(String hostName) throws IOException { + return InetAddress.getByName(hostName).getHostAddress(); + } + + /** + * Returns the IP address of the default gateway. + */ + protected String getGatewayIp() throws IOException { + Enumeration<NetworkInterface> ifces = + NetworkInterface.getNetworkInterfaces(); + for (; ifces.hasMoreElements(); ) { + NetworkInterface ni = ifces.nextElement(); + int gateway = NetworkUtils.getDefaultRoute(ni.getName()); + if (gateway == 0) continue; + return toInetAddress(gateway).getHostAddress(); + } + throw new IOException("Default gateway is not available"); + } + + /** + * Sets the system property. The method is blocked until the value is + * settled in. + * @param name the name of the property + * @param value the value of the property + * @throws IOException if it fails to set the property within 2 seconds + */ + protected void setSystemProperty(String name, String value) + throws IOException { + SystemProperties.set(name, value); + for (int i = 0; i < 5; i++) { + String v = SystemProperties.get(name); + if (v.equals(value)) { + return; + } else { + Log.d(TAG, "sys_prop: wait for " + name + " to settle in"); + sleep(400); + } + } + throw new IOException("Failed to set system property: " + name); + } + + void setContext(VpnServiceBinder context, E profile) { + mContext = context; + mProfile = profile; + } + + VpnState getState() { + return mState; + } + + synchronized void onConnect(String username, String password) + throws IOException { + mState = VpnState.CONNECTING; + broadcastConnectivity(VpnState.CONNECTING); + + String serverIp = getIp(getProfile().getServerName()); + + onBeforeConnect(); + connect(serverIp, username, password); + waitUntilConnectedOrTimedout(); + } + + synchronized void onDisconnect(boolean cleanUpServices) { + try { + mState = VpnState.DISCONNECTING; + broadcastConnectivity(VpnState.DISCONNECTING); + mNotification.showDisconnect(); + + // subclass implementation + if (cleanUpServices) disconnect(); + + mServiceHelper.stop(); + } catch (Throwable e) { + Log.e(TAG, "onError()", e); + onFinalCleanUp(); + } + } + + synchronized void onError() { + // error may occur during or after connection setup + // and it may be due to one or all services gone + mInError = true; + switch (mState) { + case CONNECTED: + onDisconnect(true); + break; + + case CONNECTING: + onDisconnect(false); + break; + } + } + + private void onBeforeConnect() { + mNotification.disableNotification(); + + SystemProperties.set(VPN_DNS1, "-"); + SystemProperties.set(VPN_DNS2, "-"); + SystemProperties.set(VPN_UP, VPN_IS_DOWN); + Log.d(TAG, " VPN UP: " + SystemProperties.get(VPN_UP)); + } + + private void waitUntilConnectedOrTimedout() { + sleep(2000); // 2 seconds + for (int i = 0; i < 60; i++) { + if (VPN_IS_UP.equals(SystemProperties.get(VPN_UP))) { + onConnected(); + return; + } + sleep(500); // 0.5 second + } + + synchronized (this) { + if (mState == VpnState.CONNECTING) { + Log.d(TAG, " connecting timed out !!"); + onError(); + } + } + } + + private synchronized void onConnected() { + Log.d(TAG, "onConnected()"); + + saveVpnDnsProperties(); + saveAndSetDomainSuffices(); + startConnectivityMonitor(); + + mState = VpnState.CONNECTED; + broadcastConnectivity(VpnState.CONNECTED); + } + + private synchronized void onFinalCleanUp() { + Log.d(TAG, "onFinalCleanUp()"); + + if (mState == VpnState.IDLE) return; + + // keep the notification when error occurs + if (!mInError) mNotification.disableNotification(); + + restoreOriginalDnsProperties(); + restoreOriginalDomainSuffices(); + mState = VpnState.IDLE; + broadcastConnectivity(VpnState.IDLE); + + // stop the service itself + mContext.stopSelf(); + } + + private synchronized void onOneServiceGone() { + switch (mState) { + case IDLE: + case DISCONNECTING: + break; + + default: + onError(); + } + } + + private synchronized void onAllServicesGone() { + switch (mState) { + case IDLE: + break; + + case DISCONNECTING: + // daemons are gone; now clean up everything + onFinalCleanUp(); + break; + + default: + onError(); + } + } + + private void restoreOriginalDnsProperties() { + // restore only if they are not overridden + if (mVpnDns1.equals(SystemProperties.get(DNS1))) { + Log.d(TAG, String.format("restore original dns prop: %s --> %s", + SystemProperties.get(DNS1), mOriginalDns1)); + Log.d(TAG, String.format("restore original dns prop: %s --> %s", + SystemProperties.get(DNS2), mOriginalDns2)); + SystemProperties.set(DNS1, mOriginalDns1); + SystemProperties.set(DNS2, mOriginalDns2); + } + } + + private void saveVpnDnsProperties() { + mOriginalDns1 = mOriginalDns2 = ""; + for (int i = 0; i < 10; i++) { + mVpnDns1 = SystemProperties.get(VPN_DNS1); + mVpnDns2 = SystemProperties.get(VPN_DNS2); + if (mOriginalDns1.equals(mVpnDns1)) { + Log.d(TAG, "wait for vpn dns to settle in..." + i); + sleep(500); + } else { + mOriginalDns1 = SystemProperties.get(DNS1); + mOriginalDns2 = SystemProperties.get(DNS2); + SystemProperties.set(DNS1, mVpnDns1); + SystemProperties.set(DNS2, mVpnDns2); + Log.d(TAG, String.format("save original dns prop: %s, %s", + mOriginalDns1, mOriginalDns2)); + Log.d(TAG, String.format("set vpn dns prop: %s, %s", + mVpnDns1, mVpnDns2)); + return; + } + } + Log.e(TAG, "saveVpnDnsProperties(): DNS not updated??"); + } + + private void saveAndSetDomainSuffices() { + mOriginalDomainSuffices = SystemProperties.get(DNS_DOMAIN_SUFFICES); + Log.d(TAG, "save original dns search: " + mOriginalDomainSuffices); + String list = mProfile.getDomainSuffices(); + if (!TextUtils.isEmpty(list)) { + SystemProperties.set(DNS_DOMAIN_SUFFICES, list); + } + } + + private void restoreOriginalDomainSuffices() { + Log.d(TAG, "restore original dns search --> " + mOriginalDomainSuffices); + SystemProperties.set(DNS_DOMAIN_SUFFICES, mOriginalDomainSuffices); + } + + private void broadcastConnectivity(VpnState s) { + new VpnManager(mContext).broadcastConnectivity(mProfile.getName(), s); + } + + private void startConnectivityMonitor() { + mStartTime = System.currentTimeMillis(); + + new Thread(new Runnable() { + public void run() { + Log.d(TAG, " +++++ connectivity monitor running"); + try { + for (;;) { + synchronized (VpnService.this) { + if (mState != VpnState.CONNECTED) break; + mNotification.update(); + checkConnectivity(); + VpnService.this.wait(1000); // 1 second + } + } + } catch (InterruptedException e) { + Log.e(TAG, "connectivity monitor", e); + } + Log.d(TAG, " ----- connectivity monitor stopped"); + } + }).start(); + } + + private void checkConnectivity() { + checkDnsProperties(); + } + + private void checkDnsProperties() { + String dns1 = SystemProperties.get(DNS1); + if (!mVpnDns1.equals(dns1)) { + Log.w(TAG, " @@ !!! dns being overridden"); + onError(); + } + } + + private String reallyGetHostIp() throws IOException { + Enumeration<NetworkInterface> ifces = + NetworkInterface.getNetworkInterfaces(); + for (; ifces.hasMoreElements(); ) { + NetworkInterface ni = ifces.nextElement(); + int gateway = NetworkUtils.getDefaultRoute(ni.getName()); + if (gateway == 0) continue; + Enumeration<InetAddress> addrs = ni.getInetAddresses(); + for (; addrs.hasMoreElements(); ) { + return addrs.nextElement().getHostAddress(); + } + } + throw new IOException("Host IP is not available"); + } + + protected void sleep(int ms) { + try { + Thread.currentThread().sleep(ms); + } catch (InterruptedException e) { + } + } + + private InetAddress toInetAddress(int addr) throws IOException { + byte[] aa = new byte[4]; + for (int i= 0; i < aa.length; i++) { + aa[i] = (byte) (addr & 0x0FF); + addr >>= 8; + } + return InetAddress.getByAddress(aa); + } + + private class ServiceHelper implements ProcessProxy.Callback { + private List<AndroidServiceProxy> mServiceList = + new ArrayList<AndroidServiceProxy>(); + + // starts an Android service + AndroidServiceProxy startService(String serviceName) + throws IOException { + AndroidServiceProxy service = new AndroidServiceProxy(serviceName); + mServiceList.add(service); + service.start(this); + return service; + } + + // stops all the Android services + void stop() { + if (mServiceList.isEmpty()) { + onFinalCleanUp(); + } else { + for (AndroidServiceProxy s : mServiceList) s.stop(); + } + } + + //@Override + public void done(ProcessProxy p) { + Log.d(TAG, "service done: " + p.getName()); + commonCallback((AndroidServiceProxy) p); + } + + //@Override + public void error(ProcessProxy p, Throwable e) { + Log.e(TAG, "service error: " + p.getName(), e); + commonCallback((AndroidServiceProxy) p); + } + + private void commonCallback(AndroidServiceProxy service) { + mServiceList.remove(service); + onOneServiceGone(); + if (mServiceList.isEmpty()) onAllServicesGone(); + } + } + + // Helper class for showing, updating notification. + private class NotificationHelper { + void update() { + String title = getNotificationTitle(true); + Notification n = new Notification(R.drawable.vpn_connected, title, + mStartTime); + n.setLatestEventInfo(mContext, title, + getNotificationMessage(true), prepareNotificationIntent()); + n.flags |= Notification.FLAG_NO_CLEAR; + n.flags |= Notification.FLAG_ONGOING_EVENT; + enableNotification(n); + } + + void showDisconnect() { + String title = getNotificationTitle(false); + Notification n = new Notification(R.drawable.vpn_disconnected, + title, System.currentTimeMillis()); + n.setLatestEventInfo(mContext, title, + getNotificationMessage(false), prepareNotificationIntent()); + n.flags |= Notification.FLAG_AUTO_CANCEL; + disableNotification(); + enableNotification(n); + } + + void disableNotification() { + ((NotificationManager) mContext.getSystemService( + Context.NOTIFICATION_SERVICE)).cancel(NOTIFICATION_ID); + } + + private void enableNotification(Notification n) { + ((NotificationManager) mContext.getSystemService( + Context.NOTIFICATION_SERVICE)).notify(NOTIFICATION_ID, n); + } + + private PendingIntent prepareNotificationIntent() { + return PendingIntent.getActivity(mContext, 0, + new VpnManager(mContext).createSettingsActivityIntent(), 0); + } + + private String getNotificationTitle(boolean connected) { + String formatString = connected + ? mContext.getString( + R.string.vpn_notification_title_connected) + : mContext.getString( + R.string.vpn_notification_title_disconnected); + return String.format(formatString, mProfile.getName()); + } + + private String getFormattedTime(long duration) { + long hours = duration / 3600; + StringBuilder sb = new StringBuilder(); + if (hours > 0) sb.append(hours).append(':'); + sb.append(String.format("%02d:%02d", (duration % 3600 / 60), + (duration % 60))); + return sb.toString(); + } + + private String getNotificationMessage(boolean connected) { + if (connected) { + long time = (System.currentTimeMillis() - mStartTime) / 1000; + return getFormattedTime(time); + } else { + return mContext.getString( + R.string.vpn_notification_hint_disconnected); + } + } + } +} diff --git a/packages/VpnServices/src/com/android/server/vpn/VpnServiceBinder.java b/packages/VpnServices/src/com/android/server/vpn/VpnServiceBinder.java new file mode 100644 index 0000000..617875e --- /dev/null +++ b/packages/VpnServices/src/com/android/server/vpn/VpnServiceBinder.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2009, 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.server.vpn; + +import android.app.Service; +import android.content.Intent; +import android.net.vpn.IVpnService; +import android.net.vpn.L2tpIpsecProfile; +import android.net.vpn.L2tpIpsecPskProfile; +import android.net.vpn.L2tpProfile; +import android.net.vpn.PptpProfile; +import android.net.vpn.VpnManager; +import android.net.vpn.VpnProfile; +import android.net.vpn.VpnState; +import android.os.IBinder; +import android.util.Log; + +import java.io.IOException; + +/** + * The service class for managing a VPN connection. It implements the + * {@link IVpnService} binder interface. + */ +public class VpnServiceBinder extends Service { + private final String TAG = VpnServiceBinder.class.getSimpleName(); + + // The actual implementation is delegated to the VpnService class. + private VpnService<? extends VpnProfile> mService; + + private final IBinder mBinder = new IVpnService.Stub() { + public boolean connect(VpnProfile p, String username, String password) { + return VpnServiceBinder.this.connect(p, username, password); + } + + public void disconnect() { + if (mService != null) mService.onDisconnect(true); + } + + public void checkStatus(VpnProfile p) { + VpnServiceBinder.this.checkStatus(p); + } + }; + + public IBinder onBind(Intent intent) { + return mBinder; + } + + private synchronized boolean connect( + VpnProfile p, String username, String password) { + if (mService != null) return false; + try { + mService = createService(p); + mService.onConnect(username, password); + return true; + } catch (Throwable e) { + Log.e(TAG, "connect()", e); + if (mService != null) mService.onError(); + return false; + } + } + + private synchronized void checkStatus(VpnProfile p) { + if (mService == null) broadcastConnectivity(p.getName(), VpnState.IDLE); + + if (!p.getName().equals(mService.mProfile.getName())) { + broadcastConnectivity(p.getName(), VpnState.IDLE); + } else { + broadcastConnectivity(p.getName(), mService.getState()); + } + } + + private VpnService<? extends VpnProfile> createService(VpnProfile p) { + switch (p.getType()) { + case L2TP: + L2tpService l2tp = new L2tpService(); + l2tp.setContext(this, (L2tpProfile) p); + return l2tp; + + case PPTP: + PptpService pptp = new PptpService(); + pptp.setContext(this, (PptpProfile) p); + return pptp; + + case L2TP_IPSEC_PSK: + L2tpIpsecPskService psk = new L2tpIpsecPskService(); + psk.setContext(this, (L2tpIpsecPskProfile) p); + return psk; + + case L2TP_IPSEC: + L2tpIpsecService l2tpIpsec = new L2tpIpsecService(); + l2tpIpsec.setContext(this, (L2tpIpsecProfile) p); + return l2tpIpsec; + + default: + return null; + } + } + + private void broadcastConnectivity(String name, VpnState s) { + new VpnManager(this).broadcastConnectivity(name, s); + } +} |