summaryrefslogtreecommitdiffstats
path: root/packages
diff options
context:
space:
mode:
authorJean-Baptiste Queru <jbq@google.com>2009-07-25 19:52:22 -0700
committerJean-Baptiste Queru <jbq@google.com>2009-07-25 21:15:25 -0700
commit2af1b3db3d4f687d008db74b150f149e956b4bc6 (patch)
tree39d7d5bf15667c01f9b6dfe02bdd0e7fa36cd303 /packages
parent8ecb36eec61f119f500a805b82438aadb3396a19 (diff)
parentcf4550c3198d6b3d92cdc52707fe70d7cc0caa9f (diff)
downloadframeworks_base-2af1b3db3d4f687d008db74b150f149e956b4bc6.zip
frameworks_base-2af1b3db3d4f687d008db74b150f149e956b4bc6.tar.gz
frameworks_base-2af1b3db3d4f687d008db74b150f149e956b4bc6.tar.bz2
Merge korg/donut into korg/master
Diffstat (limited to 'packages')
-rw-r--r--packages/SettingsProvider/AndroidManifest.xml12
-rw-r--r--packages/SettingsProvider/res/values/defaults.xml10
-rw-r--r--packages/SettingsProvider/res/values/strings.xml5
-rw-r--r--packages/SettingsProvider/src/com/android/providers/settings/DatabaseHelper.java159
-rw-r--r--packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java332
-rw-r--r--packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java198
-rw-r--r--packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java96
-rw-r--r--packages/SubscribedFeedsProvider/AndroidManifest.xml2
-rw-r--r--packages/SubscribedFeedsProvider/res/values/strings.xml21
-rw-r--r--packages/SubscribedFeedsProvider/src/com/android/providers/subscribedfeeds/SubscribedFeedsIntentService.java19
-rw-r--r--packages/TtsService/Android.mk13
-rwxr-xr-xpackages/TtsService/AndroidManifest.xml16
-rw-r--r--packages/TtsService/MODULE_LICENSE_APACHE20
-rw-r--r--packages/TtsService/NOTICE190
-rwxr-xr-xpackages/TtsService/jni/Android.mk31
-rw-r--r--packages/TtsService/jni/android_tts_SynthProxy.cpp778
-rwxr-xr-xpackages/TtsService/src/android/tts/SynthProxy.java200
-rwxr-xr-xpackages/TtsService/src/android/tts/TtsService.java936
-rw-r--r--packages/VpnServices/Android.mk16
-rw-r--r--packages/VpnServices/AndroidManifest.xml22
-rw-r--r--packages/VpnServices/MODULE_LICENSE_APACHE20
-rw-r--r--packages/VpnServices/NOTICE190
-rw-r--r--packages/VpnServices/res/drawable/vpn_connected.pngbin0 -> 757 bytes
-rw-r--r--packages/VpnServices/res/drawable/vpn_disconnected.pngbin0 -> 717 bytes
-rwxr-xr-xpackages/VpnServices/res/values/strings.xml10
-rw-r--r--packages/VpnServices/src/com/android/server/vpn/AndroidServiceProxy.java243
-rw-r--r--packages/VpnServices/src/com/android/server/vpn/L2tpIpsecPskService.java49
-rw-r--r--packages/VpnServices/src/com/android/server/vpn/L2tpIpsecService.java64
-rw-r--r--packages/VpnServices/src/com/android/server/vpn/L2tpService.java38
-rw-r--r--packages/VpnServices/src/com/android/server/vpn/MtpdHelper.java60
-rw-r--r--packages/VpnServices/src/com/android/server/vpn/NormalProcessProxy.java85
-rw-r--r--packages/VpnServices/src/com/android/server/vpn/PptpService.java36
-rw-r--r--packages/VpnServices/src/com/android/server/vpn/ProcessProxy.java210
-rw-r--r--packages/VpnServices/src/com/android/server/vpn/VpnService.java520
-rw-r--r--packages/VpnServices/src/com/android/server/vpn/VpnServiceBinder.java116
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
new file mode 100644
index 0000000..65fc6db
--- /dev/null
+++ b/packages/VpnServices/res/drawable/vpn_connected.png
Binary files differ
diff --git a/packages/VpnServices/res/drawable/vpn_disconnected.png b/packages/VpnServices/res/drawable/vpn_disconnected.png
new file mode 100644
index 0000000..2440c69
--- /dev/null
+++ b/packages/VpnServices/res/drawable/vpn_disconnected.png
Binary files differ
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);
+ }
+}