diff options
Diffstat (limited to 'core/java/android')
129 files changed, 11812 insertions, 3437 deletions
diff --git a/core/java/android/accounts/AbstractAccountAuthenticator.java b/core/java/android/accounts/AbstractAccountAuthenticator.java new file mode 100644 index 0000000..474755c --- /dev/null +++ b/core/java/android/accounts/AbstractAccountAuthenticator.java @@ -0,0 +1,196 @@ +/* + * 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.os.Bundle; +import android.os.RemoteException; + +/** + * Base class for creating AccountAuthenticators. This implements the IAccountAuthenticator + * binder interface and also provides helper libraries to simplify the creation of + * AccountAuthenticators. + */ +public abstract class AbstractAccountAuthenticator { + class Transport extends IAccountAuthenticator.Stub { + public void addAccount(IAccountAuthenticatorResponse response, String accountType, + String authTokenType, String[] requiredFeatures, Bundle options) + throws RemoteException { + final Bundle result; + try { + result = AbstractAccountAuthenticator.this.addAccount( + new AccountAuthenticatorResponse(response), + accountType, authTokenType, requiredFeatures, options); + } catch (NetworkErrorException e) { + response.onError(Constants.ERROR_CODE_NETWORK_ERROR, e.getMessage()); + return; + } catch (UnsupportedOperationException e) { + response.onError(Constants.ERROR_CODE_UNSUPPORTED_OPERATION, + "addAccount not supported"); + return; + } + if (result != null) { + response.onResult(result); + } + } + + public void confirmPassword(IAccountAuthenticatorResponse response, + Account account, String password) throws RemoteException { + boolean result; + try { + result = AbstractAccountAuthenticator.this.confirmPassword( + new AccountAuthenticatorResponse(response), + account, password); + } catch (UnsupportedOperationException e) { + response.onError(Constants.ERROR_CODE_UNSUPPORTED_OPERATION, + "confirmPassword not supported"); + return; + } catch (NetworkErrorException e) { + response.onError(Constants.ERROR_CODE_NETWORK_ERROR, e.getMessage()); + return; + } + Bundle bundle = new Bundle(); + bundle.putBoolean(Constants.BOOLEAN_RESULT_KEY, result); + response.onResult(bundle); + } + + public void confirmCredentials(IAccountAuthenticatorResponse response, + Account account) throws RemoteException { + final Bundle result; + try { + result = AbstractAccountAuthenticator.this.confirmCredentials( + new AccountAuthenticatorResponse(response), account); + } catch (UnsupportedOperationException e) { + response.onError(Constants.ERROR_CODE_UNSUPPORTED_OPERATION, + "confirmCredentials not supported"); + return; + } + if (result != null) { + response.onResult(result); + } + } + + public void getAuthToken(IAccountAuthenticatorResponse response, + Account account, String authTokenType, Bundle loginOptions) + throws RemoteException { + try { + final Bundle result = AbstractAccountAuthenticator.this.getAuthToken( + new AccountAuthenticatorResponse(response), account, + authTokenType, loginOptions); + if (result != null) { + response.onResult(result); + } + } catch (UnsupportedOperationException e) { + response.onError(Constants.ERROR_CODE_UNSUPPORTED_OPERATION, + "getAuthToken not supported"); + } catch (NetworkErrorException e) { + response.onError(Constants.ERROR_CODE_NETWORK_ERROR, e.getMessage()); + } + } + + public void updateCredentials(IAccountAuthenticatorResponse response, Account account, + String authTokenType, Bundle loginOptions) throws RemoteException { + final Bundle result; + try { + result = AbstractAccountAuthenticator.this.updateCredentials( + new AccountAuthenticatorResponse(response), account, + authTokenType, loginOptions); + } catch (UnsupportedOperationException e) { + response.onError(Constants.ERROR_CODE_UNSUPPORTED_OPERATION, + "updateCredentials not supported"); + return; + } + if (result != null) { + response.onResult(result); + } + } + + public void editProperties(IAccountAuthenticatorResponse response, + String accountType) throws RemoteException { + final Bundle result; + try { + result = AbstractAccountAuthenticator.this.editProperties( + new AccountAuthenticatorResponse(response), accountType); + } catch (UnsupportedOperationException e) { + response.onError(Constants.ERROR_CODE_UNSUPPORTED_OPERATION, + "editProperties not supported"); + return; + } + if (result != null) { + response.onResult(result); + } + } + + public void hasFeatures(IAccountAuthenticatorResponse response, + Account account, String[] features) throws RemoteException { + final Bundle result; + try { + result = AbstractAccountAuthenticator.this.hasFeatures( + new AccountAuthenticatorResponse(response), account, features); + } catch (UnsupportedOperationException e) { + response.onError(Constants.ERROR_CODE_UNSUPPORTED_OPERATION, + "hasFeatures not supported"); + return; + } catch (NetworkErrorException e) { + response.onError(Constants.ERROR_CODE_NETWORK_ERROR, e.getMessage()); + return; + } + if (result != null) { + response.onResult(result); + } + } + } + + Transport mTransport = new Transport(); + + /** + * @return the IAccountAuthenticator binder transport object + */ + public final IAccountAuthenticator getIAccountAuthenticator() + { + return mTransport; + } + + /** + * Returns a Bundle that contains the Intent of the activity that can be used to edit the + * properties. In order to indicate success the activity should call response.setResult() + * with a non-null Bundle. + * @param response used to set the result for the request. If the Constants.INTENT_KEY + * is set in the bundle then this response field is to be used for sending future + * results if and when the Intent is started. + * @param accountType the AccountType whose properties are to be edited. + * @return a Bundle containing the result or the Intent to start to continue the request. + * If this is null then the request is considered to still be active and the result should + * sent later using response. + */ + public abstract Bundle editProperties(AccountAuthenticatorResponse response, + String accountType); + public abstract Bundle addAccount(AccountAuthenticatorResponse response, String accountType, + String authTokenType, String[] requiredFeatures, Bundle options) + throws NetworkErrorException; + /* @deprecated */ + public abstract boolean confirmPassword(AccountAuthenticatorResponse response, + Account account, String password) throws NetworkErrorException; + public abstract Bundle confirmCredentials(AccountAuthenticatorResponse response, + Account account); + public abstract Bundle getAuthToken(AccountAuthenticatorResponse response, + Account account, String authTokenType, Bundle loginOptions) + throws NetworkErrorException; + public abstract Bundle updateCredentials(AccountAuthenticatorResponse response, + Account account, String authTokenType, Bundle loginOptions); + public abstract Bundle hasFeatures(AccountAuthenticatorResponse response, + Account account, String[] features) throws NetworkErrorException; +} diff --git a/core/java/android/accounts/Account.aidl b/core/java/android/accounts/Account.aidl new file mode 100644 index 0000000..8752d99 --- /dev/null +++ b/core/java/android/accounts/Account.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.accounts; + +parcelable Account; diff --git a/core/java/android/accounts/Account.java b/core/java/android/accounts/Account.java new file mode 100644 index 0000000..30c91b0 --- /dev/null +++ b/core/java/android/accounts/Account.java @@ -0,0 +1,84 @@ +/* + * 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.os.Parcelable; +import android.os.Parcel; +import android.text.TextUtils; + +/** + * Value type that represents an Account in the {@link AccountManager}. This object is + * {@link Parcelable} and also overrides {@link #equals} and {@link #hashCode}, making it + * suitable for use as the key of a {@link java.util.Map} + */ +public class Account implements Parcelable { + public final String mName; + public final String mType; + + public boolean equals(Object o) { + if (o == this) return true; + if (!(o instanceof Account)) return false; + final Account other = (Account)o; + return mName.equals(other.mName) && mType.equals(other.mType); + } + + public int hashCode() { + int result = 17; + result = 31 * result + mName.hashCode(); + result = 31 * result + mType.hashCode(); + return result; + } + + public Account(String name, String type) { + if (TextUtils.isEmpty(name)) { + throw new IllegalArgumentException("the name must not be empty: " + name); + } + if (TextUtils.isEmpty(type)) { + throw new IllegalArgumentException("the type must not be empty: " + type); + } + mName = name; + mType = type; + } + + public Account(Parcel in) { + mName = in.readString(); + mType = in.readString(); + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mName); + dest.writeString(mType); + } + + public static final Creator<Account> CREATOR = new Creator<Account>() { + public Account createFromParcel(Parcel source) { + return new Account(source); + } + + public Account[] newArray(int size) { + return new Account[size]; + } + }; + + public String toString() { + return "Account {name=" + mName + ", type=" + mType + "}"; + } +} diff --git a/core/java/android/accounts/AccountAuthenticatorActivity.java b/core/java/android/accounts/AccountAuthenticatorActivity.java new file mode 100644 index 0000000..0319ab9 --- /dev/null +++ b/core/java/android/accounts/AccountAuthenticatorActivity.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.accounts; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; + +/** + * Base class for implementing an Activity that is used to help implement an + * AbstractAccountAuthenticator. If the AbstractAccountAuthenticator needs to return an Intent + * that is to be used to launch an Activity that needs to return results to satisfy an + * AbstractAccountAuthenticator request, it should store the AccountAuthenticatorResponse + * inside of the Intent as follows: + * <p> + * intent.putExtra(Constants.ACCOUNT_AUTHENTICATOR_RESPONSE_KEY, response); + * <p> + * The activity that it launches should extend the AccountAuthenticatorActivity. If this + * activity has a result that satisfies the original request it sets it via: + * <p> + * setAccountAuthenticatorResult(result) + * <p> + * This result will be sent as the result of the request when the activity finishes. If this + * is never set or if it is set to null then the request will be canceled when the activity + * finishes. + */ +public class AccountAuthenticatorActivity extends Activity { + private AccountAuthenticatorResponse mAccountAuthenticatorResponse = null; + private Bundle mResultBundle = null; + + /** + * Set the result that is to be sent as the result of the request that caused this + * Activity to be launched. If result is null or this method is never called then + * the request will be canceled. + * @param result this is returned as the result of the AbstractAccountAuthenticator request + */ + public final void setAccountAuthenticatorResult(Bundle result) { + mResultBundle = result; + } + + /** + * Retreives the AccountAuthenticatorResponse from either the intent of the icicle, if the + * icicle is non-zero. + * @param icicle the save instance data of this Activity, may be null + */ + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + + if (icicle == null) { + Intent intent = getIntent(); + mAccountAuthenticatorResponse = + intent.getParcelableExtra(Constants.ACCOUNT_AUTHENTICATOR_RESPONSE_KEY); + } else { + mAccountAuthenticatorResponse = + icicle.getParcelable(Constants.ACCOUNT_AUTHENTICATOR_RESPONSE_KEY); + } + + if (mAccountAuthenticatorResponse != null) { + mAccountAuthenticatorResponse.onRequestContinued(); + } + } + + /** + * Saves the AccountAuthenticatorResponse in the instance state. + * @param outState where to store any instance data + */ + protected void onSaveInstanceState(Bundle outState) { + outState.putParcelable(Constants.ACCOUNT_AUTHENTICATOR_RESPONSE_KEY, + mAccountAuthenticatorResponse); + super.onSaveInstanceState(outState); + } + + /** + * Sends the result or a Constants.ERROR_CODE_CANCELED error if a result isn't present. + */ + public void finish() { + if (mAccountAuthenticatorResponse != null) { + // send the result bundle back if set, otherwise send an error. + if (mResultBundle != null) { + mAccountAuthenticatorResponse.onResult(mResultBundle); + } else { + mAccountAuthenticatorResponse.onError(Constants.ERROR_CODE_CANCELED, "canceled"); + } + mAccountAuthenticatorResponse = null; + } + super.finish(); + } +} diff --git a/core/java/android/accounts/AccountAuthenticatorCache.java b/core/java/android/accounts/AccountAuthenticatorCache.java new file mode 100644 index 0000000..83aae3a --- /dev/null +++ b/core/java/android/accounts/AccountAuthenticatorCache.java @@ -0,0 +1,73 @@ +/* + * 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.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.content.pm.RegisteredServicesCache; +import android.content.res.XmlResourceParser; +import android.content.res.TypedArray; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.util.Log; +import android.util.AttributeSet; +import android.util.Xml; + +import java.io.IOException; +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import com.google.android.collect.Maps; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlPullParser; + +/** + * A cache of services that export the {@link IAccountAuthenticator} interface. This cache + * is built by interrogating the {@link PackageManager} and is updated as packages are added, + * removed and changed. The authenticators are referred to by their account type and + * are made available via the {@link RegisteredServicesCache#getServiceInfo} method. + * @hide + */ +/* package private */ class AccountAuthenticatorCache extends RegisteredServicesCache<String> { + private static final String TAG = "Account"; + + private static final String SERVICE_INTERFACE = "android.accounts.AccountAuthenticator"; + private static final String SERVICE_META_DATA = "android.accounts.AccountAuthenticator"; + private static final String ATTRIBUTES_NAME = "account-authenticator"; + + public AccountAuthenticatorCache(Context context) { + super(context, SERVICE_INTERFACE, SERVICE_META_DATA, ATTRIBUTES_NAME); + } + + public String parseServiceAttributes(AttributeSet attrs) { + TypedArray sa = mContext.getResources().obtainAttributes(attrs, + com.android.internal.R.styleable.AccountAuthenticator); + try { + return sa.getString(com.android.internal.R.styleable.AccountAuthenticator_accountType); + } finally { + sa.recycle(); + } + } +} diff --git a/core/java/android/accounts/AccountAuthenticatorResponse.java b/core/java/android/accounts/AccountAuthenticatorResponse.java new file mode 100644 index 0000000..7198046 --- /dev/null +++ b/core/java/android/accounts/AccountAuthenticatorResponse.java @@ -0,0 +1,82 @@ +/* + * 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.os.Bundle; +import android.os.Parcelable; +import android.os.Parcel; +import android.os.RemoteException; + +/** + * Object that wraps calls to an {@link IAccountAuthenticatorResponse} object. + * TODO: this interface is still in flux + */ +public class AccountAuthenticatorResponse implements Parcelable { + private IAccountAuthenticatorResponse mAccountAuthenticatorResponse; + + public AccountAuthenticatorResponse(IAccountAuthenticatorResponse response) { + mAccountAuthenticatorResponse = response; + } + + public AccountAuthenticatorResponse(Parcel parcel) { + mAccountAuthenticatorResponse = + IAccountAuthenticatorResponse.Stub.asInterface(parcel.readStrongBinder()); + } + + public void onResult(Bundle result) { + try { + mAccountAuthenticatorResponse.onResult(result); + } catch (RemoteException e) { + // this should never happen + } + } + + public void onRequestContinued() { + try { + mAccountAuthenticatorResponse.onRequestContinued(); + } catch (RemoteException e) { + // this should never happen + } + } + + public void onError(int errorCode, String errorMessage) { + try { + mAccountAuthenticatorResponse.onError(errorCode, errorMessage); + } catch (RemoteException e) { + // this should never happen + } + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeStrongBinder(mAccountAuthenticatorResponse.asBinder()); + } + + public static final Creator<AccountAuthenticatorResponse> CREATOR = + new Creator<AccountAuthenticatorResponse>() { + public AccountAuthenticatorResponse createFromParcel(Parcel source) { + return new AccountAuthenticatorResponse(source); + } + + public AccountAuthenticatorResponse[] newArray(int size) { + return new AccountAuthenticatorResponse[size]; + } + }; +} diff --git a/core/java/android/accounts/AccountManager.java b/core/java/android/accounts/AccountManager.java new file mode 100644 index 0000000..4fcaa88 --- /dev/null +++ b/core/java/android/accounts/AccountManager.java @@ -0,0 +1,1073 @@ +/* + * 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.app.Activity; +import android.content.Intent; +import android.content.Context; +import android.content.IntentFilter; +import android.content.BroadcastReceiver; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.RemoteException; +import android.os.Parcelable; +import android.util.Config; +import android.util.Log; + +import java.io.IOException; +import java.util.concurrent.Callable; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.TimeUnit; +import java.util.HashMap; +import java.util.Map; + +import com.google.android.collect.Maps; + +/** + * A class that helps with interactions with the AccountManagerService. It provides + * methods to allow for account, password, and authtoken management for all accounts on the + * device. Some of these calls are implemented with the help of the corresponding + * {@link IAccountAuthenticator} services. One accesses the {@link AccountManager} by calling: + * AccountManager accountManager = AccountManager.get(context); + * + * <p> + * TODO(fredq) this interface is still in flux + */ +public class AccountManager { + private static final String TAG = "AccountManager"; + + private final Context mContext; + private final IAccountManager mService; + private final Handler mMainHandler; + + /** + * @hide + */ + public AccountManager(Context context, IAccountManager service) { + mContext = context; + mService = service; + mMainHandler = new Handler(mContext.getMainLooper()); + } + + /** + * @hide used for testing only + */ + public AccountManager(Context context, IAccountManager service, Handler handler) { + mContext = context; + mService = service; + mMainHandler = handler; + } + + public static AccountManager get(Context context) { + return (AccountManager) context.getSystemService(Context.ACCOUNT_SERVICE); + } + + public String blockingGetPassword(Account account) { + ensureNotOnMainThread(); + try { + return mService.getPassword(account); + } catch (RemoteException e) { + // if this happens the entire runtime will restart + throw new RuntimeException(e); + } + } + + public Future1<String> getPassword(final Future1Callback<String> callback, + final Account account, final Handler handler) { + return startAsFuture(callback, handler, new Callable<String>() { + public String call() throws Exception { + return blockingGetPassword(account); + } + }); + } + + public String blockingGetUserData(Account account, String key) { + ensureNotOnMainThread(); + try { + return mService.getUserData(account, key); + } catch (RemoteException e) { + // if this happens the entire runtime will restart + throw new RuntimeException(e); + } + } + + public Future1<String> getUserData(Future1Callback<String> callback, + final Account account, final String key, Handler handler) { + return startAsFuture(callback, handler, new Callable<String>() { + public String call() throws Exception { + return blockingGetUserData(account, key); + } + }); + } + + public String[] blockingGetAuthenticatorTypes() { + ensureNotOnMainThread(); + try { + return mService.getAuthenticatorTypes(); + } catch (RemoteException e) { + // if this happens the entire runtime will restart + throw new RuntimeException(e); + } + } + + public Future1<String[]> getAuthenticatorTypes(Future1Callback<String[]> callback, + Handler handler) { + return startAsFuture(callback, handler, new Callable<String[]>() { + public String[] call() throws Exception { + return blockingGetAuthenticatorTypes(); + } + }); + } + + public Account[] blockingGetAccounts() { + ensureNotOnMainThread(); + try { + return mService.getAccounts(); + } catch (RemoteException e) { + // if this happens the entire runtime will restart + throw new RuntimeException(e); + } + } + + public Account[] blockingGetAccountsByType(String accountType) { + ensureNotOnMainThread(); + try { + return mService.getAccountsByType(accountType); + } catch (RemoteException e) { + // if this happens the entire runtime will restart + throw new RuntimeException(e); + } + } + + public Future1<Account[]> getAccounts(Future1Callback<Account[]> callback, Handler handler) { + return startAsFuture(callback, handler, new Callable<Account[]>() { + public Account[] call() throws Exception { + return blockingGetAccounts(); + } + }); + } + + public Future1<Account[]> getAccountsByType(Future1Callback<Account[]> callback, + final String type, Handler handler) { + return startAsFuture(callback, handler, new Callable<Account[]>() { + public Account[] call() throws Exception { + return blockingGetAccountsByType(type); + } + }); + } + + public boolean blockingAddAccountExplicitly(Account account, String password, Bundle extras) { + ensureNotOnMainThread(); + try { + return mService.addAccount(account, password, extras); + } catch (RemoteException e) { + // if this happens the entire runtime will restart + throw new RuntimeException(e); + } + } + + public Future1<Boolean> addAccountExplicitly(final Future1Callback<Boolean> callback, + final Account account, final String password, final Bundle extras, + final Handler handler) { + return startAsFuture(callback, handler, new Callable<Boolean>() { + public Boolean call() throws Exception { + return blockingAddAccountExplicitly(account, password, extras); + } + }); + } + + public void blockingRemoveAccount(Account account) { + ensureNotOnMainThread(); + try { + mService.removeAccount(account); + } catch (RemoteException e) { + // if this happens the entire runtime will restart + } + } + + public Future1<Void> removeAccount(Future1Callback<Void> callback, final Account account, + final Handler handler) { + return startAsFuture(callback, handler, new Callable<Void>() { + public Void call() throws Exception { + blockingRemoveAccount(account); + return null; + } + }); + } + + public void blockingInvalidateAuthToken(String accountType, String authToken) { + ensureNotOnMainThread(); + try { + mService.invalidateAuthToken(accountType, authToken); + } catch (RemoteException e) { + // if this happens the entire runtime will restart + } + } + + public Future1<Void> invalidateAuthToken(Future1Callback<Void> callback, + final String accountType, final String authToken, final Handler handler) { + return startAsFuture(callback, handler, new Callable<Void>() { + public Void call() throws Exception { + blockingInvalidateAuthToken(accountType, authToken); + return null; + } + }); + } + + public String blockingPeekAuthToken(Account account, String authTokenType) { + ensureNotOnMainThread(); + try { + return mService.peekAuthToken(account, authTokenType); + } catch (RemoteException e) { + // if this happens the entire runtime will restart + throw new RuntimeException(e); + } + } + + public Future1<String> peekAuthToken(Future1Callback<String> callback, + final Account account, final String authTokenType, final Handler handler) { + return startAsFuture(callback, handler, new Callable<String>() { + public String call() throws Exception { + return blockingPeekAuthToken(account, authTokenType); + } + }); + } + + public void blockingSetPassword(Account account, String password) { + ensureNotOnMainThread(); + try { + mService.setPassword(account, password); + } catch (RemoteException e) { + // if this happens the entire runtime will restart + } + } + + public Future1<Void> setPassword(Future1Callback<Void> callback, + final Account account, final String password, final Handler handler) { + return startAsFuture(callback, handler, new Callable<Void>() { + public Void call() throws Exception { + blockingSetPassword(account, password); + return null; + } + }); + } + + public void blockingClearPassword(Account account) { + ensureNotOnMainThread(); + try { + mService.clearPassword(account); + } catch (RemoteException e) { + // if this happens the entire runtime will restart + } + } + + public Future1<Void> clearPassword(final Future1Callback<Void> callback, final Account account, + final Handler handler) { + return startAsFuture(callback, handler, new Callable<Void>() { + public Void call() throws Exception { + blockingClearPassword(account); + return null; + } + }); + } + + public void blockingSetUserData(Account account, String key, String value) { + ensureNotOnMainThread(); + try { + mService.setUserData(account, key, value); + } catch (RemoteException e) { + // if this happens the entire runtime will restart + } + } + + public Future1<Void> setUserData(Future1Callback<Void> callback, + final Account account, final String key, final String value, final Handler handler) { + return startAsFuture(callback, handler, new Callable<Void>() { + public Void call() throws Exception { + blockingSetUserData(account, key, value); + return null; + } + }); + } + + public void blockingSetAuthToken(Account account, String authTokenType, String authToken) { + ensureNotOnMainThread(); + try { + mService.setAuthToken(account, authTokenType, authToken); + } catch (RemoteException e) { + // if this happens the entire runtime will restart + } + } + + public Future1<Void> setAuthToken(Future1Callback<Void> callback, + final Account account, final String authTokenType, final String authToken, + final Handler handler) { + return startAsFuture(callback, handler, new Callable<Void>() { + public Void call() throws Exception { + blockingSetAuthToken(account, authTokenType, authToken); + return null; + } + }); + } + + public String blockingGetAuthToken(Account account, String authTokenType, + boolean notifyAuthFailure) + throws OperationCanceledException, IOException, AuthenticatorException { + ensureNotOnMainThread(); + Bundle bundle = getAuthToken(account, authTokenType, notifyAuthFailure, null /* callback */, + null /* handler */).getResult(); + return bundle.getString(Constants.AUTHTOKEN_KEY); + } + + /** + * Request the auth token for this account/authTokenType. If this succeeds then the + * auth token will then be passed to the activity. If this results in an authentication + * failure then a login intent will be returned that can be invoked to prompt the user to + * update their credentials. This login activity will return the auth token to the calling + * activity. If activity is null then the login intent will not be invoked. + * + * @param account the account whose auth token should be retrieved + * @param authTokenType the auth token type that should be retrieved + * @param loginOptions + * @param activity the activity to launch the login intent, if necessary, and to which + */ + public Future2 getAuthToken( + final Account account, final String authTokenType, final Bundle loginOptions, + final Activity activity, Future2Callback callback, Handler handler) { + if (activity == null) throw new IllegalArgumentException("activity is null"); + if (authTokenType == null) throw new IllegalArgumentException("authTokenType is null"); + return new AmsTask(activity, handler, callback) { + public void doWork() throws RemoteException { + mService.getAuthToken(mResponse, account, authTokenType, + false /* notifyOnAuthFailure */, true /* expectActivityLaunch */, + loginOptions); + } + }.start(); + } + + public Future2 getAuthToken( + final Account account, final String authTokenType, final boolean notifyAuthFailure, + Future2Callback callback, Handler handler) { + if (account == null) throw new IllegalArgumentException("account is null"); + if (authTokenType == null) throw new IllegalArgumentException("authTokenType is null"); + return new AmsTask(null, handler, callback) { + public void doWork() throws RemoteException { + mService.getAuthToken(mResponse, account, authTokenType, + notifyAuthFailure, false /* expectActivityLaunch */, null /* options */); + } + }.start(); + } + + public Future2 addAccount(final String accountType, + final String authTokenType, final String[] requiredFeatures, + final Bundle addAccountOptions, + final Activity activity, Future2Callback callback, Handler handler) { + return new AmsTask(activity, handler, callback) { + public void doWork() throws RemoteException { + mService.addAcount(mResponse, accountType, authTokenType, + requiredFeatures, activity != null, addAccountOptions); + } + }.start(); + } + + /** @deprecated use {@link #confirmCredentials} instead */ + public Future1<Boolean> confirmPassword(final Account account, final String password, + Future1Callback<Boolean> callback, Handler handler) { + return new AMSTaskBoolean(handler, callback) { + public void doWork() throws RemoteException { + mService.confirmPassword(response, account, password); + } + }; + } + + public Account[] blockingGetAccountsWithTypeAndFeatures(String type, String[] features) + throws AuthenticatorException, IOException, OperationCanceledException { + Future2 future = getAccountsWithTypeAndFeatures(type, features, + null /* callback */, null /* handler */); + Bundle result = future.getResult(); + Parcelable[] accountsTemp = result.getParcelableArray(Constants.ACCOUNTS_KEY); + if (accountsTemp == null) { + throw new AuthenticatorException("accounts should not be null"); + } + Account[] accounts = new Account[accountsTemp.length]; + for (int i = 0; i < accountsTemp.length; i++) { + accounts[i] = (Account) accountsTemp[i]; + } + return accounts; + } + + public Future2 getAccountsWithTypeAndFeatures( + final String type, final String[] features, + Future2Callback callback, Handler handler) { + if (type == null) throw new IllegalArgumentException("type is null"); + return new AmsTask(null /* activity */, handler, callback) { + public void doWork() throws RemoteException { + mService.getAccountsByTypeAndFeatures(mResponse, type, features); + } + }.start(); + } + + public Future2 confirmCredentials(final Account account, final Activity activity, + final Future2Callback callback, + final Handler handler) { + return new AmsTask(activity, handler, callback) { + public void doWork() throws RemoteException { + mService.confirmCredentials(mResponse, account, activity != null); + } + }.start(); + } + + public Future2 updateCredentials(final Account account, final String authTokenType, + final Bundle loginOptions, final Activity activity, + final Future2Callback callback, + final Handler handler) { + return new AmsTask(activity, handler, callback) { + public void doWork() throws RemoteException { + mService.updateCredentials(mResponse, account, authTokenType, activity != null, + loginOptions); + } + }.start(); + } + + public Future2 editProperties(final String accountType, final Activity activity, + final Future2Callback callback, + final Handler handler) { + return new AmsTask(activity, handler, callback) { + public void doWork() throws RemoteException { + mService.editProperties(mResponse, accountType, activity != null); + } + }.start(); + } + + private void ensureNotOnMainThread() { + final Looper looper = Looper.myLooper(); + if (looper != null && looper == mContext.getMainLooper()) { + // We really want to throw an exception here, but GTalkService exercises this + // path quite a bit and needs some serious rewrite in order to work properly. + //noinspection ThrowableInstanceNeverThrow +// Log.e(TAG, "calling this from your main thread can lead to deadlock and/or ANRs", +// new Exception()); + // TODO(fredq) remove the log and throw this exception when the callers are fixed +// throw new IllegalStateException( +// "calling this from your main thread can lead to deadlock"); + } + } + + private void postToHandler(Handler handler, final Future2Callback callback, + final Future2 future) { + handler = handler == null ? mMainHandler : handler; + handler.post(new Runnable() { + public void run() { + callback.run(future); + } + }); + } + + private void postToHandler(Handler handler, final OnAccountsUpdatedListener listener, + final Account[] accounts) { + handler = handler == null ? mMainHandler : handler; + handler.post(new Runnable() { + public void run() { + listener.onAccountsUpdated(accounts); + } + }); + } + + private <V> void postToHandler(Handler handler, final Future1Callback<V> callback, + final Future1<V> future) { + handler = handler == null ? mMainHandler : handler; + handler.post(new Runnable() { + public void run() { + callback.run(future); + } + }); + } + + private <V> Future1<V> startAsFuture(Future1Callback<V> callback, Handler handler, + Callable<V> callable) { + final FutureTaskWithCallback<V> task = + new FutureTaskWithCallback<V>(callback, callable, handler); + new Thread(task).start(); + return task; + } + + private class FutureTaskWithCallback<V> extends FutureTask<V> implements Future1<V> { + final Future1Callback<V> mCallback; + final Handler mHandler; + + public FutureTaskWithCallback(Future1Callback<V> callback, Callable<V> callable, + Handler handler) { + super(callable); + mCallback = callback; + mHandler = handler; + } + + protected void done() { + if (mCallback != null) { + postToHandler(mHandler, mCallback, this); + } + } + + public V internalGetResult(Long timeout, TimeUnit unit) throws OperationCanceledException { + try { + if (timeout == null) { + return get(); + } else { + return get(timeout, unit); + } + } catch (InterruptedException e) { + // we will cancel the task below + } catch (CancellationException e) { + // we will cancel the task below + } catch (TimeoutException e) { + // we will cancel the task below + } catch (ExecutionException e) { + // this should never happen + throw new IllegalStateException(e.getCause()); + } finally { + cancel(true /* interruptIfRunning */); + } + throw new OperationCanceledException(); + } + + public V getResult() throws OperationCanceledException { + return internalGetResult(null, null); + } + + public V getResult(long timeout, TimeUnit unit) throws OperationCanceledException { + return internalGetResult(null, null); + } + } + + private abstract class AmsTask extends FutureTask<Bundle> implements Future2 { + final IAccountManagerResponse mResponse; + final Handler mHandler; + final Future2Callback mCallback; + final Activity mActivity; + final Thread mThread; + public AmsTask(Activity activity, Handler handler, Future2Callback callback) { + super(new Callable<Bundle>() { + public Bundle call() throws Exception { + throw new IllegalStateException("this should never be called"); + } + }); + + mHandler = handler; + mCallback = callback; + mActivity = activity; + mResponse = new Response(); + mThread = new Thread(new Runnable() { + public void run() { + try { + doWork(); + } catch (RemoteException e) { + // never happens + } + } + }, "AmsTask"); + } + + public final Future2 start() { + mThread.start(); + return this; + } + + public abstract void doWork() throws RemoteException; + + private Bundle internalGetResult(Long timeout, TimeUnit unit) + throws OperationCanceledException, IOException, AuthenticatorException { + try { + if (timeout == null) { + return get(); + } else { + return get(timeout, unit); + } + } catch (CancellationException e) { + throw new OperationCanceledException(); + } catch (TimeoutException e) { + // fall through and cancel + } catch (InterruptedException e) { + // fall through and cancel + } catch (ExecutionException e) { + final Throwable cause = e.getCause(); + if (cause instanceof IOException) { + throw (IOException) cause; + } else if (cause instanceof UnsupportedOperationException) { + throw new AuthenticatorException(cause); + } else if (cause instanceof AuthenticatorException) { + throw (AuthenticatorException) cause; + } else if (cause instanceof RuntimeException) { + throw (RuntimeException) cause; + } else if (cause instanceof Error) { + throw (Error) cause; + } else { + throw new IllegalStateException(cause); + } + } finally { + cancel(true /* interrupt if running */); + } + throw new OperationCanceledException(); + } + + public Bundle getResult() + throws OperationCanceledException, IOException, AuthenticatorException { + return internalGetResult(null, null); + } + + public Bundle getResult(long timeout, TimeUnit unit) + throws OperationCanceledException, IOException, AuthenticatorException { + return internalGetResult(timeout, unit); + } + + protected void done() { + if (mCallback != null) { + postToHandler(mHandler, mCallback, this); + } + } + + /** Handles the responses from the AccountManager */ + private class Response extends IAccountManagerResponse.Stub { + public void onResult(Bundle bundle) { + Intent intent = bundle.getParcelable("intent"); + if (intent != null && mActivity != null) { + // since the user provided an Activity we will silently start intents + // that we see + mActivity.startActivity(intent); + // leave the Future running to wait for the real response to this request + } else { + set(bundle); + } + } + + public void onError(int code, String message) { + if (code == Constants.ERROR_CODE_CANCELED) { + // the authenticator indicated that this request was canceled, do so now + cancel(true /* mayInterruptIfRunning */); + return; + } + setException(convertErrorToException(code, message)); + } + } + + } + + private abstract class AMSTaskBoolean extends FutureTask<Boolean> implements Future1<Boolean> { + final IAccountManagerResponse response; + final Handler mHandler; + final Future1Callback<Boolean> mCallback; + public AMSTaskBoolean(Handler handler, Future1Callback<Boolean> callback) { + super(new Callable<Boolean>() { + public Boolean call() throws Exception { + throw new IllegalStateException("this should never be called"); + } + }); + + mHandler = handler; + mCallback = callback; + response = new Response(); + + new Thread(new Runnable() { + public void run() { + try { + doWork(); + } catch (RemoteException e) { + // never happens + } + } + }).start(); + } + + public abstract void doWork() throws RemoteException; + + + protected void done() { + if (mCallback != null) { + postToHandler(mHandler, mCallback, this); + } + } + + private Boolean internalGetResult(Long timeout, TimeUnit unit) { + try { + if (timeout == null) { + return get(); + } else { + return get(timeout, unit); + } + } catch (InterruptedException e) { + // fall through and cancel + } catch (TimeoutException e) { + // fall through and cancel + } catch (CancellationException e) { + return false; + } catch (ExecutionException e) { + final Throwable cause = e.getCause(); + if (cause instanceof IOException) { + return false; + } else if (cause instanceof UnsupportedOperationException) { + return false; + } else if (cause instanceof AuthenticatorException) { + return false; + } else if (cause instanceof RuntimeException) { + throw (RuntimeException) cause; + } else if (cause instanceof Error) { + throw (Error) cause; + } else { + throw new IllegalStateException(cause); + } + } finally { + cancel(true /* interrupt if running */); + } + return false; + } + + public Boolean getResult() throws OperationCanceledException { + return internalGetResult(null, null); + } + + public Boolean getResult(long timeout, TimeUnit unit) throws OperationCanceledException { + return internalGetResult(timeout, unit); + } + + private class Response extends IAccountManagerResponse.Stub { + public void onResult(Bundle bundle) { + try { + if (bundle.containsKey(Constants.BOOLEAN_RESULT_KEY)) { + set(bundle.getBoolean(Constants.BOOLEAN_RESULT_KEY)); + return; + } + } catch (ClassCastException e) { + // we will set the exception below + } + onError(Constants.ERROR_CODE_INVALID_RESPONSE, "no result in response"); + } + + public void onError(int code, String message) { + if (code == Constants.ERROR_CODE_CANCELED) { + cancel(true /* mayInterruptIfRunning */); + return; + } + setException(convertErrorToException(code, message)); + } + } + + } + + private Exception convertErrorToException(int code, String message) { + if (code == Constants.ERROR_CODE_NETWORK_ERROR) { + return new IOException(message); + } + + if (code == Constants.ERROR_CODE_UNSUPPORTED_OPERATION) { + return new UnsupportedOperationException(message); + } + + if (code == Constants.ERROR_CODE_INVALID_RESPONSE) { + return new AuthenticatorException(message); + } + + if (code == Constants.ERROR_CODE_BAD_ARGUMENTS) { + return new IllegalArgumentException(message); + } + + return new AuthenticatorException(message); + } + + private class GetAuthTokenByTypeAndFeaturesTask extends AmsTask implements Future2Callback { + GetAuthTokenByTypeAndFeaturesTask(final String accountType, final String authTokenType, + final String[] features, Activity activityForPrompting, + final Bundle addAccountOptions, final Bundle loginOptions, + Future2Callback callback, Handler handler) { + super(activityForPrompting, handler, callback); + if (accountType == null) throw new IllegalArgumentException("account type is null"); + mAccountType = accountType; + mAuthTokenType = authTokenType; + mFeatures = features; + mAddAccountOptions = addAccountOptions; + mLoginOptions = loginOptions; + mMyCallback = this; + } + volatile Future2 mFuture = null; + final String mAccountType; + final String mAuthTokenType; + final String[] mFeatures; + final Bundle mAddAccountOptions; + final Bundle mLoginOptions; + final Future2Callback mMyCallback; + + public void doWork() throws RemoteException { + getAccountsWithTypeAndFeatures(mAccountType, mFeatures, new Future2Callback() { + public void run(Future2 future) { + Bundle getAccountsResult; + try { + getAccountsResult = future.getResult(); + } catch (OperationCanceledException e) { + setException(e); + return; + } catch (IOException e) { + setException(e); + return; + } catch (AuthenticatorException e) { + setException(e); + return; + } + + Parcelable[] accounts = + getAccountsResult.getParcelableArray(Constants.ACCOUNTS_KEY); + if (accounts.length == 0) { + if (mActivity != null) { + // no accounts, add one now. pretend that the user directly + // made this request + mFuture = addAccount(mAccountType, mAuthTokenType, mFeatures, + mAddAccountOptions, mActivity, mMyCallback, mHandler); + } else { + // send result since we can't prompt to add an account + Bundle result = new Bundle(); + result.putString(Constants.ACCOUNT_NAME_KEY, null); + result.putString(Constants.ACCOUNT_TYPE_KEY, null); + result.putString(Constants.AUTHTOKEN_KEY, null); + try { + mResponse.onResult(result); + } catch (RemoteException e) { + // this will never happen + } + // we are done + } + } else if (accounts.length == 1) { + // have a single account, return an authtoken for it + if (mActivity == null) { + mFuture = getAuthToken((Account) accounts[0], mAuthTokenType, + false /* notifyAuthFailure */, mMyCallback, mHandler); + } else { + mFuture = getAuthToken((Account) accounts[0], + mAuthTokenType, mLoginOptions, + mActivity, mMyCallback, mHandler); + } + } else { + if (mActivity != null) { + IAccountManagerResponse chooseResponse = + new IAccountManagerResponse.Stub() { + public void onResult(Bundle value) throws RemoteException { + Account account = new Account( + value.getString(Constants.ACCOUNT_NAME_KEY), + value.getString(Constants.ACCOUNT_TYPE_KEY)); + mFuture = getAuthToken(account, mAuthTokenType, mLoginOptions, + mActivity, mMyCallback, mHandler); + } + + public void onError(int errorCode, String errorMessage) + throws RemoteException { + mResponse.onError(errorCode, errorMessage); + } + }; + // have many accounts, launch the chooser + Intent intent = new Intent(); + intent.setClassName("android", + "android.accounts.ChooseAccountActivity"); + intent.putExtra(Constants.ACCOUNTS_KEY, accounts); + intent.putExtra(Constants.ACCOUNT_MANAGER_RESPONSE_KEY, + new AccountManagerResponse(chooseResponse)); + mActivity.startActivity(intent); + // the result will arrive via the IAccountManagerResponse + } else { + // send result since we can't prompt to select an account + Bundle result = new Bundle(); + result.putString(Constants.ACCOUNTS_KEY, null); + try { + mResponse.onResult(result); + } catch (RemoteException e) { + // this will never happen + } + // we are done + } + } + }}, mHandler); + } + + + + // TODO(fredq) pass through the calls to our implemention of Future2 to the underlying + // future that we create. We need to do things like have cancel cancel the mFuture, if set + // or to cause this to be canceled if mFuture isn't set. + // Once this is done then getAuthTokenByFeatures can be changed to return a Future2. + + public void run(Future2 future) { + try { + set(future.get()); + } catch (InterruptedException e) { + cancel(true); + } catch (CancellationException e) { + cancel(true); + } catch (ExecutionException e) { + setException(e.getCause()); + } + } + } + + public void getAuthTokenByFeatures( + final String accountType, final String authTokenType, final String[] features, + final Activity activityForPrompting, final Bundle addAccountOptions, + final Bundle loginOptions, + final Future2Callback callback, final Handler handler) { + if (accountType == null) throw new IllegalArgumentException("account type is null"); + if (authTokenType == null) throw new IllegalArgumentException("authTokenType is null"); + new GetAuthTokenByTypeAndFeaturesTask(accountType, authTokenType, features, + activityForPrompting, addAccountOptions, loginOptions, callback, handler).start(); + } + + private final HashMap<OnAccountsUpdatedListener, Handler> mAccountsUpdatedListeners = + Maps.newHashMap(); + + // These variable are only used from the LOGIN_ACCOUNTS_CHANGED_ACTION BroadcastReceiver + // and its getAccounts() callback which are both invoked only on the main thread. As a + // result we don't need to protect against concurrent accesses and any changes are guaranteed + // to be visible when used. Basically, these two variables are thread-confined. + private Future1<Account[]> mAccountsLookupFuture = null; + private boolean mAccountLookupPending = false; + + /** + * BroadcastReceiver that listens for the LOGIN_ACCOUNTS_CHANGED_ACTION intent + * so that it can read the updated list of accounts and send them to the listener + * in mAccountsUpdatedListeners. + */ + private final BroadcastReceiver mAccountsChangedBroadcastReceiver = new BroadcastReceiver() { + public void onReceive(final Context context, final Intent intent) { + if (mAccountsLookupFuture != null) { + // an accounts lookup is already in progress, + // don't bother starting another request + mAccountLookupPending = true; + return; + } + // initiate a read of the accounts + mAccountsLookupFuture = getAccounts(new Future1Callback<Account[]>() { + public void run(Future1<Account[]> future) { + // clear the future so that future receives will try the lookup again + mAccountsLookupFuture = null; + + // get the accounts array + Account[] accounts; + try { + accounts = future.getResult(); + } catch (OperationCanceledException e) { + // this should never happen, but if it does pretend we got another + // accounts changed broadcast + if (Config.LOGD) { + Log.d(TAG, "the accounts lookup for listener notifications was " + + "canceled, try again by simulating the receipt of " + + "a LOGIN_ACCOUNTS_CHANGED_ACTION broadcast"); + } + onReceive(context, intent); + return; + } + + // send the result to the listeners + synchronized (mAccountsUpdatedListeners) { + for (Map.Entry<OnAccountsUpdatedListener, Handler> entry : + mAccountsUpdatedListeners.entrySet()) { + Account[] accountsCopy = new Account[accounts.length]; + // send the listeners a copy to make sure that one doesn't + // change what another sees + System.arraycopy(accounts, 0, accountsCopy, 0, accountsCopy.length); + postToHandler(entry.getValue(), entry.getKey(), accountsCopy); + } + } + + // If mAccountLookupPending was set when the account lookup finished it + // means that we had previously ignored a LOGIN_ACCOUNTS_CHANGED_ACTION + // intent because a lookup was already in progress. Now that we are done + // with this lookup and notification pretend that another intent + // was received by calling onReceive() directly. + if (mAccountLookupPending) { + mAccountLookupPending = false; + onReceive(context, intent); + return; + } + } + }, mMainHandler); + } + }; + + /** + * Add a {@link OnAccountsUpdatedListener} to this instance of the {@link AccountManager}. + * The listener is guaranteed to be invoked on the thread of the Handler that is passed + * in or the main thread's Handler if handler is null. + * @param listener the listener to add + * @param handler the Handler whose thread will be used to invoke the listener. If null + * the AccountManager context's main thread will be used. + * @param updateImmediately if true then the listener will be invoked as a result of this + * call. + * @throws IllegalArgumentException if listener is null + * @throws IllegalStateException if listener was already added + */ + public void addOnAccountsUpdatedListener(final OnAccountsUpdatedListener listener, + Handler handler, boolean updateImmediately) { + if (listener == null) { + throw new IllegalArgumentException("the listener is null"); + } + synchronized (mAccountsUpdatedListeners) { + if (mAccountsUpdatedListeners.containsKey(listener)) { + throw new IllegalStateException("this listener is already added"); + } + final boolean wasEmpty = mAccountsUpdatedListeners.isEmpty(); + + mAccountsUpdatedListeners.put(listener, handler); + + if (wasEmpty) { + // Register a broadcast receiver to monitor account changes + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(Constants.LOGIN_ACCOUNTS_CHANGED_ACTION); + mContext.registerReceiver(mAccountsChangedBroadcastReceiver, intentFilter); + } + } + + if (updateImmediately) { + getAccounts(new Future1Callback<Account[]>() { + public void run(Future1<Account[]> future) { + try { + listener.onAccountsUpdated(future.getResult()); + } catch (OperationCanceledException e) { + // ignore + } + } + }, handler); + } + } + + /** + * Remove an {@link OnAccountsUpdatedListener} that was previously registered with + * {@link #addOnAccountsUpdatedListener}. + * @param listener the listener to remove + * @throws IllegalArgumentException if listener is null + * @throws IllegalStateException if listener was not already added + */ + public void removeOnAccountsUpdatedListener(OnAccountsUpdatedListener listener) { + if (listener == null) { + throw new IllegalArgumentException("the listener is null"); + } + synchronized (mAccountsUpdatedListeners) { + if (mAccountsUpdatedListeners.remove(listener) == null) { + throw new IllegalStateException("this listener was not previously added"); + } + if (mAccountsUpdatedListeners.isEmpty()) { + mContext.unregisterReceiver(mAccountsChangedBroadcastReceiver); + } + } + } +} diff --git a/core/java/android/accounts/AccountManagerResponse.java b/core/java/android/accounts/AccountManagerResponse.java new file mode 100644 index 0000000..25371fd --- /dev/null +++ b/core/java/android/accounts/AccountManagerResponse.java @@ -0,0 +1,74 @@ +/* + * 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.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.RemoteException; + +/** + * Object that wraps calls to an {@link android.accounts.IAccountManagerResponse} object. + * @hide + */ +public class AccountManagerResponse implements Parcelable { + private IAccountManagerResponse mResponse; + + public AccountManagerResponse(IAccountManagerResponse response) { + mResponse = response; + } + + public AccountManagerResponse(Parcel parcel) { + mResponse = + IAccountManagerResponse.Stub.asInterface(parcel.readStrongBinder()); + } + + public void onResult(Bundle result) { + try { + mResponse.onResult(result); + } catch (RemoteException e) { + // this should never happen + } + } + + public void onError(int errorCode, String errorMessage) { + try { + mResponse.onError(errorCode, errorMessage); + } catch (RemoteException e) { + // this should never happen + } + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeStrongBinder(mResponse.asBinder()); + } + + public static final Creator<AccountManagerResponse> CREATOR = + new Creator<AccountManagerResponse>() { + public AccountManagerResponse createFromParcel(Parcel source) { + return new AccountManagerResponse(source); + } + + public AccountManagerResponse[] newArray(int size) { + return new AccountManagerResponse[size]; + } + }; +}
\ No newline at end of file diff --git a/core/java/android/accounts/AccountManagerService.java b/core/java/android/accounts/AccountManagerService.java new file mode 100644 index 0000000..545241f --- /dev/null +++ b/core/java/android/accounts/AccountManagerService.java @@ -0,0 +1,1185 @@ +/* + * 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.BroadcastReceiver; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.database.Cursor; +import android.database.DatabaseUtils; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.os.Bundle; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.RemoteException; +import android.os.SystemClock; +import android.telephony.TelephonyManager; +import android.text.TextUtils; +import android.util.Log; +import android.app.PendingIntent; +import android.app.NotificationManager; +import android.app.Notification; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; + +import com.android.internal.telephony.TelephonyIntents; +import com.android.internal.R; + +/** + * A system service that provides account, password, and authtoken management for all + * accounts on the device. Some of these calls are implemented with the help of the corresponding + * {@link IAccountAuthenticator} services. This service is not accessed by users directly, + * instead one uses an instance of {@link AccountManager}, which can be accessed as follows: + * AccountManager accountManager = + * (AccountManager)context.getSystemService(Context.ACCOUNT_SERVICE) + * @hide + */ +public class AccountManagerService extends IAccountManager.Stub { + private static final String TAG = "AccountManagerService"; + + private static final int TIMEOUT_DELAY_MS = 1000 * 60; + private static final String DATABASE_NAME = "accounts.db"; + private static final int DATABASE_VERSION = 2; + + private final Context mContext; + + private HandlerThread mMessageThread; + private final MessageHandler mMessageHandler; + + // 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; + + private static final String TABLE_ACCOUNTS = "accounts"; + private static final String ACCOUNTS_ID = "_id"; + private static final String ACCOUNTS_NAME = "name"; + private static final String ACCOUNTS_TYPE = "type"; + private static final String ACCOUNTS_PASSWORD = "password"; + + private static final String TABLE_AUTHTOKENS = "authtokens"; + private static final String AUTHTOKENS_ID = "_id"; + private static final String AUTHTOKENS_ACCOUNTS_ID = "accounts_id"; + private static final String AUTHTOKENS_TYPE = "type"; + private static final String AUTHTOKENS_AUTHTOKEN = "authtoken"; + + private static final String TABLE_EXTRAS = "extras"; + private static final String EXTRAS_ID = "_id"; + private static final String EXTRAS_ACCOUNTS_ID = "accounts_id"; + private static final String EXTRAS_KEY = "key"; + private static final String EXTRAS_VALUE = "value"; + + private static final String TABLE_META = "meta"; + private static final String META_KEY = "key"; + private static final String META_VALUE = "value"; + + private static final String[] ACCOUNT_NAME_TYPE_PROJECTION = + new String[]{ACCOUNTS_ID, ACCOUNTS_NAME, ACCOUNTS_TYPE}; + private static final Intent ACCOUNTS_CHANGED_INTENT = + new Intent(Constants.LOGIN_ACCOUNTS_CHANGED_ACTION); + + private final LinkedHashMap<String, Session> mSessions = new LinkedHashMap<String, Session>(); + private static final int NOTIFICATION_ID = 234; + + public class AuthTokenKey { + public final Account mAccount; + public final String mAuthTokenType; + private final int mHashCode; + + public AuthTokenKey(Account account, String authTokenType) { + mAccount = account; + mAuthTokenType = authTokenType; + mHashCode = computeHashCode(); + } + + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (!(o instanceof AuthTokenKey)) { + return false; + } + AuthTokenKey other = (AuthTokenKey)o; + if (!mAccount.equals(other.mAccount)) { + return false; + } + return (mAuthTokenType == null) + ? other.mAuthTokenType == null + : mAuthTokenType.equals(other.mAuthTokenType); + } + + private int computeHashCode() { + int result = 17; + result = 31 * result + mAccount.hashCode(); + result = 31 * result + ((mAuthTokenType == null) ? 0 : mAuthTokenType.hashCode()); + return result; + } + + public int hashCode() { + return mHashCode; + } + } + + public AccountManagerService(Context context) { + mContext = context; + + mOpenHelper = new DatabaseHelper(mContext); + + mMessageThread = new HandlerThread("AccountManagerService"); + mMessageThread.start(); + mMessageHandler = new MessageHandler(mMessageThread.getLooper()); + + mAuthenticatorCache = new AccountAuthenticatorCache(mContext); + mBindHelper = new AuthenticatorBindHelper(mContext, mAuthenticatorCache, mMessageHandler, + MESSAGE_CONNECTED, MESSAGE_DISCONNECTED); + + mSimWatcher = new SimWatcher(mContext); + } + + public String getPassword(Account account) { + long identityToken = clearCallingIdentity(); + try { + SQLiteDatabase db = mOpenHelper.getReadableDatabase(); + Cursor cursor = db.query(TABLE_ACCOUNTS, new String[]{ACCOUNTS_PASSWORD}, + ACCOUNTS_NAME + "=? AND " + ACCOUNTS_TYPE+ "=?", + new String[]{account.mName, account.mType}, null, null, null); + try { + if (cursor.moveToNext()) { + return cursor.getString(0); + } + return null; + } finally { + cursor.close(); + } + } finally { + restoreCallingIdentity(identityToken); + } + } + + public String getUserData(Account account, String key) { + long identityToken = clearCallingIdentity(); + try { + SQLiteDatabase db = mOpenHelper.getReadableDatabase(); + db.beginTransaction(); + try { + long accountId = getAccountId(db, account); + if (accountId < 0) { + return null; + } + Cursor cursor = db.query(TABLE_EXTRAS, new String[]{EXTRAS_VALUE}, + EXTRAS_ACCOUNTS_ID + "=" + accountId + " AND " + EXTRAS_KEY + "=?", + new String[]{key}, null, null, null); + try { + if (cursor.moveToNext()) { + return cursor.getString(0); + } + return null; + } finally { + cursor.close(); + } + } finally { + db.setTransactionSuccessful(); + db.endTransaction(); + } + } finally { + restoreCallingIdentity(identityToken); + } + } + + public String[] getAuthenticatorTypes() { + long identityToken = clearCallingIdentity(); + try { + Collection<AccountAuthenticatorCache.ServiceInfo<String>> authenticatorCollection = + mAuthenticatorCache.getAllServices(); + String[] types = new String[authenticatorCollection.size()]; + int i = 0; + for (AccountAuthenticatorCache.ServiceInfo<String> authenticator + : authenticatorCollection) { + types[i] = authenticator.type; + i++; + } + return types; + } finally { + restoreCallingIdentity(identityToken); + } + } + + public Account[] getAccounts() { + long identityToken = clearCallingIdentity(); + try { + return getAccountsByType(null); + } finally { + restoreCallingIdentity(identityToken); + } + } + + public Account[] getAccountsByType(String accountType) { + long identityToken = clearCallingIdentity(); + try { + SQLiteDatabase db = mOpenHelper.getReadableDatabase(); + + final String selection = accountType == null ? null : (ACCOUNTS_TYPE + "=?"); + final String[] selectionArgs = accountType == null ? null : new String[]{accountType}; + Cursor cursor = db.query(TABLE_ACCOUNTS, ACCOUNT_NAME_TYPE_PROJECTION, + selection, selectionArgs, null, null, null); + try { + int i = 0; + Account[] accounts = new Account[cursor.getCount()]; + while (cursor.moveToNext()) { + accounts[i] = new Account(cursor.getString(1), cursor.getString(2)); + i++; + } + return accounts; + } finally { + cursor.close(); + } + } finally { + restoreCallingIdentity(identityToken); + } + } + + public boolean addAccount(Account account, String password, Bundle extras) { + // fails if the account already exists + long identityToken = clearCallingIdentity(); + try { + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + db.beginTransaction(); + try { + long numMatches = DatabaseUtils.longForQuery(db, + "select count(*) from " + TABLE_ACCOUNTS + + " WHERE " + ACCOUNTS_NAME + "=? AND " + ACCOUNTS_TYPE+ "=?", + new String[]{account.mName, account.mType}); + if (numMatches > 0) { + return false; + } + ContentValues values = new ContentValues(); + values.put(ACCOUNTS_NAME, account.mName); + values.put(ACCOUNTS_TYPE, account.mType); + values.put(ACCOUNTS_PASSWORD, password); + long accountId = db.insert(TABLE_ACCOUNTS, ACCOUNTS_NAME, values); + if (accountId < 0) { + return false; + } + if (extras != null) { + for (String key : extras.keySet()) { + final String value = extras.getString(key); + if (insertExtra(db, accountId, key, value) < 0) { + return false; + } + } + } + db.setTransactionSuccessful(); + sendAccountsChangedBroadcast(); + return true; + } finally { + db.endTransaction(); + } + } finally { + restoreCallingIdentity(identityToken); + } + } + + private long insertExtra(SQLiteDatabase db, long accountId, String key, String value) { + ContentValues values = new ContentValues(); + values.put(EXTRAS_KEY, key); + values.put(EXTRAS_ACCOUNTS_ID, accountId); + values.put(EXTRAS_VALUE, value); + return db.insert(TABLE_EXTRAS, EXTRAS_KEY, values); + } + + public void removeAccount(Account account) { + long identityToken = clearCallingIdentity(); + try { + final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + db.delete(TABLE_ACCOUNTS, ACCOUNTS_NAME + "=? AND " + ACCOUNTS_TYPE+ "=?", + new String[]{account.mName, account.mType}); + sendAccountsChangedBroadcast(); + } finally { + restoreCallingIdentity(identityToken); + } + } + + public void invalidateAuthToken(String accountType, String authToken) { + long identityToken = clearCallingIdentity(); + try { + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + db.beginTransaction(); + try { + invalidateAuthToken(db, accountType, authToken); + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } finally { + restoreCallingIdentity(identityToken); + } + } + + private void invalidateAuthToken(SQLiteDatabase db, String accountType, String authToken) { + Cursor cursor = db.rawQuery( + "SELECT " + TABLE_AUTHTOKENS + "." + AUTHTOKENS_ID + + ", " + TABLE_ACCOUNTS + "." + ACCOUNTS_NAME + + ", " + TABLE_AUTHTOKENS + "." + AUTHTOKENS_TYPE + + " FROM " + TABLE_ACCOUNTS + + " JOIN " + TABLE_AUTHTOKENS + + " ON " + TABLE_ACCOUNTS + "." + ACCOUNTS_ID + + " = " + AUTHTOKENS_ACCOUNTS_ID + + " WHERE " + AUTHTOKENS_AUTHTOKEN + " = ? AND " + + TABLE_ACCOUNTS + "." + ACCOUNTS_TYPE + " = ?", + new String[]{authToken, accountType}); + try { + while (cursor.moveToNext()) { + long authTokenId = cursor.getLong(0); + String accountName = cursor.getString(1); + String authTokenType = cursor.getString(2); + db.delete(TABLE_AUTHTOKENS, AUTHTOKENS_ID + "=" + authTokenId, null); + } + } finally { + cursor.close(); + } + } + + private boolean saveAuthTokenToDatabase(Account account, String type, String authToken) { + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + db.beginTransaction(); + try { + long accountId = getAccountId(db, account); + if (accountId < 0) { + return false; + } + db.delete(TABLE_AUTHTOKENS, + AUTHTOKENS_ACCOUNTS_ID + "=" + accountId + " AND " + AUTHTOKENS_TYPE + "=?", + new String[]{type}); + ContentValues values = new ContentValues(); + values.put(AUTHTOKENS_ACCOUNTS_ID, accountId); + values.put(AUTHTOKENS_TYPE, type); + values.put(AUTHTOKENS_AUTHTOKEN, authToken); + if (db.insert(TABLE_AUTHTOKENS, AUTHTOKENS_AUTHTOKEN, values) >= 0) { + db.setTransactionSuccessful(); + return true; + } + return false; + } finally { + db.endTransaction(); + } + } + + public String readAuthTokenFromDatabase(Account account, String authTokenType) { + SQLiteDatabase db = mOpenHelper.getReadableDatabase(); + db.beginTransaction(); + try { + long accountId = getAccountId(db, account); + if (accountId < 0) { + return null; + } + return getAuthToken(db, accountId, authTokenType); + } finally { + db.setTransactionSuccessful(); + db.endTransaction(); + } + } + + public String peekAuthToken(Account account, String authTokenType) { + long identityToken = clearCallingIdentity(); + try { + return readAuthTokenFromDatabase(account, authTokenType); + } finally { + restoreCallingIdentity(identityToken); + } + } + + public void setAuthToken(Account account, String authTokenType, String authToken) { + long identityToken = clearCallingIdentity(); + try { + cacheAuthToken(account, authTokenType, authToken); + } finally { + restoreCallingIdentity(identityToken); + } + } + + public void setPassword(Account account, String password) { + long identityToken = clearCallingIdentity(); + try { + ContentValues values = new ContentValues(); + values.put(ACCOUNTS_PASSWORD, password); + mOpenHelper.getWritableDatabase().update(TABLE_ACCOUNTS, values, + ACCOUNTS_NAME + "=? AND " + ACCOUNTS_TYPE+ "=?", + new String[]{account.mName, account.mType}); + sendAccountsChangedBroadcast(); + } finally { + restoreCallingIdentity(identityToken); + } + } + + private void sendAccountsChangedBroadcast() { + mContext.sendBroadcast(ACCOUNTS_CHANGED_INTENT); + } + + public void clearPassword(Account account) { + long identityToken = clearCallingIdentity(); + try { + setPassword(account, null); + } finally { + restoreCallingIdentity(identityToken); + } + } + + public void setUserData(Account account, String key, String value) { + long identityToken = clearCallingIdentity(); + try { + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + db.beginTransaction(); + try { + long accountId = getAccountId(db, account); + if (accountId < 0) { + return; + } + long extrasId = getExtrasId(db, accountId, key); + if (extrasId < 0 ) { + extrasId = insertExtra(db, accountId, key, value); + if (extrasId < 0) { + return; + } + } else { + ContentValues values = new ContentValues(); + values.put(EXTRAS_VALUE, value); + if (1 != db.update(TABLE_EXTRAS, values, EXTRAS_ID + "=" + extrasId, null)) { + return; + } + + } + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } finally { + restoreCallingIdentity(identityToken); + } + } + + public void getAuthToken(IAccountManagerResponse response, final Account account, + final String authTokenType, final boolean notifyOnAuthFailure, + final boolean expectActivityLaunch, final Bundle loginOptions) { + long identityToken = clearCallingIdentity(); + try { + String authToken = readAuthTokenFromDatabase(account, authTokenType); + if (authToken != null) { + try { + Bundle result = new Bundle(); + result.putString(Constants.AUTHTOKEN_KEY, authToken); + result.putString(Constants.ACCOUNT_NAME_KEY, account.mName); + result.putString(Constants.ACCOUNT_TYPE_KEY, account.mType); + response.onResult(result); + } catch (RemoteException e) { + // if the caller is dead then there is no one to care about remote exceptions + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "failure while notifying response", e); + } + } + return; + } + + new Session(response, account.mType, expectActivityLaunch) { + protected String toDebugString(long now) { + if (loginOptions != null) loginOptions.keySet(); + return super.toDebugString(now) + ", getAuthToken" + + ", " + account + + ", authTokenType " + authTokenType + + ", loginOptions " + loginOptions + + ", notifyOnAuthFailure " + notifyOnAuthFailure; + } + + public void run() throws RemoteException { + mAuthenticator.getAuthToken(this, account, authTokenType, loginOptions); + } + + public void onResult(Bundle result) { + if (result != null) { + String authToken = result.getString(Constants.AUTHTOKEN_KEY); + if (authToken != null) { + String name = result.getString(Constants.ACCOUNT_NAME_KEY); + String type = result.getString(Constants.ACCOUNT_TYPE_KEY); + if (TextUtils.isEmpty(type) || TextUtils.isEmpty(name)) { + onError(Constants.ERROR_CODE_INVALID_RESPONSE, + "the type and name should not be empty"); + return; + } + cacheAuthToken(new Account(name, type), authTokenType, authToken); + } + + Intent intent = result.getParcelable(Constants.INTENT_KEY); + if (intent != null && notifyOnAuthFailure) { + doNotification(result.getString(Constants.AUTH_FAILED_MESSAGE_KEY), + intent); + } + } + super.onResult(result); + } + }.bind(); + } finally { + restoreCallingIdentity(identityToken); + } + } + + + public void addAcount(final IAccountManagerResponse response, final String accountType, + final String authTokenType, final String[] requiredFeatures, + final boolean expectActivityLaunch, final Bundle options) { + long identityToken = clearCallingIdentity(); + try { + new Session(response, accountType, expectActivityLaunch) { + public void run() throws RemoteException { + mAuthenticator.addAccount(this, mAccountType, authTokenType, requiredFeatures, + options); + } + + protected String toDebugString(long now) { + return super.toDebugString(now) + ", addAccount" + + ", accountType " + accountType + + ", requiredFeatures " + + (requiredFeatures != null + ? TextUtils.join(",", requiredFeatures) + : null); + } + }.bind(); + } finally { + restoreCallingIdentity(identityToken); + } + } + + public void confirmCredentials(IAccountManagerResponse response, + final Account account, final boolean expectActivityLaunch) { + long identityToken = clearCallingIdentity(); + try { + new Session(response, account.mType, expectActivityLaunch) { + public void run() throws RemoteException { + mAuthenticator.confirmCredentials(this, account); + } + protected String toDebugString(long now) { + return super.toDebugString(now) + ", confirmCredentials" + + ", " + account; + } + }.bind(); + } finally { + restoreCallingIdentity(identityToken); + } + } + + public void confirmPassword(IAccountManagerResponse response, final Account account, + final String password) { + long identityToken = clearCallingIdentity(); + try { + new Session(response, account.mType, false /* expectActivityLaunch */) { + public void run() throws RemoteException { + mAuthenticator.confirmPassword(this, account, password); + } + protected String toDebugString(long now) { + return super.toDebugString(now) + ", confirmPassword" + + ", " + account; + } + }.bind(); + } finally { + restoreCallingIdentity(identityToken); + } + } + + public void updateCredentials(IAccountManagerResponse response, final Account account, + final String authTokenType, final boolean expectActivityLaunch, + final Bundle loginOptions) { + long identityToken = clearCallingIdentity(); + try { + new Session(response, account.mType, expectActivityLaunch) { + public void run() throws RemoteException { + mAuthenticator.updateCredentials(this, account, authTokenType, loginOptions); + } + protected String toDebugString(long now) { + if (loginOptions != null) loginOptions.keySet(); + return super.toDebugString(now) + ", updateCredentials" + + ", " + account + + ", authTokenType " + authTokenType + + ", loginOptions " + loginOptions; + } + }.bind(); + } finally { + restoreCallingIdentity(identityToken); + } + } + + public void editProperties(IAccountManagerResponse response, final String accountType, + final boolean expectActivityLaunch) { + long identityToken = clearCallingIdentity(); + try { + new Session(response, accountType, expectActivityLaunch) { + public void run() throws RemoteException { + mAuthenticator.editProperties(this, mAccountType); + } + protected String toDebugString(long now) { + return super.toDebugString(now) + ", editProperties" + + ", accountType " + accountType; + } + }.bind(); + } finally { + restoreCallingIdentity(identityToken); + } + } + + private class GetAccountsByTypeAndFeatureSession extends Session { + private final String[] mFeatures; + private volatile Account[] mAccountsOfType = null; + private volatile ArrayList<Account> mAccountsWithFeatures = null; + private volatile int mCurrentAccount = 0; + + public GetAccountsByTypeAndFeatureSession(IAccountManagerResponse response, + String type, String[] features) { + super(response, type, false /* expectActivityLaunch */); + mFeatures = features; + } + + public void run() throws RemoteException { + mAccountsOfType = getAccountsByType(mAccountType); + // check whether each account matches the requested features + mAccountsWithFeatures = new ArrayList<Account>(mAccountsOfType.length); + mCurrentAccount = 0; + + checkAccount(); + } + + public void checkAccount() { + if (mCurrentAccount >= mAccountsOfType.length) { + sendResult(); + return; + } + + try { + mAuthenticator.hasFeatures(this, mAccountsOfType[mCurrentAccount], mFeatures); + } catch (RemoteException e) { + onError(Constants.ERROR_CODE_REMOTE_EXCEPTION, "remote exception"); + } + } + + public void onResult(Bundle result) { + mNumResults++; + if (result == null) { + onError(Constants.ERROR_CODE_INVALID_RESPONSE, "null bundle"); + return; + } + if (result.getBoolean(Constants.BOOLEAN_RESULT_KEY, false)) { + mAccountsWithFeatures.add(mAccountsOfType[mCurrentAccount]); + } + mCurrentAccount++; + checkAccount(); + } + + public void sendResult() { + IAccountManagerResponse response = getResponseAndClose(); + if (response != null) { + try { + Account[] accounts = new Account[mAccountsWithFeatures.size()]; + for (int i = 0; i < accounts.length; i++) { + accounts[i] = mAccountsWithFeatures.get(i); + } + Bundle result = new Bundle(); + result.putParcelableArray(Constants.ACCOUNTS_KEY, accounts); + response.onResult(result); + } catch (RemoteException e) { + // if the caller is dead then there is no one to care about remote exceptions + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "failure while notifying response", e); + } + } + } + } + + + protected String toDebugString(long now) { + return super.toDebugString(now) + ", getAccountsByTypeAndFeatures" + + ", " + (mFeatures != null ? TextUtils.join(",", mFeatures) : null); + } + } + public void getAccountsByTypeAndFeatures(IAccountManagerResponse response, + String type, String[] features) { + if (type == null) { + if (response != null) { + try { + response.onError(Constants.ERROR_CODE_BAD_ARGUMENTS, "type is null"); + } catch (RemoteException e) { + // ignore this + } + } + return; + } + long identityToken = clearCallingIdentity(); + try { + new GetAccountsByTypeAndFeatureSession(response, type, features).bind(); + } finally { + restoreCallingIdentity(identityToken); + } + } + + private boolean cacheAuthToken(Account account, String authTokenType, String authToken) { + return saveAuthTokenToDatabase(account, authTokenType, authToken); + } + + private long getAccountId(SQLiteDatabase db, Account account) { + Cursor cursor = db.query(TABLE_ACCOUNTS, new String[]{ACCOUNTS_ID}, + "name=? AND type=?", new String[]{account.mName, account.mType}, null, null, null); + try { + if (cursor.moveToNext()) { + return cursor.getLong(0); + } + return -1; + } finally { + cursor.close(); + } + } + + private long getExtrasId(SQLiteDatabase db, long accountId, String key) { + Cursor cursor = db.query(TABLE_EXTRAS, new String[]{EXTRAS_ID}, + EXTRAS_ACCOUNTS_ID + "=" + accountId + " AND " + EXTRAS_KEY + "=?", + new String[]{key}, null, null, null); + try { + if (cursor.moveToNext()) { + return cursor.getLong(0); + } + return -1; + } finally { + cursor.close(); + } + } + + private String getAuthToken(SQLiteDatabase db, long accountId, String authTokenType) { + Cursor cursor = db.query(TABLE_AUTHTOKENS, new String[]{AUTHTOKENS_AUTHTOKEN}, + AUTHTOKENS_ACCOUNTS_ID + "=" + accountId + " AND " + AUTHTOKENS_TYPE + "=?", + new String[]{authTokenType}, + null, null, null); + try { + if (cursor.moveToNext()) { + return cursor.getString(0); + } + return null; + } finally { + cursor.close(); + } + } + + private abstract class Session extends IAccountAuthenticatorResponse.Stub + implements AuthenticatorBindHelper.Callback, IBinder.DeathRecipient { + IAccountManagerResponse mResponse; + final String mAccountType; + final boolean mExpectActivityLaunch; + final long mCreationTime; + + public int mNumResults = 0; + private int mNumRequestContinued = 0; + private int mNumErrors = 0; + + + IAccountAuthenticator mAuthenticator = null; + + public Session(IAccountManagerResponse response, String accountType, + boolean expectActivityLaunch) { + super(); + if (response == null) throw new IllegalArgumentException("response is null"); + if (accountType == null) throw new IllegalArgumentException("accountType is null"); + mResponse = response; + mAccountType = accountType; + mExpectActivityLaunch = expectActivityLaunch; + mCreationTime = SystemClock.elapsedRealtime(); + synchronized (mSessions) { + mSessions.put(toString(), this); + } + try { + response.asBinder().linkToDeath(this, 0 /* flags */); + } catch (RemoteException e) { + mResponse = null; + binderDied(); + } + } + + IAccountManagerResponse getResponseAndClose() { + if (mResponse == null) { + // this session has already been closed + return null; + } + IAccountManagerResponse response = mResponse; + close(); // this clears mResponse so we need to save the response before this call + return response; + } + + private void close() { + synchronized (mSessions) { + if (mSessions.remove(toString()) == null) { + // the session was already closed, so bail out now + return; + } + } + if (mResponse != null) { + // stop listening for response deaths + mResponse.asBinder().unlinkToDeath(this, 0 /* flags */); + + // clear this so that we don't accidentally send any further results + mResponse = null; + } + cancelTimeout(); + unbind(); + } + + public void binderDied() { + mResponse = null; + close(); + } + + protected String toDebugString() { + return toDebugString(SystemClock.elapsedRealtime()); + } + + protected String toDebugString(long now) { + return "Session: expectLaunch " + mExpectActivityLaunch + + ", connected " + (mAuthenticator != null) + + ", stats (" + mNumResults + "/" + mNumRequestContinued + + "/" + mNumErrors + ")" + + ", lifetime " + ((now - mCreationTime) / 1000.0); + } + + void bind() { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "initiating bind to authenticator type " + mAccountType); + } + if (!mBindHelper.bind(mAccountType, this)) { + Log.d(TAG, "bind attempt failed for " + toDebugString()); + onError(Constants.ERROR_CODE_REMOTE_EXCEPTION, "bind failure"); + } + } + + private void unbind() { + if (mAuthenticator != null) { + mAuthenticator = null; + mBindHelper.unbind(this); + } + } + + public void scheduleTimeout() { + mMessageHandler.sendMessageDelayed( + mMessageHandler.obtainMessage(MESSAGE_TIMED_OUT, this), TIMEOUT_DELAY_MS); + } + + public void cancelTimeout() { + mMessageHandler.removeMessages(MESSAGE_TIMED_OUT, this); + } + + public void onConnected(IBinder service) { + mAuthenticator = IAccountAuthenticator.Stub.asInterface(service); + try { + run(); + } catch (RemoteException e) { + onError(Constants.ERROR_CODE_REMOTE_EXCEPTION, + "remote exception"); + } + } + + public abstract void run() throws RemoteException; + + public void onDisconnected() { + mAuthenticator = null; + IAccountManagerResponse response = getResponseAndClose(); + if (response != null) { + onError(Constants.ERROR_CODE_REMOTE_EXCEPTION, + "disconnected"); + } + } + + public void onTimedOut() { + IAccountManagerResponse response = getResponseAndClose(); + if (response != null) { + onError(Constants.ERROR_CODE_REMOTE_EXCEPTION, + "timeout"); + } + } + + public void onResult(Bundle result) { + mNumResults++; + if (result != null && !TextUtils.isEmpty(result.getString(Constants.AUTHTOKEN_KEY))) { + cancelNotification(); + } + IAccountManagerResponse response; + if (mExpectActivityLaunch && result != null + && result.containsKey(Constants.INTENT_KEY)) { + response = mResponse; + } else { + response = getResponseAndClose(); + } + if (response != null) { + try { + if (result == null) { + response.onError(Constants.ERROR_CODE_INVALID_RESPONSE, + "null bundle returned"); + } else { + response.onResult(result); + } + } catch (RemoteException e) { + // if the caller is dead then there is no one to care about remote exceptions + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "failure while notifying response", e); + } + } + } + } + + public void onRequestContinued() { + mNumRequestContinued++; + } + + public void onError(int errorCode, String errorMessage) { + mNumErrors++; + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "Session.onError: " + errorCode + ", " + errorMessage); + } + IAccountManagerResponse response = getResponseAndClose(); + if (response != null) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "Session.onError: responding"); + } + try { + response.onError(errorCode, errorMessage); + } catch (RemoteException e) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "Session.onError: caught RemoteException while responding", e); + } + } + } else { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "Session.onError: already closed"); + } + } + } + } + + private class MessageHandler extends Handler { + MessageHandler(Looper looper) { + super(looper); + } + + public void handleMessage(Message msg) { + if (mBindHelper.handleMessage(msg)) { + return; + } + switch (msg.what) { + case MESSAGE_TIMED_OUT: + Session session = (Session)msg.obj; + session.onTimedOut(); + break; + + default: + throw new IllegalStateException("unhandled message: " + msg.what); + } + } + } + + private class DatabaseHelper extends SQLiteOpenHelper { + public DatabaseHelper(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL("CREATE TABLE " + TABLE_ACCOUNTS + " ( " + + ACCOUNTS_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + + ACCOUNTS_NAME + " TEXT NOT NULL, " + + ACCOUNTS_TYPE + " TEXT NOT NULL, " + + ACCOUNTS_PASSWORD + " TEXT, " + + "UNIQUE(" + ACCOUNTS_NAME + "," + ACCOUNTS_TYPE + "))"); + + db.execSQL("CREATE TABLE " + TABLE_AUTHTOKENS + " ( " + + AUTHTOKENS_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + + AUTHTOKENS_ACCOUNTS_ID + " INTEGER NOT NULL, " + + AUTHTOKENS_TYPE + " TEXT NOT NULL, " + + AUTHTOKENS_AUTHTOKEN + " TEXT, " + + "UNIQUE (" + AUTHTOKENS_ACCOUNTS_ID + "," + AUTHTOKENS_TYPE + "))"); + + db.execSQL("CREATE TABLE " + TABLE_EXTRAS + " ( " + + EXTRAS_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + + EXTRAS_ACCOUNTS_ID + " INTEGER, " + + EXTRAS_KEY + " TEXT NOT NULL, " + + EXTRAS_VALUE + " TEXT, " + + "UNIQUE(" + EXTRAS_ACCOUNTS_ID + "," + EXTRAS_KEY + "))"); + + db.execSQL("CREATE TABLE " + TABLE_META + " ( " + + META_KEY + " TEXT PRIMARY KEY NOT NULL, " + + META_VALUE + " TEXT)"); + + db.execSQL("" + + " CREATE TRIGGER " + TABLE_ACCOUNTS + "Delete DELETE ON " + TABLE_ACCOUNTS + + " BEGIN" + + " DELETE FROM " + TABLE_AUTHTOKENS + + " WHERE " + AUTHTOKENS_ACCOUNTS_ID + "=OLD." + ACCOUNTS_ID + " ;" + + " DELETE FROM " + TABLE_EXTRAS + + " WHERE " + EXTRAS_ACCOUNTS_ID + "=OLD." + ACCOUNTS_ID + " ;" + + " END"); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + Log.e(TAG, "upgrade from version " + oldVersion + " to version " + newVersion); + + if (oldVersion == 1) { + db.execSQL("" + + " CREATE TRIGGER " + TABLE_ACCOUNTS + "Delete DELETE ON " + TABLE_ACCOUNTS + + " BEGIN" + + " DELETE FROM " + TABLE_AUTHTOKENS + + " WHERE " + AUTHTOKENS_ACCOUNTS_ID + " =OLD." + ACCOUNTS_ID + " ;" + + " DELETE FROM " + TABLE_EXTRAS + + " WHERE " + EXTRAS_ACCOUNTS_ID + " =OLD." + ACCOUNTS_ID + " ;" + + " END"); + oldVersion++; + } + } + + @Override + public void onOpen(SQLiteDatabase db) { + if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "opened database " + DATABASE_NAME); + } + } + + private void setMetaValue(String key, String value) { + ContentValues values = new ContentValues(); + values.put(META_KEY, key); + values.put(META_VALUE, value); + mOpenHelper.getWritableDatabase().replace(TABLE_META, META_KEY, values); + } + + private String getMetaValue(String key) { + Cursor c = mOpenHelper.getReadableDatabase().query(TABLE_META, + new String[]{META_VALUE}, META_KEY + "=?", new String[]{key}, null, null, null); + try { + if (c.moveToNext()) { + return c.getString(0); + } + return null; + } finally { + c.close(); + } + } + + private class SimWatcher extends BroadcastReceiver { + public SimWatcher(Context context) { + // Re-scan the SIM card when the SIM state changes, and also if + // the disk recovers from a full state (we may have failed to handle + // things properly while the disk was full). + final IntentFilter filter = new IntentFilter(); + filter.addAction(TelephonyIntents.ACTION_SIM_STATE_CHANGED); + filter.addAction(Intent.ACTION_DEVICE_STORAGE_OK); + context.registerReceiver(this, filter); + } + + /** + * Compare the IMSI to the one stored in the login service's + * database. If they differ, erase all passwords and + * authtokens (and store the new IMSI). + */ + @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(); + if (TextUtils.isEmpty(imsi)) return; + + String storedImsi = getMetaValue("imsi"); + + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "current IMSI=" + imsi + "; stored IMSI=" + storedImsi); + } + + if (!imsi.equals(storedImsi) && !"initial".equals(storedImsi)) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "wiping all passwords and authtokens"); + } + SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + db.beginTransaction(); + try { + db.execSQL("DELETE from " + TABLE_AUTHTOKENS); + db.execSQL("UPDATE " + TABLE_ACCOUNTS + " SET " + ACCOUNTS_PASSWORD + " = ''"); + sendAccountsChangedBroadcast(); + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } + setMetaValue("imsi", imsi); + } + } + + public IBinder onBind(Intent intent) { + return asBinder(); + } + + protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) { + synchronized (mSessions) { + final long now = SystemClock.elapsedRealtime(); + fout.println("AccountManagerService: " + mSessions.size() + " sessions"); + for (Session session : mSessions.values()) { + fout.println(" " + session.toDebugString(now)); + } + } + + fout.println(); + + mAuthenticatorCache.dump(fd, fout, args); + } + + private void doNotification(CharSequence message, Intent intent) { + long identityToken = clearCallingIdentity(); + try { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "doNotification: " + message + " intent:" + intent); + } + + Notification n = new Notification(android.R.drawable.stat_sys_warning, null, + 0 /* when */); + n.setLatestEventInfo(mContext, mContext.getText(R.string.notification_title), message, + PendingIntent.getActivity(mContext, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT)); + ((NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE)) + .notify(NOTIFICATION_ID, n); + } finally { + restoreCallingIdentity(identityToken); + } + } + + private void cancelNotification() { + long identityToken = clearCallingIdentity(); + try { + ((NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE)) + .cancel(NOTIFICATION_ID); + } finally { + restoreCallingIdentity(identityToken); + } + } +} diff --git a/core/java/android/accounts/AccountMonitor.java b/core/java/android/accounts/AccountMonitor.java deleted file mode 100644 index f21385e..0000000 --- a/core/java/android/accounts/AccountMonitor.java +++ /dev/null @@ -1,174 +0,0 @@ -/* - * Copyright (C) 2007 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.accounts; - -import android.content.BroadcastReceiver; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.ServiceConnection; -import android.database.SQLException; -import android.os.IBinder; -import android.os.Process; -import android.os.RemoteException; -import android.util.Log; - -/** - * A helper class that calls back on the provided - * AccountMonitorListener with the set of current accounts both when - * it gets created and whenever the set changes. It does this by - * binding to the AccountsService and registering to receive the - * intent broadcast when the set of accounts is changed. The - * connection to the accounts service is only made when it needs to - * fetch the current list of accounts (that is, when the - * AccountMonitor is first created, and when the intent is received). - */ -public class AccountMonitor extends BroadcastReceiver implements ServiceConnection { - private final Context mContext; - private final AccountMonitorListener mListener; - private boolean mClosed = false; - private int pending = 0; - - // This thread runs in the background and runs the code to update accounts - // in the listener. - private class AccountUpdater extends Thread { - private IBinder mService; - - public AccountUpdater(IBinder service) { - mService = service; - } - - @Override - public void run() { - Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); - IAccountsService accountsService = IAccountsService.Stub.asInterface(mService); - String[] accounts = null; - do { - try { - accounts = accountsService.getAccounts(); - } catch (RemoteException e) { - // if the service was killed then the system will restart it and when it does we - // will get another onServiceConnected, at which point we will do a notify. - Log.w("AccountMonitor", "Remote exception when getting accounts", e); - return; - } - - synchronized (AccountMonitor.this) { - --pending; - if (pending == 0) { - break; - } - } - } while (true); - - mContext.unbindService(AccountMonitor.this); - - try { - mListener.onAccountsUpdated(accounts); - } catch (SQLException e) { - // Better luck next time. If the problem was disk-full, - // the STORAGE_OK intent will re-trigger the update. - Log.e("AccountMonitor", "Can't update accounts", e); - } - } - } - - /** - * Initializes the AccountMonitor and initiates a bind to the - * AccountsService to get the initial account list. For 1.0, - * the "list" is always a single account. - * - * @param context the context we are running in - * @param listener the user to notify when the account set changes - */ - public AccountMonitor(Context context, AccountMonitorListener listener) { - if (listener == null) { - throw new IllegalArgumentException("listener is null"); - } - - mContext = context; - mListener = listener; - - // Register a broadcast receiver to monitor account changes - IntentFilter intentFilter = new IntentFilter(); - intentFilter.addAction(AccountsServiceConstants.LOGIN_ACCOUNTS_CHANGED_ACTION); - intentFilter.addAction(Intent.ACTION_DEVICE_STORAGE_OK); // To recover from disk-full. - mContext.registerReceiver(this, intentFilter); - - // Send the listener the initial state now. - notifyListener(); - } - - @Override - public void onReceive(Context context, Intent intent) { - notifyListener(); - } - - public void onServiceConnected(ComponentName className, IBinder service) { - // Create a background thread to update the accounts. - new AccountUpdater(service).start(); - } - - public void onServiceDisconnected(ComponentName className) { - } - - private synchronized void notifyListener() { - if (pending == 0) { - // initiate the bind - if (!mContext.bindService(AccountsServiceConstants.SERVICE_INTENT, - this, Context.BIND_AUTO_CREATE)) { - // This is normal if GLS isn't part of this build. - Log.w("AccountMonitor", - "Couldn't connect to " + - AccountsServiceConstants.SERVICE_INTENT + - " (Missing service?)"); - } - } else { - // already bound. bindService will not trigger another - // call to onServiceConnected, so instead we make sure - // that the existing background thread will call - // getAccounts() after this function returns, by - // incrementing pending. - // - // Yes, this else clause contains only a comment. - } - ++pending; - } - - /** - * calls close() - * @throws Throwable - */ - @Override - protected void finalize() throws Throwable { - close(); - super.finalize(); - } - - /** - * Unregisters the account receiver. Consecutive calls to this - * method are harmless, but also do nothing. Once this call is - * made no more notifications will occur. - */ - public synchronized void close() { - if (!mClosed) { - mContext.unregisterReceiver(this); - mClosed = true; - } - } -} diff --git a/core/java/android/accounts/AccountsServiceConstants.java b/core/java/android/accounts/AccountsServiceConstants.java deleted file mode 100644 index b882e7b..0000000 --- a/core/java/android/accounts/AccountsServiceConstants.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright (C) 2008 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.accounts; - -import android.content.Intent; - -/** - * Miscellaneous constants used by the AccountsService and its - * clients. - */ -// TODO: These constants *could* come directly from the -// IAccountsService interface, but that's not possible since the -// aidl compiler doesn't let you define constants (yet.) -public class AccountsServiceConstants { - /** This class is never instantiated. */ - private AccountsServiceConstants() { - } - - /** - * Action sent as a broadcast Intent by the AccountsService - * when accounts are added to and/or removed from the device's - * database, or when the primary account is changed. - */ - public static final String LOGIN_ACCOUNTS_CHANGED_ACTION = - "android.accounts.LOGIN_ACCOUNTS_CHANGED"; - - /** - * Action sent as a broadcast Intent by the AccountsService - * when it starts up and no accounts are available (so some should be added). - */ - public static final String LOGIN_ACCOUNTS_MISSING_ACTION = - "android.accounts.LOGIN_ACCOUNTS_MISSING"; - - /** - * Action on the intent used to bind to the IAccountsService interface. This - * is used for services that have multiple interfaces (allowing - * them to differentiate the interface intended, and return the proper - * Binder.) - */ - private static final String ACCOUNTS_SERVICE_ACTION = "android.accounts.IAccountsService"; - - /* - * The intent uses a component in addition to the action to ensure the actual - * accounts service is bound to (a malicious third-party app could - * theoretically have a service with the same action). - */ - /** The intent used to bind to the accounts service. */ - public static final Intent SERVICE_INTENT = - new Intent() - .setClassName("com.google.android.googleapps", - "com.google.android.googleapps.GoogleLoginService") - .setAction(ACCOUNTS_SERVICE_ACTION); - - /** - * Checks whether the intent is to bind to the accounts service. - * - * @param bindIntent The Intent used to bind to the service. - * @return Whether the intent is to bind to the accounts service. - */ - public static final boolean isForAccountsService(Intent bindIntent) { - String otherAction = bindIntent.getAction(); - return otherAction != null && otherAction.equals(ACCOUNTS_SERVICE_ACTION); - } -} diff --git a/core/java/android/accounts/AuthenticatorBindHelper.java b/core/java/android/accounts/AuthenticatorBindHelper.java new file mode 100644 index 0000000..9d2ccf6 --- /dev/null +++ b/core/java/android/accounts/AuthenticatorBindHelper.java @@ -0,0 +1,251 @@ +/* + * 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 authenticatorInfo = + mAuthenticatorCache.getServiceInfo(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"); + } + unbindFromService(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"); + } + } + + private void unbindFromService(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) { + for (Callback callback : mServiceUsers.get(mAuthenticatorType)) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "the service became disconnected, scheduling a " + + "disconnected message for " + + mAuthenticatorType); + } + mHandler.obtainMessage(mMessageWhatDisconnected, callback).sendToTarget(); + } + unbindFromService(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/AuthenticatorException.java b/core/java/android/accounts/AuthenticatorException.java new file mode 100644 index 0000000..4023494 --- /dev/null +++ b/core/java/android/accounts/AuthenticatorException.java @@ -0,0 +1,32 @@ +/* + * 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; + +public class AuthenticatorException extends Exception { + public AuthenticatorException() { + super(); + } + public AuthenticatorException(String message) { + super(message); + } + public AuthenticatorException(String message, Throwable cause) { + super(message, cause); + } + public AuthenticatorException(Throwable cause) { + super(cause); + } +} diff --git a/core/java/android/accounts/ChooseAccountActivity.java b/core/java/android/accounts/ChooseAccountActivity.java new file mode 100644 index 0000000..83377f3 --- /dev/null +++ b/core/java/android/accounts/ChooseAccountActivity.java @@ -0,0 +1,78 @@ +/* + * 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.app.ListActivity; +import android.os.Bundle; +import android.os.Parcelable; +import android.widget.ArrayAdapter; +import android.widget.ListView; +import android.view.View; +import android.util.Log; + +public class ChooseAccountActivity extends ListActivity { + private static final String TAG = "AccountManager"; + private Parcelable[] mAccounts = null; + private AccountManagerResponse mAccountManagerResponse = null; + private Bundle mResult; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (savedInstanceState == null) { + mAccounts = getIntent().getParcelableArrayExtra(Constants.ACCOUNTS_KEY); + mAccountManagerResponse = + getIntent().getParcelableExtra(Constants.ACCOUNT_MANAGER_RESPONSE_KEY); + } else { + mAccounts = savedInstanceState.getParcelableArray(Constants.ACCOUNTS_KEY); + mAccountManagerResponse = + savedInstanceState.getParcelable(Constants.ACCOUNT_MANAGER_RESPONSE_KEY); + } + + String[] mAccountNames = new String[mAccounts.length]; + for (int i = 0; i < mAccounts.length; i++) { + mAccountNames[i] = ((Account) mAccounts[i]).mName; + } + + // Use an existing ListAdapter that will map an array + // of strings to TextViews + setListAdapter(new ArrayAdapter<String>(this, + android.R.layout.simple_list_item_1, mAccountNames)); + getListView().setTextFilterEnabled(true); + } + + protected void onListItemClick(ListView l, View v, int position, long id) { + Account account = (Account) mAccounts[position]; + Log.d(TAG, "selected account " + account); + Bundle bundle = new Bundle(); + bundle.putString(Constants.ACCOUNT_NAME_KEY, account.mName); + bundle.putString(Constants.ACCOUNT_TYPE_KEY, account.mType); + mResult = bundle; + finish(); + } + + public void finish() { + if (mAccountManagerResponse != null) { + if (mResult != null) { + mAccountManagerResponse.onResult(mResult); + } else { + mAccountManagerResponse.onError(Constants.ERROR_CODE_CANCELED, "canceled"); + } + } + super.finish(); + } +} diff --git a/core/java/android/accounts/Constants.java b/core/java/android/accounts/Constants.java new file mode 100644 index 0000000..b383c61 --- /dev/null +++ b/core/java/android/accounts/Constants.java @@ -0,0 +1,50 @@ +/* + * 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; + +public class Constants { + // this should never be instantiated + private Constants() {} + + public static final int ERROR_CODE_REMOTE_EXCEPTION = 1; + public static final int ERROR_CODE_NETWORK_ERROR = 3; + public static final int ERROR_CODE_CANCELED = 4; + public static final int ERROR_CODE_INVALID_RESPONSE = 5; + public static final int ERROR_CODE_UNSUPPORTED_OPERATION = 6; + public static final int ERROR_CODE_BAD_ARGUMENTS = 7; + + public static final String ACCOUNTS_KEY = "accounts"; + public static final String AUTHENTICATOR_TYPES_KEY = "authenticator_types"; + public static final String PASSWORD_KEY = "password"; + public static final String USERDATA_KEY = "userdata"; + public static final String AUTHTOKEN_KEY = "authtoken"; + public static final String ACCOUNT_NAME_KEY = "authAccount"; + public static final String ACCOUNT_TYPE_KEY = "accountType"; + public static final String ERROR_CODE_KEY = "errorCode"; + public static final String ERROR_MESSAGE_KEY = "errorMessage"; + public static final String INTENT_KEY = "intent"; + public static final String BOOLEAN_RESULT_KEY = "booleanResult"; + public static final String ACCOUNT_AUTHENTICATOR_RESPONSE_KEY = "accountAuthenticatorResponse"; + public static final String ACCOUNT_MANAGER_RESPONSE_KEY = "accountManagerResponse"; + public static final String AUTH_FAILED_MESSAGE_KEY = "authFailedMessage"; + /** + * Action sent as a broadcast Intent by the AccountsService + * when accounts are added to and/or removed from the device's + * database, or when the primary account is changed. + */ + public static final String LOGIN_ACCOUNTS_CHANGED_ACTION = + "android.accounts.LOGIN_ACCOUNTS_CHANGED"; +} diff --git a/core/java/android/accounts/Future1.java b/core/java/android/accounts/Future1.java new file mode 100644 index 0000000..386cb6e --- /dev/null +++ b/core/java/android/accounts/Future1.java @@ -0,0 +1,46 @@ +/* + * 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 java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +/** + * An extension of {@link Future} that provides wrappers for {@link #get()} that handle the various + * exceptions that {@link #get()} may return and rethrows them as exceptions specific to + * {@link AccountManager}. + */ +public interface Future1<V> extends Future<V> { + /** + * Wrapper for {@link Future#get()}. If the get() throws {@link InterruptedException} then the + * {@link Future1} is canceled and {@link OperationCanceledException} is thrown. + * @return the {@link android.os.Bundle} that is returned by get() + * @throws OperationCanceledException if get() throws the unchecked CancellationException + * or if the Future was interrupted. + */ + V getResult() throws OperationCanceledException; + + /** + * Wrapper for {@link Future#get()}. If the get() throws {@link InterruptedException} then the + * {@link Future1} is canceled and {@link OperationCanceledException} is thrown. + * @param timeout the maximum time to wait + * @param unit the time unit of the timeout argument + * @return the {@link android.os.Bundle} that is returned by {@link Future#get()} + * @throws OperationCanceledException if get() throws the unchecked + * {@link java.util.concurrent.CancellationException} or if the {@link Future1} was interrupted. + */ + V getResult(long timeout, TimeUnit unit) throws OperationCanceledException; +}
\ No newline at end of file diff --git a/core/java/android/accounts/Future1Callback.java b/core/java/android/accounts/Future1Callback.java new file mode 100644 index 0000000..886671b --- /dev/null +++ b/core/java/android/accounts/Future1Callback.java @@ -0,0 +1,20 @@ +/* + * 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; + +public interface Future1Callback<V> { + void run(Future1<V> future); +} diff --git a/core/java/android/accounts/Future2.java b/core/java/android/accounts/Future2.java new file mode 100644 index 0000000..b2ea84f --- /dev/null +++ b/core/java/android/accounts/Future2.java @@ -0,0 +1,57 @@ +/* + * 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.os.Bundle; + +import java.io.IOException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +/** + * An extension of {@link Future} that provides wrappers for {@link #get()} that handle the various + * exceptions that {@link #get()} may return and rethrows them as exceptions specific to + * {@link AccountManager}. + */ +public interface Future2 extends Future<Bundle> { + /** + * Wrapper for {@link Future#get()}. If the get() throws {@link InterruptedException} then the + * {@link Future2} is canceled and {@link OperationCanceledException} is thrown. + * @return the {@link android.os.Bundle} that is returned by {@link Future#get()} + * @throws OperationCanceledException if get() throws the unchecked + * {@link java.util.concurrent.CancellationException} or if the {@link Future2} was interrupted. + * @throws IOException if the request was unable to complete due to a network error + * @throws AuthenticatorException if there was an error communicating with the + * {@link AbstractAccountAuthenticator}. + */ + Bundle getResult() + throws OperationCanceledException, IOException, AuthenticatorException; + + /** + * Wrapper for {@link Future#get()}. If the get() throws {@link InterruptedException} then the + * {@link Future2} is canceled and {@link OperationCanceledException} is thrown. + * @param timeout the maximum time to wait + * @param unit the time unit of the timeout argument + * @return the {@link android.os.Bundle} that is returned by {@link Future#get()} + * @throws OperationCanceledException if get() throws the unchecked + * {@link java.util.concurrent.CancellationException} or if the {@link Future2} was interrupted. + * @throws IOException if the request was unable to complete due to a network error + * @throws AuthenticatorException if there was an error communicating with the + * {@link AbstractAccountAuthenticator}. + */ + Bundle getResult(long timeout, TimeUnit unit) + throws OperationCanceledException, IOException, AuthenticatorException; +} diff --git a/core/java/android/accounts/Future2Callback.java b/core/java/android/accounts/Future2Callback.java new file mode 100644 index 0000000..7ef0c94 --- /dev/null +++ b/core/java/android/accounts/Future2Callback.java @@ -0,0 +1,20 @@ +/* + * 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; + +public interface Future2Callback { + void run(Future2 future); +}
\ No newline at end of file diff --git a/core/java/android/accounts/IAccountAuthenticator.aidl b/core/java/android/accounts/IAccountAuthenticator.aidl new file mode 100644 index 0000000..46a7144 --- /dev/null +++ b/core/java/android/accounts/IAccountAuthenticator.aidl @@ -0,0 +1,68 @@ +/* + * 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.accounts.IAccountAuthenticatorResponse; +import android.accounts.Account; +import android.os.Bundle; + +/** + * Service that allows the interaction with an authentication server. + */ +oneway interface IAccountAuthenticator { + /** + * prompts the user for account information and adds the result to the IAccountManager + */ + void addAccount(in IAccountAuthenticatorResponse response, String accountType, + String authTokenType, in String[] requiredFeatures, in Bundle options); + + /** + * Checks that the account/password combination is valid. + * @deprecated + */ + void confirmPassword(in IAccountAuthenticatorResponse response, + in Account account, String password); + + /** + * prompts the user for the credentials of the account + */ + void confirmCredentials(in IAccountAuthenticatorResponse response, in Account account); + + /** + * gets the password by either prompting the user or querying the IAccountManager + */ + void getAuthToken(in IAccountAuthenticatorResponse response, in Account account, + String authTokenType, in Bundle options); + + /** + * prompts the user for a new password and writes it to the IAccountManager + */ + void updateCredentials(in IAccountAuthenticatorResponse response, in Account account, + String authTokenType, in Bundle options); + + /** + * launches an activity that lets the user edit and set the properties for an authenticator + */ + void editProperties(in IAccountAuthenticatorResponse response, String accountType); + + /** + * returns a Bundle where the boolean value BOOLEAN_RESULT_KEY is set if the account has the + * specified features + */ + void hasFeatures(in IAccountAuthenticatorResponse response, in Account account, + in String[] features); +} diff --git a/core/java/android/accounts/IAccountAuthenticatorResponse.aidl b/core/java/android/accounts/IAccountAuthenticatorResponse.aidl new file mode 100644 index 0000000..a9ac2f1 --- /dev/null +++ b/core/java/android/accounts/IAccountAuthenticatorResponse.aidl @@ -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.accounts; +import android.os.Bundle; + +/** + * The interface used to return responses from an {@link IAccountAuthenticator} + */ +oneway interface IAccountAuthenticatorResponse { + void onResult(in Bundle value); + void onRequestContinued(); + void onError(int errorCode, String errorMessage); +} diff --git a/core/java/android/accounts/IAccountManager.aidl b/core/java/android/accounts/IAccountManager.aidl new file mode 100644 index 0000000..5e37a1f --- /dev/null +++ b/core/java/android/accounts/IAccountManager.aidl @@ -0,0 +1,62 @@ +/* + * 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.accounts.IAccountManagerResponse; +import android.accounts.Account; +import android.os.Bundle; + +/** + * Central application service that provides account management. + * @hide + */ +interface IAccountManager { + String getPassword(in Account account); + String getUserData(in Account account, String key); + String[] getAuthenticatorTypes(); + Account[] getAccounts(); + Account[] getAccountsByType(String accountType); + boolean addAccount(in Account account, String password, in Bundle extras); + void removeAccount(in Account account); + void invalidateAuthToken(String accountType, String authToken); + String peekAuthToken(in Account account, String authTokenType); + void setAuthToken(in Account account, String authTokenType, String authToken); + void setPassword(in Account account, String password); + void clearPassword(in Account account); + void setUserData(in Account account, String key, String value); + + void getAuthToken(in IAccountManagerResponse response, in Account account, + String authTokenType, boolean notifyOnAuthFailure, boolean expectActivityLaunch, + in Bundle options); + void addAcount(in IAccountManagerResponse response, String accountType, + String authTokenType, in String[] requiredFeatures, boolean expectActivityLaunch, + in Bundle options); + void updateCredentials(in IAccountManagerResponse response, in Account account, + String authTokenType, boolean expectActivityLaunch, in Bundle options); + void editProperties(in IAccountManagerResponse response, String accountType, + boolean expectActivityLaunch); + void confirmCredentials(in IAccountManagerResponse response, in Account account, + boolean expectActivityLaunch); + void getAccountsByTypeAndFeatures(in IAccountManagerResponse response, String accountType, + in String[] features); + + /* + * @deprecated + */ + void confirmPassword(in IAccountManagerResponse response, in Account account, + String password); +} diff --git a/core/java/android/accounts/IAccountManagerResponse.aidl b/core/java/android/accounts/IAccountManagerResponse.aidl new file mode 100644 index 0000000..ca1203d --- /dev/null +++ b/core/java/android/accounts/IAccountManagerResponse.aidl @@ -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.accounts; +import android.os.Bundle; + +/** + * The interface used to return responses for asynchronous calls to the {@link IAccountManager} + * @hide + */ +oneway interface IAccountManagerResponse { + void onResult(in Bundle value); + void onError(int errorCode, String errorMessage); +} diff --git a/core/java/android/accounts/IAccountsService.aidl b/core/java/android/accounts/IAccountsService.aidl deleted file mode 100644 index dda513c..0000000 --- a/core/java/android/accounts/IAccountsService.aidl +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (C) 2008 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.accounts; - -/** - * Central application service that allows querying the list of accounts. - */ -interface IAccountsService { - /** - * Gets the list of Accounts the user has previously logged - * in to. Accounts are of the form "username@domain". - * <p> - * This method will return an empty array if the device doesn't - * know about any accounts (yet). - * - * @return The accounts. The array will be zero-length if the - * AccountsService doesn't know about any accounts yet. - */ - String[] getAccounts(); - - /** - * This is an interim solution for bypassing a forgotten gesture on the - * unlock screen (it is hidden, please make sure it stays this way!). This - * will be *removed* when the unlock screen design supports additional - * authenticators. - * <p> - * The user will be presented with username and password fields that are - * called as parameters to this method. If true is returned, the user is - * able to define a new gesture and get back into the system. If false, the - * user can try again. - * - * @param username The username entered. - * @param password The password entered. - * @return Whether to allow the user to bypass the lock screen and define a - * new gesture. - * @hide (The package is already hidden, but just in case someone - * unhides that, this should not be revealed.) - */ - boolean shouldUnlock(String username, String password); -} diff --git a/core/java/android/accounts/NetworkErrorException.java b/core/java/android/accounts/NetworkErrorException.java new file mode 100644 index 0000000..f855cc8 --- /dev/null +++ b/core/java/android/accounts/NetworkErrorException.java @@ -0,0 +1,31 @@ +/* + * 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; + +public class NetworkErrorException extends Exception { + public NetworkErrorException() { + super(); + } + public NetworkErrorException(String message) { + super(message); + } + public NetworkErrorException(String message, Throwable cause) { + super(message, cause); + } + public NetworkErrorException(Throwable cause) { + super(cause); + } +}
\ No newline at end of file diff --git a/core/java/android/accounts/AccountMonitorListener.java b/core/java/android/accounts/OnAccountsUpdatedListener.java index d0bd9a9..bd249d0 100644 --- a/core/java/android/accounts/AccountMonitorListener.java +++ b/core/java/android/accounts/OnAccountsUpdatedListener.java @@ -19,11 +19,11 @@ package android.accounts; /** * An interface that contains the callback used by the AccountMonitor */ -public interface AccountMonitorListener { +public interface OnAccountsUpdatedListener { /** * This invoked when the AccountMonitor starts up and whenever the account * set changes. - * @param currentAccounts the current accounts + * @param accounts the current accounts */ - void onAccountsUpdated(String[] currentAccounts); + void onAccountsUpdated(Account[] accounts); } diff --git a/core/java/android/accounts/OperationCanceledException.java b/core/java/android/accounts/OperationCanceledException.java new file mode 100644 index 0000000..2f2c164 --- /dev/null +++ b/core/java/android/accounts/OperationCanceledException.java @@ -0,0 +1,31 @@ +/* + * 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; + +public class OperationCanceledException extends Exception { + public OperationCanceledException() { + super(); + } + public OperationCanceledException(String message) { + super(message); + } + public OperationCanceledException(String message, Throwable cause) { + super(message, cause); + } + public OperationCanceledException(Throwable cause) { + super(cause); + } +} diff --git a/core/java/android/accounts/package.html b/core/java/android/accounts/package.html deleted file mode 100755 index c9f96a6..0000000 --- a/core/java/android/accounts/package.html +++ /dev/null @@ -1,5 +0,0 @@ -<body> - -{@hide} - -</body> diff --git a/core/java/android/app/ApplicationContext.java b/core/java/android/app/ApplicationContext.java index 98bbf7b..34aeca1 100644 --- a/core/java/android/app/ApplicationContext.java +++ b/core/java/android/app/ApplicationContext.java @@ -92,6 +92,8 @@ import android.view.LayoutInflater; import android.view.WindowManagerImpl; import android.view.accessibility.AccessibilityManager; import android.view.inputmethod.InputMethodManager; +import android.accounts.AccountManager; +import android.accounts.IAccountManager; import java.io.File; import java.io.FileInputStream; @@ -150,6 +152,7 @@ class ApplicationContext extends Context { private final static boolean DEBUG_ICONS = false; private static final Object sSync = new Object(); + private static AccountManager sAccountManager; private static AlarmManager sAlarmManager; private static PowerManager sPowerManager; private static ConnectivityManager sConnectivityManager; @@ -882,6 +885,8 @@ class ApplicationContext extends Context { return getActivityManager(); } else if (ALARM_SERVICE.equals(name)) { return getAlarmManager(); + } else if (ACCOUNT_SERVICE.equals(name)) { + return getAccountManager(); } else if (POWER_SERVICE.equals(name)) { return getPowerManager(); } else if (CONNECTIVITY_SERVICE.equals(name)) { @@ -924,6 +929,17 @@ class ApplicationContext extends Context { return null; } + private AccountManager getAccountManager() { + synchronized (sSync) { + if (sAccountManager == null) { + IBinder b = ServiceManager.getService(ACCOUNT_SERVICE); + IAccountManager service = IAccountManager.Stub.asInterface(b); + sAccountManager = new AccountManager(this, service); + } + } + return sAccountManager; + } + private ActivityManager getActivityManager() { synchronized (mSync) { if (mActivityManager == null) { diff --git a/core/java/android/bluetooth/BluetoothInputStream.java b/core/java/android/bluetooth/BluetoothInputStream.java new file mode 100644 index 0000000..e6f501c --- /dev/null +++ b/core/java/android/bluetooth/BluetoothInputStream.java @@ -0,0 +1,98 @@ +/* + * 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.bluetooth; + +import java.io.IOException; +import java.io.InputStream; + +/** + * BluetoothInputStream. + * + * Used to write to a Bluetooth socket. + * + * @hide + */ +/*package*/ final class BluetoothInputStream extends InputStream { + private BluetoothSocket mSocket; + + /*package*/ BluetoothInputStream(BluetoothSocket s) { + mSocket = s; + } + + /** + * Return number of bytes available before this stream will block. + */ + public int available() throws IOException { + return mSocket.availableNative(); + } + + public void close() throws IOException { + mSocket.close(); + } + + /** + * Reads a single byte from this stream and returns it as an integer in the + * range from 0 to 255. Returns -1 if the end of the stream has been + * reached. Blocks until one byte has been read, the end of the source + * stream is detected or an exception is thrown. + * + * @return the byte read or -1 if the end of stream has been reached. + * @throws IOException + * if the stream is closed or another IOException occurs. + * @since Android 1.5 + */ + public int read() throws IOException { + byte b[] = new byte[1]; + int ret = mSocket.readNative(b, 0, 1); + if (ret == 1) { + return (int)b[0]; + } else { + return -1; + } + } + + /** + * Reads at most {@code length} bytes from this stream and stores them in + * the byte array {@code b} starting at {@code offset}. + * + * @param b + * the byte array in which to store the bytes read. + * @param offset + * the initial position in {@code buffer} to store the bytes + * read from this stream. + * @param length + * the maximum number of bytes to store in {@code b}. + * @return the number of bytes actually read or -1 if the end of the stream + * has been reached. + * @throws IndexOutOfBoundsException + * if {@code offset < 0} or {@code length < 0}, or if + * {@code offset + length} is greater than the length of + * {@code b}. + * @throws IOException + * if the stream is closed or another IOException occurs. + * @since Android 1.5 + */ + public int read(byte[] b, int offset, int length) throws IOException { + if (b == null) { + throw new NullPointerException("byte array is null"); + } + if ((offset | length) < 0 || length > b.length - offset) { + throw new ArrayIndexOutOfBoundsException("invalid offset or length"); + } + return mSocket.readNative(b, offset, length); + } +} diff --git a/core/java/android/bluetooth/BluetoothOutputStream.java b/core/java/android/bluetooth/BluetoothOutputStream.java new file mode 100644 index 0000000..7e2ead4 --- /dev/null +++ b/core/java/android/bluetooth/BluetoothOutputStream.java @@ -0,0 +1,87 @@ +/* + * 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.bluetooth; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * BluetoothOutputStream. + * + * Used to read from a Bluetooth socket. + * + * @hide + */ +/*package*/ final class BluetoothOutputStream extends OutputStream { + private BluetoothSocket mSocket; + + /*package*/ BluetoothOutputStream(BluetoothSocket s) { + mSocket = s; + } + + /** + * Close this output stream and the socket associated with it. + */ + public void close() throws IOException { + mSocket.close(); + } + + /** + * Writes a single byte to this stream. Only the least significant byte of + * the integer {@code oneByte} is written to the stream. + * + * @param oneByte + * the byte to be written. + * @throws IOException + * if an error occurs while writing to this stream. + * @since Android 1.0 + */ + public void write(int oneByte) throws IOException { + byte b[] = new byte[1]; + b[0] = (byte)oneByte; + mSocket.writeNative(b, 0, 1); + } + + /** + * Writes {@code count} bytes from the byte array {@code buffer} starting + * at position {@code offset} to this stream. + * + * @param b + * the buffer to be written. + * @param offset + * the start position in {@code buffer} from where to get bytes. + * @param count + * the number of bytes from {@code buffer} to write to this + * stream. + * @throws IOException + * if an error occurs while writing to this stream. + * @throws IndexOutOfBoundsException + * if {@code offset < 0} or {@code count < 0}, or if + * {@code offset + count} is bigger than the length of + * {@code buffer}. + * @since Android 1.0 + */ + public void write(byte[] b, int offset, int count) throws IOException { + if (b == null) { + throw new NullPointerException("buffer is null"); + } + if ((offset | count) < 0 || count > b.length - offset) { + throw new IndexOutOfBoundsException("invalid offset or length"); + } + mSocket.writeNative(b, offset, count); + } +} diff --git a/core/java/android/bluetooth/BluetoothServerSocket.java b/core/java/android/bluetooth/BluetoothServerSocket.java new file mode 100644 index 0000000..f3baeab --- /dev/null +++ b/core/java/android/bluetooth/BluetoothServerSocket.java @@ -0,0 +1,150 @@ +/* + * 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.bluetooth; + +import java.io.Closeable; +import java.io.IOException; + +/** + * Server (listening) Bluetooth Socket. + * + * Currently only supports RFCOMM sockets. + * + * RFCOMM is a connection orientated, streaming transport over Bluetooth. It is + * also known as the Serial Port Profile (SPP). + * + * TODO: Consider exposing L2CAP sockets. + * TODO: Clean up javadoc grammer and formatting. + * TODO: Remove @hide + * @hide + */ +public final class BluetoothServerSocket implements Closeable { + private final BluetoothSocket mSocket; + + /** + * Construct a listening, secure RFCOMM server socket. + * The remote device connecting to this socket will be authenticated and + * communication on this socket will be encrypted. + * Call #accept to retrieve connections to this socket. + * @return An RFCOMM BluetoothServerSocket + * @throws IOException On error, for example Bluetooth not available, or + * insufficient permissions. + */ + public static BluetoothServerSocket listenUsingRfcommOn(int port) throws IOException { + BluetoothServerSocket socket = new BluetoothServerSocket( + BluetoothSocket.TYPE_RFCOMM, true, true, port); + try { + socket.mSocket.bindListenNative(); + } catch (IOException e) { + try { + socket.close(); + } catch (IOException e2) { } + throw e; + } + return socket; + } + + /** + * Construct an unencrypted, unauthenticated, RFCOMM server socket. + * Call #accept to retrieve connections to this socket. + * @return An RFCOMM BluetoothServerSocket + * @throws IOException On error, for example Bluetooth not available, or + * insufficient permissions. + */ + public static BluetoothServerSocket listenUsingInsecureRfcommOn(int port) throws IOException { + BluetoothServerSocket socket = new BluetoothServerSocket( + BluetoothSocket.TYPE_RFCOMM, false, false, port); + try { + socket.mSocket.bindListenNative(); + } catch (IOException e) { + try { + socket.close(); + } catch (IOException e2) { } + throw e; + } + return socket; + } + + /** + * Construct a SCO server socket. + * Call #accept to retrieve connections to this socket. + * @return A SCO BluetoothServerSocket + * @throws IOException On error, for example Bluetooth not available, or + * insufficient permissions. + */ + public static BluetoothServerSocket listenUsingScoOn() throws IOException { + BluetoothServerSocket socket = new BluetoothServerSocket( + BluetoothSocket.TYPE_SCO, false, false, -1); + try { + socket.mSocket.bindListenNative(); + } catch (IOException e) { + try { + socket.close(); + } catch (IOException e2) { } + throw e; + } + return socket; + } + + /** + * Construct a socket for incoming connections. + * @param type type of socket + * @param auth require the remote device to be authenticated + * @param encrypt require the connection to be encrypted + * @param port remote port + * @throws IOException On error, for example Bluetooth not available, or + * insufficient priveleges + */ + private BluetoothServerSocket(int type, boolean auth, boolean encrypt, int port) + throws IOException { + mSocket = new BluetoothSocket(type, -1, auth, encrypt, null, port); + } + + /** + * Block until a connection is established. + * Returns a connected #BluetoothSocket. This server socket can be reused + * for subsequent incoming connections by calling #accept repeatedly. + * #close can be used to abort this call from another thread. + * @return A connected #BluetoothSocket + * @throws IOException On error, for example this call was aborted + */ + public BluetoothSocket accept() throws IOException { + return accept(-1); + } + + /** + * Block until a connection is established, with timeout. + * Returns a connected #BluetoothSocket. This server socket can be reused + * for subsequent incoming connections by calling #accept repeatedly. + * #close can be used to abort this call from another thread. + * @return A connected #BluetoothSocket + * @throws IOException On error, for example this call was aborted, or + * timeout + */ + public BluetoothSocket accept(int timeout) throws IOException { + return mSocket.acceptNative(timeout); + } + + /** + * Closes this socket. + * This will cause other blocking calls on this socket to immediately + * throw an IOException. + */ + public void close() throws IOException { + mSocket.closeNative(); + } +} diff --git a/core/java/android/bluetooth/BluetoothSocket.java b/core/java/android/bluetooth/BluetoothSocket.java new file mode 100644 index 0000000..de1f326 --- /dev/null +++ b/core/java/android/bluetooth/BluetoothSocket.java @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.bluetooth; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * Represents a connected or connecting Bluetooth Socket. + * + * Currently only supports RFCOMM sockets. + * + * RFCOMM is a connection orientated, streaming transport over Bluetooth. It is + * also known as the Serial Port Profile (SPP). + * + * TODO: Consider exposing L2CAP sockets. + * TODO: Clean up javadoc grammer and formatting. + * TODO: Remove @hide + * @hide + */ +public final class BluetoothSocket implements Closeable { + /** Keep TYPE_RFCOMM etc in sync with BluetoothSocket.cpp */ + /*package*/ static final int TYPE_RFCOMM = 1; + /*package*/ static final int TYPE_SCO = 2; + /*package*/ static final int TYPE_L2CAP = 3; + + private final int mType; /* one of TYPE_RFCOMM etc */ + private final int mPort; /* RFCOMM channel or L2CAP psm */ + private final String mAddress; /* remote address */ + private final boolean mAuth; + private final boolean mEncrypt; + private final BluetoothInputStream mInputStream; + private final BluetoothOutputStream mOutputStream; + + private int mSocketData; /* used by native code only */ + + /** + * Construct a secure RFCOMM socket ready to start an outgoing connection. + * Call #connect on the returned #BluetoothSocket to begin the connection. + * The remote device will be authenticated and communication on this socket + * will be encrypted. + * @param address remote Bluetooth address that this socket can connect to + * @param port remote port + * @return an RFCOMM BluetoothSocket + * @throws IOException on error, for example Bluetooth not available, or + * insufficient permissions. + */ + public static BluetoothSocket createRfcommSocket(String address, int port) + throws IOException { + return new BluetoothSocket(TYPE_RFCOMM, -1, true, true, address, port); + } + + /** + * Construct an insecure RFCOMM socket ready to start an outgoing + * connection. + * Call #connect on the returned #BluetoothSocket to begin the connection. + * The remote device will not be authenticated and communication on this + * socket will not be encrypted. + * @param address remote Bluetooth address that this socket can connect to + * @param port remote port + * @return An RFCOMM BluetoothSocket + * @throws IOException On error, for example Bluetooth not available, or + * insufficient permissions. + */ + public static BluetoothSocket createInsecureRfcommSocket(String address, int port) + throws IOException { + return new BluetoothSocket(TYPE_RFCOMM, -1, false, false, address, port); + } + + /** + * Construct a SCO socket ready to start an outgoing connection. + * Call #connect on the returned #BluetoothSocket to begin the connection. + * @param address remote Bluetooth address that this socket can connect to + * @return a SCO BluetoothSocket + * @throws IOException on error, for example Bluetooth not available, or + * insufficient permissions. + */ + public static BluetoothSocket createScoSocket(String address, int port) + throws IOException { + return new BluetoothSocket(TYPE_SCO, -1, true, true, address, port); + } + + /** + * Construct a Bluetooth. + * @param type type of socket + * @param fd fd to use for connected socket, or -1 for a new socket + * @param auth require the remote device to be authenticated + * @param encrypt require the connection to be encrypted + * @param address remote Bluetooth address that this socket can connect to + * @param port remote port + * @throws IOException On error, for example Bluetooth not available, or + * insufficient priveleges + */ + /*package*/ BluetoothSocket(int type, int fd, boolean auth, boolean encrypt, String address, + int port) throws IOException { + mType = type; + mAuth = auth; + mEncrypt = encrypt; + mAddress = address; + mPort = port; + if (fd == -1) { + initSocketNative(); + } else { + initSocketFromFdNative(fd); + } + mInputStream = new BluetoothInputStream(this); + mOutputStream = new BluetoothOutputStream(this); + } + + @Override + protected void finalize() throws Throwable { + try { + close(); + } finally { + super.finalize(); + } + } + + /** + * Attempt to connect to a remote device. + * This method will block until a connection is made or the connection + * fails. If this method returns without an exception then this socket + * is now connected. #close can be used to abort this call from another + * thread. + * @throws IOException On error, for example connection failure + */ + public void connect() throws IOException { + connectNative(); + } + + /** + * Closes this socket. + * This will cause other blocking calls on this socket to immediately + * throw an IOException. + */ + public void close() throws IOException { + closeNative(); + } + + /** + * Return the address we are connecting, or connected, to. + * @return Bluetooth address, or null if this socket has not yet attempted + * or established a connection. + */ + public String getAddress() { + return mAddress; + } + + /** + * Get the input stream associated with this socket. + * The input stream will be returned even if the socket is not yet + * connected, but operations on that stream will throw IOException until + * the associated socket is connected. + * @return InputStream + */ + public InputStream getInputStream() throws IOException { + return mInputStream; + } + + /** + * Get the output stream associated with this socket. + * The output stream will be returned even if the socket is not yet + * connected, but operations on that stream will throw IOException until + * the associated socket is connected. + * @return OutputStream + */ + public OutputStream getOutputStream() throws IOException { + return mOutputStream; + } + + private native void initSocketNative() throws IOException; + private native void initSocketFromFdNative(int fd) throws IOException; + private native void connectNative() throws IOException; + /*package*/ native void bindListenNative() throws IOException; + /*package*/ native BluetoothSocket acceptNative(int timeout) throws IOException; + /*package*/ native int availableNative() throws IOException; + /*package*/ native int readNative(byte[] b, int offset, int length) throws IOException; + /*package*/ native int writeNative(byte[] b, int offset, int length) throws IOException; + /*package*/ native void closeNative() throws IOException; + private native void destroyNative() throws IOException; +} diff --git a/core/java/android/bluetooth/Database.java b/core/java/android/bluetooth/Database.java deleted file mode 100644 index fef641a..0000000 --- a/core/java/android/bluetooth/Database.java +++ /dev/null @@ -1,200 +0,0 @@ -/* - * Copyright (C) 2007 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.bluetooth; - -import android.bluetooth.RfcommSocket; - -import android.util.Log; - -import java.io.*; -import java.util.*; - -/** - * The Android Bluetooth API is not finalized, and *will* change. Use at your - * own risk. - * - * A low-level API to the Service Discovery Protocol (SDP) Database. - * - * Allows service records to be added to the local SDP database. Once added, - * these services will be advertised to remote devices when they make SDP - * queries on this device. - * - * Currently this API is a thin wrapper to the bluez SDP Database API. See: - * http://wiki.bluez.org/wiki/Database - * http://wiki.bluez.org/wiki/HOWTO/ManagingServiceRecords - * @hide - */ -public final class Database { - private static Database mInstance; - - private static final String sLogName = "android.bluetooth.Database"; - - /** - * Class load time initialization - */ - static { - classInitNative(); - } - private native static void classInitNative(); - - /** - * Private to enforce singleton property - */ - private Database() { - initializeNativeDataNative(); - } - private native void initializeNativeDataNative(); - - protected void finalize() throws Throwable { - try { - cleanupNativeDataNative(); - } finally { - super.finalize(); - } - } - private native void cleanupNativeDataNative(); - - /** - * Singelton accessor - * @return The singleton instance of Database - */ - public static synchronized Database getInstance() { - if (mInstance == null) { - mInstance = new Database(); - } - return mInstance; - } - - /** - * Advertise a service with an RfcommSocket. - * - * This adds the service the SDP Database with the following attributes - * set: Service Name, Protocol Descriptor List, Service Class ID List - * TODO: Construct a byte[] record directly, rather than via XML. - * @param socket The rfcomm socket to advertise (by channel). - * @param serviceName A short name for this service - * @param uuid - * Unique identifier for this service, by which clients - * can search for your service - * @return Handle to the new service record - */ - public int advertiseRfcommService(RfcommSocket socket, - String serviceName, - UUID uuid) throws IOException { - String xmlRecord = - "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n" + - "<record>\n" + - " <attribute id=\"0x0001\">\n" + // ServiceClassIDList - " <sequence>\n" + - " <uuid value=\"" - + uuid.toString() + // UUID for this service - "\"/>\n" + - " </sequence>\n" + - " </attribute>\n" + - " <attribute id=\"0x0004\">\n" + // ProtocolDescriptorList - " <sequence>\n" + - " <sequence>\n" + - " <uuid value=\"0x0100\"/>\n" + // L2CAP - " </sequence>\n" + - " <sequence>\n" + - " <uuid value=\"0x0003\"/>\n" + // RFCOMM - " <uint8 value=\"" + - socket.getPort() + // RFCOMM port - "\" name=\"channel\"/>\n" + - " </sequence>\n" + - " </sequence>\n" + - " </attribute>\n" + - " <attribute id=\"0x0100\">\n" + // ServiceName - " <text value=\"" + serviceName + "\"/>\n" + - " </attribute>\n" + - "</record>\n"; - Log.i(sLogName, xmlRecord); - return addServiceRecordFromXml(xmlRecord); - } - - - /** - * Add a new service record. - * @param record The byte[] record - * @return A handle to the new record - */ - public synchronized int addServiceRecord(byte[] record) throws IOException { - int handle = addServiceRecordNative(record); - Log.i(sLogName, "Added SDP record: " + Integer.toHexString(handle)); - return handle; - } - private native int addServiceRecordNative(byte[] record) - throws IOException; - - /** - * Add a new service record, using XML. - * @param record The record as an XML string - * @return A handle to the new record - */ - public synchronized int addServiceRecordFromXml(String record) throws IOException { - int handle = addServiceRecordFromXmlNative(record); - Log.i(sLogName, "Added SDP record: " + Integer.toHexString(handle)); - return handle; - } - private native int addServiceRecordFromXmlNative(String record) - throws IOException; - - /** - * Update an exisiting service record. - * @param handle Handle to exisiting record - * @param record The updated byte[] record - */ - public synchronized void updateServiceRecord(int handle, byte[] record) { - try { - updateServiceRecordNative(handle, record); - } catch (IOException e) { - Log.e(getClass().toString(), e.getMessage()); - } - } - private native void updateServiceRecordNative(int handle, byte[] record) - throws IOException; - - /** - * Update an exisiting record, using XML. - * @param handle Handle to exisiting record - * @param record The record as an XML string. - */ - public synchronized void updateServiceRecordFromXml(int handle, String record) { - try { - updateServiceRecordFromXmlNative(handle, record); - } catch (IOException e) { - Log.e(getClass().toString(), e.getMessage()); - } - } - private native void updateServiceRecordFromXmlNative(int handle, String record) - throws IOException; - - /** - * Remove a service record. - * It is only possible to remove service records that were added by the - * current connection. - * @param handle Handle to exisiting record to be removed - */ - public synchronized void removeServiceRecord(int handle) { - try { - removeServiceRecordNative(handle); - } catch (IOException e) { - Log.e(getClass().toString(), e.getMessage()); - } - } - private native void removeServiceRecordNative(int handle) throws IOException; -} diff --git a/core/java/android/bluetooth/RfcommSocket.java b/core/java/android/bluetooth/RfcommSocket.java deleted file mode 100644 index a33263f..0000000 --- a/core/java/android/bluetooth/RfcommSocket.java +++ /dev/null @@ -1,674 +0,0 @@ -/* - * Copyright (C) 2007 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.bluetooth; - -import java.io.IOException; -import java.io.FileOutputStream; -import java.io.FileInputStream; -import java.io.OutputStream; -import java.io.InputStream; -import java.io.FileDescriptor; - -/** - * The Android Bluetooth API is not finalized, and *will* change. Use at your - * own risk. - * - * This class implements an API to the Bluetooth RFCOMM layer. An RFCOMM socket - * is similar to a normal socket in that it takes an address and a port number. - * The difference is of course that the address is a Bluetooth-device address, - * and the port number is an RFCOMM channel. The API allows for the - * establishment of listening sockets via methods - * {@link #bind(String, int) bind}, {@link #listen(int) listen}, and - * {@link #accept(RfcommSocket, int) accept}, as well as for the making of - * outgoing connections with {@link #connect(String, int) connect}, - * {@link #connectAsync(String, int) connectAsync}, and - * {@link #waitForAsyncConnect(int) waitForAsyncConnect}. - * - * After constructing a socket, you need to {@link #create() create} it and then - * {@link #destroy() destroy} it when you are done using it. Both - * {@link #create() create} and {@link #accept(RfcommSocket, int) accept} return - * a {@link java.io.FileDescriptor FileDescriptor} for the actual data. - * Alternatively, you may call {@link #getInputStream() getInputStream} and - * {@link #getOutputStream() getOutputStream} to retrieve the respective streams - * without going through the FileDescriptor. - * - * @hide - */ -public class RfcommSocket { - - /** - * Used by the native implementation of the class. - */ - private int mNativeData; - - /** - * Used by the native implementation of the class. - */ - private int mPort; - - /** - * Used by the native implementation of the class. - */ - private String mAddress; - - /** - * We save the return value of {@link #create() create} and - * {@link #accept(RfcommSocket,int) accept} in this variable, and use it to - * retrieve the I/O streams. - */ - private FileDescriptor mFd; - - /** - * After a call to {@link #waitForAsyncConnect(int) waitForAsyncConnect}, - * if the return value is zero, then, the the remaining time left to wait is - * written into this variable (by the native implementation). It is possible - * that {@link #waitForAsyncConnect(int) waitForAsyncConnect} returns before - * the user-specified timeout expires, which is why we save the remaining - * time in this member variable for the user to retrieve by calling method - * {@link #getRemainingAsyncConnectWaitingTimeMs() getRemainingAsyncConnectWaitingTimeMs}. - */ - private int mTimeoutRemainingMs; - - /** - * Set to true when an asynchronous (nonblocking) connect is in progress. - * {@see #connectAsync(String,int)}. - */ - private boolean mIsConnecting; - - /** - * Set to true after a successful call to {@link #bind(String,int) bind} and - * used for error checking in {@link #listen(int) listen}. Reset to false - * on {@link #destroy() destroy}. - */ - private boolean mIsBound = false; - - /** - * Set to true after a successful call to {@link #listen(int) listen} and - * used for error checking in {@link #accept(RfcommSocket,int) accept}. - * Reset to false on {@link #destroy() destroy}. - */ - private boolean mIsListening = false; - - /** - * Used to store the remaining time after an accept with a non-negative - * timeout returns unsuccessfully. It is possible that a blocking - * {@link #accept(int) accept} may wait for less than the time specified by - * the user, which is why we store the remainder in this member variable for - * it to be retrieved with method - * {@link #getRemainingAcceptWaitingTimeMs() getRemainingAcceptWaitingTimeMs}. - */ - private int mAcceptTimeoutRemainingMs; - - /** - * Maintained by {@link #getInputStream() getInputStream}. - */ - protected FileInputStream mInputStream; - - /** - * Maintained by {@link #getOutputStream() getOutputStream}. - */ - protected FileOutputStream mOutputStream; - - private native void initializeNativeDataNative(); - - /** - * Constructor. - */ - public RfcommSocket() { - initializeNativeDataNative(); - } - - private native void cleanupNativeDataNative(); - - /** - * Called by the GC to clean up the native data that we set up when we - * construct the object. - */ - protected void finalize() throws Throwable { - try { - cleanupNativeDataNative(); - } finally { - super.finalize(); - } - } - - private native static void classInitNative(); - - static { - classInitNative(); - } - - /** - * Creates a socket. You need to call this method before performing any - * other operation on a socket. - * - * @return FileDescriptor for the data stream. - * @throws IOException - * @see #destroy() - */ - public FileDescriptor create() throws IOException { - if (mFd == null) { - mFd = createNative(); - } - if (mFd == null) { - throw new IOException("socket not created"); - } - return mFd; - } - - private native FileDescriptor createNative(); - - /** - * Destroys a socket created by {@link #create() create}. Call this - * function when you no longer use the socket in order to release the - * underlying OS resources. - * - * @see #create() - */ - public void destroy() { - synchronized (this) { - destroyNative(); - mFd = null; - mIsBound = false; - mIsListening = false; - } - } - - private native void destroyNative(); - - /** - * Returns the {@link java.io.FileDescriptor FileDescriptor} of the socket. - * - * @return the FileDescriptor - * @throws IOException - * when the socket has not been {@link #create() created}. - */ - public FileDescriptor getFileDescriptor() throws IOException { - if (mFd == null) { - throw new IOException("socket not created"); - } - return mFd; - } - - /** - * Retrieves the input stream from the socket. Alternatively, you can do - * that from the FileDescriptor returned by {@link #create() create} or - * {@link #accept(RfcommSocket, int) accept}. - * - * @return InputStream - * @throws IOException - * if you have not called {@link #create() create} on the - * socket. - */ - public InputStream getInputStream() throws IOException { - if (mFd == null) { - throw new IOException("socket not created"); - } - - synchronized (this) { - if (mInputStream == null) { - mInputStream = new FileInputStream(mFd); - } - - return mInputStream; - } - } - - /** - * Retrieves the output stream from the socket. Alternatively, you can do - * that from the FileDescriptor returned by {@link #create() create} or - * {@link #accept(RfcommSocket, int) accept}. - * - * @return OutputStream - * @throws IOException - * if you have not called {@link #create() create} on the - * socket. - */ - public OutputStream getOutputStream() throws IOException { - if (mFd == null) { - throw new IOException("socket not created"); - } - - synchronized (this) { - if (mOutputStream == null) { - mOutputStream = new FileOutputStream(mFd); - } - - return mOutputStream; - } - } - - /** - * Starts a blocking connect to a remote RFCOMM socket. It takes the address - * of a device and the RFCOMM channel (port) to which to connect. - * - * @param address - * is the Bluetooth address of the remote device. - * @param port - * is the RFCOMM channel - * @return true on success, false on failure - * @throws IOException - * if {@link #create() create} has not been called. - * @see #connectAsync(String, int) - */ - public boolean connect(String address, int port) throws IOException { - synchronized (this) { - if (mFd == null) { - throw new IOException("socket not created"); - } - return connectNative(address, port); - } - } - - private native boolean connectNative(String address, int port); - - /** - * Starts an asynchronous (nonblocking) connect to a remote RFCOMM socket. - * It takes the address of the device to connect to, as well as the RFCOMM - * channel (port). On successful return (return value is true), you need to - * call method {@link #waitForAsyncConnect(int) waitForAsyncConnect} to - * block for up to a specified number of milliseconds while waiting for the - * asyncronous connect to complete. - * - * @param address - * of remote device - * @param port - * the RFCOMM channel - * @return true when the asynchronous connect has successfully started, - * false if there was an error. - * @throws IOException - * is you have not called {@link #create() create} - * @see #waitForAsyncConnect(int) - * @see #getRemainingAsyncConnectWaitingTimeMs() - * @see #connect(String, int) - */ - public boolean connectAsync(String address, int port) throws IOException { - synchronized (this) { - if (mFd == null) { - throw new IOException("socket not created"); - } - mIsConnecting = connectAsyncNative(address, port); - return mIsConnecting; - } - } - - private native boolean connectAsyncNative(String address, int port); - - /** - * Interrupts an asynchronous connect in progress. This method does nothing - * when there is no asynchronous connect in progress. - * - * @throws IOException - * if you have not called {@link #create() create}. - * @see #connectAsync(String, int) - */ - public void interruptAsyncConnect() throws IOException { - synchronized (this) { - if (mFd == null) { - throw new IOException("socket not created"); - } - if (mIsConnecting) { - mIsConnecting = !interruptAsyncConnectNative(); - } - } - } - - private native boolean interruptAsyncConnectNative(); - - /** - * Tells you whether there is an asynchronous connect in progress. This - * method returns an undefined value when there is a synchronous connect in - * progress. - * - * @return true if there is an asyc connect in progress, false otherwise - * @see #connectAsync(String, int) - */ - public boolean isConnecting() { - return mIsConnecting; - } - - /** - * Blocks for a specified amount of milliseconds while waiting for an - * asynchronous connect to complete. Returns an integer value to indicate - * one of the following: the connect succeeded, the connect is still in - * progress, or the connect failed. It is possible for this method to block - * for less than the time specified by the user, and still return zero - * (i.e., async connect is still in progress.) For this reason, if the - * return value is zero, you need to call method - * {@link #getRemainingAsyncConnectWaitingTimeMs() getRemainingAsyncConnectWaitingTimeMs} - * to retrieve the remaining time. - * - * @param timeoutMs - * the time to block while waiting for the async connect to - * complete. - * @return a positive value if the connect succeeds; zero, if the connect is - * still in progress, and a negative value if the connect failed. - * - * @throws IOException - * @see #getRemainingAsyncConnectWaitingTimeMs() - * @see #connectAsync(String, int) - */ - public int waitForAsyncConnect(int timeoutMs) throws IOException { - synchronized (this) { - if (mFd == null) { - throw new IOException("socket not created"); - } - int ret = waitForAsyncConnectNative(timeoutMs); - if (ret != 0) { - mIsConnecting = false; - } - return ret; - } - } - - private native int waitForAsyncConnectNative(int timeoutMs); - - /** - * Returns the number of milliseconds left to wait after the last call to - * {@link #waitForAsyncConnect(int) waitForAsyncConnect}. - * - * It is possible that waitForAsyncConnect() waits for less than the time - * specified by the user, and still returns zero (i.e., async connect is - * still in progress.) For this reason, if the return value is zero, you - * need to call this method to retrieve the remaining time before you call - * waitForAsyncConnect again. - * - * @return the remaining timeout in milliseconds. - * @see #waitForAsyncConnect(int) - * @see #connectAsync(String, int) - */ - public int getRemainingAsyncConnectWaitingTimeMs() { - return mTimeoutRemainingMs; - } - - /** - * Shuts down both directions on a socket. - * - * @return true on success, false on failure; if the return value is false, - * the socket might be left in a patially shut-down state (i.e. one - * direction is shut down, but the other is still open.) In this - * case, you should {@link #destroy() destroy} and then - * {@link #create() create} the socket again. - * @throws IOException - * is you have not caled {@link #create() create}. - * @see #shutdownInput() - * @see #shutdownOutput() - */ - public boolean shutdown() throws IOException { - synchronized (this) { - if (mFd == null) { - throw new IOException("socket not created"); - } - if (shutdownNative(true)) { - return shutdownNative(false); - } - - return false; - } - } - - /** - * Shuts down the input stream of the socket, but leaves the output stream - * in its current state. - * - * @return true on success, false on failure - * @throws IOException - * is you have not called {@link #create() create} - * @see #shutdown() - * @see #shutdownOutput() - */ - public boolean shutdownInput() throws IOException { - synchronized (this) { - if (mFd == null) { - throw new IOException("socket not created"); - } - return shutdownNative(true); - } - } - - /** - * Shut down the output stream of the socket, but leaves the input stream in - * its current state. - * - * @return true on success, false on failure - * @throws IOException - * is you have not called {@link #create() create} - * @see #shutdown() - * @see #shutdownInput() - */ - public boolean shutdownOutput() throws IOException { - synchronized (this) { - if (mFd == null) { - throw new IOException("socket not created"); - } - return shutdownNative(false); - } - } - - private native boolean shutdownNative(boolean shutdownInput); - - /** - * Tells you whether a socket is connected to another socket. This could be - * for input or output or both. - * - * @return true if connected, false otherwise. - * @see #isInputConnected() - * @see #isOutputConnected() - */ - public boolean isConnected() { - return isConnectedNative() > 0; - } - - /** - * Determines whether input is connected (i.e., whether you can receive data - * on this socket.) - * - * @return true if input is connected, false otherwise. - * @see #isConnected() - * @see #isOutputConnected() - */ - public boolean isInputConnected() { - return (isConnectedNative() & 1) != 0; - } - - /** - * Determines whether output is connected (i.e., whether you can send data - * on this socket.) - * - * @return true if output is connected, false otherwise. - * @see #isConnected() - * @see #isInputConnected() - */ - public boolean isOutputConnected() { - return (isConnectedNative() & 2) != 0; - } - - private native int isConnectedNative(); - - /** - * Binds a listening socket to the local device, or a non-listening socket - * to a remote device. The port is automatically selected as the first - * available port in the range 12 to 30. - * - * NOTE: Currently we ignore the device parameter and always bind the socket - * to the local device, assuming that it is a listening socket. - * - * TODO: Use bind(0) in native code to have the kernel select an unused - * port. - * - * @param device - * Bluetooth address of device to bind to (currently ignored). - * @return true on success, false on failure - * @throws IOException - * if you have not called {@link #create() create} - * @see #listen(int) - * @see #accept(RfcommSocket,int) - */ - public boolean bind(String device) throws IOException { - if (mFd == null) { - throw new IOException("socket not created"); - } - for (int port = 12; port <= 30; port++) { - if (bindNative(device, port)) { - mIsBound = true; - return true; - } - } - mIsBound = false; - return false; - } - - /** - * Binds a listening socket to the local device, or a non-listening socket - * to a remote device. - * - * NOTE: Currently we ignore the device parameter and always bind the socket - * to the local device, assuming that it is a listening socket. - * - * @param device - * Bluetooth address of device to bind to (currently ignored). - * @param port - * RFCOMM channel to bind socket to. - * @return true on success, false on failure - * @throws IOException - * if you have not called {@link #create() create} - * @see #listen(int) - * @see #accept(RfcommSocket,int) - */ - public boolean bind(String device, int port) throws IOException { - if (mFd == null) { - throw new IOException("socket not created"); - } - mIsBound = bindNative(device, port); - return mIsBound; - } - - private native boolean bindNative(String device, int port); - - /** - * Starts listening for incoming connections on this socket, after it has - * been bound to an address and RFCOMM channel with - * {@link #bind(String,int) bind}. - * - * @param backlog - * the number of pending incoming connections to queue for - * {@link #accept(RfcommSocket, int) accept}. - * @return true on success, false on failure - * @throws IOException - * if you have not called {@link #create() create} or if the - * socket has not been bound to a device and RFCOMM channel. - */ - public boolean listen(int backlog) throws IOException { - if (mFd == null) { - throw new IOException("socket not created"); - } - if (!mIsBound) { - throw new IOException("socket not bound"); - } - mIsListening = listenNative(backlog); - return mIsListening; - } - - private native boolean listenNative(int backlog); - - /** - * Accepts incoming-connection requests for a listening socket bound to an - * RFCOMM channel. The user may provide a time to wait for an incoming - * connection. - * - * Note that this method may return null (i.e., no incoming connection) - * before the user-specified timeout expires. For this reason, on a null - * return value, you need to call - * {@link #getRemainingAcceptWaitingTimeMs() getRemainingAcceptWaitingTimeMs} - * in order to see how much time is left to wait, before you call this - * method again. - * - * @param newSock - * is set to the new socket that is created as a result of a - * successful accept. - * @param timeoutMs - * time (in milliseconds) to block while waiting to an - * incoming-connection request. A negative value is an infinite - * wait. - * @return FileDescriptor of newSock on success, null on failure. Failure - * occurs if the timeout expires without a successful connect. - * @throws IOException - * if the socket has not been {@link #create() create}ed, is - * not bound, or is not a listening socket. - * @see #bind(String, int) - * @see #listen(int) - * @see #getRemainingAcceptWaitingTimeMs() - */ - public FileDescriptor accept(RfcommSocket newSock, int timeoutMs) - throws IOException { - synchronized (newSock) { - if (mFd == null) { - throw new IOException("socket not created"); - } - if (mIsListening == false) { - throw new IOException("not listening on socket"); - } - newSock.mFd = acceptNative(newSock, timeoutMs); - return newSock.mFd; - } - } - - /** - * Returns the number of milliseconds left to wait after the last call to - * {@link #accept(RfcommSocket, int) accept}. - * - * Since accept() may return null (i.e., no incoming connection) before the - * user-specified timeout expires, you need to call this method in order to - * see how much time is left to wait, and wait for that amount of time - * before you call accept again. - * - * @return the remaining time, in milliseconds. - */ - public int getRemainingAcceptWaitingTimeMs() { - return mAcceptTimeoutRemainingMs; - } - - private native FileDescriptor acceptNative(RfcommSocket newSock, - int timeoutMs); - - /** - * Get the port (rfcomm channel) associated with this socket. - * - * This is only valid if the port has been set via a successful call to - * {@link #bind(String, int)}, {@link #connect(String, int)} - * or {@link #connectAsync(String, int)}. This can be checked - * with {@link #isListening()} and {@link #isConnected()}. - * @return Port (rfcomm channel) - */ - public int getPort() throws IOException { - if (mFd == null) { - throw new IOException("socket not created"); - } - if (!mIsListening && !isConnected()) { - throw new IOException("not listening or connected on socket"); - } - return mPort; - } - - /** - * Return true if this socket is listening ({@link #listen(int)} - * has been called successfully). - */ - public boolean isListening() { - return mIsListening; - } -} diff --git a/core/java/android/content/AbstractCursorEntityIterator.java b/core/java/android/content/AbstractCursorEntityIterator.java new file mode 100644 index 0000000..bf3c4de --- /dev/null +++ b/core/java/android/content/AbstractCursorEntityIterator.java @@ -0,0 +1,112 @@ +package android.content; + +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.os.RemoteException; + +/** + * An abstract class that makes it easy to implement an EntityIterator over a cursor. + * The user must implement {@link #newEntityFromCursorLocked}, which runs inside of a + * database transaction. + */ +public abstract class AbstractCursorEntityIterator implements EntityIterator { + private final Cursor mEntityCursor; + private final SQLiteDatabase mDb; + private volatile Entity mNextEntity; + private volatile boolean mIsClosed; + + public AbstractCursorEntityIterator(SQLiteDatabase db, Cursor entityCursor) { + mEntityCursor = entityCursor; + mDb = db; + mNextEntity = null; + mIsClosed = false; + } + + /** + * If there are entries left in the cursor then advance the cursor and use the new row to + * populate mNextEntity. If the cursor is at the end or if advancing it causes the cursor + * to become at the end then set mEntityCursor to null. If newEntityFromCursor returns null + * then continue advancing until it either returns a non-null Entity or the cursor reaches + * the end. + */ + private void fillEntityIfAvailable() { + while (mNextEntity == null) { + if (!mEntityCursor.moveToNext()) { + // the cursor is at then end, bail out + return; + } + // This may return null if newEntityFromCursor is not able to create an entity + // from the current cursor position. In that case this method will loop and try + // the next cursor position + mNextEntity = newEntityFromCursorLocked(mEntityCursor); + } + mDb.beginTransaction(); + try { + int position = mEntityCursor.getPosition(); + mNextEntity = newEntityFromCursorLocked(mEntityCursor); + int newPosition = mEntityCursor.getPosition(); + if (newPosition != position) { + throw new IllegalStateException("the cursor position changed during the call to" + + "newEntityFromCursorLocked, from " + position + " to " + newPosition); + } + } finally { + mDb.endTransaction(); + } + } + + /** + * Checks if there are more Entities accessible via this iterator. This may not be called + * if the iterator is already closed. + * @return true if the call to next() will return an Entity. + */ + public boolean hasNext() { + if (mIsClosed) { + throw new IllegalStateException("calling hasNext() when the iterator is closed"); + } + fillEntityIfAvailable(); + return mNextEntity != null; + } + + /** + * Returns the next Entity that is accessible via this iterator. This may not be called + * if the iterator is already closed. + * @return the next Entity that is accessible via this iterator + */ + public Entity next() { + if (mIsClosed) { + throw new IllegalStateException("calling next() when the iterator is closed"); + } + if (!hasNext()) { + throw new IllegalStateException("you may only call next() if hasNext() is true"); + } + + try { + return mNextEntity; + } finally { + mNextEntity = null; + } + } + + /** + * Closes this iterator making it invalid. If is invalid for the user to call any public + * method on the iterator once it has been closed. + */ + public void close() { + if (mIsClosed) { + throw new IllegalStateException("closing when already closed"); + } + mIsClosed = true; + mEntityCursor.close(); + } + + /** + * Returns a new Entity from the current cursor position. This is called from within a + * database transaction. If a new entity cannot be created from this cursor position (e.g. + * if the row that is referred to no longer exists) then this may return null. The cursor + * is guaranteed to be pointing to a valid row when this call is made. The implementation + * of newEntityFromCursorLocked is not allowed to change the position of the cursor. + * @param cursor from where to read the data for the Entity + * @return an Entity that corresponds to the current cursor position or null + */ + public abstract Entity newEntityFromCursorLocked(Cursor cursor); +} diff --git a/core/java/android/content/AbstractSyncableContentProvider.java b/core/java/android/content/AbstractSyncableContentProvider.java index 249d9ba..e628dcd 100644 --- a/core/java/android/content/AbstractSyncableContentProvider.java +++ b/core/java/android/content/AbstractSyncableContentProvider.java @@ -4,8 +4,9 @@ import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteDatabase; import android.database.Cursor; import android.net.Uri; -import android.accounts.AccountMonitor; -import android.accounts.AccountMonitorListener; +import android.accounts.OnAccountsUpdatedListener; +import android.accounts.Account; +import android.accounts.AccountManager; import android.provider.SyncConstValue; import android.util.Config; import android.util.Log; @@ -14,9 +15,12 @@ import android.text.TextUtils; import java.util.Collections; import java.util.Map; -import java.util.HashMap; import java.util.Vector; import java.util.ArrayList; +import java.util.Set; +import java.util.HashSet; + +import com.google.android.collect.Maps; /** * A specialization of the ContentProvider that centralizes functionality @@ -32,26 +36,30 @@ public abstract class AbstractSyncableContentProvider extends SyncableContentPro private final String mDatabaseName; private final int mDatabaseVersion; private final Uri mContentUri; - private AccountMonitor mAccountMonitor; /** the account set in the last call to onSyncStart() */ - private String mSyncingAccount; + private Account mSyncingAccount; private SyncStateContentProviderHelper mSyncState = null; - private static final String[] sAccountProjection = new String[] {SyncConstValue._SYNC_ACCOUNT}; + private static final String[] sAccountProjection = + new String[] {SyncConstValue._SYNC_ACCOUNT, SyncConstValue._SYNC_ACCOUNT_TYPE}; private boolean mIsTemporary; private AbstractTableMerger mCurrentMerger = null; private boolean mIsMergeCancelled = false; - private static final String SYNC_ACCOUNT_WHERE_CLAUSE = SyncConstValue._SYNC_ACCOUNT + "=?"; + private static final String SYNC_ACCOUNT_WHERE_CLAUSE = + SyncConstValue._SYNC_ACCOUNT + "=? AND " + SyncConstValue._SYNC_ACCOUNT_TYPE + "=?"; protected boolean isTemporary() { return mIsTemporary; } + private final ThreadLocal<Boolean> mApplyingBatch = new ThreadLocal<Boolean>(); + private final ThreadLocal<Set<Uri>> mPendingBatchNotifications = new ThreadLocal<Set<Uri>>(); + /** * Indicates whether or not this ContentProvider contains a full * set of data or just diffs. This knowledge comes in handy when @@ -150,19 +158,19 @@ public abstract class AbstractSyncableContentProvider extends SyncableContentPro mOpenHelper = new AbstractSyncableContentProvider.DatabaseHelper(getContext(), mDatabaseName); mSyncState = new SyncStateContentProviderHelper(mOpenHelper); - - AccountMonitorListener listener = new AccountMonitorListener() { - public void onAccountsUpdated(String[] accounts) { - // Some providers override onAccountsChanged(); give them a database to work with. - mDb = mOpenHelper.getWritableDatabase(); - onAccountsChanged(accounts); - TempProviderSyncAdapter syncAdapter = (TempProviderSyncAdapter)getSyncAdapter(); - if (syncAdapter != null) { - syncAdapter.onAccountsChanged(accounts); - } - } - }; - mAccountMonitor = new AccountMonitor(getContext(), listener); + AccountManager.get(getContext()).addOnAccountsUpdatedListener( + new OnAccountsUpdatedListener() { + public void onAccountsUpdated(Account[] accounts) { + // Some providers override onAccountsChanged(); give them a database to + // work with. + mDb = mOpenHelper.getWritableDatabase(); + onAccountsChanged(accounts); + TempProviderSyncAdapter syncAdapter = getTempProviderSyncAdapter(); + if (syncAdapter != null) { + syncAdapter.onAccountsChanged(accounts); + } + } + }, null /* handler */, true /* updateImmediately */); return true; } @@ -236,147 +244,117 @@ public abstract class AbstractSyncableContentProvider extends SyncableContentPro return Collections.emptyList(); } - /** - * <p> - * Call mOpenHelper.getWritableDatabase() and mDb.beginTransaction(). - * {@link #endTransaction} MUST be called after calling this method. - * Those methods should be used like this: - * </p> - * - * <pre class="prettyprint"> - * boolean successful = false; - * beginTransaction(); - * try { - * // Do something related to mDb - * successful = true; - * return ret; - * } finally { - * endTransaction(successful); - * } - * </pre> - * - * @hide This method is dangerous from the view of database manipulation, though using - * this makes batch insertion/update/delete much faster. - */ - public final void beginTransaction() { + @Override + public final int update(final Uri url, final ContentValues values, + final String selection, final String[] selectionArgs) { mDb = mOpenHelper.getWritableDatabase(); - mDb.beginTransaction(); - } - - /** - * <p> - * Call mDb.endTransaction(). If successful is true, try to call - * mDb.setTransactionSuccessful() before calling mDb.endTransaction(). - * This method MUST be used with {@link #beginTransaction()}. - * </p> - * - * @hide This method is dangerous from the view of database manipulation, though using - * this makes batch insertion/update/delete much faster. - */ - public final void endTransaction(boolean successful) { + final boolean notApplyingBatch = !applyingBatch(); + if (notApplyingBatch) { + mDb.beginTransaction(); + } try { - if (successful) { - // setTransactionSuccessful() must be called just once during opening the - // transaction. - mDb.setTransactionSuccessful(); + if (isTemporary() && mSyncState.matches(url)) { + int numRows = mSyncState.asContentProvider().update( + url, values, selection, selectionArgs); + if (notApplyingBatch) { + mDb.setTransactionSuccessful(); + } + return numRows; } - } finally { - mDb.endTransaction(); - } - } - @Override - public final int update(final Uri uri, final ContentValues values, - final String selection, final String[] selectionArgs) { - boolean successful = false; - beginTransaction(); - try { - int ret = nonTransactionalUpdate(uri, values, selection, selectionArgs); - successful = true; - return ret; + int result = updateInternal(url, values, selection, selectionArgs); + if (notApplyingBatch) { + mDb.setTransactionSuccessful(); + } + if (!isTemporary() && result > 0) { + if (notApplyingBatch) { + getContext().getContentResolver().notifyChange(url, null /* observer */, + changeRequiresLocalSync(url)); + } else { + mPendingBatchNotifications.get().add(url); + } + } + return result; } finally { - endTransaction(successful); - } - } - - /** - * @hide - */ - public final int nonTransactionalUpdate(final Uri uri, final ContentValues values, - final String selection, final String[] selectionArgs) { - if (isTemporary() && mSyncState.matches(uri)) { - int numRows = mSyncState.asContentProvider().update( - uri, values, selection, selectionArgs); - return numRows; - } - - int result = updateInternal(uri, values, selection, selectionArgs); - if (!isTemporary() && result > 0) { - getContext().getContentResolver().notifyChange(uri, null /* observer */, - changeRequiresLocalSync(uri)); + if (notApplyingBatch) { + mDb.endTransaction(); + } } - - return result; } @Override - public final int delete(final Uri uri, final String selection, + public final int delete(final Uri url, final String selection, final String[] selectionArgs) { - boolean successful = false; - beginTransaction(); + mDb = mOpenHelper.getWritableDatabase(); + final boolean notApplyingBatch = !applyingBatch(); + if (notApplyingBatch) { + mDb.beginTransaction(); + } try { - int ret = nonTransactionalDelete(uri, selection, selectionArgs); - successful = true; - return ret; + if (isTemporary() && mSyncState.matches(url)) { + int numRows = mSyncState.asContentProvider().delete(url, selection, selectionArgs); + if (notApplyingBatch) { + mDb.setTransactionSuccessful(); + } + return numRows; + } + int result = deleteInternal(url, selection, selectionArgs); + if (notApplyingBatch) { + mDb.setTransactionSuccessful(); + } + if (!isTemporary() && result > 0) { + if (notApplyingBatch) { + getContext().getContentResolver().notifyChange(url, null /* observer */, + changeRequiresLocalSync(url)); + } else { + mPendingBatchNotifications.get().add(url); + } + } + return result; } finally { - endTransaction(successful); + if (notApplyingBatch) { + mDb.endTransaction(); + } } } - /** - * @hide - */ - public final int nonTransactionalDelete(final Uri uri, final String selection, - final String[] selectionArgs) { - if (isTemporary() && mSyncState.matches(uri)) { - int numRows = mSyncState.asContentProvider().delete(uri, selection, selectionArgs); - return numRows; - } - int result = deleteInternal(uri, selection, selectionArgs); - if (!isTemporary() && result > 0) { - getContext().getContentResolver().notifyChange(uri, null /* observer */, - changeRequiresLocalSync(uri)); - } - return result; + private boolean applyingBatch() { + return mApplyingBatch.get() != null && mApplyingBatch.get(); } @Override - public final Uri insert(final Uri uri, final ContentValues values) { - boolean successful = false; - beginTransaction(); - try { - Uri ret = nonTransactionalInsert(uri, values); - successful = true; - return ret; - } finally { - endTransaction(successful); + public final Uri insert(final Uri url, final ContentValues values) { + mDb = mOpenHelper.getWritableDatabase(); + final boolean notApplyingBatch = !applyingBatch(); + if (notApplyingBatch) { + mDb.beginTransaction(); } - } - - /** - * @hide - */ - public final Uri nonTransactionalInsert(final Uri uri, final ContentValues values) { - if (isTemporary() && mSyncState.matches(uri)) { - Uri result = mSyncState.asContentProvider().insert(uri, values); + try { + if (isTemporary() && mSyncState.matches(url)) { + Uri result = mSyncState.asContentProvider().insert(url, values); + if (notApplyingBatch) { + mDb.setTransactionSuccessful(); + } + return result; + } + Uri result = insertInternal(url, values); + if (notApplyingBatch) { + mDb.setTransactionSuccessful(); + } + if (!isTemporary() && result != null) { + if (notApplyingBatch) { + getContext().getContentResolver().notifyChange(url, null /* observer */, + changeRequiresLocalSync(url)); + } else { + mPendingBatchNotifications.get().add(url); + } + } return result; + } finally { + if (notApplyingBatch) { + mDb.endTransaction(); + } } - Uri result = insertInternal(uri, values); - if (!isTemporary() && result != null) { - getContext().getContentResolver().notifyChange(uri, null /* observer */, - changeRequiresLocalSync(uri)); - } - return result; } @Override @@ -411,6 +389,92 @@ public abstract class AbstractSyncableContentProvider extends SyncableContentPro } /** + * <p> + * Start batch transaction. {@link #endTransaction} MUST be called after + * calling this method. Those methods should be used like this: + * </p> + * + * <pre class="prettyprint"> + * boolean successful = false; + * beginBatch() + * try { + * // Do something related to mDb + * successful = true; + * return ret; + * } finally { + * endBatch(successful); + * } + * </pre> + * + * @hide This method should be used only when {@link ContentProvider#applyBatch} is not enough and must be + * used with {@link #endBatch}. + * e.g. If returned value has to be used during one transaction, this method might be useful. + */ + public final void beginBatch() { + // initialize if this is the first time this thread has applied a batch + if (mApplyingBatch.get() == null) { + mApplyingBatch.set(false); + mPendingBatchNotifications.set(new HashSet<Uri>()); + } + + if (applyingBatch()) { + throw new IllegalStateException( + "applyBatch is not reentrant but mApplyingBatch is already set"); + } + SQLiteDatabase db = getDatabase(); + db.beginTransaction(); + boolean successful = false; + try { + mApplyingBatch.set(true); + successful = true; + } finally { + if (!successful) { + // Something unexpected happened. We must call endTransaction() at least. + db.endTransaction(); + } + } + } + + /** + * <p> + * Finish batch transaction. If "successful" is true, try to call + * mDb.setTransactionSuccessful() before calling mDb.endTransaction(). + * This method MUST be used with {@link #beginBatch()}. + * </p> + * + * @hide This method must be used with {@link #beginTransaction} + */ + public final void endBatch(boolean successful) { + try { + if (successful) { + // setTransactionSuccessful() must be called just once during opening the + // transaction. + mDb.setTransactionSuccessful(); + } + } finally { + mApplyingBatch.set(false); + getDatabase().endTransaction(); + for (Uri url : mPendingBatchNotifications.get()) { + getContext().getContentResolver().notifyChange(url, null /* observer */, + changeRequiresLocalSync(url)); + } + } + } + + public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) + throws OperationApplicationException { + boolean successful = false; + beginBatch(); + try { + ContentProviderResult[] results = super.applyBatch(operations); + successful = true; + return results; + } finally { + endBatch(successful); + } + } + + /** * Check if changes to this URI can be syncable changes. * @param uri the URI of the resource that was changed * @return true if changes to this URI can be syncable changes, false otherwise @@ -437,8 +501,8 @@ public abstract class AbstractSyncableContentProvider extends SyncableContentPro * @param context the sync context for the operation * @param account */ - public void onSyncStart(SyncContext context, String account) { - if (TextUtils.isEmpty(account)) { + public void onSyncStart(SyncContext context, Account account) { + if (account == null) { throw new IllegalArgumentException("you passed in an empty account"); } mSyncingAccount = account; @@ -457,7 +521,7 @@ public abstract class AbstractSyncableContentProvider extends SyncableContentPro * The account of the most recent call to onSyncStart() * @return the account */ - public String getSyncingAccount() { + public Account getSyncingAccount() { return mSyncingAccount; } @@ -568,12 +632,11 @@ public abstract class AbstractSyncableContentProvider extends SyncableContentPro * Make sure that there are no entries for accounts that no longer exist * @param accountsArray the array of currently-existing accounts */ - protected void onAccountsChanged(String[] accountsArray) { - Map<String, Boolean> accounts = new HashMap<String, Boolean>(); - for (String account : accountsArray) { + protected void onAccountsChanged(Account[] accountsArray) { + Map<Account, Boolean> accounts = Maps.newHashMap(); + for (Account account : accountsArray) { accounts.put(account, false); } - accounts.put(SyncConstValue.NON_SYNCABLE_ACCOUNT, false); SQLiteDatabase db = mOpenHelper.getWritableDatabase(); Map<String, String> tableMap = db.getSyncedTables(); @@ -585,8 +648,7 @@ public abstract class AbstractSyncableContentProvider extends SyncableContentPro try { mSyncState.onAccountsChanged(accountsArray); for (String table : tables) { - deleteRowsForRemovedAccounts(accounts, table, - SyncConstValue._SYNC_ACCOUNT); + deleteRowsForRemovedAccounts(accounts, table); } db.setTransactionSuccessful(); } finally { @@ -601,23 +663,23 @@ public abstract class AbstractSyncableContentProvider extends SyncableContentPro * * @param accounts a map of existing accounts * @param table the table to delete from - * @param accountColumnName the name of the column that is expected - * to hold the account. */ - protected void deleteRowsForRemovedAccounts(Map<String, Boolean> accounts, - String table, String accountColumnName) { + protected void deleteRowsForRemovedAccounts(Map<Account, Boolean> accounts, String table) { SQLiteDatabase db = mOpenHelper.getWritableDatabase(); Cursor c = db.query(table, sAccountProjection, null, null, - accountColumnName, null, null); + "_sync_account, _sync_account_type", null, null); try { while (c.moveToNext()) { - String account = c.getString(0); - if (TextUtils.isEmpty(account)) { + String accountName = c.getString(0); + String accountType = c.getString(1); + if (TextUtils.isEmpty(accountName)) { continue; } + Account account = new Account(accountName, accountType); if (!accounts.containsKey(account)) { int numDeleted; - numDeleted = db.delete(table, accountColumnName + "=?", new String[]{account}); + numDeleted = db.delete(table, "_sync_account=? AND _sync_account_type=?", + new String[]{account.mName, account.mType}); if (Config.LOGV) { Log.v(TAG, "deleted " + numDeleted + " records from table " + table @@ -634,7 +696,7 @@ public abstract class AbstractSyncableContentProvider extends SyncableContentPro * Called when the sync system determines that this provider should no longer * contain records for the specified account. */ - public void wipeAccount(String account) { + public void wipeAccount(Account account) { SQLiteDatabase db = mOpenHelper.getWritableDatabase(); Map<String, String> tableMap = db.getSyncedTables(); ArrayList<String> tables = new ArrayList<String>(); @@ -649,7 +711,8 @@ public abstract class AbstractSyncableContentProvider extends SyncableContentPro // remove the data in the synced tables for (String table : tables) { - db.delete(table, SYNC_ACCOUNT_WHERE_CLAUSE, new String[]{account}); + db.delete(table, SYNC_ACCOUNT_WHERE_CLAUSE, + new String[]{account.mName, account.mType}); } db.setTransactionSuccessful(); } finally { @@ -660,14 +723,14 @@ public abstract class AbstractSyncableContentProvider extends SyncableContentPro /** * Retrieves the SyncData bytes for the given account. The byte array returned may be null. */ - public byte[] readSyncDataBytes(String account) { + public byte[] readSyncDataBytes(Account account) { return mSyncState.readSyncDataBytes(mOpenHelper.getReadableDatabase(), account); } /** * Sets the SyncData bytes for the given account. The byte array may be null. */ - public void writeSyncDataBytes(String account, byte[] data) { + public void writeSyncDataBytes(Account account, byte[] data) { mSyncState.writeSyncDataBytes(mOpenHelper.getWritableDatabase(), account, data); } } diff --git a/core/java/android/content/AbstractTableMerger.java b/core/java/android/content/AbstractTableMerger.java index 94afcee..835dd91 100644 --- a/core/java/android/content/AbstractTableMerger.java +++ b/core/java/android/content/AbstractTableMerger.java @@ -25,6 +25,7 @@ import android.provider.BaseColumns; import static android.provider.SyncConstValue.*; import android.text.TextUtils; import android.util.Log; +import android.accounts.Account; /** * @hide @@ -55,15 +56,17 @@ public abstract class AbstractTableMerger private volatile boolean mIsMergeCancelled; - private static final String SELECT_MARKED = _SYNC_MARK + "> 0 and " + _SYNC_ACCOUNT + "=?"; + private static final String SELECT_MARKED = _SYNC_MARK + "> 0 and " + + _SYNC_ACCOUNT + "=? and " + _SYNC_ACCOUNT_TYPE + "=?"; private static final String SELECT_BY_SYNC_ID_AND_ACCOUNT = - _SYNC_ID +"=? and " + _SYNC_ACCOUNT + "=?"; + _SYNC_ID +"=? and " + _SYNC_ACCOUNT + "=? and " + _SYNC_ACCOUNT_TYPE + "=?"; private static final String SELECT_BY_ID = BaseColumns._ID +"=?"; // The last clause rejects events with a null _SYNC_VERSION if they've already been synced private static final String SELECT_UNSYNCED = "" - + _SYNC_DIRTY + " > 0 and (" + _SYNC_ACCOUNT + "=? or " + _SYNC_ACCOUNT + " is null) " + + _SYNC_DIRTY + " > 0 and ((" + _SYNC_ACCOUNT + "=? AND " + _SYNC_ACCOUNT_TYPE + "=?) " + + "or " + _SYNC_ACCOUNT + " is null) " + "and (" + _SYNC_VERSION + " is not null or " + _SYNC_ACCOUNT + " is null)"; public AbstractTableMerger(SQLiteDatabase database, @@ -134,7 +137,7 @@ public abstract class AbstractTableMerger * construct a temporary instance to hold them. */ public void merge(final SyncContext context, - final String account, + final Account account, final SyncableContentProvider serverDiffs, TempProviderSyncResult result, SyncResult syncResult, SyncableContentProvider temporaryInstanceFactory) { @@ -157,7 +160,7 @@ public abstract class AbstractTableMerger * @hide this is public for testing purposes only */ public void mergeServerDiffs(SyncContext context, - String account, SyncableContentProvider serverDiffs, SyncResult syncResult) { + Account account, SyncableContentProvider serverDiffs, SyncResult syncResult) { boolean diffsArePartial = serverDiffs.getContainsDiffs(); // mark the current rows so that we can distinguish these from new // inserts that occur during the merge @@ -166,339 +169,337 @@ public abstract class AbstractTableMerger mDb.update(mDeletedTable, mSyncMarkValues, null, null); } - // load the local database entries, so we can merge them with the server - final String[] accountSelectionArgs = new String[]{account}; - Cursor localCursor = mDb.query(mTable, syncDirtyProjection, - SELECT_MARKED, accountSelectionArgs, null, null, - mTable + "." + _SYNC_ID); - Cursor deletedCursor; - if (mDeletedTable != null) { - deletedCursor = mDb.query(mDeletedTable, syncIdAndVersionProjection, + Cursor localCursor = null; + Cursor deletedCursor = null; + Cursor diffsCursor = null; + try { + // load the local database entries, so we can merge them with the server + final String[] accountSelectionArgs = new String[]{account.mName, account.mType}; + localCursor = mDb.query(mTable, syncDirtyProjection, SELECT_MARKED, accountSelectionArgs, null, null, - mDeletedTable + "." + _SYNC_ID); - } else { - deletedCursor = - mDb.rawQuery("select 'a' as _sync_id, 'b' as _sync_version limit 0", null); - } - - // Apply updates and insertions from the server - Cursor diffsCursor = serverDiffs.query(mTableURL, - null, null, null, mTable + "." + _SYNC_ID); - int deletedSyncIDColumn = deletedCursor.getColumnIndexOrThrow(_SYNC_ID); - int deletedSyncVersionColumn = deletedCursor.getColumnIndexOrThrow(_SYNC_VERSION); - int serverSyncIDColumn = diffsCursor.getColumnIndexOrThrow(_SYNC_ID); - int serverSyncVersionColumn = diffsCursor.getColumnIndexOrThrow(_SYNC_VERSION); - int serverSyncLocalIdColumn = diffsCursor.getColumnIndexOrThrow(_SYNC_LOCAL_ID); - - String lastSyncId = null; - int diffsCount = 0; - int localCount = 0; - localCursor.moveToFirst(); - deletedCursor.moveToFirst(); - while (diffsCursor.moveToNext()) { - if (mIsMergeCancelled) { - localCursor.close(); - deletedCursor.close(); - diffsCursor.close(); - return; - } - mDb.yieldIfContended(); - String serverSyncId = diffsCursor.getString(serverSyncIDColumn); - String serverSyncVersion = diffsCursor.getString(serverSyncVersionColumn); - long localRowId = 0; - String localSyncVersion = null; - - diffsCount++; - context.setStatusText("Processing " + diffsCount + "/" - + diffsCursor.getCount()); - if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "processing server entry " + - diffsCount + ", " + serverSyncId); - - if (TRACE) { - if (diffsCount == 10) { - Debug.startMethodTracing("atmtrace"); - } - if (diffsCount == 20) { - Debug.stopMethodTracing(); - } + mTable + "." + _SYNC_ID); + if (mDeletedTable != null) { + deletedCursor = mDb.query(mDeletedTable, syncIdAndVersionProjection, + SELECT_MARKED, accountSelectionArgs, null, null, + mDeletedTable + "." + _SYNC_ID); + } else { + deletedCursor = + mDb.rawQuery("select 'a' as _sync_id, 'b' as _sync_version limit 0", null); } - boolean conflict = false; - boolean update = false; - boolean insert = false; + // Apply updates and insertions from the server + diffsCursor = serverDiffs.query(mTableURL, + null, null, null, mTable + "." + _SYNC_ID); + int deletedSyncIDColumn = deletedCursor.getColumnIndexOrThrow(_SYNC_ID); + int deletedSyncVersionColumn = deletedCursor.getColumnIndexOrThrow(_SYNC_VERSION); + int serverSyncIDColumn = diffsCursor.getColumnIndexOrThrow(_SYNC_ID); + int serverSyncVersionColumn = diffsCursor.getColumnIndexOrThrow(_SYNC_VERSION); + int serverSyncLocalIdColumn = diffsCursor.getColumnIndexOrThrow(_SYNC_LOCAL_ID); - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "found event with serverSyncID " + serverSyncId); - } - if (TextUtils.isEmpty(serverSyncId)) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.e(TAG, "server entry doesn't have a serverSyncID"); + String lastSyncId = null; + int diffsCount = 0; + int localCount = 0; + localCursor.moveToFirst(); + deletedCursor.moveToFirst(); + while (diffsCursor.moveToNext()) { + if (mIsMergeCancelled) { + return; } - continue; - } - - // It is possible that the sync adapter wrote the same record multiple times, - // e.g. if the same record came via multiple feeds. If this happens just ignore - // the duplicate records. - if (serverSyncId.equals(lastSyncId)) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "skipping record with duplicate remote server id " + lastSyncId); + mDb.yieldIfContended(); + String serverSyncId = diffsCursor.getString(serverSyncIDColumn); + String serverSyncVersion = diffsCursor.getString(serverSyncVersionColumn); + long localRowId = 0; + String localSyncVersion = null; + + diffsCount++; + context.setStatusText("Processing " + diffsCount + "/" + + diffsCursor.getCount()); + if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "processing server entry " + + diffsCount + ", " + serverSyncId); + + if (TRACE) { + if (diffsCount == 10) { + Debug.startMethodTracing("atmtrace"); + } + if (diffsCount == 20) { + Debug.stopMethodTracing(); + } } - continue; - } - lastSyncId = serverSyncId; - String localSyncID = null; - boolean localSyncDirty = false; + boolean conflict = false; + boolean update = false; + boolean insert = false; - while (!localCursor.isAfterLast()) { - if (mIsMergeCancelled) { - localCursor.deactivate(); - deletedCursor.deactivate(); - diffsCursor.deactivate(); - return; + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "found event with serverSyncID " + serverSyncId); + } + if (TextUtils.isEmpty(serverSyncId)) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.e(TAG, "server entry doesn't have a serverSyncID"); + } + continue; } - localCount++; - localSyncID = localCursor.getString(2); - // If the local record doesn't have a _sync_id then - // it is new. Ignore it for now, we will send an insert - // the the server later. - if (TextUtils.isEmpty(localSyncID)) { + // It is possible that the sync adapter wrote the same record multiple times, + // e.g. if the same record came via multiple feeds. If this happens just ignore + // the duplicate records. + if (serverSyncId.equals(lastSyncId)) { if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "local record " + - localCursor.getLong(1) + - " has no _sync_id, ignoring"); + Log.v(TAG, "skipping record with duplicate remote server id " + lastSyncId); } - localCursor.moveToNext(); - localSyncID = null; continue; } + lastSyncId = serverSyncId; - int comp = serverSyncId.compareTo(localSyncID); + String localSyncID = null; + boolean localSyncDirty = false; - // the local DB has a record that the server doesn't have - if (comp > 0) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "local record " + - localCursor.getLong(1) + - " has _sync_id " + localSyncID + - " that is < server _sync_id " + serverSyncId); + while (!localCursor.isAfterLast()) { + if (mIsMergeCancelled) { + return; } - if (diffsArePartial) { - localCursor.moveToNext(); - } else { - deleteRow(localCursor); - if (mDeletedTable != null) { - mDb.delete(mDeletedTable, _SYNC_ID +"=?", new String[] {localSyncID}); + localCount++; + localSyncID = localCursor.getString(2); + + // If the local record doesn't have a _sync_id then + // it is new. Ignore it for now, we will send an insert + // the the server later. + if (TextUtils.isEmpty(localSyncID)) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "local record " + + localCursor.getLong(1) + + " has no _sync_id, ignoring"); } - syncResult.stats.numDeletes++; - mDb.yieldIfContended(); + localCursor.moveToNext(); + localSyncID = null; + continue; } - localSyncID = null; - continue; - } - // the server has a record that the local DB doesn't have - if (comp < 0) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "local record " + - localCursor.getLong(1) + - " has _sync_id " + localSyncID + - " that is > server _sync_id " + serverSyncId); + int comp = serverSyncId.compareTo(localSyncID); + + // the local DB has a record that the server doesn't have + if (comp > 0) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "local record " + + localCursor.getLong(1) + + " has _sync_id " + localSyncID + + " that is < server _sync_id " + serverSyncId); + } + if (diffsArePartial) { + localCursor.moveToNext(); + } else { + deleteRow(localCursor); + if (mDeletedTable != null) { + mDb.delete(mDeletedTable, _SYNC_ID +"=?", new String[] {localSyncID}); + } + syncResult.stats.numDeletes++; + mDb.yieldIfContended(); + } + localSyncID = null; + continue; } - localSyncID = null; - } - // the server and the local DB both have this record - if (comp == 0) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "local record " + - localCursor.getLong(1) + - " has _sync_id " + localSyncID + - " that matches the server _sync_id"); + // the server has a record that the local DB doesn't have + if (comp < 0) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "local record " + + localCursor.getLong(1) + + " has _sync_id " + localSyncID + + " that is > server _sync_id " + serverSyncId); + } + localSyncID = null; } - localSyncDirty = localCursor.getInt(0) != 0; - localRowId = localCursor.getLong(1); - localSyncVersion = localCursor.getString(3); - localCursor.moveToNext(); - } - break; - } + // the server and the local DB both have this record + if (comp == 0) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "local record " + + localCursor.getLong(1) + + " has _sync_id " + localSyncID + + " that matches the server _sync_id"); + } + localSyncDirty = localCursor.getInt(0) != 0; + localRowId = localCursor.getLong(1); + localSyncVersion = localCursor.getString(3); + localCursor.moveToNext(); + } - // If this record is in the deleted table then update the server version - // in the deleted table, if necessary, and then ignore it here. - // We will send a deletion indication to the server down a - // little further. - if (findInCursor(deletedCursor, deletedSyncIDColumn, serverSyncId)) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "remote record " + serverSyncId + " is in the deleted table"); + break; } - final String deletedSyncVersion = deletedCursor.getString(deletedSyncVersionColumn); - if (!TextUtils.equals(deletedSyncVersion, serverSyncVersion)) { + + // If this record is in the deleted table then update the server version + // in the deleted table, if necessary, and then ignore it here. + // We will send a deletion indication to the server down a + // little further. + if (findInCursor(deletedCursor, deletedSyncIDColumn, serverSyncId)) { if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "setting version of deleted record " + serverSyncId + " to " - + serverSyncVersion); + Log.v(TAG, "remote record " + serverSyncId + " is in the deleted table"); } - ContentValues values = new ContentValues(); - values.put(_SYNC_VERSION, serverSyncVersion); - mDb.update(mDeletedTable, values, "_sync_id=?", new String[]{serverSyncId}); + final String deletedSyncVersion = deletedCursor.getString(deletedSyncVersionColumn); + if (!TextUtils.equals(deletedSyncVersion, serverSyncVersion)) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "setting version of deleted record " + serverSyncId + " to " + + serverSyncVersion); + } + ContentValues values = new ContentValues(); + values.put(_SYNC_VERSION, serverSyncVersion); + mDb.update(mDeletedTable, values, "_sync_id=?", new String[]{serverSyncId}); + } + continue; } - continue; - } - // If the _sync_local_id is present in the diffsCursor - // then this record corresponds to a local record that was just - // inserted into the server and the _sync_local_id is the row id - // of the local record. Set these fields so that the next check - // treats this record as an update, which will allow the - // merger to update the record with the server's sync id - if (!diffsCursor.isNull(serverSyncLocalIdColumn)) { - localRowId = diffsCursor.getLong(serverSyncLocalIdColumn); - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "the remote record with sync id " + serverSyncId - + " has a local sync id, " + localRowId); + // If the _sync_local_id is present in the diffsCursor + // then this record corresponds to a local record that was just + // inserted into the server and the _sync_local_id is the row id + // of the local record. Set these fields so that the next check + // treats this record as an update, which will allow the + // merger to update the record with the server's sync id + if (!diffsCursor.isNull(serverSyncLocalIdColumn)) { + localRowId = diffsCursor.getLong(serverSyncLocalIdColumn); + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "the remote record with sync id " + serverSyncId + + " has a local sync id, " + localRowId); + } + localSyncID = serverSyncId; + localSyncDirty = false; + localSyncVersion = null; } - localSyncID = serverSyncId; - localSyncDirty = false; - localSyncVersion = null; - } - if (!TextUtils.isEmpty(localSyncID)) { - // An existing server item has changed - // If serverSyncVersion is null, there is no edit URL; - // server won't let this change be written. - // Just hold onto it, I guess, in case the server permissions - // change later. - if (serverSyncVersion != null) { - boolean recordChanged = (localSyncVersion == null) || - !serverSyncVersion.equals(localSyncVersion); - if (recordChanged) { - if (localSyncDirty) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "remote record " + serverSyncId - + " conflicts with local _sync_id " + localSyncID - + ", local _id " + localRowId); - } - conflict = true; - } else { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, - "remote record " + - serverSyncId + - " updates local _sync_id " + - localSyncID + ", local _id " + - localRowId); + if (!TextUtils.isEmpty(localSyncID)) { + // An existing server item has changed + // If serverSyncVersion is null, there is no edit URL; + // server won't let this change be written. + // Just hold onto it, I guess, in case the server permissions + // change later. + if (serverSyncVersion != null) { + boolean recordChanged = (localSyncVersion == null) || + !serverSyncVersion.equals(localSyncVersion); + if (recordChanged) { + if (localSyncDirty) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "remote record " + serverSyncId + + " conflicts with local _sync_id " + localSyncID + + ", local _id " + localRowId); + } + conflict = true; + } else { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, + "remote record " + + serverSyncId + + " updates local _sync_id " + + localSyncID + ", local _id " + + localRowId); + } + update = true; } - update = true; } } + } else { + // the local db doesn't know about this record so add it + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "remote record " + serverSyncId + " is new, inserting"); + } + insert = true; } - } else { - // the local db doesn't know about this record so add it - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "remote record " + serverSyncId + " is new, inserting"); + + if (update) { + updateRow(localRowId, serverDiffs, diffsCursor); + syncResult.stats.numUpdates++; + } else if (conflict) { + resolveRow(localRowId, serverSyncId, serverDiffs, diffsCursor); + syncResult.stats.numUpdates++; + } else if (insert) { + insertRow(serverDiffs, diffsCursor); + syncResult.stats.numInserts++; } - insert = true; } - if (update) { - updateRow(localRowId, serverDiffs, diffsCursor); - syncResult.stats.numUpdates++; - } else if (conflict) { - resolveRow(localRowId, serverSyncId, serverDiffs, diffsCursor); - syncResult.stats.numUpdates++; - } else if (insert) { - insertRow(serverDiffs, diffsCursor); - syncResult.stats.numInserts++; + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "processed " + diffsCount + " server entries"); } - } - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "processed " + diffsCount + " server entries"); - } - - // If tombstones aren't in use delete any remaining local rows that - // don't have corresponding server rows. Keep the rows that don't - // have a sync id since those were created locally and haven't been - // synced to the server yet. - if (!diffsArePartial) { - while (!localCursor.isAfterLast() && !TextUtils.isEmpty(localCursor.getString(2))) { - if (mIsMergeCancelled) { - localCursor.deactivate(); - deletedCursor.deactivate(); - diffsCursor.deactivate(); - return; - } - localCount++; - final String localSyncId = localCursor.getString(2); - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, - "deleting local record " + - localCursor.getLong(1) + - " _sync_id " + localSyncId); - } - deleteRow(localCursor); - if (mDeletedTable != null) { - mDb.delete(mDeletedTable, _SYNC_ID + "=?", new String[] {localSyncId}); + // If tombstones aren't in use delete any remaining local rows that + // don't have corresponding server rows. Keep the rows that don't + // have a sync id since those were created locally and haven't been + // synced to the server yet. + if (!diffsArePartial) { + while (!localCursor.isAfterLast() && !TextUtils.isEmpty(localCursor.getString(2))) { + if (mIsMergeCancelled) { + return; + } + localCount++; + final String localSyncId = localCursor.getString(2); + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, + "deleting local record " + + localCursor.getLong(1) + + " _sync_id " + localSyncId); + } + deleteRow(localCursor); + if (mDeletedTable != null) { + mDb.delete(mDeletedTable, _SYNC_ID + "=?", new String[] {localSyncId}); + } + syncResult.stats.numDeletes++; + mDb.yieldIfContended(); } - syncResult.stats.numDeletes++; - mDb.yieldIfContended(); } + if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "checked " + localCount + + " local entries"); + } finally { + if (diffsCursor != null) diffsCursor.close(); + if (localCursor != null) localCursor.close(); + if (deletedCursor != null) deletedCursor.close(); } - if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "checked " + localCount + - " local entries"); - diffsCursor.deactivate(); - localCursor.deactivate(); - deletedCursor.deactivate(); if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "applying deletions from the server"); // Apply deletions from the server if (mDeletedTableURL != null) { diffsCursor = serverDiffs.query(mDeletedTableURL, null, null, null, null); - - while (diffsCursor.moveToNext()) { - if (mIsMergeCancelled) { - diffsCursor.deactivate(); - return; + try { + while (diffsCursor.moveToNext()) { + if (mIsMergeCancelled) { + return; + } + // delete all rows that match each element in the diffsCursor + fullyDeleteMatchingRows(diffsCursor, account, syncResult); + mDb.yieldIfContended(); } - // delete all rows that match each element in the diffsCursor - fullyDeleteMatchingRows(diffsCursor, account, syncResult); - mDb.yieldIfContended(); + } finally { + diffsCursor.close(); } - diffsCursor.deactivate(); } } - private void fullyDeleteMatchingRows(Cursor diffsCursor, String account, + private void fullyDeleteMatchingRows(Cursor diffsCursor, Account account, SyncResult syncResult) { int serverSyncIdColumn = diffsCursor.getColumnIndexOrThrow(_SYNC_ID); final boolean deleteBySyncId = !diffsCursor.isNull(serverSyncIdColumn); // delete the rows explicitly so that the delete operation can be overridden - final Cursor c; final String[] selectionArgs; - if (deleteBySyncId) { - selectionArgs = new String[]{diffsCursor.getString(serverSyncIdColumn), account}; - c = mDb.query(mTable, new String[]{BaseColumns._ID}, SELECT_BY_SYNC_ID_AND_ACCOUNT, - selectionArgs, null, null, null); - } else { - int serverSyncLocalIdColumn = diffsCursor.getColumnIndexOrThrow(_SYNC_LOCAL_ID); - selectionArgs = new String[]{diffsCursor.getString(serverSyncLocalIdColumn)}; - c = mDb.query(mTable, new String[]{BaseColumns._ID}, SELECT_BY_ID, selectionArgs, - null, null, null); - } + Cursor c = null; try { + if (deleteBySyncId) { + selectionArgs = new String[]{diffsCursor.getString(serverSyncIdColumn), + account.mName, account.mType}; + c = mDb.query(mTable, new String[]{BaseColumns._ID}, SELECT_BY_SYNC_ID_AND_ACCOUNT, + selectionArgs, null, null, null); + } else { + int serverSyncLocalIdColumn = diffsCursor.getColumnIndexOrThrow(_SYNC_LOCAL_ID); + selectionArgs = new String[]{diffsCursor.getString(serverSyncLocalIdColumn)}; + c = mDb.query(mTable, new String[]{BaseColumns._ID}, SELECT_BY_ID, selectionArgs, + null, null, null); + } c.moveToFirst(); while (!c.isAfterLast()) { deleteRow(c); // advances the cursor syncResult.stats.numDeletes++; } } finally { - c.deactivate(); + if (c != null) c.close(); } if (deleteBySyncId && mDeletedTable != null) { mDb.delete(mDeletedTable, SELECT_BY_SYNC_ID_AND_ACCOUNT, selectionArgs); @@ -516,43 +517,46 @@ public abstract class AbstractTableMerger * Finds local changes, placing the results in the given result object. * @param temporaryInstanceFactory As an optimization for the case * where there are no client-side diffs, mergeResult may initially - * have no {@link android.content.TempProviderSyncResult#tempContentProvider}. If this is + * have no {@link TempProviderSyncResult#tempContentProvider}. If this is * the first in the sequence of AbstractTableMergers to find * client-side diffs, it will use the given ContentProvider to * create a temporary instance and store its {@link - * ContentProvider} in the mergeResult. + * android.content.ContentProvider} in the mergeResult. * @param account * @param syncResult */ private void findLocalChanges(TempProviderSyncResult mergeResult, - SyncableContentProvider temporaryInstanceFactory, String account, + SyncableContentProvider temporaryInstanceFactory, Account account, SyncResult syncResult) { SyncableContentProvider clientDiffs = mergeResult.tempContentProvider; if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "generating client updates"); - final String[] accountSelectionArgs = new String[]{account}; + final String[] accountSelectionArgs = new String[]{account.mName, account.mType}; // Generate the client updates and insertions // Create a cursor for dirty records + long numInsertsOrUpdates = 0; Cursor localChangesCursor = mDb.query(mTable, null, SELECT_UNSYNCED, accountSelectionArgs, null, null, null); - long numInsertsOrUpdates = localChangesCursor.getCount(); - while (localChangesCursor.moveToNext()) { - if (mIsMergeCancelled) { - localChangesCursor.close(); - return; - } - if (clientDiffs == null) { - clientDiffs = temporaryInstanceFactory.getTemporaryInstance(); + try { + numInsertsOrUpdates = localChangesCursor.getCount(); + while (localChangesCursor.moveToNext()) { + if (mIsMergeCancelled) { + return; + } + if (clientDiffs == null) { + clientDiffs = temporaryInstanceFactory.getTemporaryInstance(); + } + mValues.clear(); + cursorRowToContentValues(localChangesCursor, mValues); + mValues.remove("_id"); + DatabaseUtils.cursorLongToContentValues(localChangesCursor, "_id", mValues, + _SYNC_LOCAL_ID); + clientDiffs.insert(mTableURL, mValues); } - mValues.clear(); - cursorRowToContentValues(localChangesCursor, mValues); - mValues.remove("_id"); - DatabaseUtils.cursorLongToContentValues(localChangesCursor, "_id", mValues, - _SYNC_LOCAL_ID); - clientDiffs.insert(mTableURL, mValues); + } finally { + localChangesCursor.close(); } - localChangesCursor.close(); // Generate the client deletions if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "generating client deletions"); @@ -561,23 +565,25 @@ public abstract class AbstractTableMerger if (mDeletedTable != null) { Cursor deletedCursor = mDb.query(mDeletedTable, syncIdAndVersionProjection, - _SYNC_ACCOUNT + "=? AND " + _SYNC_ID + " IS NOT NULL", accountSelectionArgs, + _SYNC_ACCOUNT + "=? AND " + _SYNC_ACCOUNT_TYPE + "=? AND " + + _SYNC_ID + " IS NOT NULL", accountSelectionArgs, null, null, mDeletedTable + "." + _SYNC_ID); - - numDeletedEntries = deletedCursor.getCount(); - while (deletedCursor.moveToNext()) { - if (mIsMergeCancelled) { - deletedCursor.close(); - return; - } - if (clientDiffs == null) { - clientDiffs = temporaryInstanceFactory.getTemporaryInstance(); + try { + numDeletedEntries = deletedCursor.getCount(); + while (deletedCursor.moveToNext()) { + if (mIsMergeCancelled) { + return; + } + if (clientDiffs == null) { + clientDiffs = temporaryInstanceFactory.getTemporaryInstance(); + } + mValues.clear(); + DatabaseUtils.cursorRowToContentValues(deletedCursor, mValues); + clientDiffs.insert(mDeletedTableURL, mValues); } - mValues.clear(); - DatabaseUtils.cursorRowToContentValues(deletedCursor, mValues); - clientDiffs.insert(mDeletedTableURL, mValues); + } finally { + deletedCursor.close(); } - deletedCursor.close(); } if (clientDiffs != null) { diff --git a/core/java/android/content/ActiveSyncInfo.java b/core/java/android/content/ActiveSyncInfo.java index 63be8d1..209dffa 100644 --- a/core/java/android/content/ActiveSyncInfo.java +++ b/core/java/android/content/ActiveSyncInfo.java @@ -16,17 +16,18 @@ package android.content; +import android.accounts.Account; import android.os.Parcel; import android.os.Parcelable.Creator; /** @hide */ public class ActiveSyncInfo { public final int authorityId; - public final String account; + public final Account account; public final String authority; public final long startTime; - ActiveSyncInfo(int authorityId, String account, String authority, + ActiveSyncInfo(int authorityId, Account account, String authority, long startTime) { this.authorityId = authorityId; this.account = account; @@ -40,14 +41,14 @@ public class ActiveSyncInfo { public void writeToParcel(Parcel parcel, int flags) { parcel.writeInt(authorityId); - parcel.writeString(account); + account.writeToParcel(parcel, 0); parcel.writeString(authority); parcel.writeLong(startTime); } ActiveSyncInfo(Parcel parcel) { authorityId = parcel.readInt(); - account = parcel.readString(); + account = new Account(parcel); authority = parcel.readString(); startTime = parcel.readLong(); } diff --git a/core/java/android/content/ContentProvider.java b/core/java/android/content/ContentProvider.java index 5cc5730..4e631c4 100644 --- a/core/java/android/content/ContentProvider.java +++ b/core/java/android/content/ContentProvider.java @@ -32,6 +32,7 @@ import android.os.ParcelFileDescriptor; import java.io.File; import java.io.FileNotFoundException; +import java.util.ArrayList; /** * Content providers are one of the primary building blocks of Android applications, providing @@ -130,6 +131,12 @@ public abstract class ContentProvider implements ComponentCallbacks { selectionArgs, sortOrder); } + public EntityIterator queryEntities(Uri uri, String selection, String[] selectionArgs, + String sortOrder) { + checkReadPermission(uri); + return ContentProvider.this.queryEntities(uri, selection, selectionArgs, sortOrder); + } + public String getType(Uri uri) { return ContentProvider.this.getType(uri); } @@ -145,6 +152,25 @@ public abstract class ContentProvider implements ComponentCallbacks { return ContentProvider.this.bulkInsert(uri, initialValues); } + public Uri insertEntity(Uri uri, Entity entities) { + checkWritePermission(uri); + return ContentProvider.this.insertEntity(uri, entities); + } + + public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) + throws OperationApplicationException { + for (ContentProviderOperation operation : operations) { + if (operation.isReadOperation()) { + checkReadPermission(operation.getUri()); + } + + if (operation.isWriteOperation()) { + checkWritePermission(operation.getUri()); + } + } + return ContentProvider.this.applyBatch(operations); + } + public int delete(Uri uri, String selection, String[] selectionArgs) { checkWritePermission(uri); return ContentProvider.this.delete(uri, selection, selectionArgs); @@ -156,6 +182,11 @@ public abstract class ContentProvider implements ComponentCallbacks { return ContentProvider.this.update(uri, values, selection, selectionArgs); } + public int updateEntity(Uri uri, Entity entity) { + checkWritePermission(uri); + return ContentProvider.this.updateEntity(uri, entity); + } + public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { if (mode != null && mode.startsWith("rw")) checkWritePermission(uri); @@ -170,12 +201,6 @@ public abstract class ContentProvider implements ComponentCallbacks { return ContentProvider.this.openAssetFile(uri, mode); } - public ISyncAdapter getSyncAdapter() { - checkWritePermission(null); - SyncAdapter sa = ContentProvider.this.getSyncAdapter(); - return sa != null ? sa.getISyncAdapter() : null; - } - private void checkReadPermission(Uri uri) { final String rperm = getReadPermission(); final int pid = Binder.getCallingPid(); @@ -334,6 +359,11 @@ public abstract class ContentProvider implements ComponentCallbacks { public abstract Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder); + public EntityIterator queryEntities(Uri uri, String selection, String[] selectionArgs, + String sortOrder) { + throw new UnsupportedOperationException(); + } + /** * Return the MIME type of the data at the given URI. This should start with * <code>vnd.android.cursor.item</code> for a single record, @@ -384,6 +414,10 @@ public abstract class ContentProvider implements ComponentCallbacks { return numValues; } + public Uri insertEntity(Uri uri, Entity entity) { + throw new UnsupportedOperationException(); + } + /** * A request to delete one or more rows. The selection clause is applied when performing * the deletion, allowing the operation to affect multiple rows in a @@ -428,6 +462,10 @@ public abstract class ContentProvider implements ComponentCallbacks { public abstract int update(Uri uri, ContentValues values, String selection, String[] selectionArgs); + public int updateEntity(Uri uri, Entity entity) { + throw new UnsupportedOperationException(); + } + /** * Open a file blob associated with a content URI. * This method can be called from multiple @@ -551,23 +589,6 @@ public abstract class ContentProvider implements ComponentCallbacks { } /** - * Get the sync adapter that is to be used by this content provider. - * This is intended for use by the sync system. If null then this - * content provider is considered not syncable. - * This method can be called from multiple - * threads, as described in - * <a href="{@docRoot}guide/topics/fundamentals.html#procthread">Application Fundamentals: - * Processes and Threads</a>. - * - * @return the SyncAdapter that is to be used by this ContentProvider, or null - * if this ContentProvider is not syncable - * @hide - */ - public SyncAdapter getSyncAdapter() { - return null; - } - - /** * Returns true if this instance is a temporary content provider. * @return true if this instance is a temporary content provider */ @@ -607,4 +628,27 @@ public abstract class ContentProvider implements ComponentCallbacks { ContentProvider.this.onCreate(); } } -} + + /** + * Applies each of the {@link ContentProviderOperation} objects and returns an array + * of their results. Passes through OperationApplicationException, which may be thrown + * by the call to {@link ContentProviderOperation#apply}. + * If all the applications succeed then a {@link ContentProviderResult} array with the + * same number of elements as the operations will be returned. It is implementation-specific + * how many, if any, operations will have been successfully applied if a call to + * apply results in a {@link OperationApplicationException}. + * @param operations the operations to apply + * @return the results of the applications + * @throws OperationApplicationException thrown if an application fails. + * See {@link ContentProviderOperation#apply} for more information. + */ + public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) + throws OperationApplicationException { + final int numOperations = operations.size(); + final ContentProviderResult[] results = new ContentProviderResult[numOperations]; + for (int i = 0; i < numOperations; i++) { + results[i] = operations.get(i).apply(this, results, i); + } + return results; + } +}
\ No newline at end of file diff --git a/core/java/android/content/ContentProviderClient.java b/core/java/android/content/ContentProviderClient.java new file mode 100644 index 0000000..452653e --- /dev/null +++ b/core/java/android/content/ContentProviderClient.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content; + +import android.database.Cursor; +import android.net.Uri; +import android.os.RemoteException; +import android.os.ParcelFileDescriptor; +import android.content.res.AssetFileDescriptor; + +import java.io.FileNotFoundException; +import java.util.ArrayList; + +/** + * The public interface object used to interact with a {@link ContentProvider}. This is obtained by + * calling {@link ContentResolver#acquireContentProviderClient}. This object must be released + * using {@link #release} in order to indicate to the system that the {@link ContentProvider} is + * no longer needed and can be killed to free up resources. + */ +public class ContentProviderClient { + private final IContentProvider mContentProvider; + private final ContentResolver mContentResolver; + + /** + * @hide + */ + ContentProviderClient(ContentResolver contentResolver, IContentProvider contentProvider) { + mContentProvider = contentProvider; + mContentResolver = contentResolver; + } + + /** see {@link ContentProvider#query} */ + public Cursor query(Uri url, String[] projection, String selection, + String[] selectionArgs, String sortOrder) throws RemoteException { + return mContentProvider.query(url, projection, selection, selectionArgs, sortOrder); + } + + /** see {@link ContentProvider#getType} */ + public String getType(Uri url) throws RemoteException { + return mContentProvider.getType(url); + } + + /** see {@link ContentProvider#insert} */ + public Uri insert(Uri url, ContentValues initialValues) + throws RemoteException { + return mContentProvider.insert(url, initialValues); + } + + /** see {@link ContentProvider#bulkInsert} */ + public int bulkInsert(Uri url, ContentValues[] initialValues) throws RemoteException { + return mContentProvider.bulkInsert(url, initialValues); + } + + /** see {@link ContentProvider#delete} */ + public int delete(Uri url, String selection, String[] selectionArgs) + throws RemoteException { + return mContentProvider.delete(url, selection, selectionArgs); + } + + /** see {@link ContentProvider#update} */ + public int update(Uri url, ContentValues values, String selection, + String[] selectionArgs) throws RemoteException { + return mContentProvider.update(url, values, selection, selectionArgs); + } + + /** see {@link ContentProvider#openFile} */ + public ParcelFileDescriptor openFile(Uri url, String mode) + throws RemoteException, FileNotFoundException { + return mContentProvider.openFile(url, mode); + } + + /** see {@link ContentProvider#openAssetFile} */ + public AssetFileDescriptor openAssetFile(Uri url, String mode) + throws RemoteException, FileNotFoundException { + return mContentProvider.openAssetFile(url, mode); + } + + /** see {@link ContentProvider#queryEntities} */ + public EntityIterator queryEntities(Uri uri, String selection, String[] selectionArgs, + String sortOrder) throws RemoteException { + return mContentProvider.queryEntities(uri, selection, selectionArgs, sortOrder); + } + + /** see {@link ContentProvider#insertEntity} */ + public Uri insertEntity(Uri uri, Entity entity) throws RemoteException { + return mContentProvider.insertEntity(uri, entity); + } + + /** see {@link ContentProvider#updateEntity} */ + public int updateEntity(Uri uri, Entity entity) throws RemoteException { + return mContentProvider.updateEntity(uri, entity); + } + + /** see {@link ContentProvider#applyBatch} */ + public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) + throws RemoteException, OperationApplicationException { + return mContentProvider.applyBatch(operations); + } + + /** + * Call this to indicate to the system that the associated {@link ContentProvider} is no + * longer needed by this {@link ContentProviderClient}. + * @return true if this was release, false if it was already released + */ + public boolean release() { + return mContentResolver.releaseProvider(mContentProvider); + } + + /** + * Get a reference to the {@link ContentProvider} that is associated with this + * client. If the {@link ContentProvider} is running in a different process then + * null will be returned. This can be used if you know you are running in the same + * process as a provider, and want to get direct access to its implementation details. + * + * @return If the associated {@link ContentProvider} is local, returns it. + * Otherwise returns null. + */ + public ContentProvider getLocalContentProvider() { + return ContentProvider.coerceToLocalContentProvider(mContentProvider); + } +} diff --git a/core/java/android/content/ContentProviderNative.java b/core/java/android/content/ContentProviderNative.java index e5e3f74..a4c217b 100644 --- a/core/java/android/content/ContentProviderNative.java +++ b/core/java/android/content/ContentProviderNative.java @@ -33,6 +33,7 @@ import android.os.ParcelFileDescriptor; import android.os.Parcelable; import java.io.FileNotFoundException; +import java.util.ArrayList; /** * {@hide} @@ -105,6 +106,20 @@ abstract public class ContentProviderNative extends Binder implements IContentPr return true; } + case QUERY_ENTITIES_TRANSACTION: + { + data.enforceInterface(IContentProvider.descriptor); + Uri url = Uri.CREATOR.createFromParcel(data); + String selection = data.readString(); + String[] selectionArgs = data.readStringArray(); + String sortOrder = data.readString(); + EntityIterator entityIterator = queryEntities(url, selection, selectionArgs, + sortOrder); + reply.writeNoException(); + reply.writeStrongBinder(new IEntityIteratorImpl(entityIterator).asBinder()); + return true; + } + case GET_TYPE_TRANSACTION: { data.enforceInterface(IContentProvider.descriptor); @@ -140,6 +155,43 @@ abstract public class ContentProviderNative extends Binder implements IContentPr return true; } + case INSERT_ENTITIES_TRANSACTION: + { + data.enforceInterface(IContentProvider.descriptor); + Uri uri = Uri.CREATOR.createFromParcel(data); + Entity entity = (Entity) data.readParcelable(null); + Uri newUri = insertEntity(uri, entity); + reply.writeNoException(); + Uri.writeToParcel(reply, newUri); + return true; + } + + case UPDATE_ENTITIES_TRANSACTION: + { + data.enforceInterface(IContentProvider.descriptor); + Uri uri = Uri.CREATOR.createFromParcel(data); + Entity entity = (Entity) data.readParcelable(null); + int count = updateEntity(uri, entity); + reply.writeNoException(); + reply.writeInt(count); + return true; + } + + case APPLY_BATCH_TRANSACTION: + { + data.enforceInterface(IContentProvider.descriptor); + final int numOperations = data.readInt(); + final ArrayList<ContentProviderOperation> operations = + new ArrayList<ContentProviderOperation>(numOperations); + for (int i = 0; i < numOperations; i++) { + operations.add(i, ContentProviderOperation.CREATOR.createFromParcel(data)); + } + final ContentProviderResult[] results = applyBatch(operations); + reply.writeNoException(); + reply.writeTypedArray(results, 0); + return true; + } + case DELETE_TRANSACTION: { data.enforceInterface(IContentProvider.descriptor); @@ -206,15 +258,6 @@ abstract public class ContentProviderNative extends Binder implements IContentPr } return true; } - - case GET_SYNC_ADAPTER_TRANSACTION: - { - data.enforceInterface(IContentProvider.descriptor); - ISyncAdapter sa = getSyncAdapter(); - reply.writeNoException(); - reply.writeStrongBinder(sa != null ? sa.asBinder() : null); - return true; - } } } catch (Exception e) { DatabaseUtils.writeExceptionToParcel(reply, e); @@ -224,6 +267,25 @@ abstract public class ContentProviderNative extends Binder implements IContentPr return super.onTransact(code, data, reply, flags); } + private class IEntityIteratorImpl extends IEntityIterator.Stub { + private final EntityIterator mEntityIterator; + + IEntityIteratorImpl(EntityIterator iterator) { + mEntityIterator = iterator; + } + public boolean hasNext() throws RemoteException { + return mEntityIterator.hasNext(); + } + + public Entity next() throws RemoteException { + return mEntityIterator.next(); + } + + public void close() throws RemoteException { + mEntityIterator.close(); + } + } + public IBinder asBinder() { return this; @@ -297,7 +359,7 @@ final class ContentProviderProxy implements IContentProvider BulkCursorToCursorAdaptor adaptor = new BulkCursorToCursorAdaptor(); IBulkCursor bulkCursor = bulkQuery(url, projection, selection, selectionArgs, sortOrder, adaptor.getObserver(), window); - + if (bulkCursor == null) { return null; } @@ -305,6 +367,54 @@ final class ContentProviderProxy implements IContentProvider return adaptor; } + public EntityIterator queryEntities(Uri url, String selection, String[] selectionArgs, + String sortOrder) + throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + + data.writeInterfaceToken(IContentProvider.descriptor); + + url.writeToParcel(data, 0); + data.writeString(selection); + data.writeStringArray(selectionArgs); + data.writeString(sortOrder); + + mRemote.transact(IContentProvider.QUERY_ENTITIES_TRANSACTION, data, reply, 0); + + DatabaseUtils.readExceptionFromParcel(reply); + + IBinder entityIteratorBinder = reply.readStrongBinder(); + + data.recycle(); + reply.recycle(); + + return new RemoteEntityIterator(IEntityIterator.Stub.asInterface(entityIteratorBinder)); + } + + static class RemoteEntityIterator implements EntityIterator { + private final IEntityIterator mEntityIterator; + RemoteEntityIterator(IEntityIterator entityIterator) { + mEntityIterator = entityIterator; + } + + public boolean hasNext() throws RemoteException { + return mEntityIterator.hasNext(); + } + + public Entity next() throws RemoteException { + return mEntityIterator.next(); + } + + public void close() { + try { + mEntityIterator.close(); + } catch (RemoteException e) { + // doesn't matter + } + } + } + public String getType(Uri url) throws RemoteException { Parcel data = Parcel.obtain(); @@ -366,6 +476,66 @@ final class ContentProviderProxy implements IContentProvider return count; } + public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) + throws RemoteException, OperationApplicationException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + + data.writeInterfaceToken(IContentProvider.descriptor); + data.writeInt(operations.size()); + for (ContentProviderOperation operation : operations) { + operation.writeToParcel(data, 0); + } + mRemote.transact(IContentProvider.APPLY_BATCH_TRANSACTION, data, reply, 0); + + DatabaseUtils.readExceptionWithOperationApplicationExceptionFromParcel(reply); + final ContentProviderResult[] results = + reply.createTypedArray(ContentProviderResult.CREATOR); + + data.recycle(); + reply.recycle(); + + return results; + } + + public Uri insertEntity(Uri uri, Entity entity) throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + + try { + data.writeInterfaceToken(IContentProvider.descriptor); + uri.writeToParcel(data, 0); + data.writeParcelable(entity, 0); + + mRemote.transact(IContentProvider.INSERT_ENTITIES_TRANSACTION, data, reply, 0); + + DatabaseUtils.readExceptionFromParcel(reply); + return Uri.CREATOR.createFromParcel(reply); + } finally { + data.recycle(); + reply.recycle(); + } + } + + public int updateEntity(Uri uri, Entity entity) throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + + try { + data.writeInterfaceToken(IContentProvider.descriptor); + uri.writeToParcel(data, 0); + data.writeParcelable(entity, 0); + + mRemote.transact(IContentProvider.UPDATE_ENTITIES_TRANSACTION, data, reply, 0); + + DatabaseUtils.readExceptionFromParcel(reply); + return reply.readInt(); + } finally { + data.recycle(); + reply.recycle(); + } + } + public int delete(Uri url, String selection, String[] selectionArgs) throws RemoteException { Parcel data = Parcel.obtain(); @@ -456,23 +626,6 @@ final class ContentProviderProxy implements IContentProvider return fd; } - public ISyncAdapter getSyncAdapter() throws RemoteException { - Parcel data = Parcel.obtain(); - Parcel reply = Parcel.obtain(); - - data.writeInterfaceToken(IContentProvider.descriptor); - - mRemote.transact(IContentProvider.GET_SYNC_ADAPTER_TRANSACTION, data, reply, 0); - - DatabaseUtils.readExceptionFromParcel(reply); - ISyncAdapter syncAdapter = ISyncAdapter.Stub.asInterface(reply.readStrongBinder()); - - data.recycle(); - reply.recycle(); - - return syncAdapter; - } - private IBinder mRemote; } diff --git a/core/java/android/content/ContentProviderOperation.java b/core/java/android/content/ContentProviderOperation.java new file mode 100644 index 0000000..001af16 --- /dev/null +++ b/core/java/android/content/ContentProviderOperation.java @@ -0,0 +1,523 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content; + +import android.net.Uri; +import android.database.Cursor; +import android.os.Parcelable; +import android.os.Parcel; +import android.os.Debug; + +import java.util.Map; +import java.util.HashMap; + +public class ContentProviderOperation implements Parcelable { + private final static int TYPE_INSERT = 1; + private final static int TYPE_UPDATE = 2; + private final static int TYPE_DELETE = 3; + private final static int TYPE_COUNT = 4; + + private final int mType; + private final Uri mUri; + private final String mSelection; + private final String[] mSelectionArgs; + private final ContentValues mValues; + private final Integer mExpectedCount; + private final ContentValues mValuesBackReferences; + private final Map<Integer, Integer> mSelectionArgsBackReferences; + + private static final String[] COUNT_COLUMNS = new String[]{"count(*)"}; + + /** + * Creates a {@link ContentProviderOperation} by copying the contents of a + * {@link Builder}. + */ + private ContentProviderOperation(Builder builder) { + mType = builder.mType; + mUri = builder.mUri; + mValues = builder.mValues; + mSelection = builder.mSelection; + mSelectionArgs = builder.mSelectionArgs; + mExpectedCount = builder.mExpectedCount; + mSelectionArgsBackReferences = builder.mSelectionArgsBackReferences; + mValuesBackReferences = builder.mValuesBackReferences; + } + + private ContentProviderOperation(Parcel source) { + mType = source.readInt(); + mUri = Uri.CREATOR.createFromParcel(source); + mValues = source.readInt() != 0 ? ContentValues.CREATOR.createFromParcel(source) : null; + mSelection = source.readInt() != 0 ? source.readString() : null; + mSelectionArgs = source.readInt() != 0 ? source.readStringArray() : null; + mExpectedCount = source.readInt() != 0 ? source.readInt() : null; + mValuesBackReferences = source.readInt() != 0 + + ? ContentValues.CREATOR.createFromParcel(source) + : null; + mSelectionArgsBackReferences = source.readInt() != 0 + ? new HashMap<Integer, Integer>() + : null; + if (mSelectionArgsBackReferences != null) { + final int count = source.readInt(); + for (int i = 0; i < count; i++) { + mSelectionArgsBackReferences.put(source.readInt(), source.readInt()); + } + } + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mType); + Uri.writeToParcel(dest, mUri); + if (mValues != null) { + dest.writeInt(1); + mValues.writeToParcel(dest, 0); + } else { + dest.writeInt(0); + } + if (mSelection != null) { + dest.writeInt(1); + dest.writeString(mSelection); + } else { + dest.writeInt(0); + } + if (mSelectionArgs != null) { + dest.writeInt(1); + dest.writeStringArray(mSelectionArgs); + } else { + dest.writeInt(0); + } + if (mExpectedCount != null) { + dest.writeInt(1); + dest.writeInt(mExpectedCount); + } else { + dest.writeInt(0); + } + if (mValuesBackReferences != null) { + dest.writeInt(1); + mValuesBackReferences.writeToParcel(dest, 0); + } else { + dest.writeInt(0); + } + if (mSelectionArgsBackReferences != null) { + dest.writeInt(1); + dest.writeInt(mSelectionArgsBackReferences.size()); + for (Map.Entry<Integer, Integer> entry : mSelectionArgsBackReferences.entrySet()) { + dest.writeInt(entry.getKey()); + dest.writeInt(entry.getValue()); + } + } else { + dest.writeInt(0); + } + } + + /** + * Create a {@link Builder} suitable for building an insert {@link ContentProviderOperation}. + * @param uri The {@link Uri} that is the target of the insert. + * @return a {@link Builder} + */ + public static Builder newInsert(Uri uri) { + return new Builder(TYPE_INSERT, uri); + } + + /** + * Create a {@link Builder} suitable for building an update {@link ContentProviderOperation}. + * @param uri The {@link Uri} that is the target of the update. + * @return a {@link Builder} + */ + public static Builder newUpdate(Uri uri) { + return new Builder(TYPE_UPDATE, uri); + } + + /** + * Create a {@link Builder} suitable for building a delete {@link ContentProviderOperation}. + * @param uri The {@link Uri} that is the target of the delete. + * @return a {@link Builder} + */ + public static Builder newDelete(Uri uri) { + return new Builder(TYPE_DELETE, uri); + } + + /** + * Create a {@link Builder} suitable for building a count query. When used in conjunction + * with {@link Builder#withExpectedCount(int)} this is useful for checking that the + * uri/selection has the expected number of rows. + * {@link ContentProviderOperation}. + * @param uri The {@link Uri} to query. + * @return a {@link Builder} + */ + public static Builder newCountQuery(Uri uri) { + return new Builder(TYPE_COUNT, uri); + } + + public Uri getUri() { + return mUri; + } + + public boolean isWriteOperation() { + return mType == TYPE_DELETE || mType == TYPE_INSERT || mType == TYPE_UPDATE; + } + + public boolean isReadOperation() { + return mType == TYPE_COUNT; + } + + /** + * Applies this operation using the given provider. The backRefs array is used to resolve any + * back references that were requested using + * {@link Builder#withValueBackReferences(ContentValues)} and + * {@link Builder#withSelectionBackReference}. + * @param provider the {@link ContentProvider} on which this batch is applied + * @param backRefs a {@link ContentProviderResult} array that will be consulted + * to resolve any requested back references. + * @param numBackRefs the number of valid results on the backRefs array. + * @return a {@link ContentProviderResult} that contains either the {@link Uri} of the inserted + * row if this was an insert otherwise the number of rows affected. + * @throws OperationApplicationException thrown if either the insert fails or + * if the number of rows affected didn't match the expected count + */ + public ContentProviderResult apply(ContentProvider provider, ContentProviderResult[] backRefs, + int numBackRefs) throws OperationApplicationException { + ContentValues values = resolveValueBackReferences(backRefs, numBackRefs); + String[] selectionArgs = + resolveSelectionArgsBackReferences(backRefs, numBackRefs); + + if (mType == TYPE_INSERT) { + Uri newUri = provider.insert(mUri, values); + if (newUri == null) { + throw new OperationApplicationException("insert failed"); + } + return new ContentProviderResult(newUri); + } + + int numRows; + if (mType == TYPE_DELETE) { + numRows = provider.delete(mUri, mSelection, selectionArgs); + } else if (mType == TYPE_UPDATE) { + numRows = provider.update(mUri, values, mSelection, selectionArgs); + } else if (mType == TYPE_COUNT) { + Cursor cursor = provider.query(mUri, COUNT_COLUMNS, mSelection, selectionArgs, null); + try { + if (!cursor.moveToNext()) { + throw new RuntimeException("since we are doing a count query we should always " + + "be able to move to the first row"); + } + if (cursor.getCount() != 1) { + throw new RuntimeException("since we are doing a count query there should " + + "always be exacly row, found " + cursor.getCount()); + } + numRows = cursor.getInt(0); + } finally { + cursor.close(); + } + } else { + throw new IllegalStateException("bad type, " + mType); + } + + if (mExpectedCount != null && mExpectedCount != numRows) { + throw new OperationApplicationException("wrong number of rows: " + numRows); + } + + return new ContentProviderResult(numRows); + } + + /** + * The ContentValues back references are represented as a ContentValues object where the + * key refers to a column and the value is an index of the back reference whose + * valued should be associated with the column. + * @param backRefs an array of previous results + * @param numBackRefs the number of valid previous results in backRefs + * @return the ContentValues that should be used in this operation application after + * expansion of back references. This can be called if either mValues or mValuesBackReferences + * is null + * @VisibleForTesting this is intended to be a private method but it is exposed for + * unit testing purposes + */ + public ContentValues resolveValueBackReferences( + ContentProviderResult[] backRefs, int numBackRefs) { + if (mValuesBackReferences == null) { + return mValues; + } + final ContentValues values; + if (mValues == null) { + values = new ContentValues(); + } else { + values = new ContentValues(mValues); + } + for (Map.Entry<String, Object> entry : mValuesBackReferences.valueSet()) { + String key = entry.getKey(); + Integer backRefIndex = mValuesBackReferences.getAsInteger(key); + if (backRefIndex == null) { + throw new IllegalArgumentException("values backref " + key + " is not an integer"); + } + values.put(key, backRefToValue(backRefs, numBackRefs, backRefIndex)); + } + return values; + } + + /** + * The Selection Arguments back references are represented as a Map of Integer->Integer where + * the key is an index into the selection argument array (see {@link Builder#withSelection}) + * and the value is the index of the previous result that should be used for that selection + * argument array slot. + * @param backRefs an array of previous results + * @param numBackRefs the number of valid previous results in backRefs + * @return the ContentValues that should be used in this operation application after + * expansion of back references. This can be called if either mValues or mValuesBackReferences + * is null + * @VisibleForTesting this is intended to be a private method but it is exposed for + * unit testing purposes + */ + public String[] resolveSelectionArgsBackReferences( + ContentProviderResult[] backRefs, int numBackRefs) { + if (mSelectionArgsBackReferences == null) { + return mSelectionArgs; + } + String[] newArgs = new String[mSelectionArgs.length]; + System.arraycopy(mSelectionArgs, 0, newArgs, 0, mSelectionArgs.length); + for (Map.Entry<Integer, Integer> selectionArgBackRef + : mSelectionArgsBackReferences.entrySet()) { + final Integer selectionArgIndex = selectionArgBackRef.getKey(); + final int backRefIndex = selectionArgBackRef.getValue(); + newArgs[selectionArgIndex] = backRefToValue(backRefs, numBackRefs, backRefIndex); + } + return newArgs; + } + + /** + * Return the string representation of the requested back reference. + * @param backRefs an array of results + * @param numBackRefs the number of items in the backRefs array that are valid + * @param backRefIndex which backRef to be used + * @throws ArrayIndexOutOfBoundsException thrown if the backRefIndex is larger than + * the numBackRefs + * @return the string representation of the requested back reference. + */ + private static String backRefToValue(ContentProviderResult[] backRefs, int numBackRefs, + Integer backRefIndex) { + if (backRefIndex >= numBackRefs) { + throw new ArrayIndexOutOfBoundsException("asked for back ref " + backRefIndex + + " but there are only " + numBackRefs + " back refs"); + } + ContentProviderResult backRef = backRefs[backRefIndex]; + String backRefValue; + if (backRef.uri != null) { + backRefValue = backRef.uri.getLastPathSegment(); + } else { + backRefValue = String.valueOf(backRef.count); + } + return backRefValue; + } + + public int describeContents() { + return 0; + } + + public static final Creator<ContentProviderOperation> CREATOR = + new Creator<ContentProviderOperation>() { + public ContentProviderOperation createFromParcel(Parcel source) { + return new ContentProviderOperation(source); + } + + public ContentProviderOperation[] newArray(int size) { + return new ContentProviderOperation[size]; + } + }; + + + /** + * Used to add parameters to a {@link ContentProviderOperation}. The {@link Builder} is + * first created by calling {@link ContentProviderOperation#newInsert(android.net.Uri)}, + * {@link ContentProviderOperation#newUpdate(android.net.Uri)}, + * {@link ContentProviderOperation#newDelete(android.net.Uri)} or + * {@link ContentProviderOperation#newCountQuery(android.net.Uri)}. The withXXX methods + * can then be used to add parameters to the builder. See the specific methods to find for + * which {@link Builder} type each is allowed. Call {@link #build} to create the + * {@link ContentProviderOperation} once all the parameters have been supplied. + */ + public static class Builder { + private final int mType; + private final Uri mUri; + private String mSelection; + private String[] mSelectionArgs; + private ContentValues mValues; + private Integer mExpectedCount; + private ContentValues mValuesBackReferences; + private Map<Integer, Integer> mSelectionArgsBackReferences; + + /** Create a {@link Builder} of a given type. The uri must not be null. */ + private Builder(int type, Uri uri) { + if (uri == null) { + throw new IllegalArgumentException("uri must not be null"); + } + mType = type; + mUri = uri; + } + + /** Create a ContentProviderOperation from this {@link Builder}. */ + public ContentProviderOperation build() { + return new ContentProviderOperation(this); + } + + /** + * Add a {@link ContentValues} of back references. The key is the name of the column + * and the value is an integer that is the index of the previous result whose + * value should be used for the column. The value is added as a {@link String}. + * A column value from the back references takes precedence over a value specified in + * {@link #withValues}. + * This can only be used with builders of type insert or update. + * @return this builder, to allow for chaining. + */ + public Builder withValueBackReferences(ContentValues backReferences) { + if (mType != TYPE_INSERT && mType != TYPE_UPDATE) { + throw new IllegalArgumentException( + "only inserts and updates can have value back-references"); + } + mValuesBackReferences = backReferences; + return this; + } + + /** + * Add a ContentValues back reference. + * A column value from the back references takes precedence over a value specified in + * {@link #withValues}. + * This can only be used with builders of type insert or update. + * @return this builder, to allow for chaining. + */ + public Builder withValueBackReference(String key, int previousResult) { + if (mType != TYPE_INSERT && mType != TYPE_UPDATE) { + throw new IllegalArgumentException( + "only inserts and updates can have value back-references"); + } + if (mValuesBackReferences == null) { + mValuesBackReferences = new ContentValues(); + } + mValuesBackReferences.put(key, previousResult); + return this; + } + + /** + * Add a back references as a selection arg. Any value at that index of the selection arg + * that was specified by {@link #withSelection} will be overwritten. + * This can only be used with builders of type update, delete, or count query. + * @return this builder, to allow for chaining. + */ + public Builder withSelectionBackReference(int selectionArgIndex, int previousResult) { + if (mType != TYPE_COUNT && mType != TYPE_UPDATE && mType != TYPE_DELETE) { + throw new IllegalArgumentException( + "only deletes, updates and counts can have selection back-references"); + } + if (mSelectionArgsBackReferences == null) { + mSelectionArgsBackReferences = new HashMap<Integer, Integer>(); + } + mSelectionArgsBackReferences.put(selectionArgIndex, previousResult); + return this; + } + + /** + * The ContentValues to use. This may be null. These values may be overwritten by + * the corresponding value specified by {@link #withValueBackReference} or by + * future calls to {@link #withValues} or {@link #withValue}. + * This can only be used with builders of type insert or update. + * @return this builder, to allow for chaining. + */ + public Builder withValues(ContentValues values) { + if (mType != TYPE_INSERT && mType != TYPE_UPDATE) { + throw new IllegalArgumentException("only inserts and updates can have values"); + } + if (mValues == null) { + mValues = new ContentValues(); + } + mValues.putAll(values); + return this; + } + + /** + * A value to insert or update. This value may be overwritten by + * the corresponding value specified by {@link #withValueBackReference}. + * This can only be used with builders of type insert or update. + * @param key the name of this value + * @param value the value itself. the type must be acceptable for insertion by + * {@link ContentValues#put} + * @return this builder, to allow for chaining. + */ + public Builder withValue(String key, Object value) { + if (mType != TYPE_INSERT && mType != TYPE_UPDATE) { + throw new IllegalArgumentException("only inserts and updates can have values"); + } + if (mValues == null) { + mValues = new ContentValues(); + } + if (value == null) { + mValues.putNull(key); + } else if (value instanceof String) { + mValues.put(key, (String) value); + } else if (value instanceof Byte) { + mValues.put(key, (Byte) value); + } else if (value instanceof Short) { + mValues.put(key, (Short) value); + } else if (value instanceof Integer) { + mValues.put(key, (Integer) value); + } else if (value instanceof Long) { + mValues.put(key, (Long) value); + } else if (value instanceof Float) { + mValues.put(key, (Float) value); + } else if (value instanceof Double) { + mValues.put(key, (Double) value); + } else if (value instanceof Boolean) { + mValues.put(key, (Boolean) value); + } else if (value instanceof byte[]) { + mValues.put(key, (byte[]) value); + } else { + throw new IllegalArgumentException("bad value type: " + value.getClass().getName()); + } + return this; + } + + /** + * The selection and arguments to use. An occurrence of '?' in the selection will be + * replaced with the corresponding occurence of the selection argument. Any of the + * selection arguments may be overwritten by a selection argument back reference as + * specified by {@link #withSelectionBackReference}. + * This can only be used with builders of type update, delete, or count query. + * @return this builder, to allow for chaining. + */ + public Builder withSelection(String selection, String[] selectionArgs) { + if (mType != TYPE_DELETE && mType != TYPE_UPDATE && mType != TYPE_COUNT) { + throw new IllegalArgumentException( + "only deletes, updates and counts can have selections"); + } + mSelection = selection; + mSelectionArgs = selectionArgs; + return this; + } + + /** + * If set then if the number of rows affected by this operation do not match + * this count {@link OperationApplicationException} will be throw. + * This can only be used with builders of type update, delete, or count query. + * @return this builder, to allow for chaining. + */ + public Builder withExpectedCount(int count) { + if (mType != TYPE_DELETE && mType != TYPE_UPDATE && mType != TYPE_COUNT) { + throw new IllegalArgumentException( + "only deletes, updates and counts can have expected counts"); + } + mExpectedCount = count; + return this; + } + } +} diff --git a/core/java/android/content/ContentProviderResult.java b/core/java/android/content/ContentProviderResult.java new file mode 100644 index 0000000..5d188ef --- /dev/null +++ b/core/java/android/content/ContentProviderResult.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content; + +import android.net.Uri; +import android.os.Parcelable; +import android.os.Parcel; + +/** + * Contains the result of the application of a {@link ContentProviderOperation}. It is guaranteed + * to have exactly one of {@link #uri} or {@link #count} set. + */ +public class ContentProviderResult implements Parcelable { + public final Uri uri; + public final Integer count; + + public ContentProviderResult(Uri uri) { + if (uri == null) throw new IllegalArgumentException("uri must not be null"); + this.uri = uri; + this.count = null; + } + + public ContentProviderResult(int count) { + this.count = count; + this.uri = null; + } + + public ContentProviderResult(Parcel source) { + int type = source.readInt(); + if (type == 1) { + count = source.readInt(); + uri = null; + } else { + count = null; + uri = Uri.CREATOR.createFromParcel(source); + } + } + + public void writeToParcel(Parcel dest, int flags) { + if (uri == null) { + dest.writeInt(1); + dest.writeInt(count); + } else { + dest.writeInt(2); + uri.writeToParcel(dest, 0); + } + } + + public int describeContents() { + return 0; + } + + public static final Creator<ContentProviderResult> CREATOR = + new Creator<ContentProviderResult>() { + public ContentProviderResult createFromParcel(Parcel source) { + return new ContentProviderResult(source); + } + + public ContentProviderResult[] newArray(int size) { + return new ContentProviderResult[size]; + } + }; + + public String toString() { + if (uri != null) { + return "ContentProviderResult(uri=" + uri.toString() + ")"; + } + return "ContentProviderResult(count=" + count + ")"; + } +}
\ No newline at end of file diff --git a/core/java/android/content/ContentResolver.java b/core/java/android/content/ContentResolver.java index 74144fc..a01c5d1 100644 --- a/core/java/android/content/ContentResolver.java +++ b/core/java/android/content/ContentResolver.java @@ -30,6 +30,7 @@ import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.os.ServiceManager; import android.text.TextUtils; +import android.accounts.Account; import android.util.Config; import android.util.Log; @@ -40,6 +41,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.List; +import java.util.ArrayList; /** @@ -166,6 +168,87 @@ public abstract class ContentResolver { } /** + * EntityIterator wrapper that releases the associated ContentProviderClient when the + * iterator is closed. + */ + private class EntityIteratorWrapper implements EntityIterator { + private final EntityIterator mInner; + private final ContentProviderClient mClient; + private volatile boolean mClientReleased; + + EntityIteratorWrapper(EntityIterator inner, ContentProviderClient client) { + mInner = inner; + mClient = client; + mClientReleased = false; + } + + public boolean hasNext() throws RemoteException { + if (mClientReleased) { + throw new IllegalStateException("this iterator is already closed"); + } + return mInner.hasNext(); + } + + public Entity next() throws RemoteException { + if (mClientReleased) { + throw new IllegalStateException("this iterator is already closed"); + } + return mInner.next(); + } + + public void close() { + mClient.release(); + mInner.close(); + mClientReleased = true; + } + + protected void finalize() throws Throwable { + if (!mClientReleased) { + mClient.release(); + } + super.finalize(); + } + } + + /** + * Query the given URI, returning an {@link EntityIterator} over the result set. + * + * @param uri The URI, using the content:// scheme, for the content to + * retrieve. + * @param selection A filter declaring which rows to return, formatted as an + * SQL WHERE clause (excluding the WHERE itself). Passing null will + * return all rows for the given URI. + * @param selectionArgs You may include ?s in selection, which will be + * replaced by the values from selectionArgs, in the order that they + * appear in the selection. The values will be bound as Strings. + * @param sortOrder How to order the rows, formatted as an SQL ORDER BY + * clause (excluding the ORDER BY itself). Passing null will use the + * default sort order, which may be unordered. + * @return An EntityIterator object + * @throws RemoteException thrown if a RemoteException is encountered while attempting + * to communicate with a remote provider. + * @throws IllegalArgumentException thrown if there is no provider that matches the uri + */ + public final EntityIterator queryEntities(Uri uri, + String selection, String[] selectionArgs, String sortOrder) throws RemoteException { + ContentProviderClient provider = acquireContentProviderClient(uri); + if (provider == null) { + throw new IllegalArgumentException("Unknown URL " + uri); + } + try { + EntityIterator entityIterator = + provider.queryEntities(uri, selection, selectionArgs, sortOrder); + return new EntityIteratorWrapper(entityIterator, provider); + } catch(RuntimeException e) { + provider.release(); + throw e; + } catch(RemoteException e) { + provider.release(); + throw e; + } + } + + /** * Open a stream on to the content associated with a content URI. If there * is no data associated with the URI, FileNotFoundException is thrown. * @@ -485,6 +568,36 @@ public abstract class ContentResolver { } /** + * Applies each of the {@link ContentProviderOperation} objects and returns an array + * of their results. Passes through OperationApplicationException, which may be thrown + * by the call to {@link ContentProviderOperation#apply}. + * If all the applications succeed then a {@link ContentProviderResult} array with the + * same number of elements as the operations will be returned. It is implementation-specific + * how many, if any, operations will have been successfully applied if a call to + * apply results in a {@link OperationApplicationException}. + * @param authority the authority of the ContentProvider to which this batch should be applied + * @param operations the operations to apply + * @return the results of the applications + * @throws OperationApplicationException thrown if an application fails. + * See {@link ContentProviderOperation#apply} for more information. + * @throws RemoteException thrown if a RemoteException is encountered while attempting + * to communicate with a remote provider. + */ + public ContentProviderResult[] applyBatch(String authority, + ArrayList<ContentProviderOperation> operations) + throws RemoteException, OperationApplicationException { + ContentProviderClient provider = acquireContentProviderClient(authority); + if (provider == null) { + throw new IllegalArgumentException("Unknown authority " + authority); + } + try { + return provider.applyBatch(operations); + } finally { + provider.release(); + } + } + + /** * Inserts multiple rows into a table at the given URL. * * This function make no guarantees about the atomicity of the insertions. @@ -592,6 +705,46 @@ public abstract class ContentResolver { } /** + * Returns a {@link ContentProviderClient} that is associated with the {@link ContentProvider} + * that services the content at uri, starting the provider if necessary. Returns + * null if there is no provider associated wih the uri. The caller must indicate that they are + * done with the provider by calling {@link ContentProviderClient#release} which will allow + * the system to release the provider it it determines that there is no other reason for + * keeping it active. + * @param uri specifies which provider should be acquired + * @return a {@link ContentProviderClient} that is associated with the {@link ContentProvider} + * that services the content at uri or null if there isn't one. + */ + public final ContentProviderClient acquireContentProviderClient(Uri uri) { + IContentProvider provider = acquireProvider(uri); + if (provider != null) { + return new ContentProviderClient(this, provider); + } + + return null; + } + + /** + * Returns a {@link ContentProviderClient} that is associated with the {@link ContentProvider} + * with the authority of name, starting the provider if necessary. Returns + * null if there is no provider associated wih the uri. The caller must indicate that they are + * done with the provider by calling {@link ContentProviderClient#release} which will allow + * the system to release the provider it it determines that there is no other reason for + * keeping it active. + * @param name specifies which provider should be acquired + * @return a {@link ContentProviderClient} that is associated with the {@link ContentProvider} + * with the authority of name or null if there isn't one. + */ + public final ContentProviderClient acquireContentProviderClient(String name) { + IContentProvider provider = acquireProvider(name); + if (provider != null) { + return new ContentProviderClient(this, provider); + } + + return null; + } + + /** * Register an observer class that gets callbacks when data identified by a * given content URI changes. * @@ -694,6 +847,7 @@ public abstract class ContentResolver { * <li>Float</li> * <li>Double</li> * <li>String</li> + * <li>Account</li> * <li>null</li> * </ul> * @param extras the Bundle to check @@ -709,6 +863,7 @@ public abstract class ContentResolver { if (value instanceof Float) continue; if (value instanceof Double) continue; if (value instanceof String) continue; + if (value instanceof Account) continue; throw new IllegalArgumentException("unexpected value type: " + value.getClass().getName()); } diff --git a/core/java/android/content/ContentService.java b/core/java/android/content/ContentService.java index 6cd2c54..c768ffa 100644 --- a/core/java/android/content/ContentService.java +++ b/core/java/android/content/ContentService.java @@ -16,6 +16,7 @@ package android.content; +import android.accounts.Account; import android.database.IContentObserver; import android.database.sqlite.SQLiteException; import android.net.Uri; @@ -280,7 +281,7 @@ public final class ContentService extends IContentService.Stub { } } - public boolean isSyncActive(String account, String authority) { + public boolean isSyncActive(Account account, String authority) { mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_STATS, "no permission to read the sync stats"); long identityToken = clearCallingIdentity(); @@ -327,7 +328,7 @@ public final class ContentService extends IContentService.Stub { return null; } - public boolean isAuthorityPending(String account, String authority) { + public boolean isAuthorityPending(Account account, String authority) { mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_STATS, "no permission to read the sync stats"); long identityToken = clearCallingIdentity(); diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index ec847a4..64d8c63 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -1098,6 +1098,16 @@ public abstract class Context { public static final String LAYOUT_INFLATER_SERVICE = "layout_inflater"; /** * Use with {@link #getSystemService} to retrieve a + * {@link android.accounts.AccountManager} for receiving intents at a + * time of your choosing. + * TODO STOPSHIP perform a final review of the the account apis before shipping + * + * @see #getSystemService + * @see android.accounts.AccountManager + */ + public static final String ACCOUNT_SERVICE = "account"; + /** + * Use with {@link #getSystemService} to retrieve a * {@link android.app.ActivityManager} for interacting with the global * system state. * diff --git a/core/java/android/content/Entity.aidl b/core/java/android/content/Entity.aidl new file mode 100644 index 0000000..fb201f3 --- /dev/null +++ b/core/java/android/content/Entity.aidl @@ -0,0 +1,20 @@ +/* //device/java/android/android/content/Entity.aidl +** +** Copyright 2007, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ + +package android.content; + +parcelable Entity; diff --git a/core/java/android/content/Entity.java b/core/java/android/content/Entity.java new file mode 100644 index 0000000..325dce5 --- /dev/null +++ b/core/java/android/content/Entity.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content; + +import android.os.Parcelable; +import android.os.Parcel; +import android.net.Uri; +import android.util.Log; + +import java.util.ArrayList; + +/** + * Objects that pass through the ContentProvider and ContentResolver's methods that deal with + * Entities must implement this abstract base class and thus themselves be Parcelable. + */ +public final class Entity implements Parcelable { + final private ContentValues mValues; + final private ArrayList<NamedContentValues> mSubValues; + + public Entity(ContentValues values) { + mValues = values; + mSubValues = new ArrayList<NamedContentValues>(); + } + + public ContentValues getEntityValues() { + return mValues; + } + + public ArrayList<NamedContentValues> getSubValues() { + return mSubValues; + } + + public void addSubValue(Uri uri, ContentValues values) { + mSubValues.add(new Entity.NamedContentValues(uri, values)); + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + mValues.writeToParcel(dest, 0); + dest.writeInt(mSubValues.size()); + for (NamedContentValues value : mSubValues) { + value.uri.writeToParcel(dest, 0); + value.values.writeToParcel(dest, 0); + } + } + + private Entity(Parcel source) { + mValues = ContentValues.CREATOR.createFromParcel(source); + final int numValues = source.readInt(); + mSubValues = new ArrayList<NamedContentValues>(numValues); + for (int i = 0; i < numValues; i++) { + final Uri uri = Uri.CREATOR.createFromParcel(source); + final ContentValues values = ContentValues.CREATOR.createFromParcel(source); + mSubValues.add(new NamedContentValues(uri, values)); + } + } + + public static final Creator<Entity> CREATOR = new Creator<Entity>() { + public Entity createFromParcel(Parcel source) { + return new Entity(source); + } + + public Entity[] newArray(int size) { + return new Entity[size]; + } + }; + + public static class NamedContentValues { + public final Uri uri; + public final ContentValues values; + + public NamedContentValues(Uri uri, ContentValues values) { + this.uri = uri; + this.values = values; + } + } + + public String toString() { + final StringBuilder sb = new StringBuilder(); + sb.append("Entity: ").append(getEntityValues()); + for (Entity.NamedContentValues namedValue : getSubValues()) { + sb.append("\n ").append(namedValue.uri); + sb.append("\n -> ").append(namedValue.values); + } + return sb.toString(); + } +} diff --git a/core/java/android/content/EntityIterator.java b/core/java/android/content/EntityIterator.java new file mode 100644 index 0000000..5e5f14c --- /dev/null +++ b/core/java/android/content/EntityIterator.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content; + +import android.os.RemoteException; + +public interface EntityIterator { + /** + * Returns whether there are more elements to iterate, i.e. whether the + * iterator is positioned in front of an element. + * + * @return {@code true} if there are more elements, {@code false} otherwise. + * @see #next + * @since Android 1.0 + */ + public boolean hasNext() throws RemoteException; + + /** + * Returns the next object in the iteration, i.e. returns the element in + * front of the iterator and advances the iterator by one position. + * + * @return the next object. + * @throws java.util.NoSuchElementException + * if there are no more elements. + * @see #hasNext + * @since Android 1.0 + */ + public Entity next() throws RemoteException; + + /** + * Indicates that this iterator is no longer needed and that any associated resources + * may be released (such as a SQLite cursor). + */ + public void close(); +} diff --git a/core/java/android/content/IContentProvider.java b/core/java/android/content/IContentProvider.java index 0606956..7e5aba5 100644 --- a/core/java/android/content/IContentProvider.java +++ b/core/java/android/content/IContentProvider.java @@ -28,6 +28,7 @@ import android.os.IInterface; import android.os.ParcelFileDescriptor; import java.io.FileNotFoundException; +import java.util.ArrayList; /** * The ipc interface to talk to a content provider. @@ -43,19 +44,25 @@ public interface IContentProvider extends IInterface { CursorWindow window) throws RemoteException; public Cursor query(Uri url, String[] projection, String selection, String[] selectionArgs, String sortOrder) throws RemoteException; + public EntityIterator queryEntities(Uri url, String selection, + String[] selectionArgs, String sortOrder) + throws RemoteException; public String getType(Uri url) throws RemoteException; public Uri insert(Uri url, ContentValues initialValues) throws RemoteException; public int bulkInsert(Uri url, ContentValues[] initialValues) throws RemoteException; + public Uri insertEntity(Uri uri, Entity entities) throws RemoteException; public int delete(Uri url, String selection, String[] selectionArgs) throws RemoteException; public int update(Uri url, ContentValues values, String selection, String[] selectionArgs) throws RemoteException; + public int updateEntity(Uri uri, Entity entity) throws RemoteException; public ParcelFileDescriptor openFile(Uri url, String mode) throws RemoteException, FileNotFoundException; public AssetFileDescriptor openAssetFile(Uri url, String mode) throws RemoteException, FileNotFoundException; - public ISyncAdapter getSyncAdapter() throws RemoteException; + public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) + throws RemoteException, OperationApplicationException; /* IPC constants */ static final String descriptor = "android.content.IContentProvider"; @@ -65,8 +72,11 @@ public interface IContentProvider extends IInterface { static final int INSERT_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 2; static final int DELETE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 3; static final int UPDATE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 9; - static final int GET_SYNC_ADAPTER_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 10; static final int BULK_INSERT_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 12; static final int OPEN_FILE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 13; static final int OPEN_ASSET_FILE_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 14; + static final int INSERT_ENTITIES_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 16; + static final int UPDATE_ENTITIES_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 17; + static final int QUERY_ENTITIES_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 18; + static final int APPLY_BATCH_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION + 19; } diff --git a/core/java/android/content/IContentService.aidl b/core/java/android/content/IContentService.aidl index 8617d949..4352227 100644 --- a/core/java/android/content/IContentService.aidl +++ b/core/java/android/content/IContentService.aidl @@ -16,6 +16,7 @@ package android.content; +import android.accounts.Account; import android.content.ActiveSyncInfo; import android.content.ISyncStatusObserver; import android.content.SyncStatusInfo; @@ -60,7 +61,7 @@ interface IContentService { * Returns true if there is currently a sync operation for the given * account or authority in the pending list, or actively being processed. */ - boolean isSyncActive(String account, String authority); + boolean isSyncActive(in Account account, String authority); ActiveSyncInfo getActiveSync(); @@ -75,7 +76,7 @@ interface IContentService { /** * Return true if the pending status is true of any matching authorities. */ - boolean isAuthorityPending(String account, String authority); + boolean isAuthorityPending(in Account account, String authority); void addStatusChangeListener(int mask, ISyncStatusObserver callback); diff --git a/core/java/android/content/IEntityIterator.java b/core/java/android/content/IEntityIterator.java new file mode 100644 index 0000000..1c478b3 --- /dev/null +++ b/core/java/android/content/IEntityIterator.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content; + +import android.os.Binder; +import android.os.IBinder; +import android.os.IInterface; +import android.os.Parcel; +import android.os.RemoteException; +import android.os.Parcelable; +import android.util.Log; + +/** + * ICPC interface methods for an iterator over Entity objects. + * @hide + */ +public interface IEntityIterator extends IInterface { + /** Local-side IPC implementation stub class. */ + public static abstract class Stub extends Binder implements IEntityIterator { + private static final String TAG = "IEntityIterator"; + private static final java.lang.String DESCRIPTOR = "android.content.IEntityIterator"; + + /** Construct the stub at attach it to the interface. */ + public Stub() { + this.attachInterface(this, DESCRIPTOR); + } + /** + * Cast an IBinder object into an IEntityIterator interface, + * generating a proxy if needed. + */ + public static IEntityIterator asInterface(IBinder obj) { + if ((obj==null)) { + return null; + } + IInterface iin = obj.queryLocalInterface(DESCRIPTOR); + if (((iin!=null)&&(iin instanceof IEntityIterator))) { + return ((IEntityIterator)iin); + } + return new IEntityIterator.Stub.Proxy(obj); + } + + public IBinder asBinder() { + return this; + } + + public boolean onTransact(int code, Parcel data, Parcel reply, int flags) + throws RemoteException { + switch (code) { + case INTERFACE_TRANSACTION: + { + reply.writeString(DESCRIPTOR); + return true; + } + + case TRANSACTION_hasNext: + { + data.enforceInterface(DESCRIPTOR); + boolean _result; + try { + _result = this.hasNext(); + } catch (Exception e) { + Log.e(TAG, "caught exception in hasNext()", e); + reply.writeException(e); + return true; + } + reply.writeNoException(); + reply.writeInt(((_result)?(1):(0))); + return true; + } + + case TRANSACTION_next: + { + data.enforceInterface(DESCRIPTOR); + Entity entity; + try { + entity = this.next(); + } catch (RemoteException e) { + Log.e(TAG, "caught exception in next()", e); + reply.writeException(e); + return true; + } + reply.writeNoException(); + entity.writeToParcel(reply, Parcelable.PARCELABLE_WRITE_RETURN_VALUE); + return true; + } + + case TRANSACTION_close: + { + data.enforceInterface(DESCRIPTOR); + try { + this.close(); + } catch (RemoteException e) { + Log.e(TAG, "caught exception in close()", e); + reply.writeException(e); + return true; + } + reply.writeNoException(); + return true; + } + } + return super.onTransact(code, data, reply, flags); + } + + private static class Proxy implements IEntityIterator { + private IBinder mRemote; + Proxy(IBinder remote) { + mRemote = remote; + } + public IBinder asBinder() { + return mRemote; + } + public java.lang.String getInterfaceDescriptor() { + return DESCRIPTOR; + } + public boolean hasNext() throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + boolean _result; + try { + _data.writeInterfaceToken(DESCRIPTOR); + mRemote.transact(Stub.TRANSACTION_hasNext, _data, _reply, 0); + _reply.readException(); + _result = (0!=_reply.readInt()); + } + finally { + _reply.recycle(); + _data.recycle(); + } + return _result; + } + + public Entity next() throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + mRemote.transact(Stub.TRANSACTION_next, _data, _reply, 0); + _reply.readException(); + return Entity.CREATOR.createFromParcel(_reply); + } finally { + _reply.recycle(); + _data.recycle(); + } + } + + public void close() throws RemoteException { + Parcel _data = Parcel.obtain(); + Parcel _reply = Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + mRemote.transact(Stub.TRANSACTION_close, _data, _reply, 0); + _reply.readException(); + } + finally { + _reply.recycle(); + _data.recycle(); + } + } + } + static final int TRANSACTION_hasNext = (IBinder.FIRST_CALL_TRANSACTION + 0); + static final int TRANSACTION_next = (IBinder.FIRST_CALL_TRANSACTION + 1); + static final int TRANSACTION_close = (IBinder.FIRST_CALL_TRANSACTION + 2); + } + public boolean hasNext() throws RemoteException; + public Entity next() throws RemoteException; + public void close() throws RemoteException; +} diff --git a/core/java/android/content/ISyncAdapter.aidl b/core/java/android/content/ISyncAdapter.aidl index 671188c..d228605 100644 --- a/core/java/android/content/ISyncAdapter.aidl +++ b/core/java/android/content/ISyncAdapter.aidl @@ -16,6 +16,7 @@ package android.content; +import android.accounts.Account; import android.os.Bundle; import android.content.ISyncContext; @@ -33,7 +34,7 @@ oneway interface ISyncAdapter { * @param account the account that should be synced * @param extras SyncAdapter-specific parameters */ - void startSync(ISyncContext syncContext, String account, in Bundle extras); + void startSync(ISyncContext syncContext, in Account account, in Bundle extras); /** * Cancel the most recently initiated sync. Due to race conditions, this may arrive diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index 24262f5..0e6be0a 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -1271,7 +1271,7 @@ public class Intent implements Parcelable { * that wait until power is available to trigger. */ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) - public static final String ACTION_POWER_CONNECTED = "android.intent.action.ACTION_POWER_CONNECTED"; + public static final String ACTION_POWER_CONNECTED = "android.intent.action.POWER_CONNECTED"; /** * Broadcast Action: External power has been removed from the device. * This is intended for applications that wish to register specifically to this notification. @@ -1280,7 +1280,8 @@ public class Intent implements Parcelable { * that wait until power is available to trigger. */ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) - public static final String ACTION_POWER_DISCONNECTED = "android.intent.action.ACTION_POWER_DISCONNECTED"; + public static final String ACTION_POWER_DISCONNECTED = + "android.intent.action.POWER_DISCONNECTED"; /** * Broadcast Action: Device is shutting down. * This is broadcast when the device is being shut down (completely turned @@ -1552,6 +1553,21 @@ public class Intent implements Parcelable { public static final String ACTION_REBOOT = "android.intent.action.REBOOT"; + /** + * Broadcast Action: a remote intent is to be broadcasted. + * + * A remote intent is used for remote RPC between devices. The remote intent + * is serialized and sent from one device to another device. The receiving + * device parses the remote intent and broadcasts it. Note that anyone can + * broadcast a remote intent. However, if the intent receiver of the remote intent + * does not trust intent broadcasts from arbitrary intent senders, it should require + * the sender to hold certain permissions so only trusted sender's broadcast will be + * let through. + */ + public static final String ACTION_REMOTE_INTENT = + "android.intent.action.REMOTE_INTENT"; + + // --------------------------------------------------------------------- // --------------------------------------------------------------------- // Standard intent categories (see addCategory()). @@ -1810,6 +1826,13 @@ public class Intent implements Parcelable { public static final String EXTRA_INSTALLER_PACKAGE_NAME = "android.intent.extra.INSTALLER_PACKAGE_NAME"; + /** + * Used in the extra field in the remote intent. It's astring token passed with the + * remote intent. + */ + public static final String EXTRA_REMOTE_INTENT_TOKEN = + "android.intent.extra.remote_intent_token"; + // --------------------------------------------------------------------- // --------------------------------------------------------------------- // Intent flags (see mFlags variable). diff --git a/core/java/android/content/IntentFilter.java b/core/java/android/content/IntentFilter.java index e5c5dc8..365f269 100644 --- a/core/java/android/content/IntentFilter.java +++ b/core/java/android/content/IntentFilter.java @@ -366,6 +366,7 @@ public class IntentFilter implements Parcelable { throws MalformedMimeTypeException { mPriority = 0; mActions = new ArrayList<String>(); + addAction(action); addDataType(dataType); } diff --git a/core/java/android/content/OperationApplicationException.java b/core/java/android/content/OperationApplicationException.java new file mode 100644 index 0000000..d4101bf --- /dev/null +++ b/core/java/android/content/OperationApplicationException.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content; + +/** + * Thrown when an application of a {@link ContentProviderOperation} fails due the specified + * constraints. + */ +public class OperationApplicationException extends Exception { + public OperationApplicationException() { + super(); + } + public OperationApplicationException(String message) { + super(message); + } + public OperationApplicationException(String message, Throwable cause) { + super(message, cause); + } + public OperationApplicationException(Throwable cause) { + super(cause); + } +} diff --git a/core/java/android/content/SyncAdapter.java b/core/java/android/content/SyncAdapter.java index 7826e50..c658fb7 100644 --- a/core/java/android/content/SyncAdapter.java +++ b/core/java/android/content/SyncAdapter.java @@ -18,6 +18,7 @@ package android.content; import android.os.Bundle; import android.os.RemoteException; +import android.accounts.Account; /** * @hide @@ -29,7 +30,7 @@ public abstract class SyncAdapter { public static final int LOG_SYNC_DETAILS = 2743; class Transport extends ISyncAdapter.Stub { - public void startSync(ISyncContext syncContext, String account, + public void startSync(ISyncContext syncContext, Account account, Bundle extras) throws RemoteException { SyncAdapter.this.startSync(new SyncContext(syncContext), account, extras); } @@ -42,9 +43,9 @@ public abstract class SyncAdapter { Transport mTransport = new Transport(); /** - * Get the Transport object. (note this is package private). + * Get the Transport object. */ - final ISyncAdapter getISyncAdapter() + public final ISyncAdapter getISyncAdapter() { return mTransport; } @@ -59,7 +60,7 @@ public abstract class SyncAdapter { * @param account the account that should be synced * @param extras SyncAdapter-specific parameters */ - public abstract void startSync(SyncContext syncContext, String account, Bundle extras); + public abstract void startSync(SyncContext syncContext, Account account, Bundle extras); /** * Cancel the most recently initiated sync. Due to race conditions, this may arrive diff --git a/core/java/android/content/SyncAdapterNew.java b/core/java/android/content/SyncAdapterNew.java new file mode 100644 index 0000000..5b23395 --- /dev/null +++ b/core/java/android/content/SyncAdapterNew.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content; + +import android.os.*; +import android.os.Process; +import android.accounts.Account; + +import java.util.concurrent.atomic.AtomicInteger; + +/** + * @hide + */ +public abstract class SyncAdapterNew { + private static final String TAG = "SyncAdapter"; + private final Context mContext; + private final String mAuthority; + + /** Kernel event log tag. Also listed in data/etc/event-log-tags. */ + public static final int LOG_SYNC_DETAILS = 2743; + + public SyncAdapterNew(Context context, String authority) { + mContext = context; + mAuthority = authority; + } + + class Transport extends ISyncAdapter.Stub { + private final AtomicInteger mNumSyncStarts = new AtomicInteger(0); + private volatile Thread mSyncThread; + + public void startSync(ISyncContext syncContext, Account account, Bundle extras) { + boolean alreadyInProgress; + synchronized (this) { + if (mSyncThread == null) { + mSyncThread = new Thread( + new SyncRunnable(new SyncContext(syncContext), account, extras), + "SyncAdapterThread-" + mNumSyncStarts.incrementAndGet()); + Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); + mSyncThread.start(); + alreadyInProgress = false; + } else { + alreadyInProgress = true; + } + } + + if (alreadyInProgress) { + try { + syncContext.onFinished(SyncResult.ALREADY_IN_PROGRESS); + } catch (RemoteException e) { + // don't care if the caller is no longer around + } + } + } + + public void cancelSync() { + synchronized (this) { + if (mSyncThread != null) { + mSyncThread.interrupt(); + } + } + } + + private class SyncRunnable implements Runnable { + private final SyncContext mSyncContext; + private final Account mAccount; + private final Bundle mExtras; + + private SyncRunnable(SyncContext syncContext, Account account, Bundle extras) { + mSyncContext = syncContext; + mAccount = account; + mExtras = extras; + } + + public void run() { + if (isCanceled()) { + return; + } + + SyncResult syncResult = new SyncResult(); + ContentProviderClient provider = mAuthority != null + ? mContext.getContentResolver().acquireContentProviderClient(mAuthority) + : null; + try { + SyncAdapterNew.this.performSync(mAccount, mExtras, provider, syncResult); + } finally { + if (provider != null) { + provider.release(); + } + if (!isCanceled()) { + mSyncContext.onFinished(syncResult); + } + mSyncThread = null; + } + } + + private boolean isCanceled() { + return Thread.currentThread().isInterrupted(); + } + } + } + + Transport mTransport = new Transport(); + + /** + * Get the Transport object. + */ + public final ISyncAdapter getISyncAdapter() { + return mTransport; + } + + /** + * Perform a sync for this account. SyncAdapter-specific parameters may + * be specified in extras, which is guaranteed to not be null. Invocations + * of this method are guaranteed to be serialized. + * + * @param account the account that should be synced + * @param extras SyncAdapter-specific parameters + */ + public abstract void performSync(Account account, Bundle extras, + ContentProviderClient provider, SyncResult syncResult); +}
\ No newline at end of file diff --git a/core/java/android/content/SyncAdapterType.java b/core/java/android/content/SyncAdapterType.java new file mode 100644 index 0000000..368a879 --- /dev/null +++ b/core/java/android/content/SyncAdapterType.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content; + +import android.text.TextUtils; + +/** + * Value type that represents a SyncAdapterType. This object overrides {@link #equals} and + * {@link #hashCode}, making it suitable for use as the key of a {@link java.util.Map} + */ +public class SyncAdapterType { + public final String authority; + public final String accountType; + + public SyncAdapterType(String authority, String accountType) { + if (TextUtils.isEmpty(authority)) { + throw new IllegalArgumentException("the authority must not be empty: " + authority); + } + if (TextUtils.isEmpty(accountType)) { + throw new IllegalArgumentException("the accountType must not be empty: " + accountType); + } + this.authority = authority; + this.accountType = accountType; + } + + public boolean equals(Object o) { + if (o == this) return true; + if (!(o instanceof SyncAdapterType)) return false; + final SyncAdapterType other = (SyncAdapterType)o; + return authority.equals(other.authority) && accountType.equals(other.accountType); + } + + public int hashCode() { + int result = 17; + result = 31 * result + authority.hashCode(); + result = 31 * result + accountType.hashCode(); + return result; + } + + public String toString() { + return "SyncAdapterType {name=" + authority + ", type=" + accountType + "}"; + } +}
\ No newline at end of file diff --git a/core/java/android/content/SyncAdaptersCache.java b/core/java/android/content/SyncAdaptersCache.java new file mode 100644 index 0000000..56e3e75 --- /dev/null +++ b/core/java/android/content/SyncAdaptersCache.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content; + +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.content.pm.RegisteredServicesCache; +import android.content.res.XmlResourceParser; +import android.content.res.TypedArray; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.util.Log; +import android.util.AttributeSet; +import android.util.Xml; + +import java.io.IOException; +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import com.google.android.collect.Maps; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlPullParser; + +/** + * A cache of services that export the {@link android.content.ISyncAdapter} interface. + * @hide + */ +/* package private */ class SyncAdaptersCache extends RegisteredServicesCache<SyncAdapterType> { + private static final String TAG = "Account"; + + private static final String SERVICE_INTERFACE = "android.content.SyncAdapter"; + private static final String SERVICE_META_DATA = "android.content.SyncAdapter"; + private static final String ATTRIBUTES_NAME = "sync-adapter"; + + SyncAdaptersCache(Context context) { + super(context, SERVICE_INTERFACE, SERVICE_META_DATA, ATTRIBUTES_NAME); + } + + public SyncAdapterType parseServiceAttributes(AttributeSet attrs) { + TypedArray sa = mContext.getResources().obtainAttributes(attrs, + com.android.internal.R.styleable.SyncAdapter); + try { + final String authority = + sa.getString(com.android.internal.R.styleable.SyncAdapter_contentAuthority); + final String accountType = + sa.getString(com.android.internal.R.styleable.SyncAdapter_accountType); + if (authority == null || accountType == null) { + return null; + } + return new SyncAdapterType(authority, accountType); + } finally { + sa.recycle(); + } + } +}
\ No newline at end of file diff --git a/core/java/android/content/SyncManager.java b/core/java/android/content/SyncManager.java index 4d2cce8..03cfbea 100644 --- a/core/java/android/content/SyncManager.java +++ b/core/java/android/content/SyncManager.java @@ -21,8 +21,9 @@ import com.google.android.collect.Maps; import com.android.internal.R; import com.android.internal.util.ArrayUtils; -import android.accounts.AccountMonitor; -import android.accounts.AccountMonitorListener; +import android.accounts.Account; +import android.accounts.AccountManager; +import android.accounts.OnAccountsUpdatedListener; import android.app.AlarmManager; import android.app.Notification; import android.app.NotificationManager; @@ -30,8 +31,10 @@ import android.app.PendingIntent; import android.content.pm.ApplicationInfo; import android.content.pm.IPackageManager; import android.content.pm.PackageManager; -import android.content.pm.ProviderInfo; import android.content.pm.ResolveInfo; +import android.content.pm.RegisteredServicesCache; +import android.database.Cursor; +import android.database.DatabaseUtils; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Uri; @@ -47,7 +50,9 @@ import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemClock; import android.os.SystemProperties; +import android.provider.Sync; import android.provider.Settings; +import android.provider.Sync.History; import android.text.TextUtils; import android.text.format.DateUtils; import android.text.format.Time; @@ -72,11 +77,14 @@ import java.util.List; import java.util.Map; import java.util.PriorityQueue; import java.util.Random; +import java.util.Observer; +import java.util.Observable; +import java.util.Set; /** * @hide */ -class SyncManager { +class SyncManager implements OnAccountsUpdatedListener { private static final String TAG = "SyncManager"; // used during dumping of the Sync history @@ -117,14 +125,11 @@ class SyncManager { private static final String HANDLE_SYNC_ALARM_WAKE_LOCK = "SyncManagerHandleSyncAlarmWakeLock"; private Context mContext; - private ContentResolver mContentResolver; private String mStatusText = ""; private long mHeartbeatTime = 0; - private AccountMonitor mAccountMonitor; - - private volatile String[] mAccounts = null; + private volatile Account[] mAccounts = null; volatile private PowerManager.WakeLock mSyncWakeLock; volatile private PowerManager.WakeLock mHandleAlarmWakeLock; @@ -151,10 +156,11 @@ class SyncManager { private final PendingIntent mSyncAlarmIntent; private final PendingIntent mSyncPollAlarmIntent; + private final SyncAdaptersCache mSyncAdapters; + private BroadcastReceiver mStorageIntentReceiver = new BroadcastReceiver() { public void onReceive(Context context, Intent intent) { - ensureContentResolver(); String action = intent.getAction(); if (Intent.ACTION_DEVICE_STORAGE_LOW.equals(action)) { if (Log.isLoggable(TAG, Log.VERBOSE)) { @@ -172,6 +178,43 @@ class SyncManager { } }; + private BroadcastReceiver mBootCompletedReceiver = new BroadcastReceiver() { + public void onReceive(Context context, Intent intent) { + if (!mFactoryTest) { + AccountManager.get(mContext).addOnAccountsUpdatedListener(SyncManager.this, + mSyncHandler, true /* updateImmediately */); + } + } + }; + + public void onAccountsUpdated(Account[] accounts) { + final boolean hadAccountsAlready = mAccounts != null; + mAccounts = accounts; + + // if a sync is in progress yet it is no longer in the accounts list, + // cancel it + ActiveSyncContext activeSyncContext = mActiveSyncContext; + if (activeSyncContext != null) { + if (!ArrayUtils.contains(accounts, activeSyncContext.mSyncOperation.account)) { + Log.d(TAG, "canceling sync since the account has been removed"); + sendSyncFinishedOrCanceledMessage(activeSyncContext, + null /* no result since this is a cancel */); + } + } + + // we must do this since we don't bother scheduling alarms when + // the accounts are not set yet + sendCheckAlarmsMessage(); + + mSyncStorageEngine.doDatabaseCleanup(accounts); + + if (hadAccountsAlready && accounts.length > 0) { + // request a sync so that if the password was changed we will + // retry any sync that failed when it was wrong + startSync(null /* all providers */, null /* no extras */); + } + } + private BroadcastReceiver mConnectivityIntentReceiver = new BroadcastReceiver() { public void onReceive(Context context, Intent intent) { @@ -229,7 +272,11 @@ class SyncManager { private static final String SYNCMANAGER_PREFS_FILENAME = "/data/system/syncmanager.prefs"; + private final boolean mFactoryTest; + public SyncManager(Context context, boolean factoryTest) { + mFactoryTest = factoryTest; + // Initialize the SyncStorageEngine first, before registering observers // and creating threads and so on; it may fail if the disk is full. SyncStorageEngine.init(context); @@ -244,6 +291,8 @@ class SyncManager { mPackageManager = null; + mSyncAdapters = new SyncAdaptersCache(mContext); + mSyncAlarmIntent = PendingIntent.getBroadcast( mContext, 0 /* ignored */, new Intent(ACTION_SYNC_ALARM), 0); @@ -253,6 +302,9 @@ class SyncManager { IntentFilter intentFilter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION); context.registerReceiver(mConnectivityIntentReceiver, intentFilter); + intentFilter = new IntentFilter(Intent.ACTION_BOOT_COMPLETED); + context.registerReceiver(mBootCompletedReceiver, intentFilter); + intentFilter = new IntentFilter(Intent.ACTION_DEVICE_STORAGE_LOW); intentFilter.addAction(Intent.ACTION_DEVICE_STORAGE_OK); context.registerReceiver(mStorageIntentReceiver, intentFilter); @@ -288,42 +340,6 @@ class SyncManager { sendCheckAlarmsMessage(); } }); - - if (!factoryTest) { - AccountMonitorListener listener = new AccountMonitorListener() { - public void onAccountsUpdated(String[] accounts) { - final boolean hadAccountsAlready = mAccounts != null; - // copy the accounts into a new array and change mAccounts to point to it - String[] newAccounts = new String[accounts.length]; - System.arraycopy(accounts, 0, newAccounts, 0, accounts.length); - mAccounts = newAccounts; - - // if a sync is in progress yet it is no longer in the accounts list, cancel it - ActiveSyncContext activeSyncContext = mActiveSyncContext; - if (activeSyncContext != null) { - if (!ArrayUtils.contains(newAccounts, - activeSyncContext.mSyncOperation.account)) { - Log.d(TAG, "canceling sync since the account has been removed"); - sendSyncFinishedOrCanceledMessage(activeSyncContext, - null /* no result since this is a cancel */); - } - } - - // we must do this since we don't bother scheduling alarms when - // the accounts are not set yet - sendCheckAlarmsMessage(); - - mSyncStorageEngine.doDatabaseCleanup(accounts); - - if (hadAccountsAlready && mAccounts.length > 0) { - // request a sync so that if the password was changed we will retry any sync - // that failed when it was wrong - startSync(null /* all providers */, null /* no extras */); - } - } - }; - mAccountMonitor = new AccountMonitor(context, listener); - } } private synchronized void initializeSyncPoll() { @@ -452,19 +468,13 @@ class SyncManager { return mSyncStorageEngine; } - private void ensureContentResolver() { - if (mContentResolver == null) { - mContentResolver = mContext.getContentResolver(); - } - } - private void ensureAlarmService() { if (mAlarmService == null) { mAlarmService = (AlarmManager)mContext.getSystemService(Context.ALARM_SERVICE); } } - public String getSyncingAccount() { + public Account getSyncingAccount() { ActiveSyncContext activeSyncContext = mActiveSyncContext; return (activeSyncContext != null) ? activeSyncContext.mSyncOperation.account : null; } @@ -535,10 +545,10 @@ class SyncManager { delay = -1; // this means schedule at the front of the queue } - String[] accounts; - String accountFromExtras = extras.getString(ContentResolver.SYNC_EXTRAS_ACCOUNT); - if (!TextUtils.isEmpty(accountFromExtras)) { - accounts = new String[]{accountFromExtras}; + Account[] accounts; + Account accountFromExtras = extras.getParcelable(ContentResolver.SYNC_EXTRAS_ACCOUNT); + if (accountFromExtras != null) { + accounts = new Account[]{accountFromExtras}; } else { // if the accounts aren't configured yet then we can't support an account-less // sync request @@ -575,20 +585,33 @@ class SyncManager { source = SyncStorageEngine.SOURCE_SERVER; } - List<String> names = new ArrayList<String>(); - List<ProviderInfo> providers = new ArrayList<ProviderInfo>(); - populateProvidersList(url, names, providers); + // compile a list of authorities that have sync adapters + // for each authority sync each account that matches a sync adapter + Set<String> syncableAuthorities = new HashSet<String>(); + for (RegisteredServicesCache.ServiceInfo<SyncAdapterType> syncAdapter : + mSyncAdapters.getAllServices()) { + syncableAuthorities.add(syncAdapter.type.authority); + } - final int numProviders = providers.size(); - for (int i = 0; i < numProviders; i++) { - if (!providers.get(i).isSyncable) continue; - final String name = names.get(i); - for (String account : accounts) { - scheduleSyncOperation(new SyncOperation(account, source, name, extras, delay)); - // TODO: remove this when Calendar supports multiple accounts. Until then - // pretend that only the first account exists when syncing calendar. - if ("calendar".equals(name)) { - break; + // if the url was specified then replace the list of authorities with just this authority + // or clear it if this authority isn't syncable + if (url != null) { + boolean isSyncable = syncableAuthorities.contains(url.getAuthority()); + syncableAuthorities.clear(); + if (isSyncable) syncableAuthorities.add(url.getAuthority()); + } + + for (String authority : syncableAuthorities) { + for (Account account : accounts) { + if (mSyncAdapters.getServiceInfo(new SyncAdapterType(authority, account.mType)) + != null) { + scheduleSyncOperation( + new SyncOperation(account, source, authority, extras, delay)); + // TODO: remove this when Calendar supports multiple accounts. Until then + // pretend that only the first account exists when syncing calendar. + if ("calendar".equals(authority)) { + break; + } } } } @@ -598,32 +621,6 @@ class SyncManager { mStatusText = message; } - private void populateProvidersList(Uri url, List<String> names, List<ProviderInfo> providers) { - try { - final IPackageManager packageManager = getPackageManager(); - if (url == null) { - packageManager.querySyncProviders(names, providers); - } else { - final String authority = url.getAuthority(); - ProviderInfo info = packageManager.resolveContentProvider(url.getAuthority(), 0); - if (info != null) { - // only set this provider if the requested authority is the primary authority - String[] providerNames = info.authority.split(";"); - if (url.getAuthority().equals(providerNames[0])) { - names.add(authority); - providers.add(info); - } - } - } - } catch (RemoteException ex) { - // we should really never get this, but if we do then clear the lists, which - // will result in the dropping of the sync request - Log.e(TAG, "error trying to get the ProviderInfo for " + url, ex); - names.clear(); - providers.clear(); - } - } - public void scheduleLocalSync(Uri url) { final Bundle extras = new Bundle(); extras.putBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, true); @@ -721,8 +718,7 @@ class SyncManager { } // Cap the delay - ensureContentResolver(); - long maxSyncRetryTimeInSeconds = Settings.Gservices.getLong(mContentResolver, + long maxSyncRetryTimeInSeconds = Settings.Gservices.getLong(mContext.getContentResolver(), Settings.Gservices.SYNC_MAX_RETRY_DELAY_IN_SECONDS, DEFAULT_MAX_SYNC_RETRY_TIME_IN_SECONDS); if (newDelayInMs > maxSyncRetryTimeInSeconds * 1000) { @@ -857,7 +853,7 @@ class SyncManager { * Value type that represents a sync operation. */ static class SyncOperation implements Comparable { - final String account; + final Account account; int syncSource; String authority; Bundle extras; @@ -866,7 +862,7 @@ class SyncManager { long delay; SyncStorageEngine.PendingOperation pendingOperation; - SyncOperation(String account, int source, String authority, Bundle extras, long delay) { + SyncOperation(Account account, int source, String authority, Bundle extras, long delay) { this.account = account; this.syncSource = source; this.authority = authority; @@ -937,21 +933,19 @@ class SyncManager { /** * @hide */ - class ActiveSyncContext extends ISyncContext.Stub { + class ActiveSyncContext extends ISyncContext.Stub implements ServiceConnection { final SyncOperation mSyncOperation; final long mHistoryRowId; - final IContentProvider mContentProvider; - final ISyncAdapter mSyncAdapter; + ISyncAdapter mSyncAdapter; final long mStartTime; long mTimeoutStartTime; - public ActiveSyncContext(SyncOperation syncOperation, IContentProvider contentProvider, - ISyncAdapter syncAdapter, long historyRowId) { + public ActiveSyncContext(SyncOperation syncOperation, + long historyRowId) { super(); mSyncOperation = syncOperation; mHistoryRowId = historyRowId; - mContentProvider = contentProvider; - mSyncAdapter = syncAdapter; + mSyncAdapter = null; mStartTime = SystemClock.elapsedRealtime(); mTimeoutStartTime = mStartTime; } @@ -977,6 +971,37 @@ class SyncManager { .append(", syncOperation ").append(mSyncOperation); } + public void onServiceConnected(ComponentName name, IBinder service) { + Message msg = mSyncHandler.obtainMessage(); + msg.what = SyncHandler.MESSAGE_SERVICE_CONNECTED; + msg.obj = new ServiceConnectionData(this, ISyncAdapter.Stub.asInterface(service)); + mSyncHandler.sendMessage(msg); + } + + public void onServiceDisconnected(ComponentName name) { + Message msg = mSyncHandler.obtainMessage(); + msg.what = SyncHandler.MESSAGE_SERVICE_DISCONNECTED; + msg.obj = new ServiceConnectionData(this, null); + mSyncHandler.sendMessage(msg); + } + + boolean bindToSyncAdapter(RegisteredServicesCache.ServiceInfo info) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.d(TAG, "bindToSyncAdapter: " + info.componentName + ", connection " + this); + } + Intent intent = new Intent(); + intent.setAction("android.content.SyncAdapter"); + intent.setComponent(info.componentName); + return mContext.bindService(intent, this, Context.BIND_AUTO_CREATE); + } + + void unBindFromSyncAdapter() { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.d(TAG, "unBindFromSyncAdapter: connection " + this); + } + mContext.unbindService(this); + } + @Override public String toString() { StringBuilder sb = new StringBuilder(); @@ -991,6 +1016,12 @@ class SyncManager { if (isSyncEnabled()) { dumpSyncHistory(pw, sb); } + + pw.println(); + pw.println("SyncAdapters:"); + for (RegisteredServicesCache.ServiceInfo info : mSyncAdapters.getAllServices()) { + pw.println(" " + info); + } } static String formatTime(long time) { @@ -1004,7 +1035,7 @@ class SyncManager { pw.print("data connected: "); pw.println(mDataConnectionIsConnected); pw.print("memory low: "); pw.println(mStorageIsLow); - final String[] accounts = mAccounts; + final Account[] accounts = mAccounts; pw.print("accounts: "); if (accounts != null) { pw.println(accounts.length); @@ -1068,7 +1099,8 @@ class SyncManager { for (int i=0; i<N; i++) { SyncStorageEngine.PendingOperation op = ops.get(i); pw.print(" #"); pw.print(i); pw.print(": account="); - pw.print(op.account); pw.print(" authority="); + pw.print(op.account.mName); pw.print(":"); + pw.print(op.account.mType); pw.print(" authority="); pw.println(op.authority); if (op.extras != null && op.extras.size() > 0) { sb.setLength(0); @@ -1078,7 +1110,7 @@ class SyncManager { } } - HashSet<String> processedAccounts = new HashSet<String>(); + HashSet<Account> processedAccounts = new HashSet<Account>(); ArrayList<SyncStatusInfo> statuses = mSyncStorageEngine.getSyncStatus(); if (statuses != null && statuses.size() > 0) { @@ -1090,7 +1122,7 @@ class SyncManager { SyncStorageEngine.AuthorityInfo authority = mSyncStorageEngine.getAuthority(status.authorityId); if (authority != null) { - String curAccount = authority.account; + Account curAccount = authority.account; if (processedAccounts.contains(curAccount)) { continue; @@ -1098,8 +1130,9 @@ class SyncManager { processedAccounts.add(curAccount); - pw.print(" Account "); pw.print(authority.account); - pw.println(":"); + pw.print(" Account "); pw.print(authority.account.mName); + pw.print(" "); pw.print(authority.account.mType); + pw.println(":"); for (int j=i; j<N; j++) { status = statuses.get(j); authority = mSyncStorageEngine.getAuthority(status.authorityId); @@ -1219,9 +1252,15 @@ class SyncManager { SyncStorageEngine.AuthorityInfo authority = mSyncStorageEngine.getAuthority(item.authorityId); pw.print(" #"); pw.print(i+1); pw.print(": "); - pw.print(authority != null ? authority.account : "<no account>"); - pw.print(" "); - pw.print(authority != null ? authority.authority : "<no account>"); + if (authority != null) { + pw.print(authority.account.mName); + pw.print(":"); + pw.print(authority.account.mType); + pw.print(" "); + pw.print(authority.authority); + } else { + pw.print("<no account>"); + } Time time = new Time(); time.set(item.eventTime); pw.print(" "); pw.print(SyncStorageEngine.SOURCES[item.source]); @@ -1278,6 +1317,15 @@ class SyncManager { } } + class ServiceConnectionData { + public final ActiveSyncContext activeSyncContext; + public final ISyncAdapter syncAdapter; + ServiceConnectionData(ActiveSyncContext activeSyncContext, ISyncAdapter syncAdapter) { + this.activeSyncContext = activeSyncContext; + this.syncAdapter = syncAdapter; + } + } + /** * Handles SyncOperation Messages that are posted to the associated * HandlerThread. @@ -1287,6 +1335,8 @@ class SyncManager { private static final int MESSAGE_SYNC_FINISHED = 1; private static final int MESSAGE_SYNC_ALARM = 2; private static final int MESSAGE_CHECK_ALARMS = 3; + private static final int MESSAGE_SERVICE_CONNECTED = 4; + private static final int MESSAGE_SERVICE_DISCONNECTED = 5; public final SyncNotificationInfo mSyncNotificationInfo = new SyncNotificationInfo(); private Long mAlarmScheduleTime = null; @@ -1301,7 +1351,7 @@ class SyncManager { */ class SyncNotificationInfo { // only valid if isActive is true - public String account; + public Account account; // only valid if isActive is true public String authority; @@ -1358,6 +1408,53 @@ class SyncManager { runStateIdle(); break; + case SyncHandler.MESSAGE_SERVICE_CONNECTED: { + ServiceConnectionData msgData = (ServiceConnectionData)msg.obj; + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.d(TAG, "handleSyncHandlerMessage: MESSAGE_SERVICE_CONNECTED: " + + msgData.activeSyncContext + + " active is " + mActiveSyncContext); + } + // check that this isn't an old message + if (mActiveSyncContext == msgData.activeSyncContext) { + runBoundToSyncAdapter(msgData.syncAdapter); + } + break; + } + + case SyncHandler.MESSAGE_SERVICE_DISCONNECTED: { + ServiceConnectionData msgData = (ServiceConnectionData)msg.obj; + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.d(TAG, "handleSyncHandlerMessage: MESSAGE_SERVICE_DISCONNECTED: " + + msgData.activeSyncContext + + " active is " + mActiveSyncContext); + } + // check that this isn't an old message + if (mActiveSyncContext == msgData.activeSyncContext) { + // cancel the sync if we have a syncadapter, which means one is + // outstanding + if (mActiveSyncContext.mSyncAdapter != null) { + try { + mActiveSyncContext.mSyncAdapter.cancelSync(); + } catch (RemoteException e) { + // we don't need to retry this in this case + } + } + + // pretend that the sync failed with an IOException, + // which is a soft error + SyncResult syncResult = new SyncResult(); + syncResult.stats.numIoExceptions++; + runSyncFinishedOrCanceled(syncResult); + + // since we are no longer syncing, check if it is time to start a new + // sync + runStateIdle(); + } + + break; + } + case SyncHandler.MESSAGE_SYNC_ALARM: { boolean isLoggable = Log.isLoggable(TAG, Log.VERBOSE); if (isLoggable) { @@ -1456,7 +1553,7 @@ class SyncManager { // If the accounts aren't known yet then we aren't ready to run. We will be kicked // when the account lookup request does complete. - String[] accounts = mAccounts; + Account[] accounts = mAccounts; if (accounts == null) { if (isLoggable) { Log.v(TAG, "runStateIdle: accounts not known, skipping"); @@ -1542,67 +1639,68 @@ class SyncManager { mSyncQueue.popHead(); } - String providerName = syncOperation.authority; - ensureContentResolver(); - IContentProvider contentProvider; - - // acquire the provider and update the sync history - try { - contentProvider = mContentResolver.acquireProvider(providerName); - if (contentProvider == null) { - Log.e(TAG, "Provider " + providerName + " doesn't exist"); - return; - } - if (contentProvider.getSyncAdapter() == null) { - Log.e(TAG, "Provider " + providerName + " isn't syncable, " + contentProvider); - return; + // connect to the sync adapter + SyncAdapterType syncAdapterType = new SyncAdapterType(syncOperation.authority, + syncOperation.account.mType); + RegisteredServicesCache.ServiceInfo syncAdapterInfo = + mSyncAdapters.getServiceInfo(syncAdapterType); + if (syncAdapterInfo == null) { + if (Config.LOGD) { + Log.d(TAG, "can't find a sync adapter for " + syncAdapterType); } - } catch (RemoteException remoteExc) { - Log.e(TAG, "Caught a RemoteException while preparing for sync, rescheduling " - + syncOperation, remoteExc); - rescheduleWithDelay(syncOperation); + runStateIdle(); return; - } catch (RuntimeException exc) { - Log.e(TAG, "Caught a RuntimeException while validating sync of " + providerName, - exc); + } + + ActiveSyncContext activeSyncContext = + new ActiveSyncContext(syncOperation, insertStartSyncEvent(syncOperation)); + mActiveSyncContext = activeSyncContext; + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "runStateIdle: setting mActiveSyncContext to " + mActiveSyncContext); + } + mSyncStorageEngine.setActiveSync(mActiveSyncContext); + if (!activeSyncContext.bindToSyncAdapter(syncAdapterInfo)) { + Log.e(TAG, "Bind attempt failed to " + syncAdapterInfo); + mActiveSyncContext = null; + mSyncStorageEngine.setActiveSync(mActiveSyncContext); + runStateIdle(); return; } - final long historyRowId = insertStartSyncEvent(syncOperation); + mSyncWakeLock.acquire(); + // no need to schedule an alarm, as that will be done by our caller. + + // the next step will occur when we get either a timeout or a + // MESSAGE_SERVICE_CONNECTED or MESSAGE_SERVICE_DISCONNECTED message + } + private void runBoundToSyncAdapter(ISyncAdapter syncAdapter) { + mActiveSyncContext.mSyncAdapter = syncAdapter; + final SyncOperation syncOperation = mActiveSyncContext.mSyncOperation; try { - ISyncAdapter syncAdapter = contentProvider.getSyncAdapter(); - ActiveSyncContext activeSyncContext = new ActiveSyncContext(syncOperation, - contentProvider, syncAdapter, historyRowId); - mSyncWakeLock.acquire(); - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "starting sync of " + syncOperation); - } - syncAdapter.startSync(activeSyncContext, syncOperation.account, + syncAdapter.startSync(mActiveSyncContext, syncOperation.account, syncOperation.extras); - mActiveSyncContext = activeSyncContext; - mSyncStorageEngine.setActiveSync(mActiveSyncContext); } catch (RemoteException remoteExc) { if (Config.LOGD) { Log.d(TAG, "runStateIdle: caught a RemoteException, rescheduling", remoteExc); } + mActiveSyncContext.unBindFromSyncAdapter(); mActiveSyncContext = null; mSyncStorageEngine.setActiveSync(mActiveSyncContext); rescheduleWithDelay(syncOperation); } catch (RuntimeException exc) { + mActiveSyncContext.unBindFromSyncAdapter(); mActiveSyncContext = null; mSyncStorageEngine.setActiveSync(mActiveSyncContext); Log.e(TAG, "Caught a RuntimeException while starting the sync " + syncOperation, exc); } - - // no need to schedule an alarm, as that will be done by our caller. } private void runSyncFinishedOrCanceled(SyncResult syncResult) { boolean isLoggable = Log.isLoggable(TAG, Log.VERBOSE); if (isLoggable) Log.v(TAG, "runSyncFinishedOrCanceled"); - ActiveSyncContext activeSyncContext = mActiveSyncContext; + final ActiveSyncContext activeSyncContext = mActiveSyncContext; mActiveSyncContext = null; mSyncStorageEngine.setActiveSync(mActiveSyncContext); @@ -1642,10 +1740,12 @@ class SyncManager { Log.v(TAG, "runSyncFinishedOrCanceled: is a cancel: operation " + syncOperation); } - try { - activeSyncContext.mSyncAdapter.cancelSync(); - } catch (RemoteException e) { - // we don't need to retry this in this case + if (activeSyncContext.mSyncAdapter != null) { + try { + activeSyncContext.mSyncAdapter.cancelSync(); + } catch (RemoteException e) { + // we don't need to retry this in this case + } } historyMessage = SyncStorageEngine.MESG_CANCELED; downstreamActivity = 0; @@ -1655,7 +1755,7 @@ class SyncManager { stopSyncEvent(activeSyncContext.mHistoryRowId, syncOperation, historyMessage, upstreamActivity, downstreamActivity, elapsedTime); - mContentResolver.releaseProvider(activeSyncContext.mContentProvider); + activeSyncContext.unBindFromSyncAdapter(); if (syncResult != null && syncResult.tooManyDeletions) { installHandleTooManyDeletesNotification(syncOperation.account, @@ -1860,7 +1960,7 @@ class SyncManager { mContext.sendBroadcast(syncStateIntent); } - private void installHandleTooManyDeletesNotification(String account, String authority, + private void installHandleTooManyDeletesNotification(Account account, String authority, long numDeletes) { if (mNotificationMgr == null) return; Intent clickIntent = new Intent(); @@ -2071,7 +2171,7 @@ class SyncManager { if (DEBUG_CHECK_DATA_CONSISTENCY) debugCheckDataStructures(true /* check the DB */); } - public void clear(String account, String authority) { + public void clear(Account account, String authority) { Iterator<Map.Entry<String, SyncOperation>> entries = mOpsByKey.entrySet().iterator(); while (entries.hasNext()) { Map.Entry<String, SyncOperation> entry = entries.next(); diff --git a/core/java/android/content/SyncStateContentProviderHelper.java b/core/java/android/content/SyncStateContentProviderHelper.java index f503e6f..dc728ec 100644 --- a/core/java/android/content/SyncStateContentProviderHelper.java +++ b/core/java/android/content/SyncStateContentProviderHelper.java @@ -23,6 +23,7 @@ import android.database.DatabaseUtils; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.net.Uri; +import android.accounts.Account; /** * Extends the schema of a ContentProvider to include the _sync_state table @@ -43,14 +44,15 @@ public class SyncStateContentProviderHelper { private static final Uri CONTENT_URI = Uri.parse("content://" + SYNC_STATE_AUTHORITY + "/state"); - private static final String ACCOUNT_WHERE = "_sync_account = ?"; + private static final String ACCOUNT_WHERE = "_sync_account = ? AND _sync_account_type = ?"; private final Provider mInternalProviderInterface; private static final String SYNC_STATE_TABLE = "_sync_state"; - private static long DB_VERSION = 2; + private static long DB_VERSION = 3; - private static final String[] ACCOUNT_PROJECTION = new String[]{"_sync_account"}; + private static final String[] ACCOUNT_PROJECTION = + new String[]{"_sync_account", "_sync_account_type"}; static { sURIMatcher.addURI(SYNC_STATE_AUTHORITY, "state", STATE); @@ -70,8 +72,9 @@ public class SyncStateContentProviderHelper { db.execSQL("CREATE TABLE _sync_state (" + "_id INTEGER PRIMARY KEY," + "_sync_account TEXT," + + "_sync_account_type TEXT," + "data TEXT," + - "UNIQUE(_sync_account)" + + "UNIQUE(_sync_account, _sync_account_type)" + ");"); db.execSQL("DROP TABLE IF EXISTS _sync_state_metadata"); @@ -168,15 +171,17 @@ public class SyncStateContentProviderHelper { * @param account the account of the row that should be copied over. */ public void copySyncState(SQLiteDatabase dbSrc, SQLiteDatabase dbDest, - String account) { - final String[] whereArgs = new String[]{account}; - Cursor c = dbSrc.query(SYNC_STATE_TABLE, new String[]{"_sync_account", "data"}, + Account account) { + final String[] whereArgs = new String[]{account.mName, account.mType}; + Cursor c = dbSrc.query(SYNC_STATE_TABLE, + new String[]{"_sync_account", "_sync_account_type", "data"}, ACCOUNT_WHERE, whereArgs, null, null, null); try { if (c.moveToNext()) { ContentValues values = new ContentValues(); values.put("_sync_account", c.getString(0)); - values.put("data", c.getBlob(1)); + values.put("_sync_account_type", c.getString(1)); + values.put("data", c.getBlob(2)); dbDest.replace(SYNC_STATE_TABLE, "_sync_account", values); } } finally { @@ -184,14 +189,17 @@ public class SyncStateContentProviderHelper { } } - public void onAccountsChanged(String[] accounts) { + public void onAccountsChanged(Account[] accounts) { SQLiteDatabase db = mOpenHelper.getWritableDatabase(); Cursor c = db.query(SYNC_STATE_TABLE, ACCOUNT_PROJECTION, null, null, null, null, null); try { while (c.moveToNext()) { - final String account = c.getString(0); + final String accountName = c.getString(0); + final String accountType = c.getString(1); + Account account = new Account(accountName, accountType); if (!ArrayUtils.contains(accounts, account)) { - db.delete(SYNC_STATE_TABLE, ACCOUNT_WHERE, new String[]{account}); + db.delete(SYNC_STATE_TABLE, ACCOUNT_WHERE, + new String[]{accountName, accountType}); } } } finally { @@ -199,9 +207,9 @@ public class SyncStateContentProviderHelper { } } - public void discardSyncData(SQLiteDatabase db, String account) { + public void discardSyncData(SQLiteDatabase db, Account account) { if (account != null) { - db.delete(SYNC_STATE_TABLE, ACCOUNT_WHERE, new String[]{account}); + db.delete(SYNC_STATE_TABLE, ACCOUNT_WHERE, new String[]{account.mName, account.mType}); } else { db.delete(SYNC_STATE_TABLE, null, null); } @@ -210,9 +218,9 @@ public class SyncStateContentProviderHelper { /** * Retrieves the SyncData bytes for the given account. The byte array returned may be null. */ - public byte[] readSyncDataBytes(SQLiteDatabase db, String account) { + public byte[] readSyncDataBytes(SQLiteDatabase db, Account account) { Cursor c = db.query(SYNC_STATE_TABLE, null, ACCOUNT_WHERE, - new String[]{account}, null, null, null); + new String[]{account.mName, account.mType}, null, null, null); try { if (c.moveToFirst()) { return c.getBlob(c.getColumnIndexOrThrow("data")); @@ -226,9 +234,10 @@ public class SyncStateContentProviderHelper { /** * Sets the SyncData bytes for the given account. The bytes array may be null. */ - public void writeSyncDataBytes(SQLiteDatabase db, String account, byte[] data) { + public void writeSyncDataBytes(SQLiteDatabase db, Account account, byte[] data) { ContentValues values = new ContentValues(); values.put("data", data); - db.update(SYNC_STATE_TABLE, values, ACCOUNT_WHERE, new String[]{account}); + db.update(SYNC_STATE_TABLE, values, ACCOUNT_WHERE, + new String[]{account.mName, account.mType}); } } diff --git a/core/java/android/content/SyncStorageEngine.java b/core/java/android/content/SyncStorageEngine.java index 9c25e73..aaa763d 100644 --- a/core/java/android/content/SyncStorageEngine.java +++ b/core/java/android/content/SyncStorageEngine.java @@ -24,6 +24,7 @@ import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlSerializer; +import android.accounts.Account; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; @@ -48,6 +49,8 @@ import java.util.HashMap; import java.util.Iterator; import java.util.TimeZone; +import com.google.android.collect.Sets; + /** * Singleton that tracks the sync data and overall sync * history on the device. @@ -122,7 +125,7 @@ public class SyncStorageEngine extends Handler { private static final long WRITE_STATISTICS_DELAY = 1000*60*30; // 1/2 hour public static class PendingOperation { - final String account; + final Account account; final int syncSource; final String authority; final Bundle extras; // note: read-only. @@ -130,7 +133,7 @@ public class SyncStorageEngine extends Handler { int authorityId; byte[] flatExtras; - PendingOperation(String account, int source, + PendingOperation(Account account, int source, String authority, Bundle extras) { this.account = account; this.syncSource = source; @@ -149,22 +152,22 @@ public class SyncStorageEngine extends Handler { } static class AccountInfo { - final String account; + final Account account; final HashMap<String, AuthorityInfo> authorities = new HashMap<String, AuthorityInfo>(); - AccountInfo(String account) { + AccountInfo(Account account) { this.account = account; } } public static class AuthorityInfo { - final String account; + final Account account; final String authority; final int ident; boolean enabled; - AuthorityInfo(String account, String authority, int ident) { + AuthorityInfo(Account account, String authority, int ident) { this.account = account; this.authority = authority; this.ident = ident; @@ -200,8 +203,8 @@ public class SyncStorageEngine extends Handler { private final SparseArray<AuthorityInfo> mAuthorities = new SparseArray<AuthorityInfo>(); - private final HashMap<String, AccountInfo> mAccounts = - new HashMap<String, AccountInfo>(); + private final HashMap<Account, AccountInfo> mAccounts = + new HashMap<Account, AccountInfo>(); private final ArrayList<PendingOperation> mPendingOperations = new ArrayList<PendingOperation>(); @@ -353,7 +356,7 @@ public class SyncStorageEngine extends Handler { } } - public boolean getSyncProviderAutomatically(String account, String providerName) { + public boolean getSyncProviderAutomatically(Account account, String providerName) { synchronized (mAuthorities) { if (account != null) { AuthorityInfo authority = getAuthorityLocked(account, providerName, @@ -374,7 +377,8 @@ public class SyncStorageEngine extends Handler { } } - public void setSyncProviderAutomatically(String account, String providerName, boolean sync) { + public void setSyncProviderAutomatically(Account account, String providerName, + boolean sync) { synchronized (mAuthorities) { if (account != null) { AuthorityInfo authority = getAuthorityLocked(account, providerName, @@ -412,7 +416,7 @@ public class SyncStorageEngine extends Handler { } } - public AuthorityInfo getAuthority(String account, String authority) { + public AuthorityInfo getAuthority(Account account, String authority) { synchronized (mAuthorities) { return getAuthorityLocked(account, authority, null); } @@ -428,7 +432,7 @@ public class SyncStorageEngine extends Handler { * Returns true if there is currently a sync operation for the given * account or authority in the pending list, or actively being processed. */ - public boolean isSyncActive(String account, String authority) { + public boolean isSyncActive(Account account, String authority) { synchronized (mAuthorities) { int i = mPendingOperations.size(); while (i > 0) { @@ -567,7 +571,7 @@ public class SyncStorageEngine extends Handler { * Called when the set of account has changed, given the new array of * active accounts. */ - public void doDatabaseCleanup(String[] accounts) { + public void doDatabaseCleanup(Account[] accounts) { synchronized (mAuthorities) { if (DEBUG) Log.w(TAG, "Updating for new accounts..."); SparseArray<AuthorityInfo> removing = new SparseArray<AuthorityInfo>(); @@ -659,7 +663,7 @@ public class SyncStorageEngine extends Handler { /** * Note that sync has started for the given account and authority. */ - public long insertStartSyncEvent(String accountName, String authorityName, + public long insertStartSyncEvent(Account accountName, String authorityName, long now, int source) { long id; synchronized (mAuthorities) { @@ -847,7 +851,7 @@ public class SyncStorageEngine extends Handler { /** * Return true if the pending status is true of any matching authorities. */ - public boolean isAuthorityPending(String account, String authority) { + public boolean isAuthorityPending(Account account, String authority) { synchronized (mAuthorities) { final int N = mSyncStatus.size(); for (int i=0; i<N; i++) { @@ -944,7 +948,7 @@ public class SyncStorageEngine extends Handler { * @param tag If non-null, this will be used in a log message if the * requested authority does not exist. */ - private AuthorityInfo getAuthorityLocked(String accountName, String authorityName, + private AuthorityInfo getAuthorityLocked(Account accountName, String authorityName, String tag) { AccountInfo account = mAccounts.get(accountName); if (account == null) { @@ -964,7 +968,7 @@ public class SyncStorageEngine extends Handler { return authority; } - private AuthorityInfo getOrCreateAuthorityLocked(String accountName, + private AuthorityInfo getOrCreateAuthorityLocked(Account accountName, String authorityName, int ident, boolean doWrite) { AccountInfo account = mAccounts.get(accountName); if (account == null) { @@ -1055,6 +1059,11 @@ public class SyncStorageEngine extends Handler { if (id >= 0) { String accountName = parser.getAttributeValue( null, "account"); + String accountType = parser.getAttributeValue( + null, "type"); + if (accountType == null) { + accountType = "com.google.GAIA"; + } String authorityName = parser.getAttributeValue( null, "authority"); String enabled = parser.getAttributeValue( @@ -1066,7 +1075,8 @@ public class SyncStorageEngine extends Handler { if (authority == null) { if (DEBUG_FILE) Log.v(TAG, "Creating entry"); authority = getOrCreateAuthorityLocked( - accountName, authorityName, id, false); + new Account(accountName, accountType), + authorityName, id, false); } if (authority != null) { authority.enabled = enabled == null @@ -1121,7 +1131,8 @@ public class SyncStorageEngine extends Handler { AuthorityInfo authority = mAuthorities.get(i); out.startTag(null, "authority"); out.attribute(null, "id", Integer.toString(authority.ident)); - out.attribute(null, "account", authority.account); + out.attribute(null, "account", authority.account.mName); + out.attribute(null, "type", authority.account.mType); out.attribute(null, "authority", authority.authority); if (!authority.enabled) { out.attribute(null, "enabled", "false"); @@ -1170,6 +1181,8 @@ public class SyncStorageEngine extends Handler { } if (db != null) { + final boolean hasType = db.getVersion() >= 11; + // Copy in all of the status information, as well as accounts. if (DEBUG_FILE) Log.v(TAG, "Reading legacy sync accounts db"); SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); @@ -1177,6 +1190,9 @@ public class SyncStorageEngine extends Handler { HashMap<String,String> map = new HashMap<String,String>(); map.put("_id", "status._id as _id"); map.put("account", "stats.account as account"); + if (hasType) { + map.put("account_type", "stats.account_type as account_type"); + } map.put("authority", "stats.authority as authority"); map.put("totalElapsedTime", "totalElapsedTime"); map.put("numSyncs", "numSyncs"); @@ -1195,9 +1211,15 @@ public class SyncStorageEngine extends Handler { Cursor c = qb.query(db, null, null, null, null, null, null); while (c.moveToNext()) { String accountName = c.getString(c.getColumnIndex("account")); + String accountType = hasType + ? c.getString(c.getColumnIndex("account_type")) : null; + if (accountType == null) { + accountType = "com.google.GAIA"; + } String authorityName = c.getString(c.getColumnIndex("authority")); AuthorityInfo authority = this.getOrCreateAuthorityLocked( - accountName, authorityName, -1, false); + new Account(accountName, accountType), + authorityName, -1, false); if (authority != null) { int i = mSyncStatus.size(); boolean found = false; diff --git a/core/java/android/content/SyncableContentProvider.java b/core/java/android/content/SyncableContentProvider.java index e0cd786..ab4e91c 100644 --- a/core/java/android/content/SyncableContentProvider.java +++ b/core/java/android/content/SyncableContentProvider.java @@ -19,6 +19,7 @@ package android.content; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.net.Uri; +import android.accounts.Account; import java.util.Map; @@ -32,6 +33,16 @@ import java.util.Map; public abstract class SyncableContentProvider extends ContentProvider { protected abstract boolean isTemporary(); + private volatile TempProviderSyncAdapter mTempProviderSyncAdapter; + + public void setTempProviderSyncAdapter(TempProviderSyncAdapter syncAdapter) { + mTempProviderSyncAdapter = syncAdapter; + } + + public TempProviderSyncAdapter getTempProviderSyncAdapter() { + return mTempProviderSyncAdapter; + } + /** * Close resources that must be closed. You must call this to properly release * the resources used by the SyncableContentProvider. @@ -110,7 +121,7 @@ public abstract class SyncableContentProvider extends ContentProvider { * @param context the sync context for the operation * @param account */ - public abstract void onSyncStart(SyncContext context, String account); + public abstract void onSyncStart(SyncContext context, Account account); /** * Called right after a sync is completed @@ -124,7 +135,7 @@ public abstract class SyncableContentProvider extends ContentProvider { * The account of the most recent call to onSyncStart() * @return the account */ - public abstract String getSyncingAccount(); + public abstract Account getSyncingAccount(); /** * Merge diffs from a sync source with this content provider. @@ -194,7 +205,7 @@ public abstract class SyncableContentProvider extends ContentProvider { * Make sure that there are no entries for accounts that no longer exist * @param accountsArray the array of currently-existing accounts */ - protected abstract void onAccountsChanged(String[] accountsArray); + protected abstract void onAccountsChanged(Account[] accountsArray); /** * A helper method to delete all rows whose account is not in the accounts @@ -203,26 +214,24 @@ public abstract class SyncableContentProvider extends ContentProvider { * * @param accounts a map of existing accounts * @param table the table to delete from - * @param accountColumnName the name of the column that is expected - * to hold the account. */ - protected abstract void deleteRowsForRemovedAccounts(Map<String, Boolean> accounts, - String table, String accountColumnName); + protected abstract void deleteRowsForRemovedAccounts(Map<Account, Boolean> accounts, + String table); /** * Called when the sync system determines that this provider should no longer * contain records for the specified account. */ - public abstract void wipeAccount(String account); + public abstract void wipeAccount(Account account); /** * Retrieves the SyncData bytes for the given account. The byte array returned may be null. */ - public abstract byte[] readSyncDataBytes(String account); + public abstract byte[] readSyncDataBytes(Account account); /** * Sets the SyncData bytes for the given account. The bytes array may be null. */ - public abstract void writeSyncDataBytes(String account, byte[] data); + public abstract void writeSyncDataBytes(Account account, byte[] data); } diff --git a/core/java/android/content/TempProviderSyncAdapter.java b/core/java/android/content/TempProviderSyncAdapter.java index eb3a5da..0cbe01e 100644 --- a/core/java/android/content/TempProviderSyncAdapter.java +++ b/core/java/android/content/TempProviderSyncAdapter.java @@ -12,6 +12,7 @@ import android.util.Config; import android.util.EventLog; import android.util.Log; import android.util.TimingLogger; +import android.accounts.Account; /** * @hide @@ -67,7 +68,7 @@ public abstract class TempProviderSyncAdapter extends SyncAdapter { * @return true, if the sync was successfully started. One reason it can * fail to start is if there is no user configured on the device. */ - public abstract void onSyncStarting(SyncContext context, String account, boolean forced, + public abstract void onSyncStarting(SyncContext context, Account account, boolean forced, SyncResult result); /** @@ -168,12 +169,12 @@ public abstract class TempProviderSyncAdapter extends SyncAdapter { * exist. * @param accounts the list of accounts */ - public abstract void onAccountsChanged(String[] accounts); + public abstract void onAccountsChanged(Account[] accounts); private Context mContext; private class SyncThread extends Thread { - private final String mAccount; + private final Account mAccount; private final Bundle mExtras; private final SyncContext mSyncContext; private volatile boolean mIsCanceled = false; @@ -181,7 +182,7 @@ public abstract class TempProviderSyncAdapter extends SyncAdapter { private long mInitialRxBytes; private final SyncResult mResult; - SyncThread(SyncContext syncContext, String account, Bundle extras) { + SyncThread(SyncContext syncContext, Account account, Bundle extras) { super("SyncThread"); mAccount = account; mExtras = extras; @@ -221,7 +222,7 @@ public abstract class TempProviderSyncAdapter extends SyncAdapter { } } - private void sync(SyncContext syncContext, String account, Bundle extras) { + private void sync(SyncContext syncContext, Account account, Bundle extras) { mIsCanceled = false; mProviderSyncStarted = false; @@ -273,7 +274,7 @@ public abstract class TempProviderSyncAdapter extends SyncAdapter { } } - private void runSyncLoop(SyncContext syncContext, String account, Bundle extras) { + private void runSyncLoop(SyncContext syncContext, Account account, Bundle extras) { TimingLogger syncTimer = new TimingLogger(TAG + "Profiling", "sync"); syncTimer.addSplit("start"); int loopCount = 0; @@ -518,7 +519,7 @@ public abstract class TempProviderSyncAdapter extends SyncAdapter { EventLog.writeEvent(SyncAdapter.LOG_SYNC_DETAILS, TAG, bytesSent, bytesReceived, ""); } - public void startSync(SyncContext syncContext, String account, Bundle extras) { + public void startSync(SyncContext syncContext, Account account, Bundle extras) { if (mSyncThread != null) { syncContext.onFinished(SyncResult.ALREADY_IN_PROGRESS); return; diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl index 5f62248..9eca4a5 100644 --- a/core/java/android/content/pm/IPackageManager.aidl +++ b/core/java/android/content/pm/IPackageManager.aidl @@ -122,6 +122,7 @@ interface IPackageManager { * providers that can sync. * @param outInfo Filled in with a list of the ProviderInfo for each * name in 'outNames'. + * @deprecated */ void querySyncProviders(inout List<String> outNames, inout List<ProviderInfo> outInfo); diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index 6578391..88eccf7 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -1459,8 +1459,6 @@ public abstract class PackageManager { * which market the package came from. * * @param packageName The name of the package to query - * - * @hide */ public abstract String getInstallerPackageName(String packageName); diff --git a/core/java/android/content/pm/ProviderInfo.java b/core/java/android/content/pm/ProviderInfo.java index b67ddf6..1d11b31 100644 --- a/core/java/android/content/pm/ProviderInfo.java +++ b/core/java/android/content/pm/ProviderInfo.java @@ -65,7 +65,11 @@ public final class ProviderInfo extends ComponentInfo * running in the same process. Higher goes first. */ public int initOrder = 0; - /** Whether or not this provider is syncable. */ + /** + * Whether or not this provider is syncable. + * @deprecated This flag is now being ignored. The current way to make a provider + * syncable is to provide a SyncAdapter service for a given provider/account type. + */ public boolean isSyncable = false; public ProviderInfo() { diff --git a/core/java/android/content/pm/RegisteredServicesCache.java b/core/java/android/content/pm/RegisteredServicesCache.java new file mode 100644 index 0000000..d8f8478 --- /dev/null +++ b/core/java/android/content/pm/RegisteredServicesCache.java @@ -0,0 +1,233 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content.pm; + +import android.content.Context; +import android.content.BroadcastReceiver; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.ComponentName; +import android.content.res.XmlResourceParser; +import android.util.Log; +import android.util.AttributeSet; +import android.util.Xml; + +import java.util.Map; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.io.IOException; + +import com.google.android.collect.Maps; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlPullParser; + +/** + * A cache of registered services. This cache + * is built by interrogating the {@link PackageManager} and is updated as packages are added, + * removed and changed. The services are referred to by type V and + * are made available via the {@link #getServiceInfo} method. + * @hide + */ +public abstract class RegisteredServicesCache<V> { + private static final String TAG = "PackageManager"; + + public final Context mContext; + private final String mInterfaceName; + private final String mMetaDataName; + private final String mAttributesName; + + // no need to be synchronized since the map is never changed once mService is written + private volatile Map<V, ServiceInfo<V>> mServices; + + // synchronized on "this" + private BroadcastReceiver mReceiver = null; + + public RegisteredServicesCache(Context context, String interfaceName, String metaDataName, + String attributeName) { + mContext = context; + mInterfaceName = interfaceName; + mMetaDataName = metaDataName; + mAttributesName = attributeName; + } + + public void dump(FileDescriptor fd, PrintWriter fout, String[] args) { + getAllServices(); + Map<V, ServiceInfo<V>> services = mServices; + fout.println("RegisteredServicesCache: " + services.size() + " services"); + for (ServiceInfo info : services.values()) { + fout.println(" " + info); + } + } + + private boolean maybeRegisterForPackageChanges() { + synchronized (this) { + if (mReceiver == null) { + synchronized (this) { + mReceiver = new BroadcastReceiver() { + public void onReceive(Context context, Intent intent) { + mServices = generateServicesMap(); + } + }; + } + + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(Intent.ACTION_PACKAGE_ADDED); + intentFilter.addAction(Intent.ACTION_PACKAGE_CHANGED); + intentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); + mContext.registerReceiver(mReceiver, intentFilter); + return true; + } + return false; + } + } + + private void maybeUnregisterForPackageChanges() { + synchronized (this) { + if (mReceiver != null) { + mContext.unregisterReceiver(mReceiver); + mReceiver = null; + } + } + } + + /** + * Value type that describes a Service. The information within can be used + * to bind to the service. + */ + public static class ServiceInfo<V> { + public final V type; + public final ComponentName componentName; + + private ServiceInfo(V type, ComponentName componentName) { + this.type = type; + this.componentName = componentName; + } + + public String toString() { + return "ServiceInfo: " + type + ", " + componentName; + } + } + + /** + * Accessor for the registered authenticators. + * @param type the account type of the authenticator + * @return the AuthenticatorInfo that matches the account type or null if none is present + */ + public ServiceInfo getServiceInfo(V type) { + if (mServices == null) { + maybeRegisterForPackageChanges(); + mServices = generateServicesMap(); + } + return mServices.get(type); + } + + /** + * @return a collection of {@link RegisteredServicesCache.ServiceInfo} objects for all + * registered authenticators. + */ + public Collection<ServiceInfo<V>> getAllServices() { + if (mServices == null) { + maybeRegisterForPackageChanges(); + mServices = generateServicesMap(); + } + return Collections.unmodifiableCollection(mServices.values()); + } + + /** + * Stops the monitoring of package additions, removals and changes. + */ + public void close() { + maybeUnregisterForPackageChanges(); + } + + protected void finalize() throws Throwable { + synchronized (this) { + if (mReceiver != null) { + Log.e(TAG, "RegisteredServicesCache finalized without being closed"); + } + } + close(); + super.finalize(); + } + + private Map<V, ServiceInfo<V>> generateServicesMap() { + Map<V, ServiceInfo<V>> services = Maps.newHashMap(); + PackageManager pm = mContext.getPackageManager(); + + List<ResolveInfo> resolveInfos = + pm.queryIntentServices(new Intent(mInterfaceName), PackageManager.GET_META_DATA); + + for (ResolveInfo resolveInfo : resolveInfos) { + try { + ServiceInfo<V> info = parseServiceInfo(resolveInfo); + if (info != null) { + services.put(info.type, info); + } else { + Log.w(TAG, "Unable to load input method " + resolveInfo.toString()); + } + } catch (XmlPullParserException e) { + Log.w(TAG, "Unable to load input method " + resolveInfo.toString(), e); + } catch (IOException e) { + Log.w(TAG, "Unable to load input method " + resolveInfo.toString(), e); + } + } + + return services; + } + + private ServiceInfo<V> parseServiceInfo(ResolveInfo service) + throws XmlPullParserException, IOException { + android.content.pm.ServiceInfo si = service.serviceInfo; + ComponentName componentName = new ComponentName(si.packageName, si.name); + + PackageManager pm = mContext.getPackageManager(); + + XmlResourceParser parser = null; + try { + parser = si.loadXmlMetaData(pm, mMetaDataName); + if (parser == null) { + throw new XmlPullParserException("No " + mMetaDataName + " meta-data"); + } + + AttributeSet attrs = Xml.asAttributeSet(parser); + + int type; + while ((type=parser.next()) != XmlPullParser.END_DOCUMENT + && type != XmlPullParser.START_TAG) { + } + + String nodeName = parser.getName(); + if (!mAttributesName.equals(nodeName)) { + throw new XmlPullParserException( + "Meta-data does not start with " + mAttributesName + " tag"); + } + + V v = parseServiceAttributes(attrs); + if (v == null) { + return null; + } + return new ServiceInfo<V>(v, componentName); + } finally { + if (parser != null) parser.close(); + } + } + + public abstract V parseServiceAttributes(AttributeSet attrs); +} diff --git a/core/java/android/database/AbstractWindowedCursor.java b/core/java/android/database/AbstractWindowedCursor.java index 4ac0aef..27a02e2 100644 --- a/core/java/android/database/AbstractWindowedCursor.java +++ b/core/java/android/database/AbstractWindowedCursor.java @@ -166,6 +166,48 @@ public abstract class AbstractWindowedCursor extends AbstractCursor return mWindow.isBlob(mPos, columnIndex); } + public boolean isString(int columnIndex) + { + checkPosition(); + + synchronized(mUpdatedRows) { + if (isFieldUpdated(columnIndex)) { + Object object = getUpdatedField(columnIndex); + return object == null || object instanceof String; + } + } + + return mWindow.isString(mPos, columnIndex); + } + + public boolean isLong(int columnIndex) + { + checkPosition(); + + synchronized(mUpdatedRows) { + if (isFieldUpdated(columnIndex)) { + Object object = getUpdatedField(columnIndex); + return object != null && (object instanceof Integer || object instanceof Long); + } + } + + return mWindow.isLong(mPos, columnIndex); + } + + public boolean isFloat(int columnIndex) + { + checkPosition(); + + synchronized(mUpdatedRows) { + if (isFieldUpdated(columnIndex)) { + Object object = getUpdatedField(columnIndex); + return object != null && (object instanceof Float || object instanceof Double); + } + } + + return mWindow.isFloat(mPos, columnIndex); + } + @Override protected void checkPosition() { diff --git a/core/java/android/database/CursorWindow.java b/core/java/android/database/CursorWindow.java index 8e26730..99db81b 100644 --- a/core/java/android/database/CursorWindow.java +++ b/core/java/android/database/CursorWindow.java @@ -263,7 +263,58 @@ public class CursorWindow extends SQLiteClosable implements Parcelable { } } + /** + * Checks if a field contains a long + * + * @param row the row to read from, row - getStartPosition() being the actual row in the window + * @param col the column to read from + * @return {@code true} if given field is a long + */ + public boolean isLong(int row, int col) { + acquireReference(); + try { + return isInteger_native(row - mStartPos, col); + } finally { + releaseReference(); + } + } + + /** + * Checks if a field contains a float. + * + * @param row the row to read from, row - getStartPosition() being the actual row in the window + * @param col the column to read from + * @return {@code true} if given field is a float + */ + public boolean isFloat(int row, int col) { + acquireReference(); + try { + return isFloat_native(row - mStartPos, col); + } finally { + releaseReference(); + } + } + + /** + * Checks if a field contains either a String or is null. + * + * @param row the row to read from, row - getStartPosition() being the actual row in the window + * @param col the column to read from + * @return {@code true} if given field is {@code NULL} or a String + */ + public boolean isString(int row, int col) { + acquireReference(); + try { + return isString_native(row - mStartPos, col); + } finally { + releaseReference(); + } + } + private native boolean isBlob_native(int row, int col); + private native boolean isString_native(int row, int col); + private native boolean isInteger_native(int row, int col); + private native boolean isFloat_native(int row, int col); /** * Returns a String for the given field. diff --git a/core/java/android/database/DatabaseUtils.java b/core/java/android/database/DatabaseUtils.java index 10f3806..4ca6601 100644 --- a/core/java/android/database/DatabaseUtils.java +++ b/core/java/android/database/DatabaseUtils.java @@ -20,6 +20,7 @@ import org.apache.commons.codec.binary.Hex; import android.content.ContentValues; import android.content.Context; +import android.content.OperationApplicationException; import android.database.sqlite.SQLiteAbortException; import android.database.sqlite.SQLiteConstraintException; import android.database.sqlite.SQLiteDatabase; @@ -82,6 +83,8 @@ public class DatabaseUtils { code = 8; } else if (e instanceof SQLiteException) { code = 9; + } else if (e instanceof OperationApplicationException) { + code = 10; } else { reply.writeException(e); Log.e(TAG, "Writing exception to parcel", e); @@ -123,6 +126,18 @@ public class DatabaseUtils { } } + public static void readExceptionWithOperationApplicationExceptionFromParcel( + Parcel reply) throws OperationApplicationException { + int code = reply.readInt(); + if (code == 0) return; + String msg = reply.readString(); + if (code == 10) { + throw new OperationApplicationException(msg); + } else { + DatabaseUtils.readExceptionFromParcel(reply, msg, code); + } + } + private static final void readExceptionFromParcel(Parcel reply, String msg, int code) { switch (code) { case 2: @@ -211,7 +226,7 @@ public class DatabaseUtils { sb.append(sqlString); sb.append('\''); } - + /** * SQL-escape a string. */ @@ -240,7 +255,7 @@ public class DatabaseUtils { appendEscapedSQLString(sql, value.toString()); } } - + /** * Concatenates two SQL WHERE clauses, handling empty or null values. * @hide @@ -252,12 +267,12 @@ public class DatabaseUtils { if (TextUtils.isEmpty(b)) { return a; } - + return "(" + a + ") AND (" + b + ")"; } - + /** - * return the collation key + * return the collation key * @param name * @return the collation key */ @@ -269,7 +284,7 @@ public class DatabaseUtils { return ""; } } - + /** * return the collation key in hex format * @param name @@ -280,7 +295,7 @@ public class DatabaseUtils { char[] keys = Hex.encodeHex(arr); return new String(keys, 0, getKeyLen(arr) * 2); } - + private static int getKeyLen(byte[] arr) { if (arr[arr.length - 1] != 0) { return arr.length; @@ -289,16 +304,16 @@ public class DatabaseUtils { return arr.length-1; } } - + private static byte[] getCollationKeyInBytes(String name) { if (mColl == null) { mColl = Collator.getInstance(); mColl.setStrength(Collator.PRIMARY); } - return mColl.getCollationKey(name).toByteArray(); + return mColl.getCollationKey(name).toByteArray(); } - - private static Collator mColl = null; + + private static Collator mColl = null; /** * Prints the contents of a Cursor to System.out. The position is restored * after printing. @@ -591,10 +606,12 @@ public class DatabaseUtils { public static long queryNumEntries(SQLiteDatabase db, String table) { Cursor cursor = db.query(table, countProjection, null, null, null, null, null); - cursor.moveToFirst(); - long count = cursor.getLong(0); - cursor.deactivate(); - return count; + try { + cursor.moveToFirst(); + return cursor.getLong(0); + } finally { + cursor.close(); + } } /** diff --git a/core/java/android/database/sqlite/SQLiteQueryBuilder.java b/core/java/android/database/sqlite/SQLiteQueryBuilder.java index 8a63919..af54a71 100644 --- a/core/java/android/database/sqlite/SQLiteQueryBuilder.java +++ b/core/java/android/database/sqlite/SQLiteQueryBuilder.java @@ -355,23 +355,26 @@ public class SQLiteQueryBuilder String groupBy, String having, String sortOrder, String limit) { String[] projection = computeProjection(projectionIn); + StringBuilder where = new StringBuilder(); + if (mWhereClause.length() > 0) { - mWhereClause.append(')'); + where.append(mWhereClause.toString()); + where.append(')'); } // Tack on the user's selection, if present. if (selection != null && selection.length() > 0) { if (mWhereClause.length() > 0) { - mWhereClause.append(" AND "); + where.append(" AND "); } - mWhereClause.append('('); - mWhereClause.append(selection); - mWhereClause.append(')'); + where.append('('); + where.append(selection); + where.append(')'); } return buildQueryString( - mDistinct, mTables, projection, mWhereClause.toString(), + mDistinct, mTables, projection, where.toString(), groupBy, having, sortOrder, limit); } diff --git a/core/java/android/net/WebAddress.java b/core/java/android/net/WebAddress.java index f4a2a6a..f6159de 100644 --- a/core/java/android/net/WebAddress.java +++ b/core/java/android/net/WebAddress.java @@ -54,7 +54,7 @@ public class WebAddress { static Pattern sAddressPattern = Pattern.compile( /* scheme */ "(?:(http|HTTP|https|HTTPS|file|FILE)\\:\\/\\/)?" + /* authority */ "(?:([-A-Za-z0-9$_.+!*'(),;?&=]+(?:\\:[-A-Za-z0-9$_.+!*'(),;?&=]+)?)@)?" + - /* host */ "([-A-Za-z0-9%]+(?:\\.[-A-Za-z0-9%]+)*)?" + + /* host */ "([-A-Za-z0-9%_]+(?:\\.[-A-Za-z0-9%_]+)*)?" + /* port */ "(?:\\:([0-9]+))?" + /* path */ "(\\/?.*)?"); diff --git a/core/java/android/net/http/Request.java b/core/java/android/net/http/Request.java index df4fff0..aeb85a2 100644 --- a/core/java/android/net/http/Request.java +++ b/core/java/android/net/http/Request.java @@ -116,12 +116,17 @@ class Request { mBodyProvider = bodyProvider; mBodyLength = bodyLength; - if (bodyProvider == null) { + if (bodyProvider == null && !"POST".equalsIgnoreCase(method)) { mHttpRequest = new BasicHttpRequest(method, getUri()); } else { mHttpRequest = new BasicHttpEntityEnclosingRequest( method, getUri()); - setBodyProvider(bodyProvider, bodyLength); + // it is ok to have null entity for BasicHttpEntityEnclosingRequest. + // By using BasicHttpEntityEnclosingRequest, it will set up the + // correct content-length, content-type and content-encoding. + if (bodyProvider != null) { + setBodyProvider(bodyProvider, bodyLength); + } } addHeader(HOST_HEADER, getHostPort()); diff --git a/core/java/android/net/http/RequestQueue.java b/core/java/android/net/http/RequestQueue.java index 66d5722..54a1cce 100644 --- a/core/java/android/net/http/RequestQueue.java +++ b/core/java/android/net/http/RequestQueue.java @@ -52,10 +52,44 @@ public class RequestQueue implements RequestFeeder { private Context mContext; + private static class RequestSet { + private final LinkedList<Request> mHighPriority; + private final LinkedList<Request> mLowPriority; + + RequestSet() { + mHighPriority = new LinkedList<Request>(); + mLowPriority = new LinkedList<Request>(); + } + + void add(Request req, boolean head) { + LinkedList l = mLowPriority; + if (req.mHighPriority) { + l = mHighPriority; + } + if (head) { + l.addFirst(req); + } else { + l.add(req); + } + } + + Request removeFirst() { + if (!mHighPriority.isEmpty()) { + return mHighPriority.removeFirst(); + } else if (!mLowPriority.isEmpty()) { + return mLowPriority.removeFirst(); + } + return null; + } + + boolean isEmpty() { + return mHighPriority.isEmpty() && mLowPriority.isEmpty(); + } + }; /** * Requests, indexed by HttpHost (scheme, host, port) */ - private LinkedHashMap<HttpHost, LinkedList<Request>> mPending; + private LinkedHashMap<HttpHost, RequestSet> mPending; /* Support for notifying a client when queue is empty */ private boolean mClientWaiting = false; @@ -344,7 +378,7 @@ public class RequestQueue implements RequestFeeder { public RequestQueue(Context context, int connectionCount) { mContext = context; - mPending = new LinkedHashMap<HttpHost, LinkedList<Request>>(32); + mPending = new LinkedHashMap<HttpHost, RequestSet>(32); mActivePool = new ActivePool(connectionCount); mActivePool.startup(); @@ -480,7 +514,7 @@ public class RequestQueue implements RequestFeeder { req = new Request(method, httpHost, mProxyHost, uri.mPath, bodyProvider, bodyLength, eventHandler, headers, highPriority); - queueRequest(req, highPriority); + queueRequest(req, false); mActivePool.mTotalRequest++; @@ -520,19 +554,24 @@ public class RequestQueue implements RequestFeeder { HttpLog.v("dump()"); StringBuilder dump = new StringBuilder(); int count = 0; - Iterator<Map.Entry<HttpHost, LinkedList<Request>>> iter; + Iterator<Map.Entry<HttpHost, RequestSet>> iter; // mActivePool.log(dump); if (!mPending.isEmpty()) { iter = mPending.entrySet().iterator(); while (iter.hasNext()) { - Map.Entry<HttpHost, LinkedList<Request>> entry = iter.next(); + Map.Entry<HttpHost, RequestSet> entry = iter.next(); String hostName = entry.getKey().getHostName(); StringBuilder line = new StringBuilder("p" + count++ + " " + hostName + " "); - LinkedList<Request> reqList = entry.getValue(); - ListIterator reqIter = reqList.listIterator(0); + RequestSet reqList = entry.getValue(); + ListIterator reqIter = reqList.mHighPriority.listIterator(0); + while (iter.hasNext()) { + Request request = (Request)iter.next(); + line.append(request + " "); + } + reqIter = reqList.mLowPriority.listIterator(0); while (iter.hasNext()) { Request request = (Request)iter.next(); line.append(request + " "); @@ -564,7 +603,7 @@ public class RequestQueue implements RequestFeeder { Request ret = null; if (mNetworkConnected && mPending.containsKey(host)) { - LinkedList<Request> reqList = mPending.get(host); + RequestSet reqList = mPending.get(host); ret = reqList.removeFirst(); if (reqList.isEmpty()) { mPending.remove(host); @@ -597,18 +636,14 @@ public class RequestQueue implements RequestFeeder { protected synchronized void queueRequest(Request request, boolean head) { HttpHost host = request.mProxyHost == null ? request.mHost : request.mProxyHost; - LinkedList<Request> reqList; + RequestSet reqList; if (mPending.containsKey(host)) { reqList = mPending.get(host); } else { - reqList = new LinkedList<Request>(); + reqList = new RequestSet(); mPending.put(host, reqList); } - if (head) { - reqList.addFirst(request); - } else { - reqList.add(request); - } + reqList.add(request, head); } @@ -621,12 +656,12 @@ public class RequestQueue implements RequestFeeder { } /* helper */ - private Request removeFirst(LinkedHashMap<HttpHost, LinkedList<Request>> requestQueue) { + private Request removeFirst(LinkedHashMap<HttpHost, RequestSet> requestQueue) { Request ret = null; - Iterator<Map.Entry<HttpHost, LinkedList<Request>>> iter = requestQueue.entrySet().iterator(); + Iterator<Map.Entry<HttpHost, RequestSet>> iter = requestQueue.entrySet().iterator(); if (iter.hasNext()) { - Map.Entry<HttpHost, LinkedList<Request>> entry = iter.next(); - LinkedList<Request> reqList = entry.getValue(); + Map.Entry<HttpHost, RequestSet> entry = iter.next(); + RequestSet reqList = entry.getValue(); ret = reqList.removeFirst(); if (reqList.isEmpty()) { requestQueue.remove(entry.getKey()); diff --git a/core/java/android/os/Debug.java b/core/java/android/os/Debug.java index 8fcb4d7..d9612af 100644 --- a/core/java/android/os/Debug.java +++ b/core/java/android/os/Debug.java @@ -647,6 +647,25 @@ href="{@docRoot}guide/developing/tools/traceview.html">Traceview: A Graphical Lo public static final native int getBinderDeathObjectCount(); /** + * Primes the register map cache. + * + * Only works for classes in the bootstrap class loader. Does not + * cause classes to be loaded if they're not already present. + * + * The classAndMethodDesc argument is a concatentation of the VM-internal + * class descriptor, method name, and method descriptor. Examples: + * Landroid/os/Looper;.loop:()V + * Landroid/app/ActivityThread;.main:([Ljava/lang/String;)V + * + * @param classAndMethodDesc the method to prepare + * + * @hide + */ + public static final boolean cacheRegisterMap(String classAndMethodDesc) { + return VMDebug.cacheRegisterMap(classAndMethodDesc); + } + + /** * API for gathering and querying instruction counts. * * Example usage: diff --git a/core/java/android/os/Exec.java b/core/java/android/os/Exec.java deleted file mode 100644 index a50d5fe..0000000 --- a/core/java/android/os/Exec.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright (C) 2007 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.os; - -import java.io.FileDescriptor; - -/** - * @hide - * Tools for executing commands. Not for public consumption. - */ - -public class Exec -{ - /** - * @param cmd The command to execute - * @param arg0 The first argument to the command, may be null - * @param arg1 the second argument to the command, may be null - * @return the file descriptor of the started process. - * - */ - public static FileDescriptor createSubprocess( - String cmd, String arg0, String arg1) { - return createSubprocess(cmd, arg0, arg1, null); - } - - /** - * @param cmd The command to execute - * @param arg0 The first argument to the command, may be null - * @param arg1 the second argument to the command, may be null - * @param processId A one-element array to which the process ID of the - * started process will be written. - * @return the file descriptor of the started process. - * - */ - public static native FileDescriptor createSubprocess( - String cmd, String arg0, String arg1, int[] processId); - - public static native void setPtyWindowSize(FileDescriptor fd, - int row, int col, int xpixel, int ypixel); - /** - * Causes the calling thread to wait for the process associated with the - * receiver to finish executing. - * - * @return The exit value of the Process being waited on - * - */ - public static native int waitFor(int processId); -} - diff --git a/core/java/android/os/LatencyTimer.java b/core/java/android/os/LatencyTimer.java new file mode 100644 index 0000000..ed2f0f9e --- /dev/null +++ b/core/java/android/os/LatencyTimer.java @@ -0,0 +1,94 @@ +/* + * 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 java.util.HashMap; + +/** + * A class to help with measuring latency in your code. + * + * Suggested usage: + * 1) Instanciate a LatencyTimer as a class field. + * private [static] LatencyTimer mLt = new LatencyTimer(100, 1000); + * 2) At various points in the code call sample with a string and the time delta to some fixed time. + * The string should be unique at each point of the code you are measuring. + * mLt.sample("before processing event", System.nanoTime() - event.getEventTimeNano()); + * processEvent(event); + * mLt.sample("after processing event ", System.nanoTime() - event.getEventTimeNano()); + * + * @hide + */ +public final class LatencyTimer +{ + final String TAG = "LatencyTimer"; + final int mSampleSize; + final int mScaleFactor; + volatile HashMap<String, long[]> store = new HashMap<String, long[]>(); + + /** + * Creates a LatencyTimer object + * @param sampleSize number of samples to collect before printing out the average + * @param scaleFactor divisor used to make each sample smaller to prevent overflow when + * (sampleSize * average sample value)/scaleFactor > Long.MAX_VALUE + */ + public LatencyTimer(int sampleSize, int scaleFactor) { + if (scaleFactor == 0) { + scaleFactor = 1; + } + mScaleFactor = scaleFactor; + mSampleSize = sampleSize; + } + + /** + * Add a sample delay for averaging. + * @param tag string used for printing out the result. This should be unique at each point of + * this called. + * @param delta time difference from an unique point of reference for a particular iteration + */ + public void sample(String tag, long delta) { + long[] array = getArray(tag); + + // array[mSampleSize] holds the number of used entries + final int index = (int) array[mSampleSize]++; + array[index] = delta; + if (array[mSampleSize] == mSampleSize) { + long totalDelta = 0; + for (long d : array) { + totalDelta += d/mScaleFactor; + } + array[mSampleSize] = 0; + Log.i(TAG, tag + " average = " + totalDelta / mSampleSize); + } + } + + private long[] getArray(String tag) { + long[] data = store.get(tag); + if (data == null) { + synchronized(store) { + data = store.get(tag); + if (data == null) { + data = new long[mSampleSize + 1]; + store.put(tag, data); + data[mSampleSize] = 0; + } + } + } + return data; + } +} diff --git a/core/java/android/os/RemoteCallbackList.java b/core/java/android/os/RemoteCallbackList.java index 23c0a7b..5ab305e 100644 --- a/core/java/android/os/RemoteCallbackList.java +++ b/core/java/android/os/RemoteCallbackList.java @@ -22,7 +22,7 @@ import java.util.HashMap; * Takes care of the grunt work of maintaining a list of remote interfaces, * typically for the use of performing callbacks from a * {@link android.app.Service} to its clients. In particular, this: - * + * * <ul> * <li> Keeps track of a set of registered {@link IInterface} callbacks, * taking care to identify them through their underlying unique {@link IBinder} @@ -34,13 +34,13 @@ import java.util.HashMap; * multithreaded incoming calls, and a thread-safe way to iterate over a * snapshot of the list without holding its lock. * </ul> - * + * * <p>To use this class, simply create a single instance along with your * service, and call its {@link #register} and {@link #unregister} methods * as client register and unregister with your service. To call back on to * the registered clients, use {@link #beginBroadcast}, * {@link #getBroadcastItem}, and {@link #finishBroadcast}. - * + * * <p>If a registered callback's process goes away, this class will take * care of automatically removing it from the list. If you want to do * additional work in this situation, you can create a subclass that @@ -51,7 +51,7 @@ public class RemoteCallbackList<E extends IInterface> { = new HashMap<IBinder, Callback>(); private Object[] mActiveBroadcast; private boolean mKilled = false; - + private final class Callback implements IBinder.DeathRecipient { final E mCallback; final Object mCookie; @@ -60,7 +60,7 @@ public class RemoteCallbackList<E extends IInterface> { mCallback = callback; mCookie = cookie; } - + public void binderDied() { synchronized (mCallbacks) { mCallbacks.remove(mCallback.asBinder()); @@ -68,7 +68,7 @@ public class RemoteCallbackList<E extends IInterface> { onCallbackDied(mCallback, mCookie); } } - + /** * Simple version of {@link RemoteCallbackList#register(E, Object)} * that does not take a cookie object. @@ -85,19 +85,20 @@ public class RemoteCallbackList<E extends IInterface> { * object is already in the list), then it will be left as-is. * Registrations are not counted; a single call to {@link #unregister} * will remove a callback after any number calls to register it. - * + * * @param callback The callback interface to be added to the list. Must * not be null -- passing null here will cause a NullPointerException. * Most services will want to check for null before calling this with * an object given from a client, so that clients can't crash the * service with bad data. + * * @param cookie Optional additional data to be associated with this * callback. * * @return Returns true if the callback was successfully added to the list. * Returns false if it was not added, either because {@link #kill} had * previously been called or the callback's process has gone away. - * + * * @see #unregister * @see #kill * @see #onCallbackDied @@ -118,7 +119,7 @@ public class RemoteCallbackList<E extends IInterface> { } } } - + /** * Remove from the list a callback that was previously added with * {@link #register}. This uses the @@ -126,14 +127,14 @@ public class RemoteCallbackList<E extends IInterface> { * find the previous registration. * Registrations are not counted; a single unregister call will remove * a callback after any number calls to {@link #register} for it. - * + * * @param callback The callback to be removed from the list. Passing * null here will cause a NullPointerException, so you will generally want * to check for null before calling. - * + * * @return Returns true if the callback was found and unregistered. Returns * false if the given callback was not found on the list. - * + * * @see #register */ public boolean unregister(E callback) { @@ -146,13 +147,13 @@ public class RemoteCallbackList<E extends IInterface> { return false; } } - + /** * Disable this callback list. All registered callbacks are unregistered, * and the list is disabled so that future calls to {@link #register} will * fail. This should be used when a Service is stopping, to prevent clients * from registering callbacks after it is stopped. - * + * * @see #register */ public void kill() { @@ -164,7 +165,7 @@ public class RemoteCallbackList<E extends IInterface> { mKilled = true; } } - + /** * Old version of {@link #onCallbackDied(E, Object)} that * does not provide a cookie. @@ -189,7 +190,7 @@ public class RemoteCallbackList<E extends IInterface> { public void onCallbackDied(E callback, Object cookie) { onCallbackDied(callback); } - + /** * Prepare to start making calls to the currently registered callbacks. * This creates a copy of the callback list, which you can retrieve items @@ -198,12 +199,12 @@ public class RemoteCallbackList<E extends IInterface> { * same thread (usually by scheduling with {@link Handler} or * do your own synchronization. You must call {@link #finishBroadcast} * when done. - * + * * <p>A typical loop delivering a broadcast looks like this: - * + * * <pre> * final int N = callbacks.beginBroadcast(); - * for (int i=0; i<N; i++) { + * for (int i=0; i<N; i++) { * try { * callbacks.getBroadcastItem(i).somethingHappened(); * } catch (RemoteException e) { @@ -212,11 +213,11 @@ public class RemoteCallbackList<E extends IInterface> { * } * } * callbacks.finishBroadcast();</pre> - * + * * @return Returns the number of callbacks in the broadcast, to be used * with {@link #getBroadcastItem} to determine the range of indices you * can supply. - * + * * @see #getBroadcastItem * @see #finishBroadcast */ @@ -237,26 +238,26 @@ public class RemoteCallbackList<E extends IInterface> { return i; } } - + /** * Retrieve an item in the active broadcast that was previously started * with {@link #beginBroadcast}. This can <em>only</em> be called after * the broadcast is started, and its data is no longer valid after * calling {@link #finishBroadcast}. - * + * * <p>Note that it is possible for the process of one of the returned * callbacks to go away before you call it, so you will need to catch * {@link RemoteException} when calling on to the returned object. * The callback list itself, however, will take care of unregistering * these objects once it detects that it is no longer valid, so you can * handle such an exception by simply ignoring it. - * + * * @param index Which of the registered callbacks you would like to * retrieve. Ranges from 0 to 1-{@link #beginBroadcast}. - * + * * @return Returns the callback interface that you can call. This will * always be non-null. - * + * * @see #beginBroadcast */ public E getBroadcastItem(int index) { @@ -272,12 +273,12 @@ public class RemoteCallbackList<E extends IInterface> { public Object getBroadcastCookie(int index) { return ((Callback)mActiveBroadcast[index]).mCookie; } - + /** * Clean up the state of a broadcast previously initiated by calling * {@link #beginBroadcast}. This must always be called when you are done * with a broadcast. - * + * * @see #beginBroadcast */ public void finishBroadcast() { diff --git a/core/java/android/preference/RingtonePreference.java b/core/java/android/preference/RingtonePreference.java index 6beb06d..b46f180 100644 --- a/core/java/android/preference/RingtonePreference.java +++ b/core/java/android/preference/RingtonePreference.java @@ -31,8 +31,9 @@ import android.util.Log; * The chosen ringtone's URI will be persisted as a string. * <p> * If the user chooses the "Default" item, the saved string will be one of - * {@link System#DEFAULT_RINGTONE_URI} or - * {@link System#DEFAULT_NOTIFICATION_URI}. If the user chooses the "Silent" + * {@link System#DEFAULT_RINGTONE_URI}, + * {@link System#DEFAULT_NOTIFICATION_URI}, or + * {@link System#DEFAULT_ALARM_ALERT_URI}. If the user chooses the "Silent" * item, the saved string will be an empty string. * * @attr ref android.R.styleable#RingtonePreference_ringtoneType diff --git a/core/java/android/preference/VolumePreference.java b/core/java/android/preference/VolumePreference.java index 20702a1..abdcd93 100644 --- a/core/java/android/preference/VolumePreference.java +++ b/core/java/android/preference/VolumePreference.java @@ -22,6 +22,7 @@ import android.database.ContentObserver; import android.media.Ringtone; import android.media.RingtoneManager; import android.media.AudioManager; +import android.net.Uri; import android.os.Handler; import android.preference.PreferenceManager; import android.provider.Settings; @@ -147,10 +148,16 @@ public class VolumePreference extends SeekBarPreference implements System.getUriFor(System.VOLUME_SETTINGS[mStreamType]), false, mVolumeObserver); - mRingtone = RingtoneManager.getRingtone(mContext, - mStreamType == AudioManager.STREAM_NOTIFICATION - ? Settings.System.DEFAULT_NOTIFICATION_URI - : Settings.System.DEFAULT_RINGTONE_URI); + Uri defaultUri = null; + if (mStreamType == AudioManager.STREAM_RING) { + defaultUri = Settings.System.DEFAULT_RINGTONE_URI; + } else if (mStreamType == AudioManager.STREAM_NOTIFICATION) { + defaultUri = Settings.System.DEFAULT_NOTIFICATION_URI; + } else { + defaultUri = Settings.System.DEFAULT_ALARM_ALERT_URI; + } + + mRingtone = RingtoneManager.getRingtone(mContext, defaultUri); mRingtone.setStreamType(mStreamType); } diff --git a/core/java/android/provider/Calendar.java b/core/java/android/provider/Calendar.java index 4a709f6..3a221e4 100644 --- a/core/java/android/provider/Calendar.java +++ b/core/java/android/provider/Calendar.java @@ -32,6 +32,7 @@ import android.text.format.DateUtils; import android.text.format.Time; import android.util.Config; import android.util.Log; +import android.accounts.Account; import com.android.internal.database.ArrayListCursor; import com.google.android.gdata.client.AndroidGDataClient; import com.google.android.gdata.client.AndroidXmlParserFactory; @@ -157,11 +158,12 @@ public final class Calendar { * @param account the account whose rows should be deleted * @return the count of rows that were deleted */ - public static int deleteCalendarsForAccount(ContentResolver cr, - String account) { + public static int deleteCalendarsForAccount(ContentResolver cr, Account account) { // delete all calendars that match this account - return Calendar.Calendars.delete(cr, Calendar.Calendars._SYNC_ACCOUNT + "=?", - new String[] {account}); + return Calendar.Calendars.delete(cr, + Calendar.Calendars._SYNC_ACCOUNT + "=? AND " + + Calendar.Calendars._SYNC_ACCOUNT_TYPE + "=?", + new String[] {account.mName, account.mType}); } /** diff --git a/core/java/android/provider/Contacts.java b/core/java/android/provider/Contacts.java index 84fe184..0829cfb 100644 --- a/core/java/android/provider/Contacts.java +++ b/core/java/android/provider/Contacts.java @@ -30,6 +30,7 @@ import android.net.Uri; import android.text.TextUtils; import android.util.Log; import android.widget.ImageView; +import android.accounts.Account; import java.io.ByteArrayInputStream; import java.io.InputStream; @@ -75,6 +76,12 @@ public class Contacts { public static final String _SYNC_ACCOUNT = "_sync_account"; /** + * The _SYNC_ACCOUNT_TYPE to which this setting corresponds. This may be null. + * <P>Type: TEXT</P> + */ + public static final String _SYNC_ACCOUNT_TYPE = "_sync_account_type"; + + /** * The key of this setting. * <P>Type: TEXT</P> */ @@ -134,6 +141,7 @@ public class Contacts { selectString = (account == null) ? "_sync_account is null AND key=?" : "_sync_account=? AND key=?"; +// : "_sync_account=? AND _sync_account_type=? AND key=?"; selectArgs = (account == null) ? new String[]{key} : new String[]{account, key}; @@ -158,7 +166,8 @@ public class Contacts { // the account name is, so we're using a global setting for SYNC_EVERYTHING. // Some day when we add multiple accounts to the UI this should honor the account // that was asked for. - //values.put(_SYNC_ACCOUNT, account); + //values.put(_SYNC_ACCOUNT, account.mName); + //values.put(_SYNC_ACCOUNT_TYPE, account.mType); values.put(KEY, key); values.put(VALUE, value); cr.update(Settings.CONTENT_URI, values, null, null); @@ -846,6 +855,12 @@ public class Contacts { public static final String GROUP_SYNC_ACCOUNT = "group_sync_account"; /** + * The account type of the group. + * <P>Type: TEXT</P> + */ + public static final String GROUP_SYNC_ACCOUNT_TYPE = "group_sync_account_type"; + + /** * The row id of the person. * <P>Type: TEXT</P> */ @@ -1022,13 +1037,7 @@ public class Contacts { } } else { if (!TextUtils.isEmpty(label)) { - if (label.toString().equals(MOBILE_EMAIL_TYPE_NAME)) { - display = - context.getString( - com.android.internal.R.string.mobileEmailTypeName); - } else { - display = label; - } + display = label; } } break; @@ -1229,7 +1238,7 @@ public class Contacts { */ public interface OrganizationColumns { /** - * The type of the the phone number. + * The type of the organizations. * <P>Type: INTEGER (one of the constants below)</P> */ public static final String TYPE = "type"; diff --git a/core/java/android/provider/ContactsContract.java b/core/java/android/provider/ContactsContract.java new file mode 100644 index 0000000..f32ab00 --- /dev/null +++ b/core/java/android/provider/ContactsContract.java @@ -0,0 +1,805 @@ +/* + * 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.provider; + +import android.graphics.BitmapFactory; +import android.net.Uri; + +/** + * The contract between the contacts provider and applications. Contains definitions + * for the supported URIs and columns. + * + * @hide + */ +public final class ContactsContract { + /** The authority for the contacts provider */ + public static final String AUTHORITY = "com.android.contacts"; + /** A content:// style uri to the authority for the contacts provider */ + public static final Uri AUTHORITY_URI = Uri.parse("content://" + AUTHORITY); + + public interface AccountsColumns { + /** + * The name of this account data + * <P>Type: TEXT</P> + */ + public static final String NAME = "name"; + /** + * The name of this account data + * <P>Type: TEXT</P> + */ + public static final String TYPE = "type"; + /** + * The name of this account data + * <P>Type: TEXT</P> + */ + public static final String DATA1 = "data1"; + + /** + * The value for this account data + * <P>Type: INTEGER</P> + */ + public static final String DATA2 = "data2"; + + /** + * The value for this account data + * <P>Type: INTEGER</P> + */ + public static final String DATA3 = "data3"; + + /** + * The value for this account data + * <P>Type: INTEGER</P> + */ + public static final String DATA4 = "data4"; + + /** + * The value for this account data + * <P>Type: INTEGER</P> + */ + public static final String DATA5 = "data5"; + } + + /** + * Constants for the aggregates table, which contains a record per group + * of contact representing the same person. + */ + public static final class Accounts implements BaseColumns, AccountsColumns { + /** + * This utility class cannot be instantiated + */ + private Accounts() {} + + /** + * The content:// style URI for this table + */ + public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "accounts"); + + /** + * The MIME type of {@link #CONTENT_URI} providing a directory of + * account data. + */ + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/contacts_account"; + + /** + * The MIME type of a {@link #CONTENT_URI} subdirectory of a account + */ + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/contacts_account"; + } + + public interface AggregatesColumns { + /** + * The display name for the contact. + * <P>Type: TEXT</P> + */ + public static final String DISPLAY_NAME = "display_name"; + + /** + * The number of times a person has been contacted + * <P>Type: INTEGER</P> + */ + public static final String TIMES_CONTACTED = "times_contacted"; + + /** + * The last time a person was contacted. + * <P>Type: INTEGER</P> + */ + public static final String LAST_TIME_CONTACTED = "last_time_contacted"; + + /** + * Is the contact starred? + * <P>Type: INTEGER (boolean)</P> + */ + public static final String STARRED = "starred"; + + /** + * Reference to the row in the data table holding the primary phone number. + * <P>Type: INTEGER REFERENCES data(_id)</P> + */ + public static final String PRIMARY_PHONE_ID = "primary_phone_id"; + + /** + * Reference to the row in the data table holding the primary email address. + * <P>Type: INTEGER REFERENCES data(_id)</P> + */ + public static final String PRIMARY_EMAIL_ID = "primary_email_id"; + + /** + * Reference to the row in the data table holding the photo. + * <P>Type: INTEGER REFERENCES data(_id)</P> + */ + public static final String PHOTO_ID = "photo_id"; + + /** + * Reference to a row containing custom ringtone and send to voicemail information. + * <P>Type: INTEGER REFERENCES data(_id)</P> + */ + public static final String CUSTOM_RINGTONE_ID = "custom_ringtone_id"; + } + + /** + * Constants for the aggregates table, which contains a record per group + * of contact representing the same person. + */ + public static final class Aggregates implements BaseColumns, AggregatesColumns { + /** + * This utility class cannot be instantiated + */ + private Aggregates() {} + + /** + * The content:// style URI for this table + */ + public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "aggregates"); + + /** + * The MIME type of {@link #CONTENT_URI} providing a directory of + * people. + */ + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/person_aggregate"; + + /** + * The MIME type of a {@link #CONTENT_URI} subdirectory of a single + * person. + */ + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/person_aggregate"; + + /** + * A sub-directory of a single contact aggregate that contains all of their + * {@link Data} rows. + */ + public static final class Data implements BaseColumns, DataColumns { + /** + * no public constructor since this is a utility class + */ + private Data() {} + + /** + * The directory twig for this sub-table + */ + public static final String CONTENT_DIRECTORY = "data"; + } + } + + + /** + * Constants for the contacts table, which contains the base contact information. + */ + public static final class Contacts implements BaseColumns { + /** + * This utility class cannot be instantiated + */ + private Contacts() {} + + /** + * A reference to the {@link Accounts#_ID} that this data belongs to. + */ + public static final String ACCOUNTS_ID = "accounts_id"; + + /** + * A reference to the {@link Aggregates#_ID} that this data belongs to. + */ + public static final String AGGREGATE_ID = "aggregate_id"; + + /** + * The content:// style URI for this table + */ + public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "contacts"); + + /** + * The content:// style URL for filtering people by email address. The + * filter argument should be passed as an additional path segment after + * this URI. + * + * @hide + */ + public static final Uri CONTENT_FILTER_EMAIL_URI = Uri.withAppendedPath(CONTENT_URI, "filter_email"); + + /** + * The MIME type of {@link #CONTENT_URI} providing a directory of + * people. + */ + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/person"; + + /** + * The MIME type of a {@link #CONTENT_URI} subdirectory of a single + * person. + */ + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/person"; + + /** + * A string that uniquely identifies this contact to its source, which is referred to + * by the {@link #ACCOUNTS_ID} + */ + public static final String SOURCE_ID = "sourceid"; + + /** + * An integer that is updated whenever this contact or its data changes. + */ + public static final String VERSION = "version"; + + /** + * Set to 1 whenever the version changes + */ + public static final String DIRTY = "dirty"; + + /** + * A sub-directory of a single contact that contains all of their {@link Data} rows. + * To access this directory append + */ + public static final class Data implements BaseColumns, DataColumns { + /** + * no public constructor since this is a utility class + */ + private Data() {} + + /** + * The directory twig for this sub-table + */ + public static final String CONTENT_DIRECTORY = "data"; + } + } + + private interface DataColumns { + /** + * The package name that defines this type of data. + */ + public static final String PACKAGE = "package"; + + /** + * The mime-type of the item represented by this row. + */ + public static final String MIMETYPE = "mimetype"; + + /** + * A reference to the {@link android.provider.ContactsContract.Contacts#_ID} + * that this data belongs to. + */ + public static final String CONTACT_ID = "contact_id"; + + /** + * Whether this is the primary entry of its kind for the contact it belongs to + * <P>Type: INTEGER (if set, non-0 means true)</P> + */ + public static final String IS_PRIMARY = "is_primary"; + + /** + * Whether this is the primary entry of its kind for the aggregate it belongs to. Any data + * record that is "super primary" must also be "primary". + * <P>Type: INTEGER (if set, non-0 means true)</P> + */ + public static final String IS_SUPER_PRIMARY = "is_super_primary"; + + /** Generic data column, the meaning is {@link #MIMETYPE} specific */ + public static final String DATA1 = "data1"; + /** Generic data column, the meaning is {@link #MIMETYPE} specific */ + public static final String DATA2 = "data2"; + /** Generic data column, the meaning is {@link #MIMETYPE} specific */ + public static final String DATA3 = "data3"; + /** Generic data column, the meaning is {@link #MIMETYPE} specific */ + public static final String DATA4 = "data4"; + /** Generic data column, the meaning is {@link #MIMETYPE} specific */ + public static final String DATA5 = "data5"; + /** Generic data column, the meaning is {@link #MIMETYPE} specific */ + public static final String DATA6 = "data6"; + /** Generic data column, the meaning is {@link #MIMETYPE} specific */ + public static final String DATA7 = "data7"; + /** Generic data column, the meaning is {@link #MIMETYPE} specific */ + public static final String DATA8 = "data8"; + /** Generic data column, the meaning is {@link #MIMETYPE} specific */ + public static final String DATA9 = "data9"; + /** Generic data column, the meaning is {@link #MIMETYPE} specific */ + public static final String DATA10 = "data10"; + } + + /** + * Constants for the data table, which contains data points tied to a contact. + * For example, a phone number or email address. Each row in this table contains a type + * definition and some generic columns. Each data type can define the meaning for each of + * the generic columns. + */ + public static final class Data implements BaseColumns, DataColumns { + /** + * This utility class cannot be instantiated + */ + private Data() {} + + /** + * The content:// style URI for this table + */ + public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "data"); + + /** + * The MIME type of {@link #CONTENT_URI} providing a directory of data. + */ + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/data"; + } + + /** + * A table that represents the result of looking up a phone number, for example for caller ID. + * The table joins that data row for the phone number with the contact that owns the number. + * To perform a lookup you must append the number you want to find to {@link #CONTENT_URI}. + */ + public static final class PhoneLookup implements BaseColumns, DataColumns, AggregatesColumns { + /** + * This utility class cannot be instantiated + */ + private PhoneLookup() {} + + /** + * The content:// style URI for this table. Append the phone number you want to lookup + * to this URI and query it to perform a lookup. For example: + * + * {@code + * Uri lookupUri = Uri.withAppendedPath(PhoneLookup.CONTENT_URI, phoneNumber); + * } + */ + public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "phone_lookup"); + } + + /** + * Container for definitions of common data types stored in the {@link Data} table. + */ + public static final class CommonDataKinds { + /** + * The {@link Data#PACKAGE} value for common data that should be shown + * using a default style. + */ + public static final String PACKAGE_COMMON = "common"; + + /** + * Columns common across the specific types. + */ + private interface BaseCommonColumns { + /** + * The package name that defines this type of data. + */ + public static final String PACKAGE = "package"; + + /** + * The mime-type of the item represented by this row. + */ + public static final String MIMETYPE = "mimetype"; + + /** + * A reference to the {@link android.provider.ContactsContract.Contacts#_ID} that this + * data belongs to. + */ + public static final String CONTACT_ID = "contact_id"; + } + + /** + * Columns common across the specific types. + */ + private interface CommonColumns { + /** + * The type of data, for example Home or Work. + * <P>Type: INTEGER</P> + */ + public static final String TYPE = "data1"; + + /** + * The data for the contact method. + * <P>Type: TEXT</P> + */ + public static final String DATA = "data2"; + + /** + * The user defined label for the the contact method. + * <P>Type: TEXT</P> + */ + public static final String LABEL = "data3"; + } + + /** + * Parts of the name. + */ + public static final class StructuredName { + private StructuredName() {} + + /** Mime-type used when storing this in data table. */ + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/name"; + + /** + * The given name for the contact. + * <P>Type: TEXT</P> + */ + public static final String GIVEN_NAME = "data1"; + + /** + * The family name for the contact. + * <P>Type: TEXT</P> + */ + public static final String FAMILY_NAME = "data2"; + + /** + * The contact's honorific prefix, e.g. "Sir" + * <P>Type: TEXT</P> + */ + public static final String PREFIX = "data3"; + + /** + * The contact's middle name + * <P>Type: TEXT</P> + */ + public static final String MIDDLE_NAME = "data4"; + + /** + * The contact's honorific suffix, e.g. "Jr" + */ + public static final String SUFFIX = "data5"; + + /** + * The phonetic version of the given name for the contact. + * <P>Type: TEXT</P> + */ + public static final String PHONETIC_GIVEN_NAME = "data6"; + + /** + * The phonetic version of the additional name for the contact. + * <P>Type: TEXT</P> + */ + public static final String PHONETIC_MIDDLE_NAME = "data7"; + + /** + * The phonetic version of the family name for the contact. + * <P>Type: TEXT</P> + */ + public static final String PHONETIC_FAMILY_NAME = "data8"; + + /** + * The name that should be used to display the contact. + * <P>Type: TEXT</P> + */ + public static final String DISPLAY_NAME = "data9"; + } + + /** + * A nickname. + */ + public static final class Nickname { + private Nickname() {} + + /** Mime-type used when storing this in data table. */ + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/nickname"; + + /** + * The type of data, for example Home or Work. + * <P>Type: INTEGER</P> + */ + public static final String TYPE = "data1"; + + public static final int TYPE_CUSTOM = 1; + public static final int TYPE_DEFAULT = 2; + public static final int TYPE_OTHER_NAME = 3; + public static final int TYPE_MAINDEN_NAME = 4; + public static final int TYPE_SHORT_NAME = 5; + public static final int TYPE_INITIALS = 6; + + /** + * The name itself + */ + public static final String NAME = "data2"; + + /** + * The user provided label, only used if TYPE is {@link #TYPE_CUSTOM}. + * <P>Type: TEXT</P> + */ + public static final String LABEL = "data3"; + } + + /** + * Common data definition for telephone numbers. + */ + public static final class Phone implements BaseCommonColumns, CommonColumns { + private Phone() {} + + /** Mime-type used when storing this in data table. */ + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/phone"; + + public static final int TYPE_CUSTOM = 0; + public static final int TYPE_HOME = 1; + public static final int TYPE_MOBILE = 2; + public static final int TYPE_WORK = 3; + public static final int TYPE_FAX_WORK = 4; + public static final int TYPE_FAX_HOME = 5; + public static final int TYPE_PAGER = 6; + public static final int TYPE_OTHER = 7; + + /** + * The phone number as the user entered it. + * <P>Type: TEXT</P> + */ + public static final String NUMBER = "data2"; + } + + /** + * Common data definition for email addresses. + */ + public static final class Email implements BaseCommonColumns, CommonColumns { + private Email() {} + + /** Mime-type used when storing this in data table. */ + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/email"; + + public static final int TYPE_CUSTOM = 0; + public static final int TYPE_HOME = 1; + public static final int TYPE_WORK = 2; + public static final int TYPE_OTHER = 3; + } + + /** + * Common data definition for postal addresses. + */ + public static final class Postal implements BaseCommonColumns, CommonColumns { + private Postal() {} + + /** Mime-type used when storing this in data table. */ + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/postal-address"; + + public static final int TYPE_CUSTOM = 0; + public static final int TYPE_HOME = 1; + public static final int TYPE_WORK = 2; + public static final int TYPE_OTHER = 3; + } + + /** + * Common data definition for IM addresses. + */ + public static final class Im implements BaseCommonColumns, CommonColumns { + private Im() {} + + /** Mime-type used when storing this in data table. */ + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/im"; + + public static final int TYPE_CUSTOM = 0; + public static final int TYPE_HOME = 1; + public static final int TYPE_WORK = 2; + public static final int TYPE_OTHER = 3; + + public static final String PROTOCOL = "data5"; + + /** + * The predefined IM protocol types. The protocol can either be non-present, one + * of these types, or a free-form string. These cases are encoded in the PROTOCOL + * column as: + * <ul> + * <li>null</li> + * <li>pre:<an integer, one of the protocols below></li> + * <li>custom:<a string></li> + * </ul> + */ + public static final int PROTOCOL_AIM = 0; + public static final int PROTOCOL_MSN = 1; + public static final int PROTOCOL_YAHOO = 2; + public static final int PROTOCOL_SKYPE = 3; + public static final int PROTOCOL_QQ = 4; + public static final int PROTOCOL_GOOGLE_TALK = 5; + public static final int PROTOCOL_ICQ = 6; + public static final int PROTOCOL_JABBER = 7; + + public static String encodePredefinedImProtocol(int protocol) { + return "pre:" + protocol; + } + + public static String encodeCustomImProtocol(String protocolString) { + return "custom:" + protocolString; + } + + public static Object decodeImProtocol(String encodedString) { + if (encodedString == null) { + return null; + } + + if (encodedString.startsWith("pre:")) { + return Integer.parseInt(encodedString.substring(4)); + } + + if (encodedString.startsWith("custom:")) { + return encodedString.substring(7); + } + + throw new IllegalArgumentException( + "the value is not a valid encoded protocol, " + encodedString); + } + } + + /** + * Common data definition for organizations. + */ + public static final class Organization implements BaseCommonColumns { + private Organization() {} + + /** Mime-type used when storing this in data table. */ + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/organization"; + + /** + * The type of data, for example Home or Work. + * <P>Type: INTEGER</P> + */ + public static final String TYPE = "data1"; + + public static final int TYPE_CUSTOM = 0; + public static final int TYPE_HOME = 1; + public static final int TYPE_WORK = 2; + public static final int TYPE_OTHER = 3; + + /** + * The user provided label, only used if TYPE is {@link #TYPE_CUSTOM}. + * <P>Type: TEXT</P> + */ + public static final String LABEL = "data2"; + + /** + * The company as the user entered it. + * <P>Type: TEXT</P> + */ + public static final String COMPANY = "data3"; + + /** + * The position title at this company as the user entered it. + * <P>Type: TEXT</P> + */ + public static final String TITLE = "data4"; + } + + /** + * Photo of the contact. + */ + public static final class Photo implements BaseCommonColumns { + private Photo() {} + + /** Mime-type used when storing this in data table. */ + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/photo"; + + /** + * Thumbnail photo of the contact. This is the raw bytes of an image + * that could be inflated using {@link BitmapFactory}. + * <p> + * Type: BLOB + */ + public static final String PHOTO = "data1"; + } + + /** + * Notes about the contact. + */ + public static final class Note implements BaseCommonColumns { + private Note() {} + + /** Mime-type used when storing this in data table. */ + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/note"; + + /** + * The note text. + * <P>Type: TEXT</P> + */ + public static final String NOTE = "data1"; + } + + /** + * Custom ringtone associated with the contact. + */ + public static final class CustomRingtone implements BaseCommonColumns { + private CustomRingtone() {} + + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/custom_ringtone"; + + /** + * Whether to send the number to voicemail. + * <P>Type: INTEGER (if set, non-0 means true)</P> + */ + public static final String SEND_TO_VOICEMAIL = "data1"; + + /** + * The ringtone uri. + * <P>Type: TEXT</P> + */ + public static final String RINGTONE_URI = "data2"; + } + + /** + * Group Membership. + */ + public static final class GroupMembership implements BaseCommonColumns { + private GroupMembership() {} + + /** Mime-type used when storing this in data table. */ + public static final String CONTENT_ITEM_TYPE = + "vnd.android.cursor.item/group_membership"; + + /** + * The row id of the group that this group membership refers to. Either this or the + * GROUP_SOURCE_ID must be set. If they are both set then they must refer to the same + * group. + * <P>Type: INTEGER</P> + */ + public static final String GROUP_ROW_ID = "data1"; + + /** + * The source id of the group that this membership refers to. Either this or the + * GROUP_ROW_ID must be set. If they are both set then they must refer to the same + * group. + * <P>Type: STRING</P> + */ + public static final String GROUP_SOURCE_ID = "data2"; + } + } + + /** + * Constants for the contact aggregation exceptions table, which contains + * aggregation rules overriding those used by automatic aggregation. + */ + public static final class AggregationExceptions { + /** + * This utility class cannot be instantiated + */ + private AggregationExceptions() {} + + /** + * The content:// style URI for this table + */ + public static final Uri CONTENT_URI = + Uri.withAppendedPath(AUTHORITY_URI, "aggregation_exceptions"); + + /** + * The MIME type of {@link #CONTENT_URI} providing a directory of data. + */ + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/aggregation_exception"; + + /** + * The type of exception: {@link #TYPE_NEVER_MATCH} or {@link #TYPE_ALWAYS_MATCH}. + * + * <P>Type: INTEGER</P> + */ + public static final String TYPE = "type"; + + public static final int TYPE_NEVER_MATCH = 0; + public static final int TYPE_ALWAYS_MATCH = 1; + + /** + * A reference to the {@link android.provider.ContactsContract.Contacts#_ID} of one of + * the contacts that the rule applies to. + */ + public static final String CONTACT_ID1 = "contact_id1"; + + /** + * A reference to the {@link android.provider.ContactsContract.Contacts#_ID} of the other + * contact that the rule applies to. + */ + public static final String CONTACT_ID2 = "contact_id2"; + } +} diff --git a/core/java/android/provider/Downloads.java b/core/java/android/provider/Downloads.java index 4c58e0d..790fe5c 100644 --- a/core/java/android/provider/Downloads.java +++ b/core/java/android/provider/Downloads.java @@ -63,7 +63,7 @@ public final class Downloads implements BaseColumns { * that had initiated a download when that download completes. The * download's content: uri is specified in the intent's data. */ - public static final String DOWNLOAD_COMPLETED_ACTION = + public static final String ACTION_DOWNLOAD_COMPLETED = "android.intent.action.DOWNLOAD_COMPLETED"; /** @@ -76,7 +76,7 @@ public final class Downloads implements BaseColumns { * Note: this is not currently sent for downloads that have completed * successfully. */ - public static final String NOTIFICATION_CLICKED_ACTION = + public static final String ACTION_NOTIFICATION_CLICKED = "android.intent.action.DOWNLOAD_NOTIFICATION_CLICKED"; /** @@ -84,14 +84,14 @@ public final class Downloads implements BaseColumns { * <P>Type: TEXT</P> * <P>Owner can Init/Read</P> */ - public static final String URI = "uri"; + public static final String COLUMN_URI = "uri"; /** * The name of the column containing application-specific data. * <P>Type: TEXT</P> * <P>Owner can Init/Read/Write</P> */ - public static final String APP_DATA = "entity"; + public static final String COLUMN_APP_DATA = "entity"; /** * The name of the column containing the flags that indicates whether @@ -104,7 +104,7 @@ public final class Downloads implements BaseColumns { * <P>Type: BOOLEAN</P> * <P>Owner can Init</P> */ - public static final String NO_INTEGRITY = "no_integrity"; + public static final String COLUMN_NO_INTEGRITY = "no_integrity"; /** * The name of the column containing the filename that the initiating @@ -113,7 +113,7 @@ public final class Downloads implements BaseColumns { * <P>Type: TEXT</P> * <P>Owner can Init</P> */ - public static final String FILENAME_HINT = "hint"; + public static final String COLUMN_FILE_NAME_HINT = "hint"; /** * The name of the column containing the filename where the downloaded data @@ -128,7 +128,7 @@ public final class Downloads implements BaseColumns { * <P>Type: TEXT</P> * <P>Owner can Init/Read</P> */ - public static final String MIMETYPE = "mimetype"; + public static final String COLUMN_MIME_TYPE = "mimetype"; /** * The name of the column containing the flag that controls the destination @@ -136,7 +136,7 @@ public final class Downloads implements BaseColumns { * <P>Type: INTEGER</P> * <P>Owner can Init</P> */ - public static final String DESTINATION = "destination"; + public static final String COLUMN_DESTINATION = "destination"; /** * The name of the column containing the flags that controls whether the @@ -145,7 +145,7 @@ public final class Downloads implements BaseColumns { * <P>Type: INTEGER</P> * <P>Owner can Init/Read/Write</P> */ - public static final String VISIBILITY = "visibility"; + public static final String COLUMN_VISIBILITY = "visibility"; /** * The name of the column containing the current control state of the download. @@ -154,7 +154,7 @@ public final class Downloads implements BaseColumns { * <P>Type: INTEGER</P> * <P>Owner can Read</P> */ - public static final String CONTROL = "control"; + public static final String COLUMN_CONTROL = "control"; /** * The name of the column containing the current status of the download. @@ -163,7 +163,7 @@ public final class Downloads implements BaseColumns { * <P>Type: INTEGER</P> * <P>Owner can Read</P> */ - public static final String STATUS = "status"; + public static final String COLUMN_STATUS = "status"; /** * The name of the column containing the date at which some interesting @@ -172,7 +172,7 @@ public final class Downloads implements BaseColumns { * <P>Type: BIGINT</P> * <P>Owner can Read</P> */ - public static final String LAST_MODIFICATION = "lastmod"; + public static final String COLUMN_LAST_MODIFICATION = "lastmod"; /** * The name of the column containing the package name of the application @@ -181,7 +181,7 @@ public final class Downloads implements BaseColumns { * <P>Type: TEXT</P> * <P>Owner can Init/Read</P> */ - public static final String NOTIFICATION_PACKAGE = "notificationpackage"; + public static final String COLUMN_NOTIFICATION_PACKAGE = "notificationpackage"; /** * The name of the column containing the component name of the class that @@ -191,7 +191,7 @@ public final class Downloads implements BaseColumns { * <P>Type: TEXT</P> * <P>Owner can Init/Read</P> */ - public static final String NOTIFICATION_CLASS = "notificationclass"; + public static final String COLUMN_NOTIFICATION_CLASS = "notificationclass"; /** * If extras are specified when requesting a download they will be provided in the intent that @@ -199,7 +199,7 @@ public final class Downloads implements BaseColumns { * <P>Type: TEXT</P> * <P>Owner can Init</P> */ - public static final String NOTIFICATION_EXTRAS = "notificationextras"; + public static final String COLUMN_NOTIFICATION_EXTRAS = "notificationextras"; /** * The name of the column contain the values of the cookie to be used for @@ -208,7 +208,7 @@ public final class Downloads implements BaseColumns { * <P>Type: TEXT</P> * <P>Owner can Init</P> */ - public static final String COOKIE_DATA = "cookiedata"; + public static final String COLUMN_COOKIE_DATA = "cookiedata"; /** * The name of the column containing the user agent that the initiating @@ -216,7 +216,7 @@ public final class Downloads implements BaseColumns { * <P>Type: TEXT</P> * <P>Owner can Init</P> */ - public static final String USER_AGENT = "useragent"; + public static final String COLUMN_USER_AGENT = "useragent"; /** * The name of the column containing the referer (sic) that the initiating @@ -224,7 +224,7 @@ public final class Downloads implements BaseColumns { * <P>Type: TEXT</P> * <P>Owner can Init</P> */ - public static final String REFERER = "referer"; + public static final String COLUMN_REFERER = "referer"; /** * The name of the column containing the total size of the file being @@ -232,7 +232,7 @@ public final class Downloads implements BaseColumns { * <P>Type: INTEGER</P> * <P>Owner can Read</P> */ - public static final String TOTAL_BYTES = "total_bytes"; + public static final String COLUMN_TOTAL_BYTES = "total_bytes"; /** * The name of the column containing the size of the part of the file that @@ -240,7 +240,7 @@ public final class Downloads implements BaseColumns { * <P>Type: INTEGER</P> * <P>Owner can Read</P> */ - public static final String CURRENT_BYTES = "current_bytes"; + public static final String COLUMN_CURRENT_BYTES = "current_bytes"; /** * The name of the column where the initiating application can provide the @@ -252,7 +252,7 @@ public final class Downloads implements BaseColumns { * <P>Type: INTEGER</P> * <P>Owner can Init</P> */ - public static final String OTHER_UID = "otheruid"; + public static final String COLUMN_OTHER_UID = "otheruid"; /** * The name of the column where the initiating application can provided the @@ -261,7 +261,7 @@ public final class Downloads implements BaseColumns { * <P>Type: TEXT</P> * <P>Owner can Init/Read/Write</P> */ - public static final String TITLE = "title"; + public static final String COLUMN_TITLE = "title"; /** * The name of the column where the initiating application can provide the @@ -270,7 +270,7 @@ public final class Downloads implements BaseColumns { * <P>Type: TEXT</P> * <P>Owner can Init/Read/Write</P> */ - public static final String DESCRIPTION = "description"; + public static final String COLUMN_DESCRIPTION = "description"; /* * Lists the destinations that an application can specify for a download. diff --git a/core/java/android/provider/Im.java b/core/java/android/provider/Im.java index 19ad158..c36f508 100644 --- a/core/java/android/provider/Im.java +++ b/core/java/android/provider/Im.java @@ -871,14 +871,22 @@ public class Im { } /** - * The common columns for both one-to-one chat messages or group chat messages. + * The common columns for messages table */ - public interface BaseMessageColumns { + public interface MessageColumns { /** - * The user this message belongs to - * <P>Type: TEXT</P> + * The thread_id column stores the contact id of the contact the message belongs to. + * For groupchat messages, the thread_id stores the group id, which is the contact id + * of the temporary group contact created for the groupchat. So there should be no + * collision between groupchat message thread id and regular message thread id. */ - String CONTACT = "contact"; + String THREAD_ID = "thread_id"; + + /** + * The nickname. This is used for groupchat messages to indicate the participant's + * nickname. For non groupchat messages, this field should be left empty. + */ + String NICKNAME = "nickname"; /** * The body @@ -917,68 +925,193 @@ public class Im { * <P>Type: STRING</P> */ String PACKET_ID = "packet_id"; - } - - /** - * Columns from the Messages table. - */ - public interface MessagesColumns extends BaseMessageColumns{ - /** - * The provider id - * <P> Type: INTEGER </P> - */ - String PROVIDER = "provider"; /** - * The account id - * <P> Type: INTEGER </P> + * Is groupchat message or not + * <P>Type: INTEGER</P> */ - String ACCOUNT = "account"; + String IS_GROUP_CHAT = "is_muc"; } /** * This table contains messages. */ - public static final class Messages implements BaseColumns, MessagesColumns { + public static final class Messages implements BaseColumns, MessageColumns { /** * no public constructor since this is a utility class */ private Messages() {} /** - * Gets the Uri to query messages by contact. + * Gets the Uri to query messages by thread id. + * + * @param threadId the thread id of the message. + * @return the Uri + */ + public static final Uri getContentUriByThreadId(long threadId) { + Uri.Builder builder = CONTENT_URI_MESSAGES_BY_THREAD_ID.buildUpon(); + ContentUris.appendId(builder, threadId); + return builder.build(); + } + + /** + * @deprecated + * + * Gets the Uri to query messages by account and contact. * - * @param providerId the provider id of the contact. * @param accountId the account id of the contact. * @param username the user name of the contact. * @return the Uri */ - public static final Uri getContentUriByContact(long providerId, - long accountId, String username) { - Uri.Builder builder = CONTENT_URI_MESSAGES_BY.buildUpon(); + public static final Uri getContentUriByContact(long accountId, String username) { + Uri.Builder builder = CONTENT_URI_MESSAGES_BY_ACCOUNT_AND_CONTACT.buildUpon(); + ContentUris.appendId(builder, accountId); + builder.appendPath(username); + return builder.build(); + } + + /** + * Gets the Uri to query messages by provider. + * + * @param providerId the service provider id. + * @return the Uri + */ + public static final Uri getContentUriByProvider(long providerId) { + Uri.Builder builder = CONTENT_URI_MESSAGES_BY_PROVIDER.buildUpon(); ContentUris.appendId(builder, providerId); + return builder.build(); + } + + /** + * Gets the Uri to query off the record messages by account. + * + * @param accountId the account id. + * @return the Uri + */ + public static final Uri getContentUriByAccount(long accountId) { + Uri.Builder builder = CONTENT_URI_BY_ACCOUNT.buildUpon(); + ContentUris.appendId(builder, accountId); + return builder.build(); + } + + /** + * Gets the Uri to query off the record messages by thread id. + * + * @param threadId the thread id of the message. + * @return the Uri + */ + public static final Uri getOtrMessagesContentUriByThreadId(long threadId) { + Uri.Builder builder = OTR_MESSAGES_CONTENT_URI_BY_THREAD_ID.buildUpon(); + ContentUris.appendId(builder, threadId); + return builder.build(); + } + + /** + * @deprecated + * + * Gets the Uri to query off the record messages by account and contact. + * + * @param accountId the account id of the contact. + * @param username the user name of the contact. + * @return the Uri + */ + public static final Uri getOtrMessagesContentUriByContact(long accountId, String username) { + Uri.Builder builder = OTR_MESSAGES_CONTENT_URI_BY_ACCOUNT_AND_CONTACT.buildUpon(); ContentUris.appendId(builder, accountId); builder.appendPath(username); return builder.build(); } /** + * Gets the Uri to query off the record messages by provider. + * + * @param providerId the service provider id. + * @return the Uri + */ + public static final Uri getOtrMessagesContentUriByProvider(long providerId) { + Uri.Builder builder = OTR_MESSAGES_CONTENT_URI_BY_PROVIDER.buildUpon(); + ContentUris.appendId(builder, providerId); + return builder.build(); + } + + /** + * Gets the Uri to query off the record messages by account. + * + * @param accountId the account id. + * @return the Uri + */ + public static final Uri getOtrMessagesContentUriByAccount(long accountId) { + Uri.Builder builder = OTR_MESSAGES_CONTENT_URI_BY_ACCOUNT.buildUpon(); + ContentUris.appendId(builder, accountId); + return builder.build(); + } + + /** * The content:// style URL for this table */ public static final Uri CONTENT_URI = - Uri.parse("content://im/messages"); + Uri.parse("content://im/messages"); + + /** + * The content:// style URL for messages by thread id + */ + public static final Uri CONTENT_URI_MESSAGES_BY_THREAD_ID = + Uri.parse("content://im/messagesByThreadId"); + + /** + * The content:// style URL for messages by account and contact + */ + public static final Uri CONTENT_URI_MESSAGES_BY_ACCOUNT_AND_CONTACT = + Uri.parse("content://im/messagesByAcctAndContact"); + + /** + * The content:// style URL for messages by provider + */ + public static final Uri CONTENT_URI_MESSAGES_BY_PROVIDER = + Uri.parse("content://im/messagesByProvider"); + + /** + * The content:// style URL for messages by account + */ + public static final Uri CONTENT_URI_BY_ACCOUNT = + Uri.parse("content://im/messagesByAccount"); + + /** + * The content:// style url for off the record messages + */ + public static final Uri OTR_MESSAGES_CONTENT_URI = + Uri.parse("content://im/otrMessages"); /** - * The content:// style URL for messages by provider and account + * The content:// style url for off the record messages by thread id */ - public static final Uri CONTENT_URI_MESSAGES_BY = - Uri.parse("content://im/messagesBy"); + public static final Uri OTR_MESSAGES_CONTENT_URI_BY_THREAD_ID = + Uri.parse("content://im/otrMessagesByThreadId"); + + /** + * The content:// style url for off the record messages by account and contact + */ + public static final Uri OTR_MESSAGES_CONTENT_URI_BY_ACCOUNT_AND_CONTACT = + Uri.parse("content://im/otrMessagesByAcctAndContact"); + + /** + * The content:// style URL for off the record messages by provider + */ + public static final Uri OTR_MESSAGES_CONTENT_URI_BY_PROVIDER = + Uri.parse("content://im/otrMessagesByProvider"); + + /** + * The content:// style URL for off the record messages by account + */ + public static final Uri OTR_MESSAGES_CONTENT_URI_BY_ACCOUNT = + Uri.parse("content://im/otrMessagesByAccount"); /** * The MIME type of {@link #CONTENT_URI} providing a directory of * people. */ - public static final String CONTENT_TYPE = "vnd.android.cursor.dir/im-messages"; + public static final String CONTENT_TYPE = + "vnd.android.cursor.dir/im-messages"; /** * The MIME type of a {@link #CONTENT_URI} subdirectory of a single @@ -992,6 +1125,11 @@ public class Im { */ public static final String DEFAULT_SORT_ORDER = "date ASC"; + /** + * The "contact" column. This is not a real column in the messages table, but a + * temoprary column created when querying for messages (joined with the contacts table) + */ + public static final String CONTACT = "contact"; } /** @@ -1119,67 +1257,6 @@ public class Im { } /** - * Columns from the GroupMessages table - */ - public interface GroupMessageColumns extends BaseMessageColumns { - /** - * The group this message belongs to - * <p>Type: TEXT</p> - */ - String GROUP = "groupId"; - } - - /** - * This table contains group messages. - */ - public final static class GroupMessages implements BaseColumns, - GroupMessageColumns { - private GroupMessages() {} - - /** - * Gets the Uri to query group messages by group. - * - * @param groupId the group id. - * @return the Uri - */ - public static final Uri getContentUriByGroup(long groupId) { - Uri.Builder builder = CONTENT_URI_GROUP_MESSAGES_BY.buildUpon(); - ContentUris.appendId(builder, groupId); - return builder.build(); - } - - /** - * The content:// style URL for this table - */ - public static final Uri CONTENT_URI = - Uri.parse("content://im/groupMessages"); - - /** - * The content:// style URL for group messages by provider and account - */ - public static final Uri CONTENT_URI_GROUP_MESSAGES_BY = - Uri.parse("content://im/groupMessagesBy"); - - /** - * The MIME type of {@link #CONTENT_URI} providing a directory of - * group messages. - */ - public static final String CONTENT_TYPE = "vnd.android.cursor.dir/im-groupMessages"; - - /** - * The MIME type of a {@link #CONTENT_URI} subdirectory of a single - * group message. - */ - public static final String CONTENT_ITEM_TYPE = - "vnd.android.cursor.item/im-groupMessages"; - - /** - * The default sort order for this table - */ - public static final String DEFAULT_SORT_ORDER = "date ASC"; - } - - /** * Columns from the Avatars table */ public interface AvatarsColumns { diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 42a5075..a7ba3a2 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -1174,6 +1174,22 @@ public final class Settings { public static final Uri DEFAULT_NOTIFICATION_URI = getUriFor(NOTIFICATION_SOUND); /** + * Persistent store for the system-wide default alarm alert. + * + * @see #RINGTONE + * @see #DEFAULT_ALARM_ALERT_URI + */ + public static final String ALARM_ALERT = "alarm_alert"; + + /** + * A {@link Uri} that will point to the current default alarm alert at + * any given time. + * + * @see #DEFAULT_ALARM_ALERT_URI + */ + public static final Uri DEFAULT_ALARM_ALERT_URI = getUriFor(ALARM_ALERT); + + /** * Setting to enable Auto Replace (AutoText) in text editors. 1 = On, 0 = Off */ public static final String TEXT_AUTO_REPLACE = "auto_replace"; @@ -2582,6 +2598,32 @@ public final class Settings { "gtalk_nosync_heartbeat_ping_interval_ms"; /** + * The maximum heartbeat interval used while on the WIFI network. + */ + public static final String GTALK_SERVICE_WIFI_MAX_HEARTBEAT_INTERVAL_MS = + "gtalk_wifi_max_heartbeat_ping_interval_ms"; + + /** + * The minimum interval for how frequently we send heartbeat pings to the GTalk server. + */ + public static final String GTALK_SERVICE_MIN_HEARTBEAT_INTERVAL_MS = + "gtalk_min_heartbeat_ping_interval_ms"; + + /** + * The scale down factor used by adaptive heartbeat logic (to scale down the heartbeat + * interval) when the previous interval fails to get a response from the server. + */ + public static final String GTALK_SERVICE_ADAPTIVE_HEARTBEAT_SCALER = + "gtalk_adaptive_heartbeat_scaler"; + + /** + * The trigger for adaptively scaling down the heartbeat interval. This is the number of + * consecutive times we failed to get a server response for sending the heartbeat ping. + */ + public static final String GTALK_SERVICE_ADAPTIVE_HEARTBEAT_TRIGGER = + "gtalk_adaptive_heartbeat_trigger"; + + /** * How long we wait to receive a heartbeat ping acknowledgement (or another packet) * from the GTalk server, before deeming the connection dead. */ diff --git a/core/java/android/provider/SocialContract.java b/core/java/android/provider/SocialContract.java new file mode 100644 index 0000000..d4a9c48 --- /dev/null +++ b/core/java/android/provider/SocialContract.java @@ -0,0 +1,174 @@ +/* + * 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.provider; + +import android.graphics.BitmapFactory; +import android.net.Uri; + +/** + * The contract between the social provider and applications. Contains + * definitions for the supported URIs and columns. + * + * @hide + */ +public class SocialContract { + /** The authority for the social provider */ + public static final String AUTHORITY = "com.android.social"; + + /** A content:// style uri to the authority for the contacts provider */ + public static final Uri AUTHORITY_URI = Uri.parse("content://" + AUTHORITY); + + private interface ActivitiesColumns { + /** + * The package name that owns this social activity. + * <p> + * Type: TEXT + */ + public static final String PACKAGE = "package"; + + /** + * The mime-type of this social activity. + * <p> + * Type: TEXT + */ + public static final String MIMETYPE = "mimetype"; + + /** + * Internal raw identifier for this social activity. This field is + * analogous to the <code>atom:id</code> element defined in RFC 4287. + * <p> + * Type: TEXT + */ + public static final String RAW_ID = "raw_id"; + + /** + * Reference to another {@link Activities#RAW_ID} that this social activity + * is replying to. This field is analogous to the + * <code>thr:in-reply-to</code> element defined in RFC 4685. + * <p> + * Type: TEXT + */ + public static final String IN_REPLY_TO = "in_reply_to"; + + /** + * Reference to the {@link android.provider.ContactsContract.Contacts#_ID} that authored + * this social activity. This field is analogous to the <code>atom:author</code> + * element defined in RFC 4287. + * <p> + * Type: INTEGER + */ + public static final String AUTHOR_CONTACT_ID = "author_contact_id"; + + /** + * Optional reference to the {@link android.provider.ContactsContract.Contacts#_ID} this + * social activity is targeted towards. If more than one direct target, this field may + * be left undefined. This field is analogous to the + * <code>activity:target</code> element defined in the Atom Activity + * Extensions Internet-Draft. + * <p> + * Type: INTEGER + */ + public static final String TARGET_CONTACT_ID = "target_contact_id"; + + /** + * Timestamp when this social activity was published, in a + * {@link System#currentTimeMillis()} time base. This field is analogous + * to the <code>atom:published</code> element defined in RFC 4287. + * <p> + * Type: INTEGER + */ + public static final String PUBLISHED = "published"; + + /** + * Timestamp when the original social activity in a thread was + * published. For activities that have an in-reply-to field specified, the + * content provider will automatically populate this field with the + * timestamp of the original activity. + * <p> + * This field is useful for sorting order of activities that keeps together all + * messages in each thread. + * <p> + * Type: INTEGER + */ + public static final String THREAD_PUBLISHED = "thread_published"; + + /** + * Title of this social activity. This field is analogous to the + * <code>atom:title</code> element defined in RFC 4287. + * <p> + * Type: TEXT + */ + public static final String TITLE = "title"; + + /** + * Summary of this social activity. This field is analogous to the + * <code>atom:summary</code> element defined in RFC 4287. + * <p> + * Type: TEXT + */ + public static final String SUMMARY = "summary"; + + /** + * A URI associated this social activity. This field is analogous to the + * <code>atom:link rel="alternate"</code> element defined in RFC 4287. + * <p> + * Type: TEXT + */ + public static final String LINK = "link"; + + /** + * Optional thumbnail specific to this social activity. This is the raw + * bytes of an image that could be inflated using {@link BitmapFactory}. + * <p> + * Type: BLOB + */ + public static final String THUMBNAIL = "thumbnail"; + } + + public static final class Activities implements BaseColumns, ActivitiesColumns { + /** + * This utility class cannot be instantiated + */ + private Activities() { + } + + /** + * The content:// style URI for this table + */ + public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "activities"); + + /** + * The content:// style URI for this table filtered to the set of social activities + * authored by a specific {@link android.provider.ContactsContract.Contacts#_ID}. + */ + public static final Uri CONTENT_AUTHORED_BY_URI = + Uri.withAppendedPath(CONTENT_URI, "authored_by"); + + /** + * The MIME type of {@link #CONTENT_URI} providing a directory of social + * activities. + */ + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/activity"; + + /** + * The MIME type of a {@link #CONTENT_URI} subdirectory of a single + * social activity. + */ + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/activity"; + } + +} diff --git a/core/java/android/provider/SubscribedFeeds.java b/core/java/android/provider/SubscribedFeeds.java index 4d430d5..f94b442 100644 --- a/core/java/android/provider/SubscribedFeeds.java +++ b/core/java/android/provider/SubscribedFeeds.java @@ -20,6 +20,7 @@ import android.content.ContentResolver; import android.content.ContentValues; import android.database.Cursor; import android.net.Uri; +import android.accounts.Account; /** * The SubscribedFeeds provider stores all information about subscribed feeds. @@ -99,7 +100,7 @@ public class SubscribedFeeds { /** * The default sort order for this table */ - public static final String DEFAULT_SORT_ORDER = "_SYNC_ACCOUNT ASC"; + public static final String DEFAULT_SORT_ORDER = "_SYNC_ACCOUNT_TYPE, _SYNC_ACCOUNT ASC"; } /** @@ -114,38 +115,36 @@ public class SubscribedFeeds { * @return the Uri of the feed that was added */ public static Uri addFeed(ContentResolver resolver, - String feed, String account, + String feed, Account account, String authority, String service) { ContentValues values = new ContentValues(); values.put(SubscribedFeeds.Feeds.FEED, feed); - values.put(SubscribedFeeds.Feeds._SYNC_ACCOUNT, account); + values.put(SubscribedFeeds.Feeds._SYNC_ACCOUNT, account.mName); + values.put(SubscribedFeeds.Feeds._SYNC_ACCOUNT_TYPE, account.mType); values.put(SubscribedFeeds.Feeds.AUTHORITY, authority); values.put(SubscribedFeeds.Feeds.SERVICE, service); return resolver.insert(SubscribedFeeds.Feeds.CONTENT_URI, values); } public static int deleteFeed(ContentResolver resolver, - String feed, String account, String authority) { + String feed, Account account, String authority) { StringBuilder where = new StringBuilder(); where.append(SubscribedFeeds.Feeds._SYNC_ACCOUNT + "=?"); + where.append(" AND " + SubscribedFeeds.Feeds._SYNC_ACCOUNT_TYPE + "=?"); where.append(" AND " + SubscribedFeeds.Feeds.FEED + "=?"); where.append(" AND " + SubscribedFeeds.Feeds.AUTHORITY + "=?"); return resolver.delete(SubscribedFeeds.Feeds.CONTENT_URI, - where.toString(), new String[] {account, feed, authority}); + where.toString(), new String[] {account.mName, account.mType, feed, authority}); } public static int deleteFeeds(ContentResolver resolver, - String account, String authority) { + Account account, String authority) { StringBuilder where = new StringBuilder(); where.append(SubscribedFeeds.Feeds._SYNC_ACCOUNT + "=?"); + where.append(" AND " + SubscribedFeeds.Feeds._SYNC_ACCOUNT_TYPE + "=?"); where.append(" AND " + SubscribedFeeds.Feeds.AUTHORITY + "=?"); return resolver.delete(SubscribedFeeds.Feeds.CONTENT_URI, - where.toString(), new String[] {account, authority}); - } - - public static String gtalkServiceRoutingInfoFromAccountAndResource( - String account, String res) { - return Uri.parse("gtalk://" + account + "/" + res).toString(); + where.toString(), new String[] {account.mName, account.mType, authority}); } /** @@ -157,6 +156,12 @@ public class SubscribedFeeds { * <P>Type: TEXT</P> */ public static final String _SYNC_ACCOUNT = SyncConstValue._SYNC_ACCOUNT; + + /** + * The account type. + * <P>Type: TEXT</P> + */ + public static final String _SYNC_ACCOUNT_TYPE = SyncConstValue._SYNC_ACCOUNT_TYPE; } /** @@ -199,6 +204,6 @@ public class SubscribedFeeds { /** * The default sort order for this table */ - public static final String DEFAULT_SORT_ORDER = "_SYNC_ACCOUNT ASC"; + public static final String DEFAULT_SORT_ORDER = "_SYNC_ACCOUNT_TYPE, _SYNC_ACCOUNT ASC"; } } diff --git a/core/java/android/provider/Sync.java b/core/java/android/provider/Sync.java new file mode 100644 index 0000000..c9bde0e --- /dev/null +++ b/core/java/android/provider/Sync.java @@ -0,0 +1,649 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.provider; + +import android.content.ContentQueryMap; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.database.Cursor; +import android.net.Uri; +import android.os.Handler; +import android.accounts.Account; +import android.text.TextUtils; + +import java.util.Map; + +/** + * The Sync provider stores information used in managing the syncing of the device, + * including the history and pending syncs. + * + * @hide + */ +public final class Sync { + // utility class + private Sync() {} + + /** + * The content url for this provider. + */ + public static final Uri CONTENT_URI = Uri.parse("content://sync"); + + /** + * Columns from the stats table. + */ + public interface StatsColumns { + /** + * The sync account. + * <P>Type: TEXT</P> + */ + public static final String ACCOUNT = "account"; + + /** + * The sync account type. + * <P>Type: TEXT</P> + */ + public static final String ACCOUNT_TYPE = "account_type"; + + /** + * The content authority (contacts, calendar, etc.). + * <P>Type: TEXT</P> + */ + public static final String AUTHORITY = "authority"; + } + + /** + * Provides constants and utility methods to access and use the stats table. + */ + public static final class Stats implements BaseColumns, StatsColumns { + + // utility class + private Stats() {} + + /** + * The content url for this table. + */ + public static final Uri CONTENT_URI = + Uri.parse("content://sync/stats"); + + /** Projection for the _id column in the stats table. */ + public static final String[] SYNC_STATS_PROJECTION = {_ID}; + } + + /** + * Columns from the history table. + */ + public interface HistoryColumns { + /** + * The ID of the stats row corresponding to this event. + * <P>Type: INTEGER</P> + */ + public static final String STATS_ID = "stats_id"; + + /** + * The source of the sync event (LOCAL, POLL, USER, SERVER). + * <P>Type: INTEGER</P> + */ + public static final String SOURCE = "source"; + + /** + * The type of sync event (START, STOP). + * <P>Type: INTEGER</P> + */ + public static final String EVENT = "event"; + + /** + * The time of the event. + * <P>Type: INTEGER</P> + */ + public static final String EVENT_TIME = "eventTime"; + + /** + * How long this event took. This is only valid if the EVENT is EVENT_STOP. + * <P>Type: INTEGER</P> + */ + public static final String ELAPSED_TIME = "elapsedTime"; + + /** + * Any additional message associated with this event. + * <P>Type: TEXT</P> + */ + public static final String MESG = "mesg"; + + /** + * How much activity was performed sending data to the server. This is sync adapter + * specific, but usually is something like how many record update/insert/delete attempts + * were carried out. This is only valid if the EVENT is EVENT_STOP. + * <P>Type: INTEGER</P> + */ + public static final String UPSTREAM_ACTIVITY = "upstreamActivity"; + + /** + * How much activity was performed while receiving data from the server. + * This is sync adapter specific, but usually is something like how many + * records were received from the server. This is only valid if the + * EVENT is EVENT_STOP. + * <P>Type: INTEGER</P> + */ + public static final String DOWNSTREAM_ACTIVITY = "downstreamActivity"; + } + + /** + * Columns from the history table. + */ + public interface StatusColumns { + /** + * How many syncs were completed for this account and authority. + * <P>Type: INTEGER</P> + */ + public static final String NUM_SYNCS = "numSyncs"; + + /** + * How long all the events for this account and authority took. + * <P>Type: INTEGER</P> + */ + public static final String TOTAL_ELAPSED_TIME = "totalElapsedTime"; + + /** + * The number of syncs with SOURCE_POLL. + * <P>Type: INTEGER</P> + */ + public static final String NUM_SOURCE_POLL = "numSourcePoll"; + + /** + * The number of syncs with SOURCE_SERVER. + * <P>Type: INTEGER</P> + */ + public static final String NUM_SOURCE_SERVER = "numSourceServer"; + + /** + * The number of syncs with SOURCE_LOCAL. + * <P>Type: INTEGER</P> + */ + public static final String NUM_SOURCE_LOCAL = "numSourceLocal"; + + /** + * The number of syncs with SOURCE_USER. + * <P>Type: INTEGER</P> + */ + public static final String NUM_SOURCE_USER = "numSourceUser"; + + /** + * The time in ms that the last successful sync ended. Will be null if + * there are no successful syncs. A successful sync is defined as one having + * MESG=MESG_SUCCESS. + * <P>Type: INTEGER</P> + */ + public static final String LAST_SUCCESS_TIME = "lastSuccessTime"; + + /** + * The SOURCE of the last successful sync. Will be null if + * there are no successful syncs. A successful sync is defined + * as one having MESG=MESG_SUCCESS. + * <P>Type: INTEGER</P> + */ + public static final String LAST_SUCCESS_SOURCE = "lastSuccessSource"; + + /** + * The end time in ms of the last sync that failed since the last successful sync. + * Will be null if there are no syncs or if the last one succeeded. A failed + * sync is defined as one where MESG isn't MESG_SUCCESS or MESG_CANCELED. + * <P>Type: INTEGER</P> + */ + public static final String LAST_FAILURE_TIME = "lastFailureTime"; + + /** + * The SOURCE of the last sync that failed since the last successful sync. + * Will be null if there are no syncs or if the last one succeeded. A failed + * sync is defined as one where MESG isn't MESG_SUCCESS or MESG_CANCELED. + * <P>Type: INTEGER</P> + */ + public static final String LAST_FAILURE_SOURCE = "lastFailureSource"; + + /** + * The MESG of the last sync that failed since the last successful sync. + * Will be null if there are no syncs or if the last one succeeded. A failed + * sync is defined as one where MESG isn't MESG_SUCCESS or MESG_CANCELED. + * <P>Type: STRING</P> + */ + public static final String LAST_FAILURE_MESG = "lastFailureMesg"; + + /** + * Is set to 1 if a sync is pending, 0 if not. + * <P>Type: INTEGER</P> + */ + public static final String PENDING = "pending"; + } + + /** + * Provides constants and utility methods to access and use the history + * table. + */ + public static class History implements BaseColumns, + StatsColumns, + HistoryColumns { + + /** + * The content url for this table. + */ + public static final Uri CONTENT_URI = + Uri.parse("content://sync/history"); + + /** Enum value for a sync start event. */ + public static final int EVENT_START = 0; + + /** Enum value for a sync stop event. */ + public static final int EVENT_STOP = 1; + + // TODO: i18n -- grab these out of resources. + /** String names for the sync event types. */ + public static final String[] EVENTS = { "START", "STOP" }; + + /** Enum value for a server-initiated sync. */ + public static final int SOURCE_SERVER = 0; + + /** Enum value for a local-initiated sync. */ + public static final int SOURCE_LOCAL = 1; + /** + * Enum value for a poll-based sync (e.g., upon connection to + * network) + */ + public static final int SOURCE_POLL = 2; + + /** Enum value for a user-initiated sync. */ + public static final int SOURCE_USER = 3; + + // TODO: i18n -- grab these out of resources. + /** String names for the sync source types. */ + public static final String[] SOURCES = { "SERVER", + "LOCAL", + "POLL", + "USER" }; + + // Error types + public static final int ERROR_SYNC_ALREADY_IN_PROGRESS = 1; + public static final int ERROR_AUTHENTICATION = 2; + public static final int ERROR_IO = 3; + public static final int ERROR_PARSE = 4; + public static final int ERROR_CONFLICT = 5; + public static final int ERROR_TOO_MANY_DELETIONS = 6; + public static final int ERROR_TOO_MANY_RETRIES = 7; + public static final int ERROR_INTERNAL = 8; + + // The MESG column will contain one of these or one of the Error types. + public static final String MESG_SUCCESS = "success"; + public static final String MESG_CANCELED = "canceled"; + + private static final String FINISHED_SINCE_WHERE_CLAUSE = EVENT + "=" + EVENT_STOP + + " AND " + EVENT_TIME + ">? AND " + ACCOUNT + "=? AND " + ACCOUNT_TYPE + "=?" + + " AND " + AUTHORITY + "=?"; + + public static String mesgToString(String mesg) { + if (MESG_SUCCESS.equals(mesg)) return mesg; + if (MESG_CANCELED.equals(mesg)) return mesg; + switch (Integer.parseInt(mesg)) { + case ERROR_SYNC_ALREADY_IN_PROGRESS: return "already in progress"; + case ERROR_AUTHENTICATION: return "bad authentication"; + case ERROR_IO: return "network error"; + case ERROR_PARSE: return "parse error"; + case ERROR_CONFLICT: return "conflict detected"; + case ERROR_TOO_MANY_DELETIONS: return "too many deletions"; + case ERROR_TOO_MANY_RETRIES: return "too many retries"; + case ERROR_INTERNAL: return "internal error"; + default: return "unknown error"; + } + } + + // utility class + private History() {} + + /** + * returns a cursor that queries the sync history in descending event time order + * @param contentResolver the ContentResolver to use for the query + * @return the cursor on the History table + */ + public static Cursor query(ContentResolver contentResolver) { + return contentResolver.query(CONTENT_URI, null, null, null, EVENT_TIME + " desc"); + } + + public static boolean hasNewerSyncFinished(ContentResolver contentResolver, + Account account, String authority, long when) { + Cursor c = contentResolver.query(CONTENT_URI, new String[]{_ID}, + FINISHED_SINCE_WHERE_CLAUSE, + new String[]{Long.toString(when), account.mName, account.mType, authority}, + null); + try { + return c.getCount() > 0; + } finally { + c.close(); + } + } + } + + /** + * Provides constants and utility methods to access and use the authority history + * table, which contains information about syncs aggregated by account and authority. + * All the HistoryColumns except for EVENT are present, plus the AuthorityHistoryColumns. + */ + public static class Status extends History implements StatusColumns { + + /** + * The content url for this table. + */ + public static final Uri CONTENT_URI = Uri.parse("content://sync/status"); + + // utility class + private Status() {} + + /** + * returns a cursor that queries the authority sync history in descending event order of + * ACCOUNT, AUTHORITY + * @param contentResolver the ContentResolver to use for the query + * @return the cursor on the AuthorityHistory table + */ + public static Cursor query(ContentResolver contentResolver) { + return contentResolver.query(CONTENT_URI, null, null, null, + ACCOUNT_TYPE + "," + ACCOUNT + "," + AUTHORITY); + } + + public static class QueryMap extends ContentQueryMap { + public QueryMap(ContentResolver contentResolver, + boolean keepUpdated, + Handler handlerForUpdateNotifications) { + super(contentResolver.query(CONTENT_URI, null, null, null, null), + _ID, keepUpdated, handlerForUpdateNotifications); + } + + public ContentValues get(Account account, String authority) { + Map<String, ContentValues> rows = getRows(); + for (ContentValues values : rows.values()) { + if (values.getAsString(ACCOUNT).equals(account.mName) + && values.getAsString(ACCOUNT_TYPE).equals(account.mType) + && values.getAsString(AUTHORITY).equals(authority)) { + return values; + } + } + return null; + } + } + } + + /** + * Provides constants and utility methods to access and use the pending syncs table + */ + public static final class Pending implements BaseColumns, + StatsColumns { + + /** + * The content url for this table. + */ + public static final Uri CONTENT_URI = Uri.parse("content://sync/pending"); + + // utility class + private Pending() {} + + public static class QueryMap extends ContentQueryMap { + public QueryMap(ContentResolver contentResolver, boolean keepUpdated, + Handler handlerForUpdateNotifications) { + super(contentResolver.query(CONTENT_URI, null, null, null, null), _ID, keepUpdated, + handlerForUpdateNotifications); + } + + public boolean isPending(Account account, String authority) { + Map<String, ContentValues> rows = getRows(); + for (ContentValues values : rows.values()) { + if (values.getAsString(ACCOUNT).equals(account.mName) + && values.getAsString(ACCOUNT_TYPE).equals(account.mType) + && values.getAsString(AUTHORITY).equals(authority)) { + return true; + } + } + return false; + } + } + } + + /** + * Columns from the history table. + */ + public interface ActiveColumns { + /** + * The wallclock time of when the active sync started. + * <P>Type: INTEGER</P> + */ + public static final String START_TIME = "startTime"; + } + + /** + * Provides constants and utility methods to access and use the pending syncs table + */ + public static final class Active implements BaseColumns, + StatsColumns, + ActiveColumns { + + /** + * The content url for this table. + */ + public static final Uri CONTENT_URI = Uri.parse("content://sync/active"); + + // utility class + private Active() {} + + public static class QueryMap extends ContentQueryMap { + public QueryMap(ContentResolver contentResolver, boolean keepUpdated, + Handler handlerForUpdateNotifications) { + super(contentResolver.query(CONTENT_URI, null, null, null, null), _ID, keepUpdated, + handlerForUpdateNotifications); + } + + public ContentValues getActiveSyncInfo() { + Map<String, ContentValues> rows = getRows(); + for (ContentValues values : rows.values()) { + return values; + } + return null; + } + + public Account getSyncingAccount() { + ContentValues values = getActiveSyncInfo(); + if (values == null || TextUtils.isEmpty(values.getAsString(ACCOUNT))) { + return null; + } + return new Account(values.getAsString(ACCOUNT), values.getAsString(ACCOUNT_TYPE)); + } + + public String getSyncingAuthority() { + ContentValues values = getActiveSyncInfo(); + return (values == null) ? null : values.getAsString(AUTHORITY); + } + + public long getSyncStartTime() { + ContentValues values = getActiveSyncInfo(); + return (values == null) ? -1 : values.getAsLong(START_TIME); + } + } + } + + /** + * Columns in the settings table, which holds key/value pairs of settings. + */ + public interface SettingsColumns { + /** + * The key of the setting + * <P>Type: TEXT</P> + */ + public static final String KEY = "name"; + + /** + * The value of the settings + * <P>Type: TEXT</P> + */ + public static final String VALUE = "value"; + } + + /** + * Provides constants and utility methods to access and use the settings + * table. + */ + public static final class Settings implements BaseColumns, SettingsColumns { + /** + * The Uri of the settings table. This table behaves a little differently than + * normal tables. Updates are not allowed, only inserts, and inserts cause a replace + * to be performed, which first deletes the row if it is already present. + */ + public static final Uri CONTENT_URI = Uri.parse("content://sync/settings"); + + /** controls whether or not the device listens for sync tickles */ + public static final String SETTING_LISTEN_FOR_TICKLES = "listen_for_tickles"; + + /** controls whether or not the individual provider is synced when tickles are received */ + public static final String SETTING_SYNC_PROVIDER_PREFIX = "sync_provider_"; + + /** query column project */ + private static final String[] PROJECTION = { KEY, VALUE }; + + /** + * Convenience function for updating a single settings value as a + * boolean. This will either create a new entry in the table if the + * given name does not exist, or modify the value of the existing row + * with that name. Note that internally setting values are always + * stored as strings, so this function converts the given value to a + * string before storing it. + * + * @param contentResolver the ContentResolver to use to access the settings table + * @param name The name of the setting to modify. + * @param val The new value for the setting. + */ + static private void putBoolean(ContentResolver contentResolver, String name, boolean val) { + ContentValues values = new ContentValues(); + values.put(KEY, name); + values.put(VALUE, Boolean.toString(val)); + // this insert is translated into an update by the underlying Sync provider + contentResolver.insert(CONTENT_URI, values); + } + + /** + * Convenience function for getting a setting value as a boolean without using the + * QueryMap for light-weight setting querying. + * @param contentResolver The ContentResolver for querying the setting. + * @param name The name of the setting to query + * @param def The default value for the setting. + * @return The value of the setting. + */ + static public boolean getBoolean(ContentResolver contentResolver, + String name, boolean def) { + Cursor cursor = contentResolver.query( + CONTENT_URI, + PROJECTION, + KEY + "=?", + new String[] { name }, + null); + try { + if (cursor != null && cursor.moveToFirst()) { + return Boolean.parseBoolean(cursor.getString(1)); + } + } finally { + if (cursor != null) cursor.close(); + } + return def; + } + + /** + * A convenience method to set whether or not the provider is synced when + * it receives a network tickle. + * + * @param contentResolver the ContentResolver to use to access the settings table + * @param providerName the provider whose behavior is being controlled + * @param sync true if the provider should be synced when tickles are received for it + */ + static public void setSyncProviderAutomatically(ContentResolver contentResolver, + String providerName, boolean sync) { + putBoolean(contentResolver, SETTING_SYNC_PROVIDER_PREFIX + providerName, sync); + } + + /** + * A convenience method to set whether or not the device should listen to tickles. + * + * @param contentResolver the ContentResolver to use to access the settings table + * @param flag true if it should listen. + */ + static public void setListenForNetworkTickles(ContentResolver contentResolver, + boolean flag) { + putBoolean(contentResolver, SETTING_LISTEN_FOR_TICKLES, flag); + } + + public static class QueryMap extends ContentQueryMap { + private ContentResolver mContentResolver; + + public QueryMap(ContentResolver contentResolver, boolean keepUpdated, + Handler handlerForUpdateNotifications) { + super(contentResolver.query(CONTENT_URI, null, null, null, null), KEY, keepUpdated, + handlerForUpdateNotifications); + mContentResolver = contentResolver; + } + + /** + * Check if the provider should be synced when a network tickle is received + * @param providerName the provider whose setting we are querying + * @return true of the provider should be synced when a network tickle is received + */ + public boolean getSyncProviderAutomatically(String providerName) { + return getBoolean(SETTING_SYNC_PROVIDER_PREFIX + providerName, true); + } + + /** + * Set whether or not the provider is synced when it receives a network tickle. + * + * @param providerName the provider whose behavior is being controlled + * @param sync true if the provider should be synced when tickles are received for it + */ + public void setSyncProviderAutomatically(String providerName, boolean sync) { + Settings.setSyncProviderAutomatically(mContentResolver, providerName, sync); + } + + /** + * Set whether or not the device should listen for tickles. + * + * @param flag true if it should listen. + */ + public void setListenForNetworkTickles(boolean flag) { + Settings.setListenForNetworkTickles(mContentResolver, flag); + } + + /** + * Check if the device should listen to tickles. + + * @return true if it should + */ + public boolean getListenForNetworkTickles() { + return getBoolean(SETTING_LISTEN_FOR_TICKLES, true); + } + + /** + * Convenience function for retrieving a single settings value + * as a boolean. + * + * @param name The name of the setting to retrieve. + * @param def Value to return if the setting is not defined. + * @return The setting's current value, or 'def' if it is not defined. + */ + private boolean getBoolean(String name, boolean def) { + ContentValues values = getValues(name); + return values != null ? values.getAsBoolean(VALUE) : def; + } + } + } +} diff --git a/core/java/android/provider/SyncConstValue.java b/core/java/android/provider/SyncConstValue.java index 6eb4398..30966eb 100644 --- a/core/java/android/provider/SyncConstValue.java +++ b/core/java/android/provider/SyncConstValue.java @@ -29,6 +29,12 @@ public interface SyncConstValue public static final String _SYNC_ACCOUNT = "_sync_account"; /** + * The type of the account that was used to sync the entry to the device. + * <P>Type: TEXT</P> + */ + public static final String _SYNC_ACCOUNT_TYPE = "_sync_account_type"; + + /** * The unique ID for a row assigned by the sync source. NULL if the row has never been synced. * <P>Type: TEXT</P> */ @@ -68,4 +74,9 @@ public interface SyncConstValue * Used to indicate that this account is not synced */ public static final String NON_SYNCABLE_ACCOUNT = "non_syncable"; + + /** + * Used to indicate that this account is not synced + */ + public static final String NON_SYNCABLE_ACCOUNT_TYPE = "android.local"; } diff --git a/core/java/android/provider/Telephony.java b/core/java/android/provider/Telephony.java index 4078fa6..a2c253a 100644 --- a/core/java/android/provider/Telephony.java +++ b/core/java/android/provider/Telephony.java @@ -146,7 +146,13 @@ public final class Telephony { * <P>Type: TEXT</P> */ public static final String SERVICE_CENTER = "service_center"; - } + + /** + * Has the message been locked? + * <P>Type: INTEGER (boolean)</P> + */ + public static final String LOCKED = "locked"; +} /** * Contains all text based SMS messages. @@ -1008,6 +1014,12 @@ public final class Telephony { * <P>Type: INTEGER</P> */ public static final String THREAD_ID = "thread_id"; + + /** + * Has the message been locked? + * <P>Type: INTEGER (boolean)</P> + */ + public static final String LOCKED = "locked"; } /** diff --git a/core/java/android/syncml/pim/vcard/ContactStruct.java b/core/java/android/syncml/pim/vcard/ContactStruct.java index afeb5cd..5a29112 100644 --- a/core/java/android/syncml/pim/vcard/ContactStruct.java +++ b/core/java/android/syncml/pim/vcard/ContactStruct.java @@ -17,6 +17,7 @@ package android.syncml.pim.vcard; import android.content.AbstractSyncableContentProvider; +import android.content.ContentProviderOperation; import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; @@ -762,17 +763,16 @@ public class ContactStruct { personId = ContentUris.parseId(personUri); } } else { - personUri = provider.nonTransactionalInsert(People.CONTENT_URI, contentValues); + personUri = provider.insert(People.CONTENT_URI, contentValues); if (personUri != null) { personId = ContentUris.parseId(personUri); ContentValues values = new ContentValues(); values.put(GroupMembership.PERSON_ID, personId); values.put(GroupMembership.GROUP_ID, myContactsGroupId); - Uri resultUri = provider.nonTransactionalInsert( - GroupMembership.CONTENT_URI, values); + Uri resultUri = provider.insert(GroupMembership.CONTENT_URI, values); if (resultUri == null) { Log.e(LOG_TAG, "Faild to insert the person to MyContact."); - provider.nonTransactionalDelete(personUri, null, null); + provider.delete(personUri, null, null); personUri = null; } } @@ -812,7 +812,7 @@ public class ContactStruct { if (resolver != null) { phoneUri = resolver.insert(Phones.CONTENT_URI, values); } else { - phoneUri = provider.nonTransactionalInsert(Phones.CONTENT_URI, values); + phoneUri = provider.insert(Phones.CONTENT_URI, values); } if (phoneData.isPrimary) { primaryPhoneId = Long.parseLong(phoneUri.getLastPathSegment()); @@ -838,8 +838,7 @@ public class ContactStruct { if (resolver != null) { organizationUri = resolver.insert(Organizations.CONTENT_URI, values); } else { - organizationUri = provider.nonTransactionalInsert( - Organizations.CONTENT_URI, values); + organizationUri = provider.insert(Organizations.CONTENT_URI, values); } if (organizationData.isPrimary) { primaryOrganizationId = Long.parseLong(organizationUri.getLastPathSegment()); @@ -865,8 +864,7 @@ public class ContactStruct { if (resolver != null) { emailUri = resolver.insert(ContactMethods.CONTENT_URI, values); } else { - emailUri = provider.nonTransactionalInsert( - ContactMethods.CONTENT_URI, values); + emailUri = provider.insert(ContactMethods.CONTENT_URI, values); } if (contactMethod.isPrimary) { primaryEmailId = Long.parseLong(emailUri.getLastPathSegment()); @@ -875,8 +873,7 @@ public class ContactStruct { if (resolver != null) { resolver.insert(ContactMethods.CONTENT_URI, values); } else { - provider.nonTransactionalInsert( - ContactMethods.CONTENT_URI, values); + provider.insert(ContactMethods.CONTENT_URI, values); } } } @@ -900,7 +897,7 @@ public class ContactStruct { if (resolver != null) { contentValuesArray.add(values); } else { - provider.nonTransactionalInsert(Extensions.CONTENT_URI, values); + provider.insert(Extensions.CONTENT_URI, values); } } } @@ -924,7 +921,7 @@ public class ContactStruct { if (resolver != null) { resolver.update(personUri, values, null, null); } else { - provider.nonTransactionalUpdate(personUri, values, null, null); + provider.update(personUri, values, null, null); } } } @@ -942,12 +939,12 @@ public class ContactStruct { public void pushIntoAbstractSyncableContentProvider( AbstractSyncableContentProvider provider, long myContactsGroupId) { boolean successful = false; - provider.beginTransaction(); + provider.beginBatch(); try { pushIntoContentProviderOrResolver(provider, myContactsGroupId); successful = true; } finally { - provider.endTransaction(successful); + provider.endBatch(successful); } } diff --git a/core/java/android/view/MotionEvent.java b/core/java/android/view/MotionEvent.java index 86261c4..f1bf0f4 100644 --- a/core/java/android/view/MotionEvent.java +++ b/core/java/android/view/MotionEvent.java @@ -19,7 +19,6 @@ package android.view; import android.os.Parcel; import android.os.Parcelable; import android.os.SystemClock; -import android.util.Config; /** * Object used to report movement (mouse, pen, finger, trackball) events. This @@ -87,6 +86,7 @@ public final class MotionEvent implements Parcelable { private long mDownTime; private long mEventTime; + private long mEventTimeNano; private int mAction; private float mX; private float mY; @@ -123,6 +123,62 @@ public final class MotionEvent implements Parcelable { return ev; } } + + /** + * Create a new MotionEvent, filling in all of the basic values that + * define the motion. + * + * @param downTime The time (in ms) when the user originally pressed down to start + * a stream of position events. This must be obtained from {@link SystemClock#uptimeMillis()}. + * @param eventTime The the time (in ms) when this specific event was generated. This + * must be obtained from {@link SystemClock#uptimeMillis()}. + * @param eventTimeNano The the time (in ns) when this specific event was generated. This + * must be obtained from {@link System#nanoTime()}. + * @param action The kind of action being performed -- one of either + * {@link #ACTION_DOWN}, {@link #ACTION_MOVE}, {@link #ACTION_UP}, or + * {@link #ACTION_CANCEL}. + * @param x The X coordinate of this event. + * @param y The Y coordinate of this event. + * @param pressure The current pressure of this event. The pressure generally + * ranges from 0 (no pressure at all) to 1 (normal pressure), however + * values higher than 1 may be generated depending on the calibration of + * the input device. + * @param size A scaled value of the approximate size of the area being pressed when + * touched with the finger. The actual value in pixels corresponding to the finger + * touch is normalized with a device specific range of values + * and scaled to a value between 0 and 1. + * @param metaState The state of any meta / modifier keys that were in effect when + * the event was generated. + * @param xPrecision The precision of the X coordinate being reported. + * @param yPrecision The precision of the Y coordinate being reported. + * @param deviceId The id for the device that this event came from. An id of + * zero indicates that the event didn't come from a physical device; other + * numbers are arbitrary and you shouldn't depend on the values. + * @param edgeFlags A bitfield indicating which edges, if any, where touched by this + * MotionEvent. + * + * @hide + */ + static public MotionEvent obtainNano(long downTime, long eventTime, long eventTimeNano, + int action, float x, float y, float pressure, float size, int metaState, + float xPrecision, float yPrecision, int deviceId, int edgeFlags) { + MotionEvent ev = obtain(); + ev.mDeviceId = deviceId; + ev.mEdgeFlags = edgeFlags; + ev.mDownTime = downTime; + ev.mEventTime = eventTime; + ev.mEventTimeNano = eventTimeNano; + ev.mAction = action; + ev.mX = ev.mRawX = x; + ev.mY = ev.mRawY = y; + ev.mPressure = pressure; + ev.mSize = size; + ev.mMetaState = metaState; + ev.mXPrecision = xPrecision; + ev.mYPrecision = yPrecision; + + return ev; + } /** * Create a new MotionEvent, filling in all of the basic values that @@ -163,6 +219,7 @@ public final class MotionEvent implements Parcelable { ev.mEdgeFlags = edgeFlags; ev.mDownTime = downTime; ev.mEventTime = eventTime; + ev.mEventTimeNano = eventTime * 1000000; ev.mAction = action; ev.mX = ev.mRawX = x; ev.mY = ev.mRawY = y; @@ -199,6 +256,7 @@ public final class MotionEvent implements Parcelable { ev.mEdgeFlags = 0; ev.mDownTime = downTime; ev.mEventTime = eventTime; + ev.mEventTimeNano = eventTime * 1000000; ev.mAction = action; ev.mX = ev.mRawX = x; ev.mY = ev.mRawY = y; @@ -246,6 +304,7 @@ public final class MotionEvent implements Parcelable { ev.mEdgeFlags = o.mEdgeFlags; ev.mDownTime = o.mDownTime; ev.mEventTime = o.mEventTime; + ev.mEventTimeNano = o.mEventTimeNano; ev.mAction = o.mAction; ev.mX = o.mX; ev.mRawX = o.mRawX; @@ -317,6 +376,16 @@ public final class MotionEvent implements Parcelable { } /** + * Returns the time (in ns) when this specific event was generated. + * The value is in nanosecond precision but it may not have nanosecond accuracy. + * + * @hide + */ + public final long getEventTimeNano() { + return mEventTimeNano; + } + + /** * Returns the X coordinate of this event. Whole numbers are pixels; the * value may have a fraction for input devices that are sub-pixel precise. */ @@ -644,6 +713,7 @@ public final class MotionEvent implements Parcelable { public void writeToParcel(Parcel out, int flags) { out.writeLong(mDownTime); out.writeLong(mEventTime); + out.writeLong(mEventTimeNano); out.writeInt(mAction); out.writeFloat(mX); out.writeFloat(mY); @@ -675,6 +745,7 @@ public final class MotionEvent implements Parcelable { private void readFromParcel(Parcel in) { mDownTime = in.readLong(); mEventTime = in.readLong(); + mEventTimeNano = in.readLong(); mAction = in.readInt(); mX = in.readFloat(); mY = in.readFloat(); diff --git a/core/java/android/view/SurfaceView.java b/core/java/android/view/SurfaceView.java index 082cca2..6519852 100644 --- a/core/java/android/view/SurfaceView.java +++ b/core/java/android/view/SurfaceView.java @@ -31,8 +31,9 @@ import android.os.ParcelFileDescriptor; import android.util.AttributeSet; import android.util.Config; import android.util.Log; -import java.util.ArrayList; +import java.lang.ref.WeakReference; +import java.util.ArrayList; import java.util.concurrent.locks.ReentrantLock; import java.lang.ref.WeakReference; diff --git a/core/java/android/view/ViewRoot.java b/core/java/android/view/ViewRoot.java index d8bab56..e15a61c 100644 --- a/core/java/android/view/ViewRoot.java +++ b/core/java/android/view/ViewRoot.java @@ -79,6 +79,9 @@ public final class ViewRoot extends Handler implements ViewParent, private static final boolean DEBUG_IMF = false || LOCAL_LOGV; private static final boolean WATCH_POINTER = false; + private static final boolean MEASURE_LATENCY = false; + private static LatencyTimer lt; + /** * Maximum time we allow the user to roll the trackball enough to generate * a key event, before resetting the counters. @@ -193,6 +196,10 @@ public final class ViewRoot extends Handler implements ViewParent, public ViewRoot(Context context) { super(); + if (MEASURE_LATENCY && lt == null) { + lt = new LatencyTimer(100, 1000); + } + ++sInstanceCount; // Initialize the statics when this class is first instantiated. This is @@ -1585,7 +1592,17 @@ public final class ViewRoot extends Handler implements ViewParent, boolean didFinish; if (event == null) { try { + long timeBeforeGettingEvents; + if (MEASURE_LATENCY) { + timeBeforeGettingEvents = System.nanoTime(); + } + event = sWindowSession.getPendingPointerMove(mWindow); + + if (MEASURE_LATENCY && event != null) { + lt.sample("9 Client got events ", System.nanoTime() - event.getEventTimeNano()); + lt.sample("8 Client getting events ", timeBeforeGettingEvents - event.getEventTimeNano()); + } } catch (RemoteException e) { } didFinish = true; @@ -1609,7 +1626,13 @@ public final class ViewRoot extends Handler implements ViewParent, captureMotionLog("captureDispatchPointer", event); } event.offsetLocation(0, mCurScrollY); + if (MEASURE_LATENCY) { + lt.sample("A Dispatching TouchEvents", System.nanoTime() - event.getEventTimeNano()); + } handled = mView.dispatchTouchEvent(event); + if (MEASURE_LATENCY) { + lt.sample("B Dispatched TouchEvents ", System.nanoTime() - event.getEventTimeNano()); + } if (!handled && isDown) { int edgeSlop = mViewConfiguration.getScaledEdgeSlop(); @@ -2729,7 +2752,11 @@ public final class ViewRoot extends Handler implements ViewParent, public void dispatchPointer(MotionEvent event, long eventTime) { final ViewRoot viewRoot = mViewRoot.get(); - if (viewRoot != null) { + if (viewRoot != null) { + if (MEASURE_LATENCY) { + // Note: eventTime is in milliseconds + ViewRoot.lt.sample("* ViewRoot b4 dispatchPtr", System.nanoTime() - eventTime * 1000000); + } viewRoot.dispatchPointer(event, eventTime); } else { new EventCompletion(mMainLooper, this, null, true, event); diff --git a/core/java/android/view/ViewStub.java b/core/java/android/view/ViewStub.java index e159de4..703a38f 100644 --- a/core/java/android/view/ViewStub.java +++ b/core/java/android/view/ViewStub.java @@ -23,6 +23,8 @@ import android.util.AttributeSet; import com.android.internal.R; +import java.lang.ref.WeakReference; + /** * A ViewStub is an invisible, zero-sized View that can be used to lazily inflate * layout resources at runtime. @@ -68,6 +70,8 @@ public final class ViewStub extends View { private int mLayoutResource = 0; private int mInflatedId; + private WeakReference<View> mInflatedViewRef; + private OnInflateListener mInflateListener; public ViewStub(Context context) { @@ -196,9 +200,15 @@ public final class ViewStub extends View { */ @Override public void setVisibility(int visibility) { - super.setVisibility(visibility); - - if (visibility == VISIBLE || visibility == INVISIBLE) { + if (mInflatedViewRef != null) { + View view = mInflatedViewRef.get(); + if (view != null) { + view.setVisibility(visibility); + } else { + throw new IllegalStateException("setVisibility called on un-referenced view"); + } + } else if (visibility == VISIBLE || visibility == INVISIBLE) { + super.setVisibility(visibility); inflate(); } } @@ -234,6 +244,8 @@ public final class ViewStub extends View { parent.addView(view, index); } + mInflatedViewRef = new WeakReference(view); + if (mInflateListener != null) { mInflateListener.onInflate(this, view); } diff --git a/core/java/android/view/Window.java b/core/java/android/view/Window.java index d7457a0..2c32d8b 100644 --- a/core/java/android/view/Window.java +++ b/core/java/android/view/Window.java @@ -56,11 +56,11 @@ public abstract class Window { public static final int FEATURE_CONTEXT_MENU = 6; /** Flag for custom title. You cannot combine this feature with other title features. */ public static final int FEATURE_CUSTOM_TITLE = 7; - /* Flag for asking for an OpenGL enabled window. + /** Flag for asking for an OpenGL enabled window. All 2D graphics will be handled by OpenGL ES. - Private for now, until it is better tested (not shipping in 1.0) + @hide */ - private static final int FEATURE_OPENGL = 8; + public static final int FEATURE_OPENGL = 8; /** Flag for setting the progress bar's visibility to VISIBLE */ public static final int PROGRESS_VISIBILITY_ON = -1; /** Flag for setting the progress bar's visibility to GONE */ diff --git a/core/java/android/webkit/BrowserFrame.java b/core/java/android/webkit/BrowserFrame.java index ba3f78c..e04ae72 100644 --- a/core/java/android/webkit/BrowserFrame.java +++ b/core/java/android/webkit/BrowserFrame.java @@ -109,6 +109,8 @@ class BrowserFrame extends Handler { CacheManager.init(context); // create CookieSyncManager with current Context CookieSyncManager.createInstance(context); + // create PluginManager with current Context + PluginManager.getInstance(context); } AssetManager am = context.getAssets(); nativeCreateFrame(w, am, proxy.getBackForwardList()); @@ -119,7 +121,7 @@ class BrowserFrame extends Handler { mDatabase = WebViewDatabase.getInstance(context); mWebViewCore = w; - if (WebView.LOGV_ENABLED) { + if (DebugFlags.BROWSER_FRAME) { Log.v(LOGTAG, "BrowserFrame constructor: this=" + this); } } @@ -143,6 +145,17 @@ class BrowserFrame extends Handler { } /** + * Load a url with "POST" method from the network into the main frame. + * @param url The url to load. + * @param data The data for POST request. + */ + public void postUrl(String url, byte[] data) { + mLoadInitFromJava = true; + nativePostUrl(url, data); + mLoadInitFromJava = false; + } + + /** * Load the content as if it was loaded by the provided base URL. The * failUrl is used as the history entry for the load data. If null or * an empty string is passed for the failUrl, then no history entry is @@ -330,7 +343,7 @@ class BrowserFrame extends Handler { switch (msg.what) { case FRAME_COMPLETED: { if (mSettings.getSavePassword() && hasPasswordField()) { - if (WebView.DEBUG) { + if (DebugFlags.BROWSER_FRAME) { Assert.assertNotNull(mCallbackProxy.getBackForwardList() .getCurrentItem()); } @@ -479,7 +492,7 @@ class BrowserFrame extends Handler { } if (mSettings.getSavePassword() && hasPasswordField()) { try { - if (WebView.DEBUG) { + if (DebugFlags.BROWSER_FRAME) { Assert.assertNotNull(mCallbackProxy.getBackForwardList() .getCurrentItem()); } @@ -527,7 +540,7 @@ class BrowserFrame extends Handler { // is this resource the main-frame top-level page? boolean isMainFramePage = mIsMainFrame; - if (WebView.LOGV_ENABLED) { + if (DebugFlags.BROWSER_FRAME) { Log.v(LOGTAG, "startLoadingResource: url=" + url + ", method=" + method + ", postData=" + postData + ", isHighPriority=" + isHighPriority + ", isMainFramePage=" + isMainFramePage); @@ -752,6 +765,8 @@ class BrowserFrame extends Handler { */ private native void nativeLoadUrl(String url); + private native void nativePostUrl(String url, byte[] postData); + private native void nativeLoadData(String baseUrl, String data, String mimeType, String encoding, String failUrl); diff --git a/core/java/android/webkit/CacheManager.java b/core/java/android/webkit/CacheManager.java index 7897435..4d471f7 100644 --- a/core/java/android/webkit/CacheManager.java +++ b/core/java/android/webkit/CacheManager.java @@ -51,7 +51,6 @@ public final class CacheManager { private static final String NO_STORE = "no-store"; private static final String NO_CACHE = "no-cache"; - private static final String PRIVATE = "private"; private static final String MAX_AGE = "max-age"; private static long CACHE_THRESHOLD = 6 * 1024 * 1024; @@ -321,7 +320,7 @@ public final class CacheManager { } } - if (WebView.LOGV_ENABLED) { + if (DebugFlags.CACHE_MANAGER) { Log.v(LOGTAG, "getCacheFile for url " + url); } @@ -340,7 +339,7 @@ public final class CacheManager { * @hide - hide createCacheFile since it has a parameter of type headers, which is * in a hidden package. */ - // can be called from any thread + // only called from WebCore thread public static CacheResult createCacheFile(String url, int statusCode, Headers headers, String mimeType, boolean forceCache) { if (!forceCache && mDisabled) { @@ -349,17 +348,25 @@ public final class CacheManager { // 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); return null; } // like the other browsers, do not cache redirects containing a cookie // header. if (checkCacheRedirect(statusCode) && !headers.getSetCookie().isEmpty()) { + // remove the saved cache if there is any + mDataBase.removeCache(url); return null; } CacheResult ret = parseHeaders(statusCode, headers, mimeType); - if (ret != null) { + 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); + } else { setupFiles(url, ret); try { ret.outStream = new FileOutputStream(ret.outFile); @@ -415,7 +422,7 @@ public final class CacheManager { mDataBase.addCache(url, cacheRet); - if (WebView.LOGV_ENABLED) { + if (DebugFlags.CACHE_MANAGER) { Log.v(LOGTAG, "saveCacheFile for url " + url); } } @@ -628,7 +635,7 @@ public final class CacheManager { // must be re-validated on every load. It does not mean that // the content can not be cached. set to expire 0 means it // can only be used in CACHE_MODE_CACHE_ONLY case - if (NO_CACHE.equals(controls[i]) || PRIVATE.equals(controls[i])) { + if (NO_CACHE.equals(controls[i])) { ret.expires = 0; } else if (controls[i].startsWith(MAX_AGE)) { int separator = controls[i].indexOf('='); diff --git a/core/java/android/webkit/CallbackProxy.java b/core/java/android/webkit/CallbackProxy.java index 17d3f94..c407044 100644 --- a/core/java/android/webkit/CallbackProxy.java +++ b/core/java/android/webkit/CallbackProxy.java @@ -72,36 +72,38 @@ class CallbackProxy extends Handler { private final Context mContext; // Message Ids - private static final int PAGE_STARTED = 100; - private static final int RECEIVED_ICON = 101; - private static final int RECEIVED_TITLE = 102; - private static final int OVERRIDE_URL = 103; - private static final int AUTH_REQUEST = 104; - private static final int SSL_ERROR = 105; - private static final int PROGRESS = 106; - private static final int UPDATE_VISITED = 107; - private static final int LOAD_RESOURCE = 108; - private static final int CREATE_WINDOW = 109; - private static final int CLOSE_WINDOW = 110; - private static final int SAVE_PASSWORD = 111; - private static final int JS_ALERT = 112; - private static final int JS_CONFIRM = 113; - private static final int JS_PROMPT = 114; - private static final int JS_UNLOAD = 115; - private static final int ASYNC_KEYEVENTS = 116; - private static final int TOO_MANY_REDIRECTS = 117; - private static final int DOWNLOAD_FILE = 118; - private static final int REPORT_ERROR = 119; - private static final int RESEND_POST_DATA = 120; - private static final int PAGE_FINISHED = 121; - private static final int REQUEST_FOCUS = 122; - private static final int SCALE_CHANGED = 123; - private static final int RECEIVED_CERTIFICATE = 124; - private static final int SWITCH_OUT_HISTORY = 125; - private static final int JS_TIMEOUT = 126; + private static final int PAGE_STARTED = 100; + private static final int RECEIVED_ICON = 101; + private static final int RECEIVED_TITLE = 102; + private static final int OVERRIDE_URL = 103; + private static final int AUTH_REQUEST = 104; + private static final int SSL_ERROR = 105; + private static final int PROGRESS = 106; + private static final int UPDATE_VISITED = 107; + private static final int LOAD_RESOURCE = 108; + private static final int CREATE_WINDOW = 109; + private static final int CLOSE_WINDOW = 110; + private static final int SAVE_PASSWORD = 111; + private static final int JS_ALERT = 112; + private static final int JS_CONFIRM = 113; + private static final int JS_PROMPT = 114; + private static final int JS_UNLOAD = 115; + private static final int ASYNC_KEYEVENTS = 116; + private static final int TOO_MANY_REDIRECTS = 117; + private static final int DOWNLOAD_FILE = 118; + private static final int REPORT_ERROR = 119; + private static final int RESEND_POST_DATA = 120; + private static final int PAGE_FINISHED = 121; + private static final int REQUEST_FOCUS = 122; + private static final int SCALE_CHANGED = 123; + private static final int RECEIVED_CERTIFICATE = 124; + private static final int SWITCH_OUT_HISTORY = 125; + private static final int EXCEEDED_DATABASE_QUOTA = 126; + private static final int JS_TIMEOUT = 127; + private static final int ADD_MESSAGE_TO_CONSOLE = 128; // Message triggered by the client to resume execution - private static final int NOTIFY = 200; + private static final int NOTIFY = 200; // Result transportation object for returning results across thread // boundaries. @@ -389,6 +391,23 @@ class CallbackProxy extends Handler { } break; + case EXCEEDED_DATABASE_QUOTA: + if (mWebChromeClient != null) { + HashMap<String, Object> map = + (HashMap<String, Object>) msg.obj; + String databaseIdentifier = + (String) map.get("databaseIdentifier"); + String url = (String) map.get("url"); + long currentQuota = + ((Long) map.get("currentQuota")).longValue(); + WebStorage.QuotaUpdater quotaUpdater = + (WebStorage.QuotaUpdater) map.get("quotaUpdater"); + + mWebChromeClient.onExceededDatabaseQuota(url, + databaseIdentifier, currentQuota, quotaUpdater); + } + break; + case JS_ALERT: if (mWebChromeClient != null) { final JsResult res = (JsResult) msg.obj; @@ -563,6 +582,13 @@ class CallbackProxy extends Handler { case SWITCH_OUT_HISTORY: mWebView.switchOutDrawHistory(); break; + + case ADD_MESSAGE_TO_CONSOLE: + String message = msg.getData().getString("message"); + String sourceID = msg.getData().getString("sourceID"); + int lineNumber = msg.getData().getInt("lineNumber"); + mWebChromeClient.addMessageToConsole(message, lineNumber, sourceID); + break; } } @@ -834,7 +860,7 @@ class CallbackProxy extends Handler { String password, Message resumeMsg) { // resumeMsg should be null at this point because we want to create it // within the CallbackProxy. - if (WebView.DEBUG) { + if (DebugFlags.CALLBACK_PROXY) { junit.framework.Assert.assertNull(resumeMsg); } resumeMsg = obtainMessage(NOTIFY); @@ -1037,6 +1063,60 @@ class CallbackProxy extends Handler { } /** + * Called by WebViewCore to inform the Java side that the current origin + * has overflowed it's database quota. Called in the WebCore thread so + * posts a message to the UI thread that will prompt the WebChromeClient + * for what to do. On return back to C++ side, the WebCore thread will + * sleep pending a new quota value. + * @param url The URL that caused the quota overflow. + * @param databaseIdentifier The identifier of the database that the + * transaction that caused the overflow was running on. + * @param currentQuota The current quota the origin is allowed. + * @param quotaUpdater An instance of a class encapsulating a callback + * to WebViewCore to run when the decision to allow or deny more + * quota has been made. + */ + public void onExceededDatabaseQuota( + String url, String databaseIdentifier, long currentQuota, + WebStorage.QuotaUpdater quotaUpdater) { + if (mWebChromeClient == null) { + quotaUpdater.updateQuota(currentQuota); + return; + } + + Message exceededQuota = obtainMessage(EXCEEDED_DATABASE_QUOTA); + HashMap<String, Object> map = new HashMap(); + map.put("databaseIdentifier", databaseIdentifier); + map.put("url", url); + map.put("currentQuota", currentQuota); + map.put("quotaUpdater", quotaUpdater); + exceededQuota.obj = map; + sendMessage(exceededQuota); + } + + /** + * Called by WebViewCore when we have a message to be added to the JavaScript + * error console. Sends a message to the Java side with the details. + * @param message The message to add to the console. + * @param lineNumber The lineNumber of the source file on which the error + * occurred. + * @param sourceID The filename of the source file in which the error + * occurred. + * @hide pending API counsel. + */ + public void addMessageToConsole(String message, int lineNumber, String sourceID) { + if (mWebChromeClient == null) { + return; + } + + Message msg = obtainMessage(ADD_MESSAGE_TO_CONSOLE); + msg.getData().putString("message", message); + msg.getData().putString("sourceID", sourceID); + msg.getData().putInt("lineNumber", lineNumber); + sendMessage(msg); + } + + /** * @hide pending API council approval */ public boolean onJsTimeout() { diff --git a/core/java/android/webkit/CookieManager.java b/core/java/android/webkit/CookieManager.java index e8c2279..7b91724 100644 --- a/core/java/android/webkit/CookieManager.java +++ b/core/java/android/webkit/CookieManager.java @@ -262,7 +262,7 @@ public final class CookieManager { if (!mAcceptCookie || uri == null) { return; } - if (WebView.LOGV_ENABLED) { + if (DebugFlags.COOKIE_MANAGER) { Log.v(LOGTAG, "setCookie: uri: " + uri + " value: " + value); } @@ -427,12 +427,12 @@ public final class CookieManager { } } if (ret.length() > 0) { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.COOKIE_MANAGER) { Log.v(LOGTAG, "getCookie: uri: " + uri + " value: " + ret); } return ret.toString(); } else { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.COOKIE_MANAGER) { Log.v(LOGTAG, "getCookie: uri: " + uri + " But can't find cookie."); } @@ -588,7 +588,7 @@ public final class CookieManager { Iterator<ArrayList<Cookie>> listIter = cookieLists.iterator(); while (listIter.hasNext() && count < MAX_RAM_COOKIES_COUNT) { ArrayList<Cookie> list = listIter.next(); - if (WebView.DEBUG) { + if (DebugFlags.COOKIE_MANAGER) { Iterator<Cookie> iter = list.iterator(); while (iter.hasNext() && count < MAX_RAM_COOKIES_COUNT) { Cookie cookie = iter.next(); @@ -608,7 +608,7 @@ public final class CookieManager { ArrayList<Cookie> retlist = new ArrayList<Cookie>(); if (mapSize >= MAX_RAM_DOMAIN_COUNT || count >= MAX_RAM_COOKIES_COUNT) { - if (WebView.DEBUG) { + if (DebugFlags.COOKIE_MANAGER) { Log.v(LOGTAG, count + " cookies used " + byteCount + " bytes with " + mapSize + " domains"); } @@ -616,7 +616,7 @@ public final class CookieManager { int toGo = mapSize / 10 + 1; while (toGo-- > 0){ String domain = domains[toGo].toString(); - if (WebView.LOGV_ENABLED) { + if (DebugFlags.COOKIE_MANAGER) { Log.v(LOGTAG, "delete domain: " + domain + " from RAM cache"); } diff --git a/core/java/android/webkit/CookieSyncManager.java b/core/java/android/webkit/CookieSyncManager.java index 8d66529..14375d2 100644 --- a/core/java/android/webkit/CookieSyncManager.java +++ b/core/java/android/webkit/CookieSyncManager.java @@ -24,30 +24,39 @@ import java.util.ArrayList; import java.util.Iterator; /** - * The class CookieSyncManager is used to synchronize the browser cookies - * between RAM and FLASH. To get the best performance, browser cookie is saved - * in RAM. We use a separate thread to sync the cookies between RAM and FLASH on - * a timer base. + * The CookieSyncManager is used to synchronize the browser cookie store + * between RAM and permanent storage. To get the best performance, browser cookies are + * saved in RAM. A separate thread saves the cookies between, driven by a timer. * <p> + * * To use the CookieSyncManager, the host application has to call the following - * when the application starts. - * <p> - * CookieSyncManager.createInstance(context) - * <p> - * To set up for sync, the host application has to call - * <p> - * CookieSyncManager.getInstance().startSync() + * when the application starts: * <p> - * in its Activity.onResume(), and call + * + * <pre class="prettyprint">CookieSyncManager.createInstance(context)</pre><p> + * + * To set up for sync, the host application has to call<p> + * <pre class="prettyprint">CookieSyncManager.getInstance().startSync()</pre><p> + * + * in Activity.onResume(), and call * <p> + * + * <pre class="prettyprint"> * CookieSyncManager.getInstance().stopSync() - * <p> - * in its Activity.onStop(). - * <p> + * </pre><p> + * + * in Activity.onPause().<p> + * * To get instant sync instead of waiting for the timer to trigger, the host can * call * <p> - * CookieSyncManager.getInstance().sync() + * <pre class="prettyprint">CookieSyncManager.getInstance().sync()</pre><p> + * + * The sync interval is 5 minutes, so you will want to force syncs + * manually anyway, for instance in {@link + * WebViewClient#onPageFinished}. Note that even sync() happens + * asynchronously, so don't do it just as your activity is shutting + * down. */ public final class CookieSyncManager extends WebSyncManager { @@ -90,7 +99,7 @@ public final class CookieSyncManager extends WebSyncManager { } /** - * Package level api, called from CookieManager Get all the cookies which + * Package level api, called from CookieManager. Get all the cookies which * matches a given base domain. * @param domain * @return A list of Cookie @@ -161,7 +170,7 @@ public final class CookieSyncManager extends WebSyncManager { } protected void syncFromRamToFlash() { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.COOKIE_SYNC_MANAGER) { Log.v(LOGTAG, "CookieSyncManager::syncFromRamToFlash STARTS"); } @@ -178,7 +187,7 @@ public final class CookieSyncManager extends WebSyncManager { CookieManager.getInstance().deleteLRUDomain(); syncFromRamToFlash(lruList); - if (WebView.LOGV_ENABLED) { + if (DebugFlags.COOKIE_SYNC_MANAGER) { Log.v(LOGTAG, "CookieSyncManager::syncFromRamToFlash DONE"); } } diff --git a/core/java/android/webkit/DataLoader.java b/core/java/android/webkit/DataLoader.java index dcdc949..6c5d10d 100644 --- a/core/java/android/webkit/DataLoader.java +++ b/core/java/android/webkit/DataLoader.java @@ -16,12 +16,10 @@ package android.webkit; -import org.apache.http.protocol.HTTP; - -import android.net.http.Headers; - import java.io.ByteArrayInputStream; +import org.apache.harmony.luni.util.Base64; + /** * This class is a concrete implementation of StreamLoader that uses the * content supplied as a URL as the source for the stream. The mimetype @@ -30,8 +28,6 @@ import java.io.ByteArrayInputStream; */ class DataLoader extends StreamLoader { - private String mContentType; // Content mimetype, if supplied in URL - /** * Constructor uses the dataURL as the source for an InputStream * @param dataUrl data: URL string optionally containing a mimetype @@ -41,16 +37,20 @@ class DataLoader extends StreamLoader { super(loadListener); String url = dataUrl.substring("data:".length()); - String content; + byte[] data = null; int commaIndex = url.indexOf(','); if (commaIndex != -1) { - mContentType = url.substring(0, commaIndex); - content = url.substring(commaIndex + 1); + String contentType = url.substring(0, commaIndex); + data = url.substring(commaIndex + 1).getBytes(); + loadListener.parseContentTypeHeader(contentType); + if ("base64".equals(loadListener.transferEncoding())) { + data = Base64.decode(data); + } } else { - content = url; + data = url.getBytes(); } - mDataStream = new ByteArrayInputStream(content.getBytes()); - mContentLength = content.length(); + mDataStream = new ByteArrayInputStream(data); + mContentLength = data.length; } @Override @@ -60,10 +60,7 @@ class DataLoader extends StreamLoader { } @Override - protected void buildHeaders(Headers headers) { - if (mContentType != null) { - headers.setContentType(mContentType); - } + protected void buildHeaders(android.net.http.Headers h) { } /** diff --git a/core/java/android/webkit/DebugFlags.java b/core/java/android/webkit/DebugFlags.java new file mode 100644 index 0000000..89cb606 --- /dev/null +++ b/core/java/android/webkit/DebugFlags.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.webkit; + +/** + * This class is a container for all of the debug flags used in the Java + * components of webkit. These flags must be final in order to ensure that + * the compiler optimizes the code that uses them out of the final executable. + * + * The name of each flags maps directly to the name of the class in which that + * flag is used. + * + */ +class DebugFlags { + + public static final boolean BROWSER_FRAME = false; + public static final boolean CACHE_MANAGER = false; + public static final boolean CALLBACK_PROXY = false; + public static final boolean COOKIE_MANAGER = false; + public static final boolean COOKIE_SYNC_MANAGER = false; + public static final boolean FRAME_LOADER = false; + public static final boolean J_WEB_CORE_JAVA_BRIDGE = false;// HIGHLY VERBOSE + public static final boolean LOAD_LISTENER = false; + public static final boolean NETWORK = false; + public static final boolean SSL_ERROR_HANDLER = false; + public static final boolean STREAM_LOADER = false; + public static final boolean URL_UTIL = false; + public static final boolean WEB_BACK_FORWARD_LIST = false; + public static final boolean WEB_SETTINGS = false; + public static final boolean WEB_SYNC_MANAGER = false; + public static final boolean WEB_VIEW = false; + public static final boolean WEB_VIEW_CORE = false; + +} diff --git a/core/java/android/webkit/FrameLoader.java b/core/java/android/webkit/FrameLoader.java index 6f1b160..c33744e 100644 --- a/core/java/android/webkit/FrameLoader.java +++ b/core/java/android/webkit/FrameLoader.java @@ -120,7 +120,7 @@ class FrameLoader { } else if (handleLocalFile(url, mListener, mSettings)) { return true; } - if (WebView.LOGV_ENABLED) { + if (DebugFlags.FRAME_LOADER) { Log.v(LOGTAG, "FrameLoader.executeLoad: url protocol not supported:" + mListener.url()); } @@ -180,7 +180,7 @@ class FrameLoader { return true; } - if (WebView.LOGV_ENABLED) { + if (DebugFlags.FRAME_LOADER) { Log.v(LOGTAG, "FrameLoader: http " + mMethod + " load for: " + mListener.url()); } @@ -211,7 +211,7 @@ class FrameLoader { * setup a load from the byte stream in a CacheResult. */ private void startCacheLoad(CacheResult result) { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.FRAME_LOADER) { Log.v(LOGTAG, "FrameLoader: loading from cache: " + mListener.url()); } @@ -285,7 +285,7 @@ class FrameLoader { // of it's state. If it is not in the cache, then go to the // network. case WebSettings.LOAD_CACHE_ELSE_NETWORK: { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.FRAME_LOADER) { Log.v(LOGTAG, "FrameLoader: checking cache: " + mListener.url()); } diff --git a/core/java/android/webkit/HttpDateTime.java b/core/java/android/webkit/HttpDateTime.java index c6ec2d2..48b2081 100644 --- a/core/java/android/webkit/HttpDateTime.java +++ b/core/java/android/webkit/HttpDateTime.java @@ -47,14 +47,16 @@ class HttpDateTime { * Wdy, DD Mon YYYY HH:MM:SS * Wdy Mon (SP)D HH:MM:SS YYYY * Wdy Mon DD HH:MM:SS YYYY GMT + * + * HH can be H if the first digit is zero. */ private static final String HTTP_DATE_RFC_REGEXP = "([0-9]{1,2})[- ]([A-Za-z]{3,3})[- ]([0-9]{2,4})[ ]" - + "([0-9][0-9]:[0-9][0-9]:[0-9][0-9])"; + + "([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})[ ]" - + "([0-9][0-9]:[0-9][0-9]:[0-9][0-9])[ ]([0-9]{2,4})"; + + "([0-9]{1,2}:[0-9][0-9]:[0-9][0-9])[ ]([0-9]{2,4})"; /** * The compiled version of the HTTP-date regular expressions. @@ -65,6 +67,12 @@ class HttpDateTime { Pattern.compile(HTTP_DATE_ANSIC_REGEXP); private static class TimeOfDay { + TimeOfDay(int h, int m, int s) { + this.hour = h; + this.minute = m; + this.second = s; + } + int hour; int minute; int second; @@ -76,7 +84,7 @@ class HttpDateTime { int date = 1; int month = Calendar.JANUARY; int year = 1970; - TimeOfDay timeOfDay = new TimeOfDay(); + TimeOfDay timeOfDay; Matcher rfcMatcher = HTTP_DATE_RFC_PATTERN.matcher(timeString); if (rfcMatcher.find()) { @@ -183,13 +191,22 @@ class HttpDateTime { } private static TimeOfDay getTime(String timeString) { - TimeOfDay time = new TimeOfDay(); - time.hour = (timeString.charAt(0) - '0') * 10 - + (timeString.charAt(1) - '0'); - time.minute = (timeString.charAt(3) - '0') * 10 - + (timeString.charAt(4) - '0'); - time.second = (timeString.charAt(6) - '0') * 10 - + (timeString.charAt(7) - '0'); - return time; + // HH might be H + int i = 0; + int hour = timeString.charAt(i++) - '0'; + if (timeString.charAt(i) != ':') + hour = hour * 10 + (timeString.charAt(i++) - '0'); + // Skip ':' + i++; + + int minute = (timeString.charAt(i++) - '0') * 10 + + (timeString.charAt(i++) - '0'); + // Skip ':' + i++; + + int second = (timeString.charAt(i++) - '0') * 10 + + (timeString.charAt(i++) - '0'); + + return new TimeOfDay(hour, minute, second); } } diff --git a/core/java/android/webkit/JWebCoreJavaBridge.java b/core/java/android/webkit/JWebCoreJavaBridge.java index 2a84683..878b690 100644 --- a/core/java/android/webkit/JWebCoreJavaBridge.java +++ b/core/java/android/webkit/JWebCoreJavaBridge.java @@ -33,9 +33,16 @@ final class JWebCoreJavaBridge extends Handler { // Instant timer is used to implement a timer that needs to fire almost // immediately. private boolean mHasInstantTimer; + // Reference count the pause/resume of timers private int mPauseTimerRefCount; + private boolean mTimerPaused; + private boolean mHasDeferredTimers; + + /* package */ + static final int REFRESH_PLUGINS = 100; + /** * Construct a new JWebCoreJavaBridge to interface with * WebCore timers and cookies. @@ -50,6 +57,17 @@ final class JWebCoreJavaBridge extends Handler { } /** + * Call native timer callbacks. + */ + private void fireSharedTimer() { + PerfChecker checker = new PerfChecker(); + // clear the flag so that sharedTimerFired() can set a new timer + mHasInstantTimer = false; + sharedTimerFired(); + checker.responseAlert("sharedTimer"); + } + + /** * handleMessage * @param msg The dispatched message. * @@ -59,16 +77,21 @@ final class JWebCoreJavaBridge extends Handler { public void handleMessage(Message msg) { switch (msg.what) { case TIMER_MESSAGE: { - PerfChecker checker = new PerfChecker(); - // clear the flag so that sharedTimerFired() can set a new timer - mHasInstantTimer = false; - sharedTimerFired(); - checker.responseAlert("sharedTimer"); + if (mTimerPaused) { + mHasDeferredTimers = true; + } else { + fireSharedTimer(); + } break; } case FUNCPTR_MESSAGE: nativeServiceFuncPtrQueue(); break; + case REFRESH_PLUGINS: + nativeUpdatePluginDirectories(PluginManager.getInstance(null) + .getPluginDirecoties(), ((Boolean) msg.obj) + .booleanValue()); + break; } } @@ -85,7 +108,8 @@ final class JWebCoreJavaBridge extends Handler { */ public void pause() { if (--mPauseTimerRefCount == 0) { - setDeferringTimers(true); + mTimerPaused = true; + mHasDeferredTimers = false; } } @@ -94,7 +118,11 @@ final class JWebCoreJavaBridge extends Handler { */ public void resume() { if (++mPauseTimerRefCount == 1) { - setDeferringTimers(false); + mTimerPaused = false; + if (mHasDeferredTimers) { + mHasDeferredTimers = false; + fireSharedTimer(); + } } } @@ -151,11 +179,18 @@ final class JWebCoreJavaBridge extends Handler { } /** + * Returns an array of plugin directoies + */ + private String[] getPluginDirectories() { + return PluginManager.getInstance(null).getPluginDirecoties(); + } + + /** * setSharedTimer * @param timemillis The relative time when the timer should fire */ private void setSharedTimer(long timemillis) { - if (WebView.LOGV_ENABLED) Log.v(LOGTAG, "setSharedTimer " + timemillis); + if (DebugFlags.J_WEB_CORE_JAVA_BRIDGE) Log.v(LOGTAG, "setSharedTimer " + timemillis); if (timemillis <= 0) { // we don't accumulate the sharedTimer unless it is a delayed @@ -179,16 +214,18 @@ final class JWebCoreJavaBridge extends Handler { * Stop the shared timer. */ private void stopSharedTimer() { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.J_WEB_CORE_JAVA_BRIDGE) { Log.v(LOGTAG, "stopSharedTimer removing all timers"); } removeMessages(TIMER_MESSAGE); mHasInstantTimer = false; + mHasDeferredTimers = false; } private native void nativeConstructor(); private native void nativeFinalize(); private native void sharedTimerFired(); - private native void setDeferringTimers(boolean defer); + private native void nativeUpdatePluginDirectories(String[] directories, + boolean reload); public native void setNetworkOnLine(boolean online); } diff --git a/core/java/android/webkit/LoadListener.java b/core/java/android/webkit/LoadListener.java index d583eb1..1ebdb79 100644 --- a/core/java/android/webkit/LoadListener.java +++ b/core/java/android/webkit/LoadListener.java @@ -43,8 +43,6 @@ import java.util.Vector; import java.util.regex.Pattern; import java.util.regex.Matcher; -import org.apache.commons.codec.binary.Base64; - class LoadListener extends Handler implements EventHandler { private static final String LOGTAG = "webkit"; @@ -133,15 +131,13 @@ class LoadListener extends Handler implements EventHandler { LoadListener(Context context, BrowserFrame frame, String url, int nativeLoader, boolean synchronous, boolean isMainPageLoader) { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.LOAD_LISTENER) { Log.v(LOGTAG, "LoadListener constructor url=" + url); } mContext = context; mBrowserFrame = frame; setUrl(url); mNativeLoader = nativeLoader; - mMimeType = ""; - mEncoding = ""; mSynchronous = synchronous; if (synchronous) { mMessageQueue = new Vector<Message>(); @@ -284,7 +280,7 @@ class LoadListener extends Handler implements EventHandler { * directly */ public void headers(Headers headers) { - if (WebView.LOGV_ENABLED) Log.v(LOGTAG, "LoadListener.headers"); + if (DebugFlags.LOAD_LISTENER) Log.v(LOGTAG, "LoadListener.headers"); sendMessageInternal(obtainMessage(MSG_CONTENT_HEADERS, headers)); } @@ -292,8 +288,6 @@ class LoadListener extends Handler implements EventHandler { private void handleHeaders(Headers headers) { if (mCancelled) return; mHeaders = headers; - mMimeType = ""; - mEncoding = ""; ArrayList<String> cookies = headers.getSetCookie(); for (int i = 0; i < cookies.size(); ++i) { @@ -313,24 +307,21 @@ class LoadListener extends Handler implements EventHandler { // If we have one of "generic" MIME types, try to deduce // the right MIME type from the file extension (if any): - if (mMimeType.equalsIgnoreCase("text/plain") || - mMimeType.equalsIgnoreCase("application/octet-stream")) { + if (mMimeType.equals("text/plain") || + mMimeType.equals("application/octet-stream")) { String newMimeType = guessMimeTypeFromExtension(); if (newMimeType != null) { mMimeType = newMimeType; } - } else if (mMimeType.equalsIgnoreCase("text/vnd.wap.wml")) { + } else if (mMimeType.equals("text/vnd.wap.wml")) { // As we don't support wml, render it as plain text mMimeType = "text/plain"; } else { - // XXX: Until the servers send us either correct xhtml or - // text/html, treat application/xhtml+xml as text/html. // It seems that xhtml+xml and vnd.wap.xhtml+xml mime // subtypes are used interchangeably. So treat them the same. - if (mMimeType.equalsIgnoreCase("application/xhtml+xml") || - mMimeType.equals("application/vnd.wap.xhtml+xml")) { - mMimeType = "text/html"; + if (mMimeType.equals("application/vnd.wap.xhtml+xml")) { + mMimeType = "application/xhtml+xml"; } } } else { @@ -431,7 +422,7 @@ class LoadListener extends Handler implements EventHandler { */ public void status(int majorVersion, int minorVersion, int code, /* Status-Code value */ String reasonPhrase) { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.LOAD_LISTENER) { Log.v(LOGTAG, "LoadListener: from: " + mUrl + " major: " + majorVersion + " minor: " + minorVersion @@ -445,6 +436,9 @@ class LoadListener extends Handler implements EventHandler { status.put("reason", reasonPhrase); // New status means new data. Clear the old. mDataBuilder.clear(); + mMimeType = ""; + mEncoding = ""; + mTransferEncoding = ""; sendMessageInternal(obtainMessage(MSG_STATUS, status)); } @@ -488,7 +482,7 @@ class LoadListener extends Handler implements EventHandler { * directly */ public void error(int id, String description) { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.LOAD_LISTENER) { Log.v(LOGTAG, "LoadListener.error url:" + url() + " id:" + id + " description:" + description); } @@ -516,23 +510,10 @@ class LoadListener extends Handler implements EventHandler { * mDataBuilder is a thread-safe structure. */ public void data(byte[] data, int length) { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.LOAD_LISTENER) { Log.v(LOGTAG, "LoadListener.data(): url: " + url()); } - // Decode base64 data - // Note: It's fine that we only decode base64 here and not in the other - // data call because the only caller of the stream version is not - // base64 encoded. - if ("base64".equalsIgnoreCase(mTransferEncoding)) { - if (length < data.length) { - byte[] trimmedData = new byte[length]; - System.arraycopy(data, 0, trimmedData, 0, length); - data = trimmedData; - } - data = Base64.decodeBase64(data); - length = data.length; - } // 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. @@ -554,7 +535,7 @@ class LoadListener extends Handler implements EventHandler { * directly */ public void endData() { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.LOAD_LISTENER) { Log.v(LOGTAG, "LoadListener.endData(): url: " + url()); } sendMessageInternal(obtainMessage(MSG_CONTENT_FINISHED)); @@ -607,7 +588,7 @@ class LoadListener extends Handler implements EventHandler { // before calling it. if (mCacheLoader != null) { mCacheLoader.load(); - if (WebView.LOGV_ENABLED) { + if (DebugFlags.LOAD_LISTENER) { Log.v(LOGTAG, "LoadListener cache load url=" + url()); } return; @@ -657,7 +638,7 @@ class LoadListener extends Handler implements EventHandler { CacheManager.HEADER_KEY_IFNONEMATCH) && !headers.containsKey( CacheManager.HEADER_KEY_IFMODIFIEDSINCE)) { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.LOAD_LISTENER) { Log.v(LOGTAG, "FrameLoader: HTTP URL in cache " + "and usable: " + url()); } @@ -676,7 +657,7 @@ class LoadListener extends Handler implements EventHandler { * directly */ public void handleSslErrorRequest(SslError error) { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.LOAD_LISTENER) { Log.v(LOGTAG, "LoadListener.handleSslErrorRequest(): url:" + url() + " primary error: " + error.getPrimaryError() + @@ -742,7 +723,7 @@ class LoadListener extends Handler implements EventHandler { * are null, cancel the request. */ void handleAuthResponse(String username, String password) { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.LOAD_LISTENER) { Log.v(LOGTAG, "LoadListener.handleAuthResponse: url: " + mUrl + " username: " + username + " password: " + password); @@ -839,7 +820,7 @@ class LoadListener extends Handler implements EventHandler { } void attachRequestHandle(RequestHandle requestHandle) { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.LOAD_LISTENER) { Log.v(LOGTAG, "LoadListener.attachRequestHandle(): " + "requestHandle: " + requestHandle); } @@ -847,7 +828,7 @@ class LoadListener extends Handler implements EventHandler { } void detachRequestHandle() { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.LOAD_LISTENER) { Log.v(LOGTAG, "LoadListener.detachRequestHandle(): " + "requestHandle: " + mRequestHandle); } @@ -886,7 +867,7 @@ class LoadListener extends Handler implements EventHandler { */ static boolean willLoadFromCache(String url) { boolean inCache = CacheManager.getCacheFile(url, null) != null; - if (WebView.LOGV_ENABLED) { + if (DebugFlags.LOAD_LISTENER) { Log.v(LOGTAG, "willLoadFromCache: " + url + " in cache: " + inCache); } @@ -907,6 +888,10 @@ class LoadListener extends Handler implements EventHandler { return mMimeType; } + String transferEncoding() { + return mTransferEncoding; + } + /* * Return the size of the content being downloaded. This represents the * full content size, even under the situation where the download has been @@ -1056,7 +1041,7 @@ class LoadListener extends Handler implements EventHandler { * EventHandler's method call. */ public void cancel() { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.LOAD_LISTENER) { if (mRequestHandle == null) { Log.v(LOGTAG, "LoadListener.cancel(): no requestHandle"); } else { @@ -1188,7 +1173,7 @@ class LoadListener extends Handler implements EventHandler { tearDown(); } - if (WebView.LOGV_ENABLED) { + if (DebugFlags.LOAD_LISTENER) { Log.v(LOGTAG, "LoadListener.onRedirect(): redirect to: " + redirectTo); } @@ -1201,8 +1186,8 @@ class LoadListener extends Handler implements EventHandler { private static final Pattern CONTENT_TYPE_PATTERN = Pattern.compile("^((?:[xX]-)?[a-zA-Z\\*]+/[\\w\\+\\*-]+[\\.[\\w\\+-]+]*)$"); - private void parseContentTypeHeader(String contentType) { - if (WebView.LOGV_ENABLED) { + /* package */ void parseContentTypeHeader(String contentType) { + if (DebugFlags.LOAD_LISTENER) { Log.v(LOGTAG, "LoadListener.parseContentTypeHeader: " + "contentType: " + contentType); } @@ -1223,13 +1208,14 @@ class LoadListener extends Handler implements EventHandler { mEncoding = contentType.substring(i + 1); } // Trim excess whitespace. - mEncoding = mEncoding.trim(); + mEncoding = mEncoding.trim().toLowerCase(); if (i < contentType.length() - 1) { // for data: uri the mimeType and encoding have // the form image/jpeg;base64 or text/plain;charset=utf-8 // or text/html;charset=utf-8;base64 - mTransferEncoding = contentType.substring(i + 1).trim(); + mTransferEncoding = + contentType.substring(i + 1).trim().toLowerCase(); } } else { mMimeType = contentType; @@ -1249,6 +1235,8 @@ class LoadListener extends Handler implements EventHandler { guessMimeType(); } } + // Ensure mMimeType is lower case. + mMimeType = mMimeType.toLowerCase(); } /** @@ -1379,7 +1367,7 @@ class LoadListener extends Handler implements EventHandler { mMimeType = "text/html"; String newMimeType = guessMimeTypeFromExtension(); if (newMimeType != null) { - mMimeType = newMimeType; + mMimeType = newMimeType; } } } @@ -1389,23 +1377,12 @@ class LoadListener extends Handler implements EventHandler { */ private String guessMimeTypeFromExtension() { // PENDING: need to normalize url - if (WebView.LOGV_ENABLED) { + if (DebugFlags.LOAD_LISTENER) { Log.v(LOGTAG, "guessMimeTypeFromExtension: mURL = " + mUrl); } - String mimeType = - MimeTypeMap.getSingleton().getMimeTypeFromExtension( - MimeTypeMap.getFileExtensionFromUrl(mUrl)); - - if (mimeType != null) { - // XXX: Until the servers send us either correct xhtml or - // text/html, treat application/xhtml+xml as text/html. - if (mimeType.equals("application/xhtml+xml")) { - mimeType = "text/html"; - } - } - - return mimeType; + return MimeTypeMap.getSingleton().getMimeTypeFromExtension( + MimeTypeMap.getFileExtensionFromUrl(mUrl)); } /** @@ -1424,7 +1401,7 @@ class LoadListener extends Handler implements EventHandler { * Cycle through our messages for synchronous loads. */ /* package */ void loadSynchronousMessages() { - if (WebView.DEBUG && !mSynchronous) { + if (DebugFlags.LOAD_LISTENER && !mSynchronous) { throw new AssertionError(); } // Note: this can be called twice if it is a synchronous network load, diff --git a/core/java/android/webkit/MimeTypeMap.java b/core/java/android/webkit/MimeTypeMap.java index 85c2275..096f38a 100644 --- a/core/java/android/webkit/MimeTypeMap.java +++ b/core/java/android/webkit/MimeTypeMap.java @@ -358,6 +358,7 @@ public /* package */ class MimeTypeMap { sMimeTypeMap.loadEntry("application/x-x509-ca-cert", "crt", false); sMimeTypeMap.loadEntry("application/x-xcf", "xcf", false); sMimeTypeMap.loadEntry("application/x-xfig", "fig", false); + sMimeTypeMap.loadEntry("application/xhtml+xml", "xhtml", false); sMimeTypeMap.loadEntry("audio/basic", "snd", false); sMimeTypeMap.loadEntry("audio/midi", "mid", false); sMimeTypeMap.loadEntry("audio/midi", "midi", false); diff --git a/core/java/android/webkit/Network.java b/core/java/android/webkit/Network.java index c9b80ce..8c2b09b 100644 --- a/core/java/android/webkit/Network.java +++ b/core/java/android/webkit/Network.java @@ -132,7 +132,7 @@ class Network { * XXX: Must be created in the same thread as WebCore!!!!! */ private Network(Context context) { - if (WebView.DEBUG) { + if (DebugFlags.NETWORK) { Assert.assertTrue(Thread.currentThread(). getName().equals(WebViewCore.THREAD_NAME)); } @@ -232,7 +232,7 @@ class Network { * connecting through the proxy. */ public synchronized void setProxyUsername(String proxyUsername) { - if (WebView.DEBUG) { + if (DebugFlags.NETWORK) { Assert.assertTrue(isValidProxySet()); } @@ -252,7 +252,7 @@ class Network { * connecting through the proxy. */ public synchronized void setProxyPassword(String proxyPassword) { - if (WebView.DEBUG) { + if (DebugFlags.NETWORK) { Assert.assertTrue(isValidProxySet()); } @@ -266,7 +266,7 @@ class Network { * @return True iff succeeds. */ public boolean saveState(Bundle outState) { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.NETWORK) { Log.v(LOGTAG, "Network.saveState()"); } @@ -280,7 +280,7 @@ class Network { * @return True iff succeeds. */ public boolean restoreState(Bundle inState) { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.NETWORK) { Log.v(LOGTAG, "Network.restoreState()"); } @@ -300,7 +300,7 @@ class Network { * @param loader The loader that resulted in SSL errors. */ public void handleSslErrorRequest(LoadListener loader) { - if (WebView.DEBUG) Assert.assertNotNull(loader); + if (DebugFlags.NETWORK) Assert.assertNotNull(loader); if (loader != null) { mSslErrorHandler.handleSslErrorRequest(loader); } @@ -313,7 +313,7 @@ class Network { * authentication request. */ public void handleAuthRequest(LoadListener loader) { - if (WebView.DEBUG) Assert.assertNotNull(loader); + if (DebugFlags.NETWORK) Assert.assertNotNull(loader); if (loader != null) { mHttpAuthHandler.handleAuthRequest(loader); } diff --git a/core/java/android/webkit/PluginManager.java b/core/java/android/webkit/PluginManager.java new file mode 100644 index 0000000..e4a44b9 --- /dev/null +++ b/core/java/android/webkit/PluginManager.java @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.webkit; + +import java.util.ArrayList; +import java.util.List; + +import android.annotation.SdkConstant; +import android.annotation.SdkConstant.SdkConstantType; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.content.pm.Signature; +import android.content.pm.PackageManager.NameNotFoundException; +import android.util.Log; + +/** + * Class for managing the relationship between the {@link WebView} and installed + * plugins in the system. You can find this class through + * {@link PluginManager#getInstance}. + * + * @hide pending API solidification + */ +public class PluginManager { + + /** + * Service Action: A plugin wishes to be loaded in the WebView must provide + * {@link android.content.IntentFilter IntentFilter} that accepts this + * action in their AndroidManifest.xml. + * <p> + * TODO: we may change this to a new PLUGIN_ACTION if this is going to be + * public. + */ + @SdkConstant(SdkConstantType.SERVICE_ACTION) + public static final String PLUGIN_ACTION = "android.webkit.PLUGIN"; + + /** + * A plugin wishes to be loaded in the WebView must provide this permission + * in their AndroidManifest.xml. + */ + public static final String PLUGIN_PERMISSION = "android.webkit.permission.PLUGIN"; + + private static final String LOGTAG = "webkit"; + + private static PluginManager mInstance = null; + + private final Context mContext; + + private PluginManager(Context context) { + mContext = context; + } + + public static synchronized PluginManager getInstance(Context context) { + if (mInstance == null) { + if (context == null) { + throw new IllegalStateException( + "First call to PluginManager need a valid context."); + } + mInstance = new PluginManager(context); + } + return mInstance; + } + + /** + * Signal the WebCore thread to refresh its list of plugins. Use this if the + * directory contents of one of the plugin directories has been modified and + * needs its changes reflecting. May cause plugin load and/or unload. + * + * @param reloadOpenPages Set to true to reload all open pages. + */ + public void refreshPlugins(boolean reloadOpenPages) { + BrowserFrame.sJavaBridge.obtainMessage( + JWebCoreJavaBridge.REFRESH_PLUGINS, reloadOpenPages) + .sendToTarget(); + } + + String[] getPluginDirecoties() { + ArrayList<String> directories = new ArrayList<String>(); + PackageManager pm = mContext.getPackageManager(); + List<ResolveInfo> plugins = pm.queryIntentServices(new Intent( + PLUGIN_ACTION), PackageManager.GET_SERVICES); + for (ResolveInfo info : plugins) { + ServiceInfo serviceInfo = info.serviceInfo; + if (serviceInfo == null) { + Log.w(LOGTAG, "Ignore bad plugin"); + continue; + } + PackageInfo pkgInfo; + try { + pkgInfo = pm.getPackageInfo(serviceInfo.packageName, + PackageManager.GET_PERMISSIONS + | PackageManager.GET_SIGNATURES); + } catch (NameNotFoundException e) { + Log.w(LOGTAG, "Cant find plugin: " + serviceInfo.packageName); + continue; + } + if (pkgInfo == null) { + continue; + } + String directory = pkgInfo.applicationInfo.dataDir + "/lib"; + if (directories.contains(directory)) { + continue; + } + String permissions[] = pkgInfo.requestedPermissions; + if (permissions == null) { + continue; + } + boolean permissionOk = false; + for (String permit : permissions) { + if (PLUGIN_PERMISSION.equals(permit)) { + permissionOk = true; + break; + } + } + if (!permissionOk) { + continue; + } + Signature signatures[] = pkgInfo.signatures; + if (signatures == null) { + continue; + } + boolean signatureMatch = false; + for (Signature signature : signatures) { + // TODO: check signature against Google provided one + signatureMatch = true; + break; + } + if (!signatureMatch) { + continue; + } + directories.add(directory); + } + // hack for gears for now + String gears = mContext.getDir("plugins", 0).getPath(); + if (!directories.contains(gears)) { + directories.add(gears); + } + return directories.toArray(new String[directories.size()]); + } +} diff --git a/core/java/android/webkit/SslErrorHandler.java b/core/java/android/webkit/SslErrorHandler.java index 5f84bbe..cc1e750 100644 --- a/core/java/android/webkit/SslErrorHandler.java +++ b/core/java/android/webkit/SslErrorHandler.java @@ -120,7 +120,7 @@ public class SslErrorHandler extends Handler { * Handles SSL error(s) on the way up to the user. */ /* package */ synchronized void handleSslErrorRequest(LoadListener loader) { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.SSL_ERROR_HANDLER) { Log.v(LOGTAG, "SslErrorHandler.handleSslErrorRequest(): " + "url=" + loader.url()); } @@ -157,14 +157,14 @@ public class SslErrorHandler extends Handler { SslError error = loader.sslError(); - if (WebView.DEBUG) { + if (DebugFlags.SSL_ERROR_HANDLER) { Assert.assertNotNull(error); } int primary = error.getPrimaryError(); String host = loader.host(); - if (WebView.DEBUG) { + if (DebugFlags.SSL_ERROR_HANDLER) { Assert.assertTrue(host != null && primary != 0); } @@ -205,11 +205,11 @@ public class SslErrorHandler extends Handler { */ /* package */ synchronized void handleSslErrorResponse(boolean proceed) { LoadListener loader = mLoaderQueue.poll(); - if (WebView.DEBUG) { + if (DebugFlags.SSL_ERROR_HANDLER) { Assert.assertNotNull(loader); } - if (WebView.LOGV_ENABLED) { + if (DebugFlags.SSL_ERROR_HANDLER) { Log.v(LOGTAG, "SslErrorHandler.handleSslErrorResponse():" + " proceed: " + proceed + " url:" + loader.url()); @@ -221,13 +221,13 @@ public class SslErrorHandler extends Handler { int primary = loader.sslError().getPrimaryError(); String host = loader.host(); - if (WebView.DEBUG) { + if (DebugFlags.SSL_ERROR_HANDLER) { Assert.assertTrue(host != null && primary != 0); } boolean hasKey = mSslPrefTable.containsKey(host); if (!hasKey || primary > mSslPrefTable.getInt(host)) { - mSslPrefTable.putInt(host, new Integer(primary)); + mSslPrefTable.putInt(host, primary); } } loader.handleSslErrorResponse(proceed); diff --git a/core/java/android/webkit/StreamLoader.java b/core/java/android/webkit/StreamLoader.java index 705157c..eab3350 100644 --- a/core/java/android/webkit/StreamLoader.java +++ b/core/java/android/webkit/StreamLoader.java @@ -102,7 +102,7 @@ abstract class StreamLoader extends Handler { // to pass data to the loader mData = new byte[8192]; sendHeaders(); - while (!sendData()); + while (!sendData() && !mHandler.cancelled()); closeStreamAndSendEndData(); mHandler.loadSynchronousMessages(); } @@ -113,9 +113,13 @@ abstract class StreamLoader extends Handler { * @see android.os.Handler#handleMessage(android.os.Message) */ public void handleMessage(Message msg) { - if (WebView.DEBUG && mHandler.isSynchronous()) { + if (DebugFlags.STREAM_LOADER && mHandler.isSynchronous()) { throw new AssertionError(); } + if (mHandler.cancelled()) { + closeStreamAndSendEndData(); + return; + } switch(msg.what) { case MSG_STATUS: if (setupStreamAndSendStatus()) { diff --git a/core/java/android/webkit/URLUtil.java b/core/java/android/webkit/URLUtil.java index d6ac3e9..de70fc2 100644 --- a/core/java/android/webkit/URLUtil.java +++ b/core/java/android/webkit/URLUtil.java @@ -61,7 +61,7 @@ public final class URLUtil { webAddress = new WebAddress(inUrl); } catch (ParseException ex) { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.URL_UTIL) { Log.v(LOGTAG, "smartUrlFilter: failed to parse url = " + inUrl); } return retVal; diff --git a/core/java/android/webkit/WebBackForwardList.java b/core/java/android/webkit/WebBackForwardList.java index ffd6a11..62a5531 100644 --- a/core/java/android/webkit/WebBackForwardList.java +++ b/core/java/android/webkit/WebBackForwardList.java @@ -137,7 +137,7 @@ public class WebBackForwardList implements Cloneable, Serializable { // when removing the first item, we can assert that the index is 0. // This lets us change the current index without having to query the // native BackForwardList. - if (WebView.DEBUG && (index != 0)) { + if (DebugFlags.WEB_BACK_FORWARD_LIST && (index != 0)) { throw new AssertionError(); } final WebHistoryItem h = mArray.remove(index); diff --git a/core/java/android/webkit/WebChromeClient.java b/core/java/android/webkit/WebChromeClient.java index 9d9763c..754b1d9 100644 --- a/core/java/android/webkit/WebChromeClient.java +++ b/core/java/android/webkit/WebChromeClient.java @@ -158,6 +158,23 @@ public class WebChromeClient { return false; } + /** + * Tell the client that the database quota for the origin has been exceeded. + * @param url The URL that triggered the notification + * @param databaseIdentifier The identifier of the database that caused the + * quota overflow. + * @param currentQuota The current quota for the origin. + * @param quotaUpdater A callback to inform the WebCore thread that a new + * quota is available. This callback must always be executed at some + * point to ensure that the sleeping WebCore thread is woken up. + */ + public void onExceededDatabaseQuota(String url, String databaseIdentifier, + long currentQuota, WebStorage.QuotaUpdater quotaUpdater) { + // This default implementation passes the current quota back to WebCore. + // WebCore will interpret this that new quota was declined. + quotaUpdater.updateQuota(currentQuota); + } + /** * Tell the client that a JavaScript execution timeout has occured. And the * client may decide whether or not to interrupt the execution. If the @@ -172,4 +189,15 @@ public class WebChromeClient { public boolean onJsTimeout() { return true; } + + /** + * Add a JavaScript error message to the console. Clients should override + * this to process the log message as they see fit. + * @param message The error message to report. + * @param lineNumber The line number of the error. + * @param sourceID The name of the source file that caused the error. + * @hide pending API council. + */ + public void addMessageToConsole(String message, int lineNumber, String sourceID) { + } } diff --git a/core/java/android/webkit/WebSettings.java b/core/java/android/webkit/WebSettings.java index dcba943..ea186fd 100644 --- a/core/java/android/webkit/WebSettings.java +++ b/core/java/android/webkit/WebSettings.java @@ -111,9 +111,13 @@ public class WebSettings { private boolean mSyncPending = false; // Custom handler that queues messages until the WebCore thread is active. private final EventHandler mEventHandler; + // Private settings so we don't have to go into native code to // retrieve the values. After setXXX, postSync() needs to be called. - // XXX: The default values need to match those in WebSettings.cpp + // + // The default values need to match those in WebSettings.cpp + // If the defaults change, please also update the JavaDocs so developers + // know what they are. private LayoutAlgorithm mLayoutAlgorithm = LayoutAlgorithm.NARROW_COLUMNS; private Context mContext; private TextSize mTextSize = TextSize.NORMAL; @@ -127,7 +131,6 @@ public class WebSettings { private String mUserAgent; private boolean mUseDefaultUserAgent; private String mAcceptLanguage; - private String mPluginsPath = ""; private int mMinimumFontSize = 8; private int mMinimumLogicalFontSize = 8; private int mDefaultFontSize = 16; @@ -137,6 +140,7 @@ public class WebSettings { private boolean mBlockNetworkLoads; private boolean mJavaScriptEnabled = false; private boolean mPluginsEnabled = false; + private long mWebStorageDefaultQuota = 0; private boolean mJavaScriptCanOpenWindowsAutomatically = false; private boolean mUseDoubleTree = false; private boolean mUseWideViewport = false; @@ -155,6 +159,10 @@ public class WebSettings { private boolean mSupportZoom = true; private boolean mBuiltInZoomControls = false; private boolean mAllowFileAccess = true; + private String mDatabasePath = ""; + private boolean mDatabaseEnabled = false; + private String mAppCachePath = ""; + private boolean mAppCacheEnabled = false; // Class to handle messages before WebCore is ready. private class EventHandler { @@ -461,24 +469,21 @@ public class WebSettings { } /** - * Tell the WebView to use the double tree rendering algorithm. - * @param use True if the WebView is to use double tree rendering, false - * otherwise. + * @deprecated This setting controlled a rendering optimization + * that is no longer present. Setting it now has no effect. */ + @Deprecated public synchronized void setUseDoubleTree(boolean use) { - if (mUseDoubleTree != use) { - mUseDoubleTree = use; - postSync(); - } + return; } /** - * Return true if the WebView is using the double tree rendering algorithm. - * @return True if the WebView is using the double tree rendering - * algorithm. + * @deprecated This setting controlled a rendering optimization + * that is no longer present. Setting it now has no effect. */ + @Deprecated public synchronized boolean getUseDoubleTree() { - return mUseDoubleTree; + return false; } /** @@ -583,7 +588,7 @@ public class WebSettings { } /** - * Return the current layout algorithm. + * Return the current layout algorithm. The default is NARROW_COLUMNS. * @return LayoutAlgorithm enum value describing the layout algorithm * being used. * @see WebSettings.LayoutAlgorithm @@ -604,7 +609,7 @@ public class WebSettings { } /** - * Get the standard font family name. + * Get the standard font family name. The default is "sans-serif". * @return The standard font family name as a string. */ public synchronized String getStandardFontFamily() { @@ -623,7 +628,7 @@ public class WebSettings { } /** - * Get the fixed font family name. + * Get the fixed font family name. The default is "monospace". * @return The fixed font family name as a string. */ public synchronized String getFixedFontFamily() { @@ -650,7 +655,7 @@ public class WebSettings { } /** - * Set the serif font family name. + * Set the serif font family name. The default is "sans-serif". * @param font A font family name. */ public synchronized void setSerifFontFamily(String font) { @@ -661,7 +666,7 @@ public class WebSettings { } /** - * Get the serif font family name. + * Get the serif font family name. The default is "serif". * @return The serif font family name as a string. */ public synchronized String getSerifFontFamily() { @@ -680,7 +685,7 @@ public class WebSettings { } /** - * Get the cursive font family name. + * Get the cursive font family name. The default is "cursive". * @return The cursive font family name as a string. */ public synchronized String getCursiveFontFamily() { @@ -699,7 +704,7 @@ public class WebSettings { } /** - * Get the fantasy font family name. + * Get the fantasy font family name. The default is "fantasy". * @return The fantasy font family name as a string. */ public synchronized String getFantasyFontFamily() { @@ -720,7 +725,7 @@ public class WebSettings { } /** - * Get the minimum font size. + * Get the minimum font size. The default is 8. * @return A non-negative integer between 1 and 72. */ public synchronized int getMinimumFontSize() { @@ -741,7 +746,7 @@ public class WebSettings { } /** - * Get the minimum logical font size. + * Get the minimum logical font size. The default is 8. * @return A non-negative integer between 1 and 72. */ public synchronized int getMinimumLogicalFontSize() { @@ -762,7 +767,7 @@ public class WebSettings { } /** - * Get the default font size. + * Get the default font size. The default is 16. * @return A non-negative integer between 1 and 72. */ public synchronized int getDefaultFontSize() { @@ -783,7 +788,7 @@ public class WebSettings { } /** - * Get the default fixed font size. + * Get the default fixed font size. The default is 16. * @return A non-negative integer between 1 and 72. */ public synchronized int getDefaultFixedFontSize() { @@ -803,6 +808,7 @@ public class WebSettings { /** * Return true if the WebView will load image resources automatically. + * The default is true. * @return True if the WebView loads images automatically. */ public synchronized boolean getLoadsImagesAutomatically() { @@ -822,16 +828,16 @@ public class WebSettings { } /** - * Return true if the WebView will block network image. + * Return true if the WebView will block network image. The default is false. * @return True if the WebView blocks network image. */ public synchronized boolean getBlockNetworkImage() { return mBlockNetworkImage; } - + /** * @hide - * Tell the WebView to block all network load requests. + * Tell the WebView to block all network load requests. * @param flag True if the WebView should block all network loads */ public synchronized void setBlockNetworkLoads(boolean flag) { @@ -844,13 +850,14 @@ public class WebSettings { /** * @hide * Return true if the WebView will block all network loads. + * The default is false. * @return True if the WebView blocks all network loads. */ public synchronized boolean getBlockNetworkLoads() { return mBlockNetworkLoads; } - - + + private void verifyNetworkAccess() { if (!mBlockNetworkLoads) { if (mContext.checkPermission("android.permission.INTERNET", @@ -886,19 +893,94 @@ public class WebSettings { } /** - * Set a custom path to plugins used by the WebView. The client - * must ensure it exists before this call. - * @param pluginsPath String path to the directory containing plugins. + * TODO: need to add @Deprecated */ public synchronized void setPluginsPath(String pluginsPath) { - if (pluginsPath != null && !pluginsPath.equals(mPluginsPath)) { - mPluginsPath = pluginsPath; + } + + /** + * @hide + * Set the default quota for WebStorage DBs + * @param quota the default quota in bytes + */ + public synchronized void setWebStorageDefaultQuota(long quota) { + if (mWebStorageDefaultQuota != quota) { + mWebStorageDefaultQuota = quota; + postSync(); + } + } + + /** + * Set the path to where database storage API databases should be saved. + * This will update WebCore when the Sync runs in the C++ side. + * @param databasePath String path to the directory where databases should + * be saved. May be the empty string but should never be null. + */ + public synchronized void setDatabasePath(String databasePath) { + if (databasePath != null && !databasePath.equals(mDatabasePath)) { + mDatabasePath = databasePath; + postSync(); + } + } + + /** + * Tell the WebView to enable Application Caches API. + * @param flag True if the WebView should enable Application Caches. + * @hide pending api council approval + */ + public synchronized void setAppCacheEnabled(boolean flag) { + if (mAppCacheEnabled != flag) { + mAppCacheEnabled = flag; + postSync(); + } + } + + /** + * Set a custom path to the Application Caches files. The client + * must ensure it exists before this call. + * @param appCachePath String path to the directory containing Application + * Caches files. The appCache path can be the empty string but should not + * be null. Passing null for this parameter will result in a no-op. + * @hide pending api council approval + */ + public synchronized void setAppCachePath(String appCachePath) { + if (appCachePath != null && !appCachePath.equals(mAppCachePath)) { + mAppCachePath = appCachePath; postSync(); } } /** - * Return true if javascript is enabled. + * Set whether the database storage API is enabled. + * @param flag boolean True if the WebView should use the database storage + * API. + */ + public synchronized void setDatabaseEnabled(boolean flag) { + if (mDatabaseEnabled != flag) { + mDatabaseEnabled = flag; + postSync(); + } + } + + /** + * Return the path to where database storage API databases are saved for + * the current WebView. + * @return the String path to the database storage API databases. + */ + public synchronized String getDatabasePath() { + return mDatabasePath; + } + + /** + * Returns true if database storage API is enabled. + * @return True if the database storage API is enabled. + */ + public synchronized boolean getDatabaseEnabled() { + return mDatabaseEnabled; + } + + /** + * Return true if javascript is enabled. <b>Note: The default is false.</b> * @return True if javascript is enabled. */ public synchronized boolean getJavaScriptEnabled() { @@ -914,11 +996,19 @@ public class WebSettings { } /** - * Return the current path used for plugins in the WebView. - * @return The string path to the WebView plugins. + * TODO: need to add @Deprecated */ public synchronized String getPluginsPath() { - return mPluginsPath; + return ""; + } + + /** + * @hide + * Return the default quota for WebStorage DBs + * @return the default quota in bytes + */ + public synchronized long getWebStorageDefaultQuota() { + return mWebStorageDefaultQuota; } /** @@ -935,7 +1025,8 @@ public class WebSettings { } /** - * Return true if javascript can open windows automatically. + * Return true if javascript can open windows automatically. The default + * is false. * @return True if javascript can open windows automatically during * window.open(). */ @@ -955,7 +1046,7 @@ public class WebSettings { } /** - * Get the default text encoding name. + * Get the default text encoding name. The default is "Latin-1". * @return The default text encoding name as a string. */ public synchronized String getDefaultTextEncodingName() { @@ -1044,8 +1135,8 @@ public class WebSettings { /** * Set the priority of the Render thread. Unlike the other settings, this - * one only needs to be called once per process. - * + * one only needs to be called once per process. The default is NORMAL. + * * @param priority RenderPriority, can be normal, high or low. */ public synchronized void setRenderPriority(RenderPriority priority) { @@ -1099,7 +1190,7 @@ public class WebSettings { /*package*/ synchronized void syncSettingsAndCreateHandler(BrowserFrame frame) { mBrowserFrame = frame; - if (WebView.DEBUG) { + if (DebugFlags.WEB_SETTINGS) { junit.framework.Assert.assertTrue(frame.mNativeFrame != 0); } nativeSync(frame.mNativeFrame); diff --git a/core/java/android/webkit/WebStorage.java b/core/java/android/webkit/WebStorage.java new file mode 100644 index 0000000..f27360d --- /dev/null +++ b/core/java/android/webkit/WebStorage.java @@ -0,0 +1,283 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.webkit; + +import android.os.Handler; +import android.os.Message; +import android.util.Log; + +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.HashMap; +import java.util.Vector; + +/** + * Functionality for manipulating the webstorage databases. + */ +public final class WebStorage { + + /** + * Encapsulates a callback function to be executed when a new quota is made + * available. We primarily want this to allow us to call back the sleeping + * WebCore thread from outside the WebViewCore class (as the native call + * is private). It is imperative that this the setDatabaseQuota method is + * executed once a decision to either allow or deny new quota is made, + * otherwise the WebCore thread will remain asleep. + */ + public interface QuotaUpdater { + public void updateQuota(long newQuota); + }; + + // Log tag + private static final String TAG = "webstorage"; + + // Global instance of a WebStorage + private static WebStorage sWebStorage; + + // We keep a copy of the origins, quotas and usages + // that we protect via a lock and update in syncValues() + private static Lock mLock = new ReentrantLock(); + private static Condition mCacheUpdated = mLock.newCondition(); + + // Message ids + static final int UPDATE = 0; + static final int SET_QUOTA_ORIGIN = 1; + static final int DELETE_ORIGIN = 2; + static final int DELETE_ALL = 3; + + private Vector <String> mOrigins; + private HashMap <String, Long> mQuotas = new HashMap<String, Long>(); + private HashMap <String, Long> mUsages = new HashMap<String, Long>(); + + private Handler mHandler = null; + + private class Origin { + String mOrigin = null; + long mQuota = 0; + + public Origin(String origin, long quota) { + mOrigin = origin; + mQuota = quota; + } + + public Origin(String origin) { + mOrigin = origin; + } + + public String getOrigin() { + return mOrigin; + } + + public long getQuota() { + return mQuota; + } + } + + /** + * @hide + * Message handler + */ + public void createHandler() { + if (mHandler == null) { + mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case SET_QUOTA_ORIGIN: { + Origin website = (Origin) msg.obj; + nativeSetQuotaForOrigin(website.getOrigin(), + website.getQuota()); + syncValues(); + } break; + + case DELETE_ORIGIN: { + Origin website = (Origin) msg.obj; + nativeDeleteOrigin(website.getOrigin()); + syncValues(); + } break; + + case DELETE_ALL: + nativeDeleteAllDatabases(); + syncValues(); + break; + + case UPDATE: + syncValues(); + break; + } + } + }; + } + } + + /** + * @hide + * Returns a list of origins having a database + */ + public Vector getOrigins() { + Vector ret = null; + mLock.lock(); + try { + update(); + mCacheUpdated.await(); + ret = mOrigins; + } catch (InterruptedException e) { + Log.e(TAG, "Exception while waiting on the updated origins", e); + } finally { + mLock.unlock(); + } + return ret; + } + + /** + * @hide + * Returns the use for a given origin + */ + public long getUsageForOrigin(String origin) { + long ret = 0; + if (origin == null) { + return ret; + } + mLock.lock(); + try { + update(); + mCacheUpdated.await(); + Long usage = mUsages.get(origin); + if (usage != null) { + ret = usage.longValue(); + } + } catch (InterruptedException e) { + Log.e(TAG, "Exception while waiting on the updated origins", e); + } finally { + mLock.unlock(); + } + return ret; + } + + /** + * @hide + * Returns the quota for a given origin + */ + public long getQuotaForOrigin(String origin) { + long ret = 0; + if (origin == null) { + return ret; + } + mLock.lock(); + try { + update(); + mCacheUpdated.await(); + Long quota = mQuotas.get(origin); + if (quota != null) { + ret = quota.longValue(); + } + } catch (InterruptedException e) { + Log.e(TAG, "Exception while waiting on the updated origins", e); + } finally { + mLock.unlock(); + } + return ret; + } + + /** + * @hide + * Set the quota for a given origin + */ + public void setQuotaForOrigin(String origin, long quota) { + if (origin != null) { + postMessage(Message.obtain(null, SET_QUOTA_ORIGIN, + new Origin(origin, quota))); + } + } + + /** + * @hide + * Delete a given origin + */ + public void deleteOrigin(String origin) { + if (origin != null) { + postMessage(Message.obtain(null, DELETE_ORIGIN, + new Origin(origin))); + } + } + + /** + * @hide + * Delete all databases + */ + public void deleteAllDatabases() { + postMessage(Message.obtain(null, DELETE_ALL)); + } + + /** + * Utility function to send a message to our handler + */ + private void postMessage(Message msg) { + if (mHandler != null) { + mHandler.sendMessage(msg); + } + } + + /** + * @hide + * Get the global instance of WebStorage. + * @return A single instance of WebStorage. + */ + public static WebStorage getInstance() { + if (sWebStorage == null) { + sWebStorage = new WebStorage(); + } + return sWebStorage; + } + + /** + * @hide + * Post a Sync request + */ + public void update() { + postMessage(Message.obtain(null, UPDATE)); + } + + /** + * Run on the webcore thread + * sync the local cached values with the real ones + */ + private void syncValues() { + mLock.lock(); + Vector tmp = nativeGetOrigins(); + mOrigins = new Vector<String>(); + mQuotas.clear(); + mUsages.clear(); + for (int i = 0; i < tmp.size(); i++) { + String origin = (String) tmp.get(i); + mOrigins.add(origin); + mQuotas.put(origin, new Long(nativeGetQuotaForOrigin(origin))); + mUsages.put(origin, new Long(nativeGetUsageForOrigin(origin))); + } + mCacheUpdated.signal(); + mLock.unlock(); + } + + // Native functions + private static native Vector nativeGetOrigins(); + private static native long nativeGetUsageForOrigin(String origin); + private static native long nativeGetQuotaForOrigin(String origin); + private static native void nativeSetQuotaForOrigin(String origin, long quota); + private static native void nativeDeleteOrigin(String origin); + private static native void nativeDeleteAllDatabases(); +} diff --git a/core/java/android/webkit/WebSyncManager.java b/core/java/android/webkit/WebSyncManager.java index ded17ed..d3ec603 100644 --- a/core/java/android/webkit/WebSyncManager.java +++ b/core/java/android/webkit/WebSyncManager.java @@ -47,7 +47,7 @@ abstract class WebSyncManager implements Runnable { @Override public void handleMessage(Message msg) { if (msg.what == SYNC_MESSAGE) { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.WEB_SYNC_MANAGER) { Log.v(LOGTAG, "*** WebSyncManager sync ***"); } syncFromRamToFlash(); @@ -94,7 +94,7 @@ abstract class WebSyncManager implements Runnable { * sync() forces sync manager to sync now */ public void sync() { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.WEB_SYNC_MANAGER) { Log.v(LOGTAG, "*** WebSyncManager sync ***"); } if (mHandler == null) { @@ -109,7 +109,7 @@ abstract class WebSyncManager implements Runnable { * resetSync() resets sync manager's timer */ public void resetSync() { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.WEB_SYNC_MANAGER) { Log.v(LOGTAG, "*** WebSyncManager resetSync ***"); } if (mHandler == null) { @@ -124,7 +124,7 @@ abstract class WebSyncManager implements Runnable { * startSync() requests sync manager to start sync */ public void startSync() { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.WEB_SYNC_MANAGER) { Log.v(LOGTAG, "*** WebSyncManager startSync ***, Ref count:" + mStartSyncRefCount); } @@ -142,7 +142,7 @@ abstract class WebSyncManager implements Runnable { * the queue to break the sync loop */ public void stopSync() { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.WEB_SYNC_MANAGER) { Log.v(LOGTAG, "*** WebSyncManager stopSync ***, Ref count:" + mStartSyncRefCount); } diff --git a/core/java/android/webkit/TextDialog.java b/core/java/android/webkit/WebTextView.java index 99de56d..4a8fa3c 100644 --- a/core/java/android/webkit/TextDialog.java +++ b/core/java/android/webkit/WebTextView.java @@ -17,19 +17,11 @@ package android.webkit; import android.content.Context; -import android.graphics.Color; -import android.graphics.Paint; import android.graphics.Rect; -import android.graphics.drawable.ColorDrawable; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.LayerDrawable; -import android.graphics.drawable.ShapeDrawable; -import android.graphics.drawable.shapes.RectShape; import android.text.Editable; import android.text.InputFilter; import android.text.Selection; import android.text.Spannable; -import android.text.TextPaint; import android.text.TextUtils; import android.text.method.MovementMethod; import android.view.inputmethod.EditorInfo; @@ -47,11 +39,11 @@ import android.widget.TextView; import java.util.ArrayList; /** - * TextDialog is a specialized version of EditText used by WebView + * WebTextView is a specialized version of EditText used by WebView * to overlay html textfields (and textareas) to use our standard * text editing. */ -/* package */ class TextDialog extends AutoCompleteTextView { +/* package */ class WebTextView extends AutoCompleteTextView { private WebView mWebView; private boolean mSingle; @@ -79,39 +71,17 @@ import java.util.ArrayList; private static final InputFilter[] NO_FILTERS = new InputFilter[0]; /** - * Create a new TextDialog. - * @param context The Context for this TextDialog. + * Create a new WebTextView. + * @param context The Context for this WebTextView. * @param webView The WebView that created this. */ - /* package */ TextDialog(Context context, WebView webView) { + /* package */ WebTextView(Context context, WebView webView) { super(context); mWebView = webView; - ShapeDrawable background = new ShapeDrawable(new RectShape()); - Paint shapePaint = background.getPaint(); - shapePaint.setStyle(Paint.Style.STROKE); - ColorDrawable color = new ColorDrawable(Color.WHITE); - Drawable[] array = new Drawable[2]; - array[0] = color; - array[1] = background; - LayerDrawable layers = new LayerDrawable(array); - // Hide WebCore's text behind this and allow the WebView - // to draw its own focusring. - setBackgroundDrawable(layers); - // Align the text better with the text behind it, so moving - // off of the textfield will not appear to move the text. - setPadding(3, 2, 0, 0); mMaxLength = -1; - // Turn on subpixel text, and turn off kerning, so it better matches - // the text in webkit. - TextPaint paint = getPaint(); - int flags = paint.getFlags() | Paint.SUBPIXEL_TEXT_FLAG | - Paint.ANTI_ALIAS_FLAG & ~Paint.DEV_KERN_TEXT_FLAG; - paint.setFlags(flags); - // Set the text color to black, regardless of the theme. This ensures - // that other applications that use embedded WebViews will properly - // display the text in textfields. - setTextColor(Color.BLACK); setImeOptions(EditorInfo.IME_ACTION_NONE); + // Allow webkit's drawing to show through + setWillNotDraw(true); } @Override @@ -125,7 +95,7 @@ import java.util.ArrayList; Spannable text = (Spannable) getText(); int oldLength = text.length(); // Normally the delete key's dom events are sent via onTextChanged. - // However, if the length is zero, the text did not change, so we + // However, if the length is zero, the text did not change, so we // go ahead and pass the key down immediately. if (KeyEvent.KEYCODE_DEL == keyCode && 0 == oldLength) { sendDomEvent(event); @@ -151,6 +121,10 @@ import java.util.ArrayList; if (isPopupShowing()) { return super.dispatchKeyEvent(event); } + if (!mWebView.nativeCursorMatchesFocus()) { + return down ? mWebView.onKeyDown(keyCode, event) : mWebView + .onKeyUp(keyCode, event); + } // Center key should be passed to a potential onClick if (!down) { mWebView.shortPressOnTextField(); @@ -158,6 +132,20 @@ import java.util.ArrayList; // Pass to super to handle longpress. return super.dispatchKeyEvent(event); } + boolean isArrowKey = false; + switch(keyCode) { + case KeyEvent.KEYCODE_DPAD_LEFT: + case KeyEvent.KEYCODE_DPAD_RIGHT: + case KeyEvent.KEYCODE_DPAD_UP: + case KeyEvent.KEYCODE_DPAD_DOWN: + if (!mWebView.nativeCursorMatchesFocus()) { + return down ? mWebView.onKeyDown(keyCode, event) : mWebView + .onKeyUp(keyCode, event); + + } + isArrowKey = true; + break; + } // Ensure there is a layout so arrow keys are handled properly. if (getLayout() == null) { @@ -177,7 +165,7 @@ import java.util.ArrayList; oldText = ""; } if (super.dispatchKeyEvent(event)) { - // If the TextDialog handled the key it was either an alphanumeric + // If the WebTextView handled the key it was either an alphanumeric // key, a delete, or a movement within the text. All of those are // ok to pass to javascript. @@ -187,22 +175,11 @@ import java.util.ArrayList; // so do not pass down to javascript, and instead // return true. If it is an arrow key or a delete key, we can go // ahead and pass it down. - boolean isArrowKey; - switch(keyCode) { - case KeyEvent.KEYCODE_DPAD_LEFT: - case KeyEvent.KEYCODE_DPAD_RIGHT: - case KeyEvent.KEYCODE_DPAD_UP: - case KeyEvent.KEYCODE_DPAD_DOWN: - isArrowKey = true; - break; - case KeyEvent.KEYCODE_ENTER: - // For multi-line text boxes, newlines will - // trigger onTextChanged for key down (which will send both - // key up and key down) but not key up. - mGotEnterDown = true; - default: - isArrowKey = false; - break; + if (KeyEvent.KEYCODE_ENTER == keyCode) { + // For multi-line text boxes, newlines will + // trigger onTextChanged for key down (which will send both + // key up and key down) but not key up. + mGotEnterDown = true; } if (maxedOut && !isArrowKey && keyCode != KeyEvent.KEYCODE_DEL) { if (oldEnd == oldStart) { @@ -225,32 +202,31 @@ import java.util.ArrayList; return true; } } + /* FIXME: + * In theory, we would like to send the events for the arrow keys. + * However, the TextView can arbitrarily change the selection (i.e. + * long press followed by using the trackball). Therefore, we keep + * in sync with the TextView via onSelectionChanged. If we also + * send the DOM event, we lose the correct selection. if (isArrowKey) { // Arrow key does not change the text, but we still want to send // the DOM events. sendDomEvent(event); } + */ mScrollToAccommodateCursor = true; return true; } - // FIXME: TextViews return false for up and down key events even though - // they change the selection. Since we don't want the get out of sync - // with WebCore's notion of the current selection, reset the selection - // to what it was before the key event. - Selection.setSelection(text, oldStart, oldEnd); // Ignore the key up event for newlines. This prevents // multiple newlines in the native textarea. if (mGotEnterDown && !down) { return true; } // if it is a navigation key, pass it to WebView - if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT - || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT - || keyCode == KeyEvent.KEYCODE_DPAD_UP - || keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { + if (isArrowKey) { // WebView check the trackballtime in onKeyDown to avoid calling - // native from both trackball and key handling. As this is called - // from TextDialog, we always want WebView to check with native. + // native from both trackball and key handling. As this is called + // from WebTextView, we always want WebView to check with native. // Reset trackballtime to ensure it. mWebView.resetTrackballTime(); return down ? mWebView.onKeyDown(keyCode, event) : mWebView @@ -260,9 +236,9 @@ import java.util.ArrayList; } /** - * Create a fake touch up event at (x,y) with respect to this TextDialog. + * Create a fake touch up event at (x,y) with respect to this WebTextView. * This is used by WebView to act as though a touch event which happened - * before we placed the TextDialog actually hit it, so that it can place + * before we placed the WebTextView actually hit it, so that it can place * the cursor accordingly. */ /* package */ void fakeTouchEvent(float x, float y) { @@ -279,10 +255,10 @@ import java.util.ArrayList; } /** - * Determine whether this TextDialog currently represents the node + * Determine whether this WebTextView currently represents the node * represented by ptr. * @param ptr Pointer to a node to compare to. - * @return boolean Whether this TextDialog already represents the node + * @return boolean Whether this WebTextView already represents the node * pointed to by ptr. */ /* package */ boolean isSameTextField(int ptr) { @@ -290,20 +266,19 @@ import java.util.ArrayList; } @Override - public boolean onPreDraw() { - if (getLayout() == null) { - measure(mWidthSpec, mHeightSpec); + protected void onSelectionChanged(int selStart, int selEnd) { + if (mWebView != null) { + mWebView.setSelection(selStart, selEnd); } - return super.onPreDraw(); } - + @Override protected void onTextChanged(CharSequence s,int start,int before,int count){ super.onTextChanged(s, start, before, count); String postChange = s.toString(); // Prevent calls to setText from invoking onTextChanged (since this will // mean we are on a different textfield). Also prevent the change when - // going from a textfield with a string of text to one with a smaller + // going from a textfield with a string of text to one with a smaller // limit on text length from registering the onTextChanged event. if (mPreChange == null || mPreChange.equals(postChange) || (mMaxLength > -1 && mPreChange.length() > mMaxLength && @@ -311,8 +286,7 @@ import java.util.ArrayList; return; } mPreChange = postChange; - // This was simply a delete or a cut, so just delete the - // selection. + // This was simply a delete or a cut, so just delete the selection. if (before > 0 && 0 == count) { mWebView.deleteSelection(start, start + before); // For this and all changes to the text, update our cache @@ -337,24 +311,23 @@ import java.util.ArrayList; start + count - charactersFromKeyEvents, start + count - charactersFromKeyEvents); } else { - // This corrects the selection which may have been affected by the + // This corrects the selection which may have been affected by the // trackball or auto-correct. mWebView.setSelection(start, start + before); } - updateCachedTextfield(); - if (cannotUseKeyEvents) { - return; - } - int length = events.length; - for (int i = 0; i < length; i++) { - // We never send modifier keys to native code so don't send them - // here either. - if (!KeyEvent.isModifierKey(events[i].getKeyCode())) { - sendDomEvent(events[i]); + if (!cannotUseKeyEvents) { + int length = events.length; + for (int i = 0; i < length; i++) { + // We never send modifier keys to native code so don't send them + // here either. + if (!KeyEvent.isModifierKey(events[i].getKeyCode())) { + sendDomEvent(events[i]); + } } } + updateCachedTextfield(); } - + @Override public boolean onTrackballEvent(MotionEvent event) { if (isPopupShowing()) { @@ -363,16 +336,16 @@ import java.util.ArrayList; if (event.getAction() != MotionEvent.ACTION_MOVE) { return false; } + // If the Cursor is not on the text input, webview should handle the + // trackball + if (!mWebView.nativeCursorMatchesFocus()) { + return mWebView.onTrackballEvent(event); + } Spannable text = (Spannable) getText(); MovementMethod move = getMovementMethod(); if (move != null && getLayout() != null && move.onTrackballEvent(this, text, event)) { - // Need to pass down the selection, which has changed. - // FIXME: This should work, but does not, so we set the selection - // in onTextChanged. - //int start = Selection.getSelectionStart(text); - //int end = Selection.getSelectionEnd(text); - //mWebView.setSelection(start, end); + // Selection is changed in onSelectionChanged return true; } // If the user is in a textfield, and the movement method is not @@ -385,7 +358,7 @@ import java.util.ArrayList; } /** - * Remove this TextDialog from its host WebView, and return + * Remove this WebTextView from its host WebView, and return * focus to the host. */ /* package */ void remove() { @@ -414,7 +387,7 @@ import java.util.ArrayList; } return false; } - + /** * Send the DOM events for the specified event. * @param event KeyEvent to be translated into a DOM event. @@ -425,7 +398,7 @@ import java.util.ArrayList; /** * Always use this instead of setAdapter, as this has features specific to - * the TextDialog. + * the WebTextView. */ public void setAdapterCustom(AutoCompleteAdapter adapter) { if (adapter != null) { @@ -491,16 +464,16 @@ import java.util.ArrayList; /** * Set the pointer for this node so it can be determined which node this - * TextDialog represents. + * WebTextView represents. * @param ptr Integer representing the pointer to the node which this - * TextDialog represents. + * WebTextView represents. */ /* package */ void setNodePointer(int ptr) { mNodePointer = ptr; } /** - * Determine the position and size of TextDialog, and add it to the + * Determine the position and size of WebTextView, and add it to the * WebView's view heirarchy. All parameters are presumed to be in * view coordinates. Also requests Focus and sets the cursor to not * request to be in view. @@ -551,8 +524,8 @@ import java.util.ArrayList; } /** - * Set the text for this TextDialog, and set the selection to (start, end) - * @param text Text to go into this TextDialog. + * 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. */ @@ -583,7 +556,7 @@ import java.util.ArrayList; edit.replace(0, edit.length(), text); updateCachedTextfield(); } - + /** * Update the cache to reflect the current text. */ diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java index 563d819..2940983 100644 --- a/core/java/android/webkit/WebView.java +++ b/core/java/android/webkit/WebView.java @@ -58,7 +58,7 @@ import android.view.ViewParent; import android.view.ViewTreeObserver; import android.view.animation.AlphaAnimation; import android.view.inputmethod.InputMethodManager; -import android.webkit.TextDialog.AutoCompleteAdapter; +import android.webkit.WebTextView.AutoCompleteAdapter; import android.webkit.WebViewCore.EventHub; import android.widget.AbsoluteLayout; import android.widget.Adapter; @@ -84,7 +84,7 @@ import java.util.HashMap; import java.util.List; /** - * <p>A View that displays web pages. This class is the basis upon which you + * <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. * It uses the WebKit rendering engine to display * web pages and includes methods to navigate forward and backward @@ -93,12 +93,109 @@ import java.util.List; * {@link #getSettings() WebSettings}.{@link WebSettings#setBuiltInZoomControls(boolean)} * (introduced in API version 3). * <p>Note that, in order for your Activity to access the Internet and load web pages - * in a WebView, you must add the <var>INTERNET</var> permissions to your + * in a WebView, you must add the <var>INTERNET</var> permissions to your * Android Manifest file:</p> * <pre><uses-permission android:name="android.permission.INTERNET" /></pre> + * * <p>This must be a child of the <code><manifest></code> element.</p> + * + * <h3>Basic usage</h3> + * + * <p>By default, a WebView provides no browser-like widgets, does not + * enable JavaScript and errors will be ignored. If your goal is only + * to display some HTML as a part of your UI, this is probably fine; + * the user won't need to interact with the web page beyond reading + * it, and the web page won't need to interact with the user. If you + * actually want a fully blown web browser, then you probably want to + * invoke the Browser application with your URL rather than show it + * with a WebView. See {@link android.content.Intent} for more information.</p> + * + * <pre class="prettyprint"> + * WebView webview = new WebView(this); + * setContentView(webview); + * + * // Simplest usage: note that an exception will NOT be thrown + * // if there is an error loading this page (see below). + * webview.loadUrl("http://slashdot.org/"); + * + * // Of course you can also load from any string: + * String summary = "<html><body>You scored <b>192</b> points.</body></html>"; + * webview.loadData(summary, "text/html", "utf-8"); + * // ... although note that there are restrictions on what this HTML can do. + * // See the JavaDocs for loadData and loadDataWithBaseUrl for more info. + * </pre> + * + * <p>A WebView has several customization points where you can add your + * own behavior. These are:</p> + * + * <ul> + * <li>Creating and setting a {@link android.webkit.WebChromeClient} subclass. + * This class is called when something that might impact a + * browser UI happens, for instance, progress updates and + * JavaScript alerts are sent here. + * </li> + * <li>Creating and setting a {@link android.webkit.WebViewClient} subclass. + * It will be called when things happen that impact the + * rendering of the content, eg, errors or form submissions. You + * can also intercept URL loading here.</li> + * <li>Via the {@link android.webkit.WebSettings} class, which contains + * miscellaneous configuration. </li> + * <li>With the {@link android.webkit.WebView#addJavascriptInterface} method. + * This lets you bind Java objects into the WebView so they can be + * controlled from the web pages JavaScript.</li> + * </ul> + * + * <p>Here's a more complicated example, showing error handling, + * settings, and progress notification:</p> + * + * <pre class="prettyprint"> + * // Let's display the progress in the activity title bar, like the + * // browser app does. + * getWindow().requestFeature(Window.FEATURE_PROGRESS); + * + * webview.getSettings().setJavaScriptEnabled(true); + * + * final Activity activity = this; + * webview.setWebChromeClient(new WebChromeClient() { + * public void onProgressChanged(WebView view, int progress) { + * // Activities and WebViews measure progress with different scales. + * // The progress meter will automatically disappear when we reach 100% + * activity.setProgress(progress * 1000); + * } + * }); + * webview.setWebViewClient(new WebViewClient() { + * public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { + * Toast.makeText(activity, "Oh no! " + description, Toast.LENGTH_SHORT).show(); + * } + * }); + * + * webview.loadUrl("http://slashdot.org/"); + * </pre> + * + * <h3>Cookie and window management</h3> + * + * <p>For obvious security reasons, your application has its own + * cache, cookie store etc - it does not share the Browser + * applications data. Cookies are managed on a separate thread, so + * operations like index building don't block the UI + * thread. Follow the instructions in {@link android.webkit.CookieSyncManager} + * if you want to use cookies in your application. + * </p> + * + * <p>By default, requests by the HTML to open new windows are + * ignored. This is true whether they be opened by JavaScript or by + * the target attribute on a link. You can customize your + * WebChromeClient to provide your own behaviour for opening multiple windows, + * and render them in whatever manner you want.</p> + * + * <p>Standard behavior for an Activity is to be destroyed and + * recreated when the devices orientation is changed. This will cause + * the WebView to reload the current page. If you don't want that, you + * can set your Activity to handle the orientation and keyboardHidden + * changes, and then just leave the WebView alone. It'll automatically + * re-orient itself as appropriate.</p> */ -public class WebView extends AbsoluteLayout +public class WebView extends AbsoluteLayout implements ViewTreeObserver.OnGlobalFocusChangeListener, ViewGroup.OnHierarchyChangeListener { @@ -108,12 +205,9 @@ public class WebView extends AbsoluteLayout // true means redraw the screen all-the-time. Only with AUTO_REDRAW_HACK private boolean mAutoRedraw; - // keep debugging parameters near the top of the file static final String LOGTAG = "webview"; - static final boolean DEBUG = false; - static final boolean LOGV_ENABLED = DEBUG; - private class ExtendedZoomControls extends FrameLayout { + private static class ExtendedZoomControls extends FrameLayout { public ExtendedZoomControls(Context context, AttributeSet attrs) { super(context, attrs); LayoutInflater inflater = (LayoutInflater) @@ -122,40 +216,40 @@ public class WebView extends AbsoluteLayout mZoomControls = (ZoomControls) findViewById(com.android.internal.R.id.zoomControls); mZoomMagnify = (ImageView) findViewById(com.android.internal.R.id.zoomMagnify); } - + public void show(boolean showZoom, boolean canZoomOut) { mZoomControls.setVisibility(showZoom ? View.VISIBLE : View.GONE); mZoomMagnify.setVisibility(canZoomOut ? View.VISIBLE : View.GONE); fade(View.VISIBLE, 0.0f, 1.0f); } - + public void hide() { fade(View.GONE, 1.0f, 0.0f); } - + private void fade(int visibility, float startAlpha, float endAlpha) { AlphaAnimation anim = new AlphaAnimation(startAlpha, endAlpha); anim.setDuration(500); startAnimation(anim); setVisibility(visibility); } - + public void setIsZoomMagnifyEnabled(boolean isEnabled) { mZoomMagnify.setEnabled(isEnabled); } - + public boolean hasFocus() { return mZoomControls.hasFocus() || mZoomMagnify.hasFocus(); } - + public void setOnZoomInClickListener(OnClickListener listener) { mZoomControls.setOnZoomInClickListener(listener); } - + public void setOnZoomOutClickListener(OnClickListener listener) { mZoomControls.setOnZoomOutClickListener(listener); } - + public void setOnZoomMagnifyClickListener(OnClickListener listener) { mZoomMagnify.setOnClickListener(listener); } @@ -163,7 +257,7 @@ public class WebView extends AbsoluteLayout ZoomControls mZoomControls; ImageView mZoomMagnify; } - + /** * Transportation object for returning WebView across thread boundaries. */ @@ -203,7 +297,7 @@ public class WebView extends AbsoluteLayout private WebViewCore mWebViewCore; // Handler for dispatching UI messages. /* package */ final Handler mPrivateHandler = new PrivateHandler(); - private TextDialog mTextEntry; + private WebTextView mWebTextView; // Used to ignore changes to webkit text that arrives to the UI side after // more key events. private int mTextGeneration; @@ -266,12 +360,15 @@ public class WebView extends AbsoluteLayout // take control of touch events unless it says no for touch down event. private boolean mPreventDrag; - // If updateTextEntry gets called while we are out of focus, use this + // If rebuildWebTextView gets called while we are out of focus, use this // variable to remember to do it next time we gain focus. - private boolean mNeedsUpdateTextEntry = false; - - // Whether or not to draw the focus ring. - private boolean mDrawFocusRing = true; + private boolean mNeedsRebuildWebTextView = false; + + // Whether or not to draw the cursor ring. + private boolean mDrawCursorRing = true; + + // true if onPause has been called (and not onResume) + private boolean mIsPaused; /** * Customizable constant @@ -291,7 +388,7 @@ public class WebView extends AbsoluteLayout // needed to avoid flinging after a pause of no movement private static final int MIN_FLING_TIME = 250; // The time that the Zoom Controls are visible before fading away - private static final long ZOOM_CONTROLS_TIMEOUT = + private static final long ZOOM_CONTROLS_TIMEOUT = ViewConfiguration.getZoomControlsTimeout(); // The amount of content to overlap between two screens when going through // pages with the space bar, in pixels. @@ -312,7 +409,7 @@ public class WebView extends AbsoluteLayout private int mContentWidth; // cache of value from WebViewCore private int mContentHeight; // cache of value from WebViewCore - // Need to have the separate control for horizontal and vertical scrollbar + // Need to have the separate control for horizontal and vertical scrollbar // style than the View's single scrollbar style private boolean mOverlayHorizontalScrollbar = true; private boolean mOverlayVerticalScrollbar = false; @@ -327,51 +424,48 @@ public class WebView extends AbsoluteLayout private boolean mWrapContent; - // true if we should call webcore to draw the content, false means we have - // requested something but it isn't ready to draw yet. - private WebViewCore.FocusData mFocusData; /** * Private message ids */ - private static final int REMEMBER_PASSWORD = 1; - private static final int NEVER_REMEMBER_PASSWORD = 2; - private static final int SWITCH_TO_SHORTPRESS = 3; - private static final int SWITCH_TO_LONGPRESS = 4; - private static final int UPDATE_TEXT_ENTRY_ADAPTER = 6; - private static final int SWITCH_TO_ENTER = 7; - private static final int RESUME_WEBCORE_UPDATE = 8; + private static final int REMEMBER_PASSWORD = 1; + private static final int NEVER_REMEMBER_PASSWORD = 2; + private static final int SWITCH_TO_SHORTPRESS = 3; + private static final int SWITCH_TO_LONGPRESS = 4; + private static final int REQUEST_FORM_DATA = 6; + private static final int SWITCH_TO_CLICK = 7; + private static final int RESUME_WEBCORE_UPDATE = 8; //! arg1=x, arg2=y - static final int SCROLL_TO_MSG_ID = 10; - static final int SCROLL_BY_MSG_ID = 11; + static final int SCROLL_TO_MSG_ID = 10; + static final int SCROLL_BY_MSG_ID = 11; //! arg1=x, arg2=y - static final int SPAWN_SCROLL_TO_MSG_ID = 12; + static final int SPAWN_SCROLL_TO_MSG_ID = 12; //! arg1=x, arg2=y - static final int SYNC_SCROLL_TO_MSG_ID = 13; - static final int NEW_PICTURE_MSG_ID = 14; - 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 DID_FIRST_LAYOUT_MSG_ID = 18; - static final int RECOMPUTE_FOCUS_MSG_ID = 19; - static final int NOTIFY_FOCUS_SET_MSG_ID = 20; - static final int MARK_NODE_INVALID_ID = 21; - static final int UPDATE_CLIPBOARD = 22; - static final int LONG_PRESS_ENTER = 23; - static final int PREVENT_TOUCH_ID = 24; - static final int WEBCORE_NEED_TOUCH_EVENTS = 25; + static final int SYNC_SCROLL_TO_MSG_ID = 13; + static final int NEW_PICTURE_MSG_ID = 14; + 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 DID_FIRST_LAYOUT_MSG_ID = 18; + static final int RECOMPUTE_FOCUS_MSG_ID = 19; + + static final int MARK_NODE_INVALID_ID = 21; + static final int UPDATE_CLIPBOARD = 22; + static final int LONG_PRESS_CENTER = 23; + static final int PREVENT_TOUCH_ID = 24; + static final int WEBCORE_NEED_TOUCH_EVENTS = 25; // obj=Rect in doc coordinates - static final int INVAL_RECT_MSG_ID = 26; - + static final int INVAL_RECT_MSG_ID = 26; + static final String[] HandlerDebugString = { - "REMEMBER_PASSWORD", // = 1; - "NEVER_REMEMBER_PASSWORD", // = 2; - "SWITCH_TO_SHORTPRESS", // = 3; - "SWITCH_TO_LONGPRESS", // = 4; + "REMEMBER_PASSWORD", // = 1; + "NEVER_REMEMBER_PASSWORD", // = 2; + "SWITCH_TO_SHORTPRESS", // = 3; + "SWITCH_TO_LONGPRESS", // = 4; "5", - "UPDATE_TEXT_ENTRY_ADAPTER", // = 6; - "SWITCH_TO_ENTER", // = 7; - "RESUME_WEBCORE_UPDATE", // = 8; + "REQUEST_FORM_DATA", // = 6; + "SWITCH_TO_CLICK", // = 7; + "RESUME_WEBCORE_UPDATE", // = 8; "9", "SCROLL_TO_MSG_ID", // = 10; "SCROLL_BY_MSG_ID", // = 11; @@ -383,10 +477,10 @@ public class WebView extends AbsoluteLayout "UPDATE_TEXTFIELD_TEXT_MSG_ID", // = 17; "DID_FIRST_LAYOUT_MSG_ID", // = 18; "RECOMPUTE_FOCUS_MSG_ID", // = 19; - "NOTIFY_FOCUS_SET_MSG_ID", // = 20; + "20", "MARK_NODE_INVALID_ID", // = 21; "UPDATE_CLIPBOARD", // = 22; - "LONG_PRESS_ENTER", // = 23; + "LONG_PRESS_CENTER", // = 23; "PREVENT_TOUCH_ID", // = 24; "WEBCORE_NEED_TOUCH_EVENTS", // = 25; "INVAL_RECT_MSG_ID" // = 26; @@ -427,7 +521,7 @@ public class WebView extends AbsoluteLayout private static final int SNAP_X_LOCK = 4; private static final int SNAP_Y_LOCK = 5; private boolean mSnapPositive; - + // Used to match key downs and key ups private boolean mGotKeyDown; @@ -450,7 +544,7 @@ public class WebView extends AbsoluteLayout * URI scheme for map address */ public static final String SCHEME_GEO = "geo:0,0?q="; - + private int mBackgroundColor = Color.WHITE; // Used to notify listeners of a new picture. @@ -467,7 +561,8 @@ public class WebView extends AbsoluteLayout public void onNewPicture(WebView view, Picture picture); } - public class HitTestResult { + // FIXME: Want to make this public, but need to change the API file. + public /*static*/ class HitTestResult { /** * Default HitTestResult, where the target is unknown */ @@ -537,7 +632,7 @@ public class WebView extends AbsoluteLayout private ExtendedZoomControls mZoomControls; private Runnable mZoomControlRunnable; - private ZoomButtonsController mZoomButtonsController; + private ZoomButtonsController mZoomButtonsController; private ImageView mZoomOverviewButton; private ImageView mZoomFitPageButton; @@ -562,11 +657,11 @@ public class WebView extends AbsoluteLayout } else { zoomOut(); } - + updateZoomButtonsEnabled(); } }; - + /** * Construct a new WebView with a Context object. * @param context A Context object used to access application assets. @@ -597,11 +692,6 @@ public class WebView extends AbsoluteLayout mCallbackProxy = new CallbackProxy(context, this); mWebViewCore = new WebViewCore(context, this, mCallbackProxy); mDatabase = WebViewDatabase.getInstance(context); - mFocusData = new WebViewCore.FocusData(); - mFocusData.mFrame = 0; - mFocusData.mNode = 0; - mFocusData.mX = 0; - mFocusData.mY = 0; mScroller = new Scroller(context); initZoomController(context); @@ -868,7 +958,7 @@ public class WebView extends AbsoluteLayout clearTextEntry(); if (mWebViewCore != null) { // Set the handlers to null before destroying WebViewCore so no - // more messages will be posted. + // more messages will be posted. mCallbackProxy.setWebViewClient(null); mCallbackProxy.setWebChromeClient(null); // Tell WebViewCore to destroy itself @@ -899,12 +989,12 @@ public class WebView extends AbsoluteLayout /** * If platform notifications are enabled, this should be called - * from onPause() or onStop(). + * from the Activity's onPause() or onStop(). */ public static void disablePlatformNotifications() { Network.disablePlatformNotifications(); } - + /** * Inform WebView of the network state. This is used to set * the javascript property window.navigator.isOnline and @@ -917,7 +1007,7 @@ public class WebView extends AbsoluteLayout } /** - * Save the state of this WebView used in + * Save the state of this WebView used in * {@link android.app.Activity#onSaveInstanceState}. Please note that this * method no longer stores the display data for this WebView. The previous * behavior could potentially leak files if {@link #restoreState} was never @@ -1048,10 +1138,10 @@ public class WebView extends AbsoluteLayout /** * Restore the state of this WebView from the given map used in - * {@link android.app.Activity#onRestoreInstanceState}. This method should - * be called to restore the state of the WebView before using the object. If - * it is called after the WebView has had a chance to build state (load - * pages, create a back/forward list, etc.) there may be undesirable + * {@link android.app.Activity#onRestoreInstanceState}. This method should + * be called to restore the state of the WebView before using the object. If + * it is called after the WebView has had a chance to build state (load + * pages, create a back/forward list, etc.) there may be undesirable * side-effects. Please note that this method no longer restores the * display data for this WebView. See {@link #savePicture} and {@link * #restorePicture} for saving and restoring the display data. @@ -1118,6 +1208,29 @@ public class WebView extends AbsoluteLayout } /** + * Load the url with postData using "POST" method into the WebView. If url + * is not a network url, it will be loaded with {link + * {@link #loadUrl(String)} instead. + * + * @param url The url of the resource to load. + * @param postData The data will be passed to "POST" request. + * + * @hide pending API solidification + */ + public void postUrl(String url, byte[] postData) { + if (URLUtil.isNetworkUrl(url)) { + switchOutDrawHistory(); + HashMap arg = new HashMap(); + arg.put("url", url); + arg.put("data", postData); + mWebViewCore.sendMessage(EventHub.POST_URL, arg); + clearTextEntry(); + } else { + loadUrl(url); + } + } + + /** * Load the given data into the WebView. This will load the data into * WebView using the data: scheme. Content loaded through this mechanism * does not have the ability to load content from the network. @@ -1143,7 +1256,7 @@ public class WebView extends AbsoluteLayout * able to access asset files. If the baseUrl is anything other than * http(s)/ftp(s)/about/javascript as scheme, you can access asset files for * sub resources. - * + * * @param baseUrl Url to resolve relative paths with, if null defaults to * "about:blank" * @param data A String of data in the given encoding. @@ -1154,7 +1267,7 @@ public class WebView extends AbsoluteLayout */ public void loadDataWithBaseURL(String baseUrl, String data, String mimeType, String encoding, String failUrl) { - + if (baseUrl != null && baseUrl.toLowerCase().startsWith("data:")) { loadData(data, mimeType, encoding); return; @@ -1275,7 +1388,7 @@ public class WebView extends AbsoluteLayout ignoreSnapshot ? 1 : 0); } } - + private boolean extendScroll(int y) { int finalY = mScroller.getFinalY(); int newY = pinLocY(finalY + y); @@ -1284,7 +1397,7 @@ public class WebView extends AbsoluteLayout mScroller.extendDuration(computeDuration(0, y)); return true; } - + /** * Scroll the contents of the view up by half the view size * @param top true to jump to the top of the page @@ -1308,10 +1421,10 @@ public class WebView extends AbsoluteLayout y = -h / 2; } mUserScroll = true; - return mScroller.isFinished() ? pinScrollBy(0, y, true, 0) + return mScroller.isFinished() ? pinScrollBy(0, y, true, 0) : extendScroll(y); } - + /** * Scroll the contents of the view down by half the page size * @param bottom true to jump to bottom of page @@ -1334,7 +1447,7 @@ public class WebView extends AbsoluteLayout y = h / 2; } mUserScroll = true; - return mScroller.isFinished() ? pinScrollBy(0, y, true, 0) + return mScroller.isFinished() ? pinScrollBy(0, y, true, 0) : extendScroll(y); } @@ -1347,7 +1460,7 @@ public class WebView extends AbsoluteLayout mContentHeight = 0; mWebViewCore.sendMessage(EventHub.CLEAR_CONTENT); } - + /** * Return a new picture that captures the current display of the webview. * This is a copy of the display, and will be unaffected if the webview @@ -1358,7 +1471,7 @@ public class WebView extends AbsoluteLayout * bounds of the view. */ public Picture capturePicture() { - if (null == mWebViewCore) return null; // check for out of memory tab + if (null == mWebViewCore) return null; // check for out of memory tab return mWebViewCore.copyContentPicture(); } @@ -1366,17 +1479,17 @@ public class WebView extends AbsoluteLayout * Return true if the browser is displaying a TextView for text input. */ private boolean inEditingMode() { - return mTextEntry != null && mTextEntry.getParent() != null - && mTextEntry.hasFocus(); + return mWebTextView != null && mWebTextView.getParent() != null + && mWebTextView.hasFocus(); } private void clearTextEntry() { if (inEditingMode()) { - mTextEntry.remove(); + mWebTextView.remove(); } } - /** + /** * Return the current scale of the WebView * @return The current scale. */ @@ -1440,26 +1553,26 @@ public class WebView extends AbsoluteLayout } HitTestResult result = new HitTestResult(); - - if (nativeUpdateFocusNode()) { - FocusNode node = mFocusNode; - if (node.mIsTextField || node.mIsTextArea) { + if (nativeHasCursorNode()) { + if (nativeCursorIsTextInput()) { result.setType(HitTestResult.EDIT_TEXT_TYPE); - } else if (node.mText != null) { - String text = node.mText; - if (text.startsWith(SCHEME_TEL)) { - result.setType(HitTestResult.PHONE_TYPE); - result.setExtra(text.substring(SCHEME_TEL.length())); - } else if (text.startsWith(SCHEME_MAILTO)) { - result.setType(HitTestResult.EMAIL_TYPE); - result.setExtra(text.substring(SCHEME_MAILTO.length())); - } else if (text.startsWith(SCHEME_GEO)) { - result.setType(HitTestResult.GEO_TYPE); - result.setExtra(URLDecoder.decode(text - .substring(SCHEME_GEO.length()))); - } else if (node.mIsAnchor) { - result.setType(HitTestResult.SRC_ANCHOR_TYPE); - result.setExtra(text); + } else { + String text = nativeCursorText(); + if (text != null) { + if (text.startsWith(SCHEME_TEL)) { + result.setType(HitTestResult.PHONE_TYPE); + result.setExtra(text.substring(SCHEME_TEL.length())); + } else if (text.startsWith(SCHEME_MAILTO)) { + result.setType(HitTestResult.EMAIL_TYPE); + result.setExtra(text.substring(SCHEME_MAILTO.length())); + } else if (text.startsWith(SCHEME_GEO)) { + result.setType(HitTestResult.GEO_TYPE); + result.setExtra(URLDecoder.decode(text + .substring(SCHEME_GEO.length()))); + } else if (nativeCursorIsAnchor()) { + result.setType(HitTestResult.SRC_ANCHOR_TYPE); + result.setExtra(text); + } } } } @@ -1471,8 +1584,8 @@ public class WebView extends AbsoluteLayout int contentY = viewToContent((int) mLastTouchY + mScrollY); String text = nativeImageURI(contentX, contentY); if (text != null) { - result.setType(type == HitTestResult.UNKNOWN_TYPE ? - HitTestResult.IMAGE_TYPE : + result.setType(type == HitTestResult.UNKNOWN_TYPE ? + HitTestResult.IMAGE_TYPE : HitTestResult.SRC_IMAGE_ANCHOR_TYPE); result.setExtra(text); } @@ -1484,7 +1597,7 @@ public class WebView extends AbsoluteLayout * Request the href of an anchor element due to getFocusNodePath returning * "href." If hrefMsg is null, this method returns immediately and does not * dispatch hrefMsg to its target. - * + * * @param hrefMsg This message will be dispatched with the result of the * request as the data member with "url" as key. The result can * be null. @@ -1493,22 +1606,20 @@ public class WebView extends AbsoluteLayout if (hrefMsg == null || mNativeClass == 0) { return; } - if (nativeUpdateFocusNode()) { - FocusNode node = mFocusNode; - if (node.mIsAnchor) { - // NOTE: We may already have the url of the anchor stored in - // node.mText but it may be out of date or the caller may want - // to know about javascript urls. - mWebViewCore.sendMessage(EventHub.REQUEST_FOCUS_HREF, - node.mFramePointer, node.mNodePointer, hrefMsg); - } + if (nativeCursorIsAnchor()) { + // NOTE: We may already have the url of the anchor stored in + // node.mText but it may be out of date or the caller may want + // to know about javascript urls. + mWebViewCore.sendMessage(EventHub.REQUEST_FOCUS_HREF, + nativeCursorFramePointer(), nativeCursorNodePointer(), + hrefMsg); } } - + /** * Request the url of the image last touched by the user. msg will be sent * to its target with a String representing the url as its object. - * + * * @param msg This message will be dispatched with the result of the request * as the data member with "url" as key. The result can be null. */ @@ -1583,7 +1694,7 @@ public class WebView extends AbsoluteLayout if ((w | h) == 0) { return; } - + // don't abort a scroll animation if we didn't change anything if (mContentWidth != w || mContentHeight != h) { // record new dimensions @@ -1643,7 +1754,7 @@ public class WebView extends AbsoluteLayout mActualScale = scale; mInvActualScale = 1 / scale; - // as we don't have animation for scaling, don't do animation + // as we don't have animation for scaling, don't do animation // for scrolling, as it causes weird intermediate state // pinScrollTo(Math.round(sx), Math.round(sy)); mScrollX = pinLocX(Math.round(sx)); @@ -1767,10 +1878,10 @@ public class WebView extends AbsoluteLayout WebHistoryItem h = mCallbackProxy.getBackForwardList().getCurrentItem(); return h != null ? h.getUrl() : null; } - + /** - * Get the original url for the current page. This is not always the same - * as the url passed to WebViewClient.onPageStarted because although the + * Get the original url for the current page. This is not always the same + * as the url passed to WebViewClient.onPageStarted because although the * load for that url has begun, the current page may not have changed. * Also, there may have been redirects resulting in a different url to that * originally requested. @@ -1808,7 +1919,7 @@ public class WebView extends AbsoluteLayout public int getProgress() { return mCallbackProxy.getProgress(); } - + /** * @return the height of the HTML content. */ @@ -1817,30 +1928,68 @@ public class WebView extends AbsoluteLayout } /** - * Pause all layout, parsing, and javascript timers. This can be useful if - * the WebView is not visible or the application has been paused. + * Pause all layout, parsing, and javascript timers for all webviews. This + * is a global requests, not restricted to just this webview. This can be + * useful if the application has been paused. */ public void pauseTimers() { mWebViewCore.sendMessage(EventHub.PAUSE_TIMERS); } /** - * Resume all layout, parsing, and javascript timers. This will resume - * dispatching all timers. + * Resume all layout, parsing, and javascript timers for all webviews. + * This will resume dispatching all timers. */ public void resumeTimers() { mWebViewCore.sendMessage(EventHub.RESUME_TIMERS); } /** - * Clear the resource cache. This will cause resources to be re-downloaded - * if accessed again. - * <p> - * Note: this really needs to be a static method as it clears cache for all - * WebView. But we need mWebViewCore to send message to WebCore thread, so - * we can't make this static. + * Call this to pause any extra processing associated with this view and + * its associated DOM/plugins/javascript/etc. For example, if the view is + * taken offscreen, this could be called to reduce unnecessary CPU and/or + * network traffic. When the view is again "active", call onResume(). + * + * Note that this differs from pauseTimers(), which affects all views/DOMs + * @hide + */ + public void onPause() { + if (!mIsPaused) { + mIsPaused = true; + mWebViewCore.sendMessage(EventHub.ON_PAUSE); + } + } + + /** + * Call this to balanace a previous call to onPause() + * @hide + */ + public void onResume() { + if (mIsPaused) { + mIsPaused = false; + mWebViewCore.sendMessage(EventHub.ON_RESUME); + } + } + + /** + * Returns true if the view is paused, meaning onPause() was called. Calling + * onResume() sets the paused state back to false. + * @hide + */ + public boolean isPaused() { + return mIsPaused; + } + + /** + * Clear the resource cache. Note that the cache is per-application, so + * this will clear the cache for all WebViews used. + * + * @param includeDiskFiles If false, only the RAM cache is cleared. */ public void clearCache(boolean includeDiskFiles) { + // Note: this really needs to be a static method as it clears cache for all + // WebView. But we need mWebViewCore to send message to WebCore thread, so + // we can't make this static. mWebViewCore.sendMessage(EventHub.CLEAR_CACHE, includeDiskFiles ? 1 : 0, 0); } @@ -1852,7 +2001,7 @@ public class WebView extends AbsoluteLayout public void clearFormData() { if (inEditingMode()) { AutoCompleteAdapter adapter = null; - mTextEntry.setAdapterCustom(adapter); + mWebTextView.setAdapterCustom(adapter); } } @@ -1886,7 +2035,7 @@ public class WebView extends AbsoluteLayout /* * Highlight and scroll to the next occurance of String in findAll. - * Wraps the page infinitely, and scrolls. Must be called after + * Wraps the page infinitely, and scrolls. Must be called after * calling findAll. * * @param forward Direction to search. @@ -1912,11 +2061,8 @@ public class WebView extends AbsoluteLayout // or not we draw the highlights for matches. private boolean mFindIsUp; - private native int nativeFindAll(String findLower, String findUpper); - private native void nativeFindNext(boolean forward); - /** - * Return the first substring consisting of the address of a physical + * Return the first substring consisting of the address of a physical * location. Currently, only addresses in the United States are detected, * and consist of: * - a house number @@ -1929,7 +2075,7 @@ public class WebView extends AbsoluteLayout * All names must be correctly capitalized, and the zip code, if present, * must be valid for the state. The street type must be a standard USPS * spelling or abbreviation. The state or territory must also be spelled - * or abbreviated using USPS standards. The house number may not exceed + * or abbreviated using USPS standards. The house number may not exceed * five digits. * @param addr The string to search for addresses. * @@ -2220,16 +2366,10 @@ public class WebView extends AbsoluteLayout } /** - * Signal the WebCore thread to refresh its list of plugins. Use - * this if the directory contents of one of the plugin directories - * has been modified and needs its changes reflecting. May cause - * plugin load and/or unload. - * @param reloadOpenPages Set to true to reload all open pages. + * TODO: need to add @Deprecated */ public void refreshPlugins(boolean reloadOpenPages) { - if (mWebViewCore != null) { - mWebViewCore.sendMessage(EventHub.REFRESH_PLUGINS, reloadOpenPages); - } + PluginManager.getInstance(mContext).refreshPlugins(reloadOpenPages); } //------------------------------------------------------------------------- @@ -2240,7 +2380,7 @@ public class WebView extends AbsoluteLayout protected void finalize() throws Throwable { destroy(); } - + @Override protected void onDraw(Canvas canvas) { // if mNativeClass is 0, the WebView has been destroyed. Do nothing. @@ -2249,7 +2389,7 @@ public class WebView extends AbsoluteLayout } if (mWebViewCore.mEndScaleZoom) { mWebViewCore.mEndScaleZoom = false; - if (mTouchMode >= FIRST_SCROLL_ZOOM + if (mTouchMode >= FIRST_SCROLL_ZOOM && mTouchMode <= LAST_SCROLL_ZOOM) { setHorizontalScrollBarEnabled(true); setVerticalScrollBarEnabled(true); @@ -2271,11 +2411,11 @@ public class WebView extends AbsoluteLayout // need to check it again. nativeRecordButtons(hasFocus() && hasWindowFocus(), mTouchMode == TOUCH_SHORTPRESS_START_MODE - || mTrackballDown || mGotEnterDown, false); - drawCoreAndFocusRing(canvas, mBackgroundColor, mDrawFocusRing); + || mTrackballDown || mGotCenterDown, false); + drawCoreAndCursorRing(canvas, mBackgroundColor, mDrawCursorRing); } canvas.restoreToCount(sc); - + if (AUTO_REDRAW_HACK && mAutoRedraw) { invalidate(); } @@ -2292,13 +2432,13 @@ public class WebView extends AbsoluteLayout @Override public boolean performLongClick() { if (inEditingMode()) { - return mTextEntry.performLongClick(); + return mWebTextView.performLongClick(); } else { return super.performLongClick(); } } - private void drawCoreAndFocusRing(Canvas canvas, int color, + private void drawCoreAndCursorRing(Canvas canvas, int color, boolean drawFocus) { if (mDrawHistory) { canvas.scale(mActualScale, mActualScale); @@ -2307,14 +2447,14 @@ public class WebView extends AbsoluteLayout } boolean animateZoom = mZoomScale != 0; - boolean animateScroll = !mScroller.isFinished() + boolean animateScroll = !mScroller.isFinished() || mVelocityTracker != null; if (animateZoom) { float zoomScale; int interval = (int) (SystemClock.uptimeMillis() - mZoomStart); if (interval < ZOOM_ANIMATION_LENGTH) { float ratio = (float) interval / ZOOM_ANIMATION_LENGTH; - zoomScale = 1.0f / (mInvInitialZoomScale + zoomScale = 1.0f / (mInvInitialZoomScale + (mInvFinalZoomScale - mInvInitialZoomScale) * ratio); invalidate(); } else { @@ -2354,7 +2494,7 @@ public class WebView extends AbsoluteLayout if (mTouchSelection) { nativeDrawSelectionRegion(canvas); } else { - nativeDrawSelection(canvas, mSelectX, mSelectY, + nativeDrawSelection(canvas, mSelectX, mSelectY, mExtendSelection); } } else if (drawFocus) { @@ -2368,7 +2508,7 @@ public class WebView extends AbsoluteLayout LONG_PRESS_TIMEOUT); } } - nativeDrawFocusRing(canvas); + nativeDrawCursorRing(canvas); } // When the FindDialog is up, only draw the matches if we are not in // the process of scrolling them into view. @@ -2377,14 +2517,12 @@ public class WebView extends AbsoluteLayout } } - private native void nativeDrawMatches(Canvas canvas); - private float scrollZoomGridScale(float invScale) { - float griddedInvScale = (int) (invScale * SCROLL_ZOOM_GRID) + float griddedInvScale = (int) (invScale * SCROLL_ZOOM_GRID) / (float) SCROLL_ZOOM_GRID; return 1.0f / griddedInvScale; } - + private float scrollZoomX(float scale) { int width = getViewWidth(); float maxScrollZoomX = mContentWidth * scale - width; @@ -2400,7 +2538,7 @@ public class WebView extends AbsoluteLayout return -(maxScrollZoomY > 0 ? mZoomScrollY * maxScrollZoomY / maxY : maxScrollZoomY / 2); } - + private void drawMagnifyFrame(Canvas canvas, Rect frame, Paint paint) { final float ADORNMENT_LEN = 16.0f; float width = frame.width(); @@ -2421,13 +2559,13 @@ public class WebView extends AbsoluteLayout path.offset(frame.left, frame.top); canvas.drawPath(path, paint); } - - // Returns frame surrounding magified portion of screen while + + // Returns frame surrounding magified portion of screen while // scroll-zoom is enabled. The frame is also used to center the // zoom-in zoom-out points at the start and end of the animation. private Rect scrollZoomFrame(int width, int height, float halfScale) { Rect scrollFrame = new Rect(); - scrollFrame.set(mZoomScrollX, mZoomScrollY, + scrollFrame.set(mZoomScrollX, mZoomScrollY, mZoomScrollX + width, mZoomScrollY + height); if (mContentWidth * mZoomScrollLimit < width) { float scale = zoomFrameScaleX(width, halfScale, 1.0f); @@ -2443,37 +2581,37 @@ public class WebView extends AbsoluteLayout } return scrollFrame; } - + private float zoomFrameScaleX(int width, float halfScale, float noScale) { // mContentWidth > width > mContentWidth * mZoomScrollLimit if (mContentWidth <= width) { return halfScale; } - float part = (width - mContentWidth * mZoomScrollLimit) + float part = (width - mContentWidth * mZoomScrollLimit) / (width * (1 - mZoomScrollLimit)); return halfScale * part + noScale * (1.0f - part); } - + private float zoomFrameScaleY(int height, float halfScale, float noScale) { if (mContentHeight <= height) { return halfScale; } - float part = (height - mContentHeight * mZoomScrollLimit) + float part = (height - mContentHeight * mZoomScrollLimit) / (height * (1 - mZoomScrollLimit)); return halfScale * part + noScale * (1.0f - part); } - + private float scrollZoomMagScale(float invScale) { return (invScale * 2 + mInvActualScale) / 3; } - + private void scrollZoomDraw(Canvas canvas) { - float invScale = mZoomScrollInvLimit; + float invScale = mZoomScrollInvLimit; int elapsed = 0; if (mTouchMode != SCROLL_ZOOM_OUT) { - elapsed = (int) Math.min(System.currentTimeMillis() + elapsed = (int) Math.min(System.currentTimeMillis() - mZoomScrollStart, SCROLL_ZOOM_DURATION); - float transitionScale = (mZoomScrollInvLimit - mInvActualScale) + float transitionScale = (mZoomScrollInvLimit - mInvActualScale) * elapsed / SCROLL_ZOOM_DURATION; if (mTouchMode == SCROLL_ZOOM_ANIMATION_OUT) { invScale = mInvActualScale + transitionScale; @@ -2491,9 +2629,9 @@ public class WebView extends AbsoluteLayout if (mTouchMode == SCROLL_ZOOM_ANIMATION_IN) { setHorizontalScrollBarEnabled(true); setVerticalScrollBarEnabled(true); - updateTextEntry(); - scrollTo((int) (scrollFrame.centerX() * mActualScale) - - (width >> 1), (int) (scrollFrame.centerY() + rebuildWebTextView(); + scrollTo((int) (scrollFrame.centerX() * mActualScale) + - (width >> 1), (int) (scrollFrame.centerY() * mActualScale) - (height >> 1)); mTouchMode = TOUCH_DONE_MODE; } else { @@ -2502,10 +2640,10 @@ public class WebView extends AbsoluteLayout } float newX = scrollZoomX(scale); float newY = scrollZoomY(scale); - if (LOGV_ENABLED) { + if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "scrollZoomDraw scale=" + scale + " + (" + newX + ", " + newY + ") mZoomScroll=(" + mZoomScrollX + ", " - + mZoomScrollY + ")" + " invScale=" + invScale + " scale=" + + mZoomScrollY + ")" + " invScale=" + invScale + " scale=" + scale); } canvas.translate(newX, newY); @@ -2549,8 +2687,8 @@ public class WebView extends AbsoluteLayout } canvas.scale(halfScale, halfScale, mZoomScrollX + width * halfX , mZoomScrollY + height * halfY); - if (LOGV_ENABLED) { - Log.v(LOGTAG, "scrollZoomDraw halfScale=" + halfScale + " w/h=(" + if (DebugFlags.WEB_VIEW) { + Log.v(LOGTAG, "scrollZoomDraw halfScale=" + halfScale + " w/h=(" + width + ", " + height + ") half=(" + halfX + ", " + halfY + ")"); } @@ -2578,7 +2716,7 @@ public class WebView extends AbsoluteLayout , Math.max(0, (int) ((x - left) / scale))); mZoomScrollY = Math.min(mContentHeight - height , Math.max(0, (int) ((y - top) / scale))); - if (LOGV_ENABLED) { + if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "zoomScrollTap scale=" + scale + " + (" + left + ", " + top + ") mZoomScroll=(" + mZoomScrollX + ", " + mZoomScrollY + ")" + " x=" + x + " y=" + y); @@ -2595,7 +2733,7 @@ public class WebView extends AbsoluteLayout float y = (float) height / (float) mContentHeight; mZoomScrollLimit = Math.max(DEFAULT_MIN_ZOOM_SCALE, Math.min(x, y)); mZoomScrollInvLimit = 1.0f / mZoomScrollLimit; - if (LOGV_ENABLED) { + if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "canZoomScrollOut" + " mInvActualScale=" + mInvActualScale + " mZoomScrollLimit=" + mZoomScrollLimit @@ -2610,7 +2748,7 @@ public class WebView extends AbsoluteLayout return mContentWidth >= width * limit || mContentHeight >= height * limit; } - + private void startZoomScrollOut() { setHorizontalScrollBarEnabled(false); setVerticalScrollBarEnabled(false); @@ -2636,18 +2774,18 @@ public class WebView extends AbsoluteLayout mZoomScrollStart = System.currentTimeMillis(); Rect zoomFrame = scrollZoomFrame(width, height , scrollZoomMagScale(mZoomScrollInvLimit)); - mZoomScrollX = Math.max(0, (int) ((mScrollX + halfW) * mInvActualScale) + mZoomScrollX = Math.max(0, (int) ((mScrollX + halfW) * mInvActualScale) - (zoomFrame.width() >> 1)); - mZoomScrollY = Math.max(0, (int) ((mScrollY + halfH) * mInvActualScale) + mZoomScrollY = Math.max(0, (int) ((mScrollY + halfH) * mInvActualScale) - (zoomFrame.height() >> 1)); scrollTo(0, 0); // triggers inval, starts animation clearTextEntry(); - if (LOGV_ENABLED) { - Log.v(LOGTAG, "startZoomScrollOut mZoomScroll=(" + if (DebugFlags.WEB_VIEW) { + Log.v(LOGTAG, "startZoomScrollOut mZoomScroll=(" + mZoomScrollX + ", " + mZoomScrollY +")"); } } - + private void zoomScrollOut() { if (canZoomScrollOut() == false) { mTouchMode = TOUCH_DONE_MODE; @@ -2659,7 +2797,7 @@ public class WebView extends AbsoluteLayout } private void moveZoomScrollWindow(float x, float y) { - if (Math.abs(x - mLastZoomScrollRawX) < 1.5f + if (Math.abs(x - mLastZoomScrollRawX) < 1.5f && Math.abs(y - mLastZoomScrollRawY) < 1.5f) { return; } @@ -2671,12 +2809,12 @@ public class WebView extends AbsoluteLayout int height = getViewHeight(); int maxZoomX = mContentWidth - width; if (maxZoomX > 0) { - int maxScreenX = width - (int) Math.ceil(width + int maxScreenX = width - (int) Math.ceil(width * mZoomScrollLimit) - SCROLL_ZOOM_FINGER_BUFFER; - if (LOGV_ENABLED) { - Log.v(LOGTAG, "moveZoomScrollWindow-X" + if (DebugFlags.WEB_VIEW) { + Log.v(LOGTAG, "moveZoomScrollWindow-X" + " maxScreenX=" + maxScreenX + " width=" + width - + " mZoomScrollLimit=" + mZoomScrollLimit + " x=" + x); + + " mZoomScrollLimit=" + mZoomScrollLimit + " x=" + x); } x += maxScreenX * mLastScrollX / maxZoomX - mLastTouchX; x *= Math.max(maxZoomX / maxScreenX, mZoomScrollInvLimit); @@ -2684,12 +2822,12 @@ public class WebView extends AbsoluteLayout } int maxZoomY = mContentHeight - height; if (maxZoomY > 0) { - int maxScreenY = height - (int) Math.ceil(height + int maxScreenY = height - (int) Math.ceil(height * mZoomScrollLimit) - SCROLL_ZOOM_FINGER_BUFFER; - if (LOGV_ENABLED) { - Log.v(LOGTAG, "moveZoomScrollWindow-Y" + if (DebugFlags.WEB_VIEW) { + Log.v(LOGTAG, "moveZoomScrollWindow-Y" + " maxScreenY=" + maxScreenY + " height=" + height - + " mZoomScrollLimit=" + mZoomScrollLimit + " y=" + y); + + " mZoomScrollLimit=" + mZoomScrollLimit + " y=" + y); } y += maxScreenY * mLastScrollY / maxZoomY - mLastTouchY; y *= Math.max(maxZoomY / maxScreenY, mZoomScrollInvLimit); @@ -2698,12 +2836,12 @@ public class WebView extends AbsoluteLayout if (oldX != mZoomScrollX || oldY != mZoomScrollY) { invalidate(); } - if (LOGV_ENABLED) { - Log.v(LOGTAG, "moveZoomScrollWindow" - + " scrollTo=(" + mZoomScrollX + ", " + mZoomScrollY + ")" - + " mLastTouch=(" + mLastTouchX + ", " + mLastTouchY + ")" - + " maxZoom=(" + maxZoomX + ", " + maxZoomY + ")" - + " last=("+mLastScrollX+", "+mLastScrollY+")" + if (DebugFlags.WEB_VIEW) { + Log.v(LOGTAG, "moveZoomScrollWindow" + + " scrollTo=(" + mZoomScrollX + ", " + mZoomScrollY + ")" + + " mLastTouch=(" + mLastTouchX + ", " + mLastTouchY + ")" + + " maxZoom=(" + maxZoomX + ", " + maxZoomY + ")" + + " last=("+mLastScrollX+", "+mLastScrollY+")" + " x=" + x + " y=" + y); } } @@ -2748,7 +2886,7 @@ public class WebView extends AbsoluteLayout // Should only be called in UI thread void switchOutDrawHistory() { if (null == mWebViewCore) return; // CallbackProxy may trigger this - if (mDrawHistory) { + if (mDrawHistory && mWebViewCore.pictureReady()) { mDrawHistory = false; invalidate(); int oldScrollX = mScrollX; @@ -2764,72 +2902,27 @@ public class WebView extends AbsoluteLayout } } - /** - * Class representing the node which is focused. - */ - private class FocusNode { - public FocusNode() { - mBounds = new Rect(); - } - // Only to be called by JNI - private void setAll(boolean isTextField, boolean isTextArea, boolean - isPassword, boolean isAnchor, boolean isRtlText, int maxLength, - int textSize, int boundsX, int boundsY, int boundsRight, int - boundsBottom, int nodePointer, int framePointer, String text, - String name, int rootTextGeneration) { - mIsTextField = isTextField; - mIsTextArea = isTextArea; - mIsPassword = isPassword; - mIsAnchor = isAnchor; - mIsRtlText = isRtlText; - - mMaxLength = maxLength; - mTextSize = textSize; - - mBounds.set(boundsX, boundsY, boundsRight, boundsBottom); - - - mNodePointer = nodePointer; - mFramePointer = framePointer; - mText = text; - mName = name; - mRootTextGeneration = rootTextGeneration; - } - public boolean mIsTextField; - public boolean mIsTextArea; - public boolean mIsPassword; - public boolean mIsAnchor; - public boolean mIsRtlText; - - public int mSelectionStart; - public int mSelectionEnd; - public int mMaxLength; - public int mTextSize; - - public Rect mBounds; - - public int mNodePointer; - public int mFramePointer; - public String mText; - public String mName; - public int mRootTextGeneration; - } - - // Warning: ONLY use mFocusNode AFTER calling nativeUpdateFocusNode(), - // and ONLY if it returns true; - private FocusNode mFocusNode = new FocusNode(); - + WebViewCore.CursorData cursorData() { + WebViewCore.CursorData result = new WebViewCore.CursorData(); + result.mMoveGeneration = nativeMoveGeneration(); + result.mFrame = nativeCursorFramePointer(); + result.mNode = nativeCursorNodePointer(); + Rect bounds = nativeCursorNodeBounds(); + result.mX = bounds.centerX(); + result.mY = bounds.centerY(); + return result; + } + /** * Delete text from start to end in the focused textfield. If there is no - * focus, or if start == end, silently fail. If start and end are out of + * focus, or if start == end, silently fail. If start and end are out of * order, swap them. * @param start Beginning of selection to delete. * @param end End of selection to delete. */ /* package */ void deleteSelection(int start, int end) { mTextGeneration++; - mWebViewCore.sendMessage(EventHub.DELETE_SELECTION, start, end, - new WebViewCore.FocusData(mFocusData)); + mWebViewCore.sendMessage(EventHub.DELETE_SELECTION, start, end); } /** @@ -2839,119 +2932,121 @@ public class WebView extends AbsoluteLayout * @param end End of selection. */ /* package */ void setSelection(int start, int end) { - mWebViewCore.sendMessage(EventHub.SET_SELECTION, start, end, - new WebViewCore.FocusData(mFocusData)); + mWebViewCore.sendMessage(EventHub.SET_SELECTION, start, end); } // Called by JNI when a touch event puts a textfield into focus. private void displaySoftKeyboard() { InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); - imm.showSoftInput(mTextEntry, 0); - mTextEntry.enableScrollOnScreen(true); + imm.showSoftInput(mWebTextView, 0); + mWebTextView.enableScrollOnScreen(true); // Now we need to fake a touch event to place the cursor where the // user touched. AbsoluteLayout.LayoutParams lp = (AbsoluteLayout.LayoutParams) - mTextEntry.getLayoutParams(); + mWebTextView.getLayoutParams(); if (lp != null) { // Take the last touch and adjust for the location of the - // TextDialog. + // WebTextView. float x = mLastTouchX + (float) (mScrollX - lp.x); float y = mLastTouchY + (float) (mScrollY - lp.y); - mTextEntry.fakeTouchEvent(x, y); + mWebTextView.fakeTouchEvent(x, y); } } - private void updateTextEntry() { - if (mTextEntry == null) { - mTextEntry = new TextDialog(mContext, WebView.this); - // Initialize our generation number. - mTextGeneration = 0; - } + /* + * This method checks the current focus and potentially rebuilds + * mWebTextView to have the appropriate properties, such as password, + * multiline, and what text it contains. It also removes it if necessary. + */ + private void rebuildWebTextView() { // If we do not have focus, do nothing until we gain focus. - if (!hasFocus() && !mTextEntry.hasFocus() - || (mTouchMode >= FIRST_SCROLL_ZOOM + if (!hasFocus() && (null == mWebTextView || !mWebTextView.hasFocus()) + || (mTouchMode >= FIRST_SCROLL_ZOOM && mTouchMode <= LAST_SCROLL_ZOOM)) { - mNeedsUpdateTextEntry = true; + mNeedsRebuildWebTextView = true; return; } boolean alreadyThere = inEditingMode(); - if (0 == mNativeClass || !nativeUpdateFocusNode()) { + // inEditingMode can only return true if mWebTextView is non-null, + // so we can safely call remove() if (alreadyThere) + if (0 == mNativeClass || (!nativeFocusIsTextInput() + && !nativeCursorIsTextInput())) { if (alreadyThere) { - mTextEntry.remove(); + mWebTextView.remove(); } return; } - FocusNode node = mFocusNode; - if (!node.mIsTextField && !node.mIsTextArea) { - if (alreadyThere) { - mTextEntry.remove(); - } - return; + // At this point, we know we have found an input field, so go ahead + // and create the WebTextView if necessary. + if (mWebTextView == null) { + mWebTextView = new WebTextView(mContext, WebView.this); + // Initialize our generation number. + mTextGeneration = 0; } - mTextEntry.setTextSize(contentToView(node.mTextSize)); + mWebTextView.setTextSize(contentToView(nativeFocusTextSize())); Rect visibleRect = sendOurVisibleRect(); // Note that sendOurVisibleRect calls viewToContent, so the coordinates // should be in content coordinates. - if (!Rect.intersects(node.mBounds, visibleRect)) { + Rect bounds = nativeFocusNodeBounds(); + if (!Rect.intersects(bounds, visibleRect)) { // Node is not on screen, so do not bother. return; } - int x = node.mBounds.left; - int y = node.mBounds.top; - int width = node.mBounds.width(); - int height = node.mBounds.height(); - if (alreadyThere && mTextEntry.isSameTextField(node.mNodePointer)) { + String text = nativeFocusText(); + int nodePointer = nativeFocusNodePointer(); + if (alreadyThere && mWebTextView.isSameTextField(nodePointer)) { // It is possible that we have the same textfield, but it has moved, // i.e. In the case of opening/closing the screen. // In that case, we need to set the dimensions, but not the other // aspects. // We also need to restore the selection, which gets wrecked by // calling setTextEntryRect. - Spannable spannable = (Spannable) mTextEntry.getText(); + Spannable spannable = (Spannable) mWebTextView.getText(); int start = Selection.getSelectionStart(spannable); int end = Selection.getSelectionEnd(spannable); - setTextEntryRect(x, y, width, height); // If the text has been changed by webkit, update it. However, if // there has been more UI text input, ignore it. We will receive // another update when that text is recognized. - if (node.mText != null && !node.mText.equals(spannable.toString()) - && node.mRootTextGeneration == mTextGeneration) { - mTextEntry.setTextAndKeepSelection(node.mText); + if (text != null && !text.equals(spannable.toString()) + && nativeTextGeneration() == mTextGeneration) { + mWebTextView.setTextAndKeepSelection(text); } else { Selection.setSelection(spannable, start, end); } } else { - String text = node.mText; - setTextEntryRect(x, y, width, height); - mTextEntry.setGravity(node.mIsRtlText ? Gravity.RIGHT : + Rect vBox = contentToView(bounds); + mWebTextView.setRect(vBox.left, vBox.top, vBox.width(), + vBox.height()); + mWebTextView.setGravity(nativeFocusIsRtlText() ? Gravity.RIGHT : Gravity.NO_GRAVITY); // this needs to be called before update adapter thread starts to - // ensure the mTextEntry has the same node pointer - mTextEntry.setNodePointer(node.mNodePointer); + // ensure the mWebTextView has the same node pointer + mWebTextView.setNodePointer(nodePointer); int maxLength = -1; - if (node.mIsTextField) { - maxLength = node.mMaxLength; + boolean isTextField = nativeFocusIsTextField(); + if (isTextField) { + maxLength = nativeFocusMaxLength(); + String name = nativeFocusName(); if (mWebViewCore.getSettings().getSaveFormData() - && node.mName != null) { + && name != null) { HashMap data = new HashMap(); - data.put("text", node.mText); + data.put("text", text); Message update = mPrivateHandler.obtainMessage( - UPDATE_TEXT_ENTRY_ADAPTER, node.mNodePointer, 0, - data); - UpdateTextEntryAdapter updater = new UpdateTextEntryAdapter( - node.mName, getUrl(), update); + REQUEST_FORM_DATA, nodePointer, 0, data); + RequestFormData updater = new RequestFormData(name, + getUrl(), update); Thread t = new Thread(updater); t.start(); } } - mTextEntry.setMaxLength(maxLength); + mWebTextView.setMaxLength(maxLength); AutoCompleteAdapter adapter = null; - mTextEntry.setAdapterCustom(adapter); - mTextEntry.setSingleLine(node.mIsTextField); - mTextEntry.setInPassword(node.mIsPassword); + mWebTextView.setAdapterCustom(adapter); + mWebTextView.setSingleLine(isTextField); + mWebTextView.setInPassword(nativeFocusIsPassword()); if (null == text) { - mTextEntry.setText("", 0, 0); + mWebTextView.setText("", 0, 0); } else { // Change to true to enable the old style behavior, where // entering a textfield/textarea always set the selection to the @@ -2962,24 +3057,29 @@ public class WebView extends AbsoluteLayout // textarea. Testing out a new behavior, where textfields set // selection at the end, and textareas at the beginning. if (false) { - mTextEntry.setText(text, 0, text.length()); - } else if (node.mIsTextField) { + mWebTextView.setText(text, 0, text.length()); + } else if (isTextField) { int length = text.length(); - mTextEntry.setText(text, length, length); + mWebTextView.setText(text, length, length); } else { - mTextEntry.setText(text, 0, 0); + mWebTextView.setText(text, 0, 0); } } - mTextEntry.requestFocus(); + mWebTextView.requestFocus(); } } - private class UpdateTextEntryAdapter implements Runnable { + /* + * This class requests an Adapter for the WebTextView which shows past + * entries stored in the database. It is a Runnable so that it can be done + * in its own thread, without slowing down the UI. + */ + private class RequestFormData implements Runnable { private String mName; private String mUrl; private Message mUpdateMessage; - public UpdateTextEntryAdapter(String name, String url, Message msg) { + public RequestFormData(String name, String url, Message msg) { mName = name; mUrl = url; mUpdateMessage = msg; @@ -2996,21 +3096,13 @@ public class WebView extends AbsoluteLayout } } - private void setTextEntryRect(int x, int y, int width, int height) { - x = contentToView(x); - y = contentToView(y); - width = contentToView(width); - height = contentToView(height); - mTextEntry.setRect(x, y, width, height); - } - - // This is used to determine long press with the enter key, or - // a center key. Does not affect long press with the trackball/touch. - private boolean mGotEnterDown = false; + // This is used to determine long press with the center key. Does not + // affect long press with the trackball/touch. + private boolean mGotCenterDown = false; @Override public boolean onKeyDown(int keyCode, KeyEvent event) { - if (LOGV_ENABLED) { + if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "keyDown at " + System.currentTimeMillis() + ", " + event); } @@ -3038,15 +3130,15 @@ public class WebView extends AbsoluteLayout return false; } - if (mShiftIsPressed == false && nativeFocusNodeWantsKeyEvents() == false - && (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT + if (mShiftIsPressed == false && nativeCursorWantsKeyEvents() == false + && (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT || keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT)) { mExtendSelection = false; mShiftIsPressed = true; - if (nativeUpdateFocusNode()) { - FocusNode node = mFocusNode; - mSelectX = contentToView(node.mBounds.left); - mSelectY = contentToView(node.mBounds.top); + if (nativeHasCursorNode()) { + Rect rect = nativeCursorNodeBounds(); + mSelectX = contentToView(rect.left); + mSelectY = contentToView(rect.top); } else { mSelectX = mScrollX + (int) mLastTouchX; mSelectY = mScrollY + (int) mLastTouchY; @@ -3068,13 +3160,12 @@ public class WebView extends AbsoluteLayout return false; } - if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER - || keyCode == KeyEvent.KEYCODE_ENTER) { + if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) { switchOutDrawHistory(); if (event.getRepeatCount() == 0) { - mGotEnterDown = true; + mGotCenterDown = true; mPrivateHandler.sendMessageDelayed(mPrivateHandler - .obtainMessage(LONG_PRESS_ENTER), LONG_PRESS_TIMEOUT); + .obtainMessage(LONG_PRESS_CENTER), LONG_PRESS_TIMEOUT); // Already checked mNativeClass, so we do not need to check it // again. nativeRecordButtons(hasFocus() && hasWindowFocus(), true, true); @@ -3084,6 +3175,15 @@ public class WebView extends AbsoluteLayout return false; } + if (keyCode != KeyEvent.KEYCODE_SHIFT_LEFT + && keyCode != KeyEvent.KEYCODE_SHIFT_RIGHT) { + // turn off copy select if a shift-key combo is pressed + mExtendSelection = mShiftIsPressed = false; + if (mTouchMode == TOUCH_SELECT_MODE) { + mTouchMode = TOUCH_INIT_MODE; + } + } + if (getSettings().getNavDump()) { switch (keyCode) { case KeyEvent.KEYCODE_4: @@ -3113,7 +3213,7 @@ public class WebView extends AbsoluteLayout } // TODO: should we pass all the keys to DOM or check the meta tag - if (nativeFocusNodeWantsKeyEvents() || true) { + if (nativeCursorWantsKeyEvents() || true) { // pass the key to DOM mWebViewCore.sendMessage(EventHub.KEY_DOWN, event); // return true as DOM handles the key @@ -3126,7 +3226,7 @@ public class WebView extends AbsoluteLayout @Override public boolean onKeyUp(int keyCode, KeyEvent event) { - if (LOGV_ENABLED) { + if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "keyUp at " + System.currentTimeMillis() + ", " + event); } @@ -3136,10 +3236,9 @@ public class WebView extends AbsoluteLayout } // special CALL handling when focus node's href is "tel:XXX" - if (keyCode == KeyEvent.KEYCODE_CALL && nativeUpdateFocusNode()) { - FocusNode node = mFocusNode; - String text = node.mText; - if (!node.mIsTextField && !node.mIsTextArea && text != null + if (keyCode == KeyEvent.KEYCODE_CALL && nativeHasCursorNode()) { + String text = nativeCursorText(); + if (!nativeCursorIsTextInput() && text != null && text.startsWith(SCHEME_TEL)) { Intent intent = new Intent(Intent.ACTION_DIAL, Uri.parse(text)); getContext().startActivity(intent); @@ -3166,7 +3265,7 @@ public class WebView extends AbsoluteLayout return false; } - if (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT + if (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT || keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT) { if (commitCopy()) { return true; @@ -3180,55 +3279,30 @@ public class WebView extends AbsoluteLayout return false; } - if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER - || keyCode == KeyEvent.KEYCODE_ENTER) { + if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) { // remove the long press message first - mPrivateHandler.removeMessages(LONG_PRESS_ENTER); - mGotEnterDown = false; + mPrivateHandler.removeMessages(LONG_PRESS_CENTER); + mGotCenterDown = false; - if (KeyEvent.KEYCODE_DPAD_CENTER == keyCode) { - if (mShiftIsPressed) { - return false; - } - if (getSettings().supportZoom()) { - if (mTouchMode == TOUCH_DOUBLECLICK_MODE) { - zoomScrollOut(); - } else { - if (LOGV_ENABLED) { - Log.v(LOGTAG, "TOUCH_DOUBLECLICK_MODE"); - } - mPrivateHandler.sendMessageDelayed(mPrivateHandler - .obtainMessage(SWITCH_TO_ENTER), TAP_TIMEOUT); - mTouchMode = TOUCH_DOUBLECLICK_MODE; - } - return true; - } + if (mShiftIsPressed) { + return false; } - - Rect visibleRect = sendOurVisibleRect(); - // Note that sendOurVisibleRect calls viewToContent, so the - // coordinates should be in content coordinates. - if (nativeUpdateFocusNode()) { - if (Rect.intersects(mFocusNode.mBounds, visibleRect)) { - nativeSetFollowedLink(true); - mWebViewCore.sendMessage(EventHub.SET_FINAL_FOCUS, - EventHub.BLOCK_FOCUS_CHANGE_UNTIL_KEY_UP, 0, - new WebViewCore.FocusData(mFocusData)); - playSoundEffect(SoundEffectConstants.CLICK); - if (!mCallbackProxy.uiOverrideUrlLoading(mFocusNode.mText)) { - // use CLICK instead of KEY_DOWN/KEY_UP so that we can - // trigger mouse click events - mWebViewCore.sendMessage(EventHub.CLICK); - } + if (getSettings().supportZoom() + && mTouchMode == TOUCH_DOUBLECLICK_MODE) { + zoomScrollOut(); + } else { + mPrivateHandler.sendMessageDelayed(mPrivateHandler + .obtainMessage(SWITCH_TO_CLICK), TAP_TIMEOUT); + if (DebugFlags.WEB_VIEW) { + Log.v(LOGTAG, "TOUCH_DOUBLECLICK_MODE"); } - return true; + mTouchMode = TOUCH_DOUBLECLICK_MODE; } - // Bubble up the key event as WebView doesn't handle it - return false; + return true; } // TODO: should we pass all the keys to DOM or check the meta tag - if (nativeFocusNodeWantsKeyEvents() || true) { + if (nativeCursorWantsKeyEvents() || true) { // pass the key to DOM mWebViewCore.sendMessage(EventHub.KEY_UP, event); // return true as DOM handles the key @@ -3238,7 +3312,7 @@ public class WebView extends AbsoluteLayout // Bubble up the key event as WebView doesn't handle it return false; } - + /** * @hide */ @@ -3295,15 +3369,15 @@ public class WebView extends AbsoluteLayout // Clean up the zoom controller mZoomButtonsController.setVisible(false); } - + // Implementation for OnHierarchyChangeListener public void onChildViewAdded(View parent, View child) {} - + public void onChildViewRemoved(View p, View child) { if (child == this) { if (inEditingMode()) { clearTextEntry(); - mNeedsUpdateTextEntry = true; + mNeedsRebuildWebTextView = true; } } } @@ -3317,26 +3391,27 @@ public class WebView extends AbsoluteLayout public void onGlobalFocusChanged(View oldFocus, View newFocus) { } - // To avoid drawing the focus ring, and remove the TextView when our window + // To avoid drawing the cursor ring, and remove the TextView when our window // loses focus. @Override public void onWindowFocusChanged(boolean hasWindowFocus) { if (hasWindowFocus) { if (hasFocus()) { // If our window regained focus, and we have focus, then begin - // drawing the focus ring, and restore the TextView if + // drawing the cursor ring, and restore the TextView if // necessary. - mDrawFocusRing = true; - if (mNeedsUpdateTextEntry) { - updateTextEntry(); + mDrawCursorRing = true; + if (mNeedsRebuildWebTextView) { + rebuildWebTextView(); } if (mNativeClass != 0) { nativeRecordButtons(true, false, true); } + setFocusControllerActive(true); } else { // If our window gained focus, but we do not have it, do not - // draw the focus ring. - mDrawFocusRing = false; + // draw the cursor ring. + mDrawCursorRing = false; // We do not call nativeRecordButtons here because we assume // that when we lost focus, or window focus, it got called with // false for the first parameter @@ -3345,42 +3420,57 @@ public class WebView extends AbsoluteLayout if (getSettings().getBuiltInZoomControls() && !mZoomButtonsController.isVisible()) { /* * The zoom controls come in their own window, so our window - * loses focus. Our policy is to not draw the focus ring if + * loses focus. Our policy is to not draw the cursor ring if * our window is not focused, but this is an exception since * the user can still navigate the web page with the zoom * controls showing. */ - // If our window has lost focus, stop drawing the focus ring - mDrawFocusRing = false; + // If our window has lost focus, stop drawing the cursor ring + mDrawCursorRing = false; } mGotKeyDown = false; mShiftIsPressed = false; if (mNativeClass != 0) { nativeRecordButtons(false, false, true); } + setFocusControllerActive(false); } invalidate(); super.onWindowFocusChanged(hasWindowFocus); } + /* + * Pass a message to WebCore Thread, determining whether the WebCore::Page's + * FocusController is "active" so that it will draw the blinking cursor. + */ + private void setFocusControllerActive(boolean active) { + if (mWebViewCore != null) { + mWebViewCore.sendMessage(EventHub.SET_ACTIVE, active ? 1 : 0, 0); + } + } + @Override protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { - if (LOGV_ENABLED) { + if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "MT focusChanged " + focused + ", " + direction); } if (focused) { // When we regain focus, if we have window focus, resume drawing - // the focus ring, and add the TextView if necessary. + // the cursor ring, and add the TextView if necessary. if (hasWindowFocus()) { - mDrawFocusRing = true; - if (mNeedsUpdateTextEntry) { - updateTextEntry(); - mNeedsUpdateTextEntry = false; + mDrawCursorRing = true; + if (mNeedsRebuildWebTextView) { + rebuildWebTextView(); + mNeedsRebuildWebTextView = false; } if (mNativeClass != 0) { nativeRecordButtons(true, false, true); } + // FIXME: This is unnecessary if we are gaining focus from the + // WebTextView. How can we tell if it was the last thing in + // focus? + setFocusControllerActive(true); //} else { // The WebView has gained focus while we do not have // windowfocus. When our window lost focus, we should have @@ -3388,12 +3478,13 @@ public class WebView extends AbsoluteLayout } } else { // When we lost focus, unless focus went to the TextView (which is - // true if we are in editing mode), stop drawing the focus ring. + // true if we are in editing mode), stop drawing the cursor ring. if (!inEditingMode()) { - mDrawFocusRing = false; + mDrawCursorRing = false; if (mNativeClass != 0) { nativeRecordButtons(false, false, true); } + setFocusControllerActive(false); } mGotKeyDown = false; } @@ -3424,8 +3515,8 @@ public class WebView extends AbsoluteLayout super.onScrollChanged(l, t, oldl, oldt); sendOurVisibleRect(); } - - + + @Override public boolean dispatchKeyEvent(KeyEvent event) { boolean dispatch = true; @@ -3469,7 +3560,7 @@ public class WebView extends AbsoluteLayout return false; } - if (LOGV_ENABLED) { + if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, ev + " at " + ev.getEventTime() + " mTouchMode=" + mTouchMode); } @@ -3494,7 +3585,7 @@ public class WebView extends AbsoluteLayout if (mForwardTouchEvents && mTouchMode != SCROLL_ZOOM_OUT && mTouchMode != SCROLL_ZOOM_ANIMATION_IN && mTouchMode != SCROLL_ZOOM_ANIMATION_OUT - && (action != MotionEvent.ACTION_MOVE || + && (action != MotionEvent.ACTION_MOVE || eventTime - mLastSentTouchTime > TOUCH_SENT_INTERVAL)) { WebViewCore.TouchEventData ted = new WebViewCore.TouchEventData(); ted.mAction = action; @@ -3525,7 +3616,7 @@ public class WebView extends AbsoluteLayout mSelectX = mScrollX + (int) x; mSelectY = mScrollY + (int) y; mTouchMode = TOUCH_SELECT_MODE; - if (LOGV_ENABLED) { + if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "select=" + mSelectX + "," + mSelectY); } nativeMoveSelection(viewToContent(mSelectX) @@ -3553,7 +3644,7 @@ public class WebView extends AbsoluteLayout break; } case MotionEvent.ACTION_MOVE: { - if (mTouchMode == TOUCH_DONE_MODE + if (mTouchMode == TOUCH_DONE_MODE || mTouchMode == SCROLL_ZOOM_ANIMATION_IN || mTouchMode == SCROLL_ZOOM_ANIMATION_OUT) { // no dragging during scroll zoom animation @@ -3570,7 +3661,7 @@ public class WebView extends AbsoluteLayout if (mTouchMode == TOUCH_SELECT_MODE) { mSelectX = mScrollX + (int) x; mSelectY = mScrollY + (int) y; - if (LOGV_ENABLED) { + if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "xtend=" + mSelectX + "," + mSelectY); } nativeMoveSelection(viewToContent(mSelectX) @@ -3605,9 +3696,6 @@ public class WebView extends AbsoluteLayout WebViewCore.pauseUpdate(mWebViewCore); int contentX = viewToContent((int) x + mScrollX); int contentY = viewToContent((int) y + mScrollY); - if (inEditingMode()) { - mTextEntry.updateCachedTextfield(); - } nativeClearFocus(contentX, contentY); // remove the zoom anchor if there is any if (mZoomScale != 0) { @@ -3618,7 +3706,7 @@ public class WebView extends AbsoluteLayout if (settings.supportZoom() && settings.getBuiltInZoomControls() && !mZoomButtonsController.isVisible() - && (canZoomScrollOut() || + && (canZoomScrollOut() || mMinZoomScale < mMaxZoomScale)) { mZoomButtonsController.setVisible(true); } @@ -3644,7 +3732,7 @@ public class WebView extends AbsoluteLayout } // reverse direction means lock in the snap mode if ((ax > MAX_SLOPE_FOR_DIAG * ay) && - ((mSnapPositive && + ((mSnapPositive && deltaX < -mMinLockSnapReverseDistance) || (!mSnapPositive && deltaX > mMinLockSnapReverseDistance))) { @@ -3658,9 +3746,9 @@ public class WebView extends AbsoluteLayout } // reverse direction means lock in the snap mode if ((ay > MAX_SLOPE_FOR_DIAG * ax) && - ((mSnapPositive && + ((mSnapPositive && deltaY < -mMinLockSnapReverseDistance) - || (!mSnapPositive && + || (!mSnapPositive && deltaY > mMinLockSnapReverseDistance))) { mSnapScrollMode = SNAP_Y_LOCK; } @@ -3725,7 +3813,7 @@ public class WebView extends AbsoluteLayout // no action during scroll animation break; case SCROLL_ZOOM_OUT: - if (LOGV_ENABLED) { + if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "ACTION_UP SCROLL_ZOOM_OUT" + " eventTime - mLastTouchTime=" + (eventTime - mLastTouchTime)); @@ -3785,16 +3873,13 @@ public class WebView extends AbsoluteLayout mTouchMode = TOUCH_DONE_MODE; int contentX = viewToContent((int) mLastTouchX + mScrollX); int contentY = viewToContent((int) mLastTouchY + mScrollY); - if (inEditingMode()) { - mTextEntry.updateCachedTextfield(); - } nativeClearFocus(contentX, contentY); break; } } return true; } - + private long mTrackballFirstTime = 0; private long mTrackballLastTime = 0; private float mTrackballRemainsX = 0.0f; @@ -3820,10 +3905,10 @@ public class WebView extends AbsoluteLayout private Rect mLastFocusBounds; // Set by default; BrowserActivity clears to interpret trackball data - // directly for movement. Currently, the framework only passes + // directly for movement. Currently, the framework only passes // arrow key events, not trackball events, from one child to the next private boolean mMapTrackballToArrowKeys = true; - + public void setMapTrackballToArrowKeys(boolean setMap) { mMapTrackballToArrowKeys = setMap; } @@ -3841,26 +3926,26 @@ public class WebView extends AbsoluteLayout return true; } if (ev.getAction() == MotionEvent.ACTION_DOWN) { - mPrivateHandler.removeMessages(SWITCH_TO_ENTER); + mPrivateHandler.removeMessages(SWITCH_TO_CLICK); mTrackballDown = true; if (mNativeClass != 0) { nativeRecordButtons(hasFocus() && hasWindowFocus(), true, true); } if (time - mLastFocusTime <= TRACKBALL_TIMEOUT - && !mLastFocusBounds.equals(nativeGetFocusRingBounds())) { + && !mLastFocusBounds.equals(nativeGetCursorRingBounds())) { nativeSelectBestAt(mLastFocusBounds); } - if (LOGV_ENABLED) { + if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "onTrackballEvent down ev=" + ev - + " time=" + time + + " time=" + time + " mLastFocusTime=" + mLastFocusTime); } if (isInTouchMode()) requestFocusFromTouch(); return false; // let common code in onKeyDown at it - } + } if (ev.getAction() == MotionEvent.ACTION_UP) { - // LONG_PRESS_ENTER is set in common onKeyDown - mPrivateHandler.removeMessages(LONG_PRESS_ENTER); + // LONG_PRESS_CENTER is set in common onKeyDown + mPrivateHandler.removeMessages(LONG_PRESS_CENTER); mTrackballDown = false; mTrackballUpTime = time; if (mShiftIsPressed) { @@ -3870,42 +3955,42 @@ public class WebView extends AbsoluteLayout mExtendSelection = true; } } - if (LOGV_ENABLED) { + if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "onTrackballEvent up ev=" + ev - + " time=" + time + + " time=" + time ); } return false; // let common code in onKeyUp at it } if (mMapTrackballToArrowKeys && mShiftIsPressed == false) { - if (LOGV_ENABLED) Log.v(LOGTAG, "onTrackballEvent gmail quit"); + if (DebugFlags.WEB_VIEW) Log.v(LOGTAG, "onTrackballEvent gmail quit"); return false; } - // no move if we're still waiting on SWITCH_TO_ENTER timeout + // no move if we're still waiting on SWITCH_TO_CLICK timeout if (mTouchMode == TOUCH_DOUBLECLICK_MODE) { - if (LOGV_ENABLED) Log.v(LOGTAG, "onTrackballEvent 2 click quit"); + if (DebugFlags.WEB_VIEW) Log.v(LOGTAG, "onTrackballEvent 2 click quit"); return true; } if (mTrackballDown) { - if (LOGV_ENABLED) Log.v(LOGTAG, "onTrackballEvent down quit"); + if (DebugFlags.WEB_VIEW) Log.v(LOGTAG, "onTrackballEvent down quit"); return true; // discard move if trackball is down } if (time - mTrackballUpTime < TRACKBALL_TIMEOUT) { - if (LOGV_ENABLED) Log.v(LOGTAG, "onTrackballEvent up timeout quit"); + if (DebugFlags.WEB_VIEW) Log.v(LOGTAG, "onTrackballEvent up timeout quit"); return true; } // TODO: alternatively we can do panning as touch does switchOutDrawHistory(); if (time - mTrackballLastTime > TRACKBALL_TIMEOUT) { - if (LOGV_ENABLED) { - Log.v(LOGTAG, "onTrackballEvent time=" + if (DebugFlags.WEB_VIEW) { + Log.v(LOGTAG, "onTrackballEvent time=" + time + " last=" + mTrackballLastTime); } mTrackballFirstTime = time; mTrackballXMove = mTrackballYMove = 0; } mTrackballLastTime = time; - if (LOGV_ENABLED) { + if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "onTrackballEvent ev=" + ev + " time=" + time); } mTrackballRemainsX += ev.getX(); @@ -3913,7 +3998,7 @@ public class WebView extends AbsoluteLayout doTrackball(time); return true; } - + void moveSelection(float xRate, float yRate) { if (mNativeClass == 0) return; @@ -3927,8 +4012,8 @@ public class WebView extends AbsoluteLayout , mSelectX)); mSelectY = Math.min(maxY, Math.max(mScrollY - SELECT_CURSOR_OFFSET , mSelectY)); - if (LOGV_ENABLED) { - Log.v(LOGTAG, "moveSelection" + if (DebugFlags.WEB_VIEW) { + Log.v(LOGTAG, "moveSelection" + " mSelectX=" + mSelectX + " mSelectY=" + mSelectY + " mScrollX=" + mScrollX @@ -3940,10 +4025,10 @@ public class WebView extends AbsoluteLayout nativeMoveSelection(viewToContent(mSelectX) , viewToContent(mSelectY), mExtendSelection); int scrollX = mSelectX < mScrollX ? -SELECT_CURSOR_OFFSET - : mSelectX > maxX - SELECT_CURSOR_OFFSET ? SELECT_CURSOR_OFFSET + : mSelectX > maxX - SELECT_CURSOR_OFFSET ? SELECT_CURSOR_OFFSET : 0; int scrollY = mSelectY < mScrollY ? -SELECT_CURSOR_OFFSET - : mSelectY > maxY - SELECT_CURSOR_OFFSET ? SELECT_CURSOR_OFFSET + : mSelectY > maxY - SELECT_CURSOR_OFFSET ? SELECT_CURSOR_OFFSET : 0; pinScrollBy(scrollX, scrollY, true, 0); Rect select = new Rect(mSelectX, mSelectY, mSelectX + 1, mSelectY + 1); @@ -4000,7 +4085,7 @@ public class WebView extends AbsoluteLayout if (elapsed == 0) { elapsed = TRACKBALL_TIMEOUT; } - float xRate = mTrackballRemainsX * 1000 / elapsed; + float xRate = mTrackballRemainsX * 1000 / elapsed; float yRate = mTrackballRemainsY * 1000 / elapsed; if (mShiftIsPressed) { moveSelection(xRate, yRate); @@ -4010,7 +4095,7 @@ public class WebView extends AbsoluteLayout float ax = Math.abs(xRate); float ay = Math.abs(yRate); float maxA = Math.max(ax, ay); - if (LOGV_ENABLED) { + if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "doTrackball elapsed=" + elapsed + " xRate=" + xRate + " yRate=" + yRate @@ -4027,9 +4112,9 @@ public class WebView extends AbsoluteLayout int maxWH = Math.max(width, height); mZoomScrollX += scaleTrackballX(xRate, maxWH); mZoomScrollY += scaleTrackballY(yRate, maxWH); - if (LOGV_ENABLED) { + if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "doTrackball SCROLL_ZOOM_OUT" - + " mZoomScrollX=" + mZoomScrollX + + " mZoomScrollX=" + mZoomScrollX + " mZoomScrollY=" + mZoomScrollY); } mZoomScrollX = Math.min(width, Math.max(0, mZoomScrollX)); @@ -4047,13 +4132,13 @@ public class WebView extends AbsoluteLayout int oldScrollX = mScrollX; int oldScrollY = mScrollY; if (count > 0) { - int selectKeyCode = ax < ay ? mTrackballRemainsY < 0 ? - KeyEvent.KEYCODE_DPAD_UP : KeyEvent.KEYCODE_DPAD_DOWN : + int selectKeyCode = ax < ay ? mTrackballRemainsY < 0 ? + KeyEvent.KEYCODE_DPAD_UP : KeyEvent.KEYCODE_DPAD_DOWN : mTrackballRemainsX < 0 ? KeyEvent.KEYCODE_DPAD_LEFT : KeyEvent.KEYCODE_DPAD_RIGHT; count = Math.min(count, TRACKBALL_MOVE_COUNT); - if (LOGV_ENABLED) { - Log.v(LOGTAG, "doTrackball keyCode=" + selectKeyCode + if (DebugFlags.WEB_VIEW) { + Log.v(LOGTAG, "doTrackball keyCode=" + selectKeyCode + " count=" + count + " mTrackballRemainsX=" + mTrackballRemainsX + " mTrackballRemainsY=" + mTrackballRemainsY); @@ -4066,12 +4151,12 @@ public class WebView extends AbsoluteLayout if (count >= TRACKBALL_SCROLL_COUNT) { int xMove = scaleTrackballX(xRate, width); int yMove = scaleTrackballY(yRate, height); - if (LOGV_ENABLED) { + if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "doTrackball pinScrollBy" + " count=" + count + " xMove=" + xMove + " yMove=" + yMove - + " mScrollX-oldScrollX=" + (mScrollX-oldScrollX) - + " mScrollY-oldScrollY=" + (mScrollY-oldScrollY) + + " mScrollX-oldScrollX=" + (mScrollX-oldScrollX) + + " mScrollY-oldScrollY=" + (mScrollY-oldScrollY) ); } if (Math.abs(mScrollX - oldScrollX) > Math.abs(xMove)) { @@ -4084,18 +4169,18 @@ public class WebView extends AbsoluteLayout pinScrollBy(xMove, yMove, true, 0); } mUserScroll = true; - } - mWebViewCore.sendMessage(EventHub.UNBLOCK_FOCUS); + } + mWebViewCore.sendMessage(EventHub.UNBLOCK_FOCUS); } public void flingScroll(int vx, int vy) { int maxX = Math.max(computeHorizontalScrollRange() - getViewWidth(), 0); int maxY = Math.max(computeVerticalScrollRange() - getViewHeight(), 0); - + mScroller.fling(mScrollX, mScrollY, vx, vy, 0, maxX, 0, maxY); invalidate(); } - + private void doFling() { if (mVelocityTracker == null) { return; @@ -4114,7 +4199,7 @@ public class WebView extends AbsoluteLayout vx = 0; } } - + if (true /* EMG release: make our fling more like Maps' */) { // maps cuts their velocity in half vx = vx * 3 / 4; @@ -4175,7 +4260,7 @@ public class WebView extends AbsoluteLayout } if (mZoomControls == null) { mZoomControls = createZoomControls(); - + /* * need to be set to VISIBLE first so that getMeasuredHeight() in * {@link #onSizeChanged()} can return the measured value for proper @@ -4184,7 +4269,7 @@ public class WebView extends AbsoluteLayout mZoomControls.setVisibility(View.VISIBLE); mZoomControlRunnable = new Runnable() { public void run() { - + /* Don't dismiss the controls if the user has * focus on them. Wait and check again later. */ @@ -4236,7 +4321,7 @@ public class WebView extends AbsoluteLayout /** * Gets the {@link ZoomButtonsController} which can be used to add * additional buttons to the zoom controls window. - * + * * @return The instance of {@link ZoomButtonsController} used by this class, * or null if it is unavailable. * @hide @@ -4274,19 +4359,15 @@ public class WebView extends AbsoluteLayout int contentY = viewToContent((int) mLastTouchY + mScrollY); Rect rect = new Rect(contentX - mNavSlop, contentY - mNavSlop, contentX + mNavSlop, contentY + mNavSlop); - // If we were already focused on a textfield, update its cache. - if (inEditingMode()) { - mTextEntry.updateCachedTextfield(); - } nativeSelectBestAt(rect); } /*package*/ void shortPressOnTextField() { if (inEditingMode()) { - View v = mTextEntry; + View v = mWebTextView; int x = viewToContent((v.getLeft() + v.getRight()) >> 1); int y = viewToContent((v.getTop() + v.getBottom()) >> 1); - nativeMotionUp(x, y, mNavSlop, true); + nativeMotionUp(x, y, mNavSlop); } } @@ -4298,14 +4379,13 @@ public class WebView extends AbsoluteLayout // mLastTouchX and mLastTouchY are the point in the current viewport int contentX = viewToContent((int) mLastTouchX + mScrollX); int contentY = viewToContent((int) mLastTouchY + mScrollY); - if (nativeMotionUp(contentX, contentY, mNavSlop, true)) { + if (nativeMotionUp(contentX, contentY, mNavSlop)) { if (mLogEvent) { Checkin.updateStats(mContext.getContentResolver(), Checkin.Stats.Tag.BROWSER_SNAP_CENTER, 1, 0.0); } } - if (nativeUpdateFocusNode() && !mFocusNode.mIsTextField - && !mFocusNode.mIsTextArea) { + if (nativeHasCursorNode() && !nativeCursorIsTextInput()) { playSoundEffect(SoundEffectConstants.CLICK); } } @@ -4320,7 +4400,8 @@ public class WebView extends AbsoluteLayout public boolean requestFocus(int direction, Rect previouslyFocusedRect) { boolean result = false; if (inEditingMode()) { - result = mTextEntry.requestFocus(direction, previouslyFocusedRect); + result = mWebTextView.requestFocus(direction, + previouslyFocusedRect); } else { result = super.requestFocus(direction, previouslyFocusedRect); if (mWebViewCore.getSettings().getNeedInitialFocus()) { @@ -4344,7 +4425,7 @@ public class WebView extends AbsoluteLayout default: return result; } - if (mNativeClass != 0 && !nativeUpdateFocusNode()) { + if (mNativeClass != 0 && !nativeHasCursorNode()) { navHandledKey(fakeKeyDirection, 1, true, 0); } } @@ -4413,14 +4494,19 @@ public class WebView extends AbsoluteLayout int scrollYDelta = 0; - if (rect.bottom > screenBottom && rect.top > screenTop) { - if (rect.height() > height) { - scrollYDelta += (rect.top - screenTop); + if (rect.bottom > screenBottom) { + int oneThirdOfScreenHeight = height / 3; + if (rect.height() > 2 * oneThirdOfScreenHeight) { + // If the rectangle is too tall to fit in the bottom two thirds + // of the screen, place it at the top. + scrollYDelta = rect.top - screenTop; } else { - scrollYDelta += (rect.bottom - screenBottom); + // If the rectangle will still fit on screen, we want its + // top to be in the top third of the screen. + scrollYDelta = rect.top - (screenTop + oneThirdOfScreenHeight); } } else if (rect.top < screenTop) { - scrollYDelta -= (screenTop - rect.top); + scrollYDelta = rect.top - screenTop; } int width = getWidth() - getVerticalScrollbarWidth(); @@ -4445,33 +4531,31 @@ public class WebView extends AbsoluteLayout return false; } - + /* package */ void replaceTextfieldText(int oldStart, int oldEnd, String replace, int newStart, int newEnd) { HashMap arg = new HashMap(); - arg.put("focusData", new WebViewCore.FocusData(mFocusData)); arg.put("replace", replace); - arg.put("start", new Integer(newStart)); - arg.put("end", new Integer(newEnd)); + arg.put("start", Integer.valueOf(newStart)); + arg.put("end", Integer.valueOf(newEnd)); mTextGeneration++; mWebViewCore.sendMessage(EventHub.REPLACE_TEXT, oldStart, oldEnd, arg); } /* package */ void passToJavaScript(String currentText, KeyEvent event) { HashMap arg = new HashMap(); - arg.put("focusData", new WebViewCore.FocusData(mFocusData)); arg.put("event", event); arg.put("currentText", currentText); // Increase our text generation number, and pass it to webcore thread mTextGeneration++; mWebViewCore.sendMessage(EventHub.PASS_TO_JS, mTextGeneration, 0, arg); // WebKit's document state is not saved until about to leave the page. - // To make sure the host application, like Browser, has the up to date - // document state when it goes to background, we force to save the + // To make sure the host application, like Browser, has the up to date + // document state when it goes to background, we force to save the // document state. mWebViewCore.removeMessages(EventHub.SAVE_DOCUMENT_STATE); mWebViewCore.sendMessageDelayed(EventHub.SAVE_DOCUMENT_STATE, - new WebViewCore.FocusData(mFocusData), 1000); + cursorData(), 1000); } /* package */ WebViewCore getWebViewCore() { @@ -4489,9 +4573,9 @@ public class WebView extends AbsoluteLayout class PrivateHandler extends Handler { @Override public void handleMessage(Message msg) { - if (LOGV_ENABLED) { - Log.v(LOGTAG, msg.what < REMEMBER_PASSWORD || msg.what - > INVAL_RECT_MSG_ID ? Integer.toString(msg.what) + if (DebugFlags.WEB_VIEW) { + Log.v(LOGTAG, msg.what < REMEMBER_PASSWORD || msg.what + > INVAL_RECT_MSG_ID ? Integer.toString(msg.what) : HandlerDebugString[msg.what - REMEMBER_PASSWORD]); } switch (msg.what) { @@ -4519,22 +4603,31 @@ public class WebView extends AbsoluteLayout case SWITCH_TO_LONGPRESS: { mTouchMode = TOUCH_DONE_MODE; performLongClick(); - updateTextEntry(); + rebuildWebTextView(); break; } - case SWITCH_TO_ENTER: - if (LOGV_ENABLED) Log.v(LOGTAG, "SWITCH_TO_ENTER"); + case SWITCH_TO_CLICK: mTouchMode = TOUCH_DONE_MODE; - onKeyUp(KeyEvent.KEYCODE_ENTER - , new KeyEvent(KeyEvent.ACTION_UP - , KeyEvent.KEYCODE_ENTER)); + Rect visibleRect = sendOurVisibleRect(); + // Note that sendOurVisibleRect calls viewToContent, so the + // coordinates should be in content coordinates. + if (!nativeCursorIntersects(visibleRect)) { + break; + } + nativeSetFollowedLink(true); + mWebViewCore.sendMessage(EventHub.SET_MOVE_MOUSE, + cursorData()); + playSoundEffect(SoundEffectConstants.CLICK); + if (!mCallbackProxy.uiOverrideUrlLoading(nativeCursorText())) { + mWebViewCore.sendMessage(EventHub.CLICK); + } break; case SCROLL_BY_MSG_ID: setContentScrollBy(msg.arg1, msg.arg2, (Boolean) msg.obj); break; case SYNC_SCROLL_TO_MSG_ID: if (mUserScroll) { - // if user has scrolled explicitly, don't sync the + // if user has scrolled explicitly, don't sync the // scroll position any more mUserScroll = false; break; @@ -4543,7 +4636,7 @@ public class WebView extends AbsoluteLayout case SCROLL_TO_MSG_ID: if (setContentScrollTo(msg.arg1, msg.arg2)) { // if we can't scroll to the exact position due to pin, - // send a message to WebCore to re-scroll when we get a + // send a message to WebCore to re-scroll when we get a // new picture mUserScroll = false; mWebViewCore.sendMessage(EventHub.SYNC_SCROLL, @@ -4555,7 +4648,7 @@ public class WebView extends AbsoluteLayout break; case NEW_PICTURE_MSG_ID: // called for new content - final WebViewCore.DrawData draw = + final WebViewCore.DrawData draw = (WebViewCore.DrawData) msg.obj; final Point viewSize = draw.mViewPoint; if (mZoomScale > 0) { @@ -4578,9 +4671,9 @@ public class WebView extends AbsoluteLayout // received in the fixed dimension. final boolean updateLayout = viewSize.x == mLastWidthSent && viewSize.y == mLastHeightSent; - recordNewContentSize(draw.mWidthHeight.x, + recordNewContentSize(draw.mWidthHeight.x, draw.mWidthHeight.y, updateLayout); - if (LOGV_ENABLED) { + if (DebugFlags.WEB_VIEW) { Rect b = draw.mInvalRegion.getBounds(); Log.v(LOGTAG, "NEW_PICTURE_MSG_ID {" + b.left+","+b.top+","+b.right+","+b.bottom+"}"); @@ -4596,22 +4689,23 @@ public class WebView extends AbsoluteLayout break; case UPDATE_TEXTFIELD_TEXT_MSG_ID: // Make sure that the textfield is currently focused - // and representing the same node as the pointer. - if (inEditingMode() && - mTextEntry.isSameTextField(msg.arg1)) { + // and representing the same node as the pointer. + if (inEditingMode() && + mWebTextView.isSameTextField(msg.arg1)) { if (msg.getData().getBoolean("password")) { - Spannable text = (Spannable) mTextEntry.getText(); + Spannable text = (Spannable) mWebTextView.getText(); int start = Selection.getSelectionStart(text); int end = Selection.getSelectionEnd(text); - mTextEntry.setInPassword(true); + mWebTextView.setInPassword(true); // Restore the selection, which may have been // ruined by setInPassword. - Spannable pword = (Spannable) mTextEntry.getText(); + Spannable pword = + (Spannable) mWebTextView.getText(); Selection.setSelection(pword, start, end); // If the text entry has created more events, ignore // this one. } else if (msg.arg2 == mTextGeneration) { - mTextEntry.setTextAndKeepSelection( + mWebTextView.setTextAndKeepSelection( (String) msg.obj); } } @@ -4621,7 +4715,7 @@ public class WebView extends AbsoluteLayout break; } // Do not reset the focus or clear the text; the user may have already -// navigated or entered text at this point. The focus should have gotten +// navigated or entered text at this point. The focus should have gotten // reset, if need be, when the focus cache was built. Similarly, the text // view should already be torn down and rebuilt if needed. // nativeResetFocus(); @@ -4656,6 +4750,7 @@ public class WebView extends AbsoluteLayout if (mInitialScale > 0) { scale = mInitialScale / 100.0f; } else { + if (initialScale < 0) break; if (mWebViewCore.getSettings().getUseWideViewPort()) { // force viewSizeChanged by setting mLastWidthSent // to 0 @@ -4676,22 +4771,13 @@ public class WebView extends AbsoluteLayout case MARK_NODE_INVALID_ID: nativeMarkNodeInvalid(msg.arg1); break; - case NOTIFY_FOCUS_SET_MSG_ID: - if (mNativeClass != 0) { - nativeNotifyFocusSet(inEditingMode()); - } - break; case UPDATE_TEXT_ENTRY_MSG_ID: - // this is sent after finishing resize in WebViewCore. Make + // this is sent after finishing resize in WebViewCore. Make // sure the text edit box is still on the screen. - boolean alreadyThere = inEditingMode(); - if (alreadyThere && nativeUpdateFocusNode()) { - FocusNode node = mFocusNode; - if (node.mIsTextField || node.mIsTextArea) { - mTextEntry.bringIntoView(); - } + if (inEditingMode() && nativeCursorIsTextInput()) { + mWebTextView.bringIntoView(); } - updateTextEntry(); + rebuildWebTextView(); break; case RECOMPUTE_FOCUS_MSG_ID: if (mNativeClass != 0) { @@ -4709,17 +4795,17 @@ public class WebView extends AbsoluteLayout } break; } - case UPDATE_TEXT_ENTRY_ADAPTER: + case REQUEST_FORM_DATA: HashMap data = (HashMap) msg.obj; - if (mTextEntry.isSameTextField(msg.arg1)) { + if (mWebTextView.isSameTextField(msg.arg1)) { AutoCompleteAdapter adapter = (AutoCompleteAdapter) data.get("adapter"); - mTextEntry.setAdapterCustom(adapter); + mWebTextView.setAdapterCustom(adapter); } break; case UPDATE_CLIPBOARD: String str = (String) msg.obj; - if (LOGV_ENABLED) { + if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "UPDATE_CLIPBOARD " + str); } try { @@ -4734,12 +4820,12 @@ public class WebView extends AbsoluteLayout WebViewCore.resumeUpdate(mWebViewCore); break; - case LONG_PRESS_ENTER: + case LONG_PRESS_CENTER: // as this is shared by keydown and trackballdown, reset all // the states - mGotEnterDown = false; + mGotCenterDown = false; mTrackballDown = false; - // LONG_PRESS_ENTER is sent as a delayed message. If we + // LONG_PRESS_CENTER is sent as a delayed message. If we // switch to windows overview, the WebView will be // temporarily removed from the view system. In that case, // do nothing. @@ -4770,16 +4856,12 @@ public class WebView extends AbsoluteLayout // Class used to use a dropdown for a <select> element private class InvokeListBox implements Runnable { - // Strings for the labels in the listbox. - private String[] mArray; - // Array representing whether each item is enabled. - private boolean[] mEnableArray; // Whether the listbox allows multiple selection. private boolean mMultiple; // Passed in to a list with multiple selection to tell // which items are selected. private int[] mSelectedArray; - // Passed in to a list with single selection to tell + // Passed in to a list with single selection to tell // where the initial selection is. private int mSelection; @@ -4798,14 +4880,14 @@ public class WebView extends AbsoluteLayout } /** - * Subclass ArrayAdapter so we can disable OptionGroupLabels, + * Subclass ArrayAdapter so we can disable OptionGroupLabels, * and allow filtering. */ private class MyArrayListAdapter extends ArrayAdapter<Container> { public MyArrayListAdapter(Context context, Container[] objects, boolean multiple) { - super(context, + super(context, multiple ? com.android.internal.R.layout.select_dialog_multichoice : - com.android.internal.R.layout.select_dialog_singlechoice, + com.android.internal.R.layout.select_dialog_singlechoice, objects); } @@ -4863,7 +4945,7 @@ public class WebView extends AbsoluteLayout } } - private InvokeListBox(String[] array, boolean[] enabled, int + private InvokeListBox(String[] array, boolean[] enabled, int selection) { mSelection = selection; mMultiple = false; @@ -4926,31 +5008,36 @@ public class WebView extends AbsoluteLayout public void run() { final ListView listView = (ListView) LayoutInflater.from(mContext) .inflate(com.android.internal.R.layout.select_dialog, null); - final MyArrayListAdapter adapter = new + final MyArrayListAdapter adapter = new MyArrayListAdapter(mContext, mContainers, mMultiple); AlertDialog.Builder b = new AlertDialog.Builder(mContext) .setView(listView).setCancelable(true) .setInverseBackgroundForced(true); - + if (mMultiple) { b.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { mWebViewCore.sendMessage( - EventHub.LISTBOX_CHOICES, + EventHub.LISTBOX_CHOICES, adapter.getCount(), 0, listView.getCheckedItemPositions()); }}); - b.setNegativeButton(android.R.string.cancel, null); + b.setNegativeButton(android.R.string.cancel, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + mWebViewCore.sendMessage( + EventHub.SINGLE_LISTBOX_CHOICE, -2, 0); + }}); } final AlertDialog dialog = b.create(); listView.setAdapter(adapter); listView.setFocusableInTouchMode(true); // There is a bug (1250103) where the checks in a ListView with // multiple items selected are associated with the positions, not - // the ids, so the items do not properly retain their checks when + // the ids, so the items do not properly retain their checks when // filtered. Do not allow filtering on multiple lists until // that bug is fixed. - + listView.setTextFilterEnabled(!mMultiple); if (mMultiple) { listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); @@ -5013,48 +5100,26 @@ public class WebView extends AbsoluteLayout } // called by JNI - private void sendFinalFocus(int frame, int node, int x, int y) { - WebViewCore.FocusData focusData = new WebViewCore.FocusData(); - focusData.mFrame = frame; - focusData.mNode = node; - focusData.mX = x; - focusData.mY = y; - mWebViewCore.sendMessage(EventHub.SET_FINAL_FOCUS, - EventHub.NO_FOCUS_CHANGE_BLOCK, 0, focusData); + private void sendMoveMouse(int frame, int node, int x, int y) { + mWebViewCore.sendMessage(EventHub.SET_MOVE_MOUSE, + new WebViewCore.CursorData(frame, node, x, y)); } // called by JNI - private void setFocusData(int moveGeneration, int buildGeneration, - int frame, int node, int x, int y, boolean ignoreNullFocus) { - mFocusData.mMoveGeneration = moveGeneration; - mFocusData.mBuildGeneration = buildGeneration; - mFocusData.mFrame = frame; - mFocusData.mNode = node; - mFocusData.mX = x; - mFocusData.mY = y; - mFocusData.mIgnoreNullFocus = ignoreNullFocus; - } - - // called by JNI - private void sendKitFocus() { - WebViewCore.FocusData focusData = new WebViewCore.FocusData(mFocusData); - mWebViewCore.sendMessage(EventHub.SET_KIT_FOCUS, focusData); + private void sendMoveMouseIfLatest() { + mWebViewCore.sendMessage(EventHub.SET_MOVE_MOUSE_IF_LATEST, cursorData()); } // called by JNI - private void sendMotionUp(int touchGeneration, int buildGeneration, - int frame, int node, int x, int y, int size, boolean isClick, - boolean retry) { + private void sendMotionUp(int touchGeneration, + int frame, int node, int x, int y, int size) { WebViewCore.TouchUpData touchUpData = new WebViewCore.TouchUpData(); touchUpData.mMoveGeneration = touchGeneration; - touchUpData.mBuildGeneration = buildGeneration; touchUpData.mSize = size; - touchUpData.mIsClick = isClick; - touchUpData.mRetry = retry; - mFocusData.mFrame = touchUpData.mFrame = frame; - mFocusData.mNode = touchUpData.mNode = node; - mFocusData.mX = touchUpData.mX = x; - mFocusData.mY = touchUpData.mY = y; + touchUpData.mFrame = frame; + touchUpData.mNode = node; + touchUpData.mX = x; + touchUpData.mY = y; mWebViewCore.sendMessage(EventHub.TOUCH_UP, touchUpData); } @@ -5093,7 +5158,7 @@ public class WebView extends AbsoluteLayout private void viewInvalidate() { invalidate(); } - + // return true if the key was handled private boolean navHandledKey(int keyCode, int count, boolean noScroll , long time) { @@ -5101,9 +5166,9 @@ public class WebView extends AbsoluteLayout return false; } mLastFocusTime = time; - mLastFocusBounds = nativeGetFocusRingBounds(); + mLastFocusBounds = nativeGetCursorRingBounds(); boolean keyHandled = nativeMoveFocus(keyCode, count, noScroll) == false; - if (LOGV_ENABLED) { + if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "navHandledKey mLastFocusBounds=" + mLastFocusBounds + " mLastFocusTime=" + mLastFocusTime + " handled=" + keyHandled); @@ -5111,7 +5176,7 @@ public class WebView extends AbsoluteLayout if (keyHandled == false || mHeightCanMeasure == false) { return keyHandled; } - Rect contentFocus = nativeGetFocusRingBounds(); + Rect contentFocus = nativeGetCursorRingBounds(); if (contentFocus.isEmpty()) return keyHandled; Rect viewFocus = contentToView(contentFocus); Rect visRect = new Rect(); @@ -5135,14 +5200,14 @@ public class WebView extends AbsoluteLayout } if (mLastFocusBounds.isEmpty()) return keyHandled; if (mLastFocusBounds.equals(contentFocus)) return keyHandled; - if (LOGV_ENABLED) { + if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "navHandledKey contentFocus=" + contentFocus); } requestRectangleOnScreen(viewFocus); mUserScroll = true; return keyHandled; } - + /** * Set the background color. It's white by default. Pass * zero to make the view transparent. @@ -5157,7 +5222,7 @@ public class WebView extends AbsoluteLayout nativeDebugDump(); mWebViewCore.sendMessage(EventHub.DUMP_NAVTREE); } - + /** * Update our cache with updatedText. * @param updatedText The new text to put in our cache. @@ -5167,52 +5232,70 @@ public class WebView extends AbsoluteLayout // we recognize that it is up to date. nativeUpdateCachedTextfield(updatedText, mTextGeneration); } - - // Never call this version except by updateCachedTextfield(String) - - // we always want to pass in our generation number. - private native void nativeUpdateCachedTextfield(String updatedText, - int generation); + private native void nativeClearFocus(int x, int y); private native void nativeCreate(int ptr); + private native int nativeCursorFramePointer(); + private native Rect nativeCursorNodeBounds(); + /* package */ native int nativeCursorNodePointer(); + /* package */ native boolean nativeCursorMatchesFocus(); + private native boolean nativeCursorIntersects(Rect visibleRect); + private native boolean nativeCursorIsAnchor(); + private native boolean nativeCursorIsTextInput(); + private native String nativeCursorText(); + private native boolean nativeCursorWantsKeyEvents(); private native void nativeDebugDump(); private native void nativeDestroy(); - private native void nativeDrawFocusRing(Canvas content); + private native void nativeDrawCursorRing(Canvas content); + private native void nativeDrawMatches(Canvas canvas); private native void nativeDrawSelection(Canvas content , int x, int y, boolean extendSelection); private native void nativeDrawSelectionRegion(Canvas content); - private native boolean nativeUpdateFocusNode(); - private native Rect nativeGetFocusRingBounds(); - private native Rect nativeGetNavBounds(); + private native void nativeDumpDisplayTree(String urlOrNull); + private native int nativeFindAll(String findLower, String findUpper); + private native void nativeFindNext(boolean forward); + private native boolean nativeFocusIsPassword(); + private native boolean nativeFocusIsRtlText(); + private native boolean nativeFocusIsTextField(); + private native boolean nativeFocusIsTextInput(); + private native int nativeFocusMaxLength(); + private native String nativeFocusName(); + private native Rect nativeFocusNodeBounds(); + /* package */ native int nativeFocusNodePointer(); + private native String nativeFocusText(); + private native int nativeFocusTextSize(); + /** + * Returns true if the native focus nodes says it wants to handle key events + * (ala plugins). This can only be called if mNativeClass is non-zero! + */ + private native Rect nativeGetCursorRingBounds(); + private native Region nativeGetSelection(); + private native boolean nativeHasCursorNode(); + private native boolean nativeHasFocusNode(); + private native String nativeImageURI(int x, int y); private native void nativeInstrumentReport(); private native void nativeMarkNodeInvalid(int node); // return true if the page has been scrolled - private native boolean nativeMotionUp(int x, int y, int slop, boolean isClick); + private native boolean nativeMotionUp(int x, int y, int slop); // returns false if it handled the key - private native boolean nativeMoveFocus(int keyCode, int count, + private native boolean nativeMoveFocus(int keyCode, int count, boolean noScroll); - private native void nativeNotifyFocusSet(boolean inEditingMode); + private native int nativeMoveGeneration(); + private native void nativeMoveSelection(int x, int y, + boolean extendSelection); private native void nativeRecomputeFocus(); // Like many other of our native methods, you must make sure that // mNativeClass is not null before calling this method. private native void nativeRecordButtons(boolean focused, boolean pressed, boolean invalidate); - private native void nativeResetFocus(); - private native void nativeResetNavClipBounds(); private native void nativeSelectBestAt(Rect rect); private native void nativeSetFindIsDown(); private native void nativeSetFollowedLink(boolean followed); private native void nativeSetHeightCanMeasure(boolean measure); - private native void nativeSetNavBounds(Rect rect); - private native void nativeSetNavClipBounds(Rect rect); - private native String nativeImageURI(int x, int y); - /** - * Returns true if the native focus nodes says it wants to handle key events - * (ala plugins). This can only be called if mNativeClass is non-zero! - */ - private native boolean nativeFocusNodeWantsKeyEvents(); - private native void nativeMoveSelection(int x, int y - , boolean extendSelection); - private native Region nativeGetSelection(); + private native int nativeTextGeneration(); + // Never call this version except by updateCachedTextfield(String) - + // we always want to pass in our generation number. + private native void nativeUpdateCachedTextfield(String updatedText, + int generation); - private native void nativeDumpDisplayTree(String urlOrNull); } diff --git a/core/java/android/webkit/WebViewCore.java b/core/java/android/webkit/WebViewCore.java index e9df453..f4b99b9 100644 --- a/core/java/android/webkit/WebViewCore.java +++ b/core/java/android/webkit/WebViewCore.java @@ -41,8 +41,6 @@ import junit.framework.Assert; final class WebViewCore { private static final String LOGTAG = "webcore"; - static final boolean DEBUG = false; - static final boolean LOGV_ENABLED = DEBUG; static { // Load libwebcore during static initialization. This happens in the @@ -96,7 +94,7 @@ final class WebViewCore { private int mViewportMaximumScale = 0; private boolean mViewportUserScalable = true; - + private int mRestoredScale = 100; private int mRestoredX = 0; private int mRestoredY = 0; @@ -143,6 +141,8 @@ final class WebViewCore { // The WebIconDatabase needs to be initialized within the UI thread so // just request the instance here. WebIconDatabase.getInstance(); + // Create the WebStorage singleton + WebStorage.getInstance(); // Send a message to initialize the WebViewCore. Message init = sWebCoreHandler.obtainMessage( WebCoreThread.INITIALIZE, this); @@ -162,6 +162,8 @@ final class WebViewCore { mSettings.syncSettingsAndCreateHandler(mBrowserFrame); // Create the handler and transfer messages for the IconDatabase WebIconDatabase.getInstance().createHandler(); + // Create the handler for WebStorage + WebStorage.getInstance().createHandler(); // The transferMessages call will transfer all pending messages to the // WebCore thread handler. mEventHub.transferMessages(); @@ -225,6 +227,16 @@ final class WebViewCore { } /** + * Add an error message to the client's console. + * @param message The message to add + * @param lineNumber the line on which the error occurred + * @param sourceID the filename of the source that caused the error. + */ + protected void addMessageToConsole(String message, int lineNumber, String sourceID) { + mCallbackProxy.addMessageToConsole(message, lineNumber, sourceID); + } + + /** * Invoke a javascript alert. * @param message The message displayed in the alert. */ @@ -233,6 +245,27 @@ final class WebViewCore { } /** + * Notify the user that the origin has exceeded it's database quota. + * @param url The URL that caused the overflow. + * @param databaseIdentifier The identifier of the database. + * @param currentQuota The current quota for the origin. + */ + protected void exceededDatabaseQuota(String url, + String databaseIdentifier, + long currentQuota) { + // Inform the callback proxy of the quota overflow. Send an object + // that encapsulates a call to the nativeSetDatabaseQuota method to + // awaken the sleeping webcore thread when a decision from the + // client to allow or deny quota is available. + mCallbackProxy.onExceededDatabaseQuota(url, databaseIdentifier, + currentQuota, new WebStorage.QuotaUpdater() { + public void updateQuota(long quota) { + nativeSetDatabaseQuota(quota); + } + }); + } + + /** * Invoke a javascript confirm dialog. * @param message The message displayed in the dialog. * @return True if the user confirmed or false if the user cancelled. @@ -283,25 +316,30 @@ final class WebViewCore { * Empty the picture set. */ private native void nativeClearContent(); - + /** * Create a flat picture from the set of pictures. */ private native void nativeCopyContentToPicture(Picture picture); - + /** * Draw the picture set with a background color. Returns true - * if some individual picture took too long to draw and can be + * if some individual picture took too long to draw and can be * split into parts. Called from the UI thread. */ private native boolean nativeDrawContent(Canvas canvas, int color); - + + /** + * check to see if picture is blank and in progress + */ + private native boolean nativePictureReady(); + /** * Redraw a portion of the picture set. The Point wh returns the * width and height of the overall picture. */ private native boolean nativeRecordContent(Region invalRegion, Point wh); - + /** * Splits slow parts of the picture set. Called from the webkit * thread after nativeDrawContent returns true. @@ -329,60 +367,58 @@ final class WebViewCore { float scale, int realScreenWidth, int screenHeight); private native int nativeGetContentMinPrefWidth(); - + // Start: functions that deal with text editing - private native void nativeReplaceTextfieldText(int frame, int node, int x, - int y, int oldStart, int oldEnd, String replace, int newStart, - int newEnd); + private native void nativeReplaceTextfieldText( + int oldStart, int oldEnd, String replace, int newStart, int newEnd); - private native void passToJs(int frame, int node, int x, int y, int gen, + private native void passToJs(int gen, String currentText, int keyCode, int keyValue, boolean down, boolean cap, boolean fn, boolean sym); + private native void nativeSetFocusControllerActive(boolean active); + private native void nativeSaveDocumentState(int frame); - private native void nativeSetFinalFocus(int framePtr, int nodePtr, int x, - int y, boolean block); + private native void nativeMoveMouse(int framePtr, int nodePtr, int x, + int y); - private native void nativeSetKitFocus(int moveGeneration, - int buildGeneration, int framePtr, int nodePtr, int x, int y, + private native void nativeMoveMouseIfLatest(int moveGeneration, + int framePtr, int nodePtr, int x, int y, boolean ignoreNullFocus); private native String nativeRetrieveHref(int framePtr, int nodePtr); - - private native void nativeTouchUp(int touchGeneration, - int buildGeneration, int framePtr, int nodePtr, int x, int y, - int size, boolean isClick, boolean retry); + + private native void nativeTouchUp(int touchGeneration, + int framePtr, int nodePtr, int x, int y, + int size); private native boolean nativeHandleTouchEvent(int action, int x, int y); private native void nativeUnblockFocus(); - + private native void nativeUpdateFrameCache(); - + private native void nativeSetSnapAnchor(int x, int y); - + private native void nativeSnapToAnchor(); - + private native void nativeSetBackgroundColor(int color); - + private native void nativeDumpDomTree(boolean useFile); private native void nativeDumpRenderTree(boolean useFile); private native void nativeDumpNavTree(); - private native void nativeRefreshPlugins(boolean reloadOpenPages); - /** * Delete text from start to end in the focused textfield. If there is no - * focus, or if start == end, silently fail. If start and end are out of + * focus, or if start == end, silently fail. If start and end are out of * order, swap them. * @param start Beginning of selection to delete. * @param end End of selection to delete. */ - private native void nativeDeleteSelection(int frame, int node, int x, int y, - int start, int end); + private native void nativeDeleteSelection(int start, int end); /** * Set the selection to (start, end) in the focused textfield. If start and @@ -390,15 +426,22 @@ final class WebViewCore { * @param start Beginning of selection. * @param end End of selection. */ - private native void nativeSetSelection(int frame, int node, int x, int y, - int start, int end); + private native void nativeSetSelection(int start, int end); private native String nativeGetSelection(Region sel); - + // Register a scheme to be treated as local scheme so that it can access // local asset files for resources private native void nativeRegisterURLSchemeAsLocal(String scheme); + /* + * Inform webcore that the user has decided whether to allow or deny new + * quota for the current origin and that the main thread should wake up + * now. + * @param quota The new quota. + */ + private native void nativeSetDatabaseQuota(long quota); + // EventHub for processing messages private final EventHub mEventHub; // WebCore thread handler @@ -447,7 +490,7 @@ final class WebViewCore { CacheManager.endCacheTransaction(); CacheManager.startCacheTransaction(); sendMessageDelayed( - obtainMessage(CACHE_TICKER), + obtainMessage(CACHE_TICKER), CACHE_TICKER_INTERVAL); } break; @@ -472,19 +515,15 @@ final class WebViewCore { } } - static class FocusData { - FocusData() {} - FocusData(FocusData d) { - mMoveGeneration = d.mMoveGeneration; - mBuildGeneration = d.mBuildGeneration; - mFrame = d.mFrame; - mNode = d.mNode; - mX = d.mX; - mY = d.mY; - mIgnoreNullFocus = d.mIgnoreNullFocus; + static class CursorData { + CursorData() {} + CursorData(int frame, int node, int x, int y) { + mFrame = frame; + mNode = node; + mX = x; + mY = y; } int mMoveGeneration; - int mBuildGeneration; int mFrame; int mNode; int mX; @@ -494,14 +533,11 @@ final class WebViewCore { static class TouchUpData { int mMoveGeneration; - int mBuildGeneration; int mFrame; int mNode; int mX; int mY; int mSize; - boolean mIsClick; - boolean mRetry; } static class TouchEventData { @@ -536,23 +572,26 @@ final class WebViewCore { "DELETE_SELECTION", // = 122; "LISTBOX_CHOICES", // = 123; "SINGLE_LISTBOX_CHOICE", // = 124; - "125", + "MESSAGE_RELAY", // = 125; "SET_BACKGROUND_COLOR", // = 126; "UNBLOCK_FOCUS", // = 127; "SAVE_DOCUMENT_STATE", // = 128; "GET_SELECTION", // = 129; "WEBKIT_DRAW", // = 130; "SYNC_SCROLL", // = 131; - "REFRESH_PLUGINS", // = 132; + "POST_URL", // = 132; "SPLIT_PICTURE_SET", // = 133; "CLEAR_CONTENT", // = 134; - "SET_FINAL_FOCUS", // = 135; - "SET_KIT_FOCUS", // = 136; + "SET_MOVE_MOUSE", // = 135; + "SET_MOVE_MOUSE_IF_LATEST", // = 136; "REQUEST_FOCUS_HREF", // = 137; "ADD_JS_INTERFACE", // = 138; "LOAD_DATA", // = 139; "TOUCH_UP", // = 140; "TOUCH_EVENT", // = 141; + "SET_ACTIVE", // = 142; + "ON_PAUSE", // = 143 + "ON_RESUME", // = 144 }; class EventHub { @@ -582,19 +621,20 @@ final class WebViewCore { static final int DELETE_SELECTION = 122; static final int LISTBOX_CHOICES = 123; static final int SINGLE_LISTBOX_CHOICE = 124; + static final int MESSAGE_RELAY = 125; static final int SET_BACKGROUND_COLOR = 126; static final int UNBLOCK_FOCUS = 127; static final int SAVE_DOCUMENT_STATE = 128; static final int GET_SELECTION = 129; static final int WEBKIT_DRAW = 130; static final int SYNC_SCROLL = 131; - static final int REFRESH_PLUGINS = 132; + static final int POST_URL = 132; static final int SPLIT_PICTURE_SET = 133; static final int CLEAR_CONTENT = 134; - + // UI nav messages - static final int SET_FINAL_FOCUS = 135; - static final int SET_KIT_FOCUS = 136; + static final int SET_MOVE_MOUSE = 135; + static final int SET_MOVE_MOUSE_IF_LATEST = 136; static final int REQUEST_FOCUS_HREF = 137; static final int ADD_JS_INTERFACE = 138; static final int LOAD_DATA = 139; @@ -604,6 +644,15 @@ final class WebViewCore { // message used to pass UI touch events to WebCore static final int TOUCH_EVENT = 141; + // Used to tell the focus controller whether to draw the blinking cursor + // or not, based on whether the WebView has focus. + static final int SET_ACTIVE = 142; + + // pause/resume activity for just this DOM (unlike pauseTimers, which + // is global) + static final int ON_PAUSE = 143; + static final int ON_RESUME = 144; + // Network-based messaging static final int CLEAR_SSL_PREF_TABLE = 150; @@ -618,10 +667,6 @@ final class WebViewCore { // private message ids private static final int DESTROY = 200; - - // flag values passed to message SET_FINAL_FOCUS - static final int NO_FOCUS_CHANGE_BLOCK = 0; - static final int BLOCK_FOCUS_CHANGE_UNTIL_KEY_UP = 1; // Private handler for WebCore messages. private Handler mHandler; @@ -650,9 +695,9 @@ final class WebViewCore { mHandler = new Handler() { @Override public void handleMessage(Message msg) { - if (LOGV_ENABLED) { - Log.v(LOGTAG, msg.what < LOAD_URL || msg.what - > TOUCH_EVENT ? Integer.toString(msg.what) + if (DebugFlags.WEB_VIEW_CORE) { + Log.v(LOGTAG, msg.what < LOAD_URL || msg.what + > SET_ACTIVE ? Integer.toString(msg.what) : HandlerDebugString[msg.what - LOAD_URL]); } switch (msg.what) { @@ -672,6 +717,13 @@ final class WebViewCore { loadUrl((String) msg.obj); break; + case POST_URL: { + HashMap param = (HashMap) msg.obj; + String url = (String) param.get("url"); + byte[] data = (byte[]) param.get("data"); + mBrowserFrame.postUrl(url, data); + break; + } case LOAD_DATA: HashMap loadParams = (HashMap) msg.obj; String baseUrl = (String) loadParams.get("baseUrl"); @@ -687,7 +739,7 @@ final class WebViewCore { * we automatically add the scheme of the * baseUrl for local access as long as it is * not http(s)/ftp(s)/about/javascript - */ + */ String scheme = baseUrl.substring(0, i); if (!scheme.startsWith("http") && !scheme.startsWith("ftp") && @@ -705,9 +757,9 @@ final class WebViewCore { break; case STOP_LOADING: - // If the WebCore has committed the load, but not - // finished the first layout yet, we need to set - // first layout done to trigger the interpreted side sync + // If the WebCore has committed the load, but not + // finished the first layout yet, we need to set + // first layout done to trigger the interpreted side sync // up with native side if (mBrowserFrame.committed() && !mBrowserFrame.firstLayoutDone()) { @@ -743,7 +795,7 @@ final class WebViewCore { // (inv-zoom) nativeSetScrollOffset(msg.arg1, msg.arg2); break; - + case SET_GLOBAL_BOUNDS: Rect r = (Rect) msg.obj; nativeSetGlobalBounds(r.left, r.top, r.width(), @@ -754,7 +806,7 @@ final class WebViewCore { // If it is a standard load and the load is not // committed yet, we interpret BACK as RELOAD if (!mBrowserFrame.committed() && msg.arg1 == -1 && - (mBrowserFrame.loadType() == + (mBrowserFrame.loadType() == BrowserFrame.FRAME_LOADTYPE_STANDARD)) { mBrowserFrame.reload(true); } else { @@ -791,6 +843,14 @@ final class WebViewCore { } break; + case ON_PAUSE: + nativePause(); + break; + + case ON_RESUME: + nativeResume(); + break; + case SET_NETWORK_STATE: if (BrowserFrame.sJavaBridge == null) { throw new IllegalStateException("No WebView " + @@ -812,28 +872,24 @@ final class WebViewCore { close(mBrowserFrame.mNativeFrame); break; - case REPLACE_TEXT: + case REPLACE_TEXT: HashMap jMap = (HashMap) msg.obj; - FocusData fData = (FocusData) jMap.get("focusData"); String replace = (String) jMap.get("replace"); - int newStart = + int newStart = ((Integer) jMap.get("start")).intValue(); - int newEnd = + int newEnd = ((Integer) jMap.get("end")).intValue(); - nativeReplaceTextfieldText(fData.mFrame, - fData.mNode, fData.mX, fData.mY, msg.arg1, + nativeReplaceTextfieldText(msg.arg1, msg.arg2, replace, newStart, newEnd); break; case PASS_TO_JS: { HashMap jsMap = (HashMap) msg.obj; - FocusData fDat = (FocusData) jsMap.get("focusData"); KeyEvent evt = (KeyEvent) jsMap.get("event"); int keyCode = evt.getKeyCode(); int keyValue = evt.getUnicodeChar(); int generation = msg.arg1; - passToJs(fDat.mFrame, fDat.mNode, fDat.mX, fDat.mY, - generation, + passToJs(generation, (String) jsMap.get("currentText"), keyCode, keyValue, @@ -844,7 +900,7 @@ final class WebViewCore { } case SAVE_DOCUMENT_STATE: { - FocusData fDat = (FocusData) msg.obj; + CursorData fDat = (CursorData) msg.obj; nativeSaveDocumentState(fDat.mFrame); break; } @@ -857,11 +913,9 @@ final class WebViewCore { case TOUCH_UP: TouchUpData touchUpData = (TouchUpData) msg.obj; nativeTouchUp(touchUpData.mMoveGeneration, - touchUpData.mBuildGeneration, touchUpData.mFrame, touchUpData.mNode, - touchUpData.mX, touchUpData.mY, - touchUpData.mSize, touchUpData.mIsClick, - touchUpData.mRetry); + touchUpData.mX, touchUpData.mY, + touchUpData.mSize); break; case TOUCH_EVENT: { @@ -874,6 +928,10 @@ final class WebViewCore { break; } + case SET_ACTIVE: + nativeSetFocusControllerActive(msg.arg1 == 1); + break; + case ADD_JS_INTERFACE: HashMap map = (HashMap) msg.obj; Object obj = map.get("object"); @@ -892,22 +950,20 @@ final class WebViewCore { mBrowserFrame.documentAsText((Message) msg.obj); break; - case SET_FINAL_FOCUS: - FocusData finalData = (FocusData) msg.obj; - nativeSetFinalFocus(finalData.mFrame, - finalData.mNode, finalData.mX, - finalData.mY, msg.arg1 - != EventHub.NO_FOCUS_CHANGE_BLOCK); + case SET_MOVE_MOUSE: + CursorData finalData = (CursorData) msg.obj; + nativeMoveMouse(finalData.mFrame, + finalData.mNode, finalData.mX, + finalData.mY); break; case UNBLOCK_FOCUS: nativeUnblockFocus(); break; - case SET_KIT_FOCUS: - FocusData focusData = (FocusData) msg.obj; - nativeSetKitFocus(focusData.mMoveGeneration, - focusData.mBuildGeneration, + case SET_MOVE_MOUSE_IF_LATEST: + CursorData focusData = (CursorData) msg.obj; + nativeMoveMouseIfLatest(focusData.mMoveGeneration, focusData.mFrame, focusData.mNode, focusData.mX, focusData.mY, focusData.mIgnoreNullFocus); @@ -920,7 +976,7 @@ final class WebViewCore { hrefMsg.sendToTarget(); break; } - + case UPDATE_CACHE_AND_TEXT_ENTRY: nativeUpdateFrameCache(); // FIXME: this should provide a minimal rectangle @@ -940,21 +996,15 @@ final class WebViewCore { case SET_SNAP_ANCHOR: nativeSetSnapAnchor(msg.arg1, msg.arg2); break; - + case DELETE_SELECTION: - FocusData delData = (FocusData) msg.obj; - nativeDeleteSelection(delData.mFrame, - delData.mNode, delData.mX, - delData.mY, msg.arg1, msg.arg2); + nativeDeleteSelection(msg.arg1, msg.arg2); break; case SET_SELECTION: - FocusData selData = (FocusData) msg.obj; - nativeSetSelection(selData.mFrame, - selData.mNode, selData.mX, - selData.mY, msg.arg1, msg.arg2); + nativeSetSelection(msg.arg1, msg.arg2); break; - + case LISTBOX_CHOICES: SparseBooleanArray choices = (SparseBooleanArray) msg.obj; @@ -963,18 +1013,18 @@ final class WebViewCore { for (int c = 0; c < choicesSize; c++) { choicesArray[c] = choices.get(c); } - nativeSendListBoxChoices(choicesArray, + nativeSendListBoxChoices(choicesArray, choicesSize); break; case SINGLE_LISTBOX_CHOICE: nativeSendListBoxChoice(msg.arg1); break; - + case SET_BACKGROUND_COLOR: nativeSetBackgroundColor(msg.arg1); break; - + case GET_SELECTION: String str = nativeGetSelection((Region) msg.obj); Message.obtain(mWebView.mPrivateHandler @@ -999,21 +1049,23 @@ final class WebViewCore { mWebkitScrollY = msg.arg2; break; - case REFRESH_PLUGINS: - nativeRefreshPlugins(msg.arg1 != 0); - break; - case SPLIT_PICTURE_SET: nativeSplitContent(); mSplitPictureIsScheduled = false; break; - + case CLEAR_CONTENT: // Clear the view so that onDraw() will draw nothing // but white background // (See public method WebView.clearView) nativeClearContent(); break; + + case MESSAGE_RELAY: + if (msg.obj instanceof Message) { + ((Message) msg.obj).sendToTarget(); + } + break; } } }; @@ -1103,7 +1155,7 @@ final class WebViewCore { //------------------------------------------------------------------------- void stopLoading() { - if (LOGV_ENABLED) Log.v(LOGTAG, "CORE stopLoading"); + if (DebugFlags.WEB_VIEW_CORE) Log.v(LOGTAG, "CORE stopLoading"); if (mBrowserFrame != null) { mBrowserFrame.stopLoading(); } @@ -1179,19 +1231,22 @@ final class WebViewCore { //------------------------------------------------------------------------- private void loadUrl(String url) { - if (LOGV_ENABLED) Log.v(LOGTAG, " CORE loadUrl " + url); + if (DebugFlags.WEB_VIEW_CORE) Log.v(LOGTAG, " CORE loadUrl " + url); mBrowserFrame.loadUrl(url); } private void key(KeyEvent evt, boolean isDown) { - if (LOGV_ENABLED) { + if (DebugFlags.WEB_VIEW_CORE) { Log.v(LOGTAG, "CORE key at " + System.currentTimeMillis() + ", " + evt); } - if (!nativeKey(evt.getKeyCode(), evt.getUnicodeChar(), + int keyCode = evt.getKeyCode(); + if (!nativeKey(keyCode, evt.getUnicodeChar(), evt.getRepeatCount(), evt.isShiftPressed(), evt.isAltPressed(), - isDown)) { + isDown) && keyCode != KeyEvent.KEYCODE_ENTER) { // bubble up the event handling + // but do not bubble up the ENTER key, which would open the search + // bar without any text. mCallbackProxy.onUnhandledKeyEvent(evt); } } @@ -1202,7 +1257,7 @@ final class WebViewCore { // notify webkit that our virtual view size changed size (after inv-zoom) private void viewSizeChanged(int w, int h, float scale) { - if (LOGV_ENABLED) Log.v(LOGTAG, "CORE onSizeChanged"); + if (DebugFlags.WEB_VIEW_CORE) Log.v(LOGTAG, "CORE onSizeChanged"); if (w == 0) { Log.w(LOGTAG, "skip viewSizeChanged as w is 0"); return; @@ -1211,7 +1266,7 @@ final class WebViewCore { && (w < mViewportWidth || mViewportWidth == -1)) { int width = mViewportWidth; if (mViewportWidth == -1) { - if (mSettings.getLayoutAlgorithm() == + if (mSettings.getLayoutAlgorithm() == WebSettings.LayoutAlgorithm.NORMAL) { width = WebView.ZOOM_OUT_WIDTH; } else { @@ -1242,7 +1297,7 @@ final class WebViewCore { if (needInvalidate) { // ensure {@link #webkitDraw} is called as we were blocking in // {@link #contentDraw} when mCurrentViewWidth is 0 - if (LOGV_ENABLED) Log.v(LOGTAG, "viewSizeChanged"); + if (DebugFlags.WEB_VIEW_CORE) Log.v(LOGTAG, "viewSizeChanged"); contentDraw(); } mEventHub.sendMessage(Message.obtain(null, @@ -1258,7 +1313,7 @@ final class WebViewCore { // Used to avoid posting more than one draw message. private boolean mDrawIsScheduled; - + // Used to avoid posting more than one split picture message. private boolean mSplitPictureIsScheduled; @@ -1267,7 +1322,7 @@ final class WebViewCore { // Used to end scale+scroll mode, accessed by both threads boolean mEndScaleZoom = false; - + public class DrawData { public DrawData() { mInvalRegion = new Region(); @@ -1277,21 +1332,21 @@ final class WebViewCore { public Point mViewPoint; public Point mWidthHeight; } - + private void webkitDraw() { mDrawIsScheduled = false; DrawData draw = new DrawData(); - if (LOGV_ENABLED) Log.v(LOGTAG, "webkitDraw start"); - if (nativeRecordContent(draw.mInvalRegion, draw.mWidthHeight) + if (DebugFlags.WEB_VIEW_CORE) Log.v(LOGTAG, "webkitDraw start"); + if (nativeRecordContent(draw.mInvalRegion, draw.mWidthHeight) == false) { - if (LOGV_ENABLED) Log.v(LOGTAG, "webkitDraw abort"); + if (DebugFlags.WEB_VIEW_CORE) Log.v(LOGTAG, "webkitDraw abort"); return; } if (mWebView != null) { // Send the native view size that was used during the most recent // layout. draw.mViewPoint = new Point(mCurrentViewWidth, mCurrentViewHeight); - if (LOGV_ENABLED) Log.v(LOGTAG, "webkitDraw NEW_PICTURE_MSG_ID"); + if (DebugFlags.WEB_VIEW_CORE) Log.v(LOGTAG, "webkitDraw NEW_PICTURE_MSG_ID"); Message.obtain(mWebView.mPrivateHandler, WebView.NEW_PICTURE_MSG_ID, draw).sendToTarget(); if (mWebkitScrollX != 0 || mWebkitScrollY != 0) { @@ -1339,6 +1394,10 @@ final class WebViewCore { } } + /* package */ boolean pictureReady() { + return nativePictureReady(); + } + /*package*/ Picture copyContentPicture() { Picture result = new Picture(); nativeCopyContentToPicture(result); @@ -1352,9 +1411,9 @@ final class WebViewCore { sWebCoreHandler.sendMessageAtFrontOfQueue(sWebCoreHandler .obtainMessage(WebCoreThread.REDUCE_PRIORITY)); // Note: there is one possible failure mode. If pauseUpdate() is called - // from UI thread while in webcore thread WEBKIT_DRAW is just pulled out - // of the queue and about to be executed. mDrawIsScheduled may be set to - // false in webkitDraw(). So update won't be blocked. But at least the + // from UI thread while in webcore thread WEBKIT_DRAW is just pulled out + // of the queue and about to be executed. mDrawIsScheduled may be set to + // false in webkitDraw(). So update won't be blocked. But at least the // webcore thread priority is still lowered. if (core != null) { synchronized (core) { @@ -1374,7 +1433,7 @@ final class WebViewCore { synchronized (core) { core.mDrawIsScheduled = false; core.mDrawIsPaused = false; - if (LOGV_ENABLED) Log.v(LOGTAG, "resumeUpdate"); + if (DebugFlags.WEB_VIEW_CORE) Log.v(LOGTAG, "resumeUpdate"); core.contentDraw(); } } @@ -1423,7 +1482,7 @@ final class WebViewCore { mEventHub.sendMessage(Message.obtain(null, EventHub.WEBKIT_DRAW)); } } - + // called by JNI private void contentScrollBy(int dx, int dy, boolean animate) { if (!mBrowserFrame.firstLayoutDone()) { @@ -1431,9 +1490,14 @@ final class WebViewCore { return; } if (mWebView != null) { - Message.obtain(mWebView.mPrivateHandler, - WebView.SCROLL_BY_MSG_ID, dx, dy, - new Boolean(animate)).sendToTarget(); + Message msg = Message.obtain(mWebView.mPrivateHandler, + WebView.SCROLL_BY_MSG_ID, dx, dy, new Boolean(animate)); + if (mDrawIsScheduled) { + mEventHub.sendMessage(Message.obtain(null, + EventHub.MESSAGE_RELAY, msg)); + } else { + msg.sendToTarget(); + } } } @@ -1450,8 +1514,14 @@ final class WebViewCore { return; } if (mWebView != null) { - Message.obtain(mWebView.mPrivateHandler, - WebView.SCROLL_TO_MSG_ID, x, y).sendToTarget(); + Message msg = Message.obtain(mWebView.mPrivateHandler, + WebView.SCROLL_TO_MSG_ID, x, y); + if (mDrawIsScheduled) { + mEventHub.sendMessage(Message.obtain(null, + EventHub.MESSAGE_RELAY, msg)); + } else { + msg.sendToTarget(); + } } } @@ -1468,8 +1538,14 @@ final class WebViewCore { return; } if (mWebView != null) { - Message.obtain(mWebView.mPrivateHandler, - WebView.SPAWN_SCROLL_TO_MSG_ID, x, y).sendToTarget(); + Message msg = Message.obtain(mWebView.mPrivateHandler, + WebView.SPAWN_SCROLL_TO_MSG_ID, x, y); + if (mDrawIsScheduled) { + mEventHub.sendMessage(Message.obtain(null, + EventHub.MESSAGE_RELAY, msg)); + } else { + msg.sendToTarget(); + } } } @@ -1482,14 +1558,6 @@ final class WebViewCore { } // called by JNI - private void sendNotifyFocusSet() { - if (mWebView != null) { - Message.obtain(mWebView.mPrivateHandler, - WebView.NOTIFY_FOCUS_SET_MSG_ID).sendToTarget(); - } - } - - // called by JNI private void sendNotifyProgressFinished() { sendUpdateTextEntry(); // as CacheManager can behave based on database transaction, we need to @@ -1525,12 +1593,12 @@ final class WebViewCore { } private native void setViewportSettingsFromNative(); - + // called by JNI - private void didFirstLayout() { + private void didFirstLayout(boolean standardLoad) { // Trick to ensure that the Picture has the exact height for the content // by forcing to layout with 0 height after the page is ready, which is - // indicated by didFirstLayout. This is essential to get rid of the + // indicated by didFirstLayout. This is essential to get rid of the // white space in the GMail which uses WebView for message view. if (mWebView != null && mWebView.mHeightCanMeasure) { mWebView.mLastHeightSent = 0; @@ -1575,7 +1643,7 @@ final class WebViewCore { mViewportMaximumScale = mViewportInitialScale; } else if (mViewportInitialScale == 0) { mViewportInitialScale = mViewportMaximumScale; - } + } } if (mViewportWidth < 0 && mViewportInitialScale == 100) { mViewportWidth = 0; @@ -1593,16 +1661,14 @@ final class WebViewCore { scaleLimit).sendToTarget(); mRestoredScale = 0; } else { + // if standardLoad is true, use mViewportInitialScale, otherwise + // pass -1 to the WebView to indicate no change of the scale. Message.obtain(mWebView.mPrivateHandler, - WebView.DID_FIRST_LAYOUT_MSG_ID, mViewportInitialScale, + WebView.DID_FIRST_LAYOUT_MSG_ID, + standardLoad ? mViewportInitialScale : -1, mViewportWidth, scaleLimit).sendToTarget(); } - // if no restored offset, move the new page to (0, 0) - Message.obtain(mWebView.mPrivateHandler, WebView.SCROLL_TO_MSG_ID, - mRestoredX, mRestoredY).sendToTarget(); - mRestoredX = mRestoredY = 0; - // force an early draw for quick feedback after the first layout if (mCurrentViewWidth != 0) { synchronized (this) { @@ -1610,10 +1676,19 @@ final class WebViewCore { mEventHub.removeMessages(EventHub.WEBKIT_DRAW); } mDrawIsScheduled = true; + // if no restored offset, move the new page to (0, 0) + mEventHub.sendMessageAtFrontOfQueue(Message.obtain(null, + EventHub.MESSAGE_RELAY, Message.obtain( + mWebView.mPrivateHandler, + WebView.SCROLL_TO_MSG_ID, mRestoredX, + mRestoredY))); mEventHub.sendMessageAtFrontOfQueue(Message.obtain(null, EventHub.WEBKIT_DRAW)); } } + + // reset restored offset + mRestoredX = mRestoredY = 0; } } @@ -1638,7 +1713,7 @@ final class WebViewCore { String text, int textGeneration) { if (mWebView != null) { Message msg = Message.obtain(mWebView.mPrivateHandler, - WebView.UPDATE_TEXTFIELD_TEXT_MSG_ID, ptr, + WebView.UPDATE_TEXTFIELD_TEXT_MSG_ID, ptr, textGeneration, text); msg.getData().putBoolean("password", changeToPassword); msg.sendToTarget(); @@ -1664,6 +1739,9 @@ final class WebViewCore { if (mWebView != null) { mWebView.requestListBox(array, enabledArray, selection); } - + } + + private native void nativePause(); + private native void nativeResume(); } diff --git a/core/java/android/widget/AbsSeekBar.java b/core/java/android/widget/AbsSeekBar.java index 04cb8a0..f92eb99 100644 --- a/core/java/android/widget/AbsSeekBar.java +++ b/core/java/android/widget/AbsSeekBar.java @@ -320,11 +320,6 @@ public abstract class AbsSeekBar extends ProgressBar { final int max = getMax(); progress += scale * max; - if (progress < 0) { - progress = 0; - } else if (progress > max) { - progress = max; - } setProgress((int) progress, true); } diff --git a/core/java/android/widget/AdapterView.java b/core/java/android/widget/AdapterView.java index 7d2fcbc..fe6d91a 100644 --- a/core/java/android/widget/AdapterView.java +++ b/core/java/android/widget/AdapterView.java @@ -163,7 +163,7 @@ public abstract class AdapterView<T extends Adapter> extends ViewGroup { /** * View to show if there are no items to show. */ - View mEmptyView; + private View mEmptyView; /** * The number of items in the current adapter. diff --git a/core/java/android/widget/ArrayAdapter.java b/core/java/android/widget/ArrayAdapter.java index 32e5504..c28210d 100644 --- a/core/java/android/widget/ArrayAdapter.java +++ b/core/java/android/widget/ArrayAdapter.java @@ -348,12 +348,7 @@ public class ArrayAdapter<T> extends BaseAdapter implements Filterable { "ArrayAdapter requires the resource ID to be a TextView", e); } - T item = getItem(position); - if (item instanceof CharSequence) { - text.setText((CharSequence)item); - } else { - text.setText(item.toString()); - } + text.setText(getItem(position).toString()); return view; } diff --git a/core/java/android/widget/AutoCompleteTextView.java b/core/java/android/widget/AutoCompleteTextView.java index a1d16ea..bfc5f08 100644 --- a/core/java/android/widget/AutoCompleteTextView.java +++ b/core/java/android/widget/AutoCompleteTextView.java @@ -126,7 +126,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe // Indicates whether this AutoCompleteTextView is attached to a window or not // The widget is attached to a window when mAttachCount > 0 private int mAttachCount; - + private AutoCompleteTextView.PassThroughClickListener mPassThroughClickListener; public AutoCompleteTextView(Context context) { @@ -188,7 +188,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe setFocusable(true); addTextChangedListener(new MyWatcher()); - + mPassThroughClickListener = new PassThroughClickListener(); super.setOnClickListener(mPassThroughClickListener); } @@ -290,8 +290,6 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe * @return the background drawable * * @attr ref android.R.styleable#PopupWindow_popupBackground - * - * @hide Pending API council approval */ public Drawable getDropDownBackground() { return mPopup.getBackground(); @@ -303,8 +301,6 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe * @param d the drawable to set as the background * * @attr ref android.R.styleable#PopupWindow_popupBackground - * - * @hide Pending API council approval */ public void setDropDownBackgroundDrawable(Drawable d) { mPopup.setBackgroundDrawable(d); @@ -316,47 +312,15 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe * @param id the id of the drawable to set as the background * * @attr ref android.R.styleable#PopupWindow_popupBackground - * - * @hide Pending API council approval */ public void setDropDownBackgroundResource(int id) { mPopup.setBackgroundDrawable(getResources().getDrawable(id)); } - - /** - * <p>Sets the animation style of the auto-complete drop-down list.</p> - * - * <p>If the drop-down is showing, calling this method will take effect only - * the next time the drop-down is shown.</p> - * - * @param animationStyle animation style to use when the drop-down appears - * and disappears. Set to -1 for the default animation, 0 for no - * animation, or a resource identifier for an explicit animation. - * - * @hide Pending API council approval - */ - public void setDropDownAnimationStyle(int animationStyle) { - mPopup.setAnimationStyle(animationStyle); - } - - /** - * <p>Returns the animation style that is used when the drop-down list appears and disappears - * </p> - * - * @return the animation style that is used when the drop-down list appears and disappears - * - * @hide Pending API council approval - */ - public int getDropDownAnimationStyle() { - return mPopup.getAnimationStyle(); - } /** * <p>Sets the vertical offset used for the auto-complete drop-down list.</p> * * @param offset the vertical offset - * - * @hide Pending API council approval */ public void setDropDownVerticalOffset(int offset) { mDropDownVerticalOffset = offset; @@ -366,8 +330,6 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe * <p>Gets the vertical offset used for the auto-complete drop-down list.</p> * * @return the vertical offset - * - * @hide Pending API council approval */ public int getDropDownVerticalOffset() { return mDropDownVerticalOffset; @@ -377,8 +339,6 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe * <p>Sets the horizontal offset used for the auto-complete drop-down list.</p> * * @param offset the horizontal offset - * - * @hide Pending API council approval */ public void setDropDownHorizontalOffset(int offset) { mDropDownHorizontalOffset = offset; @@ -388,13 +348,39 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe * <p>Gets the horizontal offset used for the auto-complete drop-down list.</p> * * @return the horizontal offset - * - * @hide Pending API council approval */ public int getDropDownHorizontalOffset() { return mDropDownHorizontalOffset; } + /** + * <p>Sets the animation style of the auto-complete drop-down list.</p> + * + * <p>If the drop-down is showing, calling this method will take effect only + * the next time the drop-down is shown.</p> + * + * @param animationStyle animation style to use when the drop-down appears + * and disappears. Set to -1 for the default animation, 0 for no + * animation, or a resource identifier for an explicit animation. + * + * @hide Pending API council approval + */ + public void setDropDownAnimationStyle(int animationStyle) { + mPopup.setAnimationStyle(animationStyle); + } + + /** + * <p>Returns the animation style that is used when the drop-down list appears and disappears + * </p> + * + * @return the animation style that is used when the drop-down list appears and disappears + * + * @hide Pending API council approval + */ + public int getDropDownAnimationStyle() { + return mPopup.getAnimationStyle(); + } + /** * @return Whether the drop-down is visible as long as there is {@link #enoughToFilter()} * @@ -1074,10 +1060,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe } mPopup.setHeight(height); mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED); - - // use outside touchable to dismiss drop down when touching outside of it, so - // only set this if the dropdown is not always visible - mPopup.setOutsideTouchable(!mDropDownAlwaysVisible); + mPopup.setOutsideTouchable(true); mPopup.setTouchInterceptor(new PopupTouchIntercepter()); mPopup.showAsDropDown(getDropDownAnchorView(), mDropDownHorizontalOffset, mDropDownVerticalOffset); @@ -1399,7 +1382,7 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe */ CharSequence fixText(CharSequence invalidText); } - + /** * Allows us a private hook into the on click event without preventing users from setting * their own click listener. @@ -1415,5 +1398,5 @@ public class AutoCompleteTextView extends EditText implements Filter.FilterListe if (mWrapped != null) mWrapped.onClick(v); } } - + } |