diff options
Diffstat (limited to 'core/java')
282 files changed, 14595 insertions, 23813 deletions
diff --git a/core/java/android/accounts/AccountManagerService.java b/core/java/android/accounts/AccountManagerService.java index 440668f..d5a9b02 100644 --- a/core/java/android/accounts/AccountManagerService.java +++ b/core/java/android/accounts/AccountManagerService.java @@ -16,39 +16,44 @@ package android.accounts; +import android.Manifest; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; import android.content.BroadcastReceiver; +import android.content.ComponentName; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.ServiceConnection; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.RegisteredServicesCache; -import android.content.pm.PackageInfo; -import android.content.pm.ApplicationInfo; import android.content.pm.RegisteredServicesCacheListener; import android.database.Cursor; import android.database.DatabaseUtils; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; +import android.os.Binder; import android.os.Bundle; +import android.os.Environment; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.os.RemoteException; +import android.os.ServiceManager; import android.os.SystemClock; -import android.os.Binder; import android.os.SystemProperties; import android.telephony.TelephonyManager; import android.text.TextUtils; import android.util.Log; import android.util.Pair; -import android.app.PendingIntent; -import android.app.NotificationManager; -import android.app.Notification; -import android.Manifest; +import java.io.File; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; @@ -58,8 +63,9 @@ import java.util.HashMap; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; -import com.android.internal.telephony.TelephonyIntents; import com.android.internal.R; +import com.android.internal.telephony.ITelephony; +import com.android.internal.telephony.TelephonyIntents; /** * A system service that provides account, password, and authtoken management for all @@ -90,11 +96,8 @@ public class AccountManagerService // Messages that can be sent on mHandler private static final int MESSAGE_TIMED_OUT = 3; - private static final int MESSAGE_CONNECTED = 7; - private static final int MESSAGE_DISCONNECTED = 8; private final AccountAuthenticatorCache mAuthenticatorCache; - private final AuthenticatorBindHelper mBindHelper; private final DatabaseHelper mOpenHelper; private final SimWatcher mSimWatcher; @@ -220,11 +223,38 @@ public class AccountManagerService mAuthenticatorCache = new AccountAuthenticatorCache(mContext); mAuthenticatorCache.setListener(this, null /* Handler */); - mBindHelper = new AuthenticatorBindHelper(mContext, mAuthenticatorCache, mMessageHandler, - MESSAGE_CONNECTED, MESSAGE_DISCONNECTED); mSimWatcher = new SimWatcher(mContext); sThis.set(this); + + validateAccounts(); + } + + private void validateAccounts() { + boolean accountDeleted = false; + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + Cursor cursor = db.query(TABLE_ACCOUNTS, + new String[]{ACCOUNTS_ID, ACCOUNTS_TYPE, ACCOUNTS_NAME}, + null, null, null, null, null); + try { + while (cursor.moveToNext()) { + final long accountId = cursor.getLong(0); + final String accountType = cursor.getString(1); + final String accountName = cursor.getString(2); + if (mAuthenticatorCache.getServiceInfo(AuthenticatorDescription.newKey(accountType)) + == null) { + Log.d(TAG, "deleting account " + accountName + " because type " + + accountType + " no longer has a registered authenticator"); + db.delete(TABLE_ACCOUNTS, ACCOUNTS_ID + "=" + accountId, null); + accountDeleted = true; + } + } + } finally { + cursor.close(); + if (accountDeleted) { + sendAccountsChangedBroadcast(); + } + } } public void onServiceChanged(AuthenticatorDescription desc, boolean removed) { @@ -1075,7 +1105,7 @@ public class AccountManagerService } private abstract class Session extends IAccountAuthenticatorResponse.Stub - implements AuthenticatorBindHelper.Callback, IBinder.DeathRecipient { + implements IBinder.DeathRecipient, ServiceConnection { IAccountManagerResponse mResponse; final String mAccountType; final boolean mExpectActivityLaunch; @@ -1157,7 +1187,7 @@ public class AccountManagerService if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "initiating bind to authenticator type " + mAccountType); } - if (!mBindHelper.bind(mAccountType, this)) { + if (!bindToAuthenticator(mAccountType)) { Log.d(TAG, "bind attempt failed for " + toDebugString()); onError(AccountManager.ERROR_CODE_REMOTE_EXCEPTION, "bind failure"); } @@ -1166,7 +1196,7 @@ public class AccountManagerService private void unbind() { if (mAuthenticator != null) { mAuthenticator = null; - mBindHelper.unbind(this); + mContext.unbindService(this); } } @@ -1179,7 +1209,7 @@ public class AccountManagerService mMessageHandler.removeMessages(MESSAGE_TIMED_OUT, this); } - public void onConnected(IBinder service) { + public void onServiceConnected(ComponentName name, IBinder service) { mAuthenticator = IAccountAuthenticator.Stub.asInterface(service); try { run(); @@ -1189,9 +1219,7 @@ public class AccountManagerService } } - public abstract void run() throws RemoteException; - - public void onDisconnected() { + public void onServiceDisconnected(ComponentName name) { mAuthenticator = null; IAccountManagerResponse response = getResponseAndClose(); if (response != null) { @@ -1200,6 +1228,8 @@ public class AccountManagerService } } + public abstract void run() throws RemoteException; + public void onTimedOut() { IAccountManagerResponse response = getResponseAndClose(); if (response != null) { @@ -1269,6 +1299,39 @@ public class AccountManagerService } } } + + /** + * find the component name for the authenticator and initiate a bind + * if no authenticator or the bind fails then return false, otherwise return true + */ + private boolean bindToAuthenticator(String authenticatorType) { + AccountAuthenticatorCache.ServiceInfo<AuthenticatorDescription> authenticatorInfo = + mAuthenticatorCache.getServiceInfo( + AuthenticatorDescription.newKey(authenticatorType)); + if (authenticatorInfo == null) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "there is no authenticator for " + authenticatorType + + ", bailing out"); + } + return false; + } + + Intent intent = new Intent(); + intent.setAction(AccountManager.ACTION_AUTHENTICATOR_INTENT); + intent.setComponent(authenticatorInfo.componentName); + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "performing bindService to " + authenticatorInfo.componentName); + } + if (!mContext.bindService(intent, this, Context.BIND_AUTO_CREATE)) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "bindService to " + authenticatorInfo.componentName + " failed"); + } + return false; + } + + + return true; + } } private class MessageHandler extends Handler { @@ -1277,9 +1340,6 @@ public class AccountManagerService } public void handleMessage(Message msg) { - if (mBindHelper.handleMessage(msg)) { - return; - } switch (msg.what) { case MESSAGE_TIMED_OUT: Session session = (Session)msg.obj; @@ -1292,9 +1352,20 @@ public class AccountManagerService } } + private static String getDatabaseName() { + if(Environment.isEncryptedFilesystemEnabled()) { + // Hard-coded path in case of encrypted file system + return Environment.getSystemSecureDirectory().getPath() + File.separator + DATABASE_NAME; + } else { + // Regular path in case of non-encrypted file system + return DATABASE_NAME; + } + } + private class DatabaseHelper extends SQLiteOpenHelper { + public DatabaseHelper(Context context) { - super(context, DATABASE_NAME, null, DATABASE_VERSION); + super(context, AccountManagerService.getDatabaseName(), null, DATABASE_VERSION); } @Override @@ -1419,16 +1490,58 @@ public class AccountManagerService */ @Override public void onReceive(Context context, Intent intent) { - // Check IMSI on every update; nothing happens if the IMSI is missing or unchanged. - String imsi = ((TelephonyManager) context.getSystemService( - Context.TELEPHONY_SERVICE)).getSubscriberId(); + // Check IMSI on every update; nothing happens if the IMSI + // is missing or unchanged. + TelephonyManager telephonyManager = + (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + if (telephonyManager == null) { + Log.w(TAG, "failed to get TelephonyManager"); + return; + } + String imsi = telephonyManager.getSubscriberId(); + + // If the subscriber ID is an empty string, don't do anything. if (TextUtils.isEmpty(imsi)) return; + // If the current IMSI matches what's stored, don't do anything. String storedImsi = getMetaValue("imsi"); - if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "current IMSI=" + imsi + "; stored IMSI=" + storedImsi); } + if (imsi.equals(storedImsi)) return; + + // If a CDMA phone is unprovisioned, getSubscriberId() + // will return a different value, but we *don't* erase the + // passwords. We only erase them if it has a different + // subscriber ID once it's provisioned. + if (telephonyManager.getPhoneType() == TelephonyManager.PHONE_TYPE_CDMA) { + IBinder service = ServiceManager.checkService(Context.TELEPHONY_SERVICE); + if (service == null) { + Log.w(TAG, "call to checkService(TELEPHONY_SERVICE) failed"); + return; + } + ITelephony telephony = ITelephony.Stub.asInterface(service); + if (telephony == null) { + Log.w(TAG, "failed to get ITelephony interface"); + return; + } + boolean needsProvisioning; + try { + needsProvisioning = telephony.getCdmaNeedsProvisioning(); + } catch (RemoteException e) { + Log.w(TAG, "exception while checking provisioning", e); + // default to NOT wiping out the passwords + needsProvisioning = true; + } + if (needsProvisioning) { + // if the phone needs re-provisioning, don't do anything. + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "current IMSI=" + imsi + " (needs provisioning); stored IMSI=" + + storedImsi); + } + return; + } + } if (!imsi.equals(storedImsi) && !TextUtils.isEmpty(storedImsi)) { Log.w(TAG, "wiping all passwords and authtokens because IMSI changed (" @@ -1572,6 +1685,7 @@ public class AccountManagerService } private boolean permissionIsGranted(Account account, String authTokenType, int callerUid) { + final boolean inSystemImage = inSystemImage(callerUid); final boolean fromAuthenticator = account != null && hasAuthenticatorUid(account.type, callerUid); final boolean hasExplicitGrants = account != null @@ -1582,7 +1696,7 @@ public class AccountManagerService + ": is authenticator? " + fromAuthenticator + ", has explicit permission? " + hasExplicitGrants); } - return fromAuthenticator || hasExplicitGrants || inSystemImage(callerUid); + return fromAuthenticator || hasExplicitGrants || inSystemImage; } private boolean hasAuthenticatorUid(String accountType, int callingUid) { diff --git a/core/java/android/accounts/AuthenticatorBindHelper.java b/core/java/android/accounts/AuthenticatorBindHelper.java deleted file mode 100644 index 2ca1f0e..0000000 --- a/core/java/android/accounts/AuthenticatorBindHelper.java +++ /dev/null @@ -1,258 +0,0 @@ -/* - * 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 android.accounts; - -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.ServiceConnection; -import android.os.Handler; -import android.os.IBinder; -import android.os.Message; -import android.util.Log; - -import java.util.ArrayList; -import java.util.Map; - -import com.google.android.collect.Lists; -import com.google.android.collect.Maps; - -/** - * A helper object that simplifies binding to Account Authenticators. It uses the - * {@link AccountAuthenticatorCache} to find the component name of the authenticators, - * allowing the user to bind by account name. It also allows multiple, simultaneous binds - * to the same authenticator, with each bind call guaranteed to return either - * {@link Callback#onConnected} or {@link Callback#onDisconnected} if the bind() call - * itself succeeds, even if the authenticator is already bound internally. - * @hide - */ -public class AuthenticatorBindHelper { - private static final String TAG = "Accounts"; - private final Handler mHandler; - private final Context mContext; - private final int mMessageWhatConnected; - private final int mMessageWhatDisconnected; - private final Map<String, MyServiceConnection> mServiceConnections = Maps.newHashMap(); - private final Map<String, ArrayList<Callback>> mServiceUsers = Maps.newHashMap(); - private final AccountAuthenticatorCache mAuthenticatorCache; - - public AuthenticatorBindHelper(Context context, - AccountAuthenticatorCache authenticatorCache, Handler handler, - int messageWhatConnected, int messageWhatDisconnected) { - mContext = context; - mHandler = handler; - mAuthenticatorCache = authenticatorCache; - mMessageWhatConnected = messageWhatConnected; - mMessageWhatDisconnected = messageWhatDisconnected; - } - - public interface Callback { - void onConnected(IBinder service); - void onDisconnected(); - } - - public boolean bind(String authenticatorType, Callback callback) { - // if the authenticator is connecting or connected then return true - synchronized (mServiceConnections) { - if (mServiceConnections.containsKey(authenticatorType)) { - MyServiceConnection connection = mServiceConnections.get(authenticatorType); - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "service connection already exists for " + authenticatorType); - } - mServiceUsers.get(authenticatorType).add(callback); - if (connection.mService != null) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "the service is connected, scheduling a connected message for " - + authenticatorType); - } - connection.scheduleCallbackConnectedMessage(callback); - } else { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "the service is *not* connected, waiting for for " - + authenticatorType); - } - } - return true; - } - - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "there is no service connection for " + authenticatorType); - } - - // otherwise find the component name for the authenticator and initiate a bind - // if no authenticator or the bind fails then return false, otherwise return true - AccountAuthenticatorCache.ServiceInfo<AuthenticatorDescription> authenticatorInfo = - mAuthenticatorCache.getServiceInfo( - AuthenticatorDescription.newKey(authenticatorType)); - if (authenticatorInfo == null) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "there is no authenticator for " + authenticatorType - + ", bailing out"); - } - return false; - } - - MyServiceConnection connection = new MyServiceConnection(authenticatorType); - - Intent intent = new Intent(); - intent.setAction("android.accounts.AccountAuthenticator"); - intent.setComponent(authenticatorInfo.componentName); - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "performing bindService to " + authenticatorInfo.componentName); - } - if (!mContext.bindService(intent, connection, Context.BIND_AUTO_CREATE)) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "bindService to " + authenticatorInfo.componentName + " failed"); - } - return false; - } - - mServiceConnections.put(authenticatorType, connection); - mServiceUsers.put(authenticatorType, Lists.newArrayList(callback)); - return true; - } - } - - public void unbind(Callback callbackToUnbind) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "unbinding callback " + callbackToUnbind); - } - synchronized (mServiceConnections) { - for (Map.Entry<String, ArrayList<Callback>> entry : mServiceUsers.entrySet()) { - final String authenticatorType = entry.getKey(); - final ArrayList<Callback> serviceUsers = entry.getValue(); - for (Callback callback : serviceUsers) { - if (callback == callbackToUnbind) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "found callback in service" + authenticatorType); - } - serviceUsers.remove(callbackToUnbind); - if (serviceUsers.isEmpty()) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "there are no more callbacks for service " - + authenticatorType + ", unbinding service"); - } - unbindFromServiceLocked(authenticatorType); - } else { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "leaving service " + authenticatorType - + " around since there are still callbacks using it"); - } - } - return; - } - } - } - Log.e(TAG, "did not find callback " + callbackToUnbind + " in any of the services"); - } - } - - /** - * You must synchronized on mServiceConnections before calling this - */ - private void unbindFromServiceLocked(String authenticatorType) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "unbindService from " + authenticatorType); - } - mContext.unbindService(mServiceConnections.get(authenticatorType)); - mServiceUsers.remove(authenticatorType); - mServiceConnections.remove(authenticatorType); - } - - private class ConnectedMessagePayload { - public final IBinder mService; - public final Callback mCallback; - public ConnectedMessagePayload(IBinder service, Callback callback) { - mService = service; - mCallback = callback; - } - } - - private class MyServiceConnection implements ServiceConnection { - private final String mAuthenticatorType; - private IBinder mService = null; - - public MyServiceConnection(String authenticatorType) { - mAuthenticatorType = authenticatorType; - } - - public void onServiceConnected(ComponentName name, IBinder service) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "onServiceConnected for account type " + mAuthenticatorType); - } - // post a message for each service user to tell them that the service is connected - synchronized (mServiceConnections) { - mService = service; - for (Callback callback : mServiceUsers.get(mAuthenticatorType)) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "the service became connected, scheduling a connected " - + "message for " + mAuthenticatorType); - } - scheduleCallbackConnectedMessage(callback); - } - } - } - - private void scheduleCallbackConnectedMessage(Callback callback) { - final ConnectedMessagePayload payload = - new ConnectedMessagePayload(mService, callback); - mHandler.obtainMessage(mMessageWhatConnected, payload).sendToTarget(); - } - - public void onServiceDisconnected(ComponentName name) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "onServiceDisconnected for account type " + mAuthenticatorType); - } - // post a message for each service user to tell them that the service is disconnected, - // and unbind from the service. - synchronized (mServiceConnections) { - final ArrayList<Callback> callbackList = mServiceUsers.get(mAuthenticatorType); - if (callbackList != null) { - for (Callback callback : callbackList) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "the service became disconnected, scheduling a " - + "disconnected message for " - + mAuthenticatorType); - } - mHandler.obtainMessage(mMessageWhatDisconnected, callback).sendToTarget(); - } - unbindFromServiceLocked(mAuthenticatorType); - } - } - } - } - - boolean handleMessage(Message message) { - if (message.what == mMessageWhatConnected) { - ConnectedMessagePayload payload = (ConnectedMessagePayload)message.obj; - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "notifying callback " + payload.mCallback + " that it is connected"); - } - payload.mCallback.onConnected(payload.mService); - return true; - } else if (message.what == mMessageWhatDisconnected) { - Callback callback = (Callback)message.obj; - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "notifying callback " + callback + " that it is disconnected"); - } - callback.onDisconnected(); - return true; - } else { - return false; - } - } -} diff --git a/core/java/android/accounts/GrantCredentialsPermissionActivity.java b/core/java/android/accounts/GrantCredentialsPermissionActivity.java index 4282c1b..f4b7258 100644 --- a/core/java/android/accounts/GrantCredentialsPermissionActivity.java +++ b/core/java/android/accounts/GrantCredentialsPermissionActivity.java @@ -18,15 +18,16 @@ package android.accounts; import android.app.Activity; import android.os.Bundle; import android.widget.TextView; -import android.widget.ArrayAdapter; -import android.widget.ListView; +import android.widget.LinearLayout; +import android.widget.ImageView; import android.view.View; import android.view.LayoutInflater; -import android.view.ViewGroup; +import android.view.Window; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.text.TextUtils; +import android.graphics.drawable.Drawable; import com.android.internal.R; /** @@ -44,62 +45,68 @@ public class GrantCredentialsPermissionActivity extends Activity implements View private String mAuthTokenType; private int mUid; private Bundle mResultBundle = null; + protected LayoutInflater mInflater; protected void onCreate(Bundle savedInstanceState) { + requestWindowFeature(Window.FEATURE_NO_TITLE); super.onCreate(savedInstanceState); - getWindow().setContentView(R.layout.grant_credentials_permission); - mAccount = getIntent().getExtras().getParcelable(EXTRAS_ACCOUNT); - mAuthTokenType = getIntent().getExtras().getString(EXTRAS_AUTH_TOKEN_TYPE); - mUid = getIntent().getExtras().getInt(EXTRAS_REQUESTING_UID); - final String accountTypeLabel = - getIntent().getExtras().getString(EXTRAS_ACCOUNT_TYPE_LABEL); - final String[] packages = getIntent().getExtras().getStringArray(EXTRAS_PACKAGES); + setContentView(R.layout.grant_credentials_permission); - findViewById(R.id.allow).setOnClickListener(this); - findViewById(R.id.deny).setOnClickListener(this); + mInflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); - TextView messageView = (TextView) getWindow().findViewById(R.id.message); - String authTokenLabel = getIntent().getExtras().getString(EXTRAS_AUTH_TOKEN_LABEL); - if (TextUtils.isEmpty(authTokenLabel)) { - CharSequence grantCredentialsPermissionFormat = getResources().getText( - R.string.grant_credentials_permission_message_desc); - messageView.setText(String.format(grantCredentialsPermissionFormat.toString(), - mAccount.name, accountTypeLabel)); - } else { - CharSequence grantCredentialsPermissionFormat = getResources().getText( - R.string.grant_credentials_permission_message_with_authtokenlabel_desc); - messageView.setText(String.format(grantCredentialsPermissionFormat.toString(), - authTokenLabel, mAccount.name, accountTypeLabel)); - } + final Bundle extras = getIntent().getExtras(); + mAccount = extras.getParcelable(EXTRAS_ACCOUNT); + mAuthTokenType = extras.getString(EXTRAS_AUTH_TOKEN_TYPE); + mUid = extras.getInt(EXTRAS_REQUESTING_UID); + final String accountTypeLabel = extras.getString(EXTRAS_ACCOUNT_TYPE_LABEL); + final String[] packages = extras.getStringArray(EXTRAS_PACKAGES); + final String authTokenLabel = extras.getString(EXTRAS_AUTH_TOKEN_LABEL); + + findViewById(R.id.allow_button).setOnClickListener(this); + findViewById(R.id.deny_button).setOnClickListener(this); + + LinearLayout packagesListView = (LinearLayout) findViewById(R.id.packages_list); - String[] packageLabels = new String[packages.length]; final PackageManager pm = getPackageManager(); - for (int i = 0; i < packages.length; i++) { + for (String pkg : packages) { + String packageLabel; try { - packageLabels[i] = - pm.getApplicationLabel(pm.getApplicationInfo(packages[i], 0)).toString(); + packageLabel = pm.getApplicationLabel(pm.getApplicationInfo(pkg, 0)).toString(); } catch (PackageManager.NameNotFoundException e) { - packageLabels[i] = packages[i]; + packageLabel = pkg; } + packagesListView.addView(newPackageView(packageLabel)); } - ((ListView) findViewById(R.id.packages_list)).setAdapter( - new PackagesArrayAdapter(this, packageLabels)); + + ((TextView) findViewById(R.id.account_name)).setText(mAccount.name); + ((TextView) findViewById(R.id.account_type)).setText(accountTypeLabel); + TextView authTokenTypeView = (TextView) findViewById(R.id.authtoken_type); + if (TextUtils.isEmpty(authTokenLabel)) { + authTokenTypeView.setVisibility(View.GONE); + } else { + authTokenTypeView.setText(authTokenLabel); + } + } + + private View newPackageView(String packageLabel) { + View view = mInflater.inflate(R.layout.permissions_package_list_item, null); + ((TextView) view.findViewById(R.id.package_label)).setText(packageLabel); + return view; } public void onClick(View v) { + final AccountManagerService accountManagerService = AccountManagerService.getSingleton(); switch (v.getId()) { - case R.id.allow: - AccountManagerService.getSingleton().grantAppPermission(mAccount, mAuthTokenType, - mUid); + case R.id.allow_button: + accountManagerService.grantAppPermission(mAccount, mAuthTokenType, mUid); Intent result = new Intent(); result.putExtra("retry", true); setResult(RESULT_OK, result); setAccountAuthenticatorResult(result.getExtras()); break; - case R.id.deny: - AccountManagerService.getSingleton().revokeAppPermission(mAccount, mAuthTokenType, - mUid); + case R.id.deny_button: + accountManagerService.revokeAppPermission(mAccount, mAuthTokenType, mUid); setResult(RESULT_CANCELED); break; } @@ -111,63 +118,20 @@ public class GrantCredentialsPermissionActivity extends Activity implements View } /** - * Sends the result or a Constants.ERROR_CODE_CANCELED error if a result isn't present. + * Sends the result or a {@link AccountManager#ERROR_CODE_CANCELED} error if a + * result isn't present. */ public void finish() { Intent intent = getIntent(); - AccountAuthenticatorResponse accountAuthenticatorResponse = - intent.getParcelableExtra(EXTRAS_RESPONSE); - if (accountAuthenticatorResponse != null) { + AccountAuthenticatorResponse response = intent.getParcelableExtra(EXTRAS_RESPONSE); + if (response != null) { // send the result bundle back if set, otherwise send an error. if (mResultBundle != null) { - accountAuthenticatorResponse.onResult(mResultBundle); + response.onResult(mResultBundle); } else { - accountAuthenticatorResponse.onError(AccountManager.ERROR_CODE_CANCELED, "canceled"); + response.onError(AccountManager.ERROR_CODE_CANCELED, "canceled"); } } super.finish(); } - - private static class PackagesArrayAdapter extends ArrayAdapter<String> { - protected LayoutInflater mInflater; - private static final int mResource = R.layout.simple_list_item_1; - - public PackagesArrayAdapter(Context context, String[] items) { - super(context, mResource, items); - mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - } - - static class ViewHolder { - TextView label; - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - // A ViewHolder keeps references to children views to avoid unneccessary calls - // to findViewById() on each row. - ViewHolder holder; - - // When convertView is not null, we can reuse it directly, there is no need - // to reinflate it. We only inflate a new View when the convertView supplied - // by ListView is null. - if (convertView == null) { - convertView = mInflater.inflate(mResource, null); - - // Creates a ViewHolder and store references to the two children views - // we want to bind data to. - holder = new ViewHolder(); - holder.label = (TextView) convertView.findViewById(R.id.text1); - - convertView.setTag(holder); - } else { - // Get the ViewHolder back to get fast access to the TextView - // and the ImageView. - holder = (ViewHolder) convertView.getTag(); - } - - holder.label.setText(getItem(position)); - - return convertView; - } - } } diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index 49ebce3..1c3414d 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -2087,8 +2087,8 @@ public class Activity extends ContextThemeWrapper event.setPackageName(getPackageName()); LayoutParams params = getWindow().getAttributes(); - boolean isFullScreen = (params.width == LayoutParams.FILL_PARENT) && - (params.height == LayoutParams.FILL_PARENT); + boolean isFullScreen = (params.width == LayoutParams.MATCH_PARENT) && + (params.height == LayoutParams.MATCH_PARENT); event.setFullScreen(isFullScreen); CharSequence title = getTitle(); diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java index d709deb..932ad53 100644 --- a/core/java/android/app/ActivityManager.java +++ b/core/java/android/app/ActivityManager.java @@ -606,7 +606,7 @@ public class ActivityManager { public int uid; /** - * The tag that was provided when the process crashed. + * The activity name associated with the error, if known. May be null. */ public String tag; @@ -621,9 +621,14 @@ public class ActivityManager { public String longMsg; /** - * Raw data about the crash (typically a stack trace). + * The stack trace where the error originated. May be null. */ - public byte[] crashData; + public String stackTrace; + + /** + * to be deprecated: This value will always be null. + */ + public byte[] crashData = null; public ProcessErrorStateInfo() { } @@ -640,8 +645,7 @@ public class ActivityManager { dest.writeString(tag); dest.writeString(shortMsg); dest.writeString(longMsg); - dest.writeInt(crashData == null ? -1 : crashData.length); - dest.writeByteArray(crashData); + dest.writeString(stackTrace); } public void readFromParcel(Parcel source) { @@ -652,13 +656,7 @@ public class ActivityManager { tag = source.readString(); shortMsg = source.readString(); longMsg = source.readString(); - int cdLen = source.readInt(); - if (cdLen == -1) { - crashData = null; - } else { - crashData = new byte[cdLen]; - source.readByteArray(crashData); - } + stackTrace = source.readString(); } public static final Creator<ProcessErrorStateInfo> CREATOR = @@ -892,6 +890,38 @@ public class ActivityManager { } /** + * @deprecated This is now just a wrapper for + * {@link #killBackgroundProcesses(String)}; the previous behavior here + * is no longer available to applications because it allows them to + * break other applications by removing their alarms, stopping their + * services, etc. + */ + @Deprecated + public void restartPackage(String packageName) { + killBackgroundProcesses(packageName); + } + + /** + * Have the system immediately kill all background processes associated + * with the given package. This is the same as the kernel killing those + * processes to reclaim memory; the system will take care of restarting + * these processes in the future as needed. + * + * <p>You must hold the permission + * {@link android.Manifest.permission#KILL_BACKGROUND_PROCESSES} to be able to + * call this method. + * + * @param packageName The name of the package whose processes are to + * be killed. + */ + public void killBackgroundProcesses(String packageName) { + try { + ActivityManagerNative.getDefault().killBackgroundProcesses(packageName); + } catch (RemoteException e) { + } + } + + /** * Have the system perform a force stop of everything associated with * the given application package. All processes that share its uid * will be killed, all services it has running stopped, all activities @@ -900,14 +930,18 @@ public class ActivityManager { * be stopped, notifications removed, etc. * * <p>You must hold the permission - * {@link android.Manifest.permission#RESTART_PACKAGES} to be able to + * {@link android.Manifest.permission#FORCE_STOP_PACKAGES} to be able to * call this method. * * @param packageName The name of the package to be stopped. + * + * @hide This is not available to third party applications due to + * it allowing them to break other applications by stopping their + * services, removing their alarms, etc. */ - public void restartPackage(String packageName) { + public void forceStopPackage(String packageName) { try { - ActivityManagerNative.getDefault().restartPackage(packageName); + ActivityManagerNative.getDefault().forceStopPackage(packageName); } catch (RemoteException e) { } } diff --git a/core/java/android/app/ActivityManagerNative.java b/core/java/android/app/ActivityManagerNative.java index 3b8aee9..09b88ee 100644 --- a/core/java/android/app/ActivityManagerNative.java +++ b/core/java/android/app/ActivityManagerNative.java @@ -979,21 +979,26 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM return true; } - case HANDLE_APPLICATION_ERROR_TRANSACTION: { + case HANDLE_APPLICATION_CRASH_TRANSACTION: { + data.enforceInterface(IActivityManager.descriptor); + IBinder app = data.readStrongBinder(); + ApplicationErrorReport.CrashInfo ci = new ApplicationErrorReport.CrashInfo(data); + handleApplicationCrash(app, ci); + reply.writeNoException(); + return true; + } + + case HANDLE_APPLICATION_WTF_TRANSACTION: { data.enforceInterface(IActivityManager.descriptor); IBinder app = data.readStrongBinder(); - int fl = data.readInt(); String tag = data.readString(); - String shortMsg = data.readString(); - String longMsg = data.readString(); - byte[] crashData = data.createByteArray(); - int res = handleApplicationError(app, fl, tag, shortMsg, longMsg, - crashData); + ApplicationErrorReport.CrashInfo ci = new ApplicationErrorReport.CrashInfo(data); + boolean res = handleApplicationWtf(app, tag, ci); reply.writeNoException(); - reply.writeInt(res); + reply.writeInt(res ? 1 : 0); return true; } - + case SIGNAL_PERSISTENT_PROCESSES_TRANSACTION: { data.enforceInterface(IActivityManager.descriptor); int sig = data.readInt(); @@ -1002,10 +1007,18 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM return true; } - case RESTART_PACKAGE_TRANSACTION: { - data.enforceInterface(IActivityManager.descriptor); + case KILL_BACKGROUND_PROCESSES_TRANSACTION: { + data.enforceInterface(IActivityManager.descriptor); + String packageName = data.readString(); + killBackgroundProcesses(packageName); + reply.writeNoException(); + return true; + } + + case FORCE_STOP_PACKAGE_TRANSACTION: { + data.enforceInterface(IActivityManager.descriptor); String packageName = data.readString(); - restartPackage(packageName); + forceStopPackage(packageName); reply.writeNoException(); return true; } @@ -2342,27 +2355,36 @@ class ActivityManagerProxy implements IActivityManager /* this base class version is never called */ return true; } - public int handleApplicationError(IBinder app, int flags, - String tag, String shortMsg, String longMsg, - byte[] crashData) throws RemoteException + public void handleApplicationCrash(IBinder app, + ApplicationErrorReport.CrashInfo crashInfo) throws RemoteException + { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IActivityManager.descriptor); + data.writeStrongBinder(app); + crashInfo.writeToParcel(data, 0); + mRemote.transact(HANDLE_APPLICATION_CRASH_TRANSACTION, data, reply, 0); + reply.readException(); + reply.recycle(); + data.recycle(); + } + public boolean handleApplicationWtf(IBinder app, String tag, + ApplicationErrorReport.CrashInfo crashInfo) throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); data.writeInterfaceToken(IActivityManager.descriptor); data.writeStrongBinder(app); - data.writeInt(flags); data.writeString(tag); - data.writeString(shortMsg); - data.writeString(longMsg); - data.writeByteArray(crashData); - mRemote.transact(HANDLE_APPLICATION_ERROR_TRANSACTION, data, reply, 0); + crashInfo.writeToParcel(data, 0); + mRemote.transact(HANDLE_APPLICATION_WTF_TRANSACTION, data, reply, 0); reply.readException(); - int res = reply.readInt(); + boolean res = reply.readInt() != 0; reply.recycle(); data.recycle(); return res; } - + public void signalPersistentProcesses(int sig) throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); @@ -2374,12 +2396,23 @@ class ActivityManagerProxy implements IActivityManager reply.recycle(); } - public void restartPackage(String packageName) throws RemoteException { + public void killBackgroundProcesses(String packageName) throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IActivityManager.descriptor); + data.writeString(packageName); + mRemote.transact(KILL_BACKGROUND_PROCESSES_TRANSACTION, data, reply, 0); + reply.readException(); + data.recycle(); + reply.recycle(); + } + + public void forceStopPackage(String packageName) throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); data.writeInterfaceToken(IActivityManager.descriptor); data.writeString(packageName); - mRemote.transact(RESTART_PACKAGE_TRANSACTION, data, reply, 0); + mRemote.transact(FORCE_STOP_PACKAGE_TRANSACTION, data, reply, 0); reply.readException(); data.recycle(); reply.recycle(); diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 909620d..10fef0d 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -40,7 +40,6 @@ import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDebug; import android.graphics.Bitmap; import android.graphics.Canvas; -import android.net.http.AndroidHttpClient; import android.os.Bundle; import android.os.Debug; import android.os.Handler; @@ -4303,7 +4302,6 @@ public final class ActivityThread { private final void attach(boolean system) { sThreadLocal.set(this); mSystemThread = system; - AndroidHttpClient.setThreadBlocked(true); if (!system) { android.ddm.DdmHandleAppName.setAppName("<pre-initialized>"); RuntimeInit.setApplicationObject(mAppThread.asBinder()); @@ -4333,7 +4331,6 @@ public final class ActivityThread { private final void detach() { - AndroidHttpClient.setThreadBlocked(false); sThreadLocal.set(null); } diff --git a/core/java/android/app/AlertDialog.java b/core/java/android/app/AlertDialog.java index 20a579a..2603579 100644 --- a/core/java/android/app/AlertDialog.java +++ b/core/java/android/app/AlertDialog.java @@ -40,7 +40,7 @@ import com.android.internal.app.AlertController; * * <pre> * FrameLayout fl = (FrameLayout) findViewById(R.id.body); - * fl.add(myView, new LayoutParams(FILL_PARENT, WRAP_CONTENT)); + * fl.add(myView, new LayoutParams(MATCH_PARENT, WRAP_CONTENT)); * </pre> * * <p>The AlertDialog class takes care of automatically setting diff --git a/core/java/android/app/AliasActivity.java b/core/java/android/app/AliasActivity.java index 4f91e02..7527a5b 100644 --- a/core/java/android/app/AliasActivity.java +++ b/core/java/android/app/AliasActivity.java @@ -26,7 +26,7 @@ import android.content.res.XmlResourceParser; import android.os.Bundle; import android.util.AttributeSet; import android.util.Xml; -import com.android.internal.util.XmlUtils; +import com.android.common.XmlUtils; import java.io.IOException; diff --git a/core/java/android/app/ApplicationContext.java b/core/java/android/app/ApplicationContext.java index f48f150..d89b877 100644 --- a/core/java/android/app/ApplicationContext.java +++ b/core/java/android/app/ApplicationContext.java @@ -17,7 +17,7 @@ package android.app; import com.android.internal.policy.PolicyManager; -import com.android.internal.util.XmlUtils; +import com.android.common.XmlUtils; import com.google.android.collect.Maps; import org.xmlpull.v1.XmlPullParserException; @@ -70,6 +70,7 @@ import android.net.wifi.IWifiManager; import android.net.wifi.WifiManager; import android.os.Binder; import android.os.Bundle; +import android.os.DropBoxManager; import android.os.FileUtils; import android.os.Handler; import android.os.IBinder; @@ -93,6 +94,8 @@ import android.view.inputmethod.InputMethodManager; import android.accounts.AccountManager; import android.accounts.IAccountManager; +import com.android.internal.os.IDropBoxManagerService; + import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; @@ -182,6 +185,7 @@ class ApplicationContext extends Context { private ClipboardManager mClipboardManager = null; private boolean mRestricted; private AccountManager mAccountManager; // protected by mSync + private DropBoxManager mDropBoxManager = null; private final Object mSync = new Object(); @@ -462,14 +466,7 @@ class ApplicationContext extends Context { @Override public SQLiteDatabase openOrCreateDatabase(String name, int mode, CursorFactory factory) { - File dir = getDatabasesDir(); - if (!dir.isDirectory() && dir.mkdir()) { - FileUtils.setPermissions(dir.getPath(), - FileUtils.S_IRWXU|FileUtils.S_IRWXG|FileUtils.S_IXOTH, - -1, -1); - } - - File f = makeFilename(dir, name); + File f = validateFilePath(name, true); SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(f, factory); setFilePermissionsFromMode(f.getPath(), mode, 0); return db; @@ -478,7 +475,7 @@ class ApplicationContext extends Context { @Override public boolean deleteDatabase(String name) { try { - File f = makeFilename(getDatabasesDir(), name); + File f = validateFilePath(name, false); return f.delete(); } catch (Exception e) { } @@ -487,7 +484,7 @@ class ApplicationContext extends Context { @Override public File getDatabasePath(String name) { - return makeFilename(getDatabasesDir(), name); + return validateFilePath(name, false); } @Override @@ -896,6 +893,8 @@ class ApplicationContext extends Context { return getClipboardManager(); } else if (WALLPAPER_SERVICE.equals(name)) { return getWallpaperManager(); + } else if (DROPBOX_SERVICE.equals(name)) { + return getDropBoxManager(); } return null; @@ -1045,7 +1044,7 @@ class ApplicationContext extends Context { } return mVibrator; } - + private AudioManager getAudioManager() { if (mAudioManager == null) { @@ -1054,6 +1053,17 @@ class ApplicationContext extends Context { return mAudioManager; } + private DropBoxManager getDropBoxManager() { + synchronized (mSync) { + if (mDropBoxManager == null) { + IBinder b = ServiceManager.getService(DROPBOX_SERVICE); + IDropBoxManagerService service = IDropBoxManagerService.Stub.asInterface(b); + mDropBoxManager = new DropBoxManager(service); + } + } + return mDropBoxManager; + } + @Override public int checkPermission(String permission, int pid, int uid) { if (permission == null) { @@ -1437,12 +1447,35 @@ class ApplicationContext extends Context { FileUtils.setPermissions(name, perms, -1, -1); } + private File validateFilePath(String name, boolean createDirectory) { + File dir; + File f; + + if (name.charAt(0) == File.separatorChar) { + String dirPath = name.substring(0, name.lastIndexOf(File.separatorChar)); + dir = new File(dirPath); + name = name.substring(name.lastIndexOf(File.separatorChar)); + f = new File(dir, name); + } else { + dir = getDatabasesDir(); + f = makeFilename(dir, name); + } + + if (createDirectory && !dir.isDirectory() && dir.mkdir()) { + FileUtils.setPermissions(dir.getPath(), + FileUtils.S_IRWXU|FileUtils.S_IRWXG|FileUtils.S_IXOTH, + -1, -1); + } + + return f; + } + private File makeFilename(File base, String name) { if (name.indexOf(File.separatorChar) < 0) { return new File(base, name); } throw new IllegalArgumentException( - "File " + name + " contains a path separator"); + "File " + name + " contains a path separator"); } // ---------------------------------------------------------------------- diff --git a/core/java/android/app/ApplicationErrorReport.java b/core/java/android/app/ApplicationErrorReport.java index aeae5f9..a4b692f 100644 --- a/core/java/android/app/ApplicationErrorReport.java +++ b/core/java/android/app/ApplicationErrorReport.java @@ -19,6 +19,8 @@ package android.app; import android.os.Parcel; import android.os.Parcelable; import android.util.Printer; +import java.io.PrintWriter; +import java.io.StringWriter; /** * Describes an application error. @@ -187,6 +189,32 @@ public class ApplicationErrorReport implements Parcelable { } /** + * Create an instance of CrashInfo initialized from an exception. + */ + public CrashInfo(Throwable tr) { + StringWriter sw = new StringWriter(); + tr.printStackTrace(new PrintWriter(sw)); + stackTrace = sw.toString(); + exceptionMessage = tr.getMessage(); + + // Populate fields with the "root cause" exception + while (tr.getCause() != null) { + tr = tr.getCause(); + String msg = tr.getMessage(); + if (msg != null && msg.length() > 0) { + exceptionMessage = msg; + } + } + + exceptionClassName = tr.getClass().getName(); + StackTraceElement trace = tr.getStackTrace()[0]; + throwFileName = trace.getFileName(); + throwClassName = trace.getClassName(); + throwMethodName = trace.getMethodName(); + throwLineNumber = trace.getLineNumber(); + } + + /** * Create an instance of CrashInfo initialized from a Parcel. */ public CrashInfo(Parcel in) { diff --git a/core/java/android/app/Dialog.java b/core/java/android/app/Dialog.java index 58e8b32..fa5d4a8 100644 --- a/core/java/android/app/Dialog.java +++ b/core/java/android/app/Dialog.java @@ -668,8 +668,8 @@ public class Dialog implements DialogInterface, Window.Callback, event.setPackageName(mContext.getPackageName()); LayoutParams params = getWindow().getAttributes(); - boolean isFullScreen = (params.width == LayoutParams.FILL_PARENT) && - (params.height == LayoutParams.FILL_PARENT); + boolean isFullScreen = (params.width == LayoutParams.MATCH_PARENT) && + (params.height == LayoutParams.MATCH_PARENT); event.setFullScreen(isFullScreen); return false; diff --git a/core/java/android/app/ExpandableListActivity.java b/core/java/android/app/ExpandableListActivity.java index a2e048f..9651078 100644 --- a/core/java/android/app/ExpandableListActivity.java +++ b/core/java/android/app/ExpandableListActivity.java @@ -65,21 +65,21 @@ import java.util.Map; * <?xml version="1.0" encoding="UTF-8"?> * <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" * android:orientation="vertical" - * android:layout_width="fill_parent" - * android:layout_height="fill_parent" + * android:layout_width="match_parent" + * android:layout_height="match_parent" * android:paddingLeft="8dp" * android:paddingRight="8dp"> * * <ExpandableListView android:id="@id/android:list" - * android:layout_width="fill_parent" - * android:layout_height="fill_parent" + * android:layout_width="match_parent" + * android:layout_height="match_parent" * android:background="#00FF00" * android:layout_weight="1" * android:drawSelectorOnTop="false"/> * * <TextView android:id="@id/android:empty" - * android:layout_width="fill_parent" - * android:layout_height="fill_parent" + * android:layout_width="match_parent" + * android:layout_height="match_parent" * android:background="#FF0000" * android:text="No data"/> * </LinearLayout> @@ -114,19 +114,19 @@ import java.util.Map; * <pre> * <?xml version="1.0" encoding="utf-8"?> * <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - * android:layout_width="fill_parent" + * android:layout_width="match_parent" * android:layout_height="wrap_content" * android:orientation="vertical"> * * <TextView android:id="@+id/text1" * android:textSize="16sp" * android:textStyle="bold" - * android:layout_width="fill_parent" + * android:layout_width="match_parent" * android:layout_height="wrap_content"/> * * <TextView android:id="@+id/text2" * android:textSize="16sp" - * android:layout_width="fill_parent" + * android:layout_width="match_parent" * android:layout_height="wrap_content"/> * </LinearLayout> * </pre> diff --git a/core/java/android/app/IActivityController.aidl b/core/java/android/app/IActivityController.aidl index 8f6b252..c76a517 100644 --- a/core/java/android/app/IActivityController.aidl +++ b/core/java/android/app/IActivityController.aidl @@ -43,8 +43,9 @@ interface IActivityController * normal error recovery (app crash dialog) to occur, false to kill * it immediately. */ - boolean appCrashed(String processName, int pid, String shortMsg, - String longMsg, in byte[] crashData); + boolean appCrashed(String processName, int pid, + String shortMsg, String longMsg, + long timeMillis, String stackTrace); /** * An application process is not responding. Return 0 to show the "app diff --git a/core/java/android/app/IActivityManager.java b/core/java/android/app/IActivityManager.java index 9f505ac..016d465 100644 --- a/core/java/android/app/IActivityManager.java +++ b/core/java/android/app/IActivityManager.java @@ -216,7 +216,8 @@ public interface IActivityManager extends IInterface { public void getMemoryInfo(ActivityManager.MemoryInfo outInfo) throws RemoteException; - public void restartPackage(final String packageName) throws RemoteException; + public void killBackgroundProcesses(final String packageName) throws RemoteException; + public void forceStopPackage(final String packageName) throws RemoteException; // Note: probably don't want to allow applications access to these. public void goingToSleep() throws RemoteException; @@ -242,11 +243,10 @@ public interface IActivityManager extends IInterface { // Special low-level communication with activity manager. public void startRunning(String pkg, String cls, String action, String data) throws RemoteException; - // Returns 1 if the user wants to debug. - public int handleApplicationError(IBinder app, - int flags, /* 1 == can debug */ - String tag, String shortMsg, String longMsg, - byte[] crashData) throws RemoteException; + public void handleApplicationCrash(IBinder app, + ApplicationErrorReport.CrashInfo crashInfo) throws RemoteException; + public boolean handleApplicationWtf(IBinder app, String tag, + ApplicationErrorReport.CrashInfo crashInfo) throws RemoteException; /* * This will deliver the specified signal to all the persistent processes. Currently only @@ -351,7 +351,7 @@ public interface IActivityManager extends IInterface { // Please keep these transaction codes the same -- they are also // sent by C++ code. int START_RUNNING_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION; - int HANDLE_APPLICATION_ERROR_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+1; + int HANDLE_APPLICATION_CRASH_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+1; int START_ACTIVITY_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+2; int UNHANDLED_BACK_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+3; int OPEN_CONTENT_URI_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+4; @@ -425,7 +425,7 @@ public interface IActivityManager extends IInterface { int GET_MEMORY_INFO_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+75; int GET_PROCESSES_IN_ERROR_STATE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+76; int CLEAR_APP_DATA_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+77; - int RESTART_PACKAGE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+78; + int FORCE_STOP_PACKAGE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+78; int KILL_PIDS_FOR_MEMORY_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+79; int GET_SERVICES_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+80; int REPORT_PSS_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+81; @@ -448,4 +448,6 @@ public interface IActivityManager extends IInterface { int KILL_APPLICATION_PROCESS_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+98; int START_ACTIVITY_INTENT_SENDER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+99; int OVERRIDE_PENDING_TRANSITION_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+100; + int HANDLE_APPLICATION_WTF_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+101; + int KILL_BACKGROUND_PROCESSES_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+102; } diff --git a/core/java/android/app/ISearchManager.aidl b/core/java/android/app/ISearchManager.aidl index a7d6378..0920467 100644 --- a/core/java/android/app/ISearchManager.aidl +++ b/core/java/android/app/ISearchManager.aidl @@ -16,11 +16,11 @@ package android.app; +import android.app.SearchableInfo; import android.app.ISearchManagerCallback; import android.content.ComponentName; import android.content.res.Configuration; import android.os.Bundle; -import android.server.search.SearchableInfo; /** @hide */ interface ISearchManager { diff --git a/core/java/android/app/ListActivity.java b/core/java/android/app/ListActivity.java index 19b99c8..4b4cc05 100644 --- a/core/java/android/app/ListActivity.java +++ b/core/java/android/app/ListActivity.java @@ -56,21 +56,21 @@ import android.widget.ListView; * <?xml version="1.0" encoding="utf-8"?> * <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" * android:orientation="vertical" - * android:layout_width="fill_parent" - * android:layout_height="fill_parent" + * android:layout_width="match_parent" + * android:layout_height="match_parent" * android:paddingLeft="8dp" * android:paddingRight="8dp"> * * <ListView android:id="@id/android:list" - * android:layout_width="fill_parent" - * android:layout_height="fill_parent" + * android:layout_width="match_parent" + * android:layout_height="match_parent" * android:background="#00FF00" * android:layout_weight="1" * android:drawSelectorOnTop="false"/> * * <TextView id="@id/android:empty" - * android:layout_width="fill_parent" - * android:layout_height="fill_parent" + * android:layout_width="match_parent" + * android:layout_height="match_parent" * android:background="#FF0000" * android:text="No data"/> * </LinearLayout> @@ -100,19 +100,19 @@ import android.widget.ListView; * <pre> * <?xml version="1.0" encoding="utf-8"?> * <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - * android:layout_width="fill_parent" + * android:layout_width="match_parent" * android:layout_height="wrap_content" * android:orientation="vertical"> * * <TextView android:id="@+id/text1" * android:textSize="16sp" * android:textStyle="bold" - * android:layout_width="fill_parent" + * android:layout_width="match_parent" * android:layout_height="wrap_content"/> * * <TextView android:id="@+id/text2" * android:textSize="16sp" - * android:layout_width="fill_parent" + * android:layout_width="match_parent" * android:layout_height="wrap_content"/> * </LinearLayout> * </pre> diff --git a/core/java/android/app/SearchDialog.java b/core/java/android/app/SearchDialog.java index e5a769b..b396396 100644 --- a/core/java/android/app/SearchDialog.java +++ b/core/java/android/app/SearchDialog.java @@ -37,13 +37,11 @@ import android.os.IBinder; import android.os.RemoteException; import android.os.SystemClock; import android.provider.Browser; -import android.server.search.SearchableInfo; import android.speech.RecognizerIntent; import android.text.Editable; import android.text.InputType; import android.text.TextUtils; import android.text.TextWatcher; -import android.text.util.Regex; import android.util.AndroidRuntimeException; import android.util.AttributeSet; import android.util.Log; @@ -62,13 +60,14 @@ import android.widget.AdapterView; import android.widget.AutoCompleteTextView; import android.widget.Button; import android.widget.ImageButton; -import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.ListView; import android.widget.TextView; import android.widget.AdapterView.OnItemClickListener; import android.widget.AdapterView.OnItemSelectedListener; +import com.android.common.Patterns; + import java.util.ArrayList; import java.util.WeakHashMap; import java.util.concurrent.atomic.AtomicLong; @@ -106,7 +105,7 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS // views & widgets private TextView mBadgeLabel; - private ImageView mAppIcon; + private SearchSourceSelector mSourceSelector; private SearchAutoComplete mSearchAutoComplete; private Button mGoButton; private ImageButton mVoiceButton; @@ -182,11 +181,11 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS Window theWindow = getWindow(); WindowManager.LayoutParams lp = theWindow.getAttributes(); lp.type = WindowManager.LayoutParams.TYPE_SEARCH_BAR; - lp.width = ViewGroup.LayoutParams.FILL_PARENT; + lp.width = ViewGroup.LayoutParams.MATCH_PARENT; // taking up the whole window (even when transparent) is less than ideal, // but necessary to show the popup window until the window manager supports // having windows anchored by their parent but not clipped by them. - lp.height = ViewGroup.LayoutParams.FILL_PARENT; + lp.height = ViewGroup.LayoutParams.MATCH_PARENT; lp.gravity = Gravity.TOP | Gravity.FILL_HORIZONTAL; lp.softInputMode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE; theWindow.setAttributes(lp); @@ -209,7 +208,8 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS mBadgeLabel = (TextView) findViewById(com.android.internal.R.id.search_badge); mSearchAutoComplete = (SearchAutoComplete) findViewById(com.android.internal.R.id.search_src_text); - mAppIcon = (ImageView) findViewById(com.android.internal.R.id.search_app_icon); + mSourceSelector = new SearchSourceSelector( + findViewById(com.android.internal.R.id.search_source_selector)); mGoButton = (Button) findViewById(com.android.internal.R.id.search_go_btn); mVoiceButton = (ImageButton) findViewById(com.android.internal.R.id.search_voice_btn); mSearchPlate = findViewById(com.android.internal.R.id.search_plate); @@ -606,13 +606,16 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS } private void updateSearchAppIcon() { + mSourceSelector.setSource(mSearchable.getSearchActivity()); + mSourceSelector.setAppSearchData(mAppSearchData); + // In Donut, we special-case the case of the browser to hide the app icon as if it were // global search, for extra space for url entry. // // TODO: Remove this special case once the issue has been reconciled in Eclair. if (mGlobalSearchMode || isBrowserSearch()) { - mAppIcon.setImageResource(0); - mAppIcon.setVisibility(View.GONE); + mSourceSelector.setSourceIcon(null); + mSourceSelector.setVisibility(View.GONE); mSearchPlate.setPadding(SEARCH_PLATE_LEFT_PADDING_GLOBAL, mSearchPlate.getPaddingTop(), mSearchPlate.getPaddingRight(), @@ -628,8 +631,8 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS icon = pm.getDefaultActivityIcon(); Log.w(LOG_TAG, mLaunchComponent + " not found, using generic app icon"); } - mAppIcon.setImageDrawable(icon); - mAppIcon.setVisibility(View.VISIBLE); + mSourceSelector.setSourceIcon(icon); + mSourceSelector.setVisibility(View.VISIBLE); mSearchPlate.setPadding(SEARCH_PLATE_LEFT_PADDING_NON_GLOBAL, mSearchPlate.getPaddingTop(), mSearchPlate.getPaddingRight(), @@ -812,6 +815,7 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS if (!mSearchAutoComplete.isPerformingCompletion()) { // The user changed the query, remember it. mUserQuery = s == null ? "" : s.toString(); + mSourceSelector.setQuery(mUserQuery); } } @@ -823,7 +827,7 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS // The user changed the query, check if it is a URL and if so change the search // button in the soft keyboard to the 'Go' button. int options = (mSearchAutoComplete.getImeOptions() & (~EditorInfo.IME_MASK_ACTION)); - if (Regex.WEB_URL_PATTERN.matcher(mUserQuery).matches()) { + if (Patterns.WEB_URL.matcher(mUserQuery).matches()) { options = options | EditorInfo.IME_ACTION_GO; } else { options = options | EditorInfo.IME_ACTION_SEARCH; @@ -1927,6 +1931,7 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS query = ""; } mUserQuery = query; + mSourceSelector.setQuery(query); mSearchAutoComplete.setText(query); mSearchAutoComplete.setSelection(query.length()); } diff --git a/core/java/android/app/SearchManager.java b/core/java/android/app/SearchManager.java index 2e94a2f..a75e8dc 100644 --- a/core/java/android/app/SearchManager.java +++ b/core/java/android/app/SearchManager.java @@ -16,17 +16,22 @@ package android.app; +import android.Manifest; +import android.content.ActivityNotFoundException; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; import android.content.DialogInterface; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.RemoteException; import android.os.ServiceManager; -import android.server.search.SearchableInfo; import android.text.TextUtils; import android.util.Log; import android.view.KeyEvent; @@ -1327,6 +1332,22 @@ public class SearchManager public final static String EXTRA_DATA_KEY = "intent_extra_data_key"; /** + * String extra data key for {@link Intent#ACTION_GLOBAL_SEARCH} intents. Contains the initial + * query to show in the global search activity. + * + * @hide Pending API council approval + */ + public final static String INITIAL_QUERY = "initial_query"; + + /** + * Boolean extra data key for {@link Intent#ACTION_GLOBAL_SEARCH} intents. If {@code true}, + * the initial query should be selected. + * + * @hide Pending API council approval + */ + public final static String SELECT_INITIAL_QUERY = "select_initial_query"; + + /** * Defines the constants used in the communication between {@link android.app.SearchDialog} and * the global search provider via {@link Cursor#respond(android.os.Bundle)}. * @@ -1592,6 +1613,15 @@ public class SearchManager public final static String SUGGEST_PARAMETER_LIMIT = "limit"; /** + * Intent action for opening the search source selection activity. + * The intent may include these extra values: + * {@link #QUERY}, + * {@link #APP_DATA}. + */ + public static final String INTENT_ACTION_SELECT_SEARCH_SOURCE + = "android.intent.action.SELECT_SEARCH_SOURCE"; + + /** * If a suggestion has this value in {@link #SUGGEST_COLUMN_INTENT_ACTION}, * the search dialog will switch to a different suggestion source when the * suggestion is clicked. @@ -1757,7 +1787,13 @@ public class SearchManager boolean globalSearch) { if (mIdent == 0) throw new IllegalArgumentException( "Called from outside of an Activity context"); - if (!globalSearch && !mAssociatedPackage.equals(launchActivity.getPackageName())) { + + if (globalSearch) { + startGlobalSearch(initialQuery, selectInitialQuery, appSearchData); + return; + } + + if (!mAssociatedPackage.equals(launchActivity.getPackageName())) { Log.w(TAG, "invoking app search on a different package " + "not associated with this search manager"); } @@ -1771,6 +1807,65 @@ public class SearchManager } /** + * Starts the global search activity. + */ + private void startGlobalSearch(String initialQuery, boolean selectInitialQuery, + Bundle appSearchData) { + ComponentName globalSearchActivity = getGlobalSearchActivity(); + if (globalSearchActivity == null) { + Log.w(TAG, "No global search activity found."); + return; + } + Intent intent = new Intent(Intent.ACTION_GLOBAL_SEARCH); + intent.setComponent(globalSearchActivity); + // TODO: Always pass name of calling package as an extra? + if (appSearchData != null) { + intent.putExtra(APP_DATA, appSearchData); + } + if (!TextUtils.isEmpty(initialQuery)) { + intent.putExtra(INITIAL_QUERY, initialQuery); + } + if (selectInitialQuery) { + intent.putExtra(SELECT_INITIAL_QUERY, selectInitialQuery); + } + try { + if (DBG) Log.d(TAG, "Starting global search: " + intent.toUri(0)); + mContext.startActivity(intent); + } catch (ActivityNotFoundException ex) { + Log.e(TAG, "Global search activity not found: " + globalSearchActivity); + } + } + + /** + * Gets the name of the global search activity. + * + * This is currently implemented by returning the first activity that handles + * the GLOBAL_SEARCH intent and has the GLOBAL_SEARCH permission. If we allow + * more than one global search acitivity to be installed, this code must be changed. + * + * TODO: Doing this every time we start global search is inefficient. Will fix that once + * we have settled on the right mechanism for finding the global search activity. + */ + private ComponentName getGlobalSearchActivity() { + Intent intent = new Intent(Intent.ACTION_GLOBAL_SEARCH); + PackageManager pm = mContext.getPackageManager(); + List<ResolveInfo> activities = + pm.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); + int count = activities.size(); + for (int i = 0; i < count; i++) { + ActivityInfo ai = activities.get(i).activityInfo; + if (pm.checkPermission(Manifest.permission.GLOBAL_SEARCH, + ai.packageName) == PackageManager.PERMISSION_GRANTED) { + return new ComponentName(ai.packageName, ai.name); + } else { + Log.w(TAG, "Package " + ai.packageName + " wants to handle GLOBAL_SEARCH, " + + "but does not have the GLOBAL_SEARCH permission."); + } + } + return null; + } + + /** * Similar to {@link #startSearch} but actually fires off the search query after invoking * the search dialog. Made available for testing purposes. * @@ -1935,8 +2030,23 @@ public class SearchManager } /** - * Gets information about a searchable activity. This method is static so that it can - * be used from non-Activity contexts. + * Gets information about a searchable activity. + * + * @param componentName The activity to get searchable information for. + * @return Searchable information, or <code>null</code> if the activity does not + * exist, or is not searchable. + */ + public SearchableInfo getSearchableInfo(ComponentName componentName) { + try { + return mService.getSearchableInfo(componentName, false); + } catch (RemoteException ex) { + Log.e(TAG, "getSearchableInfo() failed: " + ex); + return null; + } + } + + /** + * Gets information about a searchable activity. * * @param componentName The activity to get searchable information for. * @param globalSearch If <code>false</code>, return information about the given activity. @@ -2039,10 +2149,8 @@ public class SearchManager * Returns a list of the searchable activities that can be included in global search. * * @return a list containing searchable information for all searchable activities - * that have the <code>exported</code> attribute set in their searchable - * meta-data. - * - * @hide because SearchableInfo is not part of the API. + * that have the <code>android:includeInGlobalSearch</code> attribute set + * in their searchable meta-data. */ public List<SearchableInfo> getSearchablesInGlobalSearch() { try { diff --git a/core/java/android/app/SearchSourceSelector.java b/core/java/android/app/SearchSourceSelector.java new file mode 100644 index 0000000..fabf858 --- /dev/null +++ b/core/java/android/app/SearchSourceSelector.java @@ -0,0 +1,197 @@ +/* + * 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 android.app; + +import com.android.internal.R; + +import android.content.ActivityNotFoundException; +import android.content.ComponentName; +import android.content.ContentResolver; +import android.content.Intent; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +import android.view.View; +import android.widget.ImageButton; + +import java.util.List; + +/** + * Utilities for setting up the search source selector. + * + * This class has two copies: + * android.app.SearchSourceSelector + * com.android.quicksearchbox.ui.SearchSourceSelector + * + * They should keep the same look and feel as much as possible, + * but only the intent details must absolutely stay in sync. + * + * @hide + */ +public class SearchSourceSelector implements View.OnClickListener { + + private static final String TAG = "SearchSourceSelector"; + + // TODO: This should be defined in android.provider.Applications, + // and have a less made-up value. + private static final String APPLICATION_TYPE = "application/vnd.android.application"; + + public static final int ICON_VIEW_ID = R.id.search_source_selector_icon; + + private final View mView; + + private final ImageButton mIconView; + + private ComponentName mSource; + + private Bundle mAppSearchData; + + private String mQuery; + + public SearchSourceSelector(View view) { + mView = view; + mIconView = (ImageButton) view.findViewById(ICON_VIEW_ID); + mIconView.setOnClickListener(this); + } + + /** + * Sets the icon displayed in the search source selector. + */ + public void setSourceIcon(Drawable icon) { + mIconView.setImageDrawable(icon); + } + + /** + * Sets the current search source. + */ + public void setSource(ComponentName source) { + mSource = source; + } + + /** + * Sets the app-specific data that will be passed to the search activity if + * the user opens the source selector and chooses a source. + */ + public void setAppSearchData(Bundle appSearchData) { + mAppSearchData = appSearchData; + } + + /** + * Sets the initial query that will be passed to the search activity if + * the user opens the source selector and chooses a source. + */ + public void setQuery(String query) { + mQuery = query; + } + + public void setVisibility(int visibility) { + mView.setVisibility(visibility); + } + + /** + * Creates an intent for opening the search source selector activity. + * + * @param source The current search source. + * @param query The initial query that will be passed to the search activity if + * the user opens the source selector and chooses a source. + * @param appSearchData The app-specific data that will be passed to the search + * activity if the user opens the source selector and chooses a source. + */ + public static Intent createIntent(ComponentName source, String query, Bundle appSearchData) { + Intent intent = new Intent(SearchManager.INTENT_ACTION_SELECT_SEARCH_SOURCE); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_CLEAR_TOP + | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); + Uri sourceUri = componentNameToUri(source); + if (sourceUri != null) { + intent.setDataAndType(sourceUri, APPLICATION_TYPE); + } + if (query != null) { + intent.putExtra(SearchManager.QUERY, query); + } + if (query != null) { + intent.putExtra(SearchManager.APP_DATA, appSearchData); + } + return intent; + } + + /** + * Gets the search source from which the given + * {@link SearchManager.INTENT_ACTION_SELECT_SEARCH_SOURCE} intent was sent. + */ + public static ComponentName getSource(Intent intent) { + return uriToComponentName(intent.getData()); + } + + private static Uri componentNameToUri(ComponentName name) { + if (name == null) return null; + // TODO: This URI format is specificed in android.provider.Applications which is @hidden + return new Uri.Builder() + .scheme(ContentResolver.SCHEME_CONTENT) + .authority("applications") + .appendEncodedPath("applications") + .appendPath(name.getPackageName()) + .appendPath(name.getClassName()) + .build(); + } + + private static ComponentName uriToComponentName(Uri uri) { + if (uri == null) return null; + List<String> path = uri.getPathSegments(); + if (path == null || path.size() != 3) return null; + String pkg = path.get(1); + String cls = path.get(2); + if (TextUtils.isEmpty(pkg) || TextUtils.isEmpty(cls)) return null; + return new ComponentName(pkg, cls); + } + + public void onClick(View v) { + trigger(); + } + + private void trigger() { + try { + Intent intent = createIntent(mSource, mQuery, mAppSearchData); + intent.setSourceBounds(getOnScreenRect(mIconView)); + mIconView.getContext().startActivity(intent); + } catch (ActivityNotFoundException ex) { + Log.e(TAG, "No source selector activity found", ex); + } + } + + // TODO: This code is replicated in lots of places: + // - android.provider.ContactsContract.QuickContact.showQuickContact() + // - android.widget.RemoteViews.setOnClickPendingIntent() + // - com.android.launcher2.Launcher.onClick() + // - com.android.launcher.Launcher.onClick() + // - com.android.server.status.StatusBarService.Launcher.onClick() + private static Rect getOnScreenRect(View v) { + final float appScale = v.getResources().getCompatibilityInfo().applicationScale; + final int[] pos = new int[2]; + v.getLocationOnScreen(pos); + final Rect rect = new Rect(); + rect.left = (int) (pos[0] * appScale + 0.5f); + rect.top = (int) (pos[1] * appScale + 0.5f); + rect.right = (int) ((pos[0] + v.getWidth()) * appScale + 0.5f); + rect.bottom = (int) ((pos[1] + v.getHeight()) * appScale + 0.5f); + return rect; + } + +} diff --git a/core/java/android/server/search/SearchableInfo.aidl b/core/java/android/app/SearchableInfo.aidl index 9576c2b..146b373 100644 --- a/core/java/android/server/search/SearchableInfo.aidl +++ b/core/java/android/app/SearchableInfo.aidl @@ -14,6 +14,6 @@ * limitations under the License. */ -package android.server.search; +package android.app; parcelable SearchableInfo; diff --git a/core/java/android/server/search/SearchableInfo.java b/core/java/android/app/SearchableInfo.java index 69ef98c..9897742 100644 --- a/core/java/android/server/search/SearchableInfo.java +++ b/core/java/android/app/SearchableInfo.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package android.server.search; +package android.app; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -37,6 +37,11 @@ import android.view.inputmethod.EditorInfo; import java.io.IOException; import java.util.HashMap; +/** + * Searchability meta-data for an activity. + * See <a href="SearchManager.html#SearchabilityMetadata">Searchability meta-data</a> + * for more information. + */ public final class SearchableInfo implements Parcelable { // general debugging support @@ -83,9 +88,9 @@ public final class SearchableInfo implements Parcelable { private final String mSuggestProviderPackage; // Flag values for Searchable_voiceSearchMode - private static int VOICE_SEARCH_SHOW_BUTTON = 1; - private static int VOICE_SEARCH_LAUNCH_WEB_SEARCH = 2; - private static int VOICE_SEARCH_LAUNCH_RECOGNIZER = 4; + private static final int VOICE_SEARCH_SHOW_BUTTON = 1; + private static final int VOICE_SEARCH_LAUNCH_WEB_SEARCH = 2; + private static final int VOICE_SEARCH_LAUNCH_RECOGNIZER = 4; private final int mVoiceSearchMode; private final int mVoiceLanguageModeId; // voiceLanguageModel private final int mVoicePromptTextId; // voicePromptText @@ -119,6 +124,8 @@ public final class SearchableInfo implements Parcelable { /** * Checks whether the badge should be a text label. + * + * @hide This feature is deprecated, no need to add it to the API. */ public boolean useBadgeLabel() { return 0 != (mSearchMode & SEARCH_MODE_BADGE_LABEL); @@ -126,6 +133,8 @@ public final class SearchableInfo implements Parcelable { /** * Checks whether the badge should be an icon. + * + * @hide This feature is deprecated, no need to add it to the API. */ public boolean useBadgeIcon() { return (0 != (mSearchMode & SEARCH_MODE_BADGE_ICON)) && (mIconId != 0); @@ -216,6 +225,7 @@ public final class SearchableInfo implements Parcelable { * * @param context You need to supply a context to start with * @return Returns a context related to the searchable activity + * @hide */ public Context getActivityContext(Context context) { return createActivityContext(context, mSearchActivity); @@ -247,6 +257,7 @@ public final class SearchableInfo implements Parcelable { * @param activityContext If we can determine that the provider and the activity are the * same, we'll just return this one. * @return Returns a context related to the context provider + * @hide */ public Context getProviderContext(Context context, Context activityContext) { Context theirContext = null; @@ -347,7 +358,11 @@ public final class SearchableInfo implements Parcelable { } /** - * Private class used to hold the "action key" configuration + * Information about an action key in searchability meta-data. + * See <a href="SearchManager.html#SearchabilityMetadata">Searchability meta-data</a> + * for more information. + * + * @see SearchableInfo#findActionKey(int) */ public static class ActionKeyInfo implements Parcelable { @@ -364,7 +379,7 @@ public final class SearchableInfo implements Parcelable { * construct the object. * @throws IllegalArgumentException if the action key configuration is invalid */ - public ActionKeyInfo(Context activityContext, AttributeSet attr) { + ActionKeyInfo(Context activityContext, AttributeSet attr) { TypedArray a = activityContext.obtainStyledAttributes(attr, com.android.internal.R.styleable.SearchableActionKey); @@ -395,7 +410,7 @@ public final class SearchableInfo implements Parcelable { * @param in The Parcel containing the previously written ActionKeyInfo, * positioned at the location in the buffer where it was written. */ - public ActionKeyInfo(Parcel in) { + private ActionKeyInfo(Parcel in) { mKeyCode = in.readInt(); mQueryActionMsg = in.readString(); mSuggestActionMsg = in.readString(); @@ -457,6 +472,8 @@ public final class SearchableInfo implements Parcelable { * @param activityInfo Activity to get search information from. * @return Search information about the given activity, or {@code null} if * the activity has no or invalid searchability meta-data. + * + * @hide For use by SearchManagerService. */ public static SearchableInfo getActivityMetaData(Context context, ActivityInfo activityInfo) { // for each component, try to find metadata @@ -716,7 +733,7 @@ public final class SearchableInfo implements Parcelable { * @param in The Parcel containing the previously written SearchableInfo, * positioned at the location in the buffer where it was written. */ - public SearchableInfo(Parcel in) { + SearchableInfo(Parcel in) { mLabelId = in.readInt(); mSearchActivity = ComponentName.readFromParcel(in); mHintId = in.readInt(); diff --git a/core/java/android/app/Service.java b/core/java/android/app/Service.java index 30e1712..c4e1877 100644 --- a/core/java/android/app/Service.java +++ b/core/java/android/app/Service.java @@ -287,6 +287,14 @@ public abstract class Service extends ContextWrapper implements ComponentCallbac * {@link #onStart} and returns either {@link #START_STICKY} * or {@link #START_STICKY_COMPATIBILITY}. * + * <p>If you need your application to run on platform versions prior to API + * level 5, you can use the following model to handle the older {@link #onStart} + * callback in that case. The <code>handleCommand</code> method is implemented by + * you as appropriate: + * + * <pre>{@include development/samples/ApiDemos/src/com/example/android/apis/app/ForegroundService.java + * start_compatibility}</pre> + * * @param intent The Intent supplied to {@link android.content.Context#startService}, * as given. This may be null if the service is being restarted after * its process has gone away, and it had previously returned anything @@ -462,6 +470,13 @@ public abstract class Service extends ContextWrapper implements ComponentCallbac * if your service is performing background music playback, so the user * would notice if their music stopped playing. * + * <p>If you need your application to run on platform versions prior to API + * level 5, you can use the following model to call the the older {@link #setForeground} + * or this modern method as appropriate: + * + * <pre>{@include development/samples/ApiDemos/src/com/example/android/apis/app/ForegroundService.java + * foreground_compatibility}</pre> + * * @param id The identifier for this notification as per * {@link NotificationManager#notify(int, Notification) * NotificationManager.notify(int, Notification)}. diff --git a/core/java/android/app/SuggestionsAdapter.java b/core/java/android/app/SuggestionsAdapter.java index 12be97c..173c3e1 100644 --- a/core/java/android/app/SuggestionsAdapter.java +++ b/core/java/android/app/SuggestionsAdapter.java @@ -31,7 +31,6 @@ import android.graphics.drawable.Drawable; import android.graphics.drawable.StateListDrawable; import android.net.Uri; import android.os.Bundle; -import android.server.search.SearchableInfo; import android.text.Html; import android.text.TextUtils; import android.util.Log; diff --git a/core/java/android/appwidget/AppWidgetHostView.java b/core/java/android/appwidget/AppWidgetHostView.java index 2f719f3..792b289 100644 --- a/core/java/android/appwidget/AppWidgetHostView.java +++ b/core/java/android/appwidget/AppWidgetHostView.java @@ -311,8 +311,8 @@ public class AppWidgetHostView extends FrameLayout { // Take requested dimensions from child, but apply default gravity. FrameLayout.LayoutParams requested = (FrameLayout.LayoutParams)view.getLayoutParams(); if (requested == null) { - requested = new FrameLayout.LayoutParams(LayoutParams.FILL_PARENT, - LayoutParams.FILL_PARENT); + requested = new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, + LayoutParams.MATCH_PARENT); } requested.gravity = Gravity.CENTER; diff --git a/core/java/android/bluetooth/BluetoothAdapter.java b/core/java/android/bluetooth/BluetoothAdapter.java index 8eda844..42d87f4 100644 --- a/core/java/android/bluetooth/BluetoothAdapter.java +++ b/core/java/android/bluetooth/BluetoothAdapter.java @@ -63,7 +63,7 @@ import java.util.UUID; */ public final class BluetoothAdapter { private static final String TAG = "BluetoothAdapter"; - private static final boolean DBG = false; + private static final boolean DBG = true; //STOPSHIP: Remove excess logging /** * Sentinel error value for this class. Guaranteed to not equal any other diff --git a/core/java/android/content/AbstractCursorEntityIterator.java b/core/java/android/content/AbstractCursorEntityIterator.java deleted file mode 100644 index a804f3c..0000000 --- a/core/java/android/content/AbstractCursorEntityIterator.java +++ /dev/null @@ -1,121 +0,0 @@ -package android.content; - -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.os.RemoteException; - -/** - * An abstract class that makes it easy to implement an EntityIterator over a cursor. - * The user must implement {@link #newEntityFromCursorLocked}, which runs inside of a - * database transaction. - * @hide - */ -public abstract class AbstractCursorEntityIterator implements EntityIterator { - private final Cursor mEntityCursor; - private final SQLiteDatabase mDb; - private volatile Entity mNextEntity; - private volatile boolean mIsClosed; - - public AbstractCursorEntityIterator(SQLiteDatabase db, Cursor entityCursor) { - mEntityCursor = entityCursor; - mDb = db; - mNextEntity = null; - mIsClosed = false; - } - - /** - * If there are entries left in the cursor then advance the cursor and use the new row to - * populate mNextEntity. If the cursor is at the end or if advancing it causes the cursor - * to become at the end then set mEntityCursor to null. If newEntityFromCursor returns null - * then continue advancing until it either returns a non-null Entity or the cursor reaches - * the end. - */ - private void fillEntityIfAvailable() { - while (mNextEntity == null) { - if (!mEntityCursor.moveToNext()) { - // the cursor is at then end, bail out - return; - } - // This may return null if newEntityFromCursor is not able to create an entity - // from the current cursor position. In that case this method will loop and try - // the next cursor position - mNextEntity = newEntityFromCursorLocked(mEntityCursor); - } - mDb.beginTransaction(); - try { - int position = mEntityCursor.getPosition(); - mNextEntity = newEntityFromCursorLocked(mEntityCursor); - int newPosition = mEntityCursor.getPosition(); - if (newPosition != position) { - throw new IllegalStateException("the cursor position changed during the call to" - + "newEntityFromCursorLocked, from " + position + " to " + newPosition); - } - } finally { - mDb.endTransaction(); - } - } - - /** - * Checks if there are more Entities accessible via this iterator. This may not be called - * if the iterator is already closed. - * @return true if the call to next() will return an Entity. - */ - public boolean hasNext() { - if (mIsClosed) { - throw new IllegalStateException("calling hasNext() when the iterator is closed"); - } - fillEntityIfAvailable(); - return mNextEntity != null; - } - - /** - * Returns the next Entity that is accessible via this iterator. This may not be called - * if the iterator is already closed. - * @return the next Entity that is accessible via this iterator - */ - public Entity next() { - if (mIsClosed) { - throw new IllegalStateException("calling next() when the iterator is closed"); - } - if (!hasNext()) { - throw new IllegalStateException("you may only call next() if hasNext() is true"); - } - - try { - return mNextEntity; - } finally { - mNextEntity = null; - } - } - - public void reset() throws RemoteException { - if (mIsClosed) { - throw new IllegalStateException("calling reset() when the iterator is closed"); - } - mEntityCursor.moveToPosition(-1); - mNextEntity = null; - } - - /** - * Closes this iterator making it invalid. If is invalid for the user to call any public - * method on the iterator once it has been closed. - */ - public void close() { - if (mIsClosed) { - throw new IllegalStateException("closing when already closed"); - } - mIsClosed = true; - mEntityCursor.close(); - } - - /** - * Returns a new Entity from the current cursor position. This is called from within a - * database transaction. If a new entity cannot be created from this cursor position (e.g. - * if the row that is referred to no longer exists) then this may return null. The cursor - * is guaranteed to be pointing to a valid row when this call is made. The implementation - * of newEntityFromCursorLocked is not allowed to change the position of the cursor. - * @param cursor from where to read the data for the Entity - * @return an Entity that corresponds to the current cursor position or null - */ - public abstract Entity newEntityFromCursorLocked(Cursor cursor); -} diff --git a/core/java/android/content/AbstractSyncableContentProvider.java b/core/java/android/content/AbstractSyncableContentProvider.java deleted file mode 100644 index dd89097..0000000 --- a/core/java/android/content/AbstractSyncableContentProvider.java +++ /dev/null @@ -1,755 +0,0 @@ -package android.content; - -import android.database.sqlite.SQLiteOpenHelper; -import android.database.sqlite.SQLiteDatabase; -import android.database.Cursor; -import android.net.Uri; -import android.accounts.OnAccountsUpdateListener; -import android.accounts.Account; -import android.accounts.AccountManager; -import android.provider.SyncConstValue; -import android.util.Config; -import android.util.Log; -import android.os.Bundle; -import android.text.TextUtils; - -import java.util.Collections; -import java.util.Map; -import java.util.Vector; -import java.util.ArrayList; -import java.util.Set; -import java.util.HashSet; - -import com.google.android.collect.Maps; - -/** - * A specialization of the ContentProvider that centralizes functionality - * used by ContentProviders that are syncable. It also wraps calls to the ContentProvider - * inside of database transactions. - * - * @hide - */ -public abstract class AbstractSyncableContentProvider extends SyncableContentProvider { - private static final String TAG = "SyncableContentProvider"; - protected SQLiteOpenHelper mOpenHelper; - protected SQLiteDatabase mDb; - private final String mDatabaseName; - private final int mDatabaseVersion; - private final Uri mContentUri; - - /** the account set in the last call to onSyncStart() */ - private Account mSyncingAccount; - - private SyncStateContentProviderHelper mSyncState = null; - - private static final String[] sAccountProjection = - new String[] {SyncConstValue._SYNC_ACCOUNT, SyncConstValue._SYNC_ACCOUNT_TYPE}; - - private boolean mIsTemporary; - - private AbstractTableMerger mCurrentMerger = null; - private boolean mIsMergeCancelled = false; - - private static final String SYNC_ACCOUNT_WHERE_CLAUSE = - SyncConstValue._SYNC_ACCOUNT + "=? AND " + SyncConstValue._SYNC_ACCOUNT_TYPE + "=?"; - - protected boolean isTemporary() { - return mIsTemporary; - } - - private final ThreadLocal<Boolean> mApplyingBatch = new ThreadLocal<Boolean>(); - private final ThreadLocal<Set<Uri>> mPendingBatchNotifications = new ThreadLocal<Set<Uri>>(); - - /** - * Indicates whether or not this ContentProvider contains a full - * set of data or just diffs. This knowledge comes in handy when - * determining how to incorporate the contents of a temporary - * provider into a real provider. - */ - private boolean mContainsDiffs; - - /** - * Initializes the AbstractSyncableContentProvider - * @param dbName the filename of the database - * @param dbVersion the current version of the database schema - * @param contentUri The base Uri of the syncable content in this provider - */ - public AbstractSyncableContentProvider(String dbName, int dbVersion, Uri contentUri) { - super(); - - mDatabaseName = dbName; - mDatabaseVersion = dbVersion; - mContentUri = contentUri; - mIsTemporary = false; - setContainsDiffs(false); - if (Config.LOGV) { - Log.v(TAG, "created SyncableContentProvider " + this); - } - } - - /** - * Close resources that must be closed. You must call this to properly release - * the resources used by the AbstractSyncableContentProvider. - */ - public void close() { - if (mOpenHelper != null) { - mOpenHelper.close(); // OK to call .close() repeatedly. - } - } - - /** - * Override to create your schema and do anything else you need to do with a new database. - * This is run inside a transaction (so you don't need to use one). - * This method may not use getDatabase(), or call content provider methods, it must only - * use the database handle passed to it. - */ - protected void bootstrapDatabase(SQLiteDatabase db) {} - - /** - * Override to upgrade your database from an old version to the version you specified. - * Don't set the DB version; this will automatically be done after the method returns. - * This method may not use getDatabase(), or call content provider methods, it must only - * use the database handle passed to it. - * - * @param oldVersion version of the existing database - * @param newVersion current version to upgrade to - * @return true if the upgrade was lossless, false if it was lossy - */ - protected abstract boolean upgradeDatabase(SQLiteDatabase db, int oldVersion, int newVersion); - - /** - * Override to do anything (like cleanups or checks) you need to do after opening a database. - * Does nothing by default. This is run inside a transaction (so you don't need to use one). - * This method may not use getDatabase(), or call content provider methods, it must only - * use the database handle passed to it. - */ - protected void onDatabaseOpened(SQLiteDatabase db) {} - - private class DatabaseHelper extends SQLiteOpenHelper { - DatabaseHelper(Context context, String name) { - // Note: context and name may be null for temp providers - super(context, name, null, mDatabaseVersion); - } - - @Override - public void onCreate(SQLiteDatabase db) { - bootstrapDatabase(db); - mSyncState.createDatabase(db); - if (!isTemporary()) { - ContentResolver.requestSync(null /* all accounts */, - mContentUri.getAuthority(), new Bundle()); - } - } - - @Override - public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { - if (!upgradeDatabase(db, oldVersion, newVersion)) { - mSyncState.discardSyncData(db, null /* all accounts */); - ContentResolver.requestSync(null /* all accounts */, - mContentUri.getAuthority(), new Bundle()); - } - } - - @Override - public void onOpen(SQLiteDatabase db) { - onDatabaseOpened(db); - mSyncState.onDatabaseOpened(db); - } - } - - @Override - public boolean onCreate() { - if (isTemporary()) throw new IllegalStateException("onCreate() called for temp provider"); - mOpenHelper = new AbstractSyncableContentProvider.DatabaseHelper(getContext(), - mDatabaseName); - mSyncState = new SyncStateContentProviderHelper(mOpenHelper); - AccountManager.get(getContext()).addOnAccountsUpdatedListener( - new OnAccountsUpdateListener() { - public void onAccountsUpdated(Account[] accounts) { - // Some providers override onAccountsChanged(); give them a database to - // work with. - mDb = mOpenHelper.getWritableDatabase(); - // Only call onAccountsChanged on GAIA accounts; otherwise, the contacts and - // calendar providers will choke as they try to sync unknown accounts with - // AbstractGDataSyncAdapter, which will put acore into a crash loop - ArrayList<Account> gaiaAccounts = new ArrayList<Account>(); - for (Account acct: accounts) { - if (acct.type.equals("com.google")) { - gaiaAccounts.add(acct); - } - } - accounts = new Account[gaiaAccounts.size()]; - int i = 0; - for (Account acct: gaiaAccounts) { - accounts[i++] = acct; - } - onAccountsChanged(accounts); - TempProviderSyncAdapter syncAdapter = getTempProviderSyncAdapter(); - if (syncAdapter != null) { - syncAdapter.onAccountsChanged(accounts); - } - } - }, null /* handler */, true /* updateImmediately */); - - return true; - } - /** - * Get a non-persistent instance of this content provider. - * You must call {@link #close} on the returned - * SyncableContentProvider when you are done with it. - * - * @return a non-persistent content provider with the same layout as this - * provider. - */ - public AbstractSyncableContentProvider getTemporaryInstance() { - AbstractSyncableContentProvider temp; - try { - temp = getClass().newInstance(); - } catch (InstantiationException e) { - throw new RuntimeException("unable to instantiate class, " - + "this should never happen", e); - } catch (IllegalAccessException e) { - throw new RuntimeException( - "IllegalAccess while instantiating class, " - + "this should never happen", e); - } - - // Note: onCreate() isn't run for the temp provider, and it has no Context. - temp.mIsTemporary = true; - temp.setContainsDiffs(true); - temp.mOpenHelper = temp.new DatabaseHelper(null, null); - temp.mSyncState = new SyncStateContentProviderHelper(temp.mOpenHelper); - if (!isTemporary()) { - mSyncState.copySyncState( - mOpenHelper.getReadableDatabase(), - temp.mOpenHelper.getWritableDatabase(), - getSyncingAccount()); - } - return temp; - } - - public SQLiteDatabase getDatabase() { - if (mDb == null) mDb = mOpenHelper.getWritableDatabase(); - return mDb; - } - - public boolean getContainsDiffs() { - return mContainsDiffs; - } - - public void setContainsDiffs(boolean containsDiffs) { - if (containsDiffs && !isTemporary()) { - throw new IllegalStateException( - "only a temporary provider can contain diffs"); - } - mContainsDiffs = containsDiffs; - } - - /** - * Each subclass of this class should define a subclass of {@link - * android.content.AbstractTableMerger} for each table they wish to merge. It - * should then override this method and return one instance of - * each merger, in sequence. Their {@link - * android.content.AbstractTableMerger#merge merge} methods will be called, one at a - * time, in the order supplied. - * - * <p>The default implementation returns an empty list, so that no - * merging will occur. - * @return A sequence of subclasses of {@link - * android.content.AbstractTableMerger}, one for each table that should be merged. - */ - protected Iterable<? extends AbstractTableMerger> getMergers() { - return Collections.emptyList(); - } - - @Override - public final int update(final Uri url, final ContentValues values, - final String selection, final String[] selectionArgs) { - mDb = mOpenHelper.getWritableDatabase(); - final boolean notApplyingBatch = !applyingBatch(); - if (notApplyingBatch) { - mDb.beginTransaction(); - } - try { - if (isTemporary() && mSyncState.matches(url)) { - int numRows = mSyncState.asContentProvider().update( - url, values, selection, selectionArgs); - if (notApplyingBatch) { - mDb.setTransactionSuccessful(); - } - return numRows; - } - - int result = updateInternal(url, values, selection, selectionArgs); - if (notApplyingBatch) { - mDb.setTransactionSuccessful(); - } - if (!isTemporary() && result > 0) { - if (notApplyingBatch) { - getContext().getContentResolver().notifyChange(url, null /* observer */, - changeRequiresLocalSync(url)); - } else { - mPendingBatchNotifications.get().add(url); - } - } - return result; - } finally { - if (notApplyingBatch) { - mDb.endTransaction(); - } - } - } - - @Override - public final int delete(final Uri url, final String selection, - final String[] selectionArgs) { - mDb = mOpenHelper.getWritableDatabase(); - final boolean notApplyingBatch = !applyingBatch(); - if (notApplyingBatch) { - mDb.beginTransaction(); - } - try { - if (isTemporary() && mSyncState.matches(url)) { - int numRows = mSyncState.asContentProvider().delete(url, selection, selectionArgs); - if (notApplyingBatch) { - mDb.setTransactionSuccessful(); - } - return numRows; - } - int result = deleteInternal(url, selection, selectionArgs); - if (notApplyingBatch) { - mDb.setTransactionSuccessful(); - } - if (!isTemporary() && result > 0) { - if (notApplyingBatch) { - getContext().getContentResolver().notifyChange(url, null /* observer */, - changeRequiresLocalSync(url)); - } else { - mPendingBatchNotifications.get().add(url); - } - } - return result; - } finally { - if (notApplyingBatch) { - mDb.endTransaction(); - } - } - } - - private boolean applyingBatch() { - return mApplyingBatch.get() != null && mApplyingBatch.get(); - } - - @Override - public final Uri insert(final Uri url, final ContentValues values) { - mDb = mOpenHelper.getWritableDatabase(); - final boolean notApplyingBatch = !applyingBatch(); - if (notApplyingBatch) { - mDb.beginTransaction(); - } - try { - if (isTemporary() && mSyncState.matches(url)) { - Uri result = mSyncState.asContentProvider().insert(url, values); - if (notApplyingBatch) { - mDb.setTransactionSuccessful(); - } - return result; - } - Uri result = insertInternal(url, values); - if (notApplyingBatch) { - mDb.setTransactionSuccessful(); - } - if (!isTemporary() && result != null) { - if (notApplyingBatch) { - getContext().getContentResolver().notifyChange(url, null /* observer */, - changeRequiresLocalSync(url)); - } else { - mPendingBatchNotifications.get().add(url); - } - } - return result; - } finally { - if (notApplyingBatch) { - mDb.endTransaction(); - } - } - } - - @Override - public final int bulkInsert(final Uri uri, final ContentValues[] values) { - int size = values.length; - int completed = 0; - final boolean isSyncStateUri = mSyncState.matches(uri); - mDb = mOpenHelper.getWritableDatabase(); - mDb.beginTransaction(); - try { - for (int i = 0; i < size; i++) { - Uri result; - if (isTemporary() && isSyncStateUri) { - result = mSyncState.asContentProvider().insert(uri, values[i]); - } else { - result = insertInternal(uri, values[i]); - mDb.yieldIfContended(); - } - if (result != null) { - completed++; - } - } - mDb.setTransactionSuccessful(); - } finally { - mDb.endTransaction(); - } - if (!isTemporary() && completed == size) { - getContext().getContentResolver().notifyChange(uri, null /* observer */, - changeRequiresLocalSync(uri)); - } - return completed; - } - - /** - * <p> - * Start batch transaction. {@link #endTransaction} MUST be called after - * calling this method. Those methods should be used like this: - * </p> - * - * <pre class="prettyprint"> - * boolean successful = false; - * beginBatch() - * try { - * // Do something related to mDb - * successful = true; - * return ret; - * } finally { - * endBatch(successful); - * } - * </pre> - * - * @hide This method should be used only when {@link ContentProvider#applyBatch} is not enough and must be - * used with {@link #endBatch}. - * e.g. If returned value has to be used during one transaction, this method might be useful. - */ - public final void beginBatch() { - // initialize if this is the first time this thread has applied a batch - if (mApplyingBatch.get() == null) { - mApplyingBatch.set(false); - mPendingBatchNotifications.set(new HashSet<Uri>()); - } - - if (applyingBatch()) { - throw new IllegalStateException( - "applyBatch is not reentrant but mApplyingBatch is already set"); - } - SQLiteDatabase db = getDatabase(); - db.beginTransaction(); - boolean successful = false; - try { - mApplyingBatch.set(true); - successful = true; - } finally { - if (!successful) { - // Something unexpected happened. We must call endTransaction() at least. - db.endTransaction(); - } - } - } - - /** - * <p> - * Finish batch transaction. If "successful" is true, try to call - * mDb.setTransactionSuccessful() before calling mDb.endTransaction(). - * This method MUST be used with {@link #beginBatch()}. - * </p> - * - * @hide This method must be used with {@link #beginTransaction} - */ - public final void endBatch(boolean successful) { - try { - if (successful) { - // setTransactionSuccessful() must be called just once during opening the - // transaction. - mDb.setTransactionSuccessful(); - } - } finally { - mApplyingBatch.set(false); - getDatabase().endTransaction(); - for (Uri url : mPendingBatchNotifications.get()) { - getContext().getContentResolver().notifyChange(url, null /* observer */, - changeRequiresLocalSync(url)); - } - } - } - - public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) - throws OperationApplicationException { - boolean successful = false; - beginBatch(); - try { - ContentProviderResult[] results = super.applyBatch(operations); - successful = true; - return results; - } finally { - endBatch(successful); - } - } - - /** - * Check if changes to this URI can be syncable changes. - * @param uri the URI of the resource that was changed - * @return true if changes to this URI can be syncable changes, false otherwise - */ - public boolean changeRequiresLocalSync(Uri uri) { - return true; - } - - @Override - public final Cursor query(final Uri url, final String[] projection, - final String selection, final String[] selectionArgs, - final String sortOrder) { - mDb = mOpenHelper.getReadableDatabase(); - if (isTemporary() && mSyncState.matches(url)) { - return mSyncState.asContentProvider().query( - url, projection, selection, selectionArgs, sortOrder); - } - return queryInternal(url, projection, selection, selectionArgs, sortOrder); - } - - /** - * Called right before a sync is started. - * - * @param context the sync context for the operation - * @param account - */ - public void onSyncStart(SyncContext context, Account account) { - if (account == null) { - throw new IllegalArgumentException("you passed in an empty account"); - } - mSyncingAccount = account; - } - - /** - * Called right after a sync is completed - * - * @param context the sync context for the operation - * @param success true if the sync succeeded, false if an error occurred - */ - public void onSyncStop(SyncContext context, boolean success) { - } - - /** - * The account of the most recent call to onSyncStart() - * @return the account - */ - public Account getSyncingAccount() { - return mSyncingAccount; - } - - /** - * Merge diffs from a sync source with this content provider. - * - * @param context the SyncContext within which this merge is taking place - * @param diffs A temporary content provider containing diffs from a sync - * source. - * @param result a MergeResult that contains information about the merge, including - * a temporary content provider with the same layout as this provider containing - * @param syncResult - */ - public void merge(SyncContext context, SyncableContentProvider diffs, - TempProviderSyncResult result, SyncResult syncResult) { - SQLiteDatabase db = mOpenHelper.getWritableDatabase(); - db.beginTransaction(); - try { - synchronized(this) { - mIsMergeCancelled = false; - } - Iterable<? extends AbstractTableMerger> mergers = getMergers(); - try { - for (AbstractTableMerger merger : mergers) { - synchronized(this) { - if (mIsMergeCancelled) break; - mCurrentMerger = merger; - } - merger.merge(context, getSyncingAccount(), diffs, result, syncResult, this); - } - if (mIsMergeCancelled) return; - if (diffs != null) { - mSyncState.copySyncState( - ((AbstractSyncableContentProvider)diffs).mOpenHelper.getReadableDatabase(), - mOpenHelper.getWritableDatabase(), - getSyncingAccount()); - } - } finally { - synchronized (this) { - mCurrentMerger = null; - } - } - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } - } - - - /** - * Invoked when the active sync has been canceled. Sets the sync state of this provider and - * its merger to canceled. - */ - public void onSyncCanceled() { - synchronized (this) { - mIsMergeCancelled = true; - if (mCurrentMerger != null) { - mCurrentMerger.onMergeCancelled(); - } - } - } - - - public boolean isMergeCancelled() { - return mIsMergeCancelled; - } - - /** - * Subclasses should override this instead of update(). See update() - * for details. - * - * <p> This method is called within a acquireDbLock()/releaseDbLock() block, - * which means a database transaction will be active during the call; - */ - protected abstract int updateInternal(Uri url, ContentValues values, - String selection, String[] selectionArgs); - - /** - * Subclasses should override this instead of delete(). See delete() - * for details. - * - * <p> This method is called within a acquireDbLock()/releaseDbLock() block, - * which means a database transaction will be active during the call; - */ - protected abstract int deleteInternal(Uri url, String selection, String[] selectionArgs); - - /** - * Subclasses should override this instead of insert(). See insert() - * for details. - * - * <p> This method is called within a acquireDbLock()/releaseDbLock() block, - * which means a database transaction will be active during the call; - */ - protected abstract Uri insertInternal(Uri url, ContentValues values); - - /** - * Subclasses should override this instead of query(). See query() - * for details. - * - * <p> This method is *not* called within a acquireDbLock()/releaseDbLock() - * block for performance reasons. If an implementation needs atomic access - * to the database the lock can be acquired then. - */ - protected abstract Cursor queryInternal(Uri url, String[] projection, - String selection, String[] selectionArgs, String sortOrder); - - /** - * Make sure that there are no entries for accounts that no longer exist - * @param accountsArray the array of currently-existing accounts - */ - protected void onAccountsChanged(Account[] accountsArray) { - Map<Account, Boolean> accounts = Maps.newHashMap(); - for (Account account : accountsArray) { - accounts.put(account, false); - } - - SQLiteDatabase db = mOpenHelper.getWritableDatabase(); - Map<String, String> tableMap = db.getSyncedTables(); - Vector<String> tables = new Vector<String>(); - tables.addAll(tableMap.keySet()); - tables.addAll(tableMap.values()); - - db.beginTransaction(); - try { - mSyncState.onAccountsChanged(accountsArray); - for (String table : tables) { - deleteRowsForRemovedAccounts(accounts, table); - } - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } - } - - /** - * A helper method to delete all rows whose account is not in the accounts - * map. The accountColumnName is the name of the column that is expected - * to hold the account. If a row has an empty account it is never deleted. - * - * @param accounts a map of existing accounts - * @param table the table to delete from - */ - protected void deleteRowsForRemovedAccounts(Map<Account, Boolean> accounts, String table) { - SQLiteDatabase db = mOpenHelper.getWritableDatabase(); - Cursor c = db.query(table, sAccountProjection, null, null, - "_sync_account, _sync_account_type", null, null); - try { - while (c.moveToNext()) { - String accountName = c.getString(0); - String accountType = c.getString(1); - if ("localhost".equals(accountType)) continue; - if (TextUtils.isEmpty(accountName)) { - continue; - } - Account account = new Account(accountName, accountType); - if (!accounts.containsKey(account)) { - int numDeleted; - numDeleted = db.delete(table, "_sync_account=? AND _sync_account_type=?", - new String[]{account.name, account.type}); - if (Config.LOGV) { - Log.v(TAG, "deleted " + numDeleted - + " records from table " + table - + " for account " + account); - } - } - } - } finally { - c.close(); - } - } - - /** - * Called when the sync system determines that this provider should no longer - * contain records for the specified account. - */ - public void wipeAccount(Account account) { - SQLiteDatabase db = mOpenHelper.getWritableDatabase(); - Map<String, String> tableMap = db.getSyncedTables(); - ArrayList<String> tables = new ArrayList<String>(); - tables.addAll(tableMap.keySet()); - tables.addAll(tableMap.values()); - - db.beginTransaction(); - - try { - // remove the SyncState data - mSyncState.discardSyncData(db, account); - - // remove the data in the synced tables - for (String table : tables) { - db.delete(table, SYNC_ACCOUNT_WHERE_CLAUSE, - new String[]{account.name, account.type}); - } - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } - } - - /** - * Retrieves the SyncData bytes for the given account. The byte array returned may be null. - */ - public byte[] readSyncDataBytes(Account account) { - return mSyncState.readSyncDataBytes(mOpenHelper.getReadableDatabase(), account); - } - - /** - * Sets the SyncData bytes for the given account. The byte array may be null. - */ - public void writeSyncDataBytes(Account account, byte[] data) { - mSyncState.writeSyncDataBytes(mOpenHelper.getWritableDatabase(), account, data); - } -} diff --git a/core/java/android/content/AbstractTableMerger.java b/core/java/android/content/AbstractTableMerger.java deleted file mode 100644 index 9545fd7..0000000 --- a/core/java/android/content/AbstractTableMerger.java +++ /dev/null @@ -1,599 +0,0 @@ -/* - * Copyright (C) 2006 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 android.content; - -import android.database.Cursor; -import android.database.DatabaseUtils; -import android.database.sqlite.SQLiteDatabase; -import android.net.Uri; -import android.os.Debug; -import android.provider.BaseColumns; -import static android.provider.SyncConstValue.*; -import android.text.TextUtils; -import android.util.Log; -import android.accounts.Account; - -/** - * @hide - */ -public abstract class AbstractTableMerger -{ - private ContentValues mValues; - - protected SQLiteDatabase mDb; - protected String mTable; - protected Uri mTableURL; - protected String mDeletedTable; - protected Uri mDeletedTableURL; - static protected ContentValues mSyncMarkValues; - static private boolean TRACE; - - static { - mSyncMarkValues = new ContentValues(); - mSyncMarkValues.put(_SYNC_MARK, 1); - TRACE = false; - } - - private static final String TAG = "AbstractTableMerger"; - private static final String[] syncDirtyProjection = - new String[] {_SYNC_DIRTY, BaseColumns._ID, _SYNC_ID, _SYNC_VERSION}; - private static final String[] syncIdAndVersionProjection = - new String[] {_SYNC_ID, _SYNC_VERSION}; - - private volatile boolean mIsMergeCancelled; - - private static final String SELECT_MARKED = _SYNC_MARK + "> 0 and " - + _SYNC_ACCOUNT + "=? and " + _SYNC_ACCOUNT_TYPE + "=?"; - - private static final String SELECT_BY_SYNC_ID_AND_ACCOUNT = - _SYNC_ID +"=? and " + _SYNC_ACCOUNT + "=? and " + _SYNC_ACCOUNT_TYPE + "=?"; - private static final String SELECT_BY_ID = BaseColumns._ID +"=?"; - - private static final String SELECT_UNSYNCED = - "(" + _SYNC_ACCOUNT + " IS NULL OR (" - + _SYNC_ACCOUNT + "=? and " + _SYNC_ACCOUNT_TYPE + "=?)) and " - + "(" + _SYNC_ID + " IS NULL OR (" + _SYNC_DIRTY + " > 0 and " - + _SYNC_VERSION + " IS NOT NULL))"; - - public AbstractTableMerger(SQLiteDatabase database, - String table, Uri tableURL, String deletedTable, - Uri deletedTableURL) - { - mDb = database; - mTable = table; - mTableURL = tableURL; - mDeletedTable = deletedTable; - mDeletedTableURL = deletedTableURL; - mValues = new ContentValues(); - } - - public abstract void insertRow(ContentProvider diffs, - Cursor diffsCursor); - public abstract void updateRow(long localPersonID, - ContentProvider diffs, Cursor diffsCursor); - public abstract void resolveRow(long localPersonID, - String syncID, ContentProvider diffs, Cursor diffsCursor); - - /** - * This is called when it is determined that a row should be deleted from the - * ContentProvider. The localCursor is on a table from the local ContentProvider - * and its current position is of the row that should be deleted. The localCursor - * is only guaranteed to contain the BaseColumns.ID column so the implementation - * of deleteRow() must query the database directly if other columns are needed. - * <p> - * It is the responsibility of the implementation of this method to ensure that the cursor - * points to the next row when this method returns, either by calling Cursor.deleteRow() or - * Cursor.next(). - * - * @param localCursor The Cursor into the local table, which points to the row that - * is to be deleted. - */ - public void deleteRow(Cursor localCursor) { - localCursor.deleteRow(); - } - - /** - * After {@link #merge} has completed, this method is called to send - * notifications to {@link android.database.ContentObserver}s of changes - * to the containing {@link ContentProvider}. These notifications likely - * do not want to request a sync back to the network. - */ - protected abstract void notifyChanges(); - - private static boolean findInCursor(Cursor cursor, int column, String id) { - while (!cursor.isAfterLast() && !cursor.isNull(column)) { - int comp = id.compareTo(cursor.getString(column)); - if (comp > 0) { - cursor.moveToNext(); - continue; - } - return comp == 0; - } - return false; - } - - public void onMergeCancelled() { - mIsMergeCancelled = true; - } - - /** - * Carry out a merge of the given diffs, and add the results to - * the given MergeResult. If we are the first merge to find - * client-side diffs, we'll use the given ContentProvider to - * construct a temporary instance to hold them. - */ - public void merge(final SyncContext context, - final Account account, - final SyncableContentProvider serverDiffs, - TempProviderSyncResult result, - SyncResult syncResult, SyncableContentProvider temporaryInstanceFactory) { - mIsMergeCancelled = false; - if (serverDiffs != null) { - if (!mDb.isDbLockedByCurrentThread()) { - throw new IllegalStateException("this must be called from within a DB transaction"); - } - mergeServerDiffs(context, account, serverDiffs, syncResult); - notifyChanges(); - } - - if (result != null) { - findLocalChanges(result, temporaryInstanceFactory, account, syncResult); - } - if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "merge complete"); - } - - /** - * @hide this is public for testing purposes only - */ - public void mergeServerDiffs(SyncContext context, - Account account, SyncableContentProvider serverDiffs, SyncResult syncResult) { - boolean diffsArePartial = serverDiffs.getContainsDiffs(); - // mark the current rows so that we can distinguish these from new - // inserts that occur during the merge - mDb.update(mTable, mSyncMarkValues, null, null); - if (mDeletedTable != null) { - mDb.update(mDeletedTable, mSyncMarkValues, null, null); - } - - Cursor localCursor = null; - Cursor deletedCursor = null; - Cursor diffsCursor = null; - try { - // load the local database entries, so we can merge them with the server - final String[] accountSelectionArgs = new String[]{account.name, account.type}; - localCursor = mDb.query(mTable, syncDirtyProjection, - SELECT_MARKED, accountSelectionArgs, null, null, - mTable + "." + _SYNC_ID); - if (mDeletedTable != null) { - deletedCursor = mDb.query(mDeletedTable, syncIdAndVersionProjection, - SELECT_MARKED, accountSelectionArgs, null, null, - mDeletedTable + "." + _SYNC_ID); - } else { - deletedCursor = - mDb.rawQuery("select 'a' as _sync_id, 'b' as _sync_version limit 0", null); - } - - // Apply updates and insertions from the server - diffsCursor = serverDiffs.query(mTableURL, - null, null, null, mTable + "." + _SYNC_ID); - int deletedSyncIDColumn = deletedCursor.getColumnIndexOrThrow(_SYNC_ID); - int deletedSyncVersionColumn = deletedCursor.getColumnIndexOrThrow(_SYNC_VERSION); - int serverSyncIDColumn = diffsCursor.getColumnIndexOrThrow(_SYNC_ID); - int serverSyncVersionColumn = diffsCursor.getColumnIndexOrThrow(_SYNC_VERSION); - int serverSyncLocalIdColumn = diffsCursor.getColumnIndexOrThrow(_SYNC_LOCAL_ID); - - String lastSyncId = null; - int diffsCount = 0; - int localCount = 0; - localCursor.moveToFirst(); - deletedCursor.moveToFirst(); - while (diffsCursor.moveToNext()) { - if (mIsMergeCancelled) { - return; - } - mDb.yieldIfContended(); - String serverSyncId = diffsCursor.getString(serverSyncIDColumn); - String serverSyncVersion = diffsCursor.getString(serverSyncVersionColumn); - long localRowId = 0; - String localSyncVersion = null; - - diffsCount++; - context.setStatusText("Processing " + diffsCount + "/" - + diffsCursor.getCount()); - if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "processing server entry " + - diffsCount + ", " + serverSyncId); - - if (TRACE) { - if (diffsCount == 10) { - Debug.startMethodTracing("atmtrace"); - } - if (diffsCount == 20) { - Debug.stopMethodTracing(); - } - } - - boolean conflict = false; - boolean update = false; - boolean insert = false; - - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "found event with serverSyncID " + serverSyncId); - } - if (TextUtils.isEmpty(serverSyncId)) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.e(TAG, "server entry doesn't have a serverSyncID"); - } - continue; - } - - // It is possible that the sync adapter wrote the same record multiple times, - // e.g. if the same record came via multiple feeds. If this happens just ignore - // the duplicate records. - if (serverSyncId.equals(lastSyncId)) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "skipping record with duplicate remote server id " + lastSyncId); - } - continue; - } - lastSyncId = serverSyncId; - - String localSyncID = null; - boolean localSyncDirty = false; - - while (!localCursor.isAfterLast()) { - if (mIsMergeCancelled) { - return; - } - localCount++; - localSyncID = localCursor.getString(2); - - // If the local record doesn't have a _sync_id then - // it is new. Ignore it for now, we will send an insert - // the the server later. - if (TextUtils.isEmpty(localSyncID)) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "local record " + - localCursor.getLong(1) + - " has no _sync_id, ignoring"); - } - localCursor.moveToNext(); - localSyncID = null; - continue; - } - - int comp = serverSyncId.compareTo(localSyncID); - - // the local DB has a record that the server doesn't have - if (comp > 0) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "local record " + - localCursor.getLong(1) + - " has _sync_id " + localSyncID + - " that is < server _sync_id " + serverSyncId); - } - if (diffsArePartial) { - localCursor.moveToNext(); - } else { - deleteRow(localCursor); - if (mDeletedTable != null) { - mDb.delete(mDeletedTable, _SYNC_ID +"=?", new String[] {localSyncID}); - } - syncResult.stats.numDeletes++; - mDb.yieldIfContended(); - } - localSyncID = null; - continue; - } - - // the server has a record that the local DB doesn't have - if (comp < 0) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "local record " + - localCursor.getLong(1) + - " has _sync_id " + localSyncID + - " that is > server _sync_id " + serverSyncId); - } - localSyncID = null; - } - - // the server and the local DB both have this record - if (comp == 0) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "local record " + - localCursor.getLong(1) + - " has _sync_id " + localSyncID + - " that matches the server _sync_id"); - } - localSyncDirty = localCursor.getInt(0) != 0; - localRowId = localCursor.getLong(1); - localSyncVersion = localCursor.getString(3); - localCursor.moveToNext(); - } - - break; - } - - // If this record is in the deleted table then update the server version - // in the deleted table, if necessary, and then ignore it here. - // We will send a deletion indication to the server down a - // little further. - if (findInCursor(deletedCursor, deletedSyncIDColumn, serverSyncId)) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "remote record " + serverSyncId + " is in the deleted table"); - } - final String deletedSyncVersion = deletedCursor.getString(deletedSyncVersionColumn); - if (!TextUtils.equals(deletedSyncVersion, serverSyncVersion)) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "setting version of deleted record " + serverSyncId + " to " - + serverSyncVersion); - } - ContentValues values = new ContentValues(); - values.put(_SYNC_VERSION, serverSyncVersion); - mDb.update(mDeletedTable, values, "_sync_id=?", new String[]{serverSyncId}); - } - continue; - } - - // If the _sync_local_id is present in the diffsCursor - // then this record corresponds to a local record that was just - // inserted into the server and the _sync_local_id is the row id - // of the local record. Set these fields so that the next check - // treats this record as an update, which will allow the - // merger to update the record with the server's sync id - if (!diffsCursor.isNull(serverSyncLocalIdColumn)) { - localRowId = diffsCursor.getLong(serverSyncLocalIdColumn); - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "the remote record with sync id " + serverSyncId - + " has a local sync id, " + localRowId); - } - localSyncID = serverSyncId; - localSyncDirty = false; - localSyncVersion = null; - } - - if (!TextUtils.isEmpty(localSyncID)) { - // An existing server item has changed - // If serverSyncVersion is null, there is no edit URL; - // server won't let this change be written. - boolean recordChanged = (localSyncVersion == null) || - (serverSyncVersion == null) || - !serverSyncVersion.equals(localSyncVersion); - if (recordChanged) { - if (localSyncDirty) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "remote record " + serverSyncId - + " conflicts with local _sync_id " + localSyncID - + ", local _id " + localRowId); - } - conflict = true; - } else { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, - "remote record " + - serverSyncId + - " updates local _sync_id " + - localSyncID + ", local _id " + - localRowId); - } - update = true; - } - } else { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, - "Skipping update: localSyncVersion: " + localSyncVersion + - ", serverSyncVersion: " + serverSyncVersion); - } - } - } else { - // the local db doesn't know about this record so add it - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "remote record " + serverSyncId + " is new, inserting"); - } - insert = true; - } - - if (update) { - updateRow(localRowId, serverDiffs, diffsCursor); - syncResult.stats.numUpdates++; - } else if (conflict) { - resolveRow(localRowId, serverSyncId, serverDiffs, diffsCursor); - syncResult.stats.numUpdates++; - } else if (insert) { - insertRow(serverDiffs, diffsCursor); - syncResult.stats.numInserts++; - } - } - - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "processed " + diffsCount + " server entries"); - } - - // If tombstones aren't in use delete any remaining local rows that - // don't have corresponding server rows. Keep the rows that don't - // have a sync id since those were created locally and haven't been - // synced to the server yet. - if (!diffsArePartial) { - while (!localCursor.isAfterLast() && !TextUtils.isEmpty(localCursor.getString(2))) { - if (mIsMergeCancelled) { - return; - } - localCount++; - final String localSyncId = localCursor.getString(2); - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, - "deleting local record " + - localCursor.getLong(1) + - " _sync_id " + localSyncId); - } - deleteRow(localCursor); - if (mDeletedTable != null) { - mDb.delete(mDeletedTable, _SYNC_ID + "=?", new String[] {localSyncId}); - } - syncResult.stats.numDeletes++; - mDb.yieldIfContended(); - } - } - if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "checked " + localCount + - " local entries"); - } finally { - if (diffsCursor != null) diffsCursor.close(); - if (localCursor != null) localCursor.close(); - if (deletedCursor != null) deletedCursor.close(); - } - - - if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "applying deletions from the server"); - - // Apply deletions from the server - if (mDeletedTableURL != null) { - diffsCursor = serverDiffs.query(mDeletedTableURL, null, null, null, null); - try { - while (diffsCursor.moveToNext()) { - if (mIsMergeCancelled) { - return; - } - // delete all rows that match each element in the diffsCursor - fullyDeleteMatchingRows(diffsCursor, account, syncResult); - mDb.yieldIfContended(); - } - } finally { - diffsCursor.close(); - } - } - } - - private void fullyDeleteMatchingRows(Cursor diffsCursor, Account account, - SyncResult syncResult) { - int serverSyncIdColumn = diffsCursor.getColumnIndexOrThrow(_SYNC_ID); - final boolean deleteBySyncId = !diffsCursor.isNull(serverSyncIdColumn); - - // delete the rows explicitly so that the delete operation can be overridden - final String[] selectionArgs; - Cursor c = null; - try { - if (deleteBySyncId) { - selectionArgs = new String[]{diffsCursor.getString(serverSyncIdColumn), - account.name, account.type}; - c = mDb.query(mTable, new String[]{BaseColumns._ID}, SELECT_BY_SYNC_ID_AND_ACCOUNT, - selectionArgs, null, null, null); - } else { - int serverSyncLocalIdColumn = diffsCursor.getColumnIndexOrThrow(_SYNC_LOCAL_ID); - selectionArgs = new String[]{diffsCursor.getString(serverSyncLocalIdColumn)}; - c = mDb.query(mTable, new String[]{BaseColumns._ID}, SELECT_BY_ID, selectionArgs, - null, null, null); - } - c.moveToFirst(); - while (!c.isAfterLast()) { - deleteRow(c); // advances the cursor - syncResult.stats.numDeletes++; - } - } finally { - if (c != null) c.close(); - } - if (deleteBySyncId && mDeletedTable != null) { - mDb.delete(mDeletedTable, SELECT_BY_SYNC_ID_AND_ACCOUNT, selectionArgs); - } - } - - /** - * Converts cursor into a Map, using the correct types for the values. - */ - protected void cursorRowToContentValues(Cursor cursor, ContentValues map) { - DatabaseUtils.cursorRowToContentValues(cursor, map); - } - - /** - * Finds local changes, placing the results in the given result object. - * @param temporaryInstanceFactory As an optimization for the case - * where there are no client-side diffs, mergeResult may initially - * have no {@link TempProviderSyncResult#tempContentProvider}. If this is - * the first in the sequence of AbstractTableMergers to find - * client-side diffs, it will use the given ContentProvider to - * create a temporary instance and store its {@link - * android.content.ContentProvider} in the mergeResult. - * @param account - * @param syncResult - */ - private void findLocalChanges(TempProviderSyncResult mergeResult, - SyncableContentProvider temporaryInstanceFactory, Account account, - SyncResult syncResult) { - SyncableContentProvider clientDiffs = mergeResult.tempContentProvider; - if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "generating client updates"); - - final String[] accountSelectionArgs = new String[]{account.name, account.type}; - - // Generate the client updates and insertions - // Create a cursor for dirty records - long numInsertsOrUpdates = 0; - Cursor localChangesCursor = mDb.query(mTable, null, SELECT_UNSYNCED, accountSelectionArgs, - null, null, null); - try { - numInsertsOrUpdates = localChangesCursor.getCount(); - while (localChangesCursor.moveToNext()) { - if (mIsMergeCancelled) { - return; - } - if (clientDiffs == null) { - clientDiffs = temporaryInstanceFactory.getTemporaryInstance(); - } - mValues.clear(); - cursorRowToContentValues(localChangesCursor, mValues); - mValues.remove("_id"); - DatabaseUtils.cursorLongToContentValues(localChangesCursor, "_id", mValues, - _SYNC_LOCAL_ID); - clientDiffs.insert(mTableURL, mValues); - } - } finally { - localChangesCursor.close(); - } - - // Generate the client deletions - if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "generating client deletions"); - long numEntries = DatabaseUtils.queryNumEntries(mDb, mTable); - long numDeletedEntries = 0; - if (mDeletedTable != null) { - Cursor deletedCursor = mDb.query(mDeletedTable, - syncIdAndVersionProjection, - _SYNC_ACCOUNT + "=? AND " + _SYNC_ACCOUNT_TYPE + "=? AND " - + _SYNC_ID + " IS NOT NULL", accountSelectionArgs, - null, null, mDeletedTable + "." + _SYNC_ID); - try { - numDeletedEntries = deletedCursor.getCount(); - while (deletedCursor.moveToNext()) { - if (mIsMergeCancelled) { - return; - } - if (clientDiffs == null) { - clientDiffs = temporaryInstanceFactory.getTemporaryInstance(); - } - mValues.clear(); - DatabaseUtils.cursorRowToContentValues(deletedCursor, mValues); - clientDiffs.insert(mDeletedTableURL, mValues); - } - } finally { - deletedCursor.close(); - } - } - - if (clientDiffs != null) { - mergeResult.tempContentProvider = clientDiffs; - } - syncResult.stats.numDeletes += numDeletedEntries; - syncResult.stats.numUpdates += numInsertsOrUpdates; - syncResult.stats.numEntries += numEntries; - } -} diff --git a/core/java/android/content/AbstractThreadedSyncAdapter.java b/core/java/android/content/AbstractThreadedSyncAdapter.java index fb6091a..14bc5dd 100644 --- a/core/java/android/content/AbstractThreadedSyncAdapter.java +++ b/core/java/android/content/AbstractThreadedSyncAdapter.java @@ -17,10 +17,11 @@ package android.content; import android.accounts.Account; +import android.net.TrafficStats; import android.os.Bundle; -import android.os.Process; -import android.os.NetStat; import android.os.IBinder; +import android.os.Process; +import android.os.RemoteException; import android.util.EventLog; import java.util.concurrent.atomic.AtomicInteger; @@ -36,6 +37,12 @@ import java.util.concurrent.atomic.AtomicInteger; * that the sync has been canceled. */ public abstract class AbstractThreadedSyncAdapter { + /** + * Kernel event log tag. Also listed in data/etc/event-log-tags. + * @Deprecated + */ + public static final int LOG_SYNC_DETAILS = 2743; + private final Context mContext; private final AtomicInteger mNumSyncStarts; private final ISyncAdapterImpl mISyncAdapterImpl; @@ -44,9 +51,6 @@ public abstract class AbstractThreadedSyncAdapter { private SyncThread mSyncThread; private final Object mSyncThreadLock = new Object(); - /** Kernel event log tag. Also listed in data/etc/event-log-tags. */ - public static final int LOG_SYNC_DETAILS = 2743; - private static final String TAG = "Sync"; private final boolean mAutoInitialize; /** @@ -113,10 +117,16 @@ public abstract class AbstractThreadedSyncAdapter { if (mSyncThread != null && mSyncThread.mSyncContext.getSyncContextBinder() == syncContext.asBinder()) { - mSyncThread.interrupt(); + onSyncCanceled(mSyncThread); } } } + + public void initialize(Account account, String authority) throws RemoteException { + Bundle extras = new Bundle(); + extras.putBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, true); + startSync(null, authority, account, extras); + } } /** @@ -129,8 +139,6 @@ public abstract class AbstractThreadedSyncAdapter { private final String mAuthority; private final Account mAccount; private final Bundle mExtras; - private long mInitialTxBytes; - private long mInitialRxBytes; private SyncThread(String name, SyncContext syncContext, String authority, Account account, Bundle extras) { @@ -149,9 +157,6 @@ public abstract class AbstractThreadedSyncAdapter { } SyncResult syncResult = new SyncResult(); - int uid = Process.myUid(); - mInitialTxBytes = NetStat.getUidTxBytes(uid); - mInitialRxBytes = NetStat.getUidRxBytes(uid); ContentProviderClient provider = null; try { provider = mContext.getContentResolver().acquireContentProviderClient(mAuthority); @@ -168,8 +173,6 @@ public abstract class AbstractThreadedSyncAdapter { if (!isCanceled()) { mSyncContext.onFinished(syncResult); } - onLogSyncDetails(NetStat.getUidTxBytes(uid) - mInitialTxBytes, - NetStat.getUidRxBytes(uid) - mInitialRxBytes, syncResult); // synchronize so that the assignment will be seen by other threads // that also synchronize accesses to mSyncThread synchronized (mSyncThreadLock) { @@ -206,16 +209,13 @@ public abstract class AbstractThreadedSyncAdapter { String authority, ContentProviderClient provider, SyncResult syncResult); /** - * Logs details on the sync. - * Normally this will be overridden by a subclass that will provide - * provider-specific details. + * Indicates that a sync operation has been canceled. This will be invoked on a separate + * thread than the sync thread and so you must consider the multi-threaded implications + * of the work that you do in this method. * - * @param bytesSent number of bytes the sync sent over the network - * @param bytesReceived number of bytes the sync received over the network - * @param result The SyncResult object holding info on the sync - * @hide + * @param thread the thread that is running the sync operation to cancel */ - protected void onLogSyncDetails(long bytesSent, long bytesReceived, SyncResult result) { - EventLog.writeEvent(SyncAdapter.LOG_SYNC_DETAILS, TAG, bytesSent, bytesReceived, ""); + public void onSyncCanceled(Thread thread) { + thread.interrupt(); } } diff --git a/core/java/android/content/AsyncQueryHandler.java b/core/java/android/content/AsyncQueryHandler.java index 0a4a804..882879b 100644 --- a/core/java/android/content/AsyncQueryHandler.java +++ b/core/java/android/content/AsyncQueryHandler.java @@ -38,7 +38,6 @@ public abstract class AsyncQueryHandler extends Handler { private static final int EVENT_ARG_INSERT = 2; private static final int EVENT_ARG_UPDATE = 3; private static final int EVENT_ARG_DELETE = 4; - private static final int EVENT_ARG_QUERY_ENTITIES = 5; /* package */ final WeakReference<ContentResolver> mResolver; @@ -93,18 +92,6 @@ public abstract class AsyncQueryHandler extends Handler { args.result = cursor; break; - case EVENT_ARG_QUERY_ENTITIES: - EntityIterator iterator = null; - try { - iterator = resolver.queryEntities(args.uri, args.selection, - args.selectionArgs, args.orderBy); - } catch (Exception e) { - Log.w(TAG, e.toString()); - } - - args.result = iterator; - break; - case EVENT_ARG_INSERT: args.result = resolver.insert(args.uri, args.values); break; @@ -195,45 +182,6 @@ public abstract class AsyncQueryHandler extends Handler { } /** - * This method begins an asynchronous query for an {@link EntityIterator}. - * When the query is done {@link #onQueryEntitiesComplete} is called. - * - * @param token A token passed into {@link #onQueryComplete} to identify the - * query. - * @param cookie An object that gets passed into {@link #onQueryComplete} - * @param uri The URI, using the content:// scheme, for the content to - * retrieve. - * @param selection A filter declaring which rows to return, formatted as an - * SQL WHERE clause (excluding the WHERE itself). Passing null - * will return all rows for the given URI. - * @param selectionArgs You may include ?s in selection, which will be - * replaced by the values from selectionArgs, in the order that - * they appear in the selection. The values will be bound as - * Strings. - * @param orderBy How to order the rows, formatted as an SQL ORDER BY clause - * (excluding the ORDER BY itself). Passing null will use the - * default sort order, which may be unordered. - * @hide - */ - public void startQueryEntities(int token, Object cookie, Uri uri, String selection, - String[] selectionArgs, String orderBy) { - // Use the token as what so cancelOperations works properly - Message msg = mWorkerThreadHandler.obtainMessage(token); - msg.arg1 = EVENT_ARG_QUERY_ENTITIES; - - WorkerArgs args = new WorkerArgs(); - args.handler = this; - args.uri = uri; - args.selection = selection; - args.selectionArgs = selectionArgs; - args.orderBy = orderBy; - args.cookie = cookie; - msg.obj = args; - - mWorkerThreadHandler.sendMessage(msg); - } - - /** * Attempts to cancel operation that has not already started. Note that * there is no guarantee that the operation will be canceled. They still may * result in a call to on[Query/Insert/Update/Delete]Complete after this @@ -340,18 +288,6 @@ public abstract class AsyncQueryHandler extends Handler { } /** - * Called when an asynchronous query is completed. - * - * @param token The token to identify the query. - * @param cookie The cookie object. - * @param iterator The iterator holding the query results. - * @hide - */ - protected void onQueryEntitiesComplete(int token, Object cookie, EntityIterator iterator) { - // Empty - } - - /** * Called when an asynchronous insert is completed. * * @param token the token to identify the query, passed in from @@ -408,10 +344,6 @@ public abstract class AsyncQueryHandler extends Handler { onQueryComplete(token, args.cookie, (Cursor) args.result); break; - case EVENT_ARG_QUERY_ENTITIES: - onQueryEntitiesComplete(token, args.cookie, (EntityIterator)args.result); - break; - case EVENT_ARG_INSERT: onInsertComplete(token, args.cookie, (Uri) args.result); break; diff --git a/core/java/android/content/ContentProvider.java b/core/java/android/content/ContentProvider.java index a341c9b..91b1c4e 100644 --- a/core/java/android/content/ContentProvider.java +++ b/core/java/android/content/ContentProvider.java @@ -67,6 +67,11 @@ import java.util.ArrayList; * process a request is coming from.</p> */ public abstract class ContentProvider implements ComponentCallbacks { + /* + * Note: if you add methods to ContentProvider, you must add similar methods to + * MockContentProvider. + */ + private Context mContext = null; private int mMyUid; private String mReadPermission; @@ -75,6 +80,33 @@ public abstract class ContentProvider implements ComponentCallbacks { private Transport mTransport = new Transport(); + public ContentProvider() { + } + + /** + * Constructor just for mocking. + * + * @param context A Context object which should be some mock instance (like the + * instance of {@link android.test.mock.MockContext}). + * @param readPermission The read permision you want this instance should have in the + * test, which is available via {@link #getReadPermission()}. + * @param writePermission The write permission you want this instance should have + * in the test, which is available via {@link #getWritePermission()}. + * @param pathPermissions The PathPermissions you want this instance should have + * in the test, which is available via {@link #getPathPermissions()}. + * @hide + */ + public ContentProvider( + Context context, + String readPermission, + String writePermission, + PathPermission[] pathPermissions) { + mContext = context; + mReadPermission = readPermission; + mWritePermission = writePermission; + mPathPermissions = pathPermissions; + } + /** * Given an IContentProvider, try to coerce it back to the real * ContentProvider object if it is running in the local process. This can @@ -131,15 +163,6 @@ public abstract class ContentProvider implements ComponentCallbacks { selectionArgs, sortOrder); } - /** - * @hide - */ - public EntityIterator queryEntities(Uri uri, String selection, String[] selectionArgs, - String sortOrder) { - enforceReadPermission(uri); - return ContentProvider.this.queryEntities(uri, selection, selectionArgs, sortOrder); - } - public String getType(Uri uri) { return ContentProvider.this.getType(uri); } @@ -445,14 +468,6 @@ public abstract class ContentProvider implements ComponentCallbacks { String selection, String[] selectionArgs, String sortOrder); /** - * @hide - */ - public EntityIterator queryEntities(Uri uri, String selection, String[] selectionArgs, - String sortOrder) { - throw new UnsupportedOperationException(); - } - - /** * Return the MIME type of the data at the given URI. This should start with * <code>vnd.android.cursor.item</code> for a single record, * or <code>vnd.android.cursor.dir/</code> for multiple items. @@ -549,7 +564,7 @@ public abstract class ContentProvider implements ComponentCallbacks { /** * Open a file blob associated with a content URI. * This method can be called from multiple - * threads, as described inentity + * threads, as described in * <a href="{@docRoot}guide/topics/fundamentals.html#procthread">Application Fundamentals: * Processes and Threads</a>. * diff --git a/core/java/android/content/ContentProviderClient.java b/core/java/android/content/ContentProviderClient.java index 403c4d8..0858ea5 100644 --- a/core/java/android/content/ContentProviderClient.java +++ b/core/java/android/content/ContentProviderClient.java @@ -89,16 +89,7 @@ public class ContentProviderClient { return mContentProvider.openAssetFile(url, mode); } - /** - * see {@link ContentProvider#queryEntities} - * @hide - */ - public EntityIterator queryEntities(Uri uri, String selection, String[] selectionArgs, - String sortOrder) throws RemoteException { - return mContentProvider.queryEntities(uri, selection, selectionArgs, sortOrder); - } - - /** see {@link ContentProvider#applyBatch} */ + /** see {@link ContentProvider#applyBatch} */ public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) throws RemoteException, OperationApplicationException { return mContentProvider.applyBatch(operations); diff --git a/core/java/android/content/ContentProviderNative.java b/core/java/android/content/ContentProviderNative.java index adc3f60..bacb684 100644 --- a/core/java/android/content/ContentProviderNative.java +++ b/core/java/android/content/ContentProviderNative.java @@ -106,20 +106,6 @@ abstract public class ContentProviderNative extends Binder implements IContentPr return true; } - case QUERY_ENTITIES_TRANSACTION: - { - data.enforceInterface(IContentProvider.descriptor); - Uri url = Uri.CREATOR.createFromParcel(data); - String selection = data.readString(); - String[] selectionArgs = data.readStringArray(); - String sortOrder = data.readString(); - EntityIterator entityIterator = queryEntities(url, selection, selectionArgs, - sortOrder); - reply.writeNoException(); - reply.writeStrongBinder(new IEntityIteratorImpl(entityIterator).asBinder()); - return true; - } - case GET_TYPE_TRANSACTION: { data.enforceInterface(IContentProvider.descriptor); @@ -245,32 +231,6 @@ abstract public class ContentProviderNative extends Binder implements IContentPr return super.onTransact(code, data, reply, flags); } - /** - * @hide - */ - private class IEntityIteratorImpl extends IEntityIterator.Stub { - private final EntityIterator mEntityIterator; - - IEntityIteratorImpl(EntityIterator iterator) { - mEntityIterator = iterator; - } - public boolean hasNext() throws RemoteException { - return mEntityIterator.hasNext(); - } - - public Entity next() throws RemoteException { - return mEntityIterator.next(); - } - - public void reset() throws RemoteException { - mEntityIterator.reset(); - } - - public void close() throws RemoteException { - mEntityIterator.close(); - } - } - public IBinder asBinder() { return this; @@ -352,64 +312,6 @@ final class ContentProviderProxy implements IContentProvider return adaptor; } - /** - * @hide - */ - public EntityIterator queryEntities(Uri url, String selection, String[] selectionArgs, - String sortOrder) - throws RemoteException { - Parcel data = Parcel.obtain(); - Parcel reply = Parcel.obtain(); - - data.writeInterfaceToken(IContentProvider.descriptor); - - url.writeToParcel(data, 0); - data.writeString(selection); - data.writeStringArray(selectionArgs); - data.writeString(sortOrder); - - mRemote.transact(IContentProvider.QUERY_ENTITIES_TRANSACTION, data, reply, 0); - - DatabaseUtils.readExceptionFromParcel(reply); - - IBinder entityIteratorBinder = reply.readStrongBinder(); - - data.recycle(); - reply.recycle(); - - return new RemoteEntityIterator(IEntityIterator.Stub.asInterface(entityIteratorBinder)); - } - - /** - * @hide - */ - static class RemoteEntityIterator implements EntityIterator { - private final IEntityIterator mEntityIterator; - RemoteEntityIterator(IEntityIterator entityIterator) { - mEntityIterator = entityIterator; - } - - public boolean hasNext() throws RemoteException { - return mEntityIterator.hasNext(); - } - - public Entity next() throws RemoteException { - return mEntityIterator.next(); - } - - public void reset() throws RemoteException { - mEntityIterator.reset(); - } - - public void close() { - try { - mEntityIterator.close(); - } catch (RemoteException e) { - // doesn't matter - } - } - } - public String getType(Uri url) throws RemoteException { Parcel data = Parcel.obtain(); diff --git a/core/java/android/content/ContentResolver.java b/core/java/android/content/ContentResolver.java index c4b0807..eb2d7b1 100644 --- a/core/java/android/content/ContentResolver.java +++ b/core/java/android/content/ContentResolver.java @@ -88,11 +88,11 @@ public abstract class ContentResolver { * <code>content://com.company.provider.imap/inbox/1</code> for a particular * message in the inbox, whose MIME type would be reported as * <code>CURSOR_ITEM_BASE_TYPE + "/vnd.company.imap-msg"</code> - * + * * <p>Compare with {@link #CURSOR_DIR_BASE_TYPE}. */ public static final String CURSOR_ITEM_BASE_TYPE = "vnd.android.cursor.item"; - + /** * This is the Android platform's base MIME type for a content: URI * containing a Cursor of zero or more items. Applications should use this @@ -102,7 +102,7 @@ public abstract class ContentResolver { * <code>content://com.company.provider.imap/inbox</code> for all of the * messages in its inbox, whose MIME type would be reported as * <code>CURSOR_DIR_BASE_TYPE + "/vnd.company.imap-msg"</code> - * + * * <p>Note how the base MIME type varies between this and * {@link #CURSOR_ITEM_BASE_TYPE} depending on whether there is * one single item or multiple items in the data set, while the sub-type @@ -173,13 +173,25 @@ public abstract class ContentResolver { } /** + * <p> * Query the given URI, returning a {@link Cursor} over the result set. + * </p> + * <p> + * For best performance, the caller should follow these guidelines: + * <ul> + * <li>Provide an explicit projection, to prevent + * reading data from storage that aren't going to be used.</li> + * <li>Use question mark parameter markers such as 'phone=?' instead of + * explicit values in the {@code selection} parameter, so that queries + * that differ only by those values will be recognized as the same + * for caching purposes.</li> + * </ul> + * </p> * * @param uri The URI, using the content:// scheme, for the content to * retrieve. * @param projection A list of which columns to return. Passing null will - * return all columns, which is discouraged to prevent reading data - * from storage that isn't going to be used. + * return all columns, which is inefficient. * @param selection A filter declaring which rows to return, formatted as an * SQL WHERE clause (excluding the WHERE itself). Passing null will * return all rows for the given URI. @@ -216,96 +228,6 @@ public abstract class ContentResolver { } /** - * EntityIterator wrapper that releases the associated ContentProviderClient when the - * iterator is closed. - * @hide - */ - private class EntityIteratorWrapper implements EntityIterator { - private final EntityIterator mInner; - private final ContentProviderClient mClient; - private volatile boolean mClientReleased; - - EntityIteratorWrapper(EntityIterator inner, ContentProviderClient client) { - mInner = inner; - mClient = client; - mClientReleased = false; - } - - public boolean hasNext() throws RemoteException { - if (mClientReleased) { - throw new IllegalStateException("this iterator is already closed"); - } - return mInner.hasNext(); - } - - public Entity next() throws RemoteException { - if (mClientReleased) { - throw new IllegalStateException("this iterator is already closed"); - } - return mInner.next(); - } - - public void reset() throws RemoteException { - if (mClientReleased) { - throw new IllegalStateException("this iterator is already closed"); - } - mInner.reset(); - } - - public void close() { - mClient.release(); - mInner.close(); - mClientReleased = true; - } - - protected void finalize() throws Throwable { - if (!mClientReleased) { - mClient.release(); - } - super.finalize(); - } - } - - /** - * Query the given URI, returning an {@link EntityIterator} over the result set. - * - * @param uri The URI, using the content:// scheme, for the content to - * retrieve. - * @param selection A filter declaring which rows to return, formatted as an - * SQL WHERE clause (excluding the WHERE itself). Passing null will - * return all rows for the given URI. - * @param selectionArgs You may include ?s in selection, which will be - * replaced by the values from selectionArgs, in the order that they - * appear in the selection. The values will be bound as Strings. - * @param sortOrder How to order the rows, formatted as an SQL ORDER BY - * clause (excluding the ORDER BY itself). Passing null will use the - * default sort order, which may be unordered. - * @return An EntityIterator object - * @throws RemoteException thrown if a RemoteException is encountered while attempting - * to communicate with a remote provider. - * @throws IllegalArgumentException thrown if there is no provider that matches the uri - * @hide - */ - public final EntityIterator queryEntities(Uri uri, - String selection, String[] selectionArgs, String sortOrder) throws RemoteException { - ContentProviderClient provider = acquireContentProviderClient(uri); - if (provider == null) { - throw new IllegalArgumentException("Unknown URL " + uri); - } - try { - EntityIterator entityIterator = - provider.queryEntities(uri, selection, selectionArgs, sortOrder); - return new EntityIteratorWrapper(entityIterator, provider); - } catch(RuntimeException e) { - provider.release(); - throw e; - } catch(RemoteException e) { - provider.release(); - throw e; - } - } - - /** * Open a stream on to the content associated with a content URI. If there * is no data associated with the URI, FileNotFoundException is thrown. * @@ -315,10 +237,10 @@ public abstract class ContentResolver { * <li>android.resource ({@link #SCHEME_ANDROID_RESOURCE})</li> * <li>file ({@link #SCHEME_FILE})</li> * </ul> - * + * * <p>See {@link #openAssetFileDescriptor(Uri, String)} for more information * on these schemes. - * + * * @param uri The desired URI. * @return InputStream * @throws FileNotFoundException if the provided URI could not be opened. @@ -373,7 +295,7 @@ public abstract class ContentResolver { * * <p>See {@link #openAssetFileDescriptor(Uri, String)} for more information * on these schemes. - * + * * @param uri The desired URI. * @param mode May be "w", "wa", "rw", or "rwt". * @return OutputStream @@ -408,7 +330,7 @@ public abstract class ContentResolver { * * <p>See {@link #openAssetFileDescriptor(Uri, String)} for more information * on these schemes. - * + * * @param uri The desired URI to open. * @param mode The file mode to use, as per {@link ContentProvider#openFile * ContentProvider.openFile}. @@ -424,19 +346,19 @@ public abstract class ContentResolver { if (afd == null) { return null; } - + if (afd.getDeclaredLength() < 0) { // This is a full file! return afd.getParcelFileDescriptor(); } - + // Client can't handle a sub-section of a file, so close what // we got and bail with an exception. try { afd.close(); } catch (IOException e) { } - + throw new FileNotFoundException("Not a whole file"); } @@ -581,7 +503,7 @@ public abstract class ContentResolver { res.id = id; return res; } - + /** @hide */ static public int modeToMode(Uri uri, String mode) throws FileNotFoundException { int modeBits; @@ -608,7 +530,7 @@ public abstract class ContentResolver { } return modeBits; } - + /** * Inserts a row into a table at the given URL. * @@ -1236,7 +1158,7 @@ public abstract class ContentResolver { /** @hide */ public static final String CONTENT_SERVICE_NAME = "content"; - + /** @hide */ public static IContentService getContentService() { if (sContentService != null) { @@ -1248,7 +1170,7 @@ public abstract class ContentResolver { if (Config.LOGV) Log.v("ContentService", "default service = " + sContentService); return sContentService; } - + private static IContentService sContentService; private final Context mContext; private static final String TAG = "ContentResolver"; diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 799bc22..0fafe5d 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -104,6 +104,18 @@ public abstract class Context { */ public static final int BIND_DEBUG_UNBIND = 0x0002; + /** + * Flag for {@link #bindService}: don't allow this binding to raise + * the target service's process to the foreground scheduling priority. + * It will still be raised to the at least the same memory priority + * as the client (so that its process will not be killable in any + * situation where the client is not killable), but for CPU scheduling + * purposes it may be left in the background. This only has an impact + * in the situation where the binding client is a foreground process + * and the target service is in a background process. + */ + public static final int BIND_NOT_FOREGROUND = 0x0004; + /** Return an AssetManager instance for your application's package. */ public abstract AssetManager getAssets(); @@ -1302,23 +1314,31 @@ public abstract class Context { /** * Use with {@link #getSystemService} to retrieve a - * {@blink android.appwidget.AppWidgetManager} for accessing AppWidgets. + * {@link android.appwidget.AppWidgetManager} for accessing AppWidgets. * * @hide * @see #getSystemService */ public static final String APPWIDGET_SERVICE = "appwidget"; - + /** * Use with {@link #getSystemService} to retrieve an - * {@blink android.backup.IBackupManager IBackupManager} for communicating + * {@link android.backup.IBackupManager IBackupManager} for communicating * with the backup mechanism. * @hide * * @see #getSystemService */ public static final String BACKUP_SERVICE = "backup"; - + + /** + * Use with {@link #getSystemService} to retrieve a + * {@link android.os.DropBoxManager} instance for recording + * diagnostic logs. + * @see #getSystemService + */ + public static final String DROPBOX_SERVICE = "dropbox"; + /** * Determine whether the given permission is allowed for a particular * process and user ID running in the system. diff --git a/core/java/android/content/CursorEntityIterator.java b/core/java/android/content/CursorEntityIterator.java new file mode 100644 index 0000000..0c66646 --- /dev/null +++ b/core/java/android/content/CursorEntityIterator.java @@ -0,0 +1,88 @@ +package android.content; + +import android.database.Cursor; +import android.os.RemoteException; + +/** + * Abstract implementation of EntityIterator that makes it easy to wrap a cursor + * that can contain several consecutive rows for an entity. + * @hide + */ +public abstract class CursorEntityIterator implements EntityIterator { + private final Cursor mCursor; + private boolean mIsClosed; + + /** + * Constructor that makes initializes the cursor such that the iterator points to the + * first Entity, if there are any. + * @param cursor the cursor that contains the rows that make up the entities + */ + public CursorEntityIterator(Cursor cursor) { + mIsClosed = false; + mCursor = cursor; + mCursor.moveToFirst(); + } + + /** + * Returns the entity that the cursor is currently pointing to. This must take care to advance + * the cursor past this entity. This will never be called if the cursor is at the end. + * @param cursor the cursor that contains the entity rows + * @return the entity that the cursor is currently pointing to + * @throws RemoteException if a RemoteException is caught while attempting to build the Entity + */ + public abstract Entity getEntityAndIncrementCursor(Cursor cursor) throws RemoteException; + + /** + * Returns whether there are more elements to iterate, i.e. whether the + * iterator is positioned in front of an element. + * + * @return {@code true} if there are more elements, {@code false} otherwise. + * @see #next + */ + public final boolean hasNext() throws RemoteException { + if (mIsClosed) { + throw new IllegalStateException("calling hasNext() when the iterator is closed"); + } + + return !mCursor.isAfterLast(); + } + + /** + * Returns the next object in the iteration, i.e. returns the element in + * front of the iterator and advances the iterator by one position. + * + * @return the next object. + * @throws java.util.NoSuchElementException + * if there are no more elements. + * @see #hasNext + */ + public Entity next() throws RemoteException { + if (mIsClosed) { + throw new IllegalStateException("calling next() when the iterator is closed"); + } + if (!hasNext()) { + throw new IllegalStateException("you may only call next() if hasNext() is true"); + } + + return getEntityAndIncrementCursor(mCursor); + } + + public final void reset() throws RemoteException { + if (mIsClosed) { + throw new IllegalStateException("calling reset() when the iterator is closed"); + } + mCursor.moveToFirst(); + } + + /** + * Indicates that this iterator is no longer needed and that any associated resources + * may be released (such as a SQLite cursor). + */ + public final void close() { + if (mIsClosed) { + throw new IllegalStateException("closing when already closed"); + } + mIsClosed = true; + mCursor.close(); + } +} diff --git a/core/java/android/content/Entity.aidl b/core/java/android/content/Entity.aidl deleted file mode 100644 index fb201f3..0000000 --- a/core/java/android/content/Entity.aidl +++ /dev/null @@ -1,20 +0,0 @@ -/* //device/java/android/android/content/Entity.aidl -** -** Copyright 2007, The Android Open Source Project -** -** Licensed under the Apache License, Version 2.0 (the "License"); -** you may not use this file except in compliance with the License. -** You may obtain a copy of the License at -** -** http://www.apache.org/licenses/LICENSE-2.0 -** -** Unless required by applicable law or agreed to in writing, software -** distributed under the License is distributed on an "AS IS" BASIS, -** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -** See the License for the specific language governing permissions and -** limitations under the License. -*/ - -package android.content; - -parcelable Entity; diff --git a/core/java/android/content/Entity.java b/core/java/android/content/Entity.java index ee8112e..7842de0 100644 --- a/core/java/android/content/Entity.java +++ b/core/java/android/content/Entity.java @@ -24,11 +24,13 @@ import android.util.Log; import java.util.ArrayList; /** - * Objects that pass through the ContentProvider and ContentResolver's methods that deal with - * Entities must implement this abstract base class and thus themselves be Parcelable. - * @hide + * A representation of a item using ContentValues. It contains one top level ContentValue + * plus a collection of Uri, ContentValues tuples as subvalues. One example of its use + * is in Contacts, where the top level ContentValue contains the columns from the RawContacts + * table and the subvalues contain a ContentValues object for each row from the Data table that + * corresponds to that RawContact. The uri refers to the Data table uri for each row. */ -public final class Entity implements Parcelable { +public final class Entity { final private ContentValues mValues; final private ArrayList<NamedContentValues> mSubValues; @@ -49,40 +51,6 @@ public final class Entity implements Parcelable { mSubValues.add(new Entity.NamedContentValues(uri, values)); } - public int describeContents() { - return 0; - } - - public void writeToParcel(Parcel dest, int flags) { - mValues.writeToParcel(dest, 0); - dest.writeInt(mSubValues.size()); - for (NamedContentValues value : mSubValues) { - value.uri.writeToParcel(dest, 0); - value.values.writeToParcel(dest, 0); - } - } - - private Entity(Parcel source) { - mValues = ContentValues.CREATOR.createFromParcel(source); - final int numValues = source.readInt(); - mSubValues = new ArrayList<NamedContentValues>(numValues); - for (int i = 0; i < numValues; i++) { - final Uri uri = Uri.CREATOR.createFromParcel(source); - final ContentValues values = ContentValues.CREATOR.createFromParcel(source); - mSubValues.add(new NamedContentValues(uri, values)); - } - } - - public static final Creator<Entity> CREATOR = new Creator<Entity>() { - public Entity createFromParcel(Parcel source) { - return new Entity(source); - } - - public Entity[] newArray(int size) { - return new Entity[size]; - } - }; - public static class NamedContentValues { public final Uri uri; public final ContentValues values; diff --git a/core/java/android/content/EntityIterator.java b/core/java/android/content/EntityIterator.java index 1b73439..3cc1040 100644 --- a/core/java/android/content/EntityIterator.java +++ b/core/java/android/content/EntityIterator.java @@ -18,9 +18,6 @@ package android.content; import android.os.RemoteException; -/** - * @hide - */ public interface EntityIterator { /** * Returns whether there are more elements to iterate, i.e. whether the diff --git a/core/java/android/content/IContentProvider.java b/core/java/android/content/IContentProvider.java index 0798adf..1b0ca58 100644 --- a/core/java/android/content/IContentProvider.java +++ b/core/java/android/content/IContentProvider.java @@ -44,12 +44,6 @@ public interface IContentProvider extends IInterface { CursorWindow window) throws RemoteException; public Cursor query(Uri url, String[] projection, String selection, String[] selectionArgs, String sortOrder) throws RemoteException; - /** - * @hide - */ - public EntityIterator queryEntities(Uri url, String selection, - String[] selectionArgs, String sortOrder) - throws RemoteException; public String getType(Uri url) throws RemoteException; public Uri insert(Uri url, ContentValues initialValues) throws RemoteException; @@ -76,9 +70,5 @@ public interface IContentProvider extends IInterface { static final int BULK_INSERT_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 12; static final int OPEN_FILE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 13; static final int OPEN_ASSET_FILE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 14; - /** - * @hide - */ - static final int QUERY_ENTITIES_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 18; static final int APPLY_BATCH_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 19; } diff --git a/core/java/android/content/IEntityIterator.java b/core/java/android/content/IEntityIterator.java deleted file mode 100644 index 068581e..0000000 --- a/core/java/android/content/IEntityIterator.java +++ /dev/null @@ -1,210 +0,0 @@ -/* - * 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 android.content; - -import android.os.Binder; -import android.os.IBinder; -import android.os.IInterface; -import android.os.Parcel; -import android.os.RemoteException; -import android.os.Parcelable; -import android.util.Log; - -/** - * ICPC interface methods for an iterator over Entity objects. - * @hide - */ -public interface IEntityIterator extends IInterface { - /** Local-side IPC implementation stub class. */ - public static abstract class Stub extends Binder implements IEntityIterator { - private static final String TAG = "IEntityIterator"; - private static final java.lang.String DESCRIPTOR = "android.content.IEntityIterator"; - - /** Construct the stub at attach it to the interface. */ - public Stub() { - this.attachInterface(this, DESCRIPTOR); - } - /** - * Cast an IBinder object into an IEntityIterator interface, - * generating a proxy if needed. - */ - public static IEntityIterator asInterface(IBinder obj) { - if ((obj==null)) { - return null; - } - IInterface iin = obj.queryLocalInterface(DESCRIPTOR); - if (((iin!=null)&&(iin instanceof IEntityIterator))) { - return ((IEntityIterator)iin); - } - return new IEntityIterator.Stub.Proxy(obj); - } - - public IBinder asBinder() { - return this; - } - - public boolean onTransact(int code, Parcel data, Parcel reply, int flags) - throws RemoteException { - switch (code) { - case INTERFACE_TRANSACTION: - { - reply.writeString(DESCRIPTOR); - return true; - } - - case TRANSACTION_hasNext: - { - data.enforceInterface(DESCRIPTOR); - boolean _result; - try { - _result = this.hasNext(); - } catch (Exception e) { - Log.e(TAG, "caught exception in hasNext()", e); - reply.writeException(e); - return true; - } - reply.writeNoException(); - reply.writeInt(((_result)?(1):(0))); - return true; - } - - case TRANSACTION_next: - { - data.enforceInterface(DESCRIPTOR); - Entity entity; - try { - entity = this.next(); - } catch (RemoteException e) { - Log.e(TAG, "caught exception in next()", e); - reply.writeException(e); - return true; - } - reply.writeNoException(); - entity.writeToParcel(reply, Parcelable.PARCELABLE_WRITE_RETURN_VALUE); - return true; - } - - case TRANSACTION_reset: - { - data.enforceInterface(DESCRIPTOR); - try { - this.reset(); - } catch (RemoteException e) { - Log.e(TAG, "caught exception in next()", e); - reply.writeException(e); - return true; - } - reply.writeNoException(); - return true; - } - - case TRANSACTION_close: - { - data.enforceInterface(DESCRIPTOR); - try { - this.close(); - } catch (RemoteException e) { - Log.e(TAG, "caught exception in close()", e); - reply.writeException(e); - return true; - } - reply.writeNoException(); - return true; - } - } - return super.onTransact(code, data, reply, flags); - } - - private static class Proxy implements IEntityIterator { - private IBinder mRemote; - Proxy(IBinder remote) { - mRemote = remote; - } - public IBinder asBinder() { - return mRemote; - } - public java.lang.String getInterfaceDescriptor() { - return DESCRIPTOR; - } - public boolean hasNext() throws RemoteException { - Parcel _data = Parcel.obtain(); - Parcel _reply = Parcel.obtain(); - boolean _result; - try { - _data.writeInterfaceToken(DESCRIPTOR); - mRemote.transact(Stub.TRANSACTION_hasNext, _data, _reply, 0); - _reply.readException(); - _result = (0!=_reply.readInt()); - } - finally { - _reply.recycle(); - _data.recycle(); - } - return _result; - } - - public Entity next() throws RemoteException { - Parcel _data = Parcel.obtain(); - Parcel _reply = Parcel.obtain(); - try { - _data.writeInterfaceToken(DESCRIPTOR); - mRemote.transact(Stub.TRANSACTION_next, _data, _reply, 0); - _reply.readException(); - return Entity.CREATOR.createFromParcel(_reply); - } finally { - _reply.recycle(); - _data.recycle(); - } - } - - public void reset() throws RemoteException { - Parcel _data = Parcel.obtain(); - Parcel _reply = Parcel.obtain(); - try { - _data.writeInterfaceToken(DESCRIPTOR); - mRemote.transact(Stub.TRANSACTION_reset, _data, _reply, 0); - _reply.readException(); - } finally { - _reply.recycle(); - _data.recycle(); - } - } - - public void close() throws RemoteException { - Parcel _data = Parcel.obtain(); - Parcel _reply = Parcel.obtain(); - try { - _data.writeInterfaceToken(DESCRIPTOR); - mRemote.transact(Stub.TRANSACTION_close, _data, _reply, 0); - _reply.readException(); - } - finally { - _reply.recycle(); - _data.recycle(); - } - } - } - static final int TRANSACTION_hasNext = (IBinder.FIRST_CALL_TRANSACTION + 0); - static final int TRANSACTION_next = (IBinder.FIRST_CALL_TRANSACTION + 1); - static final int TRANSACTION_close = (IBinder.FIRST_CALL_TRANSACTION + 2); - static final int TRANSACTION_reset = (IBinder.FIRST_CALL_TRANSACTION + 3); - } - public boolean hasNext() throws RemoteException; - public Entity next() throws RemoteException; - public void reset() throws RemoteException; - public void close() throws RemoteException; -} diff --git a/core/java/android/content/ISyncAdapter.aidl b/core/java/android/content/ISyncAdapter.aidl index 4660527..dd9d14e 100644 --- a/core/java/android/content/ISyncAdapter.aidl +++ b/core/java/android/content/ISyncAdapter.aidl @@ -44,4 +44,12 @@ oneway interface ISyncAdapter { * @param syncContext the ISyncContext that was passed to {@link #startSync} */ void cancelSync(ISyncContext syncContext); + + /** + * Initialize the SyncAdapter for this account and authority. + * + * @param account the account that should be synced + * @param authority the authority that should be synced + */ + void initialize(in Account account, String authority); } diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index a96e896..bf37b62 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -34,7 +34,7 @@ import android.os.Parcel; import android.os.Parcelable; import android.util.AttributeSet; import android.util.Log; -import com.android.internal.util.XmlUtils; +import com.android.common.XmlUtils; import java.io.IOException; import java.io.Serializable; @@ -575,7 +575,7 @@ import java.util.Set; * {@link #setFlags} and {@link #addFlags}. See {@link #setFlags} for a list * of all possible flags. */ -public class Intent implements Parcelable { +public class Intent implements Parcelable, Cloneable { // --------------------------------------------------------------------- // --------------------------------------------------------------------- // Standard intent activity actions (see action variable). @@ -1102,6 +1102,16 @@ public class Intent implements Parcelable { public static final String ACTION_SEARCH_LONG_PRESS = "android.intent.action.SEARCH_LONG_PRESS"; /** + * Activity Action: Start the global search activity. + * <p>Input: Nothing. + * <p>Output: Nothing. + * + * @hide Pending API council approval + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_GLOBAL_SEARCH = "android.intent.action.GLOBAL_SEARCH"; + + /** * Activity Action: The user pressed the "Report" button in the crash/ANR dialog. * This intent is delivered to the package which installed the application, usually * the Market. @@ -1120,7 +1130,7 @@ public class Intent implements Parcelable { */ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_POWER_USAGE_SUMMARY = "android.intent.action.POWER_USAGE_SUMMARY"; - + /** * Activity Action: Setup wizard to launch after a platform update. This * activity should have a string meta-data field associated with it, @@ -1134,7 +1144,7 @@ public class Intent implements Parcelable { */ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_UPGRADE_SETUP = "android.intent.action.UPGRADE_SETUP"; - + /** * A string associated with a {@link #ACTION_UPGRADE_SETUP} activity * describing the last run version of the platform that was setup. @@ -1148,7 +1158,7 @@ public class Intent implements Parcelable { /** * Broadcast Action: Sent after the screen turns off. - * + * * <p class="note">This is a protected intent that can only be sent * by the system. */ @@ -1156,7 +1166,7 @@ public class Intent implements Parcelable { public static final String ACTION_SCREEN_OFF = "android.intent.action.SCREEN_OFF"; /** * Broadcast Action: Sent after the screen turns on. - * + * * <p class="note">This is a protected intent that can only be sent * by the system. */ @@ -1166,12 +1176,12 @@ public class Intent implements Parcelable { /** * Broadcast Action: Sent when the user is present after device wakes up (e.g when the * keyguard is gone). - * + * * <p class="note">This is a protected intent that can only be sent * by the system. */ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) - public static final String ACTION_USER_PRESENT= "android.intent.action.USER_PRESENT"; + public static final String ACTION_USER_PRESENT = "android.intent.action.USER_PRESENT"; /** * Broadcast Action: The current time has changed. Sent every @@ -1179,7 +1189,7 @@ public class Intent implements Parcelable { * in manifests, only by exlicitly registering for it with * {@link Context#registerReceiver(BroadcastReceiver, IntentFilter) * Context.registerReceiver()}. - * + * * <p class="note">This is a protected intent that can only be sent * by the system. */ @@ -1200,7 +1210,7 @@ public class Intent implements Parcelable { * <ul> * <li><em>time-zone</em> - The java.util.TimeZone.getID() value identifying the new time zone.</li> * </ul> - * + * * <p class="note">This is a protected intent that can only be sent * by the system. */ @@ -1228,7 +1238,7 @@ public class Intent implements Parcelable { * such as installing alarms. You must hold the * {@link android.Manifest.permission#RECEIVE_BOOT_COMPLETED} permission * in order to receive this broadcast. - * + * * <p class="note">This is a protected intent that can only be sent * by the system. */ @@ -1244,7 +1254,7 @@ public class Intent implements Parcelable { * Broadcast Action: Trigger the download and eventual installation * of a package. * <p>Input: {@link #getData} is the URI of the package file to download. - * + * * <p class="note">This is a protected intent that can only be sent * by the system. */ @@ -1260,7 +1270,7 @@ public class Intent implements Parcelable { * <li> {@link #EXTRA_REPLACING} is set to true if this is following * an {@link #ACTION_PACKAGE_REMOVED} broadcast for the same package. * </ul> - * + * * <p class="note">This is a protected intent that can only be sent * by the system. */ @@ -1274,7 +1284,7 @@ public class Intent implements Parcelable { * <ul> * <li> {@link #EXTRA_UID} containing the integer uid assigned to the new package. * </ul> - * + * * <p class="note">This is a protected intent that can only be sent * by the system. */ @@ -1292,7 +1302,7 @@ public class Intent implements Parcelable { * <li> {@link #EXTRA_REPLACING} is set to true if this will be followed * by an {@link #ACTION_PACKAGE_ADDED} broadcast for the same package. * </ul> - * + * * <p class="note">This is a protected intent that can only be sent * by the system. */ @@ -1309,7 +1319,7 @@ public class Intent implements Parcelable { * <li> {@link #EXTRA_DONT_KILL_APP} containing boolean field to override the * default action of restarting the application. * </ul> - * + * * <p class="note">This is a protected intent that can only be sent * by the system. */ @@ -1325,7 +1335,7 @@ public class Intent implements Parcelable { * <ul> * <li> {@link #EXTRA_UID} containing the integer uid assigned to the package. * </ul> - * + * * <p class="note">This is a protected intent that can only be sent * by the system. */ @@ -1340,7 +1350,7 @@ public class Intent implements Parcelable { * <ul> * <li> {@link #EXTRA_UID} containing the integer uid assigned to the package. * </ul> - * + * * <p class="note">This is a protected intent that can only be sent * by the system. */ @@ -1349,7 +1359,7 @@ public class Intent implements Parcelable { /** * Broadcast Action: A user ID has been removed from the system. The user * ID number is stored in the extra data under {@link #EXTRA_UID}. - * + * * <p class="note">This is a protected intent that can only be sent * by the system. */ @@ -1370,13 +1380,13 @@ public class Intent implements Parcelable { * application to make sure it sees the new changes. Some system code that * can not be restarted will need to watch for this action and handle it * appropriately. - * + * * <p class="note"> * You can <em>not</em> receive this through components declared * in manifests, only by explicitly registering for it with * {@link Context#registerReceiver(BroadcastReceiver, IntentFilter) * Context.registerReceiver()}. - * + * * <p class="note">This is a protected intent that can only be sent * by the system. * @@ -1386,7 +1396,7 @@ public class Intent implements Parcelable { public static final String ACTION_CONFIGURATION_CHANGED = "android.intent.action.CONFIGURATION_CHANGED"; /** * Broadcast Action: The current device's locale has changed. - * + * * <p class="note">This is a protected intent that can only be sent * by the system. */ @@ -1407,7 +1417,7 @@ public class Intent implements Parcelable { * and {@link #ACTION_POWER_DISCONNECTED} for distinct battery-related * broadcasts that are sent and can be received through manifest * receivers. - * + * * <p class="note">This is a protected intent that can only be sent * by the system. */ @@ -1416,7 +1426,7 @@ public class Intent implements Parcelable { /** * Broadcast Action: Indicates low battery condition on the device. * This broadcast corresponds to the "Low battery warning" system dialog. - * + * * <p class="note">This is a protected intent that can only be sent * by the system. */ @@ -1426,7 +1436,7 @@ public class Intent implements Parcelable { * Broadcast Action: Indicates the battery is now okay after being low. * This will be sent after {@link #ACTION_BATTERY_LOW} once the battery has * gone back up to an okay state. - * + * * <p class="note">This is a protected intent that can only be sent * by the system. */ @@ -1438,7 +1448,7 @@ public class Intent implements Parcelable { * Unlike ACTION_BATTERY_CHANGED, applications will be woken for this and so do not have to * stay active to receive this notification. This action can be used to implement actions * that wait until power is available to trigger. - * + * * <p class="note">This is a protected intent that can only be sent * by the system. */ @@ -1450,7 +1460,7 @@ public class Intent implements Parcelable { * Unlike ACTION_BATTERY_CHANGED, applications will be woken for this and so do not have to * stay active to receive this notification. This action can be used to implement actions * that wait until power is available to trigger. - * + * * <p class="note">This is a protected intent that can only be sent * by the system. */ @@ -1463,7 +1473,7 @@ public class Intent implements Parcelable { * off, not sleeping). Once the broadcast is complete, the final shutdown * will proceed and all unsaved data lost. Apps will not normally need * to handle this, since the foreground activity will be paused as well. - * + * * <p class="note">This is a protected intent that can only be sent * by the system. */ @@ -1483,7 +1493,7 @@ public class Intent implements Parcelable { /** * Broadcast Action: A sticky broadcast that indicates low memory * condition on the device - * + * * <p class="note">This is a protected intent that can only be sent * by the system. */ @@ -1491,7 +1501,7 @@ public class Intent implements Parcelable { public static final String ACTION_DEVICE_STORAGE_LOW = "android.intent.action.DEVICE_STORAGE_LOW"; /** * Broadcast Action: Indicates low memory condition on the device no longer exists - * + * * <p class="note">This is a protected intent that can only be sent * by the system. */ @@ -1658,7 +1668,7 @@ public class Intent implements Parcelable { * then cell radio and possibly other radios such as bluetooth or WiFi may have also been * turned off</li> * </ul> - * + * * <p class="note">This is a protected intent that can only be sent * by the system. */ @@ -1740,7 +1750,7 @@ public class Intent implements Parcelable { * <p>You must hold the * {@link android.Manifest.permission#PROCESS_OUTGOING_CALLS} * permission to receive this Intent.</p> - * + * * <p class="note">This is a protected intent that can only be sent * by the system. */ @@ -1751,7 +1761,7 @@ public class Intent implements Parcelable { /** * Broadcast Action: Have the device reboot. This is only for use by * system code. - * + * * <p class="note">This is a protected intent that can only be sent * by the system. */ @@ -2102,7 +2112,7 @@ public class Intent implements Parcelable { * indicate that the dock should take over the home key when it is active. */ public static final String METADATA_DOCK_HOME = "android.dock_home"; - + /** * Used as a parcelable extra field in {@link #ACTION_APP_ERROR}, containing * the bug report. @@ -2389,6 +2399,20 @@ public class Intent implements Parcelable { */ public static final int FLAG_RECEIVER_REGISTERED_ONLY = 0x40000000; /** + * If set, when sending a broadcast the new broadcast will replace + * any existing pending broadcast that matches it. Matching is defined + * by {@link Intent#filterEquals(Intent) Intent.filterEquals} returning + * true for the intents of the two broadcasts. When a match is found, + * the new broadcast (and receivers associated with it) will replace the + * existing one in the pending broadcast list, remaining at the same + * position in the list. + * + * <p>This flag is most typically used with sticky broadcasts, which + * only care about delivering the most recent values of the broadcast + * to their receivers. + */ + public static final int FLAG_RECEIVER_REPLACE_PENDING = 0x20000000; + /** * If set, when sending a broadcast <i>before boot has completed</i> only * registered receivers will be called -- no BroadcastReceiver components * will be launched. Sticky intent state will be recorded properly even @@ -2401,14 +2425,14 @@ public class Intent implements Parcelable { * * @hide */ - public static final int FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT = 0x20000000; + public static final int FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT = 0x10000000; /** * Set when this broadcast is for a boot upgrade, a special mode that * allows the broadcast to be sent before the system is ready and launches * the app process with no providers running in it. * @hide */ - public static final int FLAG_RECEIVER_BOOT_UPGRADE = 0x10000000; + public static final int FLAG_RECEIVER_BOOT_UPGRADE = 0x08000000; /** * @hide Flags that can't be changed with PendingIntent. @@ -2416,7 +2440,7 @@ public class Intent implements Parcelable { public static final int IMMUTABLE_FLAGS = FLAG_GRANT_READ_URI_PERMISSION | FLAG_GRANT_WRITE_URI_PERMISSION; - + // --------------------------------------------------------------------- // --------------------------------------------------------------------- // toUri() and parseUri() options. @@ -2430,7 +2454,7 @@ public class Intent implements Parcelable { * VIEW action for that raw URI. */ public static final int URI_INTENT_SCHEME = 1<<0; - + // --------------------------------------------------------------------- private String mAction; @@ -2589,7 +2613,7 @@ public class Intent implements Parcelable { public static Intent getIntent(String uri) throws URISyntaxException { return parseUri(uri, 0); } - + /** * Create an intent from a URI. This URI may encode the action, * category, and other intent fields, if it was returned by @@ -2608,7 +2632,7 @@ public class Intent implements Parcelable { * @throws URISyntaxException Throws URISyntaxError if the basic URI syntax * it bad (as parsed by the Uri class) or the Intent data within the * URI is invalid. - * + * * @see #toUri */ public static Intent parseUri(String uri, int flags) throws URISyntaxException { @@ -2626,7 +2650,7 @@ public class Intent implements Parcelable { return intent; } } - + // simple case i = uri.lastIndexOf("#"); if (i == -1) return new Intent(ACTION_VIEW, Uri.parse(uri)); @@ -2718,7 +2742,7 @@ public class Intent implements Parcelable { data = scheme + ':' + data; } } - + if (data.length() > 0) { try { intent.mData = Uri.parse(data); @@ -2727,7 +2751,7 @@ public class Intent implements Parcelable { } } } - + return intent; } catch (IndexOutOfBoundsException e) { @@ -2878,7 +2902,7 @@ public class Intent implements Parcelable { } else { intent.mData = Uri.parse(uri); } - + if (intent.mAction == null) { // By default, if no action is specified, then use VIEW. intent.mAction = ACTION_VIEW; @@ -5103,13 +5127,13 @@ public class Intent implements Parcelable { * used with {@link Uri#parse Uri.parse(String)}. The URI contains the * Intent's data as the base URI, with an additional fragment describing * the action, categories, type, flags, package, component, and extras. - * + * * <p>You can convert the returned string back to an Intent with * {@link #getIntent}. - * + * * @param flags Additional operating flags. Either 0 or * {@link #URI_INTENT_SCHEME}. - * + * * @return Returns a URI encoding URI string describing the entire contents * of the Intent. */ @@ -5133,13 +5157,13 @@ public class Intent implements Parcelable { data = data.substring(i+1); break; } - + // No scheme. break; } } uri.append(data); - + } else if ((flags&URI_INTENT_SCHEME) != 0) { uri.append("intent:"); } diff --git a/core/java/android/content/IntentFilter.java b/core/java/android/content/IntentFilter.java index 365f269..023c024 100644 --- a/core/java/android/content/IntentFilter.java +++ b/core/java/android/content/IntentFilter.java @@ -34,7 +34,7 @@ import android.util.AndroidException; import android.util.Config; import android.util.Log; import android.util.Printer; -import com.android.internal.util.XmlUtils; +import com.android.common.XmlUtils; /** * Structured description of Intent values to be matched. An IntentFilter can diff --git a/core/java/android/content/SyncAdapter.java b/core/java/android/content/SyncAdapter.java deleted file mode 100644 index 88dc332..0000000 --- a/core/java/android/content/SyncAdapter.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright (C) 2006 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 android.content; - -import android.os.Bundle; -import android.os.RemoteException; -import android.accounts.Account; - -/** - * @hide - */ -public abstract class SyncAdapter { - private static final String TAG = "SyncAdapter"; - - /** Kernel event log tag. Also listed in data/etc/event-log-tags. */ - public static final int LOG_SYNC_DETAILS = 2743; - - class Transport extends ISyncAdapter.Stub { - public void startSync(ISyncContext syncContext, String authority, Account account, - Bundle extras) throws RemoteException { - SyncAdapter.this.startSync(new SyncContext(syncContext), account, authority, extras); - } - - public void cancelSync(ISyncContext syncContext) throws RemoteException { - SyncAdapter.this.cancelSync(); - } - } - - Transport mTransport = new Transport(); - - /** - * Get the Transport object. - */ - public final ISyncAdapter getISyncAdapter() - { - return mTransport; - } - - /** - * Initiate a sync for this account. SyncAdapter-specific parameters may - * be specified in extras, which is guaranteed to not be null. IPC invocations - * of this method and cancelSync() are guaranteed to be serialized. - * - * @param syncContext the ISyncContext used to indicate the progress of the sync. When - * the sync is finished (successfully or not) ISyncContext.onFinished() must be called. - * @param account the account that should be synced - * @param authority the authority if the sync request - * @param extras SyncAdapter-specific parameters - */ - public abstract void startSync(SyncContext syncContext, Account account, String authority, - Bundle extras); - - /** - * Cancel the most recently initiated sync. Due to race conditions, this may arrive - * after the ISyncContext.onFinished() for that sync was called. IPC invocations - * of this method and startSync() are guaranteed to be serialized. - */ - public abstract void cancelSync(); -} diff --git a/core/java/android/content/SyncContext.java b/core/java/android/content/SyncContext.java index 587586d..cc914c0 100644 --- a/core/java/android/content/SyncContext.java +++ b/core/java/android/content/SyncContext.java @@ -56,7 +56,9 @@ public class SyncContext { if (now < mLastHeartbeatSendTime + HEARTBEAT_SEND_INTERVAL_IN_MS) return; try { mLastHeartbeatSendTime = now; - mSyncContext.sendHeartbeat(); + if (mSyncContext != null) { + mSyncContext.sendHeartbeat(); + } } catch (RemoteException e) { // this should never happen } @@ -64,13 +66,15 @@ public class SyncContext { public void onFinished(SyncResult result) { try { - mSyncContext.onFinished(result); + if (mSyncContext != null) { + mSyncContext.onFinished(result); + } } catch (RemoteException e) { // this should never happen } } public IBinder getSyncContextBinder() { - return mSyncContext.asBinder(); + return (mSyncContext == null) ? null : mSyncContext.asBinder(); } } diff --git a/core/java/android/content/SyncManager.java b/core/java/android/content/SyncManager.java index b2d406b..a9c61dc 100644 --- a/core/java/android/content/SyncManager.java +++ b/core/java/android/content/SyncManager.java @@ -544,6 +544,46 @@ class SyncManager implements OnAccountsUpdateListener { return (activeSyncContext != null) ? activeSyncContext.mSyncOperation.account : null; } + private void initializeSyncAdapter(Account account, String authority) { + SyncAdapterType syncAdapterType = SyncAdapterType.newKey(authority, account.type); + RegisteredServicesCache.ServiceInfo<SyncAdapterType> syncAdapterInfo = + mSyncAdapters.getServiceInfo(syncAdapterType); + if (syncAdapterInfo == null) { + Log.w(TAG, "can't find a sync adapter for " + syncAdapterType); + return; + } + + Intent intent = new Intent(); + intent.setAction("android.content.SyncAdapter"); + intent.setComponent(syncAdapterInfo.componentName); + mContext.bindService(intent, new InitializerServiceConnection(account, authority), + Context.BIND_AUTO_CREATE | Context.BIND_NOT_FOREGROUND); + } + + private class InitializerServiceConnection implements ServiceConnection { + private final Account mAccount; + private final String mAuthority; + + public InitializerServiceConnection(Account account, String authority) { + mAccount = account; + mAuthority = authority; + } + + public void onServiceConnected(ComponentName name, IBinder service) { + try { + ISyncAdapter.Stub.asInterface(service).initialize(mAccount, mAuthority); + } catch (RemoteException e) { + // doesn't matter, we will retry again later + } finally { + mContext.unbindService(this); + } + } + + public void onServiceDisconnected(ComponentName name) { + mContext.unbindService(this); + } + } + /** * Returns whether or not sync is enabled. Sync can be enabled by * setting the system property "ro.config.sync" to the value "yes". @@ -686,36 +726,34 @@ class SyncManager implements OnAccountsUpdateListener { continue; } - // make this an initialization sync if the isSyncable state is unknown - Bundle extrasCopy = extras; - long delayCopy = delay; + // initialize the SyncAdapter if the isSyncable state is unknown if (isSyncable < 0) { - extrasCopy = new Bundle(extras); - extrasCopy.putBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, true); - delayCopy = -1; // expedite this - } else { - final boolean syncAutomatically = masterSyncAutomatically - && mSyncStorageEngine.getSyncAutomatically(account, authority); - boolean syncAllowed = - manualSync || (backgroundDataUsageAllowed && syncAutomatically); - if (!syncAllowed) { - if (isLoggable) { - Log.d(TAG, "scheduleSync: sync of " + account + ", " + authority - + " is not allowed, dropping request"); - } - continue; + initializeSyncAdapter(account, authority); + continue; + } + + final boolean syncAutomatically = masterSyncAutomatically + && mSyncStorageEngine.getSyncAutomatically(account, authority); + boolean syncAllowed = + manualSync || (backgroundDataUsageAllowed && syncAutomatically); + if (!syncAllowed) { + if (isLoggable) { + Log.d(TAG, "scheduleSync: sync of " + account + ", " + authority + + " is not allowed, dropping request"); } + continue; } + if (isLoggable) { Log.v(TAG, "scheduleSync:" - + " delay " + delayCopy + + " delay " + delay + ", source " + source + ", account " + account + ", authority " + authority - + ", extras " + extrasCopy); + + ", extras " + extras); } scheduleSyncOperation( - new SyncOperation(account, source, authority, extrasCopy, delayCopy)); + new SyncOperation(account, source, authority, extras, delay)); } } } @@ -821,8 +859,8 @@ class SyncManager implements OnAccountsUpdateListener { } // Cap the delay - long maxSyncRetryTimeInSeconds = Settings.Gservices.getLong(mContext.getContentResolver(), - Settings.Gservices.SYNC_MAX_RETRY_DELAY_IN_SECONDS, + long maxSyncRetryTimeInSeconds = Settings.Secure.getLong(mContext.getContentResolver(), + Settings.Secure.SYNC_MAX_RETRY_DELAY_IN_SECONDS, DEFAULT_MAX_SYNC_RETRY_TIME_IN_SECONDS); if (newDelayInMs > maxSyncRetryTimeInSeconds * 1000) { newDelayInMs = maxSyncRetryTimeInSeconds * 1000; @@ -1107,7 +1145,8 @@ class SyncManager implements OnAccountsUpdateListener { com.android.internal.R.string.sync_binding_label); intent.putExtra(Intent.EXTRA_CLIENT_INTENT, PendingIntent.getActivity( mContext, 0, new Intent(Settings.ACTION_SYNC_SETTINGS), 0)); - return mContext.bindService(intent, this, Context.BIND_AUTO_CREATE); + return mContext.bindService(intent, this, + Context.BIND_AUTO_CREATE | Context.BIND_NOT_FOREGROUND); } void unBindFromSyncAdapter() { diff --git a/core/java/android/content/SyncStateContentProviderHelper.java b/core/java/android/content/SyncStateContentProviderHelper.java deleted file mode 100644 index 64bbe25..0000000 --- a/core/java/android/content/SyncStateContentProviderHelper.java +++ /dev/null @@ -1,243 +0,0 @@ -/* - * Copyright (C) 2007 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.content; - -import com.android.internal.util.ArrayUtils; - -import android.database.Cursor; -import android.database.DatabaseUtils; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteOpenHelper; -import android.net.Uri; -import android.accounts.Account; - -/** - * Extends the schema of a ContentProvider to include the _sync_state table - * and implements query/insert/update/delete to access that table using the - * authority "syncstate". This can be used to store the sync state for a - * set of accounts. - * - * @hide - */ -public class SyncStateContentProviderHelper { - final SQLiteOpenHelper mOpenHelper; - - private static final String SYNC_STATE_AUTHORITY = "syncstate"; - private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH); - - private static final int STATE = 0; - - private static final Uri CONTENT_URI = - Uri.parse("content://" + SYNC_STATE_AUTHORITY + "/state"); - - private static final String ACCOUNT_WHERE = "_sync_account = ? AND _sync_account_type = ?"; - - private final Provider mInternalProviderInterface; - - private static final String SYNC_STATE_TABLE = "_sync_state"; - private static long DB_VERSION = 3; - - private static final String[] ACCOUNT_PROJECTION = - new String[]{"_sync_account", "_sync_account_type"}; - - static { - sURIMatcher.addURI(SYNC_STATE_AUTHORITY, "state", STATE); - } - - public SyncStateContentProviderHelper(SQLiteOpenHelper openHelper) { - mOpenHelper = openHelper; - mInternalProviderInterface = new Provider(); - } - - public ContentProvider asContentProvider() { - return mInternalProviderInterface; - } - - public void createDatabase(SQLiteDatabase db) { - db.execSQL("DROP TABLE IF EXISTS _sync_state"); - db.execSQL("CREATE TABLE _sync_state (" + - "_id INTEGER PRIMARY KEY," + - "_sync_account TEXT," + - "_sync_account_type TEXT," + - "data TEXT," + - "UNIQUE(_sync_account, _sync_account_type)" + - ");"); - - db.execSQL("DROP TABLE IF EXISTS _sync_state_metadata"); - db.execSQL("CREATE TABLE _sync_state_metadata (" + - "version INTEGER" + - ");"); - ContentValues values = new ContentValues(); - values.put("version", DB_VERSION); - db.insert("_sync_state_metadata", "version", values); - } - - protected void onDatabaseOpened(SQLiteDatabase db) { - long version = DatabaseUtils.longForQuery(db, - "select version from _sync_state_metadata", null); - if (version != DB_VERSION) { - createDatabase(db); - } - } - - class Provider extends ContentProvider { - public boolean onCreate() { - throw new UnsupportedOperationException("not implemented"); - } - - public Cursor query(Uri url, String[] projection, String selection, String[] selectionArgs, - String sortOrder) { - SQLiteDatabase db = mOpenHelper.getReadableDatabase(); - int match = sURIMatcher.match(url); - switch (match) { - case STATE: - return db.query(SYNC_STATE_TABLE, projection, selection, selectionArgs, - null, null, sortOrder); - default: - throw new UnsupportedOperationException("Cannot query URL: " + url); - } - } - - public String getType(Uri uri) { - throw new UnsupportedOperationException("not implemented"); - } - - public Uri insert(Uri url, ContentValues values) { - SQLiteDatabase db = mOpenHelper.getWritableDatabase(); - int match = sURIMatcher.match(url); - switch (match) { - case STATE: { - long id = db.insert(SYNC_STATE_TABLE, "feed", values); - return CONTENT_URI.buildUpon().appendPath(String.valueOf(id)).build(); - } - default: - throw new UnsupportedOperationException("Cannot insert into URL: " + url); - } - } - - public int delete(Uri url, String userWhere, String[] whereArgs) { - SQLiteDatabase db = mOpenHelper.getWritableDatabase(); - switch (sURIMatcher.match(url)) { - case STATE: - return db.delete(SYNC_STATE_TABLE, userWhere, whereArgs); - default: - throw new IllegalArgumentException("Unknown URL " + url); - } - - } - - public int update(Uri url, ContentValues values, String selection, String[] selectionArgs) { - SQLiteDatabase db = mOpenHelper.getWritableDatabase(); - switch (sURIMatcher.match(url)) { - case STATE: - return db.update(SYNC_STATE_TABLE, values, selection, selectionArgs); - default: - throw new UnsupportedOperationException("Cannot update URL: " + url); - } - - } - } - - /** - * Check if the url matches content that this ContentProvider manages. - * @param url the Uri to check - * @return true if this ContentProvider can handle that Uri. - */ - public boolean matches(Uri url) { - return (SYNC_STATE_AUTHORITY.equals(url.getAuthority())); - } - - /** - * Replaces the contents of the _sync_state table in the destination ContentProvider - * with the row that matches account, if any, in the source ContentProvider. - * <p> - * The ContentProviders must expose the _sync_state table as URI content://syncstate/state. - * @param dbSrc the database to read from - * @param dbDest the database to write to - * @param account the account of the row that should be copied over. - */ - public void copySyncState(SQLiteDatabase dbSrc, SQLiteDatabase dbDest, - Account account) { - final String[] whereArgs = new String[]{account.name, account.type}; - Cursor c = dbSrc.query(SYNC_STATE_TABLE, - new String[]{"_sync_account", "_sync_account_type", "data"}, - ACCOUNT_WHERE, whereArgs, null, null, null); - try { - if (c.moveToNext()) { - ContentValues values = new ContentValues(); - values.put("_sync_account", c.getString(0)); - values.put("_sync_account_type", c.getString(1)); - values.put("data", c.getBlob(2)); - dbDest.replace(SYNC_STATE_TABLE, "_sync_account", values); - } - } finally { - c.close(); - } - } - - public void onAccountsChanged(Account[] accounts) { - SQLiteDatabase db = mOpenHelper.getWritableDatabase(); - Cursor c = db.query(SYNC_STATE_TABLE, ACCOUNT_PROJECTION, null, null, null, null, null); - try { - while (c.moveToNext()) { - final String accountName = c.getString(0); - final String accountType = c.getString(1); - Account account = new Account(accountName, accountType); - if (!ArrayUtils.contains(accounts, account)) { - db.delete(SYNC_STATE_TABLE, ACCOUNT_WHERE, - new String[]{accountName, accountType}); - } - } - } finally { - c.close(); - } - } - - public void discardSyncData(SQLiteDatabase db, Account account) { - if (account != null) { - db.delete(SYNC_STATE_TABLE, ACCOUNT_WHERE, new String[]{account.name, account.type}); - } else { - db.delete(SYNC_STATE_TABLE, null, null); - } - } - - /** - * Retrieves the SyncData bytes for the given account. The byte array returned may be null. - */ - public byte[] readSyncDataBytes(SQLiteDatabase db, Account account) { - Cursor c = db.query(SYNC_STATE_TABLE, null, ACCOUNT_WHERE, - new String[]{account.name, account.type}, null, null, null); - try { - if (c.moveToFirst()) { - return c.getBlob(c.getColumnIndexOrThrow("data")); - } - } finally { - c.close(); - } - return null; - } - - /** - * Sets the SyncData bytes for the given account. The bytes array may be null. - */ - public void writeSyncDataBytes(SQLiteDatabase db, Account account, byte[] data) { - ContentValues values = new ContentValues(); - values.put("data", data); - db.update(SYNC_STATE_TABLE, values, ACCOUNT_WHERE, - new String[]{account.name, account.type}); - } -} diff --git a/core/java/android/content/SyncStorageEngine.java b/core/java/android/content/SyncStorageEngine.java index be70909..4c53201 100644 --- a/core/java/android/content/SyncStorageEngine.java +++ b/core/java/android/content/SyncStorageEngine.java @@ -18,14 +18,13 @@ package android.content; import com.android.internal.os.AtomicFile; import com.android.internal.util.ArrayUtils; -import com.android.internal.util.FastXmlSerializer; +import com.android.common.FastXmlSerializer; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlSerializer; import android.accounts.Account; -import android.backup.IBackupManager; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; @@ -37,7 +36,7 @@ import android.os.Message; import android.os.Parcel; import android.os.RemoteCallbackList; import android.os.RemoteException; -import android.os.ServiceManager; +import android.os.SystemProperties; import android.util.Log; import android.util.SparseArray; import android.util.Xml; @@ -258,7 +257,9 @@ public class SyncStorageEngine extends Handler { mCal = Calendar.getInstance(TimeZone.getTimeZone("GMT+0")); - File dataDir = Environment.getDataDirectory(); + // This call will return the correct directory whether Encrypted File Systems is + // enabled or not. + File dataDir = Environment.getSecureDataDirectory(); File systemDir = new File(dataDir, "system"); File syncDir = new File(systemDir, "sync"); mAccountInfoFile = new AtomicFile(new File(syncDir, "accounts.xml")); @@ -511,7 +512,7 @@ public class SyncStorageEngine extends Handler { SyncStatusInfo status = getOrCreateSyncStatusLocked(authority.ident); status.pending = true; - status.initialize = op.extras != null && + status.initialize = op.extras != null && op.extras.containsKey(ContentResolver.SYNC_EXTRAS_INITIALIZE) && op.extras.getBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE); } diff --git a/core/java/android/content/SyncableContentProvider.java b/core/java/android/content/SyncableContentProvider.java deleted file mode 100644 index ab4e91c..0000000 --- a/core/java/android/content/SyncableContentProvider.java +++ /dev/null @@ -1,237 +0,0 @@ -/* - * Copyright (C) 2007 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.content; - -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.net.Uri; -import android.accounts.Account; - -import java.util.Map; - -/** - * A specialization of the ContentProvider that centralizes functionality - * used by ContentProviders that are syncable. It also wraps calls to the ContentProvider - * inside of database transactions. - * - * @hide - */ -public abstract class SyncableContentProvider extends ContentProvider { - protected abstract boolean isTemporary(); - - private volatile TempProviderSyncAdapter mTempProviderSyncAdapter; - - public void setTempProviderSyncAdapter(TempProviderSyncAdapter syncAdapter) { - mTempProviderSyncAdapter = syncAdapter; - } - - public TempProviderSyncAdapter getTempProviderSyncAdapter() { - return mTempProviderSyncAdapter; - } - - /** - * Close resources that must be closed. You must call this to properly release - * the resources used by the SyncableContentProvider. - */ - public abstract void close(); - - /** - * Override to create your schema and do anything else you need to do with a new database. - * This is run inside a transaction (so you don't need to use one). - * This method may not use getDatabase(), or call content provider methods, it must only - * use the database handle passed to it. - */ - protected abstract void bootstrapDatabase(SQLiteDatabase db); - - /** - * Override to upgrade your database from an old version to the version you specified. - * Don't set the DB version, this will automatically be done after the method returns. - * This method may not use getDatabase(), or call content provider methods, it must only - * use the database handle passed to it. - * - * @param oldVersion version of the existing database - * @param newVersion current version to upgrade to - * @return true if the upgrade was lossless, false if it was lossy - */ - protected abstract boolean upgradeDatabase(SQLiteDatabase db, int oldVersion, int newVersion); - - /** - * Override to do anything (like cleanups or checks) you need to do after opening a database. - * Does nothing by default. This is run inside a transaction (so you don't need to use one). - * This method may not use getDatabase(), or call content provider methods, it must only - * use the database handle passed to it. - */ - protected abstract void onDatabaseOpened(SQLiteDatabase db); - - /** - * Get a non-persistent instance of this content provider. - * You must call {@link #close} on the returned - * SyncableContentProvider when you are done with it. - * - * @return a non-persistent content provider with the same layout as this - * provider. - */ - public abstract SyncableContentProvider getTemporaryInstance(); - - public abstract SQLiteDatabase getDatabase(); - - public abstract boolean getContainsDiffs(); - - public abstract void setContainsDiffs(boolean containsDiffs); - - /** - * Each subclass of this class should define a subclass of {@link - * AbstractTableMerger} for each table they wish to merge. It - * should then override this method and return one instance of - * each merger, in sequence. Their {@link - * AbstractTableMerger#merge merge} methods will be called, one at a - * time, in the order supplied. - * - * <p>The default implementation returns an empty list, so that no - * merging will occur. - * @return A sequence of subclasses of {@link - * AbstractTableMerger}, one for each table that should be merged. - */ - protected abstract Iterable<? extends AbstractTableMerger> getMergers(); - - /** - * Check if changes to this URI can be syncable changes. - * @param uri the URI of the resource that was changed - * @return true if changes to this URI can be syncable changes, false otherwise - */ - public abstract boolean changeRequiresLocalSync(Uri uri); - - /** - * Called right before a sync is started. - * - * @param context the sync context for the operation - * @param account - */ - public abstract void onSyncStart(SyncContext context, Account account); - - /** - * Called right after a sync is completed - * - * @param context the sync context for the operation - * @param success true if the sync succeeded, false if an error occurred - */ - public abstract void onSyncStop(SyncContext context, boolean success); - - /** - * The account of the most recent call to onSyncStart() - * @return the account - */ - public abstract Account getSyncingAccount(); - - /** - * Merge diffs from a sync source with this content provider. - * - * @param context the SyncContext within which this merge is taking place - * @param diffs A temporary content provider containing diffs from a sync - * source. - * @param result a MergeResult that contains information about the merge, including - * a temporary content provider with the same layout as this provider containing - * @param syncResult - */ - public abstract void merge(SyncContext context, SyncableContentProvider diffs, - TempProviderSyncResult result, SyncResult syncResult); - - - /** - * Invoked when the active sync has been canceled. The default - * implementation doesn't do anything (except ensure that this - * provider is syncable). Subclasses of ContentProvider - * that support canceling of sync should override this. - */ - public abstract void onSyncCanceled(); - - - public abstract boolean isMergeCancelled(); - - /** - * Subclasses should override this instead of update(). See update() - * for details. - * - * <p> This method is called within a acquireDbLock()/releaseDbLock() block, - * which means a database transaction will be active during the call; - */ - protected abstract int updateInternal(Uri url, ContentValues values, - String selection, String[] selectionArgs); - - /** - * Subclasses should override this instead of delete(). See delete() - * for details. - * - * <p> This method is called within a acquireDbLock()/releaseDbLock() block, - * which means a database transaction will be active during the call; - */ - protected abstract int deleteInternal(Uri url, String selection, String[] selectionArgs); - - /** - * Subclasses should override this instead of insert(). See insert() - * for details. - * - * <p> This method is called within a acquireDbLock()/releaseDbLock() block, - * which means a database transaction will be active during the call; - */ - protected abstract Uri insertInternal(Uri url, ContentValues values); - - /** - * Subclasses should override this instead of query(). See query() - * for details. - * - * <p> This method is *not* called within a acquireDbLock()/releaseDbLock() - * block for performance reasons. If an implementation needs atomic access - * to the database the lock can be acquired then. - */ - protected abstract Cursor queryInternal(Uri url, String[] projection, - String selection, String[] selectionArgs, String sortOrder); - - /** - * Make sure that there are no entries for accounts that no longer exist - * @param accountsArray the array of currently-existing accounts - */ - protected abstract void onAccountsChanged(Account[] accountsArray); - - /** - * A helper method to delete all rows whose account is not in the accounts - * map. The accountColumnName is the name of the column that is expected - * to hold the account. If a row has an empty account it is never deleted. - * - * @param accounts a map of existing accounts - * @param table the table to delete from - */ - protected abstract void deleteRowsForRemovedAccounts(Map<Account, Boolean> accounts, - String table); - - /** - * Called when the sync system determines that this provider should no longer - * contain records for the specified account. - */ - public abstract void wipeAccount(Account account); - - /** - * Retrieves the SyncData bytes for the given account. The byte array returned may be null. - */ - public abstract byte[] readSyncDataBytes(Account account); - - /** - * Sets the SyncData bytes for the given account. The bytes array may be null. - */ - public abstract void writeSyncDataBytes(Account account, byte[] data); -} - diff --git a/core/java/android/content/TempProviderSyncAdapter.java b/core/java/android/content/TempProviderSyncAdapter.java deleted file mode 100644 index b46c545..0000000 --- a/core/java/android/content/TempProviderSyncAdapter.java +++ /dev/null @@ -1,585 +0,0 @@ -package android.content; - -import android.database.SQLException; -import android.os.Bundle; -import android.os.Debug; -import android.os.NetStat; -import android.os.Parcelable; -import android.os.Process; -import android.os.SystemProperties; -import android.text.TextUtils; -import android.util.Config; -import android.util.EventLog; -import android.util.Log; -import android.util.TimingLogger; -import android.accounts.Account; -import android.accounts.AuthenticatorException; -import android.accounts.OperationCanceledException; - -import java.io.IOException; - -/** - * @hide - */ -public abstract class TempProviderSyncAdapter extends SyncAdapter { - private static final String TAG = "Sync"; - - private static final int MAX_GET_SERVER_DIFFS_LOOP_COUNT = 20; - private static final int MAX_UPLOAD_CHANGES_LOOP_COUNT = 10; - private static final int NUM_ALLOWED_SIMULTANEOUS_DELETIONS = 5; - private static final long PERCENT_ALLOWED_SIMULTANEOUS_DELETIONS = 20; - - private volatile SyncableContentProvider mProvider; - private volatile SyncThread mSyncThread = null; - private volatile boolean mProviderSyncStarted; - private volatile boolean mAdapterSyncStarted; - - public TempProviderSyncAdapter(SyncableContentProvider provider) { - super(); - mProvider = provider; - } - - /** - * Used by getServerDiffs() to track the sync progress for a given - * sync adapter. Implementations of SyncAdapter generally specialize - * this class in order to track specific data about that SyncAdapter's - * sync. If an implementation of SyncAdapter doesn't need to store - * any data for a sync it may use TrivialSyncData. - */ - public static abstract class SyncData implements Parcelable { - - } - - public final void setContext(Context context) { - mContext = context; - } - - /** - * Retrieve the Context this adapter is running in. Only available - * once onSyncStarting() is called (not available from constructor). - */ - final public Context getContext() { - return mContext; - } - - /** - * Called right before a sync is started. - * - * @param context allows you to publish status and interact with the - * @param account the account to sync - * @param manualSync true if this sync was requested manually by the user - * @param result information to track what happened during this sync attempt - */ - public abstract void onSyncStarting(SyncContext context, Account account, boolean manualSync, - SyncResult result); - - /** - * Called right after a sync is completed - * - * @param context allows you to publish status and interact with the - * user during interactive syncs. - * @param success true if the sync suceeded, false if an error occured - */ - public abstract void onSyncEnding(SyncContext context, boolean success); - - /** - * Implement this to return true if the data in your content provider - * is read only. - */ - public abstract boolean isReadOnly(); - - public abstract boolean getIsSyncable(Account account) - throws IOException, AuthenticatorException, OperationCanceledException; - - /** - * Get diffs from the server since the last completed sync and put them - * into a temporary provider. - * - * @param context allows you to publish status and interact with the - * user during interactive syncs. - * @param syncData used to track the progress this client has made in syncing data - * from the server - * @param tempProvider this is where the diffs should be stored - * @param extras any extra data describing the sync that is desired - * @param syncInfo sync adapter-specific data that is used during a single sync operation - * @param syncResult information to track what happened during this sync attempt - */ - public abstract void getServerDiffs(SyncContext context, - SyncData syncData, SyncableContentProvider tempProvider, - Bundle extras, Object syncInfo, SyncResult syncResult); - - /** - * Send client diffs to the server, optionally receiving more diffs from the server - * - * @param context allows you to publish status and interact with the - * user during interactive syncs. - * @param clientDiffs the diffs from the client - * @param serverDiffs the SyncableContentProvider that should be populated with -* the entries that were returned in response to an insert/update/delete request -* to the server - * @param syncResult information to track what happened during this sync attempt - * @param dontActuallySendDeletes - */ - public abstract void sendClientDiffs(SyncContext context, - SyncableContentProvider clientDiffs, - SyncableContentProvider serverDiffs, SyncResult syncResult, - boolean dontActuallySendDeletes); - - /** - * Reads the sync data from the ContentProvider - * @param contentProvider the ContentProvider to read from - * @return the SyncData for the provider. This may be null. - */ - public SyncData readSyncData(SyncableContentProvider contentProvider) { - return null; - } - - /** - * Create and return a new, empty SyncData object - */ - public SyncData newSyncData() { - return null; - } - - /** - * Stores the sync data in the Sync Stats database, keying it by - * the account that was set in the last call to onSyncStarting() - */ - public void writeSyncData(SyncData syncData, SyncableContentProvider contentProvider) {} - - /** - * Indicate to the SyncAdapter that the last sync that was started has - * been cancelled. - */ - public abstract void onSyncCanceled(); - - /** - * Initializes the temporary content providers used during - * {@link TempProviderSyncAdapter#sendClientDiffs}. - * May copy relevant data from the underlying db into this provider so - * joins, etc., can work. - * - * @param cp The ContentProvider to initialize. - */ - protected void initTempProvider(SyncableContentProvider cp) {} - - protected Object createSyncInfo() { - return null; - } - - /** - * Called when the accounts list possibly changed, to give the - * SyncAdapter a chance to do any necessary bookkeeping, e.g. - * to make sure that any required SubscribedFeeds subscriptions - * exist. - * @param accounts the list of accounts - */ - public abstract void onAccountsChanged(Account[] accounts); - - private Context mContext; - - private class SyncThread extends Thread { - private final Account mAccount; - private final String mAuthority; - private final Bundle mExtras; - private final SyncContext mSyncContext; - private volatile boolean mIsCanceled = false; - private long mInitialTxBytes; - private long mInitialRxBytes; - private final SyncResult mResult; - - SyncThread(SyncContext syncContext, Account account, String authority, Bundle extras) { - super("SyncThread"); - mAccount = account; - mAuthority = authority; - mExtras = extras; - mSyncContext = syncContext; - mResult = new SyncResult(); - } - - void cancelSync() { - mIsCanceled = true; - if (mAdapterSyncStarted) onSyncCanceled(); - if (mProviderSyncStarted) mProvider.onSyncCanceled(); - // We may lose the last few sync events when canceling. Oh well. - int uid = Process.myUid(); - logSyncDetails(NetStat.getUidTxBytes(uid) - mInitialTxBytes, - NetStat.getUidRxBytes(uid) - mInitialRxBytes, mResult); - } - - @Override - public void run() { - Process.setThreadPriority(Process.myTid(), - Process.THREAD_PRIORITY_BACKGROUND); - int uid = Process.myUid(); - mInitialTxBytes = NetStat.getUidTxBytes(uid); - mInitialRxBytes = NetStat.getUidRxBytes(uid); - try { - sync(mSyncContext, mAccount, mAuthority, mExtras); - } catch (SQLException e) { - Log.e(TAG, "Sync failed", e); - mResult.databaseError = true; - } finally { - mSyncThread = null; - if (!mIsCanceled) { - logSyncDetails(NetStat.getUidTxBytes(uid) - mInitialTxBytes, - NetStat.getUidRxBytes(uid) - mInitialRxBytes, mResult); - mSyncContext.onFinished(mResult); - } - } - } - - private void sync(SyncContext syncContext, Account account, String authority, - Bundle extras) { - mIsCanceled = false; - - mProviderSyncStarted = false; - mAdapterSyncStarted = false; - String message = null; - - // always attempt to initialize if the isSyncable state isn't set yet - int isSyncable = ContentResolver.getIsSyncable(account, authority); - if (isSyncable < 0) { - try { - isSyncable = (getIsSyncable(account)) ? 1 : 0; - ContentResolver.setIsSyncable(account, authority, isSyncable); - } catch (IOException e) { - ++mResult.stats.numIoExceptions; - } catch (AuthenticatorException e) { - ++mResult.stats.numParseExceptions; - } catch (OperationCanceledException e) { - // do nothing - } - } - - // if this is an initialization request then our work is done here - if (extras.getBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, false)) { - return; - } - - // if we aren't syncable then get out - if (isSyncable <= 0) { - return; - } - - boolean manualSync = extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false); - - try { - mProvider.onSyncStart(syncContext, account); - mProviderSyncStarted = true; - onSyncStarting(syncContext, account, manualSync, mResult); - if (mResult.hasError()) { - message = "SyncAdapter failed while trying to start sync"; - return; - } - mAdapterSyncStarted = true; - if (mIsCanceled) { - return; - } - final String syncTracingEnabledValue = SystemProperties.get(TAG + "Tracing"); - final boolean syncTracingEnabled = !TextUtils.isEmpty(syncTracingEnabledValue); - try { - if (syncTracingEnabled) { - System.gc(); - System.gc(); - Debug.startMethodTracing("synctrace." + System.currentTimeMillis()); - } - runSyncLoop(syncContext, account, extras); - } finally { - if (syncTracingEnabled) Debug.stopMethodTracing(); - } - onSyncEnding(syncContext, !mResult.hasError()); - mAdapterSyncStarted = false; - mProvider.onSyncStop(syncContext, true); - mProviderSyncStarted = false; - } finally { - if (mAdapterSyncStarted) { - mAdapterSyncStarted = false; - onSyncEnding(syncContext, false); - } - if (mProviderSyncStarted) { - mProviderSyncStarted = false; - mProvider.onSyncStop(syncContext, false); - } - if (!mIsCanceled) { - if (message != null) syncContext.setStatusText(message); - } - } - } - - private void runSyncLoop(SyncContext syncContext, Account account, Bundle extras) { - TimingLogger syncTimer = new TimingLogger(TAG + "Profiling", "sync"); - syncTimer.addSplit("start"); - int loopCount = 0; - boolean tooManyGetServerDiffsAttempts = false; - - final boolean overrideTooManyDeletions = - extras.getBoolean(ContentResolver.SYNC_EXTRAS_OVERRIDE_TOO_MANY_DELETIONS, - false); - final boolean discardLocalDeletions = - extras.getBoolean(ContentResolver.SYNC_EXTRAS_DISCARD_LOCAL_DELETIONS, false); - boolean uploadOnly = extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, - false /* default this flag to false */); - SyncableContentProvider serverDiffs = null; - TempProviderSyncResult result = new TempProviderSyncResult(); - try { - if (!uploadOnly) { - /** - * This loop repeatedly calls SyncAdapter.getServerDiffs() - * (to get changes from the feed) followed by - * ContentProvider.merge() (to incorporate these changes - * into the provider), stopping when the SyncData returned - * from getServerDiffs() indicates that all the data was - * fetched. - */ - while (!mIsCanceled) { - // Don't let a bad sync go forever - if (loopCount++ == MAX_GET_SERVER_DIFFS_LOOP_COUNT) { - Log.e(TAG, "runSyncLoop: Hit max loop count while getting server diffs " - + getClass().getName()); - // TODO: change the structure here to schedule a new sync - // with a backoff time, keeping track to be sure - // we don't keep doing this forever (due to some bug or - // mismatch between the client and the server) - tooManyGetServerDiffsAttempts = true; - break; - } - - // Get an empty content provider to put the diffs into - if (serverDiffs != null) serverDiffs.close(); - serverDiffs = mProvider.getTemporaryInstance(); - - // Get records from the server which will be put into the serverDiffs - initTempProvider(serverDiffs); - Object syncInfo = createSyncInfo(); - SyncData syncData = readSyncData(serverDiffs); - // syncData will only be null if there was a demarshalling error - // while reading the sync data. - if (syncData == null) { - mProvider.wipeAccount(account); - syncData = newSyncData(); - } - mResult.clear(); - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "runSyncLoop: running getServerDiffs using syncData " - + syncData.toString()); - } - getServerDiffs(syncContext, syncData, serverDiffs, extras, syncInfo, - mResult); - - if (mIsCanceled) return; - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "runSyncLoop: result: " + mResult); - } - if (mResult.hasError()) return; - if (mResult.partialSyncUnavailable) { - if (Config.LOGD) { - Log.d(TAG, "partialSyncUnavailable is set, setting " - + "ignoreSyncData and retrying"); - } - mProvider.wipeAccount(account); - continue; - } - - // write the updated syncData back into the temp provider - writeSyncData(syncData, serverDiffs); - - // apply the downloaded changes to the provider - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "runSyncLoop: running merge"); - } - mProvider.merge(syncContext, serverDiffs, - null /* don't return client diffs */, mResult); - if (mIsCanceled) return; - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "runSyncLoop: result: " + mResult); - } - - // if the server has no more changes then break out of the loop - if (!mResult.moreRecordsToGet) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "runSyncLoop: fetched all data, moving on"); - } - break; - } - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "runSyncLoop: more data to fetch, looping"); - } - } - } - - /** - * This loop repeatedly calls ContentProvider.merge() followed - * by SyncAdapter.merge() until either indicate that there is - * no more work to do by returning null. - * <p> - * The initial ContentProvider.merge() returns a temporary - * ContentProvider that contains any local changes that need - * to be committed to the server. - * <p> - * The SyncAdapter.merge() calls upload the changes to the server - * and populates temporary provider (the serverDiffs) with the - * result. - * <p> - * Subsequent calls to ContentProvider.merge() incoporate the - * result of previous SyncAdapter.merge() calls into the - * real ContentProvider and again return a temporary - * ContentProvider that contains any local changes that need - * to be committed to the server. - */ - loopCount = 0; - boolean readOnly = isReadOnly(); - long previousNumModifications = 0; - if (serverDiffs != null) { - serverDiffs.close(); - serverDiffs = null; - } - - // If we are discarding local deletions then we need to redownload all the items - // again (since some of them might have been deleted). We do this by deleting the - // sync data for the current account by writing in a null one. - if (discardLocalDeletions) { - serverDiffs = mProvider.getTemporaryInstance(); - initTempProvider(serverDiffs); - writeSyncData(null, serverDiffs); - } - - while (!mIsCanceled) { - if (Config.LOGV) { - Log.v(TAG, "runSyncLoop: Merging diffs from server to client"); - } - if (result.tempContentProvider != null) { - result.tempContentProvider.close(); - result.tempContentProvider = null; - } - mResult.clear(); - mProvider.merge(syncContext, serverDiffs, readOnly ? null : result, - mResult); - if (mIsCanceled) return; - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "runSyncLoop: result: " + mResult); - } - - SyncableContentProvider clientDiffs = - readOnly ? null : result.tempContentProvider; - if (clientDiffs == null) { - // Nothing to commit back to the server - if (Config.LOGV) Log.v(TAG, "runSyncLoop: No client diffs"); - break; - } - - long numModifications = mResult.stats.numUpdates - + mResult.stats.numDeletes - + mResult.stats.numInserts; - - // as long as we are making progress keep resetting the loop count - if (numModifications < previousNumModifications) { - loopCount = 0; - } - previousNumModifications = numModifications; - - // Don't let a bad sync go forever - if (loopCount++ >= MAX_UPLOAD_CHANGES_LOOP_COUNT) { - Log.e(TAG, "runSyncLoop: Hit max loop count while syncing " - + getClass().getName()); - mResult.tooManyRetries = true; - break; - } - - if (!overrideTooManyDeletions && !discardLocalDeletions - && hasTooManyDeletions(mResult.stats)) { - if (Config.LOGD) { - Log.d(TAG, "runSyncLoop: Too many deletions were found in provider " - + getClass().getName() + ", not doing any more updates"); - } - long numDeletes = mResult.stats.numDeletes; - mResult.stats.clear(); - mResult.tooManyDeletions = true; - mResult.stats.numDeletes = numDeletes; - break; - } - - if (Config.LOGV) Log.v(TAG, "runSyncLoop: Merging diffs from client to server"); - if (serverDiffs != null) serverDiffs.close(); - serverDiffs = clientDiffs.getTemporaryInstance(); - initTempProvider(serverDiffs); - mResult.clear(); - sendClientDiffs(syncContext, clientDiffs, serverDiffs, mResult, - discardLocalDeletions); - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "runSyncLoop: result: " + mResult); - } - - if (!mResult.madeSomeProgress()) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "runSyncLoop: No data from client diffs merge"); - } - break; - } - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "runSyncLoop: made some progress, looping"); - } - } - - // add in any status codes that we saved from earlier - mResult.tooManyRetries |= tooManyGetServerDiffsAttempts; - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "runSyncLoop: final result: " + mResult); - } - } finally { - // do this in the finally block to guarantee that is is set and not overwritten - if (discardLocalDeletions) { - mResult.fullSyncRequested = true; - } - if (serverDiffs != null) serverDiffs.close(); - if (result.tempContentProvider != null) result.tempContentProvider.close(); - syncTimer.addSplit("stop"); - syncTimer.dumpToLog(); - } - } - } - - /** - * Logs details on the sync. - * Normally this will be overridden by a subclass that will provide - * provider-specific details. - * - * @param bytesSent number of bytes the sync sent over the network - * @param bytesReceived number of bytes the sync received over the network - * @param result The SyncResult object holding info on the sync - */ - protected void logSyncDetails(long bytesSent, long bytesReceived, SyncResult result) { - EventLog.writeEvent(SyncAdapter.LOG_SYNC_DETAILS, TAG, bytesSent, bytesReceived, ""); - } - - public void startSync(SyncContext syncContext, Account account, String authority, - Bundle extras) { - if (mSyncThread != null) { - syncContext.onFinished(SyncResult.ALREADY_IN_PROGRESS); - return; - } - - mSyncThread = new SyncThread(syncContext, account, authority, extras); - mSyncThread.start(); - } - - public void cancelSync() { - if (mSyncThread != null) { - mSyncThread.cancelSync(); - } - } - - protected boolean hasTooManyDeletions(SyncStats stats) { - long numEntries = stats.numEntries; - long numDeletedEntries = stats.numDeletes; - - long percentDeleted = (numDeletedEntries == 0) - ? 0 - : (100 * numDeletedEntries / - (numEntries + numDeletedEntries)); - boolean tooManyDeletions = - (numDeletedEntries > NUM_ALLOWED_SIMULTANEOUS_DELETIONS) - && (percentDeleted > PERCENT_ALLOWED_SIMULTANEOUS_DELETIONS); - return tooManyDeletions; - } -} diff --git a/core/java/android/content/TempProviderSyncResult.java b/core/java/android/content/TempProviderSyncResult.java deleted file mode 100644 index 81f6f79..0000000 --- a/core/java/android/content/TempProviderSyncResult.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (C) 2007 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.content; - -/** - * Used to hold data returned from a given phase of a TempProviderSync. - * @hide - */ -public class TempProviderSyncResult { - /** - * An interface to a temporary content provider that contains - * the result of updates that were sent to the server. This - * provider must be merged into the permanent content provider. - * This may be null, which indicates that there is nothing to - * merge back into the content provider. - */ - public SyncableContentProvider tempContentProvider; - - public TempProviderSyncResult() { - tempContentProvider = null; - } -} diff --git a/core/java/android/content/package.html b/core/java/android/content/package.html index dd5360f..eac679d 100644 --- a/core/java/android/content/package.html +++ b/core/java/android/content/package.html @@ -421,7 +421,7 @@ can supply them explicitly in the XML file:</p> <?xml version="1.0" encoding="utf-8"?> <root> <EditText id="text" - android:layout_width="fill_parent" android:layout_height="fill_parent" + android:layout_width="match_parent" android:layout_height="match_parent" <b>android:textSize="18" android:textColor="#008"</b> android:text="Hello, World!" /> </root> @@ -447,7 +447,7 @@ one of those resources:</p> <?xml version="1.0" encoding="utf-8"?> <root> <EditText id="text" - android:layout_width="fill_parent" android:layout_height="fill_parent" + android:layout_width="match_parent" android:layout_height="match_parent" <b>android:textColor="@color/opaque_red"</b> android:text="Hello, World!" /> </root> @@ -463,7 +463,7 @@ reference a system resource, you would need to write:</p> <?xml version="1.0" encoding="utf-8"?> <root> <EditText id="text" - android:layout_width="fill_parent" android:layout_height="fill_parent" + android:layout_width="match_parent" android:layout_height="match_parent" android:textColor="@<b>android:</b>color/opaque_red" android:text="Hello, World!" /> </root> @@ -476,7 +476,7 @@ strings in a layout file so that they can be localized:</p> <?xml version="1.0" encoding="utf-8"?> <root> <EditText id="text" - android:layout_width="fill_parent" android:layout_height="fill_parent" + android:layout_width="match_parent" android:layout_height="match_parent" android:textColor="@android:color/opaque_red" android:text="@string/hello_world" /> </root> @@ -509,7 +509,7 @@ one of the standard colors defined in the base system theme:</p> <?xml version="1.0" encoding="utf-8"?> <root> <EditText id="text" - android:layout_width="fill_parent" android:layout_height="fill_parent" + android:layout_width="match_parent" android:layout_height="match_parent" <b>android:textColor="?android:textDisabledColor"</b> android:text="@string/hello_world" /> </root> @@ -637,10 +637,10 @@ new style resource with the desired values:</p> <?xml version="1.0" encoding="utf-8"?> <root> <EditText id="text1" <b>style="@style/SpecialText"</b> - android:layout_width="fill_parent" android:layout_height="wrap_content" + android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Hello, World!" /> <EditText id="text2" <b>style="@style/SpecialText"</b> - android:layout_width="fill_parent" android:layout_height="wrap_content" + android:layout_width="match_parent" android:layout_height="wrap_content" android:text="I love you all." /> </root></pre> <h4> </h4> diff --git a/core/java/android/content/pm/ApplicationInfo.java b/core/java/android/content/pm/ApplicationInfo.java index 1800c30..3dea286 100644 --- a/core/java/android/content/pm/ApplicationInfo.java +++ b/core/java/android/content/pm/ApplicationInfo.java @@ -208,6 +208,16 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { public static final int FLAG_RESTORE_NEEDS_APPLICATION = 1<<16; /** + * Value for {@link #flags}: this is true if the application has set + * its android:neverEncrypt to true, false otherwise. It is used to specify + * that this package specifically "opts-out" of a secured file system solution, + * and will always store its data in-the-clear. + * + * {@hide} + */ + public static final int FLAG_NEVER_ENCRYPT = 1<<17; + + /** * Flags associated with the application. Any combination of * {@link #FLAG_SYSTEM}, {@link #FLAG_DEBUGGABLE}, {@link #FLAG_HAS_CODE}, * {@link #FLAG_PERSISTENT}, {@link #FLAG_FACTORY_TEST}, and diff --git a/core/java/android/content/pm/PackageParser.java b/core/java/android/content/pm/PackageParser.java index 3f8c71e..ad99f54 100644 --- a/core/java/android/content/pm/PackageParser.java +++ b/core/java/android/content/pm/PackageParser.java @@ -35,7 +35,7 @@ import android.util.Config; import android.util.DisplayMetrics; import android.util.Log; import android.util.TypedValue; -import com.android.internal.util.XmlUtils; +import com.android.common.XmlUtils; import java.io.File; import java.io.IOException; @@ -1399,6 +1399,12 @@ public class PackageParser { ai.flags |= ApplicationInfo.FLAG_TEST_ONLY; } + if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestApplication_neverEncrypt, + false)) { + ai.flags |= ApplicationInfo.FLAG_NEVER_ENCRYPT; + } + String str; str = sa.getNonResourceString( com.android.internal.R.styleable.AndroidManifestApplication_permission); diff --git a/core/java/android/content/pm/RegisteredServicesCache.java b/core/java/android/content/pm/RegisteredServicesCache.java index b39a67d..b819fa0 100644 --- a/core/java/android/content/pm/RegisteredServicesCache.java +++ b/core/java/android/content/pm/RegisteredServicesCache.java @@ -43,7 +43,7 @@ import java.io.IOException; import java.io.FileInputStream; import com.android.internal.os.AtomicFile; -import com.android.internal.util.FastXmlSerializer; +import com.android.common.FastXmlSerializer; import com.google.android.collect.Maps; import com.google.android.collect.Lists; diff --git a/core/java/android/content/res/ColorStateList.java b/core/java/android/content/res/ColorStateList.java index 453a83d..70baaef 100644 --- a/core/java/android/content/res/ColorStateList.java +++ b/core/java/android/content/res/ColorStateList.java @@ -44,7 +44,7 @@ import java.util.Arrays; * <selector xmlns:android="http://schemas.android.com/apk/res/android"> * <item android:state_focused="true" android:color="@color/testcolor1"/> * <item android:state_pressed="true" android:state_enabled="false" android:color="@color/testcolor2" /> - * <item android:state_enabled="false" android:colore="@color/testcolor3" /> + * <item android:state_enabled="false" android:color="@color/testcolor3" /> * <item android:state_active="true" android:color="@color/testcolor4" /> * <item android:color="@color/testcolor5"/> * </selector> diff --git a/core/java/android/content/res/Resources.java b/core/java/android/content/res/Resources.java index 1c0ed36..e4fc259 100644 --- a/core/java/android/content/res/Resources.java +++ b/core/java/android/content/res/Resources.java @@ -17,7 +17,7 @@ package android.content.res; -import com.android.internal.util.XmlUtils; +import com.android.common.XmlUtils; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; diff --git a/core/java/android/content/res/StringBlock.java b/core/java/android/content/res/StringBlock.java index 8fb82be..2411177 100644 --- a/core/java/android/content/res/StringBlock.java +++ b/core/java/android/content/res/StringBlock.java @@ -24,7 +24,7 @@ import android.util.SparseArray; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.Typeface; -import com.android.internal.util.XmlUtils; +import com.android.common.XmlUtils; /** * Conveniences for retrieving data out of a compiled string resource. diff --git a/core/java/android/content/res/TypedArray.java b/core/java/android/content/res/TypedArray.java index 016ee7f..8f0003b 100644 --- a/core/java/android/content/res/TypedArray.java +++ b/core/java/android/content/res/TypedArray.java @@ -5,7 +5,7 @@ import android.util.AttributeSet; import android.util.DisplayMetrics; import android.util.Log; import android.util.TypedValue; -import com.android.internal.util.XmlUtils; +import com.android.common.XmlUtils; import java.util.Arrays; diff --git a/core/java/android/content/res/XmlBlock.java b/core/java/android/content/res/XmlBlock.java index 6336678..f800232 100644 --- a/core/java/android/content/res/XmlBlock.java +++ b/core/java/android/content/res/XmlBlock.java @@ -17,7 +17,7 @@ package android.content.res; import android.util.TypedValue; -import com.android.internal.util.XmlUtils; +import com.android.common.XmlUtils; import org.xmlpull.v1.XmlPullParserException; diff --git a/core/java/android/database/DatabaseUtils.java b/core/java/android/database/DatabaseUtils.java index 4ca6601..9bfbb74 100644 --- a/core/java/android/database/DatabaseUtils.java +++ b/core/java/android/database/DatabaseUtils.java @@ -671,6 +671,102 @@ public class DatabaseUtils { } /** + * Reads a String out of a column in a Cursor and writes it to a ContentValues. + * Adds nothing to the ContentValues if the column isn't present or if its value is null. + * + * @param cursor The cursor to read from + * @param column The column to read + * @param values The {@link ContentValues} to put the value into + */ + public static void cursorStringToContentValuesIfPresent(Cursor cursor, ContentValues values, + String column) { + final int index = cursor.getColumnIndexOrThrow(column); + if (!cursor.isNull(index)) { + values.put(column, cursor.getString(index)); + } + } + + /** + * Reads a Long out of a column in a Cursor and writes it to a ContentValues. + * Adds nothing to the ContentValues if the column isn't present or if its value is null. + * + * @param cursor The cursor to read from + * @param column The column to read + * @param values The {@link ContentValues} to put the value into + */ + public static void cursorLongToContentValuesIfPresent(Cursor cursor, ContentValues values, + String column) { + final int index = cursor.getColumnIndexOrThrow(column); + if (!cursor.isNull(index)) { + values.put(column, cursor.getLong(index)); + } + } + + /** + * Reads a Short out of a column in a Cursor and writes it to a ContentValues. + * Adds nothing to the ContentValues if the column isn't present or if its value is null. + * + * @param cursor The cursor to read from + * @param column The column to read + * @param values The {@link ContentValues} to put the value into + */ + public static void cursorShortToContentValuesIfPresent(Cursor cursor, ContentValues values, + String column) { + final int index = cursor.getColumnIndexOrThrow(column); + if (!cursor.isNull(index)) { + values.put(column, cursor.getShort(index)); + } + } + + /** + * Reads a Integer out of a column in a Cursor and writes it to a ContentValues. + * Adds nothing to the ContentValues if the column isn't present or if its value is null. + * + * @param cursor The cursor to read from + * @param column The column to read + * @param values The {@link ContentValues} to put the value into + */ + public static void cursorIntToContentValuesIfPresent(Cursor cursor, ContentValues values, + String column) { + final int index = cursor.getColumnIndexOrThrow(column); + if (!cursor.isNull(index)) { + values.put(column, cursor.getInt(index)); + } + } + + /** + * Reads a Float out of a column in a Cursor and writes it to a ContentValues. + * Adds nothing to the ContentValues if the column isn't present or if its value is null. + * + * @param cursor The cursor to read from + * @param column The column to read + * @param values The {@link ContentValues} to put the value into + */ + public static void cursorFloatToContentValuesIfPresent(Cursor cursor, ContentValues values, + String column) { + final int index = cursor.getColumnIndexOrThrow(column); + if (!cursor.isNull(index)) { + values.put(column, cursor.getFloat(index)); + } + } + + /** + * Reads a Double out of a column in a Cursor and writes it to a ContentValues. + * Adds nothing to the ContentValues if the column isn't present or if its value is null. + * + * @param cursor The cursor to read from + * @param column The column to read + * @param values The {@link ContentValues} to put the value into + */ + public static void cursorDoubleToContentValuesIfPresent(Cursor cursor, ContentValues values, + String column) { + final int index = cursor.getColumnIndexOrThrow(column); + if (!cursor.isNull(index)) { + values.put(column, cursor.getDouble(index)); + } + } + + /** * This class allows users to do multiple inserts into a table but * compile the SQL insert statement only once, which may increase * performance. diff --git a/core/java/android/database/sqlite/SQLiteCompiledSql.java b/core/java/android/database/sqlite/SQLiteCompiledSql.java new file mode 100644 index 0000000..79527b4 --- /dev/null +++ b/core/java/android/database/sqlite/SQLiteCompiledSql.java @@ -0,0 +1,111 @@ +/* + * 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 android.database.sqlite; + +import android.util.Log; + +/** + * This class encapsulates compilation of sql statement and release of the compiled statement obj. + * Once a sql statement is compiled, it is cached in {@link SQLiteDatabase} + * and it is released in one of the 2 following ways + * 1. when {@link SQLiteDatabase} object is closed. + * 2. dalvikVM wants to reclaim some memory and releases it from the cache in + * {@link SQLiteDatabase}. + */ +/* package */ class SQLiteCompiledSql { + + /** The database this program is compiled against. */ + /* package */ SQLiteDatabase mDatabase; + + /** + * Native linkage, do not modify. This comes from the database. + */ + /* package */ int nHandle = 0; + + /** + * Native linkage, do not modify. When non-0 this holds a reference to a valid + * sqlite3_statement object. It is only updated by the native code, but may be + * checked in this class when the database lock is held to determine if there + * is a valid native-side program or not. + */ + /* package */ int nStatement = 0; + + /* package */ SQLiteCompiledSql(SQLiteDatabase db, String sql) { + mDatabase = db; + this.nHandle = db.mNativeHandle; + compile(sql, true); + } + + /** + * Compiles the given SQL into a SQLite byte code program using sqlite3_prepare_v2(). If + * this method has been called previously without a call to close and forCompilation is set + * to false the previous compilation will be used. Setting forceCompilation to true will + * always re-compile the program and should be done if you pass differing SQL strings to this + * method. + * + * <P>Note: this method acquires the database lock.</P> + * + * @param sql the SQL string to compile + * @param forceCompilation forces the SQL to be recompiled in the event that there is an + * existing compiled SQL program already around + */ + private void compile(String sql, boolean forceCompilation) { + // Only compile if we don't have a valid statement already or the caller has + // explicitly requested a recompile. + if (forceCompilation) { + mDatabase.lock(); + try { + // Note that the native_compile() takes care of destroying any previously + // existing programs before it compiles. + native_compile(sql); + } finally { + mDatabase.unlock(); + } + } + } + + /* package */ void releaseSqlStatement() { + // Note that native_finalize() checks to make sure that nStatement is + // non-null before destroying it. + if (nStatement != 0) { + try { + mDatabase.lock(); + native_finalize(); + nStatement = 0; + } finally { + mDatabase.unlock(); + } + } + } + + /** + * Make sure that the native resource is cleaned up. + */ + @Override + protected void finalize() { + releaseSqlStatement(); + } + + /** + * Compiles SQL into a SQLite program. + * + * <P>The database lock must be held when calling this method. + * @param sql The SQL to compile. + */ + private final native void native_compile(String sql); + private final native void native_finalize(); +} diff --git a/core/java/android/database/sqlite/SQLiteCursor.java b/core/java/android/database/sqlite/SQLiteCursor.java index 70b9b83..b178d4f 100644 --- a/core/java/android/database/sqlite/SQLiteCursor.java +++ b/core/java/android/database/sqlite/SQLiteCursor.java @@ -582,21 +582,23 @@ public class SQLiteCursor extends AbstractWindowedCursor { @Override protected void finalize() { try { + // if the cursor hasn't been closed yet, close it first if (mWindow != null) { close(); - String message = "Finalizing cursor " + this + " on " + mEditTable - + " that has not been deactivated or closed"; + Log.e(TAG, "Finalizing cursor that has not been deactivated or closed." + + " database = " + mDatabase.getPath() + ", table = " + mEditTable + + ", query = " + mQuery.mSql); if (SQLiteDebug.DEBUG_ACTIVE_CURSOR_FINALIZATION) { - Log.d(TAG, message + "\nThis cursor was created in:"); + Log.d(TAG, "This cursor was created in:"); for (StackTraceElement ste : mStackTraceElements) { Log.d(TAG, " " + ste); } } SQLiteDebug.notifyActiveCursorFinalized(); - throw new IllegalStateException(message); } else { if (Config.LOGV) { - Log.v(TAG, "Finalizing cursor " + this + " on " + mEditTable); + Log.v(TAG, "Finalizing cursor on database = " + mDatabase.getPath() + + ", table = " + mEditTable + ", query = " + mQuery.mSql); } } } finally { diff --git a/core/java/android/database/sqlite/SQLiteDatabase.java b/core/java/android/database/sqlite/SQLiteDatabase.java index 9ebf5d9..f310586 100644 --- a/core/java/android/database/sqlite/SQLiteDatabase.java +++ b/core/java/android/database/sqlite/SQLiteDatabase.java @@ -16,6 +16,9 @@ package android.database.sqlite; +import com.google.android.collect.Maps; + +import android.app.ActivityThread; import android.content.ContentValues; import android.database.Cursor; import android.database.DatabaseUtils; @@ -29,10 +32,12 @@ import android.util.EventLog; import android.util.Log; import java.io.File; +import java.text.SimpleDateFormat; import java.util.HashMap; import java.util.Iterator; import java.util.Locale; import java.util.Map; +import java.util.Random; import java.util.Set; import java.util.WeakHashMap; import java.util.concurrent.locks.ReentrantLock; @@ -200,6 +205,11 @@ public class SQLiteDatabase extends SQLiteClosable { private long mLastLockMessageTime = 0L; + // always log queries which take 100ms+; shorter queries are sampled accordingly + private static final int QUERY_LOG_TIME_IN_NANOS = 100 * 1000000; + private static final int QUERY_LOG_SQL_LENGTH = 64; + private final Random mRandom = new Random(); + /** Used by native code, do not rename */ /* package */ int mNativeHandle = 0; @@ -217,11 +227,35 @@ public class SQLiteDatabase extends SQLiteClosable { private WeakHashMap<SQLiteClosable, Object> mPrograms; - private final RuntimeException mLeakedException; + /** + * for each instance of this class, a cache is maintained to store + * the compiled query statement ids returned by sqlite database. + * key = sql statement with "?" for bind args + * value = {@link SQLiteCompiledSql} + * If an application opens the database and keeps it open during its entire life, then + * there will not be an overhead of compilation of sql statements by sqlite. + * + * why is this cache NOT static? because sqlite attaches compiledsql statements to the + * struct created when {@link SQLiteDatabase#openDatabase(String, CursorFactory, int)} is + * invoked. + * + * this cache has an upper limit of mMaxSqlCacheSize (settable by calling the method + * (@link setMaxCacheSize(int)}). its default is 0 - i.e., no caching by default because + * most of the apps don't use "?" syntax in their sql, caching is not useful for them. + */ + private Map<String, SQLiteCompiledSql> mCompiledQueries = Maps.newHashMap(); + private int mMaxSqlCacheSize = 0; // no caching by default + private static final int MAX_SQL_CACHE_SIZE = 1000; - // package visible, since callers will access directly to minimize overhead in the case - // that logging is not enabled. - /* package */ final boolean mLogStats; + /** maintain stats about number of cache hits and misses */ + private int mNumCacheHits; + private int mNumCacheMisses; + + /** the following 2 members maintain the time when a database is opened and closed */ + private String mTimeOpened = null; + private String mTimeClosed = null; + + private final RuntimeException mLeakedException; // System property that enables logging of slow queries. Specify the threshold in ms. private static final String LOG_SLOW_QUERIES_PROPERTY = "db.log.slow_query_threshold"; @@ -251,6 +285,9 @@ public class SQLiteDatabase extends SQLiteClosable { @Override protected void onAllReferencesReleased() { if (isOpen()) { + if (SQLiteDebug.DEBUG_SQL_CACHE) { + mTimeClosed = getTime(); + } dbclose(); } } @@ -799,6 +836,13 @@ public class SQLiteDatabase extends SQLiteClosable { program.onAllReferencesReleasedFromContainer(); } } + + // finalize all compiled sql statement objects in compiledQueries cache + synchronized (mCompiledQueries) { + for (SQLiteCompiledSql compiledStatement : mCompiledQueries.values()) { + compiledStatement.releaseSqlStatement(); + } + } } /** @@ -1603,8 +1647,7 @@ public class SQLiteDatabase extends SQLiteClosable { * @throws SQLException If the SQL string is invalid for some reason */ public void execSQL(String sql) throws SQLException { - boolean logStats = mLogStats; - long timeStart = logStats ? SystemClock.elapsedRealtime() : 0; + long timeStart = Debug.threadCpuTimeNanos(); lock(); try { native_execSQL(sql); @@ -1614,9 +1657,7 @@ public class SQLiteDatabase extends SQLiteClosable { } finally { unlock(); } - if (logStats) { - logTimeStat(false /* not a read */, timeStart, SystemClock.elapsedRealtime()); - } + logTimeStat(sql, timeStart); } /** @@ -1633,8 +1674,7 @@ public class SQLiteDatabase extends SQLiteClosable { throw new IllegalArgumentException("Empty bindArgs"); } - boolean logStats = mLogStats; - long timeStart = logStats ? SystemClock.elapsedRealtime() : 0; + long timeStart = Debug.threadCpuTimeNanos(); lock(); SQLiteStatement statement = null; try { @@ -1655,9 +1695,7 @@ public class SQLiteDatabase extends SQLiteClosable { } unlock(); } - if (logStats) { - logTimeStat(false /* not a read */, timeStart, SystemClock.elapsedRealtime()); - } + logTimeStat(sql, timeStart); } @Override @@ -1689,23 +1727,32 @@ public class SQLiteDatabase extends SQLiteClosable { } mFlags = flags; mPath = path; - mLogStats = "1".equals(android.os.SystemProperties.get("db.logstats")); mSlowQueryThreshold = SystemProperties.getInt(LOG_SLOW_QUERIES_PROPERTY, -1); mLeakedException = new IllegalStateException(path + " SQLiteDatabase created and never closed"); mFactory = factory; dbopen(mPath, mFlags); + if (SQLiteDebug.DEBUG_SQL_CACHE) { + mTimeOpened = getTime(); + } mPrograms = new WeakHashMap<SQLiteClosable,Object>(); try { setLocale(Locale.getDefault()); } catch (RuntimeException e) { Log.e(TAG, "Failed to setLocale() when constructing, closing the database", e); dbclose(); + if (SQLiteDebug.DEBUG_SQL_CACHE) { + mTimeClosed = getTime(); + } throw e; } } + private String getTime() { + return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS ").format(System.currentTimeMillis()); + } + /** * return whether the DB is opened as read only. * @return true if DB is opened as read only @@ -1734,8 +1781,141 @@ public class SQLiteDatabase extends SQLiteClosable { return mPath; } - /* package */ void logTimeStat(boolean read, long begin, long end) { - EventLog.writeEvent(EVENT_DB_OPERATION, mPath, read ? 0 : 1, end - begin); + /** + * set the max size of the compiled sql cache for this database after purging the cache. + * (size of the cache = number of compiled-sql-statements stored in the cache) + * + * synchronized because we don't want t threads to change cache size at the same time. + * @param cacheSize the size of the cache. can be (0 to MAX_SQL_CACHE_SIZE) + */ + public void setMaxSqlCacheSize(int cacheSize) { + synchronized(mCompiledQueries) { + resetCompiledSqlCache(); + mMaxSqlCacheSize = (cacheSize > MAX_SQL_CACHE_SIZE) ? MAX_SQL_CACHE_SIZE + : (cacheSize < 0) ? 0 : cacheSize; + } + } + + /** + * remove everything from the compiled sql cache + */ + public void resetCompiledSqlCache() { + synchronized(mCompiledQueries) { + mCompiledQueries.clear(); + } + } + + /** + * adds the given sql and its compiled-statement-id-returned-by-sqlite to the + * cache of compiledQueries attached to 'this'. + * + * if there is already a {@link SQLiteCompiledSql} in compiledQueries for the given sql, + * the new {@link SQLiteCompiledSql} object is NOT inserted into the cache (i.e.,the current + * mapping is NOT replaced with the new mapping). + * + * @return true if the given obj is added to cache. false otherwise. + */ + /* package */ boolean addToCompiledQueries(String sql, SQLiteCompiledSql compiledStatement) { + if (mMaxSqlCacheSize == 0) { + // for this database, there is no cache of compiled sql. + if (SQLiteDebug.DEBUG_SQL_CACHE) { + Log.v(TAG, "|NOT adding_sql_to_cache|" + getPath() + "|" + sql); + } + return false; + } + + SQLiteCompiledSql compiledSql = null; + synchronized(mCompiledQueries) { + // don't insert the new mapping if a mapping already exists + compiledSql = mCompiledQueries.get(sql); + if (compiledSql != null) { + return false; + } + // add this <sql, compiledStatement> to the cache + if (mCompiledQueries.size() == mMaxSqlCacheSize) { + /* reached max cachesize. before adding new entry, remove an entry from the + * cache. we don't want to wipe out the entire cache because of this: + * GCing {@link SQLiteCompiledSql} requires call to sqlite3_finalize + * JNI method. If entire cache is wiped out, it could be cause a big GC activity + * just because a (rogue) process is using the cache incorrectly. + */ + Set<String> keySet = mCompiledQueries.keySet(); + for (String s : keySet) { + mCompiledQueries.remove(s); + break; + } + } + compiledSql = new SQLiteCompiledSql(this, sql); + mCompiledQueries.put(sql, compiledSql); + } + if (SQLiteDebug.DEBUG_SQL_CACHE) { + Log.v(TAG, "|adding_sql_to_cache|" + getPath() + "|" + mCompiledQueries.size() + "|" + + sql); + } + return true; + } + + /** + * from the compiledQueries cache, returns the compiled-statement-id for the given sql. + * returns null, if not found in the cache. + */ + /* package */ SQLiteCompiledSql getCompiledStatementForSql(String sql) { + SQLiteCompiledSql compiledStatement = null; + boolean cacheHit; + synchronized(mCompiledQueries) { + if (mMaxSqlCacheSize == 0) { + // for this database, there is no cache of compiled sql. + if (SQLiteDebug.DEBUG_SQL_CACHE) { + Log.v(TAG, "|cache NOT found|" + getPath()); + } + return null; + } + cacheHit = (compiledStatement = mCompiledQueries.get(sql)) != null; + } + if (cacheHit) { + mNumCacheHits++; + } else { + mNumCacheMisses++; + } + + if (SQLiteDebug.DEBUG_SQL_CACHE) { + Log.v(TAG, "|cache_stats|" + + getPath() + "|" + mCompiledQueries.size() + + "|" + mNumCacheHits + "|" + mNumCacheMisses + + "|" + cacheHit + "|" + mTimeOpened + "|" + mTimeClosed + "|" + sql); + } + return compiledStatement; + } + + /* package */ void logTimeStat(String sql, long beginNanos) { + // Sample fast queries in proportion to the time taken. + // Quantize the % first, so the logged sampling probability + // exactly equals the actual sampling rate for this query. + + int samplePercent; + long nanos = Debug.threadCpuTimeNanos() - beginNanos; + if (nanos >= QUERY_LOG_TIME_IN_NANOS) { + samplePercent = 100; + } else { + samplePercent = (int) (100 * nanos / QUERY_LOG_TIME_IN_NANOS) + 1; + if (mRandom.nextInt(100) >= samplePercent) return; + } + + if (sql.length() > QUERY_LOG_SQL_LENGTH) sql = sql.substring(0, QUERY_LOG_SQL_LENGTH); + + // ActivityThread.currentPackageName() only returns non-null if the + // current thread is an application main thread. This parameter tells + // us whether an event loop is blocked, and if so, which app it is. + // + // Sadly, there's no fast way to determine app name if this is *not* a + // main thread, or when we are invoked via Binder (e.g. ContentProvider). + // Hopefully the full path to the database will be informative enough. + + String blockingPackage = ActivityThread.currentPackageName(); + if (blockingPackage == null) blockingPackage = ""; + + int millis = (int) (nanos / 1000000); + EventLog.writeEvent(EVENT_DB_OPERATION, mPath, sql, millis, blockingPackage, samplePercent); } /** diff --git a/core/java/android/database/sqlite/SQLiteDebug.java b/core/java/android/database/sqlite/SQLiteDebug.java index 84d8879..d4d3059 100644 --- a/core/java/android/database/sqlite/SQLiteDebug.java +++ b/core/java/android/database/sqlite/SQLiteDebug.java @@ -32,6 +32,12 @@ public final class SQLiteDebug { Log.isLoggable("SQLiteStatements", Log.VERBOSE); /** + * Controls the printing of compiled-sql-statement cache stats. + */ + public static final boolean DEBUG_SQL_CACHE = + Log.isLoggable("SQLiteCompiledSql", Log.VERBOSE); + + /** * Controls the stack trace reporting of active cursors being * finalized. */ diff --git a/core/java/android/database/sqlite/SQLiteProgram.java b/core/java/android/database/sqlite/SQLiteProgram.java index 9e85452..edc15cb 100644 --- a/core/java/android/database/sqlite/SQLiteProgram.java +++ b/core/java/android/database/sqlite/SQLiteProgram.java @@ -27,6 +27,9 @@ public abstract class SQLiteProgram extends SQLiteClosable { /** The database this program is compiled against. */ protected SQLiteDatabase mDatabase; + /** The SQL used to create this query */ + /* package */ final String mSql; + /** * Native linkage, do not modify. This comes from the database and should not be modified * in here or in the native code. @@ -34,87 +37,88 @@ public abstract class SQLiteProgram extends SQLiteClosable { protected int nHandle = 0; /** - * Native linkage, do not modify. When non-0 this holds a reference to a valid - * sqlite3_statement object. It is only updated by the native code, but may be - * checked in this class when the database lock is held to determine if there - * is a valid native-side program or not. + * the compiledSql object for the given sql statement. */ - protected int nStatement = 0; + private SQLiteCompiledSql compiledSql; + private boolean myCompiledSqlIsInCache; /** - * Used to find out where a cursor was allocated in case it never got - * released. + * compiledSql statement id is populated with the corresponding object from the above + * member compiledSql. + * this member is used by the native_bind_* methods */ - private StackTraceElement[] mStackTraceElements; - + protected int nStatement = 0; + /* package */ SQLiteProgram(SQLiteDatabase db, String sql) { if (SQLiteDebug.DEBUG_SQL_STATEMENTS) { - mStackTraceElements = new Exception().getStackTrace(); + Log.d(TAG, "processing sql: " + sql); } - + mDatabase = db; + mSql = sql; db.acquireReference(); db.addSQLiteClosable(this); this.nHandle = db.mNativeHandle; - compile(sql, false); - } - + + compiledSql = db.getCompiledStatementForSql(sql); + if (compiledSql == null) { + // create a new compiled-sql obj + compiledSql = new SQLiteCompiledSql(db, sql); + + // add it to the cache of compiled-sqls + myCompiledSqlIsInCache = db.addToCompiledQueries(sql, compiledSql); + } else { + myCompiledSqlIsInCache = true; + } + nStatement = compiledSql.nStatement; + } + @Override protected void onAllReferencesReleased() { - // Note that native_finalize() checks to make sure that nStatement is - // non-null before destroying it. - native_finalize(); + // release the compiled sql statement used by me if it is NOT in cache + if (!myCompiledSqlIsInCache && compiledSql != null) { + compiledSql.releaseSqlStatement(); + compiledSql = null; // so that GC doesn't call finalize() on it + } mDatabase.releaseReference(); mDatabase.removeSQLiteClosable(this); } - + @Override - protected void onAllReferencesReleasedFromContainer(){ - // Note that native_finalize() checks to make sure that nStatement is - // non-null before destroying it. - native_finalize(); - mDatabase.releaseReference(); + protected void onAllReferencesReleasedFromContainer() { + // release the compiled sql statement used by me if it is NOT in cache + if (!myCompiledSqlIsInCache && compiledSql != null) { + compiledSql.releaseSqlStatement(); + compiledSql = null; // so that GC doesn't call finalize() on it + } + mDatabase.releaseReference(); } /** * Returns a unique identifier for this program. - * + * * @return a unique identifier for this program */ public final int getUniqueId() { - return nStatement; + return compiledSql.nStatement; + } + + /* package */ String getSqlString() { + return mSql; } /** - * Compiles the given SQL into a SQLite byte code program using sqlite3_prepare_v2(). If - * this method has been called previously without a call to close and forCompilation is set - * to false the previous compilation will be used. Setting forceCompilation to true will - * always re-compile the program and should be done if you pass differing SQL strings to this - * method. - * - * <P>Note: this method acquires the database lock.</P> + * @deprecated use this.compiledStatement.compile instead * * @param sql the SQL string to compile * @param forceCompilation forces the SQL to be recompiled in the event that there is an * existing compiled SQL program already around */ + @Deprecated protected void compile(String sql, boolean forceCompilation) { - // Only compile if we don't have a valid statement already or the caller has - // explicitly requested a recompile. - if (nStatement == 0 || forceCompilation) { - mDatabase.lock(); - try { - // Note that the native_compile() takes care of destroying any previously - // existing programs before it compiles. - acquireReference(); - native_compile(sql); - } finally { - releaseReference(); - mDatabase.unlock(); - } - } - } - + // TODO is there a need for this? + } + /** * Bind a NULL value to this statement. The value remains bound until * {@link #clearBindings} is called. @@ -221,37 +225,18 @@ public abstract class SQLiteProgram extends SQLiteClosable { releaseReference(); } finally { mDatabase.unlock(); - } - } - - /** - * Make sure that the native resource is cleaned up. - */ - @Override - protected void finalize() { - if (nStatement != 0) { - if (SQLiteDebug.DEBUG_SQL_STATEMENTS) { - String message = "Finalizing " + this + - " that has not been closed"; - - Log.d(TAG, message + "\nThis cursor was created in:"); - for (StackTraceElement ste : mStackTraceElements) { - Log.d(TAG, " " + ste); - } - } - // when in finalize() it is already removed from weakhashmap - // so it is safe to not removed itself from db - onAllReferencesReleasedFromContainer(); } } /** * Compiles SQL into a SQLite program. - * + * * <P>The database lock must be held when calling this method. * @param sql The SQL to compile. */ + @Deprecated protected final native void native_compile(String sql); + @Deprecated protected final native void native_finalize(); protected final native void native_bind_null(int index); diff --git a/core/java/android/database/sqlite/SQLiteQuery.java b/core/java/android/database/sqlite/SQLiteQuery.java index cdd9f86..c34661d 100644 --- a/core/java/android/database/sqlite/SQLiteQuery.java +++ b/core/java/android/database/sqlite/SQLiteQuery.java @@ -17,6 +17,7 @@ package android.database.sqlite; import android.database.CursorWindow; +import android.os.Debug; import android.os.SystemClock; import android.util.Log; @@ -30,9 +31,6 @@ public class SQLiteQuery extends SQLiteProgram { /** The index of the unbound OFFSET parameter */ private int mOffsetIndex; - /** The SQL used to create this query */ - private String mQuery; - /** Args to bind on requery */ private String[] mBindArgs; @@ -49,7 +47,6 @@ public class SQLiteQuery extends SQLiteProgram { super(db, query); mOffsetIndex = offsetIndex; - mQuery = query; mBindArgs = bindArgs; } @@ -61,10 +58,9 @@ public class SQLiteQuery extends SQLiteProgram { */ /* package */ int fillWindow(CursorWindow window, int maxRead, int lastPos) { + long timeStart = Debug.threadCpuTimeNanos(); mDatabase.lock(); - boolean logStats = mDatabase.mLogStats; - long startTime = logStats ? SystemClock.elapsedRealtime() : 0; try { acquireReference(); try { @@ -77,12 +73,9 @@ public class SQLiteQuery extends SQLiteProgram { // Logging if (SQLiteDebug.DEBUG_SQL_STATEMENTS) { - Log.d(TAG, "fillWindow(): " + mQuery); - } - if (logStats) { - mDatabase.logTimeStat(true /* read */, startTime, - SystemClock.elapsedRealtime()); + Log.d(TAG, "fillWindow(): " + mSql); } + mDatabase.logTimeStat(mSql, timeStart); return numRows; } catch (IllegalStateException e){ // simply ignore it @@ -133,7 +126,7 @@ public class SQLiteQuery extends SQLiteProgram { @Override public String toString() { - return "SQLiteQuery: " + mQuery; + return "SQLiteQuery: " + mSql; } @Override @@ -153,7 +146,7 @@ public class SQLiteQuery extends SQLiteProgram { super.bindString(i + 1, mBindArgs[i]); } } catch (SQLiteMisuseException e) { - StringBuilder errMsg = new StringBuilder("mQuery " + mQuery); + StringBuilder errMsg = new StringBuilder("mSql " + mSql); for (int i = 0; i < len; i++) { errMsg.append(" "); errMsg.append(mBindArgs[i]); diff --git a/core/java/android/database/sqlite/SQLiteStatement.java b/core/java/android/database/sqlite/SQLiteStatement.java index 5889ad9..cc714ee 100644 --- a/core/java/android/database/sqlite/SQLiteStatement.java +++ b/core/java/android/database/sqlite/SQLiteStatement.java @@ -16,6 +16,7 @@ package android.database.sqlite; +import android.os.Debug; import android.os.SystemClock; import android.util.Log; @@ -29,8 +30,6 @@ public class SQLiteStatement extends SQLiteProgram { private static final String TAG = "SQLiteStatement"; - private final String mSql; - /** * Don't use SQLiteStatement constructor directly, please use * {@link SQLiteDatabase#compileStatement(String)} @@ -39,11 +38,6 @@ public class SQLiteStatement extends SQLiteProgram */ /* package */ SQLiteStatement(SQLiteDatabase db, String sql) { super(db, sql); - if (SQLiteDebug.DEBUG_SQL_STATEMENTS) { - mSql = sql; - } else { - mSql = null; - } } /** @@ -54,9 +48,8 @@ public class SQLiteStatement extends SQLiteProgram * some reason */ public void execute() { + long timeStart = Debug.threadCpuTimeNanos(); mDatabase.lock(); - boolean logStats = mDatabase.mLogStats; - long startTime = logStats ? SystemClock.elapsedRealtime() : 0; acquireReference(); try { @@ -64,10 +57,8 @@ public class SQLiteStatement extends SQLiteProgram Log.v(TAG, "execute() for [" + mSql + "]"); } native_execute(); - if (logStats) { - mDatabase.logTimeStat(false /* write */, startTime, SystemClock.elapsedRealtime()); - } - } finally { + mDatabase.logTimeStat(mSql, timeStart); + } finally { releaseReference(); mDatabase.unlock(); } @@ -84,9 +75,8 @@ public class SQLiteStatement extends SQLiteProgram * some reason */ public long executeInsert() { + long timeStart = Debug.threadCpuTimeNanos(); mDatabase.lock(); - boolean logStats = mDatabase.mLogStats; - long startTime = logStats ? SystemClock.elapsedRealtime() : 0; acquireReference(); try { @@ -94,9 +84,7 @@ public class SQLiteStatement extends SQLiteProgram Log.v(TAG, "executeInsert() for [" + mSql + "]"); } native_execute(); - if (logStats) { - mDatabase.logTimeStat(false /* write */, startTime, SystemClock.elapsedRealtime()); - } + mDatabase.logTimeStat(mSql, timeStart); return mDatabase.lastInsertRow(); } finally { releaseReference(); @@ -113,9 +101,8 @@ public class SQLiteStatement extends SQLiteProgram * @throws android.database.sqlite.SQLiteDoneException if the query returns zero rows */ public long simpleQueryForLong() { + long timeStart = Debug.threadCpuTimeNanos(); mDatabase.lock(); - boolean logStats = mDatabase.mLogStats; - long startTime = logStats ? SystemClock.elapsedRealtime() : 0; acquireReference(); try { @@ -123,9 +110,7 @@ public class SQLiteStatement extends SQLiteProgram Log.v(TAG, "simpleQueryForLong() for [" + mSql + "]"); } long retValue = native_1x1_long(); - if (logStats) { - mDatabase.logTimeStat(false /* write */, startTime, SystemClock.elapsedRealtime()); - } + mDatabase.logTimeStat(mSql, timeStart); return retValue; } finally { releaseReference(); @@ -142,9 +127,8 @@ public class SQLiteStatement extends SQLiteProgram * @throws android.database.sqlite.SQLiteDoneException if the query returns zero rows */ public String simpleQueryForString() { + long timeStart = Debug.threadCpuTimeNanos(); mDatabase.lock(); - boolean logStats = mDatabase.mLogStats; - long startTime = logStats ? SystemClock.elapsedRealtime() : 0; acquireReference(); try { @@ -152,9 +136,7 @@ public class SQLiteStatement extends SQLiteProgram Log.v(TAG, "simpleQueryForString() for [" + mSql + "]"); } String retValue = native_1x1_string(); - if (logStats) { - mDatabase.logTimeStat(false /* write */, startTime, SystemClock.elapsedRealtime()); - } + mDatabase.logTimeStat(mSql, timeStart); return retValue; } finally { releaseReference(); diff --git a/core/java/android/ddm/DdmHandleHello.java b/core/java/android/ddm/DdmHandleHello.java index c5d591f..0603ca5 100644 --- a/core/java/android/ddm/DdmHandleHello.java +++ b/core/java/android/ddm/DdmHandleHello.java @@ -152,8 +152,8 @@ public class DdmHandleHello extends ChunkHandler { "hprof-heap-dump", "method-trace-profiling" }; - if (Config.LOGD) - Log.d("ddm-heap", "Got feature list request"); + if (Config.LOGV) + Log.v("ddm-heap", "Got feature list request"); int size = 4 + 4 * features.length; for (int i = features.length-1; i >= 0; i--) diff --git a/core/java/android/gesture/GestureStore.java b/core/java/android/gesture/GestureStore.java index 5f1a445..11a94d1 100644 --- a/core/java/android/gesture/GestureStore.java +++ b/core/java/android/gesture/GestureStore.java @@ -65,7 +65,12 @@ public class GestureStore { // ORIENTATION_SENSITIVE and ORIENTATION_INVARIANT are only for SEQUENCE_SENSITIVE gestures public static final int ORIENTATION_INVARIANT = 1; + // at most 2 directions can be recognized public static final int ORIENTATION_SENSITIVE = 2; + // at most 4 directions can be recognized + static final int ORIENTATION_SENSITIVE_4 = 4; + // at most 8 directions can be recognized + static final int ORIENTATION_SENSITIVE_8 = 8; private static final short FILE_FORMAT_VERSION = 1; @@ -131,7 +136,7 @@ public class GestureStore { public ArrayList<Prediction> recognize(Gesture gesture) { Instance instance = Instance.createInstance(mSequenceType, mOrientationStyle, gesture, null); - return mClassifier.classify(mSequenceType, instance.vector); + return mClassifier.classify(mSequenceType, mOrientationStyle, instance.vector); } /** diff --git a/core/java/android/gesture/GestureUtilities.java b/core/java/android/gesture/GestureUtilities.java index 40d7029..f1dcd89 100755 --- a/core/java/android/gesture/GestureUtilities.java +++ b/core/java/android/gesture/GestureUtilities.java @@ -366,6 +366,38 @@ final class GestureUtilities { } return Math.acos(sum); } + + /** + * Calculate the "minimum" cosine distance between two instances + * + * @param vector1 + * @param vector2 + * @param numOrientations the maximum number of orientation allowed + * @return the distance between the two instances (between 0 and Math.PI) + */ + static double minimumCosineDistance(float[] vector1, float[] vector2, int numOrientations) { + final int len = vector1.length; + double a = 0; + double b = 0; + for (int i = 0; i < len; i += 2) { + a += vector1[i] * vector2[i] + vector1[i + 1] * vector2[i + 1]; + b += vector1[i] * vector2[i + 1] - vector1[i + 1] * vector2[i]; + } + if (a != 0) { + final double tan = b/a; + final double angle = Math.atan(tan); + if (numOrientations > 2 && Math.abs(angle) >= Math.PI / numOrientations) { + return Math.acos(a); + } else { + final double cosine = Math.cos(angle); + final double sine = cosine * tan; + return Math.acos(a * cosine + b * sine); + } + } else { + return Math.PI / 2; + } + } + static OrientedBoundingBox computeOrientedBoundingBox(ArrayList<GesturePoint> pts) { GestureStroke stroke = new GestureStroke(pts); diff --git a/core/java/android/gesture/Instance.java b/core/java/android/gesture/Instance.java index ef208ac..68a2985 100755 --- a/core/java/android/gesture/Instance.java +++ b/core/java/android/gesture/Instance.java @@ -94,7 +94,7 @@ class Instance { float orientation = (float)Math.atan2(pts[1] - center[1], pts[0] - center[0]); float adjustment = -orientation; - if (orientationType == GestureStore.ORIENTATION_SENSITIVE) { + if (orientationType != GestureStore.ORIENTATION_INVARIANT) { int count = ORIENTATIONS.length; for (int i = 0; i < count; i++) { float delta = ORIENTATIONS[i] - orientation; diff --git a/core/java/android/gesture/InstanceLearner.java b/core/java/android/gesture/InstanceLearner.java index b93b76f..9987e69 100644 --- a/core/java/android/gesture/InstanceLearner.java +++ b/core/java/android/gesture/InstanceLearner.java @@ -41,7 +41,7 @@ class InstanceLearner extends Learner { }; @Override - ArrayList<Prediction> classify(int sequenceType, float[] vector) { + ArrayList<Prediction> classify(int sequenceType, int orientationType, float[] vector) { ArrayList<Prediction> predictions = new ArrayList<Prediction>(); ArrayList<Instance> instances = getInstances(); int count = instances.size(); @@ -53,7 +53,7 @@ class InstanceLearner extends Learner { } double distance; if (sequenceType == GestureStore.SEQUENCE_SENSITIVE) { - distance = GestureUtilities.cosineDistance(sample.vector, vector); + distance = GestureUtilities.minimumCosineDistance(sample.vector, vector, orientationType); } else { distance = GestureUtilities.squaredEuclideanDistance(sample.vector, vector); } diff --git a/core/java/android/gesture/Learner.java b/core/java/android/gesture/Learner.java index feacde5..60997e0 100755 --- a/core/java/android/gesture/Learner.java +++ b/core/java/android/gesture/Learner.java @@ -79,5 +79,5 @@ abstract class Learner { instances.removeAll(toDelete); } - abstract ArrayList<Prediction> classify(int gestureType, float[] vector); + abstract ArrayList<Prediction> classify(int sequenceType, int orientationType, float[] vector); } diff --git a/core/java/android/hardware/SensorManager.java b/core/java/android/hardware/SensorManager.java index 271f973..0c6bb1a 100644 --- a/core/java/android/hardware/SensorManager.java +++ b/core/java/android/hardware/SensorManager.java @@ -364,7 +364,7 @@ public class SensorManager final float[] values = new float[3]; final int[] status = new int[1]; final long timestamp[] = new long[1]; - Process.setThreadPriority(Process.THREAD_PRIORITY_DISPLAY); + Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_DISPLAY); if (!open()) { return; @@ -545,8 +545,8 @@ public class SensorManager i = sensors_module_get_next_sensor(sensor, i); if (i>=0) { - Log.d(TAG, "found sensor: " + sensor.getName() + - ", handle=" + sensor.getHandle()); + //Log.d(TAG, "found sensor: " + sensor.getName() + + // ", handle=" + sensor.getHandle()); sensor.setLegacyType(getLegacySensorType(sensor.getType())); fullList.add(sensor); sHandleToSensor.append(sensor.getHandle(), sensor); diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java index 5499bba..b315932 100644 --- a/core/java/android/inputmethodservice/InputMethodService.java +++ b/core/java/android/inputmethodservice/InputMethodService.java @@ -16,7 +16,7 @@ package android.inputmethodservice; -import static android.view.ViewGroup.LayoutParams.FILL_PARENT; +import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; import android.app.Dialog; @@ -556,7 +556,7 @@ public class InputMethodService extends AbstractInputMethodService { Context.LAYOUT_INFLATER_SERVICE); mWindow = new SoftInputWindow(this, mTheme, mDispatcherState); initViews(); - mWindow.getWindow().setLayout(FILL_PARENT, WRAP_CONTENT); + mWindow.getWindow().setLayout(MATCH_PARENT, WRAP_CONTENT); } /** @@ -803,8 +803,8 @@ public class InputMethodService extends AbstractInputMethodService { * candidates only mode changes. * * <p>The default implementation makes the layout for the window - * FILL_PARENT x FILL_PARENT when in fullscreen mode, and - * FILL_PARENT x WRAP_CONTENT when in non-fullscreen mode. + * MATCH_PARENT x MATCH_PARENT when in fullscreen mode, and + * MATCH_PARENT x WRAP_CONTENT when in non-fullscreen mode. * * @param win The input method's window. * @param isFullscreen If true, the window is running in fullscreen mode @@ -816,9 +816,9 @@ public class InputMethodService extends AbstractInputMethodService { public void onConfigureWindow(Window win, boolean isFullscreen, boolean isCandidatesOnly) { if (isFullscreen) { - mWindow.getWindow().setLayout(FILL_PARENT, FILL_PARENT); + mWindow.getWindow().setLayout(MATCH_PARENT, MATCH_PARENT); } else { - mWindow.getWindow().setLayout(FILL_PARENT, WRAP_CONTENT); + mWindow.getWindow().setLayout(MATCH_PARENT, WRAP_CONTENT); } } @@ -1049,8 +1049,8 @@ public class InputMethodService extends AbstractInputMethodService { public void setExtractView(View view) { mExtractFrame.removeAllViews(); mExtractFrame.addView(view, new FrameLayout.LayoutParams( - ViewGroup.LayoutParams.FILL_PARENT, - ViewGroup.LayoutParams.FILL_PARENT)); + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); mExtractView = view; if (view != null) { mExtractEditText = (ExtractEditText)view.findViewById( @@ -1079,7 +1079,7 @@ public class InputMethodService extends AbstractInputMethodService { public void setCandidatesView(View view) { mCandidatesFrame.removeAllViews(); mCandidatesFrame.addView(view, new FrameLayout.LayoutParams( - ViewGroup.LayoutParams.FILL_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); } @@ -1092,7 +1092,7 @@ public class InputMethodService extends AbstractInputMethodService { public void setInputView(View view) { mInputFrame.removeAllViews(); mInputFrame.addView(view, new FrameLayout.LayoutParams( - ViewGroup.LayoutParams.FILL_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); mInputView = view; } diff --git a/core/java/android/inputmethodservice/KeyboardView.java b/core/java/android/inputmethodservice/KeyboardView.java index 0f7ef22..b0c3909 100755 --- a/core/java/android/inputmethodservice/KeyboardView.java +++ b/core/java/android/inputmethodservice/KeyboardView.java @@ -1118,6 +1118,11 @@ public class KeyboardView extends View implements View.OnClickListener { if (action == MotionEvent.ACTION_DOWN) mSwipeTracker.clear(); mSwipeTracker.addMovement(me); + // Ignore all motion events until a DOWN. + if (mAbortKey && action != MotionEvent.ACTION_DOWN) { + return true; + } + if (mGestureDetector.onTouchEvent(me)) { showPreview(NOT_A_KEY); mHandler.removeMessages(MSG_REPEAT); @@ -1127,7 +1132,7 @@ public class KeyboardView extends View implements View.OnClickListener { // Needs to be called after the gesture detector gets a turn, as it may have // displayed the mini keyboard - if (mMiniKeyboardOnScreen) { + if (mMiniKeyboardOnScreen && action != MotionEvent.ACTION_CANCEL) { return true; } @@ -1150,9 +1155,14 @@ public class KeyboardView extends View implements View.OnClickListener { mKeys[keyIndex].codes[0] : 0); if (mCurrentKey >= 0 && mKeys[mCurrentKey].repeatable) { mRepeatKeyIndex = mCurrentKey; - repeatKey(); Message msg = mHandler.obtainMessage(MSG_REPEAT); mHandler.sendMessageDelayed(msg, REPEAT_START_DELAY); + repeatKey(); + // Delivering the key could have caused an abort + if (mAbortKey) { + mRepeatKeyIndex = NOT_A_KEY; + break; + } } if (mCurrentKey != NOT_A_KEY) { Message msg = mHandler.obtainMessage(MSG_LONGPRESS, me); @@ -1222,6 +1232,7 @@ public class KeyboardView extends View implements View.OnClickListener { break; case MotionEvent.ACTION_CANCEL: removeMessages(); + dismissPopupKeyboard(); mAbortKey = true; showPreview(NOT_A_KEY); invalidateKey(mCurrentKey); diff --git a/core/java/android/net/MobileDataStateTracker.java b/core/java/android/net/MobileDataStateTracker.java index 538e51a..b254961 100644 --- a/core/java/android/net/MobileDataStateTracker.java +++ b/core/java/android/net/MobileDataStateTracker.java @@ -60,12 +60,11 @@ public class MobileDataStateTracker extends NetworkStateTracker { * @param apnType the Phone apnType * @param tag the name of this network */ - public MobileDataStateTracker(Context context, Handler target, - int netType, String apnType, String tag) { + public MobileDataStateTracker(Context context, Handler target, int netType, String tag) { super(context, target, netType, TelephonyManager.getDefault().getNetworkType(), tag, TelephonyManager.getDefault().getNetworkTypeName()); - mApnType = apnType; + mApnType = networkTypeToApnType(netType); mPhoneService = null; if(netType == ConnectivityManager.TYPE_MOBILE) { mEnabled = true; @@ -501,4 +500,22 @@ public class MobileDataStateTracker extends NetworkStateTracker { + " APN type \"" + apnType + "\""); return Phone.APN_REQUEST_FAILED; } + + public static String networkTypeToApnType(int netType) { + switch(netType) { + case ConnectivityManager.TYPE_MOBILE: + return Phone.APN_TYPE_DEFAULT; // TODO - use just one of these + case ConnectivityManager.TYPE_MOBILE_MMS: + return Phone.APN_TYPE_MMS; + case ConnectivityManager.TYPE_MOBILE_SUPL: + return Phone.APN_TYPE_SUPL; + case ConnectivityManager.TYPE_MOBILE_DUN: + return Phone.APN_TYPE_DUN; + case ConnectivityManager.TYPE_MOBILE_HIPRI: + return Phone.APN_TYPE_HIPRI; + default: + Log.e(TAG, "Error mapping networkType " + netType + " to apnType."); + return null; + } + } } diff --git a/core/java/android/net/NetworkConnectivityListener.java b/core/java/android/net/NetworkConnectivityListener.java deleted file mode 100644 index 858fc77..0000000 --- a/core/java/android/net/NetworkConnectivityListener.java +++ /dev/null @@ -1,220 +0,0 @@ -/* - * Copyright (C) 2006 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 android.net; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.os.Handler; -import android.os.Message; -import android.util.Log; - -import java.util.HashMap; -import java.util.Iterator; - -/** - * A wrapper for a broadcast receiver that provides network connectivity - * state information, independent of network type (mobile, Wi-Fi, etc.). - * {@hide} - */ -public class NetworkConnectivityListener { - private static final String TAG = "NetworkConnectivityListener"; - private static final boolean DBG = false; - - private Context mContext; - private HashMap<Handler, Integer> mHandlers = new HashMap<Handler, Integer>(); - private State mState; - private boolean mListening; - private String mReason; - private boolean mIsFailover; - - /** Network connectivity information */ - private NetworkInfo mNetworkInfo; - - /** - * In case of a Disconnect, the connectivity manager may have - * already established, or may be attempting to establish, connectivity - * with another network. If so, {@code mOtherNetworkInfo} will be non-null. - */ - private NetworkInfo mOtherNetworkInfo; - - private ConnectivityBroadcastReceiver mReceiver; - - private class ConnectivityBroadcastReceiver extends BroadcastReceiver { - @Override - public void onReceive(Context context, Intent intent) { - String action = intent.getAction(); - - if (!action.equals(ConnectivityManager.CONNECTIVITY_ACTION) || - mListening == false) { - Log.w(TAG, "onReceived() called with " + mState.toString() + " and " + intent); - return; - } - - boolean noConnectivity = - intent.getBooleanExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, false); - - if (noConnectivity) { - mState = State.NOT_CONNECTED; - } else { - mState = State.CONNECTED; - } - - mNetworkInfo = (NetworkInfo) - intent.getParcelableExtra(ConnectivityManager.EXTRA_NETWORK_INFO); - mOtherNetworkInfo = (NetworkInfo) - intent.getParcelableExtra(ConnectivityManager.EXTRA_OTHER_NETWORK_INFO); - - mReason = intent.getStringExtra(ConnectivityManager.EXTRA_REASON); - mIsFailover = - intent.getBooleanExtra(ConnectivityManager.EXTRA_IS_FAILOVER, false); - - if (DBG) { - Log.d(TAG, "onReceive(): mNetworkInfo=" + mNetworkInfo + " mOtherNetworkInfo = " - + (mOtherNetworkInfo == null ? "[none]" : mOtherNetworkInfo + - " noConn=" + noConnectivity) + " mState=" + mState.toString()); - } - - // Notifiy any handlers. - Iterator<Handler> it = mHandlers.keySet().iterator(); - while (it.hasNext()) { - Handler target = it.next(); - Message message = Message.obtain(target, mHandlers.get(target)); - target.sendMessage(message); - } - } - }; - - public enum State { - UNKNOWN, - - /** This state is returned if there is connectivity to any network **/ - CONNECTED, - /** - * This state is returned if there is no connectivity to any network. This is set - * to true under two circumstances: - * <ul> - * <li>When connectivity is lost to one network, and there is no other available - * network to attempt to switch to.</li> - * <li>When connectivity is lost to one network, and the attempt to switch to - * another network fails.</li> - */ - NOT_CONNECTED - } - - /** - * Create a new NetworkConnectivityListener. - */ - public NetworkConnectivityListener() { - mState = State.UNKNOWN; - mReceiver = new ConnectivityBroadcastReceiver(); - } - - /** - * This method starts listening for network connectivity state changes. - * @param context - */ - public synchronized void startListening(Context context) { - if (!mListening) { - mContext = context; - - IntentFilter filter = new IntentFilter(); - filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); - context.registerReceiver(mReceiver, filter); - mListening = true; - } - } - - /** - * This method stops this class from listening for network changes. - */ - public synchronized void stopListening() { - if (mListening) { - mContext.unregisterReceiver(mReceiver); - mContext = null; - mNetworkInfo = null; - mOtherNetworkInfo = null; - mIsFailover = false; - mReason = null; - mListening = false; - } - } - - /** - * This methods registers a Handler to be called back onto with the specified what code when - * the network connectivity state changes. - * - * @param target The target handler. - * @param what The what code to be used when posting a message to the handler. - */ - public void registerHandler(Handler target, int what) { - mHandlers.put(target, what); - } - - /** - * This methods unregisters the specified Handler. - * @param target - */ - public void unregisterHandler(Handler target) { - mHandlers.remove(target); - } - - public State getState() { - return mState; - } - - /** - * Return the NetworkInfo associated with the most recent connectivity event. - * @return {@code NetworkInfo} for the network that had the most recent connectivity event. - */ - public NetworkInfo getNetworkInfo() { - return mNetworkInfo; - } - - /** - * If the most recent connectivity event was a DISCONNECT, return - * any information supplied in the broadcast about an alternate - * network that might be available. If this returns a non-null - * value, then another broadcast should follow shortly indicating - * whether connection to the other network succeeded. - * - * @return NetworkInfo - */ - public NetworkInfo getOtherNetworkInfo() { - return mOtherNetworkInfo; - } - - /** - * Returns true if the most recent event was for an attempt to switch over to - * a new network following loss of connectivity on another network. - * @return {@code true} if this was a failover attempt, {@code false} otherwise. - */ - public boolean isFailover() { - return mIsFailover; - } - - /** - * An optional reason for the connectivity state change may have been supplied. - * This returns it. - * @return the reason for the state change, if available, or {@code null} - * otherwise. - */ - public String getReason() { - return mReason; - } -} diff --git a/core/java/android/net/SSLCertificateSocketFactory.java b/core/java/android/net/SSLCertificateSocketFactory.java index a97b9e5..e40f1b8 100644 --- a/core/java/android/net/SSLCertificateSocketFactory.java +++ b/core/java/android/net/SSLCertificateSocketFactory.java @@ -16,11 +16,12 @@ package android.net; -import android.net.http.DomainNameChecker; import android.os.SystemProperties; import android.util.Config; import android.util.Log; +import com.android.common.DomainNameValidator; + import java.io.IOException; import java.net.InetAddress; import java.net.Socket; @@ -200,7 +201,7 @@ public class SSLCertificateSocketFactory extends SSLSocketFactory { X509Certificate lastChainCert = (X509Certificate) certs[0]; - if (!DomainNameChecker.match(lastChainCert, destHost)) { + if (!DomainNameValidator.match(lastChainCert, destHost)) { if (Config.LOGD) { Log.d(LOG_TAG,"validateSocket(): domain name check failed"); } diff --git a/core/java/android/os/NetStat.java b/core/java/android/net/TrafficStats.java index e294cdf..62e9f1f 100644 --- a/core/java/android/os/NetStat.java +++ b/core/java/android/net/TrafficStats.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package android.os; +package android.net; import android.util.Log; @@ -22,11 +22,22 @@ import java.io.File; import java.io.RandomAccessFile; import java.io.IOException; -/** @hide */ -public class NetStat { +/** + * Class that provides network traffic statistics. These statistics include bytes transmitted and + * received and network packets transmitted and received, over all interfaces, over the mobile + * interface, and on a per-UID basis. + * <p> + * These statistics may not be available on all platforms. If the statistics are not supported + * by this device, {@link #UNSUPPORTED} will be returned. + */ +public class TrafficStats { + /** + * The return value to indicate that the device does not support the statistic. + */ + public final static int UNSUPPORTED = -1; // Logging tag. - private final static String TAG = "netstat"; + private final static String TAG = "trafficstats"; // We pre-create all the File objects so we don't spend a lot of // CPU at runtime converting from Java Strings to byte[] for the @@ -38,36 +49,40 @@ public class NetStat { private final static File SYS_CLASS_NET_DIR = new File("/sys/class/net"); /** - * Get total number of tx packets sent through rmnet0 or ppp0 + * Get the total number of packets transmitted through the mobile interface. * - * @return number of Tx packets through rmnet0 or ppp0 + * @return number of packets. If the statistics are not supported by this device, + * {@link #UNSUPPORTED} will be returned. */ public static long getMobileTxPkts() { return getMobileStat(MOBILE_TX_PACKETS); } /** - * Get total number of rx packets received through rmnet0 or ppp0 + * Get the total number of packets received through the mobile interface. * - * @return number of Rx packets through rmnet0 or ppp0 + * @return number of packets. If the statistics are not supported by this device, + * {@link #UNSUPPORTED} will be returned. */ public static long getMobileRxPkts() { return getMobileStat(MOBILE_RX_PACKETS); } /** - * Get total number of tx bytes received through rmnet0 or ppp0 + * Get the total number of bytes transmitted through the mobile interface. * - * @return number of Tx bytes through rmnet0 or ppp0 + * @return number of bytes. If the statistics are not supported by this device, + * {@link #UNSUPPORTED} will be returned. */ public static long getMobileTxBytes() { return getMobileStat(MOBILE_TX_BYTES); } /** - * Get total number of rx bytes received through rmnet0 or ppp0 + * Get the total number of bytes received through the mobile interface. * - * @return number of Rx bytes through rmnet0 or ppp0 + * @return number of bytes. If the statistics are not supported by this device, + * {@link #UNSUPPORTED} will be returned. */ public static long getMobileRxBytes() { return getMobileStat(MOBILE_RX_BYTES); @@ -76,7 +91,8 @@ public class NetStat { /** * Get the total number of packets sent through all network interfaces. * - * @return the number of packets sent through all network interfaces + * @return the number of packets. If the statistics are not supported by this device, + * {@link #UNSUPPORTED} will be returned. */ public static long getTotalTxPkts() { return getTotalStat("tx_packets"); @@ -85,7 +101,8 @@ public class NetStat { /** * Get the total number of packets received through all network interfaces. * - * @return the number of packets received through all network interfaces + * @return number of packets. If the statistics are not supported by this device, + * {@link #UNSUPPORTED} will be returned. */ public static long getTotalRxPkts() { return getTotalStat("rx_packets"); @@ -94,7 +111,8 @@ public class NetStat { /** * Get the total number of bytes sent through all network interfaces. * - * @return the number of bytes sent through all network interfaces + * @return number of bytes. If the statistics are not supported by this device, + * {@link #UNSUPPORTED} will be returned. */ public static long getTotalTxBytes() { return getTotalStat("tx_bytes"); @@ -103,35 +121,35 @@ public class NetStat { /** * Get the total number of bytes received through all network interfaces. * - * @return the number of bytes received through all network interfaces + * @return number of bytes. If the statistics are not supported by this device, + * {@link #UNSUPPORTED} will be returned. */ public static long getTotalRxBytes() { return getTotalStat("rx_bytes"); } /** - * Gets network bytes sent for this UID. + * Get the number of bytes sent through the network for this UID. * The statistics are across all interfaces. - * The statistics come from /proc/uid_stat. * * {@see android.os.Process#myUid()}. * - * @param uid - * @return byte count + * @param uid The UID of the process to examine. + * @return number of bytes. If the statistics are not supported by this device, + * {@link #UNSUPPORTED} will be returned. */ public static long getUidTxBytes(int uid) { return getNumberFromFilePath("/proc/uid_stat/" + uid + "/tcp_snd"); } /** - * Gets network bytes received for this UID. + * Get the number of bytes received through the network for this UID. * The statistics are across all interfaces. - * The statistics come from /proc/uid_stat. * * {@see android.os.Process#myUid()}. * - * @param uid - * @return byte count + * @param uid The UID of the process to examine. + * @return number of bytes */ public static long getUidRxBytes(int uid) { return getNumberFromFilePath("/proc/uid_stat/" + uid + "/tcp_rcv"); @@ -159,7 +177,7 @@ public class NetStat { File[] nets = SYS_CLASS_NET_DIR.listFiles(); if (nets == null) { - return 0; + return UNSUPPORTED; } long total = 0; StringBuffer strbuf = new StringBuffer(); @@ -187,14 +205,14 @@ public class NetStat { e); } } - return 0L; + return UNSUPPORTED; } // File will have format <number><newline> private static long getNumberFromFilePath(String filename) { RandomAccessFile raf = getFile(filename); if (raf == null) { - return 0L; + return UNSUPPORTED; } return getNumberFromFile(raf, filename); } @@ -209,7 +227,7 @@ public class NetStat { raf.close(); } catch (IOException e) { Log.w(TAG, "Exception getting TCP bytes from " + filename, e); - return 0L; + return UNSUPPORTED; } finally { if (raf != null) { try { diff --git a/core/java/android/net/Uri.java b/core/java/android/net/Uri.java index 9a1b65d..f2ea539 100644 --- a/core/java/android/net/Uri.java +++ b/core/java/android/net/Uri.java @@ -1567,51 +1567,40 @@ public abstract class Uri implements Parcelable, Comparable<Uri> { if (isOpaque()) { throw new UnsupportedOperationException(NOT_HIERARCHICAL); } + if (key == null) { + throw new NullPointerException("key"); + } - String query = getEncodedQuery(); - + final String query = getEncodedQuery(); if (query == null) { return null; } - String encodedKey; - try { - encodedKey = URLEncoder.encode(key, DEFAULT_ENCODING); - } catch (UnsupportedEncodingException e) { - throw new AssertionError(e); - } - - String prefix = encodedKey + "="; + final String encodedKey = encode(key, null); + final int encodedKeyLength = encodedKey.length(); - if (query.length() < prefix.length()) { - return null; - } + int encodedKeySearchIndex = 0; + final int encodedKeySearchEnd = query.length() - (encodedKeyLength + 1); - int start; - if (query.startsWith(prefix)) { - // It's the first parameter. - start = prefix.length(); - } else { - // It must be later in the query string. - prefix = "&" + prefix; - start = query.indexOf(prefix); - - if (start == -1) { - // Not found. - return null; + while (encodedKeySearchIndex <= encodedKeySearchEnd) { + int keyIndex = query.indexOf(encodedKey, encodedKeySearchIndex); + if (keyIndex == -1) { + break; + } + final int equalsIndex = keyIndex + encodedKeyLength; + if (query.charAt(equalsIndex) != '=') { + encodedKeySearchIndex = equalsIndex + 1; + continue; + } + if (keyIndex == 0 || query.charAt(keyIndex - 1) == '&') { + int end = query.indexOf('&', equalsIndex); + if (end == -1) { + end = query.length(); + } + return decode(query.substring(equalsIndex + 1, end)); } - - start += prefix.length(); - } - - // Find end of value. - int end = query.indexOf('&', start); - if (end == -1) { - end = query.length(); } - - String value = query.substring(start, end); - return decode(value); + return null; } /** Identifies a null parcelled Uri. */ diff --git a/core/java/android/net/http/AndroidHttpClient.java b/core/java/android/net/http/AndroidHttpClient.java deleted file mode 100644 index c2013d5..0000000 --- a/core/java/android/net/http/AndroidHttpClient.java +++ /dev/null @@ -1,508 +0,0 @@ -/* - * Copyright (C) 2007 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.net.http; - -import org.apache.http.Header; -import org.apache.http.HttpEntity; -import org.apache.http.HttpEntityEnclosingRequest; -import org.apache.http.HttpException; -import org.apache.http.HttpHost; -import org.apache.http.HttpRequest; -import org.apache.http.HttpRequestInterceptor; -import org.apache.http.HttpResponse; -import org.apache.http.entity.AbstractHttpEntity; -import org.apache.http.entity.ByteArrayEntity; -import org.apache.http.client.HttpClient; -import org.apache.http.client.ResponseHandler; -import org.apache.http.client.ClientProtocolException; -import org.apache.http.client.protocol.ClientContext; -import org.apache.http.client.methods.HttpUriRequest; -import org.apache.http.client.params.HttpClientParams; -import org.apache.http.conn.ClientConnectionManager; -import org.apache.http.conn.scheme.PlainSocketFactory; -import org.apache.http.conn.scheme.Scheme; -import org.apache.http.conn.scheme.SchemeRegistry; -import org.apache.http.conn.ssl.SSLSocketFactory; -import org.apache.http.impl.client.DefaultHttpClient; -import org.apache.http.impl.client.RequestWrapper; -import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; -import org.apache.http.params.BasicHttpParams; -import org.apache.http.params.HttpConnectionParams; -import org.apache.http.params.HttpParams; -import org.apache.http.params.HttpProtocolParams; -import org.apache.http.protocol.BasicHttpProcessor; -import org.apache.http.protocol.HttpContext; -import org.apache.http.protocol.BasicHttpContext; -import org.apache.harmony.xnet.provider.jsse.SSLClientSessionCache; -import org.apache.harmony.xnet.provider.jsse.SSLContextImpl; - -import java.io.IOException; -import java.io.InputStream; -import java.io.ByteArrayOutputStream; -import java.io.OutputStream; -import java.util.zip.GZIPInputStream; -import java.util.zip.GZIPOutputStream; -import java.net.URI; -import java.security.KeyManagementException; - -import android.util.Log; -import android.content.ContentResolver; -import android.provider.Settings; -import android.text.TextUtils; -import android.os.SystemProperties; - -/** - * Subclass of the Apache {@link DefaultHttpClient} that is configured with - * reasonable default settings and registered schemes for Android, and - * also lets the user add {@link HttpRequestInterceptor} classes. - * Don't create this directly, use the {@link #newInstance} factory method. - * - * <p>This client processes cookies but does not retain them by default. - * To retain cookies, simply add a cookie store to the HttpContext:</p> - * - * <pre>context.setAttribute(ClientContext.COOKIE_STORE, cookieStore);</pre> - * - * {@hide} - */ -public final class AndroidHttpClient implements HttpClient { - - // Gzip of data shorter than this probably won't be worthwhile - public static long DEFAULT_SYNC_MIN_GZIP_BYTES = 256; - - private static final String TAG = "AndroidHttpClient"; - - - /** Set if HTTP requests are blocked from being executed on this thread */ - private static final ThreadLocal<Boolean> sThreadBlocked = - new ThreadLocal<Boolean>(); - - /** Interceptor throws an exception if the executing thread is blocked */ - private static final HttpRequestInterceptor sThreadCheckInterceptor = - new HttpRequestInterceptor() { - public void process(HttpRequest request, HttpContext context) { - if (sThreadBlocked.get() != null && sThreadBlocked.get()) { - throw new RuntimeException("This thread forbids HTTP requests"); - } - } - }; - - /** - * Create a new HttpClient with reasonable defaults (which you can update). - * - * @param userAgent to report in your HTTP requests. - * @param sessionCache persistent session cache - * @return AndroidHttpClient for you to use for all your requests. - */ - public static AndroidHttpClient newInstance(String userAgent, - SSLClientSessionCache sessionCache) { - HttpParams params = new BasicHttpParams(); - - // Turn off stale checking. Our connections break all the time anyway, - // and it's not worth it to pay the penalty of checking every time. - HttpConnectionParams.setStaleCheckingEnabled(params, false); - - // Default connection and socket timeout of 20 seconds. Tweak to taste. - HttpConnectionParams.setConnectionTimeout(params, 20 * 1000); - HttpConnectionParams.setSoTimeout(params, 20 * 1000); - HttpConnectionParams.setSocketBufferSize(params, 8192); - - // Don't handle redirects -- return them to the caller. Our code - // often wants to re-POST after a redirect, which we must do ourselves. - HttpClientParams.setRedirecting(params, false); - - // Set the specified user agent and register standard protocols. - HttpProtocolParams.setUserAgent(params, userAgent); - SchemeRegistry schemeRegistry = new SchemeRegistry(); - schemeRegistry.register(new Scheme("http", - PlainSocketFactory.getSocketFactory(), 80)); - schemeRegistry.register(new Scheme("https", - socketFactoryWithCache(sessionCache), 443)); - - ClientConnectionManager manager = - new ThreadSafeClientConnManager(params, schemeRegistry); - - // We use a factory method to modify superclass initialization - // parameters without the funny call-a-static-method dance. - return new AndroidHttpClient(manager, params); - } - - /** - * Returns a socket factory backed by the given persistent session cache. - * - * @param sessionCache to retrieve sessions from, null for no cache - */ - private static SSLSocketFactory socketFactoryWithCache( - SSLClientSessionCache sessionCache) { - if (sessionCache == null) { - // Use the default factory which doesn't support persistent - // caching. - return SSLSocketFactory.getSocketFactory(); - } - - // Create a new SSL context backed by the cache. - // TODO: Keep a weak *identity* hash map of caches to engines. In the - // mean time, if we have two engines for the same cache, they'll still - // share sessions but will have to do so through the persistent cache. - SSLContextImpl sslContext = new SSLContextImpl(); - try { - sslContext.engineInit(null, null, null, sessionCache, null); - } catch (KeyManagementException e) { - throw new AssertionError(e); - } - return new SSLSocketFactory(sslContext.engineGetSocketFactory()); - } - - /** - * Create a new HttpClient with reasonable defaults (which you can update). - * @param userAgent to report in your HTTP requests. - * @return AndroidHttpClient for you to use for all your requests. - */ - public static AndroidHttpClient newInstance(String userAgent) { - return newInstance(userAgent, null /* session cache */); - } - - private final HttpClient delegate; - - private RuntimeException mLeakedException = new IllegalStateException( - "AndroidHttpClient created and never closed"); - - private AndroidHttpClient(ClientConnectionManager ccm, HttpParams params) { - this.delegate = new DefaultHttpClient(ccm, params) { - @Override - protected BasicHttpProcessor createHttpProcessor() { - // Add interceptor to prevent making requests from main thread. - BasicHttpProcessor processor = super.createHttpProcessor(); - processor.addRequestInterceptor(sThreadCheckInterceptor); - processor.addRequestInterceptor(new CurlLogger()); - - return processor; - } - - @Override - protected HttpContext createHttpContext() { - // Same as DefaultHttpClient.createHttpContext() minus the - // cookie store. - HttpContext context = new BasicHttpContext(); - context.setAttribute( - ClientContext.AUTHSCHEME_REGISTRY, - getAuthSchemes()); - context.setAttribute( - ClientContext.COOKIESPEC_REGISTRY, - getCookieSpecs()); - context.setAttribute( - ClientContext.CREDS_PROVIDER, - getCredentialsProvider()); - return context; - } - }; - } - - @Override - protected void finalize() throws Throwable { - super.finalize(); - if (mLeakedException != null) { - Log.e(TAG, "Leak found", mLeakedException); - mLeakedException = null; - } - } - - /** - * Block this thread from executing HTTP requests. - * Used to guard against HTTP requests blocking the main application thread. - * @param blocked if HTTP requests run on this thread should be denied - */ - public static void setThreadBlocked(boolean blocked) { - sThreadBlocked.set(blocked); - } - - /** - * Modifies a request to indicate to the server that we would like a - * gzipped response. (Uses the "Accept-Encoding" HTTP header.) - * @param request the request to modify - * @see #getUngzippedContent - */ - public static void modifyRequestToAcceptGzipResponse(HttpRequest request) { - request.addHeader("Accept-Encoding", "gzip"); - } - - /** - * Gets the input stream from a response entity. If the entity is gzipped - * then this will get a stream over the uncompressed data. - * - * @param entity the entity whose content should be read - * @return the input stream to read from - * @throws IOException - */ - public static InputStream getUngzippedContent(HttpEntity entity) - throws IOException { - InputStream responseStream = entity.getContent(); - if (responseStream == null) return responseStream; - Header header = entity.getContentEncoding(); - if (header == null) return responseStream; - String contentEncoding = header.getValue(); - if (contentEncoding == null) return responseStream; - if (contentEncoding.contains("gzip")) responseStream - = new GZIPInputStream(responseStream); - return responseStream; - } - - /** - * Release resources associated with this client. You must call this, - * or significant resources (sockets and memory) may be leaked. - */ - public void close() { - if (mLeakedException != null) { - getConnectionManager().shutdown(); - mLeakedException = null; - } - } - - public HttpParams getParams() { - return delegate.getParams(); - } - - public ClientConnectionManager getConnectionManager() { - return delegate.getConnectionManager(); - } - - public HttpResponse execute(HttpUriRequest request) throws IOException { - return delegate.execute(request); - } - - public HttpResponse execute(HttpUriRequest request, HttpContext context) - throws IOException { - return delegate.execute(request, context); - } - - public HttpResponse execute(HttpHost target, HttpRequest request) - throws IOException { - return delegate.execute(target, request); - } - - public HttpResponse execute(HttpHost target, HttpRequest request, - HttpContext context) throws IOException { - return delegate.execute(target, request, context); - } - - public <T> T execute(HttpUriRequest request, - ResponseHandler<? extends T> responseHandler) - throws IOException, ClientProtocolException { - return delegate.execute(request, responseHandler); - } - - public <T> T execute(HttpUriRequest request, - ResponseHandler<? extends T> responseHandler, HttpContext context) - throws IOException, ClientProtocolException { - return delegate.execute(request, responseHandler, context); - } - - public <T> T execute(HttpHost target, HttpRequest request, - ResponseHandler<? extends T> responseHandler) throws IOException, - ClientProtocolException { - return delegate.execute(target, request, responseHandler); - } - - public <T> T execute(HttpHost target, HttpRequest request, - ResponseHandler<? extends T> responseHandler, HttpContext context) - throws IOException, ClientProtocolException { - return delegate.execute(target, request, responseHandler, context); - } - - /** - * Compress data to send to server. - * Creates a Http Entity holding the gzipped data. - * The data will not be compressed if it is too short. - * @param data The bytes to compress - * @return Entity holding the data - */ - public static AbstractHttpEntity getCompressedEntity(byte data[], ContentResolver resolver) - throws IOException { - AbstractHttpEntity entity; - if (data.length < getMinGzipSize(resolver)) { - entity = new ByteArrayEntity(data); - } else { - ByteArrayOutputStream arr = new ByteArrayOutputStream(); - OutputStream zipper = new GZIPOutputStream(arr); - zipper.write(data); - zipper.close(); - entity = new ByteArrayEntity(arr.toByteArray()); - entity.setContentEncoding("gzip"); - } - return entity; - } - - /** - * Retrieves the minimum size for compressing data. - * Shorter data will not be compressed. - */ - public static long getMinGzipSize(ContentResolver resolver) { - String sMinGzipBytes = Settings.Gservices.getString(resolver, - Settings.Gservices.SYNC_MIN_GZIP_BYTES); - - if (!TextUtils.isEmpty(sMinGzipBytes)) { - try { - return Long.parseLong(sMinGzipBytes); - } catch (NumberFormatException nfe) { - Log.w(TAG, "Unable to parse " + - Settings.Gservices.SYNC_MIN_GZIP_BYTES + " " + - sMinGzipBytes, nfe); - } - } - return DEFAULT_SYNC_MIN_GZIP_BYTES; - } - - /* cURL logging support. */ - - /** - * Logging tag and level. - */ - private static class LoggingConfiguration { - - private final String tag; - private final int level; - - private LoggingConfiguration(String tag, int level) { - this.tag = tag; - this.level = level; - } - - /** - * Returns true if logging is turned on for this configuration. - */ - private boolean isLoggable() { - return Log.isLoggable(tag, level); - } - - /** - * Returns true if auth logging is turned on for this configuration. Can only be set on - * insecure devices. - */ - private boolean isAuthLoggable() { - String secure = SystemProperties.get("ro.secure"); - return "0".equals(secure) && Log.isLoggable(tag + "-auth", level); - } - - /** - * Prints a message using this configuration. - */ - private void println(String message) { - Log.println(level, tag, message); - } - } - - /** cURL logging configuration. */ - private volatile LoggingConfiguration curlConfiguration; - - /** - * Enables cURL request logging for this client. - * - * @param name to log messages with - * @param level at which to log messages (see {@link android.util.Log}) - */ - public void enableCurlLogging(String name, int level) { - if (name == null) { - throw new NullPointerException("name"); - } - if (level < Log.VERBOSE || level > Log.ASSERT) { - throw new IllegalArgumentException("Level is out of range [" - + Log.VERBOSE + ".." + Log.ASSERT + "]"); - } - - curlConfiguration = new LoggingConfiguration(name, level); - } - - /** - * Disables cURL logging for this client. - */ - public void disableCurlLogging() { - curlConfiguration = null; - } - - /** - * Logs cURL commands equivalent to requests. - */ - private class CurlLogger implements HttpRequestInterceptor { - public void process(HttpRequest request, HttpContext context) - throws HttpException, IOException { - LoggingConfiguration configuration = curlConfiguration; - if (configuration != null - && configuration.isLoggable() - && request instanceof HttpUriRequest) { - configuration.println(toCurl((HttpUriRequest) request, - configuration.isAuthLoggable())); - } - } - } - - /** - * Generates a cURL command equivalent to the given request. - */ - private static String toCurl(HttpUriRequest request, boolean logAuthToken) throws IOException { - StringBuilder builder = new StringBuilder(); - - builder.append("curl "); - - for (Header header: request.getAllHeaders()) { - if (!logAuthToken - && (header.getName().equals("Authorization") || - header.getName().equals("Cookie"))) { - continue; - } - builder.append("--header \""); - builder.append(header.toString().trim()); - builder.append("\" "); - } - - URI uri = request.getURI(); - - // If this is a wrapped request, use the URI from the original - // request instead. getURI() on the wrapper seems to return a - // relative URI. We want an absolute URI. - if (request instanceof RequestWrapper) { - HttpRequest original = ((RequestWrapper) request).getOriginal(); - if (original instanceof HttpUriRequest) { - uri = ((HttpUriRequest) original).getURI(); - } - } - - builder.append("\""); - builder.append(uri); - builder.append("\""); - - if (request instanceof HttpEntityEnclosingRequest) { - HttpEntityEnclosingRequest entityRequest = - (HttpEntityEnclosingRequest) request; - HttpEntity entity = entityRequest.getEntity(); - if (entity != null && entity.isRepeatable()) { - if (entity.getContentLength() < 1024) { - ByteArrayOutputStream stream = new ByteArrayOutputStream(); - entity.writeTo(stream); - String entityString = stream.toString(); - - // TODO: Check the content type, too. - builder.append(" --data-ascii \"") - .append(entityString) - .append("\""); - } else { - builder.append(" [TOO MUCH DATA TO INCLUDE]"); - } - } - } - - return builder.toString(); - } -} diff --git a/core/java/android/net/http/CertificateChainValidator.java b/core/java/android/net/http/CertificateChainValidator.java index ed6b4c2..da6af9d 100644 --- a/core/java/android/net/http/CertificateChainValidator.java +++ b/core/java/android/net/http/CertificateChainValidator.java @@ -16,6 +16,8 @@ package android.net.http; +import com.android.common.DomainNameValidator; + import org.apache.harmony.xnet.provider.jsse.SSLParameters; import java.io.IOException; @@ -112,7 +114,7 @@ class CertificateChainValidator { closeSocketThrowException( sslSocket, "certificate for this site is null"); } else { - if (!DomainNameChecker.match(currCertificate, domain)) { + if (!DomainNameValidator.match(currCertificate, domain)) { String errorMessage = "certificate not for this host: " + domain; if (HttpLog.LOGV) { diff --git a/core/java/android/net/http/Connection.java b/core/java/android/net/http/Connection.java index 2d39e39..b8e17da 100644 --- a/core/java/android/net/http/Connection.java +++ b/core/java/android/net/http/Connection.java @@ -94,7 +94,6 @@ abstract class Connection { */ private static final String HTTP_CONNECTION = "http.connection"; - RequestQueue.ConnectionManager mConnectionManager; RequestFeeder mRequestFeeder; /** @@ -104,11 +103,9 @@ abstract class Connection { private byte[] mBuf; protected Connection(Context context, HttpHost host, - RequestQueue.ConnectionManager connectionManager, RequestFeeder requestFeeder) { mContext = context; mHost = host; - mConnectionManager = connectionManager; mRequestFeeder = requestFeeder; mCanPersist = false; @@ -124,18 +121,15 @@ abstract class Connection { * necessary */ static Connection getConnection( - Context context, HttpHost host, - RequestQueue.ConnectionManager connectionManager, + Context context, HttpHost host, HttpHost proxy, RequestFeeder requestFeeder) { if (host.getSchemeName().equals("http")) { - return new HttpConnection(context, host, connectionManager, - requestFeeder); + return new HttpConnection(context, host, requestFeeder); } // Otherwise, default to https - return new HttpsConnection(context, host, connectionManager, - requestFeeder); + return new HttpsConnection(context, host, proxy, requestFeeder); } /** @@ -338,7 +332,7 @@ abstract class Connection { mRequestFeeder.requeueRequest(tReq); empty = false; } - if (empty) empty = mRequestFeeder.haveRequest(mHost); + if (empty) empty = !mRequestFeeder.haveRequest(mHost); } return empty; } diff --git a/core/java/android/net/http/ConnectionThread.java b/core/java/android/net/http/ConnectionThread.java index 0b30e58..32191d2 100644 --- a/core/java/android/net/http/ConnectionThread.java +++ b/core/java/android/net/http/ConnectionThread.java @@ -108,24 +108,11 @@ class ConnectionThread extends Thread { if (HttpLog.LOGV) HttpLog.v("ConnectionThread: new request " + request.mHost + " " + request ); - HttpHost proxy = mConnectionManager.getProxyHost(); - - HttpHost host; - if (false) { - // Allow https proxy - host = proxy == null ? request.mHost : proxy; - } else { - // Disallow https proxy -- tmob proxy server - // serves a request loop for https reqs - host = (proxy == null || - request.mHost.getSchemeName().equals("https")) ? - request.mHost : proxy; - } - mConnection = mConnectionManager.getConnection(mContext, host); + mConnection = mConnectionManager.getConnection(mContext, + request.mHost); mConnection.processRequests(request); if (mConnection.getCanPersist()) { - if (!mConnectionManager.recycleConnection(host, - mConnection)) { + if (!mConnectionManager.recycleConnection(mConnection)) { mConnection.closeConnection(); } } else { diff --git a/core/java/android/net/http/DomainNameChecker.java b/core/java/android/net/http/DomainNameChecker.java deleted file mode 100644 index e4c8009..0000000 --- a/core/java/android/net/http/DomainNameChecker.java +++ /dev/null @@ -1,277 +0,0 @@ -/* - * 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 android.net.http; - -import org.bouncycastle.asn1.x509.X509Name; - -import java.net.InetAddress; -import java.net.UnknownHostException; -import java.security.cert.X509Certificate; -import java.security.cert.CertificateParsingException; -import java.util.Collection; -import java.util.Iterator; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.regex.PatternSyntaxException; -import java.util.Vector; - -/** - * Implements basic domain-name validation as specified by RFC2818. - * - * {@hide} - */ -public class DomainNameChecker { - private static Pattern QUICK_IP_PATTERN; - static { - try { - QUICK_IP_PATTERN = Pattern.compile("^[a-f0-9\\.:]+$"); - } catch (PatternSyntaxException e) {} - } - - private static final int ALT_DNS_NAME = 2; - private static final int ALT_IPA_NAME = 7; - - /** - * Checks the site certificate against the domain name of the site being visited - * @param certificate The certificate to check - * @param thisDomain The domain name of the site being visited - * @return True iff if there is a domain match as specified by RFC2818 - */ - public static boolean match(X509Certificate certificate, String thisDomain) { - if (certificate == null || thisDomain == null || thisDomain.length() == 0) { - return false; - } - - thisDomain = thisDomain.toLowerCase(); - if (!isIpAddress(thisDomain)) { - return matchDns(certificate, thisDomain); - } else { - return matchIpAddress(certificate, thisDomain); - } - } - - /** - * @return True iff the domain name is specified as an IP address - */ - private static boolean isIpAddress(String domain) { - boolean rval = (domain != null && domain.length() != 0); - if (rval) { - try { - // do a quick-dirty IP match first to avoid DNS lookup - rval = QUICK_IP_PATTERN.matcher(domain).matches(); - if (rval) { - rval = domain.equals( - InetAddress.getByName(domain).getHostAddress()); - } - } catch (UnknownHostException e) { - String errorMessage = e.getMessage(); - if (errorMessage == null) { - errorMessage = "unknown host exception"; - } - - if (HttpLog.LOGV) { - HttpLog.v("DomainNameChecker.isIpAddress(): " + errorMessage); - } - - rval = false; - } - } - - return rval; - } - - /** - * Checks the site certificate against the IP domain name of the site being visited - * @param certificate The certificate to check - * @param thisDomain The DNS domain name of the site being visited - * @return True iff if there is a domain match as specified by RFC2818 - */ - private static boolean matchIpAddress(X509Certificate certificate, String thisDomain) { - if (HttpLog.LOGV) { - HttpLog.v("DomainNameChecker.matchIpAddress(): this domain: " + thisDomain); - } - - try { - Collection subjectAltNames = certificate.getSubjectAlternativeNames(); - if (subjectAltNames != null) { - Iterator i = subjectAltNames.iterator(); - while (i.hasNext()) { - List altNameEntry = (List)(i.next()); - if (altNameEntry != null && 2 <= altNameEntry.size()) { - Integer altNameType = (Integer)(altNameEntry.get(0)); - if (altNameType != null) { - if (altNameType.intValue() == ALT_IPA_NAME) { - String altName = (String)(altNameEntry.get(1)); - if (altName != null) { - if (HttpLog.LOGV) { - HttpLog.v("alternative IP: " + altName); - } - if (thisDomain.equalsIgnoreCase(altName)) { - return true; - } - } - } - } - } - } - } - } catch (CertificateParsingException e) {} - - return false; - } - - /** - * Checks the site certificate against the DNS domain name of the site being visited - * @param certificate The certificate to check - * @param thisDomain The DNS domain name of the site being visited - * @return True iff if there is a domain match as specified by RFC2818 - */ - private static boolean matchDns(X509Certificate certificate, String thisDomain) { - boolean hasDns = false; - try { - Collection subjectAltNames = certificate.getSubjectAlternativeNames(); - if (subjectAltNames != null) { - Iterator i = subjectAltNames.iterator(); - while (i.hasNext()) { - List altNameEntry = (List)(i.next()); - if (altNameEntry != null && 2 <= altNameEntry.size()) { - Integer altNameType = (Integer)(altNameEntry.get(0)); - if (altNameType != null) { - if (altNameType.intValue() == ALT_DNS_NAME) { - hasDns = true; - String altName = (String)(altNameEntry.get(1)); - if (altName != null) { - if (matchDns(thisDomain, altName)) { - return true; - } - } - } - } - } - } - } - } catch (CertificateParsingException e) { - // one way we can get here is if an alternative name starts with - // '*' character, which is contrary to one interpretation of the - // spec (a valid DNS name must start with a letter); there is no - // good way around this, and in order to be compatible we proceed - // to check the common name (ie, ignore alternative names) - if (HttpLog.LOGV) { - String errorMessage = e.getMessage(); - if (errorMessage == null) { - errorMessage = "failed to parse certificate"; - } - - if (HttpLog.LOGV) { - HttpLog.v("DomainNameChecker.matchDns(): " + errorMessage); - } - } - } - - if (!hasDns) { - X509Name xName = new X509Name(certificate.getSubjectDN().getName()); - Vector val = xName.getValues(); - Vector oid = xName.getOIDs(); - for (int i = 0; i < oid.size(); i++) { - if (oid.elementAt(i).equals(X509Name.CN)) { - return matchDns(thisDomain, (String)(val.elementAt(i))); - } - } - } - - return false; - } - - /** - * @param thisDomain The domain name of the site being visited - * @param thatDomain The domain name from the certificate - * @return True iff thisDomain matches thatDomain as specified by RFC2818 - */ - private static boolean matchDns(String thisDomain, String thatDomain) { - if (HttpLog.LOGV) { - HttpLog.v("DomainNameChecker.matchDns():" + - " this domain: " + thisDomain + - " that domain: " + thatDomain); - } - - if (thisDomain == null || thisDomain.length() == 0 || - thatDomain == null || thatDomain.length() == 0) { - return false; - } - - thatDomain = thatDomain.toLowerCase(); - - // (a) domain name strings are equal, ignoring case: X matches X - boolean rval = thisDomain.equals(thatDomain); - if (!rval) { - String[] thisDomainTokens = thisDomain.split("\\."); - String[] thatDomainTokens = thatDomain.split("\\."); - - int thisDomainTokensNum = thisDomainTokens.length; - int thatDomainTokensNum = thatDomainTokens.length; - - // (b) OR thatHost is a '.'-suffix of thisHost: Z.Y.X matches X - if (thisDomainTokensNum >= thatDomainTokensNum) { - for (int i = thatDomainTokensNum - 1; i >= 0; --i) { - rval = thisDomainTokens[i].equals(thatDomainTokens[i]); - if (!rval) { - // (c) OR we have a special *-match: - // Z.Y.X matches *.Y.X but does not match *.X - rval = (i == 0 && thisDomainTokensNum == thatDomainTokensNum); - if (rval) { - rval = thatDomainTokens[0].equals("*"); - if (!rval) { - // (d) OR we have a *-component match: - // f*.com matches foo.com but not bar.com - rval = domainTokenMatch( - thisDomainTokens[0], thatDomainTokens[0]); - } - } - - break; - } - } - } - } - - return rval; - } - - /** - * @param thisDomainToken The domain token from the current domain name - * @param thatDomainToken The domain token from the certificate - * @return True iff thisDomainToken matches thatDomainToken, using the - * wildcard match as specified by RFC2818-3.1. For example, f*.com must - * match foo.com but not bar.com - */ - private static boolean domainTokenMatch(String thisDomainToken, String thatDomainToken) { - if (thisDomainToken != null && thatDomainToken != null) { - int starIndex = thatDomainToken.indexOf('*'); - if (starIndex >= 0) { - if (thatDomainToken.length() - 1 <= thisDomainToken.length()) { - String prefix = thatDomainToken.substring(0, starIndex); - String suffix = thatDomainToken.substring(starIndex + 1); - - return thisDomainToken.startsWith(prefix) && thisDomainToken.endsWith(suffix); - } - } - } - - return false; - } -} diff --git a/core/java/android/net/http/HttpConnection.java b/core/java/android/net/http/HttpConnection.java index 8b12d0b..6df86bf 100644 --- a/core/java/android/net/http/HttpConnection.java +++ b/core/java/android/net/http/HttpConnection.java @@ -35,9 +35,8 @@ import org.apache.http.params.HttpConnectionParams; class HttpConnection extends Connection { HttpConnection(Context context, HttpHost host, - RequestQueue.ConnectionManager connectionManager, RequestFeeder requestFeeder) { - super(context, host, connectionManager, requestFeeder); + super(context, host, requestFeeder); } /** diff --git a/core/java/android/net/http/HttpsConnection.java b/core/java/android/net/http/HttpsConnection.java index 8a69d0d..f735f3d 100644 --- a/core/java/android/net/http/HttpsConnection.java +++ b/core/java/android/net/http/HttpsConnection.java @@ -131,13 +131,16 @@ public class HttpsConnection extends Connection { */ private boolean mAborted = false; + // Used when connecting through a proxy. + private HttpHost mProxyHost; + /** * Contructor for a https connection. */ - HttpsConnection(Context context, HttpHost host, - RequestQueue.ConnectionManager connectionManager, + HttpsConnection(Context context, HttpHost host, HttpHost proxy, RequestFeeder requestFeeder) { - super(context, host, connectionManager, requestFeeder); + super(context, host, requestFeeder); + mProxyHost = proxy; } /** @@ -159,8 +162,7 @@ public class HttpsConnection extends Connection { AndroidHttpClientConnection openConnection(Request req) throws IOException { SSLSocket sslSock = null; - HttpHost proxyHost = mConnectionManager.getProxyHost(); - if (proxyHost != null) { + if (mProxyHost != null) { // If we have a proxy set, we first send a CONNECT request // to the proxy; if the proxy returns 200 OK, we negotiate // a secure connection to the target server via the proxy. @@ -172,7 +174,7 @@ public class HttpsConnection extends Connection { Socket proxySock = null; try { proxySock = new Socket - (proxyHost.getHostName(), proxyHost.getPort()); + (mProxyHost.getHostName(), mProxyHost.getPort()); proxySock.setSoTimeout(60 * 1000); diff --git a/core/java/android/net/http/RequestHandle.java b/core/java/android/net/http/RequestHandle.java index 190ae7a..77cd544 100644 --- a/core/java/android/net/http/RequestHandle.java +++ b/core/java/android/net/http/RequestHandle.java @@ -42,15 +42,13 @@ public class RequestHandle { private WebAddress mUri; private String mMethod; private Map<String, String> mHeaders; - private RequestQueue mRequestQueue; - private Request mRequest; - private InputStream mBodyProvider; private int mBodyLength; - private int mRedirectCount = 0; + // Used only with synchronous requests. + private Connection mConnection; private final static String AUTHORIZATION_HEADER = "Authorization"; private final static String PROXY_AUTHORIZATION_HEADER = "Proxy-Authorization"; @@ -81,6 +79,19 @@ public class RequestHandle { } /** + * Creates a new request session with a given Connection. This connection + * is used during a synchronous load to handle this request. + */ + public RequestHandle(RequestQueue requestQueue, String url, WebAddress uri, + String method, Map<String, String> headers, + InputStream bodyProvider, int bodyLength, Request request, + Connection conn) { + this(requestQueue, url, uri, method, headers, bodyProvider, bodyLength, + request); + mConnection = conn; + } + + /** * Cancels this request */ public void cancel() { @@ -262,6 +273,12 @@ public class RequestHandle { mRequest.waitUntilComplete(); } + public void processRequest() { + if (mConnection != null) { + mConnection.processRequests(mRequest); + } + } + /** * @return Digest-scheme authentication response. */ diff --git a/core/java/android/net/http/RequestQueue.java b/core/java/android/net/http/RequestQueue.java index 875caa0..84b6487 100644 --- a/core/java/android/net/http/RequestQueue.java +++ b/core/java/android/net/http/RequestQueue.java @@ -171,16 +171,17 @@ public class RequestQueue implements RequestFeeder { } public Connection getConnection(Context context, HttpHost host) { + host = RequestQueue.this.determineHost(host); Connection con = mIdleCache.getConnection(host); if (con == null) { mTotalConnection++; - con = Connection.getConnection( - mContext, host, this, RequestQueue.this); + con = Connection.getConnection(mContext, host, mProxyHost, + RequestQueue.this); } return con; } - public boolean recycleConnection(HttpHost host, Connection connection) { - return mIdleCache.cacheConnection(host, connection); + public boolean recycleConnection(Connection connection) { + return mIdleCache.cacheConnection(connection.getHost(), connection); } } @@ -342,6 +343,66 @@ public class RequestQueue implements RequestFeeder { req); } + private static class SyncFeeder implements RequestFeeder { + // This is used in the case where the request fails and needs to be + // requeued into the RequestFeeder. + private Request mRequest; + SyncFeeder() { + } + public Request getRequest() { + Request r = mRequest; + mRequest = null; + return r; + } + public Request getRequest(HttpHost host) { + return getRequest(); + } + public boolean haveRequest(HttpHost host) { + return mRequest != null; + } + public void requeueRequest(Request r) { + mRequest = r; + } + } + + public RequestHandle queueSynchronousRequest(String url, WebAddress uri, + String method, Map<String, String> headers, + EventHandler eventHandler, InputStream bodyProvider, + int bodyLength) { + if (HttpLog.LOGV) { + HttpLog.v("RequestQueue.dispatchSynchronousRequest " + uri); + } + + HttpHost host = new HttpHost(uri.mHost, uri.mPort, uri.mScheme); + + Request req = new Request(method, host, mProxyHost, uri.mPath, + bodyProvider, bodyLength, eventHandler, headers); + + // Open a new connection that uses our special RequestFeeder + // implementation. + host = determineHost(host); + Connection conn = Connection.getConnection(mContext, host, mProxyHost, + new SyncFeeder()); + + // TODO: I would like to process the request here but LoadListener + // needs a RequestHandle to process some messages. + return new RequestHandle(this, url, uri, method, headers, bodyProvider, + bodyLength, req, conn); + + } + + // Chooses between the proxy and the request's host. + private HttpHost determineHost(HttpHost host) { + // There used to be a comment in ConnectionThread about t-mob's proxy + // being really bad about https. But, HttpsConnection actually looks + // for a proxy and connects through it anyway. I think that this check + // is still valid because if a site is https, we will use + // HttpsConnection rather than HttpConnection if the proxy address is + // not secure. + return (mProxyHost == null || "https".equals(host.getSchemeName())) + ? host : mProxyHost; + } + /** * @return true iff there are any non-active requests pending */ @@ -478,6 +539,6 @@ public class RequestQueue implements RequestFeeder { interface ConnectionManager { HttpHost getProxyHost(); Connection getConnection(Context context, HttpHost host); - boolean recycleConnection(HttpHost host, Connection connection); + boolean recycleConnection(Connection connection); } } diff --git a/core/java/android/net/http/SslError.java b/core/java/android/net/http/SslError.java index 2788cb1..e1b9deb 100644 --- a/core/java/android/net/http/SslError.java +++ b/core/java/android/net/http/SslError.java @@ -20,8 +20,6 @@ import java.security.cert.X509Certificate; /** * One or more individual SSL errors and the associated SSL certificate - * - * {@hide} */ public class SslError { diff --git a/core/java/android/os/AsyncTask.java b/core/java/android/os/AsyncTask.java index 7d2c698..d28148c 100644 --- a/core/java/android/os/AsyncTask.java +++ b/core/java/android/os/AsyncTask.java @@ -82,7 +82,7 @@ import java.util.concurrent.atomic.AtomicInteger; * <li><code>Result</code>, the type of the result of the background * computation.</li> * </ol> - * <p>Not all types are always used by am asynchronous task. To mark a type as unused, + * <p>Not all types are always used by an asynchronous task. To mark a type as unused, * simply use the type {@link Void}:</p> * <pre> * private class MyTask extends AsyncTask<Void, Void, Void> { ... } diff --git a/core/java/android/os/Build.java b/core/java/android/os/Build.java index e9353d8..fcd8f38 100644 --- a/core/java/android/os/Build.java +++ b/core/java/android/os/Build.java @@ -21,7 +21,7 @@ package android.os; */ public class Build { /** Value used for when a build property is unknown. */ - private static final String UNKNOWN = "unknown"; + public static final String UNKNOWN = "unknown"; /** Either a changelist number, or a label like "M4-rc20". */ public static final String ID = getString("ro.build.id"); @@ -41,6 +41,9 @@ public class Build { /** The name of the instruction set (CPU type + ABI convention) of native code. */ public static final String CPU_ABI = getString("ro.product.cpu.abi"); + /** The name of the second instruction set (CPU type + ABI convention) of native code. */ + public static final String CPU_ABI2 = getString("ro.product.cpu.abi2"); + /** The manufacturer of the product/hardware. */ public static final String MANUFACTURER = getString("ro.product.manufacturer"); @@ -50,6 +53,15 @@ public class Build { /** The end-user-visible name for the end product. */ public static final String MODEL = getString("ro.product.model"); + /** @pending The system bootloader version number. */ + public static final String BOOTLOADER = getString("ro.bootloader"); + + /** @pending The radio firmware version number. */ + public static final String RADIO = getString("gsm.version.baseband"); + + /** @pending The device serial number. */ + public static final String SERIAL = getString("ro.serialno"); + /** Various version strings. */ public static class VERSION { /** diff --git a/core/java/android/os/Debug.java b/core/java/android/os/Debug.java index b4f64b6..b33e8be 100644 --- a/core/java/android/os/Debug.java +++ b/core/java/android/os/Debug.java @@ -753,6 +753,16 @@ href="{@docRoot}guide/developing/tools/traceview.html">Traceview: A Graphical Lo } /** + * Dumps the contents of VM reference tables (e.g. JNI locals and + * globals) to the log file. + * + * @hide + */ + public static final void dumpReferenceTables() { + VMDebug.dumpReferenceTables(); + } + + /** * API for gathering and querying instruction counts. * * Example usage: diff --git a/core/java/android/os/HandlerState.java b/core/java/android/os/DropBoxManager.aidl index 0708f7d..6474ec2 100644 --- a/core/java/android/os/HandlerState.java +++ b/core/java/android/os/DropBoxManager.aidl @@ -1,5 +1,5 @@ /* - * Copyright (C) 2006 The Android Open Source Project + * 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. @@ -16,18 +16,4 @@ package android.os; -/** - * {@hide} - */ -public abstract class HandlerState { - public HandlerState() { - } - - public void enter(Message message) { - } - - public abstract void processMessage(Message message); - - public void exit(Message message) { - } -} +parcelable DropBoxManager.Entry; diff --git a/core/java/android/os/DropBoxManager.java b/core/java/android/os/DropBoxManager.java new file mode 100644 index 0000000..7889a92 --- /dev/null +++ b/core/java/android/os/DropBoxManager.java @@ -0,0 +1,281 @@ +/* + * 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 android.os; + +import android.util.Log; + +import com.android.internal.os.IDropBoxManagerService; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.zip.GZIPInputStream; + +/** + * Enqueues chunks of data (from various sources -- application crashes, kernel + * log records, etc.). The queue is size bounded and will drop old data if the + * enqueued data exceeds the maximum size. You can think of this as a + * persistent, system-wide, blob-oriented "logcat". + * + * <p>You can obtain an instance of this class by calling + * {@link android.content.Context#getSystemService} + * with {@link android.content.Context#DROPBOX_SERVICE}. + * + * <p>DropBoxManager entries are not sent anywhere directly, but other system + * services and debugging tools may scan and upload entries for processing. + */ +public class DropBoxManager { + private static final String TAG = "DropBoxManager"; + private final IDropBoxManagerService mService; + + /** Flag value: Entry's content was deleted to save space. */ + public static final int IS_EMPTY = 1; + + /** Flag value: Content is human-readable UTF-8 text (can be combined with IS_GZIPPED). */ + public static final int IS_TEXT = 2; + + /** Flag value: Content can be decompressed with {@link java.util.zip.GZIPOutputStream}. */ + public static final int IS_GZIPPED = 4; + + /** + * A single entry retrieved from the drop box. + * This may include a reference to a stream, so you must call + * {@link #close()} when you are done using it. + */ + public static class Entry implements Parcelable { + private final String mTag; + private final long mTimeMillis; + + private final byte[] mData; + private final ParcelFileDescriptor mFileDescriptor; + private final int mFlags; + + /** Create a new empty Entry with no contents. */ + public Entry(String tag, long millis) { + this(tag, millis, (Object) null, IS_EMPTY); + } + + /** Create a new Entry with plain text contents. */ + public Entry(String tag, long millis, String text) { + this(tag, millis, (Object) text.getBytes(), IS_TEXT); + } + + /** + * Create a new Entry with byte array contents. + * The data array must not be modified after creating this entry. + */ + public Entry(String tag, long millis, byte[] data, int flags) { + this(tag, millis, (Object) data, flags); + } + + /** + * Create a new Entry with streaming data contents. + * Takes ownership of the ParcelFileDescriptor. + */ + public Entry(String tag, long millis, ParcelFileDescriptor data, int flags) { + this(tag, millis, (Object) data, flags); + } + + /** + * Create a new Entry with the contents read from a file. + * The file will be read when the entry's contents are requested. + */ + public Entry(String tag, long millis, File data, int flags) throws IOException { + this(tag, millis, (Object) ParcelFileDescriptor.open( + data, ParcelFileDescriptor.MODE_READ_ONLY), flags); + } + + /** Internal constructor for CREATOR.createFromParcel(). */ + private Entry(String tag, long millis, Object value, int flags) { + if (tag == null) throw new NullPointerException(); + if (((flags & IS_EMPTY) != 0) != (value == null)) throw new IllegalArgumentException(); + + mTag = tag; + mTimeMillis = millis; + mFlags = flags; + + if (value == null) { + mData = null; + mFileDescriptor = null; + } else if (value instanceof byte[]) { + mData = (byte[]) value; + mFileDescriptor = null; + } else if (value instanceof ParcelFileDescriptor) { + mData = null; + mFileDescriptor = (ParcelFileDescriptor) value; + } else { + throw new IllegalArgumentException(); + } + } + + /** Close the input stream associated with this entry. */ + public void close() { + try { if (mFileDescriptor != null) mFileDescriptor.close(); } catch (IOException e) { } + } + + /** @return the tag originally attached to the entry. */ + public String getTag() { return mTag; } + + /** @return time when the entry was originally created. */ + public long getTimeMillis() { return mTimeMillis; } + + /** @return flags describing the content returned by @{link #getInputStream()}. */ + public int getFlags() { return mFlags & ~IS_GZIPPED; } // getInputStream() decompresses. + + /** + * @param maxBytes of string to return (will truncate at this length). + * @return the uncompressed text contents of the entry, null if the entry is not text. + */ + public String getText(int maxBytes) { + if ((mFlags & IS_TEXT) == 0) return null; + if (mData != null) return new String(mData, 0, Math.min(maxBytes, mData.length)); + + InputStream is = null; + try { + is = getInputStream(); + byte[] buf = new byte[maxBytes]; + return new String(buf, 0, Math.max(0, is.read(buf))); + } catch (IOException e) { + return null; + } finally { + try { if (is != null) is.close(); } catch (IOException e) {} + } + } + + /** @return the uncompressed contents of the entry, or null if the contents were lost */ + public InputStream getInputStream() throws IOException { + InputStream is; + if (mData != null) { + is = new ByteArrayInputStream(mData); + } else if (mFileDescriptor != null) { + is = new ParcelFileDescriptor.AutoCloseInputStream(mFileDescriptor); + } else { + return null; + } + return (mFlags & IS_GZIPPED) != 0 ? new GZIPInputStream(is) : is; + } + + public static final Parcelable.Creator<Entry> CREATOR = new Parcelable.Creator() { + public Entry[] newArray(int size) { return new Entry[size]; } + public Entry createFromParcel(Parcel in) { + return new Entry( + in.readString(), in.readLong(), in.readValue(null), in.readInt()); + } + }; + + public int describeContents() { + return mFileDescriptor != null ? Parcelable.CONTENTS_FILE_DESCRIPTOR : 0; + } + + public void writeToParcel(Parcel out, int flags) { + out.writeString(mTag); + out.writeLong(mTimeMillis); + if (mFileDescriptor != null) { + out.writeValue(mFileDescriptor); + } else { + out.writeValue(mData); + } + out.writeInt(mFlags); + } + } + + /** {@hide} */ + public DropBoxManager(IDropBoxManagerService service) { mService = service; } + + /** + * Create a dummy instance for testing. All methods will fail unless + * overridden with an appropriate mock implementation. To obtain a + * functional instance, use {@link android.content.Context#getSystemService}. + */ + protected DropBoxManager() { mService = null; } + + /** + * Stores human-readable text. The data may be discarded eventually (or even + * immediately) if space is limited, or ignored entirely if the tag has been + * blocked (see {@link #isTagEnabled}). + * + * @param tag describing the type of entry being stored + * @param data value to store + */ + public void addText(String tag, String data) { + try { mService.add(new Entry(tag, 0, data)); } catch (RemoteException e) {} + } + + /** + * Stores binary data, which may be ignored or discarded as with {@link #addText}. + * + * @param tag describing the type of entry being stored + * @param data value to store + * @param flags describing the data + */ + public void addData(String tag, byte[] data, int flags) { + if (data == null) throw new NullPointerException(); + try { mService.add(new Entry(tag, 0, data, flags)); } catch (RemoteException e) {} + } + + /** + * Stores the contents of a file, which may be ignored or discarded as with + * {@link #addText}. + * + * @param tag describing the type of entry being stored + * @param file to read from + * @param flags describing the data + * @throws IOException if the file can't be opened + */ + public void addFile(String tag, File file, int flags) throws IOException { + if (file == null) throw new NullPointerException(); + Entry entry = new Entry(tag, 0, file, flags); + try { + mService.add(new Entry(tag, 0, file, flags)); + } catch (RemoteException e) { + // ignore + } finally { + entry.close(); + } + } + + /** + * Checks any blacklists (set in system settings) to see whether a certain + * tag is allowed. Entries with disabled tags will be dropped immediately, + * so you can save the work of actually constructing and sending the data. + * + * @param tag that would be used in {@link #addText} or {@link #addFile} + * @return whether events with that tag would be accepted + */ + public boolean isTagEnabled(String tag) { + try { return mService.isTagEnabled(tag); } catch (RemoteException e) { return false; } + } + + /** + * Gets the next entry from the drop box *after* the specified time. + * Requires android.permission.READ_LOGS. You must always call + * {@link Entry#close()} on the return value! + * + * @param tag of entry to look for, null for all tags + * @param msec time of the last entry seen + * @return the next entry, or null if there are no more entries + */ + public Entry getNextEntry(String tag, long msec) { + try { return mService.getNextEntry(tag, msec); } catch (RemoteException e) { return null; } + } + + // TODO: It may be useful to have some sort of notification mechanism + // when data is added to the dropbox, for demand-driven readers -- + // for now readers need to poll the dropbox to find new data. +} diff --git a/core/java/android/os/Environment.java b/core/java/android/os/Environment.java index f761e8e..9491bd4 100644 --- a/core/java/android/os/Environment.java +++ b/core/java/android/os/Environment.java @@ -18,6 +18,8 @@ package android.os; import java.io.File; +import android.os.IMountService; + /** * Provides access to environment variables. */ @@ -26,6 +28,10 @@ public class Environment { private static final File ROOT_DIRECTORY = getDirectory("ANDROID_ROOT", "/system"); + private static final String SYSTEM_PROPERTY_EFS_ENABLED = "persist.security.efs.enabled"; + + private static IMountService mMntSvc = null; + /** * Gets the Android root directory. */ @@ -33,9 +39,55 @@ public class Environment { return ROOT_DIRECTORY; } + /** + * Gets the system directory available for secure storage. + * If Encrypted File system is enabled, it returns an encrypted directory (/data/secure/system). + * Otherwise, it returns the unencrypted /data/system directory. + * @return File object representing the secure storage system directory. + * @hide + */ + public static File getSystemSecureDirectory() { + if (isEncryptedFilesystemEnabled()) { + return new File(SECURE_DATA_DIRECTORY, "system"); + } else { + return new File(DATA_DIRECTORY, "system"); + } + } + + /** + * Gets the data directory for secure storage. + * If Encrypted File system is enabled, it returns an encrypted directory (/data/secure). + * Otherwise, it returns the unencrypted /data directory. + * @return File object representing the data directory for secure storage. + * @hide + */ + public static File getSecureDataDirectory() { + if (isEncryptedFilesystemEnabled()) { + return SECURE_DATA_DIRECTORY; + } else { + return DATA_DIRECTORY; + } + } + + /** + * Returns whether the Encrypted File System feature is enabled on the device or not. + * @return <code>true</code> if Encrypted File System feature is enabled, <code>false</code> + * if disabled. + * @hide + */ + public static boolean isEncryptedFilesystemEnabled() { + return SystemProperties.getBoolean(SYSTEM_PROPERTY_EFS_ENABLED, false); + } + private static final File DATA_DIRECTORY = getDirectory("ANDROID_DATA", "/data"); + /** + * @hide + */ + private static final File SECURE_DATA_DIRECTORY + = getDirectory("ANDROID_SECURE_DATA", "/data/secure"); + private static final File EXTERNAL_STORAGE_DIRECTORY = getDirectory("EXTERNAL_STORAGE", "/sdcard"); @@ -119,9 +171,19 @@ public class Environment { /** * Gets the current state of the external storage device. + * Note: This call should be deprecated as it doesn't support + * multiple volumes. */ public static String getExternalStorageState() { - return SystemProperties.get("EXTERNAL_STORAGE_STATE", MEDIA_REMOVED); + try { + if (mMntSvc == null) { + mMntSvc = IMountService.Stub.asInterface(ServiceManager + .getService("mount")); + } + return mMntSvc.getVolumeState(getExternalStorageDirectory().toString()); + } catch (Exception rex) { + return Environment.MEDIA_REMOVED; + } } static File getDirectory(String variableName, String defaultPath) { diff --git a/core/java/android/os/FileObserver.java b/core/java/android/os/FileObserver.java index 38d252e..3457815 100644 --- a/core/java/android/os/FileObserver.java +++ b/core/java/android/os/FileObserver.java @@ -103,9 +103,7 @@ public abstract class FileObserver { try { observer.onEvent(mask, path); } catch (Throwable throwable) { - Log.e(LOG_TAG, "Unhandled throwable " + throwable.toString() + - " (returned by observer " + observer + ")", throwable); - RuntimeInit.crash("FileObserver", throwable); + Log.wtf(LOG_TAG, "Unhandled exception in FileObserver " + observer, throwable); } } } diff --git a/core/java/android/os/FileUtils.java b/core/java/android/os/FileUtils.java index 51dfb5b..4780cf3 100644 --- a/core/java/android/os/FileUtils.java +++ b/core/java/android/os/FileUtils.java @@ -153,14 +153,16 @@ public class FileUtils public static String readTextFile(File file, int max, String ellipsis) throws IOException { InputStream input = new FileInputStream(file); try { - if (max > 0) { // "head" mode: read the first N bytes + long size = file.length(); + if (max > 0 || (size > 0 && max == 0)) { // "head" mode: read the first N bytes + if (size > 0 && (max == 0 || size < max)) max = (int) size; byte[] data = new byte[max + 1]; int length = input.read(data); if (length <= 0) return ""; if (length <= max) return new String(data, 0, length); if (ellipsis == null) return new String(data, 0, max); return new String(data, 0, max) + ellipsis; - } else if (max < 0) { // "tail" mode: read it all, keep the last N + } else if (max < 0) { // "tail" mode: keep the last N int len; boolean rolled = false; byte[] last = null, data = null; @@ -180,7 +182,7 @@ public class FileUtils } if (ellipsis == null || !rolled) return new String(last); return ellipsis + new String(last); - } else { // "cat" mode: read it all + } else { // "cat" mode: size unknown, read it all in streaming fashion ByteArrayOutputStream contents = new ByteArrayOutputStream(); int len; byte[] data = new byte[1024]; diff --git a/core/java/android/os/HandlerStateMachine.java b/core/java/android/os/HandlerStateMachine.java deleted file mode 100644 index 9e7902b..0000000 --- a/core/java/android/os/HandlerStateMachine.java +++ /dev/null @@ -1,290 +0,0 @@ -/* - * Copyright (C) 2006 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 android.os; - -import android.util.Log; -import android.util.LogPrinter; - -/** - * {@hide} - * - * Implement a state machine where each state is an object, - * HandlerState. Each HandlerState must implement processMessage - * and optionally enter/exit. When a state machine is created - * the initial state must be set. When messages are sent to - * a state machine the current state's processMessage method is - * invoked. If this is the first message for this state the - * enter method is called prior to processMessage and when - * transtionTo is invoked the state's exit method will be - * called after returning from processMessage. - * - * If a message should be handled in a different state the - * processMessage method may call deferMessage. This causes - * the message to be saved on a list until transitioning - * to a new state, at which time all of the deferred messages - * will be put on the front of the state machines queue and - * processed by the new current state's processMessage - * method. - * - * Below is an example state machine with two state's, S1 and S2. - * The initial state is S1 which defers all messages and only - * transition to S2 when message.what == TEST_WHAT_2. State S2 - * will process each messages until it receives TEST_WHAT_2 - * where it will transition back to S1: -<code> - class StateMachine1 extends HandlerStateMachine { - private static final int TEST_WHAT_1 = 1; - private static final int TEST_WHAT_2 = 2; - - StateMachine1(String name) { - super(name); - setInitialState(mS1); - } - - class S1 extends HandlerState { - &#064;Override public void enter(Message message) { - } - - &#064;Override public void processMessage(Message message) { - deferMessage(message); - if (message.what == TEST_WHAT_2) { - transitionTo(mS2); - } - } - - &#064;Override public void exit(Message message) { - } - } - - class S2 extends HandlerState { - &#064;Override public void processMessage(Message message) { - // Do some processing - if (message.what == TEST_WHAT_2) { - transtionTo(mS1); - } - } - } - - private S1 mS1 = new S1(); - private S2 mS2 = new S2(); - } -</code> - */ -public class HandlerStateMachine { - - private boolean mDbg = false; - private static final String TAG = "HandlerStateMachine"; - private String mName; - private SmHandler mHandler; - private HandlerThread mHandlerThread; - - /** - * Handle messages sent to the state machine by calling - * the current state's processMessage. It also handles - * the enter/exit calls and placing any deferred messages - * back onto the queue when transitioning to a new state. - */ - class SmHandler extends Handler { - - SmHandler(Looper looper) { - super(looper); - } - - /** - * This will dispatch the message to the - * current state's processMessage. - */ - @Override - final public void handleMessage(Message msg) { - if (mDbg) Log.d(TAG, "SmHandler.handleMessage E"); - if (mDestState != null) { - if (mDbg) Log.d(TAG, "SmHandler.handleMessage; new destation call enter"); - mCurrentState = mDestState; - mDestState = null; - mCurrentState.enter(msg); - } - if (mCurrentState != null) { - if (mDbg) Log.d(TAG, "SmHandler.handleMessage; call processMessage"); - mCurrentState.processMessage(msg); - } else { - /* Strange no state to execute */ - Log.e(TAG, "handleMessage: no current state, did you call setInitialState"); - } - - if (mDestState != null) { - if (mDbg) Log.d(TAG, "SmHandler.handleMessage; new destination call exit"); - mCurrentState.exit(msg); - - /** - * Place the messages from the deferred queue:t - * on to the Handler's message queue in the - * same order that they originally arrived. - * - * We set cur.when = 0 to circumvent the check - * that this message has already been sent. - */ - while (mDeferredMessages != null) { - Message cur = mDeferredMessages; - mDeferredMessages = mDeferredMessages.next; - cur.when = 0; - if (mDbg) Log.d(TAG, "SmHandler.handleMessage; queue deferred message what=" - + cur.what + " target=" + cur.target); - sendMessageAtFrontOfQueue(cur); - } - if (mDbg) Log.d(TAG, "SmHandler.handleMessage X"); - } - } - - public HandlerState mCurrentState; - public HandlerState mDestState; - public Message mDeferredMessages; - } - - /** - * Create an active StateMachine, one that has a - * dedicated thread/looper/queue. - */ - public HandlerStateMachine(String name) { - mName = name; - mHandlerThread = new HandlerThread(name); - mHandlerThread.start(); - mHandler = new SmHandler(mHandlerThread.getLooper()); - } - - /** - * Get a message and set Message.target = this. - */ - public final Message obtainMessage() - { - Message msg = Message.obtain(mHandler); - if (mDbg) Log.d(TAG, "StateMachine.obtainMessage() EX target=" + msg.target); - return msg; - } - - /** - * Get a message and set Message.target = this and - * Message.what = what. - */ - public final Message obtainMessage(int what) { - Message msg = Message.obtain(mHandler, what); - if (mDbg) { - Log.d(TAG, "StateMachine.obtainMessage(what) EX what=" + msg.what + - " target=" + msg.target); - } - return msg; - } - - /** - * Enqueue a message to this state machine. - */ - public final void sendMessage(Message msg) { - if (mDbg) Log.d(TAG, "StateMachine.sendMessage EX msg.what=" + msg.what); - mHandler.sendMessage(msg); - } - - /** - * Enqueue a message to this state machine after a delay. - */ - public final void sendMessageDelayed(Message msg, long delayMillis) { - if (mDbg) { - Log.d(TAG, "StateMachine.sendMessageDelayed EX msg.what=" - + msg.what + " delay=" + delayMillis); - } - mHandler.sendMessageDelayed(msg, delayMillis); - } - - /** - * Set the initial state. This must be invoked before - * and messages are sent to the state machine. - */ - public void setInitialState(HandlerState initialState) { - if (mDbg) { - Log.d(TAG, "StateMachine.setInitialState EX initialState" - + initialState.getClass().getName()); - } - mHandler.mDestState = initialState; - } - - /** - * transition to destination state. Upon returning - * from processMessage the current state's exit will - * be executed and upon the next message arriving - * destState.enter will be invoked. - */ - final public void transitionTo(HandlerState destState) { - if (mDbg) { - Log.d(TAG, "StateMachine.transitionTo EX destState" - + destState.getClass().getName()); - } - mHandler.mDestState = destState; - } - - /** - * Defer this message until next state transition. - * Upon transitioning all deferred messages will be - * placed on the queue and reprocessed in the original - * order. (i.e. The next state the oldest messages will - * be processed first) - */ - final public void deferMessage(Message msg) { - if (mDbg) { - Log.d(TAG, "StateMachine.deferMessage EX mDeferredMessages=" - + mHandler.mDeferredMessages); - } - - /* Copy the "msg" to "newMsg" as "msg" will be recycled */ - Message newMsg = obtainMessage(); - newMsg.copyFrom(msg); - - /* Place on front of queue */ - newMsg.next = mHandler.mDeferredMessages; - mHandler.mDeferredMessages = newMsg; - } - - /** - * @return the name - */ - public String getName() { - return mName; - } - - /** - * @return Handler - */ - public Handler getHandler() { - return mHandler; - } - - /** - * @return if debugging is enabled - */ - public boolean isDbg() { - return mDbg; - } - - /** - * Set debug enable/disabled. - */ - public void setDbg(boolean dbg) { - mDbg = dbg; - if (mDbg) { - mHandlerThread.getLooper().setMessageLogging(new LogPrinter(Log.VERBOSE, TAG)); - } else { - mHandlerThread.getLooper().setMessageLogging(null); - } - } -} diff --git a/core/java/android/os/Hardware.java b/core/java/android/os/Hardware.java deleted file mode 100644 index efc5617..0000000 --- a/core/java/android/os/Hardware.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (C) 2006 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 android.os; - -/** - * {@hide} - */ -public class Hardware -{ - - - /* ******************************************************************************** - * - * - * - * - * - * - * - * - * Don't add anything else to this class. Add it to HardwareService instead. - * - * - * - * - * - * - * - * ********************************************************************************/ - - - public static native boolean getFlashlightEnabled(); - public static native void setFlashlightEnabled(boolean on); - public static native void enableCameraFlash(int milliseconds); -} diff --git a/core/java/android/os/ICheckinService.aidl b/core/java/android/os/ICheckinService.aidl index e56b55d..e5609b0 100644 --- a/core/java/android/os/ICheckinService.aidl +++ b/core/java/android/os/ICheckinService.aidl @@ -26,24 +26,12 @@ import android.os.IParentalControlCallback; * {@hide} */ interface ICheckinService { - /** Synchronously attempt a checkin with the server, return true - * on success. - * @throws IllegalStateException whenever an error occurs. The - * cause of the exception will be the real exception: - * IOException for network errors, JSONException for invalid - * server responses, etc. - */ - boolean checkin(); - - /** Direct submission of crash data; returns after writing the crash. */ - void reportCrashSync(in byte[] crashData); - - /** Asynchronous "fire and forget" version of crash reporting. */ - oneway void reportCrashAsync(in byte[] crashData); - /** Reboot into the recovery system and wipe all user data. */ void masterClear(); + /** Reboot into the recovery system, wipe all user data and enable Encrypted File Systems. */ + void masterClearAndToggleEFS(boolean efsEnabled); + /** * Determine if the device is under parental control. Return null if * we are unable to check the parental control status. diff --git a/core/java/android/os/IMountService.aidl b/core/java/android/os/IMountService.aidl index 4491a8a..e73569a 100644 --- a/core/java/android/os/IMountService.aidl +++ b/core/java/android/os/IMountService.aidl @@ -42,17 +42,17 @@ interface IMountService /** * Mount external storage at given mount point. */ - void mountMedia(String mountPoint); + void mountVolume(String mountPoint); /** * Safely unmount external storage at given mount point. */ - void unmountMedia(String mountPoint); + void unmountVolume(String mountPoint); /** * Format external storage given a mount point. */ - void formatMedia(String mountPoint); + void formatVolume(String mountPoint); /** * Returns true if media notification sounds are enabled. @@ -65,16 +65,43 @@ interface IMountService void setPlayNotificationSounds(boolean value); /** - * Returns true if USB Mass Storage is automatically started - * when a UMS host is detected. + * Gets the state of an volume via it's mountpoint. */ - boolean getAutoStartUms(); + String getVolumeState(String mountPoint); + + /* + * Creates a secure container with the specified parameters. + * On success, the filesystem container-path is returned. + */ + String createSecureContainer(String id, int sizeMb, String fstype, String key, int ownerUid); + + /* + * Finalize a container which has just been created and populated. + * After finalization, the container is immutable. + */ + void finalizeSecureContainer(String id); + + /* + * Destroy a secure container, and free up all resources associated with it. + * NOTE: Ensure all references are released prior to deleting. + */ + void destroySecureContainer(String id); + + /* + * Mount a secure container with the specified key and owner UID. + * On success, the filesystem container-path is returned. + */ + String mountSecureContainer(String id, String key, int ownerUid); + + /* + * Returns the filesystem path of a mounted secure container. + */ + String getSecureContainerPath(String id); /** - * Sets whether or not USB Mass Storage is automatically started - * when a UMS host is detected. + * Gets an Array of currently known secure container IDs */ - void setAutoStartUms(boolean value); + String[] getSecureContainerList(); /** * Shuts down the MountService and gracefully unmounts all external media. diff --git a/core/java/android/os/IPowerManager.aidl b/core/java/android/os/IPowerManager.aidl index b9dc860..23762ca 100644 --- a/core/java/android/os/IPowerManager.aidl +++ b/core/java/android/os/IPowerManager.aidl @@ -28,11 +28,12 @@ interface IPowerManager void setPokeLock(int pokey, IBinder lock, String tag); int getSupportedWakeLockFlags(); void setStayOnSetting(int val); - long getScreenOnTime(); void preventScreenOn(boolean prevent); - void setScreenBrightnessOverride(int brightness); boolean isScreenOn(); + void reboot(String reason); + void crash(String message); // sets the brightness of the backlights (screen, keyboard, button) 0-255 void setBacklightBrightness(int brightness); + void setAttentionLight(boolean on, int color); } diff --git a/core/java/android/os/IHardwareService.aidl b/core/java/android/os/IVibratorService.aidl index 34f30a7..c98fb56 100755 --- a/core/java/android/os/IHardwareService.aidl +++ b/core/java/android/os/IVibratorService.aidl @@ -17,19 +17,10 @@ package android.os; /** {@hide} */ -interface IHardwareService +interface IVibratorService { - // Vibrator support void vibrate(long milliseconds, IBinder token); void vibratePattern(in long[] pattern, int repeat, IBinder token); void cancelVibrate(IBinder token); - - // flashlight support - boolean getFlashlightEnabled(); - void setFlashlightEnabled(boolean on); - void enableCameraFlash(int milliseconds); - - // for the phone - void setAttentionLight(boolean on, int color); } diff --git a/core/java/android/os/LocalPowerManager.java b/core/java/android/os/LocalPowerManager.java index 3fe21d9..d348f07 100644 --- a/core/java/android/os/LocalPowerManager.java +++ b/core/java/android/os/LocalPowerManager.java @@ -44,7 +44,10 @@ public interface LocalPowerManager { void enableUserActivity(boolean enabled); // the same as the method on PowerManager - public void userActivity(long time, boolean noChangeLights, int eventType); + void userActivity(long time, boolean noChangeLights, int eventType); boolean isScreenOn(); + + void setScreenBrightnessOverride(int brightness); + void setButtonBrightnessOverride(int brightness); } diff --git a/core/java/android/os/MemoryFile.java b/core/java/android/os/MemoryFile.java index 03542dd..9742b05 100644 --- a/core/java/android/os/MemoryFile.java +++ b/core/java/android/os/MemoryFile.java @@ -52,7 +52,7 @@ public class MemoryFile private static native void native_write(FileDescriptor fd, int address, byte[] buffer, int srcOffset, int destOffset, int count, boolean isUnpinned) throws IOException; private static native void native_pin(FileDescriptor fd, boolean pin) throws IOException; - private static native int native_get_mapped_size(FileDescriptor fd) throws IOException; + private static native int native_get_size(FileDescriptor fd) throws IOException; private FileDescriptor mFD; // ashmem file descriptor private int mAddress; // address of ashmem memory @@ -300,20 +300,19 @@ public class MemoryFile * @hide */ public static boolean isMemoryFile(FileDescriptor fd) throws IOException { - return (native_get_mapped_size(fd) >= 0); + return (native_get_size(fd) >= 0); } /** - * Returns the size of the memory file, rounded up to a page boundary, that - * the file descriptor refers to, or -1 if the file descriptor does not - * refer to a memory file. + * Returns the size of the memory file that the file descriptor refers to, + * or -1 if the file descriptor does not refer to a memory file. * * @throws IOException If <code>fd</code> is not a valid file descriptor. * * @hide */ - public static int getMappedSize(FileDescriptor fd) throws IOException { - return native_get_mapped_size(fd); + public static int getSize(FileDescriptor fd) throws IOException { + return native_get_size(fd); } /** diff --git a/core/java/android/os/MessageQueue.java b/core/java/android/os/MessageQueue.java index caf0923..bc653d6 100644 --- a/core/java/android/os/MessageQueue.java +++ b/core/java/android/os/MessageQueue.java @@ -115,9 +115,7 @@ public class MessageQueue { didIdle = true; keep = ((IdleHandler)idler).queueIdle(); } catch (Throwable t) { - Log.e("MessageQueue", - "IdleHandler threw exception", t); - RuntimeInit.crash("MessageQueue", t); + Log.wtf("MessageQueue", "IdleHandler threw exception", t); } if (!keep) { diff --git a/core/java/android/os/PowerManager.java b/core/java/android/os/PowerManager.java index 4b3b6f6..e4eaf45 100644 --- a/core/java/android/os/PowerManager.java +++ b/core/java/android/os/PowerManager.java @@ -326,12 +326,11 @@ public class PowerManager { synchronized (mToken) { if (mHeld) { + Log.wtf(TAG, "WakeLock finalized while still held: " + mTag); try { mService.releaseWakeLock(mToken, 0); } catch (RemoteException e) { } - RuntimeInit.crash(TAG, new Exception( - "WakeLock finalized while still held: "+mTag)); } } } @@ -465,6 +464,22 @@ public class PowerManager } } + /** + * Reboot the device. Will not return if the reboot is + * successful. Requires the {@link android.Manifest.permission#REBOOT} + * permission. + * + * @param reason code to pass to the kernel (e.g., "recovery") to + * request special boot modes, or null. + */ + public void reboot(String reason) + { + try { + mService.reboot(reason); + } catch (RemoteException e) { + } + } + private PowerManager() { } @@ -488,4 +503,3 @@ public class PowerManager IPowerManager mService; Handler mHandler; } - diff --git a/core/java/android/os/RecoverySystem.java b/core/java/android/os/RecoverySystem.java new file mode 100644 index 0000000..3dd3918 --- /dev/null +++ b/core/java/android/os/RecoverySystem.java @@ -0,0 +1,418 @@ +/* + * Copyright (C) 2010 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 android.os; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileWriter; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.security.GeneralSecurityException; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Collection; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import android.content.Context; +import android.util.Log; + +import org.apache.harmony.security.asn1.BerInputStream; +import org.apache.harmony.security.pkcs7.ContentInfo; +import org.apache.harmony.security.pkcs7.SignedData; +import org.apache.harmony.security.pkcs7.SignerInfo; +import org.apache.harmony.security.provider.cert.X509CertImpl; + +/** + * RecoverySystem contains methods for interacting with the Android + * recovery system (the separate partition that can be used to install + * system updates, wipe user data, etc.) + */ +public class RecoverySystem { + private static final String TAG = "RecoverySystem"; + + /** + * Default location of zip file containing public keys (X509 + * certs) authorized to sign OTA updates. + */ + private static final File DEFAULT_KEYSTORE = + new File("/system/etc/security/otacerts.zip"); + + /** Send progress to listeners no more often than this (in ms). */ + private static final long PUBLISH_PROGRESS_INTERVAL_MS = 500; + + /** Used to communicate with recovery. See bootable/recovery/recovery.c. */ + private static File RECOVERY_DIR = new File("/cache/recovery"); + private static File COMMAND_FILE = new File(RECOVERY_DIR, "command"); + private static File LOG_FILE = new File(RECOVERY_DIR, "log"); + + // Length limits for reading files. + private static int LOG_FILE_MAX_LENGTH = 8 * 1024; + + /** + * Interface definition for a callback to be invoked regularly as + * verification proceeds. + */ + public interface ProgressListener { + /** + * Called periodically as the verification progresses. + * + * @param progress the approximate percentage of the + * verification that has been completed, ranging from 0 + * to 100 (inclusive). + */ + public void onProgress(int progress); + } + + /** @return the set of certs that can be used to sign an OTA package. */ + private static HashSet<Certificate> getTrustedCerts(File keystore) + throws IOException, GeneralSecurityException { + HashSet<Certificate> trusted = new HashSet<Certificate>(); + if (keystore == null) { + keystore = DEFAULT_KEYSTORE; + } + ZipFile zip = new ZipFile(keystore); + try { + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + Enumeration<? extends ZipEntry> entries = zip.entries(); + while (entries.hasMoreElements()) { + ZipEntry entry = entries.nextElement(); + trusted.add(cf.generateCertificate(zip.getInputStream(entry))); + } + } finally { + zip.close(); + } + return trusted; + } + + /** + * Verify the cryptographic signature of a system update package + * before installing it. Note that the package is also verified + * separately by the installer once the device is rebooted into + * the recovery system. This function will return only if the + * package was successfully verified; otherwise it will throw an + * exception. + * + * Verification of a package can take significant time, so this + * function should not be called from a UI thread. + * + * @param packageFile the package to be verified + * @param listener an object to receive periodic progress + * updates as verification proceeds. May be null. + * @param deviceCertsZipFile the zip file of certificates whose + * public keys we will accept. Verification succeeds if the + * package is signed by the private key corresponding to any + * public key in this file. May be null to use the system default + * file (currently "/system/etc/security/otacerts.zip"). + * + * @throws IOException if there were any errors reading the + * package or certs files. + * @throws GeneralSecurityException if verification failed + */ + public static void verifyPackage(File packageFile, + ProgressListener listener, + File deviceCertsZipFile) + throws IOException, GeneralSecurityException { + long fileLen = packageFile.length(); + + RandomAccessFile raf = new RandomAccessFile(packageFile, "r"); + try { + int lastPercent = 0; + long lastPublishTime = System.currentTimeMillis(); + if (listener != null) { + listener.onProgress(lastPercent); + } + + raf.seek(fileLen - 6); + byte[] footer = new byte[6]; + raf.readFully(footer); + + if (footer[2] != (byte)0xff || footer[3] != (byte)0xff) { + throw new SignatureException("no signature in file (no footer)"); + } + + int commentSize = (footer[4] & 0xff) | ((footer[5] & 0xff) << 8); + int signatureStart = (footer[0] & 0xff) | ((footer[1] & 0xff) << 8); + Log.v(TAG, String.format("comment size %d; signature start %d", + commentSize, signatureStart)); + + byte[] eocd = new byte[commentSize + 22]; + raf.seek(fileLen - (commentSize + 22)); + raf.readFully(eocd); + + // Check that we have found the start of the + // end-of-central-directory record. + if (eocd[0] != (byte)0x50 || eocd[1] != (byte)0x4b || + eocd[2] != (byte)0x05 || eocd[3] != (byte)0x06) { + throw new SignatureException("no signature in file (bad footer)"); + } + + for (int i = 4; i < eocd.length-3; ++i) { + if (eocd[i ] == (byte)0x50 && eocd[i+1] == (byte)0x4b && + eocd[i+2] == (byte)0x05 && eocd[i+3] == (byte)0x06) { + throw new SignatureException("EOCD marker found after start of EOCD"); + } + } + + // The following code is largely copied from + // JarUtils.verifySignature(). We could just *call* that + // method here if that function didn't read the entire + // input (ie, the whole OTA package) into memory just to + // compute its message digest. + + BerInputStream bis = new BerInputStream( + new ByteArrayInputStream(eocd, commentSize+22-signatureStart, signatureStart)); + ContentInfo info = (ContentInfo)ContentInfo.ASN1.decode(bis); + SignedData signedData = info.getSignedData(); + if (signedData == null) { + throw new IOException("signedData is null"); + } + Collection encCerts = signedData.getCertificates(); + if (encCerts.isEmpty()) { + throw new IOException("encCerts is empty"); + } + // Take the first certificate from the signature (packages + // should contain only one). + Iterator it = encCerts.iterator(); + X509Certificate cert = null; + if (it.hasNext()) { + cert = new X509CertImpl((org.apache.harmony.security.x509.Certificate)it.next()); + } else { + throw new SignatureException("signature contains no certificates"); + } + + List sigInfos = signedData.getSignerInfos(); + SignerInfo sigInfo; + if (!sigInfos.isEmpty()) { + sigInfo = (SignerInfo)sigInfos.get(0); + } else { + throw new IOException("no signer infos!"); + } + + // Check that the public key of the certificate contained + // in the package equals one of our trusted public keys. + + HashSet<Certificate> trusted = getTrustedCerts( + deviceCertsZipFile == null ? DEFAULT_KEYSTORE : deviceCertsZipFile); + + PublicKey signatureKey = cert.getPublicKey(); + boolean verified = false; + for (Certificate c : trusted) { + if (c.getPublicKey().equals(signatureKey)) { + verified = true; + break; + } + } + if (!verified) { + throw new SignatureException("signature doesn't match any trusted key"); + } + + // The signature cert matches a trusted key. Now verify that + // the digest in the cert matches the actual file data. + + // The verifier in recovery *only* handles SHA1withRSA + // signatures. SignApk.java always uses SHA1withRSA, no + // matter what the cert says to use. Ignore + // cert.getSigAlgName(), and instead use whatever + // algorithm is used by the signature (which should be + // SHA1withRSA). + + String da = sigInfo.getdigestAlgorithm(); + String dea = sigInfo.getDigestEncryptionAlgorithm(); + String alg = null; + if (da == null || dea == null) { + // fall back to the cert algorithm if the sig one + // doesn't look right. + alg = cert.getSigAlgName(); + } else { + alg = da + "with" + dea; + } + Signature sig = Signature.getInstance(alg); + sig.initVerify(cert); + + // The signature covers all of the OTA package except the + // archive comment and its 2-byte length. + long toRead = fileLen - commentSize - 2; + long soFar = 0; + raf.seek(0); + byte[] buffer = new byte[4096]; + while (soFar < toRead) { + int size = buffer.length; + if (soFar + size > toRead) { + size = (int)(toRead - soFar); + } + int read = raf.read(buffer, 0, size); + sig.update(buffer, 0, read); + soFar += read; + + if (listener != null) { + long now = System.currentTimeMillis(); + int p = (int)(soFar * 100 / toRead); + if (p > lastPercent && + now - lastPublishTime > PUBLISH_PROGRESS_INTERVAL_MS) { + lastPercent = p; + lastPublishTime = now; + listener.onProgress(lastPercent); + } + } + } + if (listener != null) { + listener.onProgress(100); + } + + if (!sig.verify(sigInfo.getEncryptedDigest())) { + throw new SignatureException("signature digest verification failed"); + } + } finally { + raf.close(); + } + } + + /** + * Reboots the device in order to install the given update + * package. + * Requires the {@link android.Manifest.permission#REBOOT} + * and {@link android.Manifest.permission#ACCESS_CACHE_FILESYSTEM} + * permissions. + * + * @param context the Context to use + * @param packageFile the update package to install. Currently + * must be on the /cache or /data partitions. + * + * @throws IOException if writing the recovery command file + * fails, or if the reboot itself fails. + */ + public static void installPackage(Context context, File packageFile) + throws IOException { + String filename = packageFile.getCanonicalPath(); + + if (filename.startsWith("/cache/")) { + filename = "CACHE:" + filename.substring(7); + } else if (filename.startsWith("/data/")) { + filename = "DATA:" + filename.substring(6); + } else { + throw new IllegalArgumentException( + "Must start with /cache or /data: " + filename); + } + Log.w(TAG, "!!! REBOOTING TO INSTALL " + filename + " !!!"); + String arg = "--update_package=" + filename; + bootCommand(context, arg); + } + + /** + * Reboots the device and wipes the user data partition. This is + * sometimes called a "factory reset", which is something of a + * misnomer because the system partition is not restored to its + * factory state. + * Requires the {@link android.Manifest.permission#REBOOT} + * and {@link android.Manifest.permission#ACCESS_CACHE_FILESYSTEM} + * permissions. + * + * @param context the Context to use + * + * @throws IOException if writing the recovery command file + * fails, or if the reboot itself fails. + */ + public static void rebootWipeUserData(Context context) + throws IOException { + bootCommand(context, "--wipe_data"); + } + + /** + * Reboot into the recovery system to wipe the /data partition and toggle + * Encrypted File Systems on/off. + * @param extras to add to the RECOVERY_COMPLETED intent after rebooting. + * @throws IOException if something goes wrong. + * + * @hide + */ + public static void rebootToggleEFS(Context context, boolean efsEnabled) + throws IOException { + if (efsEnabled) { + bootCommand(context, "--set_encrypted_filesystem=on"); + } else { + bootCommand(context, "--set_encrypted_filesystem=off"); + } + } + + /** + * Reboot into the recovery system with the supplied argument. + * @param arg to pass to the recovery utility. + * @throws IOException if something goes wrong. + */ + private static void bootCommand(Context context, String arg) throws IOException { + RECOVERY_DIR.mkdirs(); // In case we need it + COMMAND_FILE.delete(); // In case it's not writable + LOG_FILE.delete(); + + FileWriter command = new FileWriter(COMMAND_FILE); + try { + command.write(arg); + command.write("\n"); + } finally { + command.close(); + } + + // Having written the command file, go ahead and reboot + PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + pm.reboot("recovery"); + + throw new IOException("Reboot failed (no permissions?)"); + } + + /** + * Called after booting to process and remove recovery-related files. + * @return the log file from recovery, or null if none was found. + * + * @hide + */ + public static String handleAftermath() { + // Record the tail of the LOG_FILE + String log = null; + try { + log = FileUtils.readTextFile(LOG_FILE, -LOG_FILE_MAX_LENGTH, "...\n"); + } catch (FileNotFoundException e) { + Log.i(TAG, "No recovery log file"); + } catch (IOException e) { + Log.e(TAG, "Error reading recovery log", e); + } + + // Delete everything in RECOVERY_DIR + String[] names = RECOVERY_DIR.list(); + for (int i = 0; names != null && i < names.length; i++) { + File f = new File(RECOVERY_DIR, names[i]); + if (!f.delete()) { + Log.e(TAG, "Can't delete: " + f); + } else { + Log.i(TAG, "Deleted: " + f); + } + } + + return log; + } + + private void RecoverySystem() { } // Do not instantiate +} diff --git a/core/java/android/os/Vibrator.java b/core/java/android/os/Vibrator.java index 51dcff1..1895cf8 100644 --- a/core/java/android/os/Vibrator.java +++ b/core/java/android/os/Vibrator.java @@ -23,14 +23,14 @@ package android.os; */ public class Vibrator { - IHardwareService mService; + IVibratorService mService; private final Binder mToken = new Binder(); /** @hide */ public Vibrator() { - mService = IHardwareService.Stub.asInterface( - ServiceManager.getService("hardware")); + mService = IVibratorService.Stub.asInterface( + ServiceManager.getService("vibrator")); } /** diff --git a/core/java/android/pim/vcard/Constants.java b/core/java/android/pim/vcard/Constants.java deleted file mode 100644 index ca41ce5..0000000 --- a/core/java/android/pim/vcard/Constants.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * 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 android.pim.vcard; - -/** - * Constants used in both composer and parser. - */ -/* package */ class Constants { - - public static final String ATTR_TYPE = "TYPE"; - - public static final String VERSION_V21 = "2.1"; - public static final String VERSION_V30 = "3.0"; - - // Properties both the current (as of 2009-08-17) ContactsStruct and de-fact vCard extensions - // shown in http://en.wikipedia.org/wiki/VCard support are defined here. - public static final String PROPERTY_X_AIM = "X-AIM"; - public static final String PROPERTY_X_MSN = "X-MSN"; - public static final String PROPERTY_X_YAHOO = "X-YAHOO"; - public static final String PROPERTY_X_ICQ = "X-ICQ"; - public static final String PROPERTY_X_JABBER = "X-JABBER"; - public static final String PROPERTY_X_GOOGLE_TALK = "X-GOOGLE-TALK"; - public static final String PROPERTY_X_SKYPE_USERNAME = "X-SKYPE-USERNAME"; - // Phone number for Skype, available as usual phone. - public static final String PROPERTY_X_SKYPE_PSTNNUMBER = "X-SKYPE-PSTNNUMBER"; - // Some device emits this "X-" attribute, which is specifically invalid but should be - // always properly accepted, and emitted in some special case (for that device/application). - public static final String PROPERTY_X_GOOGLE_TALK_WITH_SPACE = "X-GOOGLE TALK"; - - // How more than one TYPE fields are expressed is different between vCard 2.1 and vCard 3.0 - // - // e.g. - // 1) Probably valid in both vCard 2.1 and vCard 3.0: "ADR;TYPE=DOM;TYPE=HOME:..." - // 2) Valid in vCard 2.1 but not in vCard 3.0: "ADR;DOM;HOME:..." - // 3) Valid in vCard 3.0 but not in vCard 2.1: "ADR;TYPE=DOM,HOME:..." - // - // 2) has been the default of VCard exporter/importer in Android, but we can see the other - // formats in vCard data emitted by the other softwares/devices. - // - // So we are currently not sure which type is the best; probably we will have to change which - // type should be emitted depending on the device. - public static final String ATTR_TYPE_HOME = "HOME"; - public static final String ATTR_TYPE_WORK = "WORK"; - public static final String ATTR_TYPE_FAX = "FAX"; - public static final String ATTR_TYPE_CELL = "CELL"; - public static final String ATTR_TYPE_VOICE = "VOICE"; - public static final String ATTR_TYPE_INTERNET = "INTERNET"; - - public static final String ATTR_TYPE_PREF = "PREF"; - - // Phone types valid in vCard and known to ContactsContract, but not so common. - public static final String ATTR_TYPE_CAR = "CAR"; - public static final String ATTR_TYPE_ISDN = "ISDN"; - public static final String ATTR_TYPE_PAGER = "PAGER"; - - // Phone types existing in vCard 2.1 but not known to ContactsContract. - // TODO: should make parser make these TYPE_CUSTOM. - public static final String ATTR_TYPE_MODEM = "MODEM"; - public static final String ATTR_TYPE_MSG = "MSG"; - public static final String ATTR_TYPE_BBS = "BBS"; - public static final String ATTR_TYPE_VIDEO = "VIDEO"; - - // Phone types existing in the current Contacts structure but not valid in vCard (at least 2.1) - // These types are encoded to "X-" attributes when composing vCard for now. - // Parser passes these even if "X-" is added to the attribute. - public static final String ATTR_TYPE_PHONE_EXTRA_OTHER = "OTHER"; - public static final String ATTR_TYPE_PHONE_EXTRA_CALLBACK = "CALLBACK"; - // TODO: may be "TYPE=COMPANY,PREF", not "COMPANY-MAIN". - public static final String ATTR_TYPE_PHONE_EXTRA_COMPANY_MAIN = "COMPANY-MAIN"; - public static final String ATTR_TYPE_PHONE_EXTRA_RADIO = "RADIO"; - public static final String ATTR_TYPE_PHONE_EXTRA_TELEX = "TELEX"; - public static final String ATTR_TYPE_PHONE_EXTRA_TTY_TDD = "TTY-TDD"; - public static final String ATTR_TYPE_PHONE_EXTRA_ASSISTANT = "ASSISTANT"; - - // DoCoMo specific attribute. Used with "SOUND" property, which is alternate of SORT-STRING in - // vCard 3.0. - public static final String ATTR_TYPE_X_IRMC_N = "X-IRMC-N"; - - private Constants() { - } -}
\ No newline at end of file diff --git a/core/java/android/pim/vcard/JapaneseUtils.java b/core/java/android/pim/vcard/JapaneseUtils.java new file mode 100644 index 0000000..875c29e --- /dev/null +++ b/core/java/android/pim/vcard/JapaneseUtils.java @@ -0,0 +1,380 @@ +/* + * 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 android.pim.vcard; + +import java.util.HashMap; +import java.util.Map; + +/** + * TextUtils especially for Japanese. + */ +/* package */ class JapaneseUtils { + static private final Map<Character, String> sHalfWidthMap = + new HashMap<Character, String>(); + + static { + // There's no logical mapping rule in Unicode. Sigh. + sHalfWidthMap.put('\u3001', "\uFF64"); + sHalfWidthMap.put('\u3002', "\uFF61"); + sHalfWidthMap.put('\u300C', "\uFF62"); + sHalfWidthMap.put('\u300D', "\uFF63"); + sHalfWidthMap.put('\u301C', "~"); + sHalfWidthMap.put('\u3041', "\uFF67"); + sHalfWidthMap.put('\u3042', "\uFF71"); + sHalfWidthMap.put('\u3043', "\uFF68"); + sHalfWidthMap.put('\u3044', "\uFF72"); + sHalfWidthMap.put('\u3045', "\uFF69"); + sHalfWidthMap.put('\u3046', "\uFF73"); + sHalfWidthMap.put('\u3047', "\uFF6A"); + sHalfWidthMap.put('\u3048', "\uFF74"); + sHalfWidthMap.put('\u3049', "\uFF6B"); + sHalfWidthMap.put('\u304A', "\uFF75"); + sHalfWidthMap.put('\u304B', "\uFF76"); + sHalfWidthMap.put('\u304C', "\uFF76\uFF9E"); + sHalfWidthMap.put('\u304D', "\uFF77"); + sHalfWidthMap.put('\u304E', "\uFF77\uFF9E"); + sHalfWidthMap.put('\u304F', "\uFF78"); + sHalfWidthMap.put('\u3050', "\uFF78\uFF9E"); + sHalfWidthMap.put('\u3051', "\uFF79"); + sHalfWidthMap.put('\u3052', "\uFF79\uFF9E"); + sHalfWidthMap.put('\u3053', "\uFF7A"); + sHalfWidthMap.put('\u3054', "\uFF7A\uFF9E"); + sHalfWidthMap.put('\u3055', "\uFF7B"); + sHalfWidthMap.put('\u3056', "\uFF7B\uFF9E"); + sHalfWidthMap.put('\u3057', "\uFF7C"); + sHalfWidthMap.put('\u3058', "\uFF7C\uFF9E"); + sHalfWidthMap.put('\u3059', "\uFF7D"); + sHalfWidthMap.put('\u305A', "\uFF7D\uFF9E"); + sHalfWidthMap.put('\u305B', "\uFF7E"); + sHalfWidthMap.put('\u305C', "\uFF7E\uFF9E"); + sHalfWidthMap.put('\u305D', "\uFF7F"); + sHalfWidthMap.put('\u305E', "\uFF7F\uFF9E"); + sHalfWidthMap.put('\u305F', "\uFF80"); + sHalfWidthMap.put('\u3060', "\uFF80\uFF9E"); + sHalfWidthMap.put('\u3061', "\uFF81"); + sHalfWidthMap.put('\u3062', "\uFF81\uFF9E"); + sHalfWidthMap.put('\u3063', "\uFF6F"); + sHalfWidthMap.put('\u3064', "\uFF82"); + sHalfWidthMap.put('\u3065', "\uFF82\uFF9E"); + sHalfWidthMap.put('\u3066', "\uFF83"); + sHalfWidthMap.put('\u3067', "\uFF83\uFF9E"); + sHalfWidthMap.put('\u3068', "\uFF84"); + sHalfWidthMap.put('\u3069', "\uFF84\uFF9E"); + sHalfWidthMap.put('\u306A', "\uFF85"); + sHalfWidthMap.put('\u306B', "\uFF86"); + sHalfWidthMap.put('\u306C', "\uFF87"); + sHalfWidthMap.put('\u306D', "\uFF88"); + sHalfWidthMap.put('\u306E', "\uFF89"); + sHalfWidthMap.put('\u306F', "\uFF8A"); + sHalfWidthMap.put('\u3070', "\uFF8A\uFF9E"); + sHalfWidthMap.put('\u3071', "\uFF8A\uFF9F"); + sHalfWidthMap.put('\u3072', "\uFF8B"); + sHalfWidthMap.put('\u3073', "\uFF8B\uFF9E"); + sHalfWidthMap.put('\u3074', "\uFF8B\uFF9F"); + sHalfWidthMap.put('\u3075', "\uFF8C"); + sHalfWidthMap.put('\u3076', "\uFF8C\uFF9E"); + sHalfWidthMap.put('\u3077', "\uFF8C\uFF9F"); + sHalfWidthMap.put('\u3078', "\uFF8D"); + sHalfWidthMap.put('\u3079', "\uFF8D\uFF9E"); + sHalfWidthMap.put('\u307A', "\uFF8D\uFF9F"); + sHalfWidthMap.put('\u307B', "\uFF8E"); + sHalfWidthMap.put('\u307C', "\uFF8E\uFF9E"); + sHalfWidthMap.put('\u307D', "\uFF8E\uFF9F"); + sHalfWidthMap.put('\u307E', "\uFF8F"); + sHalfWidthMap.put('\u307F', "\uFF90"); + sHalfWidthMap.put('\u3080', "\uFF91"); + sHalfWidthMap.put('\u3081', "\uFF92"); + sHalfWidthMap.put('\u3082', "\uFF93"); + sHalfWidthMap.put('\u3083', "\uFF6C"); + sHalfWidthMap.put('\u3084', "\uFF94"); + sHalfWidthMap.put('\u3085', "\uFF6D"); + sHalfWidthMap.put('\u3086', "\uFF95"); + sHalfWidthMap.put('\u3087', "\uFF6E"); + sHalfWidthMap.put('\u3088', "\uFF96"); + sHalfWidthMap.put('\u3089', "\uFF97"); + sHalfWidthMap.put('\u308A', "\uFF98"); + sHalfWidthMap.put('\u308B', "\uFF99"); + sHalfWidthMap.put('\u308C', "\uFF9A"); + sHalfWidthMap.put('\u308D', "\uFF9B"); + sHalfWidthMap.put('\u308E', "\uFF9C"); + sHalfWidthMap.put('\u308F', "\uFF9C"); + sHalfWidthMap.put('\u3090', "\uFF72"); + sHalfWidthMap.put('\u3091', "\uFF74"); + sHalfWidthMap.put('\u3092', "\uFF66"); + sHalfWidthMap.put('\u3093', "\uFF9D"); + sHalfWidthMap.put('\u309B', "\uFF9E"); + sHalfWidthMap.put('\u309C', "\uFF9F"); + sHalfWidthMap.put('\u30A1', "\uFF67"); + sHalfWidthMap.put('\u30A2', "\uFF71"); + sHalfWidthMap.put('\u30A3', "\uFF68"); + sHalfWidthMap.put('\u30A4', "\uFF72"); + sHalfWidthMap.put('\u30A5', "\uFF69"); + sHalfWidthMap.put('\u30A6', "\uFF73"); + sHalfWidthMap.put('\u30A7', "\uFF6A"); + sHalfWidthMap.put('\u30A8', "\uFF74"); + sHalfWidthMap.put('\u30A9', "\uFF6B"); + sHalfWidthMap.put('\u30AA', "\uFF75"); + sHalfWidthMap.put('\u30AB', "\uFF76"); + sHalfWidthMap.put('\u30AC', "\uFF76\uFF9E"); + sHalfWidthMap.put('\u30AD', "\uFF77"); + sHalfWidthMap.put('\u30AE', "\uFF77\uFF9E"); + sHalfWidthMap.put('\u30AF', "\uFF78"); + sHalfWidthMap.put('\u30B0', "\uFF78\uFF9E"); + sHalfWidthMap.put('\u30B1', "\uFF79"); + sHalfWidthMap.put('\u30B2', "\uFF79\uFF9E"); + sHalfWidthMap.put('\u30B3', "\uFF7A"); + sHalfWidthMap.put('\u30B4', "\uFF7A\uFF9E"); + sHalfWidthMap.put('\u30B5', "\uFF7B"); + sHalfWidthMap.put('\u30B6', "\uFF7B\uFF9E"); + sHalfWidthMap.put('\u30B7', "\uFF7C"); + sHalfWidthMap.put('\u30B8', "\uFF7C\uFF9E"); + sHalfWidthMap.put('\u30B9', "\uFF7D"); + sHalfWidthMap.put('\u30BA', "\uFF7D\uFF9E"); + sHalfWidthMap.put('\u30BB', "\uFF7E"); + sHalfWidthMap.put('\u30BC', "\uFF7E\uFF9E"); + sHalfWidthMap.put('\u30BD', "\uFF7F"); + sHalfWidthMap.put('\u30BE', "\uFF7F\uFF9E"); + sHalfWidthMap.put('\u30BF', "\uFF80"); + sHalfWidthMap.put('\u30C0', "\uFF80\uFF9E"); + sHalfWidthMap.put('\u30C1', "\uFF81"); + sHalfWidthMap.put('\u30C2', "\uFF81\uFF9E"); + sHalfWidthMap.put('\u30C3', "\uFF6F"); + sHalfWidthMap.put('\u30C4', "\uFF82"); + sHalfWidthMap.put('\u30C5', "\uFF82\uFF9E"); + sHalfWidthMap.put('\u30C6', "\uFF83"); + sHalfWidthMap.put('\u30C7', "\uFF83\uFF9E"); + sHalfWidthMap.put('\u30C8', "\uFF84"); + sHalfWidthMap.put('\u30C9', "\uFF84\uFF9E"); + sHalfWidthMap.put('\u30CA', "\uFF85"); + sHalfWidthMap.put('\u30CB', "\uFF86"); + sHalfWidthMap.put('\u30CC', "\uFF87"); + sHalfWidthMap.put('\u30CD', "\uFF88"); + sHalfWidthMap.put('\u30CE', "\uFF89"); + sHalfWidthMap.put('\u30CF', "\uFF8A"); + sHalfWidthMap.put('\u30D0', "\uFF8A\uFF9E"); + sHalfWidthMap.put('\u30D1', "\uFF8A\uFF9F"); + sHalfWidthMap.put('\u30D2', "\uFF8B"); + sHalfWidthMap.put('\u30D3', "\uFF8B\uFF9E"); + sHalfWidthMap.put('\u30D4', "\uFF8B\uFF9F"); + sHalfWidthMap.put('\u30D5', "\uFF8C"); + sHalfWidthMap.put('\u30D6', "\uFF8C\uFF9E"); + sHalfWidthMap.put('\u30D7', "\uFF8C\uFF9F"); + sHalfWidthMap.put('\u30D8', "\uFF8D"); + sHalfWidthMap.put('\u30D9', "\uFF8D\uFF9E"); + sHalfWidthMap.put('\u30DA', "\uFF8D\uFF9F"); + sHalfWidthMap.put('\u30DB', "\uFF8E"); + sHalfWidthMap.put('\u30DC', "\uFF8E\uFF9E"); + sHalfWidthMap.put('\u30DD', "\uFF8E\uFF9F"); + sHalfWidthMap.put('\u30DE', "\uFF8F"); + sHalfWidthMap.put('\u30DF', "\uFF90"); + sHalfWidthMap.put('\u30E0', "\uFF91"); + sHalfWidthMap.put('\u30E1', "\uFF92"); + sHalfWidthMap.put('\u30E2', "\uFF93"); + sHalfWidthMap.put('\u30E3', "\uFF6C"); + sHalfWidthMap.put('\u30E4', "\uFF94"); + sHalfWidthMap.put('\u30E5', "\uFF6D"); + sHalfWidthMap.put('\u30E6', "\uFF95"); + sHalfWidthMap.put('\u30E7', "\uFF6E"); + sHalfWidthMap.put('\u30E8', "\uFF96"); + sHalfWidthMap.put('\u30E9', "\uFF97"); + sHalfWidthMap.put('\u30EA', "\uFF98"); + sHalfWidthMap.put('\u30EB', "\uFF99"); + sHalfWidthMap.put('\u30EC', "\uFF9A"); + sHalfWidthMap.put('\u30ED', "\uFF9B"); + sHalfWidthMap.put('\u30EE', "\uFF9C"); + sHalfWidthMap.put('\u30EF', "\uFF9C"); + sHalfWidthMap.put('\u30F0', "\uFF72"); + sHalfWidthMap.put('\u30F1', "\uFF74"); + sHalfWidthMap.put('\u30F2', "\uFF66"); + sHalfWidthMap.put('\u30F3', "\uFF9D"); + sHalfWidthMap.put('\u30F4', "\uFF73\uFF9E"); + sHalfWidthMap.put('\u30F5', "\uFF76"); + sHalfWidthMap.put('\u30F6', "\uFF79"); + sHalfWidthMap.put('\u30FB', "\uFF65"); + sHalfWidthMap.put('\u30FC', "\uFF70"); + sHalfWidthMap.put('\uFF01', "!"); + sHalfWidthMap.put('\uFF02', "\""); + sHalfWidthMap.put('\uFF03', "#"); + sHalfWidthMap.put('\uFF04', "$"); + sHalfWidthMap.put('\uFF05', "%"); + sHalfWidthMap.put('\uFF06', "&"); + sHalfWidthMap.put('\uFF07', "'"); + sHalfWidthMap.put('\uFF08', "("); + sHalfWidthMap.put('\uFF09', ")"); + sHalfWidthMap.put('\uFF0A', "*"); + sHalfWidthMap.put('\uFF0B', "+"); + sHalfWidthMap.put('\uFF0C', ","); + sHalfWidthMap.put('\uFF0D', "-"); + sHalfWidthMap.put('\uFF0E', "."); + sHalfWidthMap.put('\uFF0F', "/"); + sHalfWidthMap.put('\uFF10', "0"); + sHalfWidthMap.put('\uFF11', "1"); + sHalfWidthMap.put('\uFF12', "2"); + sHalfWidthMap.put('\uFF13', "3"); + sHalfWidthMap.put('\uFF14', "4"); + sHalfWidthMap.put('\uFF15', "5"); + sHalfWidthMap.put('\uFF16', "6"); + sHalfWidthMap.put('\uFF17', "7"); + sHalfWidthMap.put('\uFF18', "8"); + sHalfWidthMap.put('\uFF19', "9"); + sHalfWidthMap.put('\uFF1A', ":"); + sHalfWidthMap.put('\uFF1B', ";"); + sHalfWidthMap.put('\uFF1C', "<"); + sHalfWidthMap.put('\uFF1D', "="); + sHalfWidthMap.put('\uFF1E', ">"); + sHalfWidthMap.put('\uFF1F', "?"); + sHalfWidthMap.put('\uFF20', "@"); + sHalfWidthMap.put('\uFF21', "A"); + sHalfWidthMap.put('\uFF22', "B"); + sHalfWidthMap.put('\uFF23', "C"); + sHalfWidthMap.put('\uFF24', "D"); + sHalfWidthMap.put('\uFF25', "E"); + sHalfWidthMap.put('\uFF26', "F"); + sHalfWidthMap.put('\uFF27', "G"); + sHalfWidthMap.put('\uFF28', "H"); + sHalfWidthMap.put('\uFF29', "I"); + sHalfWidthMap.put('\uFF2A', "J"); + sHalfWidthMap.put('\uFF2B', "K"); + sHalfWidthMap.put('\uFF2C', "L"); + sHalfWidthMap.put('\uFF2D', "M"); + sHalfWidthMap.put('\uFF2E', "N"); + sHalfWidthMap.put('\uFF2F', "O"); + sHalfWidthMap.put('\uFF30', "P"); + sHalfWidthMap.put('\uFF31', "Q"); + sHalfWidthMap.put('\uFF32', "R"); + sHalfWidthMap.put('\uFF33', "S"); + sHalfWidthMap.put('\uFF34', "T"); + sHalfWidthMap.put('\uFF35', "U"); + sHalfWidthMap.put('\uFF36', "V"); + sHalfWidthMap.put('\uFF37', "W"); + sHalfWidthMap.put('\uFF38', "X"); + sHalfWidthMap.put('\uFF39', "Y"); + sHalfWidthMap.put('\uFF3A', "Z"); + sHalfWidthMap.put('\uFF3B', "["); + sHalfWidthMap.put('\uFF3C', "\\"); + sHalfWidthMap.put('\uFF3D', "]"); + sHalfWidthMap.put('\uFF3E', "^"); + sHalfWidthMap.put('\uFF3F', "_"); + sHalfWidthMap.put('\uFF41', "a"); + sHalfWidthMap.put('\uFF42', "b"); + sHalfWidthMap.put('\uFF43', "c"); + sHalfWidthMap.put('\uFF44', "d"); + sHalfWidthMap.put('\uFF45', "e"); + sHalfWidthMap.put('\uFF46', "f"); + sHalfWidthMap.put('\uFF47', "g"); + sHalfWidthMap.put('\uFF48', "h"); + sHalfWidthMap.put('\uFF49', "i"); + sHalfWidthMap.put('\uFF4A', "j"); + sHalfWidthMap.put('\uFF4B', "k"); + sHalfWidthMap.put('\uFF4C', "l"); + sHalfWidthMap.put('\uFF4D', "m"); + sHalfWidthMap.put('\uFF4E', "n"); + sHalfWidthMap.put('\uFF4F', "o"); + sHalfWidthMap.put('\uFF50', "p"); + sHalfWidthMap.put('\uFF51', "q"); + sHalfWidthMap.put('\uFF52', "r"); + sHalfWidthMap.put('\uFF53', "s"); + sHalfWidthMap.put('\uFF54', "t"); + sHalfWidthMap.put('\uFF55', "u"); + sHalfWidthMap.put('\uFF56', "v"); + sHalfWidthMap.put('\uFF57', "w"); + sHalfWidthMap.put('\uFF58', "x"); + sHalfWidthMap.put('\uFF59', "y"); + sHalfWidthMap.put('\uFF5A', "z"); + sHalfWidthMap.put('\uFF5B', "{"); + sHalfWidthMap.put('\uFF5C', "|"); + sHalfWidthMap.put('\uFF5D', "}"); + sHalfWidthMap.put('\uFF5E', "~"); + sHalfWidthMap.put('\uFF61', "\uFF61"); + sHalfWidthMap.put('\uFF62', "\uFF62"); + sHalfWidthMap.put('\uFF63', "\uFF63"); + sHalfWidthMap.put('\uFF64', "\uFF64"); + sHalfWidthMap.put('\uFF65', "\uFF65"); + sHalfWidthMap.put('\uFF66', "\uFF66"); + sHalfWidthMap.put('\uFF67', "\uFF67"); + sHalfWidthMap.put('\uFF68', "\uFF68"); + sHalfWidthMap.put('\uFF69', "\uFF69"); + sHalfWidthMap.put('\uFF6A', "\uFF6A"); + sHalfWidthMap.put('\uFF6B', "\uFF6B"); + sHalfWidthMap.put('\uFF6C', "\uFF6C"); + sHalfWidthMap.put('\uFF6D', "\uFF6D"); + sHalfWidthMap.put('\uFF6E', "\uFF6E"); + sHalfWidthMap.put('\uFF6F', "\uFF6F"); + sHalfWidthMap.put('\uFF70', "\uFF70"); + sHalfWidthMap.put('\uFF71', "\uFF71"); + sHalfWidthMap.put('\uFF72', "\uFF72"); + sHalfWidthMap.put('\uFF73', "\uFF73"); + sHalfWidthMap.put('\uFF74', "\uFF74"); + sHalfWidthMap.put('\uFF75', "\uFF75"); + sHalfWidthMap.put('\uFF76', "\uFF76"); + sHalfWidthMap.put('\uFF77', "\uFF77"); + sHalfWidthMap.put('\uFF78', "\uFF78"); + sHalfWidthMap.put('\uFF79', "\uFF79"); + sHalfWidthMap.put('\uFF7A', "\uFF7A"); + sHalfWidthMap.put('\uFF7B', "\uFF7B"); + sHalfWidthMap.put('\uFF7C', "\uFF7C"); + sHalfWidthMap.put('\uFF7D', "\uFF7D"); + sHalfWidthMap.put('\uFF7E', "\uFF7E"); + sHalfWidthMap.put('\uFF7F', "\uFF7F"); + sHalfWidthMap.put('\uFF80', "\uFF80"); + sHalfWidthMap.put('\uFF81', "\uFF81"); + sHalfWidthMap.put('\uFF82', "\uFF82"); + sHalfWidthMap.put('\uFF83', "\uFF83"); + sHalfWidthMap.put('\uFF84', "\uFF84"); + sHalfWidthMap.put('\uFF85', "\uFF85"); + sHalfWidthMap.put('\uFF86', "\uFF86"); + sHalfWidthMap.put('\uFF87', "\uFF87"); + sHalfWidthMap.put('\uFF88', "\uFF88"); + sHalfWidthMap.put('\uFF89', "\uFF89"); + sHalfWidthMap.put('\uFF8A', "\uFF8A"); + sHalfWidthMap.put('\uFF8B', "\uFF8B"); + sHalfWidthMap.put('\uFF8C', "\uFF8C"); + sHalfWidthMap.put('\uFF8D', "\uFF8D"); + sHalfWidthMap.put('\uFF8E', "\uFF8E"); + sHalfWidthMap.put('\uFF8F', "\uFF8F"); + sHalfWidthMap.put('\uFF90', "\uFF90"); + sHalfWidthMap.put('\uFF91', "\uFF91"); + sHalfWidthMap.put('\uFF92', "\uFF92"); + sHalfWidthMap.put('\uFF93', "\uFF93"); + sHalfWidthMap.put('\uFF94', "\uFF94"); + sHalfWidthMap.put('\uFF95', "\uFF95"); + sHalfWidthMap.put('\uFF96', "\uFF96"); + sHalfWidthMap.put('\uFF97', "\uFF97"); + sHalfWidthMap.put('\uFF98', "\uFF98"); + sHalfWidthMap.put('\uFF99', "\uFF99"); + sHalfWidthMap.put('\uFF9A', "\uFF9A"); + sHalfWidthMap.put('\uFF9B', "\uFF9B"); + sHalfWidthMap.put('\uFF9C', "\uFF9C"); + sHalfWidthMap.put('\uFF9D', "\uFF9D"); + sHalfWidthMap.put('\uFF9E', "\uFF9E"); + sHalfWidthMap.put('\uFF9F', "\uFF9F"); + sHalfWidthMap.put('\uFFE5', "\u005C\u005C"); + } + + /** + * Return half-width version of that character if possible. Return null if not possible + * @param ch input character + * @return CharSequence object if the mapping for ch exists. Return null otherwise. + */ + public static String tryGetHalfWidthText(char ch) { + if (sHalfWidthMap.containsKey(ch)) { + return sHalfWidthMap.get(ch); + } else { + return null; + } + } +} diff --git a/core/java/android/pim/vcard/VCardBuilder.java b/core/java/android/pim/vcard/VCardBuilder.java index e1c4b33..09ac1fd 100644 --- a/core/java/android/pim/vcard/VCardBuilder.java +++ b/core/java/android/pim/vcard/VCardBuilder.java @@ -1,64 +1,1911 @@ /* * 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 + * 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 + * 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. + * 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.pim.vcard; +import android.content.ContentValues; +import android.provider.ContactsContract.CommonDataKinds.Email; +import android.provider.ContactsContract.CommonDataKinds.Event; +import android.provider.ContactsContract.CommonDataKinds.Im; +import android.provider.ContactsContract.CommonDataKinds.Nickname; +import android.provider.ContactsContract.CommonDataKinds.Note; +import android.provider.ContactsContract.CommonDataKinds.Organization; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.CommonDataKinds.Photo; +import android.provider.ContactsContract.CommonDataKinds.Relation; +import android.provider.ContactsContract.CommonDataKinds.StructuredName; +import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; +import android.provider.ContactsContract.CommonDataKinds.Website; +import android.telephony.PhoneNumberUtils; +import android.text.TextUtils; +import android.util.CharsetUtils; +import android.util.Log; + +import org.apache.commons.codec.binary.Base64; + +import java.io.UnsupportedEncodingException; +import java.nio.charset.UnsupportedCharsetException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * The class which lets users create their own vCard String. + */ +public class VCardBuilder { + private static final String LOG_TAG = "VCardBuilder"; + + // If you add the other element, please check all the columns are able to be + // converted to String. + // + // e.g. BLOB is not what we can handle here now. + private static final Set<String> sAllowedAndroidPropertySet = + Collections.unmodifiableSet(new HashSet<String>(Arrays.asList( + Nickname.CONTENT_ITEM_TYPE, Event.CONTENT_ITEM_TYPE, + Relation.CONTENT_ITEM_TYPE))); + + public static final int DEFAULT_PHONE_TYPE = Phone.TYPE_HOME; + public static final int DEFAULT_POSTAL_TYPE = StructuredPostal.TYPE_HOME; + public static final int DEFAULT_EMAIL_TYPE = Email.TYPE_OTHER; + + private static final String VCARD_DATA_VCARD = "VCARD"; + private static final String VCARD_DATA_PUBLIC = "PUBLIC"; + + private static final String VCARD_PARAM_SEPARATOR = ";"; + private static final String VCARD_END_OF_LINE = "\r\n"; + private static final String VCARD_DATA_SEPARATOR = ":"; + private static final String VCARD_ITEM_SEPARATOR = ";"; + private static final String VCARD_WS = " "; + private static final String VCARD_PARAM_EQUAL = "="; + + private static final String VCARD_PARAM_ENCODING_QP = "ENCODING=QUOTED-PRINTABLE"; + + private static final String VCARD_PARAM_ENCODING_BASE64_V21 = "ENCODING=BASE64"; + private static final String VCARD_PARAM_ENCODING_BASE64_V30 = "ENCODING=b"; + + private static final String SHIFT_JIS = "SHIFT_JIS"; + private static final String UTF_8 = "UTF-8"; + + private final int mVCardType; + + private final boolean mIsV30; + private final boolean mIsJapaneseMobilePhone; + private final boolean mOnlyOneNoteFieldIsAvailable; + private final boolean mIsDoCoMo; + private final boolean mShouldUseQuotedPrintable; + private final boolean mUsesAndroidProperty; + private final boolean mUsesDefactProperty; + private final boolean mUsesUtf8; + private final boolean mUsesShiftJis; + private final boolean mAppendTypeParamName; + private final boolean mRefrainsQPToNameProperties; + private final boolean mNeedsToConvertPhoneticString; + + private final boolean mShouldAppendCharsetParam; + + private final String mCharsetString; + private final String mVCardCharsetParameter; + + private StringBuilder mBuilder; + private boolean mEndAppended; + + public VCardBuilder(final int vcardType) { + mVCardType = vcardType; + + mIsV30 = VCardConfig.isV30(vcardType); + mShouldUseQuotedPrintable = VCardConfig.shouldUseQuotedPrintable(vcardType); + mIsDoCoMo = VCardConfig.isDoCoMo(vcardType); + mIsJapaneseMobilePhone = VCardConfig.needsToConvertPhoneticString(vcardType); + mOnlyOneNoteFieldIsAvailable = VCardConfig.onlyOneNoteFieldIsAvailable(vcardType); + mUsesAndroidProperty = VCardConfig.usesAndroidSpecificProperty(vcardType); + mUsesDefactProperty = VCardConfig.usesDefactProperty(vcardType); + mUsesUtf8 = VCardConfig.usesUtf8(vcardType); + mUsesShiftJis = VCardConfig.usesShiftJis(vcardType); + mRefrainsQPToNameProperties = VCardConfig.shouldRefrainQPToNameProperties(vcardType); + mAppendTypeParamName = VCardConfig.appendTypeParamName(vcardType); + mNeedsToConvertPhoneticString = VCardConfig.needsToConvertPhoneticString(vcardType); + + mShouldAppendCharsetParam = !(mIsV30 && mUsesUtf8); + + if (mIsDoCoMo) { + String charset; + try { + charset = CharsetUtils.charsetForVendor(SHIFT_JIS, "docomo").name(); + } catch (UnsupportedCharsetException e) { + Log.e(LOG_TAG, "DoCoMo-specific SHIFT_JIS was not found. Use SHIFT_JIS as is."); + charset = SHIFT_JIS; + } + mCharsetString = charset; + // Do not use mCharsetString bellow since it is different from "SHIFT_JIS" but + // may be "DOCOMO_SHIFT_JIS" or something like that (internal expression used in + // Android, not shown to the public). + mVCardCharsetParameter = "CHARSET=" + SHIFT_JIS; + } else if (mUsesShiftJis) { + String charset; + try { + charset = CharsetUtils.charsetForVendor(SHIFT_JIS).name(); + } catch (UnsupportedCharsetException e) { + Log.e(LOG_TAG, "Vendor-specific SHIFT_JIS was not found. Use SHIFT_JIS as is."); + charset = SHIFT_JIS; + } + mCharsetString = charset; + mVCardCharsetParameter = "CHARSET=" + SHIFT_JIS; + } else { + mCharsetString = UTF_8; + mVCardCharsetParameter = "CHARSET=" + UTF_8; + } + clear(); + } + + public void clear() { + mBuilder = new StringBuilder(); + mEndAppended = false; + appendLine(VCardConstants.PROPERTY_BEGIN, VCARD_DATA_VCARD); + if (mIsV30) { + appendLine(VCardConstants.PROPERTY_VERSION, VCardConstants.VERSION_V30); + } else { + appendLine(VCardConstants.PROPERTY_VERSION, VCardConstants.VERSION_V21); + } + } + + private boolean containsNonEmptyName(final ContentValues contentValues) { + final String familyName = contentValues.getAsString(StructuredName.FAMILY_NAME); + final String middleName = contentValues.getAsString(StructuredName.MIDDLE_NAME); + final String givenName = contentValues.getAsString(StructuredName.GIVEN_NAME); + final String prefix = contentValues.getAsString(StructuredName.PREFIX); + final String suffix = contentValues.getAsString(StructuredName.SUFFIX); + final String phoneticFamilyName = + contentValues.getAsString(StructuredName.PHONETIC_FAMILY_NAME); + final String phoneticMiddleName = + contentValues.getAsString(StructuredName.PHONETIC_MIDDLE_NAME); + final String phoneticGivenName = + contentValues.getAsString(StructuredName.PHONETIC_GIVEN_NAME); + final String displayName = contentValues.getAsString(StructuredName.DISPLAY_NAME); + return !(TextUtils.isEmpty(familyName) && TextUtils.isEmpty(middleName) && + TextUtils.isEmpty(givenName) && TextUtils.isEmpty(prefix) && + TextUtils.isEmpty(suffix) && TextUtils.isEmpty(phoneticFamilyName) && + TextUtils.isEmpty(phoneticMiddleName) && TextUtils.isEmpty(phoneticGivenName) && + TextUtils.isEmpty(displayName)); + } + + private ContentValues getPrimaryContentValue(final List<ContentValues> contentValuesList) { + ContentValues primaryContentValues = null; + ContentValues subprimaryContentValues = null; + for (ContentValues contentValues : contentValuesList) { + if (contentValues == null){ + continue; + } + Integer isSuperPrimary = contentValues.getAsInteger(StructuredName.IS_SUPER_PRIMARY); + if (isSuperPrimary != null && isSuperPrimary > 0) { + // We choose "super primary" ContentValues. + primaryContentValues = contentValues; + break; + } else if (primaryContentValues == null) { + // We choose the first "primary" ContentValues + // if "super primary" ContentValues does not exist. + final Integer isPrimary = contentValues.getAsInteger(StructuredName.IS_PRIMARY); + if (isPrimary != null && isPrimary > 0 && + containsNonEmptyName(contentValues)) { + primaryContentValues = contentValues; + // Do not break, since there may be ContentValues with "super primary" + // afterword. + } else if (subprimaryContentValues == null && + containsNonEmptyName(contentValues)) { + subprimaryContentValues = contentValues; + } + } + } + + if (primaryContentValues == null) { + if (subprimaryContentValues != null) { + // We choose the first ContentValues if any "primary" ContentValues does not exist. + primaryContentValues = subprimaryContentValues; + } else { + Log.e(LOG_TAG, "All ContentValues given from database is empty."); + primaryContentValues = new ContentValues(); + } + } + + return primaryContentValues; + } + + /** + * For safety, we'll emit just one value around StructuredName, as external importers + * may get confused with multiple "N", "FN", etc. properties, though it is valid in + * vCard spec. + */ + public VCardBuilder appendNameProperties(final List<ContentValues> contentValuesList) { + if (contentValuesList == null || contentValuesList.isEmpty()) { + if (mIsDoCoMo) { + appendLine(VCardConstants.PROPERTY_N, ""); + } else if (mIsV30) { + // vCard 3.0 requires "N" and "FN" properties. + appendLine(VCardConstants.PROPERTY_N, ""); + appendLine(VCardConstants.PROPERTY_FN, ""); + } + return this; + } + + final ContentValues contentValues = getPrimaryContentValue(contentValuesList); + final String familyName = contentValues.getAsString(StructuredName.FAMILY_NAME); + final String middleName = contentValues.getAsString(StructuredName.MIDDLE_NAME); + final String givenName = contentValues.getAsString(StructuredName.GIVEN_NAME); + final String prefix = contentValues.getAsString(StructuredName.PREFIX); + final String suffix = contentValues.getAsString(StructuredName.SUFFIX); + final String displayName = contentValues.getAsString(StructuredName.DISPLAY_NAME); + + if (!TextUtils.isEmpty(familyName) || !TextUtils.isEmpty(givenName)) { + final boolean reallyAppendCharsetParameterToName = + shouldAppendCharsetParam(familyName, givenName, middleName, prefix, suffix); + final boolean reallyUseQuotedPrintableToName = + (!mRefrainsQPToNameProperties && + !(VCardUtils.containsOnlyNonCrLfPrintableAscii(familyName) && + VCardUtils.containsOnlyNonCrLfPrintableAscii(givenName) && + VCardUtils.containsOnlyNonCrLfPrintableAscii(middleName) && + VCardUtils.containsOnlyNonCrLfPrintableAscii(prefix) && + VCardUtils.containsOnlyNonCrLfPrintableAscii(suffix))); + + final String formattedName; + if (!TextUtils.isEmpty(displayName)) { + formattedName = displayName; + } else { + formattedName = VCardUtils.constructNameFromElements( + VCardConfig.getNameOrderType(mVCardType), + familyName, middleName, givenName, prefix, suffix); + } + final boolean reallyAppendCharsetParameterToFN = + shouldAppendCharsetParam(formattedName); + final boolean reallyUseQuotedPrintableToFN = + !mRefrainsQPToNameProperties && + !VCardUtils.containsOnlyNonCrLfPrintableAscii(formattedName); + + final String encodedFamily; + final String encodedGiven; + final String encodedMiddle; + final String encodedPrefix; + final String encodedSuffix; + if (reallyUseQuotedPrintableToName) { + encodedFamily = encodeQuotedPrintable(familyName); + encodedGiven = encodeQuotedPrintable(givenName); + encodedMiddle = encodeQuotedPrintable(middleName); + encodedPrefix = encodeQuotedPrintable(prefix); + encodedSuffix = encodeQuotedPrintable(suffix); + } else { + encodedFamily = escapeCharacters(familyName); + encodedGiven = escapeCharacters(givenName); + encodedMiddle = escapeCharacters(middleName); + encodedPrefix = escapeCharacters(prefix); + encodedSuffix = escapeCharacters(suffix); + } + + final String encodedFormattedname = + (reallyUseQuotedPrintableToFN ? + encodeQuotedPrintable(formattedName) : escapeCharacters(formattedName)); + + mBuilder.append(VCardConstants.PROPERTY_N); + if (mIsDoCoMo) { + if (reallyAppendCharsetParameterToName) { + mBuilder.append(VCARD_PARAM_SEPARATOR); + mBuilder.append(mVCardCharsetParameter); + } + if (reallyUseQuotedPrintableToName) { + mBuilder.append(VCARD_PARAM_SEPARATOR); + mBuilder.append(VCARD_PARAM_ENCODING_QP); + } + mBuilder.append(VCARD_DATA_SEPARATOR); + // DoCoMo phones require that all the elements in the "family name" field. + mBuilder.append(formattedName); + mBuilder.append(VCARD_ITEM_SEPARATOR); + mBuilder.append(VCARD_ITEM_SEPARATOR); + mBuilder.append(VCARD_ITEM_SEPARATOR); + mBuilder.append(VCARD_ITEM_SEPARATOR); + } else { + if (reallyAppendCharsetParameterToName) { + mBuilder.append(VCARD_PARAM_SEPARATOR); + mBuilder.append(mVCardCharsetParameter); + } + if (reallyUseQuotedPrintableToName) { + mBuilder.append(VCARD_PARAM_SEPARATOR); + mBuilder.append(VCARD_PARAM_ENCODING_QP); + } + mBuilder.append(VCARD_DATA_SEPARATOR); + mBuilder.append(encodedFamily); + mBuilder.append(VCARD_ITEM_SEPARATOR); + mBuilder.append(encodedGiven); + mBuilder.append(VCARD_ITEM_SEPARATOR); + mBuilder.append(encodedMiddle); + mBuilder.append(VCARD_ITEM_SEPARATOR); + mBuilder.append(encodedPrefix); + mBuilder.append(VCARD_ITEM_SEPARATOR); + mBuilder.append(encodedSuffix); + } + mBuilder.append(VCARD_END_OF_LINE); + + // FN property + mBuilder.append(VCardConstants.PROPERTY_FN); + if (reallyAppendCharsetParameterToFN) { + mBuilder.append(VCARD_PARAM_SEPARATOR); + mBuilder.append(mVCardCharsetParameter); + } + if (reallyUseQuotedPrintableToFN) { + mBuilder.append(VCARD_PARAM_SEPARATOR); + mBuilder.append(VCARD_PARAM_ENCODING_QP); + } + mBuilder.append(VCARD_DATA_SEPARATOR); + mBuilder.append(encodedFormattedname); + mBuilder.append(VCARD_END_OF_LINE); + } else if (!TextUtils.isEmpty(displayName)) { + final boolean reallyUseQuotedPrintableToDisplayName = + (!mRefrainsQPToNameProperties && + !VCardUtils.containsOnlyNonCrLfPrintableAscii(displayName)); + final String encodedDisplayName = + reallyUseQuotedPrintableToDisplayName ? + encodeQuotedPrintable(displayName) : + escapeCharacters(displayName); + + mBuilder.append(VCardConstants.PROPERTY_N); + if (shouldAppendCharsetParam(displayName)) { + mBuilder.append(VCARD_PARAM_SEPARATOR); + mBuilder.append(mVCardCharsetParameter); + } + if (reallyUseQuotedPrintableToDisplayName) { + mBuilder.append(VCARD_PARAM_SEPARATOR); + mBuilder.append(VCARD_PARAM_ENCODING_QP); + } + mBuilder.append(VCARD_DATA_SEPARATOR); + mBuilder.append(encodedDisplayName); + mBuilder.append(VCARD_ITEM_SEPARATOR); + mBuilder.append(VCARD_ITEM_SEPARATOR); + mBuilder.append(VCARD_ITEM_SEPARATOR); + mBuilder.append(VCARD_ITEM_SEPARATOR); + mBuilder.append(VCARD_END_OF_LINE); + mBuilder.append(VCardConstants.PROPERTY_FN); + + // Note: "CHARSET" param is not allowed in vCard 3.0, but we may add it + // when it would be useful for external importers, assuming no external + // importer allows this vioration. + if (shouldAppendCharsetParam(displayName)) { + mBuilder.append(VCARD_PARAM_SEPARATOR); + mBuilder.append(mVCardCharsetParameter); + } + mBuilder.append(VCARD_DATA_SEPARATOR); + mBuilder.append(encodedDisplayName); + mBuilder.append(VCARD_END_OF_LINE); + } else if (mIsV30) { + // vCard 3.0 specification requires these fields. + appendLine(VCardConstants.PROPERTY_N, ""); + appendLine(VCardConstants.PROPERTY_FN, ""); + } else if (mIsDoCoMo) { + appendLine(VCardConstants.PROPERTY_N, ""); + } + + appendPhoneticNameFields(contentValues); + return this; + } + + private void appendPhoneticNameFields(final ContentValues contentValues) { + final String phoneticFamilyName; + final String phoneticMiddleName; + final String phoneticGivenName; + { + final String tmpPhoneticFamilyName = + contentValues.getAsString(StructuredName.PHONETIC_FAMILY_NAME); + final String tmpPhoneticMiddleName = + contentValues.getAsString(StructuredName.PHONETIC_MIDDLE_NAME); + final String tmpPhoneticGivenName = + contentValues.getAsString(StructuredName.PHONETIC_GIVEN_NAME); + if (mNeedsToConvertPhoneticString) { + phoneticFamilyName = VCardUtils.toHalfWidthString(tmpPhoneticFamilyName); + phoneticMiddleName = VCardUtils.toHalfWidthString(tmpPhoneticMiddleName); + phoneticGivenName = VCardUtils.toHalfWidthString(tmpPhoneticGivenName); + } else { + phoneticFamilyName = tmpPhoneticFamilyName; + phoneticMiddleName = tmpPhoneticMiddleName; + phoneticGivenName = tmpPhoneticGivenName; + } + } + + if (TextUtils.isEmpty(phoneticFamilyName) + && TextUtils.isEmpty(phoneticMiddleName) + && TextUtils.isEmpty(phoneticGivenName)) { + if (mIsDoCoMo) { + mBuilder.append(VCardConstants.PROPERTY_SOUND); + mBuilder.append(VCARD_PARAM_SEPARATOR); + mBuilder.append(VCardConstants.PARAM_TYPE_X_IRMC_N); + mBuilder.append(VCARD_DATA_SEPARATOR); + mBuilder.append(VCARD_ITEM_SEPARATOR); + mBuilder.append(VCARD_ITEM_SEPARATOR); + mBuilder.append(VCARD_ITEM_SEPARATOR); + mBuilder.append(VCARD_ITEM_SEPARATOR); + mBuilder.append(VCARD_END_OF_LINE); + } + return; + } + + // Try to emit the field(s) related to phonetic name. + if (mIsV30) { + final String sortString = VCardUtils + .constructNameFromElements(mVCardType, + phoneticFamilyName, phoneticMiddleName, phoneticGivenName); + mBuilder.append(VCardConstants.PROPERTY_SORT_STRING); + if (shouldAppendCharsetParam(sortString)) { + mBuilder.append(VCARD_PARAM_SEPARATOR); + mBuilder.append(mVCardCharsetParameter); + } + mBuilder.append(VCARD_DATA_SEPARATOR); + mBuilder.append(escapeCharacters(sortString)); + mBuilder.append(VCARD_END_OF_LINE); + } else if (mIsJapaneseMobilePhone) { + // Note: There is no appropriate property for expressing + // phonetic name in vCard 2.1, while there is in + // vCard 3.0 (SORT-STRING). + // We chose to use DoCoMo's way when the device is Japanese one + // since it is supported by + // a lot of Japanese mobile phones. This is "X-" property, so + // any parser hopefully would not get confused with this. + // + // Also, DoCoMo's specification requires vCard composer to use just the first + // column. + // i.e. + // o SOUND;X-IRMC-N:Miyakawa Daisuke;;;; + // x SOUND;X-IRMC-N:Miyakawa;Daisuke;;; + mBuilder.append(VCardConstants.PROPERTY_SOUND); + mBuilder.append(VCARD_PARAM_SEPARATOR); + mBuilder.append(VCardConstants.PARAM_TYPE_X_IRMC_N); + + boolean reallyUseQuotedPrintable = + (!mRefrainsQPToNameProperties + && !(VCardUtils.containsOnlyNonCrLfPrintableAscii( + phoneticFamilyName) + && VCardUtils.containsOnlyNonCrLfPrintableAscii( + phoneticMiddleName) + && VCardUtils.containsOnlyNonCrLfPrintableAscii( + phoneticGivenName))); + + final String encodedPhoneticFamilyName; + final String encodedPhoneticMiddleName; + final String encodedPhoneticGivenName; + if (reallyUseQuotedPrintable) { + encodedPhoneticFamilyName = encodeQuotedPrintable(phoneticFamilyName); + encodedPhoneticMiddleName = encodeQuotedPrintable(phoneticMiddleName); + encodedPhoneticGivenName = encodeQuotedPrintable(phoneticGivenName); + } else { + encodedPhoneticFamilyName = escapeCharacters(phoneticFamilyName); + encodedPhoneticMiddleName = escapeCharacters(phoneticMiddleName); + encodedPhoneticGivenName = escapeCharacters(phoneticGivenName); + } + + if (shouldAppendCharsetParam(encodedPhoneticFamilyName, + encodedPhoneticMiddleName, encodedPhoneticGivenName)) { + mBuilder.append(VCARD_PARAM_SEPARATOR); + mBuilder.append(mVCardCharsetParameter); + } + mBuilder.append(VCARD_DATA_SEPARATOR); + { + boolean first = true; + if (!TextUtils.isEmpty(encodedPhoneticFamilyName)) { + mBuilder.append(encodedPhoneticFamilyName); + first = false; + } + if (!TextUtils.isEmpty(encodedPhoneticMiddleName)) { + if (first) { + first = false; + } else { + mBuilder.append(' '); + } + mBuilder.append(encodedPhoneticMiddleName); + } + if (!TextUtils.isEmpty(encodedPhoneticGivenName)) { + if (!first) { + mBuilder.append(' '); + } + mBuilder.append(encodedPhoneticGivenName); + } + } + mBuilder.append(VCARD_ITEM_SEPARATOR); + mBuilder.append(VCARD_ITEM_SEPARATOR); + mBuilder.append(VCARD_ITEM_SEPARATOR); + mBuilder.append(VCARD_ITEM_SEPARATOR); + mBuilder.append(VCARD_END_OF_LINE); + } + + if (mUsesDefactProperty) { + if (!TextUtils.isEmpty(phoneticGivenName)) { + final boolean reallyUseQuotedPrintable = + (mShouldUseQuotedPrintable && + !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticGivenName)); + final String encodedPhoneticGivenName; + if (reallyUseQuotedPrintable) { + encodedPhoneticGivenName = encodeQuotedPrintable(phoneticGivenName); + } else { + encodedPhoneticGivenName = escapeCharacters(phoneticGivenName); + } + mBuilder.append(VCardConstants.PROPERTY_X_PHONETIC_FIRST_NAME); + if (shouldAppendCharsetParam(phoneticGivenName)) { + mBuilder.append(VCARD_PARAM_SEPARATOR); + mBuilder.append(mVCardCharsetParameter); + } + if (reallyUseQuotedPrintable) { + mBuilder.append(VCARD_PARAM_SEPARATOR); + mBuilder.append(VCARD_PARAM_ENCODING_QP); + } + mBuilder.append(VCARD_DATA_SEPARATOR); + mBuilder.append(encodedPhoneticGivenName); + mBuilder.append(VCARD_END_OF_LINE); + } + if (!TextUtils.isEmpty(phoneticMiddleName)) { + final boolean reallyUseQuotedPrintable = + (mShouldUseQuotedPrintable && + !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticMiddleName)); + final String encodedPhoneticMiddleName; + if (reallyUseQuotedPrintable) { + encodedPhoneticMiddleName = encodeQuotedPrintable(phoneticMiddleName); + } else { + encodedPhoneticMiddleName = escapeCharacters(phoneticMiddleName); + } + mBuilder.append(VCardConstants.PROPERTY_X_PHONETIC_MIDDLE_NAME); + if (shouldAppendCharsetParam(phoneticMiddleName)) { + mBuilder.append(VCARD_PARAM_SEPARATOR); + mBuilder.append(mVCardCharsetParameter); + } + if (reallyUseQuotedPrintable) { + mBuilder.append(VCARD_PARAM_SEPARATOR); + mBuilder.append(VCARD_PARAM_ENCODING_QP); + } + mBuilder.append(VCARD_DATA_SEPARATOR); + mBuilder.append(encodedPhoneticMiddleName); + mBuilder.append(VCARD_END_OF_LINE); + } + if (!TextUtils.isEmpty(phoneticFamilyName)) { + final boolean reallyUseQuotedPrintable = + (mShouldUseQuotedPrintable && + !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticFamilyName)); + final String encodedPhoneticFamilyName; + if (reallyUseQuotedPrintable) { + encodedPhoneticFamilyName = encodeQuotedPrintable(phoneticFamilyName); + } else { + encodedPhoneticFamilyName = escapeCharacters(phoneticFamilyName); + } + mBuilder.append(VCardConstants.PROPERTY_X_PHONETIC_LAST_NAME); + if (shouldAppendCharsetParam(phoneticFamilyName)) { + mBuilder.append(VCARD_PARAM_SEPARATOR); + mBuilder.append(mVCardCharsetParameter); + } + if (reallyUseQuotedPrintable) { + mBuilder.append(VCARD_PARAM_SEPARATOR); + mBuilder.append(VCARD_PARAM_ENCODING_QP); + } + mBuilder.append(VCARD_DATA_SEPARATOR); + mBuilder.append(encodedPhoneticFamilyName); + mBuilder.append(VCARD_END_OF_LINE); + } + } + } + + public VCardBuilder appendNickNames(final List<ContentValues> contentValuesList) { + final boolean useAndroidProperty; + if (mIsV30) { + useAndroidProperty = false; + } else if (mUsesAndroidProperty) { + useAndroidProperty = true; + } else { + // There's no way to add this field. + return this; + } + if (contentValuesList != null) { + for (ContentValues contentValues : contentValuesList) { + final String nickname = contentValues.getAsString(Nickname.NAME); + if (TextUtils.isEmpty(nickname)) { + continue; + } + if (useAndroidProperty) { + appendAndroidSpecificProperty(Nickname.CONTENT_ITEM_TYPE, contentValues); + } else { + appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_NICKNAME, nickname); + } + } + } + return this; + } + + public VCardBuilder appendPhones(final List<ContentValues> contentValuesList) { + boolean phoneLineExists = false; + if (contentValuesList != null) { + Set<String> phoneSet = new HashSet<String>(); + for (ContentValues contentValues : contentValuesList) { + final Integer typeAsObject = contentValues.getAsInteger(Phone.TYPE); + final String label = contentValues.getAsString(Phone.LABEL); + final Integer isPrimaryAsInteger = contentValues.getAsInteger(Phone.IS_PRIMARY); + final boolean isPrimary = (isPrimaryAsInteger != null ? + (isPrimaryAsInteger > 0) : false); + String phoneNumber = contentValues.getAsString(Phone.NUMBER); + if (phoneNumber != null) { + phoneNumber = phoneNumber.trim(); + } + if (TextUtils.isEmpty(phoneNumber)) { + continue; + } + int type = (typeAsObject != null ? typeAsObject : DEFAULT_PHONE_TYPE); + if (type == Phone.TYPE_PAGER) { + phoneLineExists = true; + if (!phoneSet.contains(phoneNumber)) { + phoneSet.add(phoneNumber); + appendTelLine(type, label, phoneNumber, isPrimary); + } + } else { + // The entry "may" have several phone numbers when the contact entry is + // corrupted because of its original source. + // + // e.g. I encountered the entry like the following. + // "111-222-3333 (Miami)\n444-555-6666 (Broward; 305-653-6796 (Miami); ..." + // This kind of entry is not able to be inserted via Android devices, but + // possible if the source of the data is already corrupted. + List<String> phoneNumberList = splitIfSeveralPhoneNumbersExist(phoneNumber); + if (phoneNumberList.isEmpty()) { + continue; + } + phoneLineExists = true; + for (String actualPhoneNumber : phoneNumberList) { + if (!phoneSet.contains(actualPhoneNumber)) { + final int format = VCardUtils.getPhoneNumberFormat(mVCardType); + final String formattedPhoneNumber = + PhoneNumberUtils.formatNumber(actualPhoneNumber, format); + phoneSet.add(actualPhoneNumber); + appendTelLine(type, label, formattedPhoneNumber, isPrimary); + } + } + } + } + } + + if (!phoneLineExists && mIsDoCoMo) { + appendTelLine(Phone.TYPE_HOME, "", "", false); + } + + return this; + } + + private List<String> splitIfSeveralPhoneNumbersExist(final String phoneNumber) { + List<String> phoneList = new ArrayList<String>(); + + StringBuilder builder = new StringBuilder(); + final int length = phoneNumber.length(); + for (int i = 0; i < length; i++) { + final char ch = phoneNumber.charAt(i); + if (Character.isDigit(ch)) { + builder.append(ch); + } else if ((ch == ';' || ch == '\n') && builder.length() > 0) { + phoneList.add(builder.toString()); + builder = new StringBuilder(); + } + } + if (builder.length() > 0) { + phoneList.add(builder.toString()); + } + + return phoneList; + } + + public VCardBuilder appendEmails(final List<ContentValues> contentValuesList) { + boolean emailAddressExists = false; + if (contentValuesList != null) { + final Set<String> addressSet = new HashSet<String>(); + for (ContentValues contentValues : contentValuesList) { + String emailAddress = contentValues.getAsString(Email.DATA); + if (emailAddress != null) { + emailAddress = emailAddress.trim(); + } + if (TextUtils.isEmpty(emailAddress)) { + continue; + } + Integer typeAsObject = contentValues.getAsInteger(Email.TYPE); + final int type = (typeAsObject != null ? + typeAsObject : DEFAULT_EMAIL_TYPE); + final String label = contentValues.getAsString(Email.LABEL); + Integer isPrimaryAsInteger = contentValues.getAsInteger(Email.IS_PRIMARY); + final boolean isPrimary = (isPrimaryAsInteger != null ? + (isPrimaryAsInteger > 0) : false); + emailAddressExists = true; + if (!addressSet.contains(emailAddress)) { + addressSet.add(emailAddress); + appendEmailLine(type, label, emailAddress, isPrimary); + } + } + } + + if (!emailAddressExists && mIsDoCoMo) { + appendEmailLine(Email.TYPE_HOME, "", "", false); + } + + return this; + } + + public VCardBuilder appendPostals(final List<ContentValues> contentValuesList) { + if (contentValuesList == null || contentValuesList.isEmpty()) { + if (mIsDoCoMo) { + mBuilder.append(VCardConstants.PROPERTY_ADR); + mBuilder.append(VCARD_PARAM_SEPARATOR); + mBuilder.append(VCardConstants.PARAM_TYPE_HOME); + mBuilder.append(VCARD_DATA_SEPARATOR); + mBuilder.append(VCARD_END_OF_LINE); + } + } else { + if (mIsDoCoMo) { + appendPostalsForDoCoMo(contentValuesList); + } else { + appendPostalsForGeneric(contentValuesList); + } + } + + return this; + } + + private static final Map<Integer, Integer> sPostalTypePriorityMap; + + static { + sPostalTypePriorityMap = new HashMap<Integer, Integer>(); + sPostalTypePriorityMap.put(StructuredPostal.TYPE_HOME, 0); + sPostalTypePriorityMap.put(StructuredPostal.TYPE_WORK, 1); + sPostalTypePriorityMap.put(StructuredPostal.TYPE_OTHER, 2); + sPostalTypePriorityMap.put(StructuredPostal.TYPE_CUSTOM, 3); + } + + /** + * Tries to append just one line. If there's no appropriate address + * information, append an empty line. + */ + private void appendPostalsForDoCoMo(final List<ContentValues> contentValuesList) { + int currentPriority = Integer.MAX_VALUE; + int currentType = Integer.MAX_VALUE; + ContentValues currentContentValues = null; + for (final ContentValues contentValues : contentValuesList) { + if (contentValues == null) { + continue; + } + final Integer typeAsInteger = contentValues.getAsInteger(StructuredPostal.TYPE); + final Integer priorityAsInteger = sPostalTypePriorityMap.get(typeAsInteger); + final int priority = + (priorityAsInteger != null ? priorityAsInteger : Integer.MAX_VALUE); + if (priority < currentPriority) { + currentPriority = priority; + currentType = typeAsInteger; + currentContentValues = contentValues; + if (priority == 0) { + break; + } + } + } + + if (currentContentValues == null) { + Log.w(LOG_TAG, "Should not come here. Must have at least one postal data."); + return; + } -public interface VCardBuilder { - void start(); + final String label = currentContentValues.getAsString(StructuredPostal.LABEL); + appendPostalLine(currentType, label, currentContentValues, false, true); + } - void end(); + private void appendPostalsForGeneric(final List<ContentValues> contentValuesList) { + for (final ContentValues contentValues : contentValuesList) { + if (contentValues == null) { + continue; + } + final Integer typeAsInteger = contentValues.getAsInteger(StructuredPostal.TYPE); + final int type = (typeAsInteger != null ? + typeAsInteger : DEFAULT_POSTAL_TYPE); + final String label = contentValues.getAsString(StructuredPostal.LABEL); + final Integer isPrimaryAsInteger = + contentValues.getAsInteger(StructuredPostal.IS_PRIMARY); + final boolean isPrimary = (isPrimaryAsInteger != null ? + (isPrimaryAsInteger > 0) : false); + appendPostalLine(type, label, contentValues, isPrimary, false); + } + } - /** - * BEGIN:VCARD + private static class PostalStruct { + final boolean reallyUseQuotedPrintable; + final boolean appendCharset; + final String addressData; + public PostalStruct(final boolean reallyUseQuotedPrintable, + final boolean appendCharset, final String addressData) { + this.reallyUseQuotedPrintable = reallyUseQuotedPrintable; + this.appendCharset = appendCharset; + this.addressData = addressData; + } + } + + /** + * @return null when there's no information available to construct the data. */ - void startRecord(String type); + private PostalStruct tryConstructPostalStruct(ContentValues contentValues) { + // adr-value = 0*6(text-value ";") text-value + // ; PO Box, Extended Address, Street, Locality, Region, Postal + // ; Code, Country Name + final String rawPoBox = contentValues.getAsString(StructuredPostal.POBOX); + final String rawNeighborhood = contentValues.getAsString(StructuredPostal.NEIGHBORHOOD); + final String rawStreet = contentValues.getAsString(StructuredPostal.STREET); + final String rawLocality = contentValues.getAsString(StructuredPostal.CITY); + final String rawRegion = contentValues.getAsString(StructuredPostal.REGION); + final String rawPostalCode = contentValues.getAsString(StructuredPostal.POSTCODE); + final String rawCountry = contentValues.getAsString(StructuredPostal.COUNTRY); + final String[] rawAddressArray = new String[]{ + rawPoBox, rawNeighborhood, rawStreet, rawLocality, + rawRegion, rawPostalCode, rawCountry}; + if (!VCardUtils.areAllEmpty(rawAddressArray)) { + final boolean reallyUseQuotedPrintable = + (mShouldUseQuotedPrintable && + !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawAddressArray)); + final boolean appendCharset = + !VCardUtils.containsOnlyPrintableAscii(rawAddressArray); + final String encodedPoBox; + final String encodedStreet; + final String encodedLocality; + final String encodedRegion; + final String encodedPostalCode; + final String encodedCountry; + final String encodedNeighborhood; + + final String rawLocality2; + // This looks inefficient since we encode rawLocality and rawNeighborhood twice, + // but this is intentional. + // + // QP encoding may add line feeds when needed and the result of + // - encodeQuotedPrintable(rawLocality + " " + rawNeighborhood) + // may be different from + // - encodedLocality + " " + encodedNeighborhood. + // + // We use safer way. + if (TextUtils.isEmpty(rawLocality)) { + if (TextUtils.isEmpty(rawNeighborhood)) { + rawLocality2 = ""; + } else { + rawLocality2 = rawNeighborhood; + } + } else { + if (TextUtils.isEmpty(rawNeighborhood)) { + rawLocality2 = rawLocality; + } else { + rawLocality2 = rawLocality + " " + rawNeighborhood; + } + } + if (reallyUseQuotedPrintable) { + encodedPoBox = encodeQuotedPrintable(rawPoBox); + encodedStreet = encodeQuotedPrintable(rawStreet); + encodedLocality = encodeQuotedPrintable(rawLocality2); + encodedRegion = encodeQuotedPrintable(rawRegion); + encodedPostalCode = encodeQuotedPrintable(rawPostalCode); + encodedCountry = encodeQuotedPrintable(rawCountry); + } else { + encodedPoBox = escapeCharacters(rawPoBox); + encodedStreet = escapeCharacters(rawStreet); + encodedLocality = escapeCharacters(rawLocality2); + encodedRegion = escapeCharacters(rawRegion); + encodedPostalCode = escapeCharacters(rawPostalCode); + encodedCountry = escapeCharacters(rawCountry); + encodedNeighborhood = escapeCharacters(rawNeighborhood); + } + final StringBuffer addressBuffer = new StringBuffer(); + addressBuffer.append(encodedPoBox); + addressBuffer.append(VCARD_ITEM_SEPARATOR); + addressBuffer.append(VCARD_ITEM_SEPARATOR); + addressBuffer.append(encodedStreet); + addressBuffer.append(VCARD_ITEM_SEPARATOR); + addressBuffer.append(encodedLocality); + addressBuffer.append(VCARD_ITEM_SEPARATOR); + addressBuffer.append(encodedRegion); + addressBuffer.append(VCARD_ITEM_SEPARATOR); + addressBuffer.append(encodedPostalCode); + addressBuffer.append(VCARD_ITEM_SEPARATOR); + addressBuffer.append(encodedCountry); + return new PostalStruct( + reallyUseQuotedPrintable, appendCharset, addressBuffer.toString()); + } else { // VCardUtils.areAllEmpty(rawAddressArray) == true + // Try to use FORMATTED_ADDRESS instead. + final String rawFormattedAddress = + contentValues.getAsString(StructuredPostal.FORMATTED_ADDRESS); + if (TextUtils.isEmpty(rawFormattedAddress)) { + return null; + } + final boolean reallyUseQuotedPrintable = + (mShouldUseQuotedPrintable && + !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawFormattedAddress)); + final boolean appendCharset = + !VCardUtils.containsOnlyPrintableAscii(rawFormattedAddress); + final String encodedFormattedAddress; + if (reallyUseQuotedPrintable) { + encodedFormattedAddress = encodeQuotedPrintable(rawFormattedAddress); + } else { + encodedFormattedAddress = escapeCharacters(rawFormattedAddress); + } + + // We use the second value ("Extended Address") just because Japanese mobile phones + // do so. If the other importer expects the value be in the other field, some flag may + // be needed. + final StringBuffer addressBuffer = new StringBuffer(); + addressBuffer.append(VCARD_ITEM_SEPARATOR); + addressBuffer.append(encodedFormattedAddress); + addressBuffer.append(VCARD_ITEM_SEPARATOR); + addressBuffer.append(VCARD_ITEM_SEPARATOR); + addressBuffer.append(VCARD_ITEM_SEPARATOR); + addressBuffer.append(VCARD_ITEM_SEPARATOR); + addressBuffer.append(VCARD_ITEM_SEPARATOR); + return new PostalStruct( + reallyUseQuotedPrintable, appendCharset, addressBuffer.toString()); + } + } + + public VCardBuilder appendIms(final List<ContentValues> contentValuesList) { + if (contentValuesList != null) { + for (ContentValues contentValues : contentValuesList) { + final Integer protocolAsObject = contentValues.getAsInteger(Im.PROTOCOL); + if (protocolAsObject == null) { + continue; + } + final String propertyName = VCardUtils.getPropertyNameForIm(protocolAsObject); + if (propertyName == null) { + continue; + } + String data = contentValues.getAsString(Im.DATA); + if (data != null) { + data = data.trim(); + } + if (TextUtils.isEmpty(data)) { + continue; + } + final String typeAsString; + { + final Integer typeAsInteger = contentValues.getAsInteger(Im.TYPE); + switch (typeAsInteger != null ? typeAsInteger : Im.TYPE_OTHER) { + case Im.TYPE_HOME: { + typeAsString = VCardConstants.PARAM_TYPE_HOME; + break; + } + case Im.TYPE_WORK: { + typeAsString = VCardConstants.PARAM_TYPE_WORK; + break; + } + case Im.TYPE_CUSTOM: { + final String label = contentValues.getAsString(Im.LABEL); + typeAsString = (label != null ? "X-" + label : null); + break; + } + case Im.TYPE_OTHER: // Ignore + default: { + typeAsString = null; + break; + } + } + } + + final List<String> parameterList = new ArrayList<String>(); + if (!TextUtils.isEmpty(typeAsString)) { + parameterList.add(typeAsString); + } + final Integer isPrimaryAsInteger = contentValues.getAsInteger(Im.IS_PRIMARY); + final boolean isPrimary = (isPrimaryAsInteger != null ? + (isPrimaryAsInteger > 0) : false); + if (isPrimary) { + parameterList.add(VCardConstants.PARAM_TYPE_PREF); + } + + appendLineWithCharsetAndQPDetection(propertyName, parameterList, data); + } + } + return this; + } + + public VCardBuilder appendWebsites(final List<ContentValues> contentValuesList) { + if (contentValuesList != null) { + for (ContentValues contentValues : contentValuesList) { + String website = contentValues.getAsString(Website.URL); + if (website != null) { + website = website.trim(); + } + + // Note: vCard 3.0 does not allow any parameter addition toward "URL" + // property, while there's no document in vCard 2.1. + if (!TextUtils.isEmpty(website)) { + appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_URL, website); + } + } + } + return this; + } + + public VCardBuilder appendOrganizations(final List<ContentValues> contentValuesList) { + if (contentValuesList != null) { + for (ContentValues contentValues : contentValuesList) { + String company = contentValues.getAsString(Organization.COMPANY); + if (company != null) { + company = company.trim(); + } + String department = contentValues.getAsString(Organization.DEPARTMENT); + if (department != null) { + department = department.trim(); + } + String title = contentValues.getAsString(Organization.TITLE); + if (title != null) { + title = title.trim(); + } + + StringBuilder orgBuilder = new StringBuilder(); + if (!TextUtils.isEmpty(company)) { + orgBuilder.append(company); + } + if (!TextUtils.isEmpty(department)) { + if (orgBuilder.length() > 0) { + orgBuilder.append(';'); + } + orgBuilder.append(department); + } + final String orgline = orgBuilder.toString(); + appendLine(VCardConstants.PROPERTY_ORG, orgline, + !VCardUtils.containsOnlyPrintableAscii(orgline), + (mShouldUseQuotedPrintable && + !VCardUtils.containsOnlyNonCrLfPrintableAscii(orgline))); + + if (!TextUtils.isEmpty(title)) { + appendLine(VCardConstants.PROPERTY_TITLE, title, + !VCardUtils.containsOnlyPrintableAscii(title), + (mShouldUseQuotedPrintable && + !VCardUtils.containsOnlyNonCrLfPrintableAscii(title))); + } + } + } + return this; + } + + public VCardBuilder appendPhotos(final List<ContentValues> contentValuesList) { + if (contentValuesList != null) { + for (ContentValues contentValues : contentValuesList) { + if (contentValues == null) { + continue; + } + byte[] data = contentValues.getAsByteArray(Photo.PHOTO); + if (data == null) { + continue; + } + final String photoType = VCardUtils.guessImageType(data); + if (photoType == null) { + Log.d(LOG_TAG, "Unknown photo type. Ignored."); + continue; + } + final String photoString = new String(Base64.encodeBase64(data)); + if (!TextUtils.isEmpty(photoString)) { + appendPhotoLine(photoString, photoType); + } + } + } + return this; + } + + public VCardBuilder appendNotes(final List<ContentValues> contentValuesList) { + if (contentValuesList != null) { + if (mOnlyOneNoteFieldIsAvailable) { + final StringBuilder noteBuilder = new StringBuilder(); + boolean first = true; + for (final ContentValues contentValues : contentValuesList) { + String note = contentValues.getAsString(Note.NOTE); + if (note == null) { + note = ""; + } + if (note.length() > 0) { + if (first) { + first = false; + } else { + noteBuilder.append('\n'); + } + noteBuilder.append(note); + } + } + final String noteStr = noteBuilder.toString(); + // This means we scan noteStr completely twice, which is redundant. + // But for now, we assume this is not so time-consuming.. + final boolean shouldAppendCharsetInfo = + !VCardUtils.containsOnlyPrintableAscii(noteStr); + final boolean reallyUseQuotedPrintable = + (mShouldUseQuotedPrintable && + !VCardUtils.containsOnlyNonCrLfPrintableAscii(noteStr)); + appendLine(VCardConstants.PROPERTY_NOTE, noteStr, + shouldAppendCharsetInfo, reallyUseQuotedPrintable); + } else { + for (ContentValues contentValues : contentValuesList) { + final String noteStr = contentValues.getAsString(Note.NOTE); + if (!TextUtils.isEmpty(noteStr)) { + final boolean shouldAppendCharsetInfo = + !VCardUtils.containsOnlyPrintableAscii(noteStr); + final boolean reallyUseQuotedPrintable = + (mShouldUseQuotedPrintable && + !VCardUtils.containsOnlyNonCrLfPrintableAscii(noteStr)); + appendLine(VCardConstants.PROPERTY_NOTE, noteStr, + shouldAppendCharsetInfo, reallyUseQuotedPrintable); + } + } + } + } + return this; + } - /** END:VXX */ - void endRecord(); + public VCardBuilder appendEvents(final List<ContentValues> contentValuesList) { + if (contentValuesList != null) { + String primaryBirthday = null; + String secondaryBirthday = null; + for (final ContentValues contentValues : contentValuesList) { + if (contentValues == null) { + continue; + } + final Integer eventTypeAsInteger = contentValues.getAsInteger(Event.TYPE); + final int eventType; + if (eventTypeAsInteger != null) { + eventType = eventTypeAsInteger; + } else { + eventType = Event.TYPE_OTHER; + } + if (eventType == Event.TYPE_BIRTHDAY) { + final String birthdayCandidate = contentValues.getAsString(Event.START_DATE); + if (birthdayCandidate == null) { + continue; + } + final Integer isSuperPrimaryAsInteger = + contentValues.getAsInteger(Event.IS_SUPER_PRIMARY); + final boolean isSuperPrimary = (isSuperPrimaryAsInteger != null ? + (isSuperPrimaryAsInteger > 0) : false); + if (isSuperPrimary) { + // "super primary" birthday should the prefered one. + primaryBirthday = birthdayCandidate; + break; + } + final Integer isPrimaryAsInteger = + contentValues.getAsInteger(Event.IS_PRIMARY); + final boolean isPrimary = (isPrimaryAsInteger != null ? + (isPrimaryAsInteger > 0) : false); + if (isPrimary) { + // We don't break here since "super primary" birthday may exist later. + primaryBirthday = birthdayCandidate; + } else if (secondaryBirthday == null) { + // First entry is set to the "secondary" candidate. + secondaryBirthday = birthdayCandidate; + } + } else if (mUsesAndroidProperty) { + // Event types other than Birthday is not supported by vCard. + appendAndroidSpecificProperty(Event.CONTENT_ITEM_TYPE, contentValues); + } + } + if (primaryBirthday != null) { + appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_BDAY, + primaryBirthday.trim()); + } else if (secondaryBirthday != null){ + appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_BDAY, + secondaryBirthday.trim()); + } + } + return this; + } - void startProperty(); + public VCardBuilder appendRelation(final List<ContentValues> contentValuesList) { + if (mUsesAndroidProperty && contentValuesList != null) { + for (final ContentValues contentValues : contentValuesList) { + if (contentValues == null) { + continue; + } + appendAndroidSpecificProperty(Relation.CONTENT_ITEM_TYPE, contentValues); + } + } + return this; + } - void endProperty(); + public void appendPostalLine(final int type, final String label, + final ContentValues contentValues, + final boolean isPrimary, final boolean emitLineEveryTime) { + final boolean reallyUseQuotedPrintable; + final boolean appendCharset; + final String addressValue; + { + PostalStruct postalStruct = tryConstructPostalStruct(contentValues); + if (postalStruct == null) { + if (emitLineEveryTime) { + reallyUseQuotedPrintable = false; + appendCharset = false; + addressValue = ""; + } else { + return; + } + } else { + reallyUseQuotedPrintable = postalStruct.reallyUseQuotedPrintable; + appendCharset = postalStruct.appendCharset; + addressValue = postalStruct.addressData; + } + } + + List<String> parameterList = new ArrayList<String>(); + if (isPrimary) { + parameterList.add(VCardConstants.PARAM_TYPE_PREF); + } + switch (type) { + case StructuredPostal.TYPE_HOME: { + parameterList.add(VCardConstants.PARAM_TYPE_HOME); + break; + } + case StructuredPostal.TYPE_WORK: { + parameterList.add(VCardConstants.PARAM_TYPE_WORK); + break; + } + case StructuredPostal.TYPE_CUSTOM: { + if (!TextUtils.isEmpty(label) + && VCardUtils.containsOnlyAlphaDigitHyphen(label)) { + // We're not sure whether the label is valid in the spec + // ("IANA-token" in the vCard 3.0 is unclear...) + // Just for safety, we add "X-" at the beggining of each label. + // Also checks the label obeys with vCard 3.0 spec. + parameterList.add("X-" + label); + } + break; + } + case StructuredPostal.TYPE_OTHER: { + break; + } + default: { + Log.e(LOG_TAG, "Unknown StructuredPostal type: " + type); + break; + } + } + + mBuilder.append(VCardConstants.PROPERTY_ADR); + if (!parameterList.isEmpty()) { + mBuilder.append(VCARD_PARAM_SEPARATOR); + appendTypeParameters(parameterList); + } + if (appendCharset) { + // Strictly, vCard 3.0 does not allow exporters to emit charset information, + // but we will add it since the information should be useful for importers, + // + // Assume no parser does not emit error with this parameter in vCard 3.0. + mBuilder.append(VCARD_PARAM_SEPARATOR); + mBuilder.append(mVCardCharsetParameter); + } + if (reallyUseQuotedPrintable) { + mBuilder.append(VCARD_PARAM_SEPARATOR); + mBuilder.append(VCARD_PARAM_ENCODING_QP); + } + mBuilder.append(VCARD_DATA_SEPARATOR); + mBuilder.append(addressValue); + mBuilder.append(VCARD_END_OF_LINE); + } + + public void appendEmailLine(final int type, final String label, + final String rawValue, final boolean isPrimary) { + final String typeAsString; + switch (type) { + case Email.TYPE_CUSTOM: { + if (VCardUtils.isMobilePhoneLabel(label)) { + typeAsString = VCardConstants.PARAM_TYPE_CELL; + } else if (!TextUtils.isEmpty(label) + && VCardUtils.containsOnlyAlphaDigitHyphen(label)) { + typeAsString = "X-" + label; + } else { + typeAsString = null; + } + break; + } + case Email.TYPE_HOME: { + typeAsString = VCardConstants.PARAM_TYPE_HOME; + break; + } + case Email.TYPE_WORK: { + typeAsString = VCardConstants.PARAM_TYPE_WORK; + break; + } + case Email.TYPE_OTHER: { + typeAsString = null; + break; + } + case Email.TYPE_MOBILE: { + typeAsString = VCardConstants.PARAM_TYPE_CELL; + break; + } + default: { + Log.e(LOG_TAG, "Unknown Email type: " + type); + typeAsString = null; + break; + } + } + + final List<String> parameterList = new ArrayList<String>(); + if (isPrimary) { + parameterList.add(VCardConstants.PARAM_TYPE_PREF); + } + if (!TextUtils.isEmpty(typeAsString)) { + parameterList.add(typeAsString); + } + + appendLineWithCharsetAndQPDetection(VCardConstants.PROPERTY_EMAIL, parameterList, + rawValue); + } + + public void appendTelLine(final Integer typeAsInteger, final String label, + final String encodedValue, boolean isPrimary) { + mBuilder.append(VCardConstants.PROPERTY_TEL); + mBuilder.append(VCARD_PARAM_SEPARATOR); + + final int type; + if (typeAsInteger == null) { + type = Phone.TYPE_OTHER; + } else { + type = typeAsInteger; + } + + ArrayList<String> parameterList = new ArrayList<String>(); + switch (type) { + case Phone.TYPE_HOME: { + parameterList.addAll( + Arrays.asList(VCardConstants.PARAM_TYPE_HOME)); + break; + } + case Phone.TYPE_WORK: { + parameterList.addAll( + Arrays.asList(VCardConstants.PARAM_TYPE_WORK)); + break; + } + case Phone.TYPE_FAX_HOME: { + parameterList.addAll( + Arrays.asList(VCardConstants.PARAM_TYPE_HOME, VCardConstants.PARAM_TYPE_FAX)); + break; + } + case Phone.TYPE_FAX_WORK: { + parameterList.addAll( + Arrays.asList(VCardConstants.PARAM_TYPE_WORK, VCardConstants.PARAM_TYPE_FAX)); + break; + } + case Phone.TYPE_MOBILE: { + parameterList.add(VCardConstants.PARAM_TYPE_CELL); + break; + } + case Phone.TYPE_PAGER: { + if (mIsDoCoMo) { + // Not sure about the reason, but previous implementation had + // used "VOICE" instead of "PAGER" + parameterList.add(VCardConstants.PARAM_TYPE_VOICE); + } else { + parameterList.add(VCardConstants.PARAM_TYPE_PAGER); + } + break; + } + case Phone.TYPE_OTHER: { + parameterList.add(VCardConstants.PARAM_TYPE_VOICE); + break; + } + case Phone.TYPE_CAR: { + parameterList.add(VCardConstants.PARAM_TYPE_CAR); + break; + } + case Phone.TYPE_COMPANY_MAIN: { + // There's no relevant field in vCard (at least 2.1). + parameterList.add(VCardConstants.PARAM_TYPE_WORK); + isPrimary = true; + break; + } + case Phone.TYPE_ISDN: { + parameterList.add(VCardConstants.PARAM_TYPE_ISDN); + break; + } + case Phone.TYPE_MAIN: { + isPrimary = true; + break; + } + case Phone.TYPE_OTHER_FAX: { + parameterList.add(VCardConstants.PARAM_TYPE_FAX); + break; + } + case Phone.TYPE_TELEX: { + parameterList.add(VCardConstants.PARAM_TYPE_TLX); + break; + } + case Phone.TYPE_WORK_MOBILE: { + parameterList.addAll( + Arrays.asList(VCardConstants.PARAM_TYPE_WORK, VCardConstants.PARAM_TYPE_CELL)); + break; + } + case Phone.TYPE_WORK_PAGER: { + parameterList.add(VCardConstants.PARAM_TYPE_WORK); + // See above. + if (mIsDoCoMo) { + parameterList.add(VCardConstants.PARAM_TYPE_VOICE); + } else { + parameterList.add(VCardConstants.PARAM_TYPE_PAGER); + } + break; + } + case Phone.TYPE_MMS: { + parameterList.add(VCardConstants.PARAM_TYPE_MSG); + break; + } + case Phone.TYPE_CUSTOM: { + if (TextUtils.isEmpty(label)) { + // Just ignore the custom type. + parameterList.add(VCardConstants.PARAM_TYPE_VOICE); + } else if (VCardUtils.isMobilePhoneLabel(label)) { + parameterList.add(VCardConstants.PARAM_TYPE_CELL); + } else { + final String upperLabel = label.toUpperCase(); + if (VCardUtils.isValidInV21ButUnknownToContactsPhoteType(upperLabel)) { + parameterList.add(upperLabel); + } else if (VCardUtils.containsOnlyAlphaDigitHyphen(label)) { + // Note: Strictly, vCard 2.1 does not allow "X-" parameter without + // "TYPE=" string. + parameterList.add("X-" + label); + } + } + break; + } + case Phone.TYPE_RADIO: + case Phone.TYPE_TTY_TDD: + default: { + break; + } + } + + if (isPrimary) { + parameterList.add(VCardConstants.PARAM_TYPE_PREF); + } + + if (parameterList.isEmpty()) { + appendUncommonPhoneType(mBuilder, type); + } else { + appendTypeParameters(parameterList); + } + + mBuilder.append(VCARD_DATA_SEPARATOR); + mBuilder.append(encodedValue); + mBuilder.append(VCARD_END_OF_LINE); + } /** - * @param group + * Appends phone type string which may not be available in some devices. */ - void propertyGroup(String group); - + private void appendUncommonPhoneType(final StringBuilder builder, final Integer type) { + if (mIsDoCoMo) { + // The previous implementation for DoCoMo had been conservative + // about miscellaneous types. + builder.append(VCardConstants.PARAM_TYPE_VOICE); + } else { + String phoneType = VCardUtils.getPhoneTypeString(type); + if (phoneType != null) { + appendTypeParameter(phoneType); + } else { + Log.e(LOG_TAG, "Unknown or unsupported (by vCard) Phone type: " + type); + } + } + } + /** - * @param name - * N <br> - * N + * @param encodedValue Must be encoded by BASE64 + * @param photoType */ - void propertyName(String name); + public void appendPhotoLine(final String encodedValue, final String photoType) { + StringBuilder tmpBuilder = new StringBuilder(); + tmpBuilder.append(VCardConstants.PROPERTY_PHOTO); + tmpBuilder.append(VCARD_PARAM_SEPARATOR); + if (mIsV30) { + tmpBuilder.append(VCARD_PARAM_ENCODING_BASE64_V30); + } else { + tmpBuilder.append(VCARD_PARAM_ENCODING_BASE64_V21); + } + tmpBuilder.append(VCARD_PARAM_SEPARATOR); + appendTypeParameter(tmpBuilder, photoType); + tmpBuilder.append(VCARD_DATA_SEPARATOR); + tmpBuilder.append(encodedValue); + + final String tmpStr = tmpBuilder.toString(); + tmpBuilder = new StringBuilder(); + int lineCount = 0; + final int length = tmpStr.length(); + final int maxNumForFirstLine = VCardConstants.MAX_CHARACTER_NUMS_BASE64_V30 + - VCARD_END_OF_LINE.length(); + final int maxNumInGeneral = maxNumForFirstLine - VCARD_WS.length(); + int maxNum = maxNumForFirstLine; + for (int i = 0; i < length; i++) { + tmpBuilder.append(tmpStr.charAt(i)); + lineCount++; + if (lineCount > maxNum) { + tmpBuilder.append(VCARD_END_OF_LINE); + tmpBuilder.append(VCARD_WS); + maxNum = maxNumInGeneral; + lineCount = 0; + } + } + mBuilder.append(tmpBuilder.toString()); + mBuilder.append(VCARD_END_OF_LINE); + mBuilder.append(VCARD_END_OF_LINE); + } + + public void appendAndroidSpecificProperty(final String mimeType, ContentValues contentValues) { + if (!sAllowedAndroidPropertySet.contains(mimeType)) { + return; + } + final List<String> rawValueList = new ArrayList<String>(); + for (int i = 1; i <= VCardConstants.MAX_DATA_COLUMN; i++) { + String value = contentValues.getAsString("data" + i); + if (value == null) { + value = ""; + } + rawValueList.add(value); + } + + boolean needCharset = + (mShouldAppendCharsetParam && + !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValueList)); + boolean reallyUseQuotedPrintable = + (mShouldUseQuotedPrintable && + !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValueList)); + mBuilder.append(VCardConstants.PROPERTY_X_ANDROID_CUSTOM); + if (needCharset) { + mBuilder.append(VCARD_PARAM_SEPARATOR); + mBuilder.append(mVCardCharsetParameter); + } + if (reallyUseQuotedPrintable) { + mBuilder.append(VCARD_PARAM_SEPARATOR); + mBuilder.append(VCARD_PARAM_ENCODING_QP); + } + mBuilder.append(VCARD_DATA_SEPARATOR); + mBuilder.append(mimeType); // Should not be encoded. + for (String rawValue : rawValueList) { + final String encodedValue; + if (reallyUseQuotedPrintable) { + encodedValue = encodeQuotedPrintable(rawValue); + } else { + // TODO: one line may be too huge, which may be invalid in vCard 3.0 + // (which says "When generating a content line, lines longer than + // 75 characters SHOULD be folded"), though several + // (even well-known) applications do not care this. + encodedValue = escapeCharacters(rawValue); + } + mBuilder.append(VCARD_ITEM_SEPARATOR); + mBuilder.append(encodedValue); + } + mBuilder.append(VCARD_END_OF_LINE); + } + + public void appendLineWithCharsetAndQPDetection(final String propertyName, + final String rawValue) { + appendLineWithCharsetAndQPDetection(propertyName, null, rawValue); + } + + public void appendLineWithCharsetAndQPDetection( + final String propertyName, final List<String> rawValueList) { + appendLineWithCharsetAndQPDetection(propertyName, null, rawValueList); + } + + public void appendLineWithCharsetAndQPDetection(final String propertyName, + final List<String> parameterList, final String rawValue) { + final boolean needCharset = + !VCardUtils.containsOnlyPrintableAscii(rawValue); + final boolean reallyUseQuotedPrintable = + (mShouldUseQuotedPrintable && + !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValue)); + appendLine(propertyName, parameterList, + rawValue, needCharset, reallyUseQuotedPrintable); + } + + public void appendLineWithCharsetAndQPDetection(final String propertyName, + final List<String> parameterList, final List<String> rawValueList) { + boolean needCharset = + (mShouldAppendCharsetParam && + !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValueList)); + boolean reallyUseQuotedPrintable = + (mShouldUseQuotedPrintable && + !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawValueList)); + appendLine(propertyName, parameterList, rawValueList, + needCharset, reallyUseQuotedPrintable); + } + + /** + * Appends one line with a given property name and value. + */ + public void appendLine(final String propertyName, final String rawValue) { + appendLine(propertyName, rawValue, false, false); + } + + public void appendLine(final String propertyName, final List<String> rawValueList) { + appendLine(propertyName, rawValueList, false, false); + } + + public void appendLine(final String propertyName, + final String rawValue, final boolean needCharset, + boolean reallyUseQuotedPrintable) { + appendLine(propertyName, null, rawValue, needCharset, reallyUseQuotedPrintable); + } + + public void appendLine(final String propertyName, final List<String> parameterList, + final String rawValue) { + appendLine(propertyName, parameterList, rawValue, false, false); + } + + public void appendLine(final String propertyName, final List<String> parameterList, + final String rawValue, final boolean needCharset, + boolean reallyUseQuotedPrintable) { + mBuilder.append(propertyName); + if (parameterList != null && parameterList.size() > 0) { + mBuilder.append(VCARD_PARAM_SEPARATOR); + appendTypeParameters(parameterList); + } + if (needCharset) { + mBuilder.append(VCARD_PARAM_SEPARATOR); + mBuilder.append(mVCardCharsetParameter); + } + + final String encodedValue; + if (reallyUseQuotedPrintable) { + mBuilder.append(VCARD_PARAM_SEPARATOR); + mBuilder.append(VCARD_PARAM_ENCODING_QP); + encodedValue = encodeQuotedPrintable(rawValue); + } else { + // TODO: one line may be too huge, which may be invalid in vCard spec, though + // several (even well-known) applications do not care this. + encodedValue = escapeCharacters(rawValue); + } + + mBuilder.append(VCARD_DATA_SEPARATOR); + mBuilder.append(encodedValue); + mBuilder.append(VCARD_END_OF_LINE); + } + + public void appendLine(final String propertyName, final List<String> rawValueList, + final boolean needCharset, boolean needQuotedPrintable) { + appendLine(propertyName, null, rawValueList, needCharset, needQuotedPrintable); + } + + public void appendLine(final String propertyName, final List<String> parameterList, + final List<String> rawValueList, final boolean needCharset, + final boolean needQuotedPrintable) { + mBuilder.append(propertyName); + if (parameterList != null && parameterList.size() > 0) { + mBuilder.append(VCARD_PARAM_SEPARATOR); + appendTypeParameters(parameterList); + } + if (needCharset) { + mBuilder.append(VCARD_PARAM_SEPARATOR); + mBuilder.append(mVCardCharsetParameter); + } + if (needQuotedPrintable) { + mBuilder.append(VCARD_PARAM_SEPARATOR); + mBuilder.append(VCARD_PARAM_ENCODING_QP); + } + + mBuilder.append(VCARD_DATA_SEPARATOR); + boolean first = true; + for (String rawValue : rawValueList) { + final String encodedValue; + if (needQuotedPrintable) { + encodedValue = encodeQuotedPrintable(rawValue); + } else { + // TODO: one line may be too huge, which may be invalid in vCard 3.0 + // (which says "When generating a content line, lines longer than + // 75 characters SHOULD be folded"), though several + // (even well-known) applications do not care this. + encodedValue = escapeCharacters(rawValue); + } + + if (first) { + first = false; + } else { + mBuilder.append(VCARD_ITEM_SEPARATOR); + } + mBuilder.append(encodedValue); + } + mBuilder.append(VCARD_END_OF_LINE); + } + + /** + * VCARD_PARAM_SEPARATOR must be appended before this method being called. + */ + private void appendTypeParameters(final List<String> types) { + // We may have to make this comma separated form like "TYPE=DOM,WORK" in the future, + // which would be recommended way in vcard 3.0 though not valid in vCard 2.1. + boolean first = true; + for (final String typeValue : types) { + // Note: vCard 3.0 specifies the different type of acceptable type Strings, but + // we don't emit that kind of vCard 3.0 specific type since there should be + // high probabilyty in which external importers cannot understand them. + // + // e.g. TYPE="\u578B\u306B\u3087" (vCard 3.0 allows non-Ascii characters if they + // are quoted.) + if (!VCardUtils.isV21Word(typeValue)) { + continue; + } + if (first) { + first = false; + } else { + mBuilder.append(VCARD_PARAM_SEPARATOR); + } + appendTypeParameter(typeValue); + } + } /** - * @param type - * LANGUAGE \ ENCODING <br> - * ;LANGUage= \ ;ENCODING= + * VCARD_PARAM_SEPARATOR must be appended before this method being called. */ - void propertyParamType(String type); + private void appendTypeParameter(final String type) { + appendTypeParameter(mBuilder, type); + } + + private void appendTypeParameter(final StringBuilder builder, final String type) { + // Refrain from using appendType() so that "TYPE=" is not be appended when the + // device is DoCoMo's (just for safety). + // + // Note: In vCard 3.0, Type strings also can be like this: "TYPE=HOME,PREF" + if ((mIsV30 || mAppendTypeParamName) && !mIsDoCoMo) { + builder.append(VCardConstants.PARAM_TYPE).append(VCARD_PARAM_EQUAL); + } + builder.append(type); + } /** - * @param value - * FR-EN \ GBK <br> - * FR-EN \ GBK + * Returns true when the property line should contain charset parameter + * information. This method may return true even when vCard version is 3.0. + * + * Strictly, adding charset information is invalid in VCard 3.0. + * However we'll add the info only when charset we use is not UTF-8 + * in vCard 3.0 format, since parser side may be able to use the charset + * via this field, though we may encounter another problem by adding it. + * + * e.g. Japanese mobile phones use Shift_Jis while RFC 2426 + * recommends UTF-8. By adding this field, parsers may be able + * to know this text is NOT UTF-8 but Shift_Jis. */ - void propertyParamValue(String value); + private boolean shouldAppendCharsetParam(String...propertyValueList) { + if (!mShouldAppendCharsetParam) { + return false; + } + for (String propertyValue : propertyValueList) { + if (!VCardUtils.containsOnlyPrintableAscii(propertyValue)) { + return true; + } + } + return false; + } + + private String encodeQuotedPrintable(final String str) { + if (TextUtils.isEmpty(str)) { + return ""; + } + + final StringBuilder builder = new StringBuilder(); + int index = 0; + int lineCount = 0; + byte[] strArray = null; + + try { + strArray = str.getBytes(mCharsetString); + } catch (UnsupportedEncodingException e) { + Log.e(LOG_TAG, "Charset " + mCharsetString + " cannot be used. " + + "Try default charset"); + strArray = str.getBytes(); + } + while (index < strArray.length) { + builder.append(String.format("=%02X", strArray[index])); + index += 1; + lineCount += 3; + + if (lineCount >= 67) { + // Specification requires CRLF must be inserted before the + // length of the line + // becomes more than 76. + // Assuming that the next character is a multi-byte character, + // it will become + // 6 bytes. + // 76 - 6 - 3 = 67 + builder.append("=\r\n"); + lineCount = 0; + } + } + + return builder.toString(); + } + + /** + * Append '\' to the characters which should be escaped. The character set is different + * not only between vCard 2.1 and vCard 3.0 but also among each device. + * + * Note that Quoted-Printable string must not be input here. + */ + @SuppressWarnings("fallthrough") + private String escapeCharacters(final String unescaped) { + if (TextUtils.isEmpty(unescaped)) { + return ""; + } + + final StringBuilder tmpBuilder = new StringBuilder(); + final int length = unescaped.length(); + for (int i = 0; i < length; i++) { + final char ch = unescaped.charAt(i); + switch (ch) { + case ';': { + tmpBuilder.append('\\'); + tmpBuilder.append(';'); + break; + } + case '\r': { + if (i + 1 < length) { + char nextChar = unescaped.charAt(i); + if (nextChar == '\n') { + break; + } else { + // fall through + } + } else { + // fall through + } + } + case '\n': { + // In vCard 2.1, there's no specification about this, while + // vCard 3.0 explicitly requires this should be encoded to "\n". + tmpBuilder.append("\\n"); + break; + } + case '\\': { + if (mIsV30) { + tmpBuilder.append("\\\\"); + break; + } else { + // fall through + } + } + case '<': + case '>': { + if (mIsDoCoMo) { + tmpBuilder.append('\\'); + tmpBuilder.append(ch); + } else { + tmpBuilder.append(ch); + } + break; + } + case ',': { + if (mIsV30) { + tmpBuilder.append("\\,"); + } else { + tmpBuilder.append(ch); + } + break; + } + default: { + tmpBuilder.append(ch); + break; + } + } + } + return tmpBuilder.toString(); + } - void propertyValues(List<String> values); + @Override + public String toString() { + if (!mEndAppended) { + if (mIsDoCoMo) { + appendLine(VCardConstants.PROPERTY_X_CLASS, VCARD_DATA_PUBLIC); + appendLine(VCardConstants.PROPERTY_X_REDUCTION, ""); + appendLine(VCardConstants.PROPERTY_X_NO, ""); + appendLine(VCardConstants.PROPERTY_X_DCM_HMN_MODE, ""); + } + appendLine(VCardConstants.PROPERTY_END, VCARD_DATA_VCARD); + mEndAppended = true; + } + return mBuilder.toString(); + } } diff --git a/core/java/android/pim/vcard/VCardComposer.java b/core/java/android/pim/vcard/VCardComposer.java index 7807595..389c9f4 100644 --- a/core/java/android/pim/vcard/VCardComposer.java +++ b/core/java/android/pim/vcard/VCardComposer.java @@ -38,11 +38,10 @@ import android.provider.ContactsContract.CommonDataKinds.Note; import android.provider.ContactsContract.CommonDataKinds.Organization; import android.provider.ContactsContract.CommonDataKinds.Phone; import android.provider.ContactsContract.CommonDataKinds.Photo; +import android.provider.ContactsContract.CommonDataKinds.Relation; import android.provider.ContactsContract.CommonDataKinds.StructuredName; import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; import android.provider.ContactsContract.CommonDataKinds.Website; -import android.telephony.PhoneNumberUtils; -import android.text.SpannableStringBuilder; import android.text.TextUtils; import android.text.format.Time; import android.util.CharsetUtils; @@ -55,13 +54,13 @@ import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.UnsupportedEncodingException; import java.io.Writer; +import java.lang.reflect.Method; +import java.nio.charset.UnsupportedCharsetException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Set; /** * <p> @@ -74,19 +73,37 @@ import java.util.Set; * Usually, this class should be used like this. * </p> * - * <pre class="prettyprint"> VCardComposer composer = null; try { composer = new - * VCardComposer(context); composer.addHandler(composer.new - * HandlerForOutputStream(outputStream)); if (!composer.init()) { // Do - * something handling the situation. return; } while (!composer.isAfterLast()) { - * if (mCanceled) { // Assume a user may cancel this operation during the - * export. return; } if (!composer.createOneEntry()) { // Do something handling - * the error situation. return; } } } finally { if (composer != null) { - * composer.terminate(); } } </pre> + * <pre class="prettyprint">VCardComposer composer = null; + * try { + * composer = new VCardComposer(context); + * composer.addHandler( + * composer.new HandlerForOutputStream(outputStream)); + * if (!composer.init()) { + * // Do something handling the situation. + * return; + * } + * while (!composer.isAfterLast()) { + * if (mCanceled) { + * // Assume a user may cancel this operation during the export. + * return; + * } + * if (!composer.createOneEntry()) { + * // Do something handling the error situation. + * return; + * } + * } + * } finally { + * if (composer != null) { + * composer.terminate(); + * } + * } </pre> */ public class VCardComposer { - private static final String LOG_TAG = "vcard.VCardComposer"; + private static final String LOG_TAG = "VCardComposer"; - private static final String DEFAULT_EMAIL_TYPE = Constants.ATTR_TYPE_INTERNET; + public static final int DEFAULT_PHONE_TYPE = Phone.TYPE_HOME; + public static final int DEFAULT_POSTAL_TYPE = StructuredPostal.TYPE_HOME; + public static final int DEFAULT_EMAIL_TYPE = Email.TYPE_OTHER; public static final String FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO = "Failed to get database information"; @@ -97,31 +114,57 @@ public class VCardComposer { public static final String FAILURE_REASON_NOT_INITIALIZED = "The vCard composer object is not correctly initialized"; + /** Should be visible only from developers... (no need to translate, hopefully) */ + public static final String FAILURE_REASON_UNSUPPORTED_URI = + "The Uri vCard composer received is not supported by the composer."; + public static final String NO_ERROR = "No error"; - private static final Uri sDataRequestUri; + public static final String VCARD_TYPE_STRING_DOCOMO = "docomo"; + + // Property for call log entry + private static final String VCARD_PROPERTY_X_TIMESTAMP = "X-IRMC-CALL-DATETIME"; + private static final String VCARD_PROPERTY_CALLTYPE_INCOMING = "INCOMING"; + private static final String VCARD_PROPERTY_CALLTYPE_OUTGOING = "OUTGOING"; + private static final String VCARD_PROPERTY_CALLTYPE_MISSED = "MISSED"; + + private static final String SHIFT_JIS = "SHIFT_JIS"; + private static final String UTF_8 = "UTF-8"; + + /** + * Special URI for testing. + */ + public static final String VCARD_TEST_AUTHORITY = "com.android.unit_tests.vcard"; + public static final Uri VCARD_TEST_AUTHORITY_URI = + Uri.parse("content://" + VCARD_TEST_AUTHORITY); + public static final Uri CONTACTS_TEST_CONTENT_URI = + Uri.withAppendedPath(VCARD_TEST_AUTHORITY_URI, "contacts"); + + private static final Map<Integer, String> sImMap; static { - Uri.Builder builder = RawContacts.CONTENT_URI.buildUpon(); - builder.appendQueryParameter(Data.FOR_EXPORT_ONLY, "1"); - sDataRequestUri = builder.build(); + sImMap = new HashMap<Integer, String>(); + sImMap.put(Im.PROTOCOL_AIM, VCardConstants.PROPERTY_X_AIM); + sImMap.put(Im.PROTOCOL_MSN, VCardConstants.PROPERTY_X_MSN); + sImMap.put(Im.PROTOCOL_YAHOO, VCardConstants.PROPERTY_X_YAHOO); + sImMap.put(Im.PROTOCOL_ICQ, VCardConstants.PROPERTY_X_ICQ); + sImMap.put(Im.PROTOCOL_JABBER, VCardConstants.PROPERTY_X_JABBER); + sImMap.put(Im.PROTOCOL_SKYPE, VCardConstants.PROPERTY_X_SKYPE_USERNAME); + // Google talk is a special case. } public static interface OneEntryHandler { public boolean onInit(Context context); - public boolean onEntryCreated(String vcard); - public void onTerminate(); } /** * <p> - * An useful example handler, which emits VCard String to outputstream one - * by one. + * An useful example handler, which emits VCard String to outputstream one by one. * </p> * <p> - * The input OutputStream object is closed() on {{@link #onTerminate()}. + * The input OutputStream object is closed() on {@link #onTerminate()}. * Must not close the stream outside. * </p> */ @@ -155,7 +198,7 @@ public class VCardComposer { if (mIsDoCoMo) { try { // Create one empty entry. - mWriter.write(createOneEntryInternal("-1")); + mWriter.write(createOneEntryInternal("-1", null)); } catch (IOException e) { Log.e(LOG_TAG, "IOException occurred during exportOneContactData: " @@ -213,109 +256,23 @@ public class VCardComposer { } } - public static final String VCARD_TYPE_STRING_DOCOMO = "docomo"; - - private static final String VCARD_PROPERTY_ADR = "ADR"; - private static final String VCARD_PROPERTY_BEGIN = "BEGIN"; - private static final String VCARD_PROPERTY_EMAIL = "EMAIL"; - private static final String VCARD_PROPERTY_END = "END"; - private static final String VCARD_PROPERTY_NAME = "N"; - private static final String VCARD_PROPERTY_FULL_NAME = "FN"; - private static final String VCARD_PROPERTY_NOTE = "NOTE"; - private static final String VCARD_PROPERTY_ORG = "ORG"; - private static final String VCARD_PROPERTY_SOUND = "SOUND"; - private static final String VCARD_PROPERTY_SORT_STRING = "SORT-STRING"; - private static final String VCARD_PROPERTY_NICKNAME = "NICKNAME"; - private static final String VCARD_PROPERTY_TEL = "TEL"; - private static final String VCARD_PROPERTY_TITLE = "TITLE"; - private static final String VCARD_PROPERTY_PHOTO = "PHOTO"; - private static final String VCARD_PROPERTY_VERSION = "VERSION"; - private static final String VCARD_PROPERTY_URL = "URL"; - private static final String VCARD_PROPERTY_BIRTHDAY = "BDAY"; - - private static final String VCARD_PROPERTY_X_PHONETIC_FIRST_NAME = "X-PHONETIC-FIRST-NAME"; - private static final String VCARD_PROPERTY_X_PHONETIC_MIDDLE_NAME = "X-PHONETIC-MIDDLE-NAME"; - private static final String VCARD_PROPERTY_X_PHONETIC_LAST_NAME = "X-PHONETIC-LAST-NAME"; - - // Android specific properties - // TODO: ues extra MIME-TYPE instead of adding this kind of inflexible fields - private static final String VCARD_PROPERTY_X_NICKNAME = "X-NICKNAME"; - - // Property for call log entry - private static final String VCARD_PROPERTY_X_TIMESTAMP = "X-IRMC-CALL-DATETIME"; - private static final String VCARD_PROPERTY_CALLTYPE_INCOMING = "INCOMING"; - private static final String VCARD_PROPERTY_CALLTYPE_OUTGOING = "OUTGOING"; - private static final String VCARD_PROPERTY_CALLTYPE_MISSED = "MISSED"; - - // Properties for DoCoMo vCard. - private static final String VCARD_PROPERTY_X_CLASS = "X-CLASS"; - private static final String VCARD_PROPERTY_X_REDUCTION = "X-REDUCTION"; - private static final String VCARD_PROPERTY_X_NO = "X-NO"; - private static final String VCARD_PROPERTY_X_DCM_HMN_MODE = "X-DCM-HMN-MODE"; - - private static final String VCARD_DATA_VCARD = "VCARD"; - private static final String VCARD_DATA_PUBLIC = "PUBLIC"; - - private static final String VCARD_ATTR_SEPARATOR = ";"; - private static final String VCARD_COL_SEPARATOR = "\r\n"; - private static final String VCARD_DATA_SEPARATOR = ":"; - private static final String VCARD_ITEM_SEPARATOR = ";"; - private static final String VCARD_WS = " "; - private static final String VCARD_ATTR_EQUAL = "="; - - // Type strings are now in VCardConstants.java. - - private static final String VCARD_ATTR_ENCODING_QP = "ENCODING=QUOTED-PRINTABLE"; - - private static final String VCARD_ATTR_ENCODING_BASE64_V21 = "ENCODING=BASE64"; - private static final String VCARD_ATTR_ENCODING_BASE64_V30 = "ENCODING=b"; - - private static final String SHIFT_JIS = "SHIFT_JIS"; - private final Context mContext; private final int mVCardType; private final boolean mCareHandlerErrors; private final ContentResolver mContentResolver; - // Convenient member variables about the restriction of the vCard format. - // Used for not calling the same methods returning same results. - private final boolean mIsV30; - private final boolean mIsJapaneseMobilePhone; - private final boolean mOnlyOneNoteFieldIsAvailable; private final boolean mIsDoCoMo; - private final boolean mUsesQuotedPrintable; - private final boolean mUsesAndroidProperty; - private final boolean mUsesDefactProperty; - private final boolean mUsesUtf8; private final boolean mUsesShiftJis; - private final boolean mUsesQPToPrimaryProperties; - private Cursor mCursor; private int mIdColumn; private final String mCharsetString; - private final String mVCardAttributeCharset; private boolean mTerminateIsCalled; final private List<OneEntryHandler> mHandlerList; private String mErrorReason = NO_ERROR; - private static final Map<Integer, String> sImMap; - - static { - sImMap = new HashMap<Integer, String>(); - sImMap.put(Im.PROTOCOL_AIM, Constants.PROPERTY_X_AIM); - sImMap.put(Im.PROTOCOL_MSN, Constants.PROPERTY_X_MSN); - sImMap.put(Im.PROTOCOL_YAHOO, Constants.PROPERTY_X_YAHOO); - sImMap.put(Im.PROTOCOL_ICQ, Constants.PROPERTY_X_ICQ); - sImMap.put(Im.PROTOCOL_JABBER, Constants.PROPERTY_X_JABBER); - sImMap.put(Im.PROTOCOL_SKYPE, Constants.PROPERTY_X_SKYPE_USERNAME); - // Google talk is a special case. - } - - private boolean mIsCallLogComposer = false; - - private boolean mNeedPhotoForVCard = true; + private boolean mIsCallLogComposer; private static final String[] sContactsProjection = new String[] { Contacts._ID, @@ -336,110 +293,83 @@ public class VCardComposer { private static final String FLAG_TIMEZONE_UTC = "Z"; public VCardComposer(Context context) { - this(context, VCardConfig.VCARD_TYPE_DEFAULT, true, false, true); + this(context, VCardConfig.VCARD_TYPE_DEFAULT, true); } - public VCardComposer(Context context, String vcardTypeStr, - boolean careHandlerErrors) { - this(context, VCardConfig.getVCardTypeFromString(vcardTypeStr), - careHandlerErrors, false, true); + public VCardComposer(Context context, int vcardType) { + this(context, vcardType, true); } - public VCardComposer(Context context, int vcardType, boolean careHandlerErrors) { - this(context, vcardType, careHandlerErrors, false, true); + public VCardComposer(Context context, String vcardTypeStr, boolean careHandlerErrors) { + this(context, VCardConfig.getVCardTypeFromString(vcardTypeStr), careHandlerErrors); } /** * Construct for supporting call log entry vCard composing. - * - * @param isCallLogComposer true if this composer is for creating Call Log vCard. */ - public VCardComposer(Context context, int vcardType, boolean careHandlerErrors, - boolean isCallLogComposer, boolean needPhotoInVCard) { + public VCardComposer(final Context context, final int vcardType, + final boolean careHandlerErrors) { mContext = context; mVCardType = vcardType; mCareHandlerErrors = careHandlerErrors; - mIsCallLogComposer = isCallLogComposer; - mNeedPhotoForVCard = needPhotoInVCard; mContentResolver = context.getContentResolver(); - mIsV30 = VCardConfig.isV30(vcardType); - mUsesQuotedPrintable = VCardConfig.usesQuotedPrintable(vcardType); mIsDoCoMo = VCardConfig.isDoCoMo(vcardType); - mIsJapaneseMobilePhone = VCardConfig - .needsToConvertPhoneticString(vcardType); - mOnlyOneNoteFieldIsAvailable = VCardConfig - .onlyOneNoteFieldIsAvailable(vcardType); - mUsesAndroidProperty = VCardConfig - .usesAndroidSpecificProperty(vcardType); - mUsesDefactProperty = VCardConfig.usesDefactProperty(vcardType); - mUsesUtf8 = VCardConfig.usesUtf8(vcardType); mUsesShiftJis = VCardConfig.usesShiftJis(vcardType); - mUsesQPToPrimaryProperties = VCardConfig.usesQPToPrimaryProperties(vcardType); mHandlerList = new ArrayList<OneEntryHandler>(); if (mIsDoCoMo) { - mCharsetString = CharsetUtils.charsetForVendor(SHIFT_JIS, "docomo").name(); - // Do not use mCharsetString bellow since it is different from "SHIFT_JIS" but - // may be "DOCOMO_SHIFT_JIS" or something like that (internal expression used in - // Android, not shown to the public). - mVCardAttributeCharset = "CHARSET=" + SHIFT_JIS; + String charset; + try { + charset = CharsetUtils.charsetForVendor(SHIFT_JIS, "docomo").name(); + } catch (UnsupportedCharsetException e) { + Log.e(LOG_TAG, "DoCoMo-specific SHIFT_JIS was not found. Use SHIFT_JIS as is."); + charset = SHIFT_JIS; + } + mCharsetString = charset; } else if (mUsesShiftJis) { - mCharsetString = CharsetUtils.charsetForVendor(SHIFT_JIS).name(); - mVCardAttributeCharset = "CHARSET=" + SHIFT_JIS; + String charset; + try { + charset = CharsetUtils.charsetForVendor(SHIFT_JIS).name(); + } catch (UnsupportedCharsetException e) { + Log.e(LOG_TAG, "Vendor-specific SHIFT_JIS was not found. Use SHIFT_JIS as is."); + charset = SHIFT_JIS; + } + mCharsetString = charset; } else { - mCharsetString = "UTF-8"; - mVCardAttributeCharset = "CHARSET=UTF-8"; + mCharsetString = UTF_8; } } /** - * This static function is to compose vCard for phone own number + * Must be called before {@link #init()}. */ - public String composeVCardForPhoneOwnNumber(int phonetype, String phoneName, - String phoneNumber, boolean vcardVer21) { - final StringBuilder builder = new StringBuilder(); - appendVCardLine(builder, VCARD_PROPERTY_BEGIN, VCARD_DATA_VCARD); - if (!vcardVer21) { - appendVCardLine(builder, VCARD_PROPERTY_VERSION, Constants.VERSION_V30); - } else { - appendVCardLine(builder, VCARD_PROPERTY_VERSION, Constants.VERSION_V21); - } - - boolean needCharset = false; - if (!(VCardUtils.containsOnlyPrintableAscii(phoneName))) { - needCharset = true; - } - // TODO: QP should be used? Using mUsesQPToPrimaryProperties should help. - appendVCardLine(builder, VCARD_PROPERTY_FULL_NAME, phoneName, needCharset, false); - appendVCardLine(builder, VCARD_PROPERTY_NAME, phoneName, needCharset, false); - - if (!TextUtils.isEmpty(phoneNumber)) { - String label = Integer.toString(phonetype); - appendVCardTelephoneLine(builder, phonetype, label, phoneNumber); + public void addHandler(OneEntryHandler handler) { + if (handler != null) { + mHandlerList.add(handler); } - - appendVCardLine(builder, VCARD_PROPERTY_END, VCARD_DATA_VCARD); - - return builder.toString(); } /** - * Must call before {{@link #init()}. + * @return Returns true when initialization is successful and all the other + * methods are available. Returns false otherwise. */ - public void addHandler(OneEntryHandler handler) { - mHandlerList.add(handler); - } - public boolean init() { return init(null, null); } + public boolean init(final String selection, final String[] selectionArgs) { + return init(Contacts.CONTENT_URI, selection, selectionArgs, null); + } + /** - * @return Returns true when initialization is successful and all the other - * methods are available. Returns false otherwise. + * Note that this is unstable interface, may be deleted in the future. */ - public boolean init(final String selection, final String[] selectionArgs) { + public boolean init(final Uri contentUri, final String selection, + final String[] selectionArgs, final String sortOrder) { + if (contentUri == null) { + return false; + } if (mCareHandlerErrors) { List<OneEntryHandler> finishedList = new ArrayList<OneEntryHandler>( mHandlerList.size()); @@ -458,13 +388,19 @@ public class VCardComposer { } } - if (mIsCallLogComposer) { - mCursor = mContentResolver.query(CallLog.Calls.CONTENT_URI, sCallLogProjection, - selection, selectionArgs, null); + final String[] projection; + if (CallLog.Calls.CONTENT_URI.equals(contentUri)) { + projection = sCallLogProjection; + mIsCallLogComposer = true; + } else if (Contacts.CONTENT_URI.equals(contentUri) || + CONTACTS_TEST_CONTENT_URI.equals(contentUri)) { + projection = sContactsProjection; } else { - mCursor = mContentResolver.query(Contacts.CONTENT_URI, sContactsProjection, - selection, selectionArgs, null); + mErrorReason = FAILURE_REASON_UNSUPPORTED_URI; + return false; } + mCursor = mContentResolver.query( + contentUri, projection, selection, selectionArgs, sortOrder); if (mCursor == null) { mErrorReason = FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO; @@ -493,6 +429,14 @@ public class VCardComposer { } public boolean createOneEntry() { + return createOneEntry(null); + } + + /** + * @param getEntityIteratorMethod For Dependency Injection. + * @hide just for testing. + */ + public boolean createOneEntry(Method getEntityIteratorMethod) { if (mCursor == null || mCursor.isAfterLast()) { mErrorReason = FAILURE_REASON_NOT_INITIALIZED; return false; @@ -504,7 +448,8 @@ public class VCardComposer { vcard = createOneCallLogEntryInternal(); } else { if (mIdColumn >= 0) { - vcard = createOneEntryInternal(mCursor.getString(mIdColumn)); + vcard = createOneEntryInternal(mCursor.getString(mIdColumn), + getEntityIteratorMethod); } else { Log.e(LOG_TAG, "Incorrect mIdColumn: " + mIdColumn); return true; @@ -513,8 +458,7 @@ public class VCardComposer { } catch (OutOfMemoryError error) { // Maybe some data (e.g. photo) is too big to have in memory. But it // should be rare. - Log.e(LOG_TAG, "OutOfMemoryError occured. Ignore the entry: " - + name); + Log.e(LOG_TAG, "OutOfMemoryError occured. Ignore the entry: " + name); System.gc(); // TODO: should tell users what happened? return true; @@ -541,107 +485,48 @@ public class VCardComposer { return true; } - /** - * Format according to RFC 2445 DATETIME type. - * The format is: ("%Y%m%dT%H%M%SZ"). - */ - private final String toRfc2455Format(final long millSecs) { - Time startDate = new Time(); - startDate.set(millSecs); - String date = startDate.format2445(); - return date + FLAG_TIMEZONE_UTC; - } - - /** - * Try to append the property line for a call history time stamp field if possible. - * Do nothing if the call log type gotton from the database is invalid. - */ - private void tryAppendCallHistoryTimeStampField(final StringBuilder builder) { - // Extension for call history as defined in - // in the Specification for Ic Mobile Communcation - ver 1.1, - // Oct 2000. This is used to send the details of the call - // history - missed, incoming, outgoing along with date and time - // to the requesting device (For example, transferring phone book - // when connected over bluetooth) - // - // e.g. "X-IRMC-CALL-DATETIME;MISSED:20050320T100000Z" - final int callLogType = mCursor.getInt(CALL_TYPE_COLUMN_INDEX); - final String callLogTypeStr; - switch (callLogType) { - case Calls.INCOMING_TYPE: { - callLogTypeStr = VCARD_PROPERTY_CALLTYPE_INCOMING; - break; - } - case Calls.OUTGOING_TYPE: { - callLogTypeStr = VCARD_PROPERTY_CALLTYPE_OUTGOING; - break; - } - case Calls.MISSED_TYPE: { - callLogTypeStr = VCARD_PROPERTY_CALLTYPE_MISSED; - break; - } - default: { - Log.w(LOG_TAG, "Call log type not correct."); - return; - } - } - - final long dateAsLong = mCursor.getLong(DATE_COLUMN_INDEX); - builder.append(VCARD_PROPERTY_X_TIMESTAMP); - builder.append(VCARD_ATTR_SEPARATOR); - appendTypeAttribute(builder, callLogTypeStr); - builder.append(VCARD_DATA_SEPARATOR); - builder.append(toRfc2455Format(dateAsLong)); - builder.append(VCARD_COL_SEPARATOR); - } - - private String createOneCallLogEntryInternal() { - final StringBuilder builder = new StringBuilder(); - appendVCardLine(builder, VCARD_PROPERTY_BEGIN, VCARD_DATA_VCARD); - if (mIsV30) { - appendVCardLine(builder, VCARD_PROPERTY_VERSION, Constants.VERSION_V30); - } else { - appendVCardLine(builder, VCARD_PROPERTY_VERSION, Constants.VERSION_V21); - } - String name = mCursor.getString(CALLER_NAME_COLUMN_INDEX); - if (TextUtils.isEmpty(name)) { - name = mCursor.getString(NUMBER_COLUMN_INDEX); - } - final boolean needCharset = !(VCardUtils.containsOnlyPrintableAscii(name)); - // TODO: QP should be used? Using mUsesQPToPrimaryProperties should help. - appendVCardLine(builder, VCARD_PROPERTY_FULL_NAME, name, needCharset, false); - appendVCardLine(builder, VCARD_PROPERTY_NAME, name, needCharset, false); - - String number = mCursor.getString(NUMBER_COLUMN_INDEX); - int type = mCursor.getInt(CALLER_NUMBERTYPE_COLUMN_INDEX); - String label = mCursor.getString(CALLER_NUMBERLABEL_COLUMN_INDEX); - if (TextUtils.isEmpty(label)) { - label = Integer.toString(type); - } - appendVCardTelephoneLine(builder, type, label, number); - tryAppendCallHistoryTimeStampField(builder); - appendVCardLine(builder, VCARD_PROPERTY_END, VCARD_DATA_VCARD); - return builder.toString(); - } - - private String createOneEntryInternal(final String contactId) { + private String createOneEntryInternal(final String contactId, + Method getEntityIteratorMethod) { final Map<String, List<ContentValues>> contentValuesListMap = new HashMap<String, List<ContentValues>>(); - final String selection = Data.CONTACT_ID + "=?"; - final String[] selectionArgs = new String[] {contactId}; // The resolver may return the entity iterator with no data. It is possiible. // e.g. If all the data in the contact of the given contact id are not exportable ones, // they are hidden from the view of this method, though contact id itself exists. boolean dataExists = false; EntityIterator entityIterator = null; try { - entityIterator = mContentResolver.queryEntities( - sDataRequestUri, selection, selectionArgs, null); + + if (getEntityIteratorMethod != null) { + try { + final Uri uri = RawContacts.CONTENT_URI.buildUpon() + .appendQueryParameter(Data.FOR_EXPORT_ONLY, "1") + .build(); + final String selection = Data.CONTACT_ID + "=?"; + final String[] selectionArgs = new String[] {contactId}; + entityIterator = (EntityIterator)getEntityIteratorMethod.invoke(null, + mContentResolver, uri, selection, selectionArgs, null); + } catch (Exception e) { + e.printStackTrace(); + } + } else { + final Uri uri = RawContacts.CONTENT_URI.buildUpon() + .appendEncodedPath(contactId) + .appendEncodedPath(RawContacts.Entity.CONTENT_DIRECTORY) + .appendQueryParameter(Data.FOR_EXPORT_ONLY, "1") + .build(); + entityIterator = RawContacts.newEntityIterator(mContentResolver.query( + uri, null, null, null, null)); + } + + if (entityIterator == null) { + Log.e(LOG_TAG, "EntityIterator is null"); + return ""; + } + dataExists = entityIterator.hasNext(); while (entityIterator.hasNext()) { Entity entity = entityIterator.next(); - for (NamedContentValues namedContentValues : entity - .getSubValues()) { + for (NamedContentValues namedContentValues : entity.getSubValues()) { ContentValues contentValues = namedContentValues.values; String key = contentValues.getAsString(Data.MIMETYPE); if (key != null) { @@ -669,38 +554,19 @@ public class VCardComposer { return ""; } - final StringBuilder builder = new StringBuilder(); - appendVCardLine(builder, VCARD_PROPERTY_BEGIN, VCARD_DATA_VCARD); - if (mIsV30) { - appendVCardLine(builder, VCARD_PROPERTY_VERSION, Constants.VERSION_V30); - } else { - appendVCardLine(builder, VCARD_PROPERTY_VERSION, Constants.VERSION_V21); - } - - appendStructuredNames(builder, contentValuesListMap); - appendNickNames(builder, contentValuesListMap); - appendPhones(builder, contentValuesListMap); - appendEmails(builder, contentValuesListMap); - appendPostals(builder, contentValuesListMap); - appendIms(builder, contentValuesListMap); - appendWebsites(builder, contentValuesListMap); - appendBirthday(builder, contentValuesListMap); - appendOrganizations(builder, contentValuesListMap); - if (mNeedPhotoForVCard) { - appendPhotos(builder, contentValuesListMap); - } - appendNotes(builder, contentValuesListMap); - // TODO: GroupMembership - - if (mIsDoCoMo) { - appendVCardLine(builder, VCARD_PROPERTY_X_CLASS, VCARD_DATA_PUBLIC); - appendVCardLine(builder, VCARD_PROPERTY_X_REDUCTION, ""); - appendVCardLine(builder, VCARD_PROPERTY_X_NO, ""); - appendVCardLine(builder, VCARD_PROPERTY_X_DCM_HMN_MODE, ""); - } - - appendVCardLine(builder, VCARD_PROPERTY_END, VCARD_DATA_VCARD); - + final VCardBuilder builder = new VCardBuilder(mVCardType); + builder.appendNameProperties(contentValuesListMap.get(StructuredName.CONTENT_ITEM_TYPE)) + .appendNickNames(contentValuesListMap.get(Nickname.CONTENT_ITEM_TYPE)) + .appendPhones(contentValuesListMap.get(Phone.CONTENT_ITEM_TYPE)) + .appendEmails(contentValuesListMap.get(Email.CONTENT_ITEM_TYPE)) + .appendPostals(contentValuesListMap.get(StructuredPostal.CONTENT_ITEM_TYPE)) + .appendOrganizations(contentValuesListMap.get(Organization.CONTENT_ITEM_TYPE)) + .appendWebsites(contentValuesListMap.get(Website.CONTENT_ITEM_TYPE)) + .appendPhotos(contentValuesListMap.get(Photo.CONTENT_ITEM_TYPE)) + .appendNotes(contentValuesListMap.get(Note.CONTENT_ITEM_TYPE)) + .appendEvents(contentValuesListMap.get(Event.CONTENT_ITEM_TYPE)) + .appendIms(contentValuesListMap.get(Im.CONTENT_ITEM_TYPE)) + .appendRelation(contentValuesListMap.get(Relation.CONTENT_ITEM_TYPE)); return builder.toString(); } @@ -713,8 +579,7 @@ public class VCardComposer { try { mCursor.close(); } catch (SQLiteException e) { - Log.e(LOG_TAG, "SQLiteException on Cursor#close(): " - + e.getMessage()); + Log.e(LOG_TAG, "SQLiteException on Cursor#close(): " + e.getMessage()); } mCursor = null; } @@ -750,1302 +615,98 @@ public class VCardComposer { return mErrorReason; } - private void appendStructuredNames(final StringBuilder builder, - final Map<String, List<ContentValues>> contentValuesListMap) { - final List<ContentValues> contentValuesList = contentValuesListMap - .get(StructuredName.CONTENT_ITEM_TYPE); - if (contentValuesList != null && contentValuesList.size() > 0) { - appendStructuredNamesInternal(builder, contentValuesList); - } else if (mIsDoCoMo) { - appendVCardLine(builder, VCARD_PROPERTY_NAME, ""); - } else if (mIsV30) { - // vCard 3.0 requires "N" and "FN" properties. - appendVCardLine(builder, VCARD_PROPERTY_NAME, ""); - appendVCardLine(builder, VCARD_PROPERTY_FULL_NAME, ""); - } - } - - private boolean containsNonEmptyName(ContentValues contentValues) { - final String familyName = contentValues.getAsString(StructuredName.FAMILY_NAME); - final String middleName = contentValues.getAsString(StructuredName.MIDDLE_NAME); - final String givenName = contentValues.getAsString(StructuredName.GIVEN_NAME); - final String prefix = contentValues.getAsString(StructuredName.PREFIX); - final String suffix = contentValues.getAsString(StructuredName.SUFFIX); - final String displayName = contentValues.getAsString(StructuredName.DISPLAY_NAME); - return !(TextUtils.isEmpty(familyName) && TextUtils.isEmpty(middleName) && - TextUtils.isEmpty(givenName) && TextUtils.isEmpty(prefix) && - TextUtils.isEmpty(suffix) && TextUtils.isEmpty(displayName)); - } - - private void appendStructuredNamesInternal(final StringBuilder builder, - final List<ContentValues> contentValuesList) { - // For safety, we'll emit just one value around StructuredName, as external importers - // may get confused with multiple "N", "FN", etc. properties, though it is valid in - // vCard spec. - ContentValues primaryContentValues = null; - ContentValues subprimaryContentValues = null; - for (ContentValues contentValues : contentValuesList) { - if (contentValues == null){ - continue; - } - Integer isSuperPrimary = contentValues.getAsInteger(StructuredName.IS_SUPER_PRIMARY); - if (isSuperPrimary != null && isSuperPrimary > 0) { - // We choose "super primary" ContentValues. - primaryContentValues = contentValues; - break; - } else if (primaryContentValues == null) { - // We choose the first "primary" ContentValues - // if "super primary" ContentValues does not exist. - Integer isPrimary = contentValues.getAsInteger(StructuredName.IS_PRIMARY); - if (isPrimary != null && isPrimary > 0 && - containsNonEmptyName(contentValues)) { - primaryContentValues = contentValues; - // Do not break, since there may be ContentValues with "super primary" - // afterword. - } else if (subprimaryContentValues == null && - containsNonEmptyName(contentValues)) { - subprimaryContentValues = contentValues; - } - } - } - - if (primaryContentValues == null) { - if (subprimaryContentValues != null) { - // We choose the first ContentValues if any "primary" ContentValues does not exist. - primaryContentValues = subprimaryContentValues; - } else { - Log.e(LOG_TAG, "All ContentValues given from database is empty."); - primaryContentValues = new ContentValues(); - } - } - - final String familyName = primaryContentValues - .getAsString(StructuredName.FAMILY_NAME); - final String middleName = primaryContentValues - .getAsString(StructuredName.MIDDLE_NAME); - final String givenName = primaryContentValues - .getAsString(StructuredName.GIVEN_NAME); - final String prefix = primaryContentValues - .getAsString(StructuredName.PREFIX); - final String suffix = primaryContentValues - .getAsString(StructuredName.SUFFIX); - final String displayName = primaryContentValues - .getAsString(StructuredName.DISPLAY_NAME); - - if (!TextUtils.isEmpty(familyName) || !TextUtils.isEmpty(givenName)) { - final String encodedFamily; - final String encodedGiven; - final String encodedMiddle; - final String encodedPrefix; - final String encodedSuffix; - - final boolean reallyUseQuotedPrintableToName = - (mUsesQPToPrimaryProperties && - !(VCardUtils.containsOnlyNonCrLfPrintableAscii(familyName) && - VCardUtils.containsOnlyNonCrLfPrintableAscii(givenName) && - VCardUtils.containsOnlyNonCrLfPrintableAscii(middleName) && - VCardUtils.containsOnlyNonCrLfPrintableAscii(prefix) && - VCardUtils.containsOnlyNonCrLfPrintableAscii(suffix))); - - if (reallyUseQuotedPrintableToName) { - encodedFamily = encodeQuotedPrintable(familyName); - encodedGiven = encodeQuotedPrintable(givenName); - encodedMiddle = encodeQuotedPrintable(middleName); - encodedPrefix = encodeQuotedPrintable(prefix); - encodedSuffix = encodeQuotedPrintable(suffix); - } else { - encodedFamily = escapeCharacters(familyName); - encodedGiven = escapeCharacters(givenName); - encodedMiddle = escapeCharacters(middleName); - encodedPrefix = escapeCharacters(prefix); - encodedSuffix = escapeCharacters(suffix); - } - - // N property. This order is specified by vCard spec and does not depend on countries. - builder.append(VCARD_PROPERTY_NAME); - if (shouldAppendCharsetAttribute(Arrays.asList( - familyName, givenName, middleName, prefix, suffix))) { - builder.append(VCARD_ATTR_SEPARATOR); - builder.append(mVCardAttributeCharset); - } - if (reallyUseQuotedPrintableToName) { - builder.append(VCARD_ATTR_SEPARATOR); - builder.append(VCARD_ATTR_ENCODING_QP); - } - - builder.append(VCARD_DATA_SEPARATOR); - builder.append(encodedFamily); - builder.append(VCARD_ITEM_SEPARATOR); - builder.append(encodedGiven); - builder.append(VCARD_ITEM_SEPARATOR); - builder.append(encodedMiddle); - builder.append(VCARD_ITEM_SEPARATOR); - builder.append(encodedPrefix); - builder.append(VCARD_ITEM_SEPARATOR); - builder.append(encodedSuffix); - builder.append(VCARD_COL_SEPARATOR); - - final String fullname = VCardUtils.constructNameFromElements( - VCardConfig.getNameOrderType(mVCardType), - encodedFamily, encodedMiddle, encodedGiven, encodedPrefix, encodedSuffix); - final boolean reallyUseQuotedPrintableToFullname = - mUsesQPToPrimaryProperties && - !VCardUtils.containsOnlyNonCrLfPrintableAscii(fullname); - - final String encodedFullname = - reallyUseQuotedPrintableToFullname ? - encodeQuotedPrintable(fullname) : - escapeCharacters(fullname); - - // FN property - builder.append(VCARD_PROPERTY_FULL_NAME); - if (shouldAppendCharsetAttribute(encodedFullname)) { - builder.append(VCARD_ATTR_SEPARATOR); - builder.append(mVCardAttributeCharset); - } - if (reallyUseQuotedPrintableToFullname) { - builder.append(VCARD_ATTR_SEPARATOR); - builder.append(VCARD_ATTR_ENCODING_QP); - } - builder.append(VCARD_DATA_SEPARATOR); - builder.append(encodedFullname); - builder.append(VCARD_COL_SEPARATOR); - } else if (!TextUtils.isEmpty(displayName)) { - final boolean reallyUseQuotedPrintableToDisplayName = - (mUsesQPToPrimaryProperties && - !VCardUtils.containsOnlyNonCrLfPrintableAscii(displayName)); - final String encodedDisplayName = - reallyUseQuotedPrintableToDisplayName ? - encodeQuotedPrintable(displayName) : - escapeCharacters(displayName); - - builder.append(VCARD_PROPERTY_NAME); - if (shouldAppendCharsetAttribute(encodedDisplayName)) { - builder.append(VCARD_ATTR_SEPARATOR); - builder.append(mVCardAttributeCharset); - } - if (reallyUseQuotedPrintableToDisplayName) { - builder.append(VCARD_ATTR_SEPARATOR); - builder.append(VCARD_ATTR_ENCODING_QP); - } - builder.append(VCARD_DATA_SEPARATOR); - builder.append(encodedDisplayName); - builder.append(VCARD_ITEM_SEPARATOR); - builder.append(VCARD_ITEM_SEPARATOR); - builder.append(VCARD_ITEM_SEPARATOR); - builder.append(VCARD_ITEM_SEPARATOR); - builder.append(VCARD_COL_SEPARATOR); - } else if (mIsDoCoMo) { - appendVCardLine(builder, VCARD_PROPERTY_NAME, ""); - } else if (mIsV30) { - appendVCardLine(builder, VCARD_PROPERTY_NAME, ""); - appendVCardLine(builder, VCARD_PROPERTY_FULL_NAME, ""); - } - - String phoneticFamilyName = primaryContentValues - .getAsString(StructuredName.PHONETIC_FAMILY_NAME); - String phoneticMiddleName = primaryContentValues - .getAsString(StructuredName.PHONETIC_MIDDLE_NAME); - String phoneticGivenName = primaryContentValues - .getAsString(StructuredName.PHONETIC_GIVEN_NAME); - if (!(TextUtils.isEmpty(phoneticFamilyName) - && TextUtils.isEmpty(phoneticMiddleName) && - TextUtils.isEmpty(phoneticGivenName))) { // if not empty - if (mIsJapaneseMobilePhone) { - phoneticFamilyName = VCardUtils - .toHalfWidthString(phoneticFamilyName); - phoneticMiddleName = VCardUtils - .toHalfWidthString(phoneticMiddleName); - phoneticGivenName = VCardUtils - .toHalfWidthString(phoneticGivenName); - } - - if (mIsV30) { - final String sortString = VCardUtils - .constructNameFromElements(mVCardType, - phoneticFamilyName, - phoneticMiddleName, - phoneticGivenName); - builder.append(VCARD_PROPERTY_SORT_STRING); - - // Do not need to care about QP, since vCard 3.0 does not allow it. - final String encodedSortString = escapeCharacters(sortString); - if (shouldAppendCharsetAttribute(encodedSortString)) { - builder.append(VCARD_ATTR_SEPARATOR); - builder.append(mVCardAttributeCharset); - } - builder.append(VCARD_DATA_SEPARATOR); - builder.append(encodedSortString); - builder.append(VCARD_COL_SEPARATOR); - } else { - // Note: There is no appropriate property for expressing - // phonetic name in vCard 2.1, while there is in - // vCard 3.0 (SORT-STRING). - // We chose to use DoCoMo's way since it is supported by - // a lot of Japanese mobile phones. This is "X-" property, so - // any parser hopefully would not get confused with this. - builder.append(VCARD_PROPERTY_SOUND); - builder.append(VCARD_ATTR_SEPARATOR); - builder.append(Constants.ATTR_TYPE_X_IRMC_N); - - boolean reallyUseQuotedPrintable = - (mUsesQPToPrimaryProperties && - !(VCardUtils.containsOnlyNonCrLfPrintableAscii( - phoneticFamilyName) && - VCardUtils.containsOnlyNonCrLfPrintableAscii( - phoneticMiddleName) && - VCardUtils.containsOnlyNonCrLfPrintableAscii( - phoneticGivenName))); - - final String encodedPhoneticFamilyName; - final String encodedPhoneticMiddleName; - final String encodedPhoneticGivenName; - if (reallyUseQuotedPrintable) { - encodedPhoneticFamilyName = encodeQuotedPrintable(phoneticFamilyName); - encodedPhoneticMiddleName = encodeQuotedPrintable(phoneticMiddleName); - encodedPhoneticGivenName = encodeQuotedPrintable(phoneticGivenName); - } else { - encodedPhoneticFamilyName = escapeCharacters(phoneticFamilyName); - encodedPhoneticMiddleName = escapeCharacters(phoneticMiddleName); - encodedPhoneticGivenName = escapeCharacters(phoneticGivenName); - } - - if (shouldAppendCharsetAttribute(Arrays.asList( - encodedPhoneticFamilyName, encodedPhoneticMiddleName, - encodedPhoneticGivenName))) { - builder.append(VCARD_ATTR_SEPARATOR); - builder.append(mVCardAttributeCharset); - } - builder.append(VCARD_DATA_SEPARATOR); - builder.append(encodedPhoneticFamilyName); - builder.append(VCARD_ITEM_SEPARATOR); - builder.append(encodedPhoneticGivenName); - builder.append(VCARD_ITEM_SEPARATOR); - builder.append(encodedPhoneticMiddleName); - builder.append(VCARD_ITEM_SEPARATOR); - builder.append(VCARD_ITEM_SEPARATOR); - builder.append(VCARD_COL_SEPARATOR); - } - } else if (mIsDoCoMo) { - builder.append(VCARD_PROPERTY_SOUND); - builder.append(VCARD_ATTR_SEPARATOR); - builder.append(Constants.ATTR_TYPE_X_IRMC_N); - builder.append(VCARD_DATA_SEPARATOR); - builder.append(VCARD_ITEM_SEPARATOR); - builder.append(VCARD_ITEM_SEPARATOR); - builder.append(VCARD_ITEM_SEPARATOR); - builder.append(VCARD_ITEM_SEPARATOR); - builder.append(VCARD_COL_SEPARATOR); - } - - if (mUsesDefactProperty) { - if (!TextUtils.isEmpty(phoneticGivenName)) { - final boolean reallyUseQuotedPrintable = - (mUsesQPToPrimaryProperties && - !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticGivenName)); - final String encodedPhoneticGivenName; - if (reallyUseQuotedPrintable) { - encodedPhoneticGivenName = encodeQuotedPrintable(phoneticGivenName); - } else { - encodedPhoneticGivenName = escapeCharacters(phoneticGivenName); - } - builder.append(VCARD_PROPERTY_X_PHONETIC_FIRST_NAME); - if (shouldAppendCharsetAttribute(encodedPhoneticGivenName)) { - builder.append(VCARD_ATTR_SEPARATOR); - builder.append(mVCardAttributeCharset); - } - if (reallyUseQuotedPrintable) { - builder.append(VCARD_ATTR_SEPARATOR); - builder.append(VCARD_ATTR_ENCODING_QP); - } - builder.append(VCARD_DATA_SEPARATOR); - builder.append(encodedPhoneticGivenName); - builder.append(VCARD_COL_SEPARATOR); - } - if (!TextUtils.isEmpty(phoneticMiddleName)) { - final boolean reallyUseQuotedPrintable = - (mUsesQPToPrimaryProperties && - !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticMiddleName)); - final String encodedPhoneticMiddleName; - if (reallyUseQuotedPrintable) { - encodedPhoneticMiddleName = encodeQuotedPrintable(phoneticMiddleName); - } else { - encodedPhoneticMiddleName = escapeCharacters(phoneticMiddleName); - } - builder.append(VCARD_PROPERTY_X_PHONETIC_MIDDLE_NAME); - if (shouldAppendCharsetAttribute(encodedPhoneticMiddleName)) { - builder.append(VCARD_ATTR_SEPARATOR); - builder.append(mVCardAttributeCharset); - } - if (reallyUseQuotedPrintable) { - builder.append(VCARD_ATTR_SEPARATOR); - builder.append(VCARD_ATTR_ENCODING_QP); - } - builder.append(VCARD_DATA_SEPARATOR); - builder.append(encodedPhoneticMiddleName); - builder.append(VCARD_COL_SEPARATOR); - } - if (!TextUtils.isEmpty(phoneticFamilyName)) { - final boolean reallyUseQuotedPrintable = - (mUsesQPToPrimaryProperties && - !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticFamilyName)); - final String encodedPhoneticFamilyName; - if (reallyUseQuotedPrintable) { - encodedPhoneticFamilyName = encodeQuotedPrintable(phoneticFamilyName); - } else { - encodedPhoneticFamilyName = escapeCharacters(phoneticFamilyName); - } - builder.append(VCARD_PROPERTY_X_PHONETIC_LAST_NAME); - if (shouldAppendCharsetAttribute(encodedPhoneticFamilyName)) { - builder.append(VCARD_ATTR_SEPARATOR); - builder.append(mVCardAttributeCharset); - } - if (reallyUseQuotedPrintable) { - builder.append(VCARD_ATTR_SEPARATOR); - builder.append(VCARD_ATTR_ENCODING_QP); - } - builder.append(VCARD_DATA_SEPARATOR); - builder.append(encodedPhoneticFamilyName); - builder.append(VCARD_COL_SEPARATOR); - } - } - } - - private void appendNickNames(final StringBuilder builder, - final Map<String, List<ContentValues>> contentValuesListMap) { - final List<ContentValues> contentValuesList = contentValuesListMap - .get(Nickname.CONTENT_ITEM_TYPE); - if (contentValuesList != null) { - final String propertyNickname; - if (mIsV30) { - propertyNickname = VCARD_PROPERTY_NICKNAME; - } else if (mUsesAndroidProperty) { - propertyNickname = VCARD_PROPERTY_X_NICKNAME; - } else { - // There's no way to add this field. - return; - } - - for (ContentValues contentValues : contentValuesList) { - final String nickname = contentValues.getAsString(Nickname.NAME); - if (TextUtils.isEmpty(nickname)) { - continue; - } - - final String encodedNickname; - final boolean reallyUseQuotedPrintable = - (mUsesQuotedPrintable && - !VCardUtils.containsOnlyNonCrLfPrintableAscii(nickname)); - if (reallyUseQuotedPrintable) { - encodedNickname = encodeQuotedPrintable(nickname); - } else { - encodedNickname = escapeCharacters(nickname); - } - - builder.append(propertyNickname); - if (shouldAppendCharsetAttribute(propertyNickname)) { - builder.append(VCARD_ATTR_SEPARATOR); - builder.append(mVCardAttributeCharset); - } - if (reallyUseQuotedPrintable) { - builder.append(VCARD_ATTR_SEPARATOR); - builder.append(VCARD_ATTR_ENCODING_QP); - } - builder.append(VCARD_DATA_SEPARATOR); - builder.append(encodedNickname); - builder.append(VCARD_COL_SEPARATOR); - } - } - } - - private void appendPhones(final StringBuilder builder, - final Map<String, List<ContentValues>> contentValuesListMap) { - final List<ContentValues> contentValuesList = contentValuesListMap - .get(Phone.CONTENT_ITEM_TYPE); - boolean phoneLineExists = false; - if (contentValuesList != null) { - Set<String> phoneSet = new HashSet<String>(); - for (ContentValues contentValues : contentValuesList) { - final Integer typeAsObject = contentValues.getAsInteger(Phone.TYPE); - final String label = contentValues.getAsString(Phone.LABEL); - String phoneNumber = contentValues.getAsString(Phone.NUMBER); - if (phoneNumber != null) { - phoneNumber = phoneNumber.trim(); - } - if (TextUtils.isEmpty(phoneNumber)) { - continue; - } - int type = (typeAsObject != null ? typeAsObject : Phone.TYPE_HOME); - - phoneLineExists = true; - if (type == Phone.TYPE_PAGER) { - phoneLineExists = true; - if (!phoneSet.contains(phoneNumber)) { - phoneSet.add(phoneNumber); - appendVCardTelephoneLine(builder, type, label, phoneNumber); - } - } else { - // The entry "may" have several phone numbers when the contact entry is - // corrupted because of its original source. - // - // e.g. I encountered the entry like the following. - // "111-222-3333 (Miami)\n444-555-6666 (Broward; 305-653-6796 (Miami); ..." - // This kind of entry is not able to be inserted via Android devices, but - // possible if the source of the data is already corrupted. - List<String> phoneNumberList = splitIfSeveralPhoneNumbersExist(phoneNumber); - if (phoneNumberList.isEmpty()) { - continue; - } - phoneLineExists = true; - for (String actualPhoneNumber : phoneNumberList) { - if (!phoneSet.contains(actualPhoneNumber)) { - final int format = VCardUtils.getPhoneNumberFormat(mVCardType); - SpannableStringBuilder tmpBuilder = - new SpannableStringBuilder(actualPhoneNumber); - PhoneNumberUtils.formatNumber(tmpBuilder, format); - final String formattedPhoneNumber = tmpBuilder.toString(); - phoneSet.add(actualPhoneNumber); - appendVCardTelephoneLine(builder, type, label, formattedPhoneNumber); - } - } - } - } - } - - if (!phoneLineExists && mIsDoCoMo) { - appendVCardTelephoneLine(builder, Phone.TYPE_HOME, "", ""); - } - } - - private List<String> splitIfSeveralPhoneNumbersExist(final String phoneNumber) { - List<String> phoneList = new ArrayList<String>(); - - StringBuilder builder = new StringBuilder(); - final int length = phoneNumber.length(); - for (int i = 0; i < length; i++) { - final char ch = phoneNumber.charAt(i); - if (Character.isDigit(ch)) { - builder.append(ch); - } else if ((ch == ';' || ch == '\n') && builder.length() > 0) { - phoneList.add(builder.toString()); - builder = new StringBuilder(); - } - } - if (builder.length() > 0) { - phoneList.add(builder.toString()); - } - - return phoneList; - } - - private void appendEmails(final StringBuilder builder, - final Map<String, List<ContentValues>> contentValuesListMap) { - final List<ContentValues> contentValuesList = contentValuesListMap - .get(Email.CONTENT_ITEM_TYPE); - boolean emailAddressExists = false; - if (contentValuesList != null) { - Set<String> addressSet = new HashSet<String>(); - for (ContentValues contentValues : contentValuesList) { - Integer typeAsObject = contentValues.getAsInteger(Email.TYPE); - final int type = (typeAsObject != null ? - typeAsObject : Email.TYPE_OTHER); - final String label = contentValues.getAsString(Email.LABEL); - String emailAddress = contentValues.getAsString(Email.DATA); - if (emailAddress != null) { - emailAddress = emailAddress.trim(); - } - if (TextUtils.isEmpty(emailAddress)) { - continue; - } - emailAddressExists = true; - if (!addressSet.contains(emailAddress)) { - addressSet.add(emailAddress); - appendVCardEmailLine(builder, type, label, emailAddress); - } - } - } - - if (!emailAddressExists && mIsDoCoMo) { - appendVCardEmailLine(builder, Email.TYPE_HOME, "", ""); - } - } - - private void appendPostals(final StringBuilder builder, - final Map<String, List<ContentValues>> contentValuesListMap) { - final List<ContentValues> contentValuesList = contentValuesListMap - .get(StructuredPostal.CONTENT_ITEM_TYPE); - if (contentValuesList != null) { - if (mIsDoCoMo) { - appendPostalsForDoCoMo(builder, contentValuesList); - } else { - appendPostalsForGeneric(builder, contentValuesList); - } - } else if (mIsDoCoMo) { - builder.append(VCARD_PROPERTY_ADR); - builder.append(VCARD_ATTR_SEPARATOR); - builder.append(Constants.ATTR_TYPE_HOME); - builder.append(VCARD_DATA_SEPARATOR); - builder.append(VCARD_COL_SEPARATOR); - } - } - /** - * Tries to append just one line. If there's no appropriate address - * information, append an empty line. + * This static function is to compose vCard for phone own number */ - private void appendPostalsForDoCoMo(final StringBuilder builder, - final List<ContentValues> contentValuesList) { - // TODO: from old, inefficient code. fix this. - if (appendPostalsForDoCoMoInternal(builder, contentValuesList, - StructuredPostal.TYPE_HOME)) { - return; - } - if (appendPostalsForDoCoMoInternal(builder, contentValuesList, - StructuredPostal.TYPE_WORK)) { - return; - } - if (appendPostalsForDoCoMoInternal(builder, contentValuesList, - StructuredPostal.TYPE_OTHER)) { - return; - } - if (appendPostalsForDoCoMoInternal(builder, contentValuesList, - StructuredPostal.TYPE_CUSTOM)) { - return; - } - - Log.w(LOG_TAG, - "Should not come here. Must have at least one postal data."); - } - - private boolean appendPostalsForDoCoMoInternal(final StringBuilder builder, - final List<ContentValues> contentValuesList, Integer preferedType) { - for (ContentValues contentValues : contentValuesList) { - final Integer type = contentValues.getAsInteger(StructuredPostal.TYPE); - final String label = contentValues.getAsString(StructuredPostal.LABEL); - if (type == preferedType) { - appendVCardPostalLine(builder, type, label, contentValues); - return true; - } - } - return false; - } - - private void appendPostalsForGeneric(final StringBuilder builder, - final List<ContentValues> contentValuesList) { - for (ContentValues contentValues : contentValuesList) { - final Integer type = contentValues.getAsInteger(StructuredPostal.TYPE); - final String label = contentValues.getAsString(StructuredPostal.LABEL); - if (type != null) { - appendVCardPostalLine(builder, type, label, contentValues); - } - } - } - - private void appendIms(final StringBuilder builder, - final Map<String, List<ContentValues>> contentValuesListMap) { - final List<ContentValues> contentValuesList = contentValuesListMap - .get(Im.CONTENT_ITEM_TYPE); - if (contentValuesList != null) { - for (ContentValues contentValues : contentValuesList) { - Integer protocol = contentValues.getAsInteger(Im.PROTOCOL); - String data = contentValues.getAsString(Im.DATA); - if (data != null) { - data = data.trim(); - } - if (TextUtils.isEmpty(data)) { - continue; - } - - if (protocol != null && protocol == Im.PROTOCOL_GOOGLE_TALK) { - if (VCardConfig.usesAndroidSpecificProperty(mVCardType)) { - appendVCardLine(builder, Constants.PROPERTY_X_GOOGLE_TALK, data); - } - // TODO: add "X-GOOGLE TALK" case... - } - } - } - } - - private void appendWebsites(final StringBuilder builder, - final Map<String, List<ContentValues>> contentValuesListMap) { - final List<ContentValues> contentValuesList = contentValuesListMap - .get(Website.CONTENT_ITEM_TYPE); - if (contentValuesList != null) { - for (ContentValues contentValues : contentValuesList) { - String website = contentValues.getAsString(Website.URL); - if (website != null) { - website = website.trim(); - } - if (!TextUtils.isEmpty(website)) { - appendVCardLine(builder, VCARD_PROPERTY_URL, website); - } - } - } - } - - private void appendBirthday(final StringBuilder builder, - final Map<String, List<ContentValues>> contentValuesListMap) { - final List<ContentValues> contentValuesList = contentValuesListMap - .get(Event.CONTENT_ITEM_TYPE); - if (contentValuesList != null && contentValuesList.size() > 0) { - Integer eventType = contentValuesList.get(0).getAsInteger(Event.TYPE); - if (eventType == null || !eventType.equals(Event.TYPE_BIRTHDAY)) { - return; - } - // Theoretically, there must be only one birthday for each vCard data and - // we are afraid of some parse error occuring in some devices, so - // we emit only one birthday entry for now. - String birthday = contentValuesList.get(0).getAsString(Event.START_DATE); - if (birthday != null) { - birthday = birthday.trim(); - } - if (!TextUtils.isEmpty(birthday)) { - appendVCardLine(builder, VCARD_PROPERTY_BIRTHDAY, birthday); - } - } - } - - private void appendOrganizations(final StringBuilder builder, - final Map<String, List<ContentValues>> contentValuesListMap) { - final List<ContentValues> contentValuesList = contentValuesListMap - .get(Organization.CONTENT_ITEM_TYPE); - if (contentValuesList != null) { - for (ContentValues contentValues : contentValuesList) { - String company = contentValues - .getAsString(Organization.COMPANY); - if (company != null) { - company = company.trim(); - } - String title = contentValues - .getAsString(Organization.TITLE); - if (title != null) { - title = title.trim(); - } - - if (!TextUtils.isEmpty(company)) { - appendVCardLine(builder, VCARD_PROPERTY_ORG, company, - !VCardUtils.containsOnlyPrintableAscii(company), - (mUsesQuotedPrintable && - !VCardUtils.containsOnlyNonCrLfPrintableAscii(company))); - } - if (!TextUtils.isEmpty(title)) { - appendVCardLine(builder, VCARD_PROPERTY_TITLE, title, - !VCardUtils.containsOnlyPrintableAscii(title), - (mUsesQuotedPrintable && - !VCardUtils.containsOnlyNonCrLfPrintableAscii(title))); - } - } + public String composeVCardForPhoneOwnNumber(int phonetype, String phoneName, + String phoneNumber, boolean vcardVer21) { + final int vcardType = (vcardVer21 ? + VCardConfig.VCARD_TYPE_V21_GENERIC_UTF8 : + VCardConfig.VCARD_TYPE_V30_GENERIC_UTF8); + final VCardBuilder builder = new VCardBuilder(vcardType); + boolean needCharset = false; + if (!(VCardUtils.containsOnlyPrintableAscii(phoneName))) { + needCharset = true; } - } + builder.appendLine(VCardConstants.PROPERTY_FN, phoneName, needCharset, false); + builder.appendLine(VCardConstants.PROPERTY_N, phoneName, needCharset, false); - private void appendPhotos(final StringBuilder builder, - final Map<String, List<ContentValues>> contentValuesListMap) { - final List<ContentValues> contentValuesList = contentValuesListMap - .get(Photo.CONTENT_ITEM_TYPE); - if (contentValuesList != null) { - for (ContentValues contentValues : contentValuesList) { - byte[] data = contentValues.getAsByteArray(Photo.PHOTO); - if (data == null) { - continue; - } - final String photoType; - // Use some heuristics for guessing the format of the image. - // TODO: there should be some general API for detecting the file format. - if (data.length >= 3 && data[0] == 'G' && data[1] == 'I' - && data[2] == 'F') { - photoType = "GIF"; - } else if (data.length >= 4 && data[0] == (byte) 0x89 - && data[1] == 'P' && data[2] == 'N' && data[3] == 'G') { - // Note: vCard 2.1 officially does not support PNG, but we - // may have it - // and using X- word like "X-PNG" may not let importers know - // it is - // PNG. So we use the String "PNG" as is... - photoType = "PNG"; - } else if (data.length >= 2 && data[0] == (byte) 0xff - && data[1] == (byte) 0xd8) { - photoType = "JPEG"; - } else { - Log.d(LOG_TAG, "Unknown photo type. Ignore."); - continue; - } - final String photoString = VCardUtils.encodeBase64(data); - if (photoString.length() > 0) { - appendVCardPhotoLine(builder, photoString, photoType); - } - } + if (!TextUtils.isEmpty(phoneNumber)) { + String label = Integer.toString(phonetype); + builder.appendTelLine(phonetype, label, phoneNumber, false); } - } - private void appendNotes(final StringBuilder builder, - final Map<String, List<ContentValues>> contentValuesListMap) { - final List<ContentValues> contentValuesList = - contentValuesListMap.get(Note.CONTENT_ITEM_TYPE); - if (contentValuesList != null) { - if (mOnlyOneNoteFieldIsAvailable) { - StringBuilder noteBuilder = new StringBuilder(); - boolean first = true; - for (ContentValues contentValues : contentValuesList) { - String note = contentValues.getAsString(Note.NOTE); - if (note == null) { - note = ""; - } - if (note.length() > 0) { - if (first) { - first = false; - } else { - noteBuilder.append('\n'); - } - noteBuilder.append(note); - } - } - final String noteStr = noteBuilder.toString(); - // This means we scan noteStr completely twice, which is redundant. - // But for now, we assume this is not so time-consuming.. - final boolean shouldAppendCharsetInfo = - !VCardUtils.containsOnlyPrintableAscii(noteStr); - final boolean reallyUseQuotedPrintable = - (mUsesQuotedPrintable && - !VCardUtils.containsOnlyNonCrLfPrintableAscii(noteStr)); - appendVCardLine(builder, VCARD_PROPERTY_NOTE, noteStr, - shouldAppendCharsetInfo, reallyUseQuotedPrintable); - } else { - for (ContentValues contentValues : contentValuesList) { - final String noteStr = contentValues.getAsString(Note.NOTE); - if (!TextUtils.isEmpty(noteStr)) { - final boolean shouldAppendCharsetInfo = - !VCardUtils.containsOnlyPrintableAscii(noteStr); - final boolean reallyUseQuotedPrintable = - (mUsesQuotedPrintable && - !VCardUtils.containsOnlyNonCrLfPrintableAscii(noteStr)); - appendVCardLine(builder, VCARD_PROPERTY_NOTE, noteStr, - shouldAppendCharsetInfo, reallyUseQuotedPrintable); - } - } - } - } + return builder.toString(); } /** - * Append '\' to the characters which should be escaped. The character set is different - * not only between vCard 2.1 and vCard 3.0 but also among each device. - * - * Note that Quoted-Printable string must not be input here. + * Format according to RFC 2445 DATETIME type. + * The format is: ("%Y%m%dT%H%M%SZ"). */ - @SuppressWarnings("fallthrough") - private String escapeCharacters(final String unescaped) { - if (TextUtils.isEmpty(unescaped)) { - return ""; - } - - final StringBuilder tmpBuilder = new StringBuilder(); - final int length = unescaped.length(); - for (int i = 0; i < length; i++) { - char ch = unescaped.charAt(i); - switch (ch) { - case ';': { - tmpBuilder.append('\\'); - tmpBuilder.append(';'); - break; - } - case '\r': { - if (i + 1 < length) { - char nextChar = unescaped.charAt(i); - if (nextChar == '\n') { - continue; - } else { - // fall through - } - } else { - // fall through - } - } - case '\n': { - // In vCard 2.1, there's no specification about this, while - // vCard 3.0 explicitly requires this should be encoded to "\n". - tmpBuilder.append("\\n"); - break; - } - case '\\': { - if (mIsV30) { - tmpBuilder.append("\\\\"); - break; - } else { - // fall through - } - } - case '<': - case '>': { - if (mIsDoCoMo) { - tmpBuilder.append('\\'); - tmpBuilder.append(ch); - } else { - tmpBuilder.append(ch); - } - break; - } - case ',': { - if (mIsV30) { - tmpBuilder.append("\\,"); - } else { - tmpBuilder.append(ch); - } - break; - } - default: { - tmpBuilder.append(ch); - break; - } - } - } - return tmpBuilder.toString(); - } - - private void appendVCardPhotoLine(final StringBuilder builder, - final String encodedData, final String photoType) { - StringBuilder tmpBuilder = new StringBuilder(); - tmpBuilder.append(VCARD_PROPERTY_PHOTO); - tmpBuilder.append(VCARD_ATTR_SEPARATOR); - if (mIsV30) { - tmpBuilder.append(VCARD_ATTR_ENCODING_BASE64_V30); - } else { - tmpBuilder.append(VCARD_ATTR_ENCODING_BASE64_V21); - } - tmpBuilder.append(VCARD_ATTR_SEPARATOR); - appendTypeAttribute(tmpBuilder, photoType); - tmpBuilder.append(VCARD_DATA_SEPARATOR); - tmpBuilder.append(encodedData); - - final String tmpStr = tmpBuilder.toString(); - tmpBuilder = new StringBuilder(); - int lineCount = 0; - int length = tmpStr.length(); - for (int i = 0; i < length; i++) { - tmpBuilder.append(tmpStr.charAt(i)); - lineCount++; - if (lineCount > 72) { - tmpBuilder.append(VCARD_COL_SEPARATOR); - tmpBuilder.append(VCARD_WS); - lineCount = 0; - } - } - builder.append(tmpBuilder.toString()); - builder.append(VCARD_COL_SEPARATOR); - builder.append(VCARD_COL_SEPARATOR); - } - - private void appendVCardPostalLine(final StringBuilder builder, - final Integer typeAsObject, final String label, - final ContentValues contentValues) { - builder.append(VCARD_PROPERTY_ADR); - builder.append(VCARD_ATTR_SEPARATOR); - - // Note: Not sure why we need to emit "empty" line even when actual data does not exist. - // There may be some reason or may not be any. We keep safer side. - // TODO: investigate this. - boolean dataExists = false; - String[] dataArray = VCardUtils.getVCardPostalElements(contentValues); - boolean actuallyUseQuotedPrintable = false; - boolean shouldAppendCharset = false; - for (String data : dataArray) { - if (!TextUtils.isEmpty(data)) { - dataExists = true; - if (!shouldAppendCharset && !VCardUtils.containsOnlyPrintableAscii(data)) { - shouldAppendCharset = true; - } - if (mUsesQuotedPrintable && !VCardUtils.containsOnlyNonCrLfPrintableAscii(data)) { - actuallyUseQuotedPrintable = true; - break; - } - } - } - - int length = dataArray.length; - for (int i = 0; i < length; i++) { - String data = dataArray[i]; - if (!TextUtils.isEmpty(data)) { - if (actuallyUseQuotedPrintable) { - dataArray[i] = encodeQuotedPrintable(data); - } else { - dataArray[i] = escapeCharacters(data); - } - } - } - - final int typeAsPrimitive; - if (typeAsObject == null) { - typeAsPrimitive = StructuredPostal.TYPE_OTHER; - } else { - typeAsPrimitive = typeAsObject; - } - - String typeAsString = null; - switch (typeAsPrimitive) { - case StructuredPostal.TYPE_HOME: { - typeAsString = Constants.ATTR_TYPE_HOME; - break; - } - case StructuredPostal.TYPE_WORK: { - typeAsString = Constants.ATTR_TYPE_WORK; - break; - } - case StructuredPostal.TYPE_CUSTOM: { - if (mUsesAndroidProperty && !TextUtils.isEmpty(label) - && VCardUtils.containsOnlyAlphaDigitHyphen(label)) { - // We're not sure whether the label is valid in the spec - // ("IANA-token" in the vCard 3.0 is unclear...) - // Just for safety, we add "X-" at the beggining of each label. - // Also checks the label obeys with vCard 3.0 spec. - builder.append("X-"); - builder.append(label); - builder.append(VCARD_DATA_SEPARATOR); - } - break; - } - case StructuredPostal.TYPE_OTHER: { - break; - } - default: { - Log.e(LOG_TAG, "Unknown StructuredPostal type: " + typeAsPrimitive); - break; - } - } - - // Attribute(s). - - { - boolean shouldAppendAttrSeparator = false; - if (typeAsString != null) { - appendTypeAttribute(builder, typeAsString); - shouldAppendAttrSeparator = true; - } - - if (dataExists) { - if (shouldAppendCharset) { - // Strictly, vCard 3.0 does not allow exporters to emit charset information, - // but we will add it since the information should be useful for importers, - // - // Assume no parser does not emit error with this attribute in vCard 3.0. - if (shouldAppendAttrSeparator) { - builder.append(VCARD_ATTR_SEPARATOR); - } - builder.append(mVCardAttributeCharset); - shouldAppendAttrSeparator = true; - } - - if (actuallyUseQuotedPrintable) { - if (shouldAppendAttrSeparator) { - builder.append(VCARD_ATTR_SEPARATOR); - } - builder.append(VCARD_ATTR_ENCODING_QP); - shouldAppendAttrSeparator = true; - } - } - } - - // Property values. - - builder.append(VCARD_DATA_SEPARATOR); - if (dataExists) { - // The elements in dataArray are already encoded to quoted printable - // if needed. - // See above. - // - // TODO: in vCard 3.0, one line may become too huge. Fix this. - builder.append(dataArray[0]); - builder.append(VCARD_ITEM_SEPARATOR); - builder.append(dataArray[1]); - builder.append(VCARD_ITEM_SEPARATOR); - builder.append(dataArray[2]); - builder.append(VCARD_ITEM_SEPARATOR); - builder.append(dataArray[3]); - builder.append(VCARD_ITEM_SEPARATOR); - builder.append(dataArray[4]); - builder.append(VCARD_ITEM_SEPARATOR); - builder.append(dataArray[5]); - builder.append(VCARD_ITEM_SEPARATOR); - builder.append(dataArray[6]); - } - builder.append(VCARD_COL_SEPARATOR); + private final String toRfc2455Format(final long millSecs) { + Time startDate = new Time(); + startDate.set(millSecs); + String date = startDate.format2445(); + return date + FLAG_TIMEZONE_UTC; } - private void appendVCardEmailLine(final StringBuilder builder, - final Integer typeAsObject, final String label, final String data) { - builder.append(VCARD_PROPERTY_EMAIL); - - final int typeAsPrimitive; - if (typeAsObject == null) { - typeAsPrimitive = Email.TYPE_OTHER; - } else { - typeAsPrimitive = typeAsObject; - } - - final String typeAsString; - switch (typeAsPrimitive) { - case Email.TYPE_CUSTOM: { - // For backward compatibility. - // Detail: Until Donut, there isn't TYPE_MOBILE for email while there is now. - // To support mobile type at that time, this custom label had been used. - if (android.provider.Contacts.ContactMethodsColumns.MOBILE_EMAIL_TYPE_NAME - .equals(label)) { - typeAsString = Constants.ATTR_TYPE_CELL; - } else if (mUsesAndroidProperty && !TextUtils.isEmpty(label) - && VCardUtils.containsOnlyAlphaDigitHyphen(label)) { - typeAsString = "X-" + label; - } else { - typeAsString = DEFAULT_EMAIL_TYPE; - } - break; - } - case Email.TYPE_HOME: { - typeAsString = Constants.ATTR_TYPE_HOME; - break; - } - case Email.TYPE_WORK: { - typeAsString = Constants.ATTR_TYPE_WORK; + /** + * Try to append the property line for a call history time stamp field if possible. + * Do nothing if the call log type gotton from the database is invalid. + */ + private void tryAppendCallHistoryTimeStampField(final VCardBuilder builder) { + // Extension for call history as defined in + // in the Specification for Ic Mobile Communcation - ver 1.1, + // Oct 2000. This is used to send the details of the call + // history - missed, incoming, outgoing along with date and time + // to the requesting device (For example, transferring phone book + // when connected over bluetooth) + // + // e.g. "X-IRMC-CALL-DATETIME;MISSED:20050320T100000Z" + final int callLogType = mCursor.getInt(CALL_TYPE_COLUMN_INDEX); + final String callLogTypeStr; + switch (callLogType) { + case Calls.INCOMING_TYPE: { + callLogTypeStr = VCARD_PROPERTY_CALLTYPE_INCOMING; break; } - case Email.TYPE_OTHER: { - typeAsString = DEFAULT_EMAIL_TYPE; + case Calls.OUTGOING_TYPE: { + callLogTypeStr = VCARD_PROPERTY_CALLTYPE_OUTGOING; break; } - case Email.TYPE_MOBILE: { - typeAsString = Constants.ATTR_TYPE_CELL; + case Calls.MISSED_TYPE: { + callLogTypeStr = VCARD_PROPERTY_CALLTYPE_MISSED; break; } default: { - Log.e(LOG_TAG, "Unknown Email type: " + typeAsPrimitive); - typeAsString = DEFAULT_EMAIL_TYPE; - break; - } - } - - builder.append(VCARD_ATTR_SEPARATOR); - appendTypeAttribute(builder, typeAsString); - builder.append(VCARD_DATA_SEPARATOR); - builder.append(data); - builder.append(VCARD_COL_SEPARATOR); - } - - private void appendVCardTelephoneLine(final StringBuilder builder, - final Integer typeAsObject, final String label, - String encodedData) { - builder.append(VCARD_PROPERTY_TEL); - builder.append(VCARD_ATTR_SEPARATOR); - - final int typeAsPrimitive; - if (typeAsObject == null) { - typeAsPrimitive = Phone.TYPE_OTHER; - } else { - typeAsPrimitive = typeAsObject; - } - - switch (typeAsPrimitive) { - case Phone.TYPE_HOME: - appendTypeAttributes(builder, Arrays.asList( - Constants.ATTR_TYPE_HOME, Constants.ATTR_TYPE_VOICE)); - break; - case Phone.TYPE_WORK: - appendTypeAttributes(builder, Arrays.asList( - Constants.ATTR_TYPE_WORK, Constants.ATTR_TYPE_VOICE)); - break; - case Phone.TYPE_FAX_HOME: - appendTypeAttributes(builder, Arrays.asList( - Constants.ATTR_TYPE_HOME, Constants.ATTR_TYPE_FAX)); - break; - case Phone.TYPE_FAX_WORK: - appendTypeAttributes(builder, Arrays.asList( - Constants.ATTR_TYPE_WORK, Constants.ATTR_TYPE_FAX)); - break; - case Phone.TYPE_MOBILE: - builder.append(Constants.ATTR_TYPE_CELL); - break; - case Phone.TYPE_PAGER: - if (mIsDoCoMo) { - // Not sure about the reason, but previous implementation had - // used "VOICE" instead of "PAGER" - // Also, refrain from using appendType() so that "TYPE=" is never be appended. - builder.append(Constants.ATTR_TYPE_VOICE); - } else { - appendTypeAttribute(builder, Constants.ATTR_TYPE_PAGER); - } - break; - case Phone.TYPE_OTHER: - appendTypeAttribute(builder, Constants.ATTR_TYPE_VOICE); - break; - case Phone.TYPE_CUSTOM: - if (mUsesAndroidProperty && !TextUtils.isEmpty(label) - && VCardUtils.containsOnlyAlphaDigitHyphen(label)) { - appendTypeAttribute(builder, "X-" + label); - } else { - // Just ignore the custom type. - appendTypeAttribute(builder, Constants.ATTR_TYPE_VOICE); - } - break; - default: - appendUncommonPhoneType(builder, typeAsPrimitive); - break; - } - - builder.append(VCARD_DATA_SEPARATOR); - builder.append(encodedData); - builder.append(VCARD_COL_SEPARATOR); - } - - /** - * Appends phone type string which may not be available in some devices. - */ - private void appendUncommonPhoneType(final StringBuilder builder, final Integer type) { - if (mIsDoCoMo) { - // The previous implementation for DoCoMo had been conservative - // about miscellaneous types. - builder.append(Constants.ATTR_TYPE_VOICE); - } else { - String phoneAttribute = VCardUtils.getPhoneAttributeString(type); - if (phoneAttribute != null) { - appendTypeAttribute(builder, phoneAttribute); - } else { - Log.e(LOG_TAG, "Unknown or unsupported (by vCard) Phone type: " + type); - } - } - } - - private void appendVCardLine(final StringBuilder builder, - final String propertyName, final String rawData) { - appendVCardLine(builder, propertyName, rawData, false, false); - } - - private void appendVCardLine(final StringBuilder builder, - final String field, final String rawData, final boolean needCharset, - boolean needQuotedPrintable) { - builder.append(field); - if (needCharset) { - builder.append(VCARD_ATTR_SEPARATOR); - builder.append(mVCardAttributeCharset); - } - - final String encodedData; - if (needQuotedPrintable) { - builder.append(VCARD_ATTR_SEPARATOR); - builder.append(VCARD_ATTR_ENCODING_QP); - encodedData = encodeQuotedPrintable(rawData); - } else { - // TODO: one line may be too huge, which may be invalid in vCard spec, though - // several (even well-known) applications do not care this. - encodedData = escapeCharacters(rawData); - } - - builder.append(VCARD_DATA_SEPARATOR); - builder.append(encodedData); - builder.append(VCARD_COL_SEPARATOR); - } - - private void appendTypeAttributes(final StringBuilder builder, - final List<String> types) { - // We may have to make this comma separated form like "TYPE=DOM,WORK" in the future, - // which would be recommended way in vcard 3.0 though not valid in vCard 2.1. - boolean first = true; - for (String type : types) { - if (first) { - first = false; - } else { - builder.append(VCARD_ATTR_SEPARATOR); + Log.w(LOG_TAG, "Call log type not correct."); + return; } - appendTypeAttribute(builder, type); - } - } - - private void appendTypeAttribute(final StringBuilder builder, final String type) { - // Note: In vCard 3.0, Type strings also can be like this: "TYPE=HOME,PREF" - if (mIsV30) { - builder.append(Constants.ATTR_TYPE).append(VCARD_ATTR_EQUAL); } - builder.append(type); - } - - /** - * Returns true when the property line should contain charset attribute - * information. This method may return true even when vCard version is 3.0. - * - * Strictly, adding charset information is invalid in VCard 3.0. - * However we'll add the info only when used charset is not UTF-8 - * in vCard 3.0 format, since parser side may be able to use the charset - * via this field, though we may encounter another problem by adding it... - * - * e.g. Japanese mobile phones use Shift_Jis while RFC 2426 - * recommends UTF-8. By adding this field, parsers may be able - * to know this text is NOT UTF-8 but Shift_Jis. - */ - private boolean shouldAppendCharsetAttribute(final String propertyValue) { - return (!VCardUtils.containsOnlyPrintableAscii(propertyValue) && - (!mIsV30 || !mUsesUtf8)); - } - private boolean shouldAppendCharsetAttribute(final List<String> propertyValueList) { - boolean shouldAppendBasically = false; - for (String propertyValue : propertyValueList) { - if (!VCardUtils.containsOnlyPrintableAscii(propertyValue)) { - shouldAppendBasically = true; - break; - } - } - return shouldAppendBasically && (!mIsV30 || !mUsesUtf8); + final long dateAsLong = mCursor.getLong(DATE_COLUMN_INDEX); + builder.appendLine(VCARD_PROPERTY_X_TIMESTAMP, + Arrays.asList(callLogTypeStr), toRfc2455Format(dateAsLong)); } - private String encodeQuotedPrintable(String str) { - if (TextUtils.isEmpty(str)) { - return ""; - } - { - // Replace "\n" and "\r" with "\r\n". - StringBuilder tmpBuilder = new StringBuilder(); - int length = str.length(); - for (int i = 0; i < length; i++) { - char ch = str.charAt(i); - if (ch == '\r') { - if (i + 1 < length && str.charAt(i + 1) == '\n') { - i++; - } - tmpBuilder.append("\r\n"); - } else if (ch == '\n') { - tmpBuilder.append("\r\n"); - } else { - tmpBuilder.append(ch); - } - } - str = tmpBuilder.toString(); + private String createOneCallLogEntryInternal() { + final VCardBuilder builder = new VCardBuilder(VCardConfig.VCARD_TYPE_V21_GENERIC_UTF8); + String name = mCursor.getString(CALLER_NAME_COLUMN_INDEX); + if (TextUtils.isEmpty(name)) { + name = mCursor.getString(NUMBER_COLUMN_INDEX); } + final boolean needCharset = !(VCardUtils.containsOnlyPrintableAscii(name)); + builder.appendLine(VCardConstants.PROPERTY_FN, name, needCharset, false); + builder.appendLine(VCardConstants.PROPERTY_N, name, needCharset, false); - final StringBuilder tmpBuilder = new StringBuilder(); - int index = 0; - int lineCount = 0; - byte[] strArray = null; - - try { - strArray = str.getBytes(mCharsetString); - } catch (UnsupportedEncodingException e) { - Log.e(LOG_TAG, "Charset " + mCharsetString + " cannot be used. " - + "Try default charset"); - strArray = str.getBytes(); - } - while (index < strArray.length) { - tmpBuilder.append(String.format("=%02X", strArray[index])); - index += 1; - lineCount += 3; - - if (lineCount >= 67) { - // Specification requires CRLF must be inserted before the - // length of the line - // becomes more than 76. - // Assuming that the next character is a multi-byte character, - // it will become - // 6 bytes. - // 76 - 6 - 3 = 67 - tmpBuilder.append("=\r\n"); - lineCount = 0; - } + final String number = mCursor.getString(NUMBER_COLUMN_INDEX); + final int type = mCursor.getInt(CALLER_NUMBERTYPE_COLUMN_INDEX); + String label = mCursor.getString(CALLER_NUMBERLABEL_COLUMN_INDEX); + if (TextUtils.isEmpty(label)) { + label = Integer.toString(type); } - - return tmpBuilder.toString(); + builder.appendTelLine(type, label, number, false); + tryAppendCallHistoryTimeStampField(builder); + return builder.toString(); } } diff --git a/core/java/android/pim/vcard/VCardConfig.java b/core/java/android/pim/vcard/VCardConfig.java index 68cd0df..ecd089a 100644 --- a/core/java/android/pim/vcard/VCardConfig.java +++ b/core/java/android/pim/vcard/VCardConfig.java @@ -15,16 +15,19 @@ */ package android.pim.vcard; +import android.util.Log; + import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Set; /** * The class representing VCard related configurations. Useful static methods are not in this class * but in VCardUtils. */ public class VCardConfig { - // TODO: may be better to make the instance of this available and stop using static methods and - // one integer. + private static final String LOG_TAG = "VCardConfig"; /* package */ static final int LOG_LEVEL_NONE = 0; /* package */ static final int LOG_LEVEL_PERFORMANCE_MEASUREMENT = 0x1; @@ -34,15 +37,18 @@ public class VCardConfig { /* package */ static final int LOG_LEVEL = LOG_LEVEL_NONE; + /* package */ static final int PARSE_TYPE_UNKNOWN = 0; + /* package */ static final int PARSE_TYPE_APPLE = 1; + /* package */ static final int PARSE_TYPE_MOBILE_PHONE_JP = 2; // For Japanese mobile phones. + /* package */ static final int PARSE_TYPE_FOMA = 3; // For Japanese FOMA mobile phones. + /* package */ static final int PARSE_TYPE_WINDOWS_MOBILE_JP = 4; + // Assumes that "iso-8859-1" is able to map "all" 8bit characters to some unicode and // decode the unicode to the original charset. If not, this setting will cause some bug. public static final String DEFAULT_CHARSET = "iso-8859-1"; - // TODO: make the other codes use this flag - public static final boolean IGNORE_CASE_EXCEPT_VALUE = true; - - private static final int FLAG_V21 = 0; - private static final int FLAG_V30 = 1; + public static final int FLAG_V21 = 0; + public static final int FLAG_V30 = 1; // 0x2 is reserved for the future use ... @@ -54,7 +60,8 @@ public class VCardConfig { // 0x10 is reserved for safety private static final int FLAG_CHARSET_UTF8 = 0; - private static final int FLAG_CHARSET_SHIFT_JIS = 0x20; + private static final int FLAG_CHARSET_SHIFT_JIS = 0x100; + private static final int FLAG_CHARSET_MASK = 0xF00; /** * The flag indicating the vCard composer will add some "X-" properties used only in Android @@ -95,96 +102,196 @@ public class VCardConfig { private static final int FLAG_DOCOMO = 0x20000000; /** - * The flag indicating the vCard composer use Quoted-Printable toward even "primary" types. - * In this context, "primary" types means "N", "FN", etc. which are usually "not" encoded - * into Quoted-Printable format in external exporters. - * This flag is useful when some target importer does not accept "primary" property values - * without Quoted-Printable encoding. - * - * @hide Temporaly made public. We don't strictly define "primary", so we may change the - * behavior around this flag in the future. Do not use this flag without any reason. + * <P> + * The flag indicating the vCard composer does "NOT" use Quoted-Printable toward "primary" + * properties even though it is required by vCard 2.1 (QP is prohibited in vCard 3.0). + * </P> + * <P> + * We actually cannot define what is the "primary" property. Note that this is NOT defined + * in vCard specification either. Also be aware that it is NOT related to "primary" notion + * used in {@link android.provider.ContactsContract}. + * This notion is just for vCard composition in Android. + * </P> + * <P> + * We added this Android-specific notion since some (incomplete) vCard exporters for vCard 2.1 + * do NOT use Quoted-Printable encoding toward some properties related names like "N", "FN", etc. + * even when their values contain non-ascii or/and CR/LF, while they use the encoding in the + * other properties like "ADR", "ORG", etc. + * <P> + * We are afraid of the case where some vCard importer also forget handling QP presuming QP is + * not used in such fields. + * </P> + * <P> + * This flag is useful when some target importer you are going to focus on does not accept + * such properties with Quoted-Printable encoding. + * </P> + * <P> + * Again, we should not use this flag at all for complying vCard 2.1 spec. + * </P> + * <P> + * In vCard 3.0, Quoted-Printable is explicitly "prohibitted", so we don't need to care this + * kind of problem (hopefully). + * </P> */ - public static final int FLAG_USE_QP_TO_PRIMARY_PROPERTIES = 0x10000000; - - // VCard types + public static final int FLAG_REFRAIN_QP_TO_NAME_PROPERTIES = 0x10000000; /** - * General vCard format with the version 2.1. Uses UTF-8 for the charset. - * When composing a vCard entry, the US convension will be used. - * + * <P> + * The flag indicating that phonetic name related fields must be converted to + * appropriate form. Note that "appropriate" is not defined in any vCard specification. + * This is Android-specific. + * </P> + * <P> + * One typical (and currently sole) example where we need this flag is the time when + * we need to emit Japanese phonetic names into vCard entries. The property values + * should be encoded into half-width katakana when the target importer is Japanese mobile + * phones', which are probably not able to parse full-width hiragana/katakana for + * historical reasons, while the vCard importers embedded to softwares for PC should be + * able to parse them as we expect. + * </P> + */ + public static final int FLAG_CONVERT_PHONETIC_NAME_STRINGS = 0x0800000; + + /** + * <P> + * The flag indicating the vCard composer "for 2.1" emits "TYPE=" string toward TYPE params + * every time possible. The default behavior does not emit it and is valid in the spec. + * In vCrad 3.0, this flag is unnecessary, since "TYPE=" is MUST in vCard 3.0 specification. + * </P> + * <P> + * Detail: + * How more than one TYPE fields are expressed is different between vCard 2.1 and vCard 3.0. + * </p> + * <P> + * e.g.<BR /> + * 1) Probably valid in both vCard 2.1 and vCard 3.0: "ADR;TYPE=DOM;TYPE=HOME:..."<BR /> + * 2) Valid in vCard 2.1 but not in vCard 3.0: "ADR;DOM;HOME:..."<BR /> + * 3) Valid in vCard 3.0 but not in vCard 2.1: "ADR;TYPE=DOM,HOME:..."<BR /> + * </P> + * <P> + * 2) had been the default of VCard exporter/importer in Android, but it is found that + * some external exporter is not able to parse the type format like 2) but only 3). + * </P> + * <P> + * If you are targeting to the importer which cannot accept TYPE params without "TYPE=" + * strings (which should be rare though), please use this flag. + * </P> + * <P> + * Example usage: int vcardType = (VCARD_TYPE_V21_GENERIC | FLAG_APPEND_TYPE_PARAM); + * </P> + */ + public static final int FLAG_APPEND_TYPE_PARAM = 0x04000000; + + //// The followings are VCard types available from importer/exporter. //// + + /** + * <P> + * Generic vCard format with the vCard 2.1. Uses UTF-8 for the charset. + * When composing a vCard entry, the US convension will be used toward formatting + * some values. + * </P> + * <P> * e.g. The order of the display name would be "Prefix Given Middle Family Suffix", - * while in Japan, it should be "Prefix Family Middle Given Suffix". + * while it should be "Prefix Family Middle Given Suffix" in Japan for example. + * </P> */ - public static final int VCARD_TYPE_V21_GENERIC = + public static final int VCARD_TYPE_V21_GENERIC_UTF8 = (FLAG_V21 | NAME_ORDER_DEFAULT | FLAG_CHARSET_UTF8 | FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); - /* package */ static String VCARD_TYPE_V21_GENERIC_STR = "v21_generic"; + /* package */ static String VCARD_TYPE_V21_GENERIC_UTF8_STR = "v21_generic"; /** + * <P> * General vCard format with the version 3.0. Uses UTF-8 for the charset. - * - * Note that this type is not fully implemented, so probably some bugs remain both in - * parsing and composing. - * - * TODO: implement this type correctly. + * </P> + * <P> + * Not fully ready yet. Use with caution when you use this. + * </P> */ - public static final int VCARD_TYPE_V30_GENERIC = + public static final int VCARD_TYPE_V30_GENERIC_UTF8 = (FLAG_V30 | NAME_ORDER_DEFAULT | FLAG_CHARSET_UTF8 | FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); - /* package */ static final String VCARD_TYPE_V30_GENERIC_STR = "v30_generic"; + /* package */ static final String VCARD_TYPE_V30_GENERIC_UTF8_STR = "v30_generic"; /** - * General vCard format with the version 2.1 with some Europe convension. Uses Utf-8. + * <P> + * General vCard format for the vCard 2.1 with some Europe convension. Uses Utf-8. * Currently, only name order is considered ("Prefix Middle Given Family Suffix") + * </P> */ - public static final int VCARD_TYPE_V21_EUROPE = + public static final int VCARD_TYPE_V21_EUROPE_UTF8 = (FLAG_V21 | NAME_ORDER_EUROPE | FLAG_CHARSET_UTF8 | FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); - /* package */ static final String VCARD_TYPE_V21_EUROPE_STR = "v21_europe"; + /* package */ static final String VCARD_TYPE_V21_EUROPE_UTF8_STR = "v21_europe"; /** - * General vCard format with the version 3.0 with some Europe convension. Uses UTF-8 + * <P> + * General vCard format with the version 3.0 with some Europe convension. Uses UTF-8. + * </P> + * <P> + * Not ready yet. Use with caution when you use this. + * </P> */ - public static final int VCARD_TYPE_V30_EUROPE = + public static final int VCARD_TYPE_V30_EUROPE_UTF8 = (FLAG_V30 | NAME_ORDER_EUROPE | FLAG_CHARSET_UTF8 | FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); /* package */ static final String VCARD_TYPE_V30_EUROPE_STR = "v30_europe"; - - /** - * vCard 2.1 format for miscellaneous Japanese devices. Shift_Jis is used for - * parsing/composing the vCard data. - */ - public static final int VCARD_TYPE_V21_JAPANESE = - (FLAG_V21 | NAME_ORDER_JAPANESE | FLAG_CHARSET_SHIFT_JIS | - FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); - /* package */ static final String VCARD_TYPE_V21_JAPANESE_STR = "v21_japanese"; - /** - * vCard 2.1 format for miscellaneous Japanese devices, using UTF-8 as default charset. + * <P> + * The vCard 2.1 format for miscellaneous Japanese devices, using UTF-8 as default charset. + * </P> + * <P> + * Not ready yet. Use with caution when you use this. + * </P> */ public static final int VCARD_TYPE_V21_JAPANESE_UTF8 = (FLAG_V21 | NAME_ORDER_JAPANESE | FLAG_CHARSET_UTF8 | FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); /* package */ static final String VCARD_TYPE_V21_JAPANESE_UTF8_STR = "v21_japanese_utf8"; + + /** + * <P> + * vCard 2.1 format for miscellaneous Japanese devices. Shift_Jis is used for + * parsing/composing the vCard data. + * </P> + * <P> + * Not ready yet. Use with caution when you use this. + * </P> + */ + public static final int VCARD_TYPE_V21_JAPANESE_SJIS = + (FLAG_V21 | NAME_ORDER_JAPANESE | FLAG_CHARSET_SHIFT_JIS | + FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); + + /* package */ static final String VCARD_TYPE_V21_JAPANESE_SJIS_STR = "v21_japanese_sjis"; /** + * <P> * vCard format for miscellaneous Japanese devices, using Shift_Jis for * parsing/composing the vCard data. + * </P> + * <P> + * Not ready yet. Use with caution when you use this. + * </P> */ - public static final int VCARD_TYPE_V30_JAPANESE = + public static final int VCARD_TYPE_V30_JAPANESE_SJIS = (FLAG_V30 | NAME_ORDER_JAPANESE | FLAG_CHARSET_SHIFT_JIS | FLAG_USE_DEFACT_PROPERTY | FLAG_USE_ANDROID_PROPERTY); - /* package */ static final String VCARD_TYPE_V30_JAPANESE_STR = "v30_japanese"; + /* package */ static final String VCARD_TYPE_V30_JAPANESE_SJIS_STR = "v30_japanese_sjis"; /** - * vCard 3.0 format for miscellaneous Japanese devices, using UTF-8 as default charset. + * <P> + * The vCard 3.0 format for miscellaneous Japanese devices, using UTF-8 as default charset. + * </P> + * <P> + * Not ready yet. Use with caution when you use this. + * </P> */ public static final int VCARD_TYPE_V30_JAPANESE_UTF8 = (FLAG_V30 | NAME_ORDER_JAPANESE | FLAG_CHARSET_UTF8 | @@ -193,111 +300,137 @@ public class VCardConfig { /* package */ static final String VCARD_TYPE_V30_JAPANESE_UTF8_STR = "v30_japanese_utf8"; /** - * VCard format used in DoCoMo, which is one of Japanese mobile phone careers. - * Base version is vCard 2.1, but the data has several DoCoMo-specific convensions. - * No Android-specific property nor defact property is included. + * <P> + * The vCard 2.1 based format which (partially) considers the convention in Japanese + * mobile phones, where phonetic names are translated to half-width katakana if + * possible, etc. + * </P> + * <P> + * Not ready yet. Use with caution when you use this. + * </P> + */ + public static final int VCARD_TYPE_V21_JAPANESE_MOBILE = + (FLAG_V21 | NAME_ORDER_JAPANESE | FLAG_CHARSET_SHIFT_JIS | + FLAG_CONVERT_PHONETIC_NAME_STRINGS | + FLAG_REFRAIN_QP_TO_NAME_PROPERTIES); + + /* package */ static final String VCARD_TYPE_V21_JAPANESE_MOBILE_STR = "v21_japanese_mobile"; + + /** + * <P> + * VCard format used in DoCoMo, which is one of Japanese mobile phone careers. + * </p> + * <P> + * Base version is vCard 2.1, but the data has several DoCoMo-specific convensions. + * No Android-specific property nor defact property is included. The "Primary" properties + * are NOT encoded to Quoted-Printable. + * </P> */ public static final int VCARD_TYPE_DOCOMO = - (FLAG_V21 | NAME_ORDER_JAPANESE | FLAG_CHARSET_SHIFT_JIS | FLAG_DOCOMO); + (VCARD_TYPE_V21_JAPANESE_MOBILE | FLAG_DOCOMO); - private static final String VCARD_TYPE_DOCOMO_STR = "docomo"; + /* package */ static final String VCARD_TYPE_DOCOMO_STR = "docomo"; - public static int VCARD_TYPE_DEFAULT = VCARD_TYPE_V21_GENERIC; + public static int VCARD_TYPE_DEFAULT = VCARD_TYPE_V21_GENERIC_UTF8; - private static final Map<String, Integer> VCARD_TYPES_MAP; + private static final Map<String, Integer> sVCardTypeMap; + private static final Set<Integer> sJapaneseMobileTypeSet; static { - VCARD_TYPES_MAP = new HashMap<String, Integer>(); - VCARD_TYPES_MAP.put(VCARD_TYPE_V21_GENERIC_STR, VCARD_TYPE_V21_GENERIC); - VCARD_TYPES_MAP.put(VCARD_TYPE_V30_GENERIC_STR, VCARD_TYPE_V30_GENERIC); - VCARD_TYPES_MAP.put(VCARD_TYPE_V21_EUROPE_STR, VCARD_TYPE_V21_EUROPE); - VCARD_TYPES_MAP.put(VCARD_TYPE_V30_EUROPE_STR, VCARD_TYPE_V30_EUROPE); - VCARD_TYPES_MAP.put(VCARD_TYPE_V21_JAPANESE_STR, VCARD_TYPE_V21_JAPANESE); - VCARD_TYPES_MAP.put(VCARD_TYPE_V21_JAPANESE_UTF8_STR, VCARD_TYPE_V21_JAPANESE_UTF8); - VCARD_TYPES_MAP.put(VCARD_TYPE_V30_JAPANESE_STR, VCARD_TYPE_V30_JAPANESE); - VCARD_TYPES_MAP.put(VCARD_TYPE_V30_JAPANESE_UTF8_STR, VCARD_TYPE_V30_JAPANESE_UTF8); - VCARD_TYPES_MAP.put(VCARD_TYPE_DOCOMO_STR, VCARD_TYPE_DOCOMO); + sVCardTypeMap = new HashMap<String, Integer>(); + sVCardTypeMap.put(VCARD_TYPE_V21_GENERIC_UTF8_STR, VCARD_TYPE_V21_GENERIC_UTF8); + sVCardTypeMap.put(VCARD_TYPE_V30_GENERIC_UTF8_STR, VCARD_TYPE_V30_GENERIC_UTF8); + sVCardTypeMap.put(VCARD_TYPE_V21_EUROPE_UTF8_STR, VCARD_TYPE_V21_EUROPE_UTF8); + sVCardTypeMap.put(VCARD_TYPE_V30_EUROPE_STR, VCARD_TYPE_V30_EUROPE_UTF8); + sVCardTypeMap.put(VCARD_TYPE_V21_JAPANESE_SJIS_STR, VCARD_TYPE_V21_JAPANESE_SJIS); + sVCardTypeMap.put(VCARD_TYPE_V21_JAPANESE_UTF8_STR, VCARD_TYPE_V21_JAPANESE_UTF8); + sVCardTypeMap.put(VCARD_TYPE_V30_JAPANESE_SJIS_STR, VCARD_TYPE_V30_JAPANESE_SJIS); + sVCardTypeMap.put(VCARD_TYPE_V30_JAPANESE_UTF8_STR, VCARD_TYPE_V30_JAPANESE_UTF8); + sVCardTypeMap.put(VCARD_TYPE_V21_JAPANESE_MOBILE_STR, VCARD_TYPE_V21_JAPANESE_MOBILE); + sVCardTypeMap.put(VCARD_TYPE_DOCOMO_STR, VCARD_TYPE_DOCOMO); + + sJapaneseMobileTypeSet = new HashSet<Integer>(); + sJapaneseMobileTypeSet.add(VCARD_TYPE_V21_JAPANESE_SJIS); + sJapaneseMobileTypeSet.add(VCARD_TYPE_V21_JAPANESE_UTF8); + sJapaneseMobileTypeSet.add(VCARD_TYPE_V21_JAPANESE_SJIS); + sJapaneseMobileTypeSet.add(VCARD_TYPE_V30_JAPANESE_SJIS); + sJapaneseMobileTypeSet.add(VCARD_TYPE_V30_JAPANESE_UTF8); + sJapaneseMobileTypeSet.add(VCARD_TYPE_V21_JAPANESE_MOBILE); + sJapaneseMobileTypeSet.add(VCARD_TYPE_DOCOMO); } - public static int getVCardTypeFromString(String vcardTypeString) { - String loweredKey = vcardTypeString.toLowerCase(); - if (VCARD_TYPES_MAP.containsKey(loweredKey)) { - return VCARD_TYPES_MAP.get(loweredKey); + public static int getVCardTypeFromString(final String vcardTypeString) { + final String loweredKey = vcardTypeString.toLowerCase(); + if (sVCardTypeMap.containsKey(loweredKey)) { + return sVCardTypeMap.get(loweredKey); } else { - // XXX: should return the value indicating the input is invalid? + Log.e(LOG_TAG, "Unknown vCard type String: \"" + vcardTypeString + "\""); return VCARD_TYPE_DEFAULT; } } - public static boolean isV30(int vcardType) { + public static boolean isV30(final int vcardType) { return ((vcardType & FLAG_V30) != 0); } - public static boolean usesQuotedPrintable(int vcardType) { + public static boolean shouldUseQuotedPrintable(final int vcardType) { return !isV30(vcardType); } - public static boolean isDoCoMo(int vcardType) { - return ((vcardType & FLAG_DOCOMO) != 0); - } - - /** - * @return true if the device is Japanese and some Japanese convension is - * applied to creating "formatted" something like FORMATTED_ADDRESS. - */ - public static boolean isJapaneseDevice(int vcardType) { - return ((vcardType == VCARD_TYPE_V21_JAPANESE) || - (vcardType == VCARD_TYPE_V21_JAPANESE_UTF8) || - (vcardType == VCARD_TYPE_V30_JAPANESE) || - (vcardType == VCARD_TYPE_V30_JAPANESE_UTF8) || - (vcardType == VCARD_TYPE_DOCOMO)); + public static boolean usesUtf8(final int vcardType) { + return ((vcardType & FLAG_CHARSET_MASK) == FLAG_CHARSET_UTF8); } - public static boolean usesUtf8(int vcardType) { - return ((vcardType & FLAG_CHARSET_UTF8) != 0); + public static boolean usesShiftJis(final int vcardType) { + return ((vcardType & FLAG_CHARSET_MASK) == FLAG_CHARSET_SHIFT_JIS); } - public static boolean usesShiftJis(int vcardType) { - return ((vcardType & FLAG_CHARSET_SHIFT_JIS) != 0); - } - - /** - * @return true when Japanese phonetic string must be converted to a string - * containing only half-width katakana. This method exists since Japanese mobile - * phones usually use only half-width katakana for expressing phonetic names and - * some devices are not ready for parsing other phonetic strings like hiragana and - * full-width katakana. - */ - public static boolean needsToConvertPhoneticString(int vcardType) { - return (vcardType == VCARD_TYPE_DOCOMO); - } - - public static int getNameOrderType(int vcardType) { + public static int getNameOrderType(final int vcardType) { return vcardType & NAME_ORDER_MASK; } - public static boolean usesAndroidSpecificProperty(int vcardType) { + public static boolean usesAndroidSpecificProperty(final int vcardType) { return ((vcardType & FLAG_USE_ANDROID_PROPERTY) != 0); } - public static boolean usesDefactProperty(int vcardType) { + public static boolean usesDefactProperty(final int vcardType) { return ((vcardType & FLAG_USE_DEFACT_PROPERTY) != 0); } - public static boolean onlyOneNoteFieldIsAvailable(int vcardType) { - return vcardType == VCARD_TYPE_DOCOMO; - } - public static boolean showPerformanceLog() { return (VCardConfig.LOG_LEVEL & VCardConfig.LOG_LEVEL_PERFORMANCE_MEASUREMENT) != 0; } + public static boolean shouldRefrainQPToNameProperties(final int vcardType) { + return (!shouldUseQuotedPrintable(vcardType) || + ((vcardType & FLAG_REFRAIN_QP_TO_NAME_PROPERTIES) != 0)); + } + + public static boolean appendTypeParamName(final int vcardType) { + return (isV30(vcardType) || ((vcardType & FLAG_APPEND_TYPE_PARAM) != 0)); + } + /** - * @hide + * @return true if the device is Japanese and some Japanese convension is + * applied to creating "formatted" something like FORMATTED_ADDRESS. */ - public static boolean usesQPToPrimaryProperties(int vcardType) { - return (usesQuotedPrintable(vcardType) && - ((vcardType & FLAG_USE_QP_TO_PRIMARY_PROPERTIES) != 0)); + public static boolean isJapaneseDevice(final int vcardType) { + // TODO: Some mask will be required so that this method wrongly interpret + // Japanese"-like" vCard type. + // e.g. VCARD_TYPE_V21_JAPANESE_SJIS | FLAG_APPEND_TYPE_PARAMS + return sJapaneseMobileTypeSet.contains(vcardType); + } + + public static boolean needsToConvertPhoneticString(final int vcardType) { + return ((vcardType & FLAG_CONVERT_PHONETIC_NAME_STRINGS) != 0); + } + + public static boolean onlyOneNoteFieldIsAvailable(final int vcardType) { + return vcardType == VCARD_TYPE_DOCOMO; + } + + public static boolean isDoCoMo(final int vcardType) { + return ((vcardType & FLAG_DOCOMO) != 0); } private VCardConfig() { diff --git a/core/java/android/pim/vcard/VCardConstants.java b/core/java/android/pim/vcard/VCardConstants.java new file mode 100644 index 0000000..8c07126 --- /dev/null +++ b/core/java/android/pim/vcard/VCardConstants.java @@ -0,0 +1,152 @@ +/* + * 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 android.pim.vcard; + +/** + * Constants used in both exporter and importer code. + */ +public class VCardConstants { + public static final String VERSION_V21 = "2.1"; + public static final String VERSION_V30 = "3.0"; + + // The property names valid both in vCard 2.1 and 3.0. + public static final String PROPERTY_BEGIN = "BEGIN"; + public static final String PROPERTY_VERSION = "VERSION"; + public static final String PROPERTY_N = "N"; + public static final String PROPERTY_FN = "FN"; + public static final String PROPERTY_ADR = "ADR"; + public static final String PROPERTY_EMAIL = "EMAIL"; + public static final String PROPERTY_NOTE = "NOTE"; + public static final String PROPERTY_ORG = "ORG"; + public static final String PROPERTY_SOUND = "SOUND"; // Not fully supported. + public static final String PROPERTY_TEL = "TEL"; + public static final String PROPERTY_TITLE = "TITLE"; + public static final String PROPERTY_ROLE = "ROLE"; + public static final String PROPERTY_PHOTO = "PHOTO"; + public static final String PROPERTY_LOGO = "LOGO"; + public static final String PROPERTY_URL = "URL"; + public static final String PROPERTY_BDAY = "BDAY"; // Birthday + public static final String PROPERTY_END = "END"; + + // Valid property names not supported (not appropriately handled) by our vCard importer now. + public static final String PROPERTY_REV = "REV"; + public static final String PROPERTY_AGENT = "AGENT"; + + // Available in vCard 3.0. Shoud not use when composing vCard 2.1 file. + public static final String PROPERTY_NAME = "NAME"; + public static final String PROPERTY_NICKNAME = "NICKNAME"; + public static final String PROPERTY_SORT_STRING = "SORT-STRING"; + + // De-fact property values expressing phonetic names. + public static final String PROPERTY_X_PHONETIC_FIRST_NAME = "X-PHONETIC-FIRST-NAME"; + public static final String PROPERTY_X_PHONETIC_MIDDLE_NAME = "X-PHONETIC-MIDDLE-NAME"; + public static final String PROPERTY_X_PHONETIC_LAST_NAME = "X-PHONETIC-LAST-NAME"; + + // Properties both ContactsStruct in Eclair and de-fact vCard extensions + // shown in http://en.wikipedia.org/wiki/VCard support are defined here. + public static final String PROPERTY_X_AIM = "X-AIM"; + public static final String PROPERTY_X_MSN = "X-MSN"; + public static final String PROPERTY_X_YAHOO = "X-YAHOO"; + public static final String PROPERTY_X_ICQ = "X-ICQ"; + public static final String PROPERTY_X_JABBER = "X-JABBER"; + public static final String PROPERTY_X_GOOGLE_TALK = "X-GOOGLE-TALK"; + public static final String PROPERTY_X_SKYPE_USERNAME = "X-SKYPE-USERNAME"; + // Properties only ContactsStruct has. We alse use this. + public static final String PROPERTY_X_QQ = "X-QQ"; + public static final String PROPERTY_X_NETMEETING = "X-NETMEETING"; + + // Phone number for Skype, available as usual phone. + public static final String PROPERTY_X_SKYPE_PSTNNUMBER = "X-SKYPE-PSTNNUMBER"; + + // Property for Android-specific fields. + public static final String PROPERTY_X_ANDROID_CUSTOM = "X-ANDROID-CUSTOM"; + + // Properties for DoCoMo vCard. + public static final String PROPERTY_X_CLASS = "X-CLASS"; + public static final String PROPERTY_X_REDUCTION = "X-REDUCTION"; + public static final String PROPERTY_X_NO = "X-NO"; + public static final String PROPERTY_X_DCM_HMN_MODE = "X-DCM-HMN-MODE"; + + public static final String PARAM_TYPE = "TYPE"; + + public static final String PARAM_TYPE_HOME = "HOME"; + public static final String PARAM_TYPE_WORK = "WORK"; + public static final String PARAM_TYPE_FAX = "FAX"; + public static final String PARAM_TYPE_CELL = "CELL"; + public static final String PARAM_TYPE_VOICE = "VOICE"; + public static final String PARAM_TYPE_INTERNET = "INTERNET"; + + // Abbreviation of "prefered" according to vCard 2.1 specification. + // We interpret this value as "primary" property during import/export. + // + // Note: Both vCard specs does not mention anything about the requirement for this parameter, + // but there may be some vCard importer which will get confused with more than + // one "PREF"s in one property name, while Android accepts them. + public static final String PARAM_TYPE_PREF = "PREF"; + + // Phone type parameters valid in vCard and known to ContactsContract, but not so common. + public static final String PARAM_TYPE_CAR = "CAR"; + public static final String PARAM_TYPE_ISDN = "ISDN"; + public static final String PARAM_TYPE_PAGER = "PAGER"; + public static final String PARAM_TYPE_TLX = "TLX"; // Telex + + // Phone types existing in vCard 2.1 but not known to ContactsContract. + public static final String PARAM_TYPE_MODEM = "MODEM"; + public static final String PARAM_TYPE_MSG = "MSG"; + public static final String PARAM_TYPE_BBS = "BBS"; + public static final String PARAM_TYPE_VIDEO = "VIDEO"; + + // TYPE parameters for Phones, which are not formally valid in vCard (at least 2.1). + // These types are basically encoded to "X-" parameters when composing vCard. + // Parser passes these when "X-" is added to the parameter or not. + public static final String PARAM_PHONE_EXTRA_TYPE_CALLBACK = "CALLBACK"; + public static final String PARAM_PHONE_EXTRA_TYPE_RADIO = "RADIO"; + public static final String PARAM_PHONE_EXTRA_TYPE_TTY_TDD = "TTY-TDD"; + public static final String PARAM_PHONE_EXTRA_TYPE_ASSISTANT = "ASSISTANT"; + // vCard composer translates this type to "WORK" + "PREF". Just for parsing. + public static final String PARAM_PHONE_EXTRA_TYPE_COMPANY_MAIN = "COMPANY-MAIN"; + // vCard composer translates this type to "VOICE" Just for parsing. + public static final String PARAM_PHONE_EXTRA_TYPE_OTHER = "OTHER"; + + // TYPE parameters for postal addresses. + public static final String PARAM_ADR_TYPE_PARCEL = "PARCEL"; + public static final String PARAM_ADR_TYPE_DOM = "DOM"; + public static final String PARAM_ADR_TYPE_INTL = "INTL"; + + // TYPE parameters not officially valid but used in some vCard exporter. + // Do not use in composer side. + public static final String PARAM_EXTRA_TYPE_COMPANY = "COMPANY"; + + // DoCoMo specific type parameter. Used with "SOUND" property, which is alternate of SORT-STRING in + // vCard 3.0. + public static final String PARAM_TYPE_X_IRMC_N = "X-IRMC-N"; + + public interface ImportOnly { + public static final String PROPERTY_X_NICKNAME = "X-NICKNAME"; + // Some device emits this "X-" parameter for expressing Google Talk, + // which is specifically invalid but should be always properly accepted, and emitted + // in some special case (for that device/application). + public static final String PROPERTY_X_GOOGLE_TALK_WITH_SPACE = "X-GOOGLE TALK"; + } + + /* package */ static final int MAX_DATA_COLUMN = 15; + + /* package */ static final int MAX_CHARACTER_NUMS_QP = 76; + static final int MAX_CHARACTER_NUMS_BASE64_V30 = 75; + + private VCardConstants() { + } +}
\ No newline at end of file diff --git a/core/java/android/pim/vcard/ContactStruct.java b/core/java/android/pim/vcard/VCardEntry.java index 36e5e23..20eee84 100644 --- a/core/java/android/pim/vcard/ContactStruct.java +++ b/core/java/android/pim/vcard/VCardEntry.java @@ -20,8 +20,10 @@ import android.content.ContentProviderOperation; import android.content.ContentResolver; import android.content.OperationApplicationException; import android.database.Cursor; +import android.net.Uri; import android.os.RemoteException; import android.provider.ContactsContract; +import android.provider.ContactsContract.Contacts; import android.provider.ContactsContract.Data; import android.provider.ContactsContract.Groups; import android.provider.ContactsContract.RawContacts; @@ -44,36 +46,37 @@ import android.util.Log; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; -import java.util.Iterator; import java.util.List; import java.util.Map; /** * This class bridges between data structure of Contact app and VCard data. */ -public class ContactStruct { - private static final String LOG_TAG = "vcard.ContactStruct"; - - // Key: the name shown in VCard. e.g. "X-AIM", "X-ICQ" - // Value: the result of {@link Contacts.ContactMethods#encodePredefinedImProtocol} +public class VCardEntry { + private static final String LOG_TAG = "VCardEntry"; + + private final static int DEFAULT_ORGANIZATION_TYPE = Organization.TYPE_WORK; + + private static final String ACCOUNT_TYPE_GOOGLE = "com.google"; + private static final String GOOGLE_MY_CONTACTS_GROUP = "System Group: My Contacts"; + private static final Map<String, Integer> sImMap = new HashMap<String, Integer>(); - + static { - sImMap.put(Constants.PROPERTY_X_AIM, Im.PROTOCOL_AIM); - sImMap.put(Constants.PROPERTY_X_MSN, Im.PROTOCOL_MSN); - sImMap.put(Constants.PROPERTY_X_YAHOO, Im.PROTOCOL_YAHOO); - sImMap.put(Constants.PROPERTY_X_ICQ, Im.PROTOCOL_ICQ); - sImMap.put(Constants.PROPERTY_X_JABBER, Im.PROTOCOL_JABBER); - sImMap.put(Constants.PROPERTY_X_SKYPE_USERNAME, Im.PROTOCOL_SKYPE); - sImMap.put(Constants.PROPERTY_X_GOOGLE_TALK, Im.PROTOCOL_GOOGLE_TALK); - sImMap.put(Constants.PROPERTY_X_GOOGLE_TALK_WITH_SPACE, Im.PROTOCOL_GOOGLE_TALK); + sImMap.put(VCardConstants.PROPERTY_X_AIM, Im.PROTOCOL_AIM); + sImMap.put(VCardConstants.PROPERTY_X_MSN, Im.PROTOCOL_MSN); + sImMap.put(VCardConstants.PROPERTY_X_YAHOO, Im.PROTOCOL_YAHOO); + sImMap.put(VCardConstants.PROPERTY_X_ICQ, Im.PROTOCOL_ICQ); + sImMap.put(VCardConstants.PROPERTY_X_JABBER, Im.PROTOCOL_JABBER); + sImMap.put(VCardConstants.PROPERTY_X_SKYPE_USERNAME, Im.PROTOCOL_SKYPE); + sImMap.put(VCardConstants.PROPERTY_X_GOOGLE_TALK, Im.PROTOCOL_GOOGLE_TALK); + sImMap.put(VCardConstants.ImportOnly.PROPERTY_X_GOOGLE_TALK_WITH_SPACE, + Im.PROTOCOL_GOOGLE_TALK); } - - /** - * @hide only for testing - */ + static public class PhoneData { public final int type; public final String data; @@ -90,7 +93,7 @@ public class ContactStruct { @Override public boolean equals(Object obj) { - if (obj instanceof PhoneData) { + if (!(obj instanceof PhoneData)) { return false; } PhoneData phoneData = (PhoneData)obj; @@ -105,9 +108,6 @@ public class ContactStruct { } } - /** - * @hide only for testing - */ static public class EmailData { public final int type; public final String data; @@ -125,7 +125,7 @@ public class ContactStruct { @Override public boolean equals(Object obj) { - if (obj instanceof EmailData) { + if (!(obj instanceof EmailData)) { return false; } EmailData emailData = (EmailData)obj; @@ -152,17 +152,12 @@ public class ContactStruct { public final String region; public final String postalCode; public final String country; - public final int type; - - // Used only when type variable is TYPE_CUSTOM. public final String label; - - // isPrimary is changable only when there's no appropriate one existing in - // the original VCard. public boolean isPrimary; - public PostalData(int type, List<String> propValueList, - String label, boolean isPrimary) { + + public PostalData(final int type, final List<String> propValueList, + final String label, boolean isPrimary) { this.type = type; dataArray = new String[ADDR_MAX_DATA_SIZE]; @@ -171,9 +166,9 @@ public class ContactStruct { size = ADDR_MAX_DATA_SIZE; } - // adr-value = 0*6(text-value ";") text-value - // ; PO Box, Extended Address, Street, Locality, Region, Postal - // ; Code, Country Name + // adr-value = 0*6(text-value ";") text-value + // ; PO Box, Extended Address, Street, Locality, Region, Postal + // ; Code, Country Name // // Use Iterator assuming List may be LinkedList, though actually it is // always ArrayList in the current implementation. @@ -195,25 +190,24 @@ public class ContactStruct { this.region = dataArray[4]; this.postalCode = dataArray[5]; this.country = dataArray[6]; - this.label = label; this.isPrimary = isPrimary; } @Override public boolean equals(Object obj) { - if (obj instanceof PostalData) { + if (!(obj instanceof PostalData)) { return false; } - PostalData postalData = (PostalData)obj; + final PostalData postalData = (PostalData)obj; return (Arrays.equals(dataArray, postalData.dataArray) && (type == postalData.type && (type == StructuredPostal.TYPE_CUSTOM ? (label == postalData.label) : true)) && (isPrimary == postalData.isPrimary)); } - - public String getFormattedAddress(int vcardType) { + + public String getFormattedAddress(final int vcardType) { StringBuilder builder = new StringBuilder(); boolean empty = true; if (VCardConfig.isJapaneseDevice(vcardType)) { @@ -223,9 +217,10 @@ public class ContactStruct { if (!TextUtils.isEmpty(addressPart)) { if (!empty) { builder.append(' '); + } else { + empty = false; } builder.append(addressPart); - empty = false; } } } else { @@ -234,9 +229,10 @@ public class ContactStruct { if (!TextUtils.isEmpty(addressPart)) { if (!empty) { builder.append(' '); + } else { + empty = false; } builder.append(addressPart); - empty = false; } } } @@ -250,102 +246,127 @@ public class ContactStruct { type, label, isPrimary); } } - - /** - * @hide only for testing. - */ + static public class OrganizationData { public final int type; - public final String companyName; - // can be changed in some VCard format. - public String positionName; - // isPrimary is changable only when there's no appropriate one existing in - // the original VCard. + // non-final is Intentional: we may change the values since this info is separated into + // two parts in vCard: "ORG" + "TITLE". + public String companyName; + public String departmentName; + public String titleName; public boolean isPrimary; - public OrganizationData(int type, String companyName, String positionName, + + public OrganizationData(int type, + String companyName, + String departmentName, + String titleName, boolean isPrimary) { this.type = type; this.companyName = companyName; - this.positionName = positionName; + this.departmentName = departmentName; + this.titleName = titleName; this.isPrimary = isPrimary; } @Override public boolean equals(Object obj) { - if (obj instanceof OrganizationData) { + if (!(obj instanceof OrganizationData)) { return false; } OrganizationData organization = (OrganizationData)obj; - return (type == organization.type && companyName.equals(organization.companyName) && - positionName.equals(organization.positionName) && + return (type == organization.type && + TextUtils.equals(companyName, organization.companyName) && + TextUtils.equals(departmentName, organization.departmentName) && + TextUtils.equals(titleName, organization.titleName) && isPrimary == organization.isPrimary); } - + @Override public String toString() { - return String.format("type: %d, company: %s, position: %s, isPrimary: %s", - type, companyName, positionName, isPrimary); + return String.format( + "type: %d, company: %s, department: %s, title: %s, isPrimary: %s", + type, companyName, departmentName, titleName, isPrimary); } } static public class ImData { + public final int protocol; + public final String customProtocol; public final int type; public final String data; - public final String label; public final boolean isPrimary; - - // TODO: ContactsConstant#PROTOCOL, ContactsConstant#CUSTOM_PROTOCOL should be used? - public ImData(int type, String data, String label, boolean isPrimary) { + + public ImData(final int protocol, final String customProtocol, final int type, + final String data, final boolean isPrimary) { + this.protocol = protocol; + this.customProtocol = customProtocol; this.type = type; this.data = data; - this.label = label; this.isPrimary = isPrimary; } - + @Override public boolean equals(Object obj) { - if (obj instanceof ImData) { + if (!(obj instanceof ImData)) { return false; } ImData imData = (ImData)obj; - return (type == imData.type && data.equals(imData.data) && - label.equals(imData.label) && isPrimary == imData.isPrimary); + return (type == imData.type && protocol == imData.protocol + && (customProtocol != null ? customProtocol.equals(imData.customProtocol) : + (imData.customProtocol == null)) + && (data != null ? data.equals(imData.data) : (imData.data == null)) + && isPrimary == imData.isPrimary); } - + @Override public String toString() { - return String.format("type: %d, data: %s, label: %s, isPrimary: %s", - type, data, label, isPrimary); + return String.format( + "type: %d, protocol: %d, custom_protcol: %s, data: %s, isPrimary: %s", + type, protocol, customProtocol, data, isPrimary); } } - /** - * @hide only for testing. - */ - static public class PhotoData { + public static class PhotoData { public static final String FORMAT_FLASH = "SWF"; public final int type; public final String formatName; // used when type is not defined in ContactsContract. public final byte[] photoBytes; + public final boolean isPrimary; - public PhotoData(int type, String formatName, byte[] photoBytes) { + public PhotoData(int type, String formatName, byte[] photoBytes, boolean isPrimary) { this.type = type; this.formatName = formatName; this.photoBytes = photoBytes; + this.isPrimary = isPrimary; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof PhotoData)) { + return false; + } + PhotoData photoData = (PhotoData)obj; + return (type == photoData.type && + (formatName == null ? (photoData.formatName == null) : + formatName.equals(photoData.formatName)) && + (Arrays.equals(photoBytes, photoData.photoBytes)) && + (isPrimary == photoData.isPrimary)); + } + + @Override + public String toString() { + return String.format("type: %d, format: %s: size: %d, isPrimary: %s", + type, formatName, photoBytes.length, isPrimary); } } - static /* package */ class Property { + /* package */ static class Property { private String mPropertyName; private Map<String, Collection<String>> mParameterMap = new HashMap<String, Collection<String>>(); private List<String> mPropertyValueList = new ArrayList<String>(); private byte[] mPropertyBytes; - - public Property() { - clear(); - } - + public void setPropertyName(final String propertyName) { mPropertyName = propertyName; } @@ -385,6 +406,7 @@ public class ContactStruct { mPropertyName = null; mParameterMap.clear(); mPropertyValueList.clear(); + mPropertyBytes = null; } } @@ -417,236 +439,48 @@ public class ContactStruct { private List<ImData> mImList; private List<PhotoData> mPhotoList; private List<String> mWebsiteList; - + private List<List<String>> mAndroidCustomPropertyList; + private final int mVCardType; private final Account mAccount; - // Each Column of four properties has ISPRIMARY field - // (See android.provider.Contacts) - // If false even after the parsing loop, we choose the first entry as a "primary" - // entry. - private boolean mPrefIsSet_Address; - private boolean mPrefIsSet_Phone; - private boolean mPrefIsSet_Email; - private boolean mPrefIsSet_Organization; - - public ContactStruct() { - this(VCardConfig.VCARD_TYPE_V21_GENERIC); + public VCardEntry() { + this(VCardConfig.VCARD_TYPE_V21_GENERIC_UTF8); } - public ContactStruct(int vcardType) { + public VCardEntry(int vcardType) { this(vcardType, null); } - public ContactStruct(int vcardType, Account account) { + public VCardEntry(int vcardType, Account account) { mVCardType = vcardType; mAccount = account; } - /** - * @hide only for testing. - */ - public ContactStruct(String givenName, - String familyName, - String middleName, - String prefix, - String suffix, - String phoneticGivenName, - String pheneticFamilyName, - String phoneticMiddleName, - List<byte[]> photoBytesList, - List<String> notes, - List<PhoneData> phoneList, - List<EmailData> emailList, - List<PostalData> postalList, - List<OrganizationData> organizationList, - List<PhotoData> photoList, - List<String> websiteList) { - this(VCardConfig.VCARD_TYPE_DEFAULT); - mGivenName = givenName; - mFamilyName = familyName; - mPrefix = prefix; - mSuffix = suffix; - mPhoneticGivenName = givenName; - mPhoneticFamilyName = familyName; - mPhoneticMiddleName = middleName; - mEmailList = emailList; - mPostalList = postalList; - mOrganizationList = organizationList; - mPhotoList = photoList; - mWebsiteList = websiteList; - } - - // All getter methods should be used carefully, since they may change - // in the future as of 2009-09-24, on which I cannot be sure this structure - // is completely consolidated. - // When we are sure we will no longer change them, we'll be happy to - // make it complete public (withouth @hide tag) - // - // Also note that these getter methods should be used only after - // all properties being pushed into this object. If not, incorrect - // value will "be stored in the local cache and" be returned to you. - - /** - * @hide - */ - public String getFamilyName() { - return mFamilyName; - } - - /** - * @hide - */ - public String getGivenName() { - return mGivenName; - } - - /** - * @hide - */ - public String getMiddleName() { - return mMiddleName; - } - - /** - * @hide - */ - public String getPrefix() { - return mPrefix; - } - - /** - * @hide - */ - public String getSuffix() { - return mSuffix; - } - - /** - * @hide - */ - public String getFullName() { - return mFullName; - } - - /** - * @hide - */ - public String getPhoneticFamilyName() { - return mPhoneticFamilyName; - } - - /** - * @hide - */ - public String getPhoneticGivenName() { - return mPhoneticGivenName; - } - - /** - * @hide - */ - public String getPhoneticMiddleName() { - return mPhoneticMiddleName; - } - - /** - * @hide - */ - public String getPhoneticFullName() { - return mPhoneticFullName; - } - - /** - * @hide - */ - public final List<String> getNickNameList() { - return mNickNameList; - } - - /** - * @hide - */ - public String getDisplayName() { - if (mDisplayName == null) { - constructDisplayName(); - } - return mDisplayName; - } - - /** - * @hide - */ - public String getBirthday() { - return mBirthday; - } - - /** - * @hide - */ - public final List<PhotoData> getPhotoList() { - return mPhotoList; - } - - /** - * @hide - */ - public final List<String> getNotes() { - return mNoteList; - } - - /** - * @hide - */ - public final List<PhoneData> getPhoneList() { - return mPhoneList; - } - - /** - * @hide - */ - public final List<EmailData> getEmailList() { - return mEmailList; - } - - /** - * @hide - */ - public final List<PostalData> getPostalList() { - return mPostalList; - } - - /** - * @hide - */ - public final List<OrganizationData> getOrganizationList() { - return mOrganizationList; - } - - /** - * Add a phone info to phoneList. - * @param data phone number - * @param type type col of content://contacts/phones - * @param label lable col of content://contacts/phones - */ - private void addPhone(int type, String data, String label, boolean isPrimary){ + private void addPhone(int type, String data, String label, boolean isPrimary) { if (mPhoneList == null) { mPhoneList = new ArrayList<PhoneData>(); } - StringBuilder builder = new StringBuilder(); - String trimed = data.trim(); - int length = trimed.length(); - for (int i = 0; i < length; i++) { - char ch = trimed.charAt(i); - if (('0' <= ch && ch <= '9') || (i == 0 && ch == '+')) { - builder.append(ch); + final StringBuilder builder = new StringBuilder(); + final String trimed = data.trim(); + final String formattedNumber; + if (type == Phone.TYPE_PAGER) { + formattedNumber = trimed; + } else { + final int length = trimed.length(); + for (int i = 0; i < length; i++) { + char ch = trimed.charAt(i); + if (('0' <= ch && ch <= '9') || (i == 0 && ch == '+')) { + builder.append(ch); + } } - } - - PhoneData phoneData = new PhoneData(type, - PhoneNumberUtils.formatNumber(builder.toString()), - label, isPrimary); + // Use NANP in default when there's no information about locale. + final int formattingType = (VCardConfig.isJapaneseDevice(mVCardType) ? + PhoneNumberUtils.FORMAT_JAPAN : PhoneNumberUtils.FORMAT_NANP); + formattedNumber = PhoneNumberUtils.formatNumber(builder.toString(), formattingType); + } + PhoneData phoneData = new PhoneData(type, formattedNumber, label, isPrimary); mPhoneList.add(phoneData); } @@ -666,24 +500,122 @@ public class ContactStruct { private void addPostal(int type, List<String> propValueList, String label, boolean isPrimary){ if (mPostalList == null) { - mPostalList = new ArrayList<PostalData>(); + mPostalList = new ArrayList<PostalData>(0); } mPostalList.add(new PostalData(type, propValueList, label, isPrimary)); } - private void addOrganization(int type, final String companyName, - final String positionName, boolean isPrimary) { + /** + * Should be called via {@link #handleOrgValue(int, List, boolean)} or + * {@link #handleTitleValue(String)}. + */ + private void addNewOrganization(int type, final String companyName, + final String departmentName, + final String titleName, boolean isPrimary) { if (mOrganizationList == null) { mOrganizationList = new ArrayList<OrganizationData>(); } - mOrganizationList.add(new OrganizationData(type, companyName, positionName, isPrimary)); + mOrganizationList.add(new OrganizationData(type, companyName, + departmentName, titleName, isPrimary)); } - - private void addIm(int type, String data, String label, boolean isPrimary) { + + private static final List<String> sEmptyList = + Collections.unmodifiableList(new ArrayList<String>(0)); + + /** + * Set "ORG" related values to the appropriate data. If there's more than one + * {@link OrganizationData} objects, this input data are attached to the last one which + * does not have valid values (not including empty but only null). If there's no + * {@link OrganizationData} object, a new {@link OrganizationData} is created, + * whose title is set to null. + */ + private void handleOrgValue(final int type, List<String> orgList, boolean isPrimary) { + if (orgList == null) { + orgList = sEmptyList; + } + final String companyName; + final String departmentName; + final int size = orgList.size(); + switch (size) { + case 0: { + companyName = ""; + departmentName = null; + break; + } + case 1: { + companyName = orgList.get(0); + departmentName = null; + break; + } + default: { // More than 1. + companyName = orgList.get(0); + // We're not sure which is the correct string for department. + // In order to keep all the data, concatinate the rest of elements. + StringBuilder builder = new StringBuilder(); + for (int i = 1; i < size; i++) { + if (i > 1) { + builder.append(' '); + } + builder.append(orgList.get(i)); + } + departmentName = builder.toString(); + } + } + if (mOrganizationList == null) { + // Create new first organization entry, with "null" title which may be + // added via handleTitleValue(). + addNewOrganization(type, companyName, departmentName, null, isPrimary); + return; + } + for (OrganizationData organizationData : mOrganizationList) { + // Not use TextUtils.isEmpty() since ORG was set but the elements might be empty. + // e.g. "ORG;PREF:;" -> Both companyName and departmentName become empty but not null. + if (organizationData.companyName == null && + organizationData.departmentName == null) { + // Probably the "TITLE" property comes before the "ORG" property via + // handleTitleLine(). + organizationData.companyName = companyName; + organizationData.departmentName = departmentName; + organizationData.isPrimary = isPrimary; + return; + } + } + // No OrganizatioData is available. Create another one, with "null" title, which may be + // added via handleTitleValue(). + addNewOrganization(type, companyName, departmentName, null, isPrimary); + } + + /** + * Set "title" value to the appropriate data. If there's more than one + * OrganizationData objects, this input is attached to the last one which does not + * have valid title value (not including empty but only null). If there's no + * OrganizationData object, a new OrganizationData is created, whose company name is + * set to null. + */ + private void handleTitleValue(final String title) { + if (mOrganizationList == null) { + // Create new first organization entry, with "null" other info, which may be + // added via handleOrgValue(). + addNewOrganization(DEFAULT_ORGANIZATION_TYPE, null, null, title, false); + return; + } + for (OrganizationData organizationData : mOrganizationList) { + if (organizationData.titleName == null) { + organizationData.titleName = title; + return; + } + } + // No Organization is available. Create another one, with "null" other info, which may be + // added via handleOrgValue(). + addNewOrganization(DEFAULT_ORGANIZATION_TYPE, null, null, title, false); + } + + private void addIm(int protocol, String customProtocol, int type, + String propValue, boolean isPrimary) { if (mImList == null) { mImList = new ArrayList<ImData>(); } - mImList.add(new ImData(type, data, label, isPrimary)); + mImList.add(new ImData(protocol, customProtocol, type, propValue, isPrimary)); } private void addNote(final String note) { @@ -693,43 +625,14 @@ public class ContactStruct { mNoteList.add(note); } - private void addPhotoBytes(String formatName, byte[] photoBytes) { + private void addPhotoBytes(String formatName, byte[] photoBytes, boolean isPrimary) { if (mPhotoList == null) { mPhotoList = new ArrayList<PhotoData>(1); } - final PhotoData photoData = new PhotoData(0, null, photoBytes); + final PhotoData photoData = new PhotoData(0, null, photoBytes, isPrimary); mPhotoList.add(photoData); } - /** - * Set "position" value to the appropriate data. If there's more than one - * OrganizationData objects, the value is set to the last one. If there's no - * OrganizationData object, a new OrganizationData is created, whose company name is - * empty. - * - * TODO: incomplete logic. fix this: - * - * e.g. This assumes ORG comes earlier, but TITLE may come earlier like this, though we do not - * know how to handle it in general cases... - * ---- - * TITLE:Software Engineer - * ORG:Google - * ---- - */ - private void setPosition(String positionValue) { - if (mOrganizationList == null) { - mOrganizationList = new ArrayList<OrganizationData>(); - } - int size = mOrganizationList.size(); - if (size == 0) { - addOrganization(ContactsContract.CommonDataKinds.Organization.TYPE_OTHER, - "", null, false); - size = 1; - } - OrganizationData lastData = mOrganizationList.get(size - 1); - lastData.positionName = positionValue; - } - @SuppressWarnings("fallthrough") private void handleNProperty(List<String> elems) { // Family, Given, Middle, Prefix, Suffix. (1 - 5) @@ -742,51 +645,80 @@ public class ContactStruct { } switch (size) { - // fallthrough - case 5: - mSuffix = elems.get(4); - case 4: - mPrefix = elems.get(3); - case 3: - mMiddleName = elems.get(2); - case 2: - mGivenName = elems.get(1); - default: - mFamilyName = elems.get(0); + // fallthrough + case 5: mSuffix = elems.get(4); + case 4: mPrefix = elems.get(3); + case 3: mMiddleName = elems.get(2); + case 2: mGivenName = elems.get(1); + default: mFamilyName = elems.get(0); } } - + /** - * Some Japanese mobile phones use this field for phonetic name, - * since vCard 2.1 does not have "SORT-STRING" type. - * Also, in some cases, the field has some ';'s in it. - * Assume the ';' means the same meaning in N property + * Note: Some Japanese mobile phones use this field for phonetic name, + * since vCard 2.1 does not have "SORT-STRING" type. + * Also, in some cases, the field has some ';'s in it. + * Assume the ';' means the same meaning in N property */ @SuppressWarnings("fallthrough") private void handlePhoneticNameFromSound(List<String> elems) { - // Family, Given, Middle. (1-3) - // This is not from specification but mere assumption. Some Japanese phones use this order. + if (!(TextUtils.isEmpty(mPhoneticFamilyName) && + TextUtils.isEmpty(mPhoneticMiddleName) && + TextUtils.isEmpty(mPhoneticGivenName))) { + // This means the other properties like "X-PHONETIC-FIRST-NAME" was already found. + // Ignore "SOUND;X-IRMC-N". + return; + } + int size; if (elems == null || (size = elems.size()) < 1) { return; } + + // Assume that the order is "Family, Given, Middle". + // This is not from specification but mere assumption. Some Japanese phones use this order. if (size > 3) { size = 3; } + if (elems.get(0).length() > 0) { + boolean onlyFirstElemIsNonEmpty = true; + for (int i = 1; i < size; i++) { + if (elems.get(i).length() > 0) { + onlyFirstElemIsNonEmpty = false; + break; + } + } + if (onlyFirstElemIsNonEmpty) { + final String[] namesArray = elems.get(0).split(" "); + final int nameArrayLength = namesArray.length; + if (nameArrayLength == 3) { + // Assume the string is "Family Middle Given". + mPhoneticFamilyName = namesArray[0]; + mPhoneticMiddleName = namesArray[1]; + mPhoneticGivenName = namesArray[2]; + } else if (nameArrayLength == 2) { + // Assume the string is "Family Given" based on the Japanese mobile + // phones' preference. + mPhoneticFamilyName = namesArray[0]; + mPhoneticGivenName = namesArray[1]; + } else { + mPhoneticFullName = elems.get(0); + } + return; + } + } + switch (size) { - // fallthrough - case 3: - mPhoneticMiddleName = elems.get(2); - case 2: - mPhoneticGivenName = elems.get(1); - default: - mPhoneticFamilyName = elems.get(0); + // fallthrough + case 3: mPhoneticMiddleName = elems.get(2); + case 2: mPhoneticGivenName = elems.get(1); + default: mPhoneticFamilyName = elems.get(0); } } - public void addProperty(Property property) { - String propName = property.mPropertyName; + public void addProperty(final Property property) { + final String propName = property.mPropertyName; final Map<String, Collection<String>> paramMap = property.mParameterMap; final List<String> propValueList = property.mPropertyValueList; byte[] propBytes = property.mPropertyBytes; @@ -796,28 +728,37 @@ public class ContactStruct { } final String propValue = listToString(propValueList).trim(); - if (propName.equals("VERSION")) { + if (propName.equals(VCardConstants.PROPERTY_VERSION)) { // vCard version. Ignore this. - } else if (propName.equals("FN")) { + } else if (propName.equals(VCardConstants.PROPERTY_FN)) { mFullName = propValue; - } else if (propName.equals("NAME") && mFullName == null) { + } else if (propName.equals(VCardConstants.PROPERTY_NAME) && mFullName == null) { // Only in vCard 3.0. Use this if FN, which must exist in vCard 3.0 but may not // actually exist in the real vCard data, does not exist. mFullName = propValue; - } else if (propName.equals("N")) { + } else if (propName.equals(VCardConstants.PROPERTY_N)) { handleNProperty(propValueList); - } else if (propName.equals("SORT-STRING")) { + } else if (propName.equals(VCardConstants.PROPERTY_SORT_STRING)) { mPhoneticFullName = propValue; - } else if (propName.equals("NICKNAME") || propName.equals("X-NICKNAME")) { + } else if (propName.equals(VCardConstants.PROPERTY_NICKNAME) || + propName.equals(VCardConstants.ImportOnly.PROPERTY_X_NICKNAME)) { addNickName(propValue); - } else if (propName.equals("SOUND")) { - Collection<String> typeCollection = paramMap.get(Constants.ATTR_TYPE); - if (typeCollection != null && typeCollection.contains(Constants.ATTR_TYPE_X_IRMC_N)) { - handlePhoneticNameFromSound(propValueList); + } else if (propName.equals(VCardConstants.PROPERTY_SOUND)) { + Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE); + if (typeCollection != null + && typeCollection.contains(VCardConstants.PARAM_TYPE_X_IRMC_N)) { + // As of 2009-10-08, Parser side does not split a property value into separated + // values using ';' (in other words, propValueList.size() == 1), + // which is correct behavior from the view of vCard 2.1. + // But we want it to be separated, so do the separation here. + final List<String> phoneticNameList = + VCardUtils.constructListFromValue(propValue, + VCardConfig.isV30(mVCardType)); + handlePhoneticNameFromSound(phoneticNameList); } else { // Ignore this field since Android cannot understand what it is. } - } else if (propName.equals("ADR")) { + } else if (propName.equals(VCardConstants.PROPERTY_ADR)) { boolean valuesAreAllEmpty = true; for (String value : propValueList) { if (value.length() > 0) { @@ -832,27 +773,25 @@ public class ContactStruct { int type = -1; String label = ""; boolean isPrimary = false; - Collection<String> typeCollection = paramMap.get(Constants.ATTR_TYPE); + Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE); if (typeCollection != null) { for (String typeString : typeCollection) { typeString = typeString.toUpperCase(); - if (typeString.equals(Constants.ATTR_TYPE_PREF) && !mPrefIsSet_Address) { - // Only first "PREF" is considered. - mPrefIsSet_Address = true; + if (typeString.equals(VCardConstants.PARAM_TYPE_PREF)) { isPrimary = true; - } else if (typeString.equals(Constants.ATTR_TYPE_HOME)) { + } else if (typeString.equals(VCardConstants.PARAM_TYPE_HOME)) { type = StructuredPostal.TYPE_HOME; label = ""; - } else if (typeString.equals(Constants.ATTR_TYPE_WORK) || - typeString.equalsIgnoreCase("COMPANY")) { + } else if (typeString.equals(VCardConstants.PARAM_TYPE_WORK) || + typeString.equalsIgnoreCase(VCardConstants.PARAM_EXTRA_TYPE_COMPANY)) { // "COMPANY" seems emitted by Windows Mobile, which is not // specifically supported by vCard 2.1. We assume this is same // as "WORK". type = StructuredPostal.TYPE_WORK; label = ""; - } else if (typeString.equals("PARCEL") || - typeString.equals("DOM") || - typeString.equals("INTL")) { + } else if (typeString.equals(VCardConstants.PARAM_ADR_TYPE_PARCEL) || + typeString.equals(VCardConstants.PARAM_ADR_TYPE_DOM) || + typeString.equals(VCardConstants.PARAM_ADR_TYPE_INTL)) { // We do not have any appropriate way to store this information. } else { if (typeString.startsWith("X-") && type < 0) { @@ -871,23 +810,21 @@ public class ContactStruct { } addPostal(type, propValueList, label, isPrimary); - } else if (propName.equals("EMAIL")) { + } else if (propName.equals(VCardConstants.PROPERTY_EMAIL)) { int type = -1; String label = null; boolean isPrimary = false; - Collection<String> typeCollection = paramMap.get(Constants.ATTR_TYPE); + Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE); if (typeCollection != null) { for (String typeString : typeCollection) { typeString = typeString.toUpperCase(); - if (typeString.equals(Constants.ATTR_TYPE_PREF) && !mPrefIsSet_Email) { - // Only first "PREF" is considered. - mPrefIsSet_Email = true; + if (typeString.equals(VCardConstants.PARAM_TYPE_PREF)) { isPrimary = true; - } else if (typeString.equals(Constants.ATTR_TYPE_HOME)) { + } else if (typeString.equals(VCardConstants.PARAM_TYPE_HOME)) { type = Email.TYPE_HOME; - } else if (typeString.equals(Constants.ATTR_TYPE_WORK)) { + } else if (typeString.equals(VCardConstants.PARAM_TYPE_WORK)) { type = Email.TYPE_WORK; - } else if (typeString.equals(Constants.ATTR_TYPE_CELL)) { + } else if (typeString.equals(VCardConstants.PARAM_TYPE_CELL)) { type = Email.TYPE_MOBILE; } else { if (typeString.startsWith("X-") && type < 0) { @@ -905,50 +842,48 @@ public class ContactStruct { type = Email.TYPE_OTHER; } addEmail(type, propValue, label, isPrimary); - } else if (propName.equals("ORG")) { + } else if (propName.equals(VCardConstants.PROPERTY_ORG)) { // vCard specification does not specify other types. - int type = Organization.TYPE_WORK; + final int type = Organization.TYPE_WORK; boolean isPrimary = false; - - Collection<String> typeCollection = paramMap.get(Constants.ATTR_TYPE); + Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE); if (typeCollection != null) { for (String typeString : typeCollection) { - if (typeString.equals(Constants.ATTR_TYPE_PREF) && !mPrefIsSet_Organization) { - // vCard specification officially does not have PREF in ORG. - // This is just for safety. - mPrefIsSet_Organization = true; + if (typeString.equals(VCardConstants.PARAM_TYPE_PREF)) { isPrimary = true; } } } - - StringBuilder builder = new StringBuilder(); - for (Iterator<String> iter = propValueList.iterator(); iter.hasNext();) { - builder.append(iter.next()); - if (iter.hasNext()) { - builder.append(' '); - } - } - addOrganization(type, builder.toString(), "", isPrimary); - } else if (propName.equals("TITLE")) { - setPosition(propValue); - } else if (propName.equals("ROLE")) { - setPosition(propValue); - } else if (propName.equals("PHOTO") || propName.equals("LOGO")) { - String formatName = null; - Collection<String> typeCollection = paramMap.get("TYPE"); - if (typeCollection != null) { - formatName = typeCollection.iterator().next(); - } + handleOrgValue(type, propValueList, isPrimary); + } else if (propName.equals(VCardConstants.PROPERTY_TITLE)) { + handleTitleValue(propValue); + } else if (propName.equals(VCardConstants.PROPERTY_ROLE)) { + // This conflicts with TITLE. Ignore for now... + // handleTitleValue(propValue); + } else if (propName.equals(VCardConstants.PROPERTY_PHOTO) || + propName.equals(VCardConstants.PROPERTY_LOGO)) { Collection<String> paramMapValue = paramMap.get("VALUE"); if (paramMapValue != null && paramMapValue.contains("URL")) { // Currently we do not have appropriate example for testing this case. } else { - addPhotoBytes(formatName, propBytes); + final Collection<String> typeCollection = paramMap.get("TYPE"); + String formatName = null; + boolean isPrimary = false; + if (typeCollection != null) { + for (String typeValue : typeCollection) { + if (VCardConstants.PARAM_TYPE_PREF.equals(typeValue)) { + isPrimary = true; + } else if (formatName == null){ + formatName = typeValue; + } + } + } + addPhotoBytes(formatName, propBytes, isPrimary); } - } else if (propName.equals("TEL")) { - Collection<String> typeCollection = paramMap.get(Constants.ATTR_TYPE); - Object typeObject = VCardUtils.getPhoneTypeFromStrings(typeCollection); + } else if (propName.equals(VCardConstants.PROPERTY_TEL)) { + final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE); + final Object typeObject = + VCardUtils.getPhoneTypeFromStrings(typeCollection, propValue); final int type; final String label; if (typeObject instanceof Integer) { @@ -960,63 +895,65 @@ public class ContactStruct { } final boolean isPrimary; - if (!mPrefIsSet_Phone && typeCollection != null && - typeCollection.contains(Constants.ATTR_TYPE_PREF)) { - mPrefIsSet_Phone = true; + if (typeCollection != null && typeCollection.contains(VCardConstants.PARAM_TYPE_PREF)) { isPrimary = true; } else { isPrimary = false; } addPhone(type, propValue, label, isPrimary); - } else if (propName.equals(Constants.PROPERTY_X_SKYPE_PSTNNUMBER)) { + } else if (propName.equals(VCardConstants.PROPERTY_X_SKYPE_PSTNNUMBER)) { // The phone number available via Skype. - Collection<String> typeCollection = paramMap.get(Constants.ATTR_TYPE); - // XXX: should use TYPE_CUSTOM + the label "Skype"? (which may need localization) - int type = Phone.TYPE_OTHER; - final String label = null; + Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE); + final int type = Phone.TYPE_OTHER; final boolean isPrimary; - if (!mPrefIsSet_Phone && typeCollection != null && - typeCollection.contains(Constants.ATTR_TYPE_PREF)) { - mPrefIsSet_Phone = true; + if (typeCollection != null && typeCollection.contains(VCardConstants.PARAM_TYPE_PREF)) { isPrimary = true; } else { isPrimary = false; } - addPhone(type, propValue, label, isPrimary); - } else if (sImMap.containsKey(propName)){ - int type = sImMap.get(propName); + addPhone(type, propValue, null, isPrimary); + } else if (sImMap.containsKey(propName)) { + final int protocol = sImMap.get(propName); boolean isPrimary = false; - final Collection<String> typeCollection = paramMap.get(Constants.ATTR_TYPE); + int type = -1; + final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE); if (typeCollection != null) { for (String typeString : typeCollection) { - if (typeString.equals(Constants.ATTR_TYPE_PREF)) { + if (typeString.equals(VCardConstants.PARAM_TYPE_PREF)) { isPrimary = true; - } else if (typeString.equalsIgnoreCase(Constants.ATTR_TYPE_HOME)) { - type = Phone.TYPE_HOME; - } else if (typeString.equalsIgnoreCase(Constants.ATTR_TYPE_WORK)) { - type = Phone.TYPE_WORK; + } else if (type < 0) { + if (typeString.equalsIgnoreCase(VCardConstants.PARAM_TYPE_HOME)) { + type = Im.TYPE_HOME; + } else if (typeString.equalsIgnoreCase(VCardConstants.PARAM_TYPE_WORK)) { + type = Im.TYPE_WORK; + } } } } if (type < 0) { type = Phone.TYPE_HOME; } - addIm(type, propValue, null, isPrimary); - } else if (propName.equals("NOTE")) { + addIm(protocol, null, type, propValue, isPrimary); + } else if (propName.equals(VCardConstants.PROPERTY_NOTE)) { addNote(propValue); - } else if (propName.equals("URL")) { + } else if (propName.equals(VCardConstants.PROPERTY_URL)) { if (mWebsiteList == null) { mWebsiteList = new ArrayList<String>(1); } mWebsiteList.add(propValue); - } else if (propName.equals("X-PHONETIC-FIRST-NAME")) { + } else if (propName.equals(VCardConstants.PROPERTY_BDAY)) { + mBirthday = propValue; + } else if (propName.equals(VCardConstants.PROPERTY_X_PHONETIC_FIRST_NAME)) { mPhoneticGivenName = propValue; - } else if (propName.equals("X-PHONETIC-MIDDLE-NAME")) { + } else if (propName.equals(VCardConstants.PROPERTY_X_PHONETIC_MIDDLE_NAME)) { mPhoneticMiddleName = propValue; - } else if (propName.equals("X-PHONETIC-LAST-NAME")) { + } else if (propName.equals(VCardConstants.PROPERTY_X_PHONETIC_LAST_NAME)) { mPhoneticFamilyName = propValue; - } else if (propName.equals("BDAY")) { - mBirthday = propValue; + } else if (propName.equals(VCardConstants.PROPERTY_X_ANDROID_CUSTOM)) { + final List<String> customPropertyList = + VCardUtils.constructListFromValue(propValue, + VCardConfig.isV30(mVCardType)); + handleAndroidCustomProperty(customPropertyList); /*} else if (propName.equals("REV")) { // Revision of this VCard entry. I think we can ignore this. } else if (propName.equals("UID")) { @@ -1044,43 +981,23 @@ public class ContactStruct { } } + private void handleAndroidCustomProperty(final List<String> customPropertyList) { + if (mAndroidCustomPropertyList == null) { + mAndroidCustomPropertyList = new ArrayList<List<String>>(); + } + mAndroidCustomPropertyList.add(customPropertyList); + } + /** * Construct the display name. The constructed data must not be null. */ private void constructDisplayName() { - if (!(TextUtils.isEmpty(mFamilyName) && TextUtils.isEmpty(mGivenName))) { - StringBuilder builder = new StringBuilder(); - List<String> nameList; - switch (VCardConfig.getNameOrderType(mVCardType)) { - case VCardConfig.NAME_ORDER_JAPANESE: - if (VCardUtils.containsOnlyPrintableAscii(mFamilyName) && - VCardUtils.containsOnlyPrintableAscii(mGivenName)) { - nameList = Arrays.asList(mPrefix, mGivenName, mMiddleName, mFamilyName, mSuffix); - } else { - nameList = Arrays.asList(mPrefix, mFamilyName, mMiddleName, mGivenName, mSuffix); - } - break; - case VCardConfig.NAME_ORDER_EUROPE: - nameList = Arrays.asList(mPrefix, mMiddleName, mGivenName, mFamilyName, mSuffix); - break; - default: - nameList = Arrays.asList(mPrefix, mGivenName, mMiddleName, mFamilyName, mSuffix); - break; - } - boolean first = true; - for (String namePart : nameList) { - if (!TextUtils.isEmpty(namePart)) { - if (first) { - first = false; - } else { - builder.append(' '); - } - builder.append(namePart); - } - } - mDisplayName = builder.toString(); - } else if (!TextUtils.isEmpty(mFullName)) { + // FullName (created via "FN" or "NAME" field) is prefered. + if (!TextUtils.isEmpty(mFullName)) { mDisplayName = mFullName; + } else if (!(TextUtils.isEmpty(mFamilyName) && TextUtils.isEmpty(mGivenName))) { + mDisplayName = VCardUtils.constructNameFromElements(mVCardType, + mFamilyName, mMiddleName, mGivenName, mPrefix, mSuffix); } else if (!(TextUtils.isEmpty(mPhoneticFamilyName) && TextUtils.isEmpty(mPhoneticGivenName))) { mDisplayName = VCardUtils.constructNameFromElements(mVCardType, @@ -1097,36 +1014,17 @@ public class ContactStruct { mDisplayName = ""; } } - + /** * Consolidate several fielsds (like mName) using name candidates, */ public void consolidateFields() { constructDisplayName(); - + if (mPhoneticFullName != null) { mPhoneticFullName = mPhoneticFullName.trim(); } - - // If there is no "PREF", we choose the first entries as primary. - if (!mPrefIsSet_Phone && mPhoneList != null && mPhoneList.size() > 0) { - mPhoneList.get(0).isPrimary = true; - } - - if (!mPrefIsSet_Address && mPostalList != null && mPostalList.size() > 0) { - mPostalList.get(0).isPrimary = true; - } - if (!mPrefIsSet_Email && mEmailList != null && mEmailList.size() > 0) { - mEmailList.get(0).isPrimary = true; - } - if (!mPrefIsSet_Organization && mOrganizationList != null && mOrganizationList.size() > 0) { - mOrganizationList.get(0).isPrimary = true; - } } - - // From GoogleSource.java in Contacts app. - private static final String ACCOUNT_TYPE_GOOGLE = "com.google"; - private static final String GOOGLE_MY_CONTACTS_GROUP = "System Group: My Contacts"; public void pushIntoContentResolver(ContentResolver resolver) { ArrayList<ContentProviderOperation> operationList = @@ -1139,7 +1037,6 @@ public class ContactStruct { builder.withValue(RawContacts.ACCOUNT_TYPE, mAccount.type); // Assume that caller side creates this group if it does not exist. - // TODO: refactor this code along with the change in GoogleSource.java if (ACCOUNT_TYPE_GOOGLE.equals(mAccount.type)) { final Cursor cursor = resolver.query(Groups.CONTENT_URI, new String[] { Groups.SOURCE_ID }, @@ -1161,7 +1058,7 @@ public class ContactStruct { } operationList.add(builder.build()); - { + if (!nameFieldsAreEmpty()) { builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); builder.withValueBackReference(StructuredName.RAW_CONTACT_ID, 0); builder.withValue(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE); @@ -1172,31 +1069,31 @@ public class ContactStruct { builder.withValue(StructuredName.PREFIX, mPrefix); builder.withValue(StructuredName.SUFFIX, mSuffix); - builder.withValue(StructuredName.PHONETIC_GIVEN_NAME, mPhoneticGivenName); - builder.withValue(StructuredName.PHONETIC_FAMILY_NAME, mPhoneticFamilyName); - builder.withValue(StructuredName.PHONETIC_MIDDLE_NAME, mPhoneticMiddleName); + if (!(TextUtils.isEmpty(mPhoneticGivenName) + && TextUtils.isEmpty(mPhoneticFamilyName) + && TextUtils.isEmpty(mPhoneticMiddleName))) { + builder.withValue(StructuredName.PHONETIC_GIVEN_NAME, mPhoneticGivenName); + builder.withValue(StructuredName.PHONETIC_FAMILY_NAME, mPhoneticFamilyName); + builder.withValue(StructuredName.PHONETIC_MIDDLE_NAME, mPhoneticMiddleName); + } else if (!TextUtils.isEmpty(mPhoneticFullName)) { + builder.withValue(StructuredName.PHONETIC_GIVEN_NAME, mPhoneticFullName); + } builder.withValue(StructuredName.DISPLAY_NAME, getDisplayName()); operationList.add(builder.build()); } if (mNickNameList != null && mNickNameList.size() > 0) { - boolean first = true; for (String nickName : mNickNameList) { builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); builder.withValueBackReference(Nickname.RAW_CONTACT_ID, 0); builder.withValue(Data.MIMETYPE, Nickname.CONTENT_ITEM_TYPE); - builder.withValue(Nickname.TYPE, Nickname.TYPE_DEFAULT); builder.withValue(Nickname.NAME, nickName); - if (first) { - builder.withValue(Data.IS_PRIMARY, 1); - first = false; - } operationList.add(builder.build()); } } - + if (mPhoneList != null) { for (PhoneData phoneData : mPhoneList) { builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); @@ -1209,30 +1106,34 @@ public class ContactStruct { } builder.withValue(Phone.NUMBER, phoneData.data); if (phoneData.isPrimary) { - builder.withValue(Data.IS_PRIMARY, 1); + builder.withValue(Phone.IS_PRIMARY, 1); } operationList.add(builder.build()); } } - + if (mOrganizationList != null) { - boolean first = true; for (OrganizationData organizationData : mOrganizationList) { builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); builder.withValueBackReference(Organization.RAW_CONTACT_ID, 0); builder.withValue(Data.MIMETYPE, Organization.CONTENT_ITEM_TYPE); - - // Currently, we do not use TYPE_CUSTOM. builder.withValue(Organization.TYPE, organizationData.type); - builder.withValue(Organization.COMPANY, organizationData.companyName); - builder.withValue(Organization.TITLE, organizationData.positionName); - if (first) { - builder.withValue(Data.IS_PRIMARY, 1); + if (organizationData.companyName != null) { + builder.withValue(Organization.COMPANY, organizationData.companyName); + } + if (organizationData.departmentName != null) { + builder.withValue(Organization.DEPARTMENT, organizationData.departmentName); + } + if (organizationData.titleName != null) { + builder.withValue(Organization.TITLE, organizationData.titleName); + } + if (organizationData.isPrimary) { + builder.withValue(Organization.IS_PRIMARY, 1); } operationList.add(builder.build()); } } - + if (mEmailList != null) { for (EmailData emailData : mEmailList) { builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); @@ -1265,12 +1166,11 @@ public class ContactStruct { builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); builder.withValueBackReference(Im.RAW_CONTACT_ID, 0); builder.withValue(Data.MIMETYPE, Im.CONTENT_ITEM_TYPE); - builder.withValue(Im.TYPE, imData.type); - if (imData.type == Im.TYPE_CUSTOM) { - builder.withValue(Im.LABEL, imData.label); + builder.withValue(Im.PROTOCOL, imData.protocol); + if (imData.protocol == Im.PROTOCOL_CUSTOM) { + builder.withValue(Im.CUSTOM_PROTOCOL, imData.customProtocol); } - builder.withValue(Im.DATA, imData.data); if (imData.isPrimary) { builder.withValue(Data.IS_PRIMARY, 1); } @@ -1282,22 +1182,19 @@ public class ContactStruct { builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); builder.withValueBackReference(Note.RAW_CONTACT_ID, 0); builder.withValue(Data.MIMETYPE, Note.CONTENT_ITEM_TYPE); - builder.withValue(Note.NOTE, note); operationList.add(builder.build()); } } - + if (mPhotoList != null) { - boolean first = true; for (PhotoData photoData : mPhotoList) { builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); builder.withValueBackReference(Photo.RAW_CONTACT_ID, 0); builder.withValue(Data.MIMETYPE, Photo.CONTENT_ITEM_TYPE); builder.withValue(Photo.PHOTO, photoData.photoBytes); - if (first) { - builder.withValue(Data.IS_PRIMARY, 1); - first = false; + if (photoData.isPrimary) { + builder.withValue(Photo.IS_PRIMARY, 1); } operationList.add(builder.build()); } @@ -1310,12 +1207,12 @@ public class ContactStruct { builder.withValue(Data.MIMETYPE, Website.CONTENT_ITEM_TYPE); builder.withValue(Website.URL, website); // There's no information about the type of URL in vCard. - // We use TYPE_HOME for safety. - builder.withValue(Website.TYPE, Website.TYPE_HOME); + // We use TYPE_HOMEPAGE for safety. + builder.withValue(Website.TYPE, Website.TYPE_HOMEPAGE); operationList.add(builder.build()); } } - + if (!TextUtils.isEmpty(mBirthday)) { builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); builder.withValueBackReference(Event.RAW_CONTACT_ID, 0); @@ -1325,6 +1222,36 @@ public class ContactStruct { operationList.add(builder.build()); } + if (mAndroidCustomPropertyList != null) { + for (List<String> customPropertyList : mAndroidCustomPropertyList) { + int size = customPropertyList.size(); + if (size < 2 || TextUtils.isEmpty(customPropertyList.get(0))) { + continue; + } else if (size > VCardConstants.MAX_DATA_COLUMN + 1) { + size = VCardConstants.MAX_DATA_COLUMN + 1; + customPropertyList = + customPropertyList.subList(0, VCardConstants.MAX_DATA_COLUMN + 2); + } + + int i = 0; + for (final String customPropertyValue : customPropertyList) { + if (i == 0) { + final String mimeType = customPropertyValue; + builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); + builder.withValueBackReference(GroupMembership.RAW_CONTACT_ID, 0); + builder.withValue(Data.MIMETYPE, mimeType); + } else { // 1 <= i && i <= MAX_DATA_COLUMNS + if (!TextUtils.isEmpty(customPropertyValue)) { + builder.withValue("data" + i, customPropertyValue); + } + } + + i++; + } + operationList.add(builder.build()); + } + } + if (myGroupsId != null) { builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); builder.withValueBackReference(GroupMembership.RAW_CONTACT_ID, 0); @@ -1342,6 +1269,28 @@ public class ContactStruct { } } + public static VCardEntry buildFromResolver(ContentResolver resolver) { + return buildFromResolver(resolver, Contacts.CONTENT_URI); + } + + public static VCardEntry buildFromResolver(ContentResolver resolver, Uri uri) { + + return null; + } + + private boolean nameFieldsAreEmpty() { + return (TextUtils.isEmpty(mFamilyName) + && TextUtils.isEmpty(mMiddleName) + && TextUtils.isEmpty(mGivenName) + && TextUtils.isEmpty(mPrefix) + && TextUtils.isEmpty(mSuffix) + && TextUtils.isEmpty(mFullName) + && TextUtils.isEmpty(mPhoneticFamilyName) + && TextUtils.isEmpty(mPhoneticMiddleName) + && TextUtils.isEmpty(mPhoneticGivenName) + && TextUtils.isEmpty(mPhoneticFullName)); + } + public boolean isIgnorable() { return getDisplayName().length() == 0; } @@ -1364,4 +1313,99 @@ public class ContactStruct { return ""; } } + + // All getter methods should be used carefully, since they may change + // in the future as of 2009-10-05, on which I cannot be sure this structure + // is completely consolidated. + // + // Also note that these getter methods should be used only after + // all properties being pushed into this object. If not, incorrect + // value will "be stored in the local cache and" be returned to you. + + public String getFamilyName() { + return mFamilyName; + } + + public String getGivenName() { + return mGivenName; + } + + public String getMiddleName() { + return mMiddleName; + } + + public String getPrefix() { + return mPrefix; + } + + public String getSuffix() { + return mSuffix; + } + + public String getFullName() { + return mFullName; + } + + public String getPhoneticFamilyName() { + return mPhoneticFamilyName; + } + + public String getPhoneticGivenName() { + return mPhoneticGivenName; + } + + public String getPhoneticMiddleName() { + return mPhoneticMiddleName; + } + + public String getPhoneticFullName() { + return mPhoneticFullName; + } + + public final List<String> getNickNameList() { + return mNickNameList; + } + + public String getBirthday() { + return mBirthday; + } + + public final List<String> getNotes() { + return mNoteList; + } + + public final List<PhoneData> getPhoneList() { + return mPhoneList; + } + + public final List<EmailData> getEmailList() { + return mEmailList; + } + + public final List<PostalData> getPostalList() { + return mPostalList; + } + + public final List<OrganizationData> getOrganizationList() { + return mOrganizationList; + } + + public final List<ImData> getImList() { + return mImList; + } + + public final List<PhotoData> getPhotoList() { + return mPhotoList; + } + + public final List<String> getWebsiteList() { + return mWebsiteList; + } + + public String getDisplayName() { + if (mDisplayName == null) { + constructDisplayName(); + } + return mDisplayName; + } } diff --git a/core/java/android/pim/vcard/EntryCommitter.java b/core/java/android/pim/vcard/VCardEntryCommitter.java index 3f1655d..ebbbab6 100644 --- a/core/java/android/pim/vcard/EntryCommitter.java +++ b/core/java/android/pim/vcard/VCardEntryCommitter.java @@ -19,28 +19,36 @@ import android.content.ContentResolver; import android.util.Log; /** - * EntryHandler implementation which commits the entry to Contacts Provider + * <P> + * {@link VCardEntryHandler} implementation which commits the entry to ContentResolver. + * </P> + * <P> + * Note:<BR /> + * Each vCard may contain big photo images encoded by BASE64, + * If we store all vCard entries in memory, OutOfMemoryError may be thrown. + * Thus, this class push each VCard entry into ContentResolver immediately. + * </P> */ -public class EntryCommitter implements EntryHandler { - public static String LOG_TAG = "vcard.EntryComitter"; +public class VCardEntryCommitter implements VCardEntryHandler { + public static String LOG_TAG = "VCardEntryComitter"; - private ContentResolver mContentResolver; + private final ContentResolver mContentResolver; private long mTimeToCommit; - - public EntryCommitter(ContentResolver resolver) { + + public VCardEntryCommitter(ContentResolver resolver) { mContentResolver = resolver; } - public void onParsingStart() { + public void onStart() { } - - public void onParsingEnd() { + + public void onEnd() { if (VCardConfig.showPerformanceLog()) { Log.d(LOG_TAG, String.format("time to commit entries: %d ms", mTimeToCommit)); } } - public void onEntryCreated(final ContactStruct contactStruct) { + public void onEntryCreated(final VCardEntry contactStruct) { long start = System.currentTimeMillis(); contactStruct.pushIntoContentResolver(mContentResolver); mTimeToCommit += System.currentTimeMillis() - start; diff --git a/core/java/android/pim/vcard/VCardDataBuilder.java b/core/java/android/pim/vcard/VCardEntryConstructor.java index d2026d0..290ca2b 100644 --- a/core/java/android/pim/vcard/VCardDataBuilder.java +++ b/core/java/android/pim/vcard/VCardEntryConstructor.java @@ -30,123 +30,105 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; -/** - * VBuilder for VCard. VCard may contain big photo images encoded by BASE64, - * If we store all VNode entries in memory like VDataBuilder.java, - * OutOfMemoryError may be thrown. Thus, this class push each VCard entry into - * ContentResolver immediately. - */ -public class VCardDataBuilder implements VCardBuilder { - static private String LOG_TAG = "VCardDataBuilder"; - +public class VCardEntryConstructor implements VCardInterpreter { + private static String LOG_TAG = "VCardEntryConstructor"; + /** * If there's no other information available, this class uses this charset for encoding - * byte arrays. + * byte arrays to String. */ - static public String TARGET_CHARSET = "UTF-8"; - - private ContactStruct.Property mCurrentProperty = new ContactStruct.Property(); - private ContactStruct mCurrentContactStruct; + /* package */ static final String DEFAULT_CHARSET_FOR_DECODED_BYTES = "UTF-8"; + + private VCardEntry.Property mCurrentProperty = new VCardEntry.Property(); + private VCardEntry mCurrentContactStruct; private String mParamType; /** - * The charset using which VParser parses the text. + * The charset using which {@link VCardInterpreter} parses the text. */ - private String mSourceCharset; - + private String mInputCharset; + /** * The charset with which byte array is encoded to String. */ - private String mTargetCharset; - private boolean mStrictLineBreakParsing; - + final private String mCharsetForDecodedBytes; + final private boolean mStrictLineBreakParsing; final private int mVCardType; final private Account mAccount; - // Just for testing. + /** For measuring performance. */ private long mTimePushIntoContentResolver; - - private List<EntryHandler> mEntryHandlers = new ArrayList<EntryHandler>(); - - public VCardDataBuilder() { - this(null, null, false, VCardConfig.VCARD_TYPE_V21_GENERIC, null); + + final private List<VCardEntryHandler> mEntryHandlers = new ArrayList<VCardEntryHandler>(); + + public VCardEntryConstructor() { + this(null, null, false, VCardConfig.VCARD_TYPE_V21_GENERIC_UTF8, null); } - /** - * @hide - */ - public VCardDataBuilder(int vcardType) { + public VCardEntryConstructor(final int vcardType) { this(null, null, false, vcardType, null); } - /** - * @hide - */ - public VCardDataBuilder(String charset, - boolean strictLineBreakParsing, int vcardType, Account account) { + public VCardEntryConstructor(final String charset, final boolean strictLineBreakParsing, + final int vcardType, final Account account) { this(null, charset, strictLineBreakParsing, vcardType, account); } - - /** - * @hide - */ - public VCardDataBuilder(String sourceCharset, - String targetCharset, - boolean strictLineBreakParsing, - int vcardType, - Account account) { - if (sourceCharset != null) { - mSourceCharset = sourceCharset; + + public VCardEntryConstructor(final String inputCharset, final String charsetForDetodedBytes, + final boolean strictLineBreakParsing, final int vcardType, + final Account account) { + if (inputCharset != null) { + mInputCharset = inputCharset; } else { - mSourceCharset = VCardConfig.DEFAULT_CHARSET; + mInputCharset = VCardConfig.DEFAULT_CHARSET; } - if (targetCharset != null) { - mTargetCharset = targetCharset; + if (charsetForDetodedBytes != null) { + mCharsetForDecodedBytes = charsetForDetodedBytes; } else { - mTargetCharset = TARGET_CHARSET; + mCharsetForDecodedBytes = DEFAULT_CHARSET_FOR_DECODED_BYTES; } mStrictLineBreakParsing = strictLineBreakParsing; mVCardType = vcardType; mAccount = account; } - - public void addEntryHandler(EntryHandler entryHandler) { + + public void addEntryHandler(VCardEntryHandler entryHandler) { mEntryHandlers.add(entryHandler); } public void start() { - for (EntryHandler entryHandler : mEntryHandlers) { - entryHandler.onParsingStart(); + for (VCardEntryHandler entryHandler : mEntryHandlers) { + entryHandler.onStart(); } } public void end() { - for (EntryHandler entryHandler : mEntryHandlers) { - entryHandler.onParsingEnd(); + for (VCardEntryHandler entryHandler : mEntryHandlers) { + entryHandler.onEnd(); } } /** + * Called when the parse failed between {@link #startEntry()} and {@link #endEntry()}. + */ + public void clear() { + mCurrentContactStruct = null; + mCurrentProperty = new VCardEntry.Property(); + } + + /** * Assume that VCard is not nested. In other words, this code does not accept */ - public void startRecord(String type) { - // TODO: add the method clear() instead of using null for reducing GC? + public void startEntry() { if (mCurrentContactStruct != null) { - // This means startRecord() is called inside startRecord() - endRecord() block. - // TODO: should throw some Exception Log.e(LOG_TAG, "Nested VCard code is not supported now."); } - if (!type.equalsIgnoreCase("VCARD")) { - // TODO: add test case for this - Log.e(LOG_TAG, "This is not VCARD!"); - } - - mCurrentContactStruct = new ContactStruct(mVCardType, mAccount); + mCurrentContactStruct = new VCardEntry(mVCardType, mAccount); } - public void endRecord() { + public void endEntry() { mCurrentContactStruct.consolidateFields(); - for (EntryHandler entryHandler : mEntryHandlers) { + for (VCardEntryHandler entryHandler : mEntryHandlers) { entryHandler.onEntryCreated(mCurrentContactStruct); } mCurrentContactStruct = null; @@ -165,9 +147,8 @@ public class VCardDataBuilder implements VCardBuilder { } public void propertyGroup(String group) { - // ContactStruct does not support Group. } - + public void propertyParamType(String type) { if (mParamType != null) { Log.e(LOG_TAG, "propertyParamType() is called more than once " + @@ -184,26 +165,26 @@ public class VCardDataBuilder implements VCardBuilder { mCurrentProperty.addParameter(mParamType, value); mParamType = null; } - - private String encodeString(String originalString, String targetCharset) { - if (mSourceCharset.equalsIgnoreCase(targetCharset)) { + + private String encodeString(String originalString, String charsetForDecodedBytes) { + if (mInputCharset.equalsIgnoreCase(charsetForDecodedBytes)) { return originalString; } - Charset charset = Charset.forName(mSourceCharset); + Charset charset = Charset.forName(mInputCharset); ByteBuffer byteBuffer = charset.encode(originalString); // byteBuffer.array() "may" return byte array which is larger than // byteBuffer.remaining(). Here, we keep on the safe side. byte[] bytes = new byte[byteBuffer.remaining()]; byteBuffer.get(bytes); try { - return new String(bytes, targetCharset); + return new String(bytes, charsetForDecodedBytes); } catch (UnsupportedEncodingException e) { - Log.e(LOG_TAG, "Failed to encode: charset=" + targetCharset); + Log.e(LOG_TAG, "Failed to encode: charset=" + charsetForDecodedBytes); return null; } } - - private String handleOneValue(String value, String targetCharset, String encoding) { + + private String handleOneValue(String value, String charsetForDecodedBytes, String encoding) { if (encoding != null) { if (encoding.equals("BASE64") || encoding.equals("B")) { mCurrentProperty.setPropertyBytes(Base64.decodeBase64(value.getBytes())); @@ -269,9 +250,9 @@ public class VCardDataBuilder implements VCardBuilder { } byte[] bytes; try { - bytes = builder.toString().getBytes(mSourceCharset); + bytes = builder.toString().getBytes(mInputCharset); } catch (UnsupportedEncodingException e1) { - Log.e(LOG_TAG, "Failed to encode: charset=" + mSourceCharset); + Log.e(LOG_TAG, "Failed to encode: charset=" + mInputCharset); bytes = builder.toString().getBytes(); } @@ -283,38 +264,37 @@ public class VCardDataBuilder implements VCardBuilder { } try { - return new String(bytes, targetCharset); + return new String(bytes, charsetForDecodedBytes); } catch (UnsupportedEncodingException e) { - Log.e(LOG_TAG, "Failed to encode: charset=" + targetCharset); + Log.e(LOG_TAG, "Failed to encode: charset=" + charsetForDecodedBytes); return new String(bytes); } } // Unknown encoding. Fall back to default. } - return encodeString(value, targetCharset); + return encodeString(value, charsetForDecodedBytes); } public void propertyValues(List<String> values) { - if (values == null || values.size() == 0) { + if (values == null || values.isEmpty()) { return; } final Collection<String> charsetCollection = mCurrentProperty.getParameters("CHARSET"); - String charset = + final String charset = ((charsetCollection != null) ? charsetCollection.iterator().next() : null); - String targetCharset = CharsetUtils.nameForDefaultVendor(charset); - final Collection<String> encodingCollection = mCurrentProperty.getParameters("ENCODING"); - String encoding = + final String encoding = ((encodingCollection != null) ? encodingCollection.iterator().next() : null); - - if (targetCharset == null || targetCharset.length() == 0) { - targetCharset = mTargetCharset; + + String charsetForDecodedBytes = CharsetUtils.nameForDefaultVendor(charset); + if (charsetForDecodedBytes == null || charsetForDecodedBytes.length() == 0) { + charsetForDecodedBytes = mCharsetForDecodedBytes; } - - for (String value : values) { + + for (final String value : values) { mCurrentProperty.addToPropertyValueList( - handleOneValue(value, targetCharset, encoding)); + handleOneValue(value, charsetForDecodedBytes, encoding)); } } diff --git a/core/java/android/pim/vcard/VCardEntryCounter.java b/core/java/android/pim/vcard/VCardEntryCounter.java index f99b46c..7bab50d 100644 --- a/core/java/android/pim/vcard/VCardEntryCounter.java +++ b/core/java/android/pim/vcard/VCardEntryCounter.java @@ -17,29 +17,32 @@ package android.pim.vcard; import java.util.List; -public class VCardEntryCounter implements VCardBuilder { +/** + * The class which just counts the number of vCard entries in the specified input. + */ +public class VCardEntryCounter implements VCardInterpreter { private int mCount; - + public int getCount() { return mCount; } - + public void start() { } - + public void end() { } - public void startRecord(String type) { + public void startEntry() { } - public void endRecord() { + public void endEntry() { mCount++; } - + public void startProperty() { } - + public void endProperty() { } @@ -57,4 +60,4 @@ public class VCardEntryCounter implements VCardBuilder { public void propertyValues(List<String> values) { } -}
\ No newline at end of file +} diff --git a/core/java/android/pim/vcard/EntryHandler.java b/core/java/android/pim/vcard/VCardEntryHandler.java index 7fb8114..83a67fe 100644 --- a/core/java/android/pim/vcard/EntryHandler.java +++ b/core/java/android/pim/vcard/VCardEntryHandler.java @@ -16,23 +16,23 @@ package android.pim.vcard; /** - * Unlike {@link VCardBuilder}, this (and {@link VCardDataBuilder}) assumes - * "each VCard entry should be correctly parsed and passed to each EntryHandler object", + * The interface called by {@link VCardEntryConstructor}. Useful when you don't want to + * handle detailed information as what {@link VCardParser} provides via {@link VCardInterpreter}. */ -public interface EntryHandler { +public interface VCardEntryHandler { /** * Called when the parsing started. */ - public void onParsingStart(); + public void onStart(); /** * The method called when one VCard entry is successfully created */ - public void onEntryCreated(final ContactStruct entry); + public void onEntryCreated(final VCardEntry entry); /** * Called when the parsing ended. * Able to be use this method for showing performance log, etc. */ - public void onParsingEnd(); + public void onEnd(); } diff --git a/core/java/android/pim/vcard/VCardInterpreter.java b/core/java/android/pim/vcard/VCardInterpreter.java new file mode 100644 index 0000000..b5237c0 --- /dev/null +++ b/core/java/android/pim/vcard/VCardInterpreter.java @@ -0,0 +1,102 @@ +/* + * 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 android.pim.vcard; + +import java.util.List; + +/** + * <P> + * The interface which should be implemented by the classes which have to analyze each + * vCard entry more minutely than {@link VCardEntry} class analysis. + * </P> + * <P> + * Here, there are several terms specific to vCard (and this library). + * </P> + * <P> + * The term "entry" is one vCard representation in the input, which should start with "BEGIN:VCARD" + * and end with "END:VCARD". + * </P> + * <P> + * The term "property" is one line in vCard entry, which consists of "group", "property name", + * "parameter(param) names and values", and "property values". + * </P> + * <P> + * e.g. group1.propName;paramName1=paramValue1;paramName2=paramValue2;propertyValue1;propertyValue2... + * </P> + */ +public interface VCardInterpreter { + /** + * Called when vCard interpretation started. + */ + void start(); + + /** + * Called when vCard interpretation finished. + */ + void end(); + + /** + * Called when parsing one vCard entry started. + * More specifically, this method is called when "BEGIN:VCARD" is read. + */ + void startEntry(); + + /** + * Called when parsing one vCard entry ended. + * More specifically, this method is called when "END:VCARD" is read. + * Note that {@link #startEntry()} may be called since + * vCard (especially 2.1) allows nested vCard. + */ + void endEntry(); + + /** + * Called when reading one property started. + */ + void startProperty(); + + /** + * Called when reading one property ended. + */ + void endProperty(); + + /** + * @param group A group name. This method may be called more than once or may not be + * called at all, depending on how many gruoups are appended to the property. + */ + void propertyGroup(String group); + + /** + * @param name A property name like "N", "FN", "ADR", etc. + */ + void propertyName(String name); + + /** + * @param type A parameter name like "ENCODING", "CHARSET", etc. + */ + void propertyParamType(String type); + + /** + * @param value A parameter value. This method may be called without + * {@link #propertyParamType(String)} being called (when the vCard is vCard 2.1). + */ + void propertyParamValue(String value); + + /** + * @param values List of property values. The size of values would be 1 unless + * coressponding property name is "N", "ADR", or "ORG". + */ + void propertyValues(List<String> values); +} diff --git a/core/java/android/pim/vcard/VCardBuilderCollection.java b/core/java/android/pim/vcard/VCardInterpreterCollection.java index e3985b6..99f81f7 100644 --- a/core/java/android/pim/vcard/VCardBuilderCollection.java +++ b/core/java/android/pim/vcard/VCardInterpreterCollection.java @@ -18,81 +18,84 @@ package android.pim.vcard; import java.util.Collection; import java.util.List; -public class VCardBuilderCollection implements VCardBuilder { - - private final Collection<VCardBuilder> mVCardBuilderCollection; +/** + * The {@link VCardInterpreter} implementation which aggregates more than one + * {@link VCardInterpreter} objects and make a user object treat them as one + * {@link VCardInterpreter} object. + */ +public class VCardInterpreterCollection implements VCardInterpreter { + private final Collection<VCardInterpreter> mInterpreterCollection; - public VCardBuilderCollection(Collection<VCardBuilder> vBuilderCollection) { - mVCardBuilderCollection = vBuilderCollection; + public VCardInterpreterCollection(Collection<VCardInterpreter> interpreterCollection) { + mInterpreterCollection = interpreterCollection; } - - public Collection<VCardBuilder> getVCardBuilderBaseCollection() { - return mVCardBuilderCollection; + + public Collection<VCardInterpreter> getCollection() { + return mInterpreterCollection; } - + public void start() { - for (VCardBuilder builder : mVCardBuilderCollection) { + for (VCardInterpreter builder : mInterpreterCollection) { builder.start(); } } - + public void end() { - for (VCardBuilder builder : mVCardBuilderCollection) { + for (VCardInterpreter builder : mInterpreterCollection) { builder.end(); } } - public void startRecord(String type) { - for (VCardBuilder builder : mVCardBuilderCollection) { - builder.startRecord(type); + public void startEntry() { + for (VCardInterpreter builder : mInterpreterCollection) { + builder.startEntry(); } } - - public void endRecord() { - for (VCardBuilder builder : mVCardBuilderCollection) { - builder.endRecord(); + + public void endEntry() { + for (VCardInterpreter builder : mInterpreterCollection) { + builder.endEntry(); } } public void startProperty() { - for (VCardBuilder builder : mVCardBuilderCollection) { + for (VCardInterpreter builder : mInterpreterCollection) { builder.startProperty(); } } - public void endProperty() { - for (VCardBuilder builder : mVCardBuilderCollection) { + for (VCardInterpreter builder : mInterpreterCollection) { builder.endProperty(); } } public void propertyGroup(String group) { - for (VCardBuilder builder : mVCardBuilderCollection) { + for (VCardInterpreter builder : mInterpreterCollection) { builder.propertyGroup(group); } } public void propertyName(String name) { - for (VCardBuilder builder : mVCardBuilderCollection) { + for (VCardInterpreter builder : mInterpreterCollection) { builder.propertyName(name); } } public void propertyParamType(String type) { - for (VCardBuilder builder : mVCardBuilderCollection) { + for (VCardInterpreter builder : mInterpreterCollection) { builder.propertyParamType(type); } } public void propertyParamValue(String value) { - for (VCardBuilder builder : mVCardBuilderCollection) { + for (VCardInterpreter builder : mInterpreterCollection) { builder.propertyParamValue(value); } } public void propertyValues(List<String> values) { - for (VCardBuilder builder : mVCardBuilderCollection) { + for (VCardInterpreter builder : mInterpreterCollection) { builder.propertyValues(values); } } diff --git a/core/java/android/pim/vcard/VCardParser.java b/core/java/android/pim/vcard/VCardParser.java index b5e5049..57c52a6 100644 --- a/core/java/android/pim/vcard/VCardParser.java +++ b/core/java/android/pim/vcard/VCardParser.java @@ -21,63 +21,74 @@ import java.io.IOException; import java.io.InputStream; public abstract class VCardParser { - + protected final int mParseType; protected boolean mCanceled; - + + public VCardParser() { + this(VCardConfig.PARSE_TYPE_UNKNOWN); + } + + public VCardParser(int parseType) { + mParseType = parseType; + } + /** + * <P> * Parses the given stream and send the VCard data into VCardBuilderBase object. - * + * </P. + * <P> * Note that vCard 2.1 specification allows "CHARSET" parameter, and some career sets * local encoding to it. For example, Japanese phone career uses Shift_JIS, which is * formally allowed in VCard 2.1, but not recommended in VCard 3.0. In VCard 2.1, * In some exreme case, some VCard may have different charsets in one VCard (though * we do not see any device which emits such kind of malicious data) - * + * </P> + * <P> * In order to avoid "misunderstanding" charset as much as possible, this method * use "ISO-8859-1" for reading the stream. When charset is specified in some property - * (with "CHARSET=..." attribute), the string is decoded to raw bytes and encoded to + * (with "CHARSET=..." parameter), the string is decoded to raw bytes and encoded to * the charset. This method assumes that "ISO-8859-1" has 1 to 1 mapping in all 8bit * characters, which is not completely sure. In some cases, this "decoding-encoding" * scheme may fail. To avoid the case, - * - * We recommend you to use VCardSourceDetector and detect which kind of source the + * </P> + * <P> + * We recommend you to use {@link VCardSourceDetector} and detect which kind of source the * VCard comes from and explicitly specify a charset using the result. - * + * </P> + * * @param is The source to parse. - * @param builder The VCardBuilderBase object which used to construct data. If you want to - * include multiple VCardBuilderBase objects in this field, consider using - * {#link VCardBuilderCollection} class. + * @param interepreter A {@link VCardInterpreter} object which used to construct data. * @return Returns true for success. Otherwise returns false. * @throws IOException, VCardException */ - public abstract boolean parse(InputStream is, VCardBuilder builder) + public abstract boolean parse(InputStream is, VCardInterpreter interepreter) throws IOException, VCardException; /** + * <P> * The method variants which accept charset. - * + * </P> + * <P> * RFC 2426 "recommends" (not forces) to use UTF-8, so it may be OK to use * UTF-8 as an encoding when parsing vCard 3.0. But note that some Japanese * phone uses Shift_JIS as a charset (e.g. W61SH), and another uses - * "CHARSET=SHIFT_JIS", which is explicitly prohibited in vCard 3.0 specification - * (e.g. W53K). - * + * "CHARSET=SHIFT_JIS", which is explicitly prohibited in vCard 3.0 specification (e.g. W53K). + * </P> + * * @param is The source to parse. * @param charset Charset to be used. * @param builder The VCardBuilderBase object. * @return Returns true when successful. Otherwise returns false. * @throws IOException, VCardException */ - public abstract boolean parse(InputStream is, String charset, VCardBuilder builder) + public abstract boolean parse(InputStream is, String charset, VCardInterpreter builder) throws IOException, VCardException; /** * The method variants which tells this object the operation is already canceled. - * XXX: Is this really necessary? - * @hide */ public abstract void parse(InputStream is, String charset, - VCardBuilder builder, boolean canceled) + VCardInterpreter builder, boolean canceled) throws IOException, VCardException; /** diff --git a/core/java/android/pim/vcard/VCardParser_V21.java b/core/java/android/pim/vcard/VCardParser_V21.java index 11b3888..c2928cb 100644 --- a/core/java/android/pim/vcard/VCardParser_V21.java +++ b/core/java/android/pim/vcard/VCardParser_V21.java @@ -15,11 +15,11 @@ */ package android.pim.vcard; +import android.pim.vcard.exception.VCardAgentNotSupportedException; import android.pim.vcard.exception.VCardException; import android.pim.vcard.exception.VCardInvalidCommentLineException; import android.pim.vcard.exception.VCardInvalidLineException; import android.pim.vcard.exception.VCardNestedException; -import android.pim.vcard.exception.VCardNotSupportedException; import android.pim.vcard.exception.VCardVersionException; import android.util.Log; @@ -31,13 +31,14 @@ import java.io.Reader; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; +import java.util.Set; /** - * This class is used to parse vcard. Please refer to vCard Specification 2.1. + * This class is used to parse vCard. Please refer to vCard Specification 2.1 for more detail. */ public class VCardParser_V21 extends VCardParser { - private static final String LOG_TAG = "vcard.VCardParser_V21"; - + private static final String LOG_TAG = "VCardParser_V21"; + /** Store the known-type */ private static final HashSet<String> sKnownTypeSet = new HashSet<String>( Arrays.asList("DOM", "INTL", "POSTAL", "PARCEL", "HOME", "WORK", @@ -52,7 +53,7 @@ public class VCardParser_V21 extends VCardParser { /** Store the known-value */ private static final HashSet<String> sKnownValueSet = new HashSet<String>( Arrays.asList("INLINE", "URL", "CONTENT-ID", "CID")); - + /** Store the property names available in vCard 2.1 */ private static final HashSet<String> sAvailablePropertyNameSetV21 = new HashSet<String>(Arrays.asList( @@ -72,7 +73,7 @@ public class VCardParser_V21 extends VCardParser { private String mPreviousLine; /** The builder to build parsed data */ - protected VCardBuilder mBuilder = null; + protected VCardInterpreter mBuilder = null; /** * The encoding type. "Encoding" in vCard is different from "Charset". @@ -82,7 +83,7 @@ public class VCardParser_V21 extends VCardParser { protected final String sDefaultEncoding = "8BIT"; - // Should not directly read a line from this. Use getLine() instead. + // Should not directly read a line from this object. Use getLine() instead. protected BufferedReader mReader; // In some cases, vCard is nested. Currently, we only consider the most interior vCard data. @@ -91,9 +92,10 @@ public class VCardParser_V21 extends VCardParser { // In order to reduce warning message as much as possible, we hold the value which made Logger // emit a warning message. - protected HashSet<String> mWarningValueMap = new HashSet<String>(); - - // Just for debugging + protected Set<String> mUnknownTypeMap = new HashSet<String>(); + protected Set<String> mUnknownValueMap = new HashSet<String>(); + + // For measuring performance. private long mTimeTotal; private long mTimeReadStartRecord; private long mTimeReadEndRecord; @@ -106,23 +108,25 @@ public class VCardParser_V21 extends VCardParser { private long mTimeHandleMiscPropertyValue; private long mTimeHandleQuotedPrintable; private long mTimeHandleBase64; - - /** - * Create a new VCard parser. - */ + public VCardParser_V21() { - super(); + this(null); } public VCardParser_V21(VCardSourceDetector detector) { - super(); - if (detector != null && detector.getType() == VCardSourceDetector.TYPE_FOMA) { + this(detector != null ? detector.getEstimatedType() : VCardConfig.PARSE_TYPE_UNKNOWN); + } + + public VCardParser_V21(int parseType) { + super(parseType); + if (parseType == VCardConfig.PARSE_TYPE_FOMA) { mNestCount = 1; } } - + /** - * Parse the file at the given position + * Parses the file at the given position. + * * vcard_file = [wsls] vcard [wsls] */ protected void parseVCardFile() throws IOException, VCardException { @@ -146,18 +150,22 @@ public class VCardParser_V21 extends VCardParser { } } - protected String getVersion() { - return "2.1"; + protected int getVersion() { + return VCardConfig.FLAG_V21; } - + + protected String getVersionString() { + return VCardConstants.VERSION_V21; + } + /** * @return true when the propertyName is a valid property name. */ protected boolean isValidPropertyName(String propertyName) { if (!(sAvailablePropertyNameSetV21.contains(propertyName.toUpperCase()) || propertyName.startsWith("X-")) && - !mWarningValueMap.contains(propertyName)) { - mWarningValueMap.add(propertyName); + !mUnknownTypeMap.contains(propertyName)) { + mUnknownTypeMap.add(propertyName); Log.w(LOG_TAG, "Property name unsupported by vCard 2.1: " + propertyName); } return true; @@ -194,11 +202,11 @@ public class VCardParser_V21 extends VCardParser { } } } - + /** - * vcard = "BEGIN" [ws] ":" [ws] "VCARD" [ws] 1*CRLF - * items *CRLF - * "END" [ws] ":" [ws] "VCARD" + * vcard = "BEGIN" [ws] ":" [ws] "VCARD" [ws] 1*CRLF + * items *CRLF + * "END" [ws] ":" [ws] "VCARD" */ private boolean parseOneVCard(boolean firstReading) throws IOException, VCardException { boolean allowGarbage = false; @@ -219,7 +227,7 @@ public class VCardParser_V21 extends VCardParser { long start; if (mBuilder != null) { start = System.currentTimeMillis(); - mBuilder.startRecord("VCARD"); + mBuilder.startEntry(); mTimeReadStartRecord += System.currentTimeMillis() - start; } start = System.currentTimeMillis(); @@ -228,7 +236,7 @@ public class VCardParser_V21 extends VCardParser { readEndVCard(true, false); if (mBuilder != null) { start = System.currentTimeMillis(); - mBuilder.endRecord(); + mBuilder.endEntry(); mTimeReadEndRecord += System.currentTimeMillis() - start; } return true; @@ -239,8 +247,7 @@ public class VCardParser_V21 extends VCardParser { * @throws IOException * @throws VCardException */ - protected boolean readBeginVCard(boolean allowGarbage) - throws IOException, VCardException { + protected boolean readBeginVCard(boolean allowGarbage) throws IOException, VCardException { String line; do { while (true) { @@ -255,8 +262,9 @@ public class VCardParser_V21 extends VCardParser { int length = strArray.length; // Though vCard 2.1/3.0 specification does not allow lower cases, - // some data may have them, so we allow it (Actually, previous code - // had explicitly allowed "BEGIN:vCard" though there's no example). + // vCard file emitted by some external vCard expoter have such invalid Strings. + // So we allow it. + // e.g. BEGIN:vCard if (length == 2 && strArray[0].trim().equalsIgnoreCase("BEGIN") && strArray[1].trim().equalsIgnoreCase("VCARD")) { @@ -279,7 +287,7 @@ public class VCardParser_V21 extends VCardParser { /** * The arguments useCache and allowGarbase are usually true and false accordingly when * this function is called outside this function itself. - * + * * @param useCache When true, line is obtained from mPreviousline. Otherwise, getLine() * is used. * @param allowGarbage When true, ignore non "END:VCARD" line. @@ -322,7 +330,6 @@ public class VCardParser_V21 extends VCardParser { * / item */ protected void parseItems() throws IOException, VCardException { - /* items *CRLF item / item */ boolean ended = false; if (mBuilder != null) { @@ -363,12 +370,12 @@ public class VCardParser_V21 extends VCardParser { * / [groups "."] "ADR" [params] ":" addressparts CRLF * / [groups "."] "ORG" [params] ":" orgparts CRLF * / [groups "."] "N" [params] ":" nameparts CRLF - * / [groups "."] "AGENT" [params] ":" vcard CRLF + * / [groups "."] "AGENT" [params] ":" vcard CRLF */ protected boolean parseItem() throws IOException, VCardException { mEncoding = sDefaultEncoding; - String line = getNonEmptyLine(); + final String line = getNonEmptyLine(); long start = System.currentTimeMillis(); String[] propertyNameAndValue = separateLineAndHandleGroup(line); @@ -399,9 +406,10 @@ public class VCardParser_V21 extends VCardParser { } else { throw new VCardException("Unknown BEGIN type: " + propertyValue); } - } else if (propertyName.equals("VERSION") && !propertyValue.equals(getVersion())) { + } else if (propertyName.equals("VERSION") && + !propertyValue.equals(getVersionString())) { throw new VCardVersionException("Incompatible version: " + - propertyValue + " != " + getVersion()); + propertyValue + " != " + getVersionString()); } start = System.currentTimeMillis(); handlePropertyValue(propertyName, propertyValue); @@ -409,23 +417,22 @@ public class VCardParser_V21 extends VCardParser { return false; } - throw new VCardException("Unknown property name: \"" + - propertyName + "\""); + throw new VCardException("Unknown property name: \"" + propertyName + "\""); } static private final int STATE_GROUP_OR_PROPNAME = 0; static private final int STATE_PARAMS = 1; - // vCard 3.1 specification allows double-quoted param-value, while vCard 2.1 does not. + // vCard 3.0 specification allows double-quoted param-value, while vCard 2.1 does not. // This is just for safety. static private final int STATE_PARAMS_IN_DQUOTE = 2; - + protected String[] separateLineAndHandleGroup(String line) throws VCardException { - int length = line.length(); int state = STATE_GROUP_OR_PROPNAME; int nameIndex = 0; - String[] propertyNameAndValue = new String[2]; + final String[] propertyNameAndValue = new String[2]; + final int length = line.length(); if (length > 0 && line.charAt(0) == '#') { throw new VCardInvalidCommentLineException(); } @@ -433,82 +440,84 @@ public class VCardParser_V21 extends VCardParser { for (int i = 0; i < length; i++) { char ch = line.charAt(i); switch (state) { - case STATE_GROUP_OR_PROPNAME: - if (ch == ':') { - String propertyName = line.substring(nameIndex, i); - if (propertyName.equalsIgnoreCase("END")) { - mPreviousLine = line; - return null; - } - if (mBuilder != null) { - mBuilder.propertyName(propertyName); - } - propertyNameAndValue[0] = propertyName; - if (i < length - 1) { - propertyNameAndValue[1] = line.substring(i + 1); - } else { - propertyNameAndValue[1] = ""; - } - return propertyNameAndValue; - } else if (ch == '.') { - String groupName = line.substring(nameIndex, i); - if (mBuilder != null) { - mBuilder.propertyGroup(groupName); - } - nameIndex = i + 1; - } else if (ch == ';') { - String propertyName = line.substring(nameIndex, i); - if (propertyName.equalsIgnoreCase("END")) { - mPreviousLine = line; - return null; + case STATE_GROUP_OR_PROPNAME: { + if (ch == ':') { + final String propertyName = line.substring(nameIndex, i); + if (propertyName.equalsIgnoreCase("END")) { + mPreviousLine = line; + return null; + } + if (mBuilder != null) { + mBuilder.propertyName(propertyName); + } + propertyNameAndValue[0] = propertyName; + if (i < length - 1) { + propertyNameAndValue[1] = line.substring(i + 1); + } else { + propertyNameAndValue[1] = ""; + } + return propertyNameAndValue; + } else if (ch == '.') { + String groupName = line.substring(nameIndex, i); + if (mBuilder != null) { + mBuilder.propertyGroup(groupName); + } + nameIndex = i + 1; + } else if (ch == ';') { + String propertyName = line.substring(nameIndex, i); + if (propertyName.equalsIgnoreCase("END")) { + mPreviousLine = line; + return null; + } + if (mBuilder != null) { + mBuilder.propertyName(propertyName); + } + propertyNameAndValue[0] = propertyName; + nameIndex = i + 1; + state = STATE_PARAMS; } - if (mBuilder != null) { - mBuilder.propertyName(propertyName); - } - propertyNameAndValue[0] = propertyName; - nameIndex = i + 1; - state = STATE_PARAMS; + break; } - break; - case STATE_PARAMS: - if (ch == '"') { - state = STATE_PARAMS_IN_DQUOTE; - } else if (ch == ';') { - handleParams(line.substring(nameIndex, i)); - nameIndex = i + 1; - } else if (ch == ':') { - handleParams(line.substring(nameIndex, i)); - if (i < length - 1) { - propertyNameAndValue[1] = line.substring(i + 1); - } else { - propertyNameAndValue[1] = ""; + case STATE_PARAMS: { + if (ch == '"') { + state = STATE_PARAMS_IN_DQUOTE; + } else if (ch == ';') { + handleParams(line.substring(nameIndex, i)); + nameIndex = i + 1; + } else if (ch == ':') { + handleParams(line.substring(nameIndex, i)); + if (i < length - 1) { + propertyNameAndValue[1] = line.substring(i + 1); + } else { + propertyNameAndValue[1] = ""; + } + return propertyNameAndValue; } - return propertyNameAndValue; + break; } - break; - case STATE_PARAMS_IN_DQUOTE: - if (ch == '"') { - state = STATE_PARAMS; + case STATE_PARAMS_IN_DQUOTE: { + if (ch == '"') { + state = STATE_PARAMS; + } + break; } - break; } } throw new VCardInvalidLineException("Invalid line: \"" + line + "\""); } - - + /** - * params = ";" [ws] paramlist - * paramlist = paramlist [ws] ";" [ws] param - * / param - * param = "TYPE" [ws] "=" [ws] ptypeval - * / "VALUE" [ws] "=" [ws] pvalueval - * / "ENCODING" [ws] "=" [ws] pencodingval - * / "CHARSET" [ws] "=" [ws] charsetval - * / "LANGUAGE" [ws] "=" [ws] langval - * / "X-" word [ws] "=" [ws] word - * / knowntype + * params = ";" [ws] paramlist + * paramlist = paramlist [ws] ";" [ws] param + * / param + * param = "TYPE" [ws] "=" [ws] ptypeval + * / "VALUE" [ws] "=" [ws] pvalueval + * / "ENCODING" [ws] "=" [ws] pencodingval + * / "CHARSET" [ws] "=" [ws] charsetval + * / "LANGUAGE" [ws] "=" [ws] langval + * / "X-" word [ws] "=" [ws] word + * / knowntype */ protected void handleParams(String params) throws VCardException { String[] strArray = params.split("=", 2); @@ -531,19 +540,27 @@ public class VCardParser_V21 extends VCardParser { throw new VCardException("Unknown type \"" + paramName + "\""); } } else { - handleType(strArray[0]); + handleParamWithoutName(strArray[0]); } } /** + * vCard 3.0 parser may throw VCardException. + */ + @SuppressWarnings("unused") + protected void handleParamWithoutName(final String paramValue) throws VCardException { + handleType(paramValue); + } + + /** * ptypeval = knowntype / "X-" word */ protected void handleType(final String ptypeval) { String upperTypeValue = ptypeval; if (!(sKnownTypeSet.contains(upperTypeValue) || upperTypeValue.startsWith("X-")) && - !mWarningValueMap.contains(ptypeval)) { - mWarningValueMap.add(ptypeval); - Log.w(LOG_TAG, "Type unsupported by vCard 2.1: " + ptypeval); + !mUnknownTypeMap.contains(ptypeval)) { + mUnknownTypeMap.add(ptypeval); + Log.w(LOG_TAG, "TYPE unsupported by vCard 2.1: " + ptypeval); } if (mBuilder != null) { mBuilder.propertyParamType("TYPE"); @@ -554,15 +571,16 @@ public class VCardParser_V21 extends VCardParser { /** * pvalueval = "INLINE" / "URL" / "CONTENT-ID" / "CID" / "X-" word */ - protected void handleValue(final String pvalueval) throws VCardException { - if (sKnownValueSet.contains(pvalueval.toUpperCase()) || - pvalueval.startsWith("X-")) { - if (mBuilder != null) { - mBuilder.propertyParamType("VALUE"); - mBuilder.propertyParamValue(pvalueval); - } - } else { - throw new VCardException("Unknown value \"" + pvalueval + "\""); + protected void handleValue(final String pvalueval) { + if (!sKnownValueSet.contains(pvalueval.toUpperCase()) && + pvalueval.startsWith("X-") && + !mUnknownValueMap.contains(pvalueval)) { + mUnknownValueMap.add(pvalueval); + Log.w(LOG_TAG, "VALUE unsupported by vCard 2.1: " + pvalueval); + } + if (mBuilder != null) { + mBuilder.propertyParamType("VALUE"); + mBuilder.propertyParamValue(pvalueval); } } @@ -583,8 +601,8 @@ public class VCardParser_V21 extends VCardParser { } /** - * vCard specification only allows us-ascii and iso-8859-xxx (See RFC 1521), - * but some vCard contains other charset, so we allow them. + * vCard 2.1 specification only allows us-ascii and iso-8859-xxx (See RFC 1521), + * but today's vCard often contains other charset, so we allow them. */ protected void handleCharset(String charsetval) { if (mBuilder != null) { @@ -592,7 +610,7 @@ public class VCardParser_V21 extends VCardParser { mBuilder.propertyParamValue(charsetval); } } - + /** * See also Section 7.1 of RFC 1521 */ @@ -630,12 +648,12 @@ public class VCardParser_V21 extends VCardParser { mBuilder.propertyParamValue(paramValue); } } - - protected void handlePropertyValue(String propertyName, String propertyValue) throws - IOException, VCardException { + + protected void handlePropertyValue(String propertyName, String propertyValue) + throws IOException, VCardException { if (mEncoding.equalsIgnoreCase("QUOTED-PRINTABLE")) { - long start = System.currentTimeMillis(); - String result = getQuotedPrintable(propertyValue); + final long start = System.currentTimeMillis(); + final String result = getQuotedPrintable(propertyValue); if (mBuilder != null) { ArrayList<String> v = new ArrayList<String>(); v.add(result); @@ -644,11 +662,11 @@ public class VCardParser_V21 extends VCardParser { mTimeHandleQuotedPrintable += System.currentTimeMillis() - start; } else if (mEncoding.equalsIgnoreCase("BASE64") || mEncoding.equalsIgnoreCase("B")) { - long start = System.currentTimeMillis(); + final long start = System.currentTimeMillis(); // It is very rare, but some BASE64 data may be so big that // OutOfMemoryError occurs. To ignore such cases, use try-catch. try { - String result = getBase64(propertyValue); + final String result = getBase64(propertyValue); if (mBuilder != null) { ArrayList<String> v = new ArrayList<String>(); v.add(result); @@ -668,7 +686,7 @@ public class VCardParser_V21 extends VCardParser { Log.w(LOG_TAG, "The encoding unsupported by vCard spec: \"" + mEncoding + "\"."); } - long start = System.currentTimeMillis(); + final long start = System.currentTimeMillis(); if (mBuilder != null) { ArrayList<String> v = new ArrayList<String>(); v.add(maybeUnescapeText(propertyValue)); @@ -772,70 +790,46 @@ public class VCardParser_V21 extends VCardParser { } if (mBuilder != null) { - StringBuilder builder = new StringBuilder(); - ArrayList<String> list = new ArrayList<String>(); - int length = propertyValue.length(); - for (int i = 0; i < length; i++) { - char ch = propertyValue.charAt(i); - if (ch == '\\' && i < length - 1) { - char nextCh = propertyValue.charAt(i + 1); - String unescapedString = maybeUnescapeCharacter(nextCh); - if (unescapedString != null) { - builder.append(unescapedString); - i++; - } else { - builder.append(ch); - } - } else if (ch == ';') { - list.add(builder.toString()); - builder = new StringBuilder(); - } else { - builder.append(ch); - } - } - list.add(builder.toString()); - mBuilder.propertyValues(list); + mBuilder.propertyValues(VCardUtils.constructListFromValue( + propertyValue, (getVersion() == VCardConfig.FLAG_V30))); } } - + /** * vCard 2.1 specifies AGENT allows one vcard entry. It is not encoded at all. - * - * item = ... - * / [groups "."] "AGENT" - * [params] ":" vcard CRLF - * vcard = "BEGIN" [ws] ":" [ws] "VCARD" [ws] 1*CRLF - * items *CRLF "END" [ws] ":" [ws] "VCARD" - * + * + * item = ... + * / [groups "."] "AGENT" + * [params] ":" vcard CRLF + * vcard = "BEGIN" [ws] ":" [ws] "VCARD" [ws] 1*CRLF + * items *CRLF "END" [ws] ":" [ws] "VCARD" */ - protected void handleAgent(String propertyValue) throws VCardException { - throw new VCardNotSupportedException("AGENT Property is not supported now."); - /* This is insufficient support. Also, AGENT Property is very rare. - Ignore it for now. - - String[] strArray = propertyValue.split(":", 2); - if (!(strArray.length == 2 || - strArray[0].trim().equalsIgnoreCase("BEGIN") && - strArray[1].trim().equalsIgnoreCase("VCARD"))) { - throw new VCardException("BEGIN:VCARD != \"" + propertyValue + "\""); + protected void handleAgent(final String propertyValue) throws VCardException { + if (!propertyValue.toUpperCase().contains("BEGIN:VCARD")) { + // Apparently invalid line seen in Windows Mobile 6.5. Ignore them. + return; + } else { + throw new VCardAgentNotSupportedException("AGENT Property is not supported now."); } - parseItems(); - readEndVCard(); - */ + // TODO: Support AGENT property. } /** * For vCard 3.0. */ - protected String maybeUnescapeText(String text) { + protected String maybeUnescapeText(final String text) { return text; } - + /** * Returns unescaped String if the character should be unescaped. Return null otherwise. * e.g. In vCard 2.1, "\;" should be unescaped into ";" while "\x" should not be. */ - protected String maybeUnescapeCharacter(char ch) { + protected String maybeUnescapeCharacter(final char ch) { + return unescapeCharacter(ch); + } + + public static String unescapeCharacter(final char ch) { // Original vCard 2.1 specification does not allow transformation // "\:" -> ":", "\," -> ",", and "\\" -> "\", but previous implementation of // this class allowed them, so keep it as is. @@ -847,14 +841,17 @@ public class VCardParser_V21 extends VCardParser { } @Override - public boolean parse(InputStream is, VCardBuilder builder) + public boolean parse(final InputStream is, final VCardInterpreter builder) throws IOException, VCardException { return parse(is, VCardConfig.DEFAULT_CHARSET, builder); } @Override - public boolean parse(InputStream is, String charset, VCardBuilder builder) + public boolean parse(InputStream is, String charset, VCardInterpreter builder) throws IOException, VCardException { + if (charset == null) { + charset = VCardConfig.DEFAULT_CHARSET; + } final InputStreamReader tmpReader = new InputStreamReader(is, charset); if (VCardConfig.showPerformanceLog()) { mReader = new CustomBufferedReader(tmpReader); @@ -882,7 +879,7 @@ public class VCardParser_V21 extends VCardParser { } @Override - public void parse(InputStream is, String charset, VCardBuilder builder, boolean canceled) + public void parse(InputStream is, String charset, VCardInterpreter builder, boolean canceled) throws IOException, VCardException { mCanceled = canceled; parse(is, charset, builder); diff --git a/core/java/android/pim/vcard/VCardParser_V30.java b/core/java/android/pim/vcard/VCardParser_V30.java index 384649a..4ecfe97 100644 --- a/core/java/android/pim/vcard/VCardParser_V30.java +++ b/core/java/android/pim/vcard/VCardParser_V30.java @@ -23,12 +23,12 @@ import java.util.Arrays; import java.util.HashSet; /** - * This class is used to parse vcard3.0. <br> - * Please refer to vCard Specification 3.0 (http://tools.ietf.org/html/rfc2426) + * The class used to parse vCard 3.0. + * Please refer to vCard Specification 3.0 (http://tools.ietf.org/html/rfc2426). */ public class VCardParser_V30 extends VCardParser_V21 { - private static final String LOG_TAG = "vcard.VCardParser_V30"; - + private static final String LOG_TAG = "VCardParser_V30"; + private static final HashSet<String> sAcceptablePropsWithParam = new HashSet<String>( Arrays.asList( "BEGIN", "LOGO", "PHOTO", "LABEL", "FN", "TITLE", "SOUND", @@ -46,31 +46,64 @@ public class VCardParser_V30 extends VCardParser_V21 { private static final HashSet<String> acceptablePropsWithoutParam = new HashSet<String>(); private String mPreviousLine; - + private boolean mEmittedAgentWarning = false; - + + /** + * True when the caller wants the parser to be strict about the input. + * Currently this is only for testing. + */ + private final boolean mStrictParsing; + + public VCardParser_V30() { + super(); + mStrictParsing = false; + } + + /** + * @param strictParsing when true, this object throws VCardException when the vcard is not + * valid from the view of vCard 3.0 specification (defined in RFC 2426). Note that this class + * is not fully yet for being used with this flag and may not notice invalid line(s). + * + * @hide currently only for testing! + */ + public VCardParser_V30(boolean strictParsing) { + super(); + mStrictParsing = strictParsing; + } + + public VCardParser_V30(int parseMode) { + super(parseMode); + mStrictParsing = false; + } + @Override - protected String getVersion() { - return Constants.VERSION_V30; + protected int getVersion() { + return VCardConfig.FLAG_V30; } - + + @Override + protected String getVersionString() { + return VCardConstants.VERSION_V30; + } + @Override protected boolean isValidPropertyName(String propertyName) { if (!(sAcceptablePropsWithParam.contains(propertyName) || acceptablePropsWithoutParam.contains(propertyName) || propertyName.startsWith("X-")) && - !mWarningValueMap.contains(propertyName)) { - mWarningValueMap.add(propertyName); + !mUnknownTypeMap.contains(propertyName)) { + mUnknownTypeMap.add(propertyName); Log.w(LOG_TAG, "Property name unsupported by vCard 3.0: " + propertyName); } return true; } - + @Override protected boolean isValidEncoding(String encoding) { return sAcceptableEncodingV30.contains(encoding.toUpperCase()); } - + @Override protected String getLine() throws IOException { if (mPreviousLine != null) { @@ -152,17 +185,17 @@ public class VCardParser_V30 extends VCardParser_V21 { /** - * vcard = [group "."] "BEGIN" ":" "VCARD" 1*CRLF - * 1*(contentline) + * vcard = [group "."] "BEGIN" ":" "VCARD" 1 * CRLF + * 1 * (contentline) * ;A vCard object MUST include the VERSION, FN and N types. - * [group "."] "END" ":" "VCARD" 1*CRLF + * [group "."] "END" ":" "VCARD" 1 * CRLF */ @Override protected boolean readBeginVCard(boolean allowGarbage) throws IOException, VCardException { // TODO: vCard 3.0 supports group. return super.readBeginVCard(allowGarbage); } - + @Override protected void readEndVCard(boolean useCache, boolean allowGarbage) throws IOException, VCardException { @@ -189,17 +222,21 @@ public class VCardParser_V30 extends VCardParser_V21 { } } } - + @Override protected void handleAnyParam(String paramName, String paramValue) { - // vCard 3.0 accept comma-separated multiple values, but - // current PropertyNode does not accept it. - // For now, we do not split the values. - // - // TODO: fix this. super.handleAnyParam(paramName, paramValue); } - + + @Override + protected void handleParamWithoutName(final String paramValue) throws VCardException { + if (mStrictParsing) { + throw new VCardException("Parameter without name is not acceptable in vCard 3.0"); + } else { + super.handleParamWithoutName(paramValue); + } + } + /** * vCard 3.0 defines * @@ -224,7 +261,7 @@ public class VCardParser_V30 extends VCardParser_V21 { @Override protected void handleAgent(String propertyValue) { - // The way how vCard 3.0 supports "AGENT" is completely different from vCard 2.0. + // The way how vCard 3.0 supports "AGENT" is completely different from vCard 2.1. // // e.g. // AGENT:BEGIN:VCARD\nFN:Joe Friday\nTEL:+1-919-555-7878\n @@ -239,13 +276,13 @@ public class VCardParser_V30 extends VCardParser_V21 { // AGENT;VALUE=uri: // CID:JQPUBLIC.part3.960129T083020.xyzMail@host3.com // - // This is not VCARD. Should we support this? - // throw new VCardException("AGENT in vCard 3.0 is not supported yet."); + // This is not vCard. Should we support this? + // + // Just ignore the line for now, since we cannot know how to handle it... if (!mEmittedAgentWarning) { Log.w(LOG_TAG, "AGENT in vCard 3.0 is not supported yet. Ignore it"); mEmittedAgentWarning = true; } - // Just ignore the line for now, since we cannot know how to handle it... } /** @@ -256,7 +293,7 @@ public class VCardParser_V30 extends VCardParser_V21 { protected String getBase64(String firstString) throws IOException, VCardException { StringBuilder builder = new StringBuilder(); builder.append(firstString); - + while (true) { String line = getLine(); if (line == null) { @@ -280,10 +317,14 @@ public class VCardParser_V30 extends VCardParser_V21 { * ; \\ encodes \, \n or \N encodes newline * ; \; encodes ;, \, encodes , * - * Note: Apple escape ':' into '\:' while does not escape '\' - */ + * Note: Apple escapes ':' into '\:' while does not escape '\' + */ @Override protected String maybeUnescapeText(String text) { + return unescapeText(text); + } + + public static String unescapeText(String text) { StringBuilder builder = new StringBuilder(); int length = text.length(); for (int i = 0; i < length; i++) { @@ -299,15 +340,19 @@ public class VCardParser_V30 extends VCardParser_V21 { builder.append(ch); } } - return builder.toString(); + return builder.toString(); } - + @Override protected String maybeUnescapeCharacter(char ch) { + return unescapeCharacter(ch); + } + + public static String unescapeCharacter(char ch) { if (ch == 'n' || ch == 'N') { return "\n"; } else { return String.valueOf(ch); - } + } } } diff --git a/core/java/android/pim/vcard/VCardSourceDetector.java b/core/java/android/pim/vcard/VCardSourceDetector.java index 7e2be2b..7297c50 100644 --- a/core/java/android/pim/vcard/VCardSourceDetector.java +++ b/core/java/android/pim/vcard/VCardSourceDetector.java @@ -25,15 +25,7 @@ import java.util.Set; * Currently this implementation is very premature. * @hide */ -public class VCardSourceDetector implements VCardBuilder { - // Should only be used in package. - static final int TYPE_UNKNOWN = 0; - static final int TYPE_APPLE = 1; - static final int TYPE_JAPANESE_MOBILE_PHONE = 2; // Used in Japanese mobile phones. - static final int TYPE_FOMA = 3; // Used in some Japanese FOMA mobile phones. - static final int TYPE_WINDOWS_MOBILE_JP = 4; - // TODO: Excel, etc. - +public class VCardSourceDetector implements VCardInterpreter { private static Set<String> APPLE_SIGNS = new HashSet<String>(Arrays.asList( "X-PHONETIC-FIRST-NAME", "X-PHONETIC-MIDDLE-NAME", "X-PHONETIC-LAST-NAME", "X-ABADR", "X-ABUID")); @@ -51,7 +43,7 @@ public class VCardSourceDetector implements VCardBuilder { "X-SD-DESCRIPTION")); private static String TYPE_FOMA_CHARSET_SIGN = "X-SD-CHAR_CODE"; - private int mType = TYPE_UNKNOWN; + private int mType = VCardConfig.PARSE_TYPE_UNKNOWN; // Some mobile phones (like FOMA) tells us the charset of the data. private boolean mNeedParseSpecifiedCharset; private String mSpecifiedCharset; @@ -62,7 +54,7 @@ public class VCardSourceDetector implements VCardBuilder { public void end() { } - public void startRecord(String type) { + public void startEntry() { } public void startProperty() { @@ -72,7 +64,7 @@ public class VCardSourceDetector implements VCardBuilder { public void endProperty() { } - public void endRecord() { + public void endEntry() { } public void propertyGroup(String group) { @@ -80,21 +72,21 @@ public class VCardSourceDetector implements VCardBuilder { public void propertyName(String name) { if (name.equalsIgnoreCase(TYPE_FOMA_CHARSET_SIGN)) { - mType = TYPE_FOMA; + mType = VCardConfig.PARSE_TYPE_FOMA; mNeedParseSpecifiedCharset = true; return; } - if (mType != TYPE_UNKNOWN) { + if (mType != VCardConfig.PARSE_TYPE_UNKNOWN) { return; } if (WINDOWS_MOBILE_PHONE_SIGNS.contains(name)) { - mType = TYPE_WINDOWS_MOBILE_JP; + mType = VCardConfig.PARSE_TYPE_WINDOWS_MOBILE_JP; } else if (FOMA_SIGNS.contains(name)) { - mType = TYPE_FOMA; + mType = VCardConfig.PARSE_TYPE_FOMA; } else if (JAPANESE_MOBILE_PHONE_SIGNS.contains(name)) { - mType = TYPE_JAPANESE_MOBILE_PHONE; + mType = VCardConfig.PARSE_TYPE_MOBILE_PHONE_JP; } else if (APPLE_SIGNS.contains(name)) { - mType = TYPE_APPLE; + mType = VCardConfig.PARSE_TYPE_APPLE; } } @@ -110,7 +102,7 @@ public class VCardSourceDetector implements VCardBuilder { } } - int getType() { + /* package */ int getEstimatedType() { return mType; } @@ -124,14 +116,14 @@ public class VCardSourceDetector implements VCardBuilder { return mSpecifiedCharset; } switch (mType) { - case TYPE_WINDOWS_MOBILE_JP: - case TYPE_FOMA: - case TYPE_JAPANESE_MOBILE_PHONE: - return "SHIFT_JIS"; - case TYPE_APPLE: - return "UTF-8"; - default: - return null; + case VCardConfig.PARSE_TYPE_WINDOWS_MOBILE_JP: + case VCardConfig.PARSE_TYPE_FOMA: + case VCardConfig.PARSE_TYPE_MOBILE_PHONE_JP: + return "SHIFT_JIS"; + case VCardConfig.PARSE_TYPE_APPLE: + return "UTF-8"; + default: + return null; } } } diff --git a/core/java/android/pim/vcard/VCardUtils.java b/core/java/android/pim/vcard/VCardUtils.java index dd44288..11b112b 100644 --- a/core/java/android/pim/vcard/VCardUtils.java +++ b/core/java/android/pim/vcard/VCardUtils.java @@ -16,16 +16,19 @@ package android.pim.vcard; import android.content.ContentProviderOperation; -import android.content.ContentValues; import android.provider.ContactsContract.Data; +import android.provider.ContactsContract.CommonDataKinds.Im; import android.provider.ContactsContract.CommonDataKinds.Phone; import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; import android.telephony.PhoneNumberUtils; import android.text.TextUtils; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; @@ -33,58 +36,81 @@ import java.util.Set; * Utilities for VCard handling codes. */ public class VCardUtils { - /* - * TODO: some of methods in this class should be placed to the more appropriate place... - */ - // Note that not all types are included in this map/set, since, for example, TYPE_HOME_FAX is - // converted to two attribute Strings. These only contain some minor fields valid in both + // converted to two parameter Strings. These only contain some minor fields valid in both // vCard and current (as of 2009-08-07) Contacts structure. private static final Map<Integer, String> sKnownPhoneTypesMap_ItoS; - private static final Set<String> sPhoneTypesSetUnknownToContacts; - - private static final Map<String, Integer> sKnownPhoneTypesMap_StoI; - + private static final Set<String> sPhoneTypesUnknownToContactsSet; + private static final Map<String, Integer> sKnownPhoneTypeMap_StoI; + private static final Map<Integer, String> sKnownImPropNameMap_ItoS; + private static final Set<String> sMobilePhoneLabelSet; + static { sKnownPhoneTypesMap_ItoS = new HashMap<Integer, String>(); - sKnownPhoneTypesMap_StoI = new HashMap<String, Integer>(); - - sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_CAR, Constants.ATTR_TYPE_CAR); - sKnownPhoneTypesMap_StoI.put(Constants.ATTR_TYPE_CAR, Phone.TYPE_CAR); - sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_PAGER, Constants.ATTR_TYPE_PAGER); - sKnownPhoneTypesMap_StoI.put(Constants.ATTR_TYPE_PAGER, Phone.TYPE_PAGER); - sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_ISDN, Constants.ATTR_TYPE_ISDN); - sKnownPhoneTypesMap_StoI.put(Constants.ATTR_TYPE_ISDN, Phone.TYPE_ISDN); + sKnownPhoneTypeMap_StoI = new HashMap<String, Integer>(); + + sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_CAR, VCardConstants.PARAM_TYPE_CAR); + sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_CAR, Phone.TYPE_CAR); + sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_PAGER, VCardConstants.PARAM_TYPE_PAGER); + sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_PAGER, Phone.TYPE_PAGER); + sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_ISDN, VCardConstants.PARAM_TYPE_ISDN); + sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_ISDN, Phone.TYPE_ISDN); - sKnownPhoneTypesMap_StoI.put(Constants.ATTR_TYPE_HOME, Phone.TYPE_HOME); - sKnownPhoneTypesMap_StoI.put(Constants.ATTR_TYPE_WORK, Phone.TYPE_WORK); - sKnownPhoneTypesMap_StoI.put(Constants.ATTR_TYPE_CELL, Phone.TYPE_MOBILE); + sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_HOME, Phone.TYPE_HOME); + sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_WORK, Phone.TYPE_WORK); + sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_CELL, Phone.TYPE_MOBILE); - sKnownPhoneTypesMap_StoI.put(Constants.ATTR_TYPE_PHONE_EXTRA_OTHER, Phone.TYPE_OTHER); - sKnownPhoneTypesMap_StoI.put(Constants.ATTR_TYPE_PHONE_EXTRA_CALLBACK, Phone.TYPE_CALLBACK); - sKnownPhoneTypesMap_StoI.put( - Constants.ATTR_TYPE_PHONE_EXTRA_COMPANY_MAIN, Phone.TYPE_COMPANY_MAIN); - sKnownPhoneTypesMap_StoI.put(Constants.ATTR_TYPE_PHONE_EXTRA_RADIO, Phone.TYPE_RADIO); - sKnownPhoneTypesMap_StoI.put(Constants.ATTR_TYPE_PHONE_EXTRA_TELEX, Phone.TYPE_TELEX); - sKnownPhoneTypesMap_StoI.put(Constants.ATTR_TYPE_PHONE_EXTRA_TTY_TDD, Phone.TYPE_TTY_TDD); - sKnownPhoneTypesMap_StoI.put(Constants.ATTR_TYPE_PHONE_EXTRA_ASSISTANT, Phone.TYPE_ASSISTANT); - - sPhoneTypesSetUnknownToContacts = new HashSet<String>(); - sPhoneTypesSetUnknownToContacts.add(Constants.ATTR_TYPE_MODEM); - sPhoneTypesSetUnknownToContacts.add(Constants.ATTR_TYPE_MSG); - sPhoneTypesSetUnknownToContacts.add(Constants.ATTR_TYPE_BBS); - sPhoneTypesSetUnknownToContacts.add(Constants.ATTR_TYPE_VIDEO); + sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_OTHER, Phone.TYPE_OTHER); + sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_CALLBACK, + Phone.TYPE_CALLBACK); + sKnownPhoneTypeMap_StoI.put( + VCardConstants.PARAM_PHONE_EXTRA_TYPE_COMPANY_MAIN, Phone.TYPE_COMPANY_MAIN); + sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_RADIO, Phone.TYPE_RADIO); + sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_TTY_TDD, + Phone.TYPE_TTY_TDD); + sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_ASSISTANT, + Phone.TYPE_ASSISTANT); + + sPhoneTypesUnknownToContactsSet = new HashSet<String>(); + sPhoneTypesUnknownToContactsSet.add(VCardConstants.PARAM_TYPE_MODEM); + sPhoneTypesUnknownToContactsSet.add(VCardConstants.PARAM_TYPE_MSG); + sPhoneTypesUnknownToContactsSet.add(VCardConstants.PARAM_TYPE_BBS); + sPhoneTypesUnknownToContactsSet.add(VCardConstants.PARAM_TYPE_VIDEO); + + sKnownImPropNameMap_ItoS = new HashMap<Integer, String>(); + sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_AIM, VCardConstants.PROPERTY_X_AIM); + sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_MSN, VCardConstants.PROPERTY_X_MSN); + sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_YAHOO, VCardConstants.PROPERTY_X_YAHOO); + sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_SKYPE, VCardConstants.PROPERTY_X_SKYPE_USERNAME); + sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_GOOGLE_TALK, + VCardConstants.PROPERTY_X_GOOGLE_TALK); + sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_ICQ, VCardConstants.PROPERTY_X_ICQ); + sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_JABBER, VCardConstants.PROPERTY_X_JABBER); + sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_QQ, VCardConstants.PROPERTY_X_QQ); + sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_NETMEETING, VCardConstants.PROPERTY_X_NETMEETING); + + // \u643A\u5E2F\u96FB\u8A71 = Full-width Hiragana "Keitai-Denwa" (mobile phone) + // \u643A\u5E2F = Full-width Hiragana "Keitai" (mobile phone) + // \u30B1\u30A4\u30BF\u30A4 = Full-width Katakana "Keitai" (mobile phone) + // \uFF79\uFF72\uFF80\uFF72 = Half-width Katakana "Keitai" (mobile phone) + sMobilePhoneLabelSet = new HashSet<String>(Arrays.asList( + "MOBILE", "\u643A\u5E2F\u96FB\u8A71", "\u643A\u5E2F", "\u30B1\u30A4\u30BF\u30A4", + "\uFF79\uFF72\uFF80\uFF72")); } - - public static String getPhoneAttributeString(Integer type) { + + public static String getPhoneTypeString(Integer type) { return sKnownPhoneTypesMap_ItoS.get(type); } - + /** * Returns Interger when the given types can be parsed as known type. Returns String object * when not, which should be set to label. */ - public static Object getPhoneTypeFromStrings(Collection<String> types) { + public static Object getPhoneTypeFromStrings(Collection<String> types, + String number) { + if (number == null) { + number = ""; + } int type = -1; String label = null; boolean isFax = false; @@ -92,18 +118,37 @@ public class VCardUtils { if (types != null) { for (String typeString : types) { - typeString = typeString.toUpperCase(); - if (typeString.equals(Constants.ATTR_TYPE_PREF)) { + if (typeString == null) { + continue; + } + typeString = typeString.toUpperCase(); + if (typeString.equals(VCardConstants.PARAM_TYPE_PREF)) { hasPref = true; - } else if (typeString.equals(Constants.ATTR_TYPE_FAX)) { + } else if (typeString.equals(VCardConstants.PARAM_TYPE_FAX)) { isFax = true; } else { if (typeString.startsWith("X-") && type < 0) { typeString = typeString.substring(2); } - Integer tmp = sKnownPhoneTypesMap_StoI.get(typeString); + if (typeString.length() == 0) { + continue; + } + final Integer tmp = sKnownPhoneTypeMap_StoI.get(typeString); if (tmp != null) { - type = tmp; + final int typeCandidate = tmp; + // TYPE_PAGER is prefered when the number contains @ surronded by + // a pager number and a domain name. + // e.g. + // o 1111@domain.com + // x @domain.com + // x 1111@ + final int indexOfAt = number.indexOf("@"); + if ((typeCandidate == Phone.TYPE_PAGER + && 0 < indexOfAt && indexOfAt < number.length() - 1) + || type < 0 + || type == Phone.TYPE_CUSTOM) { + type = tmp; + } } else if (type < 0) { type = Phone.TYPE_CUSTOM; label = typeString; @@ -134,37 +179,54 @@ public class VCardUtils { return type; } } - - public static boolean isValidPhoneAttribute(String phoneAttribute, int vcardType) { - // TODO: check the following. - // - it may violate vCard spec - // - it may contain non-ASCII characters - // - // TODO: use vcardType - return (phoneAttribute.startsWith("X-") || phoneAttribute.startsWith("x-") || - sPhoneTypesSetUnknownToContacts.contains(phoneAttribute)); + + @SuppressWarnings("deprecation") + public static boolean isMobilePhoneLabel(final String label) { + // For backward compatibility. + // Detail: Until Donut, there isn't TYPE_MOBILE for email while there is now. + // To support mobile type at that time, this custom label had been used. + return (android.provider.Contacts.ContactMethodsColumns.MOBILE_EMAIL_TYPE_NAME.equals(label) + || sMobilePhoneLabelSet.contains(label)); + } + + public static boolean isValidInV21ButUnknownToContactsPhoteType(final String label) { + return sPhoneTypesUnknownToContactsSet.contains(label); + } + + public static String getPropertyNameForIm(final int protocol) { + return sKnownImPropNameMap_ItoS.get(protocol); } - - public static String[] sortNameElements(int vcardType, - String familyName, String middleName, String givenName) { - String[] list = new String[3]; - switch (VCardConfig.getNameOrderType(vcardType)) { - case VCardConfig.NAME_ORDER_JAPANESE: - // TODO: Should handle Ascii case? - list[0] = familyName; - list[1] = middleName; - list[2] = givenName; - break; - case VCardConfig.NAME_ORDER_EUROPE: - list[0] = middleName; - list[1] = givenName; - list[2] = familyName; - break; - default: - list[0] = givenName; - list[1] = middleName; - list[2] = familyName; - break; + + public static String[] sortNameElements(final int vcardType, + final String familyName, final String middleName, final String givenName) { + final String[] list = new String[3]; + final int nameOrderType = VCardConfig.getNameOrderType(vcardType); + switch (nameOrderType) { + case VCardConfig.NAME_ORDER_JAPANESE: { + if (containsOnlyPrintableAscii(familyName) && + containsOnlyPrintableAscii(givenName)) { + list[0] = givenName; + list[1] = middleName; + list[2] = familyName; + } else { + list[0] = familyName; + list[1] = middleName; + list[2] = givenName; + } + break; + } + case VCardConfig.NAME_ORDER_EUROPE: { + list[0] = middleName; + list[1] = givenName; + list[2] = familyName; + break; + } + default: { + list[0] = givenName; + list[1] = middleName; + list[2] = familyName; + break; + } } return list; } @@ -181,12 +243,11 @@ public class VCardUtils { * Inserts postal data into the builder object. * * Note that the data structure of ContactsContract is different from that defined in vCard. - * So some conversion may be performed in this method. See also - * {{@link #getVCardPostalElements(ContentValues)} + * So some conversion may be performed in this method. */ public static void insertStructuredPostalDataUsingContactsStruct(int vcardType, final ContentProviderOperation.Builder builder, - final ContactStruct.PostalData postalData) { + final VCardEntry.PostalData postalData) { builder.withValueBackReference(StructuredPostal.RAW_CONTACT_ID, 0); builder.withValue(Data.MIMETYPE, StructuredPostal.CONTENT_ITEM_TYPE); @@ -195,9 +256,22 @@ public class VCardUtils { builder.withValue(StructuredPostal.LABEL, postalData.label); } + final String streetString; + if (TextUtils.isEmpty(postalData.street)) { + if (TextUtils.isEmpty(postalData.extendedAddress)) { + streetString = null; + } else { + streetString = postalData.extendedAddress; + } + } else { + if (TextUtils.isEmpty(postalData.extendedAddress)) { + streetString = postalData.street; + } else { + streetString = postalData.street + " " + postalData.extendedAddress; + } + } builder.withValue(StructuredPostal.POBOX, postalData.pobox); - // Extended address is dropped since there's no relevant entry in ContactsContract. - builder.withValue(StructuredPostal.STREET, postalData.street); + builder.withValue(StructuredPostal.STREET, streetString); builder.withValue(StructuredPostal.CITY, postalData.localty); builder.withValue(StructuredPostal.REGION, postalData.region); builder.withValue(StructuredPostal.POSTCODE, postalData.postalCode); @@ -209,71 +283,24 @@ public class VCardUtils { builder.withValue(Data.IS_PRIMARY, 1); } } - - /** - * Returns String[] containing address information based on vCard spec - * (PO Box, Extended Address, Street, Locality, Region, Postal Code, Country Name). - * All String objects are non-null ("" is used when the relevant data is empty). - * - * Note that the data structure of ContactsContract is different from that defined in vCard. - * So some conversion may be performed in this method. See also - * {{@link #insertStructuredPostalDataUsingContactsStruct(int, - * android.content.ContentProviderOperation.Builder, - * android.pim.vcard.ContactStruct.PostalData)} - */ - public static String[] getVCardPostalElements(ContentValues contentValues) { - String[] dataArray = new String[7]; - dataArray[0] = contentValues.getAsString(StructuredPostal.POBOX); - if (dataArray[0] == null) { - dataArray[0] = ""; - } - // Extended addr. There's no relevant data in ContactsContract. - dataArray[1] = ""; - dataArray[2] = contentValues.getAsString(StructuredPostal.STREET); - if (dataArray[2] == null) { - dataArray[2] = ""; - } - // Assume that localty == city - dataArray[3] = contentValues.getAsString(StructuredPostal.CITY); - if (dataArray[3] == null) { - dataArray[3] = ""; - } - String region = contentValues.getAsString(StructuredPostal.REGION); - if (!TextUtils.isEmpty(region)) { - dataArray[4] = region; - } else { - dataArray[4] = ""; - } - dataArray[5] = contentValues.getAsString(StructuredPostal.POSTCODE); - if (dataArray[5] == null) { - dataArray[5] = ""; - } - dataArray[6] = contentValues.getAsString(StructuredPostal.COUNTRY); - if (dataArray[6] == null) { - dataArray[6] = ""; - } - return dataArray; - } - - public static String constructNameFromElements(int nameOrderType, - String familyName, String middleName, String givenName) { - return constructNameFromElements(nameOrderType, familyName, middleName, givenName, + public static String constructNameFromElements(final int vcardType, + final String familyName, final String middleName, final String givenName) { + return constructNameFromElements(vcardType, familyName, middleName, givenName, null, null); } - public static String constructNameFromElements(int nameOrderType, - String familyName, String middleName, String givenName, - String prefix, String suffix) { - StringBuilder builder = new StringBuilder(); - String[] nameList = sortNameElements(nameOrderType, - familyName, middleName, givenName); + public static String constructNameFromElements(final int vcardType, + final String familyName, final String middleName, final String givenName, + final String prefix, final String suffix) { + final StringBuilder builder = new StringBuilder(); + final String[] nameList = sortNameElements(vcardType, familyName, middleName, givenName); boolean first = true; if (!TextUtils.isEmpty(prefix)) { first = false; builder.append(prefix); } - for (String namePart : nameList) { + for (final String namePart : nameList) { if (!TextUtils.isEmpty(namePart)) { if (first) { first = false; @@ -291,18 +318,52 @@ public class VCardUtils { } return builder.toString(); } - - public static boolean containsOnlyPrintableAscii(String str) { - if (TextUtils.isEmpty(str)) { + + public static List<String> constructListFromValue(final String value, + final boolean isV30) { + final List<String> list = new ArrayList<String>(); + StringBuilder builder = new StringBuilder(); + int length = value.length(); + for (int i = 0; i < length; i++) { + char ch = value.charAt(i); + if (ch == '\\' && i < length - 1) { + char nextCh = value.charAt(i + 1); + final String unescapedString = + (isV30 ? VCardParser_V30.unescapeCharacter(nextCh) : + VCardParser_V21.unescapeCharacter(nextCh)); + if (unescapedString != null) { + builder.append(unescapedString); + i++; + } else { + builder.append(ch); + } + } else if (ch == ';') { + list.add(builder.toString()); + builder = new StringBuilder(); + } else { + builder.append(ch); + } + } + list.add(builder.toString()); + return list; + } + + public static boolean containsOnlyPrintableAscii(final String...values) { + if (values == null) { return true; } - - final int length = str.length(); - final int asciiFirst = 0x20; - final int asciiLast = 0x126; - for (int i = 0; i < length; i = str.offsetByCodePoints(i, 1)) { - int c = str.codePointAt(i); - if (c < asciiFirst || asciiLast < c) { + return containsOnlyPrintableAscii(Arrays.asList(values)); + } + + public static boolean containsOnlyPrintableAscii(final Collection<String> values) { + if (values == null) { + return true; + } + for (final String value : values) { + if (TextUtils.isEmpty(value)) { + continue; + } + if (!TextUtils.isPrintableAsciiOnly(value)) { return false; } } @@ -314,23 +375,37 @@ public class VCardUtils { * or not, which is required by vCard 2.1. * See the definition of "7bit" in vCard 2.1 spec for more information. */ - public static boolean containsOnlyNonCrLfPrintableAscii(String str) { - if (TextUtils.isEmpty(str)) { + public static boolean containsOnlyNonCrLfPrintableAscii(final String...values) { + if (values == null) { return true; } + return containsOnlyNonCrLfPrintableAscii(Arrays.asList(values)); + } - final int length = str.length(); + public static boolean containsOnlyNonCrLfPrintableAscii(final Collection<String> values) { + if (values == null) { + return true; + } final int asciiFirst = 0x20; - final int asciiLast = 0x126; - for (int i = 0; i < length; i = str.offsetByCodePoints(i, 1)) { - int c = str.codePointAt(i); - if (c < asciiFirst || asciiLast < c || c == '\n' || c == '\r') { - return false; + final int asciiLast = 0x7E; // included + for (final String value : values) { + if (TextUtils.isEmpty(value)) { + continue; + } + final int length = value.length(); + for (int i = 0; i < length; i = value.offsetByCodePoints(i, 1)) { + final int c = value.codePointAt(i); + if (!(asciiFirst <= c && c <= asciiLast)) { + return false; + } } } return true; } + private static final Set<Character> sUnAcceptableAsciiInV21WordSet = + new HashSet<Character>(Arrays.asList('[', ']', '=', ':', '.', ',', ' ')); + /** * This is useful since vCard 3.0 often requires the ("X-") properties and groups * should contain only alphabets, digits, and hyphen. @@ -340,86 +415,79 @@ public class VCardUtils { * such kind of input but must never output it unless the target is very specific * to the device which is able to parse the malformed input. */ - public static boolean containsOnlyAlphaDigitHyphen(String str) { - if (TextUtils.isEmpty(str)) { + public static boolean containsOnlyAlphaDigitHyphen(final String...values) { + if (values == null) { return true; } + return containsOnlyAlphaDigitHyphen(Arrays.asList(values)); + } - final int lowerAlphabetFirst = 0x41; // included ('A') - final int lowerAlphabetLast = 0x5b; // not included ('[') - final int upperAlphabetFirst = 0x61; // included ('a') - final int upperAlphabetLast = 0x7b; // included ('{') - final int digitFirst = 0x30; // included ('0') - final int digitLast = 0x39; // included ('9') + public static boolean containsOnlyAlphaDigitHyphen(final Collection<String> values) { + if (values == null) { + return true; + } + final int upperAlphabetFirst = 0x41; // A + final int upperAlphabetAfterLast = 0x5b; // [ + final int lowerAlphabetFirst = 0x61; // a + final int lowerAlphabetAfterLast = 0x7b; // { + final int digitFirst = 0x30; // 0 + final int digitAfterLast = 0x3A; // : final int hyphen = '-'; - final int length = str.length(); - for (int i = 0; i < length; i = str.offsetByCodePoints(i, 1)) { - int codepoint = str.codePointAt(i); - if (!((lowerAlphabetFirst <= codepoint && codepoint < lowerAlphabetLast) || - (upperAlphabetFirst <= codepoint && codepoint < upperAlphabetLast) || - (digitFirst <= codepoint && codepoint < digitLast) || + for (final String str : values) { + if (TextUtils.isEmpty(str)) { + continue; + } + final int length = str.length(); + for (int i = 0; i < length; i = str.offsetByCodePoints(i, 1)) { + int codepoint = str.codePointAt(i); + if (!((lowerAlphabetFirst <= codepoint && codepoint < lowerAlphabetAfterLast) || + (upperAlphabetFirst <= codepoint && codepoint < upperAlphabetAfterLast) || + (digitFirst <= codepoint && codepoint < digitAfterLast) || (codepoint == hyphen))) { - return false; + return false; + } } } return true; } - - // TODO: Replace wth the method in Base64 class. - private static char PAD = '='; - private static final char[] ENCODE64 = { - 'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P', - 'Q','R','S','T','U','V','W','X','Y','Z','a','b','c','d','e','f', - 'g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v', - 'w','x','y','z','0','1','2','3','4','5','6','7','8','9','+','/' - }; - - static public String encodeBase64(byte[] data) { - if (data == null) { - return ""; - } - char[] charBuffer = new char[(data.length + 2) / 3 * 4]; - int position = 0; - int _3byte = 0; - for (int i=0; i<data.length-2; i+=3) { - _3byte = ((data[i] & 0xFF) << 16) + ((data[i+1] & 0xFF) << 8) + (data[i+2] & 0xFF); - charBuffer[position++] = ENCODE64[_3byte >> 18]; - charBuffer[position++] = ENCODE64[(_3byte >> 12) & 0x3F]; - charBuffer[position++] = ENCODE64[(_3byte >> 6) & 0x3F]; - charBuffer[position++] = ENCODE64[_3byte & 0x3F]; + /** + * <P> + * Returns true when the given String is categorized as "word" specified in vCard spec 2.1. + * </P> + * <P> + * vCard 2.1 specifies:<BR /> + * word = <any printable 7bit us-ascii except []=:., > + * </P> + */ + public static boolean isV21Word(final String value) { + if (TextUtils.isEmpty(value)) { + return true; } - switch(data.length % 3) { - case 1: // [111111][11 0000][0000 00][000000] - _3byte = ((data[data.length-1] & 0xFF) << 16); - charBuffer[position++] = ENCODE64[_3byte >> 18]; - charBuffer[position++] = ENCODE64[(_3byte >> 12) & 0x3F]; - charBuffer[position++] = PAD; - charBuffer[position++] = PAD; - break; - case 2: // [111111][11 1111][1111 00][000000] - _3byte = ((data[data.length-2] & 0xFF) << 16) + ((data[data.length-1] & 0xFF) << 8); - charBuffer[position++] = ENCODE64[_3byte >> 18]; - charBuffer[position++] = ENCODE64[(_3byte >> 12) & 0x3F]; - charBuffer[position++] = ENCODE64[(_3byte >> 6) & 0x3F]; - charBuffer[position++] = PAD; - break; + final int asciiFirst = 0x20; + final int asciiLast = 0x7E; // included + final int length = value.length(); + for (int i = 0; i < length; i = value.offsetByCodePoints(i, 1)) { + final int c = value.codePointAt(i); + if (!(asciiFirst <= c && c <= asciiLast) || + sUnAcceptableAsciiInV21WordSet.contains((char)c)) { + return false; + } } - - return new String(charBuffer); + return true; } - - static public String toHalfWidthString(String orgString) { + + public static String toHalfWidthString(final String orgString) { if (TextUtils.isEmpty(orgString)) { return null; } - StringBuilder builder = new StringBuilder(); - int length = orgString.length(); - for (int i = 0; i < length; i++) { + final StringBuilder builder = new StringBuilder(); + final int length = orgString.length(); + for (int i = 0; i < length; i = orgString.offsetByCodePoints(i, 1)) { // All Japanese character is able to be expressed by char. // Do not need to use String#codepPointAt(). - char ch = orgString.charAt(i); - CharSequence halfWidthText = JapaneseUtils.tryGetHalfWidthText(ch); + final char ch = orgString.charAt(i); + final String halfWidthText = JapaneseUtils.tryGetHalfWidthText(ch); if (halfWidthText != null) { builder.append(halfWidthText); } else { @@ -428,368 +496,50 @@ public class VCardUtils { } return builder.toString(); } - - private VCardUtils() { - } -} - -/** - * TextUtils especially for Japanese. - * TODO: make this in android.text in the future - */ -class JapaneseUtils { - static private final Map<Character, String> sHalfWidthMap = - new HashMap<Character, String>(); - - static { - // There's no logical mapping rule in Unicode. Sigh. - sHalfWidthMap.put('\u3001', "\uFF64"); - sHalfWidthMap.put('\u3002', "\uFF61"); - sHalfWidthMap.put('\u300C', "\uFF62"); - sHalfWidthMap.put('\u300D', "\uFF63"); - sHalfWidthMap.put('\u301C', "~"); - sHalfWidthMap.put('\u3041', "\uFF67"); - sHalfWidthMap.put('\u3042', "\uFF71"); - sHalfWidthMap.put('\u3043', "\uFF68"); - sHalfWidthMap.put('\u3044', "\uFF72"); - sHalfWidthMap.put('\u3045', "\uFF69"); - sHalfWidthMap.put('\u3046', "\uFF73"); - sHalfWidthMap.put('\u3047', "\uFF6A"); - sHalfWidthMap.put('\u3048', "\uFF74"); - sHalfWidthMap.put('\u3049', "\uFF6B"); - sHalfWidthMap.put('\u304A', "\uFF75"); - sHalfWidthMap.put('\u304B', "\uFF76"); - sHalfWidthMap.put('\u304C', "\uFF76\uFF9E"); - sHalfWidthMap.put('\u304D', "\uFF77"); - sHalfWidthMap.put('\u304E', "\uFF77\uFF9E"); - sHalfWidthMap.put('\u304F', "\uFF78"); - sHalfWidthMap.put('\u3050', "\uFF78\uFF9E"); - sHalfWidthMap.put('\u3051', "\uFF79"); - sHalfWidthMap.put('\u3052', "\uFF79\uFF9E"); - sHalfWidthMap.put('\u3053', "\uFF7A"); - sHalfWidthMap.put('\u3054', "\uFF7A\uFF9E"); - sHalfWidthMap.put('\u3055', "\uFF7B"); - sHalfWidthMap.put('\u3056', "\uFF7B\uFF9E"); - sHalfWidthMap.put('\u3057', "\uFF7C"); - sHalfWidthMap.put('\u3058', "\uFF7C\uFF9E"); - sHalfWidthMap.put('\u3059', "\uFF7D"); - sHalfWidthMap.put('\u305A', "\uFF7D\uFF9E"); - sHalfWidthMap.put('\u305B', "\uFF7E"); - sHalfWidthMap.put('\u305C', "\uFF7E\uFF9E"); - sHalfWidthMap.put('\u305D', "\uFF7F"); - sHalfWidthMap.put('\u305E', "\uFF7F\uFF9E"); - sHalfWidthMap.put('\u305F', "\uFF80"); - sHalfWidthMap.put('\u3060', "\uFF80\uFF9E"); - sHalfWidthMap.put('\u3061', "\uFF81"); - sHalfWidthMap.put('\u3062', "\uFF81\uFF9E"); - sHalfWidthMap.put('\u3063', "\uFF6F"); - sHalfWidthMap.put('\u3064', "\uFF82"); - sHalfWidthMap.put('\u3065', "\uFF82\uFF9E"); - sHalfWidthMap.put('\u3066', "\uFF83"); - sHalfWidthMap.put('\u3067', "\uFF83\uFF9E"); - sHalfWidthMap.put('\u3068', "\uFF84"); - sHalfWidthMap.put('\u3069', "\uFF84\uFF9E"); - sHalfWidthMap.put('\u306A', "\uFF85"); - sHalfWidthMap.put('\u306B', "\uFF86"); - sHalfWidthMap.put('\u306C', "\uFF87"); - sHalfWidthMap.put('\u306D', "\uFF88"); - sHalfWidthMap.put('\u306E', "\uFF89"); - sHalfWidthMap.put('\u306F', "\uFF8A"); - sHalfWidthMap.put('\u3070', "\uFF8A\uFF9E"); - sHalfWidthMap.put('\u3071', "\uFF8A\uFF9F"); - sHalfWidthMap.put('\u3072', "\uFF8B"); - sHalfWidthMap.put('\u3073', "\uFF8B\uFF9E"); - sHalfWidthMap.put('\u3074', "\uFF8B\uFF9F"); - sHalfWidthMap.put('\u3075', "\uFF8C"); - sHalfWidthMap.put('\u3076', "\uFF8C\uFF9E"); - sHalfWidthMap.put('\u3077', "\uFF8C\uFF9F"); - sHalfWidthMap.put('\u3078', "\uFF8D"); - sHalfWidthMap.put('\u3079', "\uFF8D\uFF9E"); - sHalfWidthMap.put('\u307A', "\uFF8D\uFF9F"); - sHalfWidthMap.put('\u307B', "\uFF8E"); - sHalfWidthMap.put('\u307C', "\uFF8E\uFF9E"); - sHalfWidthMap.put('\u307D', "\uFF8E\uFF9F"); - sHalfWidthMap.put('\u307E', "\uFF8F"); - sHalfWidthMap.put('\u307F', "\uFF90"); - sHalfWidthMap.put('\u3080', "\uFF91"); - sHalfWidthMap.put('\u3081', "\uFF92"); - sHalfWidthMap.put('\u3082', "\uFF93"); - sHalfWidthMap.put('\u3083', "\uFF6C"); - sHalfWidthMap.put('\u3084', "\uFF94"); - sHalfWidthMap.put('\u3085', "\uFF6D"); - sHalfWidthMap.put('\u3086', "\uFF95"); - sHalfWidthMap.put('\u3087', "\uFF6E"); - sHalfWidthMap.put('\u3088', "\uFF96"); - sHalfWidthMap.put('\u3089', "\uFF97"); - sHalfWidthMap.put('\u308A', "\uFF98"); - sHalfWidthMap.put('\u308B', "\uFF99"); - sHalfWidthMap.put('\u308C', "\uFF9A"); - sHalfWidthMap.put('\u308D', "\uFF9B"); - sHalfWidthMap.put('\u308E', "\uFF9C"); - sHalfWidthMap.put('\u308F', "\uFF9C"); - sHalfWidthMap.put('\u3090', "\uFF72"); - sHalfWidthMap.put('\u3091', "\uFF74"); - sHalfWidthMap.put('\u3092', "\uFF66"); - sHalfWidthMap.put('\u3093', "\uFF9D"); - sHalfWidthMap.put('\u309B', "\uFF9E"); - sHalfWidthMap.put('\u309C', "\uFF9F"); - sHalfWidthMap.put('\u30A1', "\uFF67"); - sHalfWidthMap.put('\u30A2', "\uFF71"); - sHalfWidthMap.put('\u30A3', "\uFF68"); - sHalfWidthMap.put('\u30A4', "\uFF72"); - sHalfWidthMap.put('\u30A5', "\uFF69"); - sHalfWidthMap.put('\u30A6', "\uFF73"); - sHalfWidthMap.put('\u30A7', "\uFF6A"); - sHalfWidthMap.put('\u30A8', "\uFF74"); - sHalfWidthMap.put('\u30A9', "\uFF6B"); - sHalfWidthMap.put('\u30AA', "\uFF75"); - sHalfWidthMap.put('\u30AB', "\uFF76"); - sHalfWidthMap.put('\u30AC', "\uFF76\uFF9E"); - sHalfWidthMap.put('\u30AD', "\uFF77"); - sHalfWidthMap.put('\u30AE', "\uFF77\uFF9E"); - sHalfWidthMap.put('\u30AF', "\uFF78"); - sHalfWidthMap.put('\u30B0', "\uFF78\uFF9E"); - sHalfWidthMap.put('\u30B1', "\uFF79"); - sHalfWidthMap.put('\u30B2', "\uFF79\uFF9E"); - sHalfWidthMap.put('\u30B3', "\uFF7A"); - sHalfWidthMap.put('\u30B4', "\uFF7A\uFF9E"); - sHalfWidthMap.put('\u30B5', "\uFF7B"); - sHalfWidthMap.put('\u30B6', "\uFF7B\uFF9E"); - sHalfWidthMap.put('\u30B7', "\uFF7C"); - sHalfWidthMap.put('\u30B8', "\uFF7C\uFF9E"); - sHalfWidthMap.put('\u30B9', "\uFF7D"); - sHalfWidthMap.put('\u30BA', "\uFF7D\uFF9E"); - sHalfWidthMap.put('\u30BB', "\uFF7E"); - sHalfWidthMap.put('\u30BC', "\uFF7E\uFF9E"); - sHalfWidthMap.put('\u30BD', "\uFF7F"); - sHalfWidthMap.put('\u30BE', "\uFF7F\uFF9E"); - sHalfWidthMap.put('\u30BF', "\uFF80"); - sHalfWidthMap.put('\u30C0', "\uFF80\uFF9E"); - sHalfWidthMap.put('\u30C1', "\uFF81"); - sHalfWidthMap.put('\u30C2', "\uFF81\uFF9E"); - sHalfWidthMap.put('\u30C3', "\uFF6F"); - sHalfWidthMap.put('\u30C4', "\uFF82"); - sHalfWidthMap.put('\u30C5', "\uFF82\uFF9E"); - sHalfWidthMap.put('\u30C6', "\uFF83"); - sHalfWidthMap.put('\u30C7', "\uFF83\uFF9E"); - sHalfWidthMap.put('\u30C8', "\uFF84"); - sHalfWidthMap.put('\u30C9', "\uFF84\uFF9E"); - sHalfWidthMap.put('\u30CA', "\uFF85"); - sHalfWidthMap.put('\u30CB', "\uFF86"); - sHalfWidthMap.put('\u30CC', "\uFF87"); - sHalfWidthMap.put('\u30CD', "\uFF88"); - sHalfWidthMap.put('\u30CE', "\uFF89"); - sHalfWidthMap.put('\u30CF', "\uFF8A"); - sHalfWidthMap.put('\u30D0', "\uFF8A\uFF9E"); - sHalfWidthMap.put('\u30D1', "\uFF8A\uFF9F"); - sHalfWidthMap.put('\u30D2', "\uFF8B"); - sHalfWidthMap.put('\u30D3', "\uFF8B\uFF9E"); - sHalfWidthMap.put('\u30D4', "\uFF8B\uFF9F"); - sHalfWidthMap.put('\u30D5', "\uFF8C"); - sHalfWidthMap.put('\u30D6', "\uFF8C\uFF9E"); - sHalfWidthMap.put('\u30D7', "\uFF8C\uFF9F"); - sHalfWidthMap.put('\u30D8', "\uFF8D"); - sHalfWidthMap.put('\u30D9', "\uFF8D\uFF9E"); - sHalfWidthMap.put('\u30DA', "\uFF8D\uFF9F"); - sHalfWidthMap.put('\u30DB', "\uFF8E"); - sHalfWidthMap.put('\u30DC', "\uFF8E\uFF9E"); - sHalfWidthMap.put('\u30DD', "\uFF8E\uFF9F"); - sHalfWidthMap.put('\u30DE', "\uFF8F"); - sHalfWidthMap.put('\u30DF', "\uFF90"); - sHalfWidthMap.put('\u30E0', "\uFF91"); - sHalfWidthMap.put('\u30E1', "\uFF92"); - sHalfWidthMap.put('\u30E2', "\uFF93"); - sHalfWidthMap.put('\u30E3', "\uFF6C"); - sHalfWidthMap.put('\u30E4', "\uFF94"); - sHalfWidthMap.put('\u30E5', "\uFF6D"); - sHalfWidthMap.put('\u30E6', "\uFF95"); - sHalfWidthMap.put('\u30E7', "\uFF6E"); - sHalfWidthMap.put('\u30E8', "\uFF96"); - sHalfWidthMap.put('\u30E9', "\uFF97"); - sHalfWidthMap.put('\u30EA', "\uFF98"); - sHalfWidthMap.put('\u30EB', "\uFF99"); - sHalfWidthMap.put('\u30EC', "\uFF9A"); - sHalfWidthMap.put('\u30ED', "\uFF9B"); - sHalfWidthMap.put('\u30EE', "\uFF9C"); - sHalfWidthMap.put('\u30EF', "\uFF9C"); - sHalfWidthMap.put('\u30F0', "\uFF72"); - sHalfWidthMap.put('\u30F1', "\uFF74"); - sHalfWidthMap.put('\u30F2', "\uFF66"); - sHalfWidthMap.put('\u30F3', "\uFF9D"); - sHalfWidthMap.put('\u30F4', "\uFF73\uFF9E"); - sHalfWidthMap.put('\u30F5', "\uFF76"); - sHalfWidthMap.put('\u30F6', "\uFF79"); - sHalfWidthMap.put('\u30FB', "\uFF65"); - sHalfWidthMap.put('\u30FC', "\uFF70"); - sHalfWidthMap.put('\uFF01', "!"); - sHalfWidthMap.put('\uFF02', "\""); - sHalfWidthMap.put('\uFF03', "#"); - sHalfWidthMap.put('\uFF04', "$"); - sHalfWidthMap.put('\uFF05', "%"); - sHalfWidthMap.put('\uFF06', "&"); - sHalfWidthMap.put('\uFF07', "'"); - sHalfWidthMap.put('\uFF08', "("); - sHalfWidthMap.put('\uFF09', ")"); - sHalfWidthMap.put('\uFF0A', "*"); - sHalfWidthMap.put('\uFF0B', "+"); - sHalfWidthMap.put('\uFF0C', ","); - sHalfWidthMap.put('\uFF0D', "-"); - sHalfWidthMap.put('\uFF0E', "."); - sHalfWidthMap.put('\uFF0F', "/"); - sHalfWidthMap.put('\uFF10', "0"); - sHalfWidthMap.put('\uFF11', "1"); - sHalfWidthMap.put('\uFF12', "2"); - sHalfWidthMap.put('\uFF13', "3"); - sHalfWidthMap.put('\uFF14', "4"); - sHalfWidthMap.put('\uFF15', "5"); - sHalfWidthMap.put('\uFF16', "6"); - sHalfWidthMap.put('\uFF17', "7"); - sHalfWidthMap.put('\uFF18', "8"); - sHalfWidthMap.put('\uFF19', "9"); - sHalfWidthMap.put('\uFF1A', ":"); - sHalfWidthMap.put('\uFF1B', ";"); - sHalfWidthMap.put('\uFF1C', "<"); - sHalfWidthMap.put('\uFF1D', "="); - sHalfWidthMap.put('\uFF1E', ">"); - sHalfWidthMap.put('\uFF1F', "?"); - sHalfWidthMap.put('\uFF20', "@"); - sHalfWidthMap.put('\uFF21', "A"); - sHalfWidthMap.put('\uFF22', "B"); - sHalfWidthMap.put('\uFF23', "C"); - sHalfWidthMap.put('\uFF24', "D"); - sHalfWidthMap.put('\uFF25', "E"); - sHalfWidthMap.put('\uFF26', "F"); - sHalfWidthMap.put('\uFF27', "G"); - sHalfWidthMap.put('\uFF28', "H"); - sHalfWidthMap.put('\uFF29', "I"); - sHalfWidthMap.put('\uFF2A', "J"); - sHalfWidthMap.put('\uFF2B', "K"); - sHalfWidthMap.put('\uFF2C', "L"); - sHalfWidthMap.put('\uFF2D', "M"); - sHalfWidthMap.put('\uFF2E', "N"); - sHalfWidthMap.put('\uFF2F', "O"); - sHalfWidthMap.put('\uFF30', "P"); - sHalfWidthMap.put('\uFF31', "Q"); - sHalfWidthMap.put('\uFF32', "R"); - sHalfWidthMap.put('\uFF33', "S"); - sHalfWidthMap.put('\uFF34', "T"); - sHalfWidthMap.put('\uFF35', "U"); - sHalfWidthMap.put('\uFF36', "V"); - sHalfWidthMap.put('\uFF37', "W"); - sHalfWidthMap.put('\uFF38', "X"); - sHalfWidthMap.put('\uFF39', "Y"); - sHalfWidthMap.put('\uFF3A', "Z"); - sHalfWidthMap.put('\uFF3B', "["); - sHalfWidthMap.put('\uFF3C', "\\"); - sHalfWidthMap.put('\uFF3D', "]"); - sHalfWidthMap.put('\uFF3E', "^"); - sHalfWidthMap.put('\uFF3F', "_"); - sHalfWidthMap.put('\uFF41', "a"); - sHalfWidthMap.put('\uFF42', "b"); - sHalfWidthMap.put('\uFF43', "c"); - sHalfWidthMap.put('\uFF44', "d"); - sHalfWidthMap.put('\uFF45', "e"); - sHalfWidthMap.put('\uFF46', "f"); - sHalfWidthMap.put('\uFF47', "g"); - sHalfWidthMap.put('\uFF48', "h"); - sHalfWidthMap.put('\uFF49', "i"); - sHalfWidthMap.put('\uFF4A', "j"); - sHalfWidthMap.put('\uFF4B', "k"); - sHalfWidthMap.put('\uFF4C', "l"); - sHalfWidthMap.put('\uFF4D', "m"); - sHalfWidthMap.put('\uFF4E', "n"); - sHalfWidthMap.put('\uFF4F', "o"); - sHalfWidthMap.put('\uFF50', "p"); - sHalfWidthMap.put('\uFF51', "q"); - sHalfWidthMap.put('\uFF52', "r"); - sHalfWidthMap.put('\uFF53', "s"); - sHalfWidthMap.put('\uFF54', "t"); - sHalfWidthMap.put('\uFF55', "u"); - sHalfWidthMap.put('\uFF56', "v"); - sHalfWidthMap.put('\uFF57', "w"); - sHalfWidthMap.put('\uFF58', "x"); - sHalfWidthMap.put('\uFF59', "y"); - sHalfWidthMap.put('\uFF5A', "z"); - sHalfWidthMap.put('\uFF5B', "{"); - sHalfWidthMap.put('\uFF5C', "|"); - sHalfWidthMap.put('\uFF5D', "}"); - sHalfWidthMap.put('\uFF5E', "~"); - sHalfWidthMap.put('\uFF61', "\uFF61"); - sHalfWidthMap.put('\uFF62', "\uFF62"); - sHalfWidthMap.put('\uFF63', "\uFF63"); - sHalfWidthMap.put('\uFF64', "\uFF64"); - sHalfWidthMap.put('\uFF65', "\uFF65"); - sHalfWidthMap.put('\uFF66', "\uFF66"); - sHalfWidthMap.put('\uFF67', "\uFF67"); - sHalfWidthMap.put('\uFF68', "\uFF68"); - sHalfWidthMap.put('\uFF69', "\uFF69"); - sHalfWidthMap.put('\uFF6A', "\uFF6A"); - sHalfWidthMap.put('\uFF6B', "\uFF6B"); - sHalfWidthMap.put('\uFF6C', "\uFF6C"); - sHalfWidthMap.put('\uFF6D', "\uFF6D"); - sHalfWidthMap.put('\uFF6E', "\uFF6E"); - sHalfWidthMap.put('\uFF6F', "\uFF6F"); - sHalfWidthMap.put('\uFF70', "\uFF70"); - sHalfWidthMap.put('\uFF71', "\uFF71"); - sHalfWidthMap.put('\uFF72', "\uFF72"); - sHalfWidthMap.put('\uFF73', "\uFF73"); - sHalfWidthMap.put('\uFF74', "\uFF74"); - sHalfWidthMap.put('\uFF75', "\uFF75"); - sHalfWidthMap.put('\uFF76', "\uFF76"); - sHalfWidthMap.put('\uFF77', "\uFF77"); - sHalfWidthMap.put('\uFF78', "\uFF78"); - sHalfWidthMap.put('\uFF79', "\uFF79"); - sHalfWidthMap.put('\uFF7A', "\uFF7A"); - sHalfWidthMap.put('\uFF7B', "\uFF7B"); - sHalfWidthMap.put('\uFF7C', "\uFF7C"); - sHalfWidthMap.put('\uFF7D', "\uFF7D"); - sHalfWidthMap.put('\uFF7E', "\uFF7E"); - sHalfWidthMap.put('\uFF7F', "\uFF7F"); - sHalfWidthMap.put('\uFF80', "\uFF80"); - sHalfWidthMap.put('\uFF81', "\uFF81"); - sHalfWidthMap.put('\uFF82', "\uFF82"); - sHalfWidthMap.put('\uFF83', "\uFF83"); - sHalfWidthMap.put('\uFF84', "\uFF84"); - sHalfWidthMap.put('\uFF85', "\uFF85"); - sHalfWidthMap.put('\uFF86', "\uFF86"); - sHalfWidthMap.put('\uFF87', "\uFF87"); - sHalfWidthMap.put('\uFF88', "\uFF88"); - sHalfWidthMap.put('\uFF89', "\uFF89"); - sHalfWidthMap.put('\uFF8A', "\uFF8A"); - sHalfWidthMap.put('\uFF8B', "\uFF8B"); - sHalfWidthMap.put('\uFF8C', "\uFF8C"); - sHalfWidthMap.put('\uFF8D', "\uFF8D"); - sHalfWidthMap.put('\uFF8E', "\uFF8E"); - sHalfWidthMap.put('\uFF8F', "\uFF8F"); - sHalfWidthMap.put('\uFF90', "\uFF90"); - sHalfWidthMap.put('\uFF91', "\uFF91"); - sHalfWidthMap.put('\uFF92', "\uFF92"); - sHalfWidthMap.put('\uFF93', "\uFF93"); - sHalfWidthMap.put('\uFF94', "\uFF94"); - sHalfWidthMap.put('\uFF95', "\uFF95"); - sHalfWidthMap.put('\uFF96', "\uFF96"); - sHalfWidthMap.put('\uFF97', "\uFF97"); - sHalfWidthMap.put('\uFF98', "\uFF98"); - sHalfWidthMap.put('\uFF99', "\uFF99"); - sHalfWidthMap.put('\uFF9A', "\uFF9A"); - sHalfWidthMap.put('\uFF9B', "\uFF9B"); - sHalfWidthMap.put('\uFF9C', "\uFF9C"); - sHalfWidthMap.put('\uFF9D', "\uFF9D"); - sHalfWidthMap.put('\uFF9E', "\uFF9E"); - sHalfWidthMap.put('\uFF9F', "\uFF9F"); - sHalfWidthMap.put('\uFFE5', "\u005C\u005C"); - } /** - * Return half-width version of that character if possible. Return null if not possible - * @param ch input character - * @return CharSequence object if the mapping for ch exists. Return null otherwise. + * Guesses the format of input image. Currently just the first few bytes are used. + * The type "GIF", "PNG", or "JPEG" is returned when possible. Returns null when + * the guess failed. + * @param input Image as byte array. + * @return The image type or null when the type cannot be determined. */ - public static CharSequence tryGetHalfWidthText(char ch) { - if (sHalfWidthMap.containsKey(ch)) { - return sHalfWidthMap.get(ch); + public static String guessImageType(final byte[] input) { + if (input == null) { + return null; + } + if (input.length >= 3 && input[0] == 'G' && input[1] == 'I' && input[2] == 'F') { + return "GIF"; + } else if (input.length >= 4 && input[0] == (byte) 0x89 + && input[1] == 'P' && input[2] == 'N' && input[3] == 'G') { + // Note: vCard 2.1 officially does not support PNG, but we may have it and + // using X- word like "X-PNG" may not let importers know it is PNG. + // So we use the String "PNG" as is... + return "PNG"; + } else if (input.length >= 2 && input[0] == (byte) 0xff + && input[1] == (byte) 0xd8) { + return "JPEG"; } else { return null; } } -}
\ No newline at end of file + + /** + * @return True when all the given values are null or empty Strings. + */ + public static boolean areAllEmpty(final String...values) { + if (values == null) { + return true; + } + + for (final String value : values) { + if (!TextUtils.isEmpty(value)) { + return false; + } + } + return true; + } + + private VCardUtils() { + } +} diff --git a/core/java/android/pim/vcard/exception/VCardAgentNotSupportedException.java b/core/java/android/pim/vcard/exception/VCardAgentNotSupportedException.java new file mode 100644 index 0000000..e72c7df --- /dev/null +++ b/core/java/android/pim/vcard/exception/VCardAgentNotSupportedException.java @@ -0,0 +1,27 @@ +/* + * 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 android.pim.vcard.exception; + +public class VCardAgentNotSupportedException extends VCardNotSupportedException { + public VCardAgentNotSupportedException() { + super(); + } + + public VCardAgentNotSupportedException(String message) { + super(message); + } + +}
\ No newline at end of file diff --git a/core/java/android/preference/EditTextPreference.java b/core/java/android/preference/EditTextPreference.java index 84ee950..aa27627 100644 --- a/core/java/android/preference/EditTextPreference.java +++ b/core/java/android/preference/EditTextPreference.java @@ -28,7 +28,6 @@ import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; import android.widget.EditText; -import android.widget.LinearLayout; /** * A {@link Preference} that allows for string @@ -128,7 +127,7 @@ public class EditTextPreference extends DialogPreference { ViewGroup container = (ViewGroup) dialogView .findViewById(com.android.internal.R.id.edittext_container); if (container != null) { - container.addView(editText, ViewGroup.LayoutParams.FILL_PARENT, + container.addView(editText, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); } } diff --git a/core/java/android/preference/Preference.java b/core/java/android/preference/Preference.java index 08a2a9f..197d976 100644 --- a/core/java/android/preference/Preference.java +++ b/core/java/android/preference/Preference.java @@ -188,17 +188,7 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis mContext = context; TypedArray a = context.obtainStyledAttributes(attrs, - com.android.internal.R.styleable.Preference); - if (a.hasValue(com.android.internal.R.styleable.Preference_layout) || - a.hasValue(com.android.internal.R.styleable.Preference_widgetLayout)) { - // This preference has a custom layout defined (not one taken from - // the default style) - mHasSpecifiedLayout = true; - } - a.recycle(); - - a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.Preference, - defStyle, 0); + com.android.internal.R.styleable.Preference, defStyle, 0); for (int i = a.getIndexCount(); i >= 0; i--) { int attr = a.getIndex(i); switch (attr) { @@ -252,6 +242,11 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis } } a.recycle(); + + if (!getClass().getName().startsWith("android.preference")) { + // For subclasses not in this package, assume the worst and don't cache views + mHasSpecifiedLayout = true; + } } /** @@ -332,11 +327,11 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis * @see #setWidgetLayoutResource(int) */ public void setLayoutResource(int layoutResId) { - - if (!mHasSpecifiedLayout) { + if (layoutResId != mLayoutResId) { + // Layout changed mHasSpecifiedLayout = true; } - + mLayoutResId = layoutResId; } @@ -360,6 +355,10 @@ public class Preference implements Comparable<Preference>, OnDependencyChangeLis * @see #setLayoutResource(int) */ public void setWidgetLayoutResource(int widgetLayoutResId) { + if (widgetLayoutResId != mWidgetLayoutResId) { + // Layout changed + mHasSpecifiedLayout = true; + } mWidgetLayoutResId = widgetLayoutResId; } diff --git a/core/java/android/preference/PreferenceGroupAdapter.java b/core/java/android/preference/PreferenceGroupAdapter.java index 14c0054..a908ecd 100644 --- a/core/java/android/preference/PreferenceGroupAdapter.java +++ b/core/java/android/preference/PreferenceGroupAdapter.java @@ -69,7 +69,9 @@ class PreferenceGroupAdapter extends BaseAdapter implements OnPreferenceChangeIn * count once--when the adapter is being set). We will not recycle views for * Preference subclasses seen after the count has been returned. */ - private List<String> mPreferenceClassNames; + private ArrayList<PreferenceLayout> mPreferenceLayouts; + + private PreferenceLayout mTempPreferenceLayout = new PreferenceLayout(); /** * Blocks the mPreferenceClassNames from being changed anymore. @@ -86,14 +88,37 @@ class PreferenceGroupAdapter extends BaseAdapter implements OnPreferenceChangeIn } }; + private static class PreferenceLayout implements Comparable<PreferenceLayout> { + private int resId; + private int widgetResId; + private String name; + + public int compareTo(PreferenceLayout other) { + int compareNames = name.compareTo(other.name); + if (compareNames == 0) { + if (resId == other.resId) { + if (widgetResId == other.widgetResId) { + return 0; + } else { + return widgetResId - other.widgetResId; + } + } else { + return resId - other.resId; + } + } else { + return compareNames; + } + } + } + public PreferenceGroupAdapter(PreferenceGroup preferenceGroup) { mPreferenceGroup = preferenceGroup; // If this group gets or loses any children, let us know mPreferenceGroup.setOnPreferenceChangeInternalListener(this); - + mPreferenceList = new ArrayList<Preference>(); - mPreferenceClassNames = new ArrayList<String>(); - + mPreferenceLayouts = new ArrayList<PreferenceLayout>(); + syncMyPreferences(); } @@ -102,7 +127,7 @@ class PreferenceGroupAdapter extends BaseAdapter implements OnPreferenceChangeIn if (mIsSyncing) { return; } - + mIsSyncing = true; } @@ -128,7 +153,7 @@ class PreferenceGroupAdapter extends BaseAdapter implements OnPreferenceChangeIn preferences.add(preference); - if (!mHasReturnedViewTypeCount) { + if (!mHasReturnedViewTypeCount && !preference.hasSpecifiedLayout()) { addPreferenceClassName(preference); } @@ -143,15 +168,28 @@ class PreferenceGroupAdapter extends BaseAdapter implements OnPreferenceChangeIn } } + /** + * Creates a string that includes the preference name, layout id and widget layout id. + * If a particular preference type uses 2 different resources, they will be treated as + * different view types. + */ + private PreferenceLayout createPreferenceLayout(Preference preference, PreferenceLayout in) { + PreferenceLayout pl = in != null? in : new PreferenceLayout(); + pl.name = preference.getClass().getName(); + pl.resId = preference.getLayoutResource(); + pl.widgetResId = preference.getWidgetLayoutResource(); + return pl; + } + private void addPreferenceClassName(Preference preference) { - final String name = preference.getClass().getName(); - int insertPos = Collections.binarySearch(mPreferenceClassNames, name); - + final PreferenceLayout pl = createPreferenceLayout(preference, null); + int insertPos = Collections.binarySearch(mPreferenceLayouts, pl); + // Only insert if it doesn't exist (when it is negative). if (insertPos < 0) { // Convert to insert index insertPos = insertPos * -1 - 1; - mPreferenceClassNames.add(insertPos, name); + mPreferenceLayouts.add(insertPos, pl); } } @@ -171,19 +209,15 @@ class PreferenceGroupAdapter extends BaseAdapter implements OnPreferenceChangeIn public View getView(int position, View convertView, ViewGroup parent) { final Preference preference = this.getItem(position); - - if (preference.hasSpecifiedLayout()) { - // If the preference had specified a layout (as opposed to the - // default), don't use convert views. + // Build a PreferenceLayout to compare with known ones that are cacheable. + mTempPreferenceLayout = createPreferenceLayout(preference, mTempPreferenceLayout); + + // If it's not one of the cached ones, set the convertView to null so that + // the layout gets re-created by the Preference. + if (Collections.binarySearch(mPreferenceLayouts, mTempPreferenceLayout) < 0) { convertView = null; - } else { - // TODO: better way of doing this - final String name = preference.getClass().getName(); - if (Collections.binarySearch(mPreferenceClassNames, name) < 0) { - convertView = null; - } } - + return preference.getView(convertView, parent); } @@ -225,8 +259,9 @@ class PreferenceGroupAdapter extends BaseAdapter implements OnPreferenceChangeIn return IGNORE_ITEM_VIEW_TYPE; } - final String name = preference.getClass().getName(); - int viewType = Collections.binarySearch(mPreferenceClassNames, name); + mTempPreferenceLayout = createPreferenceLayout(preference, mTempPreferenceLayout); + + int viewType = Collections.binarySearch(mPreferenceLayouts, mTempPreferenceLayout); if (viewType < 0) { // This is a class that was seen after we returned the count, so // don't recycle it. @@ -242,7 +277,7 @@ class PreferenceGroupAdapter extends BaseAdapter implements OnPreferenceChangeIn mHasReturnedViewTypeCount = true; } - return Math.max(1, mPreferenceClassNames.size()); + return Math.max(1, mPreferenceLayouts.size()); } } diff --git a/core/java/android/provider/Browser.java b/core/java/android/provider/Browser.java index c8b7f99..b876f05 100644 --- a/core/java/android/provider/Browser.java +++ b/core/java/android/provider/Browser.java @@ -108,7 +108,7 @@ public class Browser { BookmarkColumns._ID, BookmarkColumns.URL, BookmarkColumns.VISITS, BookmarkColumns.DATE, BookmarkColumns.BOOKMARK, BookmarkColumns.TITLE, BookmarkColumns.FAVICON, BookmarkColumns.THUMBNAIL, - BookmarkColumns.TOUCH_ICON }; + BookmarkColumns.TOUCH_ICON, BookmarkColumns.USER_ENTERED }; /* these indices dependent on HISTORY_PROJECTION */ public static final int HISTORY_PROJECTION_ID_INDEX = 0; @@ -232,8 +232,8 @@ public class Browser { * Requires {@link android.Manifest.permission#WRITE_HISTORY_BOOKMARKS} * @param cr The ContentResolver used to access the database. * @param url The site being visited. - * @param real Whether this is an actual visit, and should be added to the - * number of visits. + * @param real If true, this is an actual visit, and should add to the + * number of visits. If false, the user entered it manually. */ public static final void updateVisitedHistory(ContentResolver cr, String url, boolean real) { @@ -253,18 +253,30 @@ public class Browser { if (real) { map.put(BookmarkColumns.VISITS, c .getInt(HISTORY_PROJECTION_VISITS_INDEX) + 1); + } else { + map.put(BookmarkColumns.USER_ENTERED, 1); } map.put(BookmarkColumns.DATE, now); cr.update(BOOKMARKS_URI, map, "_id = " + c.getInt(0), null); } else { truncateHistory(cr); ContentValues map = new ContentValues(); + int visits; + int user_entered; + if (real) { + visits = 1; + user_entered = 0; + } else { + visits = 0; + user_entered = 1; + } map.put(BookmarkColumns.URL, url); - map.put(BookmarkColumns.VISITS, real ? 1 : 0); + map.put(BookmarkColumns.VISITS, visits); map.put(BookmarkColumns.DATE, now); map.put(BookmarkColumns.BOOKMARK, 0); map.put(BookmarkColumns.TITLE, url); map.put(BookmarkColumns.CREATED, 0); + map.put(BookmarkColumns.USER_ENTERED, user_entered); cr.insert(BOOKMARKS_URI, map); } c.deactivate(); @@ -572,6 +584,10 @@ public class Browser { * @hide */ public static final String TOUCH_ICON = "touch_icon"; + /** + * @hide + */ + public static final String USER_ENTERED = "user_entered"; } public static class SearchColumns implements BaseColumns { diff --git a/core/java/android/provider/Calendar.java b/core/java/android/provider/Calendar.java index f046cef..509aac5 100644 --- a/core/java/android/provider/Calendar.java +++ b/core/java/android/provider/Calendar.java @@ -16,6 +16,7 @@ package android.provider; +import android.accounts.Account; import android.app.AlarmManager; import android.app.PendingIntent; import android.content.ContentResolver; @@ -23,8 +24,14 @@ import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.Intent; +import android.content.EntityIterator; +import android.content.CursorEntityIterator; +import android.content.Entity; +import android.content.ContentProviderClient; import android.database.Cursor; +import android.database.DatabaseUtils; import android.net.Uri; +import android.os.RemoteException; import android.pim.ICalendar; import android.pim.RecurrenceSet; import android.text.TextUtils; @@ -32,18 +39,6 @@ import android.text.format.DateUtils; import android.text.format.Time; import android.util.Config; import android.util.Log; -import android.accounts.Account; -import com.android.internal.database.ArrayListCursor; -import com.google.android.gdata.client.AndroidGDataClient; -import com.google.android.gdata.client.AndroidXmlParserFactory; -import com.google.wireless.gdata.calendar.client.CalendarClient; -import com.google.wireless.gdata.calendar.data.EventEntry; -import com.google.wireless.gdata.calendar.data.Who; -import com.google.wireless.gdata.calendar.parser.xml.XmlCalendarGDataParserFactory; -import com.google.wireless.gdata.data.StringUtils; - -import java.util.ArrayList; -import java.util.Vector; /** * The Calendar provider contains all calendar events. @@ -76,16 +71,20 @@ public final class Calendar { Uri.parse("content://" + AUTHORITY); /** + * An optional insert, update or delete URI parameter that allows the caller + * to specify that it is a sync adapter. The default value is false. If true + * the dirty flag is not automatically set and the "syncToNetwork" parameter + * is set to false when calling + * {@link ContentResolver#notifyChange(android.net.Uri, android.database.ContentObserver, boolean)}. + */ + public static final String CALLER_IS_SYNCADAPTER = "caller_is_syncadapter"; + + /** * Columns from the Calendars table that other tables join into themselves. */ public interface CalendarsColumns { /** - * A string that uniquely identifies this contact to its source - */ - public static final String SOURCE_ID = "sourceid"; - - /** * The color of the calendar * <P>Type: INTEGER (color value)</P> */ @@ -137,12 +136,60 @@ public final class Calendar { * <p>Type: String (blob)</p> */ public static final String SYNC_STATE = "sync_state"; + + /** + * The account that was used to sync the entry to the device. + * <P>Type: TEXT</P> + */ + public static final String _SYNC_ACCOUNT = "_sync_account"; + + /** + * The type of the account that was used to sync the entry to the device. + * <P>Type: TEXT</P> + */ + public static final String _SYNC_ACCOUNT_TYPE = "_sync_account_type"; + + /** + * The unique ID for a row assigned by the sync source. NULL if the row has never been synced. + * <P>Type: TEXT</P> + */ + public static final String _SYNC_ID = "_sync_id"; + + /** + * The last time, from the sync source's point of view, that this row has been synchronized. + * <P>Type: INTEGER (long)</P> + */ + public static final String _SYNC_TIME = "_sync_time"; + + /** + * The version of the row, as assigned by the server. + * <P>Type: TEXT</P> + */ + public static final String _SYNC_VERSION = "_sync_version"; + + /** + * Used in temporary provider while syncing, always NULL for rows in persistent providers. + * <P>Type: INTEGER (long)</P> + */ + public static final String _SYNC_LOCAL_ID = "_sync_local_id"; + + /** + * Used only in persistent providers, and only during merging. + * <P>Type: INTEGER (long)</P> + */ + public static final String _SYNC_MARK = "_sync_mark"; + + /** + * Used to indicate that local, unsynced, changes are present. + * <P>Type: INTEGER (long)</P> + */ + public static final String _SYNC_DIRTY = "_sync_dirty"; } /** * Contains a list of available calendars. */ - public static class Calendars implements BaseColumns, SyncConstValue, CalendarsColumns + public static class Calendars implements BaseColumns, CalendarsColumns { public static final Cursor query(ContentResolver cr, String[] projection, String where, String orderBy) @@ -341,11 +388,11 @@ public final class Calendar { * This field is copied here so that we can efficiently filter out * events that are declined without having to look in the Attendees * table. - * + * * <P>Type: INTEGER (int)</P> */ public static final String SELF_ATTENDEE_STATUS = "selfAttendeeStatus"; - + /** * The comments feed uri. * <P>Type: TEXT</P> @@ -514,13 +561,207 @@ public final class Calendar { * <P>Type: String</P> */ public static final String OWNER_ACCOUNT = "ownerAccount"; + + /** + * Whether the row has been deleted. A deleted row should be ignored. + * <P>Type: INTEGER (boolean)</P> + */ + public static final String DELETED = "deleted"; + } + + /** + * Contains one entry per calendar event. Recurring events show up as a single entry. + */ + public static final class EventsEntity implements BaseColumns, EventsColumns, CalendarsColumns { + /** + * The content:// style URL for this table + */ + public static final Uri CONTENT_URI = Uri.parse("content://calendar/event_entities"); + + /** + * The name of the account instance to which this row belongs, which when paired with + * {@link #ACCOUNT_TYPE} identifies a specific account. + * <P>Type: TEXT</P> + */ + public static final String ACCOUNT_NAME = "_sync_account"; + + /** + * The type of account to which this row belongs, which when paired with + * {@link #ACCOUNT_NAME} identifies a specific account. + * <P>Type: TEXT</P> + */ + public static final String ACCOUNT_TYPE = "_sync_account_type"; + + public static EntityIterator newEntityIterator(Cursor cursor, ContentResolver resolver) { + return new EntityIteratorImpl(cursor, resolver); + } + + public static EntityIterator newEntityIterator(Cursor cursor, + ContentProviderClient provider) { + return new EntityIteratorImpl(cursor, provider); + } + + private static class EntityIteratorImpl extends CursorEntityIterator { + private final ContentResolver mResolver; + private final ContentProviderClient mProvider; + + private static final String[] REMINDERS_PROJECTION = new String[] { + Reminders.MINUTES, + Reminders.METHOD, + }; + private static final int COLUMN_MINUTES = 0; + private static final int COLUMN_METHOD = 1; + + private static final String[] ATTENDEES_PROJECTION = new String[] { + Attendees.ATTENDEE_NAME, + Attendees.ATTENDEE_EMAIL, + Attendees.ATTENDEE_RELATIONSHIP, + Attendees.ATTENDEE_TYPE, + Attendees.ATTENDEE_STATUS, + }; + private static final int COLUMN_ATTENDEE_NAME = 0; + private static final int COLUMN_ATTENDEE_EMAIL = 1; + private static final int COLUMN_ATTENDEE_RELATIONSHIP = 2; + private static final int COLUMN_ATTENDEE_TYPE = 3; + private static final int COLUMN_ATTENDEE_STATUS = 4; + private static final String[] EXTENDED_PROJECTION = new String[] { + ExtendedProperties.NAME, + ExtendedProperties.VALUE, + }; + private static final int COLUMN_NAME = 0; + private static final int COLUMN_VALUE = 1; + + public EntityIteratorImpl(Cursor cursor, ContentResolver resolver) { + super(cursor); + mResolver = resolver; + mProvider = null; + } + + public EntityIteratorImpl(Cursor cursor, ContentProviderClient provider) { + super(cursor); + mResolver = null; + mProvider = provider; + } + + public Entity getEntityAndIncrementCursor(Cursor cursor) throws RemoteException { + // we expect the cursor is already at the row we need to read from + final long eventId = cursor.getLong(cursor.getColumnIndexOrThrow(Events._ID)); + ContentValues cv = new ContentValues(); + cv.put(Events._ID, eventId); + DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, CALENDAR_ID); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, HTML_URI); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, TITLE); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, DESCRIPTION); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, EVENT_LOCATION); + DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, STATUS); + DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, SELF_ATTENDEE_STATUS); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, COMMENTS_URI); + DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, cv, DTSTART); + DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, cv, DTEND); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, DURATION); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, EVENT_TIMEZONE); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, ALL_DAY); + DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, VISIBILITY); + DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, TRANSPARENCY); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, HAS_ALARM); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, + HAS_EXTENDED_PROPERTIES); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, RRULE); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, RDATE); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, EXRULE); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, EXDATE); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, ORIGINAL_EVENT); + DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, cv, + ORIGINAL_INSTANCE_TIME); + DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, ORIGINAL_ALL_DAY); + DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, cv, LAST_DATE); + DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, HAS_ATTENDEE_DATA); + DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, + GUESTS_CAN_INVITE_OTHERS); + DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, GUESTS_CAN_MODIFY); + DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, GUESTS_CAN_SEE_GUESTS); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, ORGANIZER); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, _SYNC_ID); + DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, cv, _SYNC_DIRTY); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, _SYNC_VERSION); + DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, DELETED); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, Calendars.URL); + + Entity entity = new Entity(cv); + Cursor subCursor; + if (mResolver != null) { + subCursor = mResolver.query(Reminders.CONTENT_URI, REMINDERS_PROJECTION, + "event_id=" + eventId, null, null); + } else { + subCursor = mProvider.query(Reminders.CONTENT_URI, REMINDERS_PROJECTION, + "event_id=" + eventId, null, null); + } + try { + while (subCursor.moveToNext()) { + ContentValues reminderValues = new ContentValues(); + reminderValues.put(Reminders.MINUTES, subCursor.getInt(COLUMN_MINUTES)); + reminderValues.put(Reminders.METHOD, subCursor.getInt(COLUMN_METHOD)); + entity.addSubValue(Reminders.CONTENT_URI, reminderValues); + } + } finally { + subCursor.close(); + } + + if (mResolver != null) { + subCursor = mResolver.query(Attendees.CONTENT_URI, ATTENDEES_PROJECTION, + "event_id=" + eventId, null /* selectionArgs */, null /* sortOrder */); + } else { + subCursor = mProvider.query(Attendees.CONTENT_URI, ATTENDEES_PROJECTION, + "event_id=" + eventId, null /* selectionArgs */, null /* sortOrder */); + } + try { + while (subCursor.moveToNext()) { + ContentValues attendeeValues = new ContentValues(); + attendeeValues.put(Attendees.ATTENDEE_NAME, + subCursor.getString(COLUMN_ATTENDEE_NAME)); + attendeeValues.put(Attendees.ATTENDEE_EMAIL, + subCursor.getString(COLUMN_ATTENDEE_EMAIL)); + attendeeValues.put(Attendees.ATTENDEE_RELATIONSHIP, + subCursor.getInt(COLUMN_ATTENDEE_RELATIONSHIP)); + attendeeValues.put(Attendees.ATTENDEE_TYPE, + subCursor.getInt(COLUMN_ATTENDEE_TYPE)); + attendeeValues.put(Attendees.ATTENDEE_STATUS, + subCursor.getInt(COLUMN_ATTENDEE_STATUS)); + entity.addSubValue(Attendees.CONTENT_URI, attendeeValues); + } + } finally { + subCursor.close(); + } + + if (mResolver != null) { + subCursor = mResolver.query(ExtendedProperties.CONTENT_URI, EXTENDED_PROJECTION, + "event_id=" + eventId, null /* selectionArgs */, null /* sortOrder */); + } else { + subCursor = mProvider.query(ExtendedProperties.CONTENT_URI, EXTENDED_PROJECTION, + "event_id=" + eventId, null /* selectionArgs */, null /* sortOrder */); + } + try { + while (subCursor.moveToNext()) { + ContentValues extendedValues = new ContentValues(); + extendedValues.put(ExtendedProperties.NAME, cursor.getString(COLUMN_NAME)); + extendedValues.put(ExtendedProperties.VALUE, + cursor.getString(COLUMN_VALUE)); + entity.addSubValue(ExtendedProperties.CONTENT_URI, extendedValues); + } + } finally { + subCursor.close(); + } + + cursor.moveToNext(); + return entity; + } + } } /** * Contains one entry per calendar event. Recurring events show up as a single entry. */ - public static final class Events implements BaseColumns, SyncConstValue, - EventsColumns, CalendarsColumns { + public static final class Events implements BaseColumns, EventsColumns, CalendarsColumns { private static final String[] FETCH_ENTRY_COLUMNS = new String[] { Events._SYNC_ACCOUNT, Events._SYNC_ID }; @@ -532,8 +773,6 @@ public final class Calendar { AttendeesColumns.ATTENDEE_TYPE, AttendeesColumns.ATTENDEE_STATUS }; - private static CalendarClient sCalendarClient = null; - public static final Cursor query(ContentResolver cr, String[] projection) { return cr.query(CONTENT_URI, projection, null, null, DEFAULT_SORT_ORDER); } @@ -585,7 +824,7 @@ public final class Calendar { // where String where = extractValue(event, "LOCATION"); - if (!StringUtils.isEmpty(where)) { + if (!TextUtils.isEmpty(where)) { values.put(EVENT_LOCATION, where); } @@ -672,47 +911,6 @@ public final class Calendar { } /** - * Returns a singleton instance of the CalendarClient used to fetch entries from the - * calendar server. - * @param cr The ContentResolver used to lookup the address of the calendar server in the - * settings database. - * @return The singleton instance of the CalendarClient used to fetch entries from the - * calendar server. - */ - private static synchronized CalendarClient getCalendarClient(ContentResolver cr) { - if (sCalendarClient == null) { - sCalendarClient = new CalendarClient( - new AndroidGDataClient(cr), - new XmlCalendarGDataParserFactory(new AndroidXmlParserFactory())); - } - return sCalendarClient; - } - - /** - * Extracts the attendees information out of event and adds it to a new ArrayList of columns - * within the supplied ArrayList of rows. These rows are expected to be used within an - * {@link ArrayListCursor}. - */ - private static final void extractAttendeesIntoArrayList(EventEntry event, - ArrayList<ArrayList> rows) { - Log.d(TAG, "EVENT: " + event.toString()); - Vector<Who> attendees = (Vector<Who>) event.getAttendees(); - - int numAttendees = attendees == null ? 0 : attendees.size(); - - for (int i = 0; i < numAttendees; ++i) { - Who attendee = attendees.elementAt(i); - ArrayList row = new ArrayList(); - row.add(attendee.getValue()); - row.add(attendee.getEmail()); - row.add(attendee.getRelationship()); - row.add(attendee.getType()); - row.add(attendee.getStatus()); - rows.add(row); - } - } - - /** * The content:// style URL for this table */ public static final Uri CONTENT_URI = @@ -851,60 +1049,41 @@ public final class Calendar { public static final String MAX_INSTANCE = "maxInstance"; /** - * The minimum Julian day in the BusyBits table. + * The minimum Julian day in the EventDays table. * <P>Type: INTEGER</P> */ - public static final String MIN_BUSYBITS = "minBusyBits"; + public static final String MIN_EVENTDAYS = "minEventDays"; /** - * The maximum Julian day in the BusyBits table. + * The maximum Julian day in the EventDays table. * <P>Type: INTEGER</P> */ - public static final String MAX_BUSYBITS = "maxBusyBits"; + public static final String MAX_EVENTDAYS = "maxEventDays"; } - + public static final class CalendarMetaData implements CalendarMetaDataColumns { } - - public interface BusyBitsColumns { - /** - * The Julian day number. - * <P>Type: INTEGER (int)</P> - */ - public static final String DAY = "day"; + public interface EventDaysColumns { /** - * The 24 bits representing the 24 1-hour time slots in a day. - * If an event in the Instances table overlaps part of a 1-hour - * time slot then the corresponding bit is set. The first time slot - * (12am to 1am) is bit 0. The last time slot (11pm to midnight) - * is bit 23. + * The Julian starting day number. * <P>Type: INTEGER (int)</P> */ - public static final String BUSYBITS = "busyBits"; + public static final String STARTDAY = "startDay"; + public static final String ENDDAY = "endDay"; - /** - * The number of all-day events that occur on this day. - * <P>Type: INTEGER (int)</P> - */ - public static final String ALL_DAY_COUNT = "allDayCount"; } - - public static final class BusyBits implements BusyBitsColumns { - public static final Uri CONTENT_URI = Uri.parse("content://calendar/busybits/when"); - public static final String[] PROJECTION = { DAY, BUSYBITS, ALL_DAY_COUNT }; - - // The number of minutes represented by one busy bit - public static final int MINUTES_PER_BUSY_INTERVAL = 60; - - // The number of intervals in a day - public static final int INTERVALS_PER_DAY = 24 * 60 / MINUTES_PER_BUSY_INTERVAL; + public static final class EventDays implements EventDaysColumns { + public static final Uri CONTENT_URI = Uri.parse("content://calendar/instances/groupbyday"); + + public static final String[] PROJECTION = { STARTDAY, ENDDAY }; + public static final String SELECTION = "selected==1"; /** - * Retrieves the busy bits for the Julian days starting at "startDay" + * Retrieves the days with events for the Julian days starting at "startDay" * for "numDays". - * + * * @param cr the ContentResolver * @param startDay the first Julian day in the range * @param numDays the number of days to load (must be at least 1) @@ -918,8 +1097,8 @@ public final class Calendar { Uri.Builder builder = CONTENT_URI.buildUpon(); ContentUris.appendId(builder, startDay); ContentUris.appendId(builder, endDay); - return cr.query(builder.build(), PROJECTION, null /* selection */, - null /* selection args */, DAY); + return cr.query(builder.build(), PROJECTION, SELECTION, + null /* selection args */, STARTDAY); } } @@ -1025,23 +1204,25 @@ public final class Calendar { /** * The default sort order for this table */ - public static final String DEFAULT_SORT_ORDER = "alarmTime ASC,begin ASC,title ASC"; + public static final String DEFAULT_SORT_ORDER = "begin ASC,title ASC"; } public static final class CalendarAlerts implements BaseColumns, CalendarAlertsColumns, EventsColumns, CalendarsColumns { public static final String TABLE_NAME = "CalendarAlerts"; public static final Uri CONTENT_URI = Uri.parse("content://calendar/calendar_alerts"); - + /** * This URI is for grouping the query results by event_id and begin * time. This will return one result per instance of an event. So * events with multiple alarms will appear just once, but multiple * instances of a repeating event will show up multiple times. */ - public static final Uri CONTENT_URI_BY_INSTANCE = + public static final Uri CONTENT_URI_BY_INSTANCE = Uri.parse("content://calendar/calendar_alerts/by_instance"); + private static final boolean DEBUG = true; + public static final Uri insert(ContentResolver cr, long eventId, long begin, long end, long alarmTime, int minutes) { ContentValues values = new ContentValues(); @@ -1059,15 +1240,15 @@ public final class Calendar { } public static final Cursor query(ContentResolver cr, String[] projection, - String selection, String[] selectionArgs) { + String selection, String[] selectionArgs, String sortOrder) { return cr.query(CONTENT_URI, projection, selection, selectionArgs, - DEFAULT_SORT_ORDER); + sortOrder); } - + /** * Finds the next alarm after (or equal to) the given time and returns * the time of that alarm or -1 if no such alarm exists. - * + * * @param cr the ContentResolver * @param millis the time in UTC milliseconds * @return the next alarm time greater than or equal to "millis", or -1 @@ -1078,7 +1259,7 @@ public final class Calendar { // TODO: construct an explicit SQL query so that we can add // "LIMIT 1" to the end and get just one result. String[] projection = new String[] { ALARM_TIME }; - Cursor cursor = query(cr, projection, selection, null); + Cursor cursor = query(cr, projection, selection, null, ALARM_TIME + " ASC"); long alarmTime = -1; try { if (cursor != null && cursor.moveToFirst()) { @@ -1091,13 +1272,13 @@ public final class Calendar { } return alarmTime; } - + /** * Searches the CalendarAlerts table for alarms that should have fired * but have not and then reschedules them. This method can be called * at boot time to restore alarms that may have been lost due to a * phone reboot. - * + * * @param cr the ContentResolver * @param context the Context * @param manager the AlarmManager @@ -1107,53 +1288,68 @@ public final class Calendar { // Get all the alerts that have been scheduled but have not fired // and should have fired by now and are not too old. long now = System.currentTimeMillis(); - long ancient = now - 24 * DateUtils.HOUR_IN_MILLIS; + long ancient = now - DateUtils.DAY_IN_MILLIS; String selection = CalendarAlerts.STATE + "=" + CalendarAlerts.SCHEDULED + " AND " + CalendarAlerts.ALARM_TIME + "<" + now + " AND " + CalendarAlerts.ALARM_TIME + ">" + ancient + " AND " + CalendarAlerts.END + ">=" + now; String[] projection = new String[] { - _ID, - BEGIN, - END, ALARM_TIME, }; - Cursor cursor = CalendarAlerts.query(cr, projection, selection, null); + + // TODO: construct an explicit SQL query so that we can add + // "GROUPBY" instead of doing a sort and de-dup + Cursor cursor = CalendarAlerts.query(cr, projection, selection, null, "alarmTime ASC"); if (cursor == null) { return; } - if (Log.isLoggable(TAG, Log.DEBUG)) { + + if (DEBUG) { Log.d(TAG, "missed alarms found: " + cursor.getCount()); } - + try { + long alarmTime = -1; + while (cursor.moveToNext()) { - long id = cursor.getLong(0); - long begin = cursor.getLong(1); - long end = cursor.getLong(2); - long alarmTime = cursor.getLong(3); - Uri uri = ContentUris.withAppendedId(CONTENT_URI, id); - Intent intent = new Intent(android.provider.Calendar.EVENT_REMINDER_ACTION); - intent.setData(uri); - intent.putExtra(android.provider.Calendar.EVENT_BEGIN_TIME, begin); - intent.putExtra(android.provider.Calendar.EVENT_END_TIME, end); - PendingIntent sender = PendingIntent.getBroadcast(context, - 0, intent, PendingIntent.FLAG_CANCEL_CURRENT); - Log.w(TAG, "rescheduling missed alarm, id: " + id + " begin: " + begin - + " end: " + end + " alarmTime: " + alarmTime); - manager.set(AlarmManager.RTC_WAKEUP, alarmTime, sender); + long newAlarmTime = cursor.getLong(0); + if (alarmTime != newAlarmTime) { + if (DEBUG) { + Log.w(TAG, "rescheduling missed alarm. alarmTime: " + newAlarmTime); + } + scheduleAlarm(context, manager, newAlarmTime); + alarmTime = newAlarmTime; + } } } finally { cursor.close(); } - } - + + public static void scheduleAlarm(Context context, AlarmManager manager, long alarmTime) { + if (DEBUG) { + Time time = new Time(); + time.set(alarmTime); + String schedTime = time.format(" %a, %b %d, %Y %I:%M%P"); + Log.d(TAG, "Schedule alarm at " + alarmTime + " " + schedTime); + } + + if (manager == null) { + manager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + } + + Intent intent = new Intent(android.provider.Calendar.EVENT_REMINDER_ACTION); + intent.putExtra(android.provider.Calendar.CalendarAlerts.ALARM_TIME, alarmTime); + PendingIntent pi = PendingIntent.getBroadcast(context, 0, intent, + PendingIntent.FLAG_CANCEL_CURRENT); + manager.set(AlarmManager.RTC_WAKEUP, alarmTime, pi); + } + /** * Searches for an entry in the CalendarAlerts table that matches * the given event id, begin time and alarm time. If one is found * then this alarm already exists and this method returns true. - * + * * @param cr the ContentResolver * @param eventId the event id to match * @param begin the start time of the event in UTC millis @@ -1169,7 +1365,7 @@ public final class Calendar { // TODO: construct an explicit SQL query so that we can add // "LIMIT 1" to the end and get just one result. String[] projection = new String[] { CalendarAlerts.ALARM_TIME }; - Cursor cursor = query(cr, projection, selection, null); + Cursor cursor = query(cr, projection, selection, null, null); boolean found = false; try { if (cursor != null && cursor.getCount() > 0) { diff --git a/core/java/android/provider/Checkin.java b/core/java/android/provider/Checkin.java index 4134dc2..75936a1 100644 --- a/core/java/android/provider/Checkin.java +++ b/core/java/android/provider/Checkin.java @@ -23,7 +23,6 @@ import android.content.ContentValues; import android.database.SQLException; import android.net.Uri; import android.os.SystemClock; -import android.server.data.CrashData; import android.util.Log; import java.io.ByteArrayOutputStream; @@ -74,7 +73,6 @@ public final class Checkin { CARRIER_BUG_REPORT, CHECKIN_FAILURE, CHECKIN_SUCCESS, - CPUFREQ_STATS, FOTA_BEGIN, FOTA_FAILURE, FOTA_INSTALL, @@ -268,59 +266,4 @@ public final class Checkin { /** {@link SystemClock#elapsedRealtime} of the last time a crash report failed. */ static private volatile long sLastCrashFailureRealtime = -MIN_CRASH_FAILURE_RETRY; - - /** - * Helper function to report a crash. - * - * @param resolver from {@link android.content.Context#getContentResolver} - * @param crash data from {@link android.server.data.CrashData} - * @return URI of the crash report that was added - */ - static public Uri reportCrash(ContentResolver resolver, byte[] crash) { - try { - // If we are in a situation where crash reports fail (such as a full disk), - // it's important that we don't get into a loop trying to report failures. - // So discard all crash reports for a few seconds after reporting fails. - long realtime = SystemClock.elapsedRealtime(); - if (realtime - sLastCrashFailureRealtime < MIN_CRASH_FAILURE_RETRY) { - Log.e(TAG, "Crash logging skipped, too soon after logging failure"); - return null; - } - - // HACK: we don't support BLOB values, so base64 encode it. - byte[] encoded = Base64.encodeBase64(crash); - ContentValues values = new ContentValues(); - values.put(Crashes.DATA, new String(encoded)); - Uri uri = resolver.insert(Crashes.CONTENT_URI, values); - if (uri == null) { - Log.e(TAG, "Error reporting crash"); - sLastCrashFailureRealtime = SystemClock.elapsedRealtime(); - } - return uri; - } catch (Throwable t) { - // To avoid an infinite crash-reporting loop, swallow all errors and exceptions. - Log.e(TAG, "Error reporting crash: " + t); - sLastCrashFailureRealtime = SystemClock.elapsedRealtime(); - return null; - } - } - - /** - * Report a crash in CrashData format. - * - * @param resolver from {@link android.content.Context#getContentResolver} - * @param crash data to report - * @return URI of the crash report that was added - */ - static public Uri reportCrash(ContentResolver resolver, CrashData crash) { - try { - ByteArrayOutputStream data = new ByteArrayOutputStream(); - crash.write(new DataOutputStream(data)); - return reportCrash(resolver, data.toByteArray()); - } catch (Throwable t) { - // Swallow all errors and exceptions when writing crash report - Log.e(TAG, "Error writing crash: " + t); - return null; - } - } } diff --git a/core/java/android/provider/Contacts.java b/core/java/android/provider/Contacts.java index 1a38166..a29ecb5 100644 --- a/core/java/android/provider/Contacts.java +++ b/core/java/android/provider/Contacts.java @@ -1334,8 +1334,26 @@ public class Contacts { } /** + * TODO find a place to put the canonical version of these. + */ + interface ProviderNames { + // + //NOTE: update Contacts.java with new providers when they're added. + // + String YAHOO = "Yahoo"; + String GTALK = "GTalk"; + String MSN = "MSN"; + String ICQ = "ICQ"; + String AIM = "AIM"; + String XMPP = "XMPP"; + String JABBER = "JABBER"; + String SKYPE = "SKYPE"; + String QQ = "QQ"; + } + + /** * This looks up the provider name defined in - * {@link android.provider.Im.ProviderNames} from the predefined IM protocol id. + * from the predefined IM protocol id. * This is used for interacting with the IM application. * * @param protocol the protocol ID @@ -1348,21 +1366,21 @@ public class Contacts { public static String lookupProviderNameFromId(int protocol) { switch (protocol) { case PROTOCOL_GOOGLE_TALK: - return Im.ProviderNames.GTALK; + return ProviderNames.GTALK; case PROTOCOL_AIM: - return Im.ProviderNames.AIM; + return ProviderNames.AIM; case PROTOCOL_MSN: - return Im.ProviderNames.MSN; + return ProviderNames.MSN; case PROTOCOL_YAHOO: - return Im.ProviderNames.YAHOO; + return ProviderNames.YAHOO; case PROTOCOL_ICQ: - return Im.ProviderNames.ICQ; + return ProviderNames.ICQ; case PROTOCOL_JABBER: - return Im.ProviderNames.JABBER; + return ProviderNames.JABBER; case PROTOCOL_SKYPE: - return Im.ProviderNames.SKYPE; + return ProviderNames.SKYPE; case PROTOCOL_QQ: - return Im.ProviderNames.QQ; + return ProviderNames.QQ; } return null; } @@ -1532,7 +1550,35 @@ public class Contacts { * @deprecated see {@link android.provider.ContactsContract} */ @Deprecated - public interface PresenceColumns extends Im.CommonPresenceColumns { + public interface PresenceColumns { + /** + * The priority, an integer, used by XMPP presence + * <P>Type: INTEGER</P> + */ + String PRIORITY = "priority"; + + /** + * The server defined status. + * <P>Type: INTEGER (one of the values below)</P> + */ + String PRESENCE_STATUS = ContactsContract.StatusUpdates.PRESENCE; + + /** + * Presence Status definition + */ + int OFFLINE = ContactsContract.StatusUpdates.OFFLINE; + int INVISIBLE = ContactsContract.StatusUpdates.INVISIBLE; + int AWAY = ContactsContract.StatusUpdates.AWAY; + int IDLE = ContactsContract.StatusUpdates.IDLE; + int DO_NOT_DISTURB = ContactsContract.StatusUpdates.DO_NOT_DISTURB; + int AVAILABLE = ContactsContract.StatusUpdates.AVAILABLE; + + /** + * The user defined status line. + * <P>Type: TEXT</P> + */ + String PRESENCE_CUSTOM_STATUS = ContactsContract.StatusUpdates.STATUS; + /** * The IM service the presence is coming from. Formatted using either * {@link Contacts.ContactMethods#encodePredefinedImProtocol} or diff --git a/core/java/android/provider/ContactsContract.java b/core/java/android/provider/ContactsContract.java index a56bb45..7fb9daf 100644 --- a/core/java/android/provider/ContactsContract.java +++ b/core/java/android/provider/ContactsContract.java @@ -23,14 +23,19 @@ import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; +import android.content.CursorEntityIterator; +import android.content.Entity; +import android.content.EntityIterator; import android.content.Intent; import android.content.res.Resources; import android.database.Cursor; +import android.database.DatabaseUtils; +import android.database.sqlite.SQLiteException; import android.graphics.Rect; import android.net.Uri; import android.os.RemoteException; -import android.provider.ContactsContract.CommonDataKinds.Email; import android.text.TextUtils; +import android.util.DisplayMetrics; import android.util.Pair; import android.view.View; @@ -57,7 +62,8 @@ import java.io.InputStream; * </li> * <li> * A row in the {@link RawContacts} table represents a set of Data describing a - * person and associated with a single account. + * person and associated with a single account (for example, a single Gmail + * account). * </li> * <li> * A row in the {@link Contacts} table represents an aggregate of one or more @@ -240,6 +246,9 @@ public final class ContactsContract { } /** + * Columns of {@link ContactsContract.Contacts} that track the user's + * preferences for, or interactions with, the contact. + * * @see Contacts * @see RawContacts * @see ContactsContract.Data @@ -266,20 +275,25 @@ public final class ContactsContract { public static final String STARRED = "starred"; /** - * A custom ringtone associated with a contact. Not always present. + * URI for a custom ringtone associated with the contact. If null or missing, + * the default ringtone is used. * <P>Type: TEXT (URI to the ringtone)</P> */ public static final String CUSTOM_RINGTONE = "custom_ringtone"; /** - * Whether the contact should always be sent to voicemail. Not always - * present. + * Whether the contact should always be sent to voicemail. If missing, + * defaults to false. * <P>Type: INTEGER (0 for false, 1 for true)</P> */ public static final String SEND_TO_VOICEMAIL = "send_to_voicemail"; } /** + * Columns of {@link ContactsContract.Contacts} that refer to intrinsic + * properties of the contact, as opposed to the user-specified options + * found in {@link ContactOptionsColumns}. + * * @see Contacts * @see ContactsContract.Data * @see PhoneLookup @@ -290,7 +304,14 @@ public final class ContactsContract { * The display name for the contact. * <P>Type: TEXT</P> */ - public static final String DISPLAY_NAME = "display_name"; + public static final String DISPLAY_NAME = ContactNameColumns.DISPLAY_NAME_PRIMARY; + + /** + * Reference to the row in the RawContacts table holding the contact name. + * <P>Type: INTEGER REFERENCES raw_contacts(_id)</P> + * @hide + */ + public static final String NAME_RAW_CONTACT_ID = "name_raw_contact_id"; /** * Reference to the row in the data table holding the photo. @@ -365,6 +386,126 @@ public final class ContactsContract { } /** + * Constants for various styles of combining given name, family name etc into + * a full name. For example, the western tradition follows the pattern + * 'given name' 'middle name' 'family name' with the alternative pattern being + * 'family name', 'given name' 'middle name'. The CJK tradition is + * 'family name' 'middle name' 'given name', with Japanese favoring a space between + * the names and Chinese omitting the space. + * @hide + */ + public interface FullNameStyle { + public static final int UNDEFINED = 0; + public static final int WESTERN = 1; + + /** + * Used if the name is written in Hanzi/Kanji/Hanja and we could not determine + * which specific language it belongs to: Chinese, Japanese or Korean. + */ + public static final int CJK = 2; + + public static final int CHINESE = 3; + public static final int JAPANESE = 4; + public static final int KOREAN = 5; + } + + /** + * Constants for various styles of capturing the pronunciation of a person's name. + * @hide + */ + public interface PhoneticNameStyle { + public static final int UNDEFINED = 0; + + /** + * Pinyin is a phonetic method of entering Chinese characters. Typically not explicitly + * shown in UIs, but used for searches and sorting. + */ + public static final int PINYIN = 3; + + /** + * Hiragana and Katakana are two common styles of writing out the pronunciation + * of a Japanese names. + */ + public static final int JAPANESE = 4; + + /** + * Hangul is the Korean phonetic alphabet. + */ + public static final int KOREAN = 5; + } + + /** + * Types of data used to produce the display name for a contact. Listed in the order + * of increasing priority. + * + * @hide + */ + public interface DisplayNameSources { + public static final int UNDEFINED = 0; + public static final int EMAIL = 10; + public static final int PHONE = 20; + public static final int ORGANIZATION = 30; + public static final int NICKNAME = 35; + public static final int STRUCTURED_NAME = 40; + } + + /** + * Contact name and contact name metadata columns in the RawContacts table. + * + * @see Contacts + * @see RawContacts + * @hide + */ + protected interface ContactNameColumns { + + /** + * The kind of data that is used as the display name for the contact, see + * DisplayNameSources. + */ + public static final String DISPLAY_NAME_SOURCE = "display_name_source"; + + /** + * The default text shown as the contact's display name. It is based on + * available data, see {@link #DISPLAY_NAME_SOURCE}. + */ + public static final String DISPLAY_NAME_PRIMARY = "display_name"; + + /** + * Alternative representation of the display name. If display name is + * based on the structured name and the structured name follows + * the Western full name style, then this field contains the "family name first" + * version of the full name. Otherwise, it is the same as DISPLAY_NAME_PRIMARY. + */ + public static final String DISPLAY_NAME_ALTERNATIVE = "display_name_alt"; + + /** + * The type of alphabet used to capture the phonetic name. See + * PhoneticNameStyle. + */ + public static final String PHONETIC_NAME_STYLE = "phonetic_name_style"; + + /** + * Pronunciation of the full name. See PhoneticNameStyle. + */ + public static final String PHONETIC_NAME = "phonetic_name"; + + /** + * Sort key that takes into account locale-based traditions for sorting + * names in address books. More specifically, for Chinese names + * the sort key is the name's Pinyin spelling; for Japanese names + * it is the Hiragana version of the phonetic name. + */ + public static final String SORT_KEY_PRIMARY = "sort_key"; + + /** + * Sort key based on the alternative representation of the full name, + * specifically the one using the 'family name first' format for + * Western names. + */ + public static final String SORT_KEY_ALTERNATIVE = "sort_key_alt"; + } + + /** * Constants for the contacts table, which contains a record per aggregate * of raw contacts representing the same person. * <h3>Operations</h3> @@ -423,13 +564,21 @@ public final class ContactsContract { * row id changed as a result of a sync or aggregation.</td> * </tr> * <tr> + * <td>long</td> + * <td>NAME_RAW_CONTACT_ID</td> + * <td>read-only</td> + * <td>The ID of the raw contact that contributes the display name + * to the aggregate contact. During aggregation one of the constituent + * raw contacts is chosen using a heuristic: a longer name or a name + * with more diacritic marks or more upper case characters is chosen.</td> + * </tr> + * <tr> * <td>String</td> - * <td>{@link #DISPLAY_NAME}</td> + * <td>DISPLAY_NAME_PRIMARY</td> * <td>read-only</td> - * <td>The display name for the contact. During aggregation display name is - * computed from display names of constituent raw contacts using a - * heuristic: a longer name or a name with more diacritic marks or more - * upper case characters is chosen.</td> + * <td>The display name for the contact. It is the display name + * contributed by the raw contact referred to by the NAME_RAW_CONTACT_ID + * column.</td> * </tr> * <tr> * <td>long</td> @@ -555,7 +704,7 @@ public final class ContactsContract { * </table> */ public static class Contacts implements BaseColumns, ContactsColumns, - ContactOptionsColumns, ContactStatusColumns { + ContactOptionsColumns, ContactNameColumns, ContactStatusColumns { /** * This utility class cannot be instantiated */ @@ -969,7 +1118,7 @@ public final class ContactsContract { * removes the raw contact from its aggregate contact. * The sync adapter then deletes the raw contact from the server and * finalizes phone-side deletion by calling {@code resolver.delete(...)} - * again and passing the {@link #CALLER_IS_SYNCADAPTER} query parameter.<p> + * again and passing the {@link ContactsContract#CALLER_IS_SYNCADAPTER} query parameter.<p> * <p>Some sync adapters are read-only, meaning that they only sync server-side * changes to the phone, but not the reverse. If one of those raw contacts * is marked for deletion, it will remain on the phone. However it will be @@ -1215,7 +1364,7 @@ public final class ContactsContract { * </table> */ public static final class RawContacts implements BaseColumns, RawContactsColumns, - ContactOptionsColumns, SyncColumns { + ContactOptionsColumns, ContactNameColumns, SyncColumns { /** * This utility class cannot be instantiated */ @@ -1313,7 +1462,7 @@ public final class ContactsContract { * <p> * A sub-directory of a single raw contact that contains all of their * {@link ContactsContract.Data} rows. To access this directory append - * {@link Entity#CONTENT_DIRECTORY} to the contact URI. See + * {@link #CONTENT_DIRECTORY} to the contact URI. See * {@link RawContactsEntity} for a stand-alone table containing the same * data. * </p> @@ -1351,6 +1500,112 @@ public final class ContactsContract { */ public static final String DATA_ID = "data_id"; } + + public static EntityIterator newEntityIterator(Cursor cursor) { + return new EntityIteratorImpl(cursor); + } + + private static class EntityIteratorImpl extends CursorEntityIterator { + private static final String[] DATA_KEYS = new String[]{ + Data.DATA1, + Data.DATA2, + Data.DATA3, + Data.DATA4, + Data.DATA5, + Data.DATA6, + Data.DATA7, + Data.DATA8, + Data.DATA9, + Data.DATA10, + Data.DATA11, + Data.DATA12, + Data.DATA13, + Data.DATA14, + Data.DATA15, + Data.SYNC1, + Data.SYNC2, + Data.SYNC3, + Data.SYNC4}; + + public EntityIteratorImpl(Cursor cursor) { + super(cursor); + } + + @Override + public android.content.Entity getEntityAndIncrementCursor(Cursor cursor) + throws RemoteException { + final int columnRawContactId = cursor.getColumnIndexOrThrow(RawContacts._ID); + final long rawContactId = cursor.getLong(columnRawContactId); + + // we expect the cursor is already at the row we need to read from + ContentValues cv = new ContentValues(); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, ACCOUNT_NAME); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, ACCOUNT_TYPE); + DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, cv, _ID); + DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, cv, DIRTY); + DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, cv, VERSION); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, SOURCE_ID); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, SYNC1); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, SYNC2); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, SYNC3); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, SYNC4); + DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, cv, DELETED); + DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, cv, CONTACT_ID); + DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, cv, STARRED); + DatabaseUtils.cursorIntToContentValuesIfPresent(cursor, cv, IS_RESTRICTED); + android.content.Entity contact = new android.content.Entity(cv); + + // read data rows until the contact id changes + do { + if (rawContactId != cursor.getLong(columnRawContactId)) { + break; + } + // add the data to to the contact + cv = new ContentValues(); + cv.put(Data._ID, cursor.getLong(cursor.getColumnIndexOrThrow(Entity.DATA_ID))); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, + Data.RES_PACKAGE); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, Data.MIMETYPE); + DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, cv, Data.IS_PRIMARY); + DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, cv, + Data.IS_SUPER_PRIMARY); + DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, cv, Data.DATA_VERSION); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, + CommonDataKinds.GroupMembership.GROUP_SOURCE_ID); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, cv, + Data.DATA_VERSION); + for (String key : DATA_KEYS) { + final int columnIndex = cursor.getColumnIndexOrThrow(key); + if (cursor.isNull(columnIndex)) { + // don't put anything + } else { + try { + cv.put(key, cursor.getString(columnIndex)); + } catch (SQLiteException e) { + cv.put(key, cursor.getBlob(columnIndex)); + } + } + // TODO: go back to this version of the code when bug + // http://b/issue?id=2306370 is fixed. +// if (cursor.isNull(columnIndex)) { +// // don't put anything +// } else if (cursor.isLong(columnIndex)) { +// values.put(key, cursor.getLong(columnIndex)); +// } else if (cursor.isFloat(columnIndex)) { +// values.put(key, cursor.getFloat(columnIndex)); +// } else if (cursor.isString(columnIndex)) { +// values.put(key, cursor.getString(columnIndex)); +// } else if (cursor.isBlob(columnIndex)) { +// values.put(key, cursor.getBlob(columnIndex)); +// } + } + contact.addSubValue(ContactsContract.Data.CONTENT_URI, cv); + } while (cursor.moveToNext()); + + return contact; + } + + } } /** @@ -1359,18 +1614,60 @@ public final class ContactsContract { * @see StatusUpdates * @see ContactsContract.Data */ - protected interface StatusColumns extends Im.CommonPresenceColumns { + protected interface StatusColumns { /** * Contact's latest presence level. * <P>Type: INTEGER (one of the values below)</P> */ - public static final String PRESENCE = PRESENCE_STATUS; + public static final String PRESENCE = "mode"; + + /** + * @deprecated use {@link #PRESENCE} + */ + @Deprecated + public static final String PRESENCE_STATUS = PRESENCE; + + /** + * An allowed value of {@link #PRESENCE}. + */ + int OFFLINE = 0; + + /** + * An allowed value of {@link #PRESENCE}. + */ + int INVISIBLE = 1; + + /** + * An allowed value of {@link #PRESENCE}. + */ + int AWAY = 2; + + /** + * An allowed value of {@link #PRESENCE}. + */ + int IDLE = 3; + + /** + * An allowed value of {@link #PRESENCE}. + */ + int DO_NOT_DISTURB = 4; + + /** + * An allowed value of {@link #PRESENCE}. + */ + int AVAILABLE = 5; /** * Contact latest status update. * <p>Type: TEXT</p> */ - public static final String STATUS = PRESENCE_CUSTOM_STATUS; + public static final String STATUS = "status"; + + /** + * @deprecated use {@link #STATUS} + */ + @Deprecated + public static final String PRESENCE_CUSTOM_STATUS = STATUS; /** * The absolute time in milliseconds when the latest status was inserted/updated. @@ -1426,7 +1723,7 @@ public final class ContactsContract { public static final String RAW_CONTACT_ID = "raw_contact_id"; /** - * Whether this is the primary entry of its kind for the raw contact it belongs to + * Whether this is the primary entry of its kind for the raw contact it belongs to. * <P>Type: INTEGER (if set, non-0 means true)</P> */ public static final String IS_PRIMARY = "is_primary"; @@ -1475,7 +1772,10 @@ public final class ContactsContract { public static final String DATA13 = "data13"; /** Generic data column, the meaning is {@link #MIMETYPE} specific */ public static final String DATA14 = "data14"; - /** Generic data column, the meaning is {@link #MIMETYPE} specific */ + /** + * Generic data column, the meaning is {@link #MIMETYPE} specific. By convention, + * this field is used to store BLOBs (binary data). + */ public static final String DATA15 = "data15"; /** Generic column for use by sync adapters. */ @@ -1494,30 +1794,35 @@ public final class ContactsContract { * @see ContactsContract.Data */ protected interface DataColumnsWithJoins extends BaseColumns, DataColumns, StatusColumns, - RawContactsColumns, ContactsColumns, ContactOptionsColumns, ContactStatusColumns { - + RawContactsColumns, ContactsColumns, ContactNameColumns, ContactOptionsColumns, + ContactStatusColumns { } /** * <p> * Constants for the data table, which contains data points tied to a raw - * contact. For example, a phone number or email address. + * contact. Each row of the data table is typically used to store a single + * piece of contact + * information (such as a phone number) and its + * associated metadata (such as whether it is a work or home number). * </p> * <h3>Data kinds</h3> * <p> - * Data is a generic table that can hold all kinds of data. Sync adapters - * and applications can introduce their own data kinds. The kind of data - * stored in a particular row is determined by the mime type in the row. - * Fields from {@link #DATA1} through {@link #DATA15} are generic columns - * whose specific use is determined by the kind of data stored in the row. + * Data is a generic table that can hold any kind of contact data. + * The kind of data stored in a given row is specified by the row's + * {@link #MIMETYPE} value, which determines the meaning of the + * generic columns {@link #DATA1} through + * {@link #DATA15}. * For example, if the data kind is - * {@link CommonDataKinds.Phone Phone.CONTENT_ITEM_TYPE}, then DATA1 stores the + * {@link CommonDataKinds.Phone Phone.CONTENT_ITEM_TYPE}, then the column + * {@link #DATA1} stores the * phone number, but if the data kind is - * {@link CommonDataKinds.Email Email.CONTENT_ITEM_TYPE}, then DATA1 stores the - * email address. + * {@link CommonDataKinds.Email Email.CONTENT_ITEM_TYPE}, then {@link #DATA1} + * stores the email address. + * Sync adapters and applications can introduce their own data kinds. * </p> * <p> - * ContactsContract defines a small number of common data kinds, e.g. + * ContactsContract defines a small number of pre-defined data kinds, e.g. * {@link CommonDataKinds.Phone}, {@link CommonDataKinds.Email} etc. As a * convenience, these classes define data kind specific aliases for DATA1 etc. * For example, {@link CommonDataKinds.Phone Phone.NUMBER} is the same as @@ -1534,8 +1839,8 @@ public final class ContactsContract { * By convention, {@link #DATA15} is used for storing BLOBs (binary data). * </p> * <p> - * Typically you should refrain from introducing new kinds of data for 3rd - * party account types. For example, if you add a data row for + * Typically you should refrain from introducing new kinds of data for an other + * party's account types. For example, if you add a data row for * "favorite song" to a raw contact owned by a Google account, it will not * get synced to the server, because the Google sync adapter does not know * how to handle this data kind. Thus new data kinds are typically @@ -1672,6 +1977,10 @@ public final class ContactsContract { * </dd> * </dl> * <h2>Columns</h2> + * <p> + * Many columns are available via a {@link Data#CONTENT_URI} query. For best performance you + * should explicitly specify a projection to only those columns that you need. + * </p> * <table class="jd-sumtable"> * <tr> * <th colspan='4'>Data</th> @@ -1681,7 +1990,7 @@ public final class ContactsContract { * <td style="width: 20em;">{@link #_ID}</td> * <td style="width: 5em;">read-only</td> * <td>Row ID. Sync adapter should try to preserve row IDs during updates. In other words, - * it would be a bad idea to delete and reinsert a data rows. A sync adapter should + * it would be a bad idea to delete and reinsert a data row. A sync adapter should * always do an update instead.</td> * </tr> * <tr> @@ -1713,21 +2022,15 @@ public final class ContactsContract { * <td>long</td> * <td>{@link #RAW_CONTACT_ID}</td> * <td>read/write-once</td> - * <td>A reference to the {@link RawContacts#_ID} that this data belongs to.</td> - * </tr> - * <tr> - * <td>long</td> - * <td>{@link #CONTACT_ID}</td> - * <td>read-only</td> - * <td>A reference to the {@link ContactsContract.Contacts#_ID} that this data row belongs - * to. It is obtained through a join with RawContacts.</td> + * <td>The id of the row in the {@link RawContacts} table that this data belongs to.</td> * </tr> * <tr> * <td>int</td> * <td>{@link #IS_PRIMARY}</td> * <td>read/write</td> * <td>Whether this is the primary entry of its kind for the raw contact it belongs to. - * "1" if true, "0" if false.</td> + * "1" if true, "0" if false. + * </td> * </tr> * <tr> * <td>int</td> @@ -1735,7 +2038,9 @@ public final class ContactsContract { * <td>read/write</td> * <td>Whether this is the primary entry of its kind for the aggregate * contact it belongs to. Any data record that is "super primary" must - * also be "primary".</td> + * also be "primary". For example, the super-primary entry may be + * interpreted as the default contact value of its kind (for example, + * the default phone number to use for the contact).</td> * </tr> * <tr> * <td>int</td> @@ -1764,7 +2069,19 @@ public final class ContactsContract { * {@link #DATA15} * </td> * <td>read/write</td> - * <td>Generic data columns, the meaning is {@link #MIMETYPE} specific.</td> + * <td> + * <p> + * Generic data columns. The meaning of each column is determined by the + * {@link #MIMETYPE}. By convention, {@link #DATA15} is used for storing + * BLOBs (binary data). + * </p> + * <p> + * Data columns whose meaning is not explicitly defined for a given MIMETYPE + * should not be used. There is no guarantee that any sync adapter will + * preserve them. Sync adapters themselves should not use such columns either, + * but should instead use {@link #SYNC1}-{@link #SYNC4}. + * </p> + * </td> * </tr> * <tr> * <td>Any type</td> @@ -1781,6 +2098,10 @@ public final class ContactsContract { * </tr> * </table> * + * <p> + * Some columns from the most recent associated status update are also available + * through an implicit join. + * </p> * <table class="jd-sumtable"> * <tr> * <th colspan='4'>Join with {@link StatusUpdates}</th> @@ -1833,18 +2154,26 @@ public final class ContactsContract { * </table> * * <p> - * Columns from the associated raw contact are also available through an - * implicit join. + * Some columns from the associated raw contact are also available through an + * implicit join. The other columns are excluded as uninteresting in this + * context. * </p> * * <table class="jd-sumtable"> * <tr> - * <th colspan='4'>Join with {@link RawContacts}</th> + * <th colspan='4'>Join with {@link ContactsContract.RawContacts}</th> * </tr> * <tr> - * <td style="width: 7em;">int</td> - * <td style="width: 20em;">{@link #AGGREGATION_MODE}</td> + * <td style="width: 7em;">long</td> + * <td style="width: 20em;">{@link #CONTACT_ID}</td> * <td style="width: 5em;">read-only</td> + * <td>The id of the row in the {@link Contacts} table that this data belongs + * to.</td> + * </tr> + * <tr> + * <td>int</td> + * <td>{@link #AGGREGATION_MODE}</td> + * <td>read-only</td> * <td>See {@link RawContacts}.</td> * </tr> * <tr> @@ -1856,13 +2185,18 @@ public final class ContactsContract { * </table> * * <p> - * Columns from the associated aggregated contact are also available through an - * implicit join. + * The ID column for the associated aggregated contact table + * {@link ContactsContract.Contacts} is available + * via the implicit join to the {@link RawContacts} table, see above. + * The remaining columns from this table are also + * available, through an implicit join. This + * facilitates lookup by + * the value of a single data element, such as the email address. * </p> * * <table class="jd-sumtable"> * <tr> - * <th colspan='4'>Join with {@link Contacts}</th> + * <th colspan='4'>Join with {@link ContactsContract.Contacts}</th> * </tr> * <tr> * <td style="width: 7em;">String</td> @@ -1969,24 +2303,30 @@ public final class ContactsContract { private Data() {} /** - * The content:// style URI for this table + * The content:// style URI for this table, which requests a directory + * of data rows matching the selection criteria. */ public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "data"); /** - * The MIME type of {@link #CONTENT_URI} providing a directory of data. + * The MIME type of the results from {@link #CONTENT_URI}. */ public static final String CONTENT_TYPE = "vnd.android.cursor.dir/data"; /** + * <p> * If {@link #FOR_EXPORT_ONLY} is explicitly set to "1", returned Cursor toward * Data.CONTENT_URI contains only exportable data. - * + * </p> + * <p> * This flag is useful (currently) only for vCard exporter in Contacts app, which * needs to exclude "un-exportable" data from available data to export, while * Contacts app itself has priviledge to access all data including "un-exportable" * ones and providers return all of them regardless of the callers' intention. - * <P>Type: INTEGER</p> + * </p> + * <p> + * Type: INTEGER + * </p> * * @hide Maybe available only in Eclair and not really ready for public use. * TODO: remove, or implement this feature completely. As of now (Eclair), @@ -1995,9 +2335,17 @@ public final class ContactsContract { public static final String FOR_EXPORT_ONLY = "for_export_only"; /** + * <p> * Build a {@link android.provider.ContactsContract.Contacts#CONTENT_LOOKUP_URI} * style {@link Uri} for the parent {@link android.provider.ContactsContract.Contacts} * entry of the given {@link ContactsContract.Data} entry. + * </p> + * <p> + * Returns the Uri for the contact in the first entry returned by + * {@link ContentResolver#query(Uri, String[], String, String[], String)} + * for the provided {@code dataUri}. If the query returns null or empty + * results, silently returns null. + * </p> */ public static Uri getContactLookupUri(ContentResolver resolver, Uri dataUri) { final Cursor cursor = resolver.query(dataUri, new String[] { @@ -2020,7 +2368,7 @@ public final class ContactsContract { /** * <p> - * Constants for the raw contacts entities table, which can be though of as + * Constants for the raw contacts entities table, which can be thought of as * an outer join of the raw_contacts table with the data table. It is a strictly * read-only table. * </p> @@ -2765,6 +3113,21 @@ public final class ContactsContract { * <P>Type: TEXT</P> */ public static final String PHONETIC_FAMILY_NAME = DATA9; + + /** + * The style used for combining given/middle/family name into a full name. + * See {@link ContactsContract.FullNameStyle}. + * + * @hide + */ + public static final String FULL_NAME_STYLE = DATA10; + + /** + * The alphabet used for capturing the phonetic name. + * See ContactsContract.PhoneticNameStyle. + * @hide + */ + public static final String PHONETIC_NAME_STYLE = DATA11; } /** @@ -3644,6 +4007,12 @@ public final class ContactsContract { * <td>{@link #DATA9}</td> * <td></td> * </tr> + * <tr> + * <td>String</td> + * <td>PHONETIC_NAME_STYLE</td> + * <td>{@link #DATA10}</td> + * <td></td> + * </tr> * </table> */ public static final class Organization implements DataColumnsWithJoins, CommonColumns { @@ -3701,6 +4070,13 @@ public final class ContactsContract { public static final String OFFICE_LOCATION = DATA9; /** + * The alphabet used for capturing the phonetic name. + * See {@link ContactsContract.PhoneticNameStyle}. + * @hide + */ + public static final String PHONETIC_NAME_STYLE = DATA10; + + /** * Return the string resource that best describes the given * {@link #TYPE}. Will always return a valid resource. */ @@ -4311,6 +4687,42 @@ public final class ContactsContract { * The MIME type of a single group. */ public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/group"; + + public static EntityIterator newEntityIterator(Cursor cursor) { + return new EntityIteratorImpl(cursor); + } + + private static class EntityIteratorImpl extends CursorEntityIterator { + public EntityIteratorImpl(Cursor cursor) { + super(cursor); + } + + @Override + public Entity getEntityAndIncrementCursor(Cursor cursor) throws RemoteException { + // we expect the cursor is already at the row we need to read from + final ContentValues values = new ContentValues(); + DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, values, _ID); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, values, ACCOUNT_NAME); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, values, ACCOUNT_TYPE); + DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, values, DIRTY); + DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, values, VERSION); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, values, SOURCE_ID); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, values, RES_PACKAGE); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, values, TITLE); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, values, TITLE_RES); + DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, values, GROUP_VISIBLE); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, values, SYNC1); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, values, SYNC2); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, values, SYNC3); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, values, SYNC4); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, values, SYSTEM_ID); + DatabaseUtils.cursorLongToContentValuesIfPresent(cursor, values, DELETED); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, values, NOTES); + DatabaseUtils.cursorStringToContentValuesIfPresent(cursor, values, SHOULD_SYNC); + cursor.moveToNext(); + return new Entity(values); + } + } } /** @@ -4568,8 +4980,10 @@ public final class ContactsContract { /** * Extra used to specify pivot dialog location in screen coordinates. + * @deprecated Use {@link Intent#setSourceBounds(Rect)} instead. * @hide */ + @Deprecated public static final String EXTRA_TARGET_RECT = "target_rect"; /** @@ -4629,15 +5043,17 @@ public final class ContactsContract { */ public static void showQuickContact(Context context, View target, Uri lookupUri, int mode, String[] excludeMimes) { - // Find location and bounds of target view - final int[] location = new int[2]; - target.getLocationOnScreen(location); + // Find location and bounds of target view, adjusting based on the + // assumed local density. + final float appScale = context.getResources().getCompatibilityInfo().applicationScale; + final int[] pos = new int[2]; + target.getLocationOnScreen(pos); final Rect rect = new Rect(); - rect.left = location[0]; - rect.top = location[1]; - rect.right = rect.left + target.getWidth(); - rect.bottom = rect.top + target.getHeight(); + rect.left = (int) (pos[0] * appScale + 0.5f); + rect.top = (int) (pos[1] * appScale + 0.5f); + rect.right = (int) ((pos[0] + target.getWidth()) * appScale + 0.5f); + rect.bottom = (int) ((pos[1] + target.getHeight()) * appScale + 0.5f); // Trigger with obtained rectangle showQuickContact(context, rect, lookupUri, mode, excludeMimes); @@ -4654,8 +5070,11 @@ public final class ContactsContract { * @param target Specific {@link Rect} that this dialog should be * centered around, in screen coordinates. In particular, if * the dialog has a "callout" arrow, it will be pointed and - * centered around this {@link Rect}. - * @param lookupUri A {@link ContactsContract.Contacts#CONTENT_LOOKUP_URI} style + * centered around this {@link Rect}. If you are running at a + * non-native density, you need to manually adjust using + * {@link DisplayMetrics#density} before calling. + * @param lookupUri A + * {@link ContactsContract.Contacts#CONTENT_LOOKUP_URI} style * {@link Uri} that describes a specific contact to feature * in this dialog. * @param mode Any of {@link #MODE_SMALL}, {@link #MODE_MEDIUM}, or @@ -4674,7 +5093,7 @@ public final class ContactsContract { | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); intent.setData(lookupUri); - intent.putExtra(EXTRA_TARGET_RECT, target); + intent.setSourceBounds(target); intent.putExtra(EXTRA_MODE, mode); intent.putExtra(EXTRA_EXCLUDE_MIMES, excludeMimes); context.startActivity(intent); diff --git a/core/java/android/provider/Gmail.java b/core/java/android/provider/Gmail.java deleted file mode 100644 index 073ae6c..0000000 --- a/core/java/android/provider/Gmail.java +++ /dev/null @@ -1,2467 +0,0 @@ -/* - * Copyright (C) 2007 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.provider; - -import com.google.android.collect.Lists; -import com.google.android.collect.Maps; -import com.google.android.collect.Sets; - -import android.content.AsyncQueryHandler; -import android.content.ContentQueryMap; -import android.content.ContentResolver; -import android.content.ContentUris; -import android.content.ContentValues; -import android.database.ContentObserver; -import android.database.Cursor; -import android.database.DataSetObserver; -import android.net.Uri; -import android.os.Bundle; -import android.os.Handler; -import android.text.Html; -import android.text.SpannableStringBuilder; -import android.text.Spanned; -import android.text.TextUtils; -import android.text.TextUtils.SimpleStringSplitter; -import android.text.style.CharacterStyle; -import android.text.util.Regex; -import android.util.Log; - -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Observable; -import java.util.Observer; -import java.util.Set; -import java.util.SortedSet; -import java.util.TreeSet; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * A thin wrapper over the content resolver for accessing the gmail provider. - * - * @hide - */ -public final class Gmail { - // Set to true to enable extra debugging. - private static final boolean DEBUG = false; - - public static final String GMAIL_AUTH_SERVICE = "mail"; - // These constants come from google3/java/com/google/caribou/backend/MailLabel.java. - public static final String LABEL_SENT = "^f"; - public static final String LABEL_INBOX = "^i"; - public static final String LABEL_DRAFT = "^r"; - public static final String LABEL_UNREAD = "^u"; - public static final String LABEL_TRASH = "^k"; - public static final String LABEL_SPAM = "^s"; - public static final String LABEL_STARRED = "^t"; - public static final String LABEL_CHAT = "^b"; // 'b' for 'buzz' - public static final String LABEL_VOICEMAIL = "^vm"; - public static final String LABEL_IGNORED = "^g"; - public static final String LABEL_ALL = "^all"; - // These constants (starting with "^^") are only used locally and are not understood by the - // server. - public static final String LABEL_VOICEMAIL_INBOX = "^^vmi"; - public static final String LABEL_CACHED = "^^cached"; - public static final String LABEL_OUTBOX = "^^out"; - - public static final String AUTHORITY = "gmail-ls"; - private static final String TAG = "Gmail"; - private static final String AUTHORITY_PLUS_CONVERSATIONS = - "content://" + AUTHORITY + "/conversations/"; - private static final String AUTHORITY_PLUS_LABELS = - "content://" + AUTHORITY + "/labels/"; - private static final String AUTHORITY_PLUS_MESSAGES = - "content://" + AUTHORITY + "/messages/"; - private static final String AUTHORITY_PLUS_SETTINGS = - "content://" + AUTHORITY + "/settings/"; - - public static final Uri BASE_URI = Uri.parse( - "content://" + AUTHORITY); - private static final Uri LABELS_URI = - Uri.parse(AUTHORITY_PLUS_LABELS); - private static final Uri CONVERSATIONS_URI = - Uri.parse(AUTHORITY_PLUS_CONVERSATIONS); - private static final Uri SETTINGS_URI = - Uri.parse(AUTHORITY_PLUS_SETTINGS); - - /** Separates email addresses in strings in the database. */ - public static final String EMAIL_SEPARATOR = "\n"; - public static final Pattern EMAIL_SEPARATOR_PATTERN = Pattern.compile(EMAIL_SEPARATOR); - - /** - * Space-separated lists have separators only between items. - */ - private static final char SPACE_SEPARATOR = ' '; - public static final Pattern SPACE_SEPARATOR_PATTERN = Pattern.compile(" "); - - /** - * Comma-separated lists have separators between each item, before the first and after the last - * item. The empty list is <tt>,</tt>. - * - * <p>This makes them easier to modify with SQL since it is not a special case to add or - * remove the last item. Having a separator on each side of each value also makes it safe to use - * SQL's REPLACE to remove an item from a string by using REPLACE(',value,', ','). - * - * <p>We could use the same separator for both lists but this makes it easier to remember which - * kind of list one is dealing with. - */ - private static final char COMMA_SEPARATOR = ','; - public static final Pattern COMMA_SEPARATOR_PATTERN = Pattern.compile(","); - - /** Separates attachment info parts in strings in the database. */ - public static final String ATTACHMENT_INFO_SEPARATOR = "\n"; - public static final Pattern ATTACHMENT_INFO_SEPARATOR_PATTERN = - Pattern.compile(ATTACHMENT_INFO_SEPARATOR); - - public static final Character SENDER_LIST_SEPARATOR = '\n'; - public static final String SENDER_LIST_TOKEN_ELIDED = "e"; - public static final String SENDER_LIST_TOKEN_NUM_MESSAGES = "n"; - public static final String SENDER_LIST_TOKEN_NUM_DRAFTS = "d"; - public static final String SENDER_LIST_TOKEN_LITERAL = "l"; - public static final String SENDER_LIST_TOKEN_SENDING = "s"; - public static final String SENDER_LIST_TOKEN_SEND_FAILED = "f"; - - /** Used for finding status in a cursor's extras. */ - public static final String EXTRA_STATUS = "status"; - - public static final String RESPOND_INPUT_COMMAND = "command"; - public static final String COMMAND_RETRY = "retry"; - public static final String COMMAND_ACTIVATE = "activate"; - public static final String COMMAND_SET_VISIBLE = "setVisible"; - public static final String SET_VISIBLE_PARAM_VISIBLE = "visible"; - public static final String RESPOND_OUTPUT_COMMAND_RESPONSE = "commandResponse"; - public static final String COMMAND_RESPONSE_OK = "ok"; - public static final String COMMAND_RESPONSE_UNKNOWN = "unknownCommand"; - - public static final String INSERT_PARAM_ATTACHMENT_ORIGIN = "origin"; - public static final String INSERT_PARAM_ATTACHMENT_ORIGIN_EXTRAS = "originExtras"; - - private static final Pattern NAME_ADDRESS_PATTERN = Pattern.compile("\"(.*)\""); - private static final Pattern UNNAMED_ADDRESS_PATTERN = Pattern.compile("([^<]+)@"); - - private static final Map<Integer, Integer> sPriorityToLength = Maps.newHashMap(); - public static final SimpleStringSplitter sSenderListSplitter = - new SimpleStringSplitter(SENDER_LIST_SEPARATOR); - public static String[] sSenderFragments = new String[8]; - - /** - * Returns the name in an address string - * @param addressString such as "bobby" <bob@example.com> - * @return returns the quoted name in the addressString, otherwise the username from the email - * address - */ - public static String getNameFromAddressString(String addressString) { - Matcher namedAddressMatch = NAME_ADDRESS_PATTERN.matcher(addressString); - if (namedAddressMatch.find()) { - String name = namedAddressMatch.group(1); - if (name.length() > 0) return name; - addressString = - addressString.substring(namedAddressMatch.end(), addressString.length()); - } - - Matcher unnamedAddressMatch = UNNAMED_ADDRESS_PATTERN.matcher(addressString); - if (unnamedAddressMatch.find()) { - return unnamedAddressMatch.group(1); - } - - return addressString; - } - - /** - * Returns the email address in an address string - * @param addressString such as "bobby" <bob@example.com> - * @return returns the email address, such as bob@example.com from the example above - */ - public static String getEmailFromAddressString(String addressString) { - String result = addressString; - Matcher match = Regex.EMAIL_ADDRESS_PATTERN.matcher(addressString); - if (match.find()) { - result = addressString.substring(match.start(), match.end()); - } - - return result; - } - - /** - * Returns whether the label is user-defined (versus system-defined labels such as inbox, whose - * names start with "^"). - */ - public static boolean isLabelUserDefined(String label) { - // TODO: label should never be empty so we should be able to say [label.charAt(0) != '^']. - // However, it's a release week and I'm too scared to make that change. - return !label.startsWith("^"); - } - - private static final Set<String> USER_SETTABLE_BUILTIN_LABELS = Sets.newHashSet( - Gmail.LABEL_INBOX, - Gmail.LABEL_UNREAD, - Gmail.LABEL_TRASH, - Gmail.LABEL_SPAM, - Gmail.LABEL_STARRED, - Gmail.LABEL_IGNORED); - - /** - * Returns whether the label is user-settable. For example, labels such as LABEL_DRAFT should - * only be set internally. - */ - public static boolean isLabelUserSettable(String label) { - return USER_SETTABLE_BUILTIN_LABELS.contains(label) || isLabelUserDefined(label); - } - - /** - * Returns the set of labels using the raw labels from a previous getRawLabels() - * as input. - * @return a copy of the set of labels. To add or remove labels call - * MessageCursor.addOrRemoveLabel on each message in the conversation. - */ - public static Set<Long> getLabelIdsFromLabelIdsString( - TextUtils.StringSplitter splitter) { - Set<Long> labelIds = Sets.newHashSet(); - for (String labelIdString : splitter) { - labelIds.add(Long.valueOf(labelIdString)); - } - return labelIds; - } - - /** - * @deprecated remove when the activities stop using canonical names to identify labels - */ - public static Set<String> getCanonicalNamesFromLabelIdsString( - LabelMap labelMap, TextUtils.StringSplitter splitter) { - Set<String> canonicalNames = Sets.newHashSet(); - for (long labelId : getLabelIdsFromLabelIdsString(splitter)) { - final String canonicalName = labelMap.getCanonicalName(labelId); - // We will sometimes see labels that the label map does not yet know about or that - // do not have names yet. - if (!TextUtils.isEmpty(canonicalName)) { - canonicalNames.add(canonicalName); - } else { - Log.w(TAG, "getCanonicalNamesFromLabelIdsString skipping label id: " + labelId); - } - } - return canonicalNames; - } - - /** - * @return a StringSplitter that is configured to split message label id strings - */ - public static TextUtils.StringSplitter newMessageLabelIdsSplitter() { - return new TextUtils.SimpleStringSplitter(SPACE_SEPARATOR); - } - - /** - * @return a StringSplitter that is configured to split conversation label id strings - */ - public static TextUtils.StringSplitter newConversationLabelIdsSplitter() { - return new CommaStringSplitter(); - } - - /** - * A splitter for strings of the form described in the docs for COMMA_SEPARATOR. - */ - private static class CommaStringSplitter extends TextUtils.SimpleStringSplitter { - - public CommaStringSplitter() { - super(COMMA_SEPARATOR); - } - - @Override - public void setString(String string) { - // The string should always be at least a single comma. - super.setString(string.substring(1)); - } - } - - /** - * Creates a single string of the form that getLabelIdsFromLabelIdsString can split. - */ - public static String getLabelIdsStringFromLabelIds(Set<Long> labelIds) { - StringBuilder sb = new StringBuilder(); - sb.append(COMMA_SEPARATOR); - for (Long labelId : labelIds) { - sb.append(labelId); - sb.append(COMMA_SEPARATOR); - } - return sb.toString(); - } - - public static final class ConversationColumns { - public static final String ID = "_id"; - public static final String SUBJECT = "subject"; - public static final String SNIPPET = "snippet"; - public static final String FROM = "fromAddress"; - public static final String DATE = "date"; - public static final String PERSONAL_LEVEL = "personalLevel"; - /** A list of label names with a space after each one (including the last one). This makes - * it easier remove individual labels from this list using SQL. */ - public static final String LABEL_IDS = "labelIds"; - public static final String NUM_MESSAGES = "numMessages"; - public static final String MAX_MESSAGE_ID = "maxMessageId"; - public static final String HAS_ATTACHMENTS = "hasAttachments"; - public static final String HAS_MESSAGES_WITH_ERRORS = "hasMessagesWithErrors"; - public static final String FORCE_ALL_UNREAD = "forceAllUnread"; - - private ConversationColumns() {} - } - - public static final class MessageColumns { - - public static final String ID = "_id"; - public static final String MESSAGE_ID = "messageId"; - public static final String CONVERSATION_ID = "conversation"; - public static final String SUBJECT = "subject"; - public static final String SNIPPET = "snippet"; - public static final String FROM = "fromAddress"; - public static final String TO = "toAddresses"; - public static final String CC = "ccAddresses"; - public static final String BCC = "bccAddresses"; - public static final String REPLY_TO = "replyToAddresses"; - public static final String DATE_SENT_MS = "dateSentMs"; - public static final String DATE_RECEIVED_MS = "dateReceivedMs"; - public static final String LIST_INFO = "listInfo"; - public static final String PERSONAL_LEVEL = "personalLevel"; - public static final String BODY = "body"; - public static final String EMBEDS_EXTERNAL_RESOURCES = "bodyEmbedsExternalResources"; - public static final String LABEL_IDS = "labelIds"; - public static final String JOINED_ATTACHMENT_INFOS = "joinedAttachmentInfos"; - public static final String ERROR = "error"; - // TODO: add a method for accessing this - public static final String REF_MESSAGE_ID = "refMessageId"; - - // Fake columns used only for saving or sending messages. - public static final String FAKE_SAVE = "save"; - public static final String FAKE_REF_MESSAGE_ID = "refMessageId"; - - private MessageColumns() {} - } - - public static final class LabelColumns { - public static final String CANONICAL_NAME = "canonicalName"; - public static final String NAME = "name"; - public static final String NUM_CONVERSATIONS = "numConversations"; - public static final String NUM_UNREAD_CONVERSATIONS = - "numUnreadConversations"; - - private LabelColumns() {} - } - - public static final class SettingsColumns { - public static final String LABELS_INCLUDED = "labelsIncluded"; - public static final String LABELS_PARTIAL = "labelsPartial"; - public static final String CONVERSATION_AGE_DAYS = - "conversationAgeDays"; - public static final String MAX_ATTACHMENET_SIZE_MB = - "maxAttachmentSize"; - } - - /** - * These flags can be included as Selection Arguments when - * querying the provider. - */ - public static class SelectionArguments { - private SelectionArguments() { - // forbid instantiation - } - - /** - * Specifies that you do NOT wish the returned cursor to - * become the Active Network Cursor. If you do not include - * this flag as a selectionArg, the new cursor will become the - * Active Network Cursor by default. - */ - public static final String DO_NOT_BECOME_ACTIVE_NETWORK_CURSOR = - "SELECTION_ARGUMENT_DO_NOT_BECOME_ACTIVE_NETWORK_CURSOR"; - } - - // These are the projections that we need when getting cursors from the - // content provider. - private static String[] CONVERSATION_PROJECTION = { - ConversationColumns.ID, - ConversationColumns.SUBJECT, - ConversationColumns.SNIPPET, - ConversationColumns.FROM, - ConversationColumns.DATE, - ConversationColumns.PERSONAL_LEVEL, - ConversationColumns.LABEL_IDS, - ConversationColumns.NUM_MESSAGES, - ConversationColumns.MAX_MESSAGE_ID, - ConversationColumns.HAS_ATTACHMENTS, - ConversationColumns.HAS_MESSAGES_WITH_ERRORS, - ConversationColumns.FORCE_ALL_UNREAD}; - private static String[] MESSAGE_PROJECTION = { - MessageColumns.ID, - MessageColumns.MESSAGE_ID, - MessageColumns.CONVERSATION_ID, - MessageColumns.SUBJECT, - MessageColumns.SNIPPET, - MessageColumns.FROM, - MessageColumns.TO, - MessageColumns.CC, - MessageColumns.BCC, - MessageColumns.REPLY_TO, - MessageColumns.DATE_SENT_MS, - MessageColumns.DATE_RECEIVED_MS, - MessageColumns.LIST_INFO, - MessageColumns.PERSONAL_LEVEL, - MessageColumns.BODY, - MessageColumns.EMBEDS_EXTERNAL_RESOURCES, - MessageColumns.LABEL_IDS, - MessageColumns.JOINED_ATTACHMENT_INFOS, - MessageColumns.ERROR}; - private static String[] LABEL_PROJECTION = { - BaseColumns._ID, - LabelColumns.CANONICAL_NAME, - LabelColumns.NAME, - LabelColumns.NUM_CONVERSATIONS, - LabelColumns.NUM_UNREAD_CONVERSATIONS}; - private static String[] SETTINGS_PROJECTION = { - SettingsColumns.LABELS_INCLUDED, - SettingsColumns.LABELS_PARTIAL, - SettingsColumns.CONVERSATION_AGE_DAYS, - SettingsColumns.MAX_ATTACHMENET_SIZE_MB, - }; - - private ContentResolver mContentResolver; - - public Gmail(ContentResolver contentResolver) { - mContentResolver = contentResolver; - } - - /** - * Returns source if source is non-null. Returns the empty string otherwise. - */ - private static String toNonnullString(String source) { - if (source == null) { - return ""; - } else { - return source; - } - } - - /** - * Behavior for a new cursor: should it become the Active Network - * Cursor? This could potentially lead to bad behavior if someone - * else is using the Active Network Cursor, since theirs will stop - * being the Active Network Cursor. - */ - public static enum BecomeActiveNetworkCursor { - /** - * The new cursor should become the one and only Active - * Network Cursor. Any other cursor that might already be the - * Active Network Cursor will cease to be so. - */ - YES, - - /** - * The new cursor should not become the Active Network - * Cursor. Any other cursor that might already be the Active - * Network Cursor will continue to be so. - */ - NO - } - - /** - * Wraps a Cursor in a ConversationCursor - * - * @param account the account the cursor is associated with - * @param cursor The Cursor to wrap - * @return a new ConversationCursor - */ - public ConversationCursor getConversationCursorForCursor(String account, Cursor cursor) { - if (TextUtils.isEmpty(account)) { - throw new IllegalArgumentException("account is empty"); - } - return new ConversationCursor(this, account, cursor); - } - - /** - * Creates an array of SelectionArguments suitable for passing to the provider's query. - * Currently this only handles one flag, but it could be expanded in the future. - */ - private static String[] getSelectionArguments( - BecomeActiveNetworkCursor becomeActiveNetworkCursor) { - if (BecomeActiveNetworkCursor.NO == becomeActiveNetworkCursor) { - return new String[] {SelectionArguments.DO_NOT_BECOME_ACTIVE_NETWORK_CURSOR}; - } else { - // Default behavior; no args required. - return null; - } - } - - /** - * Asynchronously gets a cursor over all conversations matching a query. The - * query is in Gmail's query syntax. When the operation is complete the handler's - * onQueryComplete() method is called with the resulting Cursor. - * - * @param account run the query on this account - * @param handler An AsyncQueryHanlder that will be used to run the query - * @param token The token to pass to startQuery, which will be passed back to onQueryComplete - * @param query a query in Gmail's query syntax - * @param becomeActiveNetworkCursor whether or not the returned - * cursor should become the Active Network Cursor - */ - public void runQueryForConversations(String account, AsyncQueryHandler handler, int token, - String query, BecomeActiveNetworkCursor becomeActiveNetworkCursor) { - if (TextUtils.isEmpty(account)) { - throw new IllegalArgumentException("account is empty"); - } - String[] selectionArgs = getSelectionArguments(becomeActiveNetworkCursor); - handler.startQuery(token, null, Uri.withAppendedPath(CONVERSATIONS_URI, account), - CONVERSATION_PROJECTION, query, selectionArgs, null); - } - - /** - * Synchronously gets a cursor over all conversations matching a query. The - * query is in Gmail's query syntax. - * - * @param account run the query on this account - * @param query a query in Gmail's query syntax - * @param becomeActiveNetworkCursor whether or not the returned - * cursor should become the Active Network Cursor - */ - public ConversationCursor getConversationCursorForQuery( - String account, String query, BecomeActiveNetworkCursor becomeActiveNetworkCursor) { - String[] selectionArgs = getSelectionArguments(becomeActiveNetworkCursor); - Cursor cursor = mContentResolver.query( - Uri.withAppendedPath(CONVERSATIONS_URI, account), CONVERSATION_PROJECTION, - query, selectionArgs, null); - return new ConversationCursor(this, account, cursor); - } - - /** - * Gets a message cursor over the single message with the given id. - * - * @param account get the cursor for messages in this account - * @param messageId the id of the message - * @return a cursor over the message - */ - public MessageCursor getMessageCursorForMessageId(String account, long messageId) { - if (TextUtils.isEmpty(account)) { - throw new IllegalArgumentException("account is empty"); - } - Uri uri = Uri.parse(AUTHORITY_PLUS_MESSAGES + account + "/" + messageId); - Cursor cursor = mContentResolver.query(uri, MESSAGE_PROJECTION, null, null, null); - return new MessageCursor(this, mContentResolver, account, cursor); - } - - /** - * Gets a message cursor over the messages that match the query. Note that - * this simply finds all of the messages that match and returns them. It - * does not return all messages in conversations where any message matches. - * - * @param account get the cursor for messages in this account - * @param query a query in GMail's query syntax. Currently only queries of - * the form [label:<label>] are supported - * @return a cursor over the messages - */ - public MessageCursor getLocalMessageCursorForQuery(String account, String query) { - if (TextUtils.isEmpty(account)) { - throw new IllegalArgumentException("account is empty"); - } - Uri uri = Uri.parse(AUTHORITY_PLUS_MESSAGES + account + "/"); - Cursor cursor = mContentResolver.query(uri, MESSAGE_PROJECTION, query, null, null); - return new MessageCursor(this, mContentResolver, account, cursor); - } - - /** - * Gets a cursor over all of the messages in a conversation. - * - * @param account get the cursor for messages in this account - * @param conversationId the id of the converstion to fetch messages for - * @return a cursor over messages in the conversation - */ - public MessageCursor getMessageCursorForConversationId(String account, long conversationId) { - if (TextUtils.isEmpty(account)) { - throw new IllegalArgumentException("account is empty"); - } - Uri uri = Uri.parse( - AUTHORITY_PLUS_CONVERSATIONS + account + "/" + conversationId + "/messages"); - Cursor cursor = mContentResolver.query( - uri, MESSAGE_PROJECTION, null, null, null); - return new MessageCursor(this, mContentResolver, account, cursor); - } - - /** - * Expunge the indicated message. One use of this is to discard drafts. - * - * @param account the account of the message id - * @param messageId the id of the message to expunge - */ - public void expungeMessage(String account, long messageId) { - if (TextUtils.isEmpty(account)) { - throw new IllegalArgumentException("account is empty"); - } - Uri uri = Uri.parse(AUTHORITY_PLUS_MESSAGES + account + "/" + messageId); - mContentResolver.delete(uri, null, null); - } - - /** - * Adds or removes the label on the conversation. - * - * @param account the account of the conversation - * @param conversationId the conversation - * @param maxServerMessageId the highest message id to whose labels should be changed. Note that - * everywhere else in this file messageId means local message id but here you need to use a - * server message id. - * @param label the label to add or remove - * @param add true to add the label, false to remove it - */ - public void addOrRemoveLabelOnConversation( - String account, long conversationId, long maxServerMessageId, String label, - boolean add) { - if (TextUtils.isEmpty(account)) { - throw new IllegalArgumentException("account is empty"); - } - if (add) { - Uri uri = Uri.parse( - AUTHORITY_PLUS_CONVERSATIONS + account + "/" + conversationId + "/labels"); - ContentValues values = new ContentValues(); - values.put(LabelColumns.CANONICAL_NAME, label); - values.put(ConversationColumns.MAX_MESSAGE_ID, maxServerMessageId); - mContentResolver.insert(uri, values); - } else { - String encodedLabel; - try { - encodedLabel = URLEncoder.encode(label, "utf-8"); - } catch (UnsupportedEncodingException e) { - throw new RuntimeException(e); - } - Uri uri = Uri.parse( - AUTHORITY_PLUS_CONVERSATIONS + account + "/" - + conversationId + "/labels/" + encodedLabel); - mContentResolver.delete( - uri, ConversationColumns.MAX_MESSAGE_ID, new String[]{"" + maxServerMessageId}); - } - } - - /** - * Adds or removes the label on the message. - * - * @param contentResolver the content resolver. - * @param account the account of the message - * @param conversationId the conversation containing the message - * @param messageId the id of the message to whose labels should be changed - * @param label the label to add or remove - * @param add true to add the label, false to remove it - */ - public static void addOrRemoveLabelOnMessage(ContentResolver contentResolver, String account, - long conversationId, long messageId, String label, boolean add) { - - // conversationId is unused but we want to start passing it whereever we pass a message id. - if (add) { - Uri uri = Uri.parse( - AUTHORITY_PLUS_MESSAGES + account + "/" + messageId + "/labels"); - ContentValues values = new ContentValues(); - values.put(LabelColumns.CANONICAL_NAME, label); - contentResolver.insert(uri, values); - } else { - String encodedLabel; - try { - encodedLabel = URLEncoder.encode(label, "utf-8"); - } catch (UnsupportedEncodingException e) { - throw new RuntimeException(e); - } - Uri uri = Uri.parse( - AUTHORITY_PLUS_MESSAGES + account + "/" + messageId - + "/labels/" + encodedLabel); - contentResolver.delete(uri, null, null); - } - } - - /** - * The mail provider will send an intent when certain changes happen in certain labels. - * Currently those labels are inbox and voicemail. - * - * <p>The intent will have the action ACTION_PROVIDER_CHANGED and the extras mentioned below. - * The data for the intent will be content://gmail-ls/unread/<name of label>. - * - * <p>The goal is to support the following user experience:<ul> - * <li>When present the new mail indicator reports the number of unread conversations in the - * inbox (or some other label).</li> - * <li>When the user views the inbox the indicator is removed immediately. They do not have to - * read all of the conversations.</li> - * <li>If more mail arrives the indicator reappears and shows the total number of unread - * conversations in the inbox.</li> - * <li>If the user reads the new conversations on the web the indicator disappears on the - * phone since there is no unread mail in the inbox that the user hasn't seen.</li> - * <li>The phone should vibrate/etc when it transitions from having no unseen unread inbox - * mail to having some.</li> - */ - - /** The account in which the change occurred. */ - static public final String PROVIDER_CHANGED_EXTRA_ACCOUNT = "account"; - - /** The number of unread conversations matching the label. */ - static public final String PROVIDER_CHANGED_EXTRA_COUNT = "count"; - - /** Whether to get the user's attention, perhaps by vibrating. */ - static public final String PROVIDER_CHANGED_EXTRA_GET_ATTENTION = "getAttention"; - - /** - * A label that is attached to all of the conversations being notified about. This enables the - * receiver of a notification to get a list of matching conversations. - */ - static public final String PROVIDER_CHANGED_EXTRA_TAG_LABEL = "tagLabel"; - - /** - * Settings for which conversations should be synced to the phone. - * Conversations are synced if any message matches any of the following - * criteria: - * - * <ul> - * <li>the message has a label in the include set</li> - * <li>the message is no older than conversationAgeDays and has a label in the partial set. - * </li> - * <li>also, pending changes on the server: the message has no user-controllable labels.</li> - * </ul> - * - * <p>A user-controllable label is a user-defined label or star, inbox, - * trash, spam, etc. LABEL_UNREAD is not considered user-controllable. - */ - public static class Settings { - public long conversationAgeDays; - public long maxAttachmentSizeMb; - public String[] labelsIncluded; - public String[] labelsPartial; - } - - /** - * Returns the settings. - * @param account the account whose setting should be retrieved - */ - public Settings getSettings(String account) { - if (TextUtils.isEmpty(account)) { - throw new IllegalArgumentException("account is empty"); - } - Settings settings = new Settings(); - Cursor cursor = mContentResolver.query( - Uri.withAppendedPath(SETTINGS_URI, account), SETTINGS_PROJECTION, null, null, null); - cursor.moveToNext(); - settings.labelsIncluded = TextUtils.split(cursor.getString(0), SPACE_SEPARATOR_PATTERN); - settings.labelsPartial = TextUtils.split(cursor.getString(1), SPACE_SEPARATOR_PATTERN); - settings.conversationAgeDays = Long.parseLong(cursor.getString(2)); - settings.maxAttachmentSizeMb = Long.parseLong(cursor.getString(3)); - cursor.close(); - return settings; - } - - /** - * Sets the settings. A sync will be scheduled automatically. - */ - public void setSettings(String account, Settings settings) { - if (TextUtils.isEmpty(account)) { - throw new IllegalArgumentException("account is empty"); - } - ContentValues values = new ContentValues(); - values.put( - SettingsColumns.LABELS_INCLUDED, - TextUtils.join(" ", settings.labelsIncluded)); - values.put( - SettingsColumns.LABELS_PARTIAL, - TextUtils.join(" ", settings.labelsPartial)); - values.put( - SettingsColumns.CONVERSATION_AGE_DAYS, - settings.conversationAgeDays); - values.put( - SettingsColumns.MAX_ATTACHMENET_SIZE_MB, - settings.maxAttachmentSizeMb); - mContentResolver.update(Uri.withAppendedPath(SETTINGS_URI, account), values, null, null); - } - - /** - * Uses sender instructions to build a formatted string. - * - * <p>Sender list instructions contain compact information about the sender list. Most work that - * can be done without knowing how much room will be availble for the sender list is done when - * creating the instructions. - * - * <p>The instructions string consists of tokens separated by SENDER_LIST_SEPARATOR. Here are - * the tokens, one per line:<ul> - * <li><tt>n</tt></li> - * <li><em>int</em>, the number of non-draft messages in the conversation</li> - * <li><tt>d</tt</li> - * <li><em>int</em>, the number of drafts in the conversation</li> - * <li><tt>l</tt></li> - * <li><em>literal html to be included in the output</em></li> - * <li><tt>s</tt> indicates that the message is sending (in the outbox without errors)</li> - * <li><tt>f</tt> indicates that the message failed to send (in the outbox with errors)</li> - * <li><em>for each message</em><ul> - * <li><em>int</em>, 0 for read, 1 for unread</li> - * <li><em>int</em>, the priority of the message. Zero is the most important</li> - * <li><em>text</em>, the sender text or blank for messages from 'me'</li> - * </ul></li> - * <li><tt>e</tt> to indicate that one or more messages have been elided</li> - * - * <p>The instructions indicate how many messages and drafts are in the conversation and then - * describe the most important messages in order, indicating the priority of each message and - * whether the message is unread. - * - * @param instructions instructions as described above - * @param sb the SpannableStringBuilder to append to - * @param maxChars the number of characters available to display the text - * @param unreadStyle the CharacterStyle for unread messages, or null - * @param draftsStyle the CharacterStyle for draft messages, or null - * @param sendingString the string to use when there are messages scheduled to be sent - * @param sendFailedString the string to use when there are messages that mailed to send - * @param meString the string to use for messages sent by this user - * @param draftString the string to use for "Draft" - * @param draftPluralString the string to use for "Drafts" - */ - public static void getSenderSnippet( - String instructions, SpannableStringBuilder sb, int maxChars, - CharacterStyle unreadStyle, - CharacterStyle draftsStyle, - CharSequence meString, CharSequence draftString, CharSequence draftPluralString, - CharSequence sendingString, CharSequence sendFailedString, - boolean forceAllUnread, boolean forceAllRead) { - assert !(forceAllUnread && forceAllRead); - boolean unreadStatusIsForced = forceAllUnread || forceAllRead; - boolean forcedUnreadStatus = forceAllUnread; - - // Measure each fragment. It's ok to iterate over the entire set of fragments because it is - // never a long list, even if there are many senders. - final Map<Integer, Integer> priorityToLength = sPriorityToLength; - priorityToLength.clear(); - - int maxFoundPriority = Integer.MIN_VALUE; - int numMessages = 0; - int numDrafts = 0; - CharSequence draftsFragment = ""; - CharSequence sendingFragment = ""; - CharSequence sendFailedFragment = ""; - - sSenderListSplitter.setString(instructions); - int numFragments = 0; - String[] fragments = sSenderFragments; - int currentSize = fragments.length; - while (sSenderListSplitter.hasNext()) { - fragments[numFragments++] = sSenderListSplitter.next(); - if (numFragments == currentSize) { - sSenderFragments = new String[2 * currentSize]; - System.arraycopy(fragments, 0, sSenderFragments, 0, currentSize); - currentSize *= 2; - fragments = sSenderFragments; - } - } - - for (int i = 0; i < numFragments;) { - String fragment0 = fragments[i++]; - if ("".equals(fragment0)) { - // This should be the final fragment. - } else if (Gmail.SENDER_LIST_TOKEN_ELIDED.equals(fragment0)) { - // ignore - } else if (Gmail.SENDER_LIST_TOKEN_NUM_MESSAGES.equals(fragment0)) { - numMessages = Integer.valueOf(fragments[i++]); - } else if (Gmail.SENDER_LIST_TOKEN_NUM_DRAFTS.equals(fragment0)) { - String numDraftsString = fragments[i++]; - numDrafts = Integer.parseInt(numDraftsString); - draftsFragment = numDrafts == 1 ? draftString : - draftPluralString + " (" + numDraftsString + ")"; - } else if (Gmail.SENDER_LIST_TOKEN_LITERAL.equals(fragment0)) { - sb.append(Html.fromHtml(fragments[i++])); - return; - } else if (Gmail.SENDER_LIST_TOKEN_SENDING.equals(fragment0)) { - sendingFragment = sendingString; - } else if (Gmail.SENDER_LIST_TOKEN_SEND_FAILED.equals(fragment0)) { - sendFailedFragment = sendFailedString; - } else { - String priorityString = fragments[i++]; - CharSequence nameString = fragments[i++]; - if (nameString.length() == 0) nameString = meString; - int priority = Integer.parseInt(priorityString); - priorityToLength.put(priority, nameString.length()); - maxFoundPriority = Math.max(maxFoundPriority, priority); - } - } - String numMessagesFragment = - (numMessages != 0) ? " (" + Integer.toString(numMessages + numDrafts) + ")" : ""; - - // Don't allocate fixedFragment unless we need it - SpannableStringBuilder fixedFragment = null; - int fixedFragmentLength = 0; - if (draftsFragment.length() != 0) { - if (fixedFragment == null) { - fixedFragment = new SpannableStringBuilder(); - } - fixedFragment.append(draftsFragment); - if (draftsStyle != null) { - fixedFragment.setSpan( - CharacterStyle.wrap(draftsStyle), - 0, fixedFragment.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - } - } - if (sendingFragment.length() != 0) { - if (fixedFragment == null) { - fixedFragment = new SpannableStringBuilder(); - } - if (fixedFragment.length() != 0) fixedFragment.append(", "); - fixedFragment.append(sendingFragment); - } - if (sendFailedFragment.length() != 0) { - if (fixedFragment == null) { - fixedFragment = new SpannableStringBuilder(); - } - if (fixedFragment.length() != 0) fixedFragment.append(", "); - fixedFragment.append(sendFailedFragment); - } - - if (fixedFragment != null) { - fixedFragmentLength = fixedFragment.length(); - } - - final boolean normalMessagesExist = - numMessagesFragment.length() != 0 || maxFoundPriority != Integer.MIN_VALUE; - String preFixedFragement = ""; - if (normalMessagesExist && fixedFragmentLength != 0) { - preFixedFragement = ", "; - } - int maxPriorityToInclude = -1; // inclusive - int numCharsUsed = - numMessagesFragment.length() + preFixedFragement.length() + fixedFragmentLength; - int numSendersUsed = 0; - while (maxPriorityToInclude < maxFoundPriority) { - if (priorityToLength.containsKey(maxPriorityToInclude + 1)) { - int length = numCharsUsed + priorityToLength.get(maxPriorityToInclude + 1); - if (numCharsUsed > 0) length += 2; - // We must show at least two senders if they exist. If we don't have space for both - // then we will truncate names. - if (length > maxChars && numSendersUsed >= 2) { - break; - } - numCharsUsed = length; - numSendersUsed++; - } - maxPriorityToInclude++; - } - - int numCharsToRemovePerWord = 0; - if (numCharsUsed > maxChars) { - numCharsToRemovePerWord = (numCharsUsed - maxChars) / numSendersUsed; - } - - boolean elided = false; - for (int i = 0; i < numFragments;) { - String fragment0 = fragments[i++]; - if ("".equals(fragment0)) { - // This should be the final fragment. - } else if (SENDER_LIST_TOKEN_ELIDED.equals(fragment0)) { - elided = true; - } else if (SENDER_LIST_TOKEN_NUM_MESSAGES.equals(fragment0)) { - i++; - } else if (SENDER_LIST_TOKEN_NUM_DRAFTS.equals(fragment0)) { - i++; - } else if (SENDER_LIST_TOKEN_SENDING.equals(fragment0)) { - } else if (SENDER_LIST_TOKEN_SEND_FAILED.equals(fragment0)) { - } else { - final String unreadString = fragment0; - final String priorityString = fragments[i++]; - String nameString = fragments[i++]; - if (nameString.length() == 0) nameString = meString.toString(); - if (numCharsToRemovePerWord != 0) { - nameString = nameString.substring( - 0, Math.max(nameString.length() - numCharsToRemovePerWord, 0)); - } - final boolean unread = unreadStatusIsForced - ? forcedUnreadStatus : Integer.parseInt(unreadString) != 0; - final int priority = Integer.parseInt(priorityString); - if (priority <= maxPriorityToInclude) { - if (sb.length() != 0) { - sb.append(elided ? " .. " : ", "); - } - elided = false; - int pos = sb.length(); - sb.append(nameString); - if (unread && unreadStyle != null) { - sb.setSpan(CharacterStyle.wrap(unreadStyle), - pos, sb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - } - } else { - elided = true; - } - } - } - sb.append(numMessagesFragment); - if (fixedFragmentLength != 0) { - sb.append(preFixedFragement); - sb.append(fixedFragment); - } - } - - /** - * This is a cursor that only defines methods to move throught the results - * and register to hear about changes. All access to the data is left to - * subinterfaces. - */ - public static class MailCursor extends ContentObserver { - - // A list of observers of this cursor. - private Set<MailCursorObserver> mObservers; - - // Updated values are accumulated here before being written out if the - // cursor is asked to persist the changes. - private ContentValues mUpdateValues; - - protected Cursor mCursor; - protected String mAccount; - - public Cursor getCursor() { - return mCursor; - } - - /** - * Constructs the MailCursor given a regular cursor, registering as a - * change observer of the cursor. - * @param account the account the cursor is associated with - * @param cursor the underlying cursor - */ - protected MailCursor(String account, Cursor cursor) { - super(new Handler()); - mObservers = new HashSet<MailCursorObserver>(); - mCursor = cursor; - mAccount = account; - if (mCursor != null) mCursor.registerContentObserver(this); - } - - /** - * Gets the account associated with this cursor. - * @return the account. - */ - public String getAccount() { - return mAccount; - } - - protected void checkThread() { - // Turn this on when activity code no longer runs in the sync thread - // after notifications of changes. -// Thread currentThread = Thread.currentThread(); -// if (currentThread != mThread) { -// throw new RuntimeException("Accessed from the wrong thread"); -// } - } - - /** - * Lazily constructs a map of update values to apply to the database - * if requested. This map is cleared out when we move to a different - * item in the result set. - * - * @return a map of values to be applied by an update. - */ - protected ContentValues getUpdateValues() { - if (mUpdateValues == null) { - mUpdateValues = new ContentValues(); - } - return mUpdateValues; - } - - /** - * Called whenever mCursor is changed to point to a different row. - * Subclasses should override this if they need to clear out state - * when this happens. - * - * Subclasses must call the inherited version if they override this. - */ - protected void onCursorPositionChanged() { - mUpdateValues = null; - } - - // ********* MailCursor - - /** - * Returns the numbers of rows in the cursor. - * - * @return the number of rows in the cursor. - */ - final public int count() { - if (mCursor != null) { - return mCursor.getCount(); - } else { - return 0; - } - } - - /** - * @return the current position of this cursor, or -1 if this cursor - * has not been initialized. - */ - final public int position() { - if (mCursor != null) { - return mCursor.getPosition(); - } else { - return -1; - } - } - - /** - * Move the cursor to an absolute position. The valid - * range of vaues is -1 <= position <= count. - * - * <p>This method will return true if the request destination was - * reachable, otherwise it returns false. - * - * @param position the zero-based position to move to. - * @return whether the requested move fully succeeded. - */ - final public boolean moveTo(int position) { - checkCursor(); - checkThread(); - boolean moved = mCursor.moveToPosition(position); - if (moved) onCursorPositionChanged(); - return moved; - } - - /** - * Move the cursor to the next row. - * - * <p>This method will return false if the cursor is already past the - * last entry in the result set. - * - * @return whether the move succeeded. - */ - final public boolean next() { - checkCursor(); - checkThread(); - boolean moved = mCursor.moveToNext(); - if (moved) onCursorPositionChanged(); - return moved; - } - - /** - * Release all resources and locks associated with the cursor. The - * cursor will not be valid after this function is called. - */ - final public void release() { - if (mCursor != null) { - mCursor.unregisterContentObserver(this); - mCursor.deactivate(); - } - } - - final public void registerContentObserver(ContentObserver observer) { - mCursor.registerContentObserver(observer); - } - - final public void unregisterContentObserver(ContentObserver observer) { - mCursor.unregisterContentObserver(observer); - } - - final public void registerDataSetObserver(DataSetObserver observer) { - mCursor.registerDataSetObserver(observer); - } - - final public void unregisterDataSetObserver(DataSetObserver observer) { - mCursor.unregisterDataSetObserver(observer); - } - - /** - * Register an observer to hear about changes to the cursor. - * - * @param observer the observer to register - */ - final public void registerObserver(MailCursorObserver observer) { - mObservers.add(observer); - } - - /** - * Unregister an observer. - * - * @param observer the observer to unregister - */ - final public void unregisterObserver(MailCursorObserver observer) { - mObservers.remove(observer); - } - - // ********* ContentObserver - - @Override - final public boolean deliverSelfNotifications() { - return false; - } - - @Override - public void onChange(boolean selfChange) { - if (DEBUG) { - Log.d(TAG, "MailCursor is notifying " + mObservers.size() + " observers"); - } - for (MailCursorObserver o: mObservers) { - o.onCursorChanged(this); - } - } - - protected void checkCursor() { - if (mCursor == null) { - throw new IllegalStateException( - "cannot read from an insertion cursor"); - } - } - - /** - * Returns the string value of the column, or "" if the value is null. - */ - protected String getStringInColumn(int columnIndex) { - checkCursor(); - return toNonnullString(mCursor.getString(columnIndex)); - } - } - - /** - * A MailCursor observer is notified of changes to the result set of a - * cursor. - */ - public interface MailCursorObserver { - - /** - * Called when the result set of a cursor has changed. - * - * @param cursor the cursor whose result set has changed. - */ - void onCursorChanged(MailCursor cursor); - } - - /** - * A cursor over labels. - */ - public final class LabelCursor extends MailCursor { - - private int mNameIndex; - private int mNumConversationsIndex; - private int mNumUnreadConversationsIndex; - - private LabelCursor(String account, Cursor cursor) { - super(account, cursor); - - mNameIndex = mCursor.getColumnIndexOrThrow(LabelColumns.CANONICAL_NAME); - mNumConversationsIndex = - mCursor.getColumnIndexOrThrow(LabelColumns.NUM_CONVERSATIONS); - mNumUnreadConversationsIndex = mCursor.getColumnIndexOrThrow( - LabelColumns.NUM_UNREAD_CONVERSATIONS); - } - - /** - * Gets the canonical name of the current label. - * - * @return the current label's name. - */ - public String getName() { - return getStringInColumn(mNameIndex); - } - - /** - * Gets the number of conversations with this label. - * - * @return the number of conversations with this label. - */ - public int getNumConversations() { - return mCursor.getInt(mNumConversationsIndex); - } - - /** - * Gets the number of unread conversations with this label. - * - * @return the number of unread conversations with this label. - */ - public int getNumUnreadConversations() { - return mCursor.getInt(mNumUnreadConversationsIndex); - } - } - - /** - * This is a map of labels. TODO: make it observable. - */ - public static final class LabelMap extends Observable { - private final static ContentValues EMPTY_CONTENT_VALUES = new ContentValues(); - - private ContentQueryMap mQueryMap; - private SortedSet<String> mSortedUserLabels; - private Map<String, Long> mCanonicalNameToId; - - private long mLabelIdSent; - private long mLabelIdInbox; - private long mLabelIdDraft; - private long mLabelIdUnread; - private long mLabelIdTrash; - private long mLabelIdSpam; - private long mLabelIdStarred; - private long mLabelIdChat; - private long mLabelIdVoicemail; - private long mLabelIdIgnored; - private long mLabelIdVoicemailInbox; - private long mLabelIdCached; - private long mLabelIdOutbox; - - private boolean mLabelsSynced = false; - - public LabelMap(ContentResolver contentResolver, String account, boolean keepUpdated) { - if (TextUtils.isEmpty(account)) { - throw new IllegalArgumentException("account is empty"); - } - Cursor cursor = contentResolver.query( - Uri.withAppendedPath(LABELS_URI, account), LABEL_PROJECTION, null, null, null); - init(cursor, keepUpdated); - } - - public LabelMap(Cursor cursor, boolean keepUpdated) { - init(cursor, keepUpdated); - } - - private void init(Cursor cursor, boolean keepUpdated) { - mQueryMap = new ContentQueryMap(cursor, BaseColumns._ID, keepUpdated, null); - mSortedUserLabels = new TreeSet<String>(java.text.Collator.getInstance()); - mCanonicalNameToId = Maps.newHashMap(); - updateDataStructures(); - mQueryMap.addObserver(new Observer() { - public void update(Observable observable, Object data) { - updateDataStructures(); - setChanged(); - notifyObservers(); - } - }); - } - - /** - * @return whether at least some labels have been synced. - */ - public boolean labelsSynced() { - return mLabelsSynced; - } - - /** - * Updates the data structures that are maintained separately from mQueryMap after the query - * map has changed. - */ - private void updateDataStructures() { - mSortedUserLabels.clear(); - mCanonicalNameToId.clear(); - for (Map.Entry<String, ContentValues> row : mQueryMap.getRows().entrySet()) { - long labelId = Long.valueOf(row.getKey()); - String canonicalName = row.getValue().getAsString(LabelColumns.CANONICAL_NAME); - if (isLabelUserDefined(canonicalName)) { - mSortedUserLabels.add(canonicalName); - } - mCanonicalNameToId.put(canonicalName, labelId); - - if (LABEL_SENT.equals(canonicalName)) { - mLabelIdSent = labelId; - } else if (LABEL_INBOX.equals(canonicalName)) { - mLabelIdInbox = labelId; - } else if (LABEL_DRAFT.equals(canonicalName)) { - mLabelIdDraft = labelId; - } else if (LABEL_UNREAD.equals(canonicalName)) { - mLabelIdUnread = labelId; - } else if (LABEL_TRASH.equals(canonicalName)) { - mLabelIdTrash = labelId; - } else if (LABEL_SPAM.equals(canonicalName)) { - mLabelIdSpam = labelId; - } else if (LABEL_STARRED.equals(canonicalName)) { - mLabelIdStarred = labelId; - } else if (LABEL_CHAT.equals(canonicalName)) { - mLabelIdChat = labelId; - } else if (LABEL_IGNORED.equals(canonicalName)) { - mLabelIdIgnored = labelId; - } else if (LABEL_VOICEMAIL.equals(canonicalName)) { - mLabelIdVoicemail = labelId; - } else if (LABEL_VOICEMAIL_INBOX.equals(canonicalName)) { - mLabelIdVoicemailInbox = labelId; - } else if (LABEL_CACHED.equals(canonicalName)) { - mLabelIdCached = labelId; - } else if (LABEL_OUTBOX.equals(canonicalName)) { - mLabelIdOutbox = labelId; - } - mLabelsSynced = mLabelIdSent != 0 - && mLabelIdInbox != 0 - && mLabelIdDraft != 0 - && mLabelIdUnread != 0 - && mLabelIdTrash != 0 - && mLabelIdSpam != 0 - && mLabelIdStarred != 0 - && mLabelIdChat != 0 - && mLabelIdIgnored != 0 - && mLabelIdVoicemail != 0; - } - } - - public long getLabelIdSent() { - checkLabelsSynced(); - return mLabelIdSent; - } - - public long getLabelIdInbox() { - checkLabelsSynced(); - return mLabelIdInbox; - } - - public long getLabelIdDraft() { - checkLabelsSynced(); - return mLabelIdDraft; - } - - public long getLabelIdUnread() { - checkLabelsSynced(); - return mLabelIdUnread; - } - - public long getLabelIdTrash() { - checkLabelsSynced(); - return mLabelIdTrash; - } - - public long getLabelIdSpam() { - checkLabelsSynced(); - return mLabelIdSpam; - } - - public long getLabelIdStarred() { - checkLabelsSynced(); - return mLabelIdStarred; - } - - public long getLabelIdChat() { - checkLabelsSynced(); - return mLabelIdChat; - } - - public long getLabelIdIgnored() { - checkLabelsSynced(); - return mLabelIdIgnored; - } - - public long getLabelIdVoicemail() { - checkLabelsSynced(); - return mLabelIdVoicemail; - } - - public long getLabelIdVoicemailInbox() { - checkLabelsSynced(); - return mLabelIdVoicemailInbox; - } - - public long getLabelIdCached() { - checkLabelsSynced(); - return mLabelIdCached; - } - - public long getLabelIdOutbox() { - checkLabelsSynced(); - return mLabelIdOutbox; - } - - private void checkLabelsSynced() { - if (!labelsSynced()) { - throw new IllegalStateException("LabelMap not initalized"); - } - } - - /** Returns the list of user-defined labels in alphabetical order. */ - public SortedSet<String> getSortedUserLabels() { - return mSortedUserLabels; - } - - private static final List<String> SORTED_USER_MEANINGFUL_SYSTEM_LABELS = - Lists.newArrayList( - LABEL_INBOX, LABEL_STARRED, LABEL_CHAT, LABEL_SENT, - LABEL_OUTBOX, LABEL_DRAFT, LABEL_ALL, - LABEL_SPAM, LABEL_TRASH); - - - private static final Set<String> USER_MEANINGFUL_SYSTEM_LABELS_SET = - Sets.newHashSet( - SORTED_USER_MEANINGFUL_SYSTEM_LABELS.toArray( - new String[]{})); - - public static List<String> getSortedUserMeaningfulSystemLabels() { - return SORTED_USER_MEANINGFUL_SYSTEM_LABELS; - } - - public static Set<String> getUserMeaningfulSystemLabelsSet() { - return USER_MEANINGFUL_SYSTEM_LABELS_SET; - } - - /** - * If you are ever tempted to remove outbox or draft from this set make sure you have a - * way to stop draft and outbox messages from getting purged before they are sent to the - * server. - */ - private static final Set<String> FORCED_INCLUDED_LABELS = - Sets.newHashSet(LABEL_OUTBOX, LABEL_DRAFT); - - public static Set<String> getForcedIncludedLabels() { - return FORCED_INCLUDED_LABELS; - } - - private static final Set<String> FORCED_INCLUDED_OR_PARTIAL_LABELS = - Sets.newHashSet(LABEL_INBOX); - - public static Set<String> getForcedIncludedOrPartialLabels() { - return FORCED_INCLUDED_OR_PARTIAL_LABELS; - } - - private static final Set<String> FORCED_UNSYNCED_LABELS = - Sets.newHashSet(LABEL_ALL, LABEL_CHAT, LABEL_SPAM, LABEL_TRASH); - - public static Set<String> getForcedUnsyncedLabels() { - return FORCED_UNSYNCED_LABELS; - } - - /** - * Returns the number of conversation with a given label. - * @deprecated Use {@link #getLabelId} instead. - */ - @Deprecated - public int getNumConversations(String label) { - return getNumConversations(getLabelId(label)); - } - - /** Returns the number of conversation with a given label. */ - public int getNumConversations(long labelId) { - return getLabelIdValues(labelId).getAsInteger(LabelColumns.NUM_CONVERSATIONS); - } - - /** - * Returns the number of unread conversation with a given label. - * @deprecated Use {@link #getLabelId} instead. - */ - @Deprecated - public int getNumUnreadConversations(String label) { - return getNumUnreadConversations(getLabelId(label)); - } - - /** Returns the number of unread conversation with a given label. */ - public int getNumUnreadConversations(long labelId) { - Integer unreadConversations = - getLabelIdValues(labelId).getAsInteger(LabelColumns.NUM_UNREAD_CONVERSATIONS); - // There seems to be a race condition here that can get the label maps into a bad - // state and lose state on a particular label. - int result = 0; - if (unreadConversations != null) { - result = unreadConversations < 0 ? 0 : unreadConversations; - } - - return result; - } - - /** - * @return the canonical name for a label - */ - public String getCanonicalName(long labelId) { - return getLabelIdValues(labelId).getAsString(LabelColumns.CANONICAL_NAME); - } - - /** - * @return the human name for a label - */ - public String getName(long labelId) { - return getLabelIdValues(labelId).getAsString(LabelColumns.NAME); - } - - /** - * @return whether a given label is known - */ - public boolean hasLabel(long labelId) { - return mQueryMap.getRows().containsKey(Long.toString(labelId)); - } - - /** - * @return returns the id of a label given the canonical name - * @deprecated this is only needed because most of the UI uses label names instead of ids - */ - public long getLabelId(String canonicalName) { - if (mCanonicalNameToId.containsKey(canonicalName)) { - return mCanonicalNameToId.get(canonicalName); - } else { - throw new IllegalArgumentException("Unknown canonical name: " + canonicalName); - } - } - - private ContentValues getLabelIdValues(long labelId) { - final ContentValues values = mQueryMap.getValues(Long.toString(labelId)); - if (values != null) { - return values; - } else { - return EMPTY_CONTENT_VALUES; - } - } - - /** Force the map to requery. This should not be necessary outside tests. */ - public void requery() { - mQueryMap.requery(); - } - - public void close() { - mQueryMap.close(); - } - } - - private Map<String, Gmail.LabelMap> mLabelMaps = Maps.newHashMap(); - - public LabelMap getLabelMap(String account) { - Gmail.LabelMap labelMap = mLabelMaps.get(account); - if (labelMap == null) { - labelMap = new Gmail.LabelMap(mContentResolver, account, true /* keepUpdated */); - mLabelMaps.put(account, labelMap); - } - return labelMap; - } - - public enum PersonalLevel { - NOT_TO_ME(0), - TO_ME_AND_OTHERS(1), - ONLY_TO_ME(2); - - private int mLevel; - - PersonalLevel(int level) { - mLevel = level; - } - - public int toInt() { - return mLevel; - } - - public static PersonalLevel fromInt(int level) { - switch (level) { - case 0: return NOT_TO_ME; - case 1: return TO_ME_AND_OTHERS; - case 2: return ONLY_TO_ME; - default: - throw new IllegalArgumentException( - level + " is not a personal level"); - } - } - } - - /** - * Indicates a version of an attachment. - */ - public enum AttachmentRendition { - /** - * The full version of an attachment if it can be handled on the device, otherwise the - * preview. - */ - BEST, - - /** A smaller or simpler version of the attachment, such as a scaled-down image or an HTML - * version of a document. Not always available. - */ - SIMPLE, - } - - /** - * The columns that can be requested when querying an attachment's download URI. See - * getAttachmentDownloadUri. - */ - public static final class AttachmentColumns implements BaseColumns { - - /** Contains a STATUS value from {@link android.provider.Downloads} */ - public static final String STATUS = "status"; - - /** - * The name of the file to open (with ContentProvider.open). If this is empty then continue - * to use the attachment's URI. - * - * TODO: I'm not sure that we need this. See the note in CL 66853-p9. - */ - public static final String FILENAME = "filename"; - } - - /** - * We track where an attachment came from so that we know how to download it and include it - * in new messages. - */ - public enum AttachmentOrigin { - /** Extras are "<conversationId>-<messageId>-<partId>". */ - SERVER_ATTACHMENT, - /** Extras are "<path>". */ - LOCAL_FILE; - - private static final String SERVER_EXTRAS_SEPARATOR = "_"; - - public static String serverExtras( - long conversationId, long messageId, String partId) { - return conversationId + SERVER_EXTRAS_SEPARATOR - + messageId + SERVER_EXTRAS_SEPARATOR + partId; - } - - /** - * @param extras extras as returned by serverExtras - * @return an array of conversationId, messageId, partId (all as strings) - */ - public static String[] splitServerExtras(String extras) { - return TextUtils.split(extras, SERVER_EXTRAS_SEPARATOR); - } - - public static String localFileExtras(Uri path) { - return path.toString(); - } - } - - public static final class Attachment { - /** Identifies the attachment uniquely when combined wih a message id.*/ - public String partId; - - /** The intended filename of the attachment.*/ - public String name; - - /** The native content type.*/ - public String contentType; - - /** The size of the attachment in its native form.*/ - public int size; - - /** - * The content type of the simple version of the attachment. Blank if no simple version is - * available. - */ - public String simpleContentType; - - public AttachmentOrigin origin; - - public String originExtras; - - public String toJoinedString() { - return TextUtils.join( - "|", Lists.newArrayList(partId == null ? "" : partId, - name.replace("|", ""), contentType, - size, simpleContentType, - origin.toString(), originExtras)); - } - - public static Attachment parseJoinedString(String joinedString) { - String[] fragments = TextUtils.split(joinedString, "\\|"); - int i = 0; - Attachment attachment = new Attachment(); - attachment.partId = fragments[i++]; - if (TextUtils.isEmpty(attachment.partId)) { - attachment.partId = null; - } - attachment.name = fragments[i++]; - attachment.contentType = fragments[i++]; - attachment.size = Integer.parseInt(fragments[i++]); - attachment.simpleContentType = fragments[i++]; - attachment.origin = AttachmentOrigin.valueOf(fragments[i++]); - attachment.originExtras = fragments[i++]; - return attachment; - } - } - - /** - * Any given attachment can come in two different renditions (see - * {@link android.provider.Gmail.AttachmentRendition}) and can be saved to the sd card or to a - * cache. The gmail provider automatically syncs some attachments to the cache. Other - * attachments can be downloaded on demand. Attachments in the cache will be purged as needed to - * save space. Attachments on the SD card must be managed by the user or other software. - * - * @param account which account to use - * @param messageId the id of the mesage with the attachment - * @param attachment the attachment - * @param rendition the desired rendition - * @param saveToSd whether the attachment should be saved to (or loaded from) the sd card or - * @return the URI to ask the content provider to open in order to open an attachment. - */ - public static Uri getAttachmentUri( - String account, long messageId, Attachment attachment, - AttachmentRendition rendition, boolean saveToSd) { - if (TextUtils.isEmpty(account)) { - throw new IllegalArgumentException("account is empty"); - } - if (attachment.origin == AttachmentOrigin.LOCAL_FILE) { - return Uri.parse(attachment.originExtras); - } else { - return Uri.parse( - AUTHORITY_PLUS_MESSAGES).buildUpon() - .appendPath(account).appendPath(Long.toString(messageId)) - .appendPath("attachments").appendPath(attachment.partId) - .appendPath(rendition.toString()) - .appendPath(Boolean.toString(saveToSd)) - .build(); - } - } - - /** - * Return the URI to query in order to find out whether an attachment is downloaded. - * - * <p>Querying this will also start a download if necessary. The cursor returned by querying - * this URI can contain the columns in {@link android.provider.Gmail.AttachmentColumns}. - * - * <p>Deleting this URI will cancel the download if it was not started automatically by the - * provider. It will also remove bookkeeping for saveToSd downloads. - * - * @param attachmentUri the attachment URI as returned by getAttachmentUri. The URI's authority - * Gmail.AUTHORITY. If it is not then you should open the file directly. - */ - public static Uri getAttachmentDownloadUri(Uri attachmentUri) { - if (!"content".equals(attachmentUri.getScheme())) { - throw new IllegalArgumentException("Uri's scheme must be 'content': " + attachmentUri); - } - return attachmentUri.buildUpon().appendPath("download").build(); - } - - public enum CursorStatus { - LOADED, - LOADING, - ERROR, // A network error occurred. - } - - /** - * A cursor over messages. - */ - public static final class MessageCursor extends MailCursor { - - private LabelMap mLabelMap; - - private ContentResolver mContentResolver; - - /** - * Only valid if mCursor == null, in which case we are inserting a new - * message. - */ - long mInReplyToLocalMessageId; - boolean mPreserveAttachments; - - private int mIdIndex; - private int mConversationIdIndex; - private int mSubjectIndex; - private int mSnippetIndex; - private int mFromIndex; - private int mToIndex; - private int mCcIndex; - private int mBccIndex; - private int mReplyToIndex; - private int mDateSentMsIndex; - private int mDateReceivedMsIndex; - private int mListInfoIndex; - private int mPersonalLevelIndex; - private int mBodyIndex; - private int mBodyEmbedsExternalResourcesIndex; - private int mLabelIdsIndex; - private int mJoinedAttachmentInfosIndex; - private int mErrorIndex; - - private TextUtils.StringSplitter mLabelIdsSplitter = newMessageLabelIdsSplitter(); - - public MessageCursor(Gmail gmail, ContentResolver cr, String account, Cursor cursor) { - super(account, cursor); - mLabelMap = gmail.getLabelMap(account); - if (cursor == null) { - throw new IllegalArgumentException( - "null cursor passed to MessageCursor()"); - } - - mContentResolver = cr; - - mIdIndex = mCursor.getColumnIndexOrThrow(MessageColumns.ID); - mConversationIdIndex = - mCursor.getColumnIndexOrThrow(MessageColumns.CONVERSATION_ID); - mSubjectIndex = mCursor.getColumnIndexOrThrow(MessageColumns.SUBJECT); - mSnippetIndex = mCursor.getColumnIndexOrThrow(MessageColumns.SNIPPET); - mFromIndex = mCursor.getColumnIndexOrThrow(MessageColumns.FROM); - mToIndex = mCursor.getColumnIndexOrThrow(MessageColumns.TO); - mCcIndex = mCursor.getColumnIndexOrThrow(MessageColumns.CC); - mBccIndex = mCursor.getColumnIndexOrThrow(MessageColumns.BCC); - mReplyToIndex = mCursor.getColumnIndexOrThrow(MessageColumns.REPLY_TO); - mDateSentMsIndex = - mCursor.getColumnIndexOrThrow(MessageColumns.DATE_SENT_MS); - mDateReceivedMsIndex = - mCursor.getColumnIndexOrThrow(MessageColumns.DATE_RECEIVED_MS); - mListInfoIndex = mCursor.getColumnIndexOrThrow(MessageColumns.LIST_INFO); - mPersonalLevelIndex = - mCursor.getColumnIndexOrThrow(MessageColumns.PERSONAL_LEVEL); - mBodyIndex = mCursor.getColumnIndexOrThrow(MessageColumns.BODY); - mBodyEmbedsExternalResourcesIndex = - mCursor.getColumnIndexOrThrow(MessageColumns.EMBEDS_EXTERNAL_RESOURCES); - mLabelIdsIndex = mCursor.getColumnIndexOrThrow(MessageColumns.LABEL_IDS); - mJoinedAttachmentInfosIndex = - mCursor.getColumnIndexOrThrow(MessageColumns.JOINED_ATTACHMENT_INFOS); - mErrorIndex = mCursor.getColumnIndexOrThrow(MessageColumns.ERROR); - - mInReplyToLocalMessageId = 0; - mPreserveAttachments = false; - } - - protected MessageCursor(ContentResolver cr, String account, long inReplyToMessageId, - boolean preserveAttachments) { - super(account, null); - mContentResolver = cr; - mInReplyToLocalMessageId = inReplyToMessageId; - mPreserveAttachments = preserveAttachments; - } - - @Override - protected void onCursorPositionChanged() { - super.onCursorPositionChanged(); - } - - public CursorStatus getStatus() { - Bundle extras = mCursor.getExtras(); - String stringStatus = extras.getString(EXTRA_STATUS); - return CursorStatus.valueOf(stringStatus); - } - - /** Retry a network request after errors. */ - public void retry() { - Bundle input = new Bundle(); - input.putString(RESPOND_INPUT_COMMAND, COMMAND_RETRY); - Bundle output = mCursor.respond(input); - String response = output.getString(RESPOND_OUTPUT_COMMAND_RESPONSE); - assert COMMAND_RESPONSE_OK.equals(response); - } - - /** - * Gets the message id of the current message. Note that this is an - * immutable local message (not, for example, GMail's message id, which - * is immutable). - * - * @return the message's id - */ - public long getMessageId() { - checkCursor(); - return mCursor.getLong(mIdIndex); - } - - /** - * Gets the message's conversation id. This must be immutable. (For - * example, with GMail this should be the original conversation id - * rather than the default notion of converation id.) - * - * @return the message's conversation id - */ - public long getConversationId() { - checkCursor(); - return mCursor.getLong(mConversationIdIndex); - } - - /** - * Gets the message's subject. - * - * @return the message's subject - */ - public String getSubject() { - return getStringInColumn(mSubjectIndex); - } - - /** - * Gets the message's snippet (the short piece of the body). The snippet - * is generated from the body and cannot be set directly. - * - * @return the message's snippet - */ - public String getSnippet() { - return getStringInColumn(mSnippetIndex); - } - - /** - * Gets the message's from address. - * - * @return the message's from address - */ - public String getFromAddress() { - return getStringInColumn(mFromIndex); - } - - /** - * Returns the addresses for the key, if it has been updated, or index otherwise. - */ - private String[] getAddresses(String key, int index) { - ContentValues updated = getUpdateValues(); - String addresses; - if (updated.containsKey(key)) { - addresses = (String)getUpdateValues().get(key); - } else { - addresses = getStringInColumn(index); - } - - return TextUtils.split(addresses, EMAIL_SEPARATOR_PATTERN); - } - - /** - * Gets the message's to addresses. - * @return the message's to addresses - */ - public String[] getToAddresses() { - return getAddresses(MessageColumns.TO, mToIndex); - } - - /** - * Gets the message's cc addresses. - * @return the message's cc addresses - */ - public String[] getCcAddresses() { - return getAddresses(MessageColumns.CC, mCcIndex); - } - - /** - * Gets the message's bcc addresses. - * @return the message's bcc addresses - */ - public String[] getBccAddresses() { - return getAddresses(MessageColumns.BCC, mBccIndex); - } - - /** - * Gets the message's replyTo address. - * - * @return the message's replyTo address - */ - public String[] getReplyToAddress() { - return TextUtils.split(getStringInColumn(mReplyToIndex), EMAIL_SEPARATOR_PATTERN); - } - - public long getDateSentMs() { - checkCursor(); - return mCursor.getLong(mDateSentMsIndex); - } - - public long getDateReceivedMs() { - checkCursor(); - return mCursor.getLong(mDateReceivedMsIndex); - } - - public String getListInfo() { - return getStringInColumn(mListInfoIndex); - } - - public PersonalLevel getPersonalLevel() { - checkCursor(); - int personalLevelInt = mCursor.getInt(mPersonalLevelIndex); - return PersonalLevel.fromInt(personalLevelInt); - } - - /** - * @deprecated Always returns true. - */ - @Deprecated - public boolean getExpanded() { - return true; - } - - /** - * Gets the message's body. - * - * @return the message's body - */ - public String getBody() { - return getStringInColumn(mBodyIndex); - } - - /** - * @return whether the message's body contains embedded references to external resources. In - * that case the resources should only be displayed if the user explicitly asks for them to - * be - */ - public boolean getBodyEmbedsExternalResources() { - checkCursor(); - return mCursor.getInt(mBodyEmbedsExternalResourcesIndex) != 0; - } - - /** - * @return a copy of the set of label ids - */ - public Set<Long> getLabelIds() { - String labelNames = mCursor.getString(mLabelIdsIndex); - mLabelIdsSplitter.setString(labelNames); - return getLabelIdsFromLabelIdsString(mLabelIdsSplitter); - } - - /** - * @return a joined string of labels separated by spaces. - */ - public String getRawLabelIds() { - return mCursor.getString(mLabelIdsIndex); - } - - /** - * Adds a label to a message (if add is true) or removes it (if add is - * false). - * - * @param label the label to add or remove - * @param add whether to add or remove the label - */ - public void addOrRemoveLabel(String label, boolean add) { - addOrRemoveLabelOnMessage(mContentResolver, mAccount, getConversationId(), - getMessageId(), label, add); - } - - public ArrayList<Attachment> getAttachmentInfos() { - ArrayList<Attachment> attachments = Lists.newArrayList(); - - String joinedAttachmentInfos = mCursor.getString(mJoinedAttachmentInfosIndex); - if (joinedAttachmentInfos != null) { - for (String joinedAttachmentInfo : - TextUtils.split(joinedAttachmentInfos, ATTACHMENT_INFO_SEPARATOR_PATTERN)) { - - Attachment attachment = Attachment.parseJoinedString(joinedAttachmentInfo); - attachments.add(attachment); - } - } - return attachments; - } - - /** - * @return the error text for the message. Error text gets set if the server rejects a - * message that we try to save or send. If there is error text then the message is no longer - * scheduled to be saved or sent. Calling save() or send() will clear any error as well as - * scheduling another atempt to save or send the message. - */ - public String getErrorText() { - return mCursor.getString(mErrorIndex); - } - } - - /** - * A helper class for creating or updating messags. Use the putXxx methods to provide initial or - * new values for the message. Then save or send the message. To save or send an existing - * message without making other changes to it simply provide an emty ContentValues. - */ - public static class MessageModification { - - /** - * Sets the message's subject. Only valid for drafts. - * - * @param values the ContentValues that will be used to create or update the message - * @param subject the new subject - */ - public static void putSubject(ContentValues values, String subject) { - values.put(MessageColumns.SUBJECT, subject); - } - - /** - * Sets the message's to address. Only valid for drafts. - * - * @param values the ContentValues that will be used to create or update the message - * @param toAddresses the new to addresses - */ - public static void putToAddresses(ContentValues values, String[] toAddresses) { - values.put(MessageColumns.TO, TextUtils.join(EMAIL_SEPARATOR, toAddresses)); - } - - /** - * Sets the message's cc address. Only valid for drafts. - * - * @param values the ContentValues that will be used to create or update the message - * @param ccAddresses the new cc addresses - */ - public static void putCcAddresses(ContentValues values, String[] ccAddresses) { - values.put(MessageColumns.CC, TextUtils.join(EMAIL_SEPARATOR, ccAddresses)); - } - - /** - * Sets the message's bcc address. Only valid for drafts. - * - * @param values the ContentValues that will be used to create or update the message - * @param bccAddresses the new bcc addresses - */ - public static void putBccAddresses(ContentValues values, String[] bccAddresses) { - values.put(MessageColumns.BCC, TextUtils.join(EMAIL_SEPARATOR, bccAddresses)); - } - - /** - * Saves a new body for the message. Only valid for drafts. - * - * @param values the ContentValues that will be used to create or update the message - * @param body the new body of the message - */ - public static void putBody(ContentValues values, String body) { - values.put(MessageColumns.BODY, body); - } - - /** - * Sets the attachments on a message. Only valid for drafts. - * - * @param values the ContentValues that will be used to create or update the message - * @param attachments - */ - public static void putAttachments(ContentValues values, List<Attachment> attachments) { - values.put( - MessageColumns.JOINED_ATTACHMENT_INFOS, joinedAttachmentsString(attachments)); - } - - /** - * Create a new message and save it as a draft or send it. - * - * @param contentResolver the content resolver to use - * @param account the account to use - * @param values the values for the new message - * @param refMessageId the message that is being replied to or forwarded - * @param save whether to save or send the message - * @return the id of the new message - */ - public static long sendOrSaveNewMessage( - ContentResolver contentResolver, String account, - ContentValues values, long refMessageId, boolean save) { - values.put(MessageColumns.FAKE_SAVE, save); - values.put(MessageColumns.FAKE_REF_MESSAGE_ID, refMessageId); - Uri uri = Uri.parse(AUTHORITY_PLUS_MESSAGES + account + "/"); - Uri result = contentResolver.insert(uri, values); - return ContentUris.parseId(result); - } - - /** - * Update an existing draft and save it as a new draft or send it. - * - * @param contentResolver the content resolver to use - * @param account the account to use - * @param messageId the id of the message to update - * @param updateValues the values to change. Unspecified fields will not be altered - * @param save whether to resave the message as a draft or send it - */ - public static void sendOrSaveExistingMessage( - ContentResolver contentResolver, String account, long messageId, - ContentValues updateValues, boolean save) { - updateValues.put(MessageColumns.FAKE_SAVE, save); - updateValues.put(MessageColumns.FAKE_REF_MESSAGE_ID, 0); - Uri uri = Uri.parse( - AUTHORITY_PLUS_MESSAGES + account + "/" + messageId); - contentResolver.update(uri, updateValues, null, null); - } - - /** - * The string produced here is parsed by Gmail.MessageCursor#getAttachmentInfos. - */ - public static String joinedAttachmentsString(List<Gmail.Attachment> attachments) { - StringBuilder attachmentsSb = new StringBuilder(); - for (Gmail.Attachment attachment : attachments) { - if (attachmentsSb.length() != 0) { - attachmentsSb.append(Gmail.ATTACHMENT_INFO_SEPARATOR); - } - attachmentsSb.append(attachment.toJoinedString()); - } - return attachmentsSb.toString(); - } - - } - - /** - * A cursor over conversations. - * - * "Conversation" refers to the information needed to populate a list of - * conversations, not all of the messages in a conversation. - */ - public static final class ConversationCursor extends MailCursor { - - private LabelMap mLabelMap; - - private int mConversationIdIndex; - private int mSubjectIndex; - private int mSnippetIndex; - private int mFromIndex; - private int mDateIndex; - private int mPersonalLevelIndex; - private int mLabelIdsIndex; - private int mNumMessagesIndex; - private int mMaxMessageIdIndex; - private int mHasAttachmentsIndex; - private int mHasMessagesWithErrorsIndex; - private int mForceAllUnreadIndex; - - private TextUtils.StringSplitter mLabelIdsSplitter = newConversationLabelIdsSplitter(); - - private ConversationCursor(Gmail gmail, String account, Cursor cursor) { - super(account, cursor); - mLabelMap = gmail.getLabelMap(account); - - mConversationIdIndex = - mCursor.getColumnIndexOrThrow(ConversationColumns.ID); - mSubjectIndex = mCursor.getColumnIndexOrThrow(ConversationColumns.SUBJECT); - mSnippetIndex = mCursor.getColumnIndexOrThrow(ConversationColumns.SNIPPET); - mFromIndex = mCursor.getColumnIndexOrThrow(ConversationColumns.FROM); - mDateIndex = mCursor.getColumnIndexOrThrow(ConversationColumns.DATE); - mPersonalLevelIndex = - mCursor.getColumnIndexOrThrow(ConversationColumns.PERSONAL_LEVEL); - mLabelIdsIndex = - mCursor.getColumnIndexOrThrow(ConversationColumns.LABEL_IDS); - mNumMessagesIndex = mCursor.getColumnIndexOrThrow(ConversationColumns.NUM_MESSAGES); - mMaxMessageIdIndex = mCursor.getColumnIndexOrThrow(ConversationColumns.MAX_MESSAGE_ID); - mHasAttachmentsIndex = - mCursor.getColumnIndexOrThrow(ConversationColumns.HAS_ATTACHMENTS); - mHasMessagesWithErrorsIndex = - mCursor.getColumnIndexOrThrow(ConversationColumns.HAS_MESSAGES_WITH_ERRORS); - mForceAllUnreadIndex = - mCursor.getColumnIndexOrThrow(ConversationColumns.FORCE_ALL_UNREAD); - } - - @Override - protected void onCursorPositionChanged() { - super.onCursorPositionChanged(); - } - - public CursorStatus getStatus() { - Bundle extras = mCursor.getExtras(); - String stringStatus = extras.getString(EXTRA_STATUS); - return CursorStatus.valueOf(stringStatus); - } - - /** Retry a network request after errors. */ - public void retry() { - Bundle input = new Bundle(); - input.putString(RESPOND_INPUT_COMMAND, COMMAND_RETRY); - Bundle output = mCursor.respond(input); - String response = output.getString(RESPOND_OUTPUT_COMMAND_RESPONSE); - assert COMMAND_RESPONSE_OK.equals(response); - } - - /** - * When a conversation cursor is created it becomes the active network cursor, which means - * that it will fetch results from the network if it needs to in order to show all mail that - * matches its query. If you later want to requery an older cursor and would like that - * cursor to be the active cursor you need to call this method before requerying. - */ - public void becomeActiveNetworkCursor() { - Bundle input = new Bundle(); - input.putString(RESPOND_INPUT_COMMAND, COMMAND_ACTIVATE); - Bundle output = mCursor.respond(input); - String response = output.getString(RESPOND_OUTPUT_COMMAND_RESPONSE); - assert COMMAND_RESPONSE_OK.equals(response); - } - - /** - * Tells the cursor whether its contents are visible to the user. The cursor will - * automatically broadcast intents to remove any matching new-mail notifications when this - * cursor's results become visible and, if they are visible, when the cursor is requeried. - * - * Note that contents shown in an activity that is resumed but not focused - * (onWindowFocusChanged/hasWindowFocus) then results shown in that activity do not count - * as visible. (This happens when the activity is behind the lock screen or a dialog.) - * - * @param visible whether the contents of this cursor are visible to the user. - */ - public void setContentsVisibleToUser(boolean visible) { - Bundle input = new Bundle(); - input.putString(RESPOND_INPUT_COMMAND, COMMAND_SET_VISIBLE); - input.putBoolean(SET_VISIBLE_PARAM_VISIBLE, visible); - Bundle output = mCursor.respond(input); - String response = output.getString(RESPOND_OUTPUT_COMMAND_RESPONSE); - assert COMMAND_RESPONSE_OK.equals(response); - } - - /** - * Gets the conversation id. This is immutable. (The server calls it the original - * conversation id.) - * - * @return the conversation id - */ - public long getConversationId() { - return mCursor.getLong(mConversationIdIndex); - } - - /** - * Returns the instructions for building from snippets. Pass this to getFromSnippetHtml - * in order to actually build the snippets. - * @return snippet instructions for use by getFromSnippetHtml() - */ - public String getFromSnippetInstructions() { - return getStringInColumn(mFromIndex); - } - - /** - * Gets the conversation's subject. - * - * @return the subject - */ - public String getSubject() { - return getStringInColumn(mSubjectIndex); - } - - /** - * Gets the conversation's snippet. - * - * @return the snippet - */ - public String getSnippet() { - return getStringInColumn(mSnippetIndex); - } - - /** - * Get's the conversation's personal level. - * - * @return the personal level. - */ - public PersonalLevel getPersonalLevel() { - int personalLevelInt = mCursor.getInt(mPersonalLevelIndex); - return PersonalLevel.fromInt(personalLevelInt); - } - - /** - * @return a copy of the set of labels. To add or remove labels call - * MessageCursor.addOrRemoveLabel on each message in the conversation. - * @deprecated use getLabelIds - */ - public Set<String> getLabels() { - return getLabels(getRawLabelIds(), mLabelMap); - } - - /** - * @return a copy of the set of labels. To add or remove labels call - * MessageCursor.addOrRemoveLabel on each message in the conversation. - */ - public Set<Long> getLabelIds() { - mLabelIdsSplitter.setString(getRawLabelIds()); - return getLabelIdsFromLabelIdsString(mLabelIdsSplitter); - } - - /** - * Returns the set of labels using the raw labels from a previous getRawLabels() - * as input. - * @return a copy of the set of labels. To add or remove labels call - * MessageCursor.addOrRemoveLabel on each message in the conversation. - */ - public Set<String> getLabels(String rawLabelIds, LabelMap labelMap) { - mLabelIdsSplitter.setString(rawLabelIds); - return getCanonicalNamesFromLabelIdsString(labelMap, mLabelIdsSplitter); - } - - /** - * @return a joined string of labels separated by spaces. Use - * getLabels(rawLabels) to convert this to a Set of labels. - */ - public String getRawLabelIds() { - return mCursor.getString(mLabelIdsIndex); - } - - /** - * @return the number of messages in the conversation - */ - public int getNumMessages() { - return mCursor.getInt(mNumMessagesIndex); - } - - /** - * @return the max message id in the conversation - */ - public long getMaxServerMessageId() { - return mCursor.getLong(mMaxMessageIdIndex); - } - - public long getDateMs() { - return mCursor.getLong(mDateIndex); - } - - public boolean hasAttachments() { - return mCursor.getInt(mHasAttachmentsIndex) != 0; - } - - public boolean hasMessagesWithErrors() { - return mCursor.getInt(mHasMessagesWithErrorsIndex) != 0; - } - - public boolean getForceAllUnread() { - return !mCursor.isNull(mForceAllUnreadIndex) - && mCursor.getInt(mForceAllUnreadIndex) != 0; - } - } -} diff --git a/core/java/android/provider/Im.java b/core/java/android/provider/Im.java deleted file mode 100644 index 025d5c2..0000000 --- a/core/java/android/provider/Im.java +++ /dev/null @@ -1,2352 +0,0 @@ -/* - * Copyright (C) 2007 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.provider; - -import android.content.ContentQueryMap; -import android.content.ContentResolver; -import android.content.ContentUris; -import android.content.ContentValues; -import android.database.Cursor; -import android.net.Uri; -import android.os.Handler; - -import java.util.HashMap; - -/** - * The GTalk provider stores all information about roster contacts, chat messages, presence, etc. - * - * @hide - */ -public class Im { - /** - * no public constructor since this is a utility class - */ - private Im() {} - - /** - * The Columns for IM providers - */ - public interface ProviderColumns { - /** - * The name of the IM provider - * <P>Type: TEXT</P> - */ - String NAME = "name"; - - /** - * The full name of the provider - * <P>Type: TEXT</P> - */ - String FULLNAME = "fullname"; - - /** - * The category for the provider, used to form intent. - * <P>Type: TEXT</P> - */ - String CATEGORY = "category"; - - /** - * The url users should visit to create a new account for this provider - * <P>Type: TEXT</P> - */ - String SIGNUP_URL = "signup_url"; - } - - /** - * Known names corresponding to the {@link ProviderColumns#NAME} column - */ - public interface ProviderNames { - // - //NOTE: update Contacts.java with new providers when they're added. - // - String YAHOO = "Yahoo"; - String GTALK = "GTalk"; - String MSN = "MSN"; - String ICQ = "ICQ"; - String AIM = "AIM"; - String XMPP = "XMPP"; - String JABBER = "JABBER"; - String SKYPE = "SKYPE"; - String QQ = "QQ"; - } - - /** - * This table contains the IM providers - */ - public static final class Provider implements BaseColumns, ProviderColumns { - private Provider() {} - - public static final long getProviderIdForName(ContentResolver cr, String providerName) { - String[] selectionArgs = new String[1]; - selectionArgs[0] = providerName; - - Cursor cursor = cr.query(CONTENT_URI, - PROVIDER_PROJECTION, - NAME+"=?", - selectionArgs, null); - - long retVal = 0; - try { - if (cursor.moveToFirst()) { - retVal = cursor.getLong(cursor.getColumnIndexOrThrow(_ID)); - } - } finally { - cursor.close(); - } - - return retVal; - } - - public static final String getProviderNameForId(ContentResolver cr, long providerId) { - Cursor cursor = cr.query(CONTENT_URI, - PROVIDER_PROJECTION, - _ID + "=" + providerId, - null, null); - - String retVal = null; - try { - if (cursor.moveToFirst()) { - retVal = cursor.getString(cursor.getColumnIndexOrThrow(NAME)); - } - } finally { - cursor.close(); - } - - return retVal; - } - - private static final String[] PROVIDER_PROJECTION = new String[] { - _ID, - NAME - }; - - public static final String ACTIVE_ACCOUNT_ID = "account_id"; - public static final String ACTIVE_ACCOUNT_USERNAME = "account_username"; - public static final String ACTIVE_ACCOUNT_PW = "account_pw"; - public static final String ACTIVE_ACCOUNT_LOCKED = "account_locked"; - public static final String ACTIVE_ACCOUNT_KEEP_SIGNED_IN = "account_keepSignedIn"; - public static final String ACCOUNT_PRESENCE_STATUS = "account_presenceStatus"; - public static final String ACCOUNT_CONNECTION_STATUS = "account_connStatus"; - - /** - * The content:// style URL for this table - */ - public static final Uri CONTENT_URI = - Uri.parse("content://com.google.android.providers.talk/providers"); - - public static final Uri CONTENT_URI_WITH_ACCOUNT = - Uri.parse("content://com.google.android.providers.talk/providers/account"); - - /** - * The MIME type of {@link #CONTENT_URI} providing a directory of - * people. - */ - public static final String CONTENT_TYPE = - "vnd.android.cursor.dir/gtalk-providers"; - - public static final String CONTENT_ITEM_TYPE = - "vnd.android.cursor.item/gtalk-providers"; - - /** - * The default sort order for this table - */ - public static final String DEFAULT_SORT_ORDER = "name ASC"; - } - - /** - * The columns for IM accounts. There can be more than one account for each IM provider. - */ - public interface AccountColumns { - /** - * The name of the account - * <P>Type: TEXT</P> - */ - String NAME = "name"; - - /** - * The IM provider for this account - * <P>Type: INTEGER</P> - */ - String PROVIDER = "provider"; - - /** - * The username for this account - * <P>Type: TEXT</P> - */ - String USERNAME = "username"; - - /** - * The password for this account - * <P>Type: TEXT</P> - */ - String PASSWORD = "pw"; - - /** - * A boolean value indicates if the account is active. - * <P>Type: INTEGER</P> - */ - String ACTIVE = "active"; - - /** - * A boolean value indicates if the account is locked (not editable) - * <P>Type: INTEGER</P> - */ - String LOCKED = "locked"; - - /** - * A boolean value to indicate whether this account is kept signed in. - * <P>Type: INTEGER</P> - */ - String KEEP_SIGNED_IN = "keep_signed_in"; - - /** - * A boolean value indiciating the last login state for this account - * <P>Type: INTEGER</P> - */ - String LAST_LOGIN_STATE = "last_login_state"; - } - - /** - * This table contains the IM accounts. - */ - public static final class Account implements BaseColumns, AccountColumns { - private Account() {} - - public static final long getProviderIdForAccount(ContentResolver cr, long accountId) { - Cursor cursor = cr.query(CONTENT_URI, - PROVIDER_PROJECTION, - _ID + "=" + accountId, - null /* selection args */, - null /* sort order */); - - long providerId = 0; - - try { - if (cursor.moveToFirst()) { - providerId = cursor.getLong(PROVIDER_COLUMN); - } - } finally { - cursor.close(); - } - - return providerId; - } - - private static final String[] PROVIDER_PROJECTION = new String[] { PROVIDER }; - private static final int PROVIDER_COLUMN = 0; - - /** - * The content:// style URL for this table - */ - public static final Uri CONTENT_URI = - Uri.parse("content://com.google.android.providers.talk/accounts"); - - /** - * The MIME type of {@link #CONTENT_URI} providing a directory of - * account. - */ - public static final String CONTENT_TYPE = - "vnd.android.cursor.dir/gtalk-accounts"; - - /** - * The MIME type of a {@link #CONTENT_URI} subdirectory of a single - * account. - */ - public static final String CONTENT_ITEM_TYPE = - "vnd.android.cursor.item/gtalk-accounts"; - - /** - * The default sort order for this table - */ - public static final String DEFAULT_SORT_ORDER = "name ASC"; - - } - - /** - * Connection status - */ - public interface ConnectionStatus { - /** - * The connection is offline, not logged in. - */ - int OFFLINE = 0; - - /** - * The connection is attempting to connect. - */ - int CONNECTING = 1; - - /** - * The connection is suspended due to network not available. - */ - int SUSPENDED = 2; - - /** - * The connection is logged in and online. - */ - int ONLINE = 3; - } - - public interface AccountStatusColumns { - /** - * account id - * <P>Type: INTEGER</P> - */ - String ACCOUNT = "account"; - - /** - * User's presence status, see definitions in {#link CommonPresenceColumn} - * <P>Type: INTEGER</P> - */ - String PRESENCE_STATUS = "presenceStatus"; - - /** - * The connection status of this account, see {#link ConnectionStatus} - * <P>Type: INTEGER</P> - */ - String CONNECTION_STATUS = "connStatus"; - } - - public static final class AccountStatus implements BaseColumns, AccountStatusColumns { - /** - * The content:// style URL for this table - */ - public static final Uri CONTENT_URI = - Uri.parse("content://com.google.android.providers.talk/accountStatus"); - - /** - * The MIME type of {@link #CONTENT_URI} providing a directory of account status. - */ - public static final String CONTENT_TYPE = - "vnd.android.cursor.dir/gtalk-account-status"; - - /** - * The MIME type of a {@link #CONTENT_URI} subdirectory of a single account status. - */ - public static final String CONTENT_ITEM_TYPE = - "vnd.android.cursor.item/gtalk-account-status"; - - /** - * The default sort order for this table - */ - public static final String DEFAULT_SORT_ORDER = "name ASC"; - } - - /** - * Columns from the Contacts table. - */ - public interface ContactsColumns { - /** - * The username - * <P>Type: TEXT</P> - */ - String USERNAME = "username"; - - /** - * The nickname or display name - * <P>Type: TEXT</P> - */ - String NICKNAME = "nickname"; - - /** - * The IM provider for this contact - * <P>Type: INTEGER</P> - */ - String PROVIDER = "provider"; - - /** - * The account (within a IM provider) for this contact - * <P>Type: INTEGER</P> - */ - String ACCOUNT = "account"; - - /** - * The contactList this contact belongs to - * <P>Type: INTEGER</P> - */ - String CONTACTLIST = "contactList"; - - /** - * Contact type - * <P>Type: INTEGER</P> - */ - String TYPE = "type"; - - /** - * normal IM contact - */ - int TYPE_NORMAL = 0; - /** - * temporary contact, someone not in the list of contacts that we - * subscribe presence for. Usually created because of the user is - * having a chat session with this contact. - */ - int TYPE_TEMPORARY = 1; - /** - * temporary contact created for group chat. - */ - int TYPE_GROUP = 2; - /** - * blocked contact. - */ - int TYPE_BLOCKED = 3; - /** - * the contact is hidden. The client should always display this contact to the user. - */ - int TYPE_HIDDEN = 4; - /** - * the contact is pinned. The client should always display this contact to the user. - */ - int TYPE_PINNED = 5; - - /** - * Contact subscription status - * <P>Type: INTEGER</P> - */ - String SUBSCRIPTION_STATUS = "subscriptionStatus"; - - /** - * no pending subscription - */ - int SUBSCRIPTION_STATUS_NONE = 0; - /** - * requested to subscribe - */ - int SUBSCRIPTION_STATUS_SUBSCRIBE_PENDING = 1; - /** - * requested to unsubscribe - */ - int SUBSCRIPTION_STATUS_UNSUBSCRIBE_PENDING = 2; - - /** - * Contact subscription type - * <P>Type: INTEGER </P> - */ - String SUBSCRIPTION_TYPE = "subscriptionType"; - - /** - * The user and contact have no interest in each other's presence. - */ - int SUBSCRIPTION_TYPE_NONE = 0; - /** - * The user wishes to stop receiving presence updates from the contact. - */ - int SUBSCRIPTION_TYPE_REMOVE = 1; - /** - * The user is interested in receiving presence updates from the contact. - */ - int SUBSCRIPTION_TYPE_TO = 2; - /** - * The contact is interested in receiving presence updates from the user. - */ - int SUBSCRIPTION_TYPE_FROM = 3; - /** - * The user and contact have a mutual interest in each other's presence. - */ - int SUBSCRIPTION_TYPE_BOTH = 4; - /** - * This is a special type reserved for pending subscription requests - */ - int SUBSCRIPTION_TYPE_INVITATIONS = 5; - - /** - * Quick Contact: derived from Google Contact Extension's "message_count" attribute. - * <P>Type: INTEGER</P> - */ - String QUICK_CONTACT = "qc"; - - /** - * Google Contact Extension attribute - * - * Rejected: a boolean value indicating whether a subscription request from - * this client was ever rejected by the user. "true" indicates that it has. - * This is provided so that a client can block repeated subscription requests. - * <P>Type: INTEGER</P> - */ - String REJECTED = "rejected"; - - /** - * Off The Record status: 0 for disabled, 1 for enabled - * <P>Type: INTEGER </P> - */ - String OTR = "otr"; - } - - /** - * This defines the different type of values of {@link ContactsColumns#OTR} - */ - public interface OffTheRecordType { - /* - * Off the record not turned on - */ - int DISABLED = 0; - /** - * Off the record turned on, but we don't know who turned it on - */ - int ENABLED = 1; - /** - * Off the record turned on by the user - */ - int ENABLED_BY_USER = 2; - /** - * Off the record turned on by the buddy - */ - int ENABLED_BY_BUDDY = 3; - }; - - /** - * This table contains contacts. - */ - public static final class Contacts implements BaseColumns, - ContactsColumns, PresenceColumns, ChatsColumns { - /** - * no public constructor since this is a utility class - */ - private Contacts() {} - - /** - * The content:// style URL for this table - */ - public static final Uri CONTENT_URI = - Uri.parse("content://com.google.android.providers.talk/contacts"); - - /** - * The content:// style URL for contacts joined with presence - */ - public static final Uri CONTENT_URI_WITH_PRESENCE = - Uri.parse("content://com.google.android.providers.talk/contactsWithPresence"); - - /** - * The content:// style URL for barebone contacts, not joined with any other table - */ - public static final Uri CONTENT_URI_CONTACTS_BAREBONE = - Uri.parse("content://com.google.android.providers.talk/contactsBarebone"); - - /** - * The content:// style URL for contacts who have an open chat session - */ - public static final Uri CONTENT_URI_CHAT_CONTACTS = - Uri.parse("content://com.google.android.providers.talk/contacts_chatting"); - - /** - * The content:// style URL for contacts who have been blocked - */ - public static final Uri CONTENT_URI_BLOCKED_CONTACTS = - Uri.parse("content://com.google.android.providers.talk/contacts/blocked"); - - /** - * The content:// style URL for contacts by provider and account - */ - public static final Uri CONTENT_URI_CONTACTS_BY = - Uri.parse("content://com.google.android.providers.talk/contacts"); - - /** - * The content:// style URL for contacts by provider and account, - * and who have an open chat session - */ - public static final Uri CONTENT_URI_CHAT_CONTACTS_BY = - Uri.parse("content://com.google.android.providers.talk/contacts/chatting"); - - /** - * The content:// style URL for contacts by provider and account, - * and who are online - */ - public static final Uri CONTENT_URI_ONLINE_CONTACTS_BY = - Uri.parse("content://com.google.android.providers.talk/contacts/online"); - - /** - * The content:// style URL for contacts by provider and account, - * and who are offline - */ - public static final Uri CONTENT_URI_OFFLINE_CONTACTS_BY = - Uri.parse("content://com.google.android.providers.talk/contacts/offline"); - - /** - * The content:// style URL for operations on bulk contacts - */ - public static final Uri BULK_CONTENT_URI = - Uri.parse("content://com.google.android.providers.talk/bulk_contacts"); - - /** - * The content:// style URL for the count of online contacts in each - * contact list by provider and account. - */ - public static final Uri CONTENT_URI_ONLINE_COUNT = - Uri.parse("content://com.google.android.providers.talk/contacts/onlineCount"); - - /** - * The MIME type of {@link #CONTENT_URI} providing a directory of - * people. - */ - public static final String CONTENT_TYPE = "vnd.android.cursor.dir/gtalk-contacts"; - - /** - * The MIME type of a {@link #CONTENT_URI} subdirectory of a single - * person. - */ - public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/gtalk-contacts"; - - /** - * The default sort order for this table - */ - public static final String DEFAULT_SORT_ORDER = - "subscriptionType DESC, last_message_date DESC," + - " mode DESC, nickname COLLATE UNICODE ASC"; - - public static final String CHATS_CONTACT = "chats_contact"; - - public static final String AVATAR_HASH = "avatars_hash"; - - public static final String AVATAR_DATA = "avatars_data"; - } - - /** - * Columns from the ContactList table. - */ - public interface ContactListColumns { - String NAME = "name"; - String PROVIDER = "provider"; - String ACCOUNT = "account"; - } - - /** - * This table contains the contact lists. - */ - public static final class ContactList implements BaseColumns, - ContactListColumns { - private ContactList() {} - - /** - * The content:// style URL for this table - */ - public static final Uri CONTENT_URI = - Uri.parse("content://com.google.android.providers.talk/contactLists"); - - /** - * The MIME type of {@link #CONTENT_URI} providing a directory of - * people. - */ - public static final String CONTENT_TYPE = - "vnd.android.cursor.dir/gtalk-contactLists"; - - /** - * The MIME type of a {@link #CONTENT_URI} subdirectory of a single - * person. - */ - public static final String CONTENT_ITEM_TYPE = - "vnd.android.cursor.item/gtalk-contactLists"; - - /** - * The default sort order for this table - */ - public static final String DEFAULT_SORT_ORDER = "name COLLATE UNICODE ASC"; - - public static final String PROVIDER_NAME = "provider_name"; - - public static final String ACCOUNT_NAME = "account_name"; - } - - /** - * Columns from the BlockedList table. - */ - public interface BlockedListColumns { - /** - * The username of the blocked contact. - * <P>Type: TEXT</P> - */ - String USERNAME = "username"; - - /** - * The nickname of the blocked contact. - * <P>Type: TEXT</P> - */ - String NICKNAME = "nickname"; - - /** - * The provider id of the blocked contact. - * <P>Type: INT</P> - */ - String PROVIDER = "provider"; - - /** - * The account id of the blocked contact. - * <P>Type: INT</P> - */ - String ACCOUNT = "account"; - } - - /** - * This table contains blocked lists - */ - public static final class BlockedList implements BaseColumns, BlockedListColumns { - private BlockedList() {} - - /** - * The content:// style URL for this table - */ - public static final Uri CONTENT_URI = - Uri.parse("content://com.google.android.providers.talk/blockedList"); - - /** - * The MIME type of {@link #CONTENT_URI} providing a directory of - * people. - */ - public static final String CONTENT_TYPE = - "vnd.android.cursor.dir/gtalk-blockedList"; - - /** - * The MIME type of a {@link #CONTENT_URI} subdirectory of a single - * person. - */ - public static final String CONTENT_ITEM_TYPE = - "vnd.android.cursor.item/gtalk-blockedList"; - - /** - * The default sort order for this table - */ - public static final String DEFAULT_SORT_ORDER = "nickname ASC"; - - public static final String PROVIDER_NAME = "provider_name"; - - public static final String ACCOUNT_NAME = "account_name"; - - public static final String AVATAR_DATA = "avatars_data"; - } - - /** - * Columns from the contactsEtag table - */ - public interface ContactsEtagColumns { - /** - * The roster etag, computed by the server, stored on the client. There is one etag - * per account roster. - * <P>Type: TEXT</P> - */ - String ETAG = "etag"; - - /** - * The OTR etag, computed by the server, stored on the client. There is one OTR etag - * per account roster. - * <P>Type: TEXT</P> - */ - String OTR_ETAG = "otr_etag"; - - /** - * The account id for the etag. - * <P> Type: INTEGER </P> - */ - String ACCOUNT = "account"; - } - - public static final class ContactsEtag implements BaseColumns, ContactsEtagColumns { - private ContactsEtag() {} - - public static final Cursor query(ContentResolver cr, - String[] projection) { - return cr.query(CONTENT_URI, projection, null, null, null); - } - - public static final Cursor query(ContentResolver cr, - String[] projection, String where, String orderBy) { - return cr.query(CONTENT_URI, projection, where, - null, orderBy == null ? null : orderBy); - } - - public static final String getRosterEtag(ContentResolver resolver, long accountId) { - String retVal = null; - - Cursor c = resolver.query(CONTENT_URI, - CONTACT_ETAG_PROJECTION, - ACCOUNT + "=" + accountId, - null /* selection args */, - null /* sort order */); - - try { - if (c.moveToFirst()) { - retVal = c.getString(COLUMN_ETAG); - } - } finally { - c.close(); - } - - return retVal; - } - - public static final String getOtrEtag(ContentResolver resolver, long accountId) { - String retVal = null; - - Cursor c = resolver.query(CONTENT_URI, - CONTACT_OTR_ETAG_PROJECTION, - ACCOUNT + "=" + accountId, - null /* selection args */, - null /* sort order */); - - try { - if (c.moveToFirst()) { - retVal = c.getString(COLUMN_OTR_ETAG); - } - } finally { - c.close(); - } - - return retVal; - } - - private static final String[] CONTACT_ETAG_PROJECTION = new String[] { - Im.ContactsEtag.ETAG // 0 - }; - - private static int COLUMN_ETAG = 0; - - private static final String[] CONTACT_OTR_ETAG_PROJECTION = new String[] { - Im.ContactsEtag.OTR_ETAG // 0 - }; - - private static int COLUMN_OTR_ETAG = 0; - - /** - * The content:// style URL for this table - */ - public static final Uri CONTENT_URI = - Uri.parse("content://com.google.android.providers.talk/contactsEtag"); - - /** - * The MIME type of {@link #CONTENT_URI} providing a directory of - * people. - */ - public static final String CONTENT_TYPE = - "vnd.android.cursor.dir/gtalk-contactsEtag"; - - /** - * The MIME type of a {@link #CONTENT_URI} subdirectory of a single - * person. - */ - public static final String CONTENT_ITEM_TYPE = - "vnd.android.cursor.item/gtalk-contactsEtag"; - } - - /** - * Message type definition - */ - public interface MessageType { - /* sent message */ - int OUTGOING = 0; - /* received message */ - int INCOMING = 1; - /* presence became available */ - int PRESENCE_AVAILABLE = 2; - /* presence became away */ - int PRESENCE_AWAY = 3; - /* presence became DND (busy) */ - int PRESENCE_DND = 4; - /* presence became unavailable */ - int PRESENCE_UNAVAILABLE = 5; - /* the message is converted to a group chat */ - int CONVERT_TO_GROUPCHAT = 6; - /* generic status */ - int STATUS = 7; - /* the message cannot be sent now, but will be sent later */ - int POSTPONED = 8; - /* off The Record status is turned off */ - int OTR_IS_TURNED_OFF = 9; - /* off the record status is turned on */ - int OTR_IS_TURNED_ON = 10; - /* off the record status turned on by user */ - int OTR_TURNED_ON_BY_USER = 11; - /* off the record status turned on by buddy */ - int OTR_TURNED_ON_BY_BUDDY = 12; - } - - /** - * The common columns for messages table - */ - public interface MessageColumns { - /** - * The thread_id column stores the contact id of the contact the message belongs to. - * For groupchat messages, the thread_id stores the group id, which is the contact id - * of the temporary group contact created for the groupchat. So there should be no - * collision between groupchat message thread id and regular message thread id. - */ - String THREAD_ID = "thread_id"; - - /** - * The nickname. This is used for groupchat messages to indicate the participant's - * nickname. For non groupchat messages, this field should be left empty. - */ - String NICKNAME = "nickname"; - - /** - * The body - * <P>Type: TEXT</P> - */ - String BODY = "body"; - - /** - * The date this message is sent or received. This represents the display date for - * the message. - * <P>Type: INTEGER</P> - */ - String DATE = "date"; - - /** - * The real date for this message. While 'date' can be modified by the client - * to account for server time skew, the real_date is the original timestamp set - * by the server for incoming messages. - * <P>Type: INTEGER</P> - */ - String REAL_DATE = "real_date"; - - /** - * Message Type, see {@link MessageType} - * <P>Type: INTEGER</P> - */ - String TYPE = "type"; - - /** - * Error Code: 0 means no error. - * <P>Type: INTEGER </P> - */ - String ERROR_CODE = "err_code"; - - /** - * Error Message - * <P>Type: TEXT</P> - */ - String ERROR_MESSAGE = "err_msg"; - - /** - * Packet ID, auto assigned by the GTalkService for outgoing messages or the - * GTalk server for incoming messages. The packet id field is optional for messages, - * so it could be null. - * <P>Type: STRING</P> - */ - String PACKET_ID = "packet_id"; - - /** - * Is groupchat message or not - * <P>Type: INTEGER</P> - */ - String IS_GROUP_CHAT = "is_muc"; - - /** - * A hint that the UI should show the sent time of this message - * <P>Type: INTEGER</P> - */ - String DISPLAY_SENT_TIME = "show_ts"; - } - - /** - * This table contains messages. - */ - public static final class Messages implements BaseColumns, MessageColumns { - /** - * no public constructor since this is a utility class - */ - private Messages() {} - - /** - * Gets the Uri to query messages by thread id. - * - * @param threadId the thread id of the message. - * @return the Uri - */ - public static final Uri getContentUriByThreadId(long threadId) { - Uri.Builder builder = CONTENT_URI_MESSAGES_BY_THREAD_ID.buildUpon(); - ContentUris.appendId(builder, threadId); - return builder.build(); - } - - /** - * @deprecated - * - * Gets the Uri to query messages by account and contact. - * - * @param accountId the account id of the contact. - * @param username the user name of the contact. - * @return the Uri - */ - public static final Uri getContentUriByContact(long accountId, String username) { - Uri.Builder builder = CONTENT_URI_MESSAGES_BY_ACCOUNT_AND_CONTACT.buildUpon(); - ContentUris.appendId(builder, accountId); - builder.appendPath(username); - return builder.build(); - } - - /** - * Gets the Uri to query messages by provider. - * - * @param providerId the service provider id. - * @return the Uri - */ - public static final Uri getContentUriByProvider(long providerId) { - Uri.Builder builder = CONTENT_URI_MESSAGES_BY_PROVIDER.buildUpon(); - ContentUris.appendId(builder, providerId); - return builder.build(); - } - - /** - * Gets the Uri to query off the record messages by account. - * - * @param accountId the account id. - * @return the Uri - */ - public static final Uri getContentUriByAccount(long accountId) { - Uri.Builder builder = CONTENT_URI_BY_ACCOUNT.buildUpon(); - ContentUris.appendId(builder, accountId); - return builder.build(); - } - - /** - * Gets the Uri to query off the record messages by thread id. - * - * @param threadId the thread id of the message. - * @return the Uri - */ - public static final Uri getOtrMessagesContentUriByThreadId(long threadId) { - Uri.Builder builder = OTR_MESSAGES_CONTENT_URI_BY_THREAD_ID.buildUpon(); - ContentUris.appendId(builder, threadId); - return builder.build(); - } - - /** - * @deprecated - * - * Gets the Uri to query off the record messages by account and contact. - * - * @param accountId the account id of the contact. - * @param username the user name of the contact. - * @return the Uri - */ - public static final Uri getOtrMessagesContentUriByContact(long accountId, String username) { - Uri.Builder builder = OTR_MESSAGES_CONTENT_URI_BY_ACCOUNT_AND_CONTACT.buildUpon(); - ContentUris.appendId(builder, accountId); - builder.appendPath(username); - return builder.build(); - } - - /** - * Gets the Uri to query off the record messages by provider. - * - * @param providerId the service provider id. - * @return the Uri - */ - public static final Uri getOtrMessagesContentUriByProvider(long providerId) { - Uri.Builder builder = OTR_MESSAGES_CONTENT_URI_BY_PROVIDER.buildUpon(); - ContentUris.appendId(builder, providerId); - return builder.build(); - } - - /** - * Gets the Uri to query off the record messages by account. - * - * @param accountId the account id. - * @return the Uri - */ - public static final Uri getOtrMessagesContentUriByAccount(long accountId) { - Uri.Builder builder = OTR_MESSAGES_CONTENT_URI_BY_ACCOUNT.buildUpon(); - ContentUris.appendId(builder, accountId); - return builder.build(); - } - - /** - * The content:// style URL for this table - */ - public static final Uri CONTENT_URI = - Uri.parse("content://com.google.android.providers.talk/messages"); - - /** - * The content:// style URL for messages by thread id - */ - public static final Uri CONTENT_URI_MESSAGES_BY_THREAD_ID = - Uri.parse("content://com.google.android.providers.talk/messagesByThreadId"); - - /** - * The content:// style URL for messages by account and contact - */ - public static final Uri CONTENT_URI_MESSAGES_BY_ACCOUNT_AND_CONTACT = - Uri.parse("content://com.google.android.providers.talk/messagesByAcctAndContact"); - - /** - * The content:// style URL for messages by provider - */ - public static final Uri CONTENT_URI_MESSAGES_BY_PROVIDER = - Uri.parse("content://com.google.android.providers.talk/messagesByProvider"); - - /** - * The content:// style URL for messages by account - */ - public static final Uri CONTENT_URI_BY_ACCOUNT = - Uri.parse("content://com.google.android.providers.talk/messagesByAccount"); - - /** - * The content:// style url for off the record messages - */ - public static final Uri OTR_MESSAGES_CONTENT_URI = - Uri.parse("content://com.google.android.providers.talk/otrMessages"); - - /** - * The content:// style url for off the record messages by thread id - */ - public static final Uri OTR_MESSAGES_CONTENT_URI_BY_THREAD_ID = - Uri.parse("content://com.google.android.providers.talk/otrMessagesByThreadId"); - - /** - * The content:// style url for off the record messages by account and contact - */ - public static final Uri OTR_MESSAGES_CONTENT_URI_BY_ACCOUNT_AND_CONTACT = - Uri.parse("content://com.google.android.providers.talk/otrMessagesByAcctAndContact"); - - /** - * The content:// style URL for off the record messages by provider - */ - public static final Uri OTR_MESSAGES_CONTENT_URI_BY_PROVIDER = - Uri.parse("content://com.google.android.providers.talk/otrMessagesByProvider"); - - /** - * The content:// style URL for off the record messages by account - */ - public static final Uri OTR_MESSAGES_CONTENT_URI_BY_ACCOUNT = - Uri.parse("content://com.google.android.providers.talk/otrMessagesByAccount"); - - /** - * The MIME type of {@link #CONTENT_URI} providing a directory of - * people. - */ - public static final String CONTENT_TYPE = - "vnd.android.cursor.dir/gtalk-messages"; - - /** - * The MIME type of a {@link #CONTENT_URI} subdirectory of a single - * person. - */ - public static final String CONTENT_ITEM_TYPE = - "vnd.android.cursor.item/gtalk-messages"; - - /** - * The default sort order for this table - */ - public static final String DEFAULT_SORT_ORDER = "date ASC"; - - /** - * The "contact" column. This is not a real column in the messages table, but a - * temoprary column created when querying for messages (joined with the contacts table) - */ - public static final String CONTACT = "contact"; - } - - /** - * Columns for the GroupMember table. - */ - public interface GroupMemberColumns { - /** - * The id of the group this member belongs to. - * <p>Type: INTEGER</p> - */ - String GROUP = "groupId"; - - /** - * The full name of this member. - * <p>Type: TEXT</p> - */ - String USERNAME = "username"; - - /** - * The nick name of this member. - * <p>Type: TEXT</p> - */ - String NICKNAME = "nickname"; - } - - public final static class GroupMembers implements GroupMemberColumns { - private GroupMembers(){} - - public static final Uri CONTENT_URI = - Uri.parse("content://com.google.android.providers.talk/groupMembers"); - - /** - * The MIME type of {@link #CONTENT_URI} providing a directory of - * group members. - */ - public static final String CONTENT_TYPE = - "vnd.android.cursor.dir/gtalk-groupMembers"; - - /** - * The MIME type of a {@link #CONTENT_URI} subdirectory of a single - * group member. - */ - public static final String CONTENT_ITEM_TYPE = - "vnd.android.cursor.item/gtalk-groupMembers"; - } - - /** - * Columns from the Invitation table. - */ - public interface InvitationColumns { - /** - * The provider id. - * <p>Type: INTEGER</p> - */ - String PROVIDER = "providerId"; - - /** - * The account id. - * <p>Type: INTEGER</p> - */ - String ACCOUNT = "accountId"; - - /** - * The invitation id. - * <p>Type: TEXT</p> - */ - String INVITE_ID = "inviteId"; - - /** - * The name of the sender of the invitation. - * <p>Type: TEXT</p> - */ - String SENDER = "sender"; - - /** - * The name of the group which the sender invite you to join. - * <p>Type: TEXT</p> - */ - String GROUP_NAME = "groupName"; - - /** - * A note - * <p>Type: TEXT</p> - */ - String NOTE = "note"; - - /** - * The current status of the invitation. - * <p>Type: TEXT</p> - */ - String STATUS = "status"; - - int STATUS_PENDING = 0; - int STATUS_ACCEPTED = 1; - int STATUS_REJECTED = 2; - } - - /** - * This table contains the invitations received from others. - */ - public final static class Invitation implements InvitationColumns, - BaseColumns { - private Invitation() { - } - - /** - * The content:// style URL for this table - */ - public static final Uri CONTENT_URI = - Uri.parse("content://com.google.android.providers.talk/invitations"); - - /** - * The MIME type of {@link #CONTENT_URI} providing a directory of - * invitations. - */ - public static final String CONTENT_TYPE = - "vnd.android.cursor.dir/gtalk-invitations"; - - /** - * The MIME type of a {@link #CONTENT_URI} subdirectory of a single - * invitation. - */ - public static final String CONTENT_ITEM_TYPE = - "vnd.android.cursor.item/gtalk-invitations"; - } - - /** - * Columns from the Avatars table - */ - public interface AvatarsColumns { - /** - * The contact this avatar belongs to - * <P>Type: TEXT</P> - */ - String CONTACT = "contact"; - - String PROVIDER = "provider_id"; - - String ACCOUNT = "account_id"; - - /** - * The hash of the image data - * <P>Type: TEXT</P> - */ - String HASH = "hash"; - - /** - * raw image data - * <P>Type: BLOB</P> - */ - String DATA = "data"; - } - - /** - * This table contains avatars. - */ - public static final class Avatars implements BaseColumns, AvatarsColumns { - /** - * no public constructor since this is a utility class - */ - private Avatars() {} - - /** - * The content:// style URL for this table - */ - public static final Uri CONTENT_URI = - Uri.parse("content://com.google.android.providers.talk/avatars"); - - /** - * The content:// style URL for avatars by provider, account and contact - */ - public static final Uri CONTENT_URI_AVATARS_BY = - Uri.parse("content://com.google.android.providers.talk/avatarsBy"); - - /** - * The MIME type of {@link #CONTENT_URI} providing the avatars - */ - public static final String CONTENT_TYPE = "vnd.android.cursor.dir/gtalk-avatars"; - - /** - * The MIME type of a {@link #CONTENT_URI} - */ - public static final String CONTENT_ITEM_TYPE = - "vnd.android.cursor.item/gtalk-avatars"; - - /** - * The default sort order for this table - */ - public static final String DEFAULT_SORT_ORDER = "contact ASC"; - - } - - /** - * Common presence columns shared between the IM and contacts presence tables - */ - public interface CommonPresenceColumns { - /** - * The priority, an integer, used by XMPP presence - * <P>Type: INTEGER</P> - */ - String PRIORITY = "priority"; - - /** - * The server defined status. - * <P>Type: INTEGER (one of the values below)</P> - */ - String PRESENCE_STATUS = "mode"; - - /** - * Presence Status definition - */ - int OFFLINE = 0; - int INVISIBLE = 1; - int AWAY = 2; - int IDLE = 3; - int DO_NOT_DISTURB = 4; - int AVAILABLE = 5; - - /** - * The user defined status line. - * <P>Type: TEXT</P> - */ - String PRESENCE_CUSTOM_STATUS = "status"; - } - - /** - * Columns from the Presence table. - */ - public interface PresenceColumns extends CommonPresenceColumns { - /** - * The contact id - * <P>Type: INTEGER</P> - */ - String CONTACT_ID = "contact_id"; - - /** - * The contact's JID resource, only relevant for XMPP contact - * <P>Type: TEXT</P> - */ - String JID_RESOURCE = "jid_resource"; - - /** - * The contact's client type - */ - String CLIENT_TYPE = "client_type"; - - /** - * client type definitions - */ - int CLIENT_TYPE_DEFAULT = 0; - int CLIENT_TYPE_MOBILE = 1; - int CLIENT_TYPE_ANDROID = 2; - } - - /** - * Contains presence infomation for contacts. - */ - public static final class Presence implements BaseColumns, PresenceColumns { - /** - * The content:// style URL for this table - */ - public static final Uri CONTENT_URI = - Uri.parse("content://com.google.android.providers.talk/presence"); - - /** - * The content URL for Talk presences for an account - */ - public static final Uri CONTENT_URI_BY_ACCOUNT = - Uri.parse("content://com.google.android.providers.talk/presence/account"); - - /** - * The content:// style URL for operations on bulk contacts - */ - public static final Uri BULK_CONTENT_URI = - Uri.parse("content://com.google.android.providers.talk/bulk_presence"); - - /** - * The content:// style URL for seeding presences for a given account id. - */ - public static final Uri SEED_PRESENCE_BY_ACCOUNT_CONTENT_URI = - Uri.parse("content://com.google.android.providers.talk/seed_presence/account"); - - /** - * The MIME type of a {@link #CONTENT_URI} providing a directory of presence - */ - public static final String CONTENT_TYPE = "vnd.android.cursor.dir/gtalk-presence"; - - /** - * The default sort order for this table - */ - public static final String DEFAULT_SORT_ORDER = "mode DESC"; - } - - /** - * Columns from the Chats table. - */ - public interface ChatsColumns { - /** - * The contact ID this chat belongs to. The value is a long. - * <P>Type: INT</P> - */ - String CONTACT_ID = "contact_id"; - - /** - * The GTalk JID resource. The value is a string. - * <P>Type: TEXT</P> - */ - String JID_RESOURCE = "jid_resource"; - - /** - * Whether this is a groupchat or not. - * <P>Type: INT</P> - */ - String GROUP_CHAT = "groupchat"; - - /** - * The last unread message. This both indicates that there is an - * unread message, and what the message is. - * <P>Type: TEXT</P> - */ - String LAST_UNREAD_MESSAGE = "last_unread_message"; - - /** - * The last message timestamp - * <P>Type: INT</P> - */ - String LAST_MESSAGE_DATE = "last_message_date"; - - /** - * A message that is being composed. This indicates that there was a - * message being composed when the chat screen was shutdown, and what the - * message is. - * <P>Type: TEXT</P> - */ - String UNSENT_COMPOSED_MESSAGE = "unsent_composed_message"; - - /** - * A value from 0-9 indicating which quick-switch chat screen slot this - * chat is occupying. If none (for instance, this is the 12th active chat) - * then the value is -1. - * <P>Type: INT</P> - */ - String SHORTCUT = "shortcut"; - } - - /** - * Contains ongoing chat sessions. - */ - public static final class Chats implements BaseColumns, ChatsColumns { - /** - * no public constructor since this is a utility class - */ - private Chats() {} - - /** - * The content:// style URL for this table - */ - public static final Uri CONTENT_URI = - Uri.parse("content://com.google.android.providers.talk/chats"); - - /** - * The content URL for all chats that belong to the account - */ - public static final Uri CONTENT_URI_BY_ACCOUNT = - Uri.parse("content://com.google.android.providers.talk/chats/account"); - - /** - * The MIME type of {@link #CONTENT_URI} providing a directory of chats. - */ - public static final String CONTENT_TYPE = "vnd.android.cursor.dir/gtalk-chats"; - - /** - * The MIME type of a {@link #CONTENT_URI} subdirectory of a single chat. - */ - public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/gtalk-chats"; - - /** - * The default sort order for this table - */ - public static final String DEFAULT_SORT_ORDER = "last_message_date ASC"; - } - - /** - * Columns from session cookies table. Used for IMPS. - */ - public static interface SessionCookiesColumns { - String NAME = "name"; - String VALUE = "value"; - String PROVIDER = "provider"; - String ACCOUNT = "account"; - } - - /** - * Contains IMPS session cookies. - */ - public static class SessionCookies implements SessionCookiesColumns, BaseColumns { - private SessionCookies() { - } - - /** - * The content:// style URI for this table - */ - public static final Uri CONTENT_URI = - Uri.parse("content://com.google.android.providers.talk/sessionCookies"); - - /** - * The content:// style URL for session cookies by provider and account - */ - public static final Uri CONTENT_URI_SESSION_COOKIES_BY = - Uri.parse("content://com.google.android.providers.talk/sessionCookiesBy"); - - /** - * The MIME type of {@link #CONTENT_URI} providing a directory of - * people. - */ - public static final String CONTENT_TYPE = "vnd.android-dir/gtalk-sessionCookies"; - } - - /** - * Columns from ProviderSettings table - */ - public static interface ProviderSettingsColumns { - /** - * The id in database of the related provider - * - * <P>Type: INT</P> - */ - String PROVIDER = "provider"; - - /** - * The name of the setting - * <P>Type: TEXT</P> - */ - String NAME = "name"; - - /** - * The value of the setting - * <P>Type: TEXT</P> - */ - String VALUE = "value"; - } - - public static class ProviderSettings implements ProviderSettingsColumns { - private ProviderSettings() { - } - - /** - * The content:// style URI for this table - */ - public static final Uri CONTENT_URI = - Uri.parse("content://com.google.android.providers.talk/providerSettings"); - - /** - * The MIME type of {@link #CONTENT_URI} providing provider settings - */ - public static final String CONTENT_TYPE = "vnd.android-dir/gtalk-providerSettings"; - - /** - * A boolean value to indicate whether this provider should show the offline contacts - */ - public static final String SHOW_OFFLINE_CONTACTS = "show_offline_contacts"; - - /** controls whether or not the GTalk service automatically connect to server. */ - public static final String SETTING_AUTOMATICALLY_CONNECT_GTALK = "gtalk_auto_connect"; - - /** controls whether or not the GTalk service will be automatically started after boot */ - public static final String SETTING_AUTOMATICALLY_START_SERVICE = "auto_start_service"; - - /** controls whether or not the offline contacts will be hided */ - public static final String SETTING_HIDE_OFFLINE_CONTACTS = "hide_offline_contacts"; - - /** controls whether or not enable the GTalk notification */ - public static final String SETTING_ENABLE_NOTIFICATION = "enable_notification"; - - /** specifies whether or not to vibrate */ - public static final String SETTING_VIBRATE = "vibrate"; - - /** specifies the Uri string of the ringtone */ - public static final String SETTING_RINGTONE = "ringtone"; - - /** specifies the Uri of the default ringtone */ - public static final String SETTING_RINGTONE_DEFAULT = - "content://settings/system/notification_sound"; - - /** specifies whether or not to show mobile indicator to friends */ - public static final String SETTING_SHOW_MOBILE_INDICATOR = "mobile_indicator"; - - /** specifies whether or not to show as away when device is idle */ - public static final String SETTING_SHOW_AWAY_ON_IDLE = "show_away_on_idle"; - - /** specifies whether or not to upload heartbeat stat upon login */ - public static final String SETTING_UPLOAD_HEARTBEAT_STAT = "upload_heartbeat_stat"; - - /** specifies the last heartbeat interval received from the server */ - public static final String SETTING_HEARTBEAT_INTERVAL = "heartbeat_interval"; - - /** specifiy the JID resource used for Google Talk connection */ - public static final String SETTING_JID_RESOURCE = "jid_resource"; - - /** - * Used for reliable message queue (RMQ). This is for storing the last rmq id received - * from the GTalk server - */ - public static final String LAST_RMQ_RECEIVED = "last_rmq_rec"; - - /** - * Query the settings of the provider specified by id - * - * @param cr - * the relative content resolver - * @param providerId - * the specified id of provider - * @return a HashMap which contains all the settings for the specified - * provider - */ - public static HashMap<String, String> queryProviderSettings(ContentResolver cr, - long providerId) { - HashMap<String, String> settings = new HashMap<String, String>(); - - String[] projection = { NAME, VALUE }; - Cursor c = cr.query(ContentUris.withAppendedId(CONTENT_URI, providerId), projection, null, null, null); - if (c == null) { - return null; - } - - while(c.moveToNext()) { - settings.put(c.getString(0), c.getString(1)); - } - - c.close(); - - return settings; - } - - /** - * Get the string value of setting which is specified by provider id and the setting name. - * - * @param cr The ContentResolver to use to access the settings table. - * @param providerId The id of the provider. - * @param settingName The name of the setting. - * @return The value of the setting if the setting exist, otherwise return null. - */ - public static String getStringValue(ContentResolver cr, long providerId, String settingName) { - String ret = null; - Cursor c = getSettingValue(cr, providerId, settingName); - if (c != null) { - ret = c.getString(0); - c.close(); - } - - return ret; - } - - /** - * Get the boolean value of setting which is specified by provider id and the setting name. - * - * @param cr The ContentResolver to use to access the settings table. - * @param providerId The id of the provider. - * @param settingName The name of the setting. - * @return The value of the setting if the setting exist, otherwise return false. - */ - public static boolean getBooleanValue(ContentResolver cr, long providerId, String settingName) { - boolean ret = false; - Cursor c = getSettingValue(cr, providerId, settingName); - if (c != null) { - ret = c.getInt(0) != 0; - c.close(); - } - return ret; - } - - private static Cursor getSettingValue(ContentResolver cr, long providerId, String settingName) { - Cursor c = cr.query(ContentUris.withAppendedId(CONTENT_URI, providerId), new String[]{VALUE}, NAME + "=?", - new String[]{settingName}, null); - if (c != null) { - if (!c.moveToFirst()) { - c.close(); - return null; - } - } - return c; - } - - /** - * Save a long value of setting in the table providerSetting. - * - * @param cr The ContentProvider used to access the providerSetting table. - * @param providerId The id of the provider. - * @param name The name of the setting. - * @param value The value of the setting. - */ - public static void putLongValue(ContentResolver cr, long providerId, String name, - long value) { - ContentValues v = new ContentValues(3); - v.put(PROVIDER, providerId); - v.put(NAME, name); - v.put(VALUE, value); - - cr.insert(CONTENT_URI, v); - } - - /** - * Save a boolean value of setting in the table providerSetting. - * - * @param cr The ContentProvider used to access the providerSetting table. - * @param providerId The id of the provider. - * @param name The name of the setting. - * @param value The value of the setting. - */ - public static void putBooleanValue(ContentResolver cr, long providerId, String name, - boolean value) { - ContentValues v = new ContentValues(3); - v.put(PROVIDER, providerId); - v.put(NAME, name); - v.put(VALUE, Boolean.toString(value)); - - cr.insert(CONTENT_URI, v); - } - - /** - * Save a string value of setting in the table providerSetting. - * - * @param cr The ContentProvider used to access the providerSetting table. - * @param providerId The id of the provider. - * @param name The name of the setting. - * @param value The value of the setting. - */ - public static void putStringValue(ContentResolver cr, long providerId, String name, - String value) { - ContentValues v = new ContentValues(3); - v.put(PROVIDER, providerId); - v.put(NAME, name); - v.put(VALUE, value); - - cr.insert(CONTENT_URI, v); - } - - /** - * A convenience method to set whether or not the GTalk service should be started - * automatically. - * - * @param contentResolver The ContentResolver to use to access the settings table - * @param autoConnect Whether the GTalk service should be started automatically. - */ - public static void setAutomaticallyConnectGTalk(ContentResolver contentResolver, - long providerId, boolean autoConnect) { - putBooleanValue(contentResolver, providerId, SETTING_AUTOMATICALLY_CONNECT_GTALK, - autoConnect); - } - - /** - * A convenience method to set whether or not the offline contacts should be hided - * - * @param contentResolver The ContentResolver to use to access the setting table - * @param hideOfflineContacts Whether the offline contacts should be hided - */ - public static void setHideOfflineContacts(ContentResolver contentResolver, - long providerId, boolean hideOfflineContacts) { - putBooleanValue(contentResolver, providerId, SETTING_HIDE_OFFLINE_CONTACTS, - hideOfflineContacts); - } - - /** - * A convenience method to set whether or not enable the GTalk notification. - * - * @param contentResolver The ContentResolver to use to access the setting table. - * @param enable Whether enable the GTalk notification - */ - public static void setEnableNotification(ContentResolver contentResolver, long providerId, - boolean enable) { - putBooleanValue(contentResolver, providerId, SETTING_ENABLE_NOTIFICATION, enable); - } - - /** - * A convenience method to set whether or not to vibrate. - * - * @param contentResolver The ContentResolver to use to access the setting table. - * @param vibrate Whether or not to vibrate - */ - public static void setVibrate(ContentResolver contentResolver, long providerId, - boolean vibrate) { - putBooleanValue(contentResolver, providerId, SETTING_VIBRATE, vibrate); - } - - /** - * A convenience method to set the Uri String of the ringtone. - * - * @param contentResolver The ContentResolver to use to access the setting table. - * @param ringtoneUri The Uri String of the ringtone to be set. - */ - public static void setRingtoneURI(ContentResolver contentResolver, long providerId, - String ringtoneUri) { - putStringValue(contentResolver, providerId, SETTING_RINGTONE, ringtoneUri); - } - - /** - * A convenience method to set whether or not to show mobile indicator. - * - * @param contentResolver The ContentResolver to use to access the setting table. - * @param showMobileIndicator Whether or not to show mobile indicator. - */ - public static void setShowMobileIndicator(ContentResolver contentResolver, long providerId, - boolean showMobileIndicator) { - putBooleanValue(contentResolver, providerId, SETTING_SHOW_MOBILE_INDICATOR, - showMobileIndicator); - } - - /** - * A convenience method to set whether or not to show as away when device is idle. - * - * @param contentResolver The ContentResolver to use to access the setting table. - * @param showAway Whether or not to show as away when device is idle. - */ - public static void setShowAwayOnIdle(ContentResolver contentResolver, - long providerId, boolean showAway) { - putBooleanValue(contentResolver, providerId, SETTING_SHOW_AWAY_ON_IDLE, showAway); - } - - /** - * A convenience method to set whether or not to upload heartbeat stat. - * - * @param contentResolver The ContentResolver to use to access the setting table. - * @param uploadStat Whether or not to upload heartbeat stat. - */ - public static void setUploadHeartbeatStat(ContentResolver contentResolver, - long providerId, boolean uploadStat) { - putBooleanValue(contentResolver, providerId, SETTING_UPLOAD_HEARTBEAT_STAT, uploadStat); - } - - /** - * A convenience method to set the heartbeat interval last received from the server. - * - * @param contentResolver The ContentResolver to use to access the setting table. - * @param interval The heartbeat interval last received from the server. - */ - public static void setHeartbeatInterval(ContentResolver contentResolver, - long providerId, long interval) { - putLongValue(contentResolver, providerId, SETTING_HEARTBEAT_INTERVAL, interval); - } - - /** - * A convenience method to set the jid resource. - */ - public static void setJidResource(ContentResolver contentResolver, - long providerId, String jidResource) { - putStringValue(contentResolver, providerId, SETTING_JID_RESOURCE, jidResource); - } - - public static class QueryMap extends ContentQueryMap { - private ContentResolver mContentResolver; - private long mProviderId; - - public QueryMap(ContentResolver contentResolver, long providerId, boolean keepUpdated, - Handler handlerForUpdateNotifications) { - super(contentResolver.query(CONTENT_URI, - new String[] {NAME,VALUE}, - PROVIDER + "=" + providerId, - null, // no selection args - null), // no sort order - NAME, keepUpdated, handlerForUpdateNotifications); - mContentResolver = contentResolver; - mProviderId = providerId; - } - - /** - * Set if the GTalk service should automatically connect to server. - * - * @param autoConnect if the GTalk service should auto connect to server. - */ - public void setAutomaticallyConnectToGTalkServer(boolean autoConnect) { - ProviderSettings.setAutomaticallyConnectGTalk(mContentResolver, mProviderId, - autoConnect); - } - - /** - * Check if the GTalk service should automatically connect to server. - * @return if the GTalk service should automatically connect to server. - */ - public boolean getAutomaticallyConnectToGTalkServer() { - return getBoolean(SETTING_AUTOMATICALLY_CONNECT_GTALK, - true /* default to automatically sign in */); - } - - /** - * Set whether or not the offline contacts should be hided. - * - * @param hideOfflineContacts Whether or not the offline contacts should be hided. - */ - public void setHideOfflineContacts(boolean hideOfflineContacts) { - ProviderSettings.setHideOfflineContacts(mContentResolver, mProviderId, - hideOfflineContacts); - } - - /** - * Check if the offline contacts should be hided. - * - * @return Whether or not the offline contacts should be hided. - */ - public boolean getHideOfflineContacts() { - return getBoolean(SETTING_HIDE_OFFLINE_CONTACTS, - false/* by default not hide the offline contacts*/); - } - - /** - * Set whether or not enable the GTalk notification. - * - * @param enable Whether or not enable the GTalk notification. - */ - public void setEnableNotification(boolean enable) { - ProviderSettings.setEnableNotification(mContentResolver, mProviderId, enable); - } - - /** - * Check if the GTalk notification is enabled. - * - * @return Whether or not enable the GTalk notification. - */ - public boolean getEnableNotification() { - return getBoolean(SETTING_ENABLE_NOTIFICATION, - true/* by default enable the notification */); - } - - /** - * Set whether or not to vibrate on GTalk notification. - * - * @param vibrate Whether or not to vibrate. - */ - public void setVibrate(boolean vibrate) { - ProviderSettings.setVibrate(mContentResolver, mProviderId, vibrate); - } - - /** - * Gets whether or not to vibrate on GTalk notification. - * - * @return Whether or not to vibrate. - */ - public boolean getVibrate() { - return getBoolean(SETTING_VIBRATE, false /* by default disable vibrate */); - } - - /** - * Set the Uri for the ringtone. - * - * @param ringtoneUri The Uri of the ringtone to be set. - */ - public void setRingtoneURI(String ringtoneUri) { - ProviderSettings.setRingtoneURI(mContentResolver, mProviderId, ringtoneUri); - } - - /** - * Get the Uri String of the current ringtone. - * - * @return The Uri String of the current ringtone. - */ - public String getRingtoneURI() { - return getString(SETTING_RINGTONE, SETTING_RINGTONE_DEFAULT); - } - - /** - * Set whether or not to show mobile indicator to friends. - * - * @param showMobile whether or not to show mobile indicator. - */ - public void setShowMobileIndicator(boolean showMobile) { - ProviderSettings.setShowMobileIndicator(mContentResolver, mProviderId, showMobile); - } - - /** - * Gets whether or not to show mobile indicator. - * - * @return Whether or not to show mobile indicator. - */ - public boolean getShowMobileIndicator() { - return getBoolean(SETTING_SHOW_MOBILE_INDICATOR, - true /* by default show mobile indicator */); - } - - /** - * Set whether or not to show as away when device is idle. - * - * @param showAway whether or not to show as away when device is idle. - */ - public void setShowAwayOnIdle(boolean showAway) { - ProviderSettings.setShowAwayOnIdle(mContentResolver, mProviderId, showAway); - } - - /** - * Get whether or not to show as away when device is idle. - * - * @return Whether or not to show as away when device is idle. - */ - public boolean getShowAwayOnIdle() { - return getBoolean(SETTING_SHOW_AWAY_ON_IDLE, - true /* by default show as away on idle*/); - } - - /** - * Set whether or not to upload heartbeat stat. - * - * @param uploadStat whether or not to upload heartbeat stat. - */ - public void setUploadHeartbeatStat(boolean uploadStat) { - ProviderSettings.setUploadHeartbeatStat(mContentResolver, mProviderId, uploadStat); - } - - /** - * Get whether or not to upload heartbeat stat. - * - * @return Whether or not to upload heartbeat stat. - */ - public boolean getUploadHeartbeatStat() { - return getBoolean(SETTING_UPLOAD_HEARTBEAT_STAT, - false /* by default do not upload */); - } - - /** - * Set the last received heartbeat interval from the server. - * - * @param interval the last received heartbeat interval from the server. - */ - public void setHeartbeatInterval(long interval) { - ProviderSettings.setHeartbeatInterval(mContentResolver, mProviderId, interval); - } - - /** - * Get the last received heartbeat interval from the server. - * - * @return the last received heartbeat interval from the server. - */ - public long getHeartbeatInterval() { - return getLong(SETTING_HEARTBEAT_INTERVAL, 0L /* an invalid default interval */); - } - - /** - * Set the JID resource. - * - * @param jidResource the jid resource to be stored. - */ - public void setJidResource(String jidResource) { - ProviderSettings.setJidResource(mContentResolver, mProviderId, jidResource); - } - /** - * Get the JID resource used for the Google Talk connection - * - * @return the JID resource stored. - */ - public String getJidResource() { - return getString(SETTING_JID_RESOURCE, null); - } - - /** - * Convenience function for retrieving a single settings value - * as a boolean. - * - * @param name The name of the setting to retrieve. - * @param def Value to return if the setting is not defined. - * @return The setting's current value, or 'def' if it is not defined. - */ - private boolean getBoolean(String name, boolean def) { - ContentValues values = getValues(name); - return values != null ? values.getAsBoolean(VALUE) : def; - } - - /** - * Convenience function for retrieving a single settings value - * as a String. - * - * @param name The name of the setting to retrieve. - * @param def The value to return if the setting is not defined. - * @return The setting's current value or 'def' if it is not defined. - */ - private String getString(String name, String def) { - ContentValues values = getValues(name); - return values != null ? values.getAsString(VALUE) : def; - } - - /** - * Convenience function for retrieving a single settings value - * as an Integer. - * - * @param name The name of the setting to retrieve. - * @param def The value to return if the setting is not defined. - * @return The setting's current value or 'def' if it is not defined. - */ - private int getInteger(String name, int def) { - ContentValues values = getValues(name); - return values != null ? values.getAsInteger(VALUE) : def; - } - - /** - * Convenience function for retrieving a single settings value - * as a Long. - * - * @param name The name of the setting to retrieve. - * @param def The value to return if the setting is not defined. - * @return The setting's current value or 'def' if it is not defined. - */ - private long getLong(String name, long def) { - ContentValues values = getValues(name); - return values != null ? values.getAsLong(VALUE) : def; - } - } - - } - - - /** - * Columns for GTalk branding resource map cache table. This table caches the result of - * loading the branding resources to speed up GTalk landing page start. - */ - public interface BrandingResourceMapCacheColumns { - /** - * The provider ID - * <P>Type: INTEGER</P> - */ - String PROVIDER_ID = "provider_id"; - /** - * The application resource ID - * <P>Type: INTEGER</P> - */ - String APP_RES_ID = "app_res_id"; - /** - * The plugin resource ID - * <P>Type: INTEGER</P> - */ - String PLUGIN_RES_ID = "plugin_res_id"; - } - - /** - * The table for caching the result of loading GTalk branding resources. - */ - public static final class BrandingResourceMapCache - implements BaseColumns, BrandingResourceMapCacheColumns { - /** - * The content:// style URL for this table. - */ - public static final Uri CONTENT_URI = - Uri.parse("content://com.google.android.providers.talk/brandingResMapCache"); - } - - - - /** - * //TODO: move these to MCS specific provider. - * The following are MCS stuff, and should really live in a separate provider specific to - * MCS code. - */ - - /** - * Columns from OutgoingRmq table - */ - public interface OutgoingRmqColumns { - String RMQ_ID = "rmq_id"; - String TIMESTAMP = "ts"; - String DATA = "data"; - String PROTOBUF_TAG = "type"; - } - - /** - * //TODO: we should really move these to their own provider and database. - * The table for storing outgoing rmq packets. - */ - public static final class OutgoingRmq implements BaseColumns, OutgoingRmqColumns { - private static String[] RMQ_ID_PROJECTION = new String[] { - RMQ_ID, - }; - - /** - * queryHighestRmqId - * - * @param resolver the content resolver - * @return the highest rmq id assigned to the rmq packet, or 0 if there are no rmq packets - * in the OutgoingRmq table. - */ - public static final long queryHighestRmqId(ContentResolver resolver) { - Cursor cursor = resolver.query(Im.OutgoingRmq.CONTENT_URI_FOR_HIGHEST_RMQ_ID, - RMQ_ID_PROJECTION, - null, // selection - null, // selection args - null // sort - ); - - long retVal = 0; - try { - //if (DBG) log("initializeRmqid: cursor.count= " + cursor.count()); - - if (cursor.moveToFirst()) { - retVal = cursor.getLong(cursor.getColumnIndexOrThrow(RMQ_ID)); - } - } finally { - cursor.close(); - } - - return retVal; - } - - /** - * The content:// style URL for this table. - */ - public static final Uri CONTENT_URI = - Uri.parse("content://com.google.android.providers.talk/outgoingRmqMessages"); - - /** - * The content:// style URL for the highest rmq id for the outgoing rmq messages - */ - public static final Uri CONTENT_URI_FOR_HIGHEST_RMQ_ID = - Uri.parse("content://com.google.android.providers.talk/outgoingHighestRmqId"); - - /** - * The default sort order for this table. - */ - public static final String DEFAULT_SORT_ORDER = "rmq_id ASC"; - } - - /** - * Columns for the LastRmqId table, which stores a single row for the last client rmq id - * sent to the server. - */ - public interface LastRmqIdColumns { - String RMQ_ID = "rmq_id"; - } - - /** - * //TODO: move these out into their own provider and database - * The table for storing the last client rmq id sent to the server. - */ - public static final class LastRmqId implements BaseColumns, LastRmqIdColumns { - private static String[] PROJECTION = new String[] { - RMQ_ID, - }; - - /** - * queryLastRmqId - * - * queries the last rmq id saved in the LastRmqId table. - * - * @param resolver the content resolver. - * @return the last rmq id stored in the LastRmqId table, or 0 if not found. - */ - public static final long queryLastRmqId(ContentResolver resolver) { - Cursor cursor = resolver.query(Im.LastRmqId.CONTENT_URI, - PROJECTION, - null, // selection - null, // selection args - null // sort - ); - - long retVal = 0; - try { - if (cursor.moveToFirst()) { - retVal = cursor.getLong(cursor.getColumnIndexOrThrow(RMQ_ID)); - } - } finally { - cursor.close(); - } - - return retVal; - } - - /** - * saveLastRmqId - * - * saves the rmqId to the lastRmqId table. This will override the existing row if any, - * as we only keep one row of data in this table. - * - * @param resolver the content resolver. - * @param rmqId the rmq id to be saved. - */ - public static final void saveLastRmqId(ContentResolver resolver, long rmqId) { - ContentValues values = new ContentValues(); - - // always replace the first row. - values.put(_ID, 1); - values.put(RMQ_ID, rmqId); - resolver.insert(CONTENT_URI, values); - } - - /** - * The content:// style URL for this table. - */ - public static final Uri CONTENT_URI = - Uri.parse("content://com.google.android.providers.talk/lastRmqId"); - } - - /** - * Columns for the s2dRmqIds table, which stores the server-to-device message - * persistent ids. These are used in the RMQ2 protocol, where in the login request, the - * client selective acks these s2d ids to the server. - */ - public interface ServerToDeviceRmqIdsColumn { - String RMQ_ID = "rmq_id"; - } - - public static final class ServerToDeviceRmqIds implements BaseColumns, - ServerToDeviceRmqIdsColumn { - - /** - * The content:// style URL for this table. - */ - public static final Uri CONTENT_URI = - Uri.parse("content://com.google.android.providers.talk/s2dids"); - } - -} diff --git a/core/java/android/provider/MediaStore.java b/core/java/android/provider/MediaStore.java index 062080d..1b938ee 100644 --- a/core/java/android/provider/MediaStore.java +++ b/core/java/android/provider/MediaStore.java @@ -96,14 +96,12 @@ public final class MediaStore { /** * The name of an Intent-extra used to control the UI of a ViewImage. * This is a boolean property that overrides the activity's default fullscreen state. - * @hide */ public static final String EXTRA_FULL_SCREEN = "android.intent.extra.fullScreen"; /** * The name of an Intent-extra used to control the UI of a ViewImage. * This is a boolean property that specifies whether or not to show action icons. - * @hide */ public static final String EXTRA_SHOW_ACTION_ICONS = "android.intent.extra.showActionIcons"; @@ -162,13 +160,11 @@ public final class MediaStore { /** * Specify the maximum allowed size. - * @hide */ public final static String EXTRA_SIZE_LIMIT = "android.intent.extra.sizeLimit"; /** * Specify the maximum allowed recording duration in seconds. - * @hide */ public final static String EXTRA_DURATION_LIMIT = "android.intent.extra.durationLimit"; @@ -179,6 +175,13 @@ public final class MediaStore { public final static String EXTRA_OUTPUT = "output"; /** + * The string that is used when a media attribute is not known. For example, + * if an audio file does not have any meta data, the artist and album columns + * will be set to this value. + */ + public static final String UNKNOWN_STRING = "<unknown>"; + + /** * Common fields for most MediaProvider tables */ @@ -238,6 +241,29 @@ public final class MediaStore { private static final int FULL_SCREEN_KIND = 2; private static final int MICRO_KIND = 3; private static final String[] PROJECTION = new String[] {_ID, MediaColumns.DATA}; + static final int DEFAULT_GROUP_ID = 0; + + private static Bitmap getMiniThumbFromFile(Cursor c, Uri baseUri, ContentResolver cr, BitmapFactory.Options options) { + Bitmap bitmap = null; + Uri thumbUri = null; + try { + long thumbId = c.getLong(0); + String filePath = c.getString(1); + thumbUri = ContentUris.withAppendedId(baseUri, thumbId); + ParcelFileDescriptor pfdInput = cr.openFileDescriptor(thumbUri, "r"); + bitmap = BitmapFactory.decodeFileDescriptor( + pfdInput.getFileDescriptor(), null, options); + pfdInput.close(); + } catch (FileNotFoundException ex) { + Log.e(TAG, "couldn't open thumbnail " + thumbUri + "; " + ex); + } catch (IOException ex) { + Log.e(TAG, "couldn't open thumbnail " + thumbUri + "; " + ex); + } catch (OutOfMemoryError ex) { + Log.e(TAG, "failed to allocate memory for thumbnail " + + thumbUri + "; " + ex); + } + return bitmap; + } /** * This method cancels the thumbnail request so clients waiting for getThumbnail will be @@ -246,11 +272,14 @@ public final class MediaStore { * * @param cr ContentResolver * @param origId original image or video id. use -1 to cancel all requests. + * @param groupId the same groupId used in getThumbnail * @param baseUri the base URI of requested thumbnails */ - static void cancelThumbnailRequest(ContentResolver cr, long origId, Uri baseUri) { + static void cancelThumbnailRequest(ContentResolver cr, long origId, Uri baseUri, + long groupId) { Uri cancelUri = baseUri.buildUpon().appendQueryParameter("cancel", "1") - .appendQueryParameter("orig_id", String.valueOf(origId)).build(); + .appendQueryParameter("orig_id", String.valueOf(origId)) + .appendQueryParameter("group_id", String.valueOf(groupId)).build(); Cursor c = null; try { c = cr.query(cancelUri, PROJECTION, null, null, null); @@ -271,18 +300,20 @@ public final class MediaStore { * @param kind could be MINI_KIND or MICRO_KIND * @param options this is only used for MINI_KIND when decoding the Bitmap * @param baseUri the base URI of requested thumbnails + * @param groupId the id of group to which this request belongs * @return Bitmap bitmap of specified thumbnail kind */ - static Bitmap getThumbnail(ContentResolver cr, long origId, int kind, + static Bitmap getThumbnail(ContentResolver cr, long origId, long groupId, int kind, BitmapFactory.Options options, Uri baseUri, boolean isVideo) { Bitmap bitmap = null; String filePath = null; // Log.v(TAG, "getThumbnail: origId="+origId+", kind="+kind+", isVideo="+isVideo); - // some optimization for MICRO_KIND: if the magic is non-zero, we don't bother + // If the magic is non-zero, we simply return thumbnail if it does exist. // querying MediaProvider and simply return thumbnail. - if (kind == MICRO_KIND) { - MiniThumbFile thumbFile = MiniThumbFile.instance(baseUri); - if (thumbFile.getMagic(origId) != 0) { + MiniThumbFile thumbFile = MiniThumbFile.instance(baseUri); + long magic = thumbFile.getMagic(origId); + if (magic != 0) { + if (kind == MICRO_KIND) { byte[] data = new byte[MiniThumbFile.BYTES_PER_MINTHUMB]; if (thumbFile.getMiniThumbFromFile(origId, data) != null) { bitmap = BitmapFactory.decodeByteArray(data, 0, data.length); @@ -291,20 +322,34 @@ public final class MediaStore { } } return bitmap; + } else if (kind == MINI_KIND) { + String column = isVideo ? "video_id=" : "image_id="; + Cursor c = null; + try { + c = cr.query(baseUri, PROJECTION, column + origId, null, null); + if (c != null && c.moveToFirst()) { + bitmap = getMiniThumbFromFile(c, baseUri, cr, options); + if (bitmap != null) { + return bitmap; + } + } + } finally { + if (c != null) c.close(); + } } } Cursor c = null; try { Uri blockingUri = baseUri.buildUpon().appendQueryParameter("blocking", "1") - .appendQueryParameter("orig_id", String.valueOf(origId)).build(); + .appendQueryParameter("orig_id", String.valueOf(origId)) + .appendQueryParameter("group_id", String.valueOf(groupId)).build(); c = cr.query(blockingUri, PROJECTION, null, null, null); // This happens when original image/video doesn't exist. if (c == null) return null; // Assuming thumbnail has been generated, at least original image exists. if (kind == MICRO_KIND) { - MiniThumbFile thumbFile = MiniThumbFile.instance(baseUri); byte[] data = new byte[MiniThumbFile.BYTES_PER_MINTHUMB]; if (thumbFile.getMiniThumbFromFile(origId, data) != null) { bitmap = BitmapFactory.decodeByteArray(data, 0, data.length); @@ -314,24 +359,7 @@ public final class MediaStore { } } else if (kind == MINI_KIND) { if (c.moveToFirst()) { - ParcelFileDescriptor pfdInput; - Uri thumbUri = null; - try { - long thumbId = c.getLong(0); - filePath = c.getString(1); - thumbUri = ContentUris.withAppendedId(baseUri, thumbId); - pfdInput = cr.openFileDescriptor(thumbUri, "r"); - bitmap = BitmapFactory.decodeFileDescriptor( - pfdInput.getFileDescriptor(), null, options); - pfdInput.close(); - } catch (FileNotFoundException ex) { - Log.e(TAG, "couldn't open thumbnail " + thumbUri + "; " + ex); - } catch (IOException ex) { - Log.e(TAG, "couldn't open thumbnail " + thumbUri + "; " + ex); - } catch (OutOfMemoryError ex) { - Log.e(TAG, "failed to allocate memory for thumbnail " - + thumbUri + "; " + ex); - } + bitmap = getMiniThumbFromFile(c, baseUri, cr, options); } } else { throw new IllegalArgumentException("Unsupported kind: " + kind); @@ -354,7 +382,7 @@ public final class MediaStore { } if (isVideo) { bitmap = ThumbnailUtil.createVideoThumbnail(filePath); - if (kind == MICRO_KIND) { + if (kind == MICRO_KIND && bitmap != null) { bitmap = ThumbnailUtil.extractMiniThumb(bitmap, ThumbnailUtil.MINI_THUMB_TARGET_SIZE, ThumbnailUtil.MINI_THUMB_TARGET_SIZE, @@ -669,7 +697,8 @@ public final class MediaStore { * @param origId original image id */ public static void cancelThumbnailRequest(ContentResolver cr, long origId) { - InternalThumbnails.cancelThumbnailRequest(cr, origId, EXTERNAL_CONTENT_URI); + InternalThumbnails.cancelThumbnailRequest(cr, origId, EXTERNAL_CONTENT_URI, + InternalThumbnails.DEFAULT_GROUP_ID); } /** @@ -685,7 +714,39 @@ public final class MediaStore { */ public static Bitmap getThumbnail(ContentResolver cr, long origId, int kind, BitmapFactory.Options options) { - return InternalThumbnails.getThumbnail(cr, origId, kind, options, + return InternalThumbnails.getThumbnail(cr, origId, + InternalThumbnails.DEFAULT_GROUP_ID, kind, options, + EXTERNAL_CONTENT_URI, false); + } + + /** + * This method cancels the thumbnail request so clients waiting for getThumbnail will be + * interrupted and return immediately. Only the original process which made the getThumbnail + * requests can cancel their own requests. + * + * @param cr ContentResolver + * @param origId original image id + * @param groupId the same groupId used in getThumbnail. + */ + public static void cancelThumbnailRequest(ContentResolver cr, long origId, long groupId) { + InternalThumbnails.cancelThumbnailRequest(cr, origId, EXTERNAL_CONTENT_URI, groupId); + } + + /** + * This method checks if the thumbnails of the specified image (origId) has been created. + * It will be blocked until the thumbnails are generated. + * + * @param cr ContentResolver used to dispatch queries to MediaProvider. + * @param origId Original image id associated with thumbnail of interest. + * @param groupId the id of group to which this request belongs + * @param kind The type of thumbnail to fetch. Should be either MINI_KIND or MICRO_KIND. + * @param options this is only used for MINI_KIND when decoding the Bitmap + * @return A Bitmap instance. It could be null if the original image + * associated with origId doesn't exist or memory is not enough. + */ + public static Bitmap getThumbnail(ContentResolver cr, long origId, long groupId, + int kind, BitmapFactory.Options options) { + return InternalThumbnails.getThumbnail(cr, origId, groupId, kind, options, EXTERNAL_CONTENT_URI, false); } @@ -786,7 +847,6 @@ public final class MediaStore { * The position, in ms, playback was at when playback for this file * was last stopped. * <P>Type: INTEGER (long)</P> - * @hide */ public static final String BOOKMARK = "bookmark"; @@ -865,7 +925,6 @@ public final class MediaStore { /** * Non-zero if the audio file is a podcast * <P>Type: INTEGER (boolean)</P> - * @hide */ public static final String IS_PODCAST = "is_podcast"; @@ -906,7 +965,7 @@ public final class MediaStore { public static String keyFor(String name) { if (name != null) { boolean sortfirst = false; - if (name.equals(android.media.MediaFile.UNKNOWN_STRING)) { + if (name.equals(UNKNOWN_STRING)) { return "\001"; } // Check if the first character is \001. We use this to @@ -1194,6 +1253,27 @@ public final class MediaStore { } /** + * Convenience method to move a playlist item to a new location + * @param res The content resolver to use + * @param playlistId The numeric id of the playlist + * @param from The position of the item to move + * @param to The position to move the item to + * @return true on success + */ + public static final boolean moveItem(ContentResolver res, + long playlistId, int from, int to) { + Uri uri = MediaStore.Audio.Playlists.Members.getContentUri("external", + playlistId) + .buildUpon() + .appendEncodedPath(String.valueOf(from)) + .appendQueryParameter("move", "true") + .build(); + ContentValues values = new ContentValues(); + values.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, to); + return res.update(uri, values, null, null) != 0; + } + + /** * The ID within the playlist. */ public static final String _ID = "_id"; @@ -1598,7 +1678,26 @@ public final class MediaStore { * @param origId original video id */ public static void cancelThumbnailRequest(ContentResolver cr, long origId) { - InternalThumbnails.cancelThumbnailRequest(cr, origId, EXTERNAL_CONTENT_URI); + InternalThumbnails.cancelThumbnailRequest(cr, origId, EXTERNAL_CONTENT_URI, + InternalThumbnails.DEFAULT_GROUP_ID); + } + + /** + * This method checks if the thumbnails of the specified image (origId) has been created. + * It will be blocked until the thumbnails are generated. + * + * @param cr ContentResolver used to dispatch queries to MediaProvider. + * @param origId Original image id associated with thumbnail of interest. + * @param kind The type of thumbnail to fetch. Should be either MINI_KIND or MICRO_KIND. + * @param options this is only used for MINI_KIND when decoding the Bitmap + * @return A Bitmap instance. It could be null if the original image + * associated with origId doesn't exist or memory is not enough. + */ + public static Bitmap getThumbnail(ContentResolver cr, long origId, int kind, + BitmapFactory.Options options) { + return InternalThumbnails.getThumbnail(cr, origId, + InternalThumbnails.DEFAULT_GROUP_ID, kind, options, + EXTERNAL_CONTENT_URI, true); } /** @@ -1607,18 +1706,32 @@ public final class MediaStore { * * @param cr ContentResolver used to dispatch queries to MediaProvider. * @param origId Original image id associated with thumbnail of interest. + * @param groupId the id of group to which this request belongs * @param kind The type of thumbnail to fetch. Should be either MINI_KIND or MICRO_KIND * @param options this is only used for MINI_KIND when decoding the Bitmap * @return A Bitmap instance. It could be null if the original image associated with * origId doesn't exist or memory is not enough. */ - public static Bitmap getThumbnail(ContentResolver cr, long origId, int kind, - BitmapFactory.Options options) { - return InternalThumbnails.getThumbnail(cr, origId, kind, options, + public static Bitmap getThumbnail(ContentResolver cr, long origId, long groupId, + int kind, BitmapFactory.Options options) { + return InternalThumbnails.getThumbnail(cr, origId, groupId, kind, options, EXTERNAL_CONTENT_URI, true); } /** + * This method cancels the thumbnail request so clients waiting for getThumbnail will be + * interrupted and return immediately. Only the original process which made the getThumbnail + * requests can cancel their own requests. + * + * @param cr ContentResolver + * @param origId original video id + * @param groupId the same groupId used in getThumbnail. + */ + public static void cancelThumbnailRequest(ContentResolver cr, long origId, long groupId) { + InternalThumbnails.cancelThumbnailRequest(cr, origId, EXTERNAL_CONTENT_URI, groupId); + } + + /** * Get the content:// style URI for the image media table on the * given volume. * diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index f7e55db..7db9fdc 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -38,13 +38,16 @@ import android.os.*; import android.telephony.TelephonyManager; import android.text.TextUtils; import android.util.AndroidException; +import android.util.Config; import android.util.Log; import java.net.URISyntaxException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.Map; /** @@ -440,6 +443,7 @@ public final class Settings { public static final String AUTHORITY = "settings"; private static final String TAG = "Settings"; + private static final boolean LOCAL_LOGV = Config.LOGV || false; public static class SettingNotFoundException extends AndroidException { public SettingNotFoundException(String msg) { @@ -476,38 +480,59 @@ public final class Settings { private static class NameValueCache { private final String mVersionSystemProperty; - private final HashMap<String, String> mValues = Maps.newHashMap(); - private long mValuesVersion = 0; private final Uri mUri; - NameValueCache(String versionSystemProperty, Uri uri) { + // Must synchronize(mValues) to access mValues and mValuesVersion. + private final HashMap<String, String> mValues = new HashMap<String, String>(); + private long mValuesVersion = 0; + + public NameValueCache(String versionSystemProperty, Uri uri) { mVersionSystemProperty = versionSystemProperty; mUri = uri; } - String getString(ContentResolver cr, String name) { + public String getString(ContentResolver cr, String name) { long newValuesVersion = SystemProperties.getLong(mVersionSystemProperty, 0); - if (mValuesVersion != newValuesVersion) { - mValues.clear(); - mValuesVersion = newValuesVersion; + + synchronized (mValues) { + if (mValuesVersion != newValuesVersion) { + if (LOCAL_LOGV) { + Log.v(TAG, "invalidate [" + mUri.getLastPathSegment() + "]: current " + + newValuesVersion + " != cached " + mValuesVersion); + } + + mValues.clear(); + mValuesVersion = newValuesVersion; + } + + if (mValues.containsKey(name)) { + return mValues.get(name); // Could be null, that's OK -- negative caching + } } - if (!mValues.containsKey(name)) { - String value = null; - Cursor c = null; - try { - c = cr.query(mUri, new String[] { Settings.NameValueTable.VALUE }, - Settings.NameValueTable.NAME + "=?", new String[]{name}, null); - if (c != null && c.moveToNext()) value = c.getString(0); + + Cursor c = null; + try { + c = cr.query(mUri, new String[] { Settings.NameValueTable.VALUE }, + Settings.NameValueTable.NAME + "=?", new String[]{name}, null); + if (c == null) { + Log.w(TAG, "Can't get key " + name + " from " + mUri); + return null; + } + + String value = c.moveToNext() ? c.getString(0) : null; + synchronized (mValues) { mValues.put(name, value); - } catch (SQLException e) { - // SQL error: return null, but don't cache it. - Log.w(TAG, "Can't get key " + name + " from " + mUri, e); - } finally { - if (c != null) c.close(); + } + if (LOCAL_LOGV) { + Log.v(TAG, "cache miss [" + mUri.getLastPathSegment() + "]: " + + name + " = " + (value == null ? "(null)" : value)); } return value; - } else { - return mValues.get(name); + } catch (SQLException e) { + Log.w(TAG, "Can't get key " + name + " from " + mUri, e); + return null; // Return null, but don't cache it. + } finally { + if (c != null) c.close(); } } } @@ -1169,6 +1194,12 @@ public final class Settings { public static final String VOLUME_NOTIFICATION = "volume_notification"; /** + * Bluetooth Headset volume. This is used internally, changing this value will + * not change the volume. See AudioManager. + */ + public static final String VOLUME_BLUETOOTH_SCO = "volume_bluetooth_sco"; + + /** * Whether the notifications should use the ring volume (value of 1) or * a separate notification volume (value of 0). In most cases, users * will have this enabled so the notification and ringer volumes will be @@ -1189,7 +1220,7 @@ public final class Settings { */ public static final String[] VOLUME_SETTINGS = { VOLUME_VOICE, VOLUME_SYSTEM, VOLUME_RING, VOLUME_MUSIC, - VOLUME_ALARM, VOLUME_NOTIFICATION + VOLUME_ALARM, VOLUME_NOTIFICATION, VOLUME_BLUETOOTH_SCO }; /** @@ -1422,7 +1453,6 @@ public final class Settings { */ public static final String[] SETTINGS_TO_BACKUP = { STAY_ON_WHILE_PLUGGED_IN, - END_BUTTON_BEHAVIOR, WIFI_SLEEP_POLICY, WIFI_USE_STATIC_IP, WIFI_STATIC_IP, @@ -1447,12 +1477,14 @@ public final class Settings { VOLUME_MUSIC, VOLUME_ALARM, VOLUME_NOTIFICATION, + VOLUME_BLUETOOTH_SCO, VOLUME_VOICE + APPEND_FOR_LAST_AUDIBLE, VOLUME_SYSTEM + APPEND_FOR_LAST_AUDIBLE, VOLUME_RING + APPEND_FOR_LAST_AUDIBLE, VOLUME_MUSIC + APPEND_FOR_LAST_AUDIBLE, VOLUME_ALARM + APPEND_FOR_LAST_AUDIBLE, VOLUME_NOTIFICATION + APPEND_FOR_LAST_AUDIBLE, + VOLUME_BLUETOOTH_SCO + APPEND_FOR_LAST_AUDIBLE, TEXT_AUTO_REPLACE, TEXT_AUTO_CAPS, TEXT_AUTO_PUNCTUATE, @@ -2420,222 +2452,9 @@ public final class Settings { public static final String LAST_SETUP_SHOWN = "last_setup_shown"; /** - * @hide - */ - public static final String[] SETTINGS_TO_BACKUP = { - ADB_ENABLED, - ALLOW_MOCK_LOCATION, - PARENTAL_CONTROL_ENABLED, - PARENTAL_CONTROL_REDIRECT_URL, - USB_MASS_STORAGE_ENABLED, - ACCESSIBILITY_ENABLED, - ENABLED_ACCESSIBILITY_SERVICES, - TTS_USE_DEFAULTS, - TTS_DEFAULT_RATE, - TTS_DEFAULT_PITCH, - TTS_DEFAULT_SYNTH, - TTS_DEFAULT_LANG, - TTS_DEFAULT_COUNTRY, - WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON, - WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY, - WIFI_NUM_ALLOWED_CHANNELS, - WIFI_NUM_OPEN_NETWORKS_KEPT, - }; - - /** - * Helper method for determining if a location provider is enabled. - * @param cr the content resolver to use - * @param provider the location provider to query - * @return true if the provider is enabled - * - * @hide - */ - public static final boolean isLocationProviderEnabled(ContentResolver cr, String provider) { - String allowedProviders = Settings.Secure.getString(cr, LOCATION_PROVIDERS_ALLOWED); - if (allowedProviders != null) { - return (allowedProviders.equals(provider) || - allowedProviders.contains("," + provider + ",") || - allowedProviders.startsWith(provider + ",") || - allowedProviders.endsWith("," + provider)); - } - return false; - } - - /** - * Thread-safe method for enabling or disabling a single location provider. - * @param cr the content resolver to use - * @param provider the location provider to enable or disable - * @param enabled true if the provider should be enabled - * - * @hide - */ - public static final void setLocationProviderEnabled(ContentResolver cr, - String provider, boolean enabled) { - // to ensure thread safety, we write the provider name with a '+' or '-' - // and let the SettingsProvider handle it rather than reading and modifying - // the list of enabled providers. - if (enabled) { - provider = "+" + provider; - } else { - provider = "-" + provider; - } - putString(cr, Settings.Secure.LOCATION_PROVIDERS_ALLOWED, provider); - } - } - - /** - * Gservices settings, containing the network names for Google's - * various services. This table holds simple name/addr pairs. - * Addresses can be accessed through the getString() method. - * - * TODO: This should move to partner/google/... somewhere. - * - * @hide - */ - public static final class Gservices extends NameValueTable { - public static final String SYS_PROP_SETTING_VERSION = "sys.settings_gservices_version"; - - /** - * Intent action broadcast when the Gservices table is updated by the server. - * This is broadcast once after settings change (so many values may have been updated). - */ - @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) - public static final String CHANGED_ACTION = - "com.google.gservices.intent.action.GSERVICES_CHANGED"; - - /** - * Intent action to override Gservices for testing. (Requires WRITE_GSERVICES permission.) - */ - @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) - public static final String OVERRIDE_ACTION = - "com.google.gservices.intent.action.GSERVICES_OVERRIDE"; - - private static volatile NameValueCache mNameValueCache = null; - private static final Object mNameValueCacheLock = new Object(); - - /** - * Look up a name in the database. - * @param resolver to access the database with - * @param name to look up in the table - * @return the corresponding value, or null if not present - */ - public static String getString(ContentResolver resolver, String name) { - synchronized (mNameValueCacheLock) { - if (mNameValueCache == null) { - mNameValueCache = new NameValueCache(SYS_PROP_SETTING_VERSION, CONTENT_URI); - } - return mNameValueCache.getString(resolver, name); - } - } - - /** - * Store a name/value pair into the database. - * @param resolver to access the database with - * @param name to store - * @param value to associate with the name - * @return true if the value was set, false on database errors - */ - public static boolean putString(ContentResolver resolver, - String name, String value) { - return putString(resolver, CONTENT_URI, name, value); - } - - /** - * Look up the value for name in the database, convert it to an int using Integer.parseInt - * and return it. If it is null or if a NumberFormatException is caught during the - * conversion then return defValue. - */ - public static int getInt(ContentResolver resolver, String name, int defValue) { - String valString = getString(resolver, name); - int value; - try { - value = valString != null ? Integer.parseInt(valString) : defValue; - } catch (NumberFormatException e) { - value = defValue; - } - return value; - } - - /** - * Look up the value for name in the database, convert it to a long using Long.parseLong - * and return it. If it is null or if a NumberFormatException is caught during the - * conversion then return defValue. - */ - public static long getLong(ContentResolver resolver, String name, long defValue) { - String valString = getString(resolver, name); - long value; - try { - value = valString != null ? Long.parseLong(valString) : defValue; - } catch (NumberFormatException e) { - value = defValue; - } - return value; - } - - /** - * Construct the content URI for a particular name/value pair, - * useful for monitoring changes with a ContentObserver. - * @param name to look up in the table - * @return the corresponding content URI, or null if not present - */ - public static Uri getUriFor(String name) { - return getUriFor(CONTENT_URI, name); - } - - /** - * The content:// style URL for this table - */ - public static final Uri CONTENT_URI = - Uri.parse("content://" + AUTHORITY + "/gservices"); - - /** - * MMS - URL to use for HTTP "x-wap-profile" header - */ - public static final String MMS_X_WAP_PROFILE_URL - = "mms_x_wap_profile_url"; - - /** - * YouTube - the flag to indicate whether to use proxy - */ - public static final String YOUTUBE_USE_PROXY - = "youtube_use_proxy"; - - /** - * MMS - maximum message size in bytes for a MMS message. - */ - public static final String MMS_MAXIMUM_MESSAGE_SIZE - = "mms_maximum_message_size"; - - /** - * Event tags from the kernel event log to upload during checkin. - */ - public static final String CHECKIN_EVENTS = "checkin_events"; - - /** - * Comma-separated list of service names to dump and upload during checkin. - */ - public static final String CHECKIN_DUMPSYS_LIST = "checkin_dumpsys_list"; - - /** - * Comma-separated list of packages to specify for each service that is - * dumped (currently only meaningful for user activity). - */ - public static final String CHECKIN_PACKAGE_LIST = "checkin_package_list"; - - /** - * The interval (in seconds) between periodic checkin attempts. - */ - public static final String CHECKIN_INTERVAL = "checkin_interval"; - - /** - * Boolean indicating if the market app should force market only checkins on - * install/uninstall. Any non-0 value is considered true. - */ - public static final String MARKET_FORCE_CHECKIN = "market_force_checkin"; - - /** * How frequently (in seconds) to check the memory status of the * device. + * @hide */ public static final String MEMCHECK_INTERVAL = "memcheck_interval"; @@ -2643,6 +2462,7 @@ public final class Settings { * Max frequency (in seconds) to log memory check stats, in realtime * seconds. This allows for throttling of logs when the device is * running for large amounts of time. + * @hide */ public static final String MEMCHECK_LOG_REALTIME_INTERVAL = "memcheck_log_realtime_interval"; @@ -2650,6 +2470,7 @@ public final class Settings { /** * Boolean indicating whether rebooting due to system memory checks * is enabled. + * @hide */ public static final String MEMCHECK_SYSTEM_ENABLED = "memcheck_system_enabled"; @@ -2657,12 +2478,14 @@ public final class Settings { * How many bytes the system process must be below to avoid scheduling * a soft reboot. This reboot will happen when it is next determined * to be a good time. + * @hide */ public static final String MEMCHECK_SYSTEM_SOFT_THRESHOLD = "memcheck_system_soft"; /** * How many bytes the system process must be below to avoid scheduling * a hard reboot. This reboot will happen immediately. + * @hide */ public static final String MEMCHECK_SYSTEM_HARD_THRESHOLD = "memcheck_system_hard"; @@ -2670,18 +2493,21 @@ public final class Settings { * How many bytes the phone process must be below to avoid scheduling * a soft restart. This restart will happen when it is next determined * to be a good time. + * @hide */ public static final String MEMCHECK_PHONE_SOFT_THRESHOLD = "memcheck_phone_soft"; /** * How many bytes the phone process must be below to avoid scheduling * a hard restart. This restart will happen immediately. + * @hide */ public static final String MEMCHECK_PHONE_HARD_THRESHOLD = "memcheck_phone_hard"; /** * Boolean indicating whether restarting the phone process due to * memory checks is enabled. + * @hide */ public static final String MEMCHECK_PHONE_ENABLED = "memcheck_phone_enabled"; @@ -2689,6 +2515,7 @@ public final class Settings { * First time during the day it is okay to kill processes * or reboot the device due to low memory situations. This number is * in seconds since midnight. + * @hide */ public static final String MEMCHECK_EXEC_START_TIME = "memcheck_exec_start_time"; @@ -2696,6 +2523,7 @@ public final class Settings { * Last time during the day it is okay to kill processes * or reboot the device due to low memory situations. This number is * in seconds since midnight. + * @hide */ public static final String MEMCHECK_EXEC_END_TIME = "memcheck_exec_end_time"; @@ -2703,6 +2531,7 @@ public final class Settings { * How long the screen must have been off in order to kill processes * or reboot. This number is in seconds. A value of -1 means to * entirely disregard whether the screen is on. + * @hide */ public static final String MEMCHECK_MIN_SCREEN_OFF = "memcheck_min_screen_off"; @@ -2711,6 +2540,7 @@ public final class Settings { * or reboot. This number is in seconds. Note: this value must be * smaller than {@link #MEMCHECK_RECHECK_INTERVAL} or else it will * always see an alarm scheduled within its time. + * @hide */ public static final String MEMCHECK_MIN_ALARM = "memcheck_min_alarm"; @@ -2720,12 +2550,14 @@ public final class Settings { * this value must be larger than {@link #MEMCHECK_MIN_ALARM} or else * the alarm to schedule the recheck will always appear within the * minimum "do not execute now" time. + * @hide */ public static final String MEMCHECK_RECHECK_INTERVAL = "memcheck_recheck_interval"; /** * How frequently (in DAYS) to reboot the device. If 0, no reboots * will occur. + * @hide */ public static final String REBOOT_INTERVAL = "reboot_interval"; @@ -2733,6 +2565,7 @@ public final class Settings { * First time during the day it is okay to force a reboot of the * device (if REBOOT_INTERVAL is set). This number is * in seconds since midnight. + * @hide */ public static final String REBOOT_START_TIME = "reboot_start_time"; @@ -2741,674 +2574,117 @@ public final class Settings { * a reboot can be executed. If 0, a reboot will always be executed at * exactly the given time. Otherwise, it will only be executed if * the device is idle within the window. + * @hide */ public static final String REBOOT_WINDOW = "reboot_window"; /** - * The minimum version of the server that is required in order for the device to accept - * the server's recommendations about the initial sync settings to use. When this is unset, - * blank or can't be interpreted as an integer then we will not ask the server for a - * recommendation. - */ - public static final String GMAIL_CONFIG_INFO_MIN_SERVER_VERSION = - "gmail_config_info_min_server_version"; - - /** - * Controls whether Gmail offers a preview button for images. - */ - public static final String GMAIL_DISALLOW_IMAGE_PREVIEWS = "gmail_disallow_image_previews"; - - /** - * The maximal size in bytes allowed for attachments when composing messages in Gmail - */ - public static final String GMAIL_MAX_ATTACHMENT_SIZE = "gmail_max_attachment_size_bytes"; - - /** - * The timeout in milliseconds that Gmail uses when opening a connection and reading - * from it. A missing value or a value of -1 instructs Gmail to use the defaults provided - * by GoogleHttpClient. - */ - public static final String GMAIL_TIMEOUT_MS = "gmail_timeout_ms"; - - /** - * Controls whether Gmail will request an expedited sync when a message is sent. Value must - * be an integer where non-zero means true. Defaults to 1. - */ - public static final String GMAIL_SEND_IMMEDIATELY = "gmail_send_immediately"; - - /** - * Controls whether gmail buffers server responses. Possible values are "memory", for a - * memory-based buffer, or "file", for a temp-file-based buffer. All other values - * (including not set) disable buffering. - */ - public static final String GMAIL_BUFFER_SERVER_RESPONSE = "gmail_buffer_server_response"; - - /** - * The maximum size in bytes allowed for the provider to gzip a protocol buffer uploaded to - * the server. - */ - public static final String GMAIL_MAX_GZIP_SIZE = "gmail_max_gzip_size_bytes"; - - /** - * Controls whether Gmail will discard uphill operations that repeatedly fail. Value must be - * an integer where non-zero means true. Defaults to 1. This flag controls Donut devices. - */ - public static final String GMAIL_DISCARD_ERROR_UPHILL_OP = "gmail_discard_error_uphill_op"; - - /** - * Controls whether Gmail will discard uphill operations that repeatedly fail. Value must be - * an integer where non-zero means true. Defaults to 1. This flag controls Eclair and - * future devices. - */ - public static final String GMAIL_DISCARD_ERROR_UPHILL_OP_NEW = - "gmail_discard_error_uphill_op_new"; - - /** - * Controls how many attempts Gmail will try to upload an uphill operations before it - * abandons the operation. Defaults to 20. - */ - public static final String GMAIL_NUM_RETRY_UPHILL_OP = "gmail_num_retry_uphill_op"; - - /** - * How much time in seconds Gmail will try to upload an uphill operations before it - * abandons the operation. Defaults to 36400 (one day). - */ - public static final String GMAIL_WAIT_TIME_RETRY_UPHILL_OP = - "gmail_wait_time_retry_uphill_op"; - - /** - * Controls if the protocol buffer version of the protocol will use a multipart request for - * attachment uploads. Value must be an integer where non-zero means true. Defaults to 0. - */ - public static final String GMAIL_USE_MULTIPART_PROTOBUF = "gmail_use_multipart_protobuf"; - - /** - * the transcoder URL for mobile devices. - */ - public static final String TRANSCODER_URL = "mobile_transcoder_url"; - - /** - * URL that points to the privacy terms of the Google Talk service. - */ - public static final String GTALK_TERMS_OF_SERVICE_URL = "gtalk_terms_of_service_url"; - - /** - * Hostname of the GTalk server. - */ - public static final String GTALK_SERVICE_HOSTNAME = "gtalk_hostname"; - - /** - * Secure port of the GTalk server. - */ - public static final String GTALK_SERVICE_SECURE_PORT = "gtalk_secure_port"; - - /** - * The server configurable RMQ acking interval - */ - public static final String GTALK_SERVICE_RMQ_ACK_INTERVAL = "gtalk_rmq_ack_interval"; - - /** - * The minimum reconnect delay for short network outages or when the network is suspended - * due to phone use. - */ - public static final String GTALK_SERVICE_MIN_RECONNECT_DELAY_SHORT = - "gtalk_min_reconnect_delay_short"; - - /** - * The reconnect variant range for short network outages or when the network is suspended - * due to phone use. A random number between 0 and this constant is computed and - * added to {@link #GTALK_SERVICE_MIN_RECONNECT_DELAY_SHORT} to form the initial reconnect - * delay. - */ - public static final String GTALK_SERVICE_RECONNECT_VARIANT_SHORT = - "gtalk_reconnect_variant_short"; - - /** - * The minimum reconnect delay for long network outages - */ - public static final String GTALK_SERVICE_MIN_RECONNECT_DELAY_LONG = - "gtalk_min_reconnect_delay_long"; - - /** - * The reconnect variant range for long network outages. A random number between 0 and this - * constant is computed and added to {@link #GTALK_SERVICE_MIN_RECONNECT_DELAY_LONG} to - * form the initial reconnect delay. - */ - public static final String GTALK_SERVICE_RECONNECT_VARIANT_LONG = - "gtalk_reconnect_variant_long"; - - /** - * The maximum reconnect delay time, in milliseconds. - */ - public static final String GTALK_SERVICE_MAX_RECONNECT_DELAY = - "gtalk_max_reconnect_delay"; - - /** - * The network downtime that is considered "short" for the above calculations, - * in milliseconds. - */ - public static final String GTALK_SERVICE_SHORT_NETWORK_DOWNTIME = - "gtalk_short_network_downtime"; - - /** - * How frequently we send heartbeat pings to the GTalk server. Receiving a server packet - * will reset the heartbeat timer. The away heartbeat should be used when the user is - * logged into the GTalk app, but not actively using it. - */ - public static final String GTALK_SERVICE_AWAY_HEARTBEAT_INTERVAL_MS = - "gtalk_heartbeat_ping_interval_ms"; // keep the string backward compatible - - /** - * How frequently we send heartbeat pings to the GTalk server. Receiving a server packet - * will reset the heartbeat timer. The active heartbeat should be used when the user is - * actively using the GTalk app. - */ - public static final String GTALK_SERVICE_ACTIVE_HEARTBEAT_INTERVAL_MS = - "gtalk_active_heartbeat_ping_interval_ms"; - - /** - * How frequently we send heartbeat pings to the GTalk server. Receiving a server packet - * will reset the heartbeat timer. The sync heartbeat should be used when the user isn't - * logged into the GTalk app, but auto-sync is enabled. - */ - public static final String GTALK_SERVICE_SYNC_HEARTBEAT_INTERVAL_MS = - "gtalk_sync_heartbeat_ping_interval_ms"; - - /** - * How frequently we send heartbeat pings to the GTalk server. Receiving a server packet - * will reset the heartbeat timer. The no sync heartbeat should be used when the user isn't - * logged into the GTalk app, and auto-sync is not enabled. - */ - public static final String GTALK_SERVICE_NOSYNC_HEARTBEAT_INTERVAL_MS = - "gtalk_nosync_heartbeat_ping_interval_ms"; - - /** - * The maximum heartbeat interval used while on the WIFI network. - */ - public static final String GTALK_SERVICE_WIFI_MAX_HEARTBEAT_INTERVAL_MS = - "gtalk_wifi_max_heartbeat_ping_interval_ms"; - - /** - * How long we wait to receive a heartbeat ping acknowledgement (or another packet) - * from the GTalk server, before deeming the connection dead. - */ - public static final String GTALK_SERVICE_HEARTBEAT_ACK_TIMEOUT_MS = - "gtalk_heartbeat_ack_timeout_ms"; - - /** - * How long after screen is turned off before we consider the user to be idle. - */ - public static final String GTALK_SERVICE_IDLE_TIMEOUT_MS = - "gtalk_idle_timeout_ms"; - - /** - * By default, GTalkService will always connect to the server regardless of the auto-sync - * setting. However, if this parameter is true, then GTalkService will only connect - * if auto-sync is enabled. Using the GTalk app will trigger the connection too. - */ - public static final String GTALK_SERVICE_CONNECT_ON_AUTO_SYNC = - "gtalk_connect_on_auto_sync"; - - /** - * GTalkService holds a wakelock while broadcasting the intent for data message received. - * It then automatically release the wakelock after a timeout. This setting controls what - * the timeout should be. - */ - public static final String GTALK_DATA_MESSAGE_WAKELOCK_MS = - "gtalk_data_message_wakelock_ms"; - - /** - * The socket read timeout used to control how long ssl handshake wait for reads before - * timing out. This is needed so the ssl handshake doesn't hang for a long time in some - * circumstances. - */ - public static final String GTALK_SSL_HANDSHAKE_TIMEOUT_MS = - "gtalk_ssl_handshake_timeout_ms"; - - /** - * Compress the gtalk stream. - */ - public static final String GTALK_COMPRESS = "gtalk_compress"; - - /** - * This is the timeout for which Google Talk will send the message using bareJID. In a - * established chat between two XMPP endpoints, Google Talk uses fullJID in the format - * of user@domain/resource in order to send the message to the specific client. However, - * if Google Talk hasn't received a message from that client after some time, it would - * fall back to use the bareJID, which would broadcast the message to all clients for - * the other user. - */ - public static final String GTALK_USE_BARE_JID_TIMEOUT_MS = "gtalk_use_barejid_timeout_ms"; - - /** - * This is the threshold of retry number when there is an authentication expired failure - * for Google Talk. In some situation, e.g. when a Google Apps account is disabled chat - * service, the connection keeps failing. This threshold controls when we should stop - * the retrying. - */ - public static final String GTALK_MAX_RETRIES_FOR_AUTH_EXPIRED = - "gtalk_max_retries_for_auth_expired"; - - /** - * a boolean setting indicating whether the GTalkService should use RMQ2 protocol or not. - */ - public static final String GTALK_USE_RMQ2_PROTOCOL = - "gtalk_use_rmq2"; - - /** - * a boolean setting indicating whether the GTalkService should support both RMQ and - * RMQ2 protocols. This setting is true for the transitional period when we need to - * support both protocols. - */ - public static final String GTALK_SUPPORT_RMQ_AND_RMQ2_PROTOCOLS = - "gtalk_support_rmq_and_rmq2"; - - /** - * a boolean setting controlling whether the rmq2 protocol will include stream ids in - * the protobufs. This is used for debugging. - */ - public static final String GTALK_RMQ2_INCLUDE_STREAM_ID = - "gtalk_rmq2_include_stream_id"; - - /** - * when receiving a chat message from the server, the message could be an older message - * whose "time sent" is x seconds from now. If x is significant enough, we want to flag - * it so the UI can give it some special treatment when displaying the "time sent" for - * it. This setting is to control what x is. - */ - public static final String GTALK_OLD_CHAT_MESSAGE_THRESHOLD_IN_SEC = - "gtalk_old_chat_msg_threshold_in_sec"; - - /** - * a setting to control the max connection history record GTalkService stores. - */ - public static final String GTALK_MAX_CONNECTION_HISTORY_RECORDS = - "gtalk_max_conn_history_records"; - - /** - * This is gdata url to lookup album and picture info from picasa web. It also controls - * whether url scraping for picasa is enabled (NULL to disable). - */ - public static final String GTALK_PICASA_ALBUM_URL = - "gtalk_picasa_album_url"; - - /** - * This is the url to lookup picture info from flickr. It also controls - * whether url scraping for flickr is enabled (NULL to disable). - */ - public static final String GTALK_FLICKR_PHOTO_INFO_URL = - "gtalk_flickr_photo_info_url"; - - /** - * This is the url to lookup an actual picture from flickr. - */ - public static final String GTALK_FLICKR_PHOTO_URL = - "gtalk_flickr_photo_url"; - - /** - * This is the gdata url to lookup info on a youtube video. It also controls - * whether url scraping for youtube is enabled (NULL to disable). - */ - public static final String GTALK_YOUTUBE_VIDEO_URL = - "gtalk_youtube_video_url"; - - /** - * Enable/disable GTalk URL scraping for JPG images ("true" to enable). - */ - public static final String GTALK_URL_SCRAPING_FOR_JPG = - "gtalk_url_scraping_for_jpg"; - - /** - * Chat message lifetime (for pruning old chat messages). - */ - public static final String GTALK_CHAT_MESSAGE_LIFETIME = - "gtalk_chat_message_lifetime"; - - /** - * OTR message lifetime (for pruning old otr messages). - */ - public static final String GTALK_OTR_MESSAGE_LIFETIME = - "gtalk_otr_message_lifetime"; - - /** - * Chat expiration time, i.e., time since last message in the chat (for pruning old chats). - */ - public static final String GTALK_CHAT_EXPIRATION_TIME = - "gtalk_chat_expiration_time"; - - /** - * This is the url for getting the app token for server-to-device push messaging. - */ - public static final String PUSH_MESSAGING_REGISTRATION_URL = - "push_messaging_registration_url"; - - /** - * Use android://<it> routing infos for Google Sync Server subcriptions. - */ - public static final String GSYNC_USE_RMQ2_ROUTING_INFO = "gsync_use_rmq2_routing_info"; - - /** - * Enable use of ssl session caching. - * 'db' - save each session in a (per process) database - * 'file' - save each session in a (per process) file - * not set or any other value - normal java in-memory caching - */ - public static final String SSL_SESSION_CACHE = "ssl_session_cache"; - - /** - * How many bytes long a message has to be, in order to be gzipped. - */ - public static final String SYNC_MIN_GZIP_BYTES = - "sync_min_gzip_bytes"; - - /** - * The hash value of the current provisioning settings - */ - public static final String PROVISIONING_DIGEST = "digest"; - - /** - * Provisioning keys to block from server update - */ - public static final String PROVISIONING_OVERRIDE = "override"; - - /** - * "Generic" service name for authentication requests. - */ - public static final String GOOGLE_LOGIN_GENERIC_AUTH_SERVICE - = "google_login_generic_auth_service"; - - /** - * Frequency in milliseconds at which we should sync the locally installed Vending Machine - * content with the server. - */ - public static final String VENDING_SYNC_FREQUENCY_MS = "vending_sync_frequency_ms"; - - /** - * Support URL that is opened in a browser when user clicks on 'Help and Info' in Vending - * Machine. - */ - public static final String VENDING_SUPPORT_URL = "vending_support_url"; - - /** - * Indicates if Vending Machine requires a SIM to be in the phone to allow a purchase. - * - * true = SIM is required - * false = SIM is not required - */ - public static final String VENDING_REQUIRE_SIM_FOR_PURCHASE = - "vending_require_sim_for_purchase"; - - /** - * Indicates the Vending Machine backup state. It is set if the - * Vending application has been backed up at least once. - */ - public static final String VENDING_BACKUP_STATE = "vending_backup_state"; - - /** - * The current version id of the Vending Machine terms of service. - */ - public static final String VENDING_TOS_VERSION = "vending_tos_version"; - - /** - * URL that points to the terms of service for Vending Machine. - */ - public static final String VENDING_TOS_URL = "vending_tos_url"; - - /** - * URL to navigate to in browser (not Market) when the terms of service - * for Vending Machine could not be accessed due to bad network - * connection. - */ - public static final String VENDING_TOS_MISSING_URL = "vending_tos_missing_url"; - - /** - * Whether to use sierraqa instead of sierra tokens for the purchase flow in - * Vending Machine. - * - * true = use sierraqa - * false = use sierra (default) - */ - public static final String VENDING_USE_CHECKOUT_QA_SERVICE = - "vending_use_checkout_qa_service"; - - /** - * Default value to use for all/free/priced filter in Market. - * Valid values: ALL, FREE, PAID (case insensitive) - */ - public static final String VENDING_DEFAULT_FILTER = "vending_default_filter"; - /** - * Ranking type value to use for the first category tab (currently popular) - */ - public static final String VENDING_TAB_1_RANKING_TYPE = "vending_tab_1_ranking_type"; - - /** - * Title string to use for first category tab. - */ - public static final String VENDING_TAB_1_TITLE = "vending_tab_1_title"; - - /** - * Ranking type value to use for the second category tab (currently newest) - */ - public static final String VENDING_TAB_2_RANKING_TYPE = "vending_tab_2_ranking_type"; - - /** - * Title string to use for second category tab. - */ - public static final String VENDING_TAB_2_TITLE = "vending_tab_2_title"; - - /** - * Frequency in milliseconds at which we should request MCS heartbeats - * from the Vending Machine client. - */ - public static final String VENDING_HEARTBEAT_FREQUENCY_MS = - "vending_heartbeat_frequency_ms"; - - /** - * Frequency in milliseconds at which we should resend pending download - * requests to the API Server from the Vending Machine client. - */ - public static final String VENDING_PENDING_DOWNLOAD_RESEND_FREQUENCY_MS = - "vending_pd_resend_frequency_ms"; - - /** - * Time before an asset in the 'DOWNLOADING' state is considered ready - * for an install kick on the client. - */ - public static final String VENDING_DOWNLOADING_KICK_TIMEOUT_MS = - "vending_downloading_kick_ms"; - - /** - * Size of buffer in bytes for Vending to use when reading cache files. - */ - public static final String VENDING_DISK_INPUT_BUFFER_BYTES = - "vending_disk_input_buffer_bytes"; - - /** - * Size of buffer in bytes for Vending to use when writing cache files. - */ - public static final String VENDING_DISK_OUTPUT_BUFFER_BYTES = - "vending_disk_output_buffer_bytes"; - - /** - * Frequency in milliseconds at which we should cycle through the promoted applications - * on the home screen or the categories page. - */ - public static final String VENDING_PROMO_REFRESH_FREQUENCY_MS = - "vending_promo_refresh_freq_ms"; - - /** - * Frequency in milliseconds when we should refresh the provisioning information from - * the carrier backend. - */ - public static final String VENDING_CARRIER_PROVISIONING_REFRESH_FREQUENCY_MS = - "vending_carrier_ref_freq_ms"; - - /** - * Interval in milliseconds after which a failed provisioning request should be retried. - */ - public static final String VENDING_CARRIER_PROVISIONING_RETRY_MS = - "vending_carrier_prov_retry_ms"; - - /** - * Buffer in milliseconds for carrier credentials to be considered valid. - */ - public static final String VENDING_CARRIER_CREDENTIALS_BUFFER_MS = - "vending_carrier_cred_buf_ms"; - - /** - * URL that points to the legal terms of service to display in Settings. - * <p> - * This should be a https URL. For a pretty user-friendly URL, use - * {@link #SETTINGS_TOS_PRETTY_URL}. - */ - public static final String SETTINGS_TOS_URL = "settings_tos_url"; - - /** - * URL that points to the legal terms of service to display in Settings. - * <p> - * This should be a pretty http URL. For the URL the device will access - * via Settings, use {@link #SETTINGS_TOS_URL}. - */ - public static final String SETTINGS_TOS_PRETTY_URL = "settings_tos_pretty_url"; - - /** - * URL that points to the contributors to display in Settings. - * <p> - * This should be a https URL. For a pretty user-friendly URL, use - * {@link #SETTINGS_CONTRIBUTORS_PRETTY_URL}. - */ - public static final String SETTINGS_CONTRIBUTORS_URL = "settings_contributors_url"; - - /** - * URL that points to the contributors to display in Settings. - * <p> - * This should be a pretty http URL. For the URL the device will access - * via Settings, use {@link #SETTINGS_CONTRIBUTORS_URL}. + * Threshold values for the duration and level of a discharge cycle, under + * which we log discharge cycle info. + * @hide */ - public static final String SETTINGS_CONTRIBUTORS_PRETTY_URL = - "settings_contributors_pretty_url"; + public static final String BATTERY_DISCHARGE_DURATION_THRESHOLD = + "battery_discharge_duration_threshold"; + /** @hide */ + public static final String BATTERY_DISCHARGE_THRESHOLD = "battery_discharge_threshold"; /** - * URL that points to the Terms Of Service for the device. - * <p> - * This should be a pretty http URL. + * Flag for allowing ActivityManagerService to send ACTION_APP_ERROR intents + * on application crashes and ANRs. If this is disabled, the crash/ANR dialog + * will never display the "Report" button. + * Type: int ( 0 = disallow, 1 = allow ) + * @hide */ - public static final String SETUP_GOOGLE_TOS_URL = "setup_google_tos_url"; + public static final String SEND_ACTION_APP_ERROR = "send_action_app_error"; /** - * URL that points to the Android privacy policy for the device. - * <p> - * This should be a pretty http URL. + * Nonzero causes Log.wtf() to crash. + * @hide */ - public static final String SETUP_ANDROID_PRIVACY_URL = "setup_android_privacy_url"; + public static final String WTF_IS_FATAL = "wtf_is_fatal"; /** - * URL that points to the Google privacy policy for the device. - * <p> - * This should be a pretty http URL. + * Maximum age of entries kept by {@link android.os.IDropBox}. + * @hide */ - public static final String SETUP_GOOGLE_PRIVACY_URL = "setup_google_privacy_url"; - + public static final String DROPBOX_AGE_SECONDS = + "dropbox_age_seconds"; /** - * Request an MSISDN token for various Google services. + * Maximum amount of disk space used by {@link android.os.IDropBox} no matter what. + * @hide */ - public static final String USE_MSISDN_TOKEN = "use_msisdn_token"; - + public static final String DROPBOX_QUOTA_KB = + "dropbox_quota_kb"; /** - * RSA public key used to encrypt passwords stored in the database. + * Percent of free disk (excluding reserve) which {@link android.os.IDropBox} will use. + * @hide */ - public static final String GLS_PUBLIC_KEY = "google_login_public_key"; - + public static final String DROPBOX_QUOTA_PERCENT = + "dropbox_quota_percent"; /** - * Only check parental control status if this is set to "true". + * Percent of total disk which {@link android.os.IDropBox} will never dip into. + * @hide */ - public static final String PARENTAL_CONTROL_CHECK_ENABLED = - "parental_control_check_enabled"; - + public static final String DROPBOX_RESERVE_PERCENT = + "dropbox_reserve_percent"; /** - * The list of applications we need to block if parental control is - * enabled. + * Prefix for per-tag dropbox disable/enable settings. + * @hide */ - public static final String PARENTAL_CONTROL_APPS_LIST = - "parental_control_apps_list"; + public static final String DROPBOX_TAG_PREFIX = + "dropbox:"; - /** - * Duration in which parental control status is valid. - */ - public static final String PARENTAL_CONTROL_TIMEOUT_IN_MS = - "parental_control_timeout_in_ms"; /** - * When parental control is off, we expect to get this string from the - * litmus url. + * Screen timeout in milliseconds corresponding to the + * PowerManager's POKE_LOCK_SHORT_TIMEOUT flag (i.e. the fastest + * possible screen timeout behavior.) + * @hide */ - public static final String PARENTAL_CONTROL_EXPECTED_RESPONSE = - "parental_control_expected_response"; + public static final String SHORT_KEYLIGHT_DELAY_MS = + "short_keylight_delay_ms"; /** - * When the litmus url returns a 302, declare parental control to be on - * only if the redirect url matches this regular expression. + * The interval in minutes after which the amount of free storage left on the + * device is logged to the event log + * @hide */ - public static final String PARENTAL_CONTROL_REDIRECT_REGEX = - "parental_control_redirect_regex"; + public static final String SYS_FREE_STORAGE_LOG_INTERVAL = + "sys_free_storage_log_interval"; /** * Threshold for the amount of change in disk free space required to report the amount of * free space. Used to prevent spamming the logs when the disk free space isn't changing * frequently. + * @hide */ public static final String DISK_FREE_CHANGE_REPORTING_THRESHOLD = "disk_free_change_reporting_threshold"; - /** - * Prefix for new Google services published by the checkin - * server. - */ - public static final String GOOGLE_SERVICES_PREFIX - = "google_services:"; - - /** - * The maximum reconnect delay for short network outages or when the network is suspended - * due to phone use. - */ - public static final String SYNC_MAX_RETRY_DELAY_IN_SECONDS = - "sync_max_retry_delay_in_seconds"; /** * Minimum percentage of free storage on the device that is used to determine if * the device is running low on storage. * Say this value is set to 10, the device is considered running low on storage * if 90% or more of the device storage is filled up. + * @hide */ public static final String SYS_STORAGE_THRESHOLD_PERCENTAGE = "sys_storage_threshold_percentage"; /** - * The interval in minutes after which the amount of free storage left on the - * device is logged to the event log - */ - public static final String SYS_FREE_STORAGE_LOG_INTERVAL = - "sys_free_storage_log_interval"; - - /** - * The interval in milliseconds at which to check the number of SMS sent - * out without asking for use permit, to limit the un-authorized SMS - * usage. - */ - public static final String SMS_OUTGOING_CHECK_INTERVAL_MS = - "sms_outgoing_check_interval_ms"; - - /** - * The number of outgoing SMS sent without asking for user permit - * (of {@link #SMS_OUTGOING_CHECK_INTERVAL_MS} + * The interval in milliseconds after which Wi-Fi is considered idle. + * When idle, it is possible for the device to be switched from Wi-Fi to + * the mobile data network. + * @hide */ - public static final String SMS_OUTGOING_CEHCK_MAX_COUNT = - "sms_outgoing_check_max_count"; + public static final String WIFI_IDLE_MS = "wifi_idle_ms"; /** * The interval in milliseconds at which to check packet counts on the * mobile data interface when screen is on, to detect possible data * connection problems. + * @hide */ public static final String PDP_WATCHDOG_POLL_INTERVAL_MS = "pdp_watchdog_poll_interval_ms"; @@ -3417,6 +2693,7 @@ public final class Settings { * The interval in milliseconds at which to check packet counts on the * mobile data interface when screen is off, to detect possible data * connection problems. + * @hide */ public static final String PDP_WATCHDOG_LONG_POLL_INTERVAL_MS = "pdp_watchdog_long_poll_interval_ms"; @@ -3425,6 +2702,7 @@ public final class Settings { * The interval in milliseconds at which to check packet counts on the * mobile data interface after {@link #PDP_WATCHDOG_TRIGGER_PACKET_COUNT} * outgoing packets has been reached without incoming packets. + * @hide */ public static final String PDP_WATCHDOG_ERROR_POLL_INTERVAL_MS = "pdp_watchdog_error_poll_interval_ms"; @@ -3433,6 +2711,7 @@ public final class Settings { * The number of outgoing packets sent without seeing an incoming packet * that triggers a countdown (of {@link #PDP_WATCHDOG_ERROR_POLL_COUNT} * device is logged to the event log + * @hide */ public static final String PDP_WATCHDOG_TRIGGER_PACKET_COUNT = "pdp_watchdog_trigger_packet_count"; @@ -3441,6 +2720,7 @@ public final class Settings { * The number of polls to perform (at {@link #PDP_WATCHDOG_ERROR_POLL_INTERVAL_MS}) * after hitting {@link #PDP_WATCHDOG_TRIGGER_PACKET_COUNT} before * attempting data connection recovery. + * @hide */ public static final String PDP_WATCHDOG_ERROR_POLL_COUNT = "pdp_watchdog_error_poll_count"; @@ -3448,6 +2728,7 @@ public final class Settings { /** * The number of failed PDP reset attempts before moving to something more * drastic: re-registering to the network. + * @hide */ public static final String PDP_WATCHDOG_MAX_PDP_RESET_FAIL_COUNT = "pdp_watchdog_max_pdp_reset_fail_count"; @@ -3455,12 +2736,14 @@ public final class Settings { /** * Address to ping as a last sanity check before attempting any recovery. * Unset or set to "0.0.0.0" to skip this check. + * @hide */ public static final String PDP_WATCHDOG_PING_ADDRESS = "pdp_watchdog_ping_address"; /** * The "-w deadline" parameter for the ping, ie, the max time in * seconds to spend pinging. + * @hide */ public static final String PDP_WATCHDOG_PING_DEADLINE = "pdp_watchdog_ping_deadline"; @@ -3469,221 +2752,269 @@ public final class Settings { * after the first registration mismatch of gprs and voice service, * to detect possible data network registration problems. * + * @hide */ public static final String GPRS_REGISTER_CHECK_PERIOD_MS = "gprs_register_check_period_ms"; /** - * The interval in milliseconds after which Wi-Fi is considered idle. - * When idle, it is possible for the device to be switched from Wi-Fi to - * the mobile data network. - */ - public static final String WIFI_IDLE_MS = "wifi_idle_ms"; - - /** - * Screen timeout in milliseconds corresponding to the - * PowerManager's POKE_LOCK_SHORT_TIMEOUT flag (i.e. the fastest - * possible screen timeout behavior.) - */ - public static final String SHORT_KEYLIGHT_DELAY_MS = - "short_keylight_delay_ms"; - - /** - * List of test suites (local disk filename) for the automatic instrumentation test runner. - * The file format is similar to automated_suites.xml, see AutoTesterService. - * If this setting is missing or empty, the automatic test runner will not start. - */ - public static final String AUTOTEST_SUITES_FILE = "autotest_suites_file"; - - /** - * Interval between synchronous checkins forced by the automatic test runner. - * If you set this to a value smaller than CHECKIN_INTERVAL, then the test runner's - * frequent checkins will prevent asynchronous background checkins from interfering - * with any performance measurements. + * The length of time in milli-seconds that automatic small adjustments to + * SystemClock are ignored if NITZ_UPDATE_DIFF is not exceeded. + * @hide */ - public static final String AUTOTEST_CHECKIN_SECONDS = "autotest_checkin_seconds"; + public static final String NITZ_UPDATE_SPACING = "nitz_update_spacing"; /** - * Interval between reboots forced by the automatic test runner. + * If the NITZ_UPDATE_DIFF time is exceeded then an automatic adjustment + * to SystemClock will be allowed even if NITZ_UPDATE_SPACING has not been + * exceeded. + * @hide */ - public static final String AUTOTEST_REBOOT_SECONDS = "autotest_reboot_seconds"; - + public static final String NITZ_UPDATE_DIFF = "nitz_update_diff"; /** - * Threshold values for the duration and level of a discharge cycle, under - * which we log discharge cycle info. + * The maximum reconnect delay for short network outages or when the network is suspended + * due to phone use. + * @hide */ - public static final String BATTERY_DISCHARGE_DURATION_THRESHOLD = - "battery_discharge_duration_threshold"; - public static final String BATTERY_DISCHARGE_THRESHOLD = "battery_discharge_threshold"; + public static final String SYNC_MAX_RETRY_DELAY_IN_SECONDS = + "sync_max_retry_delay_in_seconds"; /** - * An email address that anr bugreports should be sent to. + * The interval in milliseconds at which to check the number of SMS sent + * out without asking for use permit, to limit the un-authorized SMS + * usage. + * @hide */ - public static final String ANR_BUGREPORT_RECIPIENT = "anr_bugreport_recipient"; + public static final String SMS_OUTGOING_CHECK_INTERVAL_MS = + "sms_outgoing_check_interval_ms"; /** - * Flag for allowing service provider to use location information to improve products and - * services. - * Type: int ( 0 = disallow, 1 = allow ) - * @deprecated + * The number of outgoing SMS sent without asking for user permit + * (of {@link #SMS_OUTGOING_CHECK_INTERVAL_MS} + * @hide */ - public static final String USE_LOCATION_FOR_SERVICES = "use_location"; + public static final String SMS_OUTGOING_CHECK_MAX_COUNT = + "sms_outgoing_check_max_count"; /** - * The length of the calendar sync window into the future. - * This specifies the number of days into the future for the sliding window sync. - * Setting this to zero will disable sliding sync. + * Enable use of ssl session caching. + * 'db' - save each session in a (per process) database + * 'file' - save each session in a (per process) file + * not set or any other value - normal java in-memory caching + * @hide */ - public static final String GOOGLE_CALENDAR_SYNC_WINDOW_DAYS = - "google_calendar_sync_window_days"; + public static final String SSL_SESSION_CACHE = "ssl_session_cache"; /** - * How often to update the calendar sync window. - * The window will be advanced every n days. + * How many bytes long a message has to be, in order to be gzipped. + * @hide */ - public static final String GOOGLE_CALENDAR_SYNC_WINDOW_UPDATE_DAYS = - "google_calendar_sync_window_update_days"; + public static final String SYNC_MIN_GZIP_BYTES = + "sync_min_gzip_bytes"; /** * The number of promoted sources in GlobalSearch. + * @hide */ public static final String SEARCH_NUM_PROMOTED_SOURCES = "search_num_promoted_sources"; /** * The maximum number of suggestions returned by GlobalSearch. + * @hide */ public static final String SEARCH_MAX_RESULTS_TO_DISPLAY = "search_max_results_to_display"; /** * The number of suggestions GlobalSearch will ask each non-web search source for. + * @hide */ public static final String SEARCH_MAX_RESULTS_PER_SOURCE = "search_max_results_per_source"; /** * The number of suggestions the GlobalSearch will ask the web search source for. + * @hide */ public static final String SEARCH_WEB_RESULTS_OVERRIDE_LIMIT = "search_web_results_override_limit"; /** * The number of milliseconds that GlobalSearch will wait for suggestions from * promoted sources before continuing with all other sources. + * @hide */ public static final String SEARCH_PROMOTED_SOURCE_DEADLINE_MILLIS = "search_promoted_source_deadline_millis"; /** * The number of milliseconds before GlobalSearch aborts search suggesiton queries. + * @hide */ public static final String SEARCH_SOURCE_TIMEOUT_MILLIS = "search_source_timeout_millis"; /** * The maximum number of milliseconds that GlobalSearch shows the previous results * after receiving a new query. + * @hide */ public static final String SEARCH_PREFILL_MILLIS = "search_prefill_millis"; /** * The maximum age of log data used for shortcuts in GlobalSearch. + * @hide */ public static final String SEARCH_MAX_STAT_AGE_MILLIS = "search_max_stat_age_millis"; /** * The maximum age of log data used for source ranking in GlobalSearch. + * @hide */ public static final String SEARCH_MAX_SOURCE_EVENT_AGE_MILLIS = "search_max_source_event_age_millis"; /** * The minimum number of impressions needed to rank a source in GlobalSearch. + * @hide */ public static final String SEARCH_MIN_IMPRESSIONS_FOR_SOURCE_RANKING = "search_min_impressions_for_source_ranking"; /** * The minimum number of clicks needed to rank a source in GlobalSearch. + * @hide */ public static final String SEARCH_MIN_CLICKS_FOR_SOURCE_RANKING = "search_min_clicks_for_source_ranking"; /** * The maximum number of shortcuts shown by GlobalSearch. + * @hide */ public static final String SEARCH_MAX_SHORTCUTS_RETURNED = "search_max_shortcuts_returned"; /** * The size of the core thread pool for suggestion queries in GlobalSearch. + * @hide */ public static final String SEARCH_QUERY_THREAD_CORE_POOL_SIZE = "search_query_thread_core_pool_size"; /** * The maximum size of the thread pool for suggestion queries in GlobalSearch. + * @hide */ public static final String SEARCH_QUERY_THREAD_MAX_POOL_SIZE = "search_query_thread_max_pool_size"; /** * The size of the core thread pool for shortcut refreshing in GlobalSearch. + * @hide */ public static final String SEARCH_SHORTCUT_REFRESH_CORE_POOL_SIZE = "search_shortcut_refresh_core_pool_size"; /** * The maximum size of the thread pool for shortcut refreshing in GlobalSearch. + * @hide */ public static final String SEARCH_SHORTCUT_REFRESH_MAX_POOL_SIZE = "search_shortcut_refresh_max_pool_size"; /** * The maximun time that excess threads in the GlobalSeach thread pools will * wait before terminating. + * @hide */ public static final String SEARCH_THREAD_KEEPALIVE_SECONDS = "search_thread_keepalive_seconds"; /** * The maximum number of concurrent suggestion queries to each source. + * @hide */ public static final String SEARCH_PER_SOURCE_CONCURRENT_QUERY_LIMIT = "search_per_source_concurrent_query_limit"; /** - * Flag for allowing ActivityManagerService to send ACTION_APP_ERROR intents - * on application crashes and ANRs. If this is disabled, the crash/ANR dialog - * will never display the "Report" button. - * Type: int ( 0 = disallow, 1 = allow ) + * Whether or not alert sounds are played on MountService events. (0 = false, 1 = true) + * @hide */ - public static final String SEND_ACTION_APP_ERROR = "send_action_app_error"; + public static final String MOUNT_PLAY_NOTIFICATION_SND = "mount_play_not_snd"; /** - * Maximum size of /proc/last_kmsg content to upload after reboot. + * Whether or not UMS auto-starts on UMS host detection. (0 = false, 1 = true) + * @hide */ - public static final String LAST_KMSG_KB = "last_kmsg_kb"; + public static final String MOUNT_UMS_AUTOSTART = "mount_ums_autostart"; /** - * The length of time in milli-seconds that automatic small adjustments to - * SystemClock are ignored if NITZ_UPDATE_DIFF is not exceeded. + * Whether or not a notification is displayed on UMS host detection. (0 = false, 1 = true) + * @hide */ - public static final String NITZ_UPDATE_SPACING = "nitz_update_spacing"; + public static final String MOUNT_UMS_PROMPT = "mount_ums_prompt"; /** - * If the NITZ_UPDATE_DIFF time is exceeded then an automatic adjustment - * to SystemClock will be allowed even if NITZ_UPDATE_SPACING has not been - * exceeded. + * Whether or not a notification is displayed while UMS is enabled. (0 = false, 1 = true) + * @hide */ - public static final String NITZ_UPDATE_DIFF = "nitz_update_diff"; + public static final String MOUNT_UMS_NOTIFY_ENABLED = "mount_ums_notify_enabled"; /** - * @deprecated + * If nonzero, ANRs in invisible background processes bring up a dialog. + * Otherwise, the process will be silently killed. * @hide */ - @Deprecated // Obviated by NameValueCache: just fetch the value directly. - public static class QueryMap extends ContentQueryMap { + public static final String ANR_SHOW_BACKGROUND = "anr_show_background"; - public QueryMap(ContentResolver contentResolver, Cursor cursor, boolean keepUpdated, - Handler handlerForUpdateNotifications) { - super(cursor, NAME, keepUpdated, handlerForUpdateNotifications); - } + /** + * @hide + */ + public static final String[] SETTINGS_TO_BACKUP = { + ADB_ENABLED, + ALLOW_MOCK_LOCATION, + PARENTAL_CONTROL_ENABLED, + PARENTAL_CONTROL_REDIRECT_URL, + USB_MASS_STORAGE_ENABLED, + ACCESSIBILITY_ENABLED, + ENABLED_ACCESSIBILITY_SERVICES, + TTS_USE_DEFAULTS, + TTS_DEFAULT_RATE, + TTS_DEFAULT_PITCH, + TTS_DEFAULT_SYNTH, + TTS_DEFAULT_LANG, + TTS_DEFAULT_COUNTRY, + WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON, + WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY, + WIFI_NUM_ALLOWED_CHANNELS, + WIFI_NUM_OPEN_NETWORKS_KEPT, + MOUNT_PLAY_NOTIFICATION_SND, + MOUNT_UMS_AUTOSTART, + MOUNT_UMS_PROMPT, + MOUNT_UMS_NOTIFY_ENABLED + }; - public QueryMap(ContentResolver contentResolver, boolean keepUpdated, - Handler handlerForUpdateNotifications) { - this(contentResolver, - contentResolver.query(CONTENT_URI, null, null, null, null), - keepUpdated, handlerForUpdateNotifications); + /** + * Helper method for determining if a location provider is enabled. + * @param cr the content resolver to use + * @param provider the location provider to query + * @return true if the provider is enabled + * + * @hide + */ + public static final boolean isLocationProviderEnabled(ContentResolver cr, String provider) { + String allowedProviders = Settings.Secure.getString(cr, LOCATION_PROVIDERS_ALLOWED); + if (allowedProviders != null) { + return (allowedProviders.equals(provider) || + allowedProviders.contains("," + provider + ",") || + allowedProviders.startsWith(provider + ",") || + allowedProviders.endsWith("," + provider)); } + return false; + } - public String getString(String name) { - ContentValues cv = getValues(name); - if (cv == null) return null; - return cv.getAsString(VALUE); + /** + * Thread-safe method for enabling or disabling a single location provider. + * @param cr the content resolver to use + * @param provider the location provider to enable or disable + * @param enabled true if the provider should be enabled + * + * @hide + */ + public static final void setLocationProviderEnabled(ContentResolver cr, + String provider, boolean enabled) { + // to ensure thread safety, we write the provider name with a '+' or '-' + // and let the SettingsProvider handle it rather than reading and modifying + // the list of enabled providers. + if (enabled) { + provider = "+" + provider; + } else { + provider = "-" + provider; } + putString(cr, Settings.Secure.LOCATION_PROVIDERS_ALLOWED, provider); } - } /** diff --git a/core/java/android/provider/SubscribedFeeds.java b/core/java/android/provider/SubscribedFeeds.java deleted file mode 100644 index 8e9f402..0000000 --- a/core/java/android/provider/SubscribedFeeds.java +++ /dev/null @@ -1,209 +0,0 @@ -/* - * Copyright (C) 2006 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 android.provider; - -import android.content.ContentResolver; -import android.content.ContentValues; -import android.database.Cursor; -import android.net.Uri; -import android.accounts.Account; - -/** - * The SubscribedFeeds provider stores all information about subscribed feeds. - * - * @hide - */ -public class SubscribedFeeds { - private SubscribedFeeds() {} - - /** - * Columns from the Feed table that other tables join into themselves. - */ - public interface FeedColumns { - /** - * The feed url. - * <P>Type: TEXT</P> - */ - public static final String FEED = "feed"; - - /** - * The authority that cares about the feed. - * <P>Type: TEXT</P> - */ - public static final String AUTHORITY = "authority"; - - /** - * The gaia service this feed is for (used for authentication). - * <P>Type: TEXT</P> - */ - public static final String SERVICE = "service"; - } - - /** - * Provides constants to access the Feeds table and some utility methods - * to ease using the Feeds content provider. - */ - public static final class Feeds implements BaseColumns, SyncConstValue, - FeedColumns { - private Feeds() {} - - public static Cursor query(ContentResolver cr, String[] projection) { - return cr.query(CONTENT_URI, projection, null, null, DEFAULT_SORT_ORDER); - } - - public static Cursor query(ContentResolver cr, String[] projection, - String where, String[] whereArgs, String orderBy) { - return cr.query(CONTENT_URI, projection, where, - whereArgs, (orderBy == null) ? DEFAULT_SORT_ORDER : orderBy); - } - - /** - * The content:// style URL for this table - */ - public static final Uri CONTENT_URI = - Uri.parse("content://subscribedfeeds/feeds"); - - /** - * The content:// style URL for this table - */ - public static final Uri DELETED_CONTENT_URI = - Uri.parse("content://subscribedfeeds/deleted_feeds"); - - /** - * The MIME type of {@link #CONTENT_URI} providing a directory of - * subscribed feeds. - */ - public static final String CONTENT_TYPE = - "vnd.android.cursor.dir/subscribedfeeds"; - - /** - * The MIME type of a {@link #CONTENT_URI} subdirectory of a single - * subscribed feed. - */ - public static final String CONTENT_ITEM_TYPE = - "vnd.android.cursor.item/subscribedfeed"; - - /** - * The default sort order for this table - */ - public static final String DEFAULT_SORT_ORDER = "_SYNC_ACCOUNT_TYPE, _SYNC_ACCOUNT ASC"; - } - - /** - * A convenience method to add a feed to the SubscribedFeeds - * content provider. The user specifies the values of the FEED, - * _SYNC_ACCOUNT, AUTHORITY. SERVICE, and ROUTING_INFO. - * @param resolver used to access the underlying content provider - * @param feed corresponds to the FEED column - * @param account corresponds to the _SYNC_ACCOUNT column - * @param authority corresponds to the AUTHORITY column - * @param service corresponds to the SERVICE column - * @return the Uri of the feed that was added - */ - public static Uri addFeed(ContentResolver resolver, - String feed, Account account, - String authority, String service) { - ContentValues values = new ContentValues(); - values.put(SubscribedFeeds.Feeds.FEED, feed); - values.put(SubscribedFeeds.Feeds._SYNC_ACCOUNT, account.name); - values.put(SubscribedFeeds.Feeds._SYNC_ACCOUNT_TYPE, account.type); - values.put(SubscribedFeeds.Feeds.AUTHORITY, authority); - values.put(SubscribedFeeds.Feeds.SERVICE, service); - return resolver.insert(SubscribedFeeds.Feeds.CONTENT_URI, values); - } - - public static int deleteFeed(ContentResolver resolver, - String feed, Account account, String authority) { - StringBuilder where = new StringBuilder(); - where.append(SubscribedFeeds.Feeds._SYNC_ACCOUNT + "=?"); - where.append(" AND " + SubscribedFeeds.Feeds._SYNC_ACCOUNT_TYPE + "=?"); - where.append(" AND " + SubscribedFeeds.Feeds.FEED + "=?"); - where.append(" AND " + SubscribedFeeds.Feeds.AUTHORITY + "=?"); - return resolver.delete(SubscribedFeeds.Feeds.CONTENT_URI, - where.toString(), new String[] {account.name, account.type, feed, authority}); - } - - public static int deleteFeeds(ContentResolver resolver, - Account account, String authority) { - StringBuilder where = new StringBuilder(); - where.append(SubscribedFeeds.Feeds._SYNC_ACCOUNT + "=?"); - where.append(" AND " + SubscribedFeeds.Feeds._SYNC_ACCOUNT_TYPE + "=?"); - where.append(" AND " + SubscribedFeeds.Feeds.AUTHORITY + "=?"); - return resolver.delete(SubscribedFeeds.Feeds.CONTENT_URI, - where.toString(), new String[] {account.name, account.type, authority}); - } - - /** - * Columns from the Accounts table. - */ - public interface AccountColumns { - /** - * The account. - * <P>Type: TEXT</P> - */ - public static final String _SYNC_ACCOUNT = SyncConstValue._SYNC_ACCOUNT; - - /** - * The account type. - * <P>Type: TEXT</P> - */ - public static final String _SYNC_ACCOUNT_TYPE = SyncConstValue._SYNC_ACCOUNT_TYPE; - } - - /** - * Provides constants to access the Accounts table and some utility methods - * to ease using it. - */ - public static final class Accounts implements BaseColumns, AccountColumns { - private Accounts() {} - - public static Cursor query(ContentResolver cr, String[] projection) { - return cr.query(CONTENT_URI, projection, null, null, DEFAULT_SORT_ORDER); - } - - public static Cursor query(ContentResolver cr, String[] projection, - String where, String orderBy) { - return cr.query(CONTENT_URI, projection, where, - null, (orderBy == null) ? DEFAULT_SORT_ORDER : orderBy); - } - - /** - * The content:// style URL for this table - */ - public static final Uri CONTENT_URI = - Uri.parse("content://subscribedfeeds/accounts"); - - /** - * The MIME type of {@link #CONTENT_URI} providing a directory of - * accounts that have subscribed feeds. - */ - public static final String CONTENT_TYPE = - "vnd.android.cursor.dir/subscribedfeedaccounts"; - - /** - * The MIME type of a {@link #CONTENT_URI} subdirectory of a single - * account in the subscribed feeds. - */ - public static final String CONTENT_ITEM_TYPE = - "vnd.android.cursor.item/subscribedfeedaccount"; - - /** - * The default sort order for this table - */ - public static final String DEFAULT_SORT_ORDER = "_SYNC_ACCOUNT_TYPE, _SYNC_ACCOUNT ASC"; - } -} diff --git a/core/java/android/provider/Telephony.java b/core/java/android/provider/Telephony.java index d8c5a53..4860cbd 100644 --- a/core/java/android/provider/Telephony.java +++ b/core/java/android/provider/Telephony.java @@ -28,10 +28,11 @@ import android.database.Cursor; import android.net.Uri; import android.telephony.SmsMessage; import android.text.TextUtils; -import android.text.util.Regex; import android.util.Config; import android.util.Log; +import com.android.common.Patterns; + import java.util.HashSet; import java.util.Set; import java.util.regex.Matcher; @@ -152,6 +153,12 @@ public final class Telephony { * <P>Type: INTEGER (boolean)</P> */ public static final String LOCKED = "locked"; + + /** + * Error code associated with sending or receiving this message + * <P>Type: INTEGER</P> + */ + public static final String ERROR_CODE = "error_code"; } /** @@ -243,7 +250,7 @@ public final class Telephony { * @return true if the operation succeeded */ public static boolean moveMessageToFolder(Context context, - Uri uri, int folder) { + Uri uri, int folder, int error) { if (uri == null) { return false; } @@ -266,7 +273,7 @@ public final class Telephony { return false; } - ContentValues values = new ContentValues(2); + ContentValues values = new ContentValues(3); values.put(TYPE, folder); if (markAsUnread) { @@ -274,6 +281,7 @@ public final class Telephony { } else if (markAsRead) { values.put(READ, Integer.valueOf(1)); } + values.put(ERROR_CODE, error); return 1 == SqliteWrapper.update(context, context.getContentResolver(), uri, values, null, null); @@ -545,7 +553,8 @@ public final class Telephony { * <li><em>transactionId (Integer)</em> - The WAP transaction * ID</li> * <li><em>pduType (Integer)</em> - The WAP PDU type</li> - * <li><em>data</em> - The data payload of the message</li> + * <li><em>header (byte[])</em> - The header of the message</li> + * <li><em>data (byte[])</em> - The data payload of the message</li> * </ul> * * <p>If a BroadcastReceiver encounters an error while processing @@ -1283,7 +1292,7 @@ public final class Telephony { } String s = extractAddrSpec(address); - Matcher match = Regex.EMAIL_ADDRESS_PATTERN.matcher(s); + Matcher match = Patterns.EMAIL_ADDRESS.matcher(s); return match.matches(); } @@ -1298,7 +1307,7 @@ public final class Telephony { return false; } - Matcher match = Regex.PHONE_PATTERN.matcher(number); + Matcher match = Patterns.PHONE.matcher(number); return match.matches(); } diff --git a/core/java/android/server/BluetoothA2dpService.java b/core/java/android/server/BluetoothA2dpService.java index 1742e72..22bb43c 100644 --- a/core/java/android/server/BluetoothA2dpService.java +++ b/core/java/android/server/BluetoothA2dpService.java @@ -306,7 +306,11 @@ public class BluetoothA2dpService extends IBluetoothA2dp.Stub { return false; // State is DISCONNECTED + handleSinkStateChange(device, state, BluetoothA2dp.STATE_CONNECTING); + if (!connectSinkNative(path)) { + // Restore previous state + handleSinkStateChange(device, mAudioDevices.get(device), state); return false; } return true; @@ -322,7 +326,8 @@ public class BluetoothA2dpService extends IBluetoothA2dp.Stub { return false; } - switch (getSinkState(device)) { + int state = getSinkState(device); + switch (state) { case BluetoothA2dp.STATE_DISCONNECTED: return false; case BluetoothA2dp.STATE_DISCONNECTING: @@ -330,11 +335,13 @@ public class BluetoothA2dpService extends IBluetoothA2dp.Stub { } // State is CONNECTING or CONNECTED or PLAYING + handleSinkStateChange(device, state, BluetoothA2dp.STATE_DISCONNECTING); if (!disconnectSinkNative(path)) { + // Restore previous state + handleSinkStateChange(device, mAudioDevices.get(device), state); return false; - } else { - return true; } + return true; } public synchronized boolean suspendSink(BluetoothDevice device) { @@ -510,6 +517,19 @@ public class BluetoothA2dpService extends IBluetoothA2dp.Stub { return result; } + private void onConnectSinkResult(String deviceObjectPath, boolean result) { + // If the call was a success, ignore we will update the state + // when we a Sink Property Change + if (!result) { + if (deviceObjectPath != null) { + String address = mBluetoothService.getAddressFromObjectPath(deviceObjectPath); + BluetoothDevice device = mAdapter.getRemoteDevice(address); + int state = getSinkState(device); + handleSinkStateChange(device, state, BluetoothA2dp.STATE_DISCONNECTED); + } + } + } + @Override protected synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) { if (mAudioDevices.isEmpty()) return; diff --git a/core/java/android/server/BluetoothService.java b/core/java/android/server/BluetoothService.java index 018f7d7..dfb775f 100644 --- a/core/java/android/server/BluetoothService.java +++ b/core/java/android/server/BluetoothService.java @@ -69,11 +69,12 @@ import java.util.Random; public class BluetoothService extends IBluetooth.Stub { private static final String TAG = "BluetoothService"; - private static final boolean DBG = false; + private static final boolean DBG = true; private int mNativeData; private BluetoothEventLoop mEventLoop; private boolean mIsAirplaneSensitive; + private boolean mIsAirplaneToggleable; private int mBluetoothState; private boolean mRestart = false; // need to call enable() after disable() private boolean mIsDiscovering; @@ -370,7 +371,7 @@ public class BluetoothService extends IBluetooth.Stub { "Need BLUETOOTH_ADMIN permission"); // Airplane mode can prevent Bluetooth radio from being turned on. - if (mIsAirplaneSensitive && isAirplaneModeOn()) { + if (mIsAirplaneSensitive && isAirplaneModeOn() && !mIsAirplaneToggleable) { return false; } if (mBluetoothState != BluetoothAdapter.STATE_OFF) { @@ -545,7 +546,7 @@ public class BluetoothService extends IBluetooth.Stub { mEventLoop.onPropertyChanged(propVal); } - if (mIsAirplaneSensitive && isAirplaneModeOn()) { + if (mIsAirplaneSensitive && isAirplaneModeOn() && !mIsAirplaneToggleable) { disable(false); } @@ -1597,10 +1598,17 @@ public class BluetoothService extends IBluetooth.Stub { }; private void registerForAirplaneMode(IntentFilter filter) { - String airplaneModeRadios = Settings.System.getString(mContext.getContentResolver(), + final ContentResolver resolver = mContext.getContentResolver(); + final String airplaneModeRadios = Settings.System.getString(resolver, Settings.System.AIRPLANE_MODE_RADIOS); - mIsAirplaneSensitive = airplaneModeRadios == null - ? true : airplaneModeRadios.contains(Settings.System.RADIO_BLUETOOTH); + final String toggleableRadios = Settings.System.getString(resolver, + Settings.System.AIRPLANE_MODE_TOGGLEABLE_RADIOS); + + mIsAirplaneSensitive = airplaneModeRadios == null ? true : + airplaneModeRadios.contains(Settings.System.RADIO_BLUETOOTH); + mIsAirplaneToggleable = toggleableRadios == null ? false : + toggleableRadios.contains(Settings.System.RADIO_BLUETOOTH); + if (mIsAirplaneSensitive) { filter.addAction(Intent.ACTION_AIRPLANE_MODE_CHANGED); } @@ -1661,6 +1669,7 @@ public class BluetoothService extends IBluetooth.Stub { } pw.println("mIsAirplaneSensitive = " + mIsAirplaneSensitive); + pw.println("mIsAirplaneToggleable = " + mIsAirplaneToggleable); pw.println("Local address = " + getAddress()); pw.println("Local name = " + getName()); diff --git a/core/java/android/server/data/BuildData.java b/core/java/android/server/data/BuildData.java deleted file mode 100644 index 53ffa3f..0000000 --- a/core/java/android/server/data/BuildData.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright (C) 2007 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.server.data; - -import android.os.Build; - -import java.io.DataInput; -import java.io.DataOutput; -import java.io.IOException; - -import static com.android.internal.util.Objects.nonNull; - -/** - * Build data transfer object. Keep in sync. with the server side version. - */ -public class BuildData { - - /** The version of the data returned by write() and understood by the constructor. */ - private static final int VERSION = 0; - - private final String fingerprint; - private final String incrementalVersion; - private final long time; // in *seconds* since the epoch (not msec!) - - public BuildData() { - this.fingerprint = "android:" + Build.FINGERPRINT; - this.incrementalVersion = Build.VERSION.INCREMENTAL; - this.time = Build.TIME / 1000; // msec -> sec - } - - public BuildData(String fingerprint, String incrementalVersion, long time) { - this.fingerprint = nonNull(fingerprint); - this.incrementalVersion = incrementalVersion; - this.time = time; - } - - /*package*/ BuildData(DataInput in) throws IOException { - int dataVersion = in.readInt(); - if (dataVersion != VERSION) { - throw new IOException("Expected " + VERSION + ". Got: " + dataVersion); - } - - this.fingerprint = in.readUTF(); - this.incrementalVersion = Long.toString(in.readLong()); - this.time = in.readLong(); - } - - /*package*/ void write(DataOutput out) throws IOException { - out.writeInt(VERSION); - out.writeUTF(fingerprint); - - // TODO: change the format/version to expect a string for this field. - // Version 0, still used by the server side, expects a long. - long changelist; - try { - changelist = Long.parseLong(incrementalVersion); - } catch (NumberFormatException ex) { - changelist = -1; - } - out.writeLong(changelist); - out.writeLong(time); - } - - public String getFingerprint() { - return fingerprint; - } - - public String getIncrementalVersion() { - return incrementalVersion; - } - - public long getTime() { - return time; - } -} diff --git a/core/java/android/server/data/CrashData.java b/core/java/android/server/data/CrashData.java deleted file mode 100644 index d652bb3..0000000 --- a/core/java/android/server/data/CrashData.java +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright (C) 2006 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 android.server.data; - -import java.io.DataInput; -import java.io.DataOutput; -import java.io.IOException; - -import static com.android.internal.util.Objects.nonNull; - -/** - * Crash data transfer object. Keep in sync. with the server side version. - */ -public class CrashData { - - final String id; - final String activity; - final long time; - final BuildData buildData; - final ThrowableData throwableData; - final byte[] state; - - public CrashData(String id, String activity, BuildData buildData, - ThrowableData throwableData) { - this.id = nonNull(id); - this.activity = nonNull(activity); - this.buildData = nonNull(buildData); - this.throwableData = nonNull(throwableData); - this.time = System.currentTimeMillis(); - this.state = null; - } - - public CrashData(String id, String activity, BuildData buildData, - ThrowableData throwableData, byte[] state) { - this.id = nonNull(id); - this.activity = nonNull(activity); - this.buildData = nonNull(buildData); - this.throwableData = nonNull(throwableData); - this.time = System.currentTimeMillis(); - this.state = state; - } - - public CrashData(DataInput in) throws IOException { - int dataVersion = in.readInt(); - if (dataVersion != 0 && dataVersion != 1) { - throw new IOException("Expected 0 or 1. Got: " + dataVersion); - } - - this.id = in.readUTF(); - this.activity = in.readUTF(); - this.time = in.readLong(); - this.buildData = new BuildData(in); - this.throwableData = new ThrowableData(in); - if (dataVersion == 1) { - int len = in.readInt(); - if (len == 0) { - this.state = null; - } else { - this.state = new byte[len]; - in.readFully(this.state, 0, len); - } - } else { - this.state = null; - } - } - - public CrashData(String tag, Throwable throwable) { - id = ""; - activity = tag; - buildData = new BuildData(); - throwableData = new ThrowableData(throwable); - time = System.currentTimeMillis(); - state = null; - } - - public void write(DataOutput out) throws IOException { - // version - if (this.state == null) { - out.writeInt(0); - } else { - out.writeInt(1); - } - - out.writeUTF(this.id); - out.writeUTF(this.activity); - out.writeLong(this.time); - buildData.write(out); - throwableData.write(out); - if (this.state != null) { - out.writeInt(this.state.length); - out.write(this.state, 0, this.state.length); - } - } - - public BuildData getBuildData() { - return buildData; - } - - public ThrowableData getThrowableData() { - return throwableData; - } - - public String getId() { - return id; - } - - public String getActivity() { - return activity; - } - - public long getTime() { - return time; - } - - public byte[] getState() { - return state; - } - - /** - * Return a brief description of this CrashData record. The details of the - * representation are subject to change. - * - * @return Returns a String representing the contents of the object. - */ - @Override - public String toString() { - return "[CrashData: id=" + id + " activity=" + activity + " time=" + time + - " buildData=" + buildData.toString() + - " throwableData=" + throwableData.toString() + "]"; - } -} diff --git a/core/java/android/server/data/StackTraceElementData.java b/core/java/android/server/data/StackTraceElementData.java deleted file mode 100644 index 07185a0..0000000 --- a/core/java/android/server/data/StackTraceElementData.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright (C) 2006 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 android.server.data; - -import java.io.DataInput; -import java.io.DataOutput; -import java.io.IOException; - -/** - * Stack trace element data transfer object. Keep in sync. with the server side - * version. - */ -public class StackTraceElementData { - - final String className; - final String fileName; - final String methodName; - final int lineNumber; - - public StackTraceElementData(StackTraceElement element) { - this.className = element.getClassName(); - - String fileName = element.getFileName(); - this.fileName = fileName == null ? "[unknown source]" : fileName; - - this.methodName = element.getMethodName(); - this.lineNumber = element.getLineNumber(); - } - - public StackTraceElementData(DataInput in) throws IOException { - int dataVersion = in.readInt(); - if (dataVersion != 0) { - throw new IOException("Expected 0. Got: " + dataVersion); - } - - this.className = in.readUTF(); - this.fileName = in.readUTF(); - this.methodName = in.readUTF(); - this.lineNumber = in.readInt(); - } - - void write(DataOutput out) throws IOException { - out.writeInt(0); // version - - out.writeUTF(className); - out.writeUTF(fileName); - out.writeUTF(methodName); - out.writeInt(lineNumber); - } - - public String getClassName() { - return className; - } - - public String getFileName() { - return fileName; - } - - public String getMethodName() { - return methodName; - } - - public int getLineNumber() { - return lineNumber; - } -} diff --git a/core/java/android/server/data/ThrowableData.java b/core/java/android/server/data/ThrowableData.java deleted file mode 100644 index e500aca..0000000 --- a/core/java/android/server/data/ThrowableData.java +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright (C) 2006 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 android.server.data; - -import java.io.DataInput; -import java.io.DataOutput; -import java.io.IOException; - -/** - * Throwable data transfer object. Keep in sync. with the server side version. - */ -public class ThrowableData { - - final String message; - final String type; - final StackTraceElementData[] stackTrace; - final ThrowableData cause; - - public ThrowableData(Throwable throwable) { - this.type = throwable.getClass().getName(); - String message = throwable.getMessage(); - this.message = message == null ? "" : message; - - StackTraceElement[] elements = throwable.getStackTrace(); - this.stackTrace = new StackTraceElementData[elements.length]; - for (int i = 0; i < elements.length; i++) { - this.stackTrace[i] = new StackTraceElementData(elements[i]); - } - - Throwable cause = throwable.getCause(); - this.cause = cause == null ? null : new ThrowableData(cause); - } - - public ThrowableData(DataInput in) throws IOException { - int dataVersion = in.readInt(); - if (dataVersion != 0) { - throw new IOException("Expected 0. Got: " + dataVersion); - } - - this.message = in.readUTF(); - this.type = in.readUTF(); - - int count = in.readInt(); - this.stackTrace = new StackTraceElementData[count]; - for (int i = 0; i < count; i++) { - this.stackTrace[i] = new StackTraceElementData(in); - } - - this.cause = in.readBoolean() ? new ThrowableData(in) : null; - } - - public void write(DataOutput out) throws IOException { - out.writeInt(0); // version - - out.writeUTF(message); - out.writeUTF(type); - - out.writeInt(stackTrace.length); - for (StackTraceElementData elementData : stackTrace) { - elementData.write(out); - } - - out.writeBoolean(cause != null); - if (cause != null) { - cause.write(out); - } - } - - public String getMessage() { - return message; - } - - public String getType() { - return type; - } - - public StackTraceElementData[] getStackTrace() { - return stackTrace; - } - - public ThrowableData getCause() { - return cause; - } - - - public String toString() { - return toString(null); - } - - public String toString(String prefix) { - StringBuilder builder = new StringBuilder(); - append(prefix, builder, this); - return builder.toString(); - } - - private static void append(String prefix, StringBuilder builder, - ThrowableData throwableData) { - if (prefix != null) builder.append(prefix); - builder.append(throwableData.getType()) - .append(": ") - .append(throwableData.getMessage()) - .append('\n'); - for (StackTraceElementData element : throwableData.getStackTrace()) { - if (prefix != null ) builder.append(prefix); - builder.append(" at ") - .append(element.getClassName()) - .append('.') - .append(element.getMethodName()) - .append("(") - .append(element.getFileName()) - .append(':') - .append(element.getLineNumber()) - .append(")\n"); - - } - - ThrowableData cause = throwableData.getCause(); - if (cause != null) { - if (prefix != null ) builder.append(prefix); - builder.append("Caused by: "); - append(prefix, builder, cause); - } - } -} diff --git a/core/java/android/server/data/package.html b/core/java/android/server/data/package.html deleted file mode 100755 index 1c9bf9d..0000000 --- a/core/java/android/server/data/package.html +++ /dev/null @@ -1,5 +0,0 @@ -<html> -<body> - {@hide} -</body> -</html> diff --git a/core/java/android/server/search/SearchManagerService.java b/core/java/android/server/search/SearchManagerService.java index 78ea2e3..f9a0b93 100644 --- a/core/java/android/server/search/SearchManagerService.java +++ b/core/java/android/server/search/SearchManagerService.java @@ -21,6 +21,7 @@ import android.app.IActivityWatcher; import android.app.ISearchManager; import android.app.ISearchManagerCallback; import android.app.SearchManager; +import android.app.SearchableInfo; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; @@ -149,8 +150,9 @@ public class SearchManagerService extends ISearchManager.Stub { * Informs all listeners that the list of searchables has been updated. */ void broadcastSearchablesChanged() { - mContext.sendBroadcast( - new Intent(SearchManager.INTENT_ACTION_SEARCHABLES_CHANGED)); + Intent intent = new Intent(SearchManager.INTENT_ACTION_SEARCHABLES_CHANGED); + intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING); + mContext.sendBroadcast(intent); } // diff --git a/core/java/android/server/search/Searchables.java b/core/java/android/server/search/Searchables.java index c615957..cbb63a5 100644 --- a/core/java/android/server/search/Searchables.java +++ b/core/java/android/server/search/Searchables.java @@ -19,6 +19,7 @@ package android.server.search; import com.android.internal.app.ResolverActivity; import android.app.SearchManager; +import android.app.SearchableInfo; import android.content.ComponentName; import android.content.Context; import android.content.Intent; diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java index 45719e4..fe3b149 100644 --- a/core/java/android/service/wallpaper/WallpaperService.java +++ b/core/java/android/service/wallpaper/WallpaperService.java @@ -434,9 +434,9 @@ public abstract class WallpaperService extends Service { } int myWidth = mSurfaceHolder.getRequestedWidth(); - if (myWidth <= 0) myWidth = ViewGroup.LayoutParams.FILL_PARENT; + if (myWidth <= 0) myWidth = ViewGroup.LayoutParams.MATCH_PARENT; int myHeight = mSurfaceHolder.getRequestedHeight(); - if (myHeight <= 0) myHeight = ViewGroup.LayoutParams.FILL_PARENT; + if (myHeight <= 0) myHeight = ViewGroup.LayoutParams.MATCH_PARENT; final boolean creating = !mCreated; final boolean formatChanged = mFormat != mSurfaceHolder.getRequestedFormat(); diff --git a/core/java/android/speech/tts/ITts.aidl b/core/java/android/speech/tts/ITts.aidl index 1812188..2ed660a 100755 --- a/core/java/android/speech/tts/ITts.aidl +++ b/core/java/android/speech/tts/ITts.aidl @@ -59,5 +59,7 @@ interface ITts { int unregisterCallback(in String callingApp, ITtsCallback cb);
- int playSilence(in String callingApp, in long duration, in int queueMode, in String[] params);
+ int playSilence(in String callingApp, in long duration, in int queueMode, in String[] params); +
+ int setEngineByPackageName(in String enginePackageName);
}
diff --git a/core/java/android/speech/tts/TextToSpeech.java b/core/java/android/speech/tts/TextToSpeech.java index 3f369dd..bbbeb3f 100755 --- a/core/java/android/speech/tts/TextToSpeech.java +++ b/core/java/android/speech/tts/TextToSpeech.java @@ -129,8 +129,8 @@ public class TextToSpeech { * {@link TextToSpeech#synthesizeToFile(String, HashMap, String)} with the * {@link TextToSpeech.Engine#KEY_PARAM_UTTERANCE_ID} key. * @param utteranceId the identifier of the utterance. - */
- public void onUtteranceCompleted(String utteranceId);
+ */ + public void onUtteranceCompleted(String utteranceId); } @@ -286,6 +286,10 @@ public class TextToSpeech { */ public static final String KEY_PARAM_VARIANT = "variant"; /** + * {@hide} + */ + public static final String KEY_PARAM_ENGINE = "engine"; + /** * Parameter key to specify the audio stream type to be used when speaking text * or playing back a file. * @see TextToSpeech#speak(String, int, HashMap) @@ -327,10 +331,16 @@ public class TextToSpeech { * {@hide} */ protected static final int PARAM_POSITION_UTTERANCE_ID = 10; + + /** + * {@hide} + */ + protected static final int PARAM_POSITION_ENGINE = 12; + /** * {@hide} */ - protected static final int NB_CACHED_PARAMS = 6; + protected static final int NB_CACHED_PARAMS = 7; } /** @@ -373,6 +383,7 @@ public class TextToSpeech { mCachedParams[Engine.PARAM_POSITION_VARIANT] = Engine.KEY_PARAM_VARIANT; mCachedParams[Engine.PARAM_POSITION_STREAM] = Engine.KEY_PARAM_STREAM; mCachedParams[Engine.PARAM_POSITION_UTTERANCE_ID] = Engine.KEY_PARAM_UTTERANCE_ID; + mCachedParams[Engine.PARAM_POSITION_ENGINE] = Engine.KEY_PARAM_ENGINE; mCachedParams[Engine.PARAM_POSITION_RATE + 1] = String.valueOf(Engine.DEFAULT_RATE); @@ -381,10 +392,10 @@ public class TextToSpeech { mCachedParams[Engine.PARAM_POSITION_LANGUAGE + 1] = defaultLoc.getISO3Language(); mCachedParams[Engine.PARAM_POSITION_COUNTRY + 1] = defaultLoc.getISO3Country(); mCachedParams[Engine.PARAM_POSITION_VARIANT + 1] = defaultLoc.getVariant(); - mCachedParams[Engine.PARAM_POSITION_STREAM + 1] = String.valueOf(Engine.DEFAULT_STREAM); mCachedParams[Engine.PARAM_POSITION_UTTERANCE_ID + 1] = ""; + mCachedParams[Engine.PARAM_POSITION_ENGINE + 1] = Engine.DEFAULT_SYNTH; initTts(); } @@ -684,6 +695,10 @@ public class TextToSpeech { if (extra != null) { mCachedParams[Engine.PARAM_POSITION_UTTERANCE_ID + 1] = extra; } + extra = params.get(Engine.KEY_PARAM_ENGINE); + if (extra != null) { + mCachedParams[Engine.PARAM_POSITION_ENGINE + 1] = extra; + } } result = mITts.speak(mPackageName, text, queueMode, mCachedParams); } catch (RemoteException e) { @@ -819,7 +834,7 @@ public class TextToSpeech { mStarted = false; initTts(); } finally { - return result; + return result; } } } @@ -894,7 +909,7 @@ public class TextToSpeech { mStarted = false; initTts(); } finally { - return result; + return result; } } } @@ -943,7 +958,7 @@ public class TextToSpeech { mStarted = false; initTts(); } finally { - return result; + return result; } } } @@ -990,7 +1005,7 @@ public class TextToSpeech { mStarted = false; initTts(); } finally { - return result; + return result; } } } @@ -1046,7 +1061,7 @@ public class TextToSpeech { mStarted = false; initTts(); } finally { - return result; + return result; } } } @@ -1064,7 +1079,7 @@ public class TextToSpeech { return null; } try { - String[] locStrings = mITts.getLanguage(); + String[] locStrings = mITts.getLanguage(); if ((locStrings != null) && (locStrings.length == 3)) { return new Locale(locStrings[0], locStrings[1], locStrings[2]); } else { @@ -1131,7 +1146,7 @@ public class TextToSpeech { mStarted = false; initTts(); } finally { - return result; + return result; } } } @@ -1166,6 +1181,10 @@ public class TextToSpeech { if (extra != null) { mCachedParams[Engine.PARAM_POSITION_UTTERANCE_ID + 1] = extra; } + extra = params.get(Engine.KEY_PARAM_ENGINE); + if (extra != null) { + mCachedParams[Engine.PARAM_POSITION_ENGINE + 1] = extra; + } } if (mITts.synthesizeToFile(mPackageName, text, mCachedParams, filename)){ result = SUCCESS; @@ -1214,19 +1233,19 @@ public class TextToSpeech { * * @return Code indicating success or failure. See {@link #ERROR} and {@link #SUCCESS}. */ - public int setOnUtteranceCompletedListener(
- final OnUtteranceCompletedListener listener) {
+ public int setOnUtteranceCompletedListener( + final OnUtteranceCompletedListener listener) { synchronized (mStartLock) { int result = ERROR; if (!mStarted) { return result; } mITtscallback = new ITtsCallback.Stub() { - public void utteranceCompleted(String utteranceId) throws RemoteException {
- if (listener != null) {
- listener.onUtteranceCompleted(utteranceId);
- }
- }
+ public void utteranceCompleted(String utteranceId) throws RemoteException { + if (listener != null) { + listener.onUtteranceCompleted(utteranceId); + } + } }; try { result = mITts.registerCallback(mPackageName, mITtscallback); @@ -1251,7 +1270,50 @@ public class TextToSpeech { } finally { return result; } - }
+ } + } + + /** + * Sets the speech synthesis engine to be used by its packagename. + * + * @param enginePackageName + * The packagename for the synthesis engine (ie, "com.svox.pico") + * + * @return Code indicating success or failure. See {@link #ERROR} and {@link #SUCCESS}. + */ + public int setEngineByPackageName(String enginePackageName) { + synchronized (mStartLock) { + int result = TextToSpeech.ERROR; + if (!mStarted) { + return result; + } + try { + result = mITts.setEngineByPackageName(enginePackageName); + if (result == TextToSpeech.SUCCESS){ + mCachedParams[Engine.PARAM_POSITION_ENGINE + 1] = enginePackageName; + } + } catch (RemoteException e) { + // TTS died; restart it. + Log.e("TextToSpeech.java - setEngineByPackageName", "RemoteException"); + e.printStackTrace(); + mStarted = false; + initTts(); + } catch (NullPointerException e) { + // TTS died; restart it. + Log.e("TextToSpeech.java - setEngineByPackageName", "NullPointerException"); + e.printStackTrace(); + mStarted = false; + initTts(); + } catch (IllegalStateException e) { + // TTS died; restart it. + Log.e("TextToSpeech.java - setEngineByPackageName", "IllegalStateException"); + e.printStackTrace(); + mStarted = false; + initTts(); + } finally { + return result; + } + } } } diff --git a/core/java/android/text/AutoText.java b/core/java/android/text/AutoText.java index 2fc906a..862305b 100644 --- a/core/java/android/text/AutoText.java +++ b/core/java/android/text/AutoText.java @@ -18,7 +18,7 @@ package android.text; import android.content.res.Resources; import android.content.res.XmlResourceParser; -import com.android.internal.util.XmlUtils; +import com.android.common.XmlUtils; import android.view.View; import org.xmlpull.v1.XmlPullParser; diff --git a/core/java/android/text/Html.java b/core/java/android/text/Html.java index 380e5fd..33ecc01 100644 --- a/core/java/android/text/Html.java +++ b/core/java/android/text/Html.java @@ -46,7 +46,7 @@ import android.text.style.TypefaceSpan; import android.text.style.URLSpan; import android.text.style.UnderlineSpan; import android.util.Log; -import com.android.internal.util.XmlUtils; +import com.android.common.XmlUtils; import java.io.IOException; import java.io.StringReader; diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java index a92800d..afc6864 100644 --- a/core/java/android/text/Layout.java +++ b/core/java/android/text/Layout.java @@ -294,7 +294,12 @@ public abstract class Layout { lbaseline, lbottom, buf, start, end, par, this); - left += margin.getLeadingMargin(par); + boolean useMargin = par; + if (margin instanceof LeadingMarginSpan.LeadingMarginSpan2) { + int count = ((LeadingMarginSpan.LeadingMarginSpan2)margin).getLeadingMarginLineCount(); + useMargin = count > i; + } + left += margin.getLeadingMargin(useMargin); } } } @@ -1293,7 +1298,13 @@ public abstract class Layout { LeadingMarginSpan.class); for (int i = 0; i < spans.length; i++) { - left += spans[i].getLeadingMargin(par); + boolean margin = par; + LeadingMarginSpan span = spans[i]; + if (span instanceof LeadingMarginSpan.LeadingMarginSpan2) { + int count = ((LeadingMarginSpan.LeadingMarginSpan2)span).getLeadingMarginLineCount(); + margin = count >= line; + } + left += span.getLeadingMargin(margin); } } } diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java index f0a5ffd..fbf1261 100644 --- a/core/java/android/text/StaticLayout.java +++ b/core/java/android/text/StaticLayout.java @@ -161,6 +161,7 @@ extends Layout else end++; + int firstWidthLineCount = 1; int firstwidth = outerwidth; int restwidth = outerwidth; @@ -171,8 +172,12 @@ extends Layout sp = spanned.getSpans(start, end, LeadingMarginSpan.class); for (int i = 0; i < sp.length; i++) { + LeadingMarginSpan lms = sp[i]; firstwidth -= sp[i].getLeadingMargin(true); restwidth -= sp[i].getLeadingMargin(false); + if (lms instanceof LeadingMarginSpan.LeadingMarginSpan2) { + firstWidthLineCount = ((LeadingMarginSpan.LeadingMarginSpan2)lms).getLeadingMarginLineCount(); + } } chooseht = spanned.getSpans(start, end, LineHeightSpan.class); @@ -750,7 +755,9 @@ extends Layout fitascent = fitdescent = fittop = fitbottom = 0; okascent = okdescent = oktop = okbottom = 0; - width = restwidth; + if (--firstWidthLineCount <= 0) { + width = restwidth; + } } } } diff --git a/core/java/android/text/TextUtils.java b/core/java/android/text/TextUtils.java index 53096dd..afb22ac 100644 --- a/core/java/android/text/TextUtils.java +++ b/core/java/android/text/TextUtils.java @@ -1501,6 +1501,28 @@ public class TextUtils { } /** + * @hide + */ + public static boolean isPrintableAscii(final char c) { + final int asciiFirst = 0x20; + final int asciiLast = 0x7E; // included + return (asciiFirst <= c && c <= asciiLast) || c == '\r' || c == '\n'; + } + + /** + * @hide + */ + public static boolean isPrintableAsciiOnly(final CharSequence str) { + final int len = str.length(); + for (int i = 0; i < len; i++) { + if (!isPrintableAscii(str.charAt(i))) { + return false; + } + } + return true; + } + + /** * Capitalization mode for {@link #getCapsMode}: capitalize all * characters. This value is explicitly defined to be the same as * {@link InputType#TYPE_TEXT_FLAG_CAP_CHARACTERS}. diff --git a/core/java/android/text/method/ArrowKeyMovementMethod.java b/core/java/android/text/method/ArrowKeyMovementMethod.java index ab33cb3..7b307f8 100644 --- a/core/java/android/text/method/ArrowKeyMovementMethod.java +++ b/core/java/android/text/method/ArrowKeyMovementMethod.java @@ -54,7 +54,7 @@ implements MovementMethod Selection.setSelection(buffer, 0); return true; } else { - return Selection.moveUp(buffer, layout); + return Selection.moveUp(buffer, layout); } } } @@ -80,7 +80,7 @@ implements MovementMethod Selection.setSelection(buffer, buffer.length()); return true; } else { - return Selection.moveDown(buffer, layout); + return Selection.moveDown(buffer, layout); } } } @@ -133,6 +133,35 @@ implements MovementMethod } } + private int getOffset(int x, int y, TextView widget){ + // Converts the absolute X,Y coordinates to the character offset for the + // character whose position is closest to the specified + // horizontal position. + x -= widget.getTotalPaddingLeft(); + y -= widget.getTotalPaddingTop(); + + // Clamp the position to inside of the view. + if (x < 0) { + x = 0; + } else if (x >= (widget.getWidth()-widget.getTotalPaddingRight())) { + x = widget.getWidth()-widget.getTotalPaddingRight() - 1; + } + if (y < 0) { + y = 0; + } else if (y >= (widget.getHeight()-widget.getTotalPaddingBottom())) { + y = widget.getHeight()-widget.getTotalPaddingBottom() - 1; + } + + x += widget.getScrollX(); + y += widget.getScrollY(); + + Layout layout = widget.getLayout(); + int line = layout.getLineForVertical(y); + + int offset = layout.getOffsetForHorizontal(line, x); + return offset; + } + public boolean onKeyDown(TextView widget, Spannable buffer, int keyCode, KeyEvent event) { if (executeDown(widget, buffer, keyCode)) { MetaKeyKeyListener.adjustMetaAfterKeypress(buffer); @@ -196,12 +225,12 @@ implements MovementMethod } return false; } - + public boolean onTrackballEvent(TextView widget, Spannable text, MotionEvent event) { return false; } - + public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) { int initialScrollX = -1, initialScrollY = -1; @@ -209,11 +238,101 @@ implements MovementMethod initialScrollX = Touch.getInitialScrollX(widget, buffer); initialScrollY = Touch.getInitialScrollY(widget, buffer); } - + boolean handled = Touch.onTouchEvent(widget, buffer, event); if (widget.isFocused() && !widget.didTouchFocusSelect()) { - if (event.getAction() == MotionEvent.ACTION_UP) { + if (event.getAction() == MotionEvent.ACTION_DOWN) { + boolean cap = (MetaKeyKeyListener.getMetaState(buffer, + KeyEvent.META_SHIFT_ON) == 1) || + (MetaKeyKeyListener.getMetaState(buffer, + MetaKeyKeyListener.META_SELECTING) != 0); + int x = (int) event.getX(); + int y = (int) event.getY(); + int offset = getOffset(x, y, widget); + + if (cap) { + + buffer.setSpan(LAST_TAP_DOWN, offset, offset, + Spannable.SPAN_POINT_POINT); + + // Disallow intercepting of the touch events, so that + // users can scroll and select at the same time. + // without this, users would get booted out of select + // mode once the view detected it needed to scroll. + widget.getParent().requestDisallowInterceptTouchEvent(true); + } else { + OnePointFiveTapState[] tap = buffer.getSpans(0, buffer.length(), + OnePointFiveTapState.class); + + if (tap.length > 0) { + if (event.getEventTime() - tap[0].mWhen <= + ViewConfiguration.getDoubleTapTimeout() && + sameWord(buffer, offset, Selection.getSelectionEnd(buffer))) { + + tap[0].active = true; + MetaKeyKeyListener.startSelecting(widget, buffer); + widget.getParent().requestDisallowInterceptTouchEvent(true); + buffer.setSpan(LAST_TAP_DOWN, offset, offset, + Spannable.SPAN_POINT_POINT); + } + + tap[0].mWhen = event.getEventTime(); + } else { + OnePointFiveTapState newtap = new OnePointFiveTapState(); + newtap.mWhen = event.getEventTime(); + newtap.active = false; + buffer.setSpan(newtap, 0, buffer.length(), + Spannable.SPAN_INCLUSIVE_INCLUSIVE); + } + } + } else if (event.getAction() == MotionEvent.ACTION_MOVE ) { + boolean cap = (MetaKeyKeyListener.getMetaState(buffer, + KeyEvent.META_SHIFT_ON) == 1) || + (MetaKeyKeyListener.getMetaState(buffer, + MetaKeyKeyListener.META_SELECTING) != 0); + + if (cap & handled) { + // Before selecting, make sure we've moved out of the "slop". + // handled will be true, if we're in select mode AND we're + // OUT of the slop + + // Turn long press off while we're selecting. User needs to + // re-tap on the selection to enable longpress + widget.cancelLongPress(); + + // Update selection as we're moving the selection area. + + // Get the current touch position + int x = (int) event.getX(); + int y = (int) event.getY(); + int offset = getOffset(x, y, widget); + + // Get the last down touch position (the position at which the + // user started the selection) + int lastDownOffset = buffer.getSpanStart(LAST_TAP_DOWN); + + // Compute the selection boundaries + int spanstart; + int spanend; + if (offset >= lastDownOffset) { + // Expand from word start of the original tap to new word + // end, since we are selecting "forwards" + spanstart = findWordStart(buffer, lastDownOffset); + spanend = findWordEnd(buffer, offset); + } else { + // Expand to from new word start to word end of the original + // tap since we are selecting "backwards". + // The spanend will always need to be associated with the touch + // up position, so that refining the selection with the + // trackball will work as expected. + spanstart = findWordEnd(buffer, lastDownOffset); + spanend = findWordStart(buffer, offset); + } + Selection.setSelection(buffer, spanstart, spanend); + return true; + } + } else if (event.getAction() == MotionEvent.ACTION_UP) { // If we have scrolled, then the up shouldn't move the cursor, // but we do need to make sure the cursor is still visible at // the current scroll offset to avoid the scroll jumping later @@ -223,35 +342,26 @@ implements MovementMethod widget.moveCursorToVisibleOffset(); return true; } - + int x = (int) event.getX(); int y = (int) event.getY(); + int off = getOffset(x, y, widget); - x -= widget.getTotalPaddingLeft(); - y -= widget.getTotalPaddingTop(); + // XXX should do the same adjust for x as we do for the line. - // Clamp the position to inside of the view. - if (x < 0) { - x = 0; - } else if (x >= (widget.getWidth()-widget.getTotalPaddingRight())) { - x = widget.getWidth()-widget.getTotalPaddingRight() - 1; - } - if (y < 0) { - y = 0; - } else if (y >= (widget.getHeight()-widget.getTotalPaddingBottom())) { - y = widget.getHeight()-widget.getTotalPaddingBottom() - 1; + OnePointFiveTapState[] onepointfivetap = buffer.getSpans(0, buffer.length(), + OnePointFiveTapState.class); + if (onepointfivetap.length > 0 && onepointfivetap[0].active && + Selection.getSelectionStart(buffer) == Selection.getSelectionEnd(buffer)) { + // If we've set select mode, because there was a onepointfivetap, + // but there was no ensuing swipe gesture, undo the select mode + // and remove reference to the last onepointfivetap. + MetaKeyKeyListener.stopSelecting(widget, buffer); + for (int i=0; i < onepointfivetap.length; i++) { + buffer.removeSpan(onepointfivetap[i]); + } + buffer.removeSpan(LAST_TAP_DOWN); } - - x += widget.getScrollX(); - y += widget.getScrollY(); - - Layout layout = widget.getLayout(); - int line = layout.getLineForVertical(y); - - int off = layout.getOffsetForHorizontal(line, x); - - // XXX should do the same adjust for x as we do for the line. - boolean cap = (MetaKeyKeyListener.getMetaState(buffer, KeyEvent.META_SHIFT_ON) == 1) || (MetaKeyKeyListener.getMetaState(buffer, @@ -263,10 +373,10 @@ implements MovementMethod if (tap.length > 0) { if (event.getEventTime() - tap[0].mWhen <= - ViewConfiguration.getDoubleTapTimeout()) { - if (sameWord(buffer, off, Selection.getSelectionEnd(buffer))) { - doubletap = true; - } + ViewConfiguration.getDoubleTapTimeout() && + sameWord(buffer, off, Selection.getSelectionEnd(buffer))) { + + doubletap = true; } tap[0].mWhen = event.getEventTime(); @@ -278,7 +388,12 @@ implements MovementMethod } if (cap) { - Selection.extendSelection(buffer, off); + buffer.removeSpan(LAST_TAP_DOWN); + if (onepointfivetap.length > 0 && onepointfivetap[0].active) { + // If we selecting something with the onepointfivetap-and + // swipe gesture, stop it on finger up. + MetaKeyKeyListener.stopSelecting(widget, buffer); + } } else if (doubletap) { Selection.setSelection(buffer, findWordStart(buffer, off), @@ -301,6 +416,19 @@ implements MovementMethod long mWhen; } + /* We check for a onepointfive tap. This is similar to + * doubletap gesture (where a finger goes down, up, down, up, in a short + * time period), except in the onepointfive tap, a users finger only needs + * to go down, up, down in a short time period. We detect this type of tap + * to implement the onepointfivetap-and-swipe selection gesture. + * This gesture allows users to select a segment of text without going + * through the "select text" option in the context menu. + */ + private static class OnePointFiveTapState implements NoCopySpan { + long mWhen; + boolean active; + } + private static boolean sameWord(CharSequence text, int one, int two) { int start = findWordStart(text, one); int end = findWordEnd(text, one); @@ -395,5 +523,7 @@ implements MovementMethod return sInstance; } + + private static final Object LAST_TAP_DOWN = new Object(); private static ArrowKeyMovementMethod sInstance; } diff --git a/core/java/android/text/method/Touch.java b/core/java/android/text/method/Touch.java index aa8d979..42ad10e 100644 --- a/core/java/android/text/method/Touch.java +++ b/core/java/android/text/method/Touch.java @@ -24,6 +24,7 @@ import android.util.Log; import android.view.MotionEvent; import android.view.ViewConfiguration; import android.widget.TextView; +import android.view.KeyEvent; public class Touch { private Touch() { } @@ -139,10 +140,21 @@ public class Touch { if (ds[0].mFarEnough) { ds[0].mUsed = true; - - float dx = ds[0].mX - event.getX(); - float dy = ds[0].mY - event.getY(); - + boolean cap = (MetaKeyKeyListener.getMetaState(buffer, + KeyEvent.META_SHIFT_ON) == 1) || + (MetaKeyKeyListener.getMetaState(buffer, + MetaKeyKeyListener.META_SELECTING) != 0); + float dx; + float dy; + if (cap) { + // if we're selecting, we want the scroll to go in + // the direction of the drag + dx = event.getX() - ds[0].mX; + dy = event.getY() - ds[0].mY; + } else { + dx = ds[0].mX - event.getX(); + dy = ds[0].mY - event.getY(); + } ds[0].mX = event.getX(); ds[0].mY = event.getY(); diff --git a/core/java/android/text/style/LeadingMarginSpan.java b/core/java/android/text/style/LeadingMarginSpan.java index 8e212e3..cb55329 100644 --- a/core/java/android/text/style/LeadingMarginSpan.java +++ b/core/java/android/text/style/LeadingMarginSpan.java @@ -33,6 +33,11 @@ extends ParagraphStyle CharSequence text, int start, int end, boolean first, Layout layout); + + public interface LeadingMarginSpan2 extends LeadingMarginSpan, WrapTogetherSpan { + public int getLeadingMarginLineCount(); + }; + public static class Standard implements LeadingMarginSpan, ParcelableSpan { private final int mFirst, mRest; diff --git a/core/java/android/text/util/Linkify.java b/core/java/android/text/util/Linkify.java index ce25c47..7f87365 100644 --- a/core/java/android/text/util/Linkify.java +++ b/core/java/android/text/util/Linkify.java @@ -25,6 +25,8 @@ import android.text.Spanned; import android.webkit.WebView; import android.widget.TextView; +import com.android.common.Patterns; + import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.ArrayList; @@ -133,7 +135,7 @@ public class Linkify { */ public static final TransformFilter sPhoneNumberTransformFilter = new TransformFilter() { public final String transformUrl(final Matcher match, String url) { - return Regex.digitsAndPlusOnly(match); + return Patterns.digitsAndPlusOnly(match); } }; @@ -207,19 +209,19 @@ public class Linkify { ArrayList<LinkSpec> links = new ArrayList<LinkSpec>(); if ((mask & WEB_URLS) != 0) { - gatherLinks(links, text, Regex.WEB_URL_PATTERN, + gatherLinks(links, text, Patterns.WEB_URL, new String[] { "http://", "https://", "rtsp://" }, sUrlMatchFilter, null); } if ((mask & EMAIL_ADDRESSES) != 0) { - gatherLinks(links, text, Regex.EMAIL_ADDRESS_PATTERN, + gatherLinks(links, text, Patterns.EMAIL_ADDRESS, new String[] { "mailto:" }, null, null); } if ((mask & PHONE_NUMBERS) != 0) { - gatherLinks(links, text, Regex.PHONE_PATTERN, + gatherLinks(links, text, Patterns.PHONE, new String[] { "tel:" }, sPhoneNumberMatchFilter, sPhoneNumberTransformFilter); } diff --git a/core/java/android/text/util/Regex.java b/core/java/android/text/util/Regex.java deleted file mode 100644 index a6844a4..0000000 --- a/core/java/android/text/util/Regex.java +++ /dev/null @@ -1,204 +0,0 @@ -/* - * Copyright (C) 2007 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.text.util; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * @hide - */ -public class Regex { - /** - * Regular expression pattern to match all IANA top-level domains. - * List accurate as of 2007/06/15. List taken from: - * http://data.iana.org/TLD/tlds-alpha-by-domain.txt - * This pattern is auto-generated by //device/tools/make-iana-tld-pattern.py - */ - public static final Pattern TOP_LEVEL_DOMAIN_PATTERN - = Pattern.compile( - "((aero|arpa|asia|a[cdefgilmnoqrstuwxz])" - + "|(biz|b[abdefghijmnorstvwyz])" - + "|(cat|com|coop|c[acdfghiklmnoruvxyz])" - + "|d[ejkmoz]" - + "|(edu|e[cegrstu])" - + "|f[ijkmor]" - + "|(gov|g[abdefghilmnpqrstuwy])" - + "|h[kmnrtu]" - + "|(info|int|i[delmnoqrst])" - + "|(jobs|j[emop])" - + "|k[eghimnrwyz]" - + "|l[abcikrstuvy]" - + "|(mil|mobi|museum|m[acdghklmnopqrstuvwxyz])" - + "|(name|net|n[acefgilopruz])" - + "|(org|om)" - + "|(pro|p[aefghklmnrstwy])" - + "|qa" - + "|r[eouw]" - + "|s[abcdeghijklmnortuvyz]" - + "|(tel|travel|t[cdfghjklmnoprtvwz])" - + "|u[agkmsyz]" - + "|v[aceginu]" - + "|w[fs]" - + "|y[etu]" - + "|z[amw])"); - - /** - * Regular expression pattern to match RFC 1738 URLs - * List accurate as of 2007/06/15. List taken from: - * http://data.iana.org/TLD/tlds-alpha-by-domain.txt - * This pattern is auto-generated by //device/tools/make-iana-tld-pattern.py - */ - public static final Pattern WEB_URL_PATTERN - = Pattern.compile( - "((?:(http|https|Http|Https|rtsp|Rtsp):\\/\\/(?:(?:[a-zA-Z0-9\\$\\-\\_\\.\\+\\!\\*\\'\\(\\)" - + "\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2})){1,64}(?:\\:(?:[a-zA-Z0-9\\$\\-\\_" - + "\\.\\+\\!\\*\\'\\(\\)\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2})){1,25})?\\@)?)?" - + "((?:(?:[a-zA-Z0-9][a-zA-Z0-9\\-]{0,64}\\.)+" // named host - + "(?:" // plus top level domain - + "(?:aero|arpa|asia|a[cdefgilmnoqrstuwxz])" - + "|(?:biz|b[abdefghijmnorstvwyz])" - + "|(?:cat|com|coop|c[acdfghiklmnoruvxyz])" - + "|d[ejkmoz]" - + "|(?:edu|e[cegrstu])" - + "|f[ijkmor]" - + "|(?:gov|g[abdefghilmnpqrstuwy])" - + "|h[kmnrtu]" - + "|(?:info|int|i[delmnoqrst])" - + "|(?:jobs|j[emop])" - + "|k[eghimnrwyz]" - + "|l[abcikrstuvy]" - + "|(?:mil|mobi|museum|m[acdghklmnopqrstuvwxyz])" - + "|(?:name|net|n[acefgilopruz])" - + "|(?:org|om)" - + "|(?:pro|p[aefghklmnrstwy])" - + "|qa" - + "|r[eouw]" - + "|s[abcdeghijklmnortuvyz]" - + "|(?:tel|travel|t[cdfghjklmnoprtvwz])" - + "|u[agkmsyz]" - + "|v[aceginu]" - + "|w[fs]" - + "|y[etu]" - + "|z[amw]))" - + "|(?:(?:25[0-5]|2[0-4]" // or ip address - + "[0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9])\\.(?:25[0-5]|2[0-4][0-9]" - + "|[0-1][0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(?:25[0-5]|2[0-4][0-9]|[0-1]" - + "[0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(?:25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}" - + "|[1-9][0-9]|[0-9])))" - + "(?:\\:\\d{1,5})?)" // plus option port number - + "(\\/(?:(?:[a-zA-Z0-9\\;\\/\\?\\:\\@\\&\\=\\#\\~" // plus option query params - + "\\-\\.\\+\\!\\*\\'\\(\\)\\,\\_])|(?:\\%[a-fA-F0-9]{2}))*)?" - + "(?:\\b|$)"); // and finally, a word boundary or end of - // input. This is to stop foo.sure from - // matching as foo.su - - public static final Pattern IP_ADDRESS_PATTERN - = Pattern.compile( - "((25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9])\\.(25[0-5]|2[0-4]" - + "[0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1]" - + "[0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}" - + "|[1-9][0-9]|[0-9]))"); - - public static final Pattern DOMAIN_NAME_PATTERN - = Pattern.compile( - "(((([a-zA-Z0-9][a-zA-Z0-9\\-]*)*[a-zA-Z0-9]\\.)+" - + TOP_LEVEL_DOMAIN_PATTERN + ")|" - + IP_ADDRESS_PATTERN + ")"); - - public static final Pattern EMAIL_ADDRESS_PATTERN - = Pattern.compile( - "[a-zA-Z0-9\\+\\.\\_\\%\\-]{1,256}" + - "\\@" + - "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,64}" + - "(" + - "\\." + - "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,25}" + - ")+" - ); - - /** - * This pattern is intended for searching for things that look like they - * might be phone numbers in arbitrary text, not for validating whether - * something is in fact a phone number. It will miss many things that - * are legitimate phone numbers. - * - * <p> The pattern matches the following: - * <ul> - * <li>Optionally, a + sign followed immediately by one or more digits. Spaces, dots, or dashes - * may follow. - * <li>Optionally, sets of digits in parentheses, separated by spaces, dots, or dashes. - * <li>A string starting and ending with a digit, containing digits, spaces, dots, and/or dashes. - * </ul> - */ - public static final Pattern PHONE_PATTERN - = Pattern.compile( // sdd = space, dot, or dash - "(\\+[0-9]+[\\- \\.]*)?" // +<digits><sdd>* - + "(\\([0-9]+\\)[\\- \\.]*)?" // (<digits>)<sdd>* - + "([0-9][0-9\\- \\.][0-9\\- \\.]+[0-9])"); // <digit><digit|sdd>+<digit> - - /** - * Convenience method to take all of the non-null matching groups in a - * regex Matcher and return them as a concatenated string. - * - * @param matcher The Matcher object from which grouped text will - * be extracted - * - * @return A String comprising all of the non-null matched - * groups concatenated together - */ - public static final String concatGroups(Matcher matcher) { - StringBuilder b = new StringBuilder(); - final int numGroups = matcher.groupCount(); - - for (int i = 1; i <= numGroups; i++) { - String s = matcher.group(i); - - System.err.println("Group(" + i + ") : " + s); - - if (s != null) { - b.append(s); - } - } - - return b.toString(); - } - - /** - * Convenience method to return only the digits and plus signs - * in the matching string. - * - * @param matcher The Matcher object from which digits and plus will - * be extracted - * - * @return A String comprising all of the digits and plus in - * the match - */ - public static final String digitsAndPlusOnly(Matcher matcher) { - StringBuilder buffer = new StringBuilder(); - String matchingRegion = matcher.group(); - - for (int i = 0, size = matchingRegion.length(); i < size; i++) { - char character = matchingRegion.charAt(i); - - if (character == '+' || Character.isDigit(character)) { - buffer.append(character); - } - } - return buffer.toString(); - } -} diff --git a/core/java/android/text/util/Rfc822InputFilter.java b/core/java/android/text/util/Rfc822InputFilter.java deleted file mode 100644 index 8c8b7fc..0000000 --- a/core/java/android/text/util/Rfc822InputFilter.java +++ /dev/null @@ -1,58 +0,0 @@ -package android.text.util; - -import android.text.InputFilter; -import android.text.Spanned; -import android.text.SpannableStringBuilder; - -/** - * Implements special address cleanup rules: - * The first space key entry following an "@" symbol that is followed by any combination - * of letters and symbols, including one+ dots and zero commas, should insert an extra - * comma (followed by the space). - * - * @hide - */ -public class Rfc822InputFilter implements InputFilter { - - public CharSequence filter(CharSequence source, int start, int end, Spanned dest, - int dstart, int dend) { - - // quick check - did they enter a single space? - if (end-start != 1 || source.charAt(start) != ' ') { - return null; - } - - // determine if the characters before the new space fit the pattern - // follow backwards and see if we find a comma, dot, or @ - int scanBack = dstart; - boolean dotFound = false; - while (scanBack > 0) { - char c = dest.charAt(--scanBack); - switch (c) { - case '.': - dotFound = true; // one or more dots are req'd - break; - case ',': - return null; - case '@': - if (!dotFound) { - return null; - } - // we have found a comma-insert case. now just do it - // in the least expensive way we can. - if (source instanceof Spanned) { - SpannableStringBuilder sb = new SpannableStringBuilder(","); - sb.append(source); - return sb; - } else { - return ", "; - } - default: - // just keep going - } - } - - // no termination cases were found, so don't edit the input - return null; - } -} diff --git a/core/java/android/text/util/Rfc822Validator.java b/core/java/android/text/util/Rfc822Validator.java deleted file mode 100644 index 6a6bf69..0000000 --- a/core/java/android/text/util/Rfc822Validator.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * 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 android.text.util; - -import android.text.TextUtils; -import android.widget.AutoCompleteTextView; - -import java.util.regex.Pattern; - -/** - * This class works as a Validator for AutoCompleteTextView for - * email addresses. If a token does not appear to be a valid address, - * it is trimmed of characters that cannot legitimately appear in one - * and has the specified domain name added. It is meant for use with - * {@link Rfc822Token} and {@link Rfc822Tokenizer}. - * - * @deprecated In the future make sure we don't quietly alter the user's - * text in ways they did not intend. Meanwhile, hide this - * class from the public API because it does not even have - * a full understanding of the syntax it claims to correct. - * @hide - */ -public class Rfc822Validator implements AutoCompleteTextView.Validator { - /* - * Regex.EMAIL_ADDRESS_PATTERN hardcodes the TLD that we accept, but we - * want to make sure we will keep accepting email addresses with TLD's - * that don't exist at the time of this writing, so this regexp relaxes - * that constraint by accepting any kind of top level domain, not just - * ".com", ".fr", etc... - */ - private static final Pattern EMAIL_ADDRESS_PATTERN = - Pattern.compile("[^\\s@]+@[^\\s@]+\\.[a-zA-z][a-zA-Z][a-zA-Z]*"); - - private String mDomain; - - /** - * Constructs a new validator that uses the specified domain name as - * the default when none is specified. - */ - public Rfc822Validator(String domain) { - mDomain = domain; - } - - /** - * {@inheritDoc} - */ - public boolean isValid(CharSequence text) { - Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(text); - - return tokens.length == 1 && - EMAIL_ADDRESS_PATTERN. - matcher(tokens[0].getAddress()).matches(); - } - - /** - * @return a string in which all the characters that are illegal for the username - * or the domain name part of the email address have been removed. - */ - private String removeIllegalCharacters(String s) { - StringBuilder result = new StringBuilder(); - int length = s.length(); - for (int i = 0; i < length; i++) { - char c = s.charAt(i); - - /* - * An RFC822 atom can contain any ASCII printing character - * except for periods and any of the following punctuation. - * A local-part can contain multiple atoms, concatenated by - * periods, so do allow periods here. - */ - - if (c <= ' ' || c > '~') { - continue; - } - - if (c == '(' || c == ')' || c == '<' || c == '>' || - c == '@' || c == ',' || c == ';' || c == ':' || - c == '\\' || c == '"' || c == '[' || c == ']') { - continue; - } - - result.append(c); - } - return result.toString(); - } - - /** - * {@inheritDoc} - */ - public CharSequence fixText(CharSequence cs) { - // Return an empty string if the email address only contains spaces, \n or \t - if (TextUtils.getTrimmedLength(cs) == 0) return ""; - - Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(cs); - StringBuilder sb = new StringBuilder(); - - for (int i = 0; i < tokens.length; i++) { - String text = tokens[i].getAddress(); - int index = text.indexOf('@'); - if (index < 0) { - // If there is no @, just append the domain of the account - tokens[i].setAddress(removeIllegalCharacters(text) + "@" + mDomain); - } else { - // Otherwise, remove the illegal characters on both sides of the '@' - String fix = removeIllegalCharacters(text.substring(0, index)); - String domain = removeIllegalCharacters(text.substring(index + 1)); - tokens[i].setAddress(fix + "@" + (domain.length() != 0 ? domain : mDomain)); - } - - sb.append(tokens[i].toString()); - if (i + 1 < tokens.length) { - sb.append(", "); - } - } - - return sb; - } -} diff --git a/core/java/android/util/EventLog.java b/core/java/android/util/EventLog.java index 81dd96e..b596d32 100644 --- a/core/java/android/util/EventLog.java +++ b/core/java/android/util/EventLog.java @@ -16,134 +16,41 @@ package android.util; -import com.google.android.collect.Lists; - +import java.io.BufferedReader; +import java.io.FileReader; import java.io.IOException; import java.io.UnsupportedEncodingException; +import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; import java.nio.ByteOrder; -import java.util.ArrayList; import java.util.Collection; -import java.util.List; +import java.util.HashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** - * {@hide} - * Dynamically defined (in terms of event types), space efficient (i.e. "tight") event logging - * to help instrument code for large scale stability and performance monitoring. - * - * Note that this class contains all static methods. This is done for efficiency reasons. - * - * Events for the event log are self-describing binary data structures. They start with a 20 byte - * header (generated automatically) which contains all of the following in order: - * - * <ul> - * <li> Payload length: 2 bytes - length of the non-header portion </li> - * <li> Padding: 2 bytes - no meaning at this time </li> - * <li> Timestamp: - * <ul> - * <li> Seconds: 4 bytes - seconds since Epoch </li> - * <li> Nanoseconds: 4 bytes - plus extra nanoseconds </li> - * </ul></li> - * <li> Process ID: 4 bytes - matching {@link android.os.Process#myPid} </li> - * <li> Thread ID: 4 bytes - matching {@link android.os.Process#myTid} </li> - * </li> - * </ul> + * Access to the system diagnostic event record. System diagnostic events are + * used to record certain system-level events (such as garbage collection, + * activity manager state, system watchdogs, and other low level activity), + * which may be automatically collected and analyzed during system development. * - * The above is followed by a payload, comprised of the following: - * <ul> - * <li> Tag: 4 bytes - unique integer used to identify a particular event. This number is also - * used as a key to map to a string that can be displayed by log reading tools. - * </li> - * <li> Type: 1 byte - can be either {@link #INT}, {@link #LONG}, {@link #STRING}, - * or {@link #LIST}. </li> - * <li> Event log value: the size and format of which is one of: - * <ul> - * <li> INT: 4 bytes </li> - * <li> LONG: 8 bytes </li> - * <li> STRING: - * <ul> - * <li> Size of STRING: 4 bytes </li> - * <li> The string: n bytes as specified in the size fields above. </li> - * </ul></li> - * <li> {@link List LIST}: - * <ul> - * <li> Num items: 1 byte </li> - * <li> N value payloads, where N is the number of items specified above. </li> - * </ul></li> - * </ul> - * </li> - * <li> '\n': 1 byte - an automatically generated newline, used to help detect and recover from log - * corruption and enable standard unix tools like grep, tail and wc to operate - * on event logs. </li> - * </ul> + * <p>This is <b>not</b> the main "logcat" debugging log ({@link android.util.Log})! + * These diagnostic events are for system integrators, not application authors. * - * Note that all output is done in the endian-ness of the device (as determined - * by {@link ByteOrder#nativeOrder()}). + * <p>Events use integer tag codes corresponding to /system/etc/event-log-tags. + * They carry a payload of one or more int, long, or String values. The + * event-log-tags file defines the payload contents for each type code. */ - public class EventLog { + private static final String TAG = "EventLog"; - // Value types - public static final byte INT = 0; - public static final byte LONG = 1; - public static final byte STRING = 2; - public static final byte LIST = 3; + private static final String TAGS_FILE = "/system/etc/event-log-tags"; + private static final String COMMENT_PATTERN = "^\\s*(#.*)?$"; + private static final String TAG_PATTERN = "^\\s*(\\d+)\\s+(\\w+)\\s*(\\(.*\\))?\\s*$"; + private static HashMap<String, Integer> sTagCodes = null; + private static HashMap<Integer, String> sTagNames = null; - /** - * An immutable tuple used to log a heterogeneous set of loggable items. - * The items can be Integer, Long, String, or {@link List}. - * The maximum number of items is 127 - */ - public static final class List { - private Object[] mItems; - - /** - * Get a particular tuple item - * @param pos The position of the item in the tuple - */ - public final Object getItem(int pos) { - return mItems[pos]; - } - - /** - * Get the number of items in the tuple. - */ - public final byte getNumItems() { - return (byte) mItems.length; - } - - /** - * Create a new tuple. - * @param items The items to create the tuple with, as varargs. - * @throws IllegalArgumentException if the arguments are too few (0), - * too many, or aren't loggable types. - */ - public List(Object... items) throws IllegalArgumentException { - if (items.length > Byte.MAX_VALUE) { - throw new IllegalArgumentException( - "A List must have fewer than " - + Byte.MAX_VALUE + " items in it."); - } - for (int i = 0; i < items.length; i++) { - final Object item = items[i]; - if (item == null) { - // Would be nice to be able to write null strings... - items[i] = ""; - } else if (!(item instanceof List || - item instanceof String || - item instanceof Integer || - item instanceof Long)) { - throw new IllegalArgumentException( - "Attempt to create a List with illegal item type."); - } - } - this.mItems = items; - } - } - - /** - * A previously logged event read from the logs. - */ + /** A previously logged event read from the logs. */ public static final class Event { private final ByteBuffer mBuffer; @@ -158,77 +65,84 @@ public class EventLog { private static final int TAG_OFFSET = 20; private static final int DATA_START = 24; + // Value types + private static final byte INT_TYPE = 0; + private static final byte LONG_TYPE = 1; + private static final byte STRING_TYPE = 2; + private static final byte LIST_TYPE = 3; + /** @param data containing event, read from the system */ - public Event(byte[] data) { + /*package*/ Event(byte[] data) { mBuffer = ByteBuffer.wrap(data); mBuffer.order(ByteOrder.nativeOrder()); } + /** @return the process ID which wrote the log entry */ public int getProcessId() { return mBuffer.getInt(PROCESS_OFFSET); } + /** @return the thread ID which wrote the log entry */ public int getThreadId() { return mBuffer.getInt(THREAD_OFFSET); } + /** @return the wall clock time when the entry was written */ public long getTimeNanos() { return mBuffer.getInt(SECONDS_OFFSET) * 1000000000l + mBuffer.getInt(NANOSECONDS_OFFSET); } + /** @return the type tag code of the entry */ public int getTag() { return mBuffer.getInt(TAG_OFFSET); } - /** @return one of Integer, Long, String, or List. */ + /** @return one of Integer, Long, String, null, or Object[] of same. */ public synchronized Object getData() { - mBuffer.limit(PAYLOAD_START + mBuffer.getShort(LENGTH_OFFSET)); - mBuffer.position(DATA_START); // Just after the tag. - return decodeObject(); - } - - public byte[] getRawData() { - return mBuffer.array(); + try { + mBuffer.limit(PAYLOAD_START + mBuffer.getShort(LENGTH_OFFSET)); + mBuffer.position(DATA_START); // Just after the tag. + return decodeObject(); + } catch (IllegalArgumentException e) { + Log.wtf(TAG, "Illegal entry payload: tag=" + getTag(), e); + return null; + } catch (BufferUnderflowException e) { + Log.wtf(TAG, "Truncated entry payload: tag=" + getTag(), e); + return null; + } } /** @return the loggable item at the current position in mBuffer. */ private Object decodeObject() { - if (mBuffer.remaining() < 1) return null; - switch (mBuffer.get()) { - case INT: - if (mBuffer.remaining() < 4) return null; + byte type = mBuffer.get(); + switch (type) { + case INT_TYPE: return (Integer) mBuffer.getInt(); - case LONG: - if (mBuffer.remaining() < 8) return null; + case LONG_TYPE: return (Long) mBuffer.getLong(); - case STRING: + case STRING_TYPE: try { - if (mBuffer.remaining() < 4) return null; int length = mBuffer.getInt(); - if (length < 0 || mBuffer.remaining() < length) return null; int start = mBuffer.position(); mBuffer.position(start + length); return new String(mBuffer.array(), start, length, "UTF-8"); } catch (UnsupportedEncodingException e) { - throw new RuntimeException(e); // UTF-8 is guaranteed. + Log.wtf(TAG, "UTF-8 is not supported", e); + return null; } - case LIST: - if (mBuffer.remaining() < 1) return null; + case LIST_TYPE: int length = mBuffer.get(); - if (length < 0) return null; + if (length < 0) length += 256; // treat as signed byte Object[] array = new Object[length]; - for (int i = 0; i < length; ++i) { - array[i] = decodeObject(); - if (array[i] == null) return null; - } - return new List(array); + for (int i = 0; i < length; ++i) array[i] = decodeObject(); + return array; default: - return null; + throw new IllegalArgumentException("Unknown entry type: " + type); } } } @@ -236,46 +150,36 @@ public class EventLog { // We assume that the native methods deal with any concurrency issues. /** - * Send an event log message. - * @param tag An event identifer + * Record an event log message. + * @param tag The event type tag code * @param value A value to log * @return The number of bytes written */ public static native int writeEvent(int tag, int value); /** - * Send an event log message. - * @param tag An event identifer + * Record an event log message. + * @param tag The event type tag code * @param value A value to log * @return The number of bytes written */ public static native int writeEvent(int tag, long value); /** - * Send an event log message. - * @param tag An event identifer + * Record an event log message. + * @param tag The event type tag code * @param str A value to log * @return The number of bytes written */ public static native int writeEvent(int tag, String str); /** - * Send an event log message. - * @param tag An event identifer - * @param list A {@link List} to log - * @return The number of bytes written - */ - public static native int writeEvent(int tag, List list); - - /** - * Send an event log message. - * @param tag An event identifer + * Record an event log message. + * @param tag The event type tag code * @param list A list of values to log * @return The number of bytes written */ - public static int writeEvent(int tag, Object... list) { - return writeEvent(tag, new List(list)); - } + public static native int writeEvent(int tag, Object... list); /** * Read events from the log, filtered by type. @@ -287,11 +191,65 @@ public class EventLog { throws IOException; /** - * Read events from a file. - * @param path to read from - * @param output container to add events into - * @throws IOException if something goes wrong reading events + * Get the name associated with an event type tag code. + * @param tag code to look up + * @return the name of the tag, or null if no tag has that number */ - public static native void readEvents(String path, Collection<Event> output) - throws IOException; + public static String getTagName(int tag) { + readTagsFile(); + return sTagNames.get(tag); + } + + /** + * Get the event type tag code associated with an event name. + * @param name of event to look up + * @return the tag code, or -1 if no tag has that name + */ + public static int getTagCode(String name) { + readTagsFile(); + Integer code = sTagCodes.get(name); + return code != null ? code : -1; + } + + /** + * Read TAGS_FILE, populating sTagCodes and sTagNames, if not already done. + */ + private static synchronized void readTagsFile() { + if (sTagCodes != null && sTagNames != null) return; + + sTagCodes = new HashMap<String, Integer>(); + sTagNames = new HashMap<Integer, String>(); + + Pattern comment = Pattern.compile(COMMENT_PATTERN); + Pattern tag = Pattern.compile(TAG_PATTERN); + BufferedReader reader = null; + String line; + + try { + reader = new BufferedReader(new FileReader(TAGS_FILE), 256); + while ((line = reader.readLine()) != null) { + if (comment.matcher(line).matches()) continue; + + Matcher m = tag.matcher(line); + if (!m.matches()) { + Log.wtf(TAG, "Bad entry in " + TAGS_FILE + ": " + line); + continue; + } + + try { + int num = Integer.parseInt(m.group(1)); + String name = m.group(2); + sTagCodes.put(name, num); + sTagNames.put(num, name); + } catch (NumberFormatException e) { + Log.wtf(TAG, "Error in " + TAGS_FILE + ": " + line, e); + } + } + } catch (IOException e) { + Log.wtf(TAG, "Error reading " + TAGS_FILE, e); + // Leave the maps existing but unpopulated + } finally { + try { if (reader != null) reader.close(); } catch (IOException e) {} + } + } } diff --git a/core/java/android/util/EventLogTags.java b/core/java/android/util/EventLogTags.java index be905e3..5cf5332 100644 --- a/core/java/android/util/EventLogTags.java +++ b/core/java/android/util/EventLogTags.java @@ -25,16 +25,14 @@ import java.util.HashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; -/** Parsed representation of /etc/event-log-tags. */ +/** + * @deprecated This class is no longer functional. + * Use {@link android.util.EventLog} instead. + */ public class EventLogTags { - private final static String TAG = "EventLogTags"; - - private final static String TAGS_FILE = "/etc/event-log-tags"; - public static class Description { public final int mTag; public final String mName; - // TODO: Parse parameter descriptions when anyone has a use for them. Description(int tag, String name) { mTag = tag; @@ -42,49 +40,11 @@ public class EventLogTags { } } - private final static Pattern COMMENT_PATTERN = Pattern.compile( - "^\\s*(#.*)?$"); - - private final static Pattern TAG_PATTERN = Pattern.compile( - "^\\s*(\\d+)\\s+(\\w+)\\s*(\\(.*\\))?\\s*$"); + public EventLogTags() throws IOException {} - private final HashMap<String, Description> mNameMap = - new HashMap<String, Description>(); - - private final HashMap<Integer, Description> mTagMap = - new HashMap<Integer, Description>(); - - public EventLogTags() throws IOException { - this(new BufferedReader(new FileReader(TAGS_FILE), 256)); - } + public EventLogTags(BufferedReader input) throws IOException {} - public EventLogTags(BufferedReader input) throws IOException { - String line; - while ((line = input.readLine()) != null) { - Matcher m = COMMENT_PATTERN.matcher(line); - if (m.matches()) continue; + public Description get(String name) { return null; } - m = TAG_PATTERN.matcher(line); - if (m.matches()) { - try { - int tag = Integer.parseInt(m.group(1)); - Description d = new Description(tag, m.group(2)); - mNameMap.put(d.mName, d); - mTagMap.put(d.mTag, d); - } catch (NumberFormatException e) { - Log.e(TAG, "Error in event log tags entry: " + line, e); - } - } else { - Log.e(TAG, "Can't parse event log tags entry: " + line); - } - } - } - - public Description get(String name) { - return mNameMap.get(name); - } - - public Description get(int tag) { - return mTagMap.get(tag); - } + public Description get(int tag) { return null; } } diff --git a/core/java/android/util/Log.java b/core/java/android/util/Log.java index 2572679..75b1b90 100644 --- a/core/java/android/util/Log.java +++ b/core/java/android/util/Log.java @@ -81,6 +81,13 @@ public final class Log { */ public static final int ASSERT = 7; + /** + * Exception class used to capture a stack trace in {@link #wtf()}. + */ + private static class TerribleFailure extends Exception { + TerribleFailure(String msg, Throwable cause) { super(msg, cause); } + } + private Log() { } @@ -170,24 +177,24 @@ public final class Log { /** * Checks to see whether or not a log for the specified tag is loggable at the specified level. - * + * * The default level of any tag is set to INFO. This means that any level above and including * INFO will be logged. Before you make any calls to a logging method you should check to see * if your tag should be logged. You can change the default level by setting a system property: * 'setprop log.tag.<YOUR_LOG_TAG> <LEVEL>' - * Where level is either VERBOSE, DEBUG, INFO, WARN, ERROR, ASSERT, or SUPPRESS. SUPRESS will + * Where level is either VERBOSE, DEBUG, INFO, WARN, ERROR, ASSERT, or SUPPRESS. SUPPRESS will * turn off all logging for your tag. You can also create a local.prop file that with the * following in it: * 'log.tag.<YOUR_LOG_TAG>=<LEVEL>' * and place that in /data/local.prop. - * + * * @param tag The tag to check. * @param level The level to check. * @return Whether or not that this is allowed to be logged. * @throws IllegalArgumentException is thrown if the tag.length() > 23. */ public static native boolean isLoggable(String tag, int level); - + /* * Send a {@link #WARN} log message and log the exception. * @param tag Used to identify the source of a log message. It usually identifies @@ -216,9 +223,44 @@ public final class Log { * @param tr An exception to log */ public static int e(String tag, String msg, Throwable tr) { - int r = println(ERROR, tag, msg + '\n' + getStackTraceString(tr)); - RuntimeInit.reportException(tag, tr, false); // asynchronous - return r; + return println(ERROR, tag, msg + '\n' + getStackTraceString(tr)); + } + + /** + * What a Terrible Failure: Report a condition that should never happen. + * The error will always be logged at level ASSERT with the call stack. + * Depending on system configuration, a report may be added to the + * {@link android.os.DropBoxManager} and/or the process may be terminated + * immediately with an error dialog. + * @param tag Used to identify the source of a log message. + * @param msg The message you would like logged. + */ + public static int wtf(String tag, String msg) { + return wtf(tag, msg, null); + } + + /** + * What a Terrible Failure: Report an exception that should never happen. + * Similar to {@link #wtf(String, String)}, with an exception to log. + * @param tag Used to identify the source of a log message. + * @param tr An exception to log. + */ + public static int wtf(String tag, Throwable tr) { + return wtf(tag, tr.getMessage(), tr); + } + + /** + * What a Terrible Failure: Report an exception that should never happen. + * Similar to {@link #wtf(String, Throwable)}, with a message as well. + * @param tag Used to identify the source of a log message. + * @param msg The message you would like logged. + * @param tr An exception to log. May be null. + */ + public static int wtf(String tag, String msg, Throwable tr) { + tr = new TerribleFailure(msg, tr); + int bytes = println(ASSERT, tag, getStackTraceString(tr)); + RuntimeInit.wtf(tag, tr); + return bytes; } /** diff --git a/core/java/android/util/TimeUtils.java b/core/java/android/util/TimeUtils.java index 0fc70d5..4f496d7 100644 --- a/core/java/android/util/TimeUtils.java +++ b/core/java/android/util/TimeUtils.java @@ -27,7 +27,7 @@ import java.io.IOException; import java.util.TimeZone; import java.util.Date; -import com.android.internal.util.XmlUtils; +import com.android.common.XmlUtils; /** * A class containing utility methods related to time zones. diff --git a/core/java/android/util/XmlPullAttributes.java b/core/java/android/util/XmlPullAttributes.java index 12d6dd9..8f855cd 100644 --- a/core/java/android/util/XmlPullAttributes.java +++ b/core/java/android/util/XmlPullAttributes.java @@ -19,7 +19,7 @@ package android.util; import org.xmlpull.v1.XmlPullParser; import android.util.AttributeSet; -import com.android.internal.util.XmlUtils; +import com.android.common.XmlUtils; /** * Provides an implementation of AttributeSet on top of an XmlPullParser. diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 1fc3678..df4cab0 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -379,7 +379,7 @@ import java.util.WeakHashMap; * dimension, it can specify one of: * <ul> * <li> an exact number - * <li>FILL_PARENT, which means the view wants to be as big as its parent + * <li>MATCH_PARENT, which means the view wants to be as big as its parent * (minus padding) * <li> WRAP_CONTENT, which means that the view wants to be just big enough to * enclose its content (plus padding). @@ -1494,6 +1494,16 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * @hide */ static final int OPAQUE_MASK = 0x01800000; + + /** + * Indicates a prepressed state; + * the short time between ACTION_DOWN and recognizing + * a 'real' press. Prepressed is used to recognize quick taps + * even when they are shorter than ViewConfiguration.getTapTimeout(). + * + * @hide + */ + private static final int PREPRESSED = 0x02000000; /** * The parent this view is attached to. @@ -1722,6 +1732,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility private int mNextFocusDownId = View.NO_ID; private CheckForLongPress mPendingCheckForLongPress; + private CheckForTap mPendingCheckForTap = null; + private UnsetPressedState mUnsetPressedState; /** @@ -1762,6 +1774,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * Special tree observer used when mAttachInfo is null. */ private ViewTreeObserver mFloatingTreeObserver; + + /** + * Cache the touch slop from the context that created the view. + */ + private int mTouchSlop; // Used for debug only static long sInstanceCount = 0; @@ -1777,6 +1794,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility mResources = context != null ? context.getResources() : null; mViewFlags = SOUND_EFFECTS_ENABLED | HAPTIC_FEEDBACK_ENABLED; ++sInstanceCount; + mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); } /** @@ -2589,8 +2607,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * @param gainFocus True if the View has focus; false otherwise. * @param direction The direction focus has moved when requestFocus() * is called to give this view focus. Values are - * View.FOCUS_UP, View.FOCUS_DOWN, View.FOCUS_LEFT or - * View.FOCUS_RIGHT. It may not always apply, in which + * {@link #FOCUS_UP}, {@link #FOCUS_DOWN}, {@link #FOCUS_LEFT} or + * {@link #FOCUS_RIGHT}. It may not always apply, in which * case use the default. * @param previouslyFocusedRect The rectangle, in this view's coordinate * system, of the previously focused view. If applicable, this will be @@ -2726,9 +2744,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility setPressed(false); if (!mHasPerformedLongPress) { - if (mPendingCheckForLongPress != null) { - removeCallbacks(mPendingCheckForLongPress); - } + removeLongPressCallback(); } } } @@ -3750,9 +3766,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility if (imm != null && (mPrivateFlags & FOCUSED) != 0) { imm.focusOut(this); } - if (mPendingCheckForLongPress != null) { - removeCallbacks(mPendingCheckForLongPress); - } + removeLongPressCallback(); onFocusLost(); } else if (imm != null && (mPrivateFlags & FOCUSED) != 0) { imm.focusIn(this); @@ -3771,6 +3785,26 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility } /** + * Dispatch a view visibility change down the view hierarchy. + * ViewGroups should override to route to their children. + * @param changedView The view whose visibility changed. Could be 'this' or + * an ancestor view. + * @param visibility The new visibility of changedView. + */ + protected void dispatchVisibilityChanged(View changedView, int visibility) { + onVisibilityChanged(changedView, visibility); + } + + /** + * Called when the visibility of the view or an ancestor of the view is changed. + * @param changedView The view whose visibility changed. Could be 'this' or + * an ancestor view. + * @param visibility The new visibility of changedView. + */ + protected void onVisibilityChanged(View changedView, int visibility) { + } + + /** * Dispatch a window visibility change down the view hierarchy. * ViewGroups should override to route to their children. * @@ -3935,7 +3969,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility (event.getRepeatCount() == 0)) { setPressed(true); if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) { - postCheckForLongClick(); + postCheckForLongClick(0); } return true; } @@ -3978,9 +4012,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility if (!mHasPerformedLongPress) { // This is a tap, so remove the longpress check - if (mPendingCheckForLongPress != null) { - removeCallbacks(mPendingCheckForLongPress); - } + removeLongPressCallback(); result = performClick(); } @@ -4160,7 +4192,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) { switch (event.getAction()) { case MotionEvent.ACTION_UP: - if ((mPrivateFlags & PRESSED) != 0) { + boolean prepressed = (mPrivateFlags & PREPRESSED) != 0; + if ((mPrivateFlags & PRESSED) != 0 || prepressed) { // take focus if we don't have it already and we should in // touch mode. boolean focusTaken = false; @@ -4170,9 +4203,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility if (!mHasPerformedLongPress) { // This is a tap, so remove the longpress check - if (mPendingCheckForLongPress != null) { - removeCallbacks(mPendingCheckForLongPress); - } + removeLongPressCallback(); // Only perform take click actions if we were in the pressed state if (!focusTaken) { @@ -4184,24 +4215,31 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility mUnsetPressedState = new UnsetPressedState(); } - if (!post(mUnsetPressedState)) { + if (prepressed) { + mPrivateFlags |= PRESSED; + refreshDrawableState(); + postDelayed(mUnsetPressedState, + ViewConfiguration.getPressedStateDuration()); + } else if (!post(mUnsetPressedState)) { // If the post failed, unpress right now mUnsetPressedState.run(); } + removeTapCallback(); } break; case MotionEvent.ACTION_DOWN: - mPrivateFlags |= PRESSED; - refreshDrawableState(); - if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) { - postCheckForLongClick(); + if (mPendingCheckForTap == null) { + mPendingCheckForTap = new CheckForTap(); } + mPrivateFlags |= PREPRESSED; + postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); break; case MotionEvent.ACTION_CANCEL: mPrivateFlags &= ~PRESSED; refreshDrawableState(); + removeTapCallback(); break; case MotionEvent.ACTION_MOVE: @@ -4209,27 +4247,19 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility final int y = (int) event.getY(); // Be lenient about moving outside of buttons - int slop = ViewConfiguration.get(mContext).getScaledTouchSlop(); + int slop = mTouchSlop; if ((x < 0 - slop) || (x >= getWidth() + slop) || (y < 0 - slop) || (y >= getHeight() + slop)) { // Outside button + removeTapCallback(); if ((mPrivateFlags & PRESSED) != 0) { - // Remove any future long press checks - if (mPendingCheckForLongPress != null) { - removeCallbacks(mPendingCheckForLongPress); - } + // Remove any future long press/tap checks + removeLongPressCallback(); // Need to switch from pressed to not pressed mPrivateFlags &= ~PRESSED; refreshDrawableState(); } - } else { - // Inside button - if ((mPrivateFlags & PRESSED) == 0) { - // Need to switch from not pressed to pressed - mPrivateFlags |= PRESSED; - refreshDrawableState(); - } } break; } @@ -4240,15 +4270,32 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility } /** + * Remove the longpress detection timer. + */ + private void removeLongPressCallback() { + if (mPendingCheckForLongPress != null) { + removeCallbacks(mPendingCheckForLongPress); + } + } + + /** + * Remove the tap detection timer. + */ + private void removeTapCallback() { + if (mPendingCheckForTap != null) { + mPrivateFlags &= ~PREPRESSED; + removeCallbacks(mPendingCheckForTap); + } + } + + /** * Cancels a pending long press. Your subclass can use this if you * want the context menu to come up if the user presses and holds * at the same place, but you don't want it to come up if they press * and then move around enough to cause scrolling. */ public void cancelLongPress() { - if (mPendingCheckForLongPress != null) { - removeCallbacks(mPendingCheckForLongPress); - } + removeLongPressCallback(); } /** @@ -4349,6 +4396,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility } } + if ((changed & VISIBILITY_MASK) != 0) { + dispatchVisibilityChanged(this, (flags & VISIBILITY_MASK)); + } + if ((changed & WILL_NOT_CACHE_DRAWING) != 0) { destroyDrawingCache(); } @@ -5745,9 +5796,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * @see #onAttachedToWindow() */ protected void onDetachedFromWindow() { - if (mPendingCheckForLongPress != null) { - removeCallbacks(mPendingCheckForLongPress); - } + removeLongPressCallback(); destroyDrawingCache(); } @@ -5961,7 +6010,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility protected void onRestoreInstanceState(Parcelable state) { mPrivateFlags |= SAVE_STATE_CALLED; if (state != BaseSavedState.EMPTY_STATE && state != null) { - throw new IllegalArgumentException("Wrong state class -- expecting View State"); + throw new IllegalArgumentException("Wrong state class, expecting View State but " + + "received " + state.getClass().toString() + " instead. This usually happens " + + "when two views of different type have the same id in the same hierarchy. " + + "This view's id is " + ViewDebug.resolveId(mContext, getId()) + ". Make sure " + + "other views do not use the same id."); } } @@ -8404,14 +8457,15 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility } } - private void postCheckForLongClick() { + private void postCheckForLongClick(int delayOffset) { mHasPerformedLongPress = false; if (mPendingCheckForLongPress == null) { mPendingCheckForLongPress = new CheckForLongPress(); } mPendingCheckForLongPress.rememberWindowAttachCount(); - postDelayed(mPendingCheckForLongPress, ViewConfiguration.getLongPressTimeout()); + postDelayed(mPendingCheckForLongPress, + ViewConfiguration.getLongPressTimeout() - delayOffset); } private static int[] stateSetUnion(final int[] stateSet1, @@ -8588,6 +8642,17 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility mOriginalWindowAttachCount = mWindowAttachCount; } } + + private final class CheckForTap implements Runnable { + public void run() { + mPrivateFlags &= ~PREPRESSED; + mPrivateFlags |= PRESSED; + refreshDrawableState(); + if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) { + postCheckForLongClick(ViewConfiguration.getTapTimeout()); + } + } + } /** * Interface definition for a callback to be invoked when a key event is diff --git a/core/java/android/view/ViewConfiguration.java b/core/java/android/view/ViewConfiguration.java index 993048f..2344c42 100644 --- a/core/java/android/view/ViewConfiguration.java +++ b/core/java/android/view/ViewConfiguration.java @@ -49,7 +49,7 @@ public class ViewConfiguration { * Defines the duration in milliseconds of the pressed state in child * components. */ - private static final int PRESSED_STATE_DURATION = 85; + private static final int PRESSED_STATE_DURATION = 125; /** * Defines the duration in milliseconds before a press turns into @@ -69,7 +69,7 @@ public class ViewConfiguration { * is a tap or a scroll. If the user does not move within this interval, it is * considered to be a tap. */ - private static final int TAP_TIMEOUT = 100; + private static final int TAP_TIMEOUT = 115; /** * Defines the duration in milliseconds we will wait to see if a touch event diff --git a/core/java/android/view/ViewDebug.java b/core/java/android/view/ViewDebug.java index 4baf612..3ebe1c2 100644 --- a/core/java/android/view/ViewDebug.java +++ b/core/java/android/view/ViewDebug.java @@ -702,7 +702,7 @@ public class ViewDebug { if (parameter.indexOf('@') != -1) { final String[] ids = parameter.split("@"); final String className = ids[0]; - final int hashCode = Integer.parseInt(ids[1], 16); + final int hashCode = (int) Long.parseLong(ids[1], 16); View view = root.getRootView(); if (view instanceof ViewGroup) { @@ -910,7 +910,7 @@ public class ViewDebug { private static void dump(View root, OutputStream clientStream) throws IOException { BufferedWriter out = null; try { - out = new BufferedWriter(new OutputStreamWriter(clientStream), 32 * 1024); + out = new BufferedWriter(new OutputStreamWriter(clientStream, "utf-8"), 32 * 1024); View view = root.getRootView(); if (view instanceof ViewGroup) { ViewGroup group = (ViewGroup) view; @@ -1300,7 +1300,7 @@ public class ViewDebug { } } - private static Object resolveId(Context context, int id) { + static Object resolveId(Context context, int id) { Object fieldValue; final Resources resources = context.getResources(); if (id >= 0) { diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index e2f15c7..763f273 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -684,6 +684,19 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager * {@inheritDoc} */ @Override + protected void dispatchVisibilityChanged(View changedView, int visibility) { + super.dispatchVisibilityChanged(changedView, visibility); + final int count = mChildrenCount; + final View[] children = mChildren; + for (int i = 0; i < count; i++) { + children[i].dispatchVisibilityChanged(changedView, visibility); + } + } + + /** + * {@inheritDoc} + */ + @Override public void dispatchWindowVisibilityChanged(int visibility) { super.dispatchWindowVisibilityChanged(visibility); final int count = mChildrenCount; @@ -3057,7 +3070,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager if (childDimension >= 0) { resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; - } else if (childDimension == LayoutParams.FILL_PARENT) { + } else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size. So be it. resultSize = size; resultMode = MeasureSpec.EXACTLY; @@ -3075,7 +3088,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager // Child wants a specific size... so be it resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; - } else if (childDimension == LayoutParams.FILL_PARENT) { + } else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size, but our size is not fixed. // Constrain child to not be bigger than us. resultSize = size; @@ -3094,7 +3107,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager // Child wants a specific size... let him have it resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; - } else if (childDimension == LayoutParams.FILL_PARENT) { + } else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size... find out how big it should // be resultSize = 0; @@ -3349,7 +3362,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager * for both width and height. For each dimension, it can specify one of: * <ul> * <li> an exact number - * <li>FILL_PARENT, which means the view wants to be as big as its parent + * <li>MATCH_PARENT, which means the view wants to be as big as its parent * (minus padding) * <li> WRAP_CONTENT, which means that the view wants to be just big enough * to enclose its content (plus padding) @@ -3363,14 +3376,22 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager */ public static class LayoutParams { /** - * Special value for the height or width requested by a View. - * FILL_PARENT means that the view wants to fill the available space - * within the parent, taking the parent's padding into account. + * This value has the same meaning as {@link #MATCH_PARENT} but has + * been deprecated. */ + @SuppressWarnings({"UnusedDeclaration"}) + @Deprecated public static final int FILL_PARENT = -1; /** * Special value for the height or width requested by a View. + * MATCH_PARENT means that the view wants to be as bigas its parent, + * minus the parent's padding, if any. + */ + public static final int MATCH_PARENT = -1; + + /** + * Special value for the height or width requested by a View. * WRAP_CONTENT means that the view wants to be just large enough to fit * its own internal content, taking its own padding into account. */ @@ -3378,20 +3399,20 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager /** * Information about how wide the view wants to be. Can be an exact - * size, or one of the constants FILL_PARENT or WRAP_CONTENT. + * size, or one of the constants MATCH_PARENT or WRAP_CONTENT. */ @ViewDebug.ExportedProperty(mapping = { - @ViewDebug.IntToString(from = FILL_PARENT, to = "FILL_PARENT"), + @ViewDebug.IntToString(from = MATCH_PARENT, to = "MATCH_PARENT"), @ViewDebug.IntToString(from = WRAP_CONTENT, to = "WRAP_CONTENT") }) public int width; /** * Information about how tall the view wants to be. Can be an exact - * size, or one of the constants FILL_PARENT or WRAP_CONTENT. + * size, or one of the constants MATCH_PARENT or WRAP_CONTENT. */ @ViewDebug.ExportedProperty(mapping = { - @ViewDebug.IntToString(from = FILL_PARENT, to = "FILL_PARENT"), + @ViewDebug.IntToString(from = MATCH_PARENT, to = "MATCH_PARENT"), @ViewDebug.IntToString(from = WRAP_CONTENT, to = "WRAP_CONTENT") }) public int height; @@ -3408,9 +3429,9 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager * * <ul> * <li><code>layout_width</code>: the width, either an exact value, - * {@link #WRAP_CONTENT} or {@link #FILL_PARENT}</li> + * {@link #WRAP_CONTENT} or {@link #MATCH_PARENT}</li> * <li><code>layout_height</code>: the height, either an exact value, - * {@link #WRAP_CONTENT} or {@link #FILL_PARENT}</li> + * {@link #WRAP_CONTENT} or {@link #MATCH_PARENT}</li> * </ul> * * @param c the application environment @@ -3429,9 +3450,9 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager * Creates a new set of layout parameters with the specified width * and height. * - * @param width the width, either {@link #FILL_PARENT}, + * @param width the width, either {@link #MATCH_PARENT}, * {@link #WRAP_CONTENT} or a fixed size in pixels - * @param height the height, either {@link #FILL_PARENT}, + * @param height the height, either {@link #MATCH_PARENT}, * {@link #WRAP_CONTENT} or a fixed size in pixels */ public LayoutParams(int width, int height) { @@ -3494,8 +3515,8 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager if (size == WRAP_CONTENT) { return "wrap-content"; } - if (size == FILL_PARENT) { - return "fill-parent"; + if (size == MATCH_PARENT) { + return "match-parent"; } return String.valueOf(size); } diff --git a/core/java/android/view/ViewRoot.java b/core/java/android/view/ViewRoot.java index 4e12250..094b7dd 100644 --- a/core/java/android/view/ViewRoot.java +++ b/core/java/android/view/ViewRoot.java @@ -68,6 +68,7 @@ public final class ViewRoot extends Handler implements ViewParent, View.AttachInfo.Callbacks { private static final String TAG = "ViewRoot"; private static final boolean DBG = false; + private static final boolean SHOW_FPS = false; @SuppressWarnings({"ConstantConditionalExpression"}) private static final boolean LOCAL_LOGV = false ? Config.LOGD : Config.LOGV; /** @noinspection PointlessBooleanExpression*/ @@ -1166,7 +1167,7 @@ public final class ViewRoot extends Handler implements ViewParent, int measureSpec; switch (rootDimension) { - case ViewGroup.LayoutParams.FILL_PARENT: + case ViewGroup.LayoutParams.MATCH_PARENT: // Window can't resize. Force root view to be windowSize. measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY); break; @@ -1244,7 +1245,7 @@ public final class ViewRoot extends Handler implements ViewParent, mEgl.eglSwapBuffers(mEglDisplay, mEglSurface); checkEglErrors(); - if (Config.DEBUG && ViewDebug.showFps) { + if (SHOW_FPS || Config.DEBUG && ViewDebug.showFps) { int now = (int)SystemClock.elapsedRealtime(); if (sDrawTime != 0) { nativeShowFPS(canvas, now - sDrawTime); @@ -1356,7 +1357,7 @@ public final class ViewRoot extends Handler implements ViewParent, mView.dispatchConsistencyCheck(ViewDebug.CONSISTENCY_DRAWING); } - if (Config.DEBUG && ViewDebug.showFps) { + if (SHOW_FPS || Config.DEBUG && ViewDebug.showFps) { int now = (int)SystemClock.elapsedRealtime(); if (sDrawTime != 0) { nativeShowFPS(canvas, now - sDrawTime); diff --git a/core/java/android/view/ViewStub.java b/core/java/android/view/ViewStub.java index 703a38f..d5e9af4 100644 --- a/core/java/android/view/ViewStub.java +++ b/core/java/android/view/ViewStub.java @@ -207,9 +207,11 @@ public final class ViewStub extends View { } else { throw new IllegalStateException("setVisibility called on un-referenced view"); } - } else if (visibility == VISIBLE || visibility == INVISIBLE) { + } else { super.setVisibility(visibility); - inflate(); + if (visibility == VISIBLE || visibility == INVISIBLE) { + inflate(); + } } } @@ -244,7 +246,7 @@ public final class ViewStub extends View { parent.addView(view, index); } - mInflatedViewRef = new WeakReference(view); + mInflatedViewRef = new WeakReference<View>(view); if (mInflateListener != null) { mInflateListener.onInflate(this, view); diff --git a/core/java/android/view/Window.java b/core/java/android/view/Window.java index 1932765..7dd5085 100644 --- a/core/java/android/view/Window.java +++ b/core/java/android/view/Window.java @@ -484,7 +484,7 @@ public abstract class Window { /** * Set the width and height layout parameters of the window. The default - * for both of these is FILL_PARENT; you can change them to WRAP_CONTENT to + * for both of these is MATCH_PARENT; you can change them to WRAP_CONTENT to * make a window that is not full-screen. * * @param width The desired layout width of the window. diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index 6696533..8e15f89 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -649,7 +649,28 @@ public interface WindowManager extends ViewManager { * be cleared automatically after the window is displayed. */ public static final int SOFT_INPUT_IS_FORWARD_NAVIGATION = 0x100; - + + /** + * Default value for {@link #screenBrightness} and {@link #buttonBrightness} + * indicating that the brightness value is not overridden for this window + * and normal brightness policy should be used. + */ + public static final float BRIGHTNESS_OVERRIDE_NONE = -1.0f; + + /** + * Value for {@link #screenBrightness} and {@link #buttonBrightness} + * indicating that the screen or button backlight brightness should be set + * to the lowest value when this window is in front. + */ + public static final float BRIGHTNESS_OVERRIDE_OFF = 0.0f; + + /** + * Value for {@link #screenBrightness} and {@link #buttonBrightness} + * indicating that the screen or button backlight brightness should be set + * to the hightest value when this window is in front. + */ + public static final float BRIGHTNESS_OVERRIDE_FULL = 1.0f; + /** * Desired operating mode for any soft input area. May any combination * of: @@ -717,9 +738,17 @@ public interface WindowManager extends ViewManager { * preferred screen brightness. 0 to 1 adjusts the brightness from * dark to full bright. */ - public float screenBrightness = -1.0f; + public float screenBrightness = BRIGHTNESS_OVERRIDE_NONE; /** + * This can be used to override the standard behavior of the button and + * keyboard backlights. A value of less than 0, the default, means to + * use the standard backlight behavior. 0 to 1 adjusts the brightness + * from dark to full bright. + */ + public float buttonBrightness = BRIGHTNESS_OVERRIDE_NONE; + + /** * Identifier for this window. This will usually be filled in for * you. */ @@ -742,26 +771,26 @@ public interface WindowManager extends ViewManager { public LayoutParams() { - super(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT); + super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); type = TYPE_APPLICATION; format = PixelFormat.OPAQUE; } public LayoutParams(int _type) { - super(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT); + super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); type = _type; format = PixelFormat.OPAQUE; } public LayoutParams(int _type, int _flags) { - super(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT); + super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); type = _type; flags = _flags; format = PixelFormat.OPAQUE; } public LayoutParams(int _type, int _flags, int _format) { - super(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT); + super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); type = _type; flags = _flags; format = _format; @@ -816,6 +845,7 @@ public interface WindowManager extends ViewManager { out.writeFloat(alpha); out.writeFloat(dimAmount); out.writeFloat(screenBrightness); + out.writeFloat(buttonBrightness); out.writeStrongBinder(token); out.writeString(packageName); TextUtils.writeToParcel(mTitle, out, parcelableFlags); @@ -851,6 +881,7 @@ public interface WindowManager extends ViewManager { alpha = in.readFloat(); dimAmount = in.readFloat(); screenBrightness = in.readFloat(); + buttonBrightness = in.readFloat(); token = in.readStrongBinder(); packageName = in.readString(); mTitle = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); @@ -870,6 +901,8 @@ public interface WindowManager extends ViewManager { public static final int SOFT_INPUT_MODE_CHANGED = 1<<9; public static final int SCREEN_ORIENTATION_CHANGED = 1<<10; public static final int SCREEN_BRIGHTNESS_CHANGED = 1<<11; + /** {@hide} */ + public static final int BUTTON_BRIGHTNESS_CHANGED = 1<<12; // internal buffer to backup/restore parameters under compatibility mode. private int[] mCompatibilityParamsBackup = null; @@ -971,6 +1004,10 @@ public interface WindowManager extends ViewManager { screenBrightness = o.screenBrightness; changes |= SCREEN_BRIGHTNESS_CHANGED; } + if (buttonBrightness != o.buttonBrightness) { + buttonBrightness = o.buttonBrightness; + changes |= BUTTON_BRIGHTNESS_CHANGED; + } if (screenOrientation != o.screenOrientation) { screenOrientation = o.screenOrientation; @@ -999,9 +1036,9 @@ public interface WindowManager extends ViewManager { sb.append(','); sb.append(y); sb.append(")("); - sb.append((width==FILL_PARENT?"fill":(width==WRAP_CONTENT?"wrap":width))); + sb.append((width== MATCH_PARENT ?"fill":(width==WRAP_CONTENT?"wrap":width))); sb.append('x'); - sb.append((height==FILL_PARENT?"fill":(height==WRAP_CONTENT?"wrap":height))); + sb.append((height== MATCH_PARENT ?"fill":(height==WRAP_CONTENT?"wrap":height))); sb.append(")"); if (softInputMode != 0) { sb.append(" sim=#"); diff --git a/core/java/android/view/WindowManagerPolicy.java b/core/java/android/view/WindowManagerPolicy.java index 083793b..bbe9c1f 100644 --- a/core/java/android/view/WindowManagerPolicy.java +++ b/core/java/android/view/WindowManagerPolicy.java @@ -237,25 +237,6 @@ public interface WindowManagerPolicy { public boolean hasAppShownWindows(); /** - * Return true if the application token has been asked to display an - * app starting icon as the application is starting up. - * - * @return Returns true if setAppStartingIcon() was called for this - * window's token. - */ - public boolean hasAppStartingIcon(); - - /** - * Return the Window that is being displayed as this window's - * application token is being started. - * - * @return Returns the currently displayed starting window, or null if - * it was not requested, has not yet been displayed, or has - * been removed. - */ - public WindowState getAppStartingWindow(); - - /** * Is this window visible? It is not visible if there is no * surface, or we are in the process of running an exit animation * that will remove the surface. @@ -681,6 +662,14 @@ public interface WindowManagerPolicy { public boolean finishAnimationLw(); /** + * Return true if it is okay to perform animations for an app transition + * that is about to occur. You may return false for this if, for example, + * the lock screen is currently displayed so the switch should happen + * immediately. + */ + public boolean allowAppAnimationsLw(); + + /** * Called after the screen turns off. * * @param why {@link #OFF_BECAUSE_OF_USER} or @@ -694,6 +683,11 @@ public interface WindowManagerPolicy { public void screenTurnedOn(); /** + * Return whether the screen is currently on. + */ + public boolean isScreenOn(); + + /** * Perform any initial processing of a low-level input event before the * window manager handles special keys and generates a high-level event * that is dispatched to the application. @@ -795,11 +789,6 @@ public interface WindowManagerPolicy { void exitKeyguardSecurely(OnKeyguardExitResult callback); /** - * Return if keyguard is currently showing. - */ - public boolean keyguardIsShowingTq(); - - /** * inKeyguardRestrictedKeyInputMode * * if keyguard screen is showing or in restricted key input mode (i.e. in diff --git a/core/java/android/view/WindowOrientationListener.java b/core/java/android/view/WindowOrientationListener.java index 13606e7..2aba60b 100755 --- a/core/java/android/view/WindowOrientationListener.java +++ b/core/java/android/view/WindowOrientationListener.java @@ -33,13 +33,12 @@ import android.util.Log; public abstract class WindowOrientationListener { private static final String TAG = "WindowOrientationListener"; private static final boolean DEBUG = false; - private static final boolean localLOGV = DEBUG ? Config.LOGD : Config.LOGV; + private static final boolean localLOGV = DEBUG || Config.DEBUG; private SensorManager mSensorManager; private boolean mEnabled = false; private int mRate; private Sensor mSensor; - private SensorEventListener mSensorEventListener; - private int mSensorRotation = -1; + private SensorEventListenerImpl mSensorEventListener; /** * Creates a new WindowOrientationListener. @@ -80,7 +79,6 @@ public abstract class WindowOrientationListener { } if (mEnabled == false) { if (localLOGV) Log.d(TAG, "WindowOrientationListener enabled"); - mSensorRotation = -1; mSensorManager.registerListener(mSensorEventListener, mSensor, mRate); mEnabled = true; } @@ -96,23 +94,22 @@ public abstract class WindowOrientationListener { } if (mEnabled == true) { if (localLOGV) Log.d(TAG, "WindowOrientationListener disabled"); - mSensorRotation = -1; mSensorManager.unregisterListener(mSensorEventListener); mEnabled = false; } } public int getCurrentRotation() { - return mSensorRotation; + if (mEnabled) { + return mSensorEventListener.getCurrentRotation(); + } + return -1; } class SensorEventListenerImpl implements SensorEventListener { private static final int _DATA_X = 0; private static final int _DATA_Y = 1; private static final int _DATA_Z = 2; - // Angle around x-axis thats considered almost perfect vertical to hold - // the device - private static final int PIVOT = 20; // Angle around x-asis that's considered almost too vertical. Beyond // this angle will not result in any orientation changes. f phone faces uses, // the device is leaning backward. @@ -121,30 +118,61 @@ public abstract class WindowOrientationListener { // angle will not result in any orientation changes. If phone faces uses, // the device is leaning forward. private static final int PIVOT_LOWER = -10; - // Upper threshold limit for switching from portrait to landscape - private static final int PL_UPPER = 295; - // Lower threshold limit for switching from landscape to portrait - private static final int LP_LOWER = 320; - // Lower threshold limt for switching from portrait to landscape - private static final int PL_LOWER = 270; - // Upper threshold limit for switching from landscape to portrait - private static final int LP_UPPER = 359; - // Minimum angle which is considered landscape - private static final int LANDSCAPE_LOWER = 235; - // Minimum angle which is considered portrait - private static final int PORTRAIT_LOWER = 60; - - // Internal value used for calculating linear variant - private static final float PL_LF_UPPER = - ((float)(PL_UPPER-PL_LOWER))/((float)(PIVOT_UPPER-PIVOT)); - private static final float PL_LF_LOWER = - ((float)(PL_UPPER-PL_LOWER))/((float)(PIVOT-PIVOT_LOWER)); - // Internal value used for calculating linear variant - private static final float LP_LF_UPPER = - ((float)(LP_UPPER - LP_LOWER))/((float)(PIVOT_UPPER-PIVOT)); - private static final float LP_LF_LOWER = - ((float)(LP_UPPER - LP_LOWER))/((float)(PIVOT-PIVOT_LOWER)); + static final int ROTATION_0 = 0; + static final int ROTATION_90 = 1; + static final int ROTATION_180 = 2; + static final int ROTATION_270 = 3; + int mRotation = ROTATION_0; + + // Threshold values defined for device rotation positions + // follow order ROTATION_0 .. ROTATION_270 + final int THRESHOLDS[][][] = new int[][][] { + {{60, 135}, {135, 225}, {225, 300}}, + {{0, 45}, {45, 135}, {135, 210}, {330, 360}}, + {{0, 45}, {45, 120}, {240, 315}, {315, 360}}, + {{0, 30}, {150, 225}, {225, 315}, {315, 360}} + }; + + // Transform rotation ranges based on THRESHOLDS. This + // has to be in step with THESHOLDS + final int ROTATE_TO[][] = new int[][] { + {ROTATION_270, ROTATION_180, ROTATION_90}, + {ROTATION_0, ROTATION_270, ROTATION_180, ROTATION_0}, + {ROTATION_0, ROTATION_270, ROTATION_90, ROTATION_0}, + {ROTATION_0, ROTATION_180, ROTATION_90, ROTATION_0} + }; + + // Mapping into actual Surface rotation values + final int TRANSFORM_ROTATIONS[] = new int[]{Surface.ROTATION_0, + Surface.ROTATION_90, Surface.ROTATION_180, Surface.ROTATION_270}; + + int getCurrentRotation() { + return TRANSFORM_ROTATIONS[mRotation]; + } + private void calculateNewRotation(int orientation, int zyangle) { + if (localLOGV) Log.i(TAG, orientation + ", " + zyangle + ", " + mRotation); + int rangeArr[][] = THRESHOLDS[mRotation]; + int row = -1; + for (int i = 0; i < rangeArr.length; i++) { + if ((orientation >= rangeArr[i][0]) && (orientation < rangeArr[i][1])) { + row = i; + break; + } + } + if (row != -1) { + // Find new rotation based on current rotation value. + // This also takes care of irregular rotations as well. + int rotation = ROTATE_TO[mRotation][row]; + if (localLOGV) Log.i(TAG, " new rotation = " + rotation); + if (rotation != mRotation) { + mRotation = rotation; + // Trigger orientation change + onOrientationChanged(TRANSFORM_ROTATIONS[rotation]); + } + } + } + public void onSensorChanged(SensorEvent event) { float[] values = event.values; float X = values[_DATA_X]; @@ -153,53 +181,19 @@ public abstract class WindowOrientationListener { float OneEightyOverPi = 57.29577957855f; float gravity = (float) Math.sqrt(X*X+Y*Y+Z*Z); float zyangle = (float)Math.asin(Z/gravity)*OneEightyOverPi; - int rotation = -1; if ((zyangle <= PIVOT_UPPER) && (zyangle >= PIVOT_LOWER)) { // Check orientation only if the phone is flat enough // Don't trust the angle if the magnitude is small compared to the y value float angle = (float)Math.atan2(Y, -X) * OneEightyOverPi; - int orientation = 90 - (int)Math.round(angle); + int orientation = 90 - Math.round(angle); // normalize to 0 - 359 range while (orientation >= 360) { orientation -= 360; - } + } while (orientation < 0) { orientation += 360; } - // Orientation values between LANDSCAPE_LOWER and PL_LOWER - // are considered landscape. - // Ignore orientation values between 0 and LANDSCAPE_LOWER - // For orientation values between LP_UPPER and PL_LOWER, - // the threshold gets set linearly around PIVOT. - if ((orientation >= PL_LOWER) && (orientation <= LP_UPPER)) { - float threshold; - float delta = zyangle - PIVOT; - if (mSensorRotation == Surface.ROTATION_90) { - if (delta < 0) { - // Delta is negative - threshold = LP_LOWER - (LP_LF_LOWER * delta); - } else { - threshold = LP_LOWER + (LP_LF_UPPER * delta); - } - rotation = (orientation >= threshold) ? Surface.ROTATION_0 : Surface.ROTATION_90; - } else { - if (delta < 0) { - // Delta is negative - threshold = PL_UPPER+(PL_LF_LOWER * delta); - } else { - threshold = PL_UPPER-(PL_LF_UPPER * delta); - } - rotation = (orientation <= threshold) ? Surface.ROTATION_90: Surface.ROTATION_0; - } - } else if ((orientation >= LANDSCAPE_LOWER) && (orientation < LP_LOWER)) { - rotation = Surface.ROTATION_90; - } else if ((orientation >= PL_UPPER) || (orientation <= PORTRAIT_LOWER)) { - rotation = Surface.ROTATION_0; - } - if ((rotation != -1) && (rotation != mSensorRotation)) { - mSensorRotation = rotation; - onOrientationChanged(mSensorRotation); - } + calculateNewRotation(orientation, Math.round(zyangle)); } } diff --git a/core/java/android/webkit/BrowserFrame.java b/core/java/android/webkit/BrowserFrame.java index 9456ae1..1337bed 100644 --- a/core/java/android/webkit/BrowserFrame.java +++ b/core/java/android/webkit/BrowserFrame.java @@ -19,17 +19,23 @@ package android.webkit; import android.app.ActivityManager; import android.content.Context; import android.content.res.AssetManager; +import android.database.Cursor; import android.graphics.Bitmap; import android.net.ParseException; +import android.net.Uri; import android.net.WebAddress; import android.net.http.SslCertificate; import android.os.Handler; import android.os.Message; +import android.provider.OpenableColumns; import android.util.Log; import android.util.TypedValue; +import android.view.Surface; +import android.view.WindowOrientationListener; import junit.framework.Assert; +import java.io.InputStream; import java.net.URLEncoder; import java.util.HashMap; import java.util.Map; @@ -63,9 +69,14 @@ class BrowserFrame extends Handler { // Attached Javascript interfaces private Map<String, Object> mJSInterfaceMap; + // Orientation listener + private WindowOrientationListener mOrientationListener; + // message ids // a message posted when a frame loading is completed static final int FRAME_COMPLETED = 1001; + // orientation change message + static final int ORIENTATION_CHANGED = 1002; // a message posted when the user decides the policy static final int POLICY_FUNCTION = 1003; @@ -101,10 +112,13 @@ class BrowserFrame extends Handler { */ public BrowserFrame(Context context, WebViewCore w, CallbackProxy proxy, WebSettings settings, Map<String, Object> javascriptInterfaces) { + + Context appContext = context.getApplicationContext(); + // Create a global JWebCoreJavaBridge to handle timers and // cookies in the WebCore thread. if (sJavaBridge == null) { - sJavaBridge = new JWebCoreJavaBridge(context); + sJavaBridge = new JWebCoreJavaBridge(appContext); // set WebCore native cache size ActivityManager am = (ActivityManager) context .getSystemService(Context.ACTIVITY_SERVICE); @@ -114,18 +128,18 @@ class BrowserFrame extends Handler { sJavaBridge.setCacheSize(4 * 1024 * 1024); } // initialize CacheManager - CacheManager.init(context); + CacheManager.init(appContext); // create CookieSyncManager with current Context - CookieSyncManager.createInstance(context); + CookieSyncManager.createInstance(appContext); // create PluginManager with current Context - PluginManager.getInstance(context); + PluginManager.getInstance(appContext); } mJSInterfaceMap = javascriptInterfaces; mSettings = settings; mContext = context; mCallbackProxy = proxy; - mDatabase = WebViewDatabase.getInstance(context); + mDatabase = WebViewDatabase.getInstance(appContext); mWebViewCore = w; AssetManager am = context.getAssets(); @@ -134,6 +148,31 @@ class BrowserFrame extends Handler { if (DebugFlags.BROWSER_FRAME) { Log.v(LOGTAG, "BrowserFrame constructor: this=" + this); } + + mOrientationListener = new WindowOrientationListener(context) { + @Override + public void onOrientationChanged(int orientation) { + switch (orientation) { + case Surface.ROTATION_90: + orientation = 90; + break; + case Surface.ROTATION_180: + orientation = 180; + break; + case Surface.ROTATION_270: + orientation = -90; + break; + case Surface.ROTATION_0: + orientation = 0; + break; + default: + break; + } + sendMessage( + obtainMessage(ORIENTATION_CHANGED, orientation, 0)); + } + }; + mOrientationListener.enable(); } /** @@ -300,6 +339,7 @@ class BrowserFrame extends Handler { // loadType is not used yet if (isMainFrame) { mCommitted = true; + mWebViewCore.getWebView().mViewManager.postResetStateAll(); } } @@ -338,6 +378,7 @@ class BrowserFrame extends Handler { * Destroy all native components of the BrowserFrame. */ public void destroy() { + mOrientationListener.disable(); nativeDestroyFrame(); removeCallbacksAndMessages(null); } @@ -372,6 +413,11 @@ class BrowserFrame extends Handler { break; } + case ORIENTATION_CHANGED: { + nativeOrientationChanged(msg.arg1); + break; + } + default: break; } @@ -463,6 +509,63 @@ class BrowserFrame extends Handler { } /** + * Called by JNI. Given a URI, find the associated file and return its size + * @param uri A String representing the URI of the desired file. + * @return int The size of the given file. + */ + private int getFileSize(String uri) { + int size = 0; + Cursor cursor = mContext.getContentResolver().query(Uri.parse(uri), + new String[] { OpenableColumns.SIZE }, + null, + null, + null); + if (cursor != null) { + try { + if (cursor.moveToNext()) { + size = cursor.getInt(0); + } + } finally { + cursor.close(); + } + } + return size; + } + + /** + * Called by JNI. Given a URI, a buffer, and an offset into the buffer, + * copy the resource into buffer. + * @param uri A String representing the URI of the desired file. + * @param buffer The byte array to copy the data into. + * @param offset The offet into buffer to place the data. + * @param expectedSize The size that the buffer has allocated for this file. + * @return int The size of the given file, or zero if it fails. + */ + private int getFile(String uri, byte[] buffer, int offset, + int expectedSize) { + int size = 0; + try { + InputStream stream = mContext.getContentResolver() + .openInputStream(Uri.parse(uri)); + size = stream.available(); + if (size <= expectedSize && buffer != null + && buffer.length - offset >= size) { + stream.read(buffer, offset, size); + } else { + size = 0; + } + stream.close(); + } catch (java.io.FileNotFoundException e) { + Log.e(LOGTAG, "FileNotFoundException:" + e); + size = 0; + } catch (java.io.IOException e2) { + Log.e(LOGTAG, "IOException: " + e2); + size = 0; + } + return size; + } + + /** * Start loading a resource. * @param loaderHandle The native ResourceLoader that is the target of the * data. @@ -480,7 +583,10 @@ class BrowserFrame extends Handler { String method, HashMap headers, byte[] postData, + long postDataIdentifier, int cacheMode, + boolean mainResource, + boolean userGesture, boolean synchronous) { PerfChecker checker = new PerfChecker(); @@ -547,12 +653,14 @@ class BrowserFrame extends Handler { if (DebugFlags.BROWSER_FRAME) { Log.v(LOGTAG, "startLoadingResource: url=" + url + ", method=" + method + ", postData=" + postData + ", isMainFramePage=" - + isMainFramePage); + + isMainFramePage + ", mainResource=" + mainResource + + ", userGesture=" + userGesture); } // Create a LoadListener - LoadListener loadListener = LoadListener.getLoadListener(mContext, this, url, - loaderHandle, synchronous, isMainFramePage); + LoadListener loadListener = LoadListener.getLoadListener(mContext, + this, url, loaderHandle, synchronous, isMainFramePage, + mainResource, userGesture, postDataIdentifier); mCallbackProxy.onLoadResource(url); @@ -675,10 +783,13 @@ class BrowserFrame extends Handler { return mSettings.getUserAgentString(); } - // these ids need to be in sync with enum RAW_RES_ID in WebFrame + // These ids need to be in sync with enum rawResId in PlatformBridge.h private static final int NODOMAIN = 1; private static final int LOADERROR = 2; private static final int DRAWABLEDIR = 3; + private static final int FILE_UPLOAD_LABEL = 4; + private static final int RESET_LABEL = 5; + private static final int SUBMIT_LABEL = 6; String getRawResFilename(int id) { int resid; @@ -696,6 +807,18 @@ class BrowserFrame extends Handler { resid = com.android.internal.R.drawable.btn_check_off; break; + case FILE_UPLOAD_LABEL: + return mContext.getResources().getString( + com.android.internal.R.string.upload_file); + + case RESET_LABEL: + return mContext.getResources().getString( + com.android.internal.R.string.reset); + + case SUBMIT_LABEL: + return mContext.getResources().getString( + com.android.internal.R.string.submit); + default: Log.e(LOGTAG, "getRawResFilename got incompatible resource ID"); return ""; @@ -830,4 +953,6 @@ class BrowserFrame extends Handler { * returns null. */ private native HashMap getFormTextData(); + + private native void nativeOrientationChanged(int orientation); } diff --git a/core/java/android/webkit/ByteArrayBuilder.java b/core/java/android/webkit/ByteArrayBuilder.java index 145411c..334526b 100644 --- a/core/java/android/webkit/ByteArrayBuilder.java +++ b/core/java/android/webkit/ByteArrayBuilder.java @@ -16,6 +16,8 @@ package android.webkit; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.SoftReference; import java.util.LinkedList; import java.util.ListIterator; @@ -23,47 +25,37 @@ import java.util.ListIterator; them back out. It does not optimize for returning the result in a single array, though this is supported in the API. It is fastest if the retrieval can be done via iterating through chunks. - - Things to add: - - consider dynamically increasing our min_capacity, - as we see mTotalSize increase */ class ByteArrayBuilder { private static final int DEFAULT_CAPACITY = 8192; - private LinkedList<Chunk> mChunks; - - /** free pool */ - private LinkedList<Chunk> mPool; + // Global pool of chunks to be used by other ByteArrayBuilders. + private static final LinkedList<SoftReference<Chunk>> sPool = + new LinkedList<SoftReference<Chunk>>(); + // Reference queue for processing gc'd entries. + private static final ReferenceQueue<Chunk> sQueue = + new ReferenceQueue<Chunk>(); - private int mMinCapacity; + private LinkedList<Chunk> mChunks; public ByteArrayBuilder() { - init(0); - } - - public ByteArrayBuilder(int minCapacity) { - init(minCapacity); - } - - private void init(int minCapacity) { mChunks = new LinkedList<Chunk>(); - mPool = new LinkedList<Chunk>(); - - if (minCapacity <= 0) { - minCapacity = DEFAULT_CAPACITY; - } - mMinCapacity = minCapacity; - } - - public void append(byte[] array) { - append(array, 0, array.length); } public synchronized void append(byte[] array, int offset, int length) { while (length > 0) { - Chunk c = appendChunk(length); + Chunk c = null; + if (mChunks.isEmpty()) { + c = obtainChunk(length); + mChunks.addLast(c); + } else { + c = mChunks.getLast(); + if (c.mLength == c.mArray.length) { + c = obtainChunk(length); + mChunks.addLast(c); + } + } int amount = Math.min(length, c.mArray.length - c.mLength); System.arraycopy(array, offset, c.mArray, c.mLength, amount); c.mLength += amount; @@ -75,7 +67,7 @@ class ByteArrayBuilder { /** * The fastest way to retrieve the data is to iterate through the * chunks. This returns the first chunk. Note: this pulls the - * chunk out of the queue. The caller must call releaseChunk() to + * chunk out of the queue. The caller must call Chunk.release() to * dispose of it. */ public synchronized Chunk getFirstChunk() { @@ -83,23 +75,11 @@ class ByteArrayBuilder { return mChunks.removeFirst(); } - /** - * recycles chunk - */ - public synchronized void releaseChunk(Chunk c) { - c.mLength = 0; - mPool.addLast(c); - } - - public boolean isEmpty() { + public synchronized boolean isEmpty() { return mChunks.isEmpty(); } - public int size() { - return mChunks.size(); - } - - public int getByteSize() { + public synchronized int getByteSize() { int total = 0; ListIterator<Chunk> it = mChunks.listIterator(0); while (it.hasNext()) { @@ -112,37 +92,40 @@ class ByteArrayBuilder { public synchronized void clear() { Chunk c = getFirstChunk(); while (c != null) { - releaseChunk(c); + c.release(); c = getFirstChunk(); } } - private Chunk appendChunk(int length) { - if (length < mMinCapacity) { - length = mMinCapacity; - } - - Chunk c; - if (mChunks.isEmpty()) { - c = obtainChunk(length); - } else { - c = mChunks.getLast(); - if (c.mLength == c.mArray.length) { - c = obtainChunk(length); + // Must be called with lock held on sPool. + private void processPoolLocked() { + while (true) { + SoftReference<Chunk> entry = (SoftReference<Chunk>) sQueue.poll(); + if (entry == null) { + break; } + sPool.remove(entry); } - return c; } private Chunk obtainChunk(int length) { - Chunk c; - if (mPool.isEmpty()) { - c = new Chunk(length); - } else { - c = mPool.removeFirst(); + // Correct a small length. + if (length < DEFAULT_CAPACITY) { + length = DEFAULT_CAPACITY; + } + synchronized (sPool) { + // Process any queued references and remove them from the pool. + processPoolLocked(); + if (!sPool.isEmpty()) { + Chunk c = sPool.removeFirst().get(); + // The first item may have been queued after processPoolLocked + // so check for null. + if (c != null) { + return c; + } + } + return new Chunk(length); } - mChunks.addLast(c); - return c; } public static class Chunk { @@ -153,5 +136,19 @@ class ByteArrayBuilder { mArray = new byte[length]; mLength = 0; } + + /** + * Release the chunk and make it available for reuse. + */ + public void release() { + mLength = 0; + synchronized (sPool) { + // Add the chunk back to the pool as a SoftReference so it can + // be gc'd if needed. + sPool.offer(new SoftReference<Chunk>(this, sQueue)); + sPool.notifyAll(); + } + } + } } diff --git a/core/java/android/webkit/CacheManager.java b/core/java/android/webkit/CacheManager.java index c74092e..22dca3a 100644 --- a/core/java/android/webkit/CacheManager.java +++ b/core/java/android/webkit/CacheManager.java @@ -56,6 +56,9 @@ public final class CacheManager { private static long CACHE_THRESHOLD = 6 * 1024 * 1024; private static long CACHE_TRIM_AMOUNT = 2 * 1024 * 1024; + // Limit the maximum cache file size to half of the normal capacity + static long CACHE_MAX_SIZE = (CACHE_THRESHOLD - CACHE_TRIM_AMOUNT) / 2; + private static boolean mDisabled; // Reference count the enable/disable transaction @@ -167,7 +170,7 @@ public final class CacheManager { * @param context The application context. */ static void init(Context context) { - mDataBase = WebViewDatabase.getInstance(context); + mDataBase = WebViewDatabase.getInstance(context.getApplicationContext()); mBaseDir = new File(context.getCacheDir(), "webviewCache"); if (createCacheDirectory() && mClearCacheOnInit) { removeAllCacheFiles(); @@ -288,16 +291,24 @@ public final class CacheManager { // only called from WebCore thread public static CacheResult getCacheFile(String url, Map<String, String> headers) { + return getCacheFile(url, 0, headers); + } + + // only called from WebCore thread + static CacheResult getCacheFile(String url, long postIdentifier, + Map<String, String> headers) { if (mDisabled) { return null; } - CacheResult result = mDataBase.getCache(url); + String databaseKey = getDatabaseKey(url, postIdentifier); + + CacheResult result = mDataBase.getCache(databaseKey); if (result != null) { if (result.contentLength == 0) { if (!checkCacheRedirect(result.httpStatusCode)) { // this should not happen. If it does, remove it. - mDataBase.removeCache(url); + mDataBase.removeCache(databaseKey); return null; } } else { @@ -309,7 +320,7 @@ public final class CacheManager { } catch (FileNotFoundException e) { // the files in the cache directory can be removed by the // system. If it is gone, clean up the database - mDataBase.removeCache(url); + mDataBase.removeCache(databaseKey); return null; } } @@ -357,14 +368,24 @@ public final class CacheManager { // only called from WebCore thread public static CacheResult createCacheFile(String url, int statusCode, Headers headers, String mimeType, boolean forceCache) { + return createCacheFile(url, statusCode, headers, mimeType, 0, + forceCache); + } + + // only called from WebCore thread + static CacheResult createCacheFile(String url, int statusCode, + Headers headers, String mimeType, long postIdentifier, + boolean forceCache) { if (!forceCache && mDisabled) { return null; } + String databaseKey = getDatabaseKey(url, postIdentifier); + // according to the rfc 2616, the 303 response MUST NOT be cached. if (statusCode == 303) { // remove the saved cache if there is any - mDataBase.removeCache(url); + mDataBase.removeCache(databaseKey); return null; } @@ -372,7 +393,7 @@ public final class CacheManager { // header. if (checkCacheRedirect(statusCode) && !headers.getSetCookie().isEmpty()) { // remove the saved cache if there is any - mDataBase.removeCache(url); + mDataBase.removeCache(databaseKey); return null; } @@ -380,9 +401,9 @@ public final class CacheManager { if (ret == null) { // this should only happen if the headers has "no-store" in the // cache-control. remove the saved cache if there is any - mDataBase.removeCache(url); + mDataBase.removeCache(databaseKey); } else { - setupFiles(url, ret); + setupFiles(databaseKey, ret); try { ret.outStream = new FileOutputStream(ret.outFile); } catch (FileNotFoundException e) { @@ -413,6 +434,12 @@ public final class CacheManager { */ // only called from WebCore thread public static void saveCacheFile(String url, CacheResult cacheRet) { + saveCacheFile(url, 0, cacheRet); + } + + // only called from WebCore thread + static void saveCacheFile(String url, long postIdentifier, + CacheResult cacheRet) { try { cacheRet.outStream.close(); } catch (IOException e) { @@ -424,7 +451,6 @@ public final class CacheManager { return; } - cacheRet.contentLength = cacheRet.outFile.length(); boolean redirect = checkCacheRedirect(cacheRet.httpStatusCode); if (redirect) { // location is in database, no need to keep the file @@ -439,13 +465,22 @@ public final class CacheManager { return; } - mDataBase.addCache(url, cacheRet); + mDataBase.addCache(getDatabaseKey(url, postIdentifier), cacheRet); if (DebugFlags.CACHE_MANAGER) { Log.v(LOGTAG, "saveCacheFile for url " + url); } } + static boolean cleanupCacheFile(CacheResult cacheRet) { + try { + cacheRet.outStream.close(); + } catch (IOException e) { + return false; + } + return cacheRet.outFile.delete(); + } + /** * remove all cache files * @@ -518,6 +553,11 @@ public final class CacheManager { } } + private static String getDatabaseKey(String url, long postIdentifier) { + if (postIdentifier == 0) return url; + return postIdentifier + url; + } + @SuppressWarnings("deprecation") private static void setupFiles(String url, CacheResult cacheRet) { if (true) { @@ -615,6 +655,9 @@ public final class CacheManager { private static CacheResult parseHeaders(int statusCode, Headers headers, String mimeType) { + // if the contentLength is already larger than CACHE_MAX_SIZE, skip it + if (headers.getContentLength() > CACHE_MAX_SIZE) return null; + // TODO: if authenticated or secure, return null CacheResult ret = new CacheResult(); ret.httpStatusCode = statusCode; diff --git a/core/java/android/webkit/CallbackProxy.java b/core/java/android/webkit/CallbackProxy.java index e9afcb6..6790c5d 100644 --- a/core/java/android/webkit/CallbackProxy.java +++ b/core/java/android/webkit/CallbackProxy.java @@ -90,7 +90,6 @@ class CallbackProxy extends Handler { private static final int JS_PROMPT = 114; private static final int JS_UNLOAD = 115; private static final int ASYNC_KEYEVENTS = 116; - private static final int TOO_MANY_REDIRECTS = 117; private static final int DOWNLOAD_FILE = 118; private static final int REPORT_ERROR = 119; private static final int RESEND_POST_DATA = 120; @@ -107,6 +106,7 @@ class CallbackProxy extends Handler { private static final int GEOLOCATION_PERMISSIONS_HIDE_PROMPT = 131; private static final int RECEIVED_TOUCH_ICON_URL = 132; private static final int GET_VISITED_HISTORY = 133; + private static final int OPEN_FILE_CHOOSER = 134; // Message triggered by the client to resume execution private static final int NOTIFY = 200; @@ -149,6 +149,16 @@ class CallbackProxy extends Handler { } /** + * Get the WebViewClient. + * @return the current WebViewClient instance. + * + *@hide pending API council approval. + */ + public WebViewClient getWebViewClient() { + return mWebViewClient; + } + + /** * Set the WebChromeClient. * @param client An implementation of WebChromeClient. */ @@ -238,8 +248,10 @@ class CallbackProxy extends Handler { break; case PAGE_FINISHED: + String finishedUrl = (String) msg.obj; + mWebView.onPageFinished(finishedUrl); if (mWebViewClient != null) { - mWebViewClient.onPageFinished(mWebView, (String) msg.obj); + mWebViewClient.onPageFinished(mWebView, finishedUrl); } break; @@ -263,19 +275,6 @@ class CallbackProxy extends Handler { } break; - case TOO_MANY_REDIRECTS: - Message cancelMsg = - (Message) msg.getData().getParcelable("cancelMsg"); - Message continueMsg = - (Message) msg.getData().getParcelable("continueMsg"); - if (mWebViewClient != null) { - mWebViewClient.onTooManyRedirects(mWebView, cancelMsg, - continueMsg); - } else { - cancelMsg.sendToTarget(); - } - break; - case REPORT_ERROR: if (mWebViewClient != null) { int reasonCode = msg.arg1; @@ -660,6 +659,12 @@ class CallbackProxy extends Handler { mWebChromeClient.getVisitedHistory((ValueCallback<String[]>)msg.obj); } break; + + case OPEN_FILE_CHOOSER: + if (mWebChromeClient != null) { + mWebChromeClient.openFileChooser((UploadFile) msg.obj); + } + break; } } @@ -758,11 +763,6 @@ class CallbackProxy extends Handler { } public void onPageFinished(String url) { - // Do an unsynchronized quick check to avoid posting if no callback has - // been set. - if (mWebViewClient == null) { - return; - } // Performance probe if (PERF_PROBE) { // un-comment this if PERF_PROBE is true @@ -776,19 +776,10 @@ class CallbackProxy extends Handler { sendMessage(msg); } + // Because this method is public and because CallbackProxy is mistakenly + // party of the public classes, we cannot remove this method. public void onTooManyRedirects(Message cancelMsg, Message continueMsg) { - // Do an unsynchronized quick check to avoid posting if no callback has - // been set. - if (mWebViewClient == null) { - cancelMsg.sendToTarget(); - return; - } - - Message msg = obtainMessage(TOO_MANY_REDIRECTS); - Bundle bundle = msg.getData(); - bundle.putParcelable("cancelMsg", cancelMsg); - bundle.putParcelable("continueMsg", continueMsg); - sendMessage(msg); + // deprecated. } public void onReceivedError(int errorCode, String description, @@ -1000,10 +991,10 @@ class CallbackProxy extends Handler { public void onProgressChanged(int newProgress) { // Synchronize so that mLatestProgress is up-to-date. synchronized (this) { - mLatestProgress = newProgress; - if (mWebChromeClient == null) { + if (mWebChromeClient == null || mLatestProgress == newProgress) { return; } + mLatestProgress = newProgress; if (!mProgressUpdatePending) { sendEmptyMessage(PROGRESS); mProgressUpdatePending = true; @@ -1335,4 +1326,40 @@ class CallbackProxy extends Handler { msg.obj = callback; sendMessage(msg); } + + private class UploadFile implements ValueCallback<Uri> { + private Uri mValue; + public void onReceiveValue(Uri value) { + mValue = value; + synchronized (CallbackProxy.this) { + CallbackProxy.this.notify(); + } + } + public Uri getResult() { + return mValue; + } + } + + /** + * Called by WebViewCore to open a file chooser. + */ + /* package */ Uri openFileChooser() { + if (mWebChromeClient == null) { + return null; + } + Message myMessage = obtainMessage(OPEN_FILE_CHOOSER); + UploadFile uploadFile = new UploadFile(); + myMessage.obj = uploadFile; + synchronized (this) { + sendMessage(myMessage); + try { + wait(); + } catch (InterruptedException e) { + Log.e(LOGTAG, + "Caught exception while waiting for openFileChooser"); + Log.e(LOGTAG, Log.getStackTraceString(e)); + } + } + return uploadFile.getResult(); + } } diff --git a/core/java/android/webkit/CookieSyncManager.java b/core/java/android/webkit/CookieSyncManager.java index 14375d2..abe9178 100644 --- a/core/java/android/webkit/CookieSyncManager.java +++ b/core/java/android/webkit/CookieSyncManager.java @@ -93,7 +93,7 @@ public final class CookieSyncManager extends WebSyncManager { public static synchronized CookieSyncManager createInstance( Context context) { if (sRef == null) { - sRef = new CookieSyncManager(context); + sRef = new CookieSyncManager(context.getApplicationContext()); } return sRef; } diff --git a/core/java/android/webkit/DateSorter.java b/core/java/android/webkit/DateSorter.java index c46702e..16feaa9 100644 --- a/core/java/android/webkit/DateSorter.java +++ b/core/java/android/webkit/DateSorter.java @@ -38,7 +38,7 @@ public class DateSorter { /** must be >= 3 */ public static final int DAY_COUNT = 5; - private long [] mBins = new long[DAY_COUNT]; + private long [] mBins = new long[DAY_COUNT-1]; private String [] mLabels = new String[DAY_COUNT]; private static final int NUM_DAYS_AGO = 5; @@ -54,15 +54,13 @@ public class DateSorter { // Create the bins mBins[0] = c.getTimeInMillis(); // Today - c.roll(Calendar.DAY_OF_YEAR, -1); + c.add(Calendar.DAY_OF_YEAR, -1); mBins[1] = c.getTimeInMillis(); // Yesterday - c.roll(Calendar.DAY_OF_YEAR, -(NUM_DAYS_AGO - 1)); + c.add(Calendar.DAY_OF_YEAR, -(NUM_DAYS_AGO - 1)); mBins[2] = c.getTimeInMillis(); // Five days ago - c.roll(Calendar.DAY_OF_YEAR, NUM_DAYS_AGO); // move back to today - c.roll(Calendar.MONTH, -1); + c.add(Calendar.DAY_OF_YEAR, NUM_DAYS_AGO); // move back to today + c.add(Calendar.MONTH, -1); mBins[3] = c.getTimeInMillis(); // One month ago - c.roll(Calendar.MONTH, -1); - mBins[4] = c.getTimeInMillis(); // Over one month ago // build labels mLabels[0] = context.getText(com.android.internal.R.string.today).toString(); @@ -84,11 +82,11 @@ public class DateSorter { * date bin this date belongs to */ public int getIndex(long time) { - // Lame linear search - for (int i = 0; i < DAY_COUNT; i++) { + int lastDay = DAY_COUNT - 1; + for (int i = 0; i < lastDay; i++) { if (time > mBins[i]) return i; } - return DAY_COUNT - 1; + return lastDay; } /** @@ -96,6 +94,7 @@ public class DateSorter { * @return string label suitable for display to user */ public String getLabel(int index) { + if (index < 0 || index >= DAY_COUNT) return ""; return mLabels[index]; } @@ -105,17 +104,22 @@ public class DateSorter { * @return date boundary at given index */ public long getBoundary(int index) { + int lastDay = DAY_COUNT - 1; + // Error case + if (index < 0 || index > lastDay) index = 0; + // Since this provides a lower boundary on dates that will be included + // in the given bin, provide the smallest value + if (index == lastDay) return Long.MIN_VALUE; return mBins[index]; } /** * Calcuate 12:00am by zeroing out hour, minute, second, millisecond */ - private Calendar beginningOfDay(Calendar c) { + private void beginningOfDay(Calendar c) { c.set(Calendar.HOUR_OF_DAY, 0); c.set(Calendar.MINUTE, 0); c.set(Calendar.SECOND, 0); c.set(Calendar.MILLISECOND, 0); - return c; } } diff --git a/core/java/android/webkit/DebugFlags.java b/core/java/android/webkit/DebugFlags.java index 8e25395..dca52f6 100644 --- a/core/java/android/webkit/DebugFlags.java +++ b/core/java/android/webkit/DebugFlags.java @@ -32,6 +32,8 @@ class DebugFlags { public static final boolean CALLBACK_PROXY = false; public static final boolean COOKIE_MANAGER = false; public static final boolean COOKIE_SYNC_MANAGER = false; + public static final boolean DRAG_TRACKER = false; + public static final String DRAG_TRACKER_LOGTAG = "skia"; public static final boolean FRAME_LOADER = false; public static final boolean J_WEB_CORE_JAVA_BRIDGE = false;// HIGHLY VERBOSE public static final boolean LOAD_LISTENER = false; diff --git a/core/java/android/webkit/FileLoader.java b/core/java/android/webkit/FileLoader.java index 085f1f4..974ccbf 100644 --- a/core/java/android/webkit/FileLoader.java +++ b/core/java/android/webkit/FileLoader.java @@ -23,9 +23,12 @@ import android.content.res.AssetManager; import android.net.http.EventHandler; import android.net.http.Headers; import android.os.Environment; +import android.util.Log; +import android.util.TypedValue; import java.io.File; import java.io.FileInputStream; +import java.lang.reflect.Field; /** * This class is a concrete implementation of StreamLoader that uses a @@ -35,10 +38,19 @@ import java.io.FileInputStream; class FileLoader extends StreamLoader { private String mPath; // Full path to the file to load - private Context mContext; // Application context, used for asset loads - private boolean mIsAsset; // Indicates if the load is an asset or not + private Context mContext; // Application context, used for asset/res loads + private int mType; // Indicates the type of the load private boolean mAllowFileAccess; // Allow/block file system access + // used for files under asset directory + static final int TYPE_ASSET = 1; + // used for files under res directory + static final int TYPE_RES = 2; + // generic file + static final int TYPE_FILE = 3; + + private static final String LOGTAG = "webkit"; + /** * Construct a FileLoader with the file URL specified as the content * source. @@ -51,19 +63,24 @@ class FileLoader extends StreamLoader { * on the file system. */ FileLoader(String url, LoadListener loadListener, Context context, - boolean asset, boolean allowFileAccess) { + int type, boolean allowFileAccess) { super(loadListener); - mIsAsset = asset; + mType = type; mContext = context; mAllowFileAccess = allowFileAccess; // clean the Url int index = url.indexOf('?'); - if (mIsAsset) { + if (mType == TYPE_ASSET) { mPath = index > 0 ? URLUtil.stripAnchor( url.substring(URLUtil.ASSET_BASE.length(), index)) : URLUtil.stripAnchor(url.substring( URLUtil.ASSET_BASE.length())); + } else if (mType == TYPE_RES) { + mPath = index > 0 ? URLUtil.stripAnchor( + url.substring(URLUtil.RESOURCE_BASE.length(), index)) : + URLUtil.stripAnchor(url.substring( + URLUtil.RESOURCE_BASE.length())); } else { mPath = index > 0 ? URLUtil.stripAnchor( url.substring(URLUtil.FILE_BASE.length(), index)) : @@ -84,13 +101,69 @@ class FileLoader extends StreamLoader { @Override protected boolean setupStreamAndSendStatus() { try { - if (mIsAsset) { + if (mType == TYPE_ASSET) { try { mDataStream = mContext.getAssets().open(mPath); } catch (java.io.FileNotFoundException ex) { // try the rest files included in the package mDataStream = mContext.getAssets().openNonAsset(mPath); } + } else if (mType == TYPE_RES) { + // get the resource id from the path. e.g. for the path like + // drawable/foo.png, the id is located at field "foo" of class + // "<package>.R$drawable" + if (mPath == null || mPath.length() == 0) { + Log.e(LOGTAG, "Need a path to resolve the res file"); + mHandler.error(EventHandler.FILE_ERROR, mContext + .getString(R.string.httpErrorFileNotFound)); + return false; + + } + int slash = mPath.indexOf('/'); + int dot = mPath.indexOf('.', slash); + if (slash == -1 || dot == -1) { + Log.e(LOGTAG, "Incorrect res path: " + mPath); + mHandler.error(EventHandler.FILE_ERROR, mContext + .getString(R.string.httpErrorFileNotFound)); + return false; + } + String subClassName = mPath.substring(0, slash); + String fieldName = mPath.substring(slash + 1, dot); + String errorMsg = null; + try { + final Class<?> d = mContext.getApplicationContext() + .getClassLoader().loadClass( + mContext.getPackageName() + ".R$" + + subClassName); + final Field field = d.getField(fieldName); + final int id = field.getInt(null); + TypedValue value = new TypedValue(); + mContext.getResources().getValue(id, value, true); + if (value.type == TypedValue.TYPE_STRING) { + mDataStream = mContext.getAssets().openNonAsset( + value.assetCookie, value.string.toString(), + AssetManager.ACCESS_STREAMING); + } else { + errorMsg = "Only support TYPE_STRING for the res files"; + } + } catch (ClassNotFoundException e) { + errorMsg = "Can't find class: " + + mContext.getPackageName() + ".R$" + subClassName; + } catch (SecurityException e) { + errorMsg = "Caught SecurityException: " + e; + } catch (NoSuchFieldException e) { + errorMsg = "Can't find field: " + fieldName + " in " + + mContext.getPackageName() + ".R$" + subClassName; + } catch (IllegalArgumentException e) { + errorMsg = "Caught IllegalArgumentException: " + e; + } catch (IllegalAccessException e) { + errorMsg = "Caught IllegalAccessException: " + e; + } + if (errorMsg != null) { + mHandler.error(EventHandler.FILE_ERROR, mContext + .getString(R.string.httpErrorFileNotFound)); + return false; + } } else { if (!mAllowFileAccess) { mHandler.error(EventHandler.FILE_ERROR, @@ -131,8 +204,8 @@ class FileLoader extends StreamLoader { * file system. */ public static void requestUrl(String url, LoadListener loadListener, - Context context, boolean asset, boolean allowFileAccess) { - FileLoader loader = new FileLoader(url, loadListener, context, asset, + Context context, int type, boolean allowFileAccess) { + FileLoader loader = new FileLoader(url, loadListener, context, type, allowFileAccess); loader.load(); } diff --git a/core/java/android/webkit/FrameLoader.java b/core/java/android/webkit/FrameLoader.java index c1eeb3b..51f60c3 100644 --- a/core/java/android/webkit/FrameLoader.java +++ b/core/java/android/webkit/FrameLoader.java @@ -142,11 +142,15 @@ class FrameLoader { } if (URLUtil.isAssetUrl(url)) { FileLoader.requestUrl(url, loadListener, loadListener.getContext(), - true, settings.getAllowFileAccess()); + FileLoader.TYPE_ASSET, true); + return true; + } else if (URLUtil.isResourceUrl(url)) { + FileLoader.requestUrl(url, loadListener, loadListener.getContext(), + FileLoader.TYPE_RES, true); return true; } else if (URLUtil.isFileUrl(url)) { FileLoader.requestUrl(url, loadListener, loadListener.getContext(), - false, settings.getAllowFileAccess()); + FileLoader.TYPE_FILE, settings.getAllowFileAccess()); return true; } else if (URLUtil.isContentUrl(url)) { // Send the raw url to the ContentLoader because it will do a @@ -242,7 +246,7 @@ class FrameLoader { // to load POST content in a history navigation. case WebSettings.LOAD_CACHE_ONLY: { CacheResult result = CacheManager.getCacheFile(mListener.url(), - null); + mListener.postIdentifier(), null); if (result != null) { startCacheLoad(result); } else { @@ -270,7 +274,7 @@ class FrameLoader { // Get the cache file name for the current URL, passing null for // the validation headers causes no validation to occur CacheResult result = CacheManager.getCacheFile(mListener.url(), - null); + mListener.postIdentifier(), null); if (result != null) { startCacheLoad(result); return true; diff --git a/core/java/android/webkit/HTML5VideoViewProxy.java b/core/java/android/webkit/HTML5VideoViewProxy.java index b7a9065..429b335 100644 --- a/core/java/android/webkit/HTML5VideoViewProxy.java +++ b/core/java/android/webkit/HTML5VideoViewProxy.java @@ -48,6 +48,8 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.HashMap; import java.util.Map; +import java.util.Timer; +import java.util.TimerTask; /** * <p>Proxy for HTML5 video views. @@ -71,6 +73,9 @@ class HTML5VideoViewProxy extends Handler private static final int ENDED = 201; private static final int POSTER_FETCHED = 202; + // Timer thread -> UI thread + private static final int TIMEUPDATE = 300; + // The C++ MediaPlayerPrivateAndroid object. int mNativePointer; // The handler for WebCore thread messages; @@ -95,6 +100,22 @@ class HTML5VideoViewProxy extends Handler private static View mProgressView; // The container for the progress view and video view private static FrameLayout mLayout; + // The timer for timeupate events. + // See http://www.whatwg.org/specs/web-apps/current-work/#event-media-timeupdate + private static Timer mTimer; + private static final class TimeupdateTask extends TimerTask { + private HTML5VideoViewProxy mProxy; + + public TimeupdateTask(HTML5VideoViewProxy proxy) { + mProxy = proxy; + } + + public void run() { + mProxy.onTimeupdate(); + } + } + // The spec says the timer should fire every 250 ms or less. + private static final int TIMEUPDATE_PERIOD = 250; // ms private static final WebChromeClient.CustomViewCallback mCallback = new WebChromeClient.CustomViewCallback() { @@ -104,6 +125,8 @@ class HTML5VideoViewProxy extends Handler // which happens when the video view is detached from its parent // view. This happens in the WebChromeClient before this method // is invoked. + mTimer.cancel(); + mTimer = null; mCurrentProxy.playbackEnded(); mCurrentProxy = null; mLayout.removeView(mVideoView); @@ -118,11 +141,19 @@ class HTML5VideoViewProxy extends Handler public static void play(String url, int time, HTML5VideoViewProxy proxy, WebChromeClient client) { + if (mCurrentProxy == proxy) { + if (!mVideoView.isPlaying()) { + mVideoView.start(); + } + return; + } + if (mCurrentProxy != null) { // Some other video is already playing. Notify the caller that its playback ended. proxy.playbackEnded(); return; } + mCurrentProxy = proxy; // Create a FrameLayout that will contain the VideoView and the // progress view (if any). @@ -146,10 +177,23 @@ class HTML5VideoViewProxy extends Handler mProgressView.setVisibility(View.VISIBLE); } mLayout.setVisibility(View.VISIBLE); + mTimer = new Timer(); mVideoView.start(); client.onShowCustomView(mLayout, mCallback); } + public static boolean isPlaying(HTML5VideoViewProxy proxy) { + return (mCurrentProxy == proxy && mVideoView != null && mVideoView.isPlaying()); + } + + public static int getCurrentPosition() { + int currentPosMs = 0; + if (mVideoView != null) { + currentPosMs = mVideoView.getCurrentPosition(); + } + return currentPosMs; + } + public static void seek(int time, HTML5VideoViewProxy proxy) { if (mCurrentProxy == proxy && time >= 0 && mVideoView != null) { mVideoView.seekTo(time); @@ -159,10 +203,13 @@ class HTML5VideoViewProxy extends Handler public static void pause(HTML5VideoViewProxy proxy) { if (mCurrentProxy == proxy && mVideoView != null) { mVideoView.pause(); + mTimer.purge(); } } public static void onPrepared() { + mTimer.schedule(new TimeupdateTask(mCurrentProxy), TIMEUPDATE_PERIOD, TIMEUPDATE_PERIOD); + if (mProgressView == null || mLayout == null) { return; } @@ -199,9 +246,15 @@ class HTML5VideoViewProxy extends Handler public void playbackEnded() { Message msg = Message.obtain(mWebCoreHandler, ENDED); mWebCoreHandler.sendMessage(msg); + // also send a message to ourselves to return to the WebView + sendMessage(obtainMessage(ENDED)); } - // Handler for the messages from WebCore thread to the UI thread. + public void onTimeupdate() { + sendMessage(obtainMessage(TIMEUPDATE)); + } + + // Handler for the messages from WebCore or Timer thread to the UI thread. @Override public void handleMessage(Message msg) { // This executes on the UI thread. @@ -224,6 +277,7 @@ class HTML5VideoViewProxy extends Handler VideoPlayer.pause(this); break; } + case ENDED: case ERROR: { WebChromeClient client = mWebView.getWebChromeClient(); if (client != null) { @@ -238,6 +292,12 @@ class HTML5VideoViewProxy extends Handler } break; } + case TIMEUPDATE: { + if (VideoPlayer.isPlaying(this)) { + sendTimeupdate(); + } + break; + } } } @@ -407,6 +467,9 @@ class HTML5VideoViewProxy extends Handler Bitmap poster = (Bitmap) msg.obj; nativeOnPosterFetched(poster, mNativePointer); break; + case TIMEUPDATE: + nativeOnTimeupdate(msg.arg1, mNativePointer); + break; } } }; @@ -423,6 +486,12 @@ class HTML5VideoViewProxy extends Handler mWebCoreHandler.sendMessage(msg); } + private void sendTimeupdate() { + Message msg = Message.obtain(mWebCoreHandler, TIMEUPDATE); + msg.arg1 = VideoPlayer.getCurrentPosition(); + mWebCoreHandler.sendMessage(msg); + } + public Context getContext() { return mWebView.getContext(); } @@ -503,4 +572,5 @@ class HTML5VideoViewProxy extends Handler private native void nativeOnPrepared(int duration, int width, int height, int nativePointer); private native void nativeOnEnded(int nativePointer); private native void nativeOnPosterFetched(Bitmap poster, int nativePointer); + private native void nativeOnTimeupdate(int position, int nativePointer); } diff --git a/core/java/android/webkit/HttpDateTime.java b/core/java/android/webkit/HttpDateTime.java index 2f46f2b..042953c 100644 --- a/core/java/android/webkit/HttpDateTime.java +++ b/core/java/android/webkit/HttpDateTime.java @@ -50,13 +50,15 @@ public final class HttpDateTime { * Wdy Mon DD HH:MM:SS YYYY GMT * * HH can be H if the first digit is zero. + * + * Mon can be the full name of the month. */ private static final String HTTP_DATE_RFC_REGEXP = - "([0-9]{1,2})[- ]([A-Za-z]{3,3})[- ]([0-9]{2,4})[ ]" + "([0-9]{1,2})[- ]([A-Za-z]{3,9})[- ]([0-9]{2,4})[ ]" + "([0-9]{1,2}:[0-9][0-9]:[0-9][0-9])"; private static final String HTTP_DATE_ANSIC_REGEXP = - "[ ]([A-Za-z]{3,3})[ ]+([0-9]{1,2})[ ]" + "[ ]([A-Za-z]{3,9})[ ]+([0-9]{1,2})[ ]" + "([0-9]{1,2}:[0-9][0-9]:[0-9][0-9])[ ]([0-9]{2,4})"; /** diff --git a/core/java/android/webkit/LoadListener.java b/core/java/android/webkit/LoadListener.java index 4c17f99..cdc6608 100644 --- a/core/java/android/webkit/LoadListener.java +++ b/core/java/android/webkit/LoadListener.java @@ -16,9 +16,14 @@ package android.webkit; +import android.content.ActivityNotFoundException; import android.content.Context; -import android.net.WebAddress; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; import android.net.ParseException; +import android.net.Uri; +import android.net.WebAddress; import android.net.http.EventHandler; import android.net.http.Headers; import android.net.http.HttpAuthHeader; @@ -78,7 +83,7 @@ class LoadListener extends Handler implements EventHandler { private static int sNativeLoaderCount; - private final ByteArrayBuilder mDataBuilder = new ByteArrayBuilder(8192); + private final ByteArrayBuilder mDataBuilder = new ByteArrayBuilder(); private String mUrl; private WebAddress mUri; @@ -104,6 +109,7 @@ class LoadListener extends Handler implements EventHandler { private SslError mSslError; private RequestHandle mRequestHandle; private RequestHandle mSslErrorRequestHandle; + private long mPostIdentifier; // Request data. It is only valid when we are doing a load from the // cache. It is needed if the cache returns a redirect @@ -116,6 +122,8 @@ class LoadListener extends Handler implements EventHandler { // Does this loader correspond to the main-frame top-level page? private boolean mIsMainPageLoader; + private final boolean mIsMainResourceLoader; + private final boolean mUserGesture; private Headers mHeaders; @@ -123,13 +131,14 @@ class LoadListener extends Handler implements EventHandler { // Public functions // ========================================================================= - public static LoadListener getLoadListener( - Context context, BrowserFrame frame, String url, - int nativeLoader, boolean synchronous, boolean isMainPageLoader) { + public static LoadListener getLoadListener(Context context, + BrowserFrame frame, String url, int nativeLoader, + boolean synchronous, boolean isMainPageLoader, + boolean isMainResource, boolean userGesture, long postIdentifier) { sNativeLoaderCount += 1; - return new LoadListener( - context, frame, url, nativeLoader, synchronous, isMainPageLoader); + return new LoadListener(context, frame, url, nativeLoader, synchronous, + isMainPageLoader, isMainResource, userGesture, postIdentifier); } public static int getNativeLoaderCount() { @@ -137,7 +146,8 @@ class LoadListener extends Handler implements EventHandler { } LoadListener(Context context, BrowserFrame frame, String url, - int nativeLoader, boolean synchronous, boolean isMainPageLoader) { + int nativeLoader, boolean synchronous, boolean isMainPageLoader, + boolean isMainResource, boolean userGesture, long postIdentifier) { if (DebugFlags.LOAD_LISTENER) { Log.v(LOGTAG, "LoadListener constructor url=" + url); } @@ -150,6 +160,9 @@ class LoadListener extends Handler implements EventHandler { mMessageQueue = new Vector<Message>(); } mIsMainPageLoader = isMainPageLoader; + mIsMainResourceLoader = isMainResource; + mUserGesture = userGesture; + mPostIdentifier = postIdentifier; } /** @@ -291,6 +304,13 @@ class LoadListener extends Handler implements EventHandler { sendMessageInternal(obtainMessage(MSG_CONTENT_HEADERS, headers)); } + // This is the same regex that DOMImplementation uses to check for xml + // content. Use this to check if another Activity wants to handle the + // content before giving it to webkit. + private static final String XML_MIME_TYPE = + "^[\\w_\\-+~!$\\^{}|.%'`#&*]+/" + + "[\\w_\\-+~!$\\^{}|.%'`#&*]+\\+xml$"; + // Does the header parsing work on the WebCore thread. private void handleHeaders(Headers headers) { if (mCancelled) return; @@ -349,6 +369,26 @@ class LoadListener extends Handler implements EventHandler { than the headers that are returned from the server. */ guessMimeType(); } + // At this point, mMimeType has been set to non-null. + if (mIsMainPageLoader && mIsMainResourceLoader && mUserGesture && + Pattern.matches(XML_MIME_TYPE, mMimeType)) { + Intent i = new Intent(Intent.ACTION_VIEW); + i.setDataAndType(Uri.parse(url()), mMimeType); + ResolveInfo info = mContext.getPackageManager().resolveActivity(i, + PackageManager.MATCH_DEFAULT_ONLY); + if (info != null && !mContext.getPackageName().equals( + info.activityInfo.packageName)) { + // someone (other than the current app) knows how to + // handle this mime type. + try { + mContext.startActivity(i); + mBrowserFrame.stopLoading(); + return; + } catch (ActivityNotFoundException ex) { + // continue loading internally. + } + } + } // is it an authentication request? boolean mustAuthenticate = (mStatusCode == HTTP_AUTH || @@ -408,9 +448,14 @@ class LoadListener extends Handler implements EventHandler { mStatusCode == HTTP_MOVED_PERMANENTLY || mStatusCode == HTTP_TEMPORARY_REDIRECT) && mNativeLoader != 0) { - if (!mFromCache && mRequestHandle != null) { + // for POST request, only cache the result if there is an identifier + // associated with it. postUrl() or form submission should set the + // identifier while XHR POST doesn't. + if (!mFromCache && mRequestHandle != null + && (!mRequestHandle.getMethod().equals("POST") + || mPostIdentifier != 0)) { mCacheResult = CacheManager.createCacheFile(mUrl, mStatusCode, - headers, mMimeType, false); + headers, mMimeType, mPostIdentifier, false); } if (mCacheResult != null) { mCacheResult.encoding = mEncoding; @@ -522,17 +567,18 @@ class LoadListener extends Handler implements EventHandler { * IMPORTANT: as this is called from network thread, can't call native * directly * XXX: Unlike the other network thread methods, this method can do the - * work of decoding the data and appending it to the data builder because - * mDataBuilder is a thread-safe structure. + * work of decoding the data and appending it to the data builder. */ public void data(byte[] data, int length) { if (DebugFlags.LOAD_LISTENER) { Log.v(LOGTAG, "LoadListener.data(): url: " + url()); } - // Synchronize on mData because commitLoad may write mData to WebCore - // and we don't want to replace mData or mDataLength at the same time - // as a write. + // The reason isEmpty() and append() need to synchronized together is + // because it is possible for getFirstChunk() to be called multiple + // times between isEmpty() and append(). This could cause commitLoad() + // to finish before processing the newly appended data and no message + // will be sent. boolean sendMessage = false; synchronized (mDataBuilder) { sendMessage = mDataBuilder.isEmpty(); @@ -636,7 +682,7 @@ class LoadListener extends Handler implements EventHandler { */ boolean checkCache(Map<String, String> headers) { // Get the cache file name for the current URL - CacheResult result = CacheManager.getCacheFile(url(), + CacheResult result = CacheManager.getCacheFile(url(), mPostIdentifier, headers); // Go ahead and set the cache loader to null in case the result is @@ -861,6 +907,10 @@ class LoadListener extends Handler implements EventHandler { } } + long postIdentifier() { + return mPostIdentifier; + } + void attachRequestHandle(RequestHandle requestHandle) { if (DebugFlags.LOAD_LISTENER) { Log.v(LOGTAG, "LoadListener.attachRequestHandle(): " + @@ -886,8 +936,11 @@ class LoadListener extends Handler implements EventHandler { void downloadFile() { // Setting the Cache Result to null ensures that this // content is not added to the cache - mCacheResult = null; - + if (mCacheResult != null) { + CacheManager.cleanupCacheFile(mCacheResult); + mCacheResult = null; + } + // Inform the client that they should download a file mBrowserFrame.getCallbackProxy().onDownloadStart(url(), mBrowserFrame.getUserAgentString(), @@ -907,8 +960,9 @@ class LoadListener extends Handler implements EventHandler { * be used. This is just for forward/back navigation to a POST * URL. */ - static boolean willLoadFromCache(String url) { - boolean inCache = CacheManager.getCacheFile(url, null) != null; + static boolean willLoadFromCache(String url, long identifier) { + boolean inCache = + CacheManager.getCacheFile(url, identifier, null) != null; if (DebugFlags.LOAD_LISTENER) { Log.v(LOGTAG, "willLoadFromCache: " + url + " in cache: " + inCache); @@ -1009,28 +1063,34 @@ class LoadListener extends Handler implements EventHandler { if (mIsMainPageLoader) { String type = sCertificateTypeMap.get(mMimeType); if (type != null) { - // In the case of downloading certificate, we will save it to - // the KeyStore and stop the current loading so that it will not - // generate a new history page - byte[] cert = new byte[mDataBuilder.getByteSize()]; - int offset = 0; - while (true) { - ByteArrayBuilder.Chunk c = mDataBuilder.getFirstChunk(); - if (c == null) break; - - if (c.mLength != 0) { - System.arraycopy(c.mArray, 0, cert, offset, c.mLength); - offset += c.mLength; + // This must be synchronized so that no more data can be added + // after getByteSize returns. + synchronized (mDataBuilder) { + // In the case of downloading certificate, we will save it + // to the KeyStore and stop the current loading so that it + // will not generate a new history page + byte[] cert = new byte[mDataBuilder.getByteSize()]; + int offset = 0; + while (true) { + ByteArrayBuilder.Chunk c = mDataBuilder.getFirstChunk(); + if (c == null) break; + + if (c.mLength != 0) { + System.arraycopy(c.mArray, 0, cert, offset, c.mLength); + offset += c.mLength; + } + c.release(); } - mDataBuilder.releaseChunk(c); + CertTool.addCertificate(mContext, type, cert); + mBrowserFrame.stopLoading(); + return; } - CertTool.addCertificate(mContext, type, cert); - mBrowserFrame.stopLoading(); - return; } } - // Give the data to WebKit now + // Give the data to WebKit now. We don't have to synchronize on + // mDataBuilder here because pulling each chunk removes it from the + // internal list so it cannot be modified. PerfChecker checker = new PerfChecker(); ByteArrayBuilder.Chunk c; while (true) { @@ -1039,15 +1099,23 @@ class LoadListener extends Handler implements EventHandler { if (c.mLength != 0) { if (mCacheResult != null) { - try { - mCacheResult.outStream.write(c.mArray, 0, c.mLength); - } catch (IOException e) { + mCacheResult.contentLength += c.mLength; + if (mCacheResult.contentLength > CacheManager.CACHE_MAX_SIZE) { + CacheManager.cleanupCacheFile(mCacheResult); mCacheResult = null; + } else { + try { + mCacheResult.outStream + .write(c.mArray, 0, c.mLength); + } catch (IOException e) { + CacheManager.cleanupCacheFile(mCacheResult); + mCacheResult = null; + } } } nativeAddData(c.mArray, c.mLength); } - mDataBuilder.releaseChunk(c); + c.release(); checker.responseAlert("res nativeAddData"); } } @@ -1059,7 +1127,9 @@ class LoadListener extends Handler implements EventHandler { void tearDown() { if (mCacheResult != null) { if (getErrorID() == OK) { - CacheManager.saveCacheFile(mUrl, mCacheResult); + CacheManager.saveCacheFile(mUrl, mPostIdentifier, mCacheResult); + } else { + CacheManager.cleanupCacheFile(mCacheResult); } // we need to reset mCacheResult to be null @@ -1124,7 +1194,10 @@ class LoadListener extends Handler implements EventHandler { mRequestHandle = null; } - mCacheResult = null; + if (mCacheResult != null) { + CacheManager.cleanupCacheFile(mCacheResult); + mCacheResult = null; + } mCancelled = true; clearNativeLoader(); @@ -1187,7 +1260,10 @@ class LoadListener extends Handler implements EventHandler { // Cache the redirect response if (mCacheResult != null) { if (getErrorID() == OK) { - CacheManager.saveCacheFile(mUrl, mCacheResult); + CacheManager.saveCacheFile(mUrl, mPostIdentifier, + mCacheResult); + } else { + CacheManager.cleanupCacheFile(mCacheResult); } mCacheResult = null; } @@ -1210,8 +1286,17 @@ class LoadListener extends Handler implements EventHandler { // mRequestHandle can be null when the request was satisfied // by the cache, and the cache returned a redirect if (mRequestHandle != null) { - mRequestHandle.setupRedirect(mUrl, mStatusCode, - mRequestHeaders); + try { + mRequestHandle.setupRedirect(mUrl, mStatusCode, + mRequestHeaders); + } catch(RuntimeException e) { + Log.e(LOGTAG, e.getMessage()); + // Signal a bad url error if we could not load the + // redirection. + handleError(EventHandler.ERROR_BAD_URL, + mContext.getString(R.string.httpErrorBadUrl)); + return; + } } else { // If the original request came from the cache, there is no // RequestHandle, we have to create a new one through diff --git a/core/java/android/webkit/MimeTypeMap.java b/core/java/android/webkit/MimeTypeMap.java index fffba1b..84a8a3c 100644 --- a/core/java/android/webkit/MimeTypeMap.java +++ b/core/java/android/webkit/MimeTypeMap.java @@ -431,6 +431,8 @@ public class MimeTypeMap { sMimeTypeMap.loadEntry("text/calendar", "icz"); sMimeTypeMap.loadEntry("text/comma-separated-values", "csv"); sMimeTypeMap.loadEntry("text/css", "css"); + sMimeTypeMap.loadEntry("text/html", "htm"); + sMimeTypeMap.loadEntry("text/html", "html"); sMimeTypeMap.loadEntry("text/h323", "323"); sMimeTypeMap.loadEntry("text/iuls", "uls"); sMimeTypeMap.loadEntry("text/mathml", "mml"); @@ -481,6 +483,7 @@ public class MimeTypeMap { sMimeTypeMap.loadEntry("video/dv", "dif"); sMimeTypeMap.loadEntry("video/dv", "dv"); sMimeTypeMap.loadEntry("video/fli", "fli"); + sMimeTypeMap.loadEntry("video/m4v", "m4v"); sMimeTypeMap.loadEntry("video/mpeg", "mpeg"); sMimeTypeMap.loadEntry("video/mpeg", "mpg"); sMimeTypeMap.loadEntry("video/mpeg", "mpe"); diff --git a/core/java/android/webkit/Network.java b/core/java/android/webkit/Network.java index af0cb1e..598f20d 100644 --- a/core/java/android/webkit/Network.java +++ b/core/java/android/webkit/Network.java @@ -163,10 +163,10 @@ class Network { return false; } - // asset, file system or data stream are handled in the other code path. - // This only handles network request. - if (URLUtil.isAssetUrl(url) || URLUtil.isFileUrl(url) || - URLUtil.isDataUrl(url)) { + // asset, res, file system or data stream are handled in the other code + // path. This only handles network request. + if (URLUtil.isAssetUrl(url) || URLUtil.isResourceUrl(url) + || URLUtil.isFileUrl(url) || URLUtil.isDataUrl(url)) { return false; } @@ -180,20 +180,24 @@ class Network { } RequestQueue q = mRequestQueue; + RequestHandle handle = null; if (loader.isSynchronous()) { - q = new RequestQueue(loader.getContext(), 1); - } - - RequestHandle handle = q.queueRequest( - url, loader.getWebAddress(), method, headers, loader, - bodyProvider, bodyLength); - loader.attachRequestHandle(handle); - - if (loader.isSynchronous()) { - handle.waitUntilComplete(); + handle = q.queueSynchronousRequest(url, loader.getWebAddress(), + method, headers, loader, bodyProvider, bodyLength); + loader.attachRequestHandle(handle); + handle.processRequest(); loader.loadSynchronousMessages(); - q.shutdown(); + } else { + handle = q.queueRequest(url, loader.getWebAddress(), method, + headers, loader, bodyProvider, bodyLength); + // FIXME: Although this is probably a rare condition, normal network + // requests are processed in a separate thread. This means that it + // is possible to process part of the request before setting the + // request handle on the loader. We should probably refactor this to + // ensure the handle is attached before processing begins. + loader.attachRequestHandle(handle); } + return true; } diff --git a/core/java/android/webkit/PluginActivity.java b/core/java/android/webkit/PluginActivity.java deleted file mode 100644 index cda7b59..0000000 --- a/core/java/android/webkit/PluginActivity.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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 android.webkit; - -import android.app.Activity; -import android.content.Intent; -import android.os.Bundle; -import android.view.View; - -/** - * This activity is invoked when a plugin elects to go into full screen mode. - * @hide - */ -public class PluginActivity extends Activity { - - /* package */ static final String INTENT_EXTRA_PACKAGE_NAME = - "android.webkit.plugin.PACKAGE_NAME"; - /* package */ static final String INTENT_EXTRA_CLASS_NAME = - "android.webkit.plugin.CLASS_NAME"; - /* package */ static final String INTENT_EXTRA_NPP_INSTANCE = - "android.webkit.plugin.NPP_INSTANCE"; - - /** Called when the activity is first created. */ - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - final Intent intent = getIntent(); - if (intent == null) { - // No intent means no class to lookup. - finish(); - } - final String packageName = - intent.getStringExtra(INTENT_EXTRA_PACKAGE_NAME); - final String className = intent.getStringExtra(INTENT_EXTRA_CLASS_NAME); - final int npp = intent.getIntExtra(INTENT_EXTRA_NPP_INSTANCE, -1); - // Retrieve the PluginStub implemented in packageName.className - PluginStub stub = - PluginUtil.getPluginStub(this, packageName, className); - - if (stub != null) { - View pluginView = stub.getFullScreenView(npp, this); - if (pluginView != null) { - setContentView(pluginView); - } else { - // No custom full-sreen view returned by the plugin, odd but - // just in case, finish the activity. - finish(); - } - } else { - finish(); - } - } -} diff --git a/core/java/android/webkit/PluginFullScreenHolder.java b/core/java/android/webkit/PluginFullScreenHolder.java new file mode 100644 index 0000000..b641803 --- /dev/null +++ b/core/java/android/webkit/PluginFullScreenHolder.java @@ -0,0 +1,133 @@ +/* + * Copyright 2009, The Android Open Source Project + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package android.webkit; + +import android.app.Dialog; +import android.graphics.Rect; +import android.util.Log; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; + +class PluginFullScreenHolder extends Dialog { + + private static final String LOGTAG = "FullScreenHolder"; + + private final WebView mWebView; + private final int mNpp; + private View mContentView; + private int mX; + private int mY; + private int mWidth; + private int mHeight; + + PluginFullScreenHolder(WebView webView, int npp) { + super(webView.getContext(), android.R.style.Theme_NoTitleBar_Fullscreen); + mWebView = webView; + mNpp = npp; + } + + Rect getBound() { + return new Rect(mX, mY, mWidth, mHeight); + } + + /* + * x, y, width, height are in the caller's view coordinate system. (x, y) is + * relative to the top left corner of the caller's view. + */ + void updateBound(int x, int y, int width, int height) { + mX = x; + mY = y; + mWidth = width; + mHeight = height; + } + + @Override + public void setContentView(View contentView) { + super.setContentView(contentView); + mContentView = contentView; + } + + @Override + public void onBackPressed() { + mWebView.mPrivateHandler.obtainMessage(WebView.HIDE_FULLSCREEN) + .sendToTarget(); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (event.isSystem()) { + return super.onKeyDown(keyCode, event); + } + mWebView.onKeyDown(keyCode, event); + // always return true as we are the handler + return true; + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (event.isSystem()) { + return super.onKeyUp(keyCode, event); + } + mWebView.onKeyUp(keyCode, event); + // always return true as we are the handler + return true; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + final float x = event.getX(); + final float y = event.getY(); + // TODO: find a way to know when the dialog size changed so that we can + // cache the ratio + final View decorView = getWindow().getDecorView(); + event.setLocation(mX + x * mWidth / decorView.getWidth(), + mY + y * mHeight / decorView.getHeight()); + mWebView.onTouchEvent(event); + // always return true as we are the handler + return true; + } + + @Override + public boolean onTrackballEvent(MotionEvent event) { + mWebView.onTrackballEvent(event); + // always return true as we are the handler + return true; + } + + @Override + protected void onStop() { + super.onStop(); + // manually remove the contentView's parent since the dialog does not + if (mContentView != null && mContentView.getParent() != null) { + ViewGroup vg = (ViewGroup) mContentView.getParent(); + vg.removeView(mContentView); + } + mWebView.getWebViewCore().sendMessage( + WebViewCore.EventHub.HIDE_FULLSCREEN, mNpp, 0); + } + +} diff --git a/core/java/android/webkit/PluginManager.java b/core/java/android/webkit/PluginManager.java index 4588f46..cdcb662 100644 --- a/core/java/android/webkit/PluginManager.java +++ b/core/java/android/webkit/PluginManager.java @@ -21,6 +21,7 @@ import java.util.List; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; +import android.app.Service; import android.content.Context; import android.content.Intent; import android.content.pm.PackageInfo; @@ -60,6 +61,9 @@ public class PluginManager { private static final String LOGTAG = "webkit"; + private static final String PLUGIN_TYPE = "type"; + private static final String TYPE_NATIVE = "native"; + private static PluginManager mInstance = null; private final Context mContext; @@ -85,7 +89,7 @@ public class PluginManager { throw new IllegalStateException( "First call to PluginManager need a valid context."); } - mInstance = new PluginManager(context); + mInstance = new PluginManager(context.getApplicationContext()); } return mInstance; } @@ -108,7 +112,8 @@ public class PluginManager { ArrayList<String> directories = new ArrayList<String>(); PackageManager pm = mContext.getPackageManager(); List<ResolveInfo> plugins = pm.queryIntentServices(new Intent( - PLUGIN_ACTION), PackageManager.GET_SERVICES); + PLUGIN_ACTION), PackageManager.GET_SERVICES + | PackageManager.GET_META_DATA); synchronized(mPackageInfoCache) { @@ -116,27 +121,35 @@ public class PluginManager { mPackageInfoCache.clear(); for (ResolveInfo info : plugins) { + + // retrieve the plugin's service information ServiceInfo serviceInfo = info.serviceInfo; if (serviceInfo == null) { Log.w(LOGTAG, "Ignore bad plugin"); continue; } + + // retrieve information from the plugin's manifest PackageInfo pkgInfo; try { pkgInfo = pm.getPackageInfo(serviceInfo.packageName, PackageManager.GET_PERMISSIONS | PackageManager.GET_SIGNATURES); } catch (NameNotFoundException e) { - Log.w(LOGTAG, "Cant find plugin: " + serviceInfo.packageName); + Log.w(LOGTAG, "Can't find plugin: " + serviceInfo.packageName); continue; } if (pkgInfo == null) { continue; } + + // check if their is a conflict in the lib directory names String directory = pkgInfo.applicationInfo.dataDir + "/lib"; if (directories.contains(directory)) { continue; } + + // check if the plugin has the required permissions String permissions[] = pkgInfo.requestedPermissions; if (permissions == null) { continue; @@ -151,6 +164,8 @@ public class PluginManager { if (!permissionOk) { continue; } + + // check to ensure the plugin is properly signed Signature signatures[] = pkgInfo.signatures; if (signatures == null) { continue; @@ -169,6 +184,39 @@ public class PluginManager { continue; } } + + // determine the type of plugin from the manifest + if (serviceInfo.metaData == null) { + Log.e(LOGTAG, "The plugin '" + serviceInfo.name + "' has no type defined"); + continue; + } + + String pluginType = serviceInfo.metaData.getString(PLUGIN_TYPE); + if (!TYPE_NATIVE.equals(pluginType)) { + Log.e(LOGTAG, "Unrecognized plugin type: " + pluginType); + continue; + } + + try { + Class<?> cls = getPluginClass(serviceInfo.packageName, serviceInfo.name); + + //TODO implement any requirements of the plugin class here! + boolean classFound = true; + + if (!classFound) { + Log.e(LOGTAG, "The plugin's class' " + serviceInfo.name + "' does not extend the appropriate class."); + continue; + } + + } catch (NameNotFoundException e) { + Log.e(LOGTAG, "Can't find plugin: " + serviceInfo.packageName); + continue; + } catch (ClassNotFoundException e) { + Log.e(LOGTAG, "Can't find plugin's class: " + serviceInfo.name); + continue; + } + + // if all checks have passed then make the plugin available mPackageInfoCache.add(pkgInfo); directories.add(directory); } @@ -177,6 +225,7 @@ public class PluginManager { return directories.toArray(new String[directories.size()]); } + /* package */ String getPluginsAPKName(String pluginLib) { // basic error checking on input params @@ -200,4 +249,14 @@ public class PluginManager { String getPluginSharedDataDirectory() { return mContext.getDir("plugins", 0).getPath(); } + + /* package */ + Class<?> getPluginClass(String packageName, String className) + throws NameNotFoundException, ClassNotFoundException { + Context pluginContext = mContext.createPackageContext(packageName, + Context.CONTEXT_INCLUDE_CODE | + Context.CONTEXT_IGNORE_SECURITY); + ClassLoader pluginCL = pluginContext.getClassLoader(); + return pluginCL.loadClass(className); + } } diff --git a/core/java/android/webkit/PluginUtil.java b/core/java/android/webkit/PluginUtil.java deleted file mode 100644 index 8fdbd67..0000000 --- a/core/java/android/webkit/PluginUtil.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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 android.webkit; - -import android.content.Context; -import android.content.pm.PackageManager.NameNotFoundException; -import android.util.Log; - -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; - -class PluginUtil { - - private static final String LOGTAG = "PluginUtil"; - - /** - * - * @param packageName the name of the apk where the class can be found - * @param className the fully qualified name of a subclass of PluginStub - */ - /* package */ - static PluginStub getPluginStub(Context context, String packageName, - String className) { - try { - Context pluginContext = context.createPackageContext(packageName, - Context.CONTEXT_INCLUDE_CODE | - Context.CONTEXT_IGNORE_SECURITY); - ClassLoader pluginCL = pluginContext.getClassLoader(); - - Class<?> stubClass = pluginCL.loadClass(className); - Object stubObject = stubClass.newInstance(); - - if (stubObject instanceof PluginStub) { - return (PluginStub) stubObject; - } else { - Log.e(LOGTAG, "The plugin class is not of type PluginStub"); - } - } catch (Exception e) { - // Any number of things could have happened. Log the exception and - // return null. Careful not to use Log.e(LOGTAG, "String", e) - // because that reports the exception to the checkin service. - Log.e(LOGTAG, Log.getStackTraceString(e)); - } - return null; - } -} diff --git a/core/java/android/webkit/URLUtil.java b/core/java/android/webkit/URLUtil.java index 232ed36..7c5f2b0 100644 --- a/core/java/android/webkit/URLUtil.java +++ b/core/java/android/webkit/URLUtil.java @@ -28,8 +28,14 @@ import android.util.Log; public final class URLUtil { private static final String LOGTAG = "webkit"; - + + // to refer to bar.png under your package's asset/foo/ directory, use + // "file:///android_asset/foo/bar.png". static final String ASSET_BASE = "file:///android_asset/"; + // to refer to bar.png under your package's res/drawable/ directory, use + // "file:///android_res/drawable/bar.png". Use "drawable" to refer to + // "drawable-hdpi" directory as well. + static final String RESOURCE_BASE = "file:///android_res/"; static final String FILE_BASE = "file://"; static final String PROXY_BASE = "file:///cookieless_proxy/"; @@ -166,7 +172,15 @@ public final class URLUtil { public static boolean isAssetUrl(String url) { return (null != url) && url.startsWith(ASSET_BASE); } - + + /** + * @return True iff the url is a resource file. + * @hide + */ + public static boolean isResourceUrl(String url) { + return (null != url) && url.startsWith(RESOURCE_BASE); + } + /** * @return True iff the url is an proxy url to allow cookieless network * requests from a file url. @@ -251,6 +265,7 @@ public final class URLUtil { } return (isAssetUrl(url) || + isResourceUrl(url) || isFileUrl(url) || isAboutUrl(url) || isHttpUrl(url) || @@ -367,19 +382,23 @@ public final class URLUtil { /** Regex used to parse content-disposition headers */ private static final Pattern CONTENT_DISPOSITION_PATTERN = - Pattern.compile("attachment;\\s*filename\\s*=\\s*\"([^\"]*)\""); + Pattern.compile("attachment;\\s*filename\\s*=\\s*(\"?)([^\"]*)\\1\\s*$", + Pattern.CASE_INSENSITIVE); /* * Parse the Content-Disposition HTTP Header. The format of the header * is defined here: http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html * This header provides a filename for content that is going to be * downloaded to the file system. We only support the attachment type. + * Note that RFC 2616 specifies the filename value must be double-quoted. + * Unfortunately some servers do not quote the value so to maintain + * consistent behaviour with other browsers, we allow unquoted values too. */ static String parseContentDisposition(String contentDisposition) { try { Matcher m = CONTENT_DISPOSITION_PATTERN.matcher(contentDisposition); if (m.find()) { - return m.group(1); + return m.group(2); } } catch (IllegalStateException ex) { // This function is defined as returning null when it can't parse the header diff --git a/core/java/android/webkit/ViewManager.java b/core/java/android/webkit/ViewManager.java index 6a838c3..75db0a0 100644 --- a/core/java/android/webkit/ViewManager.java +++ b/core/java/android/webkit/ViewManager.java @@ -16,7 +16,6 @@ package android.webkit; -import android.content.Context; import android.view.View; import android.widget.AbsoluteLayout; @@ -26,6 +25,7 @@ class ViewManager { private final WebView mWebView; private final ArrayList<ChildView> mChildren = new ArrayList<ChildView>(); private boolean mHidden; + private boolean mReadyToDraw; class ChildView { int x; @@ -70,6 +70,9 @@ class ViewManager { void attachViewOnUIThread(AbsoluteLayout.LayoutParams lp) { mWebView.addView(mView, lp); mChildren.add(this); + if (!mReadyToDraw) { + mView.setVisibility(View.GONE); + } } void removeView() { @@ -154,4 +157,23 @@ class ViewManager { } mHidden = false; } + + void postResetStateAll() { + mWebView.mPrivateHandler.post(new Runnable() { + public void run() { + mReadyToDraw = false; + } + }); + } + + void postReadyToDrawAll() { + mWebView.mPrivateHandler.post(new Runnable() { + public void run() { + mReadyToDraw = true; + for (ChildView v : mChildren) { + v.mView.setVisibility(View.VISIBLE); + } + } + }); + } } diff --git a/core/java/android/webkit/WebChromeClient.java b/core/java/android/webkit/WebChromeClient.java index 8ca4142..f40b55c 100644 --- a/core/java/android/webkit/WebChromeClient.java +++ b/core/java/android/webkit/WebChromeClient.java @@ -17,6 +17,7 @@ package android.webkit; import android.graphics.Bitmap; +import android.net.Uri; import android.os.Message; import android.view.View; @@ -294,4 +295,13 @@ public class WebChromeClient { public void getVisitedHistory(ValueCallback<String[]> callback) { } + /** + * Tell the client to open a file chooser. + * @param uploadFile A ValueCallback to set the URI of the file to upload. + * onReceiveValue must be called to wake up the thread. + * @hide + */ + public void openFileChooser(ValueCallback<Uri> uploadFile) { + uploadFile.onReceiveValue(null); + } } diff --git a/core/java/android/webkit/WebSettings.java b/core/java/android/webkit/WebSettings.java index 6f3262a..d5bb572 100644 --- a/core/java/android/webkit/WebSettings.java +++ b/core/java/android/webkit/WebSettings.java @@ -152,6 +152,7 @@ public class WebSettings { private int mMinimumLogicalFontSize = 8; private int mDefaultFontSize = 16; private int mDefaultFixedFontSize = 13; + private int mPageCacheCapacity = 0; private boolean mLoadsImagesAutomatically = true; private boolean mBlockNetworkImage = false; private boolean mBlockNetworkLoads; @@ -879,6 +880,20 @@ public class WebSettings { } /** + * Set the number of pages cached by the WebKit for the history navigation. + * @param size A non-negative integer between 0 (no cache) and 20 (max). + * @hide + */ + public synchronized void setPageCacheCapacity(int size) { + if (size < 0) size = 0; + if (size > 20) size = 20; + if (mPageCacheCapacity != size) { + mPageCacheCapacity = size; + postSync(); + } + } + + /** * Tell the WebView to load image resources automatically. * @param flag True if the WebView should load images automatically. */ @@ -1002,7 +1017,8 @@ public class WebSettings { * should never be null. */ public synchronized void setGeolocationDatabasePath(String databasePath) { - if (databasePath != null && !databasePath.equals(mDatabasePath)) { + if (databasePath != null + && !databasePath.equals(mGeolocationDatabasePath)) { mGeolocationDatabasePath = databasePath; postSync(); } diff --git a/core/java/android/webkit/WebStorage.java b/core/java/android/webkit/WebStorage.java index a182287..cf71a84 100644 --- a/core/java/android/webkit/WebStorage.java +++ b/core/java/android/webkit/WebStorage.java @@ -389,8 +389,8 @@ public final class WebStorage { mOrigins = new HashMap<String, Origin>(); for (String origin : tmp) { Origin website = new Origin(origin, - nativeGetUsageForOrigin(origin), - nativeGetQuotaForOrigin(origin)); + nativeGetQuotaForOrigin(origin), + nativeGetUsageForOrigin(origin)); mOrigins.put(origin, website); } } diff --git a/core/java/android/webkit/WebTextView.java b/core/java/android/webkit/WebTextView.java index e0d41c2..b6891b1 100644 --- a/core/java/android/webkit/WebTextView.java +++ b/core/java/android/webkit/WebTextView.java @@ -84,14 +84,22 @@ import java.util.ArrayList; // True if the most recent drag event has caused either the TextView to // scroll or the web page to scroll. Gets reset after a touch down. private boolean mScrolled; - // Gets set to true when the the IME jumps to the next textfield. When this - // happens, the next time the user hits a key it is okay for the focus - // pointer to not match the WebTextView's node pointer - boolean mOkayForFocusNotToMatch; // Whether or not a selection change was generated from webkit. If it was, // we do not need to pass the selection back to webkit. private boolean mFromWebKit; + // Whether or not a selection change was generated from the WebTextView + // gaining focus. If it is, we do not want to pass it to webkit. This + // selection comes from the MovementMethod, but we behave differently. If + // WebTextView gained focus from a touch, webkit will determine the + // selection. + private boolean mFromFocusChange; + // Whether or not a selection change was generated from setInputType. We + // do not want to pass this change to webkit. + private boolean mFromSetInputType; private boolean mGotTouchDown; + // Keep track of whether a long press has happened. Only meaningful after + // an ACTION_DOWN MotionEvent + private boolean mHasPerformedLongClick; private boolean mInSetTextAndKeepSelection; // Array to store the final character added in onTextChanged, so that its // KeyEvents may be determined. @@ -136,19 +144,6 @@ import java.util.ArrayList; isArrowKey = true; break; } - if (!isArrowKey && !mOkayForFocusNotToMatch - && mWebView.nativeFocusNodePointer() != mNodePointer) { - mWebView.nativeClearCursor(); - // Do not call remove() here, which hides the soft keyboard. If - // the soft keyboard is being displayed, the user will still want - // it there. - mWebView.removeView(this); - mWebView.requestFocus(); - return mWebView.dispatchKeyEvent(event); - } - // After a jump to next textfield and the first key press, the cursor - // and focus will once again match, so reset this value. - mOkayForFocusNotToMatch = false; Spannable text = (Spannable) getText(); int oldLength = text.length(); @@ -185,7 +180,7 @@ import java.util.ArrayList; } // Center key should be passed to a potential onClick if (!down) { - mWebView.shortPressOnTextField(); + mWebView.centerKeyPressOnTextField(); } // Pass to super to handle longpress. return super.dispatchKeyEvent(event); @@ -303,20 +298,21 @@ import java.util.ArrayList; public void onEditorAction(int actionCode) { switch (actionCode) { case EditorInfo.IME_ACTION_NEXT: + // Since the cursor will no longer be in the same place as the + // focus, set the focus controller back to inactive + mWebView.setFocusControllerInactive(); mWebView.nativeMoveCursorToNextTextInput(); // Preemptively rebuild the WebTextView, so that the action will // be set properly. mWebView.rebuildWebTextView(); - // Since the cursor will no longer be in the same place as the - // focus, set the focus controller back to inactive - mWebView.setFocusControllerInactive(); + setDefaultSelection(); mWebView.invalidate(); - mOkayForFocusNotToMatch = true; break; case EditorInfo.IME_ACTION_DONE: super.onEditorAction(actionCode); break; case EditorInfo.IME_ACTION_GO: + case EditorInfo.IME_ACTION_SEARCH: // Send an enter and hide the soft keyboard InputMethodManager.getInstance(mContext) .hideSoftInputFromWindow(getWindowToken(), 0); @@ -331,6 +327,14 @@ import java.util.ArrayList; } @Override + protected void onFocusChanged(boolean focused, int direction, + Rect previouslyFocusedRect) { + mFromFocusChange = true; + super.onFocusChanged(focused, direction, previouslyFocusedRect); + mFromFocusChange = false; + } + + @Override protected void onSelectionChanged(int selStart, int selEnd) { // This code is copied from TextView.onDraw(). That code does not get // executed, however, because the WebTextView does not draw, allowing @@ -342,7 +346,8 @@ import java.util.ArrayList; int candEnd = EditableInputConnection.getComposingSpanEnd(sp); imm.updateSelection(this, selStart, selEnd, candStart, candEnd); } - if (!mFromWebKit && mWebView != null) { + if (!mFromWebKit && !mFromFocusChange && !mFromSetInputType + && mWebView != null) { if (DebugFlags.WEB_TEXT_VIEW) { Log.v(LOGTAG, "onSelectionChanged selStart=" + selStart + " selEnd=" + selEnd); @@ -426,8 +431,13 @@ import java.util.ArrayList; mDragSent = false; mScrolled = false; mGotTouchDown = true; + mHasPerformedLongClick = false; break; case MotionEvent.ACTION_MOVE: + if (mHasPerformedLongClick) { + mGotTouchDown = false; + return false; + } int slop = ViewConfiguration.get(mContext).getScaledTouchSlop(); Spannable buffer = getText(); int initialScrollX = Touch.getInitialScrollX(this, buffer); @@ -451,6 +461,7 @@ import java.util.ArrayList; mScrollX / maxScrollX : 0, mScrollY); } mScrolled = true; + cancelLongPress(); return true; } if (Math.abs((int) event.getX() - mDragStartX) < slop @@ -477,6 +488,10 @@ import java.util.ArrayList; return false; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: + if (mHasPerformedLongClick) { + mGotTouchDown = false; + return false; + } if (!mScrolled) { // If the page scrolled, or the TextView scrolled, we do not // want to change the selection @@ -520,6 +535,12 @@ import java.util.ArrayList; return false; } + @Override + public boolean performLongClick() { + mHasPerformedLongClick = true; + return super.performLongClick(); + } + /** * Remove this WebTextView from its host WebView, and return * focus to the host. @@ -552,7 +573,8 @@ import java.util.ArrayList; */ public void setAdapterCustom(AutoCompleteAdapter adapter) { if (adapter != null) { - setInputType(EditorInfo.TYPE_TEXT_FLAG_AUTO_COMPLETE); + setInputType(getInputType() + | EditorInfo.TYPE_TEXT_FLAG_AUTO_COMPLETE); adapter.setTextView(this); } super.setAdapter(adapter); @@ -591,6 +613,32 @@ import java.util.ArrayList; } /** + * Sets the selection when the user clicks on a textfield or textarea with + * the trackball or center key, or starts typing into it without clicking on + * it. + */ + /* package */ void setDefaultSelection() { + Spannable text = (Spannable) getText(); + int selection = mSingle ? text.length() : 0; + if (Selection.getSelectionStart(text) == selection + && Selection.getSelectionEnd(text) == selection) { + // The selection of the UI copy is set correctly, but the + // WebTextView still needs to inform the webkit thread to set the + // selection. Normally that is done in onSelectionChanged, but + // onSelectionChanged will not be called because the UI copy is not + // changing. (This can happen when the WebTextView takes focus. + // That onSelectionChanged was blocked because the selection set + // when focusing is not necessarily the desirable selection for + // WebTextView.) + if (mWebView != null) { + mWebView.setSelection(selection, selection); + } + } else { + Selection.setSelection(text, selection, selection); + } + } + + /** * Determine whether to use the system-wide password disguising method, * or to use none. * @param inPassword True if the textfield is a password field. @@ -660,7 +708,14 @@ import java.util.ArrayList; setTextColor(Color.BLACK); } - /* package */ void setMaxLength(int maxLength) { + @Override + public void setInputType(int type) { + mFromSetInputType = true; + super.setInputType(type); + mFromSetInputType = false; + } + + private void setMaxLength(int maxLength) { mMaxLength = maxLength; if (-1 == maxLength) { setFilters(NO_FILTERS); @@ -708,7 +763,6 @@ import java.util.ArrayList; // Set up a measure spec so a layout can always be recreated. mWidthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY); mHeightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); - requestFocus(); } /** @@ -725,68 +779,6 @@ import java.util.ArrayList; } /** - * Set whether this is a single-line textfield or a multi-line textarea. - * Textfields scroll horizontally, and do not handle the enter key. - * Textareas behave oppositely. - * Do NOT call this after calling setInPassword(true). This will result in - * removing the password input type. - */ - public void setSingleLine(boolean single) { - int inputType = EditorInfo.TYPE_CLASS_TEXT - | EditorInfo.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT; - if (single) { - int action = mWebView.nativeTextFieldAction(); - switch (action) { - // Keep in sync with CachedRoot::ImeAction - case 0: // NEXT - setImeOptions(EditorInfo.IME_ACTION_NEXT); - break; - case 1: // GO - setImeOptions(EditorInfo.IME_ACTION_GO); - break; - case -1: // FAILURE - case 2: // DONE - setImeOptions(EditorInfo.IME_ACTION_DONE); - break; - } - } else { - inputType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE - | EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES - | EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT; - setImeOptions(EditorInfo.IME_ACTION_NONE); - } - mSingle = single; - setHorizontallyScrolling(single); - setInputType(inputType); - } - - /** - * Set the text for this WebTextView, and set the selection to (start, end) - * @param text Text to go into this WebTextView. - * @param start Beginning of the selection. - * @param end End of the selection. - */ - /* package */ void setText(CharSequence text, int start, int end) { - mPreChange = text.toString(); - setText(text); - Spannable span = (Spannable) getText(); - int length = span.length(); - if (end > length) { - end = length; - } - if (start < 0) { - start = 0; - } else if (start > length) { - start = length; - } - if (DebugFlags.WEB_TEXT_VIEW) { - Log.v(LOGTAG, "setText start=" + start - + " end=" + end); - } - Selection.setSelection(span, start, end); - } - - /** * Set the text to the new string, but use the old selection, making sure * to keep it within the new string. * @param text The new text to place in the textfield. @@ -801,6 +793,93 @@ import java.util.ArrayList; } /** + * Called by WebView.rebuildWebTextView(). Based on the type of the <input> + * element, set up the WebTextView, its InputType, and IME Options properly. + * @param type int corresponding to enum "type" defined in WebView.cpp. + * Does not correspond to HTMLInputElement::InputType so this + * is unaffected if that changes, and also because that has no + * type corresponding to textarea (which is its own tag). + */ + /* package */ void setType(int type) { + if (mWebView == null) return; + boolean single = true; + boolean inPassword = false; + int maxLength = -1; + int inputType = EditorInfo.TYPE_CLASS_TEXT + | EditorInfo.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT; + switch (type) { + case 1: // TEXT_AREA + single = false; + inputType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE + | EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES + | EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT; + setImeOptions(EditorInfo.IME_ACTION_NONE); + break; + case 2: // PASSWORD + inPassword = true; + break; + case 3: // SEARCH + setImeOptions(EditorInfo.IME_ACTION_SEARCH); + break; + case 4: // EMAIL + // TYPE_TEXT_VARIATION_WEB_EDIT_TEXT prevents EMAIL_ADDRESS + // from working, so exclude it for now. + inputType = EditorInfo.TYPE_CLASS_TEXT + | EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS; + break; + case 5: // NUMBER + inputType = EditorInfo.TYPE_CLASS_NUMBER; + break; + case 6: // TELEPHONE + inputType = EditorInfo.TYPE_CLASS_PHONE; + break; + case 7: // URL + // TYPE_TEXT_VARIATION_WEB_EDIT_TEXT prevents URI + // from working, so exclude it for now. + inputType = EditorInfo.TYPE_CLASS_TEXT + | EditorInfo.TYPE_TEXT_VARIATION_URI; + break; + default: + break; + } + setHint(null); + if (single) { + mWebView.requestLabel(mWebView.nativeFocusCandidateFramePointer(), + mNodePointer); + maxLength = mWebView.nativeFocusCandidateMaxLength(); + if (type != 2 /* PASSWORD */) { + String name = mWebView.nativeFocusCandidateName(); + if (name != null && name.length() > 0) { + mWebView.requestFormData(name, mNodePointer); + } + } + if (type != 3 /* SEARCH */) { + int action = mWebView.nativeTextFieldAction(); + switch (action) { + // Keep in sync with CachedRoot::ImeAction + case 0: // NEXT + setImeOptions(EditorInfo.IME_ACTION_NEXT); + break; + case 1: // GO + setImeOptions(EditorInfo.IME_ACTION_GO); + break; + case -1: // FAILURE + case 2: // DONE + setImeOptions(EditorInfo.IME_ACTION_DONE); + break; + } + } + } + mSingle = single; + setMaxLength(maxLength); + setHorizontallyScrolling(single); + setInputType(inputType); + setInPassword(inPassword); + AutoCompleteAdapter adapter = null; + setAdapterCustom(adapter); + } + + /** * Update the cache to reflect the current text. */ /* package */ void updateCachedTextfield() { diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java index f6d6d22..c349606 100644 --- a/core/java/android/webkit/WebView.java +++ b/core/java/android/webkit/WebView.java @@ -21,7 +21,6 @@ import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.DialogInterface.OnCancelListener; -import android.content.pm.PackageManager; import android.database.DataSetObserver; import android.graphics.Bitmap; import android.graphics.Canvas; @@ -29,7 +28,6 @@ import android.graphics.Color; import android.graphics.Picture; import android.graphics.Point; import android.graphics.Rect; -import android.graphics.Region; import android.graphics.drawable.Drawable; import android.net.http.SslCertificate; import android.net.Uri; @@ -55,7 +53,6 @@ import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; -import android.view.ViewParent; import android.view.ViewTreeObserver; import android.view.animation.AlphaAnimation; import android.view.inputmethod.InputMethodManager; @@ -65,7 +62,9 @@ import android.widget.AbsoluteLayout; import android.widget.Adapter; import android.widget.AdapterView; import android.widget.ArrayAdapter; +import android.widget.CheckedTextView; import android.widget.FrameLayout; +import android.widget.LinearLayout; import android.widget.ListView; import android.widget.Scroller; import android.widget.Toast; @@ -83,6 +82,8 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import junit.framework.Assert; + /** * <p>A View that displays web pages. This class is the basis upon which you * can roll your own web browser or simply display some online content within your Activity. @@ -199,11 +200,16 @@ public class WebView extends AbsoluteLayout implements ViewTreeObserver.OnGlobalFocusChangeListener, ViewGroup.OnHierarchyChangeListener { + // enable debug output for drag trackers + private static final boolean DEBUG_DRAG_TRACKER = false; // if AUTO_REDRAW_HACK is true, then the CALL key will toggle redrawing // the screen all-the-time. Good for profiling our drawing code static private final boolean AUTO_REDRAW_HACK = false; // true means redraw the screen all-the-time. Only with AUTO_REDRAW_HACK private boolean mAutoRedraw; + private int mRootLayer; // C++ pointer to the root layer + private boolean mLayersHaveAnimations; + private EvaluateLayersAnimations mEvaluateThread; static final String LOGTAG = "webview"; @@ -298,6 +304,9 @@ public class WebView extends AbsoluteLayout // Used by WebViewCore to create child views. /* package */ final ViewManager mViewManager; + // Used to display in full screen mode + PluginFullScreenHolder mFullScreenHolder; + /** * Position of the last touch event. */ @@ -350,7 +359,6 @@ public class WebView extends AbsoluteLayout private static final int TOUCH_DOUBLE_TAP_MODE = 6; private static final int TOUCH_DONE_MODE = 7; private static final int TOUCH_SELECT_MODE = 8; - private static final int TOUCH_PINCH_DRAG = 9; // Whether to forward the touch events to WebCore private boolean mForwardTouchEvents = false; @@ -363,6 +371,19 @@ public class WebView extends AbsoluteLayout private static final int PREVENT_DRAG_YES = 2; private int mPreventDrag = PREVENT_DRAG_NO; + // by default mPreventLongPress is false. If it is true, long press event + // will be handled by WebKit instead of UI. + private boolean mPreventLongPress = false; + // by default mPreventDoubleTap is false. If it is true, double tap event + // will be handled by WebKit instead of UI. + private boolean mPreventDoubleTap = false; + + // this needs to be in sync with the logic in WebKit's + // EventHandler::handleTouchEvent() + private static final int TOUCH_PREVENT_DRAG = 0x1; + private static final int TOUCH_PREVENT_LONGPRESS = 0x2; + private static final int TOUCH_PREVENT_DOUBLETAP = 0x4; + // To keep track of whether the current drag was initiated by a WebTextView, // so that we know not to hide the cursor boolean mDragFromTextInput; @@ -392,6 +413,8 @@ public class WebView extends AbsoluteLayout private static final int LONG_PRESS_TIMEOUT = 1000; // needed to avoid flinging after a pause of no movement private static final int MIN_FLING_TIME = 250; + // draw unfiltered after drag is held without movement + private static final int MOTIONLESS_TIME = 100; // The time that the Zoom Controls are visible before fading away private static final long ZOOM_CONTROLS_TIMEOUT = ViewConfiguration.getZoomControlsTimeout(); @@ -429,6 +452,10 @@ public class WebView extends AbsoluteLayout private Scroller mScroller; private boolean mWrapContent; + private static final int MOTIONLESS_FALSE = 0; + private static final int MOTIONLESS_PENDING = 1; + private static final int MOTIONLESS_TRUE = 2; + private int mHeldMotionless; /** * Private message ids @@ -440,6 +467,8 @@ public class WebView extends AbsoluteLayout private static final int RELEASE_SINGLE_TAP = 5; private static final int REQUEST_FORM_DATA = 6; private static final int RESUME_WEBCORE_UPDATE = 7; + private static final int DRAG_HELD_MOTIONLESS = 8; + private static final int AWAKEN_SCROLL_BARS = 9; //! arg1=x, arg2=y static final int SCROLL_TO_MSG_ID = 10; @@ -452,17 +481,24 @@ public class WebView extends AbsoluteLayout static final int UPDATE_TEXT_ENTRY_MSG_ID = 15; static final int WEBCORE_INITIALIZED_MSG_ID = 16; static final int UPDATE_TEXTFIELD_TEXT_MSG_ID = 17; + static final int FIND_AGAIN = 18; static final int MOVE_OUT_OF_PLUGIN = 19; static final int CLEAR_TEXT_ENTRY = 20; static final int UPDATE_TEXT_SELECTION_MSG_ID = 21; - static final int UPDATE_CLIPBOARD = 22; + static final int LONG_PRESS_CENTER = 23; static final int PREVENT_TOUCH_ID = 24; static final int WEBCORE_NEED_TOUCH_EVENTS = 25; // obj=Rect in doc coordinates static final int INVAL_RECT_MSG_ID = 26; static final int REQUEST_KEYBOARD = 27; - static final int SHOW_RECT_MSG_ID = 28; + static final int DO_MOTION_UP = 28; + static final int SHOW_FULLSCREEN = 29; + static final int HIDE_FULLSCREEN = 30; + static final int DOM_FOCUS_CHANGED = 31; + static final int IMMEDIATE_REPAINT_MSG_ID = 32; + static final int SET_ROOT_LAYER_MSG_ID = 33; + static final int RETURN_LABEL = 34; static final String[] HandlerDebugString = { "REMEMBER_PASSWORD", // = 1; @@ -471,9 +507,9 @@ public class WebView extends AbsoluteLayout "SWITCH_TO_LONGPRESS", // = 4; "RELEASE_SINGLE_TAP", // = 5; "REQUEST_FORM_DATA", // = 6; - "SWITCH_TO_CLICK", // = 7; - "RESUME_WEBCORE_UPDATE", // = 8; - "9", + "RESUME_WEBCORE_UPDATE", // = 7; + "DRAG_HELD_MOTIONLESS", // = 8; + "AWAKEN_SCROLL_BARS", // = 9; "SCROLL_TO_MSG_ID", // = 10; "SCROLL_BY_MSG_ID", // = 11; "SPAWN_SCROLL_TO_MSG_ID", // = 12; @@ -482,19 +518,35 @@ public class WebView extends AbsoluteLayout "UPDATE_TEXT_ENTRY_MSG_ID", // = 15; "WEBCORE_INITIALIZED_MSG_ID", // = 16; "UPDATE_TEXTFIELD_TEXT_MSG_ID", // = 17; - "18", // = 18; + "FIND_AGAIN", // = 18; "MOVE_OUT_OF_PLUGIN", // = 19; "CLEAR_TEXT_ENTRY", // = 20; "UPDATE_TEXT_SELECTION_MSG_ID", // = 21; - "UPDATE_CLIPBOARD", // = 22; + "22", // = 22; "LONG_PRESS_CENTER", // = 23; "PREVENT_TOUCH_ID", // = 24; "WEBCORE_NEED_TOUCH_EVENTS", // = 25; "INVAL_RECT_MSG_ID", // = 26; "REQUEST_KEYBOARD", // = 27; - "SHOW_RECT_MSG_ID" // = 28; + "DO_MOTION_UP", // = 28; + "SHOW_FULLSCREEN", // = 29; + "HIDE_FULLSCREEN", // = 30; + "DOM_FOCUS_CHANGED", // = 31; + "IMMEDIATE_REPAINT_MSG_ID", // = 32; + "SET_ROOT_LAYER_MSG_ID", // = 33; + "RETURN_LABEL" // = 34; }; + // If the site doesn't use the viewport meta tag to specify the viewport, + // use DEFAULT_VIEWPORT_WIDTH as the default viewport width + static final int DEFAULT_VIEWPORT_WIDTH = 800; + + // normally we try to fit the content to the minimum preferred width + // calculated by the Webkit. To avoid the bad behavior when some site's + // minimum preferred width keeps growing when changing the viewport width or + // the minimum preferred width is huge, an upper limit is needed. + static int sMaxViewportWidth = DEFAULT_VIEWPORT_WIDTH; + // default scale limit. Depending on the display density private static float DEFAULT_MAX_ZOOM_SCALE; private static float DEFAULT_MIN_ZOOM_SCALE; @@ -514,8 +566,8 @@ public class WebView extends AbsoluteLayout // ideally mZoomOverviewWidth should be mContentWidth. But sites like espn, // engadget always have wider mContentWidth no matter what viewport size is. - int mZoomOverviewWidth = WebViewCore.DEFAULT_VIEWPORT_WIDTH; - float mTextWrapScale; + int mZoomOverviewWidth = DEFAULT_VIEWPORT_WIDTH; + float mLastScale; // default scale. Depending on the display density. static int DEFAULT_SCALE_PERCENT; @@ -539,11 +591,10 @@ public class WebView extends AbsoluteLayout private boolean mUserScroll = false; private int mSnapScrollMode = SNAP_NONE; - private static final int SNAP_NONE = 1; - private static final int SNAP_X = 2; - private static final int SNAP_Y = 3; - private static final int SNAP_X_LOCK = 4; - private static final int SNAP_Y_LOCK = 5; + private static final int SNAP_NONE = 0; + private static final int SNAP_LOCK = 1; // not a separate state + private static final int SNAP_X = 2; // may be combined with SNAP_LOCK + private static final int SNAP_Y = 4; // may be combined with SNAP_LOCK private boolean mSnapPositive; // Used to match key downs and key ups @@ -669,6 +720,9 @@ public class WebView extends AbsoluteLayout public void onVisibilityChanged(boolean visible) { if (visible) { switchOutDrawHistory(); + // Bring back the hidden zoom controls. + mZoomButtonsController.getZoomControls().setVisibility( + View.VISIBLE); updateZoomButtonsEnabled(); } } @@ -714,7 +768,7 @@ public class WebView extends AbsoluteLayout /** * Construct a new WebView with layout parameters, a default style and a set * of custom Javscript interfaces to be added to the WebView at initialization - * time. This guraratees that these interfaces will be available when the JS + * time. This guarantees that these interfaces will be available when the JS * context is initialized. * @param context A Context object used to access application assets. * @param attrs An AttributeSet passed to our parent. @@ -729,12 +783,11 @@ public class WebView extends AbsoluteLayout init(); mCallbackProxy = new CallbackProxy(context, this); + mViewManager = new ViewManager(this); mWebViewCore = new WebViewCore(context, this, mCallbackProxy, javascriptInterfaces); mDatabase = WebViewDatabase.getInstance(context); mScroller = new Scroller(context); - mViewManager = new ViewManager(this); - mZoomButtonsController = new ZoomButtonsController(this); mZoomButtonsController.setOnZoomListener(mZoomListener); // ZoomButtonsController positions the buttons at the bottom, but in @@ -747,9 +800,6 @@ public class WebView extends AbsoluteLayout params; frameParams.gravity = Gravity.RIGHT; } - - mSupportMultiTouch = context.getPackageManager().hasSystemFeature( - PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH); } private void updateZoomButtonsEnabled() { @@ -760,9 +810,6 @@ public class WebView extends AbsoluteLayout // button, if the page cannot zoom mZoomButtonsController.getZoomControls().setVisibility(View.GONE); } else { - // Bring back the hidden zoom controls. - mZoomButtonsController.getZoomControls() - .setVisibility(View.VISIBLE); // Set each one individually, as a page may be able to zoom in // or out. mZoomButtonsController.setZoomInEnabled(canZoomIn); @@ -792,7 +839,6 @@ public class WebView extends AbsoluteLayout mDefaultScale = density; mActualScale = density; mInvActualScale = 1 / density; - mTextWrapScale = density; DEFAULT_MAX_ZOOM_SCALE = 4.0f * density; DEFAULT_MIN_ZOOM_SCALE = 0.25f * density; mMaxZoomScale = DEFAULT_MAX_ZOOM_SCALE; @@ -813,7 +859,7 @@ public class WebView extends AbsoluteLayout mDefaultScale = density; mMaxZoomScale *= scaleFactor; mMinZoomScale *= scaleFactor; - setNewZoomScale(mActualScale * scaleFactor, true, false); + setNewZoomScale(mActualScale * scaleFactor, false); } } @@ -1170,8 +1216,9 @@ public class WebView extends AbsoluteLayout b.putInt("scrollX", mScrollX); b.putInt("scrollY", mScrollY); b.putFloat("scale", mActualScale); - b.putFloat("textwrapScale", mTextWrapScale); - b.putBoolean("overview", mInZoomOverview); + if (mInZoomOverview) { + b.putFloat("lastScale", mLastScale); + } return true; } return false; @@ -1216,8 +1263,13 @@ public class WebView extends AbsoluteLayout // onSizeChanged() is called, the rest will be set // correctly mActualScale = scale; - mTextWrapScale = b.getFloat("textwrapScale", scale); - mInZoomOverview = b.getBoolean("overview"); + float lastScale = b.getFloat("lastScale", -1.0f); + if (lastScale > 0) { + mInZoomOverview = true; + mLastScale = lastScale; + } else { + mInZoomOverview = false; + } invalidate(); return true; } @@ -1684,6 +1736,13 @@ public class WebView extends AbsoluteLayout return result; } + // Called by JNI when the DOM has changed the focus. Clear the focus so + // that new keys will go to the newly focused field + private void domChangedFocus() { + if (inEditingMode()) { + mPrivateHandler.obtainMessage(DOM_FOCUS_CHANGED).sendToTarget(); + } + } /** * Request the href of an anchor element due to getFocusNodePath returning * "href." If hrefMsg is null, this method returns immediately and does not @@ -1787,7 +1846,7 @@ public class WebView extends AbsoluteLayout } if (null != v) { addView(v, new AbsoluteLayout.LayoutParams( - ViewGroup.LayoutParams.FILL_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, 0, 0)); if (mTitleShadow == null) { mTitleShadow = (Drawable) mContext.getResources().getDrawable( @@ -1940,18 +1999,12 @@ public class WebView extends AbsoluteLayout contentSizeChanged(updateLayout); } - private void setNewZoomScale(float scale, boolean updateTextWrapScale, - boolean force) { + private void setNewZoomScale(float scale, boolean force) { if (scale < mMinZoomScale) { scale = mMinZoomScale; } else if (scale > mMaxZoomScale) { scale = mMaxZoomScale; } - if (updateTextWrapScale) { - mTextWrapScale = scale; - // reset mLastHeightSent to force VIEW_SIZE_CHANGED sent to WebKit - mLastHeightSent = 0; - } if (scale != mActualScale || force) { if (mDrawHistory) { // If history Picture is drawn, don't update scroll. They will @@ -1961,7 +2014,9 @@ public class WebView extends AbsoluteLayout } mActualScale = scale; mInvActualScale = 1 / scale; - sendViewSizeZoom(); + if (!mPreviewZoomOnly) { + sendViewSizeZoom(); + } } else { // update our scroll so we don't appear to jump // i.e. keep the center of the doc in the center of the view @@ -1989,9 +2044,10 @@ public class WebView extends AbsoluteLayout mScrollX = pinLocX(Math.round(sx)); mScrollY = pinLocY(Math.round(sy)); - // update webkit - sendViewSizeZoom(); - sendOurVisibleRect(); + if (!mPreviewZoomOnly) { + sendViewSizeZoom(); + sendOurVisibleRect(); + } } } } @@ -2001,8 +2057,6 @@ public class WebView extends AbsoluteLayout private Rect mLastGlobalRect; private Rect sendOurVisibleRect() { - if (mPreviewZoomOnly) return mLastVisibleRectSent; - Rect rect = new Rect(); calcOurContentVisibleRect(rect); // Rect.equals() checks for null input. @@ -2056,8 +2110,6 @@ public class WebView extends AbsoluteLayout int mWidth; int mHeight; int mTextWrapWidth; - int mAnchorX; - int mAnchorY; float mScale; boolean mIgnoreHeight; } @@ -2069,8 +2121,6 @@ public class WebView extends AbsoluteLayout * @return true if new values were sent */ private boolean sendViewSizeZoom() { - if (mPreviewZoomOnly) return false; - int viewWidth = getViewWidth(); int newWidth = Math.round(viewWidth * mInvActualScale); int newHeight = Math.round(getViewHeight() * mInvActualScale); @@ -2090,15 +2140,16 @@ public class WebView extends AbsoluteLayout ViewSizeData data = new ViewSizeData(); data.mWidth = newWidth; data.mHeight = newHeight; - data.mTextWrapWidth = Math.round(viewWidth / mTextWrapScale);; + // while in zoom overview mode, the text are wrapped to the screen + // width matching mLastScale. So that we don't trigger re-flow while + // toggling between overview mode and normal mode. + data.mTextWrapWidth = mInZoomOverview ? Math.round(viewWidth + / mLastScale) : newWidth; data.mScale = mActualScale; data.mIgnoreHeight = mZoomScale != 0 && !mHeightCanMeasure; - data.mAnchorX = mAnchorX; - data.mAnchorY = mAnchorY; mWebViewCore.sendMessage(EventHub.VIEW_SIZE_CHANGED, data); mLastWidthSent = newWidth; mLastHeightSent = newHeight; - mAnchorX = mAnchorY = 0; return true; } return false; @@ -2360,6 +2411,7 @@ public class WebView extends AbsoluteLayout } int result = nativeFindAll(find.toLowerCase(), find.toUpperCase()); invalidate(); + mLastFind = find; return result; } @@ -2367,6 +2419,9 @@ public class WebView extends AbsoluteLayout // or not we draw the highlights for matches. private boolean mFindIsUp; private int mFindHeight; + // Keep track of the last string sent, so we can search again after an + // orientation change or the dismissal of the soft keyboard. + private String mLastFind; /** * Return the first substring consisting of the address of a physical @@ -2422,12 +2477,14 @@ public class WebView extends AbsoluteLayout * Clear the highlighting surrounding text matches created by findAll. */ public void clearMatches() { + if (mNativeClass == 0) + return; if (mFindIsUp) { recordNewContentSize(mContentWidth, mContentHeight - mFindHeight, false); mFindIsUp = false; } - nativeSetFindIsDown(); + nativeSetFindIsUp(); // Now that the dialog has been removed, ensure that we scroll to a // location that is not beyond the end of the page. pinScrollTo(mScrollX, mScrollY, false, 0); @@ -2541,6 +2598,41 @@ public class WebView extends AbsoluteLayout } } + /** + * Called by CallbackProxy when the page finishes loading. + * @param url The URL of the page which has finished loading. + */ + /* package */ void onPageFinished(String url) { + if (mPageThatNeedsToSlideTitleBarOffScreen != null) { + // If the user is now on a different page, or has scrolled the page + // past the point where the title bar is offscreen, ignore the + // scroll request. + if (mPageThatNeedsToSlideTitleBarOffScreen.equals(url) + && mScrollX == 0 && mScrollY == 0) { + pinScrollTo(0, mYDistanceToSlideTitleOffScreen, true, + SLIDE_TITLE_DURATION); + } + mPageThatNeedsToSlideTitleBarOffScreen = null; + } + } + + /** + * The URL of a page that sent a message to scroll the title bar off screen. + * + * Many mobile sites tell the page to scroll to (0,1) in order to scroll the + * title bar off the screen. Sometimes, the scroll position is set before + * the page finishes loading. Rather than scrolling while the page is still + * loading, keep track of the URL and new scroll position so we can perform + * the scroll once the page finishes loading. + */ + private String mPageThatNeedsToSlideTitleBarOffScreen; + + /** + * The destination Y scroll position to be used when the page finishes + * loading. See mPageThatNeedsToSlideTitleBarOffScreen. + */ + private int mYDistanceToSlideTitleOffScreen; + // scale from content to view coordinates, and pin // return true if pin caused the final x/y different than the request cx/cy, // and a future scroll may reach the request cx/cy after our size has @@ -2575,8 +2667,18 @@ public class WebView extends AbsoluteLayout // page, assume this is an attempt to scroll off the title bar, and // animate the title bar off screen slowly enough that the user can see // it. - if (cx == 0 && cy == 1 && mScrollX == 0 && mScrollY == 0) { - pinScrollTo(vx, vy, true, SLIDE_TITLE_DURATION); + if (cx == 0 && cy == 1 && mScrollX == 0 && mScrollY == 0 + && mTitleBar != null) { + // FIXME: 100 should be defined somewhere as our max progress. + if (getProgress() < 100) { + // Wait to scroll the title bar off screen until the page has + // finished loading. Keep track of the URL and the destination + // Y position + mPageThatNeedsToSlideTitleBarOffScreen = getUrl(); + mYDistanceToSlideTitleOffScreen = vy; + } else { + pinScrollTo(vx, vy, true, SLIDE_TITLE_DURATION); + } // Since we are animating, we have not yet reached the desired // scroll position. Do not return true to request another attempt return false; @@ -2617,12 +2719,12 @@ public class WebView extends AbsoluteLayout if (mHeightCanMeasure) { if (getMeasuredHeight() != contentToViewDimension(mContentHeight) - && updateLayout) { + || updateLayout) { requestLayout(); } } else if (mWidthCanMeasure) { if (getMeasuredWidth() != contentToViewDimension(mContentWidth) - && updateLayout) { + || updateLayout) { requestLayout(); } } else { @@ -2642,6 +2744,16 @@ public class WebView extends AbsoluteLayout } /** + * Gets the WebViewClient + * @return the current WebViewClient instance. + * + *@hide pending API council approval. + */ + public WebViewClient getWebViewClient() { + return mCallbackProxy.getWebViewClient(); + } + + /** * Register the interface to be used when content can not be handled by * the rendering engine, and should be downloaded instead. This will replace * the current handler. @@ -2772,16 +2884,7 @@ public class WebView extends AbsoluteLayout return super.drawChild(canvas, child, drawingTime); } - @Override - protected void onDraw(Canvas canvas) { - // if mNativeClass is 0, the WebView has been destroyed. Do nothing. - if (mNativeClass == 0) { - return; - } - int saveCount = canvas.save(); - if (mTitleBar != null) { - canvas.translate(0, (int) mTitleBar.getHeight()); - } + private void drawContent(Canvas canvas) { // Update the buttons in the picture, so when we draw the picture // to the screen, they are in the correct state. // Tell the native side if user is a) touching the screen, @@ -2791,9 +2894,25 @@ public class WebView extends AbsoluteLayout // If mNativeClass is 0, we should not reach here, so we do not // need to check it again. nativeRecordButtons(hasFocus() && hasWindowFocus(), - mTouchMode == TOUCH_SHORTPRESS_START_MODE - || mTrackballDown || mGotCenterDown, false); + mTouchMode == TOUCH_SHORTPRESS_START_MODE + || mTrackballDown || mGotCenterDown, false); drawCoreAndCursorRing(canvas, mBackgroundColor, mDrawCursorRing); + } + + @Override + protected void onDraw(Canvas canvas) { + // if mNativeClass is 0, the WebView has been destroyed. Do nothing. + if (mNativeClass == 0) { + return; + } + + int saveCount = canvas.save(); + if (mTitleBar != null) { + canvas.translate(0, (int) mTitleBar.getHeight()); + } + if (mDragTrackerHandler == null || !mDragTrackerHandler.draw(canvas)) { + drawContent(canvas); + } canvas.restoreToCount(saveCount); // Now draw the shadow. @@ -2808,6 +2927,7 @@ public class WebView extends AbsoluteLayout if (AUTO_REDRAW_HACK && mAutoRedraw) { invalidate(); } + mWebViewCore.signalRepaintDone(); } @Override @@ -2822,10 +2942,7 @@ public class WebView extends AbsoluteLayout public boolean performLongClick() { if (mNativeClass != 0 && nativeCursorIsTextInput()) { // Send the click so that the textfield is in focus - // FIXME: When we start respecting changes to the native textfield's - // selection, need to make sure that this does not change it. - mWebViewCore.sendMessage(EventHub.CLICK, nativeCursorFramePointer(), - nativeCursorNodePointer()); + centerKeyPressOnTextField(); rebuildWebTextView(); } if (inEditingMode()) { @@ -2848,35 +2965,41 @@ public class WebView extends AbsoluteLayout */ private boolean mNeedToAdjustWebTextView; - // if checkVisibility is false, the WebTextView may trigger a move of - // WebView to bring itself into the view. - private void adjustTextView(boolean checkVisibility) { + private boolean didUpdateTextViewBounds(boolean allowIntersect) { Rect contentBounds = nativeFocusCandidateNodeBounds(); Rect vBox = contentToViewRect(contentBounds); Rect visibleRect = new Rect(); calcOurVisibleRect(visibleRect); - if (!checkVisibility || visibleRect.contains(vBox)) { - // As a result of the zoom, the textfield is now on - // screen. Place the WebTextView in its new place, - // accounting for our new scroll/zoom values. - mWebTextView - .setTextSize( - TypedValue.COMPLEX_UNIT_PX, - contentToViewDimension(nativeFocusCandidateTextSize())); + // The IME may have shown, resulting in the textfield being offscreen. + // If so, the textfield will be scrolled on screen, so treat it as + // though it is on screen. If it is on screen, place the WebTextView in + // its new place, accounting for our new scroll/zoom values. + InputMethodManager imm = InputMethodManager.peekInstance(); + if ((imm != null && imm.isActive(mWebTextView)) + || (allowIntersect ? Rect.intersects(visibleRect, vBox) + : visibleRect.contains(vBox))) { mWebTextView.setRect(vBox.left, vBox.top, vBox.width(), vBox.height()); - // If it is a password field, start drawing the - // WebTextView once again. - if (nativeFocusCandidateIsPassword()) { - mWebTextView.setInPassword(true); - } + mWebTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, + contentToViewDimension( + nativeFocusCandidateTextSize())); + return true; } else { - // The textfield is now off screen. The user probably - // was not zooming to see the textfield better. Remove - // the WebTextView. If the user types a key, and the + // The textfield is now off screen. The user probably + // was not zooming to see the textfield better. Remove + // the WebTextView. If the user types a key, and the // textfield is still in focus, we will reconstruct // the WebTextView and scroll it back on screen. mWebTextView.remove(); + return false; + } + } + + private void drawLayers(Canvas canvas) { + if (mRootLayer != 0) { + float scrollY = Math.max(mScrollY - getTitleHeight(), 0); + nativeDrawLayers(mRootLayer, mScrollX, scrollY, + mActualScale, canvas); } } @@ -2885,12 +3008,27 @@ public class WebView extends AbsoluteLayout if (mDrawHistory) { canvas.scale(mActualScale, mActualScale); canvas.drawPicture(mHistoryPicture); + drawLayers(canvas); return; } boolean animateZoom = mZoomScale != 0; - boolean animateScroll = !mScroller.isFinished() - || mVelocityTracker != null; + boolean animateScroll = (!mScroller.isFinished() + || mVelocityTracker != null) + && (mTouchMode != TOUCH_DRAG_MODE || + mHeldMotionless != MOTIONLESS_TRUE); + if (mTouchMode == TOUCH_DRAG_MODE) { + if (mHeldMotionless == MOTIONLESS_PENDING) { + mPrivateHandler.removeMessages(DRAG_HELD_MOTIONLESS); + mPrivateHandler.removeMessages(AWAKEN_SCROLL_BARS); + mHeldMotionless = MOTIONLESS_FALSE; + } + if (mHeldMotionless == MOTIONLESS_FALSE) { + mPrivateHandler.sendMessageDelayed(mPrivateHandler + .obtainMessage(DRAG_HELD_MOTIONLESS), MOTIONLESS_TIME); + mHeldMotionless = MOTIONLESS_PENDING; + } + } if (animateZoom) { float zoomScale; int interval = (int) (SystemClock.uptimeMillis() - mZoomStart); @@ -2907,7 +3045,12 @@ public class WebView extends AbsoluteLayout invalidate(); if (mNeedToAdjustWebTextView) { mNeedToAdjustWebTextView = false; - adjustTextView(true); + if (didUpdateTextViewBounds(false) + && nativeFocusCandidateIsPassword()) { + // If it is a password field, start drawing the + // WebTextView once again. + mWebTextView.setInPassword(true); + } } } // calculate the intermediate scroll position. As we need to use @@ -2941,23 +3084,26 @@ public class WebView extends AbsoluteLayout canvas.scale(mActualScale, mActualScale); } - mWebViewCore.drawContentPicture(canvas, color, - (animateZoom || mPreviewZoomOnly), animateScroll); + mWebViewCore.drawContentPicture(canvas, color, animateZoom, + animateScroll); + + drawLayers(canvas); if (mNativeClass == 0) return; - if (mShiftIsPressed && !(animateZoom || mPreviewZoomOnly)) { - if (mTouchSelection) { + if (mShiftIsPressed && !animateZoom) { + if (mTouchSelection || mExtendSelection) { nativeDrawSelectionRegion(canvas); - } else { - nativeDrawSelection(canvas, mInvActualScale, getTitleHeight(), - mSelectX, mSelectY, mExtendSelection); + } + if (!mTouchSelection) { + nativeDrawSelectionPointer(canvas, mInvActualScale, mSelectX, + mSelectY - getTitleHeight(), mExtendSelection); } } else if (drawCursorRing) { if (mTouchMode == TOUCH_SHORTPRESS_START_MODE) { mTouchMode = TOUCH_SHORTPRESS_MODE; HitTestResult hitTest = getHitTestResult(); - if (hitTest != null && - hitTest.mType != HitTestResult.UNKNOWN_TYPE) { + if (mPreventLongPress || (hitTest != null && + hitTest.mType != HitTestResult.UNKNOWN_TYPE)) { mPrivateHandler.sendMessageDelayed(mPrivateHandler .obtainMessage(SWITCH_TO_LONGPRESS), LONG_PRESS_TIMEOUT); @@ -2970,6 +3116,15 @@ public class WebView extends AbsoluteLayout if (mFindIsUp && !animateScroll) { nativeDrawMatches(canvas); } + if (mFocusSizeChanged) { + mFocusSizeChanged = false; + // If we are zooming, this will get handled above, when the zoom + // finishes. We also do not need to do this unless the WebTextView + // is showing. + if (!animateZoom && inEditingMode()) { + didUpdateTextViewBounds(true); + } + } } // draw history @@ -3037,24 +3192,22 @@ public class WebView extends AbsoluteLayout mWebViewCore.sendMessage(EventHub.SET_SELECTION, start, end); } - // Called by JNI when a touch event puts a textfield into focus. + /** + * Called in response to a message from webkit telling us that the soft + * keyboard should be launched. + */ private void displaySoftKeyboard(boolean isTextView) { InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); if (isTextView) { - if (mWebTextView == null) return; - + rebuildWebTextView(); + if (!inEditingMode()) return; imm.showSoftInput(mWebTextView, 0); - if (mActualScale < mDefaultScale) { - // bring it back to the default scale so that user can enter - // text. - mInZoomOverview = false; - mZoomCenterX = mLastTouchX; - mZoomCenterY = mLastTouchY; - // do not change text wrap scale so that there is no reflow - setNewZoomScale(mDefaultScale, false, false); - adjustTextView(false); + if (mInZoomOverview) { + // if in zoom overview mode, call doDoubleTap() to bring it back + // to normal mode so that user can enter text. + doDoubleTap(); } } else { // used by plugins @@ -3103,6 +3256,8 @@ public class WebView extends AbsoluteLayout // Note that sendOurVisibleRect calls viewToContent, so the coordinates // should be in content coordinates. Rect bounds = nativeFocusCandidateNodeBounds(); + Rect vBox = contentToViewRect(bounds); + mWebTextView.setRect(vBox.left, vBox.top, vBox.width(), vBox.height()); if (!Rect.intersects(bounds, visibleRect)) { mWebTextView.bringIntoView(); } @@ -3113,79 +3268,92 @@ public class WebView extends AbsoluteLayout // i.e. In the case of opening/closing the screen. // In that case, we need to set the dimensions, but not the other // aspects. - // We also need to restore the selection, which gets wrecked by - // calling setTextEntryRect. - Spannable spannable = (Spannable) mWebTextView.getText(); - int start = Selection.getSelectionStart(spannable); - int end = Selection.getSelectionEnd(spannable); // If the text has been changed by webkit, update it. However, if // there has been more UI text input, ignore it. We will receive // another update when that text is recognized. - if (text != null && !text.equals(spannable.toString()) + if (text != null && !text.equals(mWebTextView.getText().toString()) && nativeTextGeneration() == mTextGeneration) { mWebTextView.setTextAndKeepSelection(text); - } else { - Selection.setSelection(spannable, start, end); } } else { - Rect vBox = contentToViewRect(bounds); - mWebTextView.setRect(vBox.left, vBox.top, vBox.width(), - vBox.height()); mWebTextView.setGravity(nativeFocusCandidateIsRtlText() ? Gravity.RIGHT : Gravity.NO_GRAVITY); - // this needs to be called before update adapter thread starts to - // ensure the mWebTextView has the same node pointer + // This needs to be called before setType, which may call + // requestFormData, and it needs to have the correct nodePointer. mWebTextView.setNodePointer(nodePointer); - int maxLength = -1; - boolean isTextField = nativeFocusCandidateIsTextField(); - if (isTextField) { - maxLength = nativeFocusCandidateMaxLength(); - String name = nativeFocusCandidateName(); - if (mWebViewCore.getSettings().getSaveFormData() - && name != null) { - Message update = mPrivateHandler.obtainMessage( - REQUEST_FORM_DATA, nodePointer); - RequestFormData updater = new RequestFormData(name, - getUrl(), update); - Thread t = new Thread(updater); - t.start(); - } - } - mWebTextView.setMaxLength(maxLength); - AutoCompleteAdapter adapter = null; - mWebTextView.setAdapterCustom(adapter); - mWebTextView.setSingleLine(isTextField); - mWebTextView.setInPassword(nativeFocusCandidateIsPassword()); + mWebTextView.setType(nativeFocusCandidateType()); if (null == text) { - mWebTextView.setText("", 0, 0); if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "rebuildWebTextView null == text"); } - } else { - // Change to true to enable the old style behavior, where - // entering a textfield/textarea always set the selection to the - // whole field. This was desirable for the case where the user - // intends to scroll past the field using the trackball. - // However, it causes a problem when replying to emails - the - // user expects the cursor to be at the beginning of the - // textarea. Testing out a new behavior, where textfields set - // selection at the end, and textareas at the beginning. - if (false) { - mWebTextView.setText(text, 0, text.length()); - } else if (isTextField) { - int length = text.length(); - mWebTextView.setText(text, length, length); - if (DebugFlags.WEB_VIEW) { - Log.v(LOGTAG, "rebuildWebTextView length=" + length); + text = ""; + } + mWebTextView.setTextAndKeepSelection(text); + } + mWebTextView.requestFocus(); + } + + /** + * Called by WebTextView to find saved form data associated with the + * textfield + * @param name Name of the textfield. + * @param nodePointer Pointer to the node of the textfield, so it can be + * compared to the currently focused textfield when the data is + * retrieved. + */ + /* package */ void requestFormData(String name, int nodePointer) { + if (mWebViewCore.getSettings().getSaveFormData()) { + Message update = mPrivateHandler.obtainMessage(REQUEST_FORM_DATA); + update.arg1 = nodePointer; + RequestFormData updater = new RequestFormData(name, getUrl(), + update); + Thread t = new Thread(updater); + t.start(); + } + } + + /** + * Pass a message to find out the <label> associated with the <input> + * identified by nodePointer + * @param framePointer Pointer to the frame containing the <input> node + * @param nodePointer Pointer to the node for which a <label> is desired. + */ + /* package */ void requestLabel(int framePointer, int nodePointer) { + mWebViewCore.sendMessage(EventHub.REQUEST_LABEL, framePointer, + nodePointer); + } + + /* + * This class runs the layers animations in their own thread, + * so that we do not slow down the UI. + */ + private class EvaluateLayersAnimations extends Thread { + boolean mRunning = true; + // delay corresponds to 40fps, no need to go faster. + int mDelay = 25; // in ms + public void run() { + while (mRunning) { + if (mLayersHaveAnimations && mRootLayer != 0) { + // updates is a C++ pointer to a Vector of AnimationValues + int updates = nativeEvaluateLayersAnimations(mRootLayer); + if (updates == 0) { + mRunning = false; } + Message.obtain(mPrivateHandler, + WebView.IMMEDIATE_REPAINT_MSG_ID, + updates, 0).sendToTarget(); } else { - mWebTextView.setText(text, 0, 0); - if (DebugFlags.WEB_VIEW) { - Log.v(LOGTAG, "rebuildWebTextView !isTextField"); - } + mRunning = false; + } + try { + Thread.currentThread().sleep(mDelay); + } catch (InterruptedException e) { + mRunning = false; } } - mWebTextView.requestFocus(); + } + public void cancel() { + mRunning = false; } } @@ -3216,6 +3384,35 @@ public class WebView extends AbsoluteLayout } } + /** + * Dump the display tree to "/sdcard/displayTree.txt" + * + * @hide debug only + */ + public void dumpDisplayTree() { + nativeDumpDisplayTree(getUrl()); + } + + /** + * Dump the dom tree to adb shell if "toFile" is False, otherwise dump it to + * "/sdcard/domTree.txt" + * + * @hide debug only + */ + public void dumpDomTree(boolean toFile) { + mWebViewCore.sendMessage(EventHub.DUMP_DOMTREE, toFile ? 1 : 0, 0); + } + + /** + * Dump the render tree to adb shell if "toFile" is False, otherwise dump it + * to "/sdcard/renderTree.txt" + * + * @hide debug only + */ + public void dumpRenderTree(boolean toFile) { + mWebViewCore.sendMessage(EventHub.DUMP_RENDERTREE, toFile ? 1 : 0, 0); + } + // This is used to determine long press with the center key. Does not // affect long press with the trackball/touch. private boolean mGotCenterDown = false; @@ -3251,23 +3448,22 @@ public class WebView extends AbsoluteLayout if (mShiftIsPressed == false && nativeCursorWantsKeyEvents() == false && (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT || keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT)) { - mExtendSelection = false; - mShiftIsPressed = true; - if (nativeHasCursorNode()) { - Rect rect = nativeCursorNodeBounds(); - mSelectX = contentToViewX(rect.left); - mSelectY = contentToViewY(rect.top); - } else { - mSelectX = mScrollX + (int) mLastTouchX; - mSelectY = mScrollY + (int) mLastTouchY; - } - nativeHideCursor(); - } + setUpSelectXY(); + } if (keyCode >= KeyEvent.KEYCODE_DPAD_UP && keyCode <= KeyEvent.KEYCODE_DPAD_RIGHT) { // always handle the navigation keys in the UI thread switchOutDrawHistory(); + if (mShiftIsPressed) { + int xRate = keyCode == KeyEvent.KEYCODE_DPAD_LEFT + ? -1 : keyCode == KeyEvent.KEYCODE_DPAD_RIGHT ? 1 : 0; + int yRate = keyCode == KeyEvent.KEYCODE_DPAD_UP ? + -1 : keyCode == KeyEvent.KEYCODE_DPAD_DOWN ? 1 : 0; + int multiplier = event.getRepeatCount() + 1; + moveSelection(xRate * multiplier, yRate * multiplier); + return true; + } if (navHandledKey(keyCode, 1, false, event.getEventTime(), false)) { playSoundEffect(keyCodeToSoundsEffect(keyCode)); return true; @@ -3279,6 +3475,9 @@ public class WebView extends AbsoluteLayout if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) { switchOutDrawHistory(); if (event.getRepeatCount() == 0) { + if (mShiftIsPressed) { + return true; // discard press if copy in progress + } mGotCenterDown = true; mPrivateHandler.sendMessageDelayed(mPrivateHandler .obtainMessage(LONG_PRESS_CENTER), LONG_PRESS_TIMEOUT); @@ -3303,24 +3502,15 @@ public class WebView extends AbsoluteLayout if (getSettings().getNavDump()) { switch (keyCode) { case KeyEvent.KEYCODE_4: - // "/data/data/com.android.browser/displayTree.txt" - nativeDumpDisplayTree(getUrl()); + dumpDisplayTree(); break; case KeyEvent.KEYCODE_5: case KeyEvent.KEYCODE_6: - // 5: dump the dom tree to the file - // "/data/data/com.android.browser/domTree.txt" - // 6: dump the dom tree to the adb log - mWebViewCore.sendMessage(EventHub.DUMP_DOMTREE, - (keyCode == KeyEvent.KEYCODE_5) ? 1 : 0, 0); + dumpDomTree(keyCode == KeyEvent.KEYCODE_5); break; case KeyEvent.KEYCODE_7: case KeyEvent.KEYCODE_8: - // 7: dump the render tree to the file - // "/data/data/com.android.browser/renderTree.txt" - // 8: dump the render tree to the adb log - mWebViewCore.sendMessage(EventHub.DUMP_RENDERTREE, - (keyCode == KeyEvent.KEYCODE_7) ? 1 : 0, 0); + dumpRenderTree(keyCode == KeyEvent.KEYCODE_7); break; case KeyEvent.KEYCODE_9: nativeInstrumentReport(); @@ -3328,10 +3518,7 @@ public class WebView extends AbsoluteLayout } } - if (nativeCursorIsPlugin()) { - nativeUpdatePluginReceivesEvents(); - invalidate(); - } else if (nativeCursorIsTextInput()) { + if (nativeCursorIsTextInput()) { // This message will put the node in focus, for the DOM's notion // of focus, and make the focuscontroller active mWebViewCore.sendMessage(EventHub.CLICK, nativeCursorFramePointer(), @@ -3340,13 +3527,16 @@ public class WebView extends AbsoluteLayout // our view system's notion of focus rebuildWebTextView(); // Now we need to pass the event to it - return mWebTextView.onKeyDown(keyCode, event); + if (inEditingMode()) { + mWebTextView.setDefaultSelection(); + return mWebTextView.dispatchKeyEvent(event); + } } else if (nativeHasFocusNode()) { // In this case, the cursor is not on a text input, but the focus // might be. Check it, and if so, hand over to the WebTextView. rebuildWebTextView(); if (inEditingMode()) { - return mWebTextView.onKeyDown(keyCode, event); + return mWebTextView.dispatchKeyEvent(event); } } @@ -3411,7 +3601,13 @@ public class WebView extends AbsoluteLayout mGotCenterDown = false; if (mShiftIsPressed) { - return false; + if (mExtendSelection) { + commitCopy(); + } else { + mExtendSelection = true; + invalidate(); // draw the i-beam instead of the arrow + } + return true; // discard press if copy in progress } // perform the single click @@ -3421,21 +3617,22 @@ public class WebView extends AbsoluteLayout if (!nativeCursorIntersects(visibleRect)) { return false; } - nativeSetFollowedLink(true); - nativeUpdatePluginReceivesEvents(); WebViewCore.CursorData data = cursorData(); mWebViewCore.sendMessage(EventHub.SET_MOVE_MOUSE, data); playSoundEffect(SoundEffectConstants.CLICK); - boolean isTextInput = nativeCursorIsTextInput(); - if (isTextInput || !mCallbackProxy.uiOverrideUrlLoading( - nativeCursorText())) { + if (nativeCursorIsTextInput()) { + rebuildWebTextView(); + centerKeyPressOnTextField(); + if (inEditingMode()) { + mWebTextView.setDefaultSelection(); + } + return true; + } + nativeSetFollowedLink(true); + if (!mCallbackProxy.uiOverrideUrlLoading(nativeCursorText())) { mWebViewCore.sendMessage(EventHub.CLICK, data.mFrame, nativeCursorNodePointer()); } - if (isTextInput) { - rebuildWebTextView(); - displaySoftKeyboard(true); - } return true; } @@ -3451,70 +3648,85 @@ public class WebView extends AbsoluteLayout return false; } - /** - * @hide - */ - public void emulateShiftHeld() { - if (0 == mNativeClass) return; // client isn't initialized + private void setUpSelectXY() { mExtendSelection = false; mShiftIsPressed = true; + if (nativeHasCursorNode()) { + Rect rect = nativeCursorNodeBounds(); + mSelectX = contentToViewX(rect.left); + mSelectY = contentToViewY(rect.top); + } else if (mLastTouchY > getVisibleTitleHeight()) { + mSelectX = mScrollX + (int) mLastTouchX; + mSelectY = mScrollY + (int) mLastTouchY; + } else { + mSelectX = mScrollX + getViewWidth() / 2; + mSelectY = mScrollY + getViewHeightWithTitle() / 2; + } nativeHideCursor(); } + public void emulateShiftHeld() { + if (0 == mNativeClass) return; // client isn't initialized + setUpSelectXY(); + } + private boolean commitCopy() { boolean copiedSomething = false; if (mExtendSelection) { - // copy region so core operates on copy without touching orig. - Region selection = new Region(nativeGetSelection()); - if (selection.isEmpty() == false) { + String selection = nativeGetSelection(); + if (selection != "") { + if (DebugFlags.WEB_VIEW) { + Log.v(LOGTAG, "commitCopy \"" + selection + "\""); + } Toast.makeText(mContext , com.android.internal.R.string.text_copied , Toast.LENGTH_SHORT).show(); - mWebViewCore.sendMessage(EventHub.GET_SELECTION, selection); copiedSomething = true; + try { + IClipboard clip = IClipboard.Stub.asInterface( + ServiceManager.getService("clipboard")); + clip.setClipboardText(selection); + } catch (android.os.RemoteException e) { + Log.e(LOGTAG, "Clipboard failed", e); + } } mExtendSelection = false; } mShiftIsPressed = false; + invalidate(); // remove selection region and pointer if (mTouchMode == TOUCH_SELECT_MODE) { mTouchMode = TOUCH_INIT_MODE; } return copiedSomething; } - // Set this as a hierarchy change listener so we can know when this view - // is removed and still have access to our parent. @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); - ViewParent parent = getParent(); - if (parent instanceof ViewGroup) { - ViewGroup p = (ViewGroup) parent; - p.setOnHierarchyChangeListener(this); - } + if (hasWindowFocus()) onWindowFocusChanged(true); } @Override protected void onDetachedFromWindow() { + clearTextEntry(); super.onDetachedFromWindow(); - ViewParent parent = getParent(); - if (parent instanceof ViewGroup) { - ViewGroup p = (ViewGroup) parent; - p.setOnHierarchyChangeListener(null); - } - // Clean up the zoom controller mZoomButtonsController.setVisible(false); } - // Implementation for OnHierarchyChangeListener + /** + * @deprecated WebView no longer needs to implement + * ViewGroup.OnHierarchyChangeListener. This method does nothing now. + */ + @Deprecated public void onChildViewAdded(View parent, View child) {} - public void onChildViewRemoved(View p, View child) { - if (child == this) { - clearTextEntry(); - } - } + /** + * @deprecated WebView no longer needs to implement + * ViewGroup.OnHierarchyChangeListener. This method does nothing now. + */ + @Deprecated + public void onChildViewRemoved(View p, View child) {} /** * @deprecated WebView should not have implemented @@ -3619,6 +3831,24 @@ public class WebView extends AbsoluteLayout super.onFocusChanged(focused, direction, previouslyFocusedRect); } + /** + * @hide + */ + @Override + protected boolean setFrame(int left, int top, int right, int bottom) { + boolean changed = super.setFrame(left, top, right, bottom); + if (!changed && mHeightCanMeasure) { + // When mHeightCanMeasure is true, we will set mLastHeightSent to 0 + // in WebViewCore after we get the first layout. We do call + // requestLayout() when we get contentSizeChanged(). But the View + // system won't call onSizeChanged if the dimension is not changed. + // In this case, we need to call sendViewSizeZoom() explicitly to + // notify the WebKit about the new dimensions. + sendViewSizeZoom(); + } + return changed; + } + @Override protected void onSizeChanged(int w, int h, int ow, int oh) { super.onSizeChanged(w, h, ow, oh); @@ -3626,8 +3856,14 @@ public class WebView extends AbsoluteLayout if (mZoomScale == 0) { // unless we're already zooming mZoomCenterX = getViewWidth() * .5f; mZoomCenterY = getViewHeight() * .5f; - mAnchorX = viewToContentX((int) mZoomCenterX + mScrollX); - mAnchorY = viewToContentY((int) mZoomCenterY + mScrollY); + } + + // adjust the max viewport width depending on the view dimensions. This + // is to ensure the scaling is not going insane. So do not shrink it if + // the view size is temporarily smaller, e.g. when soft keyboard is up. + int newMaxViewportWidth = (int) (Math.max(w, h) / DEFAULT_MIN_ZOOM_SCALE); + if (newMaxViewportWidth > sMaxViewportWidth) { + sMaxViewportWidth = newMaxViewportWidth; } // update mMinZoomScale if the minimum zoom scale is not fixed @@ -3648,10 +3884,19 @@ public class WebView extends AbsoluteLayout } } - // we always force, in case our height changed, in which case we still - // want to send the notification over to webkit - // only update the text wrap scale if width changed. - setNewZoomScale(mActualScale, w != ow, true); + // onSizeChanged() is called during WebView layout. And any + // requestLayout() is blocked during layout. As setNewZoomScale() will + // call its child View to reposition itself through ViewManager's + // scaleAll(), we need to post a Runnable to ensure requestLayout(). + post(new Runnable() { + public void run() { + // we always force, in case our height changed, in which case we + // still want to send the notification over to webkit + if (mWebViewCore != null) { + setNewZoomScale(mActualScale, true); + } + } + }); } @Override @@ -3699,97 +3944,166 @@ public class WebView extends AbsoluteLayout private static final float MAX_SLOPE_FOR_DIAG = 1.5f; private static final int MIN_BREAK_SNAP_CROSS_DISTANCE = 80; - // MultiTouch handling - private static boolean mSupportMultiTouch; + private static int sign(float x) { + return x > 0 ? 1 : (x < 0 ? -1 : 0); + } - private double mPinchDistance; - private float mLastPressure; - private int mAnchorX; - private int mAnchorY; + // if the page can scroll <= this value, we won't allow the drag tracker + // to have any effect. + private static final int MIN_SCROLL_AMOUNT_TO_DISABLE_DRAG_TRACKER = 4; - private static float SCALE_INCREMENT = 0.01f; - private static float PRESSURE_THRESHOLD = 0.67f; + private class DragTrackerHandler { + private final DragTracker mProxy; + private final float mStartY, mStartX; + private final float mMinDY, mMinDX; + private final float mMaxDY, mMaxDX; + private float mCurrStretchY, mCurrStretchX; + private int mSX, mSY; - private boolean doMultiTouch(MotionEvent ev) { - int action = ev.getAction(); + public DragTrackerHandler(float x, float y, DragTracker proxy) { + mProxy = proxy; + + int docBottom = computeVerticalScrollRange() + getTitleHeight(); + int viewTop = getScrollY(); + int viewBottom = viewTop + getHeight(); + + mStartY = y; + mMinDY = -viewTop; + mMaxDY = docBottom - viewBottom; - if ((action & 0xff) == MotionEvent.ACTION_POINTER_DOWN) { - // cancel the single touch handling - cancelTouch(); - // reset the zoom overview mode so that the page won't auto grow - mInZoomOverview = false; - // If it is in password mode, turn it off so it does not draw - // misplaced. - if (inEditingMode() && nativeFocusCandidateIsPassword()) { - mWebTextView.setInPassword(false); + if (DebugFlags.DRAG_TRACKER || DEBUG_DRAG_TRACKER) { + Log.d(DebugFlags.DRAG_TRACKER_LOGTAG, " dragtracker y= " + y + + " up/down= " + mMinDY + " " + mMaxDY); } - // start multi (2-pointer) touch - float x0 = ev.getX(0); - float y0 = ev.getY(0); - float x1 = ev.getX(1); - float y1 = ev.getY(1); - mPinchDistance = Math.sqrt((x0 - x1) * (x0 - x1) + (y0 - y1) - * (y0 - y1)); - } else if ((action & 0xff) == MotionEvent.ACTION_POINTER_UP) { - if (mPreviewZoomOnly) { - mPreviewZoomOnly = false; - mAnchorX = viewToContentX((int) mZoomCenterX + mScrollX); - mAnchorY = viewToContentY((int) mZoomCenterY + mScrollY); - // for testing only, default don't reflow now - boolean reflowNow = !getSettings().getPluginsEnabled(); - // force zoom after mPreviewZoomOnly is set to false so that the - // new view size will be passed to the WebKit - setNewZoomScale(mActualScale, reflowNow, true); - // call invalidate() to draw without zoom filter - invalidate(); + + int docRight = computeHorizontalScrollRange(); + int viewLeft = getScrollX(); + int viewRight = viewLeft + getWidth(); + mStartX = x; + mMinDX = -viewLeft; + mMaxDX = docRight - viewRight; + + mProxy.onStartDrag(x, y); + + // ensure we buildBitmap at least once + mSX = -99999; + } + + private float computeStretch(float delta, float min, float max) { + float stretch = 0; + if (max - min > MIN_SCROLL_AMOUNT_TO_DISABLE_DRAG_TRACKER) { + if (delta < min) { + stretch = delta - min; + } else if (delta > max) { + stretch = delta - max; + } } - // adjust the edit text view if needed - if (inEditingMode()) { - adjustTextView(true); + return stretch; + } + + public void dragTo(float x, float y) { + float sy = computeStretch(mStartY - y, mMinDY, mMaxDY); + float sx = computeStretch(mStartX - x, mMinDX, mMaxDX); + + if (mCurrStretchX != sx || mCurrStretchY != sy) { + mCurrStretchX = sx; + mCurrStretchY = sy; + if (DebugFlags.DRAG_TRACKER || DEBUG_DRAG_TRACKER) { + Log.d(DebugFlags.DRAG_TRACKER_LOGTAG, "---- stretch " + sx + + " " + sy); + } + if (mProxy.onStretchChange(sx, sy)) { + invalidate(); + } } - // start a drag, TOUCH_PINCH_DRAG, can't use TOUCH_INIT_MODE as it - // may trigger the unwanted click, can't use TOUCH_DRAG_MODE as it - // may trigger the unwanted fling. - mTouchMode = TOUCH_PINCH_DRAG; - // action indicates which pointer is UP. Use the other one as drag's - // starting position. - int id = (((action & MotionEvent.ACTION_POINTER_ID_MASK) - >> MotionEvent.ACTION_POINTER_ID_SHIFT) == 0) ? 1 : 0; - startTouch(ev.getX(id), ev.getY(id), ev.getEventTime()); - } else if (action == MotionEvent.ACTION_MOVE) { - float x0 = ev.getX(0); - float y0 = ev.getY(0); - float x1 = ev.getX(1); - float y1 = ev.getY(1); - double distance = Math.sqrt((x0 - x1) * (x0 - x1) + (y0 - y1) - * (y0 - y1)); - float scale = (float) (Math.round(distance / mPinchDistance - * mActualScale * 100) / 100.0); - float pressure = ev.getPressure(0) + ev.getPressure(1); - if (Math.abs(scale - mActualScale) >= SCALE_INCREMENT - && (!mPreviewZoomOnly - || (pressure / mLastPressure) > PRESSURE_THRESHOLD)) { - mPreviewZoomOnly = true; - // limit the scale change per step - if (scale > mActualScale) { - scale = Math.min(scale, mActualScale * 1.25f); - } else { - scale = Math.max(scale, mActualScale * 0.8f); + } + + public void stopDrag() { + if (DebugFlags.DRAG_TRACKER || DEBUG_DRAG_TRACKER) { + Log.d(DebugFlags.DRAG_TRACKER_LOGTAG, "----- stopDrag"); + } + mProxy.onStopDrag(); + } + + private int hiddenHeightOfTitleBar() { + return getTitleHeight() - getVisibleTitleHeight(); + } + + // need a way to know if 565 or 8888 is the right config for + // capturing the display and giving it to the drag proxy + private Bitmap.Config offscreenBitmapConfig() { + // hard code 565 for now + return Bitmap.Config.RGB_565; + } + + /* If the tracker draws, then this returns true, otherwise it will + return false, and draw nothing. + */ + public boolean draw(Canvas canvas) { + if (mCurrStretchX != 0 || mCurrStretchY != 0) { + int sx = getScrollX(); + int sy = getScrollY() - hiddenHeightOfTitleBar(); + + if (mSX != sx || mSY != sy) { + buildBitmap(sx, sy); + mSX = sx; + mSY = sy; } - mZoomCenterX = (x0 + x1) / 2; - mZoomCenterY = (y0 + y1) / 2; - setNewZoomScale(scale, false, false); - invalidate(); - mPinchDistance = distance; - mLastPressure = pressure; + + int count = canvas.save(Canvas.MATRIX_SAVE_FLAG); + canvas.translate(sx, sy); + mProxy.onDraw(canvas); + canvas.restoreToCount(count); + return true; + } + if (DebugFlags.DRAG_TRACKER || DEBUG_DRAG_TRACKER) { + Log.d(DebugFlags.DRAG_TRACKER_LOGTAG, " -- draw false " + + mCurrStretchX + " " + mCurrStretchY); } - } else { - Log.w(LOGTAG, action + " should not happen during doMultiTouch"); return false; } - return true; + + private void buildBitmap(int sx, int sy) { + int w = getWidth(); + int h = getViewHeight(); + Bitmap bm = Bitmap.createBitmap(w, h, offscreenBitmapConfig()); + Canvas canvas = new Canvas(bm); + canvas.translate(-sx, -sy); + drawContent(canvas); + + if (DebugFlags.DRAG_TRACKER || DEBUG_DRAG_TRACKER) { + Log.d(DebugFlags.DRAG_TRACKER_LOGTAG, "--- buildBitmap " + sx + + " " + sy + " " + w + " " + h); + } + mProxy.onBitmapChange(bm); + } + } + + /** @hide */ + public static class DragTracker { + public void onStartDrag(float x, float y) {} + public boolean onStretchChange(float sx, float sy) { + // return true to have us inval the view + return false; + } + public void onStopDrag() {} + public void onBitmapChange(Bitmap bm) {} + public void onDraw(Canvas canvas) {} + } + + /** @hide */ + public DragTracker getDragTracker() { + return mDragTracker; + } + + /** @hide */ + public void setDragTracker(DragTracker tracker) { + mDragTracker = tracker; } + private DragTracker mDragTracker; + private DragTrackerHandler mDragTrackerHandler; + @Override public boolean onTouchEvent(MotionEvent ev) { if (mNativeClass == 0 || !isClickable() || !isLongClickable()) { @@ -3801,11 +4115,6 @@ public class WebView extends AbsoluteLayout + mTouchMode); } - if (mSupportMultiTouch && getSettings().supportZoom() - && mMinZoomScale < mMaxZoomScale && ev.getPointerCount() > 1) { - return doMultiTouch(ev); - } - int action = ev.getAction(); float x = ev.getX(); float y = ev.getY(); @@ -3833,8 +4142,10 @@ public class WebView extends AbsoluteLayout mLastSentTouchTime = eventTime; } - int deltaX = (int) (mLastTouchX - x); - int deltaY = (int) (mLastTouchY - y); + float fDeltaX = mLastTouchX - x; + float fDeltaY = mLastTouchY - y; + int deltaX = (int) fDeltaX; + int deltaY = (int) fDeltaY; switch (action) { case MotionEvent.ACTION_DOWN: { @@ -3856,6 +4167,7 @@ public class WebView extends AbsoluteLayout nativeMoveSelection(viewToContentX(mSelectX), viewToContentY(mSelectY), false); mTouchSelection = mExtendSelection = true; + invalidate(); // draw the i-beam instead of the arrow } else if (mPrivateHandler.hasMessages(RELEASE_SINGLE_TAP)) { mPrivateHandler.removeMessages(RELEASE_SINGLE_TAP); if (deltaX * deltaX + deltaY * deltaY < mDoubleTapSlopSquare) { @@ -3866,10 +4178,11 @@ public class WebView extends AbsoluteLayout // continue, mTouchMode should be still TOUCH_INIT_MODE } } else { - mPreviewZoomOnly = false; mTouchMode = TOUCH_INIT_MODE; mPreventDrag = mForwardTouchEvents ? PREVENT_DRAG_MAYBE_YES : PREVENT_DRAG_NO; + mPreventLongPress = false; + mPreventDoubleTap = false; mWebViewCore.sendMessage( EventHub.UPDATE_FRAME_CACHE_IF_LOADING); if (mLogEvent && eventTime - mLastTouchUpTime < 1000) { @@ -3884,7 +4197,15 @@ public class WebView extends AbsoluteLayout .obtainMessage(SWITCH_TO_SHORTPRESS), TAP_TIMEOUT); } // Remember where the motion event started - startTouch(x, y, eventTime); + mLastTouchX = x; + mLastTouchY = y; + mLastTouchTime = eventTime; + mVelocityTracker = VelocityTracker.obtain(); + mSnapScrollMode = SNAP_NONE; + if (mDragTracker != null) { + mDragTrackerHandler = new DragTrackerHandler(x, y, + mDragTracker); + } break; } case MotionEvent.ACTION_MOVE: { @@ -3922,6 +4243,11 @@ public class WebView extends AbsoluteLayout || mTouchMode == TOUCH_DOUBLE_TAP_MODE) { mPrivateHandler.removeMessages(SWITCH_TO_SHORTPRESS); } + if (mFullScreenHolder != null) { + // in full screen mode, the WebView can't be panned. + mTouchMode = TOUCH_DONE_MODE; + break; + } // if it starts nearly horizontal or vertical, enforce it int ax = Math.abs(deltaX); @@ -3935,36 +4261,50 @@ public class WebView extends AbsoluteLayout } mTouchMode = TOUCH_DRAG_MODE; + mLastTouchX = x; + mLastTouchY = y; + fDeltaX = 0.0f; + fDeltaY = 0.0f; + deltaX = 0; + deltaY = 0; + WebViewCore.pauseUpdate(mWebViewCore); if (!mDragFromTextInput) { nativeHideCursor(); } - if (!mSupportMultiTouch) { - WebSettings settings = getSettings(); - if (settings.supportZoom() - && settings.getBuiltInZoomControls() - && !mZoomButtonsController.isVisible() - && mMinZoomScale < mMaxZoomScale) { - mZoomButtonsController.setVisible(true); - int count = settings.getDoubleTapToastCount(); - if (mInZoomOverview && count > 0) { - settings.setDoubleTapToastCount(--count); - Toast.makeText(mContext, - com.android.internal.R.string.double_tap_toast, - Toast.LENGTH_LONG).show(); - } + WebSettings settings = getSettings(); + if (settings.supportZoom() + && settings.getBuiltInZoomControls() + && !mZoomButtonsController.isVisible() + && mMinZoomScale < mMaxZoomScale) { + mZoomButtonsController.setVisible(true); + int count = settings.getDoubleTapToastCount(); + if (mInZoomOverview && count > 0) { + settings.setDoubleTapToastCount(--count); + Toast.makeText(mContext, + com.android.internal.R.string.double_tap_toast, + Toast.LENGTH_LONG).show(); } } } // do pan int newScrollX = pinLocX(mScrollX + deltaX); - deltaX = newScrollX - mScrollX; + int newDeltaX = newScrollX - mScrollX; + if (deltaX != newDeltaX) { + deltaX = newDeltaX; + fDeltaX = (float) newDeltaX; + } int newScrollY = pinLocY(mScrollY + deltaY); - deltaY = newScrollY - mScrollY; + int newDeltaY = newScrollY - mScrollY; + if (deltaY != newDeltaY) { + deltaY = newDeltaY; + fDeltaY = (float) newDeltaY; + } boolean done = false; - if (deltaX == 0 && deltaY == 0) { - done = true; + boolean keepScrollBarsVisible = false; + if (Math.abs(fDeltaX) < 1.0f && Math.abs(fDeltaY) < 1.0f) { + keepScrollBarsVisible = done = true; } else { if (mSnapScrollMode == SNAP_X || mSnapScrollMode == SNAP_Y) { int ax = Math.abs(deltaX); @@ -3976,63 +4316,53 @@ public class WebView extends AbsoluteLayout mSnapScrollMode = SNAP_NONE; } // reverse direction means lock in the snap mode - if ((ax > MAX_SLOPE_FOR_DIAG * ay) && - ((mSnapPositive && - deltaX < -mMinLockSnapReverseDistance) - || (!mSnapPositive && - deltaX > mMinLockSnapReverseDistance))) { - mSnapScrollMode = SNAP_X_LOCK; + if (ax > MAX_SLOPE_FOR_DIAG * ay && + (mSnapPositive + ? deltaX < -mMinLockSnapReverseDistance + : deltaX > mMinLockSnapReverseDistance)) { + mSnapScrollMode |= SNAP_LOCK; } } else { // radical change means getting out of snap mode - if ((ax > MAX_SLOPE_FOR_DIAG * ay) + if (ax > MAX_SLOPE_FOR_DIAG * ay && ax > MIN_BREAK_SNAP_CROSS_DISTANCE) { mSnapScrollMode = SNAP_NONE; } // reverse direction means lock in the snap mode - if ((ay > MAX_SLOPE_FOR_DIAG * ax) && - ((mSnapPositive && - deltaY < -mMinLockSnapReverseDistance) - || (!mSnapPositive && - deltaY > mMinLockSnapReverseDistance))) { - mSnapScrollMode = SNAP_Y_LOCK; + if (ay > MAX_SLOPE_FOR_DIAG * ax && + (mSnapPositive + ? deltaY < -mMinLockSnapReverseDistance + : deltaY > mMinLockSnapReverseDistance)) { + mSnapScrollMode |= SNAP_LOCK; } } } - - if (mSnapScrollMode == SNAP_X - || mSnapScrollMode == SNAP_X_LOCK) { - if (deltaX == 0) { - // keep the scrollbar on the screen even there is no - // scroll - awakenScrollBars(ViewConfiguration - .getScrollDefaultDelay(), false); + if (mSnapScrollMode != SNAP_NONE) { + if ((mSnapScrollMode & SNAP_X) == SNAP_X) { + deltaY = 0; } else { - scrollBy(deltaX, 0); + deltaX = 0; } - mLastTouchX = x; - } else if (mSnapScrollMode == SNAP_Y - || mSnapScrollMode == SNAP_Y_LOCK) { - if (deltaY == 0) { - // keep the scrollbar on the screen even there is no - // scroll - awakenScrollBars(ViewConfiguration - .getScrollDefaultDelay(), false); - } else { - scrollBy(0, deltaY); + } + if ((deltaX | deltaY) != 0) { + scrollBy(deltaX, deltaY); + if (deltaX != 0) { + mLastTouchX = x; } - mLastTouchY = y; + if (deltaY != 0) { + mLastTouchY = y; + } + mHeldMotionless = MOTIONLESS_FALSE; } else { - scrollBy(deltaX, deltaY); - mLastTouchX = x; - mLastTouchY = y; + // keep the scrollbar on the screen even there is no + // scroll + keepScrollBarsVisible = true; } mLastTouchTime = eventTime; mUserScroll = true; } - if (!mSupportMultiTouch - && !getSettings().getBuiltInZoomControls()) { + if (!getSettings().getBuiltInZoomControls()) { boolean showPlusMinus = mMinZoomScale < mMaxZoomScale; if (mZoomControls != null && showPlusMinus) { if (mZoomControls.getVisibility() == View.VISIBLE) { @@ -4045,23 +4375,44 @@ public class WebView extends AbsoluteLayout } } - if (done) { + if (mDragTrackerHandler != null) { + mDragTrackerHandler.dragTo(x, y); + } + + if (keepScrollBarsVisible) { + if (mHeldMotionless != MOTIONLESS_TRUE) { + mHeldMotionless = MOTIONLESS_TRUE; + invalidate(); + } // keep the scrollbar on the screen even there is no scroll awakenScrollBars(ViewConfiguration.getScrollDefaultDelay(), false); // return false to indicate that we can't pan out of the // view space - return false; + return !done; } break; } case MotionEvent.ACTION_UP: { + if (mDragTrackerHandler != null) { + mDragTrackerHandler.stopDrag(); + mDragTrackerHandler = null; + } mLastTouchUpTime = eventTime; switch (mTouchMode) { case TOUCH_DOUBLE_TAP_MODE: // double tap mPrivateHandler.removeMessages(SWITCH_TO_SHORTPRESS); mTouchMode = TOUCH_DONE_MODE; - doDoubleTap(); + if (mPreventDoubleTap) { + WebViewCore.TouchEventData ted + = new WebViewCore.TouchEventData(); + ted.mAction = WebViewCore.ACTION_DOUBLETAP; + ted.mX = viewToContentX((int) x + mScrollX); + ted.mY = viewToContentY((int) y + mScrollY); + mWebViewCore.sendMessage(EventHub.TOUCH_EVENT, ted); + } else if (mFullScreenHolder == null) { + doDoubleTap(); + } break; case TOUCH_SELECT_MODE: commitCopy(); @@ -4075,8 +4426,9 @@ public class WebView extends AbsoluteLayout if ((deltaX * deltaX + deltaY * deltaY) > mTouchSlopSquare) { Log.w(LOGTAG, "Miss a drag as we are waiting for" + " WebCore's response for touch down."); - if (computeHorizontalScrollExtent() < computeHorizontalScrollRange() - || computeVerticalScrollExtent() < computeVerticalScrollRange()) { + if (mFullScreenHolder == null + && (computeHorizontalScrollExtent() < computeHorizontalScrollRange() + || computeVerticalScrollExtent() < computeVerticalScrollRange())) { // we will not rewrite drag code here, but we // will try fling if it applies. WebViewCore.pauseUpdate(mWebViewCore); @@ -4089,6 +4441,8 @@ public class WebView extends AbsoluteLayout // if mPreventDrag is not confirmed, treat it as // no so that it won't block tap or double tap. mPreventDrag = PREVENT_DRAG_NO; + mPreventLongPress = false; + mPreventDoubleTap = false; } if (mPreventDrag == PREVENT_DRAG_NO) { if (mTouchMode == TOUCH_INIT_MODE) { @@ -4104,6 +4458,9 @@ public class WebView extends AbsoluteLayout break; } case TOUCH_DRAG_MODE: + mPrivateHandler.removeMessages(DRAG_HELD_MOTIONLESS); + mPrivateHandler.removeMessages(AWAKEN_SCROLL_BARS); + mHeldMotionless = MOTIONLESS_TRUE; // redraw in high-quality, as we're done dragging invalidate(); // if the user waits a while w/o moving before the @@ -4131,38 +4488,33 @@ public class WebView extends AbsoluteLayout break; } case MotionEvent.ACTION_CANCEL: { - cancelTouch(); + if (mDragTrackerHandler != null) { + mDragTrackerHandler.stopDrag(); + mDragTrackerHandler = null; + } + // we also use mVelocityTracker == null to tell us that we are + // not "moving around", so we can take the slower/prettier + // mode in the drawing code + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + if (mTouchMode == TOUCH_DRAG_MODE) { + WebViewCore.resumeUpdate(mWebViewCore); + } + mPrivateHandler.removeMessages(SWITCH_TO_SHORTPRESS); + mPrivateHandler.removeMessages(SWITCH_TO_LONGPRESS); + mPrivateHandler.removeMessages(DRAG_HELD_MOTIONLESS); + mPrivateHandler.removeMessages(AWAKEN_SCROLL_BARS); + mHeldMotionless = MOTIONLESS_TRUE; + mTouchMode = TOUCH_DONE_MODE; + nativeHideCursor(); break; } } return true; } - private void startTouch(float x, float y, long eventTime) { - mLastTouchX = x; - mLastTouchY = y; - mLastTouchTime = eventTime; - mVelocityTracker = VelocityTracker.obtain(); - mSnapScrollMode = SNAP_NONE; - } - - private void cancelTouch() { - // we also use mVelocityTracker == null to tell us that we are not - // "moving around", so we can take the slower/prettier mode in the - // drawing code - if (mVelocityTracker != null) { - mVelocityTracker.recycle(); - mVelocityTracker = null; - } - if (mTouchMode == TOUCH_DRAG_MODE) { - WebViewCore.resumeUpdate(mWebViewCore); - } - mPrivateHandler.removeMessages(SWITCH_TO_SHORTPRESS); - mPrivateHandler.removeMessages(SWITCH_TO_LONGPRESS); - mTouchMode = TOUCH_DONE_MODE; - nativeHideCursor(); - } - private long mTrackballFirstTime = 0; private long mTrackballLastTime = 0; private float mTrackballRemainsX = 0.0f; @@ -4181,6 +4533,7 @@ public class WebView extends AbsoluteLayout private static final int SELECT_CURSOR_OFFSET = 16; private int mSelectX = 0; private int mSelectY = 0; + private boolean mFocusSizeChanged = false; private boolean mShiftIsPressed = false; private boolean mTrackballDown = false; private long mTrackballUpTime = 0; @@ -4239,6 +4592,7 @@ public class WebView extends AbsoluteLayout commitCopy(); } else { mExtendSelection = true; + invalidate(); // draw the i-beam instead of the arrow } return true; // discard press if copy in progress } @@ -4286,8 +4640,8 @@ public class WebView extends AbsoluteLayout return; int width = getViewWidth(); int height = getViewHeight(); - mSelectX += scaleTrackballX(xRate, width); - mSelectY += scaleTrackballY(yRate, height); + mSelectX += xRate; + mSelectY += yRate; int maxX = width + mScrollX; int maxY = height + mScrollY; mSelectX = Math.min(maxX, Math.max(mScrollX - SELECT_CURSOR_OFFSET @@ -4369,8 +4723,11 @@ public class WebView extends AbsoluteLayout } float xRate = mTrackballRemainsX * 1000 / elapsed; float yRate = mTrackballRemainsY * 1000 / elapsed; + int viewWidth = getViewWidth(); + int viewHeight = getViewHeight(); if (mShiftIsPressed) { - moveSelection(xRate, yRate); + moveSelection(scaleTrackballX(xRate, viewWidth), + scaleTrackballY(yRate, viewHeight)); mTrackballRemainsX = mTrackballRemainsY = 0; return; } @@ -4384,8 +4741,8 @@ public class WebView extends AbsoluteLayout + " mTrackballRemainsX=" + mTrackballRemainsX + " mTrackballRemainsY=" + mTrackballRemainsY); } - int width = mContentWidth - getViewWidth(); - int height = mContentHeight - getViewHeight(); + int width = mContentWidth - viewWidth; + int height = mContentHeight - viewHeight; if (width < 0) width = 0; if (height < 0) height = 0; ax = Math.abs(mTrackballRemainsX * TRACKBALL_MULTIPLIER); @@ -4460,7 +4817,7 @@ public class WebView extends AbsoluteLayout int vy = (int) mVelocityTracker.getYVelocity(); if (mSnapScrollMode != SNAP_NONE) { - if (mSnapScrollMode == SNAP_X || mSnapScrollMode == SNAP_X_LOCK) { + if ((mSnapScrollMode & SNAP_X) == SNAP_X) { vy = 0; } else { vx = 0; @@ -4522,7 +4879,7 @@ public class WebView extends AbsoluteLayout scale = mDefaultScale; } - setNewZoomScale(scale, true, false); + setNewZoomScale(scale, false); if (oldScale != mActualScale) { // use mZoomPickerScale to see zoom preview first @@ -4530,6 +4887,9 @@ public class WebView extends AbsoluteLayout mInvInitialZoomScale = 1.0f / oldScale; mInvFinalZoomScale = 1.0f / mActualScale; mZoomScale = mActualScale; + if (!mInZoomOverview) { + mLastScale = scale; + } invalidate(); return true; } else { @@ -4627,13 +4987,18 @@ public class WebView extends AbsoluteLayout public boolean zoomIn() { // TODO: alternatively we can disallow this during draw history mode switchOutDrawHistory(); - mInZoomOverview = false; // Center zooming to the center of the screen. - mZoomCenterX = getViewWidth() * .5f; - mZoomCenterY = getViewHeight() * .5f; - mAnchorX = viewToContentX((int) mZoomCenterX + mScrollX); - mAnchorY = viewToContentY((int) mZoomCenterY + mScrollY); - return zoomWithPreview(mActualScale * 1.25f); + if (mInZoomOverview) { + // if in overview mode, bring it back to normal mode + mLastTouchX = getViewWidth() * .5f; + mLastTouchY = getViewHeight() * .5f; + doDoubleTap(); + return true; + } else { + mZoomCenterX = getViewWidth() * .5f; + mZoomCenterY = getViewHeight() * .5f; + return zoomWithPreview(mActualScale * 1.25f); + } } /** @@ -4643,12 +5008,20 @@ public class WebView extends AbsoluteLayout public boolean zoomOut() { // TODO: alternatively we can disallow this during draw history mode switchOutDrawHistory(); - // Center zooming to the center of the screen. - mZoomCenterX = getViewWidth() * .5f; - mZoomCenterY = getViewHeight() * .5f; - mAnchorX = viewToContentX((int) mZoomCenterX + mScrollX); - mAnchorY = viewToContentY((int) mZoomCenterY + mScrollY); - return zoomWithPreview(mActualScale * 0.8f); + float scale = mActualScale * 0.8f; + if (scale < (mMinZoomScale + 0.1f) + && mWebViewCore.getSettings().getUseWideViewPort() + && mZoomOverviewWidth > Math.ceil(getViewWidth() + * mInvActualScale)) { + // when zoom out to min scale, switch to overview mode + doDoubleTap(); + return true; + } else { + // Center zooming to the center of the screen. + mZoomCenterX = getViewWidth() * .5f; + mZoomCenterY = getViewHeight() * .5f; + return zoomWithPreview(scale); + } } private void updateSelection() { @@ -4716,7 +5089,7 @@ public class WebView extends AbsoluteLayout } /** - * Do a touch up from a WebTextView. This will be handled by webkit to + * Due a touch up from a WebTextView. This will be handled by webkit to * change the selection. * @param event MotionEvent in the WebTextView's coordinates. */ @@ -4726,23 +5099,15 @@ public class WebView extends AbsoluteLayout } int x = viewToContentX((int) event.getX() + mWebTextView.getLeft()); int y = viewToContentY((int) event.getY() + mWebTextView.getTop()); - // In case the soft keyboard has been dismissed, bring it back up. - InputMethodManager.getInstance(getContext()).showSoftInput(mWebTextView, - 0); - if (nativeFocusNodePointer() != nativeCursorNodePointer()) { - nativeMotionUp(x, y, mNavSlop); - } - nativeTextInputMotionUp(x, y); + nativeMotionUp(x, y, mNavSlop); } - /*package*/ void shortPressOnTextField() { - if (inEditingMode()) { - View v = mWebTextView; - int x = viewToContentX((v.getLeft() + v.getRight()) >> 1); - int y = viewToContentY((v.getTop() + v.getBottom()) >> 1); - displaySoftKeyboard(true); - nativeTextInputMotionUp(x, y); - } + /** + * Called when pressing the center key or trackball on a textfield. + */ + /*package*/ void centerKeyPressOnTextField() { + mWebViewCore.sendMessage(EventHub.CLICK, nativeCursorFramePointer(), + nativeCursorNodePointer()); } private void doShortPress() { @@ -4753,6 +5118,22 @@ public class WebView extends AbsoluteLayout // mLastTouchX and mLastTouchY are the point in the current viewport int contentX = viewToContentX((int) mLastTouchX + mScrollX); int contentY = viewToContentY((int) mLastTouchY + mScrollY); + if (nativePointInNavCache(contentX, contentY, mNavSlop)) { + WebViewCore.MotionUpData motionUpData = new WebViewCore + .MotionUpData(); + motionUpData.mFrame = nativeCacheHitFramePointer(); + motionUpData.mNode = nativeCacheHitNodePointer(); + motionUpData.mBounds = nativeCacheHitNodeBounds(); + motionUpData.mX = contentX; + motionUpData.mY = contentY; + mWebViewCore.sendMessageAtFrontOfQueue(EventHub.VALID_NODE_BOUNDS, + motionUpData); + } else { + doMotionUp(contentX, contentY); + } + } + + private void doMotionUp(int contentX, int contentY) { if (nativeMotionUp(contentX, contentY, mNavSlop)) { if (mLogEvent) { Checkin.updateStats(mContext.getContentResolver(), @@ -4764,71 +5145,50 @@ public class WebView extends AbsoluteLayout } } - // Rule for double tap: - // 1. if the current scale is not same as the text wrap scale and layout - // algorithm is NARROW_COLUMNS, fit to column; - // 2. if the current state is not overview mode, change to overview mode; - // 3. if the current state is overview mode, change to default scale. private void doDoubleTap() { if (mWebViewCore.getSettings().getUseWideViewPort() == false) { return; } mZoomCenterX = mLastTouchX; mZoomCenterY = mLastTouchY; - mAnchorX = viewToContentX((int) mZoomCenterX + mScrollX); - mAnchorY = viewToContentY((int) mZoomCenterY + mScrollY); + mInZoomOverview = !mInZoomOverview; + // remove the zoom control after double tap WebSettings settings = getSettings(); - if (!mSupportMultiTouch) { - // remove the zoom control after double tap - if (settings.getBuiltInZoomControls()) { - if (mZoomButtonsController.isVisible()) { - mZoomButtonsController.setVisible(false); - } - } else { - if (mZoomControlRunnable != null) { - mPrivateHandler.removeCallbacks(mZoomControlRunnable); - } - if (mZoomControls != null) { - mZoomControls.hide(); - } + if (settings.getBuiltInZoomControls()) { + if (mZoomButtonsController.isVisible()) { + mZoomButtonsController.setVisible(false); } - settings.setDoubleTapToastCount(0); - } - if ((settings.getLayoutAlgorithm() == WebSettings.LayoutAlgorithm.NARROW_COLUMNS) - && (Math.abs(mActualScale - mTextWrapScale) >= 0.01f)) { - setNewZoomScale(mActualScale, true, true); - float overviewScale = (float) getViewWidth() / mZoomOverviewWidth; - if (Math.abs(mActualScale - overviewScale) < 0.01f) { - mInZoomOverview = true; + } else { + if (mZoomControlRunnable != null) { + mPrivateHandler.removeCallbacks(mZoomControlRunnable); } - } else if (!mInZoomOverview) { + if (mZoomControls != null) { + mZoomControls.hide(); + } + } + settings.setDoubleTapToastCount(0); + if (mInZoomOverview) { float newScale = (float) getViewWidth() / mZoomOverviewWidth; - if (Math.abs(mActualScale - newScale) >= 0.01f) { - mInZoomOverview = true; + if (Math.abs(mActualScale - newScale) < 0.01f) { + // reset mInZoomOverview to false if scale doesn't change + mInZoomOverview = false; + } else { // Force the titlebar fully reveal in overview mode if (mScrollY < getTitleHeight()) mScrollY = 0; zoomWithPreview(newScale); - } else if (Math.abs(mActualScale - mDefaultScale) >= 0.01f) { - mInZoomOverview = true; } } else { - mInZoomOverview = false; - int left = nativeGetBlockLeftEdge(mAnchorX, mAnchorY, mActualScale); + // mLastTouchX and mLastTouchY are the point in the current viewport + int contentX = viewToContentX((int) mLastTouchX + mScrollX); + int contentY = viewToContentY((int) mLastTouchY + mScrollY); + int left = nativeGetBlockLeftEdge(contentX, contentY, mActualScale); if (left != NO_LEFTEDGE) { - // add a 5pt padding to the left edge. - int viewLeft = contentToViewX(left < 5 ? 0 : (left - 5)) - - mScrollX; - // Re-calculate the zoom center so that the new scroll x will be - // on the left edge. - if (viewLeft > 0) { - mZoomCenterX = viewLeft * mDefaultScale - / (mDefaultScale - mActualScale); - } else { - scrollBy(viewLeft, 0); - mZoomCenterX = 0; - } + // add a 5pt padding to the left edge. Re-calculate the zoom + // center so that the new scroll x will be on the left edge. + mZoomCenterX = left < 5 ? 0 : (left - 5) * mLastScale + * mActualScale / (mLastScale - mActualScale); } - zoomWithPreview(mDefaultScale); + zoomWithPreview(mLastScale); } } @@ -4838,15 +5198,6 @@ public class WebView extends AbsoluteLayout mCallbackProxy.uiOverrideUrlLoading(url); } - // called by JNI - private void sendPluginState(int state) { - WebViewCore.PluginStateData psd = new WebViewCore.PluginStateData(); - psd.mFrame = nativeCursorFramePointer(); - psd.mNode = nativeCursorNodePointer(); - psd.mState = state; - mWebViewCore.sendMessage(EventHub.PLUGIN_STATE, psd); - } - @Override public boolean requestFocus(int direction, Rect previouslyFocusedRect) { boolean result = false; @@ -4995,14 +5346,6 @@ public class WebView extends AbsoluteLayout } /* package */ void passToJavaScript(String currentText, KeyEvent event) { - if (nativeCursorWantsKeyEvents() && !nativeCursorMatchesFocus()) { - mWebViewCore.sendMessage(EventHub.CLICK); - if (mWebTextView.mOkayForFocusNotToMatch) { - int select = nativeFocusCandidateIsTextField() ? - nativeFocusCandidateMaxLength() : 0; - setSelection(select, select); - } - } WebViewCore.JSKeyData arg = new WebViewCore.JSKeyData(); arg.mEvent = event; arg.mCurrentText = currentText; @@ -5033,11 +5376,11 @@ public class WebView extends AbsoluteLayout class PrivateHandler extends Handler { @Override public void handleMessage(Message msg) { - if (DebugFlags.WEB_VIEW) { - Log.v(LOGTAG, msg.what < REMEMBER_PASSWORD - || msg.what > SHOW_RECT_MSG_ID ? Integer - .toString(msg.what) : HandlerDebugString[msg.what - - REMEMBER_PASSWORD]); + // exclude INVAL_RECT_MSG_ID since it is frequently output + if (DebugFlags.WEB_VIEW && msg.what != INVAL_RECT_MSG_ID) { + Log.v(LOGTAG, msg.what < REMEMBER_PASSWORD || msg.what + > RETURN_LABEL ? Integer.toString(msg.what) + : HandlerDebugString[msg.what - REMEMBER_PASSWORD]); } if (mWebViewCore == null) { // after WebView's destroy() is called, skip handling messages. @@ -5063,9 +5406,13 @@ public class WebView extends AbsoluteLayout // it won't block panning the page. if (mPreventDrag == PREVENT_DRAG_MAYBE_YES) { mPreventDrag = PREVENT_DRAG_NO; + mPreventLongPress = false; + mPreventDoubleTap = false; } if (mTouchMode == TOUCH_INIT_MODE) { - mTouchMode = TOUCH_SHORTPRESS_START_MODE; + mTouchMode = mFullScreenHolder == null + ? TOUCH_SHORTPRESS_START_MODE + : TOUCH_SHORTPRESS_MODE; updateSelection(); } else if (mTouchMode == TOUCH_DOUBLE_TAP_MODE) { mTouchMode = TOUCH_DONE_MODE; @@ -5073,10 +5420,20 @@ public class WebView extends AbsoluteLayout break; } case SWITCH_TO_LONGPRESS: { - if (mPreventDrag == PREVENT_DRAG_NO) { + if (mPreventLongPress) { mTouchMode = TOUCH_DONE_MODE; - performLongClick(); - rebuildWebTextView(); + WebViewCore.TouchEventData ted + = new WebViewCore.TouchEventData(); + ted.mAction = WebViewCore.ACTION_LONGPRESS; + ted.mX = viewToContentX((int) mLastTouchX + mScrollX); + ted.mY = viewToContentY((int) mLastTouchY + mScrollY); + mWebViewCore.sendMessage(EventHub.TOUCH_EVENT, ted); + } else if (mPreventDrag == PREVENT_DRAG_NO) { + mTouchMode = TOUCH_DONE_MODE; + if (mFullScreenHolder == null) { + performLongClick(); + rebuildWebTextView(); + } } break; } @@ -5120,8 +5477,12 @@ public class WebView extends AbsoluteLayout final Point viewSize = draw.mViewPoint; boolean useWideViewport = settings.getUseWideViewPort(); WebViewCore.RestoreState restoreState = draw.mRestoreState; - if (restoreState != null) { + boolean hasRestoreState = restoreState != null; + if (hasRestoreState) { mInZoomOverview = false; + mLastScale = mInitialScaleInPercent > 0 + ? mInitialScaleInPercent / 100.0f + : restoreState.mTextWrapScale; if (restoreState.mMinScale == 0) { if (restoreState.mMobileSite) { if (draw.mMinPrefWidth > @@ -5129,8 +5490,6 @@ public class WebView extends AbsoluteLayout mMinZoomScale = (float) viewWidth / draw.mMinPrefWidth; mMinZoomScaleFixed = false; - mInZoomOverview = useWideViewport && - settings.getLoadWithOverviewMode(); } else { mMinZoomScale = restoreState.mDefaultScale; mMinZoomScaleFixed = true; @@ -5148,32 +5507,26 @@ public class WebView extends AbsoluteLayout } else { mMaxZoomScale = restoreState.mMaxScale; } - if (mInitialScaleInPercent > 0) { - setNewZoomScale(mInitialScaleInPercent / 100.0f, - true, false); - } else if (restoreState.mViewScale > 0) { - mTextWrapScale = restoreState.mTextWrapScale; - setNewZoomScale(restoreState.mViewScale, false, - false); - } else { - mInZoomOverview = useWideViewport - && settings.getLoadWithOverviewMode(); - if (mInZoomOverview) { - setNewZoomScale((float) viewWidth - / WebViewCore.DEFAULT_VIEWPORT_WIDTH, - true, false); - } else { - setNewZoomScale(restoreState.mTextWrapScale, - true, false); - } - } + setNewZoomScale(mLastScale, false); setContentScrollTo(restoreState.mScrollX, restoreState.mScrollY); + if (useWideViewport + && settings.getLoadWithOverviewMode()) { + if (restoreState.mViewScale == 0 + || (restoreState.mMobileSite + && mMinZoomScale < restoreState.mDefaultScale)) { + mInZoomOverview = true; + } + } // As we are on a new page, remove the WebTextView. This // is necessary for page loads driven by webkit, and in // particular when the user was on a password field, so // the WebTextView was visible. clearTextEntry(); + // update the zoom buttons as the scale can be changed + if (getSettings().getBuiltInZoomControls()) { + updateZoomButtonsEnabled(); + } } // We update the layout (i.e. request a layout from the // view system) if the last view size that we sent to @@ -5194,8 +5547,11 @@ public class WebView extends AbsoluteLayout mPictureListener.onNewPicture(WebView.this, capturePicture()); } if (useWideViewport) { - mZoomOverviewWidth = Math.max(draw.mMinPrefWidth, - draw.mViewPoint.x); + // limit mZoomOverviewWidth to sMaxViewportWidth so that + // if the page doesn't behave well, the WebView won't go + // insane. + mZoomOverviewWidth = Math.min(sMaxViewportWidth, Math + .max(draw.mMinPrefWidth, draw.mViewPoint.x)); } if (!mMinZoomScaleFixed) { mMinZoomScale = (float) viewWidth / mZoomOverviewWidth; @@ -5206,9 +5562,15 @@ public class WebView extends AbsoluteLayout if (Math.abs((viewWidth * mInvActualScale) - mZoomOverviewWidth) > 1) { setNewZoomScale((float) viewWidth - / mZoomOverviewWidth, true, false); + / mZoomOverviewWidth, false); } } + if (draw.mFocusSizeChanged && inEditingMode()) { + mFocusSizeChanged = true; + } + if (hasRestoreState) { + mViewManager.postReadyToDrawAll(); + } break; } case WEBCORE_INITIALIZED_MSG_ID: @@ -5248,11 +5610,23 @@ public class WebView extends AbsoluteLayout tData.mEnd); } break; - case MOVE_OUT_OF_PLUGIN: - if (nativePluginEatsNavKey()) { - navHandledKey(msg.arg1, 1, false, 0, true); + case RETURN_LABEL: + if (inEditingMode() + && mWebTextView.isSameTextField(msg.arg1)) { + mWebTextView.setHint((String) msg.obj); + InputMethodManager imm + = InputMethodManager.peekInstance(); + // The hint is propagated to the IME in + // onCreateInputConnection. If the IME is already + // active, restart it so that its hint text is updated. + if (imm != null && imm.isActive(mWebTextView)) { + imm.restartInput(mWebTextView); + } } break; + case MOVE_OUT_OF_PLUGIN: + navHandledKey(msg.arg1, 1, false, 0, true); + break; case UPDATE_TEXT_ENTRY_MSG_ID: // this is sent after finishing resize in WebViewCore. Make // sure the text edit box is still on the screen. @@ -5275,25 +5649,44 @@ public class WebView extends AbsoluteLayout } break; } + case IMMEDIATE_REPAINT_MSG_ID: { + int updates = msg.arg1; + if (updates != 0) { + // updates is a C++ pointer to a Vector of + // AnimationValues that we apply to the layers. + // The Vector is deallocated in nativeUpdateLayers(). + nativeUpdateLayers(mRootLayer, updates); + } + invalidate(); + break; + } + case SET_ROOT_LAYER_MSG_ID: { + int oldLayer = mRootLayer; + mRootLayer = msg.arg1; + if (oldLayer > 0) { + nativeDestroyLayer(oldLayer); + } + if (mRootLayer == 0) { + mLayersHaveAnimations = false; + } + if (mEvaluateThread != null) { + mEvaluateThread.cancel(); + mEvaluateThread = null; + } + if (nativeLayersHaveAnimations(mRootLayer)) { + mLayersHaveAnimations = true; + mEvaluateThread = new EvaluateLayersAnimations(); + mEvaluateThread.start(); + } + invalidate(); + break; + } case REQUEST_FORM_DATA: AutoCompleteAdapter adapter = (AutoCompleteAdapter) msg.obj; if (mWebTextView.isSameTextField(msg.arg1)) { mWebTextView.setAdapterCustom(adapter); } break; - case UPDATE_CLIPBOARD: - String str = (String) msg.obj; - if (DebugFlags.WEB_VIEW) { - Log.v(LOGTAG, "UPDATE_CLIPBOARD " + str); - } - try { - IClipboard clip = IClipboard.Stub.asInterface( - ServiceManager.getService("clipboard")); - clip.setClipboardText(str); - } catch (android.os.RemoteException e) { - Log.e(LOGTAG, "Clipboard failed", e); - } - break; case RESUME_WEBCORE_UPDATE: WebViewCore.resumeUpdate(mWebViewCore); break; @@ -5321,10 +5714,18 @@ public class WebView extends AbsoluteLayout // dont override if mPreventDrag has been set to no due // to time out if (mPreventDrag == PREVENT_DRAG_MAYBE_YES) { - mPreventDrag = msg.arg2 == 1 ? PREVENT_DRAG_YES + mPreventDrag = (msg.arg2 & TOUCH_PREVENT_DRAG) + == TOUCH_PREVENT_DRAG ? PREVENT_DRAG_YES : PREVENT_DRAG_NO; if (mPreventDrag == PREVENT_DRAG_YES) { mTouchMode = TOUCH_DONE_MODE; + } else { + mPreventLongPress = + (msg.arg2 & TOUCH_PREVENT_LONGPRESS) + == TOUCH_PREVENT_LONGPRESS; + mPreventDoubleTap = + (msg.arg2 & TOUCH_PREVENT_DOUBLETAP) + == TOUCH_PREVENT_DOUBLETAP; } } } @@ -5334,45 +5735,108 @@ public class WebView extends AbsoluteLayout if (msg.arg1 == 0) { hideSoftKeyboard(); } else { - displaySoftKeyboard(false); + displaySoftKeyboard(1 == msg.arg2); } break; - case SHOW_RECT_MSG_ID: - WebViewCore.ShowRectData data = (WebViewCore.ShowRectData) msg.obj; - int x = mScrollX; - int left = contentToViewDimension(data.mLeft); - int width = contentToViewDimension(data.mWidth); - int maxWidth = contentToViewDimension(data.mContentWidth); - int viewWidth = getViewWidth(); - if (width < viewWidth) { - // center align - x += left + width / 2 - mScrollX - viewWidth / 2; - } else { - x += (int) (left + data.mXPercentInDoc * width - - mScrollX - data.mXPercentInView * viewWidth); + case FIND_AGAIN: + // Ignore if find has been dismissed. + if (mFindIsUp) { + findAll(mLastFind); + } + break; + + case DRAG_HELD_MOTIONLESS: + mHeldMotionless = MOTIONLESS_TRUE; + invalidate(); + // fall through to keep scrollbars awake + + case AWAKEN_SCROLL_BARS: + if (mTouchMode == TOUCH_DRAG_MODE + && mHeldMotionless == MOTIONLESS_TRUE) { + awakenScrollBars(ViewConfiguration + .getScrollDefaultDelay(), false); + mPrivateHandler.sendMessageDelayed(mPrivateHandler + .obtainMessage(AWAKEN_SCROLL_BARS), + ViewConfiguration.getScrollDefaultDelay()); + } + break; + + case DO_MOTION_UP: + doMotionUp(msg.arg1, msg.arg2); + break; + + case SHOW_FULLSCREEN: + WebViewCore.PluginFullScreenData data + = (WebViewCore.PluginFullScreenData) msg.obj; + if (data.mNpp != 0 && data.mView != null) { + if (mFullScreenHolder != null) { + Log.w(LOGTAG, + "Should not have another full screen."); + mFullScreenHolder.dismiss(); + } + mFullScreenHolder = new PluginFullScreenHolder( + WebView.this, data.mNpp); + mFullScreenHolder.setContentView(data.mView); + mFullScreenHolder.setCancelable(false); + mFullScreenHolder.setCanceledOnTouchOutside(false); + mFullScreenHolder.show(); } - // use the passing content width to cap x as the current - // mContentWidth may not be updated yet - x = Math.max(0, - (Math.min(maxWidth, x + viewWidth)) - viewWidth); - int y = mScrollY; - int top = contentToViewDimension(data.mTop); - int height = contentToViewDimension(data.mHeight); - int maxHeight = contentToViewDimension(data.mContentHeight); + // move the matching embedded view fully into the view so + // that touch will be valid instead of rejected due to out + // of the visible bounds + // TODO: do we need to preserve the original position and + // scale so that we can revert it when leaving the full + // screen mode? + int x = contentToViewX(data.mDocX); + int y = contentToViewY(data.mDocY); + int width = contentToViewDimension(data.mDocWidth); + int height = contentToViewDimension(data.mDocHeight); + int viewWidth = getViewWidth(); int viewHeight = getViewHeight(); - if (height < viewHeight) { - // middle align - y += top + height / 2 - mScrollY - viewHeight / 2; - } else { - y += (int) (top + data.mYPercentInDoc * height - - mScrollY - data.mYPercentInView * viewHeight); + int newX = mScrollX; + int newY = mScrollY; + if (x < mScrollX) { + newX = x + (width > viewWidth + ? (width - viewWidth) / 2 : 0); + } else if (x + width > mScrollX + viewWidth) { + newX = x + width - viewWidth - (width > viewWidth + ? (width - viewWidth) / 2 : 0); + } + if (y < mScrollY) { + newY = y + (height > viewHeight + ? (height - viewHeight) / 2 : 0); + } else if (y + height > mScrollY + viewHeight) { + newY = y + height - viewHeight - (height > viewHeight + ? (height - viewHeight) / 2 : 0); + } + scrollTo(newX, newY); + if (width > viewWidth || height > viewHeight) { + mZoomCenterX = viewWidth * .5f; + mZoomCenterY = viewHeight * .5f; + setNewZoomScale(mActualScale + / Math.max((float) width / viewWidth, + (float) height / viewHeight), false); + } + // Now update the bound + mFullScreenHolder.updateBound(contentToViewX(data.mDocX) + - mScrollX, contentToViewY(data.mDocY) - mScrollY, + contentToViewDimension(data.mDocWidth), + contentToViewDimension(data.mDocHeight)); + break; + + case HIDE_FULLSCREEN: + if (mFullScreenHolder != null) { + mFullScreenHolder.dismiss(); + mFullScreenHolder = null; + } + break; + + case DOM_FOCUS_CHANGED: + if (inEditingMode()) { + nativeClearCursor(); + rebuildWebTextView(); } - // use the passing content height to cap y as the current - // mContentHeight may not be updated yet - y = Math.max(0, - (Math.min(maxHeight, y + viewHeight) - viewHeight)); - scrollTo(x, y); break; default: @@ -5398,8 +5862,16 @@ public class WebView extends AbsoluteLayout // Need these to provide stable ids to my ArrayAdapter, // which normally does not have stable ids. (Bug 1250098) private class Container extends Object { + /** + * Possible values for mEnabled. Keep in sync with OptionStatus in + * WebViewCore.cpp + */ + final static int OPTGROUP = -1; + final static int OPTION_DISABLED = 0; + final static int OPTION_ENABLED = 1; + String mString; - boolean mEnabled; + int mEnabled; int mId; public String toString() { @@ -5420,6 +5892,54 @@ public class WebView extends AbsoluteLayout } @Override + public View getView(int position, View convertView, + ViewGroup parent) { + // Always pass in null so that we will get a new CheckedTextView + // Otherwise, an item which was previously used as an <optgroup> + // element (i.e. has no check), could get used as an <option> + // element, which needs a checkbox/radio, but it would not have + // one. + convertView = super.getView(position, null, parent); + Container c = item(position); + if (c != null && Container.OPTION_ENABLED != c.mEnabled) { + // ListView does not draw dividers between disabled and + // enabled elements. Use a LinearLayout to provide dividers + LinearLayout layout = new LinearLayout(mContext); + layout.setOrientation(LinearLayout.VERTICAL); + if (position > 0) { + View dividerTop = new View(mContext); + dividerTop.setBackgroundResource( + android.R.drawable.divider_horizontal_bright); + layout.addView(dividerTop); + } + + if (Container.OPTGROUP == c.mEnabled) { + // Currently select_dialog_multichoice and + // select_dialog_singlechoice are CheckedTextViews. If + // that changes, the class cast will no longer be valid. + Assert.assertTrue( + convertView instanceof CheckedTextView); + ((CheckedTextView) convertView).setCheckMarkDrawable( + null); + } else { + // c.mEnabled == Container.OPTION_DISABLED + // Draw the disabled element in a disabled state. + convertView.setEnabled(false); + } + + layout.addView(convertView); + if (position < getCount() - 1) { + View dividerBottom = new View(mContext); + dividerBottom.setBackgroundResource( + android.R.drawable.divider_horizontal_bright); + layout.addView(dividerBottom); + } + return layout; + } + return convertView; + } + + @Override public boolean hasStableIds() { // AdapterView's onChanged method uses this to determine whether // to restore the old state. Return false so that the old (out @@ -5454,12 +5974,11 @@ public class WebView extends AbsoluteLayout if (item == null) { return false; } - return item.mEnabled; + return Container.OPTION_ENABLED == item.mEnabled; } } - private InvokeListBox(String[] array, - boolean[] enabled, int[] selected) { + private InvokeListBox(String[] array, int[] enabled, int[] selected) { mMultiple = true; mSelectedArray = selected; @@ -5473,8 +5992,7 @@ public class WebView extends AbsoluteLayout } } - private InvokeListBox(String[] array, boolean[] enabled, int - selection) { + private InvokeListBox(String[] array, int[] enabled, int selection) { mSelection = selection; mMultiple = false; @@ -5605,10 +6123,11 @@ public class WebView extends AbsoluteLayout * Request a dropdown menu for a listbox with multiple selection. * * @param array Labels for the listbox. - * @param enabledArray Which positions are enabled. + * @param enabledArray State for each element in the list. See static + * integers in Container class. * @param selectedArray Which positions are initally selected. */ - void requestListBox(String[] array, boolean[]enabledArray, int[] + void requestListBox(String[] array, int[] enabledArray, int[] selectedArray) { mPrivateHandler.post( new InvokeListBox(array, enabledArray, selectedArray)); @@ -5619,15 +6138,22 @@ public class WebView extends AbsoluteLayout * <select> element. * * @param array Labels for the listbox. - * @param enabledArray Which positions are enabled. + * @param enabledArray State for each element in the list. See static + * integers in Container class. * @param selection Which position is initally selected. */ - void requestListBox(String[] array, boolean[]enabledArray, int selection) { + void requestListBox(String[] array, int[] enabledArray, int selection) { mPrivateHandler.post( new InvokeListBox(array, enabledArray, selection)); } // called by JNI + private void sendMoveFocus(int frame, int node) { + mWebViewCore.sendMessage(EventHub.SET_MOVE_FOCUS, + new WebViewCore.CursorData(frame, node, 0, 0)); + } + + // called by JNI private void sendMoveMouse(int frame, int node, int x, int y) { mWebViewCore.sendMessage(EventHub.SET_MOVE_MOUSE, new WebViewCore.CursorData(frame, node, x, y)); @@ -5706,7 +6232,7 @@ public class WebView extends AbsoluteLayout if (mNativeClass == 0) { return false; } - if (ignorePlugin == false && nativePluginEatsNavKey()) { + if (ignorePlugin == false && nativeFocusIsPlugin()) { KeyEvent event = new KeyEvent(time, time, KeyEvent.ACTION_DOWN , keyCode, count, (mShiftIsPressed ? KeyEvent.META_SHIFT_ON : 0) | (false ? KeyEvent.META_ALT_ON : 0) // FIXME @@ -5781,6 +6307,17 @@ public class WebView extends AbsoluteLayout } /** + * Draw the HTML page into the specified canvas. This call ignores any + * view-specific zoom, scroll offset, or other changes. It does not draw + * any view-specific chrome, such as progress or URL bars. + * + * @hide only needs to be accessible to Browser and testing + */ + public void drawPage(Canvas canvas) { + mWebViewCore.drawContentPicture(canvas, 0, false, false); + } + + /** * Update our cache with updatedText. * @param updatedText The new text to put in our cache. */ @@ -5790,15 +6327,17 @@ public class WebView extends AbsoluteLayout nativeUpdateCachedTextfield(updatedText, mTextGeneration); } + private native int nativeCacheHitFramePointer(); + private native Rect nativeCacheHitNodeBounds(); + private native int nativeCacheHitNodePointer(); /* package */ native void nativeClearCursor(); private native void nativeCreate(int ptr); private native int nativeCursorFramePointer(); private native Rect nativeCursorNodeBounds(); - /* package */ native int nativeCursorNodePointer(); + private native int nativeCursorNodePointer(); /* package */ native boolean nativeCursorMatchesFocus(); private native boolean nativeCursorIntersects(Rect visibleRect); private native boolean nativeCursorIsAnchor(); - private native boolean nativeCursorIsPlugin(); private native boolean nativeCursorIsTextInput(); private native Point nativeCursorPosition(); private native String nativeCursorText(); @@ -5810,26 +6349,39 @@ public class WebView extends AbsoluteLayout private native void nativeDebugDump(); private native void nativeDestroy(); private native void nativeDrawCursorRing(Canvas content); + private native void nativeDestroyLayer(int layer); + private native int nativeEvaluateLayersAnimations(int layer); + private native boolean nativeLayersHaveAnimations(int layer); + private native void nativeUpdateLayers(int layer, int updates); + private native void nativeDrawLayers(int layer, + float scrollX, float scrollY, + float scale, Canvas canvas); private native void nativeDrawMatches(Canvas canvas); - private native void nativeDrawSelection(Canvas content, float scale, - int offset, int x, int y, boolean extendSelection); + private native void nativeDrawSelectionPointer(Canvas content, + float scale, int x, int y, boolean extendSelection); private native void nativeDrawSelectionRegion(Canvas content); private native void nativeDumpDisplayTree(String urlOrNull); private native int nativeFindAll(String findLower, String findUpper); private native void nativeFindNext(boolean forward); + /* package */ native int nativeFocusCandidateFramePointer(); private native boolean nativeFocusCandidateIsPassword(); private native boolean nativeFocusCandidateIsRtlText(); - private native boolean nativeFocusCandidateIsTextField(); private native boolean nativeFocusCandidateIsTextInput(); - private native int nativeFocusCandidateMaxLength(); + /* package */ native int nativeFocusCandidateMaxLength(); /* package */ native String nativeFocusCandidateName(); private native Rect nativeFocusCandidateNodeBounds(); - /* package */ native int nativeFocusCandidatePointer(); + private native int nativeFocusCandidatePointer(); private native String nativeFocusCandidateText(); private native int nativeFocusCandidateTextSize(); + /** + * Returns an integer corresponding to WebView.cpp::type. + * See WebTextView.setType() + */ + private native int nativeFocusCandidateType(); + private native boolean nativeFocusIsPlugin(); /* package */ native int nativeFocusNodePointer(); private native Rect nativeGetCursorRingBounds(); - private native Region nativeGetSelection(); + private native String nativeGetSelection(); private native boolean nativeHasCursorNode(); private native boolean nativeHasFocusNode(); private native void nativeHideCursor(); @@ -5844,29 +6396,22 @@ public class WebView extends AbsoluteLayout private native int nativeMoveGeneration(); private native void nativeMoveSelection(int x, int y, boolean extendSelection); - private native boolean nativePluginEatsNavKey(); + private native boolean nativePointInNavCache(int x, int y, int slop); // Like many other of our native methods, you must make sure that // mNativeClass is not null before calling this method. private native void nativeRecordButtons(boolean focused, boolean pressed, boolean invalidate); private native void nativeSelectBestAt(Rect rect); - private native void nativeSetFindIsDown(); + private native void nativeSetFindIsUp(); private native void nativeSetFollowedLink(boolean followed); private native void nativeSetHeightCanMeasure(boolean measure); // Returns a value corresponding to CachedFrame::ImeAction /* package */ native int nativeTextFieldAction(); - /** - * Perform a click on a currently focused text input. Since it is already - * focused, there is no need to go through the nativeMotionUp code, which - * may change the Cursor. - */ - private native void nativeTextInputMotionUp(int x, int y); private native int nativeTextGeneration(); // Never call this version except by updateCachedTextfield(String) - // we always want to pass in our generation number. private native void nativeUpdateCachedTextfield(String updatedText, int generation); - private native void nativeUpdatePluginReceivesEvents(); // return NO_LEFTEDGE means failure. private static final int NO_LEFTEDGE = -1; private native int nativeGetBlockLeftEdge(int x, int y, float scale); diff --git a/core/java/android/webkit/WebViewClient.java b/core/java/android/webkit/WebViewClient.java index 30dea74..02c7210 100644 --- a/core/java/android/webkit/WebViewClient.java +++ b/core/java/android/webkit/WebViewClient.java @@ -86,6 +86,8 @@ public class WebViewClient { * @param view The WebView that is initiating the callback. * @param cancelMsg The message to send if the host wants to cancel * @param continueMsg The message to send if the host wants to continue + * @deprecated This method is no longer called. When the WebView encounters + * a redirect loop, it will cancel the load. */ public void onTooManyRedirects(WebView view, Message cancelMsg, Message continueMsg) { @@ -173,8 +175,6 @@ public class WebViewClient { * @param handler An SslErrorHandler object that will handle the user's * response. * @param error The SSL error object. - * @hide - hide this because it contains a parameter of type SslError, - * which is located in a hidden package. */ public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) { diff --git a/core/java/android/webkit/WebViewCore.java b/core/java/android/webkit/WebViewCore.java index c3817fb..949b318 100644 --- a/core/java/android/webkit/WebViewCore.java +++ b/core/java/android/webkit/WebViewCore.java @@ -18,6 +18,8 @@ package android.webkit; import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager.NameNotFoundException; +import android.database.Cursor; import android.graphics.Canvas; import android.graphics.DrawFilter; import android.graphics.Paint; @@ -26,11 +28,13 @@ import android.graphics.Picture; import android.graphics.Point; import android.graphics.Rect; import android.graphics.Region; +import android.net.Uri; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.Process; import android.provider.Browser; +import android.provider.OpenableColumns; import android.util.Log; import android.util.SparseBooleanArray; import android.view.KeyEvent; @@ -120,10 +124,6 @@ final class WebViewCore { private int mWebkitScrollX = 0; private int mWebkitScrollY = 0; - // If the site doesn't use viewport meta tag to specify the viewport, use - // DEFAULT_VIEWPORT_WIDTH as default viewport width - static final int DEFAULT_VIEWPORT_WIDTH = 800; - // The thread name used to identify the WebCore thread and for use in // debugging other classes that require operation within the WebCore thread. /* package */ static final String THREAD_NAME = "WebViewCoreThread"; @@ -273,6 +273,39 @@ final class WebViewCore { mCallbackProxy.onJsAlert(url, message); } + + /** + * Called by JNI. Open a file chooser to upload a file. + * @return String version of the URI plus the name of the file. + * FIXME: Just return the URI here, and in FileSystem::pathGetFileName, call + * into Java to get the filename. + */ + private String openFileChooser() { + Uri uri = mCallbackProxy.openFileChooser(); + if (uri == null) return ""; + // Find out the name, and append it to the URI. + // Webkit will treat the name as the filename, and + // the URI as the path. The URI will be used + // in BrowserFrame to get the actual data. + Cursor cursor = mContext.getContentResolver().query( + uri, + new String[] { OpenableColumns.DISPLAY_NAME }, + null, + null, + null); + String name = ""; + if (cursor != null) { + try { + if (cursor.moveToNext()) { + name = cursor.getString(0); + } + } finally { + cursor.close(); + } + } + return uri.toString() + "/" + name; + } + /** * Notify the browser that the origin has exceeded it's database quota. * @param url The URL that caused the overflow. @@ -422,6 +455,8 @@ final class WebViewCore { */ private native boolean nativeRecordContent(Region invalRegion, Point wh); + private native boolean nativeFocusBoundsChanged(); + /** * Splits slow parts of the picture set. Called from the webkit * thread after nativeDrawContent returns true. @@ -447,8 +482,8 @@ final class WebViewCore { should this be called nativeSetViewPortSize? */ private native void nativeSetSize(int width, int height, int screenWidth, - float scale, int realScreenWidth, int screenHeight, int anchorX, - int anchorY, boolean ignoreHeight); + float scale, int realScreenWidth, int screenHeight, + boolean ignoreHeight); private native int nativeGetContentMinPrefWidth(); @@ -465,17 +500,19 @@ final class WebViewCore { private native void nativeSaveDocumentState(int frame); + private native void nativeMoveFocus(int framePtr, int nodePointer); private native void nativeMoveMouse(int framePtr, int x, int y); private native void nativeMoveMouseIfLatest(int moveGeneration, int framePtr, int x, int y); private native String nativeRetrieveHref(int framePtr, int nodePtr); + private native String nativeRetrieveAnchorText(int framePtr, int nodePtr); private native void nativeTouchUp(int touchGeneration, int framePtr, int nodePtr, int x, int y); - private native boolean nativeHandleTouchEvent(int action, int x, int y); + private native int nativeHandleTouchEvent(int action, int x, int y); private native void nativeUpdateFrameCache(); @@ -508,8 +545,6 @@ final class WebViewCore { */ private native void nativeSetSelection(int start, int end); - private native String nativeGetSelection(Region sel); - // Register a scheme to be treated as local scheme so that it can access // local asset files for resources private native void nativeRegisterURLSchemeAsLocal(String scheme); @@ -522,8 +557,6 @@ final class WebViewCore { */ private native void nativeSetNewStorageLimit(long limit); - private native void nativeUpdatePluginState(int framePtr, int nodePtr, int state); - /** * Provide WebCore with a Geolocation permission state for the specified * origin. @@ -624,11 +657,13 @@ final class WebViewCore { CursorData() {} CursorData(int frame, int node, int x, int y) { mFrame = frame; + mNode = node; mX = x; mY = y; } int mMoveGeneration; int mFrame; + int mNode; int mX; int mY; } @@ -643,6 +678,14 @@ final class WebViewCore { KeyEvent mEvent; } + static class MotionUpData { + int mFrame; + int mNode; + Rect mBounds; + int mX; + int mY; + } + static class PostUrlData { String mUrl; byte[] mPostData; @@ -672,25 +715,34 @@ final class WebViewCore { int mY; } + // mAction of TouchEventData can be MotionEvent.getAction() which uses the + // last two bytes or one of the following values + static final int ACTION_LONGPRESS = 0x100; + static final int ACTION_DOUBLETAP = 0x200; + static class TouchEventData { - int mAction; // MotionEvent.getAction() + int mAction; int mX; int mY; } - static class PluginStateData { - int mFrame; - int mNode; - int mState; - } - static class GeolocationPermissionsData { String mOrigin; boolean mAllow; boolean mRemember; } + static class PluginFullScreenData { + View mView; + int mNpp; + int mDocX; + int mDocY; + int mDocWidth; + int mDocHeight; + } + static final String[] HandlerDebugString = { + "REQUEST_LABEL", // 97 "UPDATE_FRAME_CACHE_IF_LOADING", // = 98 "SCROLL_TEXT_INPUT", // = 99 "LOAD_URL", // = 100; @@ -720,9 +772,9 @@ final class WebViewCore { "SINGLE_LISTBOX_CHOICE", // = 124; "MESSAGE_RELAY", // = 125; "SET_BACKGROUND_COLOR", // = 126; - "PLUGIN_STATE", // = 127; + "SET_MOVE_FOCUS", // = 127 "SAVE_DOCUMENT_STATE", // = 128; - "GET_SELECTION", // = 129; + "129", // = 129; "WEBKIT_DRAW", // = 130; "SYNC_SCROLL", // = 131; "POST_URL", // = 132; @@ -739,10 +791,12 @@ final class WebViewCore { "ON_PAUSE", // = 143 "ON_RESUME", // = 144 "FREE_MEMORY", // = 145 + "VALID_NODE_BOUNDS", // = 146 }; class EventHub { // Message Ids + static final int REQUEST_LABEL = 97; static final int UPDATE_FRAME_CACHE_IF_LOADING = 98; static final int SCROLL_TEXT_INPUT = 99; static final int LOAD_URL = 100; @@ -771,9 +825,9 @@ final class WebViewCore { static final int SINGLE_LISTBOX_CHOICE = 124; static final int MESSAGE_RELAY = 125; static final int SET_BACKGROUND_COLOR = 126; - static final int PLUGIN_STATE = 127; // plugin notifications + static final int SET_MOVE_FOCUS = 127; static final int SAVE_DOCUMENT_STATE = 128; - static final int GET_SELECTION = 129; + static final int WEBKIT_DRAW = 130; static final int SYNC_SCROLL = 131; static final int POST_URL = 132; @@ -802,6 +856,7 @@ final class WebViewCore { static final int ON_PAUSE = 143; static final int ON_RESUME = 144; static final int FREE_MEMORY = 145; + static final int VALID_NODE_BOUNDS = 146; // Network-based messaging static final int CLEAR_SSL_PREF_TABLE = 150; @@ -821,6 +876,8 @@ final class WebViewCore { static final int POPULATE_VISITED_LINKS = 181; + static final int HIDE_FULLSCREEN = 182; + // private message ids private static final int DESTROY = 200; @@ -852,11 +909,11 @@ final class WebViewCore { @Override public void handleMessage(Message msg) { if (DebugFlags.WEB_VIEW_CORE) { - Log.v(LOGTAG, (msg.what < UPDATE_FRAME_CACHE_IF_LOADING + Log.v(LOGTAG, (msg.what < REQUEST_LABEL || msg.what - > FREE_MEMORY ? Integer.toString(msg.what) + > VALID_NODE_BOUNDS ? Integer.toString(msg.what) : HandlerDebugString[msg.what - - UPDATE_FRAME_CACHE_IF_LOADING]) + - REQUEST_LABEL]) + " arg1=" + msg.arg1 + " arg2=" + msg.arg2 + " obj=" + msg.obj); } @@ -877,6 +934,19 @@ final class WebViewCore { } break; + case REQUEST_LABEL: + if (mWebView != null) { + int nodePointer = msg.arg2; + String label = nativeRequestLabel(msg.arg1, + nodePointer); + if (label != null && label.length() > 0) { + Message.obtain(mWebView.mPrivateHandler, + WebView.RETURN_LABEL, nodePointer, + 0, label).sendToTarget(); + } + } + break; + case UPDATE_FRAME_CACHE_IF_LOADING: nativeUpdateFrameCacheIfLoading(); break; @@ -961,7 +1031,6 @@ final class WebViewCore { (WebView.ViewSizeData) msg.obj; viewSizeChanged(data.mWidth, data.mHeight, data.mTextWrapWidth, data.mScale, - data.mAnchorX, data.mAnchorY, data.mIgnoreHeight); break; } @@ -1032,11 +1101,6 @@ final class WebViewCore { nativeFreeMemory(); break; - case PLUGIN_STATE: - PluginStateData psd = (PluginStateData) msg.obj; - nativeUpdatePluginState(psd.mFrame, psd.mNode, psd.mState); - break; - case SET_NETWORK_STATE: if (BrowserFrame.sJavaBridge == null) { throw new IllegalStateException("No WebView " + @@ -1102,7 +1166,7 @@ final class WebViewCore { mWebView.mPrivateHandler, WebView.PREVENT_TOUCH_ID, ted.mAction, nativeHandleTouchEvent(ted.mAction, ted.mX, - ted.mY) ? 1 : 0).sendToTarget(); + ted.mY)).sendToTarget(); break; } @@ -1125,6 +1189,11 @@ final class WebViewCore { mBrowserFrame.documentAsText((Message) msg.obj); break; + case SET_MOVE_FOCUS: + CursorData focusData = (CursorData) msg.obj; + nativeMoveFocus(focusData.mFrame, focusData.mNode); + break; + case SET_MOVE_MOUSE: CursorData cursorData = (CursorData) msg.obj; nativeMoveMouse(cursorData.mFrame, @@ -1140,8 +1209,10 @@ final class WebViewCore { case REQUEST_CURSOR_HREF: { Message hrefMsg = (Message) msg.obj; - String res = nativeRetrieveHref(msg.arg1, msg.arg2); - hrefMsg.getData().putString("url", res); + hrefMsg.getData().putString("url", + nativeRetrieveHref(msg.arg1, msg.arg2)); + hrefMsg.getData().putString("title", + nativeRetrieveAnchorText(msg.arg1, msg.arg2)); hrefMsg.sendToTarget(); break; } @@ -1193,13 +1264,6 @@ final class WebViewCore { nativeSetBackgroundColor(msg.arg1); break; - case GET_SELECTION: - String str = nativeGetSelection((Region) msg.obj); - Message.obtain(mWebView.mPrivateHandler - , WebView.UPDATE_CLIPBOARD, str) - .sendToTarget(); - break; - case DUMP_DOMTREE: nativeDumpDomTree(msg.arg1 == 1); break; @@ -1249,6 +1313,25 @@ final class WebViewCore { case POPULATE_VISITED_LINKS: nativeProvideVisitedHistory((String[])msg.obj); break; + + case VALID_NODE_BOUNDS: { + MotionUpData motionUpData = (MotionUpData) msg.obj; + if (!nativeValidNodeAndBounds( + motionUpData.mFrame, motionUpData.mNode, + motionUpData.mBounds)) { + nativeUpdateFrameCache(); + } + Message message = mWebView.mPrivateHandler + .obtainMessage(WebView.DO_MOTION_UP, + motionUpData.mX, motionUpData.mY); + mWebView.mPrivateHandler.sendMessageAtFrontOfQueue( + message); + break; + } + + case HIDE_FULLSCREEN: + nativeFullScreenPluginHidden(msg.arg1); + break; } } }; @@ -1392,6 +1475,11 @@ final class WebViewCore { mEventHub.sendMessage(Message.obtain(null, what, arg1, arg2, obj)); } + void sendMessageAtFrontOfQueue(int what, Object obj) { + mEventHub.sendMessageAtFrontOfQueue(Message.obtain( + null, what, obj)); + } + void sendMessageDelayed(int what, Object obj, long delay) { mEventHub.sendMessageDelayed(Message.obtain(null, what, obj), delay); } @@ -1484,7 +1572,7 @@ final class WebViewCore { // notify webkit that our virtual view size changed size (after inv-zoom) private void viewSizeChanged(int w, int h, int textwrapWidth, float scale, - int anchorX, int anchorY, boolean ignoreHeight) { + boolean ignoreHeight) { if (DebugFlags.WEB_VIEW_CORE) { Log.v(LOGTAG, "viewSizeChanged w=" + w + "; h=" + h + "; textwrapWidth=" + textwrapWidth + "; scale=" + scale); @@ -1498,7 +1586,7 @@ final class WebViewCore { if (mViewportWidth == -1) { if (mSettings.getLayoutAlgorithm() == WebSettings.LayoutAlgorithm.NORMAL) { - width = DEFAULT_VIEWPORT_WIDTH; + width = WebView.DEFAULT_VIEWPORT_WIDTH; } else { /* * if a page's minimum preferred width is wider than the @@ -1512,17 +1600,16 @@ final class WebViewCore { * In the worse case, the native width will be adjusted when * next zoom or screen orientation change happens. */ - width = Math.max(w, Math.max(DEFAULT_VIEWPORT_WIDTH, - nativeGetContentMinPrefWidth())); + width = Math.min(WebView.sMaxViewportWidth, Math.max(w, + Math.max(WebView.DEFAULT_VIEWPORT_WIDTH, + nativeGetContentMinPrefWidth()))); } - } else if (mViewportWidth > 0) { - width = Math.max(w, mViewportWidth); } else { - width = Math.max(w, textwrapWidth); + width = Math.max(w, mViewportWidth); } } nativeSetSize(width, width == w ? h : Math.round((float) width * h / w), - textwrapWidth, scale, w, h, anchorX, anchorY, ignoreHeight); + textwrapWidth, scale, w, h, ignoreHeight); // Remember the current width and height boolean needInvalidate = (mCurrentViewWidth == 0); mCurrentViewWidth = w; @@ -1567,9 +1654,6 @@ final class WebViewCore { // Used to avoid posting more than one split picture message. private boolean mSplitPictureIsScheduled; - // Used to suspend drawing. - private boolean mDrawIsPaused; - // mRestoreState is set in didFirstLayout(), and reset in the next // webkitDraw after passing it to the UI thread. private RestoreState mRestoreState = null; @@ -1596,6 +1680,7 @@ final class WebViewCore { int mMinPrefWidth; RestoreState mRestoreState; // only non-null if it is for the first // picture set after the first layout + boolean mFocusSizeChanged; } private void webkitDraw() { @@ -1610,10 +1695,11 @@ final class WebViewCore { if (mWebView != null) { // Send the native view size that was used during the most recent // layout. + draw.mFocusSizeChanged = nativeFocusBoundsChanged(); draw.mViewPoint = new Point(mCurrentViewWidth, mCurrentViewHeight); if (mSettings.getUseWideViewPort()) { draw.mMinPrefWidth = Math.max( - mViewportWidth == -1 ? DEFAULT_VIEWPORT_WIDTH + mViewportWidth == -1 ? WebView.DEFAULT_VIEWPORT_WIDTH : (mViewportWidth == 0 ? mCurrentViewWidth : mViewportWidth), nativeGetContentMinPrefWidth()); @@ -1646,6 +1732,9 @@ final class WebViewCore { final DrawFilter mZoomFilter = new PaintFlagsDrawFilter(ZOOM_BITS, Paint.LINEAR_TEXT_FLAG); + final DrawFilter mScrollFilter = null; + // If we need to trade more speed for less quality on slower devices + // use this: new PaintFlagsDrawFilter(SCROLL_BITS, 0); /* package */ void drawContentPicture(Canvas canvas, int color, boolean animatingZoom, @@ -1654,7 +1743,7 @@ final class WebViewCore { if (animatingZoom) { df = mZoomFilter; } else if (animatingScroll) { - df = null; + df = mScrollFilter; } canvas.setDrawFilter(df); boolean tookTooLong = nativeDrawContent(canvas, color); @@ -1683,17 +1772,6 @@ final class WebViewCore { sWebCoreHandler.removeMessages(WebCoreThread.RESUME_PRIORITY); sWebCoreHandler.sendMessageAtFrontOfQueue(sWebCoreHandler .obtainMessage(WebCoreThread.REDUCE_PRIORITY)); - // Note: there is one possible failure mode. If pauseUpdate() is called - // from UI thread while in webcore thread WEBKIT_DRAW is just pulled out - // of the queue and about to be executed. mDrawIsScheduled may be set to - // false in webkitDraw(). So update won't be blocked. But at least the - // webcore thread priority is still lowered. - if (core != null) { - synchronized (core) { - core.mDrawIsPaused = true; - core.mEventHub.removeMessages(EventHub.WEBKIT_DRAW); - } - } } static void resumeUpdate(WebViewCore core) { @@ -1702,14 +1780,6 @@ final class WebViewCore { sWebCoreHandler.removeMessages(WebCoreThread.RESUME_PRIORITY); sWebCoreHandler.sendMessageAtFrontOfQueue(sWebCoreHandler .obtainMessage(WebCoreThread.RESUME_PRIORITY)); - if (core != null) { - synchronized (core) { - core.mDrawIsScheduled = false; - core.mDrawIsPaused = false; - if (DebugFlags.WEB_VIEW_CORE) Log.v(LOGTAG, "resumeUpdate"); - core.contentDraw(); - } - } } static void startCacheTransaction() { @@ -1748,9 +1818,7 @@ final class WebViewCore { } // only fire an event if this is our first request synchronized (this) { - if (mDrawIsPaused || mDrawIsScheduled) { - return; - } + if (mDrawIsScheduled) return; mDrawIsScheduled = true; mEventHub.sendMessage(Message.obtain(null, EventHub.WEBKIT_DRAW)); } @@ -1845,6 +1913,33 @@ final class WebViewCore { } } + private static boolean mRepaintScheduled = false; + + /* + * Called by the WebView thread + */ + /* package */ void signalRepaintDone() { + mRepaintScheduled = false; + } + + // called by JNI + private void sendImmediateRepaint() { + if (mWebView != null && !mRepaintScheduled) { + mRepaintScheduled = true; + Message.obtain(mWebView.mPrivateHandler, + WebView.IMMEDIATE_REPAINT_MSG_ID).sendToTarget(); + } + } + + // called by JNI + private void setRootLayer(int layer) { + if (mWebView != null) { + Message.obtain(mWebView.mPrivateHandler, + WebView.SET_ROOT_LAYER_MSG_ID, + layer, 0).sendToTarget(); + } + } + /* package */ WebView getWebView() { return mWebView; } @@ -1861,7 +1956,14 @@ final class WebViewCore { if (mWebView == null) return; - setupViewport(standardLoad || mRestoredScale > 0); + boolean updateRestoreState = standardLoad || mRestoredScale > 0; + setupViewport(updateRestoreState); + // if updateRestoreState is true, ViewManager.postReadyToDrawAll() will + // be called after the WebView restore the state. If updateRestoreState + // is false, start to draw now as it is ready. + if (!updateRestoreState) { + mWebView.mViewManager.postReadyToDrawAll(); + } // reset the scroll position, the restored offset and scales mWebkitScrollX = mWebkitScrollY = mRestoredX = mRestoredY @@ -1956,12 +2058,14 @@ final class WebViewCore { mRestoreState.mScrollY = mRestoredY; mRestoreState.mMobileSite = (0 == mViewportWidth); if (mRestoredScale > 0) { - mRestoreState.mViewScale = mRestoredScale / 100.0f; if (mRestoredScreenWidthScale > 0) { mRestoreState.mTextWrapScale = mRestoredScreenWidthScale / 100.0f; + // 0 will trigger WebView to turn on zoom overview mode + mRestoreState.mViewScale = 0; } else { - mRestoreState.mTextWrapScale = mRestoreState.mViewScale; + mRestoreState.mViewScale = mRestoreState.mTextWrapScale = + mRestoredScale / 100.0f; } } else { if (mViewportInitialScale > 0) { @@ -1994,7 +2098,6 @@ final class WebViewCore { data.mTextWrapWidth = data.mWidth; data.mScale = -1.0f; data.mIgnoreHeight = false; - data.mAnchorX = data.mAnchorY = 0; // send VIEW_SIZE_CHANGED to the front of the queue so that we can // avoid pushing the wrong picture to the WebView side. If there is // a VIEW_SIZE_CHANGED in the queue, probably from WebView side, @@ -2015,15 +2118,21 @@ final class WebViewCore { // know the exact scale. If mRestoredScale is non-zero, use it; // otherwise just use mTextWrapScale as the initial scale. data.mScale = mRestoreState.mViewScale == 0 - ? (mRestoredScale > 0 ? mRestoredScale + ? (mRestoredScale > 0 ? mRestoredScale / 100.0f : mRestoreState.mTextWrapScale) : mRestoreState.mViewScale; + if (DebugFlags.WEB_VIEW_CORE) { + Log.v(LOGTAG, "setupViewport" + + " mRestoredScale=" + mRestoredScale + + " mViewScale=" + mRestoreState.mViewScale + + " mTextWrapScale=" + mRestoreState.mTextWrapScale + ); + } data.mWidth = Math.round(webViewWidth / data.mScale); data.mHeight = mCurrentViewHeight * data.mWidth / viewportWidth; data.mTextWrapWidth = Math.round(webViewWidth / mRestoreState.mTextWrapScale); data.mIgnoreHeight = false; - data.mAnchorX = data.mAnchorY = 0; // send VIEW_SIZE_CHANGED to the front of the queue so that we // can avoid pushing the wrong picture to the WebView side. mEventHub.removeMessages(EventHub.VIEW_SIZE_CHANGED); @@ -2089,8 +2198,15 @@ final class WebViewCore { WebView.CLEAR_TEXT_ENTRY).sendToTarget(); } - private native void nativeUpdateFrameCacheIfLoading(); + // called by JNI + private void sendFindAgain() { + if (mWebView == null) return; + Message.obtain(mWebView.mPrivateHandler, + WebView.FIND_AGAIN).sendToTarget(); + } + private native void nativeUpdateFrameCacheIfLoading(); + private native String nativeRequestLabel(int framePtr, int nodePtr); /** * Scroll the focused textfield to (xPercent, y) in document space */ @@ -2102,7 +2218,7 @@ final class WebViewCore { private native void nativeSetGlobalBounds(int x, int y, int w, int h); // called by JNI - private void requestListBox(String[] array, boolean[] enabledArray, + private void requestListBox(String[] array, int[] enabledArray, int[] selectedArray) { if (mWebView != null) { mWebView.requestListBox(array, enabledArray, selectedArray); @@ -2110,7 +2226,7 @@ final class WebViewCore { } // called by JNI - private void requestListBox(String[] array, boolean[] enabledArray, + private void requestListBox(String[] array, int[] enabledArray, int selection) { if (mWebView != null) { mWebView.requestListBox(array, enabledArray, selection); @@ -2119,102 +2235,128 @@ final class WebViewCore { } // called by JNI - private void requestKeyboard(boolean showKeyboard) { + private void requestKeyboard(boolean showKeyboard, boolean isTextView) { if (mWebView != null) { Message.obtain(mWebView.mPrivateHandler, - WebView.REQUEST_KEYBOARD, showKeyboard ? 1 : 0, 0) + WebView.REQUEST_KEYBOARD, showKeyboard ? 1 : 0, + isTextView ? 1 : 0) .sendToTarget(); } } - // called by JNI. PluginWidget function to launch an activity and overlays - // the activity with the View provided by the plugin class. - private void startFullScreenPluginActivity(String libName, String clsName, int npp) { + // called by JNI + private Context getContext() { + return mContext; + } + + // called by JNI + private Class<?> getPluginClass(String libName, String clsName) { + if (mWebView == null) { - return; + return null; } + + PluginManager pluginManager = PluginManager.getInstance(null); - String pkgName = PluginManager.getInstance(null).getPluginsAPKName(libName); + String pkgName = pluginManager.getPluginsAPKName(libName); if (pkgName == null) { Log.w(LOGTAG, "Unable to resolve " + libName + " to a plugin APK"); + return null; + } + + try { + return pluginManager.getPluginClass(pkgName, clsName); + } catch (NameNotFoundException e) { + Log.e(LOGTAG, "Unable to find plugin classloader for the apk (" + pkgName + ")"); + } catch (ClassNotFoundException e) { + Log.e(LOGTAG, "Unable to find plugin class (" + clsName + + ") in the apk (" + pkgName + ")"); + } + + return null; + } + + // called by JNI. PluginWidget function to launch a full-screen view using a + // View object provided by the plugin class. + private void showFullScreenPlugin(ViewManager.ChildView childView, + final int npp, int x, int y, int width, int height) { + + if (mWebView == null) { return; } - Intent intent = new Intent("android.intent.webkit.PLUGIN"); - intent.putExtra(PluginActivity.INTENT_EXTRA_PACKAGE_NAME, pkgName); - intent.putExtra(PluginActivity.INTENT_EXTRA_CLASS_NAME, clsName); - intent.putExtra(PluginActivity.INTENT_EXTRA_NPP_INSTANCE, npp); - mWebView.getContext().startActivity(intent); + PluginFullScreenData data = new PluginFullScreenData(); + data.mView = childView.mView; + data.mNpp = npp; + data.mDocX = x; + data.mDocY = y; + data.mDocWidth = width; + data.mDocHeight = height; + mWebView.mPrivateHandler.obtainMessage(WebView.SHOW_FULLSCREEN, data) + .sendToTarget(); } - // called by JNI. PluginWidget functions for creating an embedded View for - // the surface drawing model. - private ViewManager.ChildView createSurface(String libName, String clsName, - int npp, int x, int y, int width, int height) { + // called by JNI + private void hideFullScreenPlugin() { if (mWebView == null) { - return null; + return; } + mWebView.mPrivateHandler.obtainMessage(WebView.HIDE_FULLSCREEN) + .sendToTarget(); + } - String pkgName = PluginManager.getInstance(null).getPluginsAPKName(libName); - if (pkgName == null) { - Log.w(LOGTAG, "Unable to resolve " + libName + " to a plugin APK"); + // called by JNI + private void updateFullScreenPlugin(int x, int y, int width, int height) { + if (mWebView == null) { + return; + } + + PluginFullScreenData data = new PluginFullScreenData(); + data.mDocX = x; + data.mDocY = y; + data.mDocWidth = width; + data.mDocHeight = height; + // null mView and mNpp to indicate it is an update + mWebView.mPrivateHandler.obtainMessage(WebView.SHOW_FULLSCREEN, data) + .sendToTarget(); + } + + // called by JNI. PluginWidget functions for creating an embedded View for + // the surface drawing model. + private ViewManager.ChildView addSurface(View pluginView, int x, int y, + int width, int height) { + if (mWebView == null) { return null; } - PluginStub stub =PluginUtil.getPluginStub(mWebView.getContext(),pkgName, clsName); - if (stub == null) { - Log.e(LOGTAG, "Unable to find plugin class (" + clsName + - ") in the apk (" + pkgName + ")"); + if (pluginView == null) { + Log.e(LOGTAG, "Attempted to add an empty plugin view to the view hierarchy"); return null; } - View pluginView = stub.getEmbeddedView(npp, mWebView.getContext()); + // ensures the view system knows the view can redraw itself + pluginView.setWillNotDraw(false); ViewManager.ChildView view = mWebView.mViewManager.createView(); view.mView = pluginView; view.attachView(x, y, width, height); return view; } - - private void destroySurface(ViewManager.ChildView childView) { - childView.removeView(); + + private void updateSurface(ViewManager.ChildView childView, int x, int y, + int width, int height) { + childView.attachView(x, y, width, height); } - // called by JNI - static class ShowRectData { - int mLeft; - int mTop; - int mWidth; - int mHeight; - int mContentWidth; - int mContentHeight; - float mXPercentInDoc; - float mXPercentInView; - float mYPercentInDoc; - float mYPercentInView; - } - - private void showRect(int left, int top, int width, int height, - int contentWidth, int contentHeight, float xPercentInDoc, - float xPercentInView, float yPercentInDoc, float yPercentInView) { - if (mWebView != null) { - ShowRectData data = new ShowRectData(); - data.mLeft = left; - data.mTop = top; - data.mWidth = width; - data.mHeight = height; - data.mContentWidth = contentWidth; - data.mContentHeight = contentHeight; - data.mXPercentInDoc = xPercentInDoc; - data.mXPercentInView = xPercentInView; - data.mYPercentInDoc = yPercentInDoc; - data.mYPercentInView = yPercentInView; - Message.obtain(mWebView.mPrivateHandler, WebView.SHOW_RECT_MSG_ID, - data).sendToTarget(); - } + private void destroySurface(ViewManager.ChildView childView) { + childView.removeView(); } private native void nativePause(); private native void nativeResume(); private native void nativeFreeMemory(); + private native void nativeFullScreenPluginHidden(int npp); + private native boolean nativeValidNodeAndBounds(int frame, int node, + Rect bounds); + } diff --git a/core/java/android/webkit/WebViewDatabase.java b/core/java/android/webkit/WebViewDatabase.java index 6e10811..110e4f8 100644 --- a/core/java/android/webkit/WebViewDatabase.java +++ b/core/java/android/webkit/WebViewDatabase.java @@ -27,6 +27,7 @@ import android.content.Context; import android.database.Cursor; import android.database.DatabaseUtils; import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteStatement; import android.util.Log; import android.webkit.CookieManager.Cookie; @@ -174,7 +175,16 @@ public class WebViewDatabase { public static synchronized WebViewDatabase getInstance(Context context) { if (mInstance == null) { mInstance = new WebViewDatabase(); - mDatabase = context.openOrCreateDatabase(DATABASE_FILE, 0, null); + try { + mDatabase = context + .openOrCreateDatabase(DATABASE_FILE, 0, null); + } catch (SQLiteException e) { + // try again by deleting the old db and create a new one + if (context.deleteDatabase(DATABASE_FILE)) { + mDatabase = context.openOrCreateDatabase(DATABASE_FILE, 0, + null); + } + } // mDatabase should not be null, // the only case is RequestAPI test has problem to create db @@ -194,8 +204,16 @@ public class WebViewDatabase { mDatabase.setLockingEnabled(false); } - mCacheDatabase = context.openOrCreateDatabase(CACHE_DATABASE_FILE, - 0, null); + try { + mCacheDatabase = context.openOrCreateDatabase( + CACHE_DATABASE_FILE, 0, null); + } catch (SQLiteException e) { + // try again by deleting the old db and create a new one + if (context.deleteDatabase(CACHE_DATABASE_FILE)) { + mCacheDatabase = context.openOrCreateDatabase( + CACHE_DATABASE_FILE, 0, null); + } + } // mCacheDatabase should not be null, // the only case is RequestAPI test has problem to create db diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java index 271989a..e241c77 100644 --- a/core/java/android/widget/AbsListView.java +++ b/core/java/android/widget/AbsListView.java @@ -127,11 +127,6 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te static final int TOUCH_MODE_FLING = 4; /** - * Indicates that the user is currently dragging the fast scroll thumb - */ - static final int TOUCH_MODE_FAST_SCROLL = 5; - - /** * Regular layout - usually an unsolicited layout from the view system */ static final int LAYOUT_NORMAL = 0; @@ -440,6 +435,8 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te private Runnable mClearScrollingCache; private int mMinimumVelocity; private int mMaximumVelocity; + + final boolean[] mIsScrap = new boolean[1]; /** * Interface definition for a callback to be invoked when the list or grid @@ -1239,9 +1236,13 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te * converting an old view or making a new one. * * @param position The position to display + * @param isScrap Array of at least 1 boolean, the first entry will become true if + * the returned view was taken from the scrap heap, false if otherwise. + * * @return A view displaying the data associated with the specified position */ - View obtainView(int position) { + View obtainView(int position, boolean[] isScrap) { + isScrap[0] = false; View scrapView; scrapView = mRecycler.getScrapView(position); @@ -1269,6 +1270,8 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te ViewDebug.trace(scrapView, ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP, position, -1); } + } else { + isScrap[0] = true; } } else { child = mAdapter.getView(position, null, this); @@ -1543,6 +1546,9 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te // Dismiss the popup in case onSaveInstanceState() was not invoked dismissPopup(); + // Detach any view left in the scrap heap + mRecycler.clear(); + final ViewTreeObserver treeObserver = getViewTreeObserver(); if (treeObserver != null) { treeObserver.removeOnTouchModeChangeListener(this); @@ -1636,6 +1642,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te if (mDataChanged) return; if (mAdapter != null && mItemCount > 0 && + mClickMotionPosition != INVALID_POSITION && mClickMotionPosition < mAdapter.getCount() && sameWindow()) { performItemClick(mChild, mClickMotionPosition, getAdapter().getItemId( mClickMotionPosition)); @@ -2109,6 +2116,9 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING); mFlingRunnable.start(-initialVelocity); + } else { + mTouchMode = TOUCH_MODE_REST; + reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); } } } else { @@ -2966,7 +2976,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te break; case KeyEvent.KEYCODE_SPACE: // Only send spaces once we are filtered - okToSend = mFiltered = true; + okToSend = mFiltered; break; } @@ -3563,6 +3573,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te // into the scrap heap int viewType = lp.viewType; if (!shouldRecycleViewType(viewType)) { + removeDetachedView(scrap, false); return; } @@ -3590,12 +3601,12 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te for (int i = 0; i < count; ++i) { final View victim = activeViews[i]; if (victim != null) { - int whichScrap = ((AbsListView.LayoutParams) - victim.getLayoutParams()).viewType; + int whichScrap = ((AbsListView.LayoutParams) victim.getLayoutParams()).viewType; activeViews[i] = null; if (whichScrap == AdapterView.ITEM_VIEW_TYPE_IGNORE) { + removeDetachedView(victim, false); // Do not move views that should be ignored continue; } diff --git a/core/java/android/widget/AbsSeekBar.java b/core/java/android/widget/AbsSeekBar.java index d25530b..d6dd872 100644 --- a/core/java/android/widget/AbsSeekBar.java +++ b/core/java/android/widget/AbsSeekBar.java @@ -26,7 +26,6 @@ import android.view.KeyEvent; import android.view.MotionEvent; public abstract class AbsSeekBar extends ProgressBar { - private Drawable mThumb; private int mThumbOffset; @@ -66,8 +65,9 @@ public abstract class AbsSeekBar extends ProgressBar { Drawable thumb = a.getDrawable(com.android.internal.R.styleable.SeekBar_thumb); setThumb(thumb); // will guess mThumbOffset if thumb != null... // ...but allow layout to override this - int thumbOffset = - a.getDimensionPixelOffset(com.android.internal.R.styleable.SeekBar_thumbOffset, getThumbOffset()); + int thumbOffset = a.getDimensionPixelOffset( + com.android.internal.R.styleable.SeekBar_thumbOffset, getThumbOffset()); + setThumbOffset(thumbOffset); a.recycle(); a = context.obtainStyledAttributes(attrs, @@ -91,7 +91,7 @@ public abstract class AbsSeekBar extends ProgressBar { // Assuming the thumb drawable is symmetric, set the thumb offset // such that the thumb will hang halfway off either edge of the // progress bar. - mThumbOffset = (int)thumb.getIntrinsicWidth() / 2; + mThumbOffset = thumb.getIntrinsicWidth() / 2; } mThumb = thumb; invalidate(); @@ -368,20 +368,21 @@ public abstract class AbsSeekBar extends ProgressBar { @Override public boolean onKeyDown(int keyCode, KeyEvent event) { - int progress = getProgress(); - - switch (keyCode) { - case KeyEvent.KEYCODE_DPAD_LEFT: - if (progress <= 0) break; - setProgress(progress - mKeyProgressIncrement, true); - onKeyChange(); - return true; - - case KeyEvent.KEYCODE_DPAD_RIGHT: - if (progress >= getMax()) break; - setProgress(progress + mKeyProgressIncrement, true); - onKeyChange(); - return true; + if (isEnabled()) { + int progress = getProgress(); + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_LEFT: + if (progress <= 0) break; + setProgress(progress - mKeyProgressIncrement, true); + onKeyChange(); + return true; + + case KeyEvent.KEYCODE_DPAD_RIGHT: + if (progress >= getMax()) break; + setProgress(progress + mKeyProgressIncrement, true); + onKeyChange(); + return true; + } } return super.onKeyDown(keyCode, event); diff --git a/core/java/android/widget/AbsSpinner.java b/core/java/android/widget/AbsSpinner.java index 424a936..c939e3f 100644 --- a/core/java/android/widget/AbsSpinner.java +++ b/core/java/android/widget/AbsSpinner.java @@ -249,7 +249,7 @@ public abstract class AbsSpinner extends AdapterView<SpinnerAdapter> { @Override protected ViewGroup.LayoutParams generateDefaultLayoutParams() { return new ViewGroup.LayoutParams( - ViewGroup.LayoutParams.FILL_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); } diff --git a/core/java/android/widget/AbsoluteLayout.java b/core/java/android/widget/AbsoluteLayout.java index c77f7ae..b829655 100644 --- a/core/java/android/widget/AbsoluteLayout.java +++ b/core/java/android/widget/AbsoluteLayout.java @@ -161,9 +161,9 @@ public class AbsoluteLayout extends ViewGroup { * Creates a new set of layout parameters with the specified width, * height and location. * - * @param width the width, either {@link #FILL_PARENT}, + * @param width the width, either {@link #MATCH_PARENT}, {@link #WRAP_CONTENT} or a fixed size in pixels - * @param height the height, either {@link #FILL_PARENT}, + * @param height the height, either {@link #MATCH_PARENT}, {@link #WRAP_CONTENT} or a fixed size in pixels * @param x the X location of the child * @param y the Y location of the child diff --git a/core/java/android/widget/AutoCompleteTextView.java b/core/java/android/widget/AutoCompleteTextView.java index 75d0f31..bb9a672 100644 --- a/core/java/android/widget/AutoCompleteTextView.java +++ b/core/java/android/widget/AutoCompleteTextView.java @@ -172,7 +172,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe mDropDownAnchorId = a.getResourceId(R.styleable.AutoCompleteTextView_dropDownAnchor, View.NO_ID); - // For dropdown width, the developer can specify a specific width, or FILL_PARENT + // For dropdown width, the developer can specify a specific width, or MATCH_PARENT // (for full screen width) or WRAP_CONTENT (to match the width of the anchored view). mDropDownWidth = a.getLayoutDimension(R.styleable.AutoCompleteTextView_dropDownWidth, ViewGroup.LayoutParams.WRAP_CONTENT); @@ -240,7 +240,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe /** * <p>Returns the current width for the auto-complete drop down list. This can - * be a fixed width, or {@link ViewGroup.LayoutParams#FILL_PARENT} to fill the screen, or + * be a fixed width, or {@link ViewGroup.LayoutParams#MATCH_PARENT} to fill the screen, or * {@link ViewGroup.LayoutParams#WRAP_CONTENT} to fit the width of its anchor view.</p> * * @return the width for the drop down list @@ -253,7 +253,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe /** * <p>Sets the current width for the auto-complete drop down list. This can - * be a fixed width, or {@link ViewGroup.LayoutParams#FILL_PARENT} to fill the screen, or + * be a fixed width, or {@link ViewGroup.LayoutParams#MATCH_PARENT} to fill the screen, or * {@link ViewGroup.LayoutParams#WRAP_CONTENT} to fit the width of its anchor view.</p> * * @param width the width to use @@ -266,7 +266,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe /** * <p>Returns the current height for the auto-complete drop down list. This can - * be a fixed height, or {@link ViewGroup.LayoutParams#FILL_PARENT} to fill + * be a fixed height, or {@link ViewGroup.LayoutParams#MATCH_PARENT} to fill * the screen, or {@link ViewGroup.LayoutParams#WRAP_CONTENT} to fit the height * of the drop down's content.</p> * @@ -280,7 +280,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe /** * <p>Sets the current height for the auto-complete drop down list. This can - * be a fixed height, or {@link ViewGroup.LayoutParams#FILL_PARENT} to fill + * be a fixed height, or {@link ViewGroup.LayoutParams#MATCH_PARENT} to fill * the screen, or {@link ViewGroup.LayoutParams#WRAP_CONTENT} to fit the height * of the drop down's content.</p> * @@ -1027,7 +1027,6 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe @Override public void onWindowFocusChanged(boolean hasWindowFocus) { super.onWindowFocusChanged(hasWindowFocus); - performValidation(); if (!hasWindowFocus && !mDropDownAlwaysVisible) { dismissDropDown(); } @@ -1036,7 +1035,10 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe @Override protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { super.onFocusChanged(focused, direction, previouslyFocusedRect); - performValidation(); + // Perform validation if the view is losing focus. + if (!focused) { + performValidation(); + } if (!focused && !mDropDownAlwaysVisible) { dismissDropDown(); } @@ -1127,7 +1129,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe boolean noInputMethod = isInputMethodNotNeeded(); if (mPopup.isShowing()) { - if (mDropDownWidth == ViewGroup.LayoutParams.FILL_PARENT) { + if (mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT) { // The call to PopupWindow's update method below can accept -1 for any // value you do not want to update. widthSpec = -1; @@ -1137,19 +1139,19 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe widthSpec = mDropDownWidth; } - if (mDropDownHeight == ViewGroup.LayoutParams.FILL_PARENT) { + if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) { // The call to PopupWindow's update method below can accept -1 for any // value you do not want to update. - heightSpec = noInputMethod ? height : ViewGroup.LayoutParams.FILL_PARENT; + heightSpec = noInputMethod ? height : ViewGroup.LayoutParams.MATCH_PARENT; if (noInputMethod) { mPopup.setWindowLayoutMode( - mDropDownWidth == ViewGroup.LayoutParams.FILL_PARENT ? - ViewGroup.LayoutParams.FILL_PARENT : 0, 0); + mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT ? + ViewGroup.LayoutParams.MATCH_PARENT : 0, 0); } else { mPopup.setWindowLayoutMode( - mDropDownWidth == ViewGroup.LayoutParams.FILL_PARENT ? - ViewGroup.LayoutParams.FILL_PARENT : 0, - ViewGroup.LayoutParams.FILL_PARENT); + mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT ? + ViewGroup.LayoutParams.MATCH_PARENT : 0, + ViewGroup.LayoutParams.MATCH_PARENT); } } else if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) { heightSpec = height; @@ -1162,8 +1164,8 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe mPopup.update(getDropDownAnchorView(), mDropDownHorizontalOffset, mDropDownVerticalOffset, widthSpec, heightSpec); } else { - if (mDropDownWidth == ViewGroup.LayoutParams.FILL_PARENT) { - widthSpec = ViewGroup.LayoutParams.FILL_PARENT; + if (mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT) { + widthSpec = ViewGroup.LayoutParams.MATCH_PARENT; } else { if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) { mPopup.setWidth(getDropDownAnchorView().getWidth()); @@ -1172,8 +1174,8 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe } } - if (mDropDownHeight == ViewGroup.LayoutParams.FILL_PARENT) { - heightSpec = ViewGroup.LayoutParams.FILL_PARENT; + if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) { + heightSpec = ViewGroup.LayoutParams.MATCH_PARENT; } else { if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) { mPopup.setHeight(height); @@ -1293,7 +1295,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe hintContainer.setOrientation(LinearLayout.VERTICAL); LinearLayout.LayoutParams hintParams = new LinearLayout.LayoutParams( - ViewGroup.LayoutParams.FILL_PARENT, 0, 1.0f + ViewGroup.LayoutParams.MATCH_PARENT, 0, 1.0f ); hintContainer.addView(dropDownView, hintParams); hintContainer.addView(hintView); @@ -1329,7 +1331,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe final int maxHeight = mPopup.getMaxAvailableHeight( getDropDownAnchorView(), mDropDownVerticalOffset, ignoreBottomDecorations); - if (mDropDownAlwaysVisible || mDropDownHeight == ViewGroup.LayoutParams.FILL_PARENT) { + if (mDropDownAlwaysVisible || mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) { // getMaxAvailableHeight() subtracts the padding, so we put it back, // to get the available height for the whole window int padding = 0; @@ -1483,8 +1485,8 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe * @return the view for the specified item */ @Override - protected View obtainView(int position) { - View view = super.obtainView(position); + View obtainView(int position, boolean[] isScrap) { + View view = super.obtainView(position, isScrap); if (view instanceof TextView) { ((TextView) view).setHorizontallyScrolling(true); diff --git a/core/java/android/widget/CheckedTextView.java b/core/java/android/widget/CheckedTextView.java index fd590ed..aa9062b 100644 --- a/core/java/android/widget/CheckedTextView.java +++ b/core/java/android/widget/CheckedTextView.java @@ -117,11 +117,11 @@ public class CheckedTextView extends TextView implements Checkable { * @param d The Drawable to use for the checkmark. */ public void setCheckMarkDrawable(Drawable d) { + if (mCheckMarkDrawable != null) { + mCheckMarkDrawable.setCallback(null); + unscheduleDrawable(mCheckMarkDrawable); + } if (d != null) { - if (mCheckMarkDrawable != null) { - mCheckMarkDrawable.setCallback(null); - unscheduleDrawable(mCheckMarkDrawable); - } d.setCallback(this); d.setVisible(getVisibility() == VISIBLE, false); d.setState(CHECKED_STATE_SET); @@ -130,10 +130,10 @@ public class CheckedTextView extends TextView implements Checkable { mCheckMarkWidth = d.getIntrinsicWidth(); mPaddingRight = mCheckMarkWidth + mBasePaddingRight; d.setState(getDrawableState()); - mCheckMarkDrawable = d; } else { mPaddingRight = mBasePaddingRight; } + mCheckMarkDrawable = d; requestLayout(); } diff --git a/core/java/android/widget/DatePicker.java b/core/java/android/widget/DatePicker.java index 5e76cc3..b657e8e 100644 --- a/core/java/android/widget/DatePicker.java +++ b/core/java/android/widget/DatePicker.java @@ -26,9 +26,9 @@ import android.util.AttributeSet; import android.util.SparseArray; import android.view.LayoutInflater; +import com.android.common.widget.NumberPicker; +import com.android.common.widget.NumberPicker.OnChangedListener; import com.android.internal.R; -import com.android.internal.widget.NumberPicker; -import com.android.internal.widget.NumberPicker.OnChangedListener; import java.text.DateFormatSymbols; import java.text.SimpleDateFormat; diff --git a/core/java/android/widget/ExpandableListView.java b/core/java/android/widget/ExpandableListView.java index 6abb2ae..405461a 100644 --- a/core/java/android/widget/ExpandableListView.java +++ b/core/java/android/widget/ExpandableListView.java @@ -33,7 +33,6 @@ import android.view.ContextMenu; import android.view.SoundEffectConstants; import android.view.View; import android.view.ContextMenu.ContextMenuInfo; -import android.widget.AdapterView.AdapterContextMenuInfo; import android.widget.ExpandableListConnector.PositionMetadata; /** @@ -514,37 +513,36 @@ public class ExpandableListView extends ListView { boolean returnValue; if (posMetadata.position.type == ExpandableListPosition.GROUP) { /* It's a group, so handle collapsing/expanding */ - + + /* It's a group click, so pass on event */ + if (mOnGroupClickListener != null) { + if (mOnGroupClickListener.onGroupClick(this, v, + posMetadata.position.groupPos, id)) { + posMetadata.recycle(); + return true; + } + } + if (posMetadata.isExpanded()) { /* Collapse it */ mConnector.collapseGroup(posMetadata); playSoundEffect(SoundEffectConstants.CLICK); - + if (mOnGroupCollapseListener != null) { mOnGroupCollapseListener.onGroupCollapse(posMetadata.position.groupPos); } - } else { - /* It's a group click, so pass on event */ - if (mOnGroupClickListener != null) { - if (mOnGroupClickListener.onGroupClick(this, v, - posMetadata.position.groupPos, id)) { - posMetadata.recycle(); - return true; - } - } - /* Expand it */ mConnector.expandGroup(posMetadata); playSoundEffect(SoundEffectConstants.CLICK); - + if (mOnGroupExpandListener != null) { mOnGroupExpandListener.onGroupExpand(posMetadata.position.groupPos); } } - + returnValue = true; } else { /* It's a child, so pass on event */ @@ -553,12 +551,12 @@ public class ExpandableListView extends ListView { return mOnChildClickListener.onChildClick(this, v, posMetadata.position.groupPos, posMetadata.position.childPos, id); } - + returnValue = false; } - + posMetadata.recycle(); - + return returnValue; } diff --git a/core/java/android/widget/FrameLayout.java b/core/java/android/widget/FrameLayout.java index 3afd5d4..65a4673 100644 --- a/core/java/android/widget/FrameLayout.java +++ b/core/java/android/widget/FrameLayout.java @@ -164,12 +164,12 @@ public class FrameLayout extends ViewGroup { /** * Returns a set of layout parameters with a width of - * {@link android.view.ViewGroup.LayoutParams#FILL_PARENT}, - * and a height of {@link android.view.ViewGroup.LayoutParams#FILL_PARENT}. + * {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT}, + * and a height of {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT}. */ @Override protected LayoutParams generateDefaultLayoutParams() { - return new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT); + return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); } /** @@ -467,9 +467,9 @@ public class FrameLayout extends ViewGroup { * Creates a new set of layout parameters with the specified width, height * and weight. * - * @param width the width, either {@link #FILL_PARENT}, + * @param width the width, either {@link #MATCH_PARENT}, * {@link #WRAP_CONTENT} or a fixed size in pixels - * @param height the height, either {@link #FILL_PARENT}, + * @param height the height, either {@link #MATCH_PARENT}, * {@link #WRAP_CONTENT} or a fixed size in pixels * @param gravity the gravity * diff --git a/core/java/android/widget/GridView.java b/core/java/android/widget/GridView.java index ffe9908..30a38df 100644 --- a/core/java/android/widget/GridView.java +++ b/core/java/android/widget/GridView.java @@ -931,11 +931,11 @@ public class GridView extends AbsListView { mItemCount = mAdapter == null ? 0 : mAdapter.getCount(); final int count = mItemCount; if (count > 0) { - final View child = obtainView(0); + final View child = obtainView(0, mIsScrap); AbsListView.LayoutParams p = (AbsListView.LayoutParams)child.getLayoutParams(); if (p == null) { - p = new AbsListView.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, + p = new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, 0); child.setLayoutParams(p); } @@ -1203,7 +1203,7 @@ public class GridView extends AbsListView { View child; if (!mDataChanged) { - // Try to use an exsiting view for this position + // Try to use an existing view for this position child = mRecycler.getActiveView(position); if (child != null) { // Found it -- we're using an existing child @@ -1215,10 +1215,10 @@ public class GridView extends AbsListView { // Make a new view for this position, or convert an unused view if // possible - child = obtainView(position); + child = obtainView(position, mIsScrap); // This needs to be positioned and measured - setupChild(child, position, y, flow, childrenLeft, selected, false, where); + setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0], where); return child; } @@ -1254,7 +1254,7 @@ public class GridView extends AbsListView { // some up... AbsListView.LayoutParams p = (AbsListView.LayoutParams)child.getLayoutParams(); if (p == null) { - p = new AbsListView.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, + p = new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, 0); } p.viewType = mAdapter.getItemViewType(position); diff --git a/core/java/android/widget/ImageView.java b/core/java/android/widget/ImageView.java index b8f0a7e..3853359 100644 --- a/core/java/android/widget/ImageView.java +++ b/core/java/android/widget/ImageView.java @@ -16,6 +16,7 @@ package android.widget; +import android.content.ContentResolver; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; @@ -489,7 +490,18 @@ public class ImageView extends View { mUri = null; } } else if (mUri != null) { - if ("content".equals(mUri.getScheme())) { + String scheme = mUri.getScheme(); + if (ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme)) { + try { + // Load drawable through Resources, to get the source density information + ContentResolver.OpenResourceIdResult r = + mContext.getContentResolver().getResourceId(mUri); + d = r.r.getDrawable(r.id); + } catch (Exception e) { + Log.w("ImageView", "Unable to open content: " + mUri, e); + } + } else if (ContentResolver.SCHEME_CONTENT.equals(scheme) + || ContentResolver.SCHEME_FILE.equals(scheme)) { try { d = Drawable.createFromStream( mContext.getContentResolver().openInputStream(mUri), diff --git a/core/java/android/widget/LinearLayout.java b/core/java/android/widget/LinearLayout.java index 6cc794b..37372c5 100644 --- a/core/java/android/widget/LinearLayout.java +++ b/core/java/android/widget/LinearLayout.java @@ -378,7 +378,7 @@ public class LinearLayout extends ViewGroup { } boolean matchWidthLocally = false; - if (widthMode != MeasureSpec.EXACTLY && lp.width == LayoutParams.FILL_PARENT) { + if (widthMode != MeasureSpec.EXACTLY && lp.width == LayoutParams.MATCH_PARENT) { // The width of the linear layout will scale, and at least one // child said it wanted to match our width. Set a flag // indicating that we need to remeasure at least that view when @@ -391,7 +391,7 @@ public class LinearLayout extends ViewGroup { final int measuredWidth = child.getMeasuredWidth() + margin; maxWidth = Math.max(maxWidth, measuredWidth); - allFillParent = allFillParent && lp.width == LayoutParams.FILL_PARENT; + allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT; if (lp.weight > 0) { /* * Widths of weighted Views are bogus if we end up @@ -472,12 +472,12 @@ public class LinearLayout extends ViewGroup { maxWidth = Math.max(maxWidth, measuredWidth); boolean matchWidthLocally = widthMode != MeasureSpec.EXACTLY && - lp.width == LayoutParams.FILL_PARENT; + lp.width == LayoutParams.MATCH_PARENT; alternativeMaxWidth = Math.max(alternativeMaxWidth, matchWidthLocally ? margin : measuredWidth); - allFillParent = allFillParent && lp.width == LayoutParams.FILL_PARENT; + allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT; mTotalLength += child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin + getNextLocationOffset(child); @@ -515,7 +515,7 @@ public class LinearLayout extends ViewGroup { if (child.getVisibility() != GONE) { LinearLayout.LayoutParams lp = ((LinearLayout.LayoutParams)child.getLayoutParams()); - if (lp.width == LayoutParams.FILL_PARENT) { + if (lp.width == LayoutParams.MATCH_PARENT) { // Temporarily force children to reuse their old measured height // FIXME: this may not be right for something like wrapping text? int oldHeight = lp.height; @@ -629,7 +629,7 @@ public class LinearLayout extends ViewGroup { } boolean matchHeightLocally = false; - if (heightMode != MeasureSpec.EXACTLY && lp.height == LayoutParams.FILL_PARENT) { + if (heightMode != MeasureSpec.EXACTLY && lp.height == LayoutParams.MATCH_PARENT) { // The height of the linear layout will scale, and at least one // child said it wanted to match our height. Set a flag indicating that // we need to remeasure at least that view when we know our height. @@ -657,7 +657,7 @@ public class LinearLayout extends ViewGroup { maxHeight = Math.max(maxHeight, childHeight); - allFillParent = allFillParent && lp.height == LayoutParams.FILL_PARENT; + allFillParent = allFillParent && lp.height == LayoutParams.MATCH_PARENT; if (lp.weight > 0) { /* * Heights of weighted Views are bogus if we end up @@ -758,7 +758,7 @@ public class LinearLayout extends ViewGroup { lp.rightMargin + getNextLocationOffset(child); boolean matchHeightLocally = heightMode != MeasureSpec.EXACTLY && - lp.height == LayoutParams.FILL_PARENT; + lp.height == LayoutParams.MATCH_PARENT; final int margin = lp.topMargin + lp .bottomMargin; int childHeight = child.getMeasuredHeight() + margin; @@ -766,7 +766,7 @@ public class LinearLayout extends ViewGroup { alternativeMaxHeight = Math.max(alternativeMaxHeight, matchHeightLocally ? margin : childHeight); - allFillParent = allFillParent && lp.height == LayoutParams.FILL_PARENT; + allFillParent = allFillParent && lp.height == LayoutParams.MATCH_PARENT; if (baselineAligned) { final int childBaseline = child.getBaseline(); @@ -832,7 +832,7 @@ public class LinearLayout extends ViewGroup { if (child.getVisibility() != GONE) { LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams(); - if (lp.height == LayoutParams.FILL_PARENT) { + if (lp.height == LayoutParams.MATCH_PARENT) { // Temporarily force children to reuse their old measured width // FIXME: this may not be right for something like wrapping text? int oldWidth = lp.width; @@ -991,6 +991,9 @@ public class LinearLayout extends ViewGroup { case Gravity.RIGHT: childLeft = childRight - childWidth - lp.rightMargin; break; + default: + childLeft = paddingLeft; + break; } @@ -1062,7 +1065,7 @@ public class LinearLayout extends ViewGroup { final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams(); - if (baselineAligned && lp.height != LayoutParams.FILL_PARENT) { + if (baselineAligned && lp.height != LayoutParams.MATCH_PARENT) { childBaseline = child.getBaseline(); } @@ -1102,6 +1105,9 @@ public class LinearLayout extends ViewGroup { childTop -= (maxDescent[INDEX_BOTTOM] - descent); } break; + default: + childTop = paddingTop; + break; } childLeft += lp.leftMargin; @@ -1193,7 +1199,7 @@ public class LinearLayout extends ViewGroup { /** * Returns a set of layout parameters with a width of - * {@link android.view.ViewGroup.LayoutParams#FILL_PARENT} + * {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT} * and a height of {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} * when the layout's orientation is {@link #VERTICAL}. When the orientation is * {@link #HORIZONTAL}, the width is set to {@link LayoutParams#WRAP_CONTENT} @@ -1204,7 +1210,7 @@ public class LinearLayout extends ViewGroup { if (mOrientation == HORIZONTAL) { return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); } else if (mOrientation == VERTICAL) { - return new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.WRAP_CONTENT); + return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); } return null; } @@ -1284,9 +1290,9 @@ public class LinearLayout extends ViewGroup { * Creates a new set of layout parameters with the specified width, height * and weight. * - * @param width the width, either {@link #FILL_PARENT}, + * @param width the width, either {@link #MATCH_PARENT}, * {@link #WRAP_CONTENT} or a fixed size in pixels - * @param height the height, either {@link #FILL_PARENT}, + * @param height the height, either {@link #MATCH_PARENT}, * {@link #WRAP_CONTENT} or a fixed size in pixels * @param weight the weight */ diff --git a/core/java/android/widget/ListView.java b/core/java/android/widget/ListView.java index 7c8151e..c63774a 100644 --- a/core/java/android/widget/ListView.java +++ b/core/java/android/widget/ListView.java @@ -1033,7 +1033,7 @@ public class ListView extends AbsListView { mItemCount = mAdapter == null ? 0 : mAdapter.getCount(); if (mItemCount > 0 && (widthMode == MeasureSpec.UNSPECIFIED || heightMode == MeasureSpec.UNSPECIFIED)) { - final View child = obtainView(0); + final View child = obtainView(0, mIsScrap); measureScrapChild(child, 0, widthMeasureSpec); @@ -1067,7 +1067,7 @@ public class ListView extends AbsListView { private void measureScrapChild(View child, int position, int widthMeasureSpec) { LayoutParams p = (LayoutParams) child.getLayoutParams(); if (p == null) { - p = new LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, + p = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, 0); child.setLayoutParams(p); } @@ -1142,9 +1142,10 @@ public class ListView extends AbsListView { endPosition = (endPosition == NO_POSITION) ? adapter.getCount() - 1 : endPosition; final AbsListView.RecycleBin recycleBin = mRecycler; final boolean recyle = recycleOnMeasure(); + final boolean[] isScrap = mIsScrap; for (i = startPosition; i <= endPosition; ++i) { - child = obtainView(i); + child = obtainView(i, isScrap); measureScrapChild(child, i, widthMeasureSpec); @@ -1374,7 +1375,7 @@ public class ListView extends AbsListView { int childrenBottom = mBottom - mTop - mListPadding.bottom; int childCount = getChildCount(); - int index; + int index = 0; int delta = 0; View sel; @@ -1665,10 +1666,10 @@ public class ListView extends AbsListView { } // Make a new view for this position, or convert an unused view if possible - child = obtainView(position); + child = obtainView(position, mIsScrap); // This needs to be positioned and measured - setupChild(child, position, y, flow, childrenLeft, selected, false); + setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]); return child; } @@ -1701,7 +1702,7 @@ public class ListView extends AbsListView { // noinspection unchecked AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams(); if (p == null) { - p = new AbsListView.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, + p = new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, 0); } p.viewType = mAdapter.getItemViewType(position); @@ -2387,7 +2388,7 @@ public class ListView extends AbsListView { ViewGroup.LayoutParams p = child.getLayoutParams(); if (p == null) { p = new ViewGroup.LayoutParams( - ViewGroup.LayoutParams.FILL_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); } @@ -2823,17 +2824,19 @@ public class ListView extends AbsListView { private View addViewAbove(View theView, int position) { int abovePosition = position - 1; - View view = obtainView(abovePosition); + View view = obtainView(abovePosition, mIsScrap); int edgeOfNewChild = theView.getTop() - mDividerHeight; - setupChild(view, abovePosition, edgeOfNewChild, false, mListPadding.left, false, false); + setupChild(view, abovePosition, edgeOfNewChild, false, mListPadding.left, + false, mIsScrap[0]); return view; } private View addViewBelow(View theView, int position) { int belowPosition = position + 1; - View view = obtainView(belowPosition); + View view = obtainView(belowPosition, mIsScrap); int edgeOfNewChild = theView.getBottom() + mDividerHeight; - setupChild(view, belowPosition, edgeOfNewChild, true, mListPadding.left, false, false); + setupChild(view, belowPosition, edgeOfNewChild, true, mListPadding.left, + false, mIsScrap[0]); return view; } @@ -3080,13 +3083,19 @@ public class ListView extends AbsListView { if (gainFocus && previouslyFocusedRect != null) { previouslyFocusedRect.offset(mScrollX, mScrollY); + final ListAdapter adapter = mAdapter; + final int firstPosition = mFirstPosition; + // Don't cache the result of getChildCount here, it could change in layoutChildren. + if (adapter.getCount() < getChildCount() + firstPosition) { + mLayoutMode = LAYOUT_NORMAL; + layoutChildren(); + } + // figure out which item should be selected based on previously // focused rect Rect otherRect = mTempRect; int minDistance = Integer.MAX_VALUE; final int childCount = getChildCount(); - final int firstPosition = mFirstPosition; - final ListAdapter adapter = mAdapter; for (int i = 0; i < childCount; i++) { // only consider selectable views diff --git a/core/java/android/widget/MediaController.java b/core/java/android/widget/MediaController.java index 446a992..c246c247 100644 --- a/core/java/android/widget/MediaController.java +++ b/core/java/android/widget/MediaController.java @@ -167,8 +167,8 @@ public class MediaController extends FrameLayout { mAnchor = view; FrameLayout.LayoutParams frameParams = new FrameLayout.LayoutParams( - ViewGroup.LayoutParams.FILL_PARENT, - ViewGroup.LayoutParams.FILL_PARENT + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT ); removeAllViews(); diff --git a/core/java/android/widget/PopupWindow.java b/core/java/android/widget/PopupWindow.java index e4cc609..d20ab3b 100644 --- a/core/java/android/widget/PopupWindow.java +++ b/core/java/android/widget/PopupWindow.java @@ -569,7 +569,7 @@ public class PopupWindow { * the current width or height is requested as an explicit size from * the window manager. You can supply * {@link ViewGroup.LayoutParams#WRAP_CONTENT} or - * {@link ViewGroup.LayoutParams#FILL_PARENT} to have that measure + * {@link ViewGroup.LayoutParams#MATCH_PARENT} to have that measure * spec supplied instead, replacing the absolute width and height that * has been set in the popup.</p> * @@ -578,11 +578,11 @@ public class PopupWindow { * * @param widthSpec an explicit width measure spec mode, either * {@link ViewGroup.LayoutParams#WRAP_CONTENT}, - * {@link ViewGroup.LayoutParams#FILL_PARENT}, or 0 to use the absolute + * {@link ViewGroup.LayoutParams#MATCH_PARENT}, or 0 to use the absolute * width. * @param heightSpec an explicit height measure spec mode, either * {@link ViewGroup.LayoutParams#WRAP_CONTENT}, - * {@link ViewGroup.LayoutParams#FILL_PARENT}, or 0 to use the absolute + * {@link ViewGroup.LayoutParams#MATCH_PARENT}, or 0 to use the absolute * height. */ public void setWindowLayoutMode(int widthSpec, int heightSpec) { @@ -785,7 +785,7 @@ public class PopupWindow { if (mBackground != null) { final ViewGroup.LayoutParams layoutParams = mContentView.getLayoutParams(); - int height = ViewGroup.LayoutParams.FILL_PARENT; + int height = ViewGroup.LayoutParams.MATCH_PARENT; if (layoutParams != null && layoutParams.height == ViewGroup.LayoutParams.WRAP_CONTENT) { height = ViewGroup.LayoutParams.WRAP_CONTENT; @@ -795,7 +795,7 @@ public class PopupWindow { // within another view that owns the background drawable PopupViewContainer popupViewContainer = new PopupViewContainer(mContext); PopupViewContainer.LayoutParams listParams = new PopupViewContainer.LayoutParams( - ViewGroup.LayoutParams.FILL_PARENT, height + ViewGroup.LayoutParams.MATCH_PARENT, height ); popupViewContainer.setBackgroundDrawable(mBackground); popupViewContainer.addView(mContentView, listParams); diff --git a/core/java/android/widget/QuickContactBadge.java b/core/java/android/widget/QuickContactBadge.java index 8019f14..07c3e4b 100644 --- a/core/java/android/widget/QuickContactBadge.java +++ b/core/java/android/widget/QuickContactBadge.java @@ -25,9 +25,9 @@ import android.database.Cursor; import android.graphics.drawable.Drawable; import android.net.Uri; import android.provider.ContactsContract.Contacts; -import android.provider.ContactsContract.QuickContact; import android.provider.ContactsContract.Intents; import android.provider.ContactsContract.PhoneLookup; +import android.provider.ContactsContract.QuickContact; import android.provider.ContactsContract.RawContacts; import android.provider.ContactsContract.CommonDataKinds.Email; import android.util.AttributeSet; @@ -55,21 +55,28 @@ public class QuickContactBadge extends ImageView implements OnClickListener { static final private int TOKEN_PHONE_LOOKUP = 1; static final private int TOKEN_EMAIL_LOOKUP_AND_TRIGGER = 2; static final private int TOKEN_PHONE_LOOKUP_AND_TRIGGER = 3; + static final private int TOKEN_CONTACT_LOOKUP_AND_TRIGGER = 4; static final String[] EMAIL_LOOKUP_PROJECTION = new String[] { RawContacts.CONTACT_ID, Contacts.LOOKUP_KEY, }; - static int EMAIL_ID_COLUMN_INDEX = 0; - static int EMAIL_LOOKUP_STRING_COLUMN_INDEX = 1; + static final int EMAIL_ID_COLUMN_INDEX = 0; + static final int EMAIL_LOOKUP_STRING_COLUMN_INDEX = 1; static final String[] PHONE_LOOKUP_PROJECTION = new String[] { PhoneLookup._ID, PhoneLookup.LOOKUP_KEY, }; - static int PHONE_ID_COLUMN_INDEX = 0; - static int PHONE_LOOKUP_STRING_COLUMN_INDEX = 1; + static final int PHONE_ID_COLUMN_INDEX = 0; + static final int PHONE_LOOKUP_STRING_COLUMN_INDEX = 1; + static final String[] CONTACT_LOOKUP_PROJECTION = new String[] { + Contacts._ID, + Contacts.LOOKUP_KEY, + }; + static final int CONTACT_ID_COLUMN_INDEX = 0; + static final int CONTACT_LOOKUPKEY_COLUMN_INDEX = 1; public QuickContactBadge(Context context) { @@ -181,9 +188,9 @@ public class QuickContactBadge extends ImageView implements OnClickListener { public void onClick(View v) { if (mContactUri != null) { - final ContentResolver resolver = getContext().getContentResolver(); - final Uri lookupUri = Contacts.getLookupUri(resolver, mContactUri); - trigger(lookupUri); + mQueryHandler.startQuery(TOKEN_CONTACT_LOOKUP_AND_TRIGGER, null, + mContactUri, + CONTACT_LOOKUP_PROJECTION, null, null, null); } else if (mContactEmail != null) { mQueryHandler.startQuery(TOKEN_EMAIL_LOOKUP_AND_TRIGGER, mContactEmail, Uri.withAppendedPath(Email.CONTENT_LOOKUP_URI, Uri.encode(mContactEmail)), @@ -249,6 +256,17 @@ public class QuickContactBadge extends ImageView implements OnClickListener { lookupUri = Contacts.getLookupUri(contactId, lookupKey); } } + + case TOKEN_CONTACT_LOOKUP_AND_TRIGGER: { + if (cursor != null && cursor.moveToFirst()) { + long contactId = cursor.getLong(CONTACT_ID_COLUMN_INDEX); + String lookupKey = cursor.getString(CONTACT_LOOKUPKEY_COLUMN_INDEX); + lookupUri = Contacts.getLookupUri(contactId, lookupKey); + trigger = true; + } + + break; + } } } finally { if (cursor != null) { diff --git a/core/java/android/widget/RelativeLayout.java b/core/java/android/widget/RelativeLayout.java index e19a93d..1aa1df3 100644 --- a/core/java/android/widget/RelativeLayout.java +++ b/core/java/android/widget/RelativeLayout.java @@ -45,8 +45,7 @@ import java.util.ArrayList; /** * A Layout where the positions of the children can be described in relation to each other or to the - * parent. For the sake of efficiency, the relations between views are evaluated in one pass, so if - * view Y is dependent on the position of view X, make sure the view X comes first in the layout. + * parent. * * <p> * Note that you cannot have a circular dependency between the size of the RelativeLayout and the @@ -292,6 +291,8 @@ public class RelativeLayout extends ViewGroup { } } + // TODO: we need to find another way to implement RelativeLayout + // This implementation cannot handle every case @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (mDirtyHierarchy) { @@ -439,6 +440,10 @@ public class RelativeLayout extends ViewGroup { final int[] rules = params.getRules(); if (rules[CENTER_IN_PARENT] != 0 || rules[CENTER_HORIZONTAL] != 0) { centerHorizontal(child, params, width); + } else if (rules[ALIGN_PARENT_RIGHT] != 0) { + final int childWidth = child.getMeasuredWidth(); + params.mLeft = width - mPaddingRight - childWidth; + params.mRight = params.mLeft + childWidth; } } } @@ -465,6 +470,10 @@ public class RelativeLayout extends ViewGroup { final int[] rules = params.getRules(); if (rules[CENTER_IN_PARENT] != 0 || rules[CENTER_VERTICAL] != 0) { centerVertical(child, params, height); + } else if (rules[ALIGN_PARENT_BOTTOM] != 0) { + final int childHeight = child.getMeasuredHeight(); + params.mTop = height - mPaddingBottom - childHeight; + params.mBottom = params.mTop + childHeight; } } } @@ -561,7 +570,7 @@ public class RelativeLayout extends ViewGroup { mPaddingLeft, mPaddingRight, myWidth); int childHeightMeasureSpec; - if (params.width == LayoutParams.FILL_PARENT) { + if (params.width == LayoutParams.MATCH_PARENT) { childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(myHeight, MeasureSpec.EXACTLY); } else { childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(myHeight, MeasureSpec.AT_MOST); @@ -623,7 +632,7 @@ public class RelativeLayout extends ViewGroup { // We can grow in this dimension. childSpecSize = childSize; } - } else if (childSize == LayoutParams.FILL_PARENT) { + } else if (childSize == LayoutParams.MATCH_PARENT) { // Child wanted to be as big as possible. Give all availble // space childSpecMode = MeasureSpec.EXACTLY; @@ -674,7 +683,7 @@ public class RelativeLayout extends ViewGroup { params.mRight = params.mLeft + child.getMeasuredWidth(); } } - return false; + return rules[ALIGN_PARENT_RIGHT] != 0; } private boolean positionChildVertical(View child, LayoutParams params, int myHeight, @@ -703,7 +712,7 @@ public class RelativeLayout extends ViewGroup { params.mBottom = params.mTop + child.getMeasuredHeight(); } } - return false; + return rules[ALIGN_PARENT_BOTTOM] != 0; } private void applyHorizontalSizeRules(LayoutParams childParams, int myWidth) { diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java index b847e57..3003580 100644 --- a/core/java/android/widget/RemoteViews.java +++ b/core/java/android/widget/RemoteViews.java @@ -26,6 +26,7 @@ import android.graphics.PorterDuff; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.net.Uri; +import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; @@ -37,7 +38,6 @@ import android.view.ViewGroup; import android.view.LayoutInflater.Filter; import android.view.View.OnClickListener; -import java.lang.Class; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -137,11 +137,21 @@ public class RemoteViews implements Parcelable, Filter { if (target != null && pendingIntent != null) { OnClickListener listener = new OnClickListener() { public void onClick(View v) { - int[] pos = new int[2]; + // Find target view location in screen coordinates and + // fill into PendingIntent before sending. + final float appScale = v.getContext().getResources() + .getCompatibilityInfo().applicationScale; + final int[] pos = new int[2]; v.getLocationOnScreen(pos); - Intent intent = new Intent(); - intent.setSourceBounds(new Rect(pos[0], pos[1], - pos[0]+v.getWidth(), pos[1]+v.getHeight())); + + final Rect rect = new Rect(); + rect.left = (int) (pos[0] * appScale + 0.5f); + rect.top = (int) (pos[1] * appScale + 0.5f); + rect.right = (int) ((pos[0] + v.getWidth()) * appScale + 0.5f); + rect.bottom = (int) ((pos[1] + v.getHeight()) * appScale + 0.5f); + + final Intent intent = new Intent(); + intent.setSourceBounds(rect); try { // TODO: Unregister this handler if PendingIntent.FLAG_ONE_SHOT? v.getContext().startIntentSender( @@ -273,6 +283,7 @@ public class RemoteViews implements Parcelable, Filter { static final int CHAR_SEQUENCE = 10; static final int URI = 11; static final int BITMAP = 12; + static final int BUNDLE = 13; int viewId; String methodName; @@ -332,6 +343,9 @@ public class RemoteViews implements Parcelable, Filter { case BITMAP: this.value = Bitmap.CREATOR.createFromParcel(in); break; + case BUNDLE: + this.value = in.readBundle(); + break; default: break; } @@ -384,6 +398,9 @@ public class RemoteViews implements Parcelable, Filter { case BITMAP: ((Bitmap)this.value).writeToParcel(out, flags); break; + case BUNDLE: + out.writeBundle((Bundle) this.value); + break; default: break; } @@ -415,6 +432,8 @@ public class RemoteViews implements Parcelable, Filter { return Uri.class; case BITMAP: return Bitmap.class; + case BUNDLE: + return Bundle.class; default: return null; } @@ -876,6 +895,17 @@ public class RemoteViews implements Parcelable, Filter { } /** + * Call a method taking one Bundle on a view in the layout for this RemoteViews. + * + * @param viewId The id of the view whose text should change + * @param methodName The name of the method to call. + * @param value The value to pass to the method. + */ + public void setBundle(int viewId, String methodName, Bundle value) { + addAction(new ReflectionAction(viewId, methodName, ReflectionAction.BUNDLE, value)); + } + + /** * Inflates the view hierarchy represented by this object and applies * all of the actions. * diff --git a/core/java/android/widget/ScrollView.java b/core/java/android/widget/ScrollView.java index 24d97a5..bf16e28 100644 --- a/core/java/android/widget/ScrollView.java +++ b/core/java/android/widget/ScrollView.java @@ -51,8 +51,6 @@ import java.util.List; * <p>ScrollView only supports vertical scrolling. */ public class ScrollView extends FrameLayout { - static final String TAG = "ScrollView"; - static final int ANIMATED_SCROLL_GAP = 250; static final float MAX_SCROLL_FACTOR = 0.5f; @@ -401,6 +399,7 @@ public class ScrollView extends FrameLayout { final int yDiff = (int) Math.abs(y - mLastMotionY); if (yDiff > mTouchSlop) { mIsBeingDragged = true; + mLastMotionY = y; } break; diff --git a/core/java/android/widget/SimpleAdapter.java b/core/java/android/widget/SimpleAdapter.java index 9dd4d15..479965a 100644 --- a/core/java/android/widget/SimpleAdapter.java +++ b/core/java/android/widget/SimpleAdapter.java @@ -25,7 +25,6 @@ import android.net.Uri; import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.WeakHashMap; /** * An easy adapter to map static data to views defined in an XML file. You can specify the data @@ -58,7 +57,6 @@ public class SimpleAdapter extends BaseAdapter implements Filterable { private int mResource; private int mDropDownResource; private LayoutInflater mInflater; - private final WeakHashMap<View, View[]> mHolders = new WeakHashMap<View, View[]>(); private SimpleFilter mFilter; private ArrayList<Map<String, ?>> mUnfilteredData; @@ -121,16 +119,6 @@ public class SimpleAdapter extends BaseAdapter implements Filterable { View v; if (convertView == null) { v = mInflater.inflate(resource, parent, false); - - final int[] to = mTo; - final int count = to.length; - final View[] holder = new View[count]; - - for (int i = 0; i < count; i++) { - holder[i] = v.findViewById(to[i]); - } - - mHolders.put(v, holder); } else { v = convertView; } @@ -162,13 +150,12 @@ public class SimpleAdapter extends BaseAdapter implements Filterable { } final ViewBinder binder = mViewBinder; - final View[] holder = mHolders.get(view); final String[] from = mFrom; final int[] to = mTo; final int count = to.length; for (int i = 0; i < count; i++) { - final View v = holder[i]; + final View v = view.findViewById(to[i]); if (v != null) { final Object data = dataSet.get(from[i]); String text = data == null ? "" : data.toString(); @@ -187,7 +174,8 @@ public class SimpleAdapter extends BaseAdapter implements Filterable { ((Checkable) v).setChecked((Boolean) data); } else { throw new IllegalStateException(v.getClass().getName() + - " should be bound to a Boolean, not a " + data.getClass()); + " should be bound to a Boolean, not a " + + (data == null ? "<unknown type>" : data.getClass())); } } else if (v instanceof TextView) { // Note: keep the instanceof TextView check at the bottom of these diff --git a/core/java/android/widget/SimpleCursorAdapter.java b/core/java/android/widget/SimpleCursorAdapter.java index 436b79b..7d3459e 100644 --- a/core/java/android/widget/SimpleCursorAdapter.java +++ b/core/java/android/widget/SimpleCursorAdapter.java @@ -20,9 +20,6 @@ import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.view.View; -import android.view.ViewGroup; - -import java.util.WeakHashMap; /** * An easy adapter to map columns from a cursor to TextViews or ImageViews @@ -66,7 +63,6 @@ public class SimpleCursorAdapter extends ResourceCursorAdapter { private CursorToStringConverter mCursorToStringConverter; private ViewBinder mViewBinder; private String[] mOriginalFrom; - private final WeakHashMap<View, View[]> mHolders = new WeakHashMap<View, View[]>(); /** * Constructor. @@ -91,29 +87,6 @@ public class SimpleCursorAdapter extends ResourceCursorAdapter { findColumns(from); } - @Override - public View newView(Context context, Cursor cursor, ViewGroup parent) { - return generateViewHolder(super.newView(context, cursor, parent)); - } - - @Override - public View newDropDownView(Context context, Cursor cursor, ViewGroup parent) { - return generateViewHolder(super.newDropDownView(context, cursor, parent)); - } - - private View generateViewHolder(View v) { - final int[] to = mTo; - final int count = to.length; - final View[] holder = new View[count]; - - for (int i = 0; i < count; i++) { - holder[i] = v.findViewById(to[i]); - } - mHolders.put(v, holder); - - return v; - } - /** * Binds all of the field names passed into the "to" parameter of the * constructor with their corresponding cursor columns as specified in the @@ -140,13 +113,13 @@ public class SimpleCursorAdapter extends ResourceCursorAdapter { */ @Override public void bindView(View view, Context context, Cursor cursor) { - final View[] holder = mHolders.get(view); final ViewBinder binder = mViewBinder; final int count = mTo.length; final int[] from = mFrom; + final int[] to = mTo; for (int i = 0; i < count; i++) { - final View v = holder[i]; + final View v = view.findViewById(to[i]); if (v != null) { boolean bound = false; if (binder != null) { diff --git a/core/java/android/widget/SlidingDrawer.java b/core/java/android/widget/SlidingDrawer.java index f706744..11d72de 100644 --- a/core/java/android/widget/SlidingDrawer.java +++ b/core/java/android/widget/SlidingDrawer.java @@ -43,7 +43,7 @@ import android.view.accessibility.AccessibilityEvent; * SlidingDrawer should be used as an overlay inside layouts. This means SlidingDrawer * should only be used inside of a FrameLayout or a RelativeLayout for instance. The * size of the SlidingDrawer defines how much space the content will occupy once slid - * out so SlidingDrawer should usually use fill_parent for both its dimensions. + * out so SlidingDrawer should usually use match_parent for both its dimensions. * * Inside an XML layout, SlidingDrawer must define the id of the handle and of the * content: @@ -51,8 +51,8 @@ import android.view.accessibility.AccessibilityEvent; * <pre class="prettyprint"> * <SlidingDrawer * android:id="@+id/drawer" - * android:layout_width="fill_parent" - * android:layout_height="fill_parent" + * android:layout_width="match_parent" + * android:layout_height="match_parent" * * android:handle="@+id/handle" * android:content="@+id/content"> @@ -64,8 +64,8 @@ import android.view.accessibility.AccessibilityEvent; * * <GridView * android:id="@id/content" - * android:layout_width="fill_parent" - * android:layout_height="fill_parent" /> + * android:layout_width="match_parent" + * android:layout_height="match_parent" /> * * </SlidingDrawer> * </pre> diff --git a/core/java/android/widget/TabHost.java b/core/java/android/widget/TabHost.java index 31920e7..78e2fee 100644 --- a/core/java/android/widget/TabHost.java +++ b/core/java/android/widget/TabHost.java @@ -279,6 +279,7 @@ mTabHost.addTab(TAB_TAG_1, "Hello, world!", "Tab 1"); if (!handled && (event.getAction() == KeyEvent.ACTION_DOWN) && (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_UP) + && (mCurrentView != null) && (mCurrentView.isRootNamespace()) && (mCurrentView.hasFocus()) && (mCurrentView.findFocus().focusSearch(View.FOCUS_UP) == null)) { @@ -292,7 +293,9 @@ mTabHost.addTab(TAB_TAG_1, "Hello, world!", "Tab 1"); @Override public void dispatchWindowFocusChanged(boolean hasFocus) { - mCurrentView.dispatchWindowFocusChanged(hasFocus); + if (mCurrentView != null){ + mCurrentView.dispatchWindowFocusChanged(hasFocus); + } } public void setCurrentTab(int index) { @@ -324,8 +327,8 @@ mTabHost.addTab(TAB_TAG_1, "Hello, world!", "Tab 1"); .addView( mCurrentView, new ViewGroup.LayoutParams( - ViewGroup.LayoutParams.FILL_PARENT, - ViewGroup.LayoutParams.FILL_PARENT)); + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); } if (!mTabWidget.hasFocus()) { diff --git a/core/java/android/widget/TabWidget.java b/core/java/android/widget/TabWidget.java index 2ba6268..aa47e6d 100644 --- a/core/java/android/widget/TabWidget.java +++ b/core/java/android/widget/TabWidget.java @@ -16,8 +16,6 @@ package android.widget; -import com.android.internal.R; - import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; @@ -26,7 +24,6 @@ import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Build; import android.util.AttributeSet; -import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.view.View.OnFocusChangeListener; @@ -183,7 +180,7 @@ public class TabWidget extends LinearLayout implements OnFocusChangeListener { @Override public void childDrawableStateChanged(View child) { - if (child == getChildTabViewAt(mSelectedTab)) { + if (getTabCount() > 0 && child == getChildTabViewAt(mSelectedTab)) { // To make sure that the bottom strip is redrawn invalidate(); } @@ -194,6 +191,9 @@ public class TabWidget extends LinearLayout implements OnFocusChangeListener { public void dispatchDraw(Canvas canvas) { super.dispatchDraw(canvas); + // Do nothing if there are no tabs. + if (getTabCount() == 0) return; + // If the user specified a custom view for the tab indicators, then // do not draw the bottom strips. if (!mDrawBottomStrips) { @@ -310,7 +310,7 @@ public class TabWidget extends LinearLayout implements OnFocusChangeListener { if (child.getLayoutParams() == null) { final LinearLayout.LayoutParams lp = new LayoutParams( 0, - ViewGroup.LayoutParams.FILL_PARENT, 1.0f); + ViewGroup.LayoutParams.MATCH_PARENT, 1.0f); lp.setMargins(0, 0, 0, 0); child.setLayoutParams(lp); } @@ -325,7 +325,7 @@ public class TabWidget extends LinearLayout implements OnFocusChangeListener { ImageView divider = new ImageView(mContext); final LinearLayout.LayoutParams lp = new LayoutParams( mDividerDrawable.getIntrinsicWidth(), - LayoutParams.FILL_PARENT); + LayoutParams.MATCH_PARENT); lp.setMargins(0, 0, 0, 0); divider.setLayoutParams(lp); divider.setBackgroundDrawable(mDividerDrawable); @@ -347,7 +347,7 @@ public class TabWidget extends LinearLayout implements OnFocusChangeListener { } public void onFocusChange(View v, boolean hasFocus) { - if (v == this && hasFocus) { + if (v == this && hasFocus && getTabCount() > 0) { getChildTabViewAt(mSelectedTab).requestFocus(); return; } diff --git a/core/java/android/widget/TableLayout.java b/core/java/android/widget/TableLayout.java index afa2f3b..66500a3 100644 --- a/core/java/android/widget/TableLayout.java +++ b/core/java/android/widget/TableLayout.java @@ -52,7 +52,7 @@ import java.util.regex.Pattern; * {@link #setColumnCollapsed(int,boolean) setColumnCollapsed()}.</p> * * <p>The children of a TableLayout cannot specify the <code>layout_width</code> - * attribute. Width is always <code>FILL_PARENT</code>. However, the + * attribute. Width is always <code>MATCH_PARENT</code>. However, the * <code>layout_height</code> attribute can be defined by a child; default value * is {@link android.widget.TableLayout.LayoutParams#WRAP_CONTENT}. If the child * is a {@link android.widget.TableRow}, then the height is always @@ -621,7 +621,7 @@ public class TableLayout extends LinearLayout { /** * Returns a set of layout parameters with a width of - * {@link android.view.ViewGroup.LayoutParams#FILL_PARENT}, + * {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT}, * and a height of {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}. */ @Override @@ -647,7 +647,7 @@ public class TableLayout extends LinearLayout { /** * <p>This set of layout parameters enforces the width of each child to be - * {@link #FILL_PARENT} and the height of each child to be + * {@link #MATCH_PARENT} and the height of each child to be * {@link #WRAP_CONTENT}, but only if the height is not specified.</p> */ @SuppressWarnings({"UnusedDeclaration"}) @@ -663,14 +663,14 @@ public class TableLayout extends LinearLayout { * {@inheritDoc} */ public LayoutParams(int w, int h) { - super(FILL_PARENT, h); + super(MATCH_PARENT, h); } /** * {@inheritDoc} */ public LayoutParams(int w, int h, float initWeight) { - super(FILL_PARENT, h, initWeight); + super(MATCH_PARENT, h, initWeight); } /** @@ -679,7 +679,7 @@ public class TableLayout extends LinearLayout { * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}.</p> */ public LayoutParams() { - super(FILL_PARENT, WRAP_CONTENT); + super(MATCH_PARENT, WRAP_CONTENT); } /** @@ -698,7 +698,7 @@ public class TableLayout extends LinearLayout { /** * <p>Fixes the row's width to - * {@link android.view.ViewGroup.LayoutParams#FILL_PARENT}; the row's + * {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT}; the row's * height is fixed to * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} if no layout * height is specified.</p> @@ -710,7 +710,7 @@ public class TableLayout extends LinearLayout { @Override protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) { - this.width = FILL_PARENT; + this.width = MATCH_PARENT; if (a.hasValue(heightAttr)) { this.height = a.getLayoutDimension(heightAttr, "layout_height"); } else { diff --git a/core/java/android/widget/TableRow.java b/core/java/android/widget/TableRow.java index 5628cab..abf08bf 100644 --- a/core/java/android/widget/TableRow.java +++ b/core/java/android/widget/TableRow.java @@ -35,7 +35,7 @@ import android.view.ViewDebug; * <p>The children of a TableRow do not need to specify the * <code>layout_width</code> and <code>layout_height</code> attributes in the * XML file. TableRow always enforces those values to be respectively - * {@link android.widget.TableLayout.LayoutParams#FILL_PARENT} and + * {@link android.widget.TableLayout.LayoutParams#MATCH_PARENT} and * {@link android.widget.TableLayout.LayoutParams#WRAP_CONTENT}.</p> * * <p> @@ -299,7 +299,7 @@ public class TableRow extends LinearLayout { case LayoutParams.WRAP_CONTENT: spec = getChildMeasureSpec(widthMeasureSpec, 0, LayoutParams.WRAP_CONTENT); break; - case LayoutParams.FILL_PARENT: + case LayoutParams.MATCH_PARENT: spec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); break; default: @@ -351,7 +351,7 @@ public class TableRow extends LinearLayout { /** * Returns a set of layout parameters with a width of - * {@link android.view.ViewGroup.LayoutParams#FILL_PARENT}, + * {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT}, * a height of {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} and no spanning. */ @Override @@ -451,7 +451,7 @@ public class TableRow extends LinearLayout { * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}.</p> */ public LayoutParams() { - super(FILL_PARENT, WRAP_CONTENT); + super(MATCH_PARENT, WRAP_CONTENT); column = -1; span = 1; } @@ -459,7 +459,7 @@ public class TableRow extends LinearLayout { /** * <p>Puts the view in the specified column.</p> * - * <p>Sets the child width to {@link android.view.ViewGroup.LayoutParams#FILL_PARENT} + * <p>Sets the child width to {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT} * and the child height to * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}.</p> * @@ -490,7 +490,7 @@ public class TableRow extends LinearLayout { if (a.hasValue(widthAttr)) { width = a.getLayoutDimension(widthAttr, "layout_width"); } else { - width = FILL_PARENT; + width = MATCH_PARENT; } // We don't want to force users to specify a layout_height diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 455b593..12e8e29 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -4992,7 +4992,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener final int height = mLayoutParams.height; // If the size of the view does not depend on the size of the text, try to // start the marquee immediately - if (height != LayoutParams.WRAP_CONTENT && height != LayoutParams.FILL_PARENT) { + if (height != LayoutParams.WRAP_CONTENT && height != LayoutParams.MATCH_PARENT) { startMarquee(); } else { // Defer the start of the marquee until we know our width (see setFrame()) @@ -5307,7 +5307,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (desiredHeight != this.getHeight()) { sizeChanged = true; } - } else if (mLayoutParams.height == LayoutParams.FILL_PARENT) { + } else if (mLayoutParams.height == LayoutParams.MATCH_PARENT) { if (mDesiredHeightAtMeasure >= 0) { int desiredHeight = getDesiredHeight(); @@ -5354,7 +5354,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (mEllipsize != TextUtils.TruncateAt.MARQUEE) { // In a fixed-height view, so use our new text layout. if (mLayoutParams.height != LayoutParams.WRAP_CONTENT && - mLayoutParams.height != LayoutParams.FILL_PARENT) { + mLayoutParams.height != LayoutParams.MATCH_PARENT) { invalidate(); return; } diff --git a/core/java/android/widget/TimePicker.java b/core/java/android/widget/TimePicker.java index ab4edc5..b87e278 100644 --- a/core/java/android/widget/TimePicker.java +++ b/core/java/android/widget/TimePicker.java @@ -25,7 +25,7 @@ import android.view.LayoutInflater; import android.view.View; import com.android.internal.R; -import com.android.internal.widget.NumberPicker; +import com.android.common.widget.NumberPicker; import java.text.DateFormatSymbols; import java.util.Calendar; @@ -357,4 +357,3 @@ public class TimePicker extends FrameLayout { mOnTimeChangedListener.onTimeChanged(this, getCurrentHour(), getCurrentMinute()); } } - diff --git a/core/java/android/widget/ViewSwitcher.java b/core/java/android/widget/ViewSwitcher.java index 0dcaf95..71ae624 100644 --- a/core/java/android/widget/ViewSwitcher.java +++ b/core/java/android/widget/ViewSwitcher.java @@ -80,7 +80,7 @@ public class ViewSwitcher extends ViewAnimator { View child = mFactory.makeView(); LayoutParams lp = (LayoutParams) child.getLayoutParams(); if (lp == null) { - lp = new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.WRAP_CONTENT); + lp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); } addView(child, lp); return child; diff --git a/core/java/android/widget/ZoomButtonsController.java b/core/java/android/widget/ZoomButtonsController.java index e55fbb8..bea009c 100644 --- a/core/java/android/widget/ZoomButtonsController.java +++ b/core/java/android/widget/ZoomButtonsController.java @@ -247,7 +247,7 @@ public class ZoomButtonsController implements View.OnTouchListener { LayoutParams.FLAG_LAYOUT_NO_LIMITS | LayoutParams.FLAG_ALT_FOCUSABLE_IM; lp.height = LayoutParams.WRAP_CONTENT; - lp.width = LayoutParams.FILL_PARENT; + lp.width = LayoutParams.MATCH_PARENT; lp.type = LayoutParams.TYPE_APPLICATION_PANEL; lp.format = PixelFormat.TRANSLUCENT; lp.windowAnimations = com.android.internal.R.style.Animation_ZoomButtons; diff --git a/core/java/com/android/internal/app/AlertController.java b/core/java/com/android/internal/app/AlertController.java index 57dbb44..f56b15c 100644 --- a/core/java/com/android/internal/app/AlertController.java +++ b/core/java/com/android/internal/app/AlertController.java @@ -16,7 +16,7 @@ package com.android.internal.app; -import static android.view.ViewGroup.LayoutParams.FILL_PARENT; +import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; @@ -361,7 +361,7 @@ public class AlertController { if (mView != null) { customPanel = (FrameLayout) mWindow.findViewById(R.id.customPanel); FrameLayout custom = (FrameLayout) mWindow.findViewById(R.id.custom); - custom.addView(mView, new LayoutParams(FILL_PARENT, FILL_PARENT)); + custom.addView(mView, new LayoutParams(MATCH_PARENT, MATCH_PARENT)); if (mViewSpacingSpecified) { custom.setPadding(mViewSpacingLeft, mViewSpacingTop, mViewSpacingRight, mViewSpacingBottom); @@ -391,7 +391,7 @@ public class AlertController { if (mCustomTitleView != null) { // Add the custom title view directly to the topPanel layout LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( - LinearLayout.LayoutParams.FILL_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); + LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); topPanel.addView(mCustomTitleView, lp); @@ -460,8 +460,8 @@ public class AlertController { if (mListView != null) { contentPanel.removeView(mWindow.findViewById(R.id.scrollView)); contentPanel.addView(mListView, - new LinearLayout.LayoutParams(FILL_PARENT, FILL_PARENT)); - contentPanel.setLayoutParams(new LinearLayout.LayoutParams(FILL_PARENT, 0, 1.0f)); + new LinearLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)); + contentPanel.setLayoutParams(new LinearLayout.LayoutParams(MATCH_PARENT, 0, 1.0f)); } else { contentPanel.setVisibility(View.GONE); } @@ -657,8 +657,8 @@ public class AlertController { ViewGroup parent = (ViewGroup) mWindow.findViewById(R.id.parentPanel); parent.removeView(buttonPanel); AbsListView.LayoutParams params = new AbsListView.LayoutParams( - AbsListView.LayoutParams.FILL_PARENT, - AbsListView.LayoutParams.FILL_PARENT); + AbsListView.LayoutParams.MATCH_PARENT, + AbsListView.LayoutParams.MATCH_PARENT); buttonPanel.setLayoutParams(params); mListView.addFooterView(buttonPanel); */ diff --git a/core/java/com/android/internal/app/ExternalMediaFormatActivity.java b/core/java/com/android/internal/app/ExternalMediaFormatActivity.java index 000f6c4..2b07ae6 100644 --- a/core/java/com/android/internal/app/ExternalMediaFormatActivity.java +++ b/core/java/com/android/internal/app/ExternalMediaFormatActivity.java @@ -102,7 +102,7 @@ public class ExternalMediaFormatActivity extends AlertActivity implements Dialog .getService("mount")); if (mountService != null) { try { - mountService.formatMedia(Environment.getExternalStorageDirectory().toString()); + mountService.formatVolume(Environment.getExternalStorageDirectory().toString()); } catch (RemoteException e) { } } diff --git a/core/java/com/android/internal/database/ArrayListCursor.java b/core/java/com/android/internal/database/ArrayListCursor.java deleted file mode 100644 index 2e1d8f1..0000000 --- a/core/java/com/android/internal/database/ArrayListCursor.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Copyright (C) 2006 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.internal.database; - -import android.database.AbstractCursor; -import android.database.CursorWindow; - -import java.lang.System; -import java.util.ArrayList; - -/** - * A convenience class that presents a two-dimensional ArrayList - * as a Cursor. - */ -public class ArrayListCursor extends AbstractCursor { - private String[] mColumnNames; - private ArrayList<Object>[] mRows; - - @SuppressWarnings({"unchecked"}) - public ArrayListCursor(String[] columnNames, ArrayList<ArrayList> rows) { - int colCount = columnNames.length; - boolean foundID = false; - // Add an _id column if not in columnNames - for (int i = 0; i < colCount; ++i) { - if (columnNames[i].compareToIgnoreCase("_id") == 0) { - mColumnNames = columnNames; - foundID = true; - break; - } - } - - if (!foundID) { - mColumnNames = new String[colCount + 1]; - System.arraycopy(columnNames, 0, mColumnNames, 0, columnNames.length); - mColumnNames[colCount] = "_id"; - } - - int rowCount = rows.size(); - mRows = new ArrayList[rowCount]; - - for (int i = 0; i < rowCount; ++i) { - mRows[i] = rows.get(i); - if (!foundID) { - mRows[i].add(i); - } - } - } - - @Override - public void fillWindow(int position, CursorWindow window) { - if (position < 0 || position > getCount()) { - return; - } - - window.acquireReference(); - try { - int oldpos = mPos; - mPos = position - 1; - window.clear(); - window.setStartPosition(position); - int columnNum = getColumnCount(); - window.setNumColumns(columnNum); - while (moveToNext() && window.allocRow()) { - for (int i = 0; i < columnNum; i++) { - final Object data = mRows[mPos].get(i); - if (data != null) { - if (data instanceof byte[]) { - byte[] field = (byte[]) data; - if (!window.putBlob(field, mPos, i)) { - window.freeLastRow(); - break; - } - } else { - String field = data.toString(); - if (!window.putString(field, mPos, i)) { - window.freeLastRow(); - break; - } - } - } else { - if (!window.putNull(mPos, i)) { - window.freeLastRow(); - break; - } - } - } - } - - mPos = oldpos; - } catch (IllegalStateException e){ - // simply ignore it - } finally { - window.releaseReference(); - } - } - - @Override - public int getCount() { - return mRows.length; - } - - @Override - public boolean deleteRow() { - return false; - } - - @Override - public String[] getColumnNames() { - return mColumnNames; - } - - @Override - public byte[] getBlob(int columnIndex) { - return (byte[]) mRows[mPos].get(columnIndex); - } - - @Override - public String getString(int columnIndex) { - Object cell = mRows[mPos].get(columnIndex); - return (cell == null) ? null : cell.toString(); - } - - @Override - public short getShort(int columnIndex) { - Number num = (Number) mRows[mPos].get(columnIndex); - return num.shortValue(); - } - - @Override - public int getInt(int columnIndex) { - Number num = (Number) mRows[mPos].get(columnIndex); - return num.intValue(); - } - - @Override - public long getLong(int columnIndex) { - Number num = (Number) mRows[mPos].get(columnIndex); - return num.longValue(); - } - - @Override - public float getFloat(int columnIndex) { - Number num = (Number) mRows[mPos].get(columnIndex); - return num.floatValue(); - } - - @Override - public double getDouble(int columnIndex) { - Number num = (Number) mRows[mPos].get(columnIndex); - return num.doubleValue(); - } - - @Override - public boolean isNull(int columnIndex) { - return mRows[mPos].get(columnIndex) == null; - } -} diff --git a/core/java/com/android/internal/logging/AndroidHandler.java b/core/java/com/android/internal/logging/AndroidHandler.java index c4a1479..12f6a4f 100644 --- a/core/java/com/android/internal/logging/AndroidHandler.java +++ b/core/java/com/android/internal/logging/AndroidHandler.java @@ -17,12 +17,16 @@ package com.android.internal.logging; import android.util.Log; +import dalvik.system.DalvikLogging; +import dalvik.system.DalvikLogHandler; -import java.util.logging.*; -import java.util.Date; -import java.text.MessageFormat; import java.io.PrintWriter; import java.io.StringWriter; +import java.util.logging.Formatter; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.logging.Logger; /** * Implements a {@link java.util.logging.Logger} handler that writes to the Android log. The @@ -77,7 +81,7 @@ import java.io.StringWriter; * </tr> * </table> */ -public class AndroidHandler extends Handler { +public class AndroidHandler extends Handler implements DalvikLogHandler { /** * Holds the formatter for all Android log handlers. */ @@ -118,33 +122,13 @@ public class AndroidHandler extends Handler { @Override public void publish(LogRecord record) { - try { - int level = getAndroidLevel(record.getLevel()); - String tag = record.getLoggerName(); - - if (tag == null) { - // Anonymous logger. - tag = "null"; - } else { - // Tags must be <= 23 characters. - int length = tag.length(); - if (length > 23) { - // Most loggers use the full class name. Try dropping the - // package. - int lastPeriod = tag.lastIndexOf("."); - if (length - lastPeriod - 1 <= 23) { - tag = tag.substring(lastPeriod + 1); - } else { - // Use last 23 chars. - tag = tag.substring(tag.length() - 23); - } - } - } - - if (!Log.isLoggable(tag, level)) { - return; - } + int level = getAndroidLevel(record.getLevel()); + String tag = DalvikLogging.loggerNameToTag(record.getLoggerName()); + if (!Log.isLoggable(tag, level)) { + return; + } + try { String message = getFormatter().format(record); Log.println(level, tag, message); } catch (RuntimeException e) { @@ -152,12 +136,26 @@ public class AndroidHandler extends Handler { } } + public void publish(Logger source, String tag, Level level, String message) { + // TODO: avoid ducking into native 2x; we aren't saving any formatter calls + int priority = getAndroidLevel(level); + if (!Log.isLoggable(tag, priority)) { + return; + } + + try { + Log.println(priority, tag, message); + } catch (RuntimeException e) { + Log.e("AndroidHandler", "Error logging message.", e); + } + } + /** * Converts a {@link java.util.logging.Logger} logging level into an Android one. - * + * * @param level The {@link java.util.logging.Logger} logging level. - * - * @return The resulting Android logging level. + * + * @return The resulting Android logging level. */ static int getAndroidLevel(Level level) { int value = level.intValue(); @@ -171,5 +169,4 @@ public class AndroidHandler extends Handler { return Log.DEBUG; } } - } diff --git a/core/java/com/android/internal/net/DbSSLSessionCache.java b/core/java/com/android/internal/net/DbSSLSessionCache.java deleted file mode 100644 index 842d40b..0000000 --- a/core/java/com/android/internal/net/DbSSLSessionCache.java +++ /dev/null @@ -1,289 +0,0 @@ -// Copyright 2009 The Android Open Source Project - -package com.android.internal.net; - -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.database.SQLException; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteOpenHelper; -import android.util.Log; - -import org.apache.commons.codec.binary.Base64; -import org.apache.harmony.xnet.provider.jsse.SSLClientSessionCache; - -import java.util.HashMap; -import java.util.Map; - -import javax.net.ssl.SSLSession; - -/** - * Hook into harmony SSL cache to persist the SSL sessions. - * - * Current implementation is suitable for saving a small number of hosts - - * like google services. It can be extended with expiration and more features - * to support more hosts. - * - * {@hide} - */ -public class DbSSLSessionCache implements SSLClientSessionCache { - private static final String TAG = "DbSSLSessionCache"; - - /** - * Table where sessions are stored. - */ - public static final String SSL_CACHE_TABLE = "ssl_sessions"; - - private static final String SSL_CACHE_ID = "_id"; - - /** - * Key is host:port - port is not optional. - */ - private static final String SSL_CACHE_HOSTPORT = "hostport"; - - /** - * Base64-encoded DER value of the session. - */ - private static final String SSL_CACHE_SESSION = "session"; - - /** - * Time when the record was added - should be close to the time - * of the initial session negotiation. - */ - private static final String SSL_CACHE_TIME_SEC = "time_sec"; - - public static final String DATABASE_NAME = "ssl_sessions.db"; - - public static final int DATABASE_VERSION = 2; - - /** public for testing - */ - public static final int SSL_CACHE_ID_COL = 0; - public static final int SSL_CACHE_HOSTPORT_COL = 1; - public static final int SSL_CACHE_SESSION_COL = 2; - public static final int SSL_CACHE_TIME_SEC_COL = 3; - - public static final int MAX_CACHE_SIZE = 256; - - private final Map<String, byte[]> mExternalCache = - new HashMap<String, byte[]>(); - - - private DatabaseHelper mDatabaseHelper; - - private boolean mNeedsCacheLoad = true; - - public static final String[] PROJECTION = new String[] { - SSL_CACHE_ID, - SSL_CACHE_HOSTPORT, - SSL_CACHE_SESSION, - SSL_CACHE_TIME_SEC - }; - - private static final Map<String,DbSSLSessionCache> sInstances = - new HashMap<String,DbSSLSessionCache>(); - - /** - * Returns a singleton instance of the DbSSLSessionCache that should be used for this - * context's package. - * - * @param context The context that should be used for getting/creating the singleton instance. - * @return The singleton instance for the context's package. - */ - public static synchronized DbSSLSessionCache getInstanceForPackage(Context context) { - String packageName = context.getPackageName(); - if (sInstances.containsKey(packageName)) { - return sInstances.get(packageName); - } - DbSSLSessionCache cache = new DbSSLSessionCache(context); - sInstances.put(packageName, cache); - return cache; - } - - /** - * Create a SslSessionCache instance, using the specified context to - * initialize the database. - * - * This constructor will use the default database - created for the application - * context. - * - * @param activityContext - */ - private DbSSLSessionCache(Context activityContext) { - Context appContext = activityContext.getApplicationContext(); - mDatabaseHelper = new DatabaseHelper(appContext); - } - - /** - * Create a SslSessionCache that uses a specific database. - * - * - * @param database - */ - public DbSSLSessionCache(DatabaseHelper database) { - this.mDatabaseHelper = database; - } - - public void putSessionData(SSLSession session, byte[] der) { - if (mDatabaseHelper == null) { - return; - } - synchronized (this.getClass()) { - SQLiteDatabase db = mDatabaseHelper.getWritableDatabase(); - if (mExternalCache.size() == MAX_CACHE_SIZE) { - // remove oldest. - // TODO: check if the new one is in cached already ( i.e. update ). - Cursor byTime = mDatabaseHelper.getReadableDatabase().query(SSL_CACHE_TABLE, - PROJECTION, null, null, null, null, SSL_CACHE_TIME_SEC); - if (byTime.moveToFirst()) { - // TODO: can I do byTime.deleteRow() ? - String hostPort = byTime.getString(SSL_CACHE_HOSTPORT_COL); - db.delete(SSL_CACHE_TABLE, - SSL_CACHE_HOSTPORT + "= ?" , new String[] { hostPort }); - mExternalCache.remove(hostPort); - } else { - Log.w(TAG, "No rows found"); - // something is wrong, clear it - clear(); - } - } - // Serialize native session to standard DER encoding - long t0 = System.currentTimeMillis(); - - String b64 = new String(Base64.encodeBase64(der)); - String key = session.getPeerHost() + ":" + session.getPeerPort(); - - ContentValues values = new ContentValues(); - values.put(SSL_CACHE_HOSTPORT, key); - values.put(SSL_CACHE_SESSION, b64); - values.put(SSL_CACHE_TIME_SEC, System.currentTimeMillis() / 1000); - - mExternalCache.put(key, der); - - try { - db.insert(SSL_CACHE_TABLE, null /*nullColumnHack */ , values); - } catch(SQLException ex) { - // Ignore - nothing we can do to recover, and caller shouldn't - // be affected. - Log.w(TAG, "Ignoring SQL exception when caching session", ex); - } - if (Log.isLoggable(TAG, Log.DEBUG)) { - long t1 = System.currentTimeMillis(); - Log.d(TAG, "New SSL session " + session.getPeerHost() + - " DER len: " + der.length + " " + (t1 - t0)); - } - } - - } - - public byte[] getSessionData(String host, int port) { - // Current (simple) implementation does a single lookup to DB, then saves - // all entries to the cache. - - // This works for google services - i.e. small number of certs. - // If we extend this to all processes - we should hold a separate cache - // or do lookups to DB each time. - if (mDatabaseHelper == null) { - return null; - } - synchronized(this.getClass()) { - if (mNeedsCacheLoad) { - // Don't try to load again, if something is wrong on the first - // request it'll likely be wrong each time. - mNeedsCacheLoad = false; - long t0 = System.currentTimeMillis(); - - Cursor cur = null; - try { - cur = mDatabaseHelper.getReadableDatabase().query(SSL_CACHE_TABLE, - PROJECTION, null, null, null, null, null); - if (cur.moveToFirst()) { - do { - String hostPort = cur.getString(SSL_CACHE_HOSTPORT_COL); - String value = cur.getString(SSL_CACHE_SESSION_COL); - - if (hostPort == null || value == null) { - continue; - } - // TODO: blob support ? - byte[] der = Base64.decodeBase64(value.getBytes()); - mExternalCache.put(hostPort, der); - } while (cur.moveToNext()); - - } - } catch (SQLException ex) { - Log.d(TAG, "Error loading SSL cached entries ", ex); - } finally { - if (cur != null) { - cur.close(); - } - if (Log.isLoggable(TAG, Log.DEBUG)) { - long t1 = System.currentTimeMillis(); - Log.d(TAG, "LOADED CACHED SSL " + (t1 - t0) + " ms"); - } - } - } - - String key = host + ":" + port; - - return mExternalCache.get(key); - } - } - - /** - * Reset the database and internal state. - * Used for testing or to free space. - */ - public void clear() { - synchronized(this) { - try { - mExternalCache.clear(); - mNeedsCacheLoad = true; - mDatabaseHelper.getWritableDatabase().delete(SSL_CACHE_TABLE, - null, null); - } catch (SQLException ex) { - Log.d(TAG, "Error removing SSL cached entries ", ex); - // ignore - nothing we can do about it - } - } - } - - public byte[] getSessionData(byte[] id) { - // We support client side only - the cache will do nothing for - // server-side sessions. - return null; - } - - /** Visible for testing. - */ - public static class DatabaseHelper extends SQLiteOpenHelper { - - public DatabaseHelper(Context context) { - super(context, DATABASE_NAME, null /* factory */, DATABASE_VERSION); - } - - @Override - public void onCreate(SQLiteDatabase db) { - db.execSQL("CREATE TABLE " + SSL_CACHE_TABLE + " (" + - SSL_CACHE_ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + - SSL_CACHE_HOSTPORT + " TEXT UNIQUE ON CONFLICT REPLACE," + - SSL_CACHE_SESSION + " TEXT," + - SSL_CACHE_TIME_SEC + " INTEGER" + - ");"); - - // No index - we load on startup, index would slow down inserts. - // If we want to scale this to lots of rows - we could use - // index, but then we'll hit DB a bit too often ( including - // negative hits ) - } - - @Override - public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { - db.execSQL("DROP TABLE IF EXISTS " + SSL_CACHE_TABLE ); - onCreate(db); - } - - } - -} diff --git a/core/java/com/android/internal/os/BatteryStatsImpl.java b/core/java/com/android/internal/os/BatteryStatsImpl.java index 5199ada..e964a8f 100644 --- a/core/java/com/android/internal/os/BatteryStatsImpl.java +++ b/core/java/com/android/internal/os/BatteryStatsImpl.java @@ -17,8 +17,8 @@ package com.android.internal.os; import android.bluetooth.BluetoothHeadset; +import android.net.TrafficStats; import android.os.BatteryStats; -import android.os.NetStat; import android.os.Parcel; import android.os.ParcelFormatException; import android.os.Parcelable; @@ -1022,8 +1022,8 @@ public final class BatteryStatsImpl extends BatteryStats { public void doUnplug(long batteryUptime, long batteryRealtime) { for (int iu = mUidStats.size() - 1; iu >= 0; iu--) { Uid u = mUidStats.valueAt(iu); - u.mStartedTcpBytesReceived = NetStat.getUidRxBytes(u.mUid); - u.mStartedTcpBytesSent = NetStat.getUidTxBytes(u.mUid); + u.mStartedTcpBytesReceived = TrafficStats.getUidRxBytes(u.mUid); + u.mStartedTcpBytesSent = TrafficStats.getUidTxBytes(u.mUid); u.mTcpBytesReceivedAtLastUnplug = u.mCurrentTcpBytesReceived; u.mTcpBytesSentAtLastUnplug = u.mCurrentTcpBytesSent; } @@ -1031,10 +1031,10 @@ public final class BatteryStatsImpl extends BatteryStats { mUnpluggables.get(i).unplug(batteryUptime, batteryRealtime); } // Track total mobile data - doDataUnplug(mMobileDataRx, NetStat.getMobileRxBytes()); - doDataUnplug(mMobileDataTx, NetStat.getMobileTxBytes()); - doDataUnplug(mTotalDataRx, NetStat.getTotalRxBytes()); - doDataUnplug(mTotalDataTx, NetStat.getTotalTxBytes()); + doDataUnplug(mMobileDataRx, TrafficStats.getMobileRxBytes()); + doDataUnplug(mMobileDataTx, TrafficStats.getMobileTxBytes()); + doDataUnplug(mTotalDataRx, TrafficStats.getTotalRxBytes()); + doDataUnplug(mTotalDataTx, TrafficStats.getTotalTxBytes()); // Track radio awake time mRadioDataStart = getCurrentRadioDataUptime(); mRadioDataUptime = 0; @@ -1058,10 +1058,10 @@ public final class BatteryStatsImpl extends BatteryStats { for (int i = mUnpluggables.size() - 1; i >= 0; i--) { mUnpluggables.get(i).plug(batteryUptime, batteryRealtime); } - doDataPlug(mMobileDataRx, NetStat.getMobileRxBytes()); - doDataPlug(mMobileDataTx, NetStat.getMobileTxBytes()); - doDataPlug(mTotalDataRx, NetStat.getTotalRxBytes()); - doDataPlug(mTotalDataTx, NetStat.getTotalTxBytes()); + doDataPlug(mMobileDataRx, TrafficStats.getMobileRxBytes()); + doDataPlug(mMobileDataTx, TrafficStats.getMobileTxBytes()); + doDataPlug(mTotalDataRx, TrafficStats.getTotalRxBytes()); + doDataPlug(mTotalDataTx, TrafficStats.getTotalTxBytes()); // Track radio awake time mRadioDataUptime = getRadioDataUptime(); mRadioDataStart = -1; @@ -1519,7 +1519,7 @@ public final class BatteryStatsImpl extends BatteryStats { public long computeCurrentTcpBytesReceived() { return mCurrentTcpBytesReceived + (mStartedTcpBytesReceived >= 0 - ? (NetStat.getUidRxBytes(mUid) - mStartedTcpBytesReceived) : 0); + ? (TrafficStats.getUidRxBytes(mUid) - mStartedTcpBytesReceived) : 0); } @Override @@ -1696,7 +1696,7 @@ public final class BatteryStatsImpl extends BatteryStats { public long computeCurrentTcpBytesSent() { return mCurrentTcpBytesSent + (mStartedTcpBytesSent >= 0 - ? (NetStat.getUidTxBytes(mUid) - mStartedTcpBytesSent) : 0); + ? (TrafficStats.getUidTxBytes(mUid) - mStartedTcpBytesSent) : 0); } void writeToParcelLocked(Parcel out, long batteryRealtime) { @@ -2919,22 +2919,22 @@ public final class BatteryStatsImpl extends BatteryStats { /** Only STATS_UNPLUGGED works properly */ public long getMobileTcpBytesSent(int which) { - return getTcpBytes(NetStat.getMobileTxBytes(), mMobileDataTx, which); + return getTcpBytes(TrafficStats.getMobileTxBytes(), mMobileDataTx, which); } /** Only STATS_UNPLUGGED works properly */ public long getMobileTcpBytesReceived(int which) { - return getTcpBytes(NetStat.getMobileRxBytes(), mMobileDataRx, which); + return getTcpBytes(TrafficStats.getMobileRxBytes(), mMobileDataRx, which); } /** Only STATS_UNPLUGGED works properly */ public long getTotalTcpBytesSent(int which) { - return getTcpBytes(NetStat.getTotalTxBytes(), mTotalDataTx, which); + return getTcpBytes(TrafficStats.getTotalTxBytes(), mTotalDataTx, which); } /** Only STATS_UNPLUGGED works properly */ public long getTotalTcpBytesReceived(int which) { - return getTcpBytes(NetStat.getTotalRxBytes(), mTotalDataRx, which); + return getTcpBytes(TrafficStats.getTotalRxBytes(), mTotalDataRx, which); } @Override diff --git a/core/java/com/android/internal/os/BinderInternal.java b/core/java/com/android/internal/os/BinderInternal.java index eacf0ce..ba0bf0d 100644 --- a/core/java/com/android/internal/os/BinderInternal.java +++ b/core/java/com/android/internal/os/BinderInternal.java @@ -76,6 +76,13 @@ public class BinderInternal { */ public static final native IBinder getContextObject(); + /** + * Special for system process to not allow incoming calls to run at + * background scheduling priority. + * @hide + */ + public static final native void disableBackgroundScheduling(boolean disable); + static native final void handleGc(); public static void forceGc(String reason) { diff --git a/core/java/com/android/internal/os/IDropBoxManagerService.aidl b/core/java/com/android/internal/os/IDropBoxManagerService.aidl new file mode 100644 index 0000000..d067926 --- /dev/null +++ b/core/java/com/android/internal/os/IDropBoxManagerService.aidl @@ -0,0 +1,42 @@ +/* + * 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.internal.os; + +import android.os.DropBoxManager; +import android.os.ParcelFileDescriptor; + +/** + * "Backend" interface used by {@link android.os.DropBoxManager} to talk to the + * DropBoxManagerService that actually implements the drop box functionality. + * + * @see DropBoxManager + * @hide + */ +interface IDropBoxManagerService { + /** + * @see DropBoxManager#addText + * @see DropBoxManager#addData + * @see DropBoxManager#addFile + */ + void add(in DropBoxManager.Entry entry); + + /** @see DropBoxManager#getNextEntry */ + boolean isTagEnabled(String tag); + + /** @see DropBoxManager#getNextEntry */ + DropBoxManager.Entry getNextEntry(String tag, long millis); +} diff --git a/core/java/com/android/internal/os/LoggingPrintStream.java b/core/java/com/android/internal/os/LoggingPrintStream.java index b3d6f20..451340b 100644 --- a/core/java/com/android/internal/os/LoggingPrintStream.java +++ b/core/java/com/android/internal/os/LoggingPrintStream.java @@ -16,11 +16,17 @@ package com.android.internal.os; -import java.io.PrintStream; -import java.io.OutputStream; import java.io.IOException; -import java.util.Locale; +import java.io.OutputStream; +import java.io.PrintStream; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CoderResult; +import java.nio.charset.CodingErrorAction; import java.util.Formatter; +import java.util.Locale; /** * A print stream which logs output line by line. @@ -31,6 +37,27 @@ abstract class LoggingPrintStream extends PrintStream { private final StringBuilder builder = new StringBuilder(); + /** + * A buffer that is initialized when raw bytes are first written to this + * stream. It may contain the leading bytes of multi-byte characters. + * Between writes this buffer is always ready to receive data; ie. the + * position is at the first unassigned byte and the limit is the capacity. + */ + private ByteBuffer encodedBytes; + + /** + * A buffer that is initialized when raw bytes are first written to this + * stream. Between writes this buffer is always clear; ie. the position is + * zero and the limit is the capacity. + */ + private CharBuffer decodedChars; + + /** + * Decodes bytes to characters using the system default charset. Initialized + * when raw bytes are first written to this stream. + */ + private CharsetDecoder decoder; + protected LoggingPrintStream() { super(new OutputStream() { public void write(int oneByte) throws IOException { @@ -80,20 +107,48 @@ abstract class LoggingPrintStream extends PrintStream { } } - /* - * We have no idea of how these bytes are encoded, so just ignore them. - */ - - /** Ignored. */ - public void write(int oneByte) {} + public void write(int oneByte) { + write(new byte[] { (byte) oneByte }, 0, 1); + } - /** Ignored. */ @Override - public void write(byte buffer[]) {} + public void write(byte[] buffer) { + write(buffer, 0, buffer.length); + } - /** Ignored. */ @Override - public void write(byte bytes[], int start, int count) {} + public synchronized void write(byte bytes[], int start, int count) { + if (decoder == null) { + encodedBytes = ByteBuffer.allocate(80); + decodedChars = CharBuffer.allocate(80); + decoder = Charset.defaultCharset().newDecoder() + .onMalformedInput(CodingErrorAction.REPLACE) + .onUnmappableCharacter(CodingErrorAction.REPLACE); + } + + int end = start + count; + while (start < end) { + // copy some bytes from the array to the long-lived buffer. This + // way, if we end with a partial character we don't lose it. + int numBytes = Math.min(encodedBytes.remaining(), end - start); + encodedBytes.put(bytes, start, numBytes); + start += numBytes; + + encodedBytes.flip(); + CoderResult coderResult; + do { + // decode bytes from the byte buffer into the char buffer + coderResult = decoder.decode(encodedBytes, decodedChars, false); + + // copy chars from the char buffer into our string builder + decodedChars.flip(); + builder.append(decodedChars); + decodedChars.clear(); + } while (coderResult.isOverflow()); + encodedBytes.compact(); + } + flush(false); + } /** Always returns false. */ @Override diff --git a/core/java/com/android/internal/os/PowerProfile.java b/core/java/com/android/internal/os/PowerProfile.java index 2369d25..9e5bdff 100644 --- a/core/java/com/android/internal/os/PowerProfile.java +++ b/core/java/com/android/internal/os/PowerProfile.java @@ -20,7 +20,7 @@ package com.android.internal.os; import android.content.Context; import android.content.res.XmlResourceParser; -import com.android.internal.util.XmlUtils; +import com.android.common.XmlUtils; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; diff --git a/core/java/com/android/internal/os/RecoverySystem.java b/core/java/com/android/internal/os/RecoverySystem.java deleted file mode 100644 index c938610..0000000 --- a/core/java/com/android/internal/os/RecoverySystem.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * 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.internal.os; - -import android.os.FileUtils; -import android.os.Power; -import android.util.Log; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileWriter; -import java.io.IOException; -import java.util.Iterator; -import java.util.Map; - -/** - * Utility class for interacting with the Android recovery partition. - * The recovery partition is a small standalone system which can perform - * operations that are difficult while the main system is running, like - * upgrading system software or reformatting the data partition. - * Note that most of these operations must be run as root. - * - * @hide - */ -public class RecoverySystem { - private static final String TAG = "RecoverySystem"; // for logging - - // Used to communicate with recovery. See commands/recovery/recovery.c. - private static File RECOVERY_DIR = new File("/cache/recovery"); - private static File COMMAND_FILE = new File(RECOVERY_DIR, "command"); - private static File LOG_FILE = new File(RECOVERY_DIR, "log"); - - // Length limits for reading files. - private static int LOG_FILE_MAX_LENGTH = 8 * 1024; - - /** - * Reboot into the recovery system to install a system update. - * @param update package to install (must be in /cache or /data). - * @throws IOException if something goes wrong. - */ - public static void rebootAndUpdate(File update) throws IOException { - String path = update.getCanonicalPath(); - if (path.startsWith("/cache/")) { - path = "CACHE:" + path.substring(7); - } else if (path.startsWith("/data/")) { - path = "DATA:" + path.substring(6); - } else { - throw new IllegalArgumentException( - "Must start with /cache or /data: " + path); - } - bootCommand("--update_package=" + path); - } - - /** - * Reboot into the recovery system to wipe the /data partition. - * @param extras to add to the RECOVERY_COMPLETED intent after rebooting. - * @throws IOException if something goes wrong. - */ - public static void rebootAndWipe() throws IOException { - bootCommand("--wipe_data"); - } - - /** - * Reboot into the recovery system with the supplied argument. - * @param arg to pass to the recovery utility. - * @throws IOException if something goes wrong. - */ - private static void bootCommand(String arg) throws IOException { - RECOVERY_DIR.mkdirs(); // In case we need it - COMMAND_FILE.delete(); // In case it's not writable - LOG_FILE.delete(); - - FileWriter command = new FileWriter(COMMAND_FILE); - try { - command.write(arg); - command.write("\n"); - } finally { - command.close(); - } - - // Having written the command file, go ahead and reboot - Power.reboot("recovery"); - throw new IOException("Reboot failed (no permissions?)"); - } - - /** - * Called after booting to process and remove recovery-related files. - * @return the log file from recovery, or null if none was found. - */ - public static String handleAftermath() { - // Record the tail of the LOG_FILE - String log = null; - try { - log = FileUtils.readTextFile(LOG_FILE, -LOG_FILE_MAX_LENGTH, "...\n"); - } catch (FileNotFoundException e) { - Log.i(TAG, "No recovery log file"); - } catch (IOException e) { - Log.e(TAG, "Error reading recovery log", e); - } - - // Delete everything in RECOVERY_DIR - String[] names = RECOVERY_DIR.list(); - for (int i = 0; names != null && i < names.length; i++) { - File f = new File(RECOVERY_DIR, names[i]); - if (!f.delete()) { - Log.e(TAG, "Can't delete: " + f); - } else { - Log.i(TAG, "Deleted: " + f); - } - } - - return log; - } -} diff --git a/core/java/com/android/internal/os/RuntimeInit.java b/core/java/com/android/internal/os/RuntimeInit.java index c782c8c..57a28e6 100644 --- a/core/java/com/android/internal/os/RuntimeInit.java +++ b/core/java/com/android/internal/os/RuntimeInit.java @@ -17,7 +17,9 @@ package com.android.internal.os; import android.app.ActivityManagerNative; +import android.app.ApplicationErrorReport; import android.app.IActivityManager; +import android.os.Build; import android.os.Debug; import android.os.IBinder; import android.os.ICheckinService; @@ -25,8 +27,6 @@ import android.os.Process; import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemProperties; -import android.os.Build; -import android.server.data.CrashData; import android.util.Config; import android.util.Log; @@ -58,6 +58,10 @@ public class RuntimeInit { /** true if commonInit() has been called */ private static boolean initialized; + private static IBinder mApplicationObject; + + private static volatile boolean mCrashing = false; + /** * Use this to log a message when a thread exits due to an uncaught * exception. The framework catches these for the main threads, so @@ -66,14 +70,30 @@ public class RuntimeInit { private static class UncaughtHandler implements Thread.UncaughtExceptionHandler { public void uncaughtException(Thread t, Throwable e) { try { - Log.e(TAG, "Uncaught handler: thread " + t.getName() - + " exiting due to uncaught exception"); - } catch (Throwable error) { - // Ignore the throwable, since we're in the process of crashing anyway. - // If we don't, the crash won't happen properly and the process will - // be left around in a bad state. + // Don't re-enter -- avoid infinite loops if crash-reporting crashes. + if (mCrashing) return; + mCrashing = true; + + if (mApplicationObject == null) { + Log.e(TAG, "*** FATAL EXCEPTION IN SYSTEM PROCESS: " + t.getName(), e); + } else { + Log.e(TAG, "FATAL EXCEPTION: " + t.getName(), e); + } + + // Bring up crash dialog, wait for it to be dismissed + ActivityManagerNative.getDefault().handleApplicationCrash( + mApplicationObject, new ApplicationErrorReport.CrashInfo(e)); + } catch (Throwable t2) { + try { + Log.e(TAG, "Error reporting crash", t2); + } catch (Throwable t3) { + // Even Log.e() fails! Oh well. + } + } finally { + // Try everything to make sure this process goes away. + Process.killProcess(Process.myPid()); + System.exit(10); } - crash(TAG, e); } } @@ -300,80 +320,22 @@ public class RuntimeInit { public static native int getQwertyKeyboard(); /** - * Report a fatal error in the current process. If this is a user-process, - * a dialog may be displayed informing the user of the error. This - * function does not return; it forces the current process to exit. + * Report a serious error in the current process. May or may not cause + * the process to terminate (depends on system settings). * - * @param tag to use when logging the error - * @param t exception that was generated by the error + * @param tag to record with the error + * @param t exception describing the error site and conditions */ - public static void crash(String tag, Throwable t) { - if (mApplicationObject != null) { - byte[] crashData = null; - try { - // Log exception. - Log.e(TAG, Log.getStackTraceString(t)); - crashData = marshallException(tag, t); - if (crashData == null) { - throw new NullPointerException("Can't marshall crash data"); - } - } catch (Throwable t2) { - try { - // Log exception as a string so we don't get in an infinite loop. - Log.e(TAG, "Error reporting crash: " - + Log.getStackTraceString(t2)); - } catch (Throwable t3) { - // Do nothing, must be OOM so we can't format the message - } - } - - try { - // Display user-visible error message. - String msg = t.getMessage(); - if (msg == null) { - msg = t.toString(); - } - - IActivityManager am = ActivityManagerNative.getDefault(); - try { - int res = am.handleApplicationError(mApplicationObject, - 0, tag, msg, t.toString(), crashData); - // Is waiting for the debugger the right thing? - // For now I have turned off the Debug button, because - // I'm not sure what we should do if it is actually - // selected. - //Log.i(TAG, "Got app error result: " + res); - if (res == 1) { - Debug.waitForDebugger(); - return; - } - } catch (RemoteException e) { - } - } catch (Throwable t2) { - try { - // Log exception as a string so we don't get in an infinite loop. - Log.e(TAG, "Error reporting crash: " - + Log.getStackTraceString(t2)); - } catch (Throwable t3) { - // Do nothing, must be OOM so we can't format the message - } - } finally { - // Try everything to make sure this process goes away. - Process.killProcess(Process.myPid()); - System.exit(10); - } - } else { - try { - Log.e(TAG, "*** EXCEPTION IN SYSTEM PROCESS. System will crash."); - Log.e(tag, Log.getStackTraceString(t)); - reportException(tag, t, true); // synchronous - } catch (Throwable t2) { - // Do nothing, must be OOM so we can't format the message - } finally { - // Try everything to make sure this process goes away. + public static void wtf(String tag, Throwable t) { + try { + if (ActivityManagerNative.getDefault().handleApplicationWtf( + mApplicationObject, tag, new ApplicationErrorReport.CrashInfo(t))) { + // The Activity Manager has already written us off -- now exit. Process.killProcess(Process.myPid()); System.exit(10); } + } catch (Throwable t2) { + Log.e(TAG, "Error reporting WTF", t2); } } @@ -381,82 +343,6 @@ public class RuntimeInit { private static final AtomicInteger sInReportException = new AtomicInteger(); /** - * Report an error in the current process. The exception information will - * be handed off to the checkin service and eventually uploaded for analysis. - * This is expensive! Only use this when the exception indicates a programming - * error ("should not happen"). - * - * @param tag to use when logging the error - * @param t exception that was generated by the error - * @param sync true to wait for the report, false to "fire and forget" - */ - public static void reportException(String tag, Throwable t, boolean sync) { - if (!initialized) { - // Exceptions during, eg, zygote cannot use this mechanism - return; - } - - // It's important to prevent an infinite crash-reporting loop: - // while this function is running, don't let it be called again. - int reenter = sInReportException.getAndIncrement(); - if (reenter != 0) { - sInReportException.decrementAndGet(); - Log.e(TAG, "Crash logging skipped, already logging another crash"); - return; - } - - // TODO: Enable callers to specify a level (i.e. warn or error). - try { - // Submit crash data to statistics service. - byte[] crashData = marshallException(tag, t); - ICheckinService checkin = ICheckinService.Stub.asInterface( - ServiceManager.getService("checkin")); - if (checkin == null) { - Log.e(TAG, "Crash logging skipped, no checkin service"); - } else if (sync) { - checkin.reportCrashSync(crashData); - } else { - checkin.reportCrashAsync(crashData); - } - } catch (Throwable t2) { - // Log exception as a string so we don't get in an infinite loop. - Log.e(TAG, "Crash logging failed: " + t2); - } finally { - sInReportException.decrementAndGet(); - } - } - - private static byte[] marshallException(String tag, Throwable t) { - // Convert crash data to bytes. - ByteArrayOutputStream bout = new ByteArrayOutputStream(); - DataOutputStream dout = new DataOutputStream(bout); - try { - new CrashData(tag, t).write(dout); - dout.close(); - } catch (IOException e) { - return null; - } - return bout.toByteArray(); - } - - /** - * Replay an encoded CrashData record back into a useable CrashData record. This can be - * helpful for providing debugging output after a process error. - * - * @param crashDataBytes The byte array containing the encoded crash record - * @return new CrashData record, or null if could not create one. - */ - public static CrashData unmarshallException(byte[] crashDataBytes) { - try { - ByteArrayInputStream bin = new ByteArrayInputStream(crashDataBytes); - DataInputStream din = new DataInputStream(bin); - return new CrashData(din); - } catch (IOException e) { - return null; - } - } - - /** * Set the object identifying this application/process, for reporting VM * errors. */ @@ -471,7 +357,4 @@ public class RuntimeInit { // Register handlers for DDM messages. android.ddm.DdmRegister.registerHandlers(); } - - private static IBinder mApplicationObject; - } diff --git a/core/java/com/android/internal/util/FastXmlSerializer.java b/core/java/com/android/internal/util/FastXmlSerializer.java deleted file mode 100644 index 592a8fa..0000000 --- a/core/java/com/android/internal/util/FastXmlSerializer.java +++ /dev/null @@ -1,365 +0,0 @@ -/* - * Copyright (C) 2006 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.internal.util; - -import org.xmlpull.v1.XmlSerializer; - -import java.io.IOException; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.io.UnsupportedEncodingException; -import java.io.Writer; -import java.nio.ByteBuffer; -import java.nio.CharBuffer; -import java.nio.charset.Charset; -import java.nio.charset.CharsetEncoder; -import java.nio.charset.CoderResult; -import java.nio.charset.IllegalCharsetNameException; -import java.nio.charset.UnsupportedCharsetException; - -/** - * This is a quick and dirty implementation of XmlSerializer that isn't horribly - * painfully slow like the normal one. It only does what is needed for the - * specific XML files being written with it. - */ -public class FastXmlSerializer implements XmlSerializer { - private static final String ESCAPE_TABLE[] = new String[] { - null, null, null, null, null, null, null, null, // 0-7 - null, null, null, null, null, null, null, null, // 8-15 - null, null, null, null, null, null, null, null, // 16-23 - null, null, null, null, null, null, null, null, // 24-31 - null, null, """, null, null, null, "&", null, // 32-39 - null, null, null, null, null, null, null, null, // 40-47 - null, null, null, null, null, null, null, null, // 48-55 - null, null, null, null, "<", null, ">", null, // 56-63 - }; - - private static final int BUFFER_LEN = 8192; - - private final char[] mText = new char[BUFFER_LEN]; - private int mPos; - - private Writer mWriter; - - private OutputStream mOutputStream; - private CharsetEncoder mCharset; - private ByteBuffer mBytes = ByteBuffer.allocate(BUFFER_LEN); - - private boolean mInTag; - - private void append(char c) throws IOException { - int pos = mPos; - if (pos >= (BUFFER_LEN-1)) { - flush(); - pos = mPos; - } - mText[pos] = c; - mPos = pos+1; - } - - private void append(String str, int i, final int length) throws IOException { - if (length > BUFFER_LEN) { - final int end = i + length; - while (i < end) { - int next = i + BUFFER_LEN; - append(str, i, next<end ? BUFFER_LEN : (end-i)); - i = next; - } - return; - } - int pos = mPos; - if ((pos+length) > BUFFER_LEN) { - flush(); - pos = mPos; - } - str.getChars(i, i+length, mText, pos); - mPos = pos + length; - } - - private void append(char[] buf, int i, final int length) throws IOException { - if (length > BUFFER_LEN) { - final int end = i + length; - while (i < end) { - int next = i + BUFFER_LEN; - append(buf, i, next<end ? BUFFER_LEN : (end-i)); - i = next; - } - return; - } - int pos = mPos; - if ((pos+length) > BUFFER_LEN) { - flush(); - pos = mPos; - } - System.arraycopy(buf, i, mText, pos, length); - mPos = pos + length; - } - - private void append(String str) throws IOException { - append(str, 0, str.length()); - } - - private void escapeAndAppendString(final String string) throws IOException { - final int N = string.length(); - final char NE = (char)ESCAPE_TABLE.length; - final String[] escapes = ESCAPE_TABLE; - int lastPos = 0; - int pos; - for (pos=0; pos<N; pos++) { - char c = string.charAt(pos); - if (c >= NE) continue; - String escape = escapes[c]; - if (escape == null) continue; - if (lastPos < pos) append(string, lastPos, pos-lastPos); - lastPos = pos + 1; - append(escape); - } - if (lastPos < pos) append(string, lastPos, pos-lastPos); - } - - private void escapeAndAppendString(char[] buf, int start, int len) throws IOException { - final char NE = (char)ESCAPE_TABLE.length; - final String[] escapes = ESCAPE_TABLE; - int end = start+len; - int lastPos = start; - int pos; - for (pos=start; pos<end; pos++) { - char c = buf[pos]; - if (c >= NE) continue; - String escape = escapes[c]; - if (escape == null) continue; - if (lastPos < pos) append(buf, lastPos, pos-lastPos); - lastPos = pos + 1; - append(escape); - } - if (lastPos < pos) append(buf, lastPos, pos-lastPos); - } - - public XmlSerializer attribute(String namespace, String name, String value) throws IOException, - IllegalArgumentException, IllegalStateException { - append(' '); - if (namespace != null) { - append(namespace); - append(':'); - } - append(name); - append("=\""); - - escapeAndAppendString(value); - append('"'); - return this; - } - - public void cdsect(String text) throws IOException, IllegalArgumentException, - IllegalStateException { - throw new UnsupportedOperationException(); - } - - public void comment(String text) throws IOException, IllegalArgumentException, - IllegalStateException { - throw new UnsupportedOperationException(); - } - - public void docdecl(String text) throws IOException, IllegalArgumentException, - IllegalStateException { - throw new UnsupportedOperationException(); - } - - public void endDocument() throws IOException, IllegalArgumentException, IllegalStateException { - flush(); - } - - public XmlSerializer endTag(String namespace, String name) throws IOException, - IllegalArgumentException, IllegalStateException { - if (mInTag) { - append(" />\n"); - } else { - append("</"); - if (namespace != null) { - append(namespace); - append(':'); - } - append(name); - append(">\n"); - } - mInTag = false; - return this; - } - - public void entityRef(String text) throws IOException, IllegalArgumentException, - IllegalStateException { - throw new UnsupportedOperationException(); - } - - private void flushBytes() throws IOException { - int position; - if ((position = mBytes.position()) > 0) { - mBytes.flip(); - mOutputStream.write(mBytes.array(), 0, position); - mBytes.clear(); - } - } - - public void flush() throws IOException { - //Log.i("PackageManager", "flush mPos=" + mPos); - if (mPos > 0) { - if (mOutputStream != null) { - CharBuffer charBuffer = CharBuffer.wrap(mText, 0, mPos); - CoderResult result = mCharset.encode(charBuffer, mBytes, true); - while (true) { - if (result.isError()) { - throw new IOException(result.toString()); - } else if (result.isOverflow()) { - flushBytes(); - result = mCharset.encode(charBuffer, mBytes, true); - continue; - } - break; - } - flushBytes(); - mOutputStream.flush(); - } else { - mWriter.write(mText, 0, mPos); - mWriter.flush(); - } - mPos = 0; - } - } - - public int getDepth() { - throw new UnsupportedOperationException(); - } - - public boolean getFeature(String name) { - throw new UnsupportedOperationException(); - } - - public String getName() { - throw new UnsupportedOperationException(); - } - - public String getNamespace() { - throw new UnsupportedOperationException(); - } - - public String getPrefix(String namespace, boolean generatePrefix) - throws IllegalArgumentException { - throw new UnsupportedOperationException(); - } - - public Object getProperty(String name) { - throw new UnsupportedOperationException(); - } - - public void ignorableWhitespace(String text) throws IOException, IllegalArgumentException, - IllegalStateException { - throw new UnsupportedOperationException(); - } - - public void processingInstruction(String text) throws IOException, IllegalArgumentException, - IllegalStateException { - throw new UnsupportedOperationException(); - } - - public void setFeature(String name, boolean state) throws IllegalArgumentException, - IllegalStateException { - if (name.equals("http://xmlpull.org/v1/doc/features.html#indent-output")) { - return; - } - throw new UnsupportedOperationException(); - } - - public void setOutput(OutputStream os, String encoding) throws IOException, - IllegalArgumentException, IllegalStateException { - if (os == null) - throw new IllegalArgumentException(); - if (true) { - try { - mCharset = Charset.forName(encoding).newEncoder(); - } catch (IllegalCharsetNameException e) { - throw (UnsupportedEncodingException) (new UnsupportedEncodingException( - encoding).initCause(e)); - } catch (UnsupportedCharsetException e) { - throw (UnsupportedEncodingException) (new UnsupportedEncodingException( - encoding).initCause(e)); - } - mOutputStream = os; - } else { - setOutput( - encoding == null - ? new OutputStreamWriter(os) - : new OutputStreamWriter(os, encoding)); - } - } - - public void setOutput(Writer writer) throws IOException, IllegalArgumentException, - IllegalStateException { - mWriter = writer; - } - - public void setPrefix(String prefix, String namespace) throws IOException, - IllegalArgumentException, IllegalStateException { - throw new UnsupportedOperationException(); - } - - public void setProperty(String name, Object value) throws IllegalArgumentException, - IllegalStateException { - throw new UnsupportedOperationException(); - } - - public void startDocument(String encoding, Boolean standalone) throws IOException, - IllegalArgumentException, IllegalStateException { - append("<?xml version='1.0' encoding='utf-8' standalone='" - + (standalone ? "yes" : "no") + "' ?>\n"); - } - - public XmlSerializer startTag(String namespace, String name) throws IOException, - IllegalArgumentException, IllegalStateException { - if (mInTag) { - append(">\n"); - } - append('<'); - if (namespace != null) { - append(namespace); - append(':'); - } - append(name); - mInTag = true; - return this; - } - - public XmlSerializer text(char[] buf, int start, int len) throws IOException, - IllegalArgumentException, IllegalStateException { - if (mInTag) { - append(">"); - mInTag = false; - } - escapeAndAppendString(buf, start, len); - return this; - } - - public XmlSerializer text(String text) throws IOException, IllegalArgumentException, - IllegalStateException { - if (mInTag) { - append(">"); - mInTag = false; - } - escapeAndAppendString(text); - return this; - } - -} diff --git a/core/java/com/android/internal/util/HanziToPinyin.java b/core/java/com/android/internal/util/HanziToPinyin.java new file mode 100644 index 0000000..4368e98 --- /dev/null +++ b/core/java/com/android/internal/util/HanziToPinyin.java @@ -0,0 +1,455 @@ +/* + * 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.internal.util; + +import com.google.android.util.AbstractMessageParser.Token; + +import android.text.TextUtils; +import android.util.Log; + +import java.text.Collator; +import java.util.ArrayList; +import java.util.Locale; + +/** + * An object to convert Chinese character to its corresponding pinyin string. + * For characters with multiple possible pinyin string, only one is selected + * according to collator. Polyphone is not supported in this implementation. + * This class is implemented to achieve the best runtime performance and minimum + * runtime resources with tolerable sacrifice of accuracy. This implementation + * highly depends on zh_CN ICU collation data and must be always synchronized with + * ICU. + */ +public class HanziToPinyin { + private static final String TAG = "HanziToPinyin"; + + private static final char[] UNIHANS = { + '\u5416', '\u54ce', '\u5b89', '\u80ae', '\u51f9', '\u516b', '\u63b0', '\u6273', + '\u90a6', '\u52f9', '\u9642', '\u5954', '\u4f3b', '\u7680', '\u782d', '\u706c', + '\u618b', '\u6c43', '\u51ab', '\u7676', '\u5cec', '\u5693', '\u5072', '\u53c2', + '\u4ed3', '\u64a1', '\u518a', '\u5d7e', '\u564c', '\u6260', '\u62c6', '\u8fbf', + '\u4f25', '\u6284', '\u8f66', '\u62bb', '\u9637', '\u5403', '\u5145', '\u62bd', + '\u51fa', '\u640b', '\u5ddb', '\u5205', '\u5439', '\u65fe', '\u8e14', '\u5472', + '\u4ece', '\u51d1', '\u7c97', '\u6c46', '\u5d14', '\u90a8', '\u6413', '\u5491', + '\u5446', '\u4e39', '\u5f53', '\u5200', '\u6074', '\u6265', '\u706f', '\u4efe', + '\u55f2', '\u6541', '\u5201', '\u7239', '\u4e01', '\u4e1f', '\u4e1c', '\u543a', + '\u5262', '\u8011', '\u5796', '\u5428', '\u591a', '\u59b8', '\u5940', '\u97a5', + '\u800c', '\u53d1', '\u5e06', '\u531a', '\u98de', '\u5206', '\u4e30', '\u8985', + '\u4ecf', '\u57ba', '\u7d11', '\u592b', '\u7324', '\u65ee', '\u4f85', '\u5e72', + '\u5188', '\u768b', '\u6208', '\u7ed9', '\u6839', '\u63ef', '\u55bc', '\u55f0', + '\u5de5', '\u52fe', '\u4f30', '\u9e39', '\u4e56', '\u5173', '\u5149', '\u5f52', + '\u4e28', '\u8b34', '\u5459', '\u598e', '\u548d', '\u4f44', '\u592f', '\u8320', + '\u8bc3', '\u9ed2', '\u62eb', '\u4ea8', '\u53ff', '\u9f41', '\u4e4e', '\u82b1', + '\u6000', '\u6b22', '\u5ddf', '\u7070', '\u660f', '\u5419', '\u4e0c', '\u52a0', + '\u620b', '\u6c5f', '\u827d', '\u9636', '\u5dfe', '\u5755', '\u5182', '\u4e29', + '\u51e5', '\u59e2', '\u5658', '\u519b', '\u5494', '\u5f00', '\u938e', '\u5ffc', + '\u5c3b', '\u533c', '\u808e', '\u52a5', '\u7a7a', '\u62a0', '\u625d', '\u5938', + '\u84af', '\u5bbd', '\u5321', '\u4e8f', '\u5764', '\u6269', '\u62c9', '\u4f86', + '\u5170', '\u5577', '\u635e', '\u4ec2', '\u96f7', '\u8137', '\u68f1', '\u695e', + '\u550e', '\u4fe9', '\u5afe', '\u826f', '\u8e7d', '\u57d3', '\u53b8', '\u62ce', + '\u6e9c', '\u9f99', '\u5a04', '\u565c', '\u5b6a', '\u62a1', '\u9831', '\u5988', + '\u57cb', '\u989f', '\u7264', '\u732b', '\u5445', '\u95e8', '\u6c13', '\u54aa', + '\u5b80', '\u55b5', '\u4e5c', '\u6c11', '\u540d', '\u8c2c', '\u6478', '\u725f', + '\u6bcd', '\u62cf', '\u8149', '\u56e1', '\u56d4', '\u5b6c', '\u8bb7', '\u5a1e', + '\u5ae9', '\u80fd', '\u92b0', '\u62c8', '\u5a18', '\u9e1f', '\u634f', '\u56dc', + '\u5b81', '\u599e', '\u519c', '\u7fba', '\u5974', '\u597b', '\u9ec1', '\u90cd', + '\u5662', '\u8bb4', '\u5991', '\u62cd', '\u7705', '\u6c78', '\u629b', '\u5478', + '\u55b7', '\u5309', '\u4e76', '\u7247', '\u527d', '\u6c15', '\u59d8', '\u4e52', + '\u948b', '\u5256', '\u4ec6', '\u4e03', '\u6390', '\u5343', '\u545b', '\u6084', + '\u5207', '\u4eb2', '\u9751', '\u5b86', '\u74d7', '\u533a', '\u5cd1', '\u7094', + '\u590b', '\u5465', '\u7a63', '\u835b', '\u60f9', '\u4eba', '\u6254', '\u65e5', + '\u620e', '\u53b9', '\u909a', '\u5827', '\u6875', '\u95f0', '\u633c', '\u4ee8', + '\u6be2', '\u4e09', '\u6852', '\u63bb', '\u8272', '\u68ee', '\u50e7', '\u6740', + '\u7b5b', '\u5c71', '\u4f24', '\u5f30', '\u5962', '\u7533', '\u5347', '\u5c38', + '\u53ce', '\u4e66', '\u5237', '\u8870', '\u95e9', '\u53cc', '\u8c01', '\u542e', + '\u8bf4', '\u53b6', '\u5fea', '\u51c1', '\u82cf', '\u72fb', '\u590a', '\u5b59', + '\u5506', '\u4ed6', '\u5b61', '\u574d', '\u6c64', '\u5932', '\u5fd1', '\u81af', + '\u5254', '\u5929', '\u65eb', '\u6017', '\u5385', '\u70b5', '\u5077', '\u51f8', + '\u6e4d', '\u63a8', '\u541e', '\u8bac', '\u52b8', '\u6b6a', '\u5f2f', '\u5c23', + '\u5371', '\u6637', '\u7fc1', '\u631d', '\u4e4c', '\u5915', '\u5477', '\u4ed9', + '\u4e61', '\u7071', '\u4e9b', '\u5fc3', '\u5174', '\u51f6', '\u4f11', '\u620c', + '\u5405', '\u75b6', '\u7025', '\u4e2b', '\u54bd', '\u592e', '\u5e7a', '\u503b', + '\u4e00', '\u4e5a', '\u5e94', '\u5537', '\u4f63', '\u4f18', '\u7ea1', '\u56e6', + '\u66f0', '\u8480', '\u5e00', '\u707d', '\u5142', '\u7242', '\u50ae', '\u556b', + '\u9c61', '\u600e', '\u66fd', '\u5412', '\u635a', '\u6cbe', '\u5f20', '\u4f4b', + '\u8707', '\u8d1e', '\u9eee', '\u4e4b', '\u4e2d', '\u5dde', '\u6731', '\u6293', + '\u62fd', '\u4e13', '\u5986', '\u96b9', '\u5b92', '\u5353', '\u4ed4', '\u5b97', + '\u90b9', '\u79df', '\u5297', '\u55fa', '\u5c0a', '\u6628', + }; + private final static byte[][] PINYINS = { + {65, 00, 00, 00, 00, 00, }, {65, 73, 00, 00, 00, 00, }, + {65, 78, 00, 00, 00, 00, }, {65, 78, 71, 00, 00, 00, }, + {65, 79, 00, 00, 00, 00, }, {66, 65, 00, 00, 00, 00, }, + {66, 65, 73, 00, 00, 00, }, {66, 65, 78, 00, 00, 00, }, + {66, 65, 78, 71, 00, 00, }, {66, 65, 79, 00, 00, 00, }, + {66, 69, 73, 00, 00, 00, }, {66, 69, 78, 00, 00, 00, }, + {66, 69, 78, 71, 00, 00, }, {66, 73, 00, 00, 00, 00, }, + {66, 73, 65, 78, 00, 00, }, {66, 73, 65, 79, 00, 00, }, + {66, 73, 69, 00, 00, 00, }, {66, 73, 78, 00, 00, 00, }, + {66, 73, 78, 71, 00, 00, }, {66, 79, 00, 00, 00, 00, }, + {66, 85, 00, 00, 00, 00, }, {67, 65, 00, 00, 00, 00, }, + {67, 65, 73, 00, 00, 00, }, {67, 65, 78, 00, 00, 00, }, + {67, 65, 78, 71, 00, 00, }, {67, 65, 79, 00, 00, 00, }, + {67, 69, 00, 00, 00, 00, }, {67, 69, 78, 00, 00, 00, }, + {67, 69, 78, 71, 00, 00, }, {67, 72, 65, 00, 00, 00, }, + {67, 72, 65, 73, 00, 00, }, {67, 72, 65, 78, 00, 00, }, + {67, 72, 65, 78, 71, 00, }, {67, 72, 65, 79, 00, 00, }, + {67, 72, 69, 00, 00, 00, }, {67, 72, 69, 78, 00, 00, }, + {67, 72, 69, 78, 71, 00, }, {67, 72, 73, 00, 00, 00, }, + {67, 72, 79, 78, 71, 00, }, {67, 72, 79, 85, 00, 00, }, + {67, 72, 85, 00, 00, 00, }, {67, 72, 85, 65, 73, 00, }, + {67, 72, 85, 65, 78, 00, }, {67, 72, 85, 65, 78, 71, }, + {67, 72, 85, 73, 00, 00, }, {67, 72, 85, 78, 00, 00, }, + {67, 72, 85, 79, 00, 00, }, {67, 73, 00, 00, 00, 00, }, + {67, 79, 78, 71, 00, 00, }, {67, 79, 85, 00, 00, 00, }, + {67, 85, 00, 00, 00, 00, }, {67, 85, 65, 78, 00, 00, }, + {67, 85, 73, 00, 00, 00, }, {67, 85, 78, 00, 00, 00, }, + {67, 85, 79, 00, 00, 00, }, {68, 65, 00, 00, 00, 00, }, + {68, 65, 73, 00, 00, 00, }, {68, 65, 78, 00, 00, 00, }, + {68, 65, 78, 71, 00, 00, }, {68, 65, 79, 00, 00, 00, }, + {68, 69, 00, 00, 00, 00, }, {68, 69, 78, 00, 00, 00, }, + {68, 69, 78, 71, 00, 00, }, {68, 73, 00, 00, 00, 00, }, + {68, 73, 65, 00, 00, 00, }, {68, 73, 65, 78, 00, 00, }, + {68, 73, 65, 79, 00, 00, }, {68, 73, 69, 00, 00, 00, }, + {68, 73, 78, 71, 00, 00, }, {68, 73, 85, 00, 00, 00, }, + {68, 79, 78, 71, 00, 00, }, {68, 79, 85, 00, 00, 00, }, + {68, 85, 00, 00, 00, 00, }, {68, 85, 65, 78, 00, 00, }, + {68, 85, 73, 00, 00, 00, }, {68, 85, 78, 00, 00, 00, }, + {68, 85, 79, 00, 00, 00, }, {69, 00, 00, 00, 00, 00, }, + {69, 78, 00, 00, 00, 00, }, {69, 78, 71, 00, 00, 00, }, + {69, 82, 00, 00, 00, 00, }, {70, 65, 00, 00, 00, 00, }, + {70, 65, 78, 00, 00, 00, }, {70, 65, 78, 71, 00, 00, }, + {70, 69, 73, 00, 00, 00, }, {70, 69, 78, 00, 00, 00, }, + {70, 69, 78, 71, 00, 00, }, {70, 73, 65, 79, 00, 00, }, + {70, 79, 00, 00, 00, 00, }, {70, 85, 00, 00, 00, 00, }, + {70, 79, 85, 00, 00, 00, }, {70, 85, 00, 00, 00, 00, }, + {71, 85, 73, 00, 00, 00, }, {71, 65, 00, 00, 00, 00, }, + {71, 65, 73, 00, 00, 00, }, {71, 65, 78, 00, 00, 00, }, + {71, 65, 78, 71, 00, 00, }, {71, 65, 79, 00, 00, 00, }, + {71, 69, 00, 00, 00, 00, }, {71, 69, 73, 00, 00, 00, }, + {71, 69, 78, 00, 00, 00, }, {71, 69, 78, 71, 00, 00, }, + {74, 73, 69, 00, 00, 00, }, {71, 69, 00, 00, 00, 00, }, + {71, 79, 78, 71, 00, 00, }, {71, 79, 85, 00, 00, 00, }, + {71, 85, 00, 00, 00, 00, }, {71, 85, 65, 00, 00, 00, }, + {71, 85, 65, 73, 00, 00, }, {71, 85, 65, 78, 00, 00, }, + {71, 85, 65, 78, 71, 00, }, {71, 85, 73, 00, 00, 00, }, + {71, 85, 78, 00, 00, 00, }, {71, 85, 65, 78, 00, 00, }, + {71, 85, 79, 00, 00, 00, }, {72, 65, 00, 00, 00, 00, }, + {72, 65, 73, 00, 00, 00, }, {72, 65, 78, 00, 00, 00, }, + {72, 65, 78, 71, 00, 00, }, {72, 65, 79, 00, 00, 00, }, + {72, 69, 00, 00, 00, 00, }, {72, 69, 73, 00, 00, 00, }, + {72, 69, 78, 00, 00, 00, }, {72, 69, 78, 71, 00, 00, }, + {72, 79, 78, 71, 00, 00, }, {72, 79, 85, 00, 00, 00, }, + {72, 85, 00, 00, 00, 00, }, {72, 85, 65, 00, 00, 00, }, + {72, 85, 65, 73, 00, 00, }, {72, 85, 65, 78, 00, 00, }, + {72, 85, 65, 78, 71, 00, }, {72, 85, 73, 00, 00, 00, }, + {72, 85, 78, 00, 00, 00, }, {72, 85, 79, 00, 00, 00, }, + {74, 73, 00, 00, 00, 00, }, {74, 73, 65, 00, 00, 00, }, + {74, 73, 65, 78, 00, 00, }, {74, 73, 65, 78, 71, 00, }, + {74, 73, 65, 79, 00, 00, }, {74, 73, 69, 00, 00, 00, }, + {74, 73, 78, 00, 00, 00, }, {74, 73, 78, 71, 00, 00, }, + {74, 73, 79, 78, 71, 00, }, {74, 73, 85, 00, 00, 00, }, + {74, 85, 00, 00, 00, 00, }, {74, 85, 65, 78, 00, 00, }, + {74, 85, 69, 00, 00, 00, }, {74, 85, 78, 00, 00, 00, }, + {75, 65, 00, 00, 00, 00, }, {75, 65, 73, 00, 00, 00, }, + {75, 65, 78, 00, 00, 00, }, {75, 65, 78, 71, 00, 00, }, + {75, 65, 79, 00, 00, 00, }, {75, 69, 00, 00, 00, 00, }, + {75, 69, 78, 00, 00, 00, }, {75, 69, 78, 71, 00, 00, }, + {75, 79, 78, 71, 00, 00, }, {75, 79, 85, 00, 00, 00, }, + {75, 85, 00, 00, 00, 00, }, {75, 85, 65, 00, 00, 00, }, + {75, 85, 65, 73, 00, 00, }, {75, 85, 65, 78, 00, 00, }, + {75, 85, 65, 78, 71, 00, }, {75, 85, 73, 00, 00, 00, }, + {75, 85, 78, 00, 00, 00, }, {75, 85, 79, 00, 00, 00, }, + {76, 65, 00, 00, 00, 00, }, {76, 65, 73, 00, 00, 00, }, + {76, 65, 78, 00, 00, 00, }, {76, 65, 78, 71, 00, 00, }, + {76, 65, 79, 00, 00, 00, }, {76, 69, 00, 00, 00, 00, }, + {76, 69, 73, 00, 00, 00, }, {76, 73, 00, 00, 00, 00, }, + {76, 73, 78, 71, 00, 00, }, {76, 69, 78, 71, 00, 00, }, + {76, 73, 00, 00, 00, 00, }, {76, 73, 65, 00, 00, 00, }, + {76, 73, 65, 78, 00, 00, }, {76, 73, 65, 78, 71, 00, }, + {76, 73, 65, 79, 00, 00, }, {76, 73, 69, 00, 00, 00, }, + {76, 73, 78, 00, 00, 00, }, {76, 73, 78, 71, 00, 00, }, + {76, 73, 85, 00, 00, 00, }, {76, 79, 78, 71, 00, 00, }, + {76, 79, 85, 00, 00, 00, }, {76, 85, 00, 00, 00, 00, }, + {76, 85, 65, 78, 00, 00, }, {76, 85, 78, 00, 00, 00, }, + {76, 85, 79, 00, 00, 00, }, {77, 65, 00, 00, 00, 00, }, + {77, 65, 73, 00, 00, 00, }, {77, 65, 78, 00, 00, 00, }, + {77, 65, 78, 71, 00, 00, }, {77, 65, 79, 00, 00, 00, }, + {77, 69, 73, 00, 00, 00, }, {77, 69, 78, 00, 00, 00, }, + {77, 69, 78, 71, 00, 00, }, {77, 73, 00, 00, 00, 00, }, + {77, 73, 65, 78, 00, 00, }, {77, 73, 65, 79, 00, 00, }, + {77, 73, 69, 00, 00, 00, }, {77, 73, 78, 00, 00, 00, }, + {77, 73, 78, 71, 00, 00, }, {77, 73, 85, 00, 00, 00, }, + {77, 79, 00, 00, 00, 00, }, {77, 79, 85, 00, 00, 00, }, + {77, 85, 00, 00, 00, 00, }, {78, 65, 00, 00, 00, 00, }, + {78, 65, 73, 00, 00, 00, }, {78, 65, 78, 00, 00, 00, }, + {78, 65, 78, 71, 00, 00, }, {78, 65, 79, 00, 00, 00, }, + {78, 69, 00, 00, 00, 00, }, {78, 69, 73, 00, 00, 00, }, + {78, 69, 78, 00, 00, 00, }, {78, 69, 78, 71, 00, 00, }, + {78, 73, 00, 00, 00, 00, }, {78, 73, 65, 78, 00, 00, }, + {78, 73, 65, 78, 71, 00, }, {78, 73, 65, 79, 00, 00, }, + {78, 73, 69, 00, 00, 00, }, {78, 73, 78, 00, 00, 00, }, + {78, 73, 78, 71, 00, 00, }, {78, 73, 85, 00, 00, 00, }, + {78, 79, 78, 71, 00, 00, }, {78, 79, 85, 00, 00, 00, }, + {78, 85, 00, 00, 00, 00, }, {78, 85, 65, 78, 00, 00, }, + {78, 85, 78, 00, 00, 00, }, {78, 85, 79, 00, 00, 00, }, + {79, 00, 00, 00, 00, 00, }, {79, 85, 00, 00, 00, 00, }, + {80, 65, 00, 00, 00, 00, }, {80, 65, 73, 00, 00, 00, }, + {80, 65, 78, 00, 00, 00, }, {80, 65, 78, 71, 00, 00, }, + {80, 65, 79, 00, 00, 00, }, {80, 69, 73, 00, 00, 00, }, + {80, 69, 78, 00, 00, 00, }, {80, 69, 78, 71, 00, 00, }, + {80, 73, 00, 00, 00, 00, }, {80, 73, 65, 78, 00, 00, }, + {80, 73, 65, 79, 00, 00, }, {80, 73, 69, 00, 00, 00, }, + {80, 73, 78, 00, 00, 00, }, {80, 73, 78, 71, 00, 00, }, + {80, 79, 00, 00, 00, 00, }, {80, 79, 85, 00, 00, 00, }, + {80, 85, 00, 00, 00, 00, }, {81, 73, 00, 00, 00, 00, }, + {81, 73, 65, 00, 00, 00, }, {81, 73, 65, 78, 00, 00, }, + {81, 73, 65, 78, 71, 00, }, {81, 73, 65, 79, 00, 00, }, + {81, 73, 69, 00, 00, 00, }, {81, 73, 78, 00, 00, 00, }, + {81, 73, 78, 71, 00, 00, }, {81, 73, 79, 78, 71, 00, }, + {81, 73, 85, 00, 00, 00, }, {81, 85, 00, 00, 00, 00, }, + {81, 85, 65, 78, 00, 00, }, {81, 85, 69, 00, 00, 00, }, + {81, 85, 78, 00, 00, 00, }, {82, 65, 78, 00, 00, 00, }, + {82, 65, 78, 71, 00, 00, }, {82, 65, 79, 00, 00, 00, }, + {82, 69, 00, 00, 00, 00, }, {82, 69, 78, 00, 00, 00, }, + {82, 69, 78, 71, 00, 00, }, {82, 73, 00, 00, 00, 00, }, + {82, 79, 78, 71, 00, 00, }, {82, 79, 85, 00, 00, 00, }, + {82, 85, 00, 00, 00, 00, }, {82, 85, 65, 78, 00, 00, }, + {82, 85, 73, 00, 00, 00, }, {82, 85, 78, 00, 00, 00, }, + {82, 85, 79, 00, 00, 00, }, {83, 65, 00, 00, 00, 00, }, + {83, 65, 73, 00, 00, 00, }, {83, 65, 78, 00, 00, 00, }, + {83, 65, 78, 71, 00, 00, }, {83, 65, 79, 00, 00, 00, }, + {83, 69, 00, 00, 00, 00, }, {83, 69, 78, 00, 00, 00, }, + {83, 69, 78, 71, 00, 00, }, {83, 72, 65, 00, 00, 00, }, + {83, 72, 65, 73, 00, 00, }, {83, 72, 65, 78, 00, 00, }, + {83, 72, 65, 78, 71, 00, }, {83, 72, 65, 79, 00, 00, }, + {83, 72, 69, 00, 00, 00, }, {83, 72, 69, 78, 00, 00, }, + {83, 72, 69, 78, 71, 00, }, {83, 72, 73, 00, 00, 00, }, + {83, 72, 79, 85, 00, 00, }, {83, 72, 85, 00, 00, 00, }, + {83, 72, 85, 65, 00, 00, }, {83, 72, 85, 65, 73, 00, }, + {83, 72, 85, 65, 78, 00, }, {83, 72, 85, 65, 78, 71, }, + {83, 72, 85, 73, 00, 00, }, {83, 72, 85, 78, 00, 00, }, + {83, 72, 85, 79, 00, 00, }, {83, 73, 00, 00, 00, 00, }, + {83, 79, 78, 71, 00, 00, }, {83, 79, 85, 00, 00, 00, }, + {83, 85, 00, 00, 00, 00, }, {83, 85, 65, 78, 00, 00, }, + {83, 85, 73, 00, 00, 00, }, {83, 85, 78, 00, 00, 00, }, + {83, 85, 79, 00, 00, 00, }, {84, 65, 00, 00, 00, 00, }, + {84, 65, 73, 00, 00, 00, }, {84, 65, 78, 00, 00, 00, }, + {84, 65, 78, 71, 00, 00, }, {84, 65, 79, 00, 00, 00, }, + {84, 69, 00, 00, 00, 00, }, {84, 69, 78, 71, 00, 00, }, + {84, 73, 00, 00, 00, 00, }, {84, 73, 65, 78, 00, 00, }, + {84, 73, 65, 79, 00, 00, }, {84, 73, 69, 00, 00, 00, }, + {84, 73, 78, 71, 00, 00, }, {84, 79, 78, 71, 00, 00, }, + {84, 79, 85, 00, 00, 00, }, {84, 85, 00, 00, 00, 00, }, + {84, 85, 65, 78, 00, 00, }, {84, 85, 73, 00, 00, 00, }, + {84, 85, 78, 00, 00, 00, }, {84, 85, 79, 00, 00, 00, }, + {87, 65, 00, 00, 00, 00, }, {87, 65, 73, 00, 00, 00, }, + {87, 65, 78, 00, 00, 00, }, {87, 65, 78, 71, 00, 00, }, + {87, 69, 73, 00, 00, 00, }, {87, 69, 78, 00, 00, 00, }, + {87, 69, 78, 71, 00, 00, }, {87, 79, 00, 00, 00, 00, }, + {87, 85, 00, 00, 00, 00, }, {88, 73, 00, 00, 00, 00, }, + {88, 73, 65, 00, 00, 00, }, {88, 73, 65, 78, 00, 00, }, + {88, 73, 65, 78, 71, 00, }, {88, 73, 65, 79, 00, 00, }, + {88, 73, 69, 00, 00, 00, }, {88, 73, 78, 00, 00, 00, }, + {88, 73, 78, 71, 00, 00, }, {88, 73, 79, 78, 71, 00, }, + {88, 73, 85, 00, 00, 00, }, {88, 85, 00, 00, 00, 00, }, + {88, 85, 65, 78, 00, 00, }, {88, 85, 69, 00, 00, 00, }, + {88, 85, 78, 00, 00, 00, }, {89, 65, 00, 00, 00, 00, }, + {89, 65, 78, 00, 00, 00, }, {89, 65, 78, 71, 00, 00, }, + {89, 65, 79, 00, 00, 00, }, {89, 69, 00, 00, 00, 00, }, + {89, 73, 00, 00, 00, 00, }, {89, 73, 78, 00, 00, 00, }, + {89, 73, 78, 71, 00, 00, }, {89, 79, 00, 00, 00, 00, }, + {89, 79, 78, 71, 00, 00, }, {89, 79, 85, 00, 00, 00, }, + {89, 85, 00, 00, 00, 00, }, {89, 85, 65, 78, 00, 00, }, + {89, 85, 69, 00, 00, 00, }, {89, 85, 78, 00, 00, 00, }, + {90, 65, 00, 00, 00, 00, }, {90, 65, 73, 00, 00, 00, }, + {90, 65, 78, 00, 00, 00, }, {90, 65, 78, 71, 00, 00, }, + {90, 65, 79, 00, 00, 00, }, {90, 69, 00, 00, 00, 00, }, + {90, 69, 73, 00, 00, 00, }, {90, 69, 78, 00, 00, 00, }, + {90, 69, 78, 71, 00, 00, }, {90, 72, 65, 00, 00, 00, }, + {90, 72, 65, 73, 00, 00, }, {90, 72, 65, 78, 00, 00, }, + {90, 72, 65, 78, 71, 00, }, {90, 72, 65, 79, 00, 00, }, + {90, 72, 69, 00, 00, 00, }, {90, 72, 69, 78, 00, 00, }, + {90, 72, 69, 78, 71, 00, }, {90, 72, 73, 00, 00, 00, }, + {90, 72, 79, 78, 71, 00, }, {90, 72, 79, 85, 00, 00, }, + {90, 72, 85, 00, 00, 00, }, {90, 72, 85, 65, 00, 00, }, + {90, 72, 85, 65, 73, 00, }, {90, 72, 85, 65, 78, 00, }, + {90, 72, 85, 65, 78, 71, }, {90, 72, 85, 73, 00, 00, }, + {90, 72, 85, 78, 00, 00, }, {90, 72, 85, 79, 00, 00, }, + {90, 73, 00, 00, 00, 00, }, {90, 79, 78, 71, 00, 00, }, + {90, 79, 85, 00, 00, 00, }, {90, 85, 00, 00, 00, 00, }, + {90, 85, 65, 78, 00, 00, }, {90, 85, 73, 00, 00, 00, }, + {90, 85, 78, 00, 00, 00, }, {90, 85, 79, 00, 00, 00, }, + + }; + + /** First and last Chinese character with known Pinyin according to zh collation */ + private static final String FIRST_UNIHAN = "\u5416"; + private static final String LAST_UNIHAN = "\u5497"; + private static final Collator COLLATOR = Collator.getInstance(Locale.CHINA); + + private static HanziToPinyin sInstance; + private final boolean mHasChinaCollator; + + public static class Token { + /** + * Separator between target string for each source char + */ + public static final String SEPARATOR = " "; + + public static final int ASCII = 1; + public static final int PINYIN = 2; + public static final int UNKNOWN = 3; + + /** + * Type of this token, ASCII, PINYIN or UNKNOWN. + */ + public int type; + /** + * Original string before translation. + */ + public String source; + /** + * Translated string of source. For Han, target is corresponding Pinyin. + * Otherwise target is original string in source. + */ + public String target; + } + + protected HanziToPinyin(boolean hasChinaCollator) { + mHasChinaCollator = hasChinaCollator; + } + + public static HanziToPinyin getInstance() { + synchronized(HanziToPinyin.class) { + if (sInstance != null) { + return sInstance; + } + // Check if zh_CN collation data is available + final Locale locale[] = Collator.getAvailableLocales(); + for (int i = 0; i < locale.length; i++) { + if (locale[i].equals(Locale.CHINA)) { + sInstance = new HanziToPinyin(true); + return sInstance; + } + } + sInstance = new HanziToPinyin(false); + return sInstance; + } + } + + private Token getToken(char character) { + Token token = new Token(); + final String letter = Character.toString(character); + token.source = letter; + int offset = -1; + int cmp; + if (character < 256) { + token.type = Token.ASCII; + token.target = letter; + return token; + } else { + cmp = COLLATOR.compare(letter, FIRST_UNIHAN); + if (cmp < 0) { + token.type = Token.UNKNOWN; + token.target = letter; + return token; + } else if (cmp == 0) { + token.type = Token.PINYIN; + offset = 0; + } else { + cmp = COLLATOR.compare(letter, LAST_UNIHAN); + if (cmp > 0) { + token.type = Token.UNKNOWN; + token.target = letter; + return token; + } else if (cmp == 0) { + token.type = Token.PINYIN; + offset = UNIHANS.length - 1; + } + } + } + + token.type = Token.PINYIN; + if (offset < 0) { + int begin = 0; + int end = UNIHANS.length - 1; + while (begin <= end) { + offset = (begin + end) / 2; + final String unihan = Character.toString(UNIHANS[offset]); + cmp = COLLATOR.compare(letter, unihan); + if (cmp == 0) { + break; + } else if (cmp > 0) { + begin = offset + 1; + } else { + end = offset - 1; + } + } + } + if (cmp < 0) { + offset--; + } + StringBuilder pinyin = new StringBuilder(); + for (int j = 0; j < PINYINS[offset].length && PINYINS[offset][j] != 0; j++) { + pinyin.append((char)PINYINS[offset][j]); + } + token.target = pinyin.toString(); + return token; + } + + public ArrayList<Token> get(final String input) { + if (!mHasChinaCollator || TextUtils.isEmpty(input)) { + return null; + } + + ArrayList<Token> tokens = new ArrayList<Token>(); + Token currentToken; + + final int inputLength = input.length(); + + currentToken = getToken(input.charAt(0)); + + for (int i = 1; i < inputLength; i++) { + final char character = input.charAt(i); + Token token = getToken(character); + + if (token.type != currentToken.type) { + currentToken.target = currentToken.target.trim(); + tokens.add(currentToken); + currentToken = token; + } else { + switch (token.type) { + case Token.ASCII: + case Token.UNKNOWN: + currentToken.source += token.source; + currentToken.target += token.target; + break; + case Token.PINYIN: + currentToken.source += token.source; + currentToken.target += " " + token.target; + break; + } + } + } + + currentToken.target = currentToken.target.trim(); + tokens.add(currentToken); + + return tokens; + } +} diff --git a/core/java/com/android/internal/util/HierarchicalState.java b/core/java/com/android/internal/util/HierarchicalState.java new file mode 100644 index 0000000..002338a --- /dev/null +++ b/core/java/com/android/internal/util/HierarchicalState.java @@ -0,0 +1,75 @@ +/** + * 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.internal.util; + +import android.os.Message; + +/** + * {@hide} + * + * The abstract class for implementing states in a + * HierarchicalStateMachine and HandlerStateMachine. + */ +public abstract class HierarchicalState { + + /** + * Constructor + */ + protected HierarchicalState() { + } + + /** + * Called when a state is entered. + */ + protected void enter() { + } + + /** + * Called when a message is to be processed by the + * state machine. + * + * This routine is never reentered thus no synchronization + * is needed as only one processMessage method will ever be + * executing within a state machine at any given time. This + * does mean that processing by this routine must be completed + * as expeditiously as possible as no subsequent messages will + * be processed until this routine returns. + * + * @param msg to process + * @return true if processing has completed and false + * if the parent state's processMessage should + * be invoked. + */ + abstract protected boolean processMessage(Message msg); + + /** + * Called when a state is exited. + */ + protected void exit() { + } + + /** + * @return name of state, but default returns the states + * class name. An instance name would be better but requiring + * it seems unnecessary. + */ + public String getName() { + String name = getClass().getName(); + int lastDollar = name.lastIndexOf('$'); + return name.substring(lastDollar + 1); + } +} diff --git a/core/java/com/android/internal/util/HierarchicalStateMachine.java b/core/java/com/android/internal/util/HierarchicalStateMachine.java new file mode 100644 index 0000000..a1c5078 --- /dev/null +++ b/core/java/com/android/internal/util/HierarchicalStateMachine.java @@ -0,0 +1,1164 @@ +/** + * 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.internal.util; + +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; +import android.util.Log; + +import java.util.ArrayList; +import java.util.HashMap; + +/** + * {@hide} + * + * A hierarchical state machine is a state machine which processes messages + * and can have states arranged hierarchically. A state is a <code>HierarchicalState</code> + * object and must implement <code>processMessage</code> and optionally <code>enter/exit/getName</code>. + * The enter/exit methods are equivalent to the construction and destruction + * in Object Oriented programming and are used to perform initialization and + * cleanup of the state respectively. The <code>getName</code> method returns the + * name of the state the default implementation returns the class name it may be + * desirable to have this return the name of the state instance name instead. + * In particular if a particular state class has multiple instances. + * + * When a state machine is created <code>addState</code> is used to build the + * hierarchy and <code>setInitialState</code> is used to identify which of these + * is the initial state. After construction the programmer calls <code>start</code> + * which initializes the state machine and calls <code>enter</code> for all of the initial + * state's hierarchy, starting at its eldest parent. For example given the simple + * state machine below after start is called mP1.enter will have been called and + * then mS1.enter. +<code> + mP1 + / \ + mS2 mS1 ----> initial state +</code> + * After the state machine is created and started, messages are sent to a state + * machine using <code>sendMessage</code and the messages are created using + * <code>obtainMessage</code>. When the state machine receives a message the + * current state's <code>processMessage</code> is invoked. In the above example + * mS1.processMessage will be invoked first. The state may use <code>transitionTo</code> + * to change the current state to a new state + * + * Each state in the state machine may have a zero or one parent states and if + * a child state is unable to handle a message it may have the message processed + * by its parent by returning false. If a message is never processed <code>unhandledMessage</code> + * will be invoked to give one last chance for the state machine to process + * the message. + * + * When all processing is completed a state machine may choose to call + * <code>transitionToHaltingState</code>. When the current <code>processingMessage</code> + * returns the state machine will transfer to an internal <code>HaltingState</code> + * and invoke <code>halting</code>. Any message subsequently received by the state + * machine will cause <code>haltedProcessMessage</code> to be invoked. + * + * In addition to <code>processMessage</code> each <code>HierarchicalState</code> has + * an <code>enter</code> method and <code>exit</exit> method which may be overridden. + * + * Since the states are arranged in a hierarchy transitioning to a new state + * causes current states to be exited and new states to be entered. To determine + * the list of states to be entered/exited the common parent closest to + * the current state is found. We then exit from the current state and its + * parent's up to but not including the common parent state and then enter all + * of the new states below the common parent down to the destination state. + * If there is no common parent all states are exited and then the new states + * are entered. + * + * Two other methods that states can use are <code>deferMessage</code> and + * <code>sendMessageAtFrontOfQueue</code>. The <code>sendMessageAtFrontOfQueue</code> sends + * a message but places it on the front of the queue rather than the back. The + * <code>deferMessage</code> causes the message to be saved on a list until a + * transition is made to a new state. At which time all of the deferred messages + * will be put on the front of the state machine queue with the oldest message + * at the front. These will then be processed by the new current state before + * any other messages that are on the queue or might be added later. Both of + * these are protected and may only be invoked from within a state machine. + * + * To illustrate some of these properties we'll use state machine with 8 + * state hierarchy: +<code> + mP0 + / \ + mP1 mS0 + / \ + mS2 mS1 + / \ \ + mS3 mS4 mS5 ---> initial state +</code> + * + * After starting mS5 the list of active states is mP0, mP1, mS1 and mS5. + * So the order of calling processMessage when a message is received is mS5, + * mS1, mP1, mP0 assuming each processMessage indicates it can't handle this + * message by returning false. + * + * Now assume mS5.processMessage receives a message it can handle, and during + * the handling determines the machine should changes states. It would call + * transitionTo(mS4) and return true. Immediately after returning from + * processMessage the state machine runtime will find the common parent, + * which is mP1. It will then call mS5.exit, mS1.exit, mS2.enter and then + * mS4.enter. The new list of active states is mP0, mP1, mS2 and mS4. So + * when the next message is received mS4.processMessage will be invoked. + * + * To assist in describing an HSM a simple grammar has been created which + * is informally defined here and a formal EBNF description is at the end + * of the class comment. + * + * An HSM starts with the name and includes a set of hierarchical states. + * A state is preceeded by one or more plus signs (+), to indicate its + * depth and a hash (#) if its the initial state. Child states follow their + * parents and have one more plus sign then their parent. Inside a state + * are a series of messages, the actions they perform and if the processing + * is complete ends with a period (.). If processing isn't complete and + * the parent should process the message it ends with a caret (^). The + * actions include send a message ($MESSAGE), defer a message (%MESSAGE), + * transition to a new state (>MESSAGE) and an if statement + * (if ( expression ) { list of actions }.) + * + * The Hsm HelloWorld could documented as: + * + * HelloWorld { + * + # mState1. + * } + * + * and interpreted as HSM HelloWorld: + * + * mState1 a root state (single +) and initial state (#) which + * processes all messages completely, the period (.). + * + * The implementation is: +<code> +class HelloWorld extends HierarchicalStateMachine { + Hsm1(String name) { + super(name); + addState(mState1); + setInitialState(mState1); + } + + public static HelloWorld makeHelloWorld() { + HelloWorld hw = new HelloWorld("hw"); + hw.start(); + return hw; + } + + class State1 extends HierarchicalState { + @Override public boolean processMessage(Message message) { + Log.d(TAG, "Hello World"); + return true; + } + } + State1 mState1 = new State1(); +} + +void testHelloWorld() { + HelloWorld hw = makeHelloWorld(); + hw.sendMessage(hw.obtainMessage()); +} +</code> + * + * A more interesting state machine is one of four states + * with two independent parent states. +<code> + mP1 mP2 + / \ + mS2 mS1 +</code> + * + * documented as: + * + * Hsm1 { + * + mP1 { + * CMD_2 { + * $CMD_3 + * %CMD_2 + * >mS2 + * }. + * } + * ++ # mS1 { CMD_1{ >mS1 }^ } + * ++ mS2 { + * CMD_2{$CMD_4}. + * CMD_3{%CMD_3 ; >mP2}. + * } + * + * + mP2 e($CMD_5) { + * CMD_3, CMD_4. + * CMD_5{>HALT}. + * } + * } + * + * and interpreted as HierarchicalStateMachine Hsm1: + * + * mP1 a root state. + * processes message CMD_2 which sends CMD_3, defers CMD_2, and transitions to mS2 + * + * mS1 a child of mP1 is the initial state: + * processes message CMD_1 which transitions to itself and returns false to let mP1 handle it. + * + * mS2 a child of mP1: + * processes message CMD_2 which send CMD_4 + * processes message CMD_3 which defers CMD_3 and transitions to mP2 + * + * mP2 a root state. + * on enter it sends CMD_5 + * processes message CMD_3 + * processes message CMD_4 + * processes message CMD_5 which transitions to halt state + * + * The implementation is below and also in HierarchicalStateMachineTest: +<code> +class Hsm1 extends HierarchicalStateMachine { + private static final String TAG = "hsm1"; + + public static final int CMD_1 = 1; + public static final int CMD_2 = 2; + public static final int CMD_3 = 3; + public static final int CMD_4 = 4; + public static final int CMD_5 = 5; + + public static Hsm1 makeHsm1() { + Log.d(TAG, "makeHsm1 E"); + Hsm1 sm = new Hsm1("hsm1"); + sm.start(); + Log.d(TAG, "makeHsm1 X"); + return sm; + } + + Hsm1(String name) { + super(name); + Log.d(TAG, "ctor E"); + + // Add states, use indentation to show hierarchy + addState(mP1); + addState(mS1, mP1); + addState(mS2, mP1); + addState(mP2); + + // Set the initial state + setInitialState(mS1); + Log.d(TAG, "ctor X"); + } + + class P1 extends HierarchicalState { + @Override public void enter() { + Log.d(TAG, "mP1.enter"); + } + @Override public boolean processMessage(Message message) { + boolean retVal; + Log.d(TAG, "mP1.processMessage what=" + message.what); + switch(message.what) { + case CMD_2: + // CMD_2 will arrive in mS2 before CMD_3 + sendMessage(obtainMessage(CMD_3)); + deferMessage(message); + transitionTo(mS2); + retVal = true; + break; + default: + // Any message we don't understand in this state invokes unhandledMessage + retVal = false; + break; + } + return retVal; + } + @Override public void exit() { + Log.d(TAG, "mP1.exit"); + } + } + + class S1 extends HierarchicalState { + @Override public void enter() { + Log.d(TAG, "mS1.enter"); + } + @Override public boolean processMessage(Message message) { + Log.d(TAG, "S1.processMessage what=" + message.what); + if (message.what == CMD_1) { + // Transition to ourself to show that enter/exit is called + transitionTo(mS1); + return true; + } else { + // Let parent process all other messages + return false; + } + } + @Override public void exit() { + Log.d(TAG, "mS1.exit"); + } + } + + class S2 extends HierarchicalState { + @Override public void enter() { + Log.d(TAG, "mS2.enter"); + } + @Override public boolean processMessage(Message message) { + boolean retVal; + Log.d(TAG, "mS2.processMessage what=" + message.what); + switch(message.what) { + case(CMD_2): + sendMessage(obtainMessage(CMD_4)); + retVal = true; + break; + case(CMD_3): + deferMessage(message); + transitionTo(mP2); + retVal = true; + break; + default: + retVal = false; + break; + } + return retVal; + } + @Override public void exit() { + Log.d(TAG, "mS2.exit"); + } + } + + class P2 extends HierarchicalState { + @Override public void enter() { + Log.d(TAG, "mP2.enter"); + sendMessage(obtainMessage(CMD_5)); + } + @Override public boolean processMessage(Message message) { + Log.d(TAG, "P2.processMessage what=" + message.what); + switch(message.what) { + case(CMD_3): + break; + case(CMD_4): + break; + case(CMD_5): + transitionToHaltingState(); + break; + } + return true; + } + @Override public void exit() { + Log.d(TAG, "mP2.exit"); + } + } + + @Override + protected void halting() { + Log.d(TAG, "halting"); + synchronized (this) { + this.notifyAll(); + } + } + + P1 mP1 = new P1(); + S1 mS1 = new S1(); + S2 mS2 = new S2(); + P2 mP2 = new P2(); +} +</code> + * + * If this is executed by sending two messages CMD_1 and CMD_2 + * (Note the synchronize is only needed because we use hsm.wait()) + * + * Hsm1 hsm = makeHsm1(); + * synchronize(hsm) { + * hsm.sendMessage(obtainMessage(hsm.CMD_1)); + * hsm.sendMessage(obtainMessage(hsm.CMD_2)); + * try { + * // wait for the messages to be handled + * hsm.wait(); + * } catch (InterruptedException e) { + * Log.e(TAG, "exception while waiting " + e.getMessage()); + * } + * } + * + * + * The output is: + * + * D/hsm1 ( 1999): makeHsm1 E + * D/hsm1 ( 1999): ctor E + * D/hsm1 ( 1999): ctor X + * D/hsm1 ( 1999): mP1.enter + * D/hsm1 ( 1999): mS1.enter + * D/hsm1 ( 1999): makeHsm1 X + * D/hsm1 ( 1999): mS1.processMessage what=1 + * D/hsm1 ( 1999): mS1.exit + * D/hsm1 ( 1999): mS1.enter + * D/hsm1 ( 1999): mS1.processMessage what=2 + * D/hsm1 ( 1999): mP1.processMessage what=2 + * D/hsm1 ( 1999): mS1.exit + * D/hsm1 ( 1999): mS2.enter + * D/hsm1 ( 1999): mS2.processMessage what=2 + * D/hsm1 ( 1999): mS2.processMessage what=3 + * D/hsm1 ( 1999): mS2.exit + * D/hsm1 ( 1999): mP1.exit + * D/hsm1 ( 1999): mP2.enter + * D/hsm1 ( 1999): mP2.processMessage what=3 + * D/hsm1 ( 1999): mP2.processMessage what=4 + * D/hsm1 ( 1999): mP2.processMessage what=5 + * D/hsm1 ( 1999): mP2.exit + * D/hsm1 ( 1999): halting + * + * Here is the HSM a BNF grammar, this is a first stab at creating an + * HSM description language, suggestions corrections or alternatives + * would be much appreciated. + * + * Legend: + * {} ::= zero or more + * {}+ ::= one or more + * [] ::= zero or one + * () ::= define a group with "or" semantics. + * + * HSM EBNF: + * HSM = HSM_NAME "{" { STATE }+ "}" ; + * HSM_NAME = alpha_numeric_name ; + * STATE = INTRODUCE_STATE [ ENTER ] "{" [ MESSAGES ] "}" [ EXIT ] ; + * INTRODUCE_STATE = { STATE_DEPTH }+ [ INITIAL_STATE_INDICATOR ] STATE_NAME ; + * STATE_DEPTH = "+" ; + * INITIAL_STATE_INDICATOR = "#" + * ENTER = "e(" SEND_ACTION | TRANSITION_ACTION | HALT_ACTION ")" ; + * MESSAGES = { MSG_LIST MESSAGE_ACTIONS } ; + * MSG_LIST = { MSG_NAME { "," MSG_NAME } ; + * EXIT = "x(" SEND_ACTION | TRANSITION_ACTION | HALT_ACTION ")" ; + * PROCESS_COMPLETION = PROCESS_IN_PARENT_OR_COMPLETE | PROCESS_COMPLETE ; + * SEND_ACTION = "$" MSG_NAME ; + * DEFER_ACTION = "%" MSG_NAME ; + * TRANSITION_ACTION = ">" STATE_NAME ; + * HALT_ACTION = ">" HALT ; + * MESSAGE_ACTIONS = { "{" ACTION_LIST "}" } [ PROCESS_COMPLETION ] ; + * ACTION_LIST = ACTION { (";" | "\n") ACTION } ; + * ACTION = IF_ACTION | SEND_ACTION | DEFER_ACTION | TRANSITION_ACTION | HALT_ACTION ; + * IF_ACTION = "if(" boolean_expression ")" "{" ACTION_LIST "}" + * PROCESS_IN_PARENT_OR_COMPLETE = "^" ; + * PROCESS_COMPLETE = "." ; + * STATE_NAME = alpha_numeric_name ; + * MSG_NAME = alpha_numeric_name | ALL_OTHER_MESSAGES ; + * ALL_OTHER_MESSAGES = "*" ; + * EXP = boolean_expression ; + * + * Idioms: + * * { %* }. ::= All other messages will be deferred. + */ +public class HierarchicalStateMachine { + + private static final String TAG = "HierarchicalStateMachine"; + private String mName; + + private static class HsmHandler extends Handler { + + /** The debug flag */ + private boolean mDbg = false; + + /** A list of messages that this state machine has processed */ + private ProcessedMessages mProcessedMessages = new ProcessedMessages(); + + /** true if construction of the state machine has not been completed */ + private boolean mIsConstructionCompleted; + + /** Stack used to manage the current hierarchy of states */ + private StateInfo mStateStack[]; + + /** Top of mStateStack */ + private int mStateStackTopIndex = -1; + + /** A temporary stack used to manage the state stack */ + private StateInfo mTempStateStack[]; + + /** The top of the mTempStateStack */ + private int mTempStateStackCount; + + /** State used when state machine is halted */ + private HaltingState mHaltingState = new HaltingState(); + + /** Reference to the HierarchicalStateMachine */ + private HierarchicalStateMachine mHsm; + + /** + * Information about a state. + * Used to maintain the hierarchy. + */ + private class StateInfo { + /** The state */ + HierarchicalState state; + + /** The parent of this state, null if there is no parent */ + StateInfo parentStateInfo; + + /** True when the state has been entered and on the stack */ + boolean active; + + /** + * Convert StateInfo to string + */ + @Override + public String toString() { + return "state=" + state.getName() + ",active=" + active + + ",parent=" + ((parentStateInfo == null) ? + "null" : parentStateInfo.state.getName()); + } + } + + /** The map of all of the states in the state machine */ + private HashMap<HierarchicalState, StateInfo> mStateInfo = + new HashMap<HierarchicalState, StateInfo>(); + + /** The initial state that will process the first message */ + private HierarchicalState mInitialState; + + /** The destination state when transitionTo has been invoked */ + private HierarchicalState mDestState; + + /** The list of deferred messages */ + private ArrayList<Message> mDeferredMessages = new ArrayList<Message>(); + + /** + * State entered when transitionToHaltingState is called. + */ + private class HaltingState extends HierarchicalState { + @Override + public boolean processMessage(Message msg) { + mHsm.haltedProcessMessage(msg); + return true; + } + } + + /** + * Handle messages sent to the state machine by calling + * the current state's processMessage. It also handles + * the enter/exit calls and placing any deferred messages + * back onto the queue when transitioning to a new state. + */ + @Override + public final void handleMessage(Message msg) { + if (mDbg) Log.d(TAG, "handleMessage: E msg.what=" + msg.what); + + /** + * Check that construction was completed + */ + if (!mIsConstructionCompleted) { + Log.e(TAG, "The start method not called, ignore msg: " + msg); + return; + } + + /** + * Process the message abiding by the hierarchical semantics. + */ + processMsg(msg); + + /** + * If transitionTo has been called, exit and then enter + * the appropriate states. + */ + if (mDestState != null) { + if (mDbg) Log.d(TAG, "handleMessage: new destination call exit"); + + /** + * Determine the states to exit and enter and return the + * common ancestor state of the enter/exit states. Then + * invoke the exit methods then the enter methods. + */ + StateInfo commonStateInfo = setupTempStateStackWithStatesToEnter(mDestState); + invokeExitMethods(commonStateInfo); + int stateStackEnteringIndex = moveTempStateStackToStateStack(); + invokeEnterMethods(stateStackEnteringIndex); + + + /** + * Since we have transitioned to a new state we need to have + * any deferred messages moved to the front of the message queue + * so they will be processed before any other messages in the + * message queue. + */ + moveDeferredMessageAtFrontOfQueue(); + + /** + * Call halting() if we've transitioned to the halting + * state. All subsequent messages will be processed in + * in the halting state which invokes haltedProcessMessage(msg); + */ + if (mDestState == mHaltingState) { + mHsm.halting(); + } + mDestState = null; + } + + if (mDbg) Log.d(TAG, "handleMessage: X"); + } + + /** + * Complete the construction of the state machine. + */ + private final void completeConstruction() { + if (mDbg) Log.d(TAG, "completeConstruction: E"); + + /** + * Determine the maximum depth of the state hierarchy + * so we can allocate the state stacks. + */ + int maxDepth = 0; + for (StateInfo si : mStateInfo.values()) { + int depth = 0; + for (StateInfo i = si; i != null; depth++) { + i = i.parentStateInfo; + } + if (maxDepth < depth) { + maxDepth = depth; + } + } + if (mDbg) Log.d(TAG, "completeConstruction: maxDepth=" + maxDepth); + + mStateStack = new StateInfo[maxDepth]; + mTempStateStack = new StateInfo[maxDepth]; + setupInitialStateStack(); + + /** + * Construction is complete call all enter methods + * starting at the first entry. + */ + mIsConstructionCompleted = true; + invokeEnterMethods(0); + + if (mDbg) Log.d(TAG, "completeConstruction: X"); + } + + /** + * Process the message. If the current state doesn't handle + * it, call the states parent and so on. If it is never handled then + * call the state machines unhandledMessage method. + */ + private final void processMsg(Message msg) { + StateInfo curStateInfo = mStateStack[mStateStackTopIndex]; + if (mDbg) { + Log.d(TAG, "processMsg: " + curStateInfo.state.getName()); + } + while (!curStateInfo.state.processMessage(msg)) { + /** + * Not processed + */ + curStateInfo = curStateInfo.parentStateInfo; + if (curStateInfo == null) { + /** + * No parents left so it's not handled + */ + mHsm.unhandledMessage(msg); + break; + } + if (mDbg) { + Log.d(TAG, "processMsg: " + curStateInfo.state.getName()); + } + } + + /** + * Record that we processed the message + */ + if (curStateInfo != null) { + HierarchicalState orgState = mStateStack[mStateStackTopIndex].state; + mProcessedMessages.add(msg, curStateInfo.state, orgState); + } else { + mProcessedMessages.add(msg, null, null); + } + } + + /** + * Call the exit method for each state from the top of stack + * up to the common ancestor state. + */ + private final void invokeExitMethods(StateInfo commonStateInfo) { + while ((mStateStackTopIndex >= 0) && + (mStateStack[mStateStackTopIndex] != commonStateInfo)) { + HierarchicalState curState = mStateStack[mStateStackTopIndex].state; + if (mDbg) Log.d(TAG, "invokeExitMethods: " + curState.getName()); + curState.exit(); + mStateStack[mStateStackTopIndex].active = false; + mStateStackTopIndex -= 1; + } + } + + /** + * Invoke the enter method starting at the entering index to top of state stack + */ + private final void invokeEnterMethods(int stateStackEnteringIndex) { + for (int i = stateStackEnteringIndex; i <= mStateStackTopIndex; i++) { + if (mDbg) Log.d(TAG, "invokeEnterMethods: " + mStateStack[i].state.getName()); + mStateStack[i].state.enter(); + mStateStack[i].active = true; + } + } + + /** + * Move the deferred message to the front of the message queue. + */ + private final void moveDeferredMessageAtFrontOfQueue() { + /** + * The oldest messages on the deferred list must be at + * the front of the queue so start at the back, which + * as the most resent message and end with the oldest + * messages at the front of the queue. + */ + for (int i = mDeferredMessages.size() - 1; i >= 0; i-- ) { + Message curMsg = mDeferredMessages.get(i); + if (mDbg) Log.d(TAG, "moveDeferredMessageAtFrontOfQueue; what=" + curMsg.what); + sendMessageAtFrontOfQueue(curMsg); + } + mDeferredMessages.clear(); + } + + /** + * Move the contents of the temporary stack to the state stack + * reversing the order of the items on the temporary stack as + * they are moved. + * + * @return index into mStateState where entering needs to start + */ + private final int moveTempStateStackToStateStack() { + int startingIndex = mStateStackTopIndex + 1; + int i = mTempStateStackCount - 1; + int j = startingIndex; + while (i >= 0) { + if (mDbg) Log.d(TAG, "moveTempStackToStateStack: i=" + i + ",j=" + j); + mStateStack[j] = mTempStateStack[i]; + j += 1; + i -= 1; + } + + mStateStackTopIndex = j - 1; + if (mDbg) { + Log.d(TAG, "moveTempStackToStateStack: X mStateStackTop=" + + mStateStackTopIndex + ",startingIndex=" + startingIndex + + ",Top=" + mStateStack[mStateStackTopIndex].state.getName()); + } + return startingIndex; + } + + /** + * Setup the mTempStateStack with the states we are going to enter. + * + * This is found by searching up the destState's ancestors for a + * state that is already active i.e. StateInfo.active == true. + * The destStae and all of its inactive parents will be on the + * TempStateStack as the list of states to enter. + * + * @return StateInfo of the common ancestor for the destState and + * current state or null if there is no common parent. + */ + private final StateInfo setupTempStateStackWithStatesToEnter(HierarchicalState destState) { + /** + * Search up the parent list of the destination state for an active + * state. Use a do while() loop as the destState must always be entered + * even if it is active. This can happen if we are exiting/entering + * the current state. + */ + mTempStateStackCount = 0; + StateInfo curStateInfo = mStateInfo.get(destState); + do { + mTempStateStack[mTempStateStackCount++] = curStateInfo; + curStateInfo = curStateInfo.parentStateInfo; + } while ((curStateInfo != null) && !curStateInfo.active); + + if (mDbg) { + Log.d(TAG, "setupTempStateStackWithStatesToEnter: X mTempStateStackCount=" + + mTempStateStackCount + ",curStateInfo: " + curStateInfo); + } + return curStateInfo; + } + + /** + * Initialize StateStack to mInitialState. + */ + private final void setupInitialStateStack() { + if (mDbg) { + Log.d(TAG, "setupInitialStateStack: E mInitialState=" + + mInitialState.getName()); + } + + StateInfo curStateInfo = mStateInfo.get(mInitialState); + for (mTempStateStackCount = 0; curStateInfo != null; mTempStateStackCount++) { + mTempStateStack[mTempStateStackCount] = curStateInfo; + curStateInfo = curStateInfo.parentStateInfo; + } + + // Empty the StateStack + mStateStackTopIndex = -1; + + moveTempStateStackToStateStack(); + } + + /** + * @return current state + */ + private final HierarchicalState getCurrentState() { + return mStateStack[mStateStackTopIndex].state; + } + + /** + * Add a new state to the state machine. Bottom up addition + * of states is allowed but the same state may only exist + * in one hierarchy. + * + * @param state the state to add + * @param parent the parent of state + * @return stateInfo for this state + */ + private final StateInfo addState(HierarchicalState state, HierarchicalState parent) { + if (mDbg) { + Log.d(TAG, "addStateInternal: E state=" + state.getName() + + ",parent=" + ((parent == null) ? "" : parent.getName())); + } + StateInfo parentStateInfo = null; + if (parent != null) { + parentStateInfo = mStateInfo.get(parent); + if (parentStateInfo == null) { + // Recursively add our parent as it's not been added yet. + parentStateInfo = addState(parent, null); + } + } + StateInfo stateInfo = mStateInfo.get(state); + if (stateInfo == null) { + stateInfo = new StateInfo(); + mStateInfo.put(state, stateInfo); + } + + // Validate that we aren't adding the same state in two different hierarchies. + if ((stateInfo.parentStateInfo != null) && + (stateInfo.parentStateInfo != parentStateInfo)) { + throw new RuntimeException("state already added"); + } + stateInfo.state = state; + stateInfo.parentStateInfo = parentStateInfo; + stateInfo.active = false; + if (mDbg) Log.d(TAG, "addStateInternal: X stateInfo: " + stateInfo); + return stateInfo; + } + + /** + * Constructor + * + * @param looper for dispatching messages + * @param hsm the hierarchical state machine + */ + private HsmHandler(Looper looper, HierarchicalStateMachine hsm) { + super(looper); + mHsm = hsm; + + addState(mHaltingState, null); + } + + /** @see HierarchicalStateMachine#setInitialState(HierarchicalState) */ + private final void setInitialState(HierarchicalState initialState) { + if (mDbg) Log.d(TAG, "setInitialState: initialState" + initialState.getName()); + mInitialState = initialState; + } + + /** @see HierarchicalStateMachine#transitionTo(HierarchicalState) */ + private final void transitionTo(HierarchicalState destState) { + if (mDbg) Log.d(TAG, "StateMachine.transitionTo EX destState" + destState.getName()); + mDestState = destState; + } + + /** @see HierarchicalStateMachine#deferMessage(Message) */ + private final void deferMessage(Message msg) { + if (mDbg) Log.d(TAG, "deferMessage: msg=" + msg.what); + + /* Copy the "msg" to "newMsg" as "msg" will be recycled */ + Message newMsg = obtainMessage(); + newMsg.copyFrom(msg); + + mDeferredMessages.add(newMsg); + } + + /** @see HierarchicalStateMachine#isDbg() */ + private final boolean isDbg() { + return mDbg; + } + + /** @see HierarchicalStateMachine#setDbg(boolean) */ + private final void setDbg(boolean dbg) { + mDbg = dbg; + } + + /** @see HierarchicalStateMachine#setProcessedMessagesSize(int) */ + private final void setProcessedMessagesSize(int maxSize) { + mProcessedMessages.setSize(maxSize); + } + + /** @see HierarchicalStateMachine#getProcessedMessagesSize() */ + private final int getProcessedMessagesSize() { + return mProcessedMessages.size(); + } + + /** @see HierarchicalStateMachine#getProcessedMessagesCount() */ + private final int getProcessedMessagesCount() { + return mProcessedMessages.count(); + } + + /** @see HierarchicalStateMachine#getProcessedMessage(int) */ + private final ProcessedMessages.Info getProcessedMessage(int index) { + return mProcessedMessages.get(index); + } + + } + + private HsmHandler mHsmHandler; + private HandlerThread mHsmThread; + + /** + * Initialize. + * + * @param looper for this state machine + * @param name of the state machine + */ + private void initStateMachine(Looper looper, String name) { + mName = name; + mHsmHandler = new HsmHandler(looper, this); + } + + /** + * Constructor creates an HSM with its own thread. + * + * @param name of the state machine + */ + protected HierarchicalStateMachine(String name) { + mHsmThread = new HandlerThread(name); + mHsmThread.start(); + Looper looper = mHsmThread.getLooper(); + + initStateMachine(looper, name); + } + + /** + * Constructor creates an HSMStateMachine using the looper. + * + * @param name of the state machine + */ + protected HierarchicalStateMachine(Looper looper, String name) { + initStateMachine(looper, name); + } + + /** + * Add a new state to the state machine + * @param state the state to add + * @param parent the parent of state + */ + protected final void addState(HierarchicalState state, HierarchicalState parent) { + mHsmHandler.addState(state, parent); + } + /** + * @return current state + */ + protected final HierarchicalState getCurrentState() { + return mHsmHandler.getCurrentState(); + } + + + /** + * Add a new state to the state machine, parent will be null + * @param state to add + */ + protected final void addState(HierarchicalState state) { + mHsmHandler.addState(state, null); + } + + /** + * Set the initial state. This must be invoked before + * and messages are sent to the state machine. + * + * @param initialState is the state which will receive the first message. + */ + protected final void setInitialState(HierarchicalState initialState) { + mHsmHandler.setInitialState(initialState); + } + + /** + * transition to destination state. Upon returning + * from processMessage the current state's exit will + * be executed and upon the next message arriving + * destState.enter will be invoked. + * + * @param destState will be the state that receives the next message. + */ + protected final void transitionTo(HierarchicalState destState) { + mHsmHandler.transitionTo(destState); + } + + /** + * transition to halt state. Upon returning + * from processMessage we will exit all current + * states, execute the halting() method and then + * all subsequent messages haltedProcessMesage + * will be called. + */ + protected final void transitionToHaltingState() { + mHsmHandler.transitionTo(mHsmHandler.mHaltingState); + } + + /** + * Defer this message until next state transition. + * Upon transitioning all deferred messages will be + * placed on the queue and reprocessed in the original + * order. (i.e. The next state the oldest messages will + * be processed first) + * + * @param msg is deferred until the next transition. + */ + protected final void deferMessage(Message msg) { + mHsmHandler.deferMessage(msg); + } + + + /** + * Called when message wasn't handled + * + * @param msg that couldn't be handled. + */ + protected void unhandledMessage(Message msg) { + Log.e(TAG, "unhandledMessage: msg.what=" + msg.what); + } + + /** + * Called for any message that is received after + * transitionToHalting is called. + */ + protected void haltedProcessMessage(Message msg) { + } + + /** + * Called after the message that called transitionToHalting + * is called and should be overridden by StateMachine's that + * call transitionToHalting. + */ + protected void halting() { + } + + /** + * @return the name + */ + public final String getName() { + return mName; + } + + /** + * Set size of messages to maintain and clears all current messages. + * + * @param maxSize number of messages to maintain at anyone time. + */ + public final void setProcessedMessagesSize(int maxSize) { + mHsmHandler.setProcessedMessagesSize(maxSize); + } + + /** + * @return number of messages processed + */ + public final int getProcessedMessagesSize() { + return mHsmHandler.getProcessedMessagesSize(); + } + + /** + * @return the total number of messages processed + */ + public final int getProcessedMessagesCount() { + return mHsmHandler.getProcessedMessagesCount(); + } + + /** + * @return a processed message + */ + public final ProcessedMessages.Info getProcessedMessage(int index) { + return mHsmHandler.getProcessedMessage(index); + } + + /** + * @return Handler + */ + public final Handler getHandler() { + return mHsmHandler; + } + + /** + * Get a message and set Message.target = this. + * + * @return message + */ + public final Message obtainMessage() + { + return Message.obtain(mHsmHandler); + } + + /** + * Get a message and set Message.target = this and what + * + * @param what is the assigned to Message.what. + * @return message + */ + public final Message obtainMessage(int what) { + return Message.obtain(mHsmHandler, what); + } + + /** + * Get a message and set Message.target = this, + * what and obj. + * + * @param what is the assigned to Message.what. + * @param obj is assigned to Message.obj. + * @return message + */ + public final Message obtainMessage(int what, Object obj) + { + return Message.obtain(mHsmHandler, what, obj); + } + + /** + * Enqueue a message to this state machine. + */ + public final void sendMessage(Message msg) { + mHsmHandler.sendMessage(msg); + } + + /** + * Enqueue a message to this state machine after a delay. + */ + public final void sendMessageDelayed(Message msg, long delayMillis) { + mHsmHandler.sendMessageDelayed(msg, delayMillis); + } + + /** + * Enqueue a message to the front of the queue for this state machine. + * Protected, may only be called by instances of HierarchicalStateMachine. + */ + protected final void sendMessageAtFrontOfQueue(Message msg) { + mHsmHandler.sendMessageAtFrontOfQueue(msg); + } + + /** + * @return if debugging is enabled + */ + public boolean isDbg() { + return mHsmHandler.isDbg(); + } + + /** + * Set debug enable/disabled. + * + * @param dbg is true to enable debugging. + */ + public void setDbg(boolean dbg) { + mHsmHandler.setDbg(dbg); + } + + /** + * Start the state machine. + */ + public void start() { + /** Send the complete construction message */ + mHsmHandler.completeConstruction(); + } +} diff --git a/core/java/com/android/internal/util/ProcessedMessages.java b/core/java/com/android/internal/util/ProcessedMessages.java new file mode 100644 index 0000000..244474e --- /dev/null +++ b/core/java/com/android/internal/util/ProcessedMessages.java @@ -0,0 +1,198 @@ +/** + * 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.internal.util; + +import android.os.Message; + +import java.util.Vector; + +/** + * {@hide} + * + * A list of messages recently processed by the state machine. + * + * The class maintains a list of messages that have been most + * recently processed. The list is finite and may be set in the + * constructor or by calling setSize. The public interface also + * includes size which returns the number of recent messages, + * count which is the number of message processed since the + * the last setSize, get which returns a processed message and + * add which adds a processed messaged. + */ +public class ProcessedMessages { + + public static final int DEFAULT_SIZE = 20; + + /** + * The information maintained for a processed message. + */ + public class Info { + private int what; + private HierarchicalState state; + private HierarchicalState orgState; + + /** + * Constructor + * @param message + * @param state that handled the message + * @param orgState is the first state the received the message but + * did not processes the message. + */ + Info(Message message, HierarchicalState state, HierarchicalState orgState) { + update(message, state, orgState); + } + + /** + * Update the information in the record. + * @param state that handled the message + * @param orgState is the first state the received the message but + * did not processes the message. + */ + public void update(Message message, HierarchicalState state, HierarchicalState orgState) { + this.what = message.what; + this.state = state; + this.orgState = orgState; + } + + /** + * @return the command that was executing + */ + public int getWhat() { + return what; + } + + /** + * @return the state that handled this message + */ + public HierarchicalState getState() { + return state; + } + + /** + * @return the original state that received the message. + */ + public HierarchicalState getOriginalState() { + return orgState; + } + + /** + * @return as string + */ + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("what="); + sb.append(what); + sb.append(" state="); + sb.append(cn(state)); + sb.append(" orgState="); + sb.append(cn(orgState)); + return sb.toString(); + } + + /** + * @return an objects class name + */ + private String cn(Object n) { + if (n == null) { + return "null"; + } else { + String name = n.getClass().getName(); + int lastDollar = name.lastIndexOf('$'); + return name.substring(lastDollar + 1); + } + } + } + + private Vector<Info> mMessages = new Vector<Info>(); + private int mMaxSize = DEFAULT_SIZE; + private int mOldestIndex = 0; + private int mCount = 0; + + /** + * Constructor + */ + ProcessedMessages() { + } + + ProcessedMessages(int maxSize) { + setSize(maxSize); + } + + /** + * Set size of messages to maintain and clears all current messages. + * + * @param maxSize number of messages to maintain at anyone time. + */ + void setSize(int maxSize) { + mMaxSize = maxSize; + mCount = 0; + mMessages.clear(); + } + + /** + * @return the number of recent messages. + */ + int size() { + return mMessages.size(); + } + + /** + * @return the total number of messages processed since size was set. + */ + int count() { + return mCount; + } + + /** + * @return the information on a particular record. 0 is the oldest + * record and size()-1 is the newest record. If the index is to + * large null is returned. + */ + Info get(int index) { + int nextIndex = mOldestIndex + index; + if (nextIndex >= mMaxSize) { + nextIndex -= mMaxSize; + } + if (nextIndex >= size()) { + return null; + } else { + return mMessages.get(nextIndex); + } + } + + /** + * Add a processed message. + * + * @param message + * @param state that handled the message + * @param orgState is the first state the received the message but + * did not processes the message. + */ + void add(Message message, HierarchicalState state, HierarchicalState orgState) { + mCount += 1; + if (mMessages.size() < mMaxSize) { + mMessages.add(new Info(message, state, orgState)); + } else { + Info info = mMessages.get(mOldestIndex); + mOldestIndex += 1; + if (mOldestIndex >= mMaxSize) { + mOldestIndex = 0; + } + info.update(message, state, orgState); + } + } +} diff --git a/core/java/com/android/internal/util/XmlUtils.java b/core/java/com/android/internal/util/XmlUtils.java deleted file mode 100644 index 948e313..0000000 --- a/core/java/com/android/internal/util/XmlUtils.java +++ /dev/null @@ -1,796 +0,0 @@ -/* - * Copyright (C) 2006 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.internal.util; - -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; -import org.xmlpull.v1.XmlSerializer; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import android.util.Xml; - -/** {@hide} */ -public class XmlUtils -{ - - public static void skipCurrentTag(XmlPullParser parser) - throws XmlPullParserException, IOException { - int outerDepth = parser.getDepth(); - int type; - while ((type=parser.next()) != XmlPullParser.END_DOCUMENT - && (type != XmlPullParser.END_TAG - || parser.getDepth() > outerDepth)) { - } - } - - public static final int - convertValueToList(CharSequence value, String[] options, int defaultValue) - { - if (null != value) { - for (int i = 0; i < options.length; i++) { - if (value.equals(options[i])) - return i; - } - } - - return defaultValue; - } - - public static final boolean - convertValueToBoolean(CharSequence value, boolean defaultValue) - { - boolean result = false; - - if (null == value) - return defaultValue; - - if (value.equals("1") - || value.equals("true") - || value.equals("TRUE")) - result = true; - - return result; - } - - public static final int - convertValueToInt(CharSequence charSeq, int defaultValue) - { - if (null == charSeq) - return defaultValue; - - String nm = charSeq.toString(); - - // XXX This code is copied from Integer.decode() so we don't - // have to instantiate an Integer! - - int value; - int sign = 1; - int index = 0; - int len = nm.length(); - int base = 10; - - if ('-' == nm.charAt(0)) { - sign = -1; - index++; - } - - if ('0' == nm.charAt(index)) { - // Quick check for a zero by itself - if (index == (len - 1)) - return 0; - - char c = nm.charAt(index + 1); - - if ('x' == c || 'X' == c) { - index += 2; - base = 16; - } else { - index++; - base = 8; - } - } - else if ('#' == nm.charAt(index)) - { - index++; - base = 16; - } - - return Integer.parseInt(nm.substring(index), base) * sign; - } - - public static final int - convertValueToUnsignedInt(String value, int defaultValue) - { - if (null == value) - return defaultValue; - - return parseUnsignedIntAttribute(value); - } - - public static final int - parseUnsignedIntAttribute(CharSequence charSeq) - { - String value = charSeq.toString(); - - long bits; - int index = 0; - int len = value.length(); - int base = 10; - - if ('0' == value.charAt(index)) { - // Quick check for zero by itself - if (index == (len - 1)) - return 0; - - char c = value.charAt(index + 1); - - if ('x' == c || 'X' == c) { // check for hex - index += 2; - base = 16; - } else { // check for octal - index++; - base = 8; - } - } else if ('#' == value.charAt(index)) { - index++; - base = 16; - } - - return (int) Long.parseLong(value.substring(index), base); - } - - /** - * Flatten a Map into an output stream as XML. The map can later be - * read back with readMapXml(). - * - * @param val The map to be flattened. - * @param out Where to write the XML data. - * - * @see #writeMapXml(Map, String, XmlSerializer) - * @see #writeListXml - * @see #writeValueXml - * @see #readMapXml - */ - public static final void writeMapXml(Map val, OutputStream out) - throws XmlPullParserException, java.io.IOException { - XmlSerializer serializer = new FastXmlSerializer(); - serializer.setOutput(out, "utf-8"); - serializer.startDocument(null, true); - serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); - writeMapXml(val, null, serializer); - serializer.endDocument(); - } - - /** - * Flatten a List into an output stream as XML. The list can later be - * read back with readListXml(). - * - * @param val The list to be flattened. - * @param out Where to write the XML data. - * - * @see #writeListXml(List, String, XmlSerializer) - * @see #writeMapXml - * @see #writeValueXml - * @see #readListXml - */ - public static final void writeListXml(List val, OutputStream out) - throws XmlPullParserException, java.io.IOException - { - XmlSerializer serializer = Xml.newSerializer(); - serializer.setOutput(out, "utf-8"); - serializer.startDocument(null, true); - serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); - writeListXml(val, null, serializer); - serializer.endDocument(); - } - - /** - * Flatten a Map into an XmlSerializer. The map can later be read back - * with readThisMapXml(). - * - * @param val The map to be flattened. - * @param name Name attribute to include with this list's tag, or null for - * none. - * @param out XmlSerializer to write the map into. - * - * @see #writeMapXml(Map, OutputStream) - * @see #writeListXml - * @see #writeValueXml - * @see #readMapXml - */ - public static final void writeMapXml(Map val, String name, XmlSerializer out) - throws XmlPullParserException, java.io.IOException - { - if (val == null) { - out.startTag(null, "null"); - out.endTag(null, "null"); - return; - } - - Set s = val.entrySet(); - Iterator i = s.iterator(); - - out.startTag(null, "map"); - if (name != null) { - out.attribute(null, "name", name); - } - - while (i.hasNext()) { - Map.Entry e = (Map.Entry)i.next(); - writeValueXml(e.getValue(), (String)e.getKey(), out); - } - - out.endTag(null, "map"); - } - - /** - * Flatten a List into an XmlSerializer. The list can later be read back - * with readThisListXml(). - * - * @param val The list to be flattened. - * @param name Name attribute to include with this list's tag, or null for - * none. - * @param out XmlSerializer to write the list into. - * - * @see #writeListXml(List, OutputStream) - * @see #writeMapXml - * @see #writeValueXml - * @see #readListXml - */ - public static final void writeListXml(List val, String name, XmlSerializer out) - throws XmlPullParserException, java.io.IOException - { - if (val == null) { - out.startTag(null, "null"); - out.endTag(null, "null"); - return; - } - - out.startTag(null, "list"); - if (name != null) { - out.attribute(null, "name", name); - } - - int N = val.size(); - int i=0; - while (i < N) { - writeValueXml(val.get(i), null, out); - i++; - } - - out.endTag(null, "list"); - } - - /** - * Flatten a byte[] into an XmlSerializer. The list can later be read back - * with readThisByteArrayXml(). - * - * @param val The byte array to be flattened. - * @param name Name attribute to include with this array's tag, or null for - * none. - * @param out XmlSerializer to write the array into. - * - * @see #writeMapXml - * @see #writeValueXml - */ - public static final void writeByteArrayXml(byte[] val, String name, - XmlSerializer out) - throws XmlPullParserException, java.io.IOException { - - if (val == null) { - out.startTag(null, "null"); - out.endTag(null, "null"); - return; - } - - out.startTag(null, "byte-array"); - if (name != null) { - out.attribute(null, "name", name); - } - - final int N = val.length; - out.attribute(null, "num", Integer.toString(N)); - - StringBuilder sb = new StringBuilder(val.length*2); - for (int i=0; i<N; i++) { - int b = val[i]; - int h = b>>4; - sb.append(h >= 10 ? ('a'+h-10) : ('0'+h)); - h = b&0xff; - sb.append(h >= 10 ? ('a'+h-10) : ('0'+h)); - } - - out.text(sb.toString()); - - out.endTag(null, "byte-array"); - } - - /** - * Flatten an int[] into an XmlSerializer. The list can later be read back - * with readThisIntArrayXml(). - * - * @param val The int array to be flattened. - * @param name Name attribute to include with this array's tag, or null for - * none. - * @param out XmlSerializer to write the array into. - * - * @see #writeMapXml - * @see #writeValueXml - * @see #readThisIntArrayXml - */ - public static final void writeIntArrayXml(int[] val, String name, - XmlSerializer out) - throws XmlPullParserException, java.io.IOException { - - if (val == null) { - out.startTag(null, "null"); - out.endTag(null, "null"); - return; - } - - out.startTag(null, "int-array"); - if (name != null) { - out.attribute(null, "name", name); - } - - final int N = val.length; - out.attribute(null, "num", Integer.toString(N)); - - for (int i=0; i<N; i++) { - out.startTag(null, "item"); - out.attribute(null, "value", Integer.toString(val[i])); - out.endTag(null, "item"); - } - - out.endTag(null, "int-array"); - } - - /** - * Flatten an object's value into an XmlSerializer. The value can later - * be read back with readThisValueXml(). - * - * Currently supported value types are: null, String, Integer, Long, - * Float, Double Boolean, Map, List. - * - * @param v The object to be flattened. - * @param name Name attribute to include with this value's tag, or null - * for none. - * @param out XmlSerializer to write the object into. - * - * @see #writeMapXml - * @see #writeListXml - * @see #readValueXml - */ - public static final void writeValueXml(Object v, String name, XmlSerializer out) - throws XmlPullParserException, java.io.IOException - { - String typeStr; - if (v == null) { - out.startTag(null, "null"); - if (name != null) { - out.attribute(null, "name", name); - } - out.endTag(null, "null"); - return; - } else if (v instanceof String) { - out.startTag(null, "string"); - if (name != null) { - out.attribute(null, "name", name); - } - out.text(v.toString()); - out.endTag(null, "string"); - return; - } else if (v instanceof Integer) { - typeStr = "int"; - } else if (v instanceof Long) { - typeStr = "long"; - } else if (v instanceof Float) { - typeStr = "float"; - } else if (v instanceof Double) { - typeStr = "double"; - } else if (v instanceof Boolean) { - typeStr = "boolean"; - } else if (v instanceof byte[]) { - writeByteArrayXml((byte[])v, name, out); - return; - } else if (v instanceof int[]) { - writeIntArrayXml((int[])v, name, out); - return; - } else if (v instanceof Map) { - writeMapXml((Map)v, name, out); - return; - } else if (v instanceof List) { - writeListXml((List)v, name, out); - return; - } else if (v instanceof CharSequence) { - // XXX This is to allow us to at least write something if - // we encounter styled text... but it means we will drop all - // of the styling information. :( - out.startTag(null, "string"); - if (name != null) { - out.attribute(null, "name", name); - } - out.text(v.toString()); - out.endTag(null, "string"); - return; - } else { - throw new RuntimeException("writeValueXml: unable to write value " + v); - } - - out.startTag(null, typeStr); - if (name != null) { - out.attribute(null, "name", name); - } - out.attribute(null, "value", v.toString()); - out.endTag(null, typeStr); - } - - /** - * Read a HashMap from an InputStream containing XML. The stream can - * previously have been written by writeMapXml(). - * - * @param in The InputStream from which to read. - * - * @return HashMap The resulting map. - * - * @see #readListXml - * @see #readValueXml - * @see #readThisMapXml - * #see #writeMapXml - */ - public static final HashMap readMapXml(InputStream in) - throws XmlPullParserException, java.io.IOException - { - XmlPullParser parser = Xml.newPullParser(); - parser.setInput(in, null); - return (HashMap)readValueXml(parser, new String[1]); - } - - /** - * Read an ArrayList from an InputStream containing XML. The stream can - * previously have been written by writeListXml(). - * - * @param in The InputStream from which to read. - * - * @return HashMap The resulting list. - * - * @see #readMapXml - * @see #readValueXml - * @see #readThisListXml - * @see #writeListXml - */ - public static final ArrayList readListXml(InputStream in) - throws XmlPullParserException, java.io.IOException - { - XmlPullParser parser = Xml.newPullParser(); - parser.setInput(in, null); - return (ArrayList)readValueXml(parser, new String[1]); - } - - /** - * Read a HashMap object from an XmlPullParser. The XML data could - * previously have been generated by writeMapXml(). The XmlPullParser - * must be positioned <em>after</em> the tag that begins the map. - * - * @param parser The XmlPullParser from which to read the map data. - * @param endTag Name of the tag that will end the map, usually "map". - * @param name An array of one string, used to return the name attribute - * of the map's tag. - * - * @return HashMap The newly generated map. - * - * @see #readMapXml - */ - public static final HashMap readThisMapXml(XmlPullParser parser, String endTag, String[] name) - throws XmlPullParserException, java.io.IOException - { - HashMap map = new HashMap(); - - int eventType = parser.getEventType(); - do { - if (eventType == parser.START_TAG) { - Object val = readThisValueXml(parser, name); - if (name[0] != null) { - //System.out.println("Adding to map: " + name + " -> " + val); - map.put(name[0], val); - } else { - throw new XmlPullParserException( - "Map value without name attribute: " + parser.getName()); - } - } else if (eventType == parser.END_TAG) { - if (parser.getName().equals(endTag)) { - return map; - } - throw new XmlPullParserException( - "Expected " + endTag + " end tag at: " + parser.getName()); - } - eventType = parser.next(); - } while (eventType != parser.END_DOCUMENT); - - throw new XmlPullParserException( - "Document ended before " + endTag + " end tag"); - } - - /** - * Read an ArrayList object from an XmlPullParser. The XML data could - * previously have been generated by writeListXml(). The XmlPullParser - * must be positioned <em>after</em> the tag that begins the list. - * - * @param parser The XmlPullParser from which to read the list data. - * @param endTag Name of the tag that will end the list, usually "list". - * @param name An array of one string, used to return the name attribute - * of the list's tag. - * - * @return HashMap The newly generated list. - * - * @see #readListXml - */ - public static final ArrayList readThisListXml(XmlPullParser parser, String endTag, String[] name) - throws XmlPullParserException, java.io.IOException - { - ArrayList list = new ArrayList(); - - int eventType = parser.getEventType(); - do { - if (eventType == parser.START_TAG) { - Object val = readThisValueXml(parser, name); - list.add(val); - //System.out.println("Adding to list: " + val); - } else if (eventType == parser.END_TAG) { - if (parser.getName().equals(endTag)) { - return list; - } - throw new XmlPullParserException( - "Expected " + endTag + " end tag at: " + parser.getName()); - } - eventType = parser.next(); - } while (eventType != parser.END_DOCUMENT); - - throw new XmlPullParserException( - "Document ended before " + endTag + " end tag"); - } - - /** - * Read an int[] object from an XmlPullParser. The XML data could - * previously have been generated by writeIntArrayXml(). The XmlPullParser - * must be positioned <em>after</em> the tag that begins the list. - * - * @param parser The XmlPullParser from which to read the list data. - * @param endTag Name of the tag that will end the list, usually "list". - * @param name An array of one string, used to return the name attribute - * of the list's tag. - * - * @return Returns a newly generated int[]. - * - * @see #readListXml - */ - public static final int[] readThisIntArrayXml(XmlPullParser parser, - String endTag, String[] name) - throws XmlPullParserException, java.io.IOException { - - int num; - try { - num = Integer.parseInt(parser.getAttributeValue(null, "num")); - } catch (NullPointerException e) { - throw new XmlPullParserException( - "Need num attribute in byte-array"); - } catch (NumberFormatException e) { - throw new XmlPullParserException( - "Not a number in num attribute in byte-array"); - } - - int[] array = new int[num]; - int i = 0; - - int eventType = parser.getEventType(); - do { - if (eventType == parser.START_TAG) { - if (parser.getName().equals("item")) { - try { - array[i] = Integer.parseInt( - parser.getAttributeValue(null, "value")); - } catch (NullPointerException e) { - throw new XmlPullParserException( - "Need value attribute in item"); - } catch (NumberFormatException e) { - throw new XmlPullParserException( - "Not a number in value attribute in item"); - } - } else { - throw new XmlPullParserException( - "Expected item tag at: " + parser.getName()); - } - } else if (eventType == parser.END_TAG) { - if (parser.getName().equals(endTag)) { - return array; - } else if (parser.getName().equals("item")) { - i++; - } else { - throw new XmlPullParserException( - "Expected " + endTag + " end tag at: " - + parser.getName()); - } - } - eventType = parser.next(); - } while (eventType != parser.END_DOCUMENT); - - throw new XmlPullParserException( - "Document ended before " + endTag + " end tag"); - } - - /** - * Read a flattened object from an XmlPullParser. The XML data could - * previously have been written with writeMapXml(), writeListXml(), or - * writeValueXml(). The XmlPullParser must be positioned <em>at</em> the - * tag that defines the value. - * - * @param parser The XmlPullParser from which to read the object. - * @param name An array of one string, used to return the name attribute - * of the value's tag. - * - * @return Object The newly generated value object. - * - * @see #readMapXml - * @see #readListXml - * @see #writeValueXml - */ - public static final Object readValueXml(XmlPullParser parser, String[] name) - throws XmlPullParserException, java.io.IOException - { - int eventType = parser.getEventType(); - do { - if (eventType == parser.START_TAG) { - return readThisValueXml(parser, name); - } else if (eventType == parser.END_TAG) { - throw new XmlPullParserException( - "Unexpected end tag at: " + parser.getName()); - } else if (eventType == parser.TEXT) { - throw new XmlPullParserException( - "Unexpected text: " + parser.getText()); - } - eventType = parser.next(); - } while (eventType != parser.END_DOCUMENT); - - throw new XmlPullParserException( - "Unexpected end of document"); - } - - private static final Object readThisValueXml(XmlPullParser parser, String[] name) - throws XmlPullParserException, java.io.IOException - { - final String valueName = parser.getAttributeValue(null, "name"); - final String tagName = parser.getName(); - - //System.out.println("Reading this value tag: " + tagName + ", name=" + valueName); - - Object res; - - if (tagName.equals("null")) { - res = null; - } else if (tagName.equals("string")) { - String value = ""; - int eventType; - while ((eventType = parser.next()) != parser.END_DOCUMENT) { - if (eventType == parser.END_TAG) { - if (parser.getName().equals("string")) { - name[0] = valueName; - //System.out.println("Returning value for " + valueName + ": " + value); - return value; - } - throw new XmlPullParserException( - "Unexpected end tag in <string>: " + parser.getName()); - } else if (eventType == parser.TEXT) { - value += parser.getText(); - } else if (eventType == parser.START_TAG) { - throw new XmlPullParserException( - "Unexpected start tag in <string>: " + parser.getName()); - } - } - throw new XmlPullParserException( - "Unexpected end of document in <string>"); - } else if (tagName.equals("int")) { - res = Integer.parseInt(parser.getAttributeValue(null, "value")); - } else if (tagName.equals("long")) { - res = Long.valueOf(parser.getAttributeValue(null, "value")); - } else if (tagName.equals("float")) { - res = new Float(parser.getAttributeValue(null, "value")); - } else if (tagName.equals("double")) { - res = new Double(parser.getAttributeValue(null, "value")); - } else if (tagName.equals("boolean")) { - res = Boolean.valueOf(parser.getAttributeValue(null, "value")); - } else if (tagName.equals("int-array")) { - parser.next(); - res = readThisIntArrayXml(parser, "int-array", name); - name[0] = valueName; - //System.out.println("Returning value for " + valueName + ": " + res); - return res; - } else if (tagName.equals("map")) { - parser.next(); - res = readThisMapXml(parser, "map", name); - name[0] = valueName; - //System.out.println("Returning value for " + valueName + ": " + res); - return res; - } else if (tagName.equals("list")) { - parser.next(); - res = readThisListXml(parser, "list", name); - name[0] = valueName; - //System.out.println("Returning value for " + valueName + ": " + res); - return res; - } else { - throw new XmlPullParserException( - "Unknown tag: " + tagName); - } - - // Skip through to end tag. - int eventType; - while ((eventType = parser.next()) != parser.END_DOCUMENT) { - if (eventType == parser.END_TAG) { - if (parser.getName().equals(tagName)) { - name[0] = valueName; - //System.out.println("Returning value for " + valueName + ": " + res); - return res; - } - throw new XmlPullParserException( - "Unexpected end tag in <" + tagName + ">: " + parser.getName()); - } else if (eventType == parser.TEXT) { - throw new XmlPullParserException( - "Unexpected text in <" + tagName + ">: " + parser.getName()); - } else if (eventType == parser.START_TAG) { - throw new XmlPullParserException( - "Unexpected start tag in <" + tagName + ">: " + parser.getName()); - } - } - throw new XmlPullParserException( - "Unexpected end of document in <" + tagName + ">"); - } - - public static final void beginDocument(XmlPullParser parser, String firstElementName) throws XmlPullParserException, IOException - { - int type; - while ((type=parser.next()) != parser.START_TAG - && type != parser.END_DOCUMENT) { - ; - } - - if (type != parser.START_TAG) { - throw new XmlPullParserException("No start tag found"); - } - - if (!parser.getName().equals(firstElementName)) { - throw new XmlPullParserException("Unexpected start tag: found " + parser.getName() + - ", expected " + firstElementName); - } - } - - public static final void nextElement(XmlPullParser parser) throws XmlPullParserException, IOException - { - int type; - while ((type=parser.next()) != parser.START_TAG - && type != parser.END_DOCUMENT) { - ; - } - } -} diff --git a/core/java/com/android/internal/view/menu/IconMenuItemView.java b/core/java/com/android/internal/view/menu/IconMenuItemView.java index 66e15c1..3c5b422 100644 --- a/core/java/com/android/internal/view/menu/IconMenuItemView.java +++ b/core/java/com/android/internal/view/menu/IconMenuItemView.java @@ -254,7 +254,7 @@ public final class IconMenuItemView extends TextView implements MenuView.ItemVie if (lp == null) { // Default layout parameters lp = new IconMenuView.LayoutParams( - IconMenuView.LayoutParams.FILL_PARENT, IconMenuView.LayoutParams.FILL_PARENT); + IconMenuView.LayoutParams.MATCH_PARENT, IconMenuView.LayoutParams.MATCH_PARENT); } // Set the desired width of item diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java index 58056bd..3c5a753 100644 --- a/core/java/com/android/internal/widget/LockPatternUtils.java +++ b/core/java/com/android/internal/widget/LockPatternUtils.java @@ -38,8 +38,9 @@ import java.util.List; public class LockPatternUtils { private static final String TAG = "LockPatternUtils"; - + private static final String LOCK_PATTERN_FILE = "/system/gesture.key"; + private static final String LOCK_PASSWORD_FILE = "/system/password.key"; /** * The maximum number of incorrect attempts before the user is prevented @@ -70,20 +71,32 @@ public class LockPatternUtils { public static final int MIN_LOCK_PATTERN_SIZE = 4; /** + * Type of password being stored. + * pattern = pattern screen + * pin = digit-only password + * password = alphanumeric password + */ + public static final int MODE_PATTERN = 0; + public static final int MODE_PIN = 1; + public static final int MODE_PASSWORD = 2; + + /** * The minimum number of dots the user must include in a wrong pattern * attempt for it to be counted against the counts that affect * {@link #FAILED_ATTEMPTS_BEFORE_TIMEOUT} and {@link #FAILED_ATTEMPTS_BEFORE_RESET} */ - public static final int MIN_PATTERN_REGISTER_FAIL = 3; + public static final int MIN_PATTERN_REGISTER_FAIL = 3; private final static String LOCKOUT_PERMANENT_KEY = "lockscreen.lockedoutpermanently"; private final static String LOCKOUT_ATTEMPT_DEADLINE = "lockscreen.lockoutattemptdeadline"; - private final static String PATTERN_EVER_CHOSEN = "lockscreen.patterneverchosen"; + private final static String PATTERN_EVER_CHOSEN_KEY = "lockscreen.patterneverchosen"; + public final static String PASSWORD_TYPE_KEY = "lockscreen.password_type"; private final ContentResolver mContentResolver; private static String sLockPatternFilename; - + private static String sLockPasswordFilename; + /** * @param contentResolver Used to look up and save settings. */ @@ -91,16 +104,19 @@ public class LockPatternUtils { mContentResolver = contentResolver; // Initialize the location of gesture lock file if (sLockPatternFilename == null) { - sLockPatternFilename = android.os.Environment.getDataDirectory() + sLockPatternFilename = android.os.Environment.getDataDirectory() .getAbsolutePath() + LOCK_PATTERN_FILE; + sLockPasswordFilename = android.os.Environment.getDataDirectory() + .getAbsolutePath() + LOCK_PASSWORD_FILE; } + } /** * Check to see if a pattern matches the saved pattern. If no pattern exists, * always returns true. * @param pattern The pattern to check. - * @return Whether the pattern matchees the stored one. + * @return Whether the pattern matches the stored one. */ public boolean checkPattern(List<LockPatternView.Cell> pattern) { try { @@ -122,13 +138,40 @@ public class LockPatternUtils { } /** - * Check to see if the user has stored a lock pattern. - * @return Whether a saved pattern exists. + * Check to see if a password matches the saved password. If no password exists, + * always returns true. + * @param password The password to check. + * @return Whether the password matches the stored one. */ - public boolean savedPatternExists() { + public boolean checkPassword(String password) { + try { + // Read all the bytes from the file + RandomAccessFile raf = new RandomAccessFile(sLockPasswordFilename, "r"); + final byte[] stored = new byte[(int) raf.length()]; + int got = raf.read(stored, 0, stored.length); + raf.close(); + if (got <= 0) { + return true; + } + // Compare the hash from the file with the entered password's hash + return Arrays.equals(stored, LockPatternUtils.passwordToHash(password)); + } catch (FileNotFoundException fnfe) { + return true; + } catch (IOException ioe) { + return true; + } + } + + /** + * Checks to see if the given file exists and contains any data. Returns true if it does, + * false otherwise. + * @param filename + * @return true if file exists and is non-empty. + */ + private boolean nonEmptyFileExists(String filename) { try { // Check if we can read a byte from the file - RandomAccessFile raf = new RandomAccessFile(sLockPatternFilename, "r"); + RandomAccessFile raf = new RandomAccessFile(filename, "r"); byte first = raf.readByte(); raf.close(); return true; @@ -140,13 +183,29 @@ public class LockPatternUtils { } /** + * Check to see if the user has stored a lock pattern. + * @return Whether a saved pattern exists. + */ + public boolean savedPatternExists() { + return nonEmptyFileExists(sLockPatternFilename); + } + + /** + * Check to see if the user has stored a lock pattern. + * @return Whether a saved pattern exists. + */ + public boolean savedPasswordExists() { + return nonEmptyFileExists(sLockPasswordFilename); + } + + /** * Return true if the user has ever chosen a pattern. This is true even if the pattern is * currently cleared. * * @return True if the user has ever chosen a pattern. */ public boolean isPatternEverChosen() { - return getBoolean(PATTERN_EVER_CHOSEN); + return getBoolean(PATTERN_EVER_CHOSEN_KEY); } /** @@ -166,7 +225,8 @@ public class LockPatternUtils { raf.write(hash, 0, hash.length); } raf.close(); - setBoolean(PATTERN_EVER_CHOSEN, true); + setBoolean(PATTERN_EVER_CHOSEN_KEY, true); + setLong(PASSWORD_TYPE_KEY, MODE_PATTERN); } catch (FileNotFoundException fnfe) { // Cant do much, unless we want to fail over to using the settings provider Log.e(TAG, "Unable to save lock pattern to " + sLockPatternFilename); @@ -177,6 +237,38 @@ public class LockPatternUtils { } /** + * Save a lock password. + * @param password The password to save + */ + public void saveLockPassword(String password) { + // Compute the hash + boolean numericHint = password != null ? TextUtils.isDigitsOnly(password) : false; + final byte[] hash = LockPatternUtils.passwordToHash(password); + try { + // Write the hash to file + RandomAccessFile raf = new RandomAccessFile(sLockPasswordFilename, "rw"); + // Truncate the file if pattern is null, to clear the lock + if (password == null) { + raf.setLength(0); + } else { + raf.write(hash, 0, hash.length); + } + raf.close(); + setLong(PASSWORD_TYPE_KEY, numericHint ? MODE_PIN : MODE_PASSWORD); + } catch (FileNotFoundException fnfe) { + // Cant do much, unless we want to fail over to using the settings provider + Log.e(TAG, "Unable to save lock pattern to " + sLockPasswordFilename); + } catch (IOException ioe) { + // Cant do much + Log.e(TAG, "Unable to save lock pattern to " + sLockPasswordFilename); + } + } + + public int getPasswordMode() { + return (int) getLong(PASSWORD_TYPE_KEY, MODE_PATTERN); + } + + /** * Deserialize a pattern. * @param string The pattern serialized with {@link #patternToString} * @return The pattern. @@ -210,7 +302,7 @@ public class LockPatternUtils { } return new String(res); } - + /* * Generate an SHA-1 hash for the pattern. Not the most secure, but it is * at least a second level of protection. First level is that the file @@ -218,11 +310,11 @@ public class LockPatternUtils { * @param pattern the gesture pattern. * @return the hash of the pattern in a byte array. */ - static byte[] patternToHash(List<LockPatternView.Cell> pattern) { + private static byte[] patternToHash(List<LockPatternView.Cell> pattern) { if (pattern == null) { return null; } - + final int patternSize = pattern.size(); byte[] res = new byte[patternSize]; for (int i = 0; i < patternSize; i++) { @@ -238,11 +330,55 @@ public class LockPatternUtils { } } + /* + * Generate a hash for the given password. To avoid brute force attacks, we use a salted hash. + * Not the most secure, but it is at least a second level of protection. First level is that + * the file is in a location only readable by the system process. + * @param password the gesture pattern. + * @return the hash of the pattern in a byte array. + */ + public static byte[] passwordToHash(String password) { + if (password == null) { + return null; + } + String algo = null; + byte[] hashed = null; + try { + long salt = 0x2374868151054924L; // TODO: make this unique to device + byte[] saltedPassword = (password + Long.toString(salt)).getBytes(); + byte[] sha1 = MessageDigest.getInstance(algo = "SHA-1").digest(saltedPassword); + byte[] md5 = MessageDigest.getInstance(algo = "MD5").digest(saltedPassword); + hashed = (toHex(sha1) + toHex(md5)).getBytes(); + } catch (NoSuchAlgorithmException e) { + Log.w(TAG, "Failed to encode string because of missing algorithm: " + algo); + } + return hashed; + } + + private static String toHex(byte[] ary) { + final String hex = "0123456789ABCDEF"; + String ret = ""; + for (int i = 0; i < ary.length; i++) { + ret += hex.charAt((ary[i] >> 4) & 0xf); + ret += hex.charAt(ary[i] & 0xf); + } + return ret; + } + + /** + * @return Whether the lock password is enabled. + */ + public boolean isLockPasswordEnabled() { + long mode = getLong(PASSWORD_TYPE_KEY, 0); + return savedPasswordExists() && (mode == MODE_PASSWORD || mode == MODE_PIN); + } + /** * @return Whether the lock pattern is enabled. */ public boolean isLockPatternEnabled() { - return getBoolean(Settings.System.LOCK_PATTERN_ENABLED); + return getBoolean(Settings.System.LOCK_PATTERN_ENABLED) + && getLong(PASSWORD_TYPE_KEY, MODE_PATTERN) == MODE_PATTERN; } /** @@ -361,5 +497,10 @@ public class LockPatternUtils { android.provider.Settings.System.putLong(mContentResolver, systemSettingKey, value); } - + public boolean isSecure() { + long mode = getPasswordMode(); + boolean secure = mode == MODE_PATTERN && isLockPatternEnabled() && savedPatternExists() + || (mode == MODE_PIN || mode == MODE_PASSWORD) && savedPasswordExists(); + return secure; + } } diff --git a/core/java/com/android/internal/widget/NumberPicker.java b/core/java/com/android/internal/widget/NumberPicker.java deleted file mode 100644 index ae08eca..0000000 --- a/core/java/com/android/internal/widget/NumberPicker.java +++ /dev/null @@ -1,411 +0,0 @@ -/* - * 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.internal.widget; - -import android.content.Context; -import android.os.Handler; -import android.text.InputFilter; -import android.text.InputType; -import android.text.Spanned; -import android.text.method.NumberKeyListener; -import android.util.AttributeSet; -import android.view.LayoutInflater; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.View.OnFocusChangeListener; -import android.view.View.OnLongClickListener; -import android.widget.TextView; -import android.widget.LinearLayout; -import android.widget.EditText; - -import com.android.internal.R; - -public class NumberPicker extends LinearLayout implements OnClickListener, - OnFocusChangeListener, OnLongClickListener { - - public interface OnChangedListener { - void onChanged(NumberPicker picker, int oldVal, int newVal); - } - - public interface Formatter { - String toString(int value); - } - - /* - * Use a custom NumberPicker formatting callback to use two-digit - * minutes strings like "01". Keeping a static formatter etc. is the - * most efficient way to do this; it avoids creating temporary objects - * on every call to format(). - */ - public static final NumberPicker.Formatter TWO_DIGIT_FORMATTER = - new NumberPicker.Formatter() { - final StringBuilder mBuilder = new StringBuilder(); - final java.util.Formatter mFmt = new java.util.Formatter(mBuilder); - final Object[] mArgs = new Object[1]; - public String toString(int value) { - mArgs[0] = value; - mBuilder.delete(0, mBuilder.length()); - mFmt.format("%02d", mArgs); - return mFmt.toString(); - } - }; - - private final Handler mHandler; - private final Runnable mRunnable = new Runnable() { - public void run() { - if (mIncrement) { - changeCurrent(mCurrent + 1); - mHandler.postDelayed(this, mSpeed); - } else if (mDecrement) { - changeCurrent(mCurrent - 1); - mHandler.postDelayed(this, mSpeed); - } - } - }; - - private final EditText mText; - private final InputFilter mNumberInputFilter; - - private String[] mDisplayedValues; - protected int mStart; - protected int mEnd; - protected int mCurrent; - protected int mPrevious; - private OnChangedListener mListener; - private Formatter mFormatter; - private long mSpeed = 300; - - private boolean mIncrement; - private boolean mDecrement; - - public NumberPicker(Context context) { - this(context, null); - } - - public NumberPicker(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - @SuppressWarnings({"UnusedDeclaration"}) - public NumberPicker(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs); - setOrientation(VERTICAL); - LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - inflater.inflate(R.layout.number_picker, this, true); - mHandler = new Handler(); - InputFilter inputFilter = new NumberPickerInputFilter(); - mNumberInputFilter = new NumberRangeKeyListener(); - mIncrementButton = (NumberPickerButton) findViewById(R.id.increment); - mIncrementButton.setOnClickListener(this); - mIncrementButton.setOnLongClickListener(this); - mIncrementButton.setNumberPicker(this); - mDecrementButton = (NumberPickerButton) findViewById(R.id.decrement); - mDecrementButton.setOnClickListener(this); - mDecrementButton.setOnLongClickListener(this); - mDecrementButton.setNumberPicker(this); - - mText = (EditText) findViewById(R.id.timepicker_input); - mText.setOnFocusChangeListener(this); - mText.setFilters(new InputFilter[] {inputFilter}); - mText.setRawInputType(InputType.TYPE_CLASS_NUMBER); - - if (!isEnabled()) { - setEnabled(false); - } - } - - @Override - public void setEnabled(boolean enabled) { - super.setEnabled(enabled); - mIncrementButton.setEnabled(enabled); - mDecrementButton.setEnabled(enabled); - mText.setEnabled(enabled); - } - - public void setOnChangeListener(OnChangedListener listener) { - mListener = listener; - } - - public void setFormatter(Formatter formatter) { - mFormatter = formatter; - } - - /** - * Set the range of numbers allowed for the number picker. The current - * value will be automatically set to the start. - * - * @param start the start of the range (inclusive) - * @param end the end of the range (inclusive) - */ - public void setRange(int start, int end) { - mStart = start; - mEnd = end; - mCurrent = start; - updateView(); - } - - /** - * Set the range of numbers allowed for the number picker. The current - * value will be automatically set to the start. Also provide a mapping - * for values used to display to the user. - * - * @param start the start of the range (inclusive) - * @param end the end of the range (inclusive) - * @param displayedValues the values displayed to the user. - */ - public void setRange(int start, int end, String[] displayedValues) { - mDisplayedValues = displayedValues; - mStart = start; - mEnd = end; - mCurrent = start; - updateView(); - } - - public void setCurrent(int current) { - mCurrent = current; - updateView(); - } - - /** - * The speed (in milliseconds) at which the numbers will scroll - * when the the +/- buttons are longpressed. Default is 300ms. - */ - public void setSpeed(long speed) { - mSpeed = speed; - } - - public void onClick(View v) { - validateInput(mText); - if (!mText.hasFocus()) mText.requestFocus(); - - // now perform the increment/decrement - if (R.id.increment == v.getId()) { - changeCurrent(mCurrent + 1); - } else if (R.id.decrement == v.getId()) { - changeCurrent(mCurrent - 1); - } - } - - private String formatNumber(int value) { - return (mFormatter != null) - ? mFormatter.toString(value) - : String.valueOf(value); - } - - protected void changeCurrent(int current) { - - // Wrap around the values if we go past the start or end - if (current > mEnd) { - current = mStart; - } else if (current < mStart) { - current = mEnd; - } - mPrevious = mCurrent; - mCurrent = current; - notifyChange(); - updateView(); - } - - protected void notifyChange() { - if (mListener != null) { - mListener.onChanged(this, mPrevious, mCurrent); - } - } - - protected void updateView() { - - /* If we don't have displayed values then use the - * current number else find the correct value in the - * displayed values for the current number. - */ - if (mDisplayedValues == null) { - mText.setText(formatNumber(mCurrent)); - } else { - mText.setText(mDisplayedValues[mCurrent - mStart]); - } - mText.setSelection(mText.getText().length()); - } - - private void validateCurrentView(CharSequence str) { - int val = getSelectedPos(str.toString()); - if ((val >= mStart) && (val <= mEnd)) { - if (mCurrent != val) { - mPrevious = mCurrent; - mCurrent = val; - notifyChange(); - } - } - updateView(); - } - - public void onFocusChange(View v, boolean hasFocus) { - - /* When focus is lost check that the text field - * has valid values. - */ - if (!hasFocus) { - validateInput(v); - } - } - - private void validateInput(View v) { - String str = String.valueOf(((TextView) v).getText()); - if ("".equals(str)) { - - // Restore to the old value as we don't allow empty values - updateView(); - } else { - - // Check the new value and ensure it's in range - validateCurrentView(str); - } - } - - /** - * We start the long click here but rely on the {@link NumberPickerButton} - * to inform us when the long click has ended. - */ - public boolean onLongClick(View v) { - - /* The text view may still have focus so clear it's focus which will - * trigger the on focus changed and any typed values to be pulled. - */ - mText.clearFocus(); - - if (R.id.increment == v.getId()) { - mIncrement = true; - mHandler.post(mRunnable); - } else if (R.id.decrement == v.getId()) { - mDecrement = true; - mHandler.post(mRunnable); - } - return true; - } - - public void cancelIncrement() { - mIncrement = false; - } - - public void cancelDecrement() { - mDecrement = false; - } - - private static final char[] DIGIT_CHARACTERS = new char[] { - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' - }; - - private NumberPickerButton mIncrementButton; - private NumberPickerButton mDecrementButton; - - private class NumberPickerInputFilter implements InputFilter { - public CharSequence filter(CharSequence source, int start, int end, - Spanned dest, int dstart, int dend) { - if (mDisplayedValues == null) { - return mNumberInputFilter.filter(source, start, end, dest, dstart, dend); - } - CharSequence filtered = String.valueOf(source.subSequence(start, end)); - String result = String.valueOf(dest.subSequence(0, dstart)) - + filtered - + dest.subSequence(dend, dest.length()); - String str = String.valueOf(result).toLowerCase(); - for (String val : mDisplayedValues) { - val = val.toLowerCase(); - if (val.startsWith(str)) { - return filtered; - } - } - return ""; - } - } - - private class NumberRangeKeyListener extends NumberKeyListener { - - // XXX This doesn't allow for range limits when controlled by a - // soft input method! - public int getInputType() { - return InputType.TYPE_CLASS_NUMBER; - } - - @Override - protected char[] getAcceptedChars() { - return DIGIT_CHARACTERS; - } - - @Override - public CharSequence filter(CharSequence source, int start, int end, - Spanned dest, int dstart, int dend) { - - CharSequence filtered = super.filter(source, start, end, dest, dstart, dend); - if (filtered == null) { - filtered = source.subSequence(start, end); - } - - String result = String.valueOf(dest.subSequence(0, dstart)) - + filtered - + dest.subSequence(dend, dest.length()); - - if ("".equals(result)) { - return result; - } - int val = getSelectedPos(result); - - /* Ensure the user can't type in a value greater - * than the max allowed. We have to allow less than min - * as the user might want to delete some numbers - * and then type a new number. - */ - if (val > mEnd) { - return ""; - } else { - return filtered; - } - } - } - - private int getSelectedPos(String str) { - if (mDisplayedValues == null) { - return Integer.parseInt(str); - } else { - for (int i = 0; i < mDisplayedValues.length; i++) { - - /* Don't force the user to type in jan when ja will do */ - str = str.toLowerCase(); - if (mDisplayedValues[i].toLowerCase().startsWith(str)) { - return mStart + i; - } - } - - /* The user might have typed in a number into the month field i.e. - * 10 instead of OCT so support that too. - */ - try { - return Integer.parseInt(str); - } catch (NumberFormatException e) { - - /* Ignore as if it's not a number we don't care */ - } - } - return mStart; - } - - /** - * @return the current value. - */ - public int getCurrent() { - return mCurrent; - } -}
\ No newline at end of file diff --git a/core/java/com/android/internal/widget/NumberPickerButton.java b/core/java/com/android/internal/widget/NumberPickerButton.java deleted file mode 100644 index 39f1e2c..0000000 --- a/core/java/com/android/internal/widget/NumberPickerButton.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * 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.internal.widget; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.KeyEvent; -import android.view.MotionEvent; -import android.widget.ImageButton; - -import com.android.internal.R; - -/** - * This class exists purely to cancel long click events. - */ -public class NumberPickerButton extends ImageButton { - - private NumberPicker mNumberPicker; - - public NumberPickerButton(Context context, AttributeSet attrs, - int defStyle) { - super(context, attrs, defStyle); - } - - public NumberPickerButton(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public NumberPickerButton(Context context) { - super(context); - } - - public void setNumberPicker(NumberPicker picker) { - mNumberPicker = picker; - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - cancelLongpressIfRequired(event); - return super.onTouchEvent(event); - } - - @Override - public boolean onTrackballEvent(MotionEvent event) { - cancelLongpressIfRequired(event); - return super.onTrackballEvent(event); - } - - @Override - public boolean onKeyUp(int keyCode, KeyEvent event) { - if ((keyCode == KeyEvent.KEYCODE_DPAD_CENTER) - || (keyCode == KeyEvent.KEYCODE_ENTER)) { - cancelLongpress(); - } - return super.onKeyUp(keyCode, event); - } - - private void cancelLongpressIfRequired(MotionEvent event) { - if ((event.getAction() == MotionEvent.ACTION_CANCEL) - || (event.getAction() == MotionEvent.ACTION_UP)) { - cancelLongpress(); - } - } - - private void cancelLongpress() { - if (R.id.increment == getId()) { - mNumberPicker.cancelIncrement(); - } else if (R.id.decrement == getId()) { - mNumberPicker.cancelDecrement(); - } - } -} diff --git a/core/java/com/android/internal/widget/SlidingTab.java b/core/java/com/android/internal/widget/SlidingTab.java index f07b2f1..adafbb4 100644 --- a/core/java/com/android/internal/widget/SlidingTab.java +++ b/core/java/com/android/internal/widget/SlidingTab.java @@ -23,8 +23,6 @@ import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.drawable.Drawable; -import android.os.Handler; -import android.os.Message; import android.os.Vibrator; import android.util.AttributeSet; import android.util.Log; @@ -34,7 +32,6 @@ import android.view.View; import android.view.ViewGroup; import android.view.animation.AlphaAnimation; import android.view.animation.Animation; -import android.view.animation.AnimationSet; import android.view.animation.LinearInterpolator; import android.view.animation.TranslateAnimation; import android.view.animation.Animation.AnimationListener; @@ -214,7 +211,7 @@ public class SlidingTab extends ViewGroup { // Create hint TextView text = new TextView(parent.getContext()); text.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, - LayoutParams.FILL_PARENT)); + LayoutParams.MATCH_PARENT)); text.setBackgroundResource(barId); text.setTextAppearance(parent.getContext(), R.style.TextAppearance_SlidingTabNormal); // hint.setSingleLine(); // Hmm.. this causes the text to disappear off-screen diff --git a/core/java/com/android/internal/widget/VerticalTextSpinner.java b/core/java/com/android/internal/widget/VerticalTextSpinner.java deleted file mode 100644 index 50c528c..0000000 --- a/core/java/com/android/internal/widget/VerticalTextSpinner.java +++ /dev/null @@ -1,467 +0,0 @@ -/* - * 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.internal.widget; - -import android.content.Context; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.Rect; -import android.graphics.drawable.Drawable; -import android.text.TextPaint; -import android.util.AttributeSet; -import android.view.KeyEvent; -import android.view.MotionEvent; -import android.view.View; - - -public class VerticalTextSpinner extends View { - - private static final int SELECTOR_ARROW_HEIGHT = 15; - - private static final int TEXT_SPACING = 18; - private static final int TEXT_MARGIN_RIGHT = 25; - private static final int TEXT_SIZE = 22; - - /* Keep the calculations as this is really a for loop from - * -2 to 2 but precalculated so we don't have to do in the onDraw. - */ - private static final int TEXT1_Y = (TEXT_SIZE * (-2 + 2)) + (TEXT_SPACING * (-2 + 1)); - private static final int TEXT2_Y = (TEXT_SIZE * (-1 + 2)) + (TEXT_SPACING * (-1 + 1)); - private static final int TEXT3_Y = (TEXT_SIZE * (0 + 2)) + (TEXT_SPACING * (0 + 1)); - private static final int TEXT4_Y = (TEXT_SIZE * (1 + 2)) + (TEXT_SPACING * (1 + 1)); - private static final int TEXT5_Y = (TEXT_SIZE * (2 + 2)) + (TEXT_SPACING * (2 + 1)); - - private static final int SCROLL_MODE_NONE = 0; - private static final int SCROLL_MODE_UP = 1; - private static final int SCROLL_MODE_DOWN = 2; - - private static final long DEFAULT_SCROLL_INTERVAL_MS = 400; - private static final int SCROLL_DISTANCE = TEXT_SIZE + TEXT_SPACING; - private static final int MIN_ANIMATIONS = 4; - - private final Drawable mBackgroundFocused; - private final Drawable mSelectorFocused; - private final Drawable mSelectorNormal; - private final int mSelectorDefaultY; - private final int mSelectorMinY; - private final int mSelectorMaxY; - private final int mSelectorHeight; - private final TextPaint mTextPaintDark; - private final TextPaint mTextPaintLight; - - private int mSelectorY; - private Drawable mSelector; - private int mDownY; - private boolean isDraggingSelector; - private int mScrollMode; - private long mScrollInterval; - private boolean mIsAnimationRunning; - private boolean mStopAnimation; - private boolean mWrapAround = true; - - private int mTotalAnimatedDistance; - private int mNumberOfAnimations; - private long mDelayBetweenAnimations; - private int mDistanceOfEachAnimation; - - private String[] mTextList; - private int mCurrentSelectedPos; - private OnChangedListener mListener; - - private String mText1; - private String mText2; - private String mText3; - private String mText4; - private String mText5; - - public interface OnChangedListener { - void onChanged( - VerticalTextSpinner spinner, int oldPos, int newPos, String[] items); - } - - public VerticalTextSpinner(Context context) { - this(context, null); - } - - public VerticalTextSpinner(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public VerticalTextSpinner(Context context, AttributeSet attrs, - int defStyle) { - super(context, attrs, defStyle); - - mBackgroundFocused = context.getResources().getDrawable(com.android.internal.R.drawable.pickerbox_background); - mSelectorFocused = context.getResources().getDrawable(com.android.internal.R.drawable.pickerbox_selected); - mSelectorNormal = context.getResources().getDrawable(com.android.internal.R.drawable.pickerbox_unselected); - - mSelectorHeight = mSelectorFocused.getIntrinsicHeight(); - mSelectorDefaultY = (mBackgroundFocused.getIntrinsicHeight() - mSelectorHeight) / 2; - mSelectorMinY = 0; - mSelectorMaxY = mBackgroundFocused.getIntrinsicHeight() - mSelectorHeight; - - mSelector = mSelectorNormal; - mSelectorY = mSelectorDefaultY; - - mTextPaintDark = new TextPaint(Paint.ANTI_ALIAS_FLAG); - mTextPaintDark.setTextSize(TEXT_SIZE); - mTextPaintDark.setColor(context.getResources().getColor(com.android.internal.R.color.primary_text_light)); - - mTextPaintLight = new TextPaint(Paint.ANTI_ALIAS_FLAG); - mTextPaintLight.setTextSize(TEXT_SIZE); - mTextPaintLight.setColor(context.getResources().getColor(com.android.internal.R.color.secondary_text_dark)); - - mScrollMode = SCROLL_MODE_NONE; - mScrollInterval = DEFAULT_SCROLL_INTERVAL_MS; - calculateAnimationValues(); - } - - public void setOnChangeListener(OnChangedListener listener) { - mListener = listener; - } - - public void setItems(String[] textList) { - mTextList = textList; - calculateTextPositions(); - } - - public void setSelectedPos(int selectedPos) { - mCurrentSelectedPos = selectedPos; - calculateTextPositions(); - postInvalidate(); - } - - public void setScrollInterval(long interval) { - mScrollInterval = interval; - calculateAnimationValues(); - } - - public void setWrapAround(boolean wrap) { - mWrapAround = wrap; - } - - @Override - public boolean onKeyDown(int keyCode, KeyEvent event) { - - /* This is a bit confusing, when we get the key event - * DPAD_DOWN we actually roll the spinner up. When the - * key event is DPAD_UP we roll the spinner down. - */ - if ((keyCode == KeyEvent.KEYCODE_DPAD_UP) && canScrollDown()) { - mScrollMode = SCROLL_MODE_DOWN; - scroll(); - mStopAnimation = true; - return true; - } else if ((keyCode == KeyEvent.KEYCODE_DPAD_DOWN) && canScrollUp()) { - mScrollMode = SCROLL_MODE_UP; - scroll(); - mStopAnimation = true; - return true; - } - return super.onKeyDown(keyCode, event); - } - - private boolean canScrollDown() { - return (mCurrentSelectedPos > 0) || mWrapAround; - } - - private boolean canScrollUp() { - return ((mCurrentSelectedPos < (mTextList.length - 1)) || mWrapAround); - } - - @Override - protected void onFocusChanged(boolean gainFocus, int direction, - Rect previouslyFocusedRect) { - if (gainFocus) { - setBackgroundDrawable(mBackgroundFocused); - mSelector = mSelectorFocused; - } else { - setBackgroundDrawable(null); - mSelector = mSelectorNormal; - mSelectorY = mSelectorDefaultY; - } - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - final int action = event.getAction(); - final int y = (int) event.getY(); - - switch (action) { - case MotionEvent.ACTION_DOWN: - requestFocus(); - mDownY = y; - isDraggingSelector = (y >= mSelectorY) && (y <= (mSelectorY + mSelector.getIntrinsicHeight())); - break; - - case MotionEvent.ACTION_MOVE: - if (isDraggingSelector) { - int top = mSelectorDefaultY + (y - mDownY); - if (top <= mSelectorMinY && canScrollDown()) { - mSelectorY = mSelectorMinY; - mStopAnimation = false; - if (mScrollMode != SCROLL_MODE_DOWN) { - mScrollMode = SCROLL_MODE_DOWN; - scroll(); - } - } else if (top >= mSelectorMaxY && canScrollUp()) { - mSelectorY = mSelectorMaxY; - mStopAnimation = false; - if (mScrollMode != SCROLL_MODE_UP) { - mScrollMode = SCROLL_MODE_UP; - scroll(); - } - } else { - mSelectorY = top; - mStopAnimation = true; - } - } - break; - - case MotionEvent.ACTION_UP: - case MotionEvent.ACTION_CANCEL: - default: - mSelectorY = mSelectorDefaultY; - mStopAnimation = true; - invalidate(); - break; - } - return true; - } - - @Override - protected void onDraw(Canvas canvas) { - - /* The bounds of the selector */ - final int selectorLeft = 0; - final int selectorTop = mSelectorY; - final int selectorRight = mMeasuredWidth; - final int selectorBottom = mSelectorY + mSelectorHeight; - - /* Draw the selector */ - mSelector.setBounds(selectorLeft, selectorTop, selectorRight, selectorBottom); - mSelector.draw(canvas); - - if (mTextList == null) { - - /* We're not setup with values so don't draw anything else */ - return; - } - - final TextPaint textPaintDark = mTextPaintDark; - if (hasFocus()) { - - /* The bounds of the top area where the text should be light */ - final int topLeft = 0; - final int topTop = 0; - final int topRight = selectorRight; - final int topBottom = selectorTop + SELECTOR_ARROW_HEIGHT; - - /* Assign a bunch of local finals for performance */ - final String text1 = mText1; - final String text2 = mText2; - final String text3 = mText3; - final String text4 = mText4; - final String text5 = mText5; - final TextPaint textPaintLight = mTextPaintLight; - - /* - * Draw the 1st, 2nd and 3rd item in light only, clip it so it only - * draws in the area above the selector - */ - canvas.save(); - canvas.clipRect(topLeft, topTop, topRight, topBottom); - drawText(canvas, text1, TEXT1_Y - + mTotalAnimatedDistance, textPaintLight); - drawText(canvas, text2, TEXT2_Y - + mTotalAnimatedDistance, textPaintLight); - drawText(canvas, text3, - TEXT3_Y + mTotalAnimatedDistance, textPaintLight); - canvas.restore(); - - /* - * Draw the 2nd, 3rd and 4th clipped to the selector bounds in dark - * paint - */ - canvas.save(); - canvas.clipRect(selectorLeft, selectorTop + SELECTOR_ARROW_HEIGHT, - selectorRight, selectorBottom - SELECTOR_ARROW_HEIGHT); - drawText(canvas, text2, TEXT2_Y - + mTotalAnimatedDistance, textPaintDark); - drawText(canvas, text3, - TEXT3_Y + mTotalAnimatedDistance, textPaintDark); - drawText(canvas, text4, - TEXT4_Y + mTotalAnimatedDistance, textPaintDark); - canvas.restore(); - - /* The bounds of the bottom area where the text should be light */ - final int bottomLeft = 0; - final int bottomTop = selectorBottom - SELECTOR_ARROW_HEIGHT; - final int bottomRight = selectorRight; - final int bottomBottom = mMeasuredHeight; - - /* - * Draw the 3rd, 4th and 5th in white text, clip it so it only draws - * in the area below the selector. - */ - canvas.save(); - canvas.clipRect(bottomLeft, bottomTop, bottomRight, bottomBottom); - drawText(canvas, text3, - TEXT3_Y + mTotalAnimatedDistance, textPaintLight); - drawText(canvas, text4, - TEXT4_Y + mTotalAnimatedDistance, textPaintLight); - drawText(canvas, text5, - TEXT5_Y + mTotalAnimatedDistance, textPaintLight); - canvas.restore(); - - } else { - drawText(canvas, mText3, TEXT3_Y, textPaintDark); - } - if (mIsAnimationRunning) { - if ((Math.abs(mTotalAnimatedDistance) + mDistanceOfEachAnimation) > SCROLL_DISTANCE) { - mTotalAnimatedDistance = 0; - if (mScrollMode == SCROLL_MODE_UP) { - int oldPos = mCurrentSelectedPos; - int newPos = getNewIndex(1); - if (newPos >= 0) { - mCurrentSelectedPos = newPos; - if (mListener != null) { - mListener.onChanged(this, oldPos, mCurrentSelectedPos, mTextList); - } - } - if (newPos < 0 || ((newPos >= mTextList.length - 1) && !mWrapAround)) { - mStopAnimation = true; - } - calculateTextPositions(); - } else if (mScrollMode == SCROLL_MODE_DOWN) { - int oldPos = mCurrentSelectedPos; - int newPos = getNewIndex(-1); - if (newPos >= 0) { - mCurrentSelectedPos = newPos; - if (mListener != null) { - mListener.onChanged(this, oldPos, mCurrentSelectedPos, mTextList); - } - } - if (newPos < 0 || (newPos == 0 && !mWrapAround)) { - mStopAnimation = true; - } - calculateTextPositions(); - } - if (mStopAnimation) { - final int previousScrollMode = mScrollMode; - - /* No longer scrolling, we wait till the current animation - * completes then we stop. - */ - mIsAnimationRunning = false; - mStopAnimation = false; - mScrollMode = SCROLL_MODE_NONE; - - /* If the current selected item is an empty string - * scroll past it. - */ - if ("".equals(mTextList[mCurrentSelectedPos])) { - mScrollMode = previousScrollMode; - scroll(); - mStopAnimation = true; - } - } - } else { - if (mScrollMode == SCROLL_MODE_UP) { - mTotalAnimatedDistance -= mDistanceOfEachAnimation; - } else if (mScrollMode == SCROLL_MODE_DOWN) { - mTotalAnimatedDistance += mDistanceOfEachAnimation; - } - } - if (mDelayBetweenAnimations > 0) { - postInvalidateDelayed(mDelayBetweenAnimations); - } else { - invalidate(); - } - } - } - - /** - * Called every time the text items or current position - * changes. We calculate store we don't have to calculate - * onDraw. - */ - private void calculateTextPositions() { - mText1 = getTextToDraw(-2); - mText2 = getTextToDraw(-1); - mText3 = getTextToDraw(0); - mText4 = getTextToDraw(1); - mText5 = getTextToDraw(2); - } - - private String getTextToDraw(int offset) { - int index = getNewIndex(offset); - if (index < 0) { - return ""; - } - return mTextList[index]; - } - - private int getNewIndex(int offset) { - int index = mCurrentSelectedPos + offset; - if (index < 0) { - if (mWrapAround) { - index += mTextList.length; - } else { - return -1; - } - } else if (index >= mTextList.length) { - if (mWrapAround) { - index -= mTextList.length; - } else { - return -1; - } - } - return index; - } - - private void scroll() { - if (mIsAnimationRunning) { - return; - } - mTotalAnimatedDistance = 0; - mIsAnimationRunning = true; - invalidate(); - } - - private void calculateAnimationValues() { - mNumberOfAnimations = (int) mScrollInterval / SCROLL_DISTANCE; - if (mNumberOfAnimations < MIN_ANIMATIONS) { - mNumberOfAnimations = MIN_ANIMATIONS; - mDistanceOfEachAnimation = SCROLL_DISTANCE / mNumberOfAnimations; - mDelayBetweenAnimations = 0; - } else { - mDistanceOfEachAnimation = SCROLL_DISTANCE / mNumberOfAnimations; - mDelayBetweenAnimations = mScrollInterval / mNumberOfAnimations; - } - } - - private void drawText(Canvas canvas, String text, int y, TextPaint paint) { - int width = (int) paint.measureText(text); - int x = getMeasuredWidth() - width - TEXT_MARGIN_RIGHT; - canvas.drawText(text, x, y, paint); - } - - public int getCurrentSelectedPos() { - return mCurrentSelectedPos; - } -} diff --git a/core/java/com/google/android/collect/Sets.java b/core/java/com/google/android/collect/Sets.java index f5be0ec..fbfbe50 100644 --- a/core/java/com/google/android/collect/Sets.java +++ b/core/java/com/google/android/collect/Sets.java @@ -44,41 +44,50 @@ public class Sets { return new HashSet<K>(); } - /** - * Creates a {@code HashSet} instance containing the given elements. - * - * <p><b>Note:</b> due to a bug in javac 1.5.0_06, we cannot support the - * following: - * - * <p>{@code Set<Base> set = Sets.newHashSet(sub1, sub2);} - * - * <p>where {@code sub1} and {@code sub2} are references to subtypes of {@code - * Base}, not of {@code Base} itself. To get around this, you must use: - * - * <p>{@code Set<Base> set = Sets.<Base>newHashSet(sub1, sub2);} - * - * @param elements the elements that the set should contain - * @return a newly-created {@code HashSet} containing those elements (minus - * duplicates) - */ - public static <E> HashSet<E> newHashSet(E... elements) { - int capacity = elements.length * 4 / 3 + 1; - HashSet<E> set = new HashSet<E>(capacity); - Collections.addAll(set, elements); - return set; - } + /** + * Creates a {@code HashSet} instance containing the given elements. + * + * <p><b>Note:</b> due to a bug in javac 1.5.0_06, we cannot support the + * following: + * + * <p>{@code Set<Base> set = Sets.newHashSet(sub1, sub2);} + * + * <p>where {@code sub1} and {@code sub2} are references to subtypes of {@code + * Base}, not of {@code Base} itself. To get around this, you must use: + * + * <p>{@code Set<Base> set = Sets.<Base>newHashSet(sub1, sub2);} + * + * @param elements the elements that the set should contain + * @return a newly-created {@code HashSet} containing those elements (minus + * duplicates) + */ + public static <E> HashSet<E> newHashSet(E... elements) { + int capacity = elements.length * 4 / 3 + 1; + HashSet<E> set = new HashSet<E>(capacity); + Collections.addAll(set, elements); + return set; + } - /** - * Creates a {@code SortedSet} instance containing the given elements. - * - * @param elements the elements that the set should contain - * @return a newly-created {@code SortedSet} containing those elements (minus - * duplicates) - */ - public static <E> SortedSet<E> newSortedSet(E... elements) { - SortedSet<E> set = new TreeSet<E>(); - Collections.addAll(set, elements); - return set; - } + /** + * Creates an empty {@code SortedSet} instance. + * + * @return a newly-created, initially-empty {@code SortedSet}. + */ + public static <E> SortedSet<E> newSortedSet() { + return new TreeSet<E>(); + } + + /** + * Creates a {@code SortedSet} instance containing the given elements. + * + * @param elements the elements that the set should contain + * @return a newly-created {@code SortedSet} containing those elements (minus + * duplicates) + */ + public static <E> SortedSet<E> newSortedSet(E... elements) { + SortedSet<E> set = new TreeSet<E>(); + Collections.addAll(set, elements); + return set; + } } diff --git a/core/java/com/google/android/gdata/client/AndroidGDataClient.java b/core/java/com/google/android/gdata/client/AndroidGDataClient.java deleted file mode 100644 index 9a2a51d..0000000 --- a/core/java/com/google/android/gdata/client/AndroidGDataClient.java +++ /dev/null @@ -1,508 +0,0 @@ -// Copyright 2007 The Android Open Source Project - -package com.google.android.gdata.client; - -import com.google.android.net.GoogleHttpClient; -import com.google.wireless.gdata.client.GDataClient; -import com.google.wireless.gdata.client.HttpException; -import com.google.wireless.gdata.client.QueryParams; -import com.google.wireless.gdata.data.StringUtils; -import com.google.wireless.gdata.parser.ParseException; -import com.google.wireless.gdata.serializer.GDataSerializer; - -import org.apache.http.Header; -import org.apache.http.HttpEntity; -import org.apache.http.HttpResponse; -import org.apache.http.StatusLine; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.client.methods.HttpUriRequest; -import org.apache.http.entity.InputStreamEntity; -import org.apache.http.entity.AbstractHttpEntity; -import org.apache.http.entity.ByteArrayEntity; - -import android.content.ContentResolver; -import android.content.Context; -import android.net.http.AndroidHttpClient; -import android.text.TextUtils; -import android.util.Config; -import android.util.Log; -import android.os.SystemProperties; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.UnsupportedEncodingException; -import java.io.BufferedInputStream; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URLEncoder; - -/** - * Implementation of a GDataClient using GoogleHttpClient to make HTTP - * requests. Always issues GETs and POSTs, using the X-HTTP-Method-Override - * header when a PUT or DELETE is desired, to avoid issues with firewalls, etc., - * that do not allow methods other than GET or POST. - */ -public class AndroidGDataClient implements GDataClient { - - private static final String TAG = "GDataClient"; - private static final boolean DEBUG = false; - private static final boolean LOCAL_LOGV = DEBUG ? Config.LOGD : Config.LOGV; - - private static final String X_HTTP_METHOD_OVERRIDE = - "X-HTTP-Method-Override"; - - private static final String DEFAULT_USER_AGENT_APP_VERSION = "Android-GData/1.1"; - - private static final int MAX_REDIRECTS = 10; - - // boolean system property that can be used to control whether or not - // requests/responses are gzip'd. - private static final String NO_GZIP_SYSTEM_PROPERTY = "sync.nogzip"; - - private final GoogleHttpClient mHttpClient; - private ContentResolver mResolver; - - /** - * Interface for creating HTTP requests. Used by - * {@link AndroidGDataClient#createAndExecuteMethod}, since HttpUriRequest does not allow for - * changing the URI after creation, e.g., when you want to follow a redirect. - */ - private interface HttpRequestCreator { - HttpUriRequest createRequest(URI uri); - } - - private static class GetRequestCreator implements HttpRequestCreator { - public GetRequestCreator() { - } - - public HttpUriRequest createRequest(URI uri) { - HttpGet get = new HttpGet(uri); - return get; - } - } - - private static class PostRequestCreator implements HttpRequestCreator { - private final String mMethodOverride; - private final HttpEntity mEntity; - public PostRequestCreator(String methodOverride, HttpEntity entity) { - mMethodOverride = methodOverride; - mEntity = entity; - } - - public HttpUriRequest createRequest(URI uri) { - HttpPost post = new HttpPost(uri); - if (mMethodOverride != null) { - post.addHeader(X_HTTP_METHOD_OVERRIDE, mMethodOverride); - } - post.setEntity(mEntity); - return post; - } - } - - // MAJOR TODO: make this work across redirects (if we can reset the InputStream). - // OR, read the bits into a local buffer (yuck, the media could be large). - private static class MediaPutRequestCreator implements HttpRequestCreator { - private final InputStream mMediaInputStream; - private final String mContentType; - public MediaPutRequestCreator(InputStream mediaInputStream, String contentType) { - mMediaInputStream = mediaInputStream; - mContentType = contentType; - } - - public HttpUriRequest createRequest(URI uri) { - HttpPost post = new HttpPost(uri); - post.addHeader(X_HTTP_METHOD_OVERRIDE, "PUT"); - // mMediaInputStream.reset(); - InputStreamEntity entity = new InputStreamEntity(mMediaInputStream, - -1 /* read until EOF */); - entity.setContentType(mContentType); - post.setEntity(entity); - return post; - } - } - - /** - * @deprecated Use AndroidGDAtaClient(Context) instead. - */ - public AndroidGDataClient(ContentResolver resolver) { - mHttpClient = new GoogleHttpClient(resolver, DEFAULT_USER_AGENT_APP_VERSION, - true /* gzip capable */); - mHttpClient.enableCurlLogging(TAG, Log.VERBOSE); - mResolver = resolver; - } - - /** - * Creates a new AndroidGDataClient. - * - * @param context The ContentResolver to get URL rewriting rules from - * through the Android proxy server, using null to indicate not using proxy. - * The context will also be used by GoogleHttpClient for configuration of - * SSL session persistence. - */ - public AndroidGDataClient(Context context) { - this(context, DEFAULT_USER_AGENT_APP_VERSION); - } - - /** - * Creates a new AndroidGDataClient. - * - * @param context The ContentResolver to get URL rewriting rules from - * through the Android proxy server, using null to indicate not using proxy. - * The context will also be used by GoogleHttpClient for configuration of - * SSL session persistence. - * @param appAndVersion The application name and version to be used as the basis of the - * User-Agent. e.g., Android-GData/1.5.0. - */ - public AndroidGDataClient(Context context, String appAndVersion) { - mHttpClient = new GoogleHttpClient(context, appAndVersion, - true /* gzip capable */); - mHttpClient.enableCurlLogging(TAG, Log.VERBOSE); - mResolver = context.getContentResolver(); - } - - public void close() { - mHttpClient.close(); - } - - /* - * (non-Javadoc) - * @see GDataClient#encodeUri(java.lang.String) - */ - public String encodeUri(String uri) { - String encodedUri; - try { - encodedUri = URLEncoder.encode(uri, "UTF-8"); - } catch (UnsupportedEncodingException uee) { - // should not happen. - Log.e("JakartaGDataClient", - "UTF-8 not supported -- should not happen. " - + "Using default encoding.", uee); - encodedUri = URLEncoder.encode(uri); - } - return encodedUri; - } - - /* - * (non-Javadoc) - * @see com.google.wireless.gdata.client.GDataClient#createQueryParams() - */ - public QueryParams createQueryParams() { - return new QueryParamsImpl(); - } - - // follows redirects - private InputStream createAndExecuteMethod(HttpRequestCreator creator, - String uriString, - String authToken) - throws HttpException, IOException { - - HttpResponse response = null; - int status = 500; - int redirectsLeft = MAX_REDIRECTS; - - URI uri; - try { - uri = new URI(uriString); - } catch (URISyntaxException use) { - Log.w(TAG, "Unable to parse " + uriString + " as URI.", use); - throw new IOException("Unable to parse " + uriString + " as URI: " - + use.getMessage()); - } - - // we follow redirects ourselves, since we want to follow redirects even on POSTs, which - // the HTTP library does not do. following redirects ourselves also allows us to log - // the redirects using our own logging. - while (redirectsLeft > 0) { - - HttpUriRequest request = creator.createRequest(uri); - - if (!SystemProperties.getBoolean(NO_GZIP_SYSTEM_PROPERTY, false)) { - AndroidHttpClient.modifyRequestToAcceptGzipResponse(request); - } - - // only add the auth token if not null (to allow for GData feeds that do not require - // authentication.) - if (!TextUtils.isEmpty(authToken)) { - request.addHeader("Authorization", "GoogleLogin auth=" + authToken); - } - if (LOCAL_LOGV) { - for (Header h : request.getAllHeaders()) { - Log.v(TAG, h.getName() + ": " + h.getValue()); - } - } - - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "Executing " + request.getRequestLine().toString()); - } - - response = null; - - try { - response = mHttpClient.execute(request); - } catch (IOException ioe) { - Log.w(TAG, "Unable to execute HTTP request." + ioe); - throw ioe; - } - - StatusLine statusLine = response.getStatusLine(); - if (statusLine == null) { - Log.w(TAG, "StatusLine is null."); - throw new NullPointerException("StatusLine is null -- should not happen."); - } - - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, response.getStatusLine().toString()); - for (Header h : response.getAllHeaders()) { - Log.d(TAG, h.getName() + ": " + h.getValue()); - } - } - status = statusLine.getStatusCode(); - - HttpEntity entity = response.getEntity(); - - if ((status >= 200) && (status < 300) && entity != null) { - InputStream in = AndroidHttpClient.getUngzippedContent(entity); - if (Log.isLoggable(TAG, Log.DEBUG)) { - in = logInputStreamContents(in); - } - return in; - } - - // TODO: handle 301, 307? - // TODO: let the http client handle the redirects, if we can be sure we'll never get a - // redirect on POST. - if (status == 302) { - // consume the content, so the connection can be closed. - entity.consumeContent(); - Header location = response.getFirstHeader("Location"); - if (location == null) { - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "Redirect requested but no Location " - + "specified."); - } - break; - } - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "Following redirect to " + location.getValue()); - } - try { - uri = new URI(location.getValue()); - } catch (URISyntaxException use) { - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "Unable to parse " + location.getValue() + " as URI.", use); - throw new IOException("Unable to parse " + location.getValue() - + " as URI."); - } - break; - } - --redirectsLeft; - } else { - break; - } - } - - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "Received " + status + " status code."); - } - String errorMessage = null; - HttpEntity entity = response.getEntity(); - try { - if (response != null && entity != null) { - InputStream in = AndroidHttpClient.getUngzippedContent(entity); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - byte[] buf = new byte[8192]; - int bytesRead = -1; - while ((bytesRead = in.read(buf)) != -1) { - baos.write(buf, 0, bytesRead); - } - // TODO: use appropriate encoding, picked up from Content-Type. - errorMessage = new String(baos.toByteArray()); - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, errorMessage); - } - } - } finally { - if (entity != null) { - entity.consumeContent(); - } - } - String exceptionMessage = "Received " + status + " status code"; - if (errorMessage != null) { - exceptionMessage += (": " + errorMessage); - } - throw new HttpException(exceptionMessage, status, null /* InputStream */); - } - - /* - * (non-Javadoc) - * @see GDataClient#getFeedAsStream(java.lang.String, java.lang.String) - */ - public InputStream getFeedAsStream(String feedUrl, - String authToken) - throws HttpException, IOException { - - InputStream in = createAndExecuteMethod(new GetRequestCreator(), feedUrl, authToken); - if (in != null) { - return in; - } - throw new IOException("Unable to access feed."); - } - - /** - * Log the contents of the input stream. - * The original input stream is consumed, so the caller must use the - * BufferedInputStream that is returned. - * @param in InputStream - * @return replacement input stream for caller to use - * @throws IOException - */ - private InputStream logInputStreamContents(InputStream in) throws IOException { - if (in == null) { - return in; - } - // bufferSize is the (arbitrary) maximum amount to log. - // The original InputStream is wrapped in a - // BufferedInputStream with a 16K buffer. This lets - // us read up to 16K, write it to the log, and then - // reset the stream so the the original client can - // then read the data. The BufferedInputStream - // provides the mark and reset support, even when - // the original InputStream does not. - final int bufferSize = 16384; - BufferedInputStream bin = new BufferedInputStream(in, bufferSize); - bin.mark(bufferSize); - int wanted = bufferSize; - int totalReceived = 0; - byte buf[] = new byte[wanted]; - while (wanted > 0) { - int got = bin.read(buf, totalReceived, wanted); - if (got <= 0) break; // EOF - wanted -= got; - totalReceived += got; - } - Log.d(TAG, new String(buf, 0, totalReceived, "UTF-8")); - bin.reset(); - return bin; - } - - public InputStream getMediaEntryAsStream(String mediaEntryUrl, String authToken) - throws HttpException, IOException { - - InputStream in = createAndExecuteMethod(new GetRequestCreator(), mediaEntryUrl, authToken); - - if (in != null) { - return in; - } - throw new IOException("Unable to access media entry."); - } - - /* (non-Javadoc) - * @see GDataClient#createEntry - */ - public InputStream createEntry(String feedUrl, - String authToken, - GDataSerializer entry) - throws HttpException, IOException { - - HttpEntity entity = createEntityForEntry(entry, GDataSerializer.FORMAT_CREATE); - InputStream in = createAndExecuteMethod( - new PostRequestCreator(null /* override */, entity), - feedUrl, - authToken); - if (in != null) { - return in; - } - throw new IOException("Unable to create entry."); - } - - /* (non-Javadoc) - * @see GDataClient#updateEntry - */ - public InputStream updateEntry(String editUri, - String authToken, - GDataSerializer entry) - throws HttpException, IOException { - HttpEntity entity = createEntityForEntry(entry, GDataSerializer.FORMAT_UPDATE); - InputStream in = createAndExecuteMethod( - new PostRequestCreator("PUT", entity), - editUri, - authToken); - if (in != null) { - return in; - } - throw new IOException("Unable to update entry."); - } - - /* (non-Javadoc) - * @see GDataClient#deleteEntry - */ - public void deleteEntry(String editUri, String authToken) - throws HttpException, IOException { - if (StringUtils.isEmpty(editUri)) { - throw new IllegalArgumentException( - "you must specify an non-empty edit url"); - } - InputStream in = - createAndExecuteMethod( - new PostRequestCreator("DELETE", null /* entity */), - editUri, - authToken); - if (in == null) { - throw new IOException("Unable to delete entry."); - } - try { - in.close(); - } catch (IOException ioe) { - // ignore - } - } - - public InputStream updateMediaEntry(String editUri, String authToken, - InputStream mediaEntryInputStream, String contentType) - throws HttpException, IOException { - InputStream in = createAndExecuteMethod( - new MediaPutRequestCreator(mediaEntryInputStream, contentType), - editUri, - authToken); - if (in != null) { - return in; - } - throw new IOException("Unable to write media entry."); - } - - private HttpEntity createEntityForEntry(GDataSerializer entry, int format) throws IOException { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - try { - entry.serialize(baos, format); - } catch (IOException ioe) { - Log.e(TAG, "Unable to serialize entry.", ioe); - throw ioe; - } catch (ParseException pe) { - Log.e(TAG, "Unable to serialize entry.", pe); - throw new IOException("Unable to serialize entry: " + pe.getMessage()); - } - - byte[] entryBytes = baos.toByteArray(); - - if (entryBytes != null && Log.isLoggable(TAG, Log.DEBUG)) { - try { - Log.d(TAG, "Serialized entry: " + new String(entryBytes, "UTF-8")); - } catch (UnsupportedEncodingException uee) { - // should not happen - throw new IllegalStateException("UTF-8 should be supported!", - uee); - } - } - - AbstractHttpEntity entity; - if (SystemProperties.getBoolean(NO_GZIP_SYSTEM_PROPERTY, false)) { - entity = new ByteArrayEntity(entryBytes); - } else { - entity = AndroidHttpClient.getCompressedEntity(entryBytes, mResolver); - } - entity.setContentType(entry.getContentType()); - return entity; - } -} diff --git a/core/java/com/google/android/gdata/client/AndroidXmlParserFactory.java b/core/java/com/google/android/gdata/client/AndroidXmlParserFactory.java deleted file mode 100644 index a308fc0..0000000 --- a/core/java/com/google/android/gdata/client/AndroidXmlParserFactory.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.google.android.gdata.client; - -import com.google.wireless.gdata.parser.xml.XmlParserFactory; - -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; -import org.xmlpull.v1.XmlSerializer; - -import android.util.Xml; - -/** - * XmlParserFactory for the Android platform. - */ -public class AndroidXmlParserFactory implements XmlParserFactory { - - /* - * (non-javadoc) - * @see XmlParserFactory#createParser - */ - public XmlPullParser createParser() throws XmlPullParserException { - return Xml.newPullParser(); - } - - /* - * (non-javadoc) - * @see XmlParserFactory#createSerializer - */ - public XmlSerializer createSerializer() throws XmlPullParserException { - return Xml.newSerializer(); - } -} diff --git a/core/java/com/google/android/gdata/client/QueryParamsImpl.java b/core/java/com/google/android/gdata/client/QueryParamsImpl.java deleted file mode 100644 index fbe0d22..0000000 --- a/core/java/com/google/android/gdata/client/QueryParamsImpl.java +++ /dev/null @@ -1,102 +0,0 @@ -package com.google.android.gdata.client; - -import com.google.wireless.gdata.client.QueryParams; - -import android.text.TextUtils; -import android.util.Log; - -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; - -/** - * Simple implementation of the QueryParams interface. - */ -// TODO: deal with categories -public class QueryParamsImpl extends QueryParams { - - private final Map<String,String> mParams = new HashMap<String,String>(); - - /** - * Creates a new empty QueryParamsImpl. - */ - public QueryParamsImpl() { - } - - @Override - public void clear() { - setEntryId(null); - mParams.clear(); - } - - @Override - public String generateQueryUrl(String feedUrl) { - - if (TextUtils.isEmpty(getEntryId()) && - mParams.isEmpty()) { - // nothing to do - return feedUrl; - } - - // handle entry IDs - if (!TextUtils.isEmpty(getEntryId())) { - if (!mParams.isEmpty()) { - throw new IllegalStateException("Cannot set both an entry ID " - + "and other query paramters."); - } - return feedUrl + '/' + getEntryId(); - } - - // otherwise, append the querystring params. - StringBuilder sb = new StringBuilder(); - sb.append(feedUrl); - Set<String> params = mParams.keySet(); - boolean first = true; - if (feedUrl.contains("?")) { - first = false; - } else { - sb.append('?'); - } - for (String param : params) { - String value = mParams.get(param); - if (value == null) continue; - if (first) { - first = false; - } else { - sb.append('&'); - } - sb.append(param); - sb.append('='); - - String encodedValue = null; - - try { - encodedValue = URLEncoder.encode(value, "UTF-8"); - } catch (UnsupportedEncodingException uee) { - // should not happen. - Log.w("QueryParamsImpl", - "UTF-8 not supported -- should not happen. " - + "Using default encoding.", uee); - encodedValue = URLEncoder.encode(value); - } - sb.append(encodedValue); - } - return sb.toString(); - } - - @Override - public String getParamValue(String param) { - if (!(mParams.containsKey(param))) { - return null; - } - return mParams.get(param); - } - - @Override - public void setParamValue(String param, String value) { - mParams.put(param, value); - } - -} diff --git a/core/java/com/google/android/gdata2/client/AndroidGDataClient.java b/core/java/com/google/android/gdata2/client/AndroidGDataClient.java deleted file mode 100644 index 3721fa4..0000000 --- a/core/java/com/google/android/gdata2/client/AndroidGDataClient.java +++ /dev/null @@ -1,603 +0,0 @@ -// Copyright 2007 The Android Open Source Project - -package com.google.android.gdata2.client; - -import com.google.android.net.GoogleHttpClient; -import com.google.wireless.gdata2.client.GDataClient; -import com.google.wireless.gdata2.client.HttpException; -import com.google.wireless.gdata2.client.QueryParams; -import com.google.wireless.gdata2.data.StringUtils; -import com.google.wireless.gdata2.parser.ParseException; -import com.google.wireless.gdata2.serializer.GDataSerializer; -import com.google.android.gdata2.client.QueryParamsImpl; - -import org.apache.http.Header; -import org.apache.http.HttpEntity; -import org.apache.http.HttpResponse; -import org.apache.http.StatusLine; -import org.apache.http.params.HttpParams; -import org.apache.http.params.BasicHttpParams; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.client.methods.HttpUriRequest; -import org.apache.http.entity.InputStreamEntity; -import org.apache.http.entity.AbstractHttpEntity; -import org.apache.http.entity.ByteArrayEntity; - -import android.content.ContentResolver; -import android.content.Context; -import android.net.http.AndroidHttpClient; -import android.text.TextUtils; -import android.util.Config; -import android.util.Log; -import android.os.SystemProperties; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.UnsupportedEncodingException; -import java.io.BufferedInputStream; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URLEncoder; - -/** - * Implementation of a GDataClient using GoogleHttpClient to make HTTP - * requests. Always issues GETs and POSTs, using the X-HTTP-Method-Override - * header when a PUT or DELETE is desired, to avoid issues with firewalls, etc., - * that do not allow methods other than GET or POST. - */ -public class AndroidGDataClient implements GDataClient { - - private static final String TAG = "GDataClient"; - private static final boolean DEBUG = false; - private static final boolean LOCAL_LOGV = DEBUG ? Config.LOGD : Config.LOGV; - - private static final String X_HTTP_METHOD_OVERRIDE = - "X-HTTP-Method-Override"; - - private static final String DEFAULT_USER_AGENT_APP_VERSION = "Android-GData/1.2"; - - private static final int MAX_REDIRECTS = 10; - private static String DEFAULT_GDATA_VERSION = "2.0"; - - // boolean system property that can be used to control whether or not - // requests/responses are gzip'd. - private static final String NO_GZIP_SYSTEM_PROPERTY = "sync.nogzip"; - - private String mGDataVersion; - private final GoogleHttpClient mHttpClient; - private ContentResolver mResolver; - - /** - * Interface for creating HTTP requests. Used by - * {@link AndroidGDataClient#createAndExecuteMethod}, since HttpUriRequest does not allow for - * changing the URI after creation, e.g., when you want to follow a redirect. - */ - private interface HttpRequestCreator { - HttpUriRequest createRequest(URI uri); - } - - private static class GetRequestCreator implements HttpRequestCreator { - public GetRequestCreator() { - } - - public HttpUriRequest createRequest(URI uri) { - HttpGet get = new HttpGet(uri); - return get; - } - } - - private static class PostRequestCreator implements HttpRequestCreator { - private final String mMethodOverride; - private final HttpEntity mEntity; - public PostRequestCreator(String methodOverride, HttpEntity entity) { - mMethodOverride = methodOverride; - mEntity = entity; - } - - public HttpUriRequest createRequest(URI uri) { - HttpPost post = new HttpPost(uri); - if (mMethodOverride != null) { - post.addHeader(X_HTTP_METHOD_OVERRIDE, mMethodOverride); - } - post.setEntity(mEntity); - return post; - } - } - - // MAJOR TODO: make this work across redirects (if we can reset the InputStream). - // OR, read the bits into a local buffer (yuck, the media could be large). - private static class MediaPutRequestCreator implements HttpRequestCreator { - private final InputStream mMediaInputStream; - private final String mContentType; - public MediaPutRequestCreator(InputStream mediaInputStream, String contentType) { - mMediaInputStream = mediaInputStream; - mContentType = contentType; - } - - public HttpUriRequest createRequest(URI uri) { - HttpPost post = new HttpPost(uri); - post.addHeader(X_HTTP_METHOD_OVERRIDE, "PUT"); - // mMediaInputStream.reset(); - InputStreamEntity entity = new InputStreamEntity(mMediaInputStream, - -1 /* read until EOF */); - entity.setContentType(mContentType); - post.setEntity(entity); - return post; - } - } - - - /** - * Creates a new AndroidGDataClient. - * - * @param context The ContentResolver to get URL rewriting rules from - * through the Android proxy server, using null to indicate not using proxy. - * The context will also be used by GoogleHttpClient for configuration of - * SSL session persistence. - */ - public AndroidGDataClient(Context context) { - this(context, DEFAULT_USER_AGENT_APP_VERSION); - } - - /** - * Creates a new AndroidGDataClient. - * - * @param context The ContentResolver to get URL rewriting rules from - * through the Android proxy server, using null to indicate not using proxy. - * The context will also be used by GoogleHttpClient for configuration of - * SSL session persistence. - * @param appAndVersion The application name and version to be used as the basis of the - * User-Agent. e.g., Android-GData/1.5.0. - */ - public AndroidGDataClient(Context context, String appAndVersion) { - this(context, appAndVersion, DEFAULT_GDATA_VERSION); - } - - /** - * Creates a new AndroidGDataClient. - * - * @param context The ContentResolver to get URL rewriting rules from - * through the Android proxy server, using null to indicate not using proxy. - * The context will also be used by GoogleHttpClient for configuration of - * SSL session persistence. - * @param appAndVersion The application name and version to be used as the basis of the - * User-Agent. e.g., Android-GData/1.5.0. - * @param gdataVersion The gdata service version that should be - * used, e.g. "2.0" - * - */ - public AndroidGDataClient(Context context, String appAndVersion, String gdataVersion) { - mHttpClient = new GoogleHttpClient(context, appAndVersion, - true /* gzip capable */); - mHttpClient.enableCurlLogging(TAG, Log.VERBOSE); - mResolver = context.getContentResolver(); - mGDataVersion = gdataVersion; - } - - - public void close() { - mHttpClient.close(); - } - - /* - * (non-Javadoc) - * @see GDataClient#encodeUri(java.lang.String) - */ - public String encodeUri(String uri) { - String encodedUri; - try { - encodedUri = URLEncoder.encode(uri, "UTF-8"); - } catch (UnsupportedEncodingException uee) { - // should not happen. - Log.e("JakartaGDataClient", - "UTF-8 not supported -- should not happen. " - + "Using default encoding.", uee); - encodedUri = URLEncoder.encode(uri); - } - return encodedUri; - } - - /* - * (non-Javadoc) - * @see com.google.wireless.gdata.client.GDataClient#createQueryParams() - */ - public QueryParams createQueryParams() { - return new QueryParamsImpl(); - } - - // follows redirects - private InputStream createAndExecuteMethod(HttpRequestCreator creator, - String uriString, - String authToken, - String eTag, - String protocolVersion) - throws HttpException, IOException { - - HttpResponse response = null; - int status = 500; - int redirectsLeft = MAX_REDIRECTS; - - URI uri; - try { - uri = new URI(uriString); - } catch (URISyntaxException use) { - Log.w(TAG, "Unable to parse " + uriString + " as URI.", use); - throw new IOException("Unable to parse " + uriString + " as URI: " - + use.getMessage()); - } - - // we follow redirects ourselves, since we want to follow redirects even on POSTs, which - // the HTTP library does not do. following redirects ourselves also allows us to log - // the redirects using our own logging. - while (redirectsLeft > 0) { - - HttpUriRequest request = creator.createRequest(uri); - - if (!SystemProperties.getBoolean(NO_GZIP_SYSTEM_PROPERTY, false)) { - AndroidHttpClient.modifyRequestToAcceptGzipResponse(request); - } - - // only add the auth token if not null (to allow for GData feeds that do not require - // authentication.) - if (!TextUtils.isEmpty(authToken)) { - request.addHeader("Authorization", "GoogleLogin auth=" + authToken); - } - - // while by default we have a 2.0 in this variable, it is possible to construct - // a client that has an empty version field, to work with 1.0 services. - if (!TextUtils.isEmpty(mGDataVersion)) { - request.addHeader("GDataVersion", mGDataVersion); - } - - // if we have a passed down eTag value, we need to add several headers - if (!TextUtils.isEmpty(eTag)) { - String method = request.getMethod(); - Header overrideMethodHeader = request.getFirstHeader(X_HTTP_METHOD_OVERRIDE); - if (overrideMethodHeader != null) { - method = overrideMethodHeader.getValue(); - } - if ("GET".equals(method)) { - // add the none match header, if the resource is not changed - // this request will result in a 304 now. - request.addHeader("If-None-Match", eTag); - } else if ("DELETE".equals(method) - || "PUT".equals(method)) { - // now we send an if-match, but only if the passed in eTag is a strong eTag - // as this only makes sense for a strong eTag - if (!eTag.startsWith("W/")) { - request.addHeader("If-Match", eTag); - } - } - } - - if (LOCAL_LOGV) { - for (Header h : request.getAllHeaders()) { - Log.v(TAG, h.getName() + ": " + h.getValue()); - } - } - - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "Executing " + request.getRequestLine().toString()); - } - - response = null; - - try { - response = mHttpClient.execute(request); - } catch (IOException ioe) { - Log.w(TAG, "Unable to execute HTTP request." + ioe); - throw ioe; - } - - StatusLine statusLine = response.getStatusLine(); - if (statusLine == null) { - Log.w(TAG, "StatusLine is null."); - throw new NullPointerException("StatusLine is null -- should not happen."); - } - - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, response.getStatusLine().toString()); - for (Header h : response.getAllHeaders()) { - Log.d(TAG, h.getName() + ": " + h.getValue()); - } - } - status = statusLine.getStatusCode(); - - HttpEntity entity = response.getEntity(); - - if ((status >= 200) && (status < 300) && entity != null) { - InputStream in = AndroidHttpClient.getUngzippedContent(entity); - if (Log.isLoggable(TAG, Log.DEBUG)) { - in = logInputStreamContents(in); - } - return in; - } - - // TODO: handle 301, 307? - // TODO: let the http client handle the redirects, if we can be sure we'll never get a - // redirect on POST. - if (status == 302) { - // consume the content, so the connection can be closed. - entity.consumeContent(); - Header location = response.getFirstHeader("Location"); - if (location == null) { - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "Redirect requested but no Location " - + "specified."); - } - break; - } - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "Following redirect to " + location.getValue()); - } - try { - uri = new URI(location.getValue()); - } catch (URISyntaxException use) { - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "Unable to parse " + location.getValue() + " as URI.", use); - throw new IOException("Unable to parse " + location.getValue() - + " as URI."); - } - break; - } - --redirectsLeft; - } else { - break; - } - } - - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "Received " + status + " status code."); - } - String errorMessage = null; - HttpEntity entity = response.getEntity(); - try { - if (response != null && entity != null) { - InputStream in = AndroidHttpClient.getUngzippedContent(entity); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - byte[] buf = new byte[8192]; - int bytesRead = -1; - while ((bytesRead = in.read(buf)) != -1) { - baos.write(buf, 0, bytesRead); - } - // TODO: use appropriate encoding, picked up from Content-Type. - errorMessage = new String(baos.toByteArray()); - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, errorMessage); - } - } - } finally { - if (entity != null) { - entity.consumeContent(); - } - } - String exceptionMessage = "Received " + status + " status code"; - if (errorMessage != null) { - exceptionMessage += (": " + errorMessage); - } - throw new HttpException(exceptionMessage, status, null /* InputStream */); - } - - /* - * (non-Javadoc) - * @see GDataClient#getFeedAsStream(java.lang.String, java.lang.String) - */ - public InputStream getFeedAsStream(String feedUrl, - String authToken, - String eTag, - String protocolVersion) - throws HttpException, IOException { - - InputStream in = createAndExecuteMethod(new GetRequestCreator(), feedUrl, authToken, eTag, protocolVersion); - if (in != null) { - return in; - } - throw new IOException("Unable to access feed."); - } - - /** - * Log the contents of the input stream. - * The original input stream is consumed, so the caller must use the - * BufferedInputStream that is returned. - * @param in InputStream - * @return replacement input stream for caller to use - * @throws IOException - */ - private InputStream logInputStreamContents(InputStream in) throws IOException { - if (in == null) { - return in; - } - // bufferSize is the (arbitrary) maximum amount to log. - // The original InputStream is wrapped in a - // BufferedInputStream with a 16K buffer. This lets - // us read up to 16K, write it to the log, and then - // reset the stream so the the original client can - // then read the data. The BufferedInputStream - // provides the mark and reset support, even when - // the original InputStream does not. - final int bufferSize = 16384; - BufferedInputStream bin = new BufferedInputStream(in, bufferSize); - bin.mark(bufferSize); - int wanted = bufferSize; - int totalReceived = 0; - byte buf[] = new byte[wanted]; - while (wanted > 0) { - int got = bin.read(buf, totalReceived, wanted); - if (got <= 0) break; // EOF - wanted -= got; - totalReceived += got; - } - Log.d(TAG, new String(buf, 0, totalReceived, "UTF-8")); - bin.reset(); - return bin; - } - - public InputStream getMediaEntryAsStream(String mediaEntryUrl, String authToken, String eTag, String protocolVersion) - throws HttpException, IOException { - - InputStream in = createAndExecuteMethod(new GetRequestCreator(), mediaEntryUrl, authToken, eTag, protocolVersion); - - if (in != null) { - return in; - } - throw new IOException("Unable to access media entry."); - } - - /* (non-Javadoc) - * @see GDataClient#createEntry - */ - public InputStream createEntry(String feedUrl, - String authToken, - String protocolVersion, - GDataSerializer entry) - throws HttpException, IOException { - - HttpEntity entity = createEntityForEntry(entry, GDataSerializer.FORMAT_CREATE); - InputStream in = createAndExecuteMethod( - new PostRequestCreator(null /* override */, entity), - feedUrl, - authToken, - null, - protocolVersion); - if (in != null) { - return in; - } - throw new IOException("Unable to create entry."); - } - - /* (non-Javadoc) - * @see GDataClient#updateEntry - */ - public InputStream updateEntry(String editUri, - String authToken, - String eTag, - String protocolVersion, - GDataSerializer entry) - throws HttpException, IOException { - HttpEntity entity = createEntityForEntry(entry, GDataSerializer.FORMAT_UPDATE); - final String method = entry.getSupportsPartial() ? "PATCH" : "PUT"; - InputStream in = createAndExecuteMethod( - new PostRequestCreator(method, entity), - editUri, - authToken, - eTag, - protocolVersion); - if (in != null) { - return in; - } - throw new IOException("Unable to update entry."); - } - - /* (non-Javadoc) - * @see GDataClient#deleteEntry - */ - public void deleteEntry(String editUri, String authToken, String eTag) - throws HttpException, IOException { - if (StringUtils.isEmpty(editUri)) { - throw new IllegalArgumentException( - "you must specify an non-empty edit url"); - } - InputStream in = - createAndExecuteMethod( - new PostRequestCreator("DELETE", null /* entity */), - editUri, - authToken, - eTag, - null /* protocolVersion, not required for a delete */); - if (in == null) { - throw new IOException("Unable to delete entry."); - } - try { - in.close(); - } catch (IOException ioe) { - // ignore - } - } - - public InputStream updateMediaEntry(String editUri, String authToken, String eTag, - String protocolVersion, InputStream mediaEntryInputStream, String contentType) - throws HttpException, IOException { - InputStream in = createAndExecuteMethod( - new MediaPutRequestCreator(mediaEntryInputStream, contentType), - editUri, - authToken, - eTag, - protocolVersion); - if (in != null) { - return in; - } - throw new IOException("Unable to write media entry."); - } - - private HttpEntity createEntityForEntry(GDataSerializer entry, int format) throws IOException { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - try { - entry.serialize(baos, format); - } catch (IOException ioe) { - Log.e(TAG, "Unable to serialize entry.", ioe); - throw ioe; - } catch (ParseException pe) { - Log.e(TAG, "Unable to serialize entry.", pe); - throw new IOException("Unable to serialize entry: " + pe.getMessage()); - } - - byte[] entryBytes = baos.toByteArray(); - - if (entryBytes != null && Log.isLoggable(TAG, Log.DEBUG)) { - try { - Log.d(TAG, "Serialized entry: " + new String(entryBytes, "UTF-8")); - } catch (UnsupportedEncodingException uee) { - // should not happen - throw new IllegalStateException("UTF-8 should be supported!", - uee); - } - } - - AbstractHttpEntity entity; - if (SystemProperties.getBoolean(NO_GZIP_SYSTEM_PROPERTY, false)) { - entity = new ByteArrayEntity(entryBytes); - } else { - entity = AndroidHttpClient.getCompressedEntity(entryBytes, mResolver); - } - - entity.setContentType(entry.getContentType()); - return entity; - } - - /** - * Connects to a GData server (specified by the batchUrl) and submits a - * batch for processing. The response from the server is returned as an - * {@link InputStream}. The caller is responsible for calling - * {@link InputStream#close()} on the returned {@link InputStream}. - * - * @param batchUrl The batch url to which the batch is submitted. - * @param authToken the authentication token that should be used when - * submitting the batch. - * @param protocolVersion The version of the protocol that - * should be used for this request. - * @param batch The batch of entries to submit. - * @throws IOException Thrown if an io error occurs while communicating with - * the service. - * @throws HttpException if the service returns an error response. - */ - public InputStream submitBatch(String batchUrl, - String authToken, - String protocolVersion, - GDataSerializer batch) - throws HttpException, IOException - { - HttpEntity entity = createEntityForEntry(batch, GDataSerializer.FORMAT_BATCH); - InputStream in = createAndExecuteMethod( - new PostRequestCreator("POST", entity), - batchUrl, - authToken, - null, - protocolVersion); - if (in != null) { - return in; - } - throw new IOException("Unable to process batch request."); - } -} diff --git a/core/java/com/google/android/gdata2/client/AndroidXmlParserFactory.java b/core/java/com/google/android/gdata2/client/AndroidXmlParserFactory.java deleted file mode 100644 index f097706..0000000 --- a/core/java/com/google/android/gdata2/client/AndroidXmlParserFactory.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.google.android.gdata2.client; - -import com.google.wireless.gdata2.parser.xml.XmlParserFactory; - -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; -import org.xmlpull.v1.XmlSerializer; - -import android.util.Xml; - -/** - * XmlParserFactory for the Android platform. - */ -public class AndroidXmlParserFactory implements XmlParserFactory { - - /* - * (non-javadoc) - * @see XmlParserFactory#createParser - */ - public XmlPullParser createParser() throws XmlPullParserException { - return Xml.newPullParser(); - } - - /* - * (non-javadoc) - * @see XmlParserFactory#createSerializer - */ - public XmlSerializer createSerializer() throws XmlPullParserException { - return Xml.newSerializer(); - } -} diff --git a/core/java/com/google/android/gdata2/client/QueryParamsImpl.java b/core/java/com/google/android/gdata2/client/QueryParamsImpl.java deleted file mode 100644 index a26f4ce..0000000 --- a/core/java/com/google/android/gdata2/client/QueryParamsImpl.java +++ /dev/null @@ -1,99 +0,0 @@ -package com.google.android.gdata2.client; -import com.google.wireless.gdata2.client.QueryParams; - -import android.text.TextUtils; -import android.util.Log; - -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; - -/** - * Simple implementation of the QueryParams interface. - */ -// TODO: deal with categories -public class QueryParamsImpl extends QueryParams { - - private final Map<String,String> mParams = new HashMap<String,String>(); - - /** - * Creates a new empty QueryParamsImpl. - */ - public QueryParamsImpl() { - } - - @Override - public void clear() { - setEntryId(null); - mParams.clear(); - } - - @Override - public String generateQueryUrl(String feedUrl) { - - if (TextUtils.isEmpty(getEntryId()) && - mParams.isEmpty()) { - // nothing to do - return feedUrl; - } - - // handle entry IDs - if (!TextUtils.isEmpty(getEntryId())) { - if (!mParams.isEmpty()) { - throw new IllegalStateException("Cannot set both an entry ID " - + "and other query paramters."); - } - return feedUrl + '/' + getEntryId(); - } - - // otherwise, append the querystring params. - StringBuilder sb = new StringBuilder(); - sb.append(feedUrl); - Set<String> params = mParams.keySet(); - boolean first = true; - if (feedUrl.contains("?")) { - first = false; - } else { - sb.append('?'); - } - for (String param : params) { - if (first) { - first = false; - } else { - sb.append('&'); - } - sb.append(param); - sb.append('='); - String value = mParams.get(param); - String encodedValue = null; - - try { - encodedValue = URLEncoder.encode(value, "UTF-8"); - } catch (UnsupportedEncodingException uee) { - // should not happen. - Log.w("QueryParamsImpl", - "UTF-8 not supported -- should not happen. " - + "Using default encoding.", uee); - encodedValue = URLEncoder.encode(value); - } - sb.append(encodedValue); - } - return sb.toString(); - } - - @Override - public String getParamValue(String param) { - if (!(mParams.containsKey(param))) { - return null; - } - return mParams.get(param); - } - - @Override - public void setParamValue(String param, String value) { - mParams.put(param, value); - } - -} diff --git a/core/java/com/google/android/mms/pdu/PduPersister.java b/core/java/com/google/android/mms/pdu/PduPersister.java index 2a1f23a..1f754bc 100644 --- a/core/java/com/google/android/mms/pdu/PduPersister.java +++ b/core/java/com/google/android/mms/pdu/PduPersister.java @@ -424,6 +424,9 @@ public class PduPersister { // faster. if ("text/plain".equals(type) || "application/smil".equals(type)) { String text = c.getString(PART_COLUMN_TEXT); + if (text == null) { + text = ""; + } byte [] blob = new EncodedStringValue(text).getTextString(); baos.write(blob, 0, blob.length); } else { @@ -858,7 +861,7 @@ public class PduPersister { } else { values.put(Mms.SUBJECT, ""); } - + long messageSize = sendReq.getMessageSize(); if (messageSize > 0) { values.put(Mms.MESSAGE_SIZE, messageSize); diff --git a/core/java/com/google/android/net/GoogleHttpClient.java b/core/java/com/google/android/net/GoogleHttpClient.java deleted file mode 100644 index 8a1298f..0000000 --- a/core/java/com/google/android/net/GoogleHttpClient.java +++ /dev/null @@ -1,399 +0,0 @@ -/* - * 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.google.android.net; - -import android.content.ContentResolver; -import android.content.ContentValues; -import android.content.Context; -import android.net.http.AndroidHttpClient; -import android.os.Build; -import android.os.NetStat; -import android.os.SystemClock; -import android.provider.Checkin; -import android.util.Config; -import android.util.Log; -import org.apache.http.HttpEntity; -import org.apache.http.HttpEntityEnclosingRequest; -import org.apache.http.HttpHost; -import org.apache.http.HttpRequest; -import org.apache.http.HttpResponse; -import org.apache.http.ProtocolException; -import org.apache.http.client.ClientProtocolException; -import org.apache.http.client.HttpClient; -import org.apache.http.client.ResponseHandler; -import org.apache.http.client.methods.HttpUriRequest; -import org.apache.http.conn.ClientConnectionManager; -import org.apache.http.conn.scheme.LayeredSocketFactory; -import org.apache.http.conn.scheme.Scheme; -import org.apache.http.conn.scheme.SchemeRegistry; -import org.apache.http.conn.scheme.SocketFactory; -import org.apache.http.impl.client.EntityEnclosingRequestWrapper; -import org.apache.http.impl.client.RequestWrapper; -import org.apache.http.params.HttpParams; -import org.apache.http.protocol.HttpContext; -import org.apache.harmony.xnet.provider.jsse.SSLClientSessionCache; - -import java.io.IOException; -import java.net.InetAddress; -import java.net.Socket; -import java.net.URI; -import java.net.URISyntaxException; - -/** - * {@link AndroidHttpClient} wrapper that uses {@link UrlRules} to rewrite URLs - * and otherwise tweak HTTP requests. - */ -public class GoogleHttpClient implements HttpClient { - private static final String TAG = "GoogleHttpClient"; - private static final boolean LOCAL_LOGV = Config.LOGV || false; - - /** Exception thrown when a request is blocked by the URL rules. */ - public static class BlockedRequestException extends IOException { - private final UrlRules.Rule mRule; - BlockedRequestException(UrlRules.Rule rule) { - super("Blocked by rule: " + rule.mName); - mRule = rule; - } - } - - private final AndroidHttpClient mClient; - private final ContentResolver mResolver; - private final String mAppName, mUserAgent; - private final ThreadLocal<Boolean> mConnectionAllocated = new ThreadLocal<Boolean>(); - - /** - * Create an HTTP client without SSL session persistence. - * @deprecated Use {@link #GoogleHttpClient(android.content.Context, String, boolean)} - */ - public GoogleHttpClient(ContentResolver resolver, String userAgent) { - mClient = AndroidHttpClient.newInstance(userAgent); - mResolver = resolver; - mUserAgent = mAppName = userAgent; - } - - /** - * Create an HTTP client without SSL session persistence. - * @deprecated Use {@link #GoogleHttpClient(android.content.Context, String, boolean)} - */ - public GoogleHttpClient(ContentResolver resolver, String appAndVersion, - boolean gzipCapable) { - this(resolver, null /* cache */, appAndVersion, gzipCapable); - } - - /** - * Create an HTTP client. Normaly this client is shared throughout an app. - * The HTTP client will construct its User-Agent as follows: - * - * <appAndVersion> (<build device> <build id>) - * or - * <appAndVersion> (<build device> <build id>); gzip - * (if gzip capable) - * - * The context has settings for URL rewriting rules and is used to enable - * SSL session persistence. - * - * @param context application context. - * @param appAndVersion Base app and version to use in the User-Agent. - * e.g., "MyApp/1.0" - * @param gzipCapable Whether or not this client is able to consume gzip'd - * responses. Only used to modify the User-Agent, not other request - * headers. Needed because Google servers require gzip in the User-Agent - * in order to return gzip'd content. - */ - public GoogleHttpClient(Context context, String appAndVersion, boolean gzipCapable) { - this(context.getContentResolver(), - SSLClientSessionCacheFactory.getCache(context), - appAndVersion, gzipCapable); - } - - private GoogleHttpClient(ContentResolver resolver, - SSLClientSessionCache cache, - String appAndVersion, boolean gzipCapable) { - String userAgent = appAndVersion + " (" + Build.DEVICE + " " + Build.ID + ")"; - if (gzipCapable) { - userAgent = userAgent + "; gzip"; - } - - mClient = AndroidHttpClient.newInstance(userAgent, cache); - mResolver = resolver; - mAppName = appAndVersion; - mUserAgent = userAgent; - - // Wrap all the socket factories with the appropriate wrapper. (Apache - // HTTP, curse its black and stupid heart, inspects the SocketFactory to - // see if it's a LayeredSocketFactory, so we need two wrapper classes.) - SchemeRegistry registry = getConnectionManager().getSchemeRegistry(); - for (String name : registry.getSchemeNames()) { - Scheme scheme = registry.unregister(name); - SocketFactory sf = scheme.getSocketFactory(); - if (sf instanceof LayeredSocketFactory) { - sf = new WrappedLayeredSocketFactory((LayeredSocketFactory) sf); - } else { - sf = new WrappedSocketFactory(sf); - } - registry.register(new Scheme(name, sf, scheme.getDefaultPort())); - } - } - - /** - * Delegating wrapper for SocketFactory records when sockets are connected. - * We use this to know whether a connection was created vs reused, to - * gather per-app statistics about connection reuse rates. - * (Note, we record only *connection*, not *creation* of sockets -- - * what we care about is the network overhead of an actual TCP connect.) - */ - private class WrappedSocketFactory implements SocketFactory { - private SocketFactory mDelegate; - private WrappedSocketFactory(SocketFactory delegate) { mDelegate = delegate; } - public final Socket createSocket() throws IOException { return mDelegate.createSocket(); } - public final boolean isSecure(Socket s) { return mDelegate.isSecure(s); } - - public final Socket connectSocket( - Socket s, String h, int p, - InetAddress la, int lp, HttpParams params) throws IOException { - mConnectionAllocated.set(Boolean.TRUE); - return mDelegate.connectSocket(s, h, p, la, lp, params); - } - } - - /** Like WrappedSocketFactory, but for the LayeredSocketFactory subclass. */ - private class WrappedLayeredSocketFactory - extends WrappedSocketFactory implements LayeredSocketFactory { - private LayeredSocketFactory mDelegate; - private WrappedLayeredSocketFactory(LayeredSocketFactory sf) { super(sf); mDelegate = sf; } - - public final Socket createSocket(Socket s, String host, int port, boolean autoClose) - throws IOException { - return mDelegate.createSocket(s, host, port, autoClose); - } - } - - /** - * Release resources associated with this client. You must call this, - * or significant resources (sockets and memory) may be leaked. - */ - public void close() { - mClient.close(); - } - - /** Execute a request without applying and rewrite rules. */ - public HttpResponse executeWithoutRewriting( - HttpUriRequest request, HttpContext context) - throws IOException { - int code = -1; - long start = SystemClock.elapsedRealtime(); - try { - HttpResponse response; - mConnectionAllocated.set(null); - - if (NetworkStatsEntity.shouldLogNetworkStats()) { - // TODO: if we're logging network stats, and if the apache library is configured - // to follow redirects, count each redirect as an additional round trip. - - int uid = android.os.Process.myUid(); - long startTx = NetStat.getUidTxBytes(uid); - long startRx = NetStat.getUidRxBytes(uid); - - response = mClient.execute(request, context); - HttpEntity origEntity = response == null ? null : response.getEntity(); - if (origEntity != null) { - // yeah, we compute the same thing below. we do need to compute this here - // so we can wrap the HttpEntity in the response. - long now = SystemClock.elapsedRealtime(); - long elapsed = now - start; - NetworkStatsEntity entity = new NetworkStatsEntity(origEntity, - mAppName, uid, startTx, startRx, - elapsed /* response latency */, now /* processing start time */); - response.setEntity(entity); - } - } else { - response = mClient.execute(request, context); - } - - code = response.getStatusLine().getStatusCode(); - return response; - } finally { - // Record some statistics to the checkin service about the outcome. - // Note that this is only describing execute(), not body download. - // We assume the database writes are much faster than network I/O, - // and not worth running in a background thread or anything. - try { - long elapsed = SystemClock.elapsedRealtime() - start; - ContentValues values = new ContentValues(); - values.put(Checkin.Stats.COUNT, 1); - values.put(Checkin.Stats.SUM, elapsed / 1000.0); - - values.put(Checkin.Stats.TAG, Checkin.Stats.Tag.HTTP_REQUEST + ":" + mAppName); - mResolver.insert(Checkin.Stats.CONTENT_URI, values); - - // No sockets and no exceptions means we successfully reused a connection - if (mConnectionAllocated.get() == null && code >= 0) { - values.put(Checkin.Stats.TAG, Checkin.Stats.Tag.HTTP_REUSED + ":" + mAppName); - mResolver.insert(Checkin.Stats.CONTENT_URI, values); - } - - String status = code < 0 ? "IOException" : Integer.toString(code); - values.put(Checkin.Stats.TAG, - Checkin.Stats.Tag.HTTP_STATUS + ":" + mAppName + ":" + status); - mResolver.insert(Checkin.Stats.CONTENT_URI, values); - } catch (Exception e) { - Log.e(TAG, "Error recording stats", e); - } - } - } - - public String rewriteURI(String original) { - UrlRules rules = UrlRules.getRules(mResolver); - UrlRules.Rule rule = rules.matchRule(original); - return rule.apply(original); - } - - public HttpResponse execute(HttpUriRequest request, HttpContext context) - throws IOException { - // Rewrite the supplied URL... - URI uri = request.getURI(); - String original = uri.toString(); - UrlRules rules = UrlRules.getRules(mResolver); - UrlRules.Rule rule = rules.matchRule(original); - String rewritten = rule.apply(original); - - if (rewritten == null) { - Log.w(TAG, "Blocked by " + rule.mName + ": " + original); - throw new BlockedRequestException(rule); - } else if (rewritten == original) { - return executeWithoutRewriting(request, context); // Pass through - } - - try { - uri = new URI(rewritten); - } catch (URISyntaxException e) { - throw new RuntimeException("Bad URL from rule: " + rule.mName, e); - } - - // Wrap request so we can replace the URI. - RequestWrapper wrapper = wrapRequest(request); - wrapper.setURI(uri); - request = wrapper; - - if (LOCAL_LOGV) Log.v(TAG, "Rule " + rule.mName + ": " + original + " -> " + rewritten); - return executeWithoutRewriting(request, context); - } - - /** - * Wraps the request making it mutable. - */ - private static RequestWrapper wrapRequest(HttpUriRequest request) - throws IOException { - try { - // We have to wrap it with the right type. Some code performs - // instanceof checks. - RequestWrapper wrapped; - if (request instanceof HttpEntityEnclosingRequest) { - wrapped = new EntityEnclosingRequestWrapper( - (HttpEntityEnclosingRequest) request); - } else { - wrapped = new RequestWrapper(request); - } - - // Copy the headers from the original request into the wrapper. - wrapped.resetHeaders(); - - return wrapped; - } catch (ProtocolException e) { - throw new ClientProtocolException(e); - } - } - - /** - * Mark a user agent as one Google will trust to handle gzipped content. - * {@link AndroidHttpClient#modifyRequestToAcceptGzipResponse} is (also) - * necessary but not sufficient -- many browsers claim to accept gzip but - * have broken handling, so Google checks the user agent as well. - * - * @param originalUserAgent to modify (however you identify yourself) - * @return user agent with a "yes, I really can handle gzip" token added. - * @deprecated Use {@link #GoogleHttpClient(android.content.ContentResolver, String, boolean)} - */ - public static String getGzipCapableUserAgent(String originalUserAgent) { - return originalUserAgent + "; gzip"; - } - - // HttpClient wrapper methods. - - public HttpParams getParams() { - return mClient.getParams(); - } - - public ClientConnectionManager getConnectionManager() { - return mClient.getConnectionManager(); - } - - public HttpResponse execute(HttpUriRequest request) throws IOException { - return execute(request, (HttpContext) null); - } - - public HttpResponse execute(HttpHost target, HttpRequest request) - throws IOException { - return mClient.execute(target, request); - } - - public HttpResponse execute(HttpHost target, HttpRequest request, - HttpContext context) throws IOException { - return mClient.execute(target, request, context); - } - - public <T> T execute(HttpUriRequest request, - ResponseHandler<? extends T> responseHandler) - throws IOException, ClientProtocolException { - return mClient.execute(request, responseHandler); - } - - public <T> T execute(HttpUriRequest request, - ResponseHandler<? extends T> responseHandler, HttpContext context) - throws IOException, ClientProtocolException { - return mClient.execute(request, responseHandler, context); - } - - public <T> T execute(HttpHost target, HttpRequest request, - ResponseHandler<? extends T> responseHandler) throws IOException, - ClientProtocolException { - return mClient.execute(target, request, responseHandler); - } - - public <T> T execute(HttpHost target, HttpRequest request, - ResponseHandler<? extends T> responseHandler, HttpContext context) - throws IOException, ClientProtocolException { - return mClient.execute(target, request, responseHandler, context); - } - - /** - * Enables cURL request logging for this client. - * - * @param name to log messages with - * @param level at which to log messages (see {@link android.util.Log}) - */ - public void enableCurlLogging(String name, int level) { - mClient.enableCurlLogging(name, level); - } - - /** - * Disables cURL logging for this client. - */ - public void disableCurlLogging() { - mClient.disableCurlLogging(); - } -} diff --git a/core/java/com/google/android/net/NetworkStatsEntity.java b/core/java/com/google/android/net/NetworkStatsEntity.java deleted file mode 100644 index f5d2349..0000000 --- a/core/java/com/google/android/net/NetworkStatsEntity.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * 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.google.android.net; - -import android.os.NetStat; -import android.os.SystemClock; -import android.os.SystemProperties; -import android.util.EventLog; - -import org.apache.http.HttpEntity; -import org.apache.http.entity.HttpEntityWrapper; - -import java.io.FilterInputStream; -import java.io.IOException; -import java.io.InputStream; - - -public class NetworkStatsEntity extends HttpEntityWrapper { - - private static final int HTTP_STATS_EVENT = 52001; - - private class NetworkStatsInputStream extends FilterInputStream { - - public NetworkStatsInputStream(InputStream wrapped) { - super(wrapped); - } - - @Override - public void close() throws IOException { - try { - super.close(); - } finally { - long processingTime = SystemClock.elapsedRealtime() - mProcessingStartTime; - long tx = NetStat.getUidTxBytes(mUid); - long rx = NetStat.getUidRxBytes(mUid); - - EventLog.writeEvent(HTTP_STATS_EVENT, mUa, mResponseLatency, processingTime, - tx - mStartTx, rx - mStartRx); - } - } - } - - private final String mUa; - private final int mUid; - private final long mStartTx; - private final long mStartRx; - private final long mResponseLatency; - private final long mProcessingStartTime; - - public NetworkStatsEntity(HttpEntity orig, String ua, - int uid, long startTx, long startRx, long responseLatency, - long processingStartTime) { - super(orig); - this.mUa = ua; - this.mUid = uid; - this.mStartTx = startTx; - this.mStartRx = startRx; - this.mResponseLatency = responseLatency; - this.mProcessingStartTime = processingStartTime; - } - - public static boolean shouldLogNetworkStats() { - return "1".equals(SystemProperties.get("googlehttpclient.logstats")); - } - - @Override - public InputStream getContent() throws IOException { - InputStream orig = super.getContent(); - return new NetworkStatsInputStream(orig); - } -} diff --git a/core/java/com/google/android/net/SSLClientSessionCacheFactory.java b/core/java/com/google/android/net/SSLClientSessionCacheFactory.java deleted file mode 100644 index 6570a9bd..0000000 --- a/core/java/com/google/android/net/SSLClientSessionCacheFactory.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.google.android.net; - -import org.apache.harmony.xnet.provider.jsse.SSLClientSessionCache; -import org.apache.harmony.xnet.provider.jsse.FileClientSessionCache; -import android.content.Context; -import android.provider.Settings; -import android.util.Log; - -import java.io.File; -import java.io.IOException; - -import com.android.internal.net.DbSSLSessionCache; - -/** - * Factory that returns the appropriate implementation of a {@link SSLClientSessionCache} based - * on gservices. - * - * @hide - */ -// TODO: return a proxied implementation that is updated as the gservices value changes. -public final class SSLClientSessionCacheFactory { - - private static final String TAG = "SSLClientSessionCacheFactory"; - - public static final String DB = "db"; - public static final String FILE = "file"; - - // utility class - private SSLClientSessionCacheFactory() {} - - /** - * Returns a new {@link SSLClientSessionCache} based on the persistent cache that's specified, - * if any, in gservices. If no cache is specified, returns null. - * @param context The application context used for the per-process persistent cache. - * @return A new {@link SSLClientSessionCache}, or null if no persistent cache is configured. - */ - public static SSLClientSessionCache getCache(Context context) { - String type = Settings.Gservices.getString(context.getContentResolver(), - Settings.Gservices.SSL_SESSION_CACHE); - - if (type != null) { - if (DB.equals(type)) { - return DbSSLSessionCache.getInstanceForPackage(context); - } else if (FILE.equals(type)) { - File dir = context.getFilesDir(); - File cacheDir = new File(dir, "sslcache"); - if (!cacheDir.exists()) { - cacheDir.mkdir(); - } - try { - return FileClientSessionCache.usingDirectory(cacheDir); - } catch (IOException ioe) { - Log.w(TAG, "Unable to create FileClientSessionCache in " + cacheDir.getName(), ioe); - return null; - } - } else { - Log.w(TAG, "Ignoring unrecognized type: '" + type + "'"); - } - } - return null; - } -} diff --git a/core/java/com/google/android/net/UrlRules.java b/core/java/com/google/android/net/UrlRules.java deleted file mode 100644 index 54d139d..0000000 --- a/core/java/com/google/android/net/UrlRules.java +++ /dev/null @@ -1,236 +0,0 @@ -/* - * 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.google.android.net; - -import android.content.ContentResolver; -import android.database.Cursor; -import android.provider.Checkin; -import android.provider.Settings; -import android.util.Config; -import android.util.Log; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * A set of rules rewriting and blocking URLs. Used to offer a point of - * control for redirecting HTTP requests, often to the Android proxy server. - * - * <p>Each rule has the following format: - * - * <pre><em>url-prefix</em> [REWRITE <em>new-prefix</em>] [BLOCK]</pre> - * - * <p>Any URL which starts with <em>url-prefix</em> will trigger the rule. - * If BLOCK is specified, requests to that URL will be blocked and fail. - * If REWRITE is specified, the matching prefix will be removed and replaced - * with <em>new-prefix</em>. (If both are specified, BLOCK wins.) Case is - * insensitive for the REWRITE and BLOCK keywords, but sensitive for URLs. - * - * <p>In Gservices, the value of any key that starts with "url:" will be - * interpreted as a rule. The full name of the key is unimportant (but can - * be used to document the intent of the rule, and must be unique). - * Example gservices keys: - * - * <pre> - * url:use_proxy_for_calendar = "http://www.google.com/calendar/ REWRITE http://android.clients.google.com/proxy/calendar/" - * url:stop_crash_reports = "http://android.clients.google.com/crash/ BLOCK" - * url:use_ssl_for_contacts = "http://www.google.com/m8/ REWRITE https://www.google.com/m8/" - * </pre> - */ -public class UrlRules { - public static final String TAG = "UrlRules"; - public static final boolean LOCAL_LOGV = Config.LOGV || false; - - /** Thrown when the rewrite rules can't be parsed. */ - public static class RuleFormatException extends Exception { - public RuleFormatException(String msg) { super(msg); } - } - - /** A single rule specifying actions for URLs matching a certain prefix. */ - public static class Rule implements Comparable { - /** Name assigned to the rule (for logging and debugging). */ - public final String mName; - - /** Prefix required to match this rule. */ - public final String mPrefix; - - /** Text to replace mPrefix with (null to leave alone). */ - public final String mRewrite; - - /** True if matching URLs should be blocked. */ - public final boolean mBlock; - - /** Default rule that does nothing. */ - public static final Rule DEFAULT = new Rule(); - - /** Parse a rewrite rule as given in a Gservices value. */ - public Rule(String name, String rule) throws RuleFormatException { - mName = name; - String[] words = PATTERN_SPACE_PLUS.split(rule); - if (words.length == 0) throw new RuleFormatException("Empty rule"); - - mPrefix = words[0]; - String rewrite = null; - boolean block = false; - for (int pos = 1; pos < words.length; ) { - String word = words[pos].toLowerCase(); - if (word.equals("rewrite") && pos + 1 < words.length) { - rewrite = words[pos + 1]; - pos += 2; - } else if (word.equals("block")) { - block = true; - pos += 1; - } else { - throw new RuleFormatException("Illegal rule: " + rule); - } - // TODO: Parse timeout specifications, etc. - } - - mRewrite = rewrite; - mBlock = block; - } - - /** Create the default Rule. */ - private Rule() { - mName = "DEFAULT"; - mPrefix = ""; - mRewrite = null; - mBlock = false; - } - - /** - * Apply the rule to a particular URL (assumed to match the rule). - * @param url to rewrite or modify. - * @return modified URL, or null if the URL is blocked. - */ - public String apply(String url) { - if (mBlock) { - return null; - } else if (mRewrite != null) { - return mRewrite + url.substring(mPrefix.length()); - } else { - return url; - } - } - - /** More generic rules are greater than more specific rules. */ - public int compareTo(Object o) { - return ((Rule) o).mPrefix.compareTo(mPrefix); - } - } - - /** Cached rule set from Gservices. */ - private static UrlRules sCachedRules = new UrlRules(new Rule[] {}); - - private static final Pattern PATTERN_SPACE_PLUS = Pattern.compile(" +"); - private static final Pattern RULE_PATTERN = Pattern.compile("\\W"); - - /** Gservices digest when sCachedRules was cached. */ - private static String sCachedDigest = null; - - /** Currently active set of Rules. */ - private final Rule[] mRules; - - /** Regular expression with one capturing group for each Rule. */ - private final Pattern mPattern; - - /** - * Create a rewriter from an array of Rules. Normally used only for - * testing. Instead, use {@link #getRules} to get rules from Gservices. - * @param rules to use. - */ - public UrlRules(Rule[] rules) { - // Sort the rules to put the most specific rules first. - Arrays.sort(rules); - - // Construct a regular expression, escaping all the prefix strings. - StringBuilder pattern = new StringBuilder("("); - for (int i = 0; i < rules.length; ++i) { - if (i > 0) pattern.append(")|("); - pattern.append(RULE_PATTERN.matcher(rules[i].mPrefix).replaceAll("\\\\$0")); - } - mPattern = Pattern.compile(pattern.append(")").toString()); - mRules = rules; - } - - /** - * Match a string against every Rule and find one that matches. - * @param uri to match against the Rules in the rewriter. - * @return the most specific matching Rule, or Rule.DEFAULT if none match. - */ - public Rule matchRule(String url) { - Matcher matcher = mPattern.matcher(url); - if (matcher.lookingAt()) { - for (int i = 0; i < mRules.length; ++i) { - if (matcher.group(i + 1) != null) { - return mRules[i]; // Rules are sorted most specific first. - } - } - } - return Rule.DEFAULT; - } - - /** - * Get the (possibly cached) UrlRules based on the rules in Gservices. - * @param resolver to use for accessing the Gservices database. - * @return an updated UrlRules instance - */ - public static synchronized UrlRules getRules(ContentResolver resolver) { - String digest = Settings.Gservices.getString(resolver, - Settings.Gservices.PROVISIONING_DIGEST); - if (sCachedDigest != null && sCachedDigest.equals(digest)) { - // The digest is the same, so the rules are the same. - if (LOCAL_LOGV) Log.v(TAG, "Using cached rules for digest: " + digest); - return sCachedRules; - } - - if (LOCAL_LOGV) Log.v(TAG, "Scanning for Gservices \"url:*\" rules"); - Cursor cursor = resolver.query(Settings.Gservices.CONTENT_URI, - new String[] { - Settings.Gservices.NAME, - Settings.Gservices.VALUE - }, - Settings.Gservices.NAME + " like \"url:%\"", null, - Settings.Gservices.NAME); - try { - ArrayList<Rule> rules = new ArrayList<Rule>(); - while (cursor.moveToNext()) { - try { - String name = cursor.getString(0).substring(4); // "url:X" - String value = cursor.getString(1); - if (value == null || value.length() == 0) continue; - if (LOCAL_LOGV) Log.v(TAG, " Rule " + name + ": " + value); - rules.add(new Rule(name, value)); - } catch (RuleFormatException e) { - // Oops, Gservices has an invalid rule! Skip it. - Log.e(TAG, "Invalid rule from Gservices", e); - Checkin.logEvent(resolver, - Checkin.Events.Tag.GSERVICES_ERROR, e.toString()); - } - } - sCachedRules = new UrlRules(rules.toArray(new Rule[rules.size()])); - sCachedDigest = digest; - if (LOCAL_LOGV) Log.v(TAG, "New rules stored for digest: " + digest); - } finally { - cursor.close(); - } - - return sCachedRules; - } -} diff --git a/core/java/com/google/android/util/GoogleWebContentHelper.java b/core/java/com/google/android/util/GoogleWebContentHelper.java deleted file mode 100644 index 8291e29..0000000 --- a/core/java/com/google/android/util/GoogleWebContentHelper.java +++ /dev/null @@ -1,281 +0,0 @@ -/* - * 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.google.android.util; - -import android.content.ContentResolver; -import android.content.Context; -import android.net.http.SslError; -import android.os.Message; -import android.provider.Settings; -import android.text.TextUtils; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.webkit.HttpAuthHandler; -import android.webkit.SslErrorHandler; -import android.webkit.WebSettings; -import android.webkit.WebView; -import android.webkit.WebViewClient; -import android.widget.TextView; - -import java.util.Locale; - -/** - * Helper to display Google web content, and fallback on a static message if the - * web content is unreachable. For example, this can be used to display - * "Legal terms". - * <p> - * The typical usage pattern is to have two Gservices settings defined: - * <ul> - * <li>A secure URL that will be displayed on the device. This should be HTTPS - * so hotspots won't intercept it giving us a false positive that the page - * loaded successfully. - * <li>A pretty human-readable URL that will be displayed to the user in case we - * cannot reach the above URL. - * </ul> - * <p> - * The typical call sequence is {@link #setUrlsFromGservices(String, String)}, - * {@link #setUnsuccessfulMessage(String)}, and {@link #loadUrl()}. At some - * point, you'll want to display the layout via {@link #getLayout()}. - */ -public class GoogleWebContentHelper { - - private Context mContext; - - private String mSecureUrl; - private String mPrettyUrl; - - private String mUnsuccessfulMessage; - - private ViewGroup mLayout; - private WebView mWebView; - private View mProgressBar; - private TextView mTextView; - - private boolean mReceivedResponse; - - public GoogleWebContentHelper(Context context) { - mContext = context; - } - - /** - * Fetches the URLs from Gservices. - * - * @param secureSetting The setting key whose value contains the HTTPS URL. - * @param prettySetting The setting key whose value contains the pretty URL. - * @return This {@link GoogleWebContentHelper} so methods can be chained. - */ - public GoogleWebContentHelper setUrlsFromGservices(String secureSetting, String prettySetting) { - ContentResolver contentResolver = mContext.getContentResolver(); - mSecureUrl = fillUrl(Settings.Gservices.getString(contentResolver, secureSetting), - mContext); - mPrettyUrl = fillUrl(Settings.Gservices.getString(contentResolver, prettySetting), - mContext); - return this; - } - - /** - * Fetch directly from provided urls. - * - * @param secureUrl The HTTPS URL. - * @param prettyUrl The pretty URL. - * @return This {@link GoogleWebContentHelper} so methods can be chained. - */ - public GoogleWebContentHelper setUrls(String secureUrl, String prettyUrl) { - mSecureUrl = fillUrl(secureUrl, mContext); - mPrettyUrl = fillUrl(prettyUrl, mContext); - return this; - } - - - /** - * Sets the message that will be shown if we are unable to load the page. - * <p> - * This should be called after {@link #setUrlsFromGservices(String, String)} - * . - * - * @param message The message to load. The first argument, according to - * {@link java.util.Formatter}, will be substituted with the pretty - * URL. - * @return This {@link GoogleWebContentHelper} so methods can be chained. - */ - public GoogleWebContentHelper setUnsuccessfulMessage(String message) { - Locale locale = mContext.getResources().getConfiguration().locale; - mUnsuccessfulMessage = String.format(locale, message, mPrettyUrl); - return this; - } - - /** - * Begins loading the secure URL. - * - * @return This {@link GoogleWebContentHelper} so methods can be chained. - */ - public GoogleWebContentHelper loadUrl() { - ensureViews(); - mWebView.loadUrl(mSecureUrl); - return this; - } - - /** - * Loads data into the webview and also provides a failback url - * @return This {@link GoogleWebContentHelper} so methods can be chained. - */ - public GoogleWebContentHelper loadDataWithFailUrl(String base, String data, - String mimeType, String encoding, String failUrl) { - ensureViews(); - mWebView.loadDataWithBaseURL(base, data, mimeType, encoding, failUrl); - return this; - } - - /** - * Helper to handle the back key. Returns true if the back key was handled, - * otherwise returns false. - * @param event the key event sent to {@link Activity#dispatchKeyEvent()} - */ - public boolean handleKey(KeyEvent event) { - if (event.getKeyCode() == KeyEvent.KEYCODE_BACK - && event.getAction() == KeyEvent.ACTION_DOWN) { - if (mWebView.canGoBack()) { - mWebView.goBack(); - return true; - } - } - return false; - } - - /** - * Returns the layout containing the web view, progress bar, and text view. - * This class takes care of setting each one's visibility based on current - * state. - * - * @return The layout you should display. - */ - public ViewGroup getLayout() { - ensureViews(); - return mLayout; - } - - private synchronized void ensureViews() { - if (mLayout == null) { - initializeViews(); - } - } - - /** - * Fills the URL with the locale. - * - * @param url The URL in Formatter style for the extra info to be filled in. - * @return The filled URL. - */ - private static String fillUrl(String url, Context context) { - - if (TextUtils.isEmpty(url)) { - return ""; - } - - /* We add another layer of indirection here to allow mcc's to fill - * in Locales for TOS. TODO - REMOVE when needed locales supported - * natively (when not shipping devices to country X without support - * for their locale). - */ - String localeReplacement = context. - getString(com.android.internal.R.string.locale_replacement); - if (localeReplacement != null && localeReplacement.length() != 0) { - url = String.format(url, localeReplacement); - } - - Locale locale = Locale.getDefault(); - String tmp = locale.getLanguage() + "_" + locale.getCountry().toLowerCase(); - return String.format(url, tmp); - } - - private void initializeViews() { - - LayoutInflater inflater = - (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - - mLayout = (ViewGroup) inflater.inflate( - com.android.internal.R.layout.google_web_content_helper_layout, null); - - mWebView = (WebView) mLayout.findViewById(com.android.internal.R.id.web); - mWebView.setWebViewClient(new MyWebViewClient()); - WebSettings settings = mWebView.getSettings(); - settings.setCacheMode(WebSettings.LOAD_NO_CACHE); - - mProgressBar = mLayout.findViewById(com.android.internal.R.id.progressContainer); - TextView message = (TextView) mProgressBar.findViewById(com.android.internal.R.id.message); - message.setText(com.android.internal.R.string.googlewebcontenthelper_loading); - - mTextView = (TextView) mLayout.findViewById(com.android.internal.R.id.text); - mTextView.setText(mUnsuccessfulMessage); - } - - private synchronized void handleWebViewCompletion(boolean success) { - - if (mReceivedResponse) { - return; - } else { - mReceivedResponse = true; - } - - // In both cases, remove the progress bar - ((ViewGroup) mProgressBar.getParent()).removeView(mProgressBar); - - // Remove the view that isn't relevant - View goneView = success ? mTextView : mWebView; - ((ViewGroup) goneView.getParent()).removeView(goneView); - - // Show the next view, which depends on success - View visibleView = success ? mWebView : mTextView; - visibleView.setVisibility(View.VISIBLE); - } - - private class MyWebViewClient extends WebViewClient { - - @Override - public void onPageFinished(WebView view, String url) { - handleWebViewCompletion(true); - } - - @Override - public void onReceivedError(WebView view, int errorCode, - String description, String failingUrl) { - handleWebViewCompletion(false); - } - - @Override - public void onReceivedHttpAuthRequest(WebView view, - HttpAuthHandler handler, String host, String realm) { - handleWebViewCompletion(false); - } - - @Override - public void onReceivedSslError(WebView view, SslErrorHandler handler, - SslError error) { - handleWebViewCompletion(false); - } - - @Override - public void onTooManyRedirects(WebView view, Message cancelMsg, - Message continueMsg) { - handleWebViewCompletion(false); - } - - } - -} diff --git a/core/java/com/google/android/util/SimplePullParser.java b/core/java/com/google/android/util/SimplePullParser.java deleted file mode 100644 index 031790b..0000000 --- a/core/java/com/google/android/util/SimplePullParser.java +++ /dev/null @@ -1,391 +0,0 @@ -/* - * 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.google.android.util; - -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; - -import java.io.IOException; -import java.io.InputStream; -import java.io.StringReader; -import java.io.Reader; -import java.io.Closeable; - -import android.util.Xml; -import android.util.Log; - -/** - * This is an abstraction of a pull parser that provides several benefits:<ul> - * <li>it is easier to use robustly because it makes it trivial to handle unexpected tags (which - * might have children)</li> - * <li>it makes the handling of text (cdata) blocks more convenient</li> - * <li>it provides convenient methods for getting a mandatory attribute (and throwing an exception - * if it is missing) or an optional attribute (and using a default value if it is missing) - * </ul> - */ -public class SimplePullParser { - public static final String TEXT_TAG = "