diff options
Diffstat (limited to 'core/java')
67 files changed, 4543 insertions, 2416 deletions
diff --git a/core/java/android/accounts/AccountManagerService.java b/core/java/android/accounts/AccountManagerService.java index 800ad749..1a8d9b6 100644 --- a/core/java/android/accounts/AccountManagerService.java +++ b/core/java/android/accounts/AccountManagerService.java @@ -16,20 +16,27 @@ 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.Handler; import android.os.HandlerThread; @@ -37,17 +44,13 @@ 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.FileDescriptor; import java.io.PrintWriter; @@ -58,8 +61,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 +94,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; @@ -221,8 +222,6 @@ 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); @@ -1076,7 +1075,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; @@ -1158,7 +1157,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"); } @@ -1167,7 +1166,7 @@ public class AccountManagerService private void unbind() { if (mAuthenticator != null) { mAuthenticator = null; - mBindHelper.unbind(this); + mContext.unbindService(this); } } @@ -1180,7 +1179,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(); @@ -1190,9 +1189,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) { @@ -1201,6 +1198,8 @@ public class AccountManagerService } } + public abstract void run() throws RemoteException; + public void onTimedOut() { IAccountManagerResponse response = getResponseAndClose(); if (response != null) { @@ -1270,6 +1269,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 { @@ -1278,9 +1310,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; @@ -1420,16 +1449,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 (" @@ -1573,6 +1644,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 @@ -1583,7 +1655,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 e3ed2e9..f4b7258 100644 --- a/core/java/android/accounts/GrantCredentialsPermissionActivity.java +++ b/core/java/android/accounts/GrantCredentialsPermissionActivity.java @@ -18,14 +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; /** @@ -43,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 (authTokenLabel.length() == 0) { - 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)); + } + + ((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); } - ((ListView) findViewById(R.id.packages_list)).setAdapter( - new PackagesArrayAdapter(this, packageLabels)); + } + + 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; } @@ -110,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/ApplicationContext.java b/core/java/android/app/ApplicationContext.java index f48f150..1e04abf 100644 --- a/core/java/android/app/ApplicationContext.java +++ b/core/java/android/app/ApplicationContext.java @@ -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(); @@ -896,6 +900,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 +1051,7 @@ class ApplicationContext extends Context { } return mVibrator; } - + private AudioManager getAudioManager() { if (mAudioManager == null) { @@ -1054,6 +1060,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) { diff --git a/core/java/android/bluetooth/BluetoothAdapter.java b/core/java/android/bluetooth/BluetoothAdapter.java index bd5b07c..d88a214 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/AbstractSyncableContentProvider.java b/core/java/android/content/AbstractSyncableContentProvider.java index 3716274..5903c83 100644 --- a/core/java/android/content/AbstractSyncableContentProvider.java +++ b/core/java/android/content/AbstractSyncableContentProvider.java @@ -751,4 +751,8 @@ public abstract class AbstractSyncableContentProvider extends SyncableContentPro public void writeSyncDataBytes(Account account, byte[] data) { mSyncState.writeSyncDataBytes(mOpenHelper.getWritableDatabase(), account, data); } + + protected ContentProvider getSyncStateProvider() { + return mSyncState.asContentProvider(); + } } diff --git a/core/java/android/content/AbstractThreadedSyncAdapter.java b/core/java/android/content/AbstractThreadedSyncAdapter.java index fb6091a..0db6155 100644 --- a/core/java/android/content/AbstractThreadedSyncAdapter.java +++ b/core/java/android/content/AbstractThreadedSyncAdapter.java @@ -21,6 +21,7 @@ import android.os.Bundle; import android.os.Process; import android.os.NetStat; import android.os.IBinder; +import android.os.RemoteException; import android.util.EventLog; import java.util.concurrent.atomic.AtomicInteger; @@ -117,6 +118,12 @@ public abstract class AbstractThreadedSyncAdapter { } } } + + 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); + } } /** diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 8f1c671..d77a6ca 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -1309,7 +1309,7 @@ public abstract class Context { * @see #getSystemService */ public static final String APPWIDGET_SERVICE = "appwidget"; - + /** * Use with {@link #getSystemService} to retrieve an * {@blink android.backup.IBackupManager IBackupManager} for communicating @@ -1319,7 +1319,15 @@ public abstract class Context { * @see #getSystemService */ public static final String BACKUP_SERVICE = "backup"; - + + /** + * Use with {@link #getSystemService} to retrieve a + * {@blink android.os.DropBox DropBox} 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/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/SyncAdapter.java b/core/java/android/content/SyncAdapter.java index 88dc332..af1634e 100644 --- a/core/java/android/content/SyncAdapter.java +++ b/core/java/android/content/SyncAdapter.java @@ -38,6 +38,12 @@ public abstract class SyncAdapter { public void cancelSync(ISyncContext syncContext) throws RemoteException { SyncAdapter.this.cancelSync(); } + + 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); + } } Transport mTransport = new Transport(); 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 ba18615..1580c66 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); + } + + 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)); } } } 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/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/os/DropBoxManager.aidl b/core/java/android/os/DropBoxManager.aidl new file mode 100644 index 0000000..6474ec2 --- /dev/null +++ b/core/java/android/os/DropBoxManager.aidl @@ -0,0 +1,19 @@ +/* + * 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; + +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..b374043 --- /dev/null +++ b/core/java/android/os/DropBoxManager.java @@ -0,0 +1,274 @@ +/* + * 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 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 data read from a file descriptor. The data may be ignored or + * discarded as with {@link #addText}. You must close your + * ParcelFileDescriptor object after calling this method! + * + * @param tag describing the type of entry being stored + * @param fd file descriptor to read from + * @param flags describing the data + */ + public void addFile(String tag, ParcelFileDescriptor fd, int flags) { + if (fd == null) throw new NullPointerException(); + try { mService.add(new Entry(tag, 0, fd, flags)); } catch (RemoteException e) {} + } + + /** + * 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/pim/vcard/Constants.java b/core/java/android/pim/vcard/Constants.java index ca41ce5..1e2ccdf 100644 --- a/core/java/android/pim/vcard/Constants.java +++ b/core/java/android/pim/vcard/Constants.java @@ -16,16 +16,47 @@ package android.pim.vcard; /** - * Constants used in both composer and parser. + * Constants used in both exporter and importer code. */ /* 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"; + + // 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"; - // Properties both the current (as of 2009-08-17) ContactsStruct and de-fact vCard extensions + // 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"; @@ -34,12 +65,24 @@ package android.pim.vcard; 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"; - // 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"; - + + // 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"; + // How more than one TYPE fields are expressed is different between vCard 2.1 and vCard 3.0 // // e.g. @@ -52,42 +95,69 @@ package android.pim.vcard; // // 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 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"; - public static final String ATTR_TYPE_PREF = "PREF"; + // 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 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 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. // 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 + 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 ATTR_TYPE_X_IRMC_N = "X-IRMC-N"; + 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"; + } + + // TODO: Should be in ContactsContract? + /* package */ static final int MAX_DATA_COLUMN = 15; private Constants() { } diff --git a/core/java/android/pim/vcard/ContactStruct.java b/core/java/android/pim/vcard/ContactStruct.java index 36e5e23..530d5ad 100644 --- a/core/java/android/pim/vcard/ContactStruct.java +++ b/core/java/android/pim/vcard/ContactStruct.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; @@ -46,7 +48,6 @@ import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; -import java.util.Iterator; import java.util.List; import java.util.Map; @@ -55,11 +56,11 @@ import java.util.Map; */ 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} 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); @@ -68,12 +69,10 @@ public class ContactStruct { 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(Constants.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 +89,7 @@ public class ContactStruct { @Override public boolean equals(Object obj) { - if (obj instanceof PhoneData) { + if (!(obj instanceof PhoneData)) { return false; } PhoneData phoneData = (PhoneData)obj; @@ -125,7 +124,7 @@ public class ContactStruct { @Override public boolean equals(Object obj) { - if (obj instanceof EmailData) { + if (!(obj instanceof EmailData)) { return false; } EmailData emailData = (EmailData)obj; @@ -202,7 +201,7 @@ public class ContactStruct { @Override public boolean equals(Object obj) { - if (obj instanceof PostalData) { + if (!(obj instanceof PostalData)) { return false; } PostalData postalData = (PostalData)obj; @@ -251,87 +250,117 @@ public class ContactStruct { } } - /** - * @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; + // non-final is Intended: 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; // isPrimary is changable only when there's no appropriate one existing in // the original VCard. 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(int protocol, String customProtocol, int type, + String data, 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 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); } } @@ -342,10 +371,6 @@ public class ContactStruct { private List<String> mPropertyValueList = new ArrayList<String>(); private byte[] mPropertyBytes; - public Property() { - clear(); - } - public void setPropertyName(final String propertyName) { mPropertyName = propertyName; } @@ -385,6 +410,7 @@ public class ContactStruct { mPropertyName = null; mParameterMap.clear(); mPropertyValueList.clear(); + mPropertyBytes = null; } } @@ -417,21 +443,13 @@ 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); + this(VCardConfig.VCARD_TYPE_V21_GENERIC_UTF8); } public ContactStruct(int vcardType) { @@ -444,186 +462,6 @@ public class ContactStruct { } /** - * @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 @@ -635,18 +473,24 @@ public class ContactStruct { } 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 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 +510,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 static final List<String> sEmptyList = new ArrayList<String>(0); - private void addIm(int type, String data, String label, boolean isPrimary) { + /** + * Set "ORG" related values to the appropriate data. If there's more than one + * 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 + * OrganizationData object, a new 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); + } + + private final static int DEFAULT_ORGANIZATION_TYPE = Organization.TYPE_WORK; + + /** + * 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 +635,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) @@ -755,7 +668,7 @@ public class ContactStruct { mFamilyName = elems.get(0); } } - + /** * Some Japanese mobile phones use this field for phonetic name, * since vCard 2.1 does not have "SORT-STRING" type. @@ -764,16 +677,53 @@ public class ContactStruct { */ @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: @@ -796,28 +746,36 @@ public class ContactStruct { } final String propValue = listToString(propValueList).trim(); - if (propName.equals("VERSION")) { + if (propName.equals(Constants.PROPERTY_VERSION)) { // vCard version. Ignore this. - } else if (propName.equals("FN")) { + } else if (propName.equals(Constants.PROPERTY_FN)) { mFullName = propValue; - } else if (propName.equals("NAME") && mFullName == null) { + } else if (propName.equals(Constants.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(Constants.PROPERTY_N)) { handleNProperty(propValueList); - } else if (propName.equals("SORT-STRING")) { + } else if (propName.equals(Constants.PROPERTY_SORT_STRING)) { mPhoneticFullName = propValue; - } else if (propName.equals("NICKNAME") || propName.equals("X-NICKNAME")) { + } else if (propName.equals(Constants.PROPERTY_NICKNAME) || + propName.equals(Constants.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(Constants.PROPERTY_SOUND)) { + Collection<String> typeCollection = paramMap.get(Constants.PARAM_TYPE); + if (typeCollection != null && typeCollection.contains(Constants.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(Constants.PROPERTY_ADR)) { boolean valuesAreAllEmpty = true; for (String value : propValueList) { if (value.length() > 0) { @@ -832,27 +790,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(Constants.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(Constants.PARAM_TYPE_PREF)) { isPrimary = true; - } else if (typeString.equals(Constants.ATTR_TYPE_HOME)) { + } else if (typeString.equals(Constants.PARAM_TYPE_HOME)) { type = StructuredPostal.TYPE_HOME; label = ""; - } else if (typeString.equals(Constants.ATTR_TYPE_WORK) || - typeString.equalsIgnoreCase("COMPANY")) { + } else if (typeString.equals(Constants.PARAM_TYPE_WORK) || + typeString.equalsIgnoreCase(Constants.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(Constants.PARAM_ADR_TYPE_PARCEL) || + typeString.equals(Constants.PARAM_ADR_TYPE_DOM) || + typeString.equals(Constants.PARAM_ADR_TYPE_INTL)) { // We do not have any appropriate way to store this information. } else { if (typeString.startsWith("X-") && type < 0) { @@ -871,23 +827,21 @@ public class ContactStruct { } addPostal(type, propValueList, label, isPrimary); - } else if (propName.equals("EMAIL")) { + } else if (propName.equals(Constants.PROPERTY_EMAIL)) { int type = -1; String label = null; boolean isPrimary = false; - Collection<String> typeCollection = paramMap.get(Constants.ATTR_TYPE); + Collection<String> typeCollection = paramMap.get(Constants.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(Constants.PARAM_TYPE_PREF)) { isPrimary = true; - } else if (typeString.equals(Constants.ATTR_TYPE_HOME)) { + } else if (typeString.equals(Constants.PARAM_TYPE_HOME)) { type = Email.TYPE_HOME; - } else if (typeString.equals(Constants.ATTR_TYPE_WORK)) { + } else if (typeString.equals(Constants.PARAM_TYPE_WORK)) { type = Email.TYPE_WORK; - } else if (typeString.equals(Constants.ATTR_TYPE_CELL)) { + } else if (typeString.equals(Constants.PARAM_TYPE_CELL)) { type = Email.TYPE_MOBILE; } else { if (typeString.startsWith("X-") && type < 0) { @@ -905,50 +859,48 @@ public class ContactStruct { type = Email.TYPE_OTHER; } addEmail(type, propValue, label, isPrimary); - } else if (propName.equals("ORG")) { + } else if (propName.equals(Constants.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(Constants.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(Constants.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(Constants.PROPERTY_TITLE)) { + handleTitleValue(propValue); + } else if (propName.equals(Constants.PROPERTY_ROLE)) { + // This conflicts with TITLE. Ignore for now... + // handleTitleValue(propValue); + } else if (propName.equals(Constants.PROPERTY_PHOTO) || + propName.equals(Constants.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 (Constants.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(Constants.PROPERTY_TEL)) { + final Collection<String> typeCollection = paramMap.get(Constants.PARAM_TYPE); + final Object typeObject = + VCardUtils.getPhoneTypeFromStrings(typeCollection, propValue); final int type; final String label; if (typeObject instanceof Integer) { @@ -960,9 +912,7 @@ 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(Constants.PARAM_TYPE_PREF)) { isPrimary = true; } else { isPrimary = false; @@ -970,53 +920,59 @@ public class ContactStruct { addPhone(type, propValue, label, isPrimary); } else if (propName.equals(Constants.PROPERTY_X_SKYPE_PSTNNUMBER)) { // The phone number available via Skype. - Collection<String> typeCollection = paramMap.get(Constants.ATTR_TYPE); + Collection<String> typeCollection = paramMap.get(Constants.PARAM_TYPE); // XXX: should use TYPE_CUSTOM + the label "Skype"? (which may need localization) int type = Phone.TYPE_OTHER; final String label = null; final boolean isPrimary; - if (!mPrefIsSet_Phone && typeCollection != null && - typeCollection.contains(Constants.ATTR_TYPE_PREF)) { - mPrefIsSet_Phone = true; + if (typeCollection != null && typeCollection.contains(Constants.PARAM_TYPE_PREF)) { isPrimary = true; } else { isPrimary = false; } addPhone(type, propValue, label, isPrimary); - } else if (sImMap.containsKey(propName)){ - int type = sImMap.get(propName); + } 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(Constants.PARAM_TYPE); if (typeCollection != null) { for (String typeString : typeCollection) { - if (typeString.equals(Constants.ATTR_TYPE_PREF)) { + if (typeString.equals(Constants.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(Constants.PARAM_TYPE_HOME)) { + type = Im.TYPE_HOME; + } else if (typeString.equalsIgnoreCase(Constants.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(Constants.PROPERTY_NOTE)) { addNote(propValue); - } else if (propName.equals("URL")) { + } else if (propName.equals(Constants.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(Constants.PROPERTY_BDAY)) { + mBirthday = propValue; + } else if (propName.equals(Constants.PROPERTY_X_PHONETIC_FIRST_NAME)) { mPhoneticGivenName = propValue; - } else if (propName.equals("X-PHONETIC-MIDDLE-NAME")) { + } else if (propName.equals(Constants.PROPERTY_X_PHONETIC_MIDDLE_NAME)) { mPhoneticMiddleName = propValue; - } else if (propName.equals("X-PHONETIC-LAST-NAME")) { + } else if (propName.equals(Constants.PROPERTY_X_PHONETIC_LAST_NAME)) { mPhoneticFamilyName = propValue; - } else if (propName.equals("BDAY")) { - mBirthday = propValue; + } else if (propName.equals(Constants.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 +1000,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,33 +1033,18 @@ 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"; @@ -1161,7 +1082,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 +1093,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 +1130,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 +1190,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 +1206,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 +1231,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 +1246,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 > Constants.MAX_DATA_COLUMN + 1) { + size = Constants.MAX_DATA_COLUMN + 1; + customPropertyList = + customPropertyList.subList(0, Constants.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 +1293,28 @@ public class ContactStruct { } } + public static ContactStruct buildFromResolver(ContentResolver resolver) { + return buildFromResolver(resolver, Contacts.CONTENT_URI); + } + + public static ContactStruct 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 +1337,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/VCardComposer.java b/core/java/android/pim/vcard/VCardComposer.java index f9dce25..b7ad7df 100644 --- a/core/java/android/pim/vcard/VCardComposer.java +++ b/core/java/android/pim/vcard/VCardComposer.java @@ -42,7 +42,6 @@ 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,6 +54,7 @@ import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.UnsupportedEncodingException; import java.io.Writer; +import java.nio.charset.UnsupportedCharsetException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -74,19 +74,38 @@ 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 DEFAULT_EMAIL_TYPE = Constants.ATTR_TYPE_INTERNET; + // TODO: Should be configurable? + 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,28 +116,84 @@ 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"; + 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 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"; + + /** + * 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 Uri sDataRequestUri; + private static final Map<Integer, String> sImMap; + + /** + * See the comment in {@link VCardConfig#FLAG_REFRAIN_QP_TO_PRIMARY_PROPERTIES}. + */ + private static final Set<String> sPrimaryPropertyNameSet; 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, 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. + + // TODO: incomplete. Implement properly + sPrimaryPropertyNameSet = new HashSet<String>(); + sPrimaryPropertyNameSet.add(Constants.PROPERTY_N); + sPrimaryPropertyNameSet.add(Constants.PROPERTY_FN); + sPrimaryPropertyNameSet.add(Constants.PROPERTY_SOUND); } 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()}. @@ -213,65 +288,6 @@ 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; @@ -288,34 +304,21 @@ public class VCardComposer { private final boolean mUsesDefactProperty; private final boolean mUsesUtf8; private final boolean mUsesShiftJis; - private final boolean mUsesQPToPrimaryProperties; + private final boolean mAppendTypeParamName; + private final boolean mRefrainsQPToPrimaryProperties; + private final boolean mNeedsToConvertPhoneticString; private Cursor mCursor; private int mIdColumn; private final String mCharsetString; - private final String mVCardAttributeCharset; + private final String mVCardCharsetParameter; 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,108 +339,98 @@ 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(Context context, int vcardType, 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); + 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); + mRefrainsQPToPrimaryProperties = VCardConfig.refrainsQPToPrimaryProperties(vcardType); + mAppendTypeParamName = VCardConfig.appendTypeParamName(vcardType); + mNeedsToConvertPhoneticString = VCardConfig.needsToConvertPhoneticString(vcardType); mHandlerList = new ArrayList<OneEntryHandler>(); if (mIsDoCoMo) { - mCharsetString = CharsetUtils.charsetForVendor(SHIFT_JIS, "docomo").name(); + 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). - mVCardAttributeCharset = "CHARSET=" + SHIFT_JIS; + mVCardCharsetParameter = "CHARSET=" + SHIFT_JIS; } 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; + mVCardCharsetParameter = "CHARSET=" + SHIFT_JIS; } else { - mCharsetString = "UTF-8"; - mVCardAttributeCharset = "CHARSET=UTF-8"; + mCharsetString = UTF_8; + mVCardCharsetParameter = "CHARSET=" + UTF_8; } } /** - * This static function is to compose vCard for phone own number + * Must call 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; + public void addHandler(OneEntryHandler handler) { + if (handler != null) { + mHandlerList.add(handler); } - // 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); - - String label = Integer.toString(phonetype); - appendVCardTelephoneLine(builder, phonetype, label, phoneNumber); - - 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()); @@ -456,13 +449,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; @@ -539,89 +538,6 @@ 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) { final Map<String, List<ContentValues>> contentValuesListMap = new HashMap<String, List<ContentValues>>(); @@ -638,8 +554,7 @@ public class VCardComposer { 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) { @@ -668,11 +583,11 @@ public class VCardComposer { } final StringBuilder builder = new StringBuilder(); - appendVCardLine(builder, VCARD_PROPERTY_BEGIN, VCARD_DATA_VCARD); + appendVCardLine(builder, Constants.PROPERTY_BEGIN, VCARD_DATA_VCARD); if (mIsV30) { - appendVCardLine(builder, VCARD_PROPERTY_VERSION, Constants.VERSION_V30); + appendVCardLine(builder, Constants.PROPERTY_VERSION, Constants.VERSION_V30); } else { - appendVCardLine(builder, VCARD_PROPERTY_VERSION, Constants.VERSION_V21); + appendVCardLine(builder, Constants.PROPERTY_VERSION, Constants.VERSION_V21); } appendStructuredNames(builder, contentValuesListMap); @@ -684,20 +599,18 @@ public class VCardComposer { appendWebsites(builder, contentValuesListMap); appendBirthday(builder, contentValuesListMap); appendOrganizations(builder, contentValuesListMap); - if (mNeedPhotoForVCard) { - appendPhotos(builder, contentValuesListMap); - } + appendPhotos(builder, contentValuesListMap); appendNotes(builder, contentValuesListMap); - // TODO: GroupMembership + // TODO: GroupMembership, Relation, Event other than birthday. 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, Constants.PROPERTY_X_CLASS, VCARD_DATA_PUBLIC); + appendVCardLine(builder, Constants.PROPERTY_X_REDUCTION, ""); + appendVCardLine(builder, Constants.PROPERTY_X_NO, ""); + appendVCardLine(builder, Constants.PROPERTY_X_DCM_HMN_MODE, ""); } - appendVCardLine(builder, VCARD_PROPERTY_END, VCARD_DATA_VCARD); + appendVCardLine(builder, Constants.PROPERTY_END, VCARD_DATA_VCARD); return builder.toString(); } @@ -755,11 +668,11 @@ public class VCardComposer { if (contentValuesList != null && contentValuesList.size() > 0) { appendStructuredNamesInternal(builder, contentValuesList); } else if (mIsDoCoMo) { - appendVCardLine(builder, VCARD_PROPERTY_NAME, ""); + appendVCardLine(builder, Constants.PROPERTY_N, ""); } else if (mIsV30) { // vCard 3.0 requires "N" and "FN" properties. - appendVCardLine(builder, VCARD_PROPERTY_NAME, ""); - appendVCardLine(builder, VCARD_PROPERTY_FULL_NAME, ""); + appendVCardLine(builder, Constants.PROPERTY_N, ""); + appendVCardLine(builder, Constants.PROPERTY_FN, ""); } } @@ -769,10 +682,18 @@ public class VCardComposer { 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(displayName)); + TextUtils.isEmpty(suffix) && TextUtils.isEmpty(phoneticFamilyName) && + TextUtils.isEmpty(phoneticMiddleName) && TextUtils.isEmpty(phoneticGivenName) && + TextUtils.isEmpty(displayName)); } private void appendStructuredNamesInternal(final StringBuilder builder, @@ -817,34 +738,46 @@ public class VCardComposer { } } - 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); + 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 boolean shouldAppendCharsetParameterToName = + !(mIsV30 && UTF_8.equalsIgnoreCase(mCharsetString)) && + shouldAppendCharsetParameters(Arrays.asList( + familyName, givenName, middleName, prefix, suffix)); + final boolean reallyUseQuotedPrintableToName = + (!mRefrainsQPToPrimaryProperties && + !(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 shouldAppendCharsetParameterToFN = + !(mIsV30 && UTF_8.equalsIgnoreCase(mCharsetString)) && + shouldAppendCharsetParameter(formattedName); + final boolean reallyUseQuotedPrintableToFN = + !mRefrainsQPToPrimaryProperties && + !VCardUtils.containsOnlyNonCrLfPrintableAscii(formattedName); + 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); @@ -859,72 +792,79 @@ public class VCardComposer { 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 encodedFormattedname = + (reallyUseQuotedPrintableToFN ? + encodeQuotedPrintable(formattedName) : escapeCharacters(formattedName)); - 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); + builder.append(Constants.PROPERTY_N); + if (mIsDoCoMo) { + if (shouldAppendCharsetParameterToName) { + builder.append(VCARD_PARAM_SEPARATOR); + builder.append(mVCardCharsetParameter); + } + if (reallyUseQuotedPrintableToName) { + builder.append(VCARD_PARAM_SEPARATOR); + builder.append(VCARD_PARAM_ENCODING_QP); + } + builder.append(VCARD_DATA_SEPARATOR); + // DoCoMo phones require that all the elements in the "family name" field. + builder.append(formattedName); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(VCARD_ITEM_SEPARATOR); + builder.append(VCARD_ITEM_SEPARATOR); + } else { + if (shouldAppendCharsetParameterToName) { + builder.append(VCARD_PARAM_SEPARATOR); + builder.append(mVCardCharsetParameter); + } + if (reallyUseQuotedPrintableToName) { + builder.append(VCARD_PARAM_SEPARATOR); + builder.append(VCARD_PARAM_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_END_OF_LINE); // FN property - builder.append(VCARD_PROPERTY_FULL_NAME); - if (shouldAppendCharsetAttribute(encodedFullname)) { - builder.append(VCARD_ATTR_SEPARATOR); - builder.append(mVCardAttributeCharset); + builder.append(Constants.PROPERTY_FN); + if (shouldAppendCharsetParameterToFN) { + builder.append(VCARD_PARAM_SEPARATOR); + builder.append(mVCardCharsetParameter); } - if (reallyUseQuotedPrintableToFullname) { - builder.append(VCARD_ATTR_SEPARATOR); - builder.append(VCARD_ATTR_ENCODING_QP); + if (reallyUseQuotedPrintableToFN) { + builder.append(VCARD_PARAM_SEPARATOR); + builder.append(VCARD_PARAM_ENCODING_QP); } builder.append(VCARD_DATA_SEPARATOR); - builder.append(encodedFullname); - builder.append(VCARD_COL_SEPARATOR); + builder.append(encodedFormattedname); + builder.append(VCARD_END_OF_LINE); } else if (!TextUtils.isEmpty(displayName)) { final boolean reallyUseQuotedPrintableToDisplayName = - (mUsesQPToPrimaryProperties && + (!mRefrainsQPToPrimaryProperties && !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); + builder.append(Constants.PROPERTY_N); + if (shouldAppendCharsetParameter(displayName)) { + builder.append(VCARD_PARAM_SEPARATOR); + builder.append(mVCardCharsetParameter); } if (reallyUseQuotedPrintableToDisplayName) { - builder.append(VCARD_ATTR_SEPARATOR); - builder.append(VCARD_ATTR_ENCODING_QP); + builder.append(VCARD_PARAM_SEPARATOR); + builder.append(VCARD_PARAM_ENCODING_QP); } builder.append(VCARD_DATA_SEPARATOR); builder.append(encodedDisplayName); @@ -932,68 +872,83 @@ public class VCardComposer { 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, ""); + builder.append(VCARD_END_OF_LINE); + builder.append(Constants.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 (shouldAppendCharsetParameter(displayName)) { + builder.append(VCARD_PARAM_SEPARATOR); + builder.append(mVCardCharsetParameter); + } + builder.append(VCARD_DATA_SEPARATOR); + builder.append(encodedDisplayName); + builder.append(VCARD_END_OF_LINE); } else if (mIsV30) { - appendVCardLine(builder, VCARD_PROPERTY_NAME, ""); - appendVCardLine(builder, VCARD_PROPERTY_FULL_NAME, ""); + // vCard 3.0 specification requires these fields. + appendVCardLine(builder, Constants.PROPERTY_N, ""); + appendVCardLine(builder, Constants.PROPERTY_FN, ""); + } else if (mIsDoCoMo) { + appendVCardLine(builder, Constants.PROPERTY_N, ""); } - 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); + final String phoneticFamilyName; + final String phoneticMiddleName; + final String phoneticGivenName; + { + String tmpPhoneticFamilyName = + primaryContentValues.getAsString(StructuredName.PHONETIC_FAMILY_NAME); + String tmpPhoneticMiddleName = + primaryContentValues.getAsString(StructuredName.PHONETIC_MIDDLE_NAME); + String tmpPhoneticGivenName = + primaryContentValues.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 (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); + phoneticFamilyName, phoneticMiddleName, phoneticGivenName); + builder.append(Constants.PROPERTY_SORT_STRING); + if (shouldAppendCharsetParameter(sortString)) { + builder.append(VCARD_PARAM_SEPARATOR); + builder.append(mVCardCharsetParameter); } builder.append(VCARD_DATA_SEPARATOR); - builder.append(encodedSortString); - builder.append(VCARD_COL_SEPARATOR); - } else { + builder.append(escapeCharacters(sortString)); + builder.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 since it is supported by + // 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. - builder.append(VCARD_PROPERTY_SOUND); - builder.append(VCARD_ATTR_SEPARATOR); - builder.append(Constants.ATTR_TYPE_X_IRMC_N); + builder.append(Constants.PROPERTY_SOUND); + builder.append(VCARD_PARAM_SEPARATOR); + builder.append(Constants.PARAM_TYPE_X_IRMC_N); boolean reallyUseQuotedPrintable = - (mUsesQPToPrimaryProperties && - !(VCardUtils.containsOnlyNonCrLfPrintableAscii( - phoneticFamilyName) && - VCardUtils.containsOnlyNonCrLfPrintableAscii( - phoneticMiddleName) && - VCardUtils.containsOnlyNonCrLfPrintableAscii( - phoneticGivenName))); + (!mRefrainsQPToPrimaryProperties + && !(VCardUtils.containsOnlyNonCrLfPrintableAscii( + phoneticFamilyName) + && VCardUtils.containsOnlyNonCrLfPrintableAscii( + phoneticMiddleName) + && VCardUtils.containsOnlyNonCrLfPrintableAscii( + phoneticGivenName))); final String encodedPhoneticFamilyName; final String encodedPhoneticMiddleName; @@ -1008,38 +963,58 @@ public class VCardComposer { encodedPhoneticGivenName = escapeCharacters(phoneticGivenName); } - if (shouldAppendCharsetAttribute(Arrays.asList( + if (shouldAppendCharsetParameters(Arrays.asList( encodedPhoneticFamilyName, encodedPhoneticMiddleName, encodedPhoneticGivenName))) { - builder.append(VCARD_ATTR_SEPARATOR); - builder.append(mVCardAttributeCharset); + builder.append(VCARD_PARAM_SEPARATOR); + builder.append(mVCardCharsetParameter); } builder.append(VCARD_DATA_SEPARATOR); - builder.append(encodedPhoneticFamilyName); + // DoCoMo's specification requires vCard composer to use just the first + // column. + { + boolean first = true; + if (!TextUtils.isEmpty(encodedPhoneticFamilyName)) { + builder.append(encodedPhoneticFamilyName); + first = false; + } + if (!TextUtils.isEmpty(encodedPhoneticMiddleName)) { + if (first) { + first = false; + } else { + builder.append(' '); + } + builder.append(encodedPhoneticMiddleName); + } + if (!TextUtils.isEmpty(encodedPhoneticGivenName)) { + if (!first) { + builder.append(' '); + } + builder.append(encodedPhoneticGivenName); + } + } 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); + builder.append(VCARD_END_OF_LINE); } } else if (mIsDoCoMo) { - builder.append(VCARD_PROPERTY_SOUND); - builder.append(VCARD_ATTR_SEPARATOR); - builder.append(Constants.ATTR_TYPE_X_IRMC_N); + builder.append(Constants.PROPERTY_SOUND); + builder.append(VCARD_PARAM_SEPARATOR); + builder.append(Constants.PARAM_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); + builder.append(VCARD_END_OF_LINE); } if (mUsesDefactProperty) { if (!TextUtils.isEmpty(phoneticGivenName)) { final boolean reallyUseQuotedPrintable = - (mUsesQPToPrimaryProperties && + (mUsesQuotedPrintable && !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticGivenName)); final String encodedPhoneticGivenName; if (reallyUseQuotedPrintable) { @@ -1047,22 +1022,22 @@ public class VCardComposer { } else { encodedPhoneticGivenName = escapeCharacters(phoneticGivenName); } - builder.append(VCARD_PROPERTY_X_PHONETIC_FIRST_NAME); - if (shouldAppendCharsetAttribute(encodedPhoneticGivenName)) { - builder.append(VCARD_ATTR_SEPARATOR); - builder.append(mVCardAttributeCharset); + builder.append(Constants.PROPERTY_X_PHONETIC_FIRST_NAME); + if (shouldAppendCharsetParameter(phoneticGivenName)) { + builder.append(VCARD_PARAM_SEPARATOR); + builder.append(mVCardCharsetParameter); } if (reallyUseQuotedPrintable) { - builder.append(VCARD_ATTR_SEPARATOR); - builder.append(VCARD_ATTR_ENCODING_QP); + builder.append(VCARD_PARAM_SEPARATOR); + builder.append(VCARD_PARAM_ENCODING_QP); } builder.append(VCARD_DATA_SEPARATOR); builder.append(encodedPhoneticGivenName); - builder.append(VCARD_COL_SEPARATOR); + builder.append(VCARD_END_OF_LINE); } if (!TextUtils.isEmpty(phoneticMiddleName)) { final boolean reallyUseQuotedPrintable = - (mUsesQPToPrimaryProperties && + (mUsesQuotedPrintable && !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticMiddleName)); final String encodedPhoneticMiddleName; if (reallyUseQuotedPrintable) { @@ -1070,22 +1045,22 @@ public class VCardComposer { } else { encodedPhoneticMiddleName = escapeCharacters(phoneticMiddleName); } - builder.append(VCARD_PROPERTY_X_PHONETIC_MIDDLE_NAME); - if (shouldAppendCharsetAttribute(encodedPhoneticMiddleName)) { - builder.append(VCARD_ATTR_SEPARATOR); - builder.append(mVCardAttributeCharset); + builder.append(Constants.PROPERTY_X_PHONETIC_MIDDLE_NAME); + if (shouldAppendCharsetParameter(phoneticMiddleName)) { + builder.append(VCARD_PARAM_SEPARATOR); + builder.append(mVCardCharsetParameter); } if (reallyUseQuotedPrintable) { - builder.append(VCARD_ATTR_SEPARATOR); - builder.append(VCARD_ATTR_ENCODING_QP); + builder.append(VCARD_PARAM_SEPARATOR); + builder.append(VCARD_PARAM_ENCODING_QP); } builder.append(VCARD_DATA_SEPARATOR); builder.append(encodedPhoneticMiddleName); - builder.append(VCARD_COL_SEPARATOR); + builder.append(VCARD_END_OF_LINE); } if (!TextUtils.isEmpty(phoneticFamilyName)) { final boolean reallyUseQuotedPrintable = - (mUsesQPToPrimaryProperties && + (mUsesQuotedPrintable && !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticFamilyName)); final String encodedPhoneticFamilyName; if (reallyUseQuotedPrintable) { @@ -1093,18 +1068,18 @@ public class VCardComposer { } else { encodedPhoneticFamilyName = escapeCharacters(phoneticFamilyName); } - builder.append(VCARD_PROPERTY_X_PHONETIC_LAST_NAME); - if (shouldAppendCharsetAttribute(encodedPhoneticFamilyName)) { - builder.append(VCARD_ATTR_SEPARATOR); - builder.append(mVCardAttributeCharset); + builder.append(Constants.PROPERTY_X_PHONETIC_LAST_NAME); + if (shouldAppendCharsetParameter(phoneticFamilyName)) { + builder.append(VCARD_PARAM_SEPARATOR); + builder.append(mVCardCharsetParameter); } if (reallyUseQuotedPrintable) { - builder.append(VCARD_ATTR_SEPARATOR); - builder.append(VCARD_ATTR_ENCODING_QP); + builder.append(VCARD_PARAM_SEPARATOR); + builder.append(VCARD_PARAM_ENCODING_QP); } builder.append(VCARD_DATA_SEPARATOR); builder.append(encodedPhoneticFamilyName); - builder.append(VCARD_COL_SEPARATOR); + builder.append(VCARD_END_OF_LINE); } } } @@ -1113,45 +1088,31 @@ public class VCardComposer { 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; - } + if (contentValuesList == null) { + return; + } - final String encodedNickname; - final boolean reallyUseQuotedPrintable = - (mUsesQuotedPrintable && - !VCardUtils.containsOnlyNonCrLfPrintableAscii(nickname)); - if (reallyUseQuotedPrintable) { - encodedNickname = encodeQuotedPrintable(nickname); - } else { - encodedNickname = escapeCharacters(nickname); - } + final boolean useAndroidProperty; + if (mIsV30) { + useAndroidProperty = false; + } else if (mUsesAndroidProperty) { + useAndroidProperty = true; + } else { + // There's no way to add this field. + return; + } - 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); + for (ContentValues contentValues : contentValuesList) { + final String nickname = contentValues.getAsString(Nickname.NAME); + if (TextUtils.isEmpty(nickname)) { + continue; + } + if (useAndroidProperty) { + appendAndroidSpecificProperty(builder, Nickname.CONTENT_ITEM_TYPE, + contentValues); + } else { + appendVCardLineWithCharsetAndQPDetection(builder, + Constants.PROPERTY_NICKNAME, nickname); } } } @@ -1166,6 +1127,9 @@ public class VCardComposer { 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(); @@ -1173,14 +1137,12 @@ public class VCardComposer { if (TextUtils.isEmpty(phoneNumber)) { continue; } - int type = (typeAsObject != null ? typeAsObject : Phone.TYPE_HOME); - - phoneLineExists = true; + int type = (typeAsObject != null ? typeAsObject : DEFAULT_PHONE_TYPE); if (type == Phone.TYPE_PAGER) { phoneLineExists = true; if (!phoneSet.contains(phoneNumber)) { phoneSet.add(phoneNumber); - appendVCardTelephoneLine(builder, type, label, phoneNumber); + appendVCardTelephoneLine(builder, type, label, phoneNumber, isPrimary); } } else { // The entry "may" have several phone numbers when the contact entry is @@ -1198,12 +1160,11 @@ public class VCardComposer { 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(); + final String formattedPhoneNumber = + PhoneNumberUtils.formatNumber(actualPhoneNumber, format); phoneSet.add(actualPhoneNumber); - appendVCardTelephoneLine(builder, type, label, formattedPhoneNumber); + appendVCardTelephoneLine(builder, type, label, + formattedPhoneNumber, isPrimary); } } } @@ -1211,7 +1172,7 @@ public class VCardComposer { } if (!phoneLineExists && mIsDoCoMo) { - appendVCardTelephoneLine(builder, Phone.TYPE_HOME, "", ""); + appendVCardTelephoneLine(builder, Phone.TYPE_HOME, "", "", false); } } @@ -1240,14 +1201,11 @@ public class VCardComposer { 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>(); + final 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(); @@ -1255,16 +1213,23 @@ public class VCardComposer { 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); - appendVCardEmailLine(builder, type, label, emailAddress); + appendVCardEmailLine(builder, type, label, emailAddress, isPrimary); } } } if (!emailAddressExists && mIsDoCoMo) { - appendVCardEmailLine(builder, Email.TYPE_HOME, "", ""); + appendVCardEmailLine(builder, Email.TYPE_HOME, "", "", false); } } @@ -1279,11 +1244,11 @@ public class VCardComposer { appendPostalsForGeneric(builder, contentValuesList); } } else if (mIsDoCoMo) { - builder.append(VCARD_PROPERTY_ADR); - builder.append(VCARD_ATTR_SEPARATOR); - builder.append(Constants.ATTR_TYPE_HOME); + builder.append(Constants.PROPERTY_ADR); + builder.append(VCARD_PARAM_SEPARATOR); + builder.append(Constants.PARAM_TYPE_HOME); builder.append(VCARD_DATA_SEPARATOR); - builder.append(VCARD_COL_SEPARATOR); + builder.append(VCARD_END_OF_LINE); } } @@ -1321,7 +1286,10 @@ public class VCardComposer { final Integer type = contentValues.getAsInteger(StructuredPostal.TYPE); final String label = contentValues.getAsString(StructuredPostal.LABEL); if (type == preferedType) { - appendVCardPostalLine(builder, type, label, contentValues); + // 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. + // We keep safer side since the previous implementation did so. + appendVCardPostalLine(builder, type, label, contentValues, true, true); return true; } } @@ -1331,11 +1299,18 @@ public class VCardComposer { 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); + if (contentValues == null) { + continue; } + final Integer typeAsObject = contentValues.getAsInteger(StructuredPostal.TYPE); + final int type = (typeAsObject != null ? + typeAsObject : 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); + appendVCardPostalLine(builder, type, label, contentValues, isPrimary, false); } } @@ -1343,24 +1318,63 @@ public class VCardComposer { 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); + if (contentValuesList == null) { + return; + } + 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 = Constants.PARAM_TYPE_HOME; + break; + } + case Im.TYPE_WORK: { + typeAsString = Constants.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; } - // TODO: add "X-GOOGLE TALK" case... } } + + 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(Constants.PARAM_TYPE_PREF); + } + + appendVCardLineWithCharsetAndQPDetection( + builder, propertyName, parameterList, data); } } @@ -1368,39 +1382,84 @@ public class VCardComposer { 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); - } + if (contentValuesList == null) { + return; + } + 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. + // + // TODO: Should we allow adding it when appropriate? + // (Actually, we drop some data. Using "group.X-URL-TYPE" or something + // may help) + if (!TextUtils.isEmpty(website)) { + appendVCardLine(builder, Constants.PROPERTY_URL, website); } } } + /** + * Theoretically, there must be only one birthday for each vCard entry. + * Also, we are afraid of some importer's parse error during its import. + * We emit only one birthday entry even when there are more than one. + */ 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); + final List<ContentValues> contentValuesList = + contentValuesListMap.get(Event.CONTENT_ITEM_TYPE); + if (contentValuesList == null) { + return; + } + String primaryBirthday = null; + String secondaryBirthday = null; + for (ContentValues contentValues : contentValuesList) { + if (contentValues == null) { + continue; + } + final Integer eventType = contentValues.getAsInteger(Event.TYPE); if (eventType == null || !eventType.equals(Event.TYPE_BIRTHDAY)) { - return; + continue; } - // 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(); + 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; } - if (!TextUtils.isEmpty(birthday)) { - appendVCardLine(builder, VCARD_PROPERTY_BIRTHDAY, birthday); + 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; } } + + final String birthday; + if (primaryBirthday != null) { + birthday = primaryBirthday.trim(); + } else if (secondaryBirthday != null){ + birthday = secondaryBirthday.trim(); + } else { + return; + } + appendVCardLineWithCharsetAndQPDetection(builder, Constants.PROPERTY_BDAY, birthday); } private void appendOrganizations(final StringBuilder builder, @@ -1409,25 +1468,37 @@ public class VCardComposer { .get(Organization.CONTENT_ITEM_TYPE); if (contentValuesList != null) { for (ContentValues contentValues : contentValuesList) { - String company = contentValues - .getAsString(Organization.COMPANY); + String company = contentValues.getAsString(Organization.COMPANY); if (company != null) { company = company.trim(); } - String title = contentValues - .getAsString(Organization.TITLE); + 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)) { - appendVCardLine(builder, VCARD_PROPERTY_ORG, company, - !VCardUtils.containsOnlyPrintableAscii(company), - (mUsesQuotedPrintable && - !VCardUtils.containsOnlyNonCrLfPrintableAscii(company))); + orgBuilder.append(company); } + if (!TextUtils.isEmpty(department)) { + if (orgBuilder.length() > 0) { + orgBuilder.append(';'); + } + orgBuilder.append(department); + } + final String orgline = orgBuilder.toString(); + appendVCardLine(builder, Constants.PROPERTY_ORG, orgline, + !VCardUtils.containsOnlyPrintableAscii(orgline), + (mUsesQuotedPrintable && + !VCardUtils.containsOnlyNonCrLfPrintableAscii(orgline))); + if (!TextUtils.isEmpty(title)) { - appendVCardLine(builder, VCARD_PROPERTY_TITLE, title, + appendVCardLine(builder, Constants.PROPERTY_TITLE, title, !VCardUtils.containsOnlyPrintableAscii(title), (mUsesQuotedPrintable && !VCardUtils.containsOnlyNonCrLfPrintableAscii(title))); @@ -1454,11 +1525,9 @@ public class VCardComposer { 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... + // 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) { @@ -1505,7 +1574,7 @@ public class VCardComposer { final boolean reallyUseQuotedPrintable = (mUsesQuotedPrintable && !VCardUtils.containsOnlyNonCrLfPrintableAscii(noteStr)); - appendVCardLine(builder, VCARD_PROPERTY_NOTE, noteStr, + appendVCardLine(builder, Constants.PROPERTY_NOTE, noteStr, shouldAppendCharsetInfo, reallyUseQuotedPrintable); } else { for (ContentValues contentValues : contentValuesList) { @@ -1516,7 +1585,7 @@ public class VCardComposer { final boolean reallyUseQuotedPrintable = (mUsesQuotedPrintable && !VCardUtils.containsOnlyNonCrLfPrintableAscii(noteStr)); - appendVCardLine(builder, VCARD_PROPERTY_NOTE, noteStr, + appendVCardLine(builder, Constants.PROPERTY_NOTE, noteStr, shouldAppendCharsetInfo, reallyUseQuotedPrintable); } } @@ -1524,6 +1593,33 @@ public class VCardComposer { } } + private void appendAndroidSpecificProperty(final StringBuilder builder, + final String mimeType, ContentValues contentValues) { + List<String> rawDataList = new ArrayList<String>(); + rawDataList.add(mimeType); + final List<String> columnNameList; + if (Nickname.CONTENT_ITEM_TYPE.equals(mimeType)) { + + } else { + // If you add the other field, please check all the columns are able to be + // converted to String. + // + // e.g. BLOB is not what we can handle here now. + return; + } + + for (int i = 1; i <= Constants.MAX_DATA_COLUMN; i++) { + String value = contentValues.getAsString("data" + i); + if (value == null) { + value = ""; + } + rawDataList.add(value); + } + + appendVCardLineWithCharsetAndQPDetection(builder, + Constants.PROPERTY_X_ANDROID_CUSTOM, rawDataList); + } + /** * 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. @@ -1539,7 +1635,7 @@ public class VCardComposer { final StringBuilder tmpBuilder = new StringBuilder(); final int length = unescaped.length(); for (int i = 0; i < length; i++) { - char ch = unescaped.charAt(i); + final char ch = unescaped.charAt(i); switch (ch) { case ';': { tmpBuilder.append('\\'); @@ -1550,7 +1646,7 @@ public class VCardComposer { if (i + 1 < length) { char nextChar = unescaped.charAt(i); if (nextChar == '\n') { - continue; + break; } else { // fall through } @@ -1602,15 +1698,15 @@ public class VCardComposer { 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); + tmpBuilder.append(Constants.PROPERTY_PHOTO); + tmpBuilder.append(VCARD_PARAM_SEPARATOR); if (mIsV30) { - tmpBuilder.append(VCARD_ATTR_ENCODING_BASE64_V30); + tmpBuilder.append(VCARD_PARAM_ENCODING_BASE64_V30); } else { - tmpBuilder.append(VCARD_ATTR_ENCODING_BASE64_V21); + tmpBuilder.append(VCARD_PARAM_ENCODING_BASE64_V21); } - tmpBuilder.append(VCARD_ATTR_SEPARATOR); - appendTypeAttribute(tmpBuilder, photoType); + tmpBuilder.append(VCARD_PARAM_SEPARATOR); + appendTypeParameter(tmpBuilder, photoType); tmpBuilder.append(VCARD_DATA_SEPARATOR); tmpBuilder.append(encodedData); @@ -1622,69 +1718,136 @@ public class VCardComposer { tmpBuilder.append(tmpStr.charAt(i)); lineCount++; if (lineCount > 72) { - tmpBuilder.append(VCARD_COL_SEPARATOR); + tmpBuilder.append(VCARD_END_OF_LINE); tmpBuilder.append(VCARD_WS); lineCount = 0; } } builder.append(tmpBuilder.toString()); - builder.append(VCARD_COL_SEPARATOR); - builder.append(VCARD_COL_SEPARATOR); + builder.append(VCARD_END_OF_LINE); + builder.append(VCARD_END_OF_LINE); } - 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); + private 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; + } + } - // 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; + /** + * @return null when there's no information available to construct the data. + */ + private PostalStruct tryConstructPostalStruct(ContentValues contentValues) { + boolean reallyUseQuotedPrintable = false; + boolean appendCharset = false; + + boolean dataArrayExists = 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; + dataArrayExists = true; + if (!appendCharset && !VCardUtils.containsOnlyPrintableAscii(data)) { + appendCharset = true; } if (mUsesQuotedPrintable && !VCardUtils.containsOnlyNonCrLfPrintableAscii(data)) { - actuallyUseQuotedPrintable = true; + reallyUseQuotedPrintable = 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); + if (dataArrayExists) { + StringBuffer addressBuffer = new StringBuffer(); + boolean first = true; + for (String data : dataArray) { + if (first) { + first = false; } else { - dataArray[i] = escapeCharacters(data); + addressBuffer.append(VCARD_ITEM_SEPARATOR); + } + if (!TextUtils.isEmpty(data)) { + if (reallyUseQuotedPrintable) { + addressBuffer.append(encodeQuotedPrintable(data)); + } else { + addressBuffer.append(escapeCharacters(data)); + } } } + return new PostalStruct(reallyUseQuotedPrintable, appendCharset, + addressBuffer.toString()); } - final int typeAsPrimitive; - if (typeAsObject == null) { - typeAsPrimitive = StructuredPostal.TYPE_OTHER; - } else { - typeAsPrimitive = typeAsObject; + String formattedAddress = + contentValues.getAsString(StructuredPostal.FORMATTED_ADDRESS); + if (!TextUtils.isEmpty(formattedAddress)) { + reallyUseQuotedPrintable = + !VCardUtils.containsOnlyPrintableAscii(formattedAddress); + appendCharset = + !VCardUtils.containsOnlyNonCrLfPrintableAscii(formattedAddress); + if (reallyUseQuotedPrintable) { + formattedAddress = encodeQuotedPrintable(formattedAddress); + } else { + formattedAddress = escapeCharacters(formattedAddress); + } + // We use the second value ("Extended Address"). + // + // adr-value = 0*6(text-value ";") text-value + // ; PO Box, Extended Address, Street, Locality, Region, Postal + // ; Code, Country Name + StringBuffer addressBuffer = new StringBuffer(); + addressBuffer.append(VCARD_ITEM_SEPARATOR); + addressBuffer.append(formattedAddress); + 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()); + } + return null; // There's no data available. + } + + private void appendVCardPostalLine(final StringBuilder builder, + final int type, final String label, final ContentValues contentValues, + final boolean isPrimary, final boolean emitLineEveryTime) { + final boolean reallyUseQuotedPrintable; + final boolean appendCharset; + final String addressData; + { + PostalStruct postalStruct = tryConstructPostalStruct(contentValues); + if (postalStruct == null) { + if (emitLineEveryTime) { + reallyUseQuotedPrintable = false; + appendCharset = false; + addressData = ""; + } else { + return; + } + } else { + reallyUseQuotedPrintable = postalStruct.reallyUseQuotedPrintable; + appendCharset = postalStruct.appendCharset; + addressData = postalStruct.addressData; + } } - String typeAsString = null; - switch (typeAsPrimitive) { + List<String> parameterList = new ArrayList<String>(); + if (isPrimary) { + parameterList.add(Constants.PARAM_TYPE_PREF); + } + switch (type) { case StructuredPostal.TYPE_HOME: { - typeAsString = Constants.ATTR_TYPE_HOME; + parameterList.add(Constants.PARAM_TYPE_HOME); break; } case StructuredPostal.TYPE_WORK: { - typeAsString = Constants.ATTR_TYPE_WORK; + parameterList.add(Constants.PARAM_TYPE_WORK); break; } case StructuredPostal.TYPE_CUSTOM: { @@ -1694,9 +1857,7 @@ public class VCardComposer { // ("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); + parameterList.add("X-" + label); } break; } @@ -1704,132 +1865,111 @@ public class VCardComposer { break; } default: { - Log.e(LOG_TAG, "Unknown StructuredPostal type: " + typeAsPrimitive); + Log.e(LOG_TAG, "Unknown StructuredPostal type: " + type); break; } } - // Attribute(s). + // Actual data construction starts from here. + // TODO: add a new version of appendVCardLine() for this purpose. + + builder.append(Constants.PROPERTY_ADR); + builder.append(VCARD_PARAM_SEPARATOR); + // Parameters { - 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; + boolean shouldAppendParamSeparator = false; + if (!parameterList.isEmpty()) { + appendTypeParameters(builder, parameterList); + shouldAppendParamSeparator = true; + } + + 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. + if (shouldAppendParamSeparator) { + builder.append(VCARD_PARAM_SEPARATOR); } + builder.append(mVCardCharsetParameter); + shouldAppendParamSeparator = true; + } - if (actuallyUseQuotedPrintable) { - if (shouldAppendAttrSeparator) { - builder.append(VCARD_ATTR_SEPARATOR); - } - builder.append(VCARD_ATTR_ENCODING_QP); - shouldAppendAttrSeparator = true; + if (reallyUseQuotedPrintable) { + if (shouldAppendParamSeparator) { + builder.append(VCARD_PARAM_SEPARATOR); } + builder.append(VCARD_PARAM_ENCODING_QP); + shouldAppendParamSeparator = 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); + builder.append(addressData); + builder.append(VCARD_END_OF_LINE); } 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 int type, final String label, + final String rawData, final boolean isPrimary) { final String typeAsString; - switch (typeAsPrimitive) { + switch (type) { 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; + typeAsString = Constants.PARAM_TYPE_CELL; } else if (mUsesAndroidProperty && !TextUtils.isEmpty(label) && VCardUtils.containsOnlyAlphaDigitHyphen(label)) { typeAsString = "X-" + label; } else { - typeAsString = DEFAULT_EMAIL_TYPE; + typeAsString = null; } break; } case Email.TYPE_HOME: { - typeAsString = Constants.ATTR_TYPE_HOME; + typeAsString = Constants.PARAM_TYPE_HOME; break; } case Email.TYPE_WORK: { - typeAsString = Constants.ATTR_TYPE_WORK; + typeAsString = Constants.PARAM_TYPE_WORK; break; } case Email.TYPE_OTHER: { - typeAsString = DEFAULT_EMAIL_TYPE; + typeAsString = null; break; } case Email.TYPE_MOBILE: { - typeAsString = Constants.ATTR_TYPE_CELL; + typeAsString = Constants.PARAM_TYPE_CELL; break; } default: { - Log.e(LOG_TAG, "Unknown Email type: " + typeAsPrimitive); - typeAsString = DEFAULT_EMAIL_TYPE; + Log.e(LOG_TAG, "Unknown Email type: " + type); + typeAsString = null; break; } } - builder.append(VCARD_ATTR_SEPARATOR); - appendTypeAttribute(builder, typeAsString); - builder.append(VCARD_DATA_SEPARATOR); - builder.append(data); - builder.append(VCARD_COL_SEPARATOR); + final List<String> parameterList = new ArrayList<String>(); + if (isPrimary) { + parameterList.add(Constants.PARAM_TYPE_PREF); + } + if (!TextUtils.isEmpty(typeAsString)) { + parameterList.add(typeAsString); + } + + appendVCardLineWithCharsetAndQPDetection(builder, Constants.PROPERTY_EMAIL, + parameterList, rawData); } 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 String encodedData, boolean isPrimary) { + builder.append(Constants.PROPERTY_TEL); + builder.append(VCARD_PARAM_SEPARATOR); final int typeAsPrimitive; if (typeAsObject == null) { @@ -1838,56 +1978,105 @@ public class VCardComposer { typeAsPrimitive = typeAsObject; } + ArrayList<String> parameterList = new ArrayList<String>(); switch (typeAsPrimitive) { case Phone.TYPE_HOME: - appendTypeAttributes(builder, Arrays.asList( - Constants.ATTR_TYPE_HOME, Constants.ATTR_TYPE_VOICE)); + parameterList.addAll( + Arrays.asList(Constants.PARAM_TYPE_HOME)); break; case Phone.TYPE_WORK: - appendTypeAttributes(builder, Arrays.asList( - Constants.ATTR_TYPE_WORK, Constants.ATTR_TYPE_VOICE)); + parameterList.addAll( + Arrays.asList(Constants.PARAM_TYPE_WORK)); break; case Phone.TYPE_FAX_HOME: - appendTypeAttributes(builder, Arrays.asList( - Constants.ATTR_TYPE_HOME, Constants.ATTR_TYPE_FAX)); + parameterList.addAll( + Arrays.asList(Constants.PARAM_TYPE_HOME, Constants.PARAM_TYPE_FAX)); break; case Phone.TYPE_FAX_WORK: - appendTypeAttributes(builder, Arrays.asList( - Constants.ATTR_TYPE_WORK, Constants.ATTR_TYPE_FAX)); + parameterList.addAll( + Arrays.asList(Constants.PARAM_TYPE_WORK, Constants.PARAM_TYPE_FAX)); break; case Phone.TYPE_MOBILE: - builder.append(Constants.ATTR_TYPE_CELL); + parameterList.add(Constants.PARAM_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); + parameterList.add(Constants.PARAM_TYPE_VOICE); } else { - appendTypeAttribute(builder, Constants.ATTR_TYPE_PAGER); + parameterList.add(Constants.PARAM_TYPE_PAGER); } break; case Phone.TYPE_OTHER: - appendTypeAttribute(builder, Constants.ATTR_TYPE_VOICE); + parameterList.add(Constants.PARAM_TYPE_VOICE); + break; + case Phone.TYPE_CAR: + parameterList.add(Constants.PARAM_TYPE_CAR); + break; + case Phone.TYPE_COMPANY_MAIN: + // There's no relevant field in vCard (at least 2.1). + parameterList.add(Constants.PARAM_TYPE_WORK); + isPrimary = true; + break; + case Phone.TYPE_ISDN: + parameterList.add(Constants.PARAM_TYPE_ISDN); + break; + case Phone.TYPE_MAIN: + isPrimary = true; + break; + case Phone.TYPE_OTHER_FAX: + parameterList.add(Constants.PARAM_TYPE_FAX); + break; + case Phone.TYPE_TELEX: + parameterList.add(Constants.PARAM_TYPE_TLX); + break; + case Phone.TYPE_WORK_MOBILE: + parameterList.addAll( + Arrays.asList(Constants.PARAM_TYPE_WORK, Constants.PARAM_TYPE_CELL)); + break; + case Phone.TYPE_WORK_PAGER: + parameterList.add(Constants.PARAM_TYPE_WORK); + // See above. + if (mIsDoCoMo) { + parameterList.add(Constants.PARAM_TYPE_VOICE); + } else { + parameterList.add(Constants.PARAM_TYPE_PAGER); + } + break; + case Phone.TYPE_MMS: + parameterList.add(Constants.PARAM_TYPE_MSG); break; case Phone.TYPE_CUSTOM: if (mUsesAndroidProperty && !TextUtils.isEmpty(label) && VCardUtils.containsOnlyAlphaDigitHyphen(label)) { - appendTypeAttribute(builder, "X-" + label); + // Note: Strictly, vCard 2.1 does not allow "X-" parameter without + // "TYPE=" string. + parameterList.add("X-" + label); } else { // Just ignore the custom type. - appendTypeAttribute(builder, Constants.ATTR_TYPE_VOICE); + parameterList.add(Constants.PARAM_TYPE_VOICE); } break; + case Phone.TYPE_RADIO: + case Phone.TYPE_TTY_TDD: default: - appendUncommonPhoneType(builder, typeAsPrimitive); break; } + if (isPrimary) { + parameterList.add(Constants.PARAM_TYPE_PREF); + } + + if (parameterList.isEmpty()) { + appendUncommonPhoneType(builder, typeAsPrimitive); + } else { + appendTypeParameters(builder, parameterList); + } + builder.append(VCARD_DATA_SEPARATOR); builder.append(encodedData); - builder.append(VCARD_COL_SEPARATOR); + builder.append(VCARD_END_OF_LINE); } /** @@ -1897,35 +2086,65 @@ public class VCardComposer { if (mIsDoCoMo) { // The previous implementation for DoCoMo had been conservative // about miscellaneous types. - builder.append(Constants.ATTR_TYPE_VOICE); + builder.append(Constants.PARAM_TYPE_VOICE); } else { - String phoneAttribute = VCardUtils.getPhoneAttributeString(type); - if (phoneAttribute != null) { - appendTypeAttribute(builder, phoneAttribute); + String phoneType = VCardUtils.getPhoneTypeString(type); + if (phoneType != null) { + appendTypeParameter(builder, phoneType); } else { Log.e(LOG_TAG, "Unknown or unsupported (by vCard) Phone type: " + type); } } } + // appendVCardLine() variants accepting one String. + + private void appendVCardLineWithCharsetAndQPDetection(final StringBuilder builder, + final String propertyName, final String rawData) { + appendVCardLineWithCharsetAndQPDetection(builder, propertyName, null, rawData); + } + + private void appendVCardLineWithCharsetAndQPDetection(final StringBuilder builder, + final String propertyName, + final List<String> parameterList, final String rawData) { + final boolean needCharset = + (mUsesQuotedPrintable && !VCardUtils.containsOnlyPrintableAscii(rawData)); + final boolean reallyUseQuotedPrintable = + !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawData); + appendVCardLine(builder, propertyName, parameterList, + rawData, needCharset, reallyUseQuotedPrintable); + } + 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, + final String propertyName, final String rawData, final boolean needCharset, + boolean needQuotedPrintable) { + appendVCardLine(builder, propertyName, null, rawData, needCharset, needQuotedPrintable); + } + + private void appendVCardLine(final StringBuilder builder, + final String propertyName, + final List<String> parameterList, + final String rawData, final boolean needCharset, boolean needQuotedPrintable) { - builder.append(field); + builder.append(propertyName); + if (parameterList != null && parameterList.size() > 0) { + builder.append(VCARD_PARAM_SEPARATOR); + appendTypeParameters(builder, parameterList); + } if (needCharset) { - builder.append(VCARD_ATTR_SEPARATOR); - builder.append(mVCardAttributeCharset); + builder.append(VCARD_PARAM_SEPARATOR); + builder.append(mVCardCharsetParameter); } final String encodedData; if (needQuotedPrintable) { - builder.append(VCARD_ATTR_SEPARATOR); - builder.append(VCARD_ATTR_ENCODING_QP); + builder.append(VCARD_PARAM_SEPARATOR); + builder.append(VCARD_PARAM_ENCODING_QP); encodedData = encodeQuotedPrintable(rawData); } else { // TODO: one line may be too huge, which may be invalid in vCard spec, though @@ -1935,10 +2154,94 @@ public class VCardComposer { builder.append(VCARD_DATA_SEPARATOR); builder.append(encodedData); - builder.append(VCARD_COL_SEPARATOR); + builder.append(VCARD_END_OF_LINE); + } + + // appendVCardLine() variants accepting List<String>. + + private void appendVCardLineWithCharsetAndQPDetection(final StringBuilder builder, + final String propertyName, final List<String> rawDataList) { + appendVCardLineWithCharsetAndQPDetection(builder, propertyName, null, rawDataList); + } + + private void appendVCardLineWithCharsetAndQPDetection(final StringBuilder builder, + final String propertyName, + final List<String> parameterList, final List<String> rawDataList) { + boolean needCharset = false; + boolean reallyUseQuotedPrintable = false; + for (String rawData : rawDataList) { + if (!needCharset && mUsesQuotedPrintable && + !VCardUtils.containsOnlyPrintableAscii(rawData)) { + needCharset = true; + } + if (!reallyUseQuotedPrintable && + !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawData)) { + reallyUseQuotedPrintable = true; + } + if (needCharset && reallyUseQuotedPrintable) { + break; + } + } + + appendVCardLine(builder, propertyName, parameterList, + rawDataList, needCharset, reallyUseQuotedPrintable); + } + + /* + private void appendVCardLine(final StringBuilder builder, + final String propertyName, final List<String> rawDataList) { + appendVCardLine(builder, propertyName, rawDataList, false, false); + } + + private void appendVCardLine(final StringBuilder builder, + final String propertyName, final List<String> rawDataList, + final boolean needCharset, boolean needQuotedPrintable) { + appendVCardLine(builder, propertyName, null, rawDataList, needCharset, needQuotedPrintable); + }*/ + + private void appendVCardLine(final StringBuilder builder, + final String propertyName, + final List<String> parameterList, + final List<String> rawDataList, final boolean needCharset, + boolean needQuotedPrintable) { + builder.append(propertyName); + if (parameterList != null && parameterList.size() > 0) { + builder.append(VCARD_PARAM_SEPARATOR); + appendTypeParameters(builder, parameterList); + } + if (needCharset) { + builder.append(VCARD_PARAM_SEPARATOR); + builder.append(mVCardCharsetParameter); + } + + builder.append(VCARD_DATA_SEPARATOR); + boolean first = true; + for (String rawData : rawDataList) { + final String encodedData; + if (needQuotedPrintable) { + builder.append(VCARD_PARAM_SEPARATOR); + builder.append(VCARD_PARAM_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); + } + + if (first) { + first = false; + } else { + builder.append(VCARD_ITEM_SEPARATOR); + } + builder.append(encodedData); + } + builder.append(VCARD_END_OF_LINE); } - private void appendTypeAttributes(final StringBuilder builder, + /** + * VCARD_PARAM_SEPARATOR must be appended before this method being called. + */ + private void appendTypeParameters(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. @@ -1947,47 +2250,53 @@ public class VCardComposer { if (first) { first = false; } else { - builder.append(VCARD_ATTR_SEPARATOR); + builder.append(VCARD_PARAM_SEPARATOR); } - appendTypeAttribute(builder, type); + appendTypeParameter(builder, type); } } - private void appendTypeAttribute(final StringBuilder builder, final String type) { + /** + * VCARD_PARAM_SEPARATOR must be appended before this method being called. + */ + 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) { - builder.append(Constants.ATTR_TYPE).append(VCARD_ATTR_EQUAL); + if ((mIsV30 || mAppendTypeParamName) && !mIsDoCoMo) { + builder.append(Constants.PARAM_TYPE).append(VCARD_PARAM_EQUAL); } builder.append(type); } /** - * Returns true when the property line should contain charset attribute + * 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 used charset is not UTF-8 + * 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... + * 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 shouldAppendCharsetParameter(final String propertyValue) { + return (!(mIsV30 && mUsesUtf8) && !VCardUtils.containsOnlyPrintableAscii(propertyValue)); } - private boolean shouldAppendCharsetAttribute(final List<String> propertyValueList) { - boolean shouldAppendBasically = false; + private boolean shouldAppendCharsetParameters(final List<String> propertyValueList) { + if (mIsV30 && mUsesUtf8) { + return false; + } for (String propertyValue : propertyValueList) { if (!VCardUtils.containsOnlyPrintableAscii(propertyValue)) { - shouldAppendBasically = true; - break; + return true; } } - return shouldAppendBasically && (!mIsV30 || !mUsesUtf8); + return false; } private String encodeQuotedPrintable(String str) { @@ -2046,4 +2355,116 @@ public class VCardComposer { return tmpBuilder.toString(); } + + //// The methods bellow are for call log history //// + + /** + * This static function is to compose vCard for phone own number + */ + public String composeVCardForPhoneOwnNumber(int phonetype, String phoneName, + String phoneNumber, boolean vcardVer21) { + final StringBuilder builder = new StringBuilder(); + appendVCardLine(builder, Constants.PROPERTY_BEGIN, VCARD_DATA_VCARD); + if (!vcardVer21) { + appendVCardLine(builder, Constants.PROPERTY_VERSION, Constants.VERSION_V30); + } else { + appendVCardLine(builder, Constants.PROPERTY_VERSION, Constants.VERSION_V21); + } + + boolean needCharset = false; + if (!(VCardUtils.containsOnlyPrintableAscii(phoneName))) { + needCharset = true; + } + appendVCardLine(builder, Constants.PROPERTY_FN, phoneName, needCharset, false); + appendVCardLine(builder, Constants.PROPERTY_N, phoneName, needCharset, false); + + String label = Integer.toString(phonetype); + appendVCardTelephoneLine(builder, phonetype, label, phoneNumber, false); + + appendVCardLine(builder, Constants.PROPERTY_END, VCARD_DATA_VCARD); + + return builder.toString(); + } + + /** + * 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_PARAM_SEPARATOR); + appendTypeParameter(builder, callLogTypeStr); + builder.append(VCARD_DATA_SEPARATOR); + builder.append(toRfc2455Format(dateAsLong)); + builder.append(VCARD_END_OF_LINE); + } + + private String createOneCallLogEntryInternal() { + final StringBuilder builder = new StringBuilder(); + appendVCardLine(builder, Constants.PROPERTY_BEGIN, VCARD_DATA_VCARD); + if (mIsV30) { + appendVCardLine(builder, Constants.PROPERTY_VERSION, Constants.VERSION_V30); + } else { + appendVCardLine(builder, Constants.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)); + appendVCardLine(builder, Constants.PROPERTY_FN, name, needCharset, false); + appendVCardLine(builder, Constants.PROPERTY_N, 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, false); + tryAppendCallHistoryTimeStampField(builder); + appendVCardLine(builder, Constants.PROPERTY_END, VCARD_DATA_VCARD); + return builder.toString(); + } } diff --git a/core/java/android/pim/vcard/VCardConfig.java b/core/java/android/pim/vcard/VCardConfig.java index 68cd0df..fff4c82 100644 --- a/core/java/android/pim/vcard/VCardConfig.java +++ b/core/java/android/pim/vcard/VCardConfig.java @@ -15,14 +15,20 @@ */ 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 { + private static final String LOG_TAG = "vcard.VCardConfig"; + // TODO: may be better to make the instance of this available and stop using static methods and // one integer. @@ -41,8 +47,8 @@ public class VCardConfig { // 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,187 @@ 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. + * <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 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 "primary" property values with Quoted-Printable encoding. + * </P> + * <P> + * Again, we should not use this flag at all for complying vCard 2.1 spec. + * </P> + * <P> + * We will change the behavior around this flag in the future, after understanding the other + * real vCard cases around this problem. Please use this flag with extreme caution even when + * needed. + * </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_REFRAIN_QP_TO_PRIMARY_PROPERTIES = 0x10000000; + + /** + * <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 every time + * possible. The default behavior does not emit it and is valid, while adding "TYPE=" + * is also valid. In vCrad 3.0, this flag is unnecessary, since "TYPE=" is MUST in + * vCard 3.0 specification. * - * @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. + * If you are targeting to some importer which cannot accept type parameters + * without "TYPE=" string (which should be rare though), please use this flag. + * + * XXX: Really rare? + * + * e.g. int vcardType = (VCARD_TYPE_V21_GENERIC | FLAG_APPEND_TYPE_PARAM); */ - public static final int FLAG_USE_QP_TO_PRIMARY_PROPERTIES = 0x10000000; - - // VCard types + public static final int FLAG_APPEND_TYPE_PARAM = 0x04000000; + + //// The followings are VCard types available from importer/exporter. //// /** + * <P> * 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. - * + * 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. + * </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 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"; /** + * <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 +291,135 @@ 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_PRIMARY_PROPERTIES); + + public 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"; - 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 usesQuotedPrintable(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(int vcardType) { - return ((vcardType & FLAG_CHARSET_UTF8) != 0); + public static boolean usesUtf8(final int vcardType) { + return ((vcardType & FLAG_CHARSET_MASK) == FLAG_CHARSET_UTF8); } - 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 boolean usesShiftJis(final int vcardType) { + return ((vcardType & FLAG_CHARSET_MASK) == FLAG_CHARSET_SHIFT_JIS); } - 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 refrainsQPToPrimaryProperties(final int vcardType) { + return (!usesQuotedPrintable(vcardType) || + ((vcardType & FLAG_REFRAIN_QP_TO_PRIMARY_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) { + 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/VCardDataBuilder.java b/core/java/android/pim/vcard/VCardDataBuilder.java index d2026d0..76ad482 100644 --- a/core/java/android/pim/vcard/VCardDataBuilder.java +++ b/core/java/android/pim/vcard/VCardDataBuilder.java @@ -69,7 +69,7 @@ public class VCardDataBuilder implements VCardBuilder { private List<EntryHandler> mEntryHandlers = new ArrayList<EntryHandler>(); public VCardDataBuilder() { - this(null, null, false, VCardConfig.VCARD_TYPE_V21_GENERIC, null); + this(null, null, false, VCardConfig.VCARD_TYPE_V21_GENERIC_UTF8, null); } /** @@ -86,7 +86,7 @@ public class VCardDataBuilder implements VCardBuilder { boolean strictLineBreakParsing, int vcardType, Account account) { this(null, charset, strictLineBreakParsing, vcardType, account); } - + /** * @hide */ @@ -127,6 +127,18 @@ public class VCardDataBuilder implements VCardBuilder { } /** + * Called when the parse failed between startRecord() and endRecord(). + * Currently it happens only when the vCard format is 3.0. + * (VCardVersionException is thrown by VCardParser_V21 and this object is reused by + * VCardParser_V30. At that time, startRecord() is called twice before endRecord() is called.) + * TODO: Should this be in VCardBuilder interface? + */ + public void clear() { + mCurrentContactStruct = null; + mCurrentProperty = new ContactStruct.Property(); + } + + /** * Assume that VCard is not nested. In other words, this code does not accept */ public void startRecord(String type) { diff --git a/core/java/android/pim/vcard/VCardParser.java b/core/java/android/pim/vcard/VCardParser.java index b5e5049..462e22c 100644 --- a/core/java/android/pim/vcard/VCardParser.java +++ b/core/java/android/pim/vcard/VCardParser.java @@ -21,9 +21,23 @@ import java.io.IOException; import java.io.InputStream; public abstract class VCardParser { + public static final int PARSER_MODE_DEFAULT = 0; + /** + * The parser should ignore "AGENT" properties and nested vCard structure. + */ + public static final int PARSER_MODE_SCAN = 1; + protected final int mParserMode; protected boolean mCanceled; - + + public VCardParser() { + mParserMode = PARSER_MODE_DEFAULT; + } + + public VCardParser(int parserMode) { + mParserMode = parserMode; + } + /** * Parses the given stream and send the VCard data into VCardBuilderBase object. * @@ -35,7 +49,7 @@ public abstract class VCardParser { * * 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, diff --git a/core/java/android/pim/vcard/VCardParser_V21.java b/core/java/android/pim/vcard/VCardParser_V21.java index 11b3888..251db68 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; @@ -91,8 +91,15 @@ 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>(); - + protected HashSet<String> mUnknownTypeMap = new HashSet<String>(); + protected HashSet<String> mUnknownValueMap = new HashSet<String>(); + + // It seems Windows Mobile 6.5 uses "AGENT" property with completely wrong usage. + // We should just ignore just one line. + // e.g. + // "AGENT;CHARSET=SHIFT_JIS:some text" + private boolean mIgnoreAgentLine = false; + // Just for debugging private long mTimeTotal; private long mTimeReadStartRecord; @@ -106,21 +113,41 @@ 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, PARSER_MODE_DEFAULT); + } + + public VCardParser_V21(int parserMode) { + this(null, parserMode); } public VCardParser_V21(VCardSourceDetector detector) { - super(); - if (detector != null && detector.getType() == VCardSourceDetector.TYPE_FOMA) { - mNestCount = 1; + this(detector, PARSER_MODE_DEFAULT); + } + + /** + * TODO: Merge detector and parser mode. + */ + public VCardParser_V21(VCardSourceDetector detector, int parserMode) { + super(parserMode); + if (detector != null) { + final int type = detector.getType(); + if (type == VCardSourceDetector.TYPE_FOMA) { + mNestCount = 1; + } else if (type == VCardSourceDetector.TYPE_JAPANESE_MOBILE_PHONE) { + mIgnoreAgentLine = true; + } + } + + if (parserMode == PARSER_MODE_SCAN) { + mIgnoreAgentLine = true; } } - + /** * Parse the file at the given position * vcard_file = [wsls] vcard [wsls] @@ -146,18 +173,22 @@ public class VCardParser_V21 extends VCardParser { } } - protected String getVersion() { - return "2.1"; + protected int getVersion() { + return VCardConfig.FLAG_V21; } - + + protected String getVersionString() { + return Constants.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; @@ -363,7 +394,7 @@ 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; @@ -399,9 +430,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); @@ -531,19 +563,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 +594,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); } } @@ -772,32 +813,11 @@ 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. * @@ -808,9 +828,14 @@ public class VCardParser_V21 extends VCardParser { * 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. + protected void handleAgent(final String propertyValue) throws VCardException { + if (mIgnoreAgentLine) { + return; + } else { + throw new VCardAgentNotSupportedException("AGENT Property is not supported now."); + } + /* This is insufficient support. Also, AGENT Property is very rare and really hard to + understand the content. Ignore it for now. String[] strArray = propertyValue.split(":", 2); @@ -827,15 +852,19 @@ public class VCardParser_V21 extends VCardParser { /** * 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,7 +876,7 @@ public class VCardParser_V21 extends VCardParser { } @Override - public boolean parse(InputStream is, VCardBuilder builder) + public boolean parse(final InputStream is, final VCardBuilder builder) throws IOException, VCardException { return parse(is, VCardConfig.DEFAULT_CHARSET, builder); } @@ -855,6 +884,9 @@ public class VCardParser_V21 extends VCardParser { @Override public boolean parse(InputStream is, String charset, VCardBuilder 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); diff --git a/core/java/android/pim/vcard/VCardParser_V30.java b/core/java/android/pim/vcard/VCardParser_V30.java index 384649a..3dd467c 100644 --- a/core/java/android/pim/vcard/VCardParser_V30.java +++ b/core/java/android/pim/vcard/VCardParser_V30.java @@ -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() { + protected int getVersion() { + return VCardConfig.FLAG_V30; + } + + @Override + protected String getVersionString() { return Constants.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) { @@ -199,7 +232,16 @@ public class VCardParser_V30 extends VCardParser_V21 { // 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 * @@ -284,6 +326,10 @@ public class VCardParser_V30 extends VCardParser_V21 { */ @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 +345,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/VCardUtils.java b/core/java/android/pim/vcard/VCardUtils.java index dd44288..00679bd 100644 --- a/core/java/android/pim/vcard/VCardUtils.java +++ b/core/java/android/pim/vcard/VCardUtils.java @@ -18,14 +18,18 @@ 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 android.util.Log; +import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; @@ -38,53 +42,69 @@ public class VCardUtils { */ // 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 Map<String, Integer> sKnownPhoneTypeMap_StoI; + + private static final Map<Integer, String> sKnownImPropNameMap_ItoS; + 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, Constants.PARAM_TYPE_CAR); + sKnownPhoneTypeMap_StoI.put(Constants.PARAM_TYPE_CAR, Phone.TYPE_CAR); + sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_PAGER, Constants.PARAM_TYPE_PAGER); + sKnownPhoneTypeMap_StoI.put(Constants.PARAM_TYPE_PAGER, Phone.TYPE_PAGER); + sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_ISDN, Constants.PARAM_TYPE_ISDN); + sKnownPhoneTypeMap_StoI.put(Constants.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(Constants.PARAM_TYPE_HOME, Phone.TYPE_HOME); + sKnownPhoneTypeMap_StoI.put(Constants.PARAM_TYPE_WORK, Phone.TYPE_WORK); + sKnownPhoneTypeMap_StoI.put(Constants.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); + sKnownPhoneTypeMap_StoI.put(Constants.PARAM_PHONE_EXTRA_TYPE_OTHER, Phone.TYPE_OTHER); + sKnownPhoneTypeMap_StoI.put(Constants.PARAM_PHONE_EXTRA_TYPE_CALLBACK, Phone.TYPE_CALLBACK); + sKnownPhoneTypeMap_StoI.put( + Constants.PARAM_PHONE_EXTRA_TYPE_COMPANY_MAIN, Phone.TYPE_COMPANY_MAIN); + sKnownPhoneTypeMap_StoI.put(Constants.PARAM_PHONE_EXTRA_TYPE_RADIO, Phone.TYPE_RADIO); + sKnownPhoneTypeMap_StoI.put(Constants.PARAM_PHONE_EXTRA_TYPE_TTY_TDD, Phone.TYPE_TTY_TDD); + sKnownPhoneTypeMap_StoI.put(Constants.PARAM_PHONE_EXTRA_TYPE_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); + sPhoneTypesSetUnknownToContacts.add(Constants.PARAM_TYPE_MODEM); + sPhoneTypesSetUnknownToContacts.add(Constants.PARAM_TYPE_BBS); + sPhoneTypesSetUnknownToContacts.add(Constants.PARAM_TYPE_VIDEO); + + sKnownImPropNameMap_ItoS = new HashMap<Integer, String>(); + sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_AIM, Constants.PROPERTY_X_AIM); + sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_MSN, Constants.PROPERTY_X_MSN); + sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_YAHOO, Constants.PROPERTY_X_YAHOO); + sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_SKYPE, Constants.PROPERTY_X_SKYPE_USERNAME); + sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_GOOGLE_TALK, Constants.PROPERTY_X_GOOGLE_TALK); + sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_ICQ, Constants.PROPERTY_X_ICQ); + sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_JABBER, Constants.PROPERTY_X_JABBER); + sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_QQ, Constants.PROPERTY_X_QQ); + sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_NETMEETING, Constants.PROPERTY_X_NETMEETING); } - - 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; @@ -93,17 +113,30 @@ public class VCardUtils { if (types != null) { for (String typeString : types) { typeString = typeString.toUpperCase(); - if (typeString.equals(Constants.ATTR_TYPE_PREF)) { + if (typeString.equals(Constants.PARAM_TYPE_PREF)) { hasPref = true; - } else if (typeString.equals(Constants.ATTR_TYPE_FAX)) { + } else if (typeString.equals(Constants.PARAM_TYPE_FAX)) { isFax = true; } else { if (typeString.startsWith("X-") && type < 0) { typeString = typeString.substring(2); } - Integer tmp = sKnownPhoneTypesMap_StoI.get(typeString); + 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 +167,51 @@ public class VCardUtils { return type; } } - - public static boolean isValidPhoneAttribute(String phoneAttribute, int vcardType) { + + public static String getPropertyNameForIm(int protocol) { + return sKnownImPropNameMap_ItoS.get(protocol); + } + + public static boolean isValidPhoneType(String phoneType, 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)); + return (phoneType.startsWith("X-") || phoneType.startsWith("x-") || + sPhoneTypesSetUnknownToContacts.contains(phoneType)); } - 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; } @@ -196,7 +243,10 @@ public class VCardUtils { } builder.withValue(StructuredPostal.POBOX, postalData.pobox); - // Extended address is dropped since there's no relevant entry in ContactsContract. + // TODO: Japanese phone seems to use this field for expressing all the address including + // region, city, etc. Not sure we're ok to store them into NEIGHBORHOOD, while it would be + // better than dropping them all. + builder.withValue(StructuredPostal.NEIGHBORHOOD, postalData.extendedAddress); builder.withValue(StructuredPostal.STREET, postalData.street); builder.withValue(StructuredPostal.CITY, postalData.localty); builder.withValue(StructuredPostal.REGION, postalData.region); @@ -209,12 +259,12 @@ 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, @@ -222,13 +272,20 @@ public class VCardUtils { * android.pim.vcard.ContactStruct.PostalData)} */ public static String[] getVCardPostalElements(ContentValues contentValues) { + // adr-value = 0*6(text-value ";") text-value + // ; PO Box, Extended Address, Street, Locality, Region, Postal + // ; Code, Country Name 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] = ""; + // We keep all the data in StructuredPostal, presuming NEIGHBORHOOD is + // similar to "Extended Address". + dataArray[1] = contentValues.getAsString(StructuredPostal.NEIGHBORHOOD); + if (dataArray[1] == null) { + dataArray[1] = ""; + } dataArray[2] = contentValues.getAsString(StructuredPostal.STREET); if (dataArray[2] == null) { dataArray[2] = ""; @@ -256,24 +313,23 @@ public class VCardUtils { 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,12 +347,41 @@ public class VCardUtils { } return builder.toString(); } - + + 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(String str) { if (TextUtils.isEmpty(str)) { return true; } - + final int length = str.length(); final int asciiFirst = 0x20; final int asciiLast = 0x126; 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/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/Calendar.java b/core/java/android/provider/Calendar.java index f046cef..b8cf6c2 100644 --- a/core/java/android/provider/Calendar.java +++ b/core/java/android/provider/Calendar.java @@ -76,6 +76,15 @@ 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 @@ -341,11 +350,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,6 +523,12 @@ 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"; } /** @@ -862,10 +877,10 @@ public final class Calendar { */ public static final String MAX_BUSYBITS = "maxBusyBits"; } - + public static final class CalendarMetaData implements CalendarMetaDataColumns { } - + public interface BusyBitsColumns { /** * The Julian day number. @@ -889,22 +904,22 @@ public final class Calendar { */ 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; /** * Retrieves the busy bits 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) @@ -1032,14 +1047,14 @@ public final class Calendar { 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"); public static final Uri insert(ContentResolver cr, long eventId, @@ -1063,11 +1078,11 @@ public final class Calendar { return cr.query(CONTENT_URI, projection, selection, selectionArgs, DEFAULT_SORT_ORDER); } - + /** * 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 @@ -1091,13 +1106,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 @@ -1125,7 +1140,7 @@ public final class Calendar { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "missed alarms found: " + cursor.getCount()); } - + try { while (cursor.moveToNext()) { long id = cursor.getLong(0); @@ -1146,14 +1161,14 @@ public final class Calendar { } finally { cursor.close(); } - + } - + /** * 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 diff --git a/core/java/android/provider/MediaStore.java b/core/java/android/provider/MediaStore.java index 062080d..a796fe9 100644 --- a/core/java/android/provider/MediaStore.java +++ b/core/java/android/provider/MediaStore.java @@ -238,6 +238,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 +269,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 +297,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 +319,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 +356,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 +379,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 +694,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 +711,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); } @@ -1598,7 +1656,8 @@ 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); } /** @@ -1607,18 +1666,50 @@ public final class MediaStore { * * @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); + } + + /** + * 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, 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 7433a79..819f3a0 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -43,8 +43,10 @@ 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; /** @@ -476,7 +478,10 @@ public final class Settings { private static class NameValueCache { private final String mVersionSystemProperty; - private final HashMap<String, String> mValues = Maps.newHashMap(); + // the following needs synchronization because this structure is accessed from different + // threads and they could be performing clear(), get(), put() at the same time. + private final Map<String, String> mValues = + Collections.synchronizedMap(new HashMap<String, String>()); private long mValuesVersion = 0; private final Uri mUri; @@ -491,8 +496,29 @@ public final class Settings { mValues.clear(); mValuesVersion = newValuesVersion; } - if (!mValues.containsKey(name)) { - String value = null; + /* + * don't look for the key using containsKey() method because (key, object) mapping + * could be removed from mValues before get() is done like so: + * + * say, mValues contains mapping for "foo" + * Thread# 1 + * performs containsKey("foo") + * receives true + * Thread #2 + * triggers mValues.clear() + * Thread#1 + * since containsKey("foo") = true, performs get("foo") + * receives null + * thats incorrect! + * + * to avoid the above, thread#1 should do get("foo") instead of containsKey("foo") + * since mValues is synchronized, get() will get a consistent value. + * + * we don't want to make this method synchronized tho - because + * holding mutex is not desirable while a call could be made to database. + */ + String value = mValues.get(name); + if (value == null) { Cursor c = null; try { c = cr.query(mUri, new String[] { Settings.NameValueTable.VALUE }, @@ -505,10 +531,8 @@ public final class Settings { } finally { if (c != null) c.close(); } - return value; - } else { - return mValues.get(name); } + return value; } } @@ -2502,6 +2526,13 @@ public final class Settings { public static final String OVERRIDE_ACTION = "com.google.gservices.intent.action.GSERVICES_OVERRIDE"; + /** + * Intent action to set Gservices with new values. (Requires WRITE_GSERVICES permission.) + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String UPDATE_ACTION = + "com.google.gservices.intent.action.GSERVICES_UPDATE"; + private static volatile NameValueCache mNameValueCache = null; private static final Object mNameValueCacheLock = new Object(); @@ -2620,6 +2651,11 @@ public final class Settings { public static final String CHECKIN_INTERVAL = "checkin_interval"; /** + * The interval (in seconds) between event log aggregation runs. + */ + public static final String AGGREGATION_INTERVAL_SECONDS = "aggregation_interval_seconds"; + + /** * Boolean indicating if the market app should force market only checkins on * install/uninstall. Any non-0 value is considered true. */ @@ -2809,6 +2845,11 @@ public final class Settings { "gmail_wait_time_retry_uphill_op"; /** + * Controls if Gmail should delay sending operations that have previously failed. + */ + public static final String GMAIL_DELAY_BAD_OP = "gmail_delay_bad_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. */ @@ -3110,6 +3151,13 @@ public final class Settings { = "google_login_generic_auth_service"; /** + * Duration in milliseconds after setup at which market does not reconcile applications + * which are installed during restore. + */ + public static final String VENDING_RESTORE_WINDOW_MS = "vending_restore_window_ms"; + + + /** * Frequency in milliseconds at which we should sync the locally installed Vending Machine * content with the server. */ @@ -3623,7 +3671,6 @@ public final class Settings { */ 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 @@ -3638,6 +3685,32 @@ public final class Settings { public static final String LAST_KMSG_KB = "last_kmsg_kb"; /** + * Maximum age of entries kept by {@link android.os.IDropBox}. + */ + public static final String DROPBOX_AGE_SECONDS = + "dropbox_age_seconds"; + /** + * Maximum amount of disk space used by {@link android.os.IDropBox} no matter what. + */ + public static final String DROPBOX_QUOTA_KB = + "dropbox_quota_kb"; + /** + * Percent of free disk (excluding reserve) which {@link android.os.IDropBox} will use. + */ + public static final String DROPBOX_QUOTA_PERCENT = + "dropbox_quota_percent"; + /** + * Percent of total disk which {@link android.os.IDropBox} will never dip into. + */ + public static final String DROPBOX_RESERVE_PERCENT = + "dropbox_reserve_percent"; + /** + * Prefix for per-tag dropbox disable/enable settings. + */ + public static final String DROPBOX_TAG_PREFIX = + "dropbox:"; + + /** * The length of time in milli-seconds that automatic small adjustments to * SystemClock are ignored if NITZ_UPDATE_DIFF is not exceeded. */ diff --git a/core/java/android/provider/Telephony.java b/core/java/android/provider/Telephony.java index d8c5a53..9a72d93 100644 --- a/core/java/android/provider/Telephony.java +++ b/core/java/android/provider/Telephony.java @@ -152,6 +152,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 +249,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 +272,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 +280,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); diff --git a/core/java/android/server/BluetoothService.java b/core/java/android/server/BluetoothService.java index 70af91f..6a88ff0 100644 --- a/core/java/android/server/BluetoothService.java +++ b/core/java/android/server/BluetoothService.java @@ -61,7 +61,7 @@ import java.util.Map; 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; 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/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/view/ViewRoot.java b/core/java/android/view/ViewRoot.java index bef3e58..ed2139d 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*/ @@ -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/webkit/BrowserFrame.java b/core/java/android/webkit/BrowserFrame.java index 9456ae1..e6e26fa 100644 --- a/core/java/android/webkit/BrowserFrame.java +++ b/core/java/android/webkit/BrowserFrame.java @@ -19,17 +19,21 @@ 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 junit.framework.Assert; +import java.io.InputStream; import java.net.URLEncoder; import java.util.HashMap; import java.util.Map; @@ -463,6 +467,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 expectSize 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,6 +541,7 @@ class BrowserFrame extends Handler { String method, HashMap headers, byte[] postData, + long postDataIdentifier, int cacheMode, boolean synchronous) { PerfChecker checker = new PerfChecker(); @@ -551,8 +613,9 @@ class BrowserFrame extends Handler { } // Create a LoadListener - LoadListener loadListener = LoadListener.getLoadListener(mContext, this, url, - loaderHandle, synchronous, isMainFramePage); + LoadListener loadListener = LoadListener.getLoadListener(mContext, + this, url, loaderHandle, synchronous, isMainFramePage, + postDataIdentifier); mCallbackProxy.onLoadResource(url); 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 75028de..910d7b2 100644 --- a/core/java/android/webkit/CacheManager.java +++ b/core/java/android/webkit/CacheManager.java @@ -283,16 +283,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 { @@ -304,7 +312,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; } } @@ -352,14 +360,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; } @@ -367,7 +385,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; } @@ -375,9 +393,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) { @@ -408,6 +426,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) { @@ -434,7 +458,7 @@ 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); @@ -513,6 +537,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) { diff --git a/core/java/android/webkit/CallbackProxy.java b/core/java/android/webkit/CallbackProxy.java index f760b61..4a8e758 100644 --- a/core/java/android/webkit/CallbackProxy.java +++ b/core/java/android/webkit/CallbackProxy.java @@ -107,6 +107,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 +150,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 +249,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; @@ -660,6 +673,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 +777,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 @@ -1000,10 +1014,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 +1349,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/FrameLoader.java b/core/java/android/webkit/FrameLoader.java index c1eeb3b..c3dac6e 100644 --- a/core/java/android/webkit/FrameLoader.java +++ b/core/java/android/webkit/FrameLoader.java @@ -242,7 +242,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 +270,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..3e0be1c 100644 --- a/core/java/android/webkit/HTML5VideoViewProxy.java +++ b/core/java/android/webkit/HTML5VideoViewProxy.java @@ -199,6 +199,8 @@ 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. @@ -224,6 +226,7 @@ class HTML5VideoViewProxy extends Handler VideoPlayer.pause(this); break; } + case ENDED: case ERROR: { WebChromeClient client = mWebView.getWebChromeClient(); if (client != null) { 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..938df95 100644 --- a/core/java/android/webkit/LoadListener.java +++ b/core/java/android/webkit/LoadListener.java @@ -78,7 +78,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 +104,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 @@ -123,13 +124,13 @@ 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, long postIdentifier) { sNativeLoaderCount += 1; - return new LoadListener( - context, frame, url, nativeLoader, synchronous, isMainPageLoader); + return new LoadListener(context, frame, url, nativeLoader, synchronous, + isMainPageLoader, postIdentifier); } public static int getNativeLoaderCount() { @@ -137,7 +138,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, + long postIdentifier) { if (DebugFlags.LOAD_LISTENER) { Log.v(LOGTAG, "LoadListener constructor url=" + url); } @@ -150,6 +152,7 @@ class LoadListener extends Handler implements EventHandler { mMessageQueue = new Vector<Message>(); } mIsMainPageLoader = isMainPageLoader; + mPostIdentifier = postIdentifier; } /** @@ -408,9 +411,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 +530,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 +645,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 +870,10 @@ class LoadListener extends Handler implements EventHandler { } } + long postIdentifier() { + return mPostIdentifier; + } + void attachRequestHandle(RequestHandle requestHandle) { if (DebugFlags.LOAD_LISTENER) { Log.v(LOGTAG, "LoadListener.attachRequestHandle(): " + @@ -907,8 +920,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 +1023,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) { @@ -1047,7 +1067,7 @@ class LoadListener extends Handler implements EventHandler { } nativeAddData(c.mArray, c.mLength); } - mDataBuilder.releaseChunk(c); + c.release(); checker.responseAlert("res nativeAddData"); } } @@ -1059,7 +1079,7 @@ class LoadListener extends Handler implements EventHandler { void tearDown() { if (mCacheResult != null) { if (getErrorID() == OK) { - CacheManager.saveCacheFile(mUrl, mCacheResult); + CacheManager.saveCacheFile(mUrl, mPostIdentifier, mCacheResult); } // we need to reset mCacheResult to be null @@ -1187,7 +1207,8 @@ 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); } mCacheResult = null; } @@ -1210,8 +1231,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..b53e404 100644 --- a/core/java/android/webkit/Network.java +++ b/core/java/android/webkit/Network.java @@ -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/PluginUtil.java b/core/java/android/webkit/PluginUtil.java index 8fdbd67..33ccf9d 100644 --- a/core/java/android/webkit/PluginUtil.java +++ b/core/java/android/webkit/PluginUtil.java @@ -19,9 +19,6 @@ 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"; @@ -35,12 +32,7 @@ class PluginUtil { 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); + Class<?> stubClass = getPluginClass(context, packageName, className); Object stubObject = stubClass.newInstance(); if (stubObject instanceof PluginStub) { @@ -56,4 +48,14 @@ class PluginUtil { } return null; } + + /* package */ + static Class<?> getPluginClass(Context context, String packageName, + String className) throws NameNotFoundException, ClassNotFoundException { + Context pluginContext = context.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/URLUtil.java b/core/java/android/webkit/URLUtil.java index 232ed36..211e5e4 100644 --- a/core/java/android/webkit/URLUtil.java +++ b/core/java/android/webkit/URLUtil.java @@ -367,19 +367,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/WebChromeClient.java b/core/java/android/webkit/WebChromeClient.java index 92676aa..6adac0b 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; @@ -289,4 +290,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..8e40b23 100644 --- a/core/java/android/webkit/WebSettings.java +++ b/core/java/android/webkit/WebSettings.java @@ -1002,7 +1002,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/WebTextView.java b/core/java/android/webkit/WebTextView.java index e0d41c2..71b1f9f 100644 --- a/core/java/android/webkit/WebTextView.java +++ b/core/java/android/webkit/WebTextView.java @@ -84,13 +84,24 @@ 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 + // Gets set to true any time the WebTextView has focus, but the navigation + // cache does not yet know that the focus has been changed. This happens + // if the user presses "Next", if the user moves the cursor to a textfield + // and starts typing or clicks the trackball/center key, and when the user + // touches a textfield. 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; private boolean mInSetTextAndKeepSelection; // Array to store the final character added in onTextChanged, so that its @@ -136,20 +147,23 @@ 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; + if (down) { + if (mOkayForFocusNotToMatch) { + if (mWebView.nativeFocusNodePointer() == mNodePointer) { + mOkayForFocusNotToMatch = false; + } + } else if (mWebView.nativeFocusNodePointer() != mNodePointer + && !isArrowKey) { + 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); + } + } Spannable text = (Spannable) getText(); int oldLength = text.length(); // Normally the delete key's dom events are sent via onTextChanged. @@ -185,7 +199,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,15 +317,19 @@ 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(); + mOkayForFocusNotToMatch = true; + // Pass the click to set the focus to the textfield which will now + // have the cursor. + mWebView.centerKeyPressOnTextField(); // 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); @@ -331,6 +349,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 +368,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); @@ -591,6 +618,17 @@ 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; + 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,6 +698,13 @@ import java.util.ArrayList; setTextColor(Color.BLACK); } + @Override + public void setInputType(int type) { + mFromSetInputType = true; + super.setInputType(type); + mFromSetInputType = false; + } + /* package */ void setMaxLength(int maxLength) { mMaxLength = maxLength; if (-1 == maxLength) { @@ -761,32 +806,6 @@ import java.util.ArrayList; } /** - * 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. diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java index 691fa77..4b5f94b 100644 --- a/core/java/android/webkit/WebView.java +++ b/core/java/android/webkit/WebView.java @@ -64,7 +64,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; @@ -82,6 +84,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. @@ -390,6 +394,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(); @@ -427,6 +433,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 @@ -438,6 +448,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; @@ -450,6 +462,7 @@ 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; @@ -468,9 +481,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; @@ -479,7 +492,7 @@ 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; @@ -535,11 +548,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 @@ -2350,6 +2362,7 @@ public class WebView extends AbsoluteLayout } int result = nativeFindAll(find.toLowerCase(), find.toUpperCase()); invalidate(); + mLastFind = find; return result; } @@ -2357,6 +2370,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 @@ -2531,6 +2547,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 @@ -2565,8 +2616,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; @@ -2607,12 +2668,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 { @@ -2632,6 +2693,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. @@ -2812,10 +2883,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()) { @@ -2838,6 +2906,21 @@ public class WebView extends AbsoluteLayout */ private boolean mNeedToAdjustWebTextView; + private boolean didUpdateTextViewBounds(boolean allowIntersect) { + Rect contentBounds = nativeFocusCandidateNodeBounds(); + Rect vBox = contentToViewRect(contentBounds); + Rect visibleRect = new Rect(); + calcOurVisibleRect(visibleRect); + if (allowIntersect ? Rect.intersects(visibleRect, vBox) : + visibleRect.contains(vBox)) { + mWebTextView.setRect(vBox.left, vBox.top, vBox.width(), + vBox.height()); + return true; + } else { + return false; + } + } + private void drawCoreAndCursorRing(Canvas canvas, int color, boolean drawCursorRing) { if (mDrawHistory) { @@ -2847,8 +2930,22 @@ public class WebView extends AbsoluteLayout } 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); @@ -2865,19 +2962,13 @@ public class WebView extends AbsoluteLayout invalidate(); if (mNeedToAdjustWebTextView) { mNeedToAdjustWebTextView = false; - Rect contentBounds = nativeFocusCandidateNodeBounds(); - Rect vBox = contentToViewRect(contentBounds); - Rect visibleRect = new Rect(); - calcOurVisibleRect(visibleRect); - if (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. + // 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. + if (didUpdateTextViewBounds(false)) { mWebTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, contentToViewDimension( nativeFocusCandidateTextSize())); - mWebTextView.setRect(vBox.left, vBox.top, vBox.width(), - vBox.height()); // If it is a password field, start drawing the // WebTextView once again. if (nativeFocusCandidateIsPassword()) { @@ -2929,11 +3020,12 @@ public class WebView extends AbsoluteLayout if (mNativeClass == 0) return; if (mShiftIsPressed && !animateZoom) { - if (mTouchSelection) { + 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) { @@ -2953,6 +3045,10 @@ public class WebView extends AbsoluteLayout if (mFindIsUp && !animateScroll) { nativeDrawMatches(canvas); } + if (mFocusSizeChanged) { + mFocusSizeChanged = false; + didUpdateTextViewBounds(true); + } } // draw history @@ -3048,6 +3144,16 @@ public class WebView extends AbsoluteLayout imm.hideSoftInputFromWindow(this.getWindowToken(), 0); } + /** + * Only for calling from JNI. Allows a click on an unfocused textfield to + * put the textfield in focus. + */ + private void setOkayNotToMatch() { + if (inEditingMode()) { + mWebTextView.mOkayForFocusNotToMatch = true; + } + } + /* * This method checks the current focus and cursor and potentially rebuilds * mWebTextView to have the appropriate properties, such as password, @@ -3103,6 +3209,7 @@ public class WebView extends AbsoluteLayout && nativeTextGeneration() == mTextGeneration) { mWebTextView.setTextAndKeepSelection(text); } else { + // FIXME: Determine whether this is necessary. Selection.setSelection(spannable, start, end); } } else { @@ -3135,34 +3242,12 @@ public class WebView extends AbsoluteLayout mWebTextView.setSingleLine(isTextField); mWebTextView.setInPassword(nativeFocusCandidateIsPassword()); 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); - } - } else { - mWebTextView.setText(text, 0, 0); - if (DebugFlags.WEB_VIEW) { - Log.v(LOGTAG, "rebuildWebTextView !isTextField"); - } - } + text = ""; } + mWebTextView.setTextAndKeepSelection(text); mWebTextView.requestFocus(); } } @@ -3229,23 +3314,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; @@ -3257,6 +3341,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); @@ -3306,10 +3393,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(), @@ -3318,13 +3402,17 @@ 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(); + mWebTextView.mOkayForFocusNotToMatch = true; + 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); } } @@ -3389,7 +3477,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 @@ -3399,21 +3493,23 @@ 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(); + mWebTextView.mOkayForFocusNotToMatch = true; + } + return true; + } + nativeSetFollowedLink(true); + if (!mCallbackProxy.uiOverrideUrlLoading(nativeCursorText())) { mWebViewCore.sendMessage(EventHub.CLICK, data.mFrame, nativeCursorNodePointer()); } - if (isTextInput) { - rebuildWebTextView(); - displaySoftKeyboard(true); - } return true; } @@ -3429,14 +3525,29 @@ public class WebView extends AbsoluteLayout return false; } + 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(); + } + /** * @hide */ public void emulateShiftHeld() { if (0 == mNativeClass) return; // client isn't initialized - mExtendSelection = false; - mShiftIsPressed = true; - nativeHideCursor(); + setUpSelectXY(); } private boolean commitCopy() { @@ -3454,6 +3565,7 @@ public class WebView extends AbsoluteLayout mExtendSelection = false; } mShiftIsPressed = false; + invalidate(); // remove selection region and pointer if (mTouchMode == TOUCH_SELECT_MODE) { mTouchMode = TOUCH_INIT_MODE; } @@ -3597,6 +3709,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); @@ -3712,8 +3842,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: { @@ -3735,6 +3867,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) { @@ -3839,12 +3972,21 @@ public class WebView extends AbsoluteLayout // 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); @@ -3856,56 +3998,47 @@ 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; @@ -3924,13 +4057,17 @@ public class WebView extends AbsoluteLayout } } - if (done) { + 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; } @@ -3983,6 +4120,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 @@ -4022,6 +4162,9 @@ public class WebView extends AbsoluteLayout } 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; @@ -4048,6 +4191,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; @@ -4106,6 +4250,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 } @@ -4153,8 +4298,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 @@ -4236,8 +4381,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; } @@ -4251,8 +4399,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); @@ -4327,7 +4475,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; @@ -4607,21 +4755,21 @@ 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); } - /*package*/ void shortPressOnTextField() { - if (inEditingMode()) { - View v = mWebTextView; - int x = viewToContentX((v.getLeft() + v.getRight()) >> 1); - int y = viewToContentY((v.getTop() + v.getBottom()) >> 1); - nativeTextInputMotionUp(x, y); + /** + * Called when pressing the center key or trackball on a textfield. + */ + /*package*/ void centerKeyPressOnTextField() { + mWebViewCore.sendMessage(EventHub.CLICK, nativeCursorFramePointer(), + nativeCursorNodePointer()); + // Need to show the soft keyboard if it's not readonly. + if (!nativeCursorIsReadOnly()) { + displaySoftKeyboard(true); } } @@ -4691,15 +4839,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; @@ -4848,14 +4987,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; @@ -4886,9 +5017,10 @@ public class WebView extends AbsoluteLayout class PrivateHandler extends Handler { @Override public void handleMessage(Message msg) { - if (DebugFlags.WEB_VIEW) { + // 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 - > INVAL_RECT_MSG_ID ? Integer.toString(msg.what) + > REQUEST_KEYBOARD ? Integer.toString(msg.what) : HandlerDebugString[msg.what - REMEMBER_PASSWORD]); } if (mWebViewCore == null) { @@ -5052,6 +5184,9 @@ public class WebView extends AbsoluteLayout / mZoomOverviewWidth, false); } } + if (draw.mFocusSizeChanged && inEditingMode()) { + mFocusSizeChanged = true; + } break; } case WEBCORE_INITIALIZED_MSG_ID: @@ -5092,9 +5227,7 @@ public class WebView extends AbsoluteLayout } break; case MOVE_OUT_OF_PLUGIN: - if (nativePluginEatsNavKey()) { - navHandledKey(msg.arg1, 1, false, 0, true); - } + navHandledKey(msg.arg1, 1, false, 0, true); break; case UPDATE_TEXT_ENTRY_MSG_ID: // this is sent after finishing resize in WebViewCore. Make @@ -5178,9 +5311,36 @@ public class WebView extends AbsoluteLayout hideSoftKeyboard(); } else { displaySoftKeyboard(false); + if (DebugFlags.WEB_VIEW) { + Log.v(LOGTAG, "REQUEST_KEYBOARD" + + " focusCandidateIsPlugin=" + + nativeFocusCandidateIsPlugin()); + } } break; + 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; default: super.handleMessage(msg); break; @@ -5204,8 +5364,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() { @@ -5226,6 +5394,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 @@ -5260,12 +5476,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; @@ -5279,8 +5494,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; @@ -5411,10 +5625,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)); @@ -5425,10 +5640,11 @@ 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)); } @@ -5512,7 +5728,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 @@ -5587,6 +5803,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. */ @@ -5604,7 +5831,7 @@ public class WebView extends AbsoluteLayout /* package */ native boolean nativeCursorMatchesFocus(); private native boolean nativeCursorIntersects(Rect visibleRect); private native boolean nativeCursorIsAnchor(); - private native boolean nativeCursorIsPlugin(); + private native boolean nativeCursorIsReadOnly(); private native boolean nativeCursorIsTextInput(); private native Point nativeCursorPosition(); private native String nativeCursorText(); @@ -5617,13 +5844,15 @@ public class WebView extends AbsoluteLayout private native void nativeDestroy(); private native void nativeDrawCursorRing(Canvas content); 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); + private native int nativeFocusCandidateFramePointer(); private native boolean nativeFocusCandidateIsPassword(); + private native boolean nativeFocusCandidateIsPlugin(); private native boolean nativeFocusCandidateIsRtlText(); private native boolean nativeFocusCandidateIsTextField(); private native boolean nativeFocusCandidateIsTextInput(); @@ -5633,6 +5862,7 @@ public class WebView extends AbsoluteLayout /* package */ native int nativeFocusCandidatePointer(); private native String nativeFocusCandidateText(); private native int nativeFocusCandidateTextSize(); + private native boolean nativeFocusIsPlugin(); /* package */ native int nativeFocusNodePointer(); private native Rect nativeGetCursorRingBounds(); private native Region nativeGetSelection(); @@ -5650,7 +5880,6 @@ public class WebView extends AbsoluteLayout private native int nativeMoveGeneration(); private native void nativeMoveSelection(int x, int y, boolean extendSelection); - private native boolean nativePluginEatsNavKey(); // 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, @@ -5672,7 +5901,6 @@ public class WebView extends AbsoluteLayout // 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/WebViewCore.java b/core/java/android/webkit/WebViewCore.java index a5a4852..5460a47 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; @@ -273,6 +277,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 +459,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. @@ -522,8 +561,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. @@ -678,12 +715,6 @@ final class WebViewCore { int mY; } - static class PluginStateData { - int mFrame; - int mNode; - int mState; - } - static class GeolocationPermissionsData { String mOrigin; boolean mAllow; @@ -720,7 +751,7 @@ final class WebViewCore { "SINGLE_LISTBOX_CHOICE", // = 124; "MESSAGE_RELAY", // = 125; "SET_BACKGROUND_COLOR", // = 126; - "PLUGIN_STATE", // = 127; + "127", // = 127 "SAVE_DOCUMENT_STATE", // = 128; "GET_SELECTION", // = 129; "WEBKIT_DRAW", // = 130; @@ -771,7 +802,6 @@ 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 SAVE_DOCUMENT_STATE = 128; static final int GET_SELECTION = 129; static final int WEBKIT_DRAW = 130; @@ -1031,11 +1061,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 " + @@ -1593,6 +1618,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() { @@ -1607,6 +1633,7 @@ 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( @@ -1643,6 +1670,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, @@ -1651,7 +1681,7 @@ final class WebViewCore { if (animatingZoom) { df = mZoomFilter; } else if (animatingScroll) { - df = null; + df = mScrollFilter; } canvas.setDrawFilter(df); boolean tookTooLong = nativeDrawContent(canvas, color); @@ -2013,9 +2043,16 @@ 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 @@ -2086,6 +2123,13 @@ final class WebViewCore { WebView.CLEAR_TEXT_ENTRY).sendToTarget(); } + // called by JNI + private void sendFindAgain() { + if (mWebView == null) return; + Message.obtain(mWebView.mPrivateHandler, + WebView.FIND_AGAIN).sendToTarget(); + } + private native void nativeUpdateFrameCacheIfLoading(); /** @@ -2099,7 +2143,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); @@ -2107,7 +2151,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); @@ -2124,6 +2168,32 @@ final class WebViewCore { } } + // called by JNI + private Class<?> getPluginClass(String libName, String clsName) { + + if (mWebView == null) { + return null; + } + + String pkgName = PluginManager.getInstance(null).getPluginsAPKName(libName); + if (pkgName == null) { + Log.w(LOGTAG, "Unable to resolve " + libName + " to a plugin APK"); + return null; + } + + Class<?> pluginClass = null; + try { + pluginClass = PluginUtil.getPluginClass(mWebView.getContext(), 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 pluginClass; + } + // 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) { @@ -2173,6 +2243,11 @@ final class WebViewCore { return view; } + private void updateSurface(ViewManager.ChildView childView, int x, int y, + int width, int height) { + childView.attachView(x, y, width, height); + } + private void destroySurface(ViewManager.ChildView childView) { childView.removeView(); } 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..e353501 100644 --- a/core/java/android/widget/AbsListView.java +++ b/core/java/android/widget/AbsListView.java @@ -3563,6 +3563,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; } diff --git a/core/java/android/widget/AutoCompleteTextView.java b/core/java/android/widget/AutoCompleteTextView.java index 75d0f31..ce985e3 100644 --- a/core/java/android/widget/AutoCompleteTextView.java +++ b/core/java/android/widget/AutoCompleteTextView.java @@ -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(); } 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/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/RemoteViews.java b/core/java/android/widget/RemoteViews.java index 6771711..3b1f7a0 100644 --- a/core/java/android/widget/RemoteViews.java +++ b/core/java/android/widget/RemoteViews.java @@ -457,6 +457,46 @@ public class RemoteViews implements Parcelable, Filter { } } + /** + * Equivalent to calling {@link ViewGroup#addView(View)} after inflating the + * given {@link RemoteViews}, or calling {@link ViewGroup#removeAllViews()} + * when null. This allows users to build "nested" {@link RemoteViews}. + */ + private class ViewGroupAction extends Action { + public ViewGroupAction(int viewId, RemoteViews nestedViews) { + this.viewId = viewId; + this.nestedViews = nestedViews; + } + + public ViewGroupAction(Parcel parcel) { + viewId = parcel.readInt(); + nestedViews = parcel.readParcelable(null); + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(TAG); + dest.writeInt(viewId); + dest.writeParcelable(nestedViews, 0 /* no flags */); + } + + @Override + public void apply(View root) { + final Context context = root.getContext(); + final ViewGroup target = (ViewGroup) root.findViewById(viewId); + if (nestedViews != null) { + // Inflate nested views and add as children + target.addView(nestedViews.apply(context, target)); + } else if (target != null) { + // Clear all children when nested views omitted + target.removeAllViews(); + } + } + + int viewId; + RemoteViews nestedViews; + + public final static int TAG = 4; + } /** * Create a new RemoteViews object that will display the views contained @@ -493,6 +533,9 @@ public class RemoteViews implements Parcelable, Filter { case ReflectionAction.TAG: mActions.add(new ReflectionAction(parcel)); break; + case ViewGroupAction.TAG: + mActions.add(new ViewGroupAction(parcel)); + break; default: throw new ActionException("Tag " + tag + " not found"); } @@ -519,7 +562,31 @@ public class RemoteViews implements Parcelable, Filter { } mActions.add(a); } - + + /** + * Equivalent to calling {@link ViewGroup#addView(View)} after inflating the + * given {@link RemoteViews}. This allows users to build "nested" + * {@link RemoteViews}. In cases where consumers of {@link RemoteViews} may + * recycle layouts, use {@link #removeAllViews(int)} to clear any existing + * children. + * + * @param viewId The id of the parent {@link ViewGroup} to add child into. + * @param nestedView {@link RemoteViews} that describes the child. + */ + public void addView(int viewId, RemoteViews nestedView) { + addAction(new ViewGroupAction(viewId, nestedView)); + } + + /** + * Equivalent to calling {@link ViewGroup#removeAllViews()}. + * + * @param viewId The id of the parent {@link ViewGroup} to remove all + * children from. + */ + public void removeAllViews(int viewId) { + addAction(new ViewGroupAction(viewId, null)); + } + /** * Equivalent to calling View.setVisibility * diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 596fd98..f55ca3f 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -5197,7 +5197,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener mDesiredHeightAtMeasure = desired; if (heightMode == MeasureSpec.AT_MOST) { - height = Math.min(desired, height); + height = Math.min(desired, heightSize); } } diff --git a/core/java/android/widget/ViewFlipper.java b/core/java/android/widget/ViewFlipper.java index 8a7946b..2dd79b2 100644 --- a/core/java/android/widget/ViewFlipper.java +++ b/core/java/android/widget/ViewFlipper.java @@ -16,8 +16,10 @@ package android.widget; - +import android.content.BroadcastReceiver; import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; import android.content.res.TypedArray; import android.os.Handler; import android.os.Message; @@ -30,10 +32,19 @@ import android.widget.RemoteViews.RemoteView; * requested, can automatically flip between each child at a regular interval. * * @attr ref android.R.styleable#ViewFlipper_flipInterval + * @attr ref android.R.styleable#ViewFlipper_autoStart */ +@RemoteView public class ViewFlipper extends ViewAnimator { - private int mFlipInterval = 3000; - private boolean mKeepFlipping = false; + private static final int DEFAULT_INTERVAL = 3000; + + private int mFlipInterval = DEFAULT_INTERVAL; + private boolean mAutoStart = false; + + private boolean mRunning = false; + private boolean mStarted = false; + private boolean mVisible = false; + private boolean mUserPresent = true; public ViewFlipper(Context context) { super(context); @@ -44,14 +55,62 @@ public class ViewFlipper extends ViewAnimator { TypedArray a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.ViewFlipper); - mFlipInterval = a.getInt(com.android.internal.R.styleable.ViewFlipper_flipInterval, - 3000); + mFlipInterval = a.getInt( + com.android.internal.R.styleable.ViewFlipper_flipInterval, DEFAULT_INTERVAL); + mAutoStart = a.getBoolean( + com.android.internal.R.styleable.ViewFlipper_autoStart, false); a.recycle(); } + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + if (Intent.ACTION_SCREEN_OFF.equals(action)) { + mUserPresent = false; + updateRunning(); + } else if (Intent.ACTION_USER_PRESENT.equals(action)) { + mUserPresent = true; + updateRunning(); + } + } + }; + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + // Listen for broadcasts related to user-presence + final IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_SCREEN_OFF); + filter.addAction(Intent.ACTION_USER_PRESENT); + getContext().registerReceiver(mReceiver, filter); + + if (mAutoStart) { + // Automatically start when requested + startFlipping(); + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + mVisible = false; + + getContext().unregisterReceiver(mReceiver); + updateRunning(); + } + + @Override + protected void onWindowVisibilityChanged(int visibility) { + super.onWindowVisibilityChanged(visibility); + mVisible = visibility == VISIBLE; + updateRunning(); + } + /** * How long to wait before flipping to the next view - * + * * @param milliseconds * time in milliseconds */ @@ -64,26 +123,57 @@ public class ViewFlipper extends ViewAnimator { * Start a timer to cycle through child views */ public void startFlipping() { - if (!mKeepFlipping) { - mKeepFlipping = true; - showOnly(mWhichChild); - Message msg = mHandler.obtainMessage(FLIP_MSG); - mHandler.sendMessageDelayed(msg, mFlipInterval); - } + mStarted = true; + updateRunning(); } /** * No more flips */ public void stopFlipping() { - mKeepFlipping = false; + mStarted = false; + updateRunning(); + } + + /** + * Internal method to start or stop dispatching flip {@link Message} based + * on {@link #mRunning} and {@link #mVisible} state. + */ + private void updateRunning() { + boolean running = mVisible && mStarted && mUserPresent; + if (running != mRunning) { + if (running) { + showOnly(mWhichChild); + Message msg = mHandler.obtainMessage(FLIP_MSG); + mHandler.sendMessageDelayed(msg, mFlipInterval); + } else { + mHandler.removeMessages(FLIP_MSG); + } + mRunning = running; + } } /** * Returns true if the child views are flipping. */ public boolean isFlipping() { - return mKeepFlipping; + return mStarted; + } + + /** + * Set if this view automatically calls {@link #startFlipping()} when it + * becomes attached to a window. + */ + public void setAutoStart(boolean autoStart) { + mAutoStart = autoStart; + } + + /** + * Returns true if this view automatically calls {@link #startFlipping()} + * when it becomes attached to a window. + */ + public boolean isAutoStart() { + return mAutoStart; } private final int FLIP_MSG = 1; @@ -92,7 +182,7 @@ public class ViewFlipper extends ViewAnimator { @Override public void handleMessage(Message msg) { if (msg.what == FLIP_MSG) { - if (mKeepFlipping) { + if (mRunning) { showNext(); msg = obtainMessage(FLIP_MSG); sendMessageDelayed(msg, mFlipInterval); 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); +} |