diff options
Diffstat (limited to 'core/java')
181 files changed, 8836 insertions, 4547 deletions
diff --git a/core/java/android/accessibilityservice/AccessibilityServiceInfo.java b/core/java/android/accessibilityservice/AccessibilityServiceInfo.java index 4761f98..bf9e07d 100644 --- a/core/java/android/accessibilityservice/AccessibilityServiceInfo.java +++ b/core/java/android/accessibilityservice/AccessibilityServiceInfo.java @@ -66,10 +66,10 @@ public class AccessibilityServiceInfo implements Parcelable { * The event types an {@link AccessibilityService} is interested in. * * @see android.view.accessibility.AccessibilityEvent#TYPE_VIEW_CLICKED + * @see android.view.accessibility.AccessibilityEvent#TYPE_VIEW_LONG_CLICKED * @see android.view.accessibility.AccessibilityEvent#TYPE_VIEW_FOCUSED * @see android.view.accessibility.AccessibilityEvent#TYPE_VIEW_SELECTED * @see android.view.accessibility.AccessibilityEvent#TYPE_VIEW_TEXT_CHANGED - * @see android.view.accessibility.AccessibilityEvent#TYPE_ACTIVITY_STARTED * @see android.view.accessibility.AccessibilityEvent#TYPE_WINDOW_STATE_CHANGED * @see android.view.accessibility.AccessibilityEvent#TYPE_NOTIFICATION_STATE_CHANGED */ @@ -115,7 +115,7 @@ public class AccessibilityServiceInfo implements Parcelable { return 0; } - public void writeToParcel(Parcel parcel, int flags) { + public void writeToParcel(Parcel parcel, int flagz) { parcel.writeInt(eventTypes); parcel.writeStringArray(packageNames); parcel.writeInt(feedbackType); diff --git a/core/java/android/accounts/AbstractAccountAuthenticator.java b/core/java/android/accounts/AbstractAccountAuthenticator.java index be2bdbe..8bc7428 100644 --- a/core/java/android/accounts/AbstractAccountAuthenticator.java +++ b/core/java/android/accounts/AbstractAccountAuthenticator.java @@ -296,8 +296,7 @@ public abstract class AbstractAccountAuthenticator { * <ul> * <li> {@link AccountManager#KEY_INTENT}, or * <li> {@link AccountManager#KEY_ACCOUNT_NAME} and {@link AccountManager#KEY_ACCOUNT_TYPE} of - * the account that was added, plus {@link AccountManager#KEY_AUTHTOKEN} if an authTokenType - * was supplied, or + * the account that was added, or * <li> {@link AccountManager#KEY_ERROR_CODE} and {@link AccountManager#KEY_ERROR_MESSAGE} to * indicate an error * </ul> @@ -368,8 +367,7 @@ public abstract class AbstractAccountAuthenticator { * <ul> * <li> {@link AccountManager#KEY_INTENT}, or * <li> {@link AccountManager#KEY_ACCOUNT_NAME} and {@link AccountManager#KEY_ACCOUNT_TYPE} of - * the account that was added, plus {@link AccountManager#KEY_AUTHTOKEN} if an authTokenType - * was supplied, or + * the account that was added, or * <li> {@link AccountManager#KEY_ERROR_CODE} and {@link AccountManager#KEY_ERROR_MESSAGE} to * indicate an error * </ul> @@ -378,7 +376,7 @@ public abstract class AbstractAccountAuthenticator { */ public abstract Bundle updateCredentials(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException; - + /** * Checks if the account supports all the specified authenticator specific features. * @param response to send the result back to the AccountManager, will never be null diff --git a/core/java/android/accounts/AccountManager.java b/core/java/android/accounts/AccountManager.java index 414d963..43a0f30 100644 --- a/core/java/android/accounts/AccountManager.java +++ b/core/java/android/accounts/AccountManager.java @@ -27,6 +27,7 @@ import android.os.Handler; import android.os.Looper; import android.os.RemoteException; import android.os.Parcelable; +import android.os.Build; import android.util.Log; import java.io.IOException; @@ -66,6 +67,8 @@ import com.google.android.collect.Maps; * cause the running thread to block until the result is returned. Keep in mind that one * should not block the main thread in this way. Instead one should either use a callback, * thus making the call asynchronous, or make the blocking call on a separate thread. + * getResult() will throw an {@link IllegalStateException} if you call it from the main thread + * before the request has completed, i.e. before the callback has been invoked. * <p> * If one wants to ensure that the callback is invoked from a specific handler then they should * pass the handler to the request. This makes it easier to ensure thread-safety by running @@ -148,6 +151,8 @@ public class AccountManager { * Get the password that is associated with the account. Returns null if the account does * not exist. * <p> + * It is safe to call this method from the main thread. + * <p> * Requires that the caller has permission * {@link android.Manifest.permission#AUTHENTICATE_ACCOUNTS} and is running * with the same UID as the Authenticator for the account. @@ -165,6 +170,8 @@ public class AccountManager { * Get the user data named by "key" that is associated with the account. * Returns null if the account does not exist or if it does not have a value for key. * <p> + * It is safe to call this method from the main thread. + * <p> * Requires that the caller has permission * {@link android.Manifest.permission#AUTHENTICATE_ACCOUNTS} and is running * with the same UID as the Authenticator for the account. @@ -184,6 +191,8 @@ public class AccountManager { * @return an array that contains all the authenticators known to the AccountManager service. * This array will be empty if there are no authenticators and will never return null. * <p> + * It is safe to call this method from the main thread. + * <p> * No permission is required to make this call. */ public AuthenticatorDescription[] getAuthenticatorTypes() { @@ -200,6 +209,8 @@ public class AccountManager { * @return an array that contains all the accounts known to the AccountManager service. * This array will be empty if there are no accounts and will never return null. * <p> + * It is safe to call this method from the main thread. + * <p> * Requires that the caller has permission {@link android.Manifest.permission#GET_ACCOUNTS} */ public Account[] getAccounts() { @@ -218,6 +229,8 @@ public class AccountManager { * @return an array that contains the accounts that match the specified type. This array * will be empty if no accounts match. It will never return null. * <p> + * It is safe to call this method from the main thread. + * <p> * Requires that the caller has permission {@link android.Manifest.permission#GET_ACCOUNTS} */ public Account[] getAccountsByType(String type) { @@ -242,6 +255,22 @@ public class AccountManager { * {@link android.accounts.AccountManagerFuture#getResult()} on this method's return value, * which will then block until the request completes. * <p> + * Do not block the main thread waiting this method's result. + * <p> + * Not allowed from main thread (but allowed from other threads): + * <pre> + * Boolean result = hasFeatures(account, features, callback, handler).getResult(); + * </pre> + * Allowed from main thread: + * <pre> + * hasFeatures(account, features, new AccountManagerCallback<Boolean>() { + * public void run(AccountManagerFuture<Boolean> future) { + * Boolean result = future.getResult(); + * // use result + * } + * }, handler); + * </pre> + * <p> * Requires that the caller has permission {@link android.Manifest.permission#GET_ACCOUNTS}. * * @param account The {@link Account} to test @@ -271,7 +300,9 @@ public class AccountManager { } /** - * Add an account to the AccountManager's set of known accounts. + * Add an account to the AccountManager's set of known accounts. + * <p> + * It is safe to call this method from the main thread. * <p> * Requires that the caller has permission * {@link android.Manifest.permission#AUTHENTICATE_ACCOUNTS} and is running @@ -303,6 +334,22 @@ public class AccountManager { * {@link android.accounts.AccountManagerFuture#getResult()} on this method's return value, * which will then block until the request completes. * <p> + * Do not block the main thread waiting this method's result. + * <p> + * Not allowed from main thread (but allowed from other threads): + * <pre> + * Boolean result = removeAccount(account, callback, handler).getResult(); + * </pre> + * Allowed from main thread: + * <pre> + * removeAccount(account, new AccountManagerCallback<Boolean>() { + * public void run(AccountManagerFuture<Boolean> future) { + * Boolean result = future.getResult(); + * // use result + * } + * }, handler); + * </pre> + * <p> * Requires that the caller has permission {@link android.Manifest.permission#MANAGE_ACCOUNTS}. * * @param account The {@link Account} to remove @@ -333,6 +380,8 @@ public class AccountManager { * Removes the given authtoken. If this authtoken does not exist for the given account type * then this call has no effect. * <p> + * It is safe to call this method from the main thread. + * <p> * Requires that the caller has permission {@link android.Manifest.permission#MANAGE_ACCOUNTS}. * @param accountType the account type of the authtoken to invalidate * @param authToken the authtoken to invalidate @@ -352,6 +401,8 @@ public class AccountManager { * asking the authenticaticor to generate one. If the account or the * authtoken do not exist then null is returned. * <p> + * It is safe to call this method from the main thread. + * <p> * Requires that the caller has permission * {@link android.Manifest.permission#AUTHENTICATE_ACCOUNTS} and is running * with the same UID as the Authenticator for the account. @@ -380,6 +431,8 @@ public class AccountManager { * Sets the password for the account. The password may be null. If the account does not exist * then this call has no affect. * <p> + * It is safe to call this method from the main thread. + * <p> * Requires that the caller has permission * {@link android.Manifest.permission#AUTHENTICATE_ACCOUNTS} and is running * with the same UID as the Authenticator for the account. @@ -403,6 +456,8 @@ public class AccountManager { * Sets the password for account to null. If the account does not exist then this call * has no effect. * <p> + * It is safe to call this method from the main thread. + * <p> * Requires that the caller has permission {@link android.Manifest.permission#MANAGE_ACCOUNTS}. * @param account the account whose password is to be cleared. Must not be null. */ @@ -423,6 +478,8 @@ public class AccountManager { * Sets account's userdata named "key" to the specified value. If the account does not * exist then this call has no effect. * <p> + * It is safe to call this method from the main thread. + * <p> * Requires that the caller has permission * {@link android.Manifest.permission#AUTHENTICATE_ACCOUNTS} and is running * with the same UID as the Authenticator for the account. @@ -451,6 +508,8 @@ public class AccountManager { * Sets the authtoken named by "authTokenType" to the value specified by authToken. * If the account does not exist then this call has no effect. * <p> + * It is safe to call this method from the main thread. + * <p> * Requires that the caller has permission * {@link android.Manifest.permission#AUTHENTICATE_ACCOUNTS} and is running * with the same UID as the Authenticator for the account. @@ -472,6 +531,8 @@ public class AccountManager { * {@link #getAuthToken(Account, String, boolean, AccountManagerCallback, Handler)} * then extracts and returns the value of {@link #KEY_AUTHTOKEN} from its result. * <p> + * It is not safe to call this method from the main thread. See {@link #getAuthToken}. + * <p> * Requires that the caller has permission {@link android.Manifest.permission#USE_CREDENTIALS}. * @param account the account whose authtoken is to be retrieved, must not be null * @param authTokenType the type of authtoken to retrieve @@ -504,9 +565,8 @@ public class AccountManager { * in the result. * <p> * If the authenticator needs to prompt the user for credentials it will return an intent to - * the activity that will do the prompting. If an activity is supplied then that activity - * will be used to launch the intent and the result will come from it. Otherwise a result will - * be returned that contains the intent. + * an activity that will do the prompting. The supplied activity will be used to launch the + * intent and the result will come from the launched activity. * <p> * This call returns immediately but runs asynchronously and the result is accessed via the * {@link AccountManagerFuture} that is returned. This future is also passed as the sole @@ -517,6 +577,23 @@ public class AccountManager { * {@link android.accounts.AccountManagerFuture#getResult()} on this method's return value, * which will then block until the request completes. * <p> + * Do not block the main thread waiting this method's result. + * <p> + * Not allowed from main thread (but allowed from other threads): + * <pre> + * Bundle result = getAuthToken( + * account, authTokenType, options, activity, callback, handler).getResult(); + * </pre> + * Allowed from main thread: + * <pre> + * getAuthToken(account, authTokenType, options, activity, new AccountManagerCallback<Bundle>() { + * public void run(AccountManagerFuture<Bundle> future) { + * Bundle result = future.getResult(); + * // use result + * } + * }, handler); + * </pre> + * <p> * Requires that the caller has permission {@link android.Manifest.permission#USE_CREDENTIALS}. * * @param account The account whose credentials are to be updated. @@ -524,8 +601,9 @@ public class AccountManager { * May be null. * @param options authenticator specific options for the request * @param activity If the authenticator returns a {@link #KEY_INTENT} in the result then - * the intent will be started with this activity. If activity is null then the result will - * be returned as-is. + * the intent will be started with this activity. If you do not with to have the intent + * started automatically then use the other form, + * {@link #getAuthToken(Account, String, boolean, AccountManagerCallback, android.os.Handler)} * @param callback A callback to invoke when the request completes. If null then * no callback is invoked. * @param handler The {@link Handler} to use to invoke the callback. If null then the @@ -560,9 +638,13 @@ public class AccountManager { * user to enter credentials. If it is able to retrieve the authtoken it will be returned * in the result. * <p> - * If the authenticator needs to prompt the user for credentials it will return an intent for + * If the authenticator needs to prompt the user for credentials, rather than returning the + * authtoken it will instead return an intent for * an activity that will do the prompting. If an intent is returned and notifyAuthFailure - * is true then a notification will be created that launches this intent. + * is true then a notification will be created that launches this intent. This intent can be + * invoked by the caller directly to start the activity that prompts the user for the + * updated credentials. Otherwise this activity will not be run until the user activates + * the notification. * <p> * This call returns immediately but runs asynchronously and the result is accessed via the * {@link AccountManagerFuture} that is returned. This future is also passed as the sole @@ -573,6 +655,23 @@ public class AccountManager { * {@link android.accounts.AccountManagerFuture#getResult()} on this method's return value, * which will then block until the request completes. * <p> + * Do not block the main thread waiting this method's result. + * <p> + * Not allowed from main thread (but allowed from other threads): + * <pre> + * Bundle result = getAuthToken( + * account, authTokenType, notifyAuthFailure, callback, handler).getResult(); + * </pre> + * Allowed from main thread: + * <pre> + * getAuthToken(account, authTokenType, notifyAuthFailure, new AccountManagerCallback<Bundle>() { + * public void run(AccountManagerFuture<Bundle> future) { + * Bundle result = future.getResult(); + * // use result + * } + * }, handler); + * </pre> + * <p> * Requires that the caller has permission {@link android.Manifest.permission#USE_CREDENTIALS}. * * @param account The account whose credentials are to be updated. @@ -620,6 +719,23 @@ public class AccountManager { * {@link android.accounts.AccountManagerFuture#getResult()} on this method's return value, * which will then block until the request completes. * <p> + * Do not block the main thread waiting this method's result. + * <p> + * Not allowed from main thread (but allowed from other threads): + * <pre> + * Bundle result = addAccount( + * account, authTokenType, features, options, activity, callback, handler).getResult(); + * </pre> + * Allowed from main thread: + * <pre> + * addAccount(account, authTokenType, features, options, activity, new AccountManagerCallback<Bundle>() { + * public void run(AccountManagerFuture<Bundle> future) { + * Bundle result = future.getResult(); + * // use result + * } + * }, handler); + * </pre> + * <p> * Requires that the caller has permission {@link android.Manifest.permission#MANAGE_ACCOUNTS}. * * @param accountType The type of account to add. This must not be null. @@ -641,7 +757,6 @@ public class AccountManager { * <ul> * <li> {@link #KEY_INTENT}, or * <li> {@link #KEY_ACCOUNT_NAME}, {@link #KEY_ACCOUNT_TYPE} - * and {@link #KEY_AUTHTOKEN} (if an authTokenType was specified). * </ul> */ public AccountManagerFuture<Bundle> addAccount(final String accountType, @@ -653,7 +768,7 @@ public class AccountManager { if (accountType == null) { Log.e(TAG, "the account must not be null"); // to unblock caller waiting on Future.get() - set(new Bundle()); + set(new Bundle()); return; } mService.addAcount(mResponse, accountType, authTokenType, @@ -662,6 +777,51 @@ public class AccountManager { }.start(); } + /** + * Queries for accounts that match the given account type and feature set. + * <p> + * This call returns immediately but runs asynchronously and the result is accessed via the + * {@link AccountManagerFuture} that is returned. This future is also passed as the sole + * parameter to the {@link AccountManagerCallback}. If the caller wished to use this + * method asynchronously then they will generally pass in a callback object that will get + * invoked with the {@link AccountManagerFuture}. If they wish to use it synchronously then + * they will generally pass null for the callback and instead call + * {@link android.accounts.AccountManagerFuture#getResult()} on this method's return value, + * which will then block until the request completes. + * <p> + * Do not block the main thread waiting this method's result. + * <p> + * Not allowed from main thread (but allowed from other threads): + * <pre> + * Account[] result = + * getAccountsByTypeAndFeatures(accountType, features, callback, handler).getResult(); + * </pre> + * Allowed from main thread: + * <pre> + * getAccountsByTypeAndFeatures(accountType, features, new AccountManagerCallback<Account[]>() { + * public void run(AccountManagerFuture<Account[]> future) { + * Account[] result = future.getResult(); + * // use result + * } + * }, handler); + * </pre> + * <p> + * Requires that the caller has permission {@link android.Manifest.permission#GET_ACCOUNTS}. + * + * @param type The type of {@link Account} to return. If null is passed in then an empty + * array will be returned. + * @param features the features with which to filter the accounts list. Each returned account + * will have all specified features. This may be null, which will mean the account list will + * not be filtered by features, making this functionally identical to + * {@link #getAccountsByType(String)}. + * @param callback A callback to invoke when the request completes. If null then + * no callback is invoked. + * @param handler The {@link Handler} to use to invoke the callback. If null then the + * main thread's {@link Handler} is used. + * @return an {@link AccountManagerFuture} that represents the future result of the call. + * The future result is a an {@link Account} array that contains accounts of the specified + * type that match all the requested features. + */ public AccountManagerFuture<Account[]> getAccountsByTypeAndFeatures( final String type, final String[] features, AccountManagerCallback<Account[]> callback, Handler handler) { @@ -704,6 +864,23 @@ public class AccountManager { * {@link android.accounts.AccountManagerFuture#getResult()} on this method's return value, * which will then block until the request completes. * <p> + * Do not block the main thread waiting this method's result. + * <p> + * Not allowed from main thread (but allowed from other threads): + * <pre> + * Bundle result = confirmCredentials( + * account, options, activity, callback, handler).getResult(); + * </pre> + * Allowed from main thread: + * <pre> + * confirmCredentials(account, options, activity, new AccountManagerCallback<Bundle>() { + * public void run(AccountManagerFuture<Bundle> future) { + * Bundle result = future.getResult(); + * // use result + * } + * }, handler); + * </pre> + * <p> * Requires that the caller has permission {@link android.Manifest.permission#MANAGE_ACCOUNTS}. * * @param account The account whose credentials are to be checked @@ -752,6 +929,23 @@ public class AccountManager { * {@link android.accounts.AccountManagerFuture#getResult()} on this method's return value, * which will then block until the request completes. * <p> + * Do not block the main thread waiting this method's result. + * <p> + * Not allowed from main thread (but allowed from other threads): + * <pre> + * Bundle result = updateCredentials( + * account, authTokenType, options, activity, callback, handler).getResult(); + * </pre> + * Allowed from main thread: + * <pre> + * updateCredentials(account, authTokenType, options, activity, new AccountManagerCallback<Bundle>() { + * public void run(AccountManagerFuture<Bundle> future) { + * Bundle result = future.getResult(); + * // use result + * } + * }, handler); + * </pre> + * <p> * Requires that the caller has permission {@link android.Manifest.permission#MANAGE_ACCOUNTS}. * * @param account The account whose credentials are to be updated. @@ -770,7 +964,7 @@ public class AccountManager { * <ul> * <li> {@link #KEY_INTENT}, which is to be used to prompt the user for the credentials * <li> {@link #KEY_ACCOUNT_NAME} and {@link #KEY_ACCOUNT_TYPE} if the user enters the correct - * credentials, and optionally a {@link #KEY_AUTHTOKEN} if an authTokenType was provided. + * credentials. * </ul> * If the user presses "back" then the request will be canceled. */ @@ -802,6 +996,22 @@ public class AccountManager { * {@link android.accounts.AccountManagerFuture#getResult()} on this method's return value, * which will then block until the request completes. * <p> + * Do not block the main thread waiting this method's result. + * <p> + * Not allowed from main thread (but allowed from other threads): + * <pre> + * Bundle result = editProperties(accountType, activity, callback, handler).getResult(); + * </pre> + * Allowed from main thread: + * <pre> + * editProperties(accountType, activity, new AccountManagerCallback<Bundle>() { + * public void run(AccountManagerFuture<Bundle> future) { + * Bundle result = future.getResult(); + * // use result + * } + * }, handler); + * </pre> + * <p> * Requires that the caller has permission {@link android.Manifest.permission#MANAGE_ACCOUNTS}. * * @param accountType The account type of the authenticator whose properties are to be edited. @@ -833,14 +1043,13 @@ public class AccountManager { 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 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"); + final IllegalStateException exception = new IllegalStateException( + "calling this from your main thread can lead to deadlock"); + Log.e(TAG, "calling this from your main thread can lead to deadlock and/or ANRs", + exception); + if (mContext.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.FROYO) { + throw exception; + } } } @@ -905,7 +1114,9 @@ public class AccountManager { private Bundle internalGetResult(Long timeout, TimeUnit unit) throws OperationCanceledException, IOException, AuthenticatorException { - ensureNotOnMainThread(); + if (!isDone()) { + ensureNotOnMainThread(); + } try { if (timeout == null) { return get(); @@ -1071,7 +1282,9 @@ public class AccountManager { private T internalGetResult(Long timeout, TimeUnit unit) throws OperationCanceledException, IOException, AuthenticatorException { - ensureNotOnMainThread(); + if (!isDone()) { + ensureNotOnMainThread(); + } try { if (timeout == null) { return get(); @@ -1372,7 +1585,7 @@ public class AccountManager { IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(LOGIN_ACCOUNTS_CHANGED_ACTION); // To recover from disk-full. - intentFilter.addAction(Intent.ACTION_DEVICE_STORAGE_OK); + intentFilter.addAction(Intent.ACTION_DEVICE_STORAGE_OK); mContext.registerReceiver(mAccountsChangedBroadcastReceiver, intentFilter); } } diff --git a/core/java/android/accounts/AccountManagerService.java b/core/java/android/accounts/AccountManagerService.java index e3ccd00..770554e 100644 --- a/core/java/android/accounts/AccountManagerService.java +++ b/core/java/android/accounts/AccountManagerService.java @@ -466,7 +466,8 @@ public class AccountManagerService public TestFeaturesSession(IAccountManagerResponse response, Account account, String[] features) { - super(response, account.type, false /* expectActivityLaunch */); + super(response, account.type, false /* expectActivityLaunch */, + true /* stripAuthTokenFromResult */); mFeatures = features; mAccount = account; } @@ -520,7 +521,8 @@ public class AccountManagerService private class RemoveAccountSession extends Session { final Account mAccount; public RemoveAccountSession(IAccountManagerResponse response, Account account) { - super(response, account.type, false /* expectActivityLaunch */); + super(response, account.type, false /* expectActivityLaunch */, + true /* stripAuthTokenFromResult */); mAccount = account; } @@ -794,7 +796,8 @@ public class AccountManagerService } } - new Session(response, account.type, expectActivityLaunch) { + new Session(response, account.type, expectActivityLaunch, + false /* stripAuthTokenFromResult */) { protected String toDebugString(long now) { if (loginOptions != null) loginOptions.keySet(); return super.toDebugString(now) + ", getAuthToken" @@ -945,7 +948,8 @@ public class AccountManagerService checkManageAccountsPermission(); long identityToken = clearCallingIdentity(); try { - new Session(response, accountType, expectActivityLaunch) { + new Session(response, accountType, expectActivityLaunch, + true /* stripAuthTokenFromResult */) { public void run() throws RemoteException { mAuthenticator.addAccount(this, mAccountType, authTokenType, requiredFeatures, options); @@ -970,7 +974,8 @@ public class AccountManagerService checkManageAccountsPermission(); long identityToken = clearCallingIdentity(); try { - new Session(response, account.type, expectActivityLaunch) { + new Session(response, account.type, expectActivityLaunch, + true /* stripAuthTokenFromResult */) { public void run() throws RemoteException { mAuthenticator.confirmCredentials(this, account, options); } @@ -990,7 +995,8 @@ public class AccountManagerService checkManageAccountsPermission(); long identityToken = clearCallingIdentity(); try { - new Session(response, account.type, expectActivityLaunch) { + new Session(response, account.type, expectActivityLaunch, + true /* stripAuthTokenFromResult */) { public void run() throws RemoteException { mAuthenticator.updateCredentials(this, account, authTokenType, loginOptions); } @@ -1012,7 +1018,8 @@ public class AccountManagerService checkManageAccountsPermission(); long identityToken = clearCallingIdentity(); try { - new Session(response, accountType, expectActivityLaunch) { + new Session(response, accountType, expectActivityLaunch, + true /* stripAuthTokenFromResult */) { public void run() throws RemoteException { mAuthenticator.editProperties(this, mAccountType); } @@ -1034,7 +1041,8 @@ public class AccountManagerService public GetAccountsByTypeAndFeatureSession(IAccountManagerResponse response, String type, String[] features) { - super(response, type, false /* expectActivityLaunch */); + super(response, type, false /* expectActivityLaunch */, + true /* stripAuthTokenFromResult */); mFeatures = features; } @@ -1176,11 +1184,14 @@ public class AccountManagerService IAccountAuthenticator mAuthenticator = null; + private final boolean mStripAuthTokenFromResult; + public Session(IAccountManagerResponse response, String accountType, - boolean expectActivityLaunch) { + boolean expectActivityLaunch, boolean stripAuthTokenFromResult) { super(); if (response == null) throw new IllegalArgumentException("response is null"); if (accountType == null) throw new IllegalArgumentException("accountType is null"); + mStripAuthTokenFromResult = stripAuthTokenFromResult; mResponse = response; mAccountType = accountType; mExpectActivityLaunch = expectActivityLaunch; @@ -1319,6 +1330,9 @@ public class AccountManagerService response.onError(AccountManager.ERROR_CODE_INVALID_RESPONSE, "null bundle returned"); } else { + if (mStripAuthTokenFromResult) { + result.remove(AccountManager.KEY_AUTHTOKEN); + } response.onResult(result); } } catch (RemoteException e) { @@ -1788,7 +1802,7 @@ public class AccountManagerService if (!permissionGranted && isDebuggableMonkeyBuild) { // TODO: Skip this check when running automated tests. Replace this // with a more general solution. - Log.w(TAG, "no credentials permission for usage of " + account + ", " + Log.d(TAG, "no credentials permission for usage of " + account + ", " + authTokenType + " by uid " + Binder.getCallingUid() + " but ignoring since this is a monkey build"); return true; diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 56e44c8..13cc3ba 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -1392,7 +1392,7 @@ public final class ActivityThread { r.startsNotResumed = notResumed; r.createdConfig = config; - synchronized (mRelaunchingActivities) { + synchronized (mPackages) { mRelaunchingActivities.add(r); } @@ -1523,8 +1523,11 @@ public final class ActivityThread { } public void scheduleConfigurationChanged(Configuration config) { - synchronized (mRelaunchingActivities) { - mPendingConfiguration = config; + synchronized (mPackages) { + if (mPendingConfiguration == null || + mPendingConfiguration.isOtherSeqNewer(config)) { + mPendingConfiguration = config; + } } queueOrSendMessage(H.CONFIGURATION_CHANGED, config); } @@ -2060,6 +2063,7 @@ public final class ActivityThread { = new HashMap<IBinder, Service>(); AppBindData mBoundApplication; Configuration mConfiguration; + Configuration mResConfiguration; Application mInitialApplication; final ArrayList<Application> mAllApplications = new ArrayList<Application>(); @@ -2073,14 +2077,6 @@ public final class ActivityThread { boolean mSystemThread = false; boolean mJitEnabled = false; - /** - * Activities that are enqueued to be relaunched. This list is accessed - * by multiple threads, so you must synchronize on it when accessing it. - */ - final ArrayList<ActivityRecord> mRelaunchingActivities - = new ArrayList<ActivityRecord>(); - Configuration mPendingConfiguration = null; - // These can be accessed by multiple threads; mPackages is the lock. // XXX For now we keep around information about all packages we have // seen, not removing entries from this map. @@ -2092,6 +2088,9 @@ public final class ActivityThread { DisplayMetrics mDisplayMetrics = null; HashMap<ResourcesKey, WeakReference<Resources> > mActiveResources = new HashMap<ResourcesKey, WeakReference<Resources> >(); + final ArrayList<ActivityRecord> mRelaunchingActivities + = new ArrayList<ActivityRecord>(); + Configuration mPendingConfiguration = null; // The lock of mProviderMap protects the following variables. final HashMap<String, ProviderRecord> mProviderMap @@ -3555,7 +3554,7 @@ public final class ActivityThread { // First: make sure we have the most recent configuration and most // recent version of the activity, or skip it if some previous call // had taken a more recent version. - synchronized (mRelaunchingActivities) { + synchronized (mPackages) { int N = mRelaunchingActivities.size(); IBinder token = tmp.token; tmp = null; @@ -3585,8 +3584,12 @@ public final class ActivityThread { // assume that is really what we want regardless of what we // may have pending. if (mConfiguration == null - || mConfiguration.diff(tmp.createdConfig) != 0) { - changedConfig = tmp.createdConfig; + || (tmp.createdConfig.isOtherSeqNewer(mConfiguration) + && mConfiguration.diff(tmp.createdConfig) != 0)) { + if (changedConfig == null + || tmp.createdConfig.isOtherSeqNewer(changedConfig)) { + changedConfig = tmp.createdConfig; + } } } @@ -3761,62 +3764,81 @@ public final class ActivityThread { } } + final void applyConfigurationToResourcesLocked(Configuration config) { + if (mResConfiguration == null) { + mResConfiguration = new Configuration(); + } + if (!mResConfiguration.isOtherSeqNewer(config)) { + return; + } + mResConfiguration.updateFrom(config); + DisplayMetrics dm = getDisplayMetricsLocked(true); + + // set it for java, this also affects newly created Resources + if (config.locale != null) { + Locale.setDefault(config.locale); + } + + Resources.updateSystemConfiguration(config, dm); + + ContextImpl.ApplicationPackageManager.configurationChanged(); + //Log.i(TAG, "Configuration changed in " + currentPackageName()); + + Iterator<WeakReference<Resources>> it = + mActiveResources.values().iterator(); + //Iterator<Map.Entry<String, WeakReference<Resources>>> it = + // mActiveResources.entrySet().iterator(); + while (it.hasNext()) { + WeakReference<Resources> v = it.next(); + Resources r = v.get(); + if (r != null) { + r.updateConfiguration(config, dm); + //Log.i(TAG, "Updated app resources " + v.getKey() + // + " " + r + ": " + r.getConfiguration()); + } else { + //Log.i(TAG, "Removing old resources " + v.getKey()); + it.remove(); + } + } + } + final void handleConfigurationChanged(Configuration config) { - synchronized (mRelaunchingActivities) { + ArrayList<ComponentCallbacks> callbacks = null; + + synchronized (mPackages) { if (mPendingConfiguration != null) { - config = mPendingConfiguration; + if (!mPendingConfiguration.isOtherSeqNewer(config)) { + config = mPendingConfiguration; + } mPendingConfiguration = null; } - } - - ArrayList<ComponentCallbacks> callbacks - = new ArrayList<ComponentCallbacks>(); - if (DEBUG_CONFIGURATION) Log.v(TAG, "Handle configuration changed: " - + config); + if (config == null) { + return; + } + + if (DEBUG_CONFIGURATION) Log.v(TAG, "Handle configuration changed: " + + config); - synchronized(mPackages) { + applyConfigurationToResourcesLocked(config); + if (mConfiguration == null) { mConfiguration = new Configuration(); } - mConfiguration.updateFrom(config); - DisplayMetrics dm = getDisplayMetricsLocked(true); - - // set it for java, this also affects newly created Resources - if (config.locale != null) { - Locale.setDefault(config.locale); - } - - Resources.updateSystemConfiguration(config, dm); - - ContextImpl.ApplicationPackageManager.configurationChanged(); - //Log.i(TAG, "Configuration changed in " + currentPackageName()); - { - Iterator<WeakReference<Resources>> it = - mActiveResources.values().iterator(); - //Iterator<Map.Entry<String, WeakReference<Resources>>> it = - // mActiveResources.entrySet().iterator(); - while (it.hasNext()) { - WeakReference<Resources> v = it.next(); - Resources r = v.get(); - if (r != null) { - r.updateConfiguration(config, dm); - //Log.i(TAG, "Updated app resources " + v.getKey() - // + " " + r + ": " + r.getConfiguration()); - } else { - //Log.i(TAG, "Removing old resources " + v.getKey()); - it.remove(); - } - } + if (!mConfiguration.isOtherSeqNewer(config)) { + return; } + mConfiguration.updateFrom(config); callbacks = collectComponentCallbacksLocked(false, config); } - final int N = callbacks.size(); - for (int i=0; i<N; i++) { - performConfigurationChanged(callbacks.get(i), config); + if (callbacks != null) { + final int N = callbacks.size(); + for (int i=0; i<N; i++) { + performConfigurationChanged(callbacks.get(i), config); + } } } @@ -3856,7 +3878,7 @@ public final class ActivityThread { ArrayList<ComponentCallbacks> callbacks = new ArrayList<ComponentCallbacks>(); - synchronized(mPackages) { + synchronized (mPackages) { callbacks = collectComponentCallbacksLocked(true, null); } @@ -4348,6 +4370,25 @@ public final class ActivityThread { "Unable to instantiate Application():" + e.toString(), e); } } + + ViewRoot.addConfigCallback(new ComponentCallbacks() { + public void onConfigurationChanged(Configuration newConfig) { + synchronized (mPackages) { + if (mPendingConfiguration == null || + mPendingConfiguration.isOtherSeqNewer(newConfig)) { + mPendingConfiguration = newConfig; + + // We need to apply this change to the resources + // immediately, because upon returning the view + // hierarchy will be informed about it. + applyConfigurationToResourcesLocked(newConfig); + } + } + queueOrSendMessage(H.CONFIGURATION_CHANGED, newConfig); + } + public void onLowMemory() { + } + }); } private final void detach() diff --git a/core/java/android/app/AlarmManager.java b/core/java/android/app/AlarmManager.java index 53c7935..9082003 100644 --- a/core/java/android/app/AlarmManager.java +++ b/core/java/android/app/AlarmManager.java @@ -277,7 +277,26 @@ public class AlarmManager } catch (RemoteException ex) { } } - + + /** + * Set the system wall clock time. + * Requires the permission android.permission.SET_TIME. + * + * @param millis time in milliseconds since the Epoch + */ + public void setTime(long millis) { + try { + mService.setTime(millis); + } catch (RemoteException ex) { + } + } + + /** + * Set the system default time zone. + * Requires the permission android.permission.SET_TIME_ZONE. + * + * @param timeZone in the format understood by {@link java.util.TimeZone} + */ public void setTimeZone(String timeZone) { try { mService.setTimeZone(timeZone); diff --git a/core/java/android/app/BackupAgent.java b/core/java/android/app/BackupAgent.java index 35b6fed..a2bfc76 100644 --- a/core/java/android/app/BackupAgent.java +++ b/core/java/android/app/BackupAgent.java @@ -33,8 +33,8 @@ import java.io.IOException; /** * This is the central interface between an application and Android's * settings backup mechanism. - * - * @hide pending API solidification + * + * <p>STOPSHIP write more documentation about the backup process here. */ public abstract class BackupAgent extends ContextWrapper { private static final String TAG = "BackupAgent"; @@ -62,9 +62,9 @@ public abstract class BackupAgent extends ContextWrapper { * state provided by the application. May be null, in which * case no prior state is being provided and the application should * perform a full backup. - * @param data An open, read/write ParcelFileDescriptor pointing to the backup data - * destination. Typically the application will use backup helper - * classes to write to this file. + * @param data A structured wrapper around an open, read/write ParcelFileDescriptor + * pointing to the backup data destination. Typically the application will use + * backup helper classes to write to this file. * @param newState An open, read/write ParcelFileDescriptor pointing to an empty * file. The application should record the final backup state * here after writing the requested data to dataFd. @@ -77,10 +77,18 @@ public abstract class BackupAgent extends ContextWrapper { * existing data with the contents of the backup. The backup data is * provided in the file pointed to by the dataFd file descriptor. Once * the restore is finished, the application should write a representation - * of the final state to the newStateFd file descriptor, + * of the final state to the newStateFd file descriptor, + * + * <p>The application is responsible for properly erasing its old data and + * replacing it with the data supplied to this method. No "clear user data" + * operation will be performed automatically by the operating system. The + * exception to this is in the case of a failed restore attempt: if onRestore() + * throws an exception, the OS will assume that the application's data may now + * be in an incoherent state, and will clear it before proceeding. * - * @param data An open, read-only ParcelFileDescriptor pointing to a full snapshot - * of the application's data. + * @param data A structured wrapper around an open, read-only ParcelFileDescriptor + * pointing to a full snapshot of the application's data. Typically the + * application will use helper classes to read this data. * @param appVersionCode The android:versionCode value of the application that backed * up this particular data set. This makes it easier for an application's * agent to distinguish among several possible older data versions when diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index 5f89496..db6a4bf 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -52,6 +52,7 @@ import android.content.pm.PermissionInfo; import android.content.pm.ProviderInfo; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; +import android.content.pm.PackageParser.Package; import android.content.res.AssetManager; import android.content.res.Resources; import android.content.res.XmlResourceParser; @@ -84,7 +85,8 @@ import android.os.ServiceManager; import android.os.StatFs; import android.os.Vibrator; import android.os.FileUtils.FileStatus; -import android.storage.StorageManager; +import android.os.storage.StorageManager; +import android.provider.Settings; import android.telephony.TelephonyManager; import android.text.ClipboardManager; import android.util.AndroidRuntimeException; @@ -197,9 +199,9 @@ class ContextImpl extends Context { private File mDatabasesDir; private File mPreferencesDir; private File mFilesDir; - - private File mCacheDir; + private File mExternalFilesDir; + private File mExternalCacheDir; private static long sInstanceCount = 0; @@ -438,6 +440,38 @@ class ContextImpl extends Context { } @Override + public File getExternalFilesDir(String type) { + synchronized (mSync) { + if (mExternalFilesDir == null) { + mExternalFilesDir = Environment.getExternalStorageAppFilesDirectory( + getPackageName()); + } + if (!mExternalFilesDir.exists()) { + try { + (new File(Environment.getExternalStorageAndroidDataDir(), + ".nomedia")).createNewFile(); + } catch (IOException e) { + } + if (!mExternalFilesDir.mkdirs()) { + Log.w(TAG, "Unable to create external files directory"); + return null; + } + } + if (type == null) { + return mExternalFilesDir; + } + File dir = new File(mExternalFilesDir, type); + if (!dir.exists()) { + if (!dir.mkdirs()) { + Log.w(TAG, "Unable to create external media directory " + dir); + return null; + } + } + return dir; + } + } + + @Override public File getCacheDir() { synchronized (mSync) { if (mCacheDir == null) { @@ -457,7 +491,28 @@ class ContextImpl extends Context { return mCacheDir; } - + @Override + public File getExternalCacheDir() { + synchronized (mSync) { + if (mExternalCacheDir == null) { + mExternalCacheDir = Environment.getExternalStorageAppCacheDirectory( + getPackageName()); + } + if (!mExternalCacheDir.exists()) { + try { + (new File(Environment.getExternalStorageAndroidDataDir(), + ".nomedia")).createNewFile(); + } catch (IOException e) { + } + if (!mExternalCacheDir.mkdirs()) { + Log.w(TAG, "Unable to create external cache directory"); + return null; + } + } + return mExternalCacheDir; + } + } + @Override public File getFileStreamPath(String name) { return makeFilename(getFilesDir(), name); @@ -1560,6 +1615,24 @@ class ContextImpl extends Context { } @Override + public String[] currentToCanonicalPackageNames(String[] names) { + try { + return mPM.currentToCanonicalPackageNames(names); + } catch (RemoteException e) { + throw new RuntimeException("Package manager has died", e); + } + } + + @Override + public String[] canonicalToCurrentPackageNames(String[] names) { + try { + return mPM.canonicalToCurrentPackageNames(names); + } catch (RemoteException e) { + throw new RuntimeException("Package manager has died", e); + } + } + + @Override public Intent getLaunchIntentForPackage(String packageName) { // First see if the package has an INFO activity; the existence of // such an activity is implied to be the desired front-door for the @@ -2166,7 +2239,7 @@ class ContextImpl extends Context { filter, null, null, null); // Register for events related to sdcard installation. IntentFilter sdFilter = new IntentFilter(); - sdFilter.addAction(Intent.ACTION_MEDIA_RESOURCES_UNAVAILABLE); + sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE); mContext.registerReceiverInternal(sPackageRemovedReceiver, sdFilter, null, null, null); } @@ -2189,7 +2262,7 @@ class ContextImpl extends Context { String pkgList[] = null; String action = intent.getAction(); boolean immediateGc = false; - if (Intent.ACTION_MEDIA_RESOURCES_UNAVAILABLE.equals(action)) { + if (Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE.equals(action)) { pkgList = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST); immediateGc = true; } else { @@ -2586,76 +2659,6 @@ class ContextImpl extends Context { return PackageManager.COMPONENT_ENABLED_STATE_DEFAULT; } - // Constants related to app heuristics - // No-installation limit for internal flash: 10% or less space available - private static final double LOW_NAND_FLASH_TRESHOLD = 0.1; - - // SD-to-internal app size threshold: currently set to 1 MB - private static final long INSTALL_ON_SD_THRESHOLD = (1024 * 1024); - - @Override - public int recommendAppInstallLocation(ApplicationInfo appInfo, Uri packageURI) { - // Initial implementation: - // Package size = code size + cache size + data size - // If code size > 1 MB, install on SD card. - // Else install on internal NAND flash, unless space on NAND is less than 10% - - if ((packageURI == null) || (appInfo == null)) { - return INSTALL_PARSE_FAILED_NOT_APK; - } - - StatFs internalFlashStats = new StatFs(Environment.getDataDirectory().getPath()); - StatFs sdcardStats = new StatFs(Environment.getExternalStorageDirectory().getPath()); - - long totalInternalFlashSize = (long)internalFlashStats.getBlockCount() * - (long)internalFlashStats.getBlockSize(); - long availInternalFlashSize = (long)internalFlashStats.getAvailableBlocks() * - (long)internalFlashStats.getBlockSize(); - long availSDSize = (long)sdcardStats.getAvailableBlocks() * - (long)sdcardStats.getBlockSize(); - - double pctNandFree = (double)availInternalFlashSize / (double)totalInternalFlashSize; - - final String archiveFilePath = packageURI.getPath(); - File apkFile = new File(archiveFilePath); - long pkgLen = apkFile.length(); - - // Consider application flags preferences as well... - boolean installOnlyOnSD = ((appInfo.flags & PackageManager.INSTALL_ON_SDCARD) != 0); - - // These are not very precise measures, but I guess it is hard to estimate sizes - // before installing the package. - // As a shortcut, I am assuming that the package fits on NAND flash if the available - // space is three times that of the APK size. For SD, we only worry about the APK size. - // Since packages are downloaded into SD, this might not even be necessary. - boolean fitsOnSD = (pkgLen < availSDSize) && ((2 * pkgLen) < availInternalFlashSize); - boolean fitsOnInternalFlash = ((pkgLen * 3) < availInternalFlashSize); - - // Does not fit, recommend no installation. - if (!fitsOnSD && !fitsOnInternalFlash) { - return INSTALL_FAILED_INSUFFICIENT_STORAGE; - } - - if (pkgLen < (INSTALL_ON_SD_THRESHOLD) && fitsOnInternalFlash && !(installOnlyOnSD)) { - // recommend internal NAND likely - if (pctNandFree < LOW_NAND_FLASH_TRESHOLD) { - // Low space on NAND (<10%) - install on SD - return INSTALL_ON_SDCARD; - } - return INSTALL_ON_INTERNAL_FLASH; - } else { - if (fitsOnSD) { - // Recommend SD card - return INSTALL_ON_SDCARD; - } else if (fitsOnInternalFlash && (pctNandFree >= LOW_NAND_FLASH_TRESHOLD) && - !(installOnlyOnSD)) { - return INSTALL_ON_INTERNAL_FLASH; - } else { - return INSTALL_FAILED_INSUFFICIENT_STORAGE; - } - } - } - private final ContextImpl mContext; private final IPackageManager mPM; diff --git a/core/java/android/app/DeviceAdminInfo.java b/core/java/android/app/DeviceAdminInfo.java index ab9c44f..bedf4b4 100644 --- a/core/java/android/app/DeviceAdminInfo.java +++ b/core/java/android/app/DeviceAdminInfo.java @@ -60,8 +60,8 @@ public final class DeviceAdminInfo implements Parcelable { /** * A type of policy that this device admin can use: able to watch login - * attempts from the user, via {@link DeviceAdmin#ACTION_PASSWORD_FAILED}, - * {@link DeviceAdmin#ACTION_PASSWORD_SUCCEEDED}, and + * attempts from the user, via {@link DeviceAdminReceiver#ACTION_PASSWORD_FAILED}, + * {@link DeviceAdminReceiver#ACTION_PASSWORD_SUCCEEDED}, and * {@link DevicePolicyManager#getCurrentFailedPasswordAttempts}. * * <p>To control this policy, the device admin must have a "watch-login" @@ -80,23 +80,15 @@ public final class DeviceAdminInfo implements Parcelable { public static final int USES_POLICY_RESET_PASSWORD = 2; /** - * A type of policy that this device admin can use: able to limit the + * A type of policy that this device admin can use: able to force the device + * to lock via{@link DevicePolicyManager#lockNow} or limit the * maximum lock timeout for the device via * {@link DevicePolicyManager#setMaximumTimeToLock}. * - * <p>To control this policy, the device admin must have a "limit-unlock" - * tag in the "uses-policies" section of its meta-data. - */ - public static final int USES_POLICY_LIMIT_UNLOCK = 3; - - /** - * A type of policy that this device admin can use: able to force the device - * to lock via{@link DevicePolicyManager#lockNow}. - * * <p>To control this policy, the device admin must have a "force-lock" * tag in the "uses-policies" section of its meta-data. */ - public static final int USES_POLICY_FORCE_LOCK = 4; + public static final int USES_POLICY_FORCE_LOCK = 3; /** * A type of policy that this device admin can use: able to factory @@ -106,7 +98,7 @@ public final class DeviceAdminInfo implements Parcelable { * <p>To control this policy, the device admin must have a "wipe-data" * tag in the "uses-policies" section of its meta-data. */ - public static final int USES_POLICY_WIPE_DATA = 5; + public static final int USES_POLICY_WIPE_DATA = 4; /** @hide */ public static class PolicyInfo { @@ -140,9 +132,6 @@ public final class DeviceAdminInfo implements Parcelable { sPoliciesDisplayOrder.add(new PolicyInfo(USES_POLICY_WATCH_LOGIN, "watch-login", com.android.internal.R.string.policylab_watchLogin, com.android.internal.R.string.policydesc_watchLogin)); - sPoliciesDisplayOrder.add(new PolicyInfo(USES_POLICY_LIMIT_UNLOCK, "limit-unlock", - com.android.internal.R.string.policylab_limitUnlock, - com.android.internal.R.string.policydesc_limitUnlock)); sPoliciesDisplayOrder.add(new PolicyInfo(USES_POLICY_FORCE_LOCK, "force-lock", com.android.internal.R.string.policylab_forceLock, com.android.internal.R.string.policydesc_forceLock)); @@ -180,10 +169,10 @@ public final class DeviceAdminInfo implements Parcelable { XmlResourceParser parser = null; try { - parser = ai.loadXmlMetaData(pm, DeviceAdmin.DEVICE_ADMIN_META_DATA); + parser = ai.loadXmlMetaData(pm, DeviceAdminReceiver.DEVICE_ADMIN_META_DATA); if (parser == null) { throw new XmlPullParserException("No " - + DeviceAdmin.DEVICE_ADMIN_META_DATA + " meta-data"); + + DeviceAdminReceiver.DEVICE_ADMIN_META_DATA + " meta-data"); } AttributeSet attrs = Xml.asAttributeSet(parser); @@ -314,8 +303,8 @@ public final class DeviceAdminInfo implements Parcelable { * Return true if the device admin has requested that it be able to use * the given policy control. The possible policy identifier inputs are: * {@link #USES_POLICY_LIMIT_PASSWORD}, {@link #USES_POLICY_WATCH_LOGIN}, - * {@link #USES_POLICY_RESET_PASSWORD}, {@link #USES_POLICY_LIMIT_UNLOCK}, - * {@link #USES_POLICY_FORCE_LOCK}, {@link #USES_POLICY_WIPE_DATA}. + * {@link #USES_POLICY_RESET_PASSWORD}, {@link #USES_POLICY_FORCE_LOCK}, + * {@link #USES_POLICY_WIPE_DATA}. */ public boolean usesPolicy(int policyIdent) { return (mUsesPolicies & (1<<policyIdent)) != 0; diff --git a/core/java/android/app/DeviceAdmin.java b/core/java/android/app/DeviceAdminReceiver.java index ecbad01..453e0bf 100644 --- a/core/java/android/app/DeviceAdmin.java +++ b/core/java/android/app/DeviceAdminReceiver.java @@ -29,6 +29,13 @@ import android.os.Bundle; * class provides a convenience for interpreting the raw intent actions * that are sent by the system. * + * <p>The callback methods, like the base + * {@link BroadcastReceiver#onReceive(Context, Intent) BroadcastReceiver.onReceive()} + * method, happen on the main thread of the process. Thus long running + * operations must be done on another thread. Note that because a receiver + * is done once returning from its receive function, such long-running operations + * should probably be done in a {@link Service}. + * * <p>When publishing your DeviceAdmin subclass as a receiver, it must * handle {@link #ACTION_DEVICE_ADMIN_ENABLED} and require the * {@link android.Manifest.permission#BIND_DEVICE_ADMIN} permission. A typical @@ -40,9 +47,9 @@ import android.os.Bundle; * to the device administrator, as parsed by the {@link DeviceAdminInfo} class. * A typical file would be:</p> * - * {@sample development/samples/ApiDemos/res/xml/sample_device_admin.xml meta_data} + * {@sample development/samples/ApiDemos/res/xml/device_admin_sample.xml meta_data} */ -public class DeviceAdmin extends BroadcastReceiver { +public class DeviceAdminReceiver extends BroadcastReceiver { private static String TAG = "DevicePolicy"; private static boolean DEBUG = false; private static boolean localLOGV = DEBUG || android.util.Config.LOGV; @@ -51,7 +58,7 @@ public class DeviceAdmin extends BroadcastReceiver { * This is the primary action that a device administrator must implement to be * allowed to manage a device. This will be set to the receiver * when the user enables it for administration. You will generally - * handle this in {@link DeviceAdmin#onEnabled(Context, Intent)}. To be + * handle this in {@link DeviceAdminReceiver#onEnabled(Context, Intent)}. To be * supported, the receiver must also require the * {@link android.Manifest.permission#BIND_DEVICE_ADMIN} permission so * that other applications can not abuse it. @@ -85,7 +92,7 @@ public class DeviceAdmin extends BroadcastReceiver { * Action sent to a device administrator when the user has disabled * it. Upon return, the application no longer has access to the * protected device policy manager APIs. You will generally - * handle this in {@link DeviceAdmin#onDisabled(Context, Intent)}. Note + * handle this in {@link DeviceAdminReceiver#onDisabled(Context, Intent)}. Note * that this action will be * sent the receiver regardless of whether it is explicitly listed in * its intent filter. @@ -100,7 +107,7 @@ public class DeviceAdmin extends BroadcastReceiver { * of the new password with {@link DevicePolicyManager#isActivePasswordSufficient() * DevicePolicyManager.isActivePasswordSufficient()}. * You will generally - * handle this in {@link DeviceAdmin#onPasswordChanged}. + * handle this in {@link DeviceAdminReceiver#onPasswordChanged}. * * <p>The calling device admin must have requested * {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to receive @@ -116,7 +123,7 @@ public class DeviceAdmin extends BroadcastReceiver { * number of failed password attempts there have been with * {@link DevicePolicyManager#getCurrentFailedPasswordAttempts * DevicePolicyManager.getCurrentFailedPasswordAttempts()}. You will generally - * handle this in {@link DeviceAdmin#onPasswordFailed}. + * handle this in {@link DeviceAdminReceiver#onPasswordFailed}. * * <p>The calling device admin must have requested * {@link DeviceAdminInfo#USES_POLICY_WATCH_LOGIN} to receive diff --git a/core/java/android/app/DevicePolicyManager.java b/core/java/android/app/DevicePolicyManager.java index 08cdd05..d611807 100644 --- a/core/java/android/app/DevicePolicyManager.java +++ b/core/java/android/app/DevicePolicyManager.java @@ -36,7 +36,7 @@ import java.util.List; /** * Public interface for managing policies enforced on a device. Most clients - * of this class must have published a {@link DeviceAdmin} that the user + * of this class must have published a {@link DeviceAdminReceiver} that the user * has currently enabled. */ public class DevicePolicyManager { @@ -195,7 +195,7 @@ public class DevicePolicyManager { * {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call * this method; if it has not, a security exception will be thrown. * - * @param admin Which {@link DeviceAdmin} this request is associated with. + * @param admin Which {@link DeviceAdminReceiver} this request is associated with. * @param quality The new desired quality. One of * {@link #PASSWORD_QUALITY_UNSPECIFIED}, {@link #PASSWORD_QUALITY_SOMETHING}, * {@link #PASSWORD_QUALITY_NUMERIC}, or {@link #PASSWORD_QUALITY_ALPHANUMERIC}. @@ -243,7 +243,7 @@ public class DevicePolicyManager { * {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call * this method; if it has not, a security exception will be thrown. * - * @param admin Which {@link DeviceAdmin} this request is associated with. + * @param admin Which {@link DeviceAdminReceiver} this request is associated with. * @param length The new desired minimum password length. A value of 0 * means there is no restriction. */ @@ -328,15 +328,21 @@ public class DevicePolicyManager { } /** - * Set the maximum number of failed password attempts that are allowed - * before the device wipes its data. This is convenience for implementing - * the corresponding functionality with a combination of watching failed - * password attempts and calling {@link #wipeData} upon reaching a certain - * count, and as such requires that you request both - * {@link DeviceAdminInfo#USES_POLICY_WATCH_LOGIN} and + * Setting this to a value greater than zero enables a built-in policy + * that will perform a device wipe after too many incorrect + * device-unlock passwords have been entered. This built-in policy combines + * watching for failed passwords and wiping the device, and requires + * that you request both {@link DeviceAdminInfo#USES_POLICY_WATCH_LOGIN} and * {@link DeviceAdminInfo#USES_POLICY_WIPE_DATA}}. * - * @param admin Which {@link DeviceAdmin} this request is associated with. + * <p>To implement any other policy (e.g. wiping data for a particular + * application only, erasing or revoking credentials, or reporting the + * failure to a server), you should implement + * {@link DeviceAdminReceiver#onPasswordFailed(Context, android.content.Intent)} + * instead. Do not use this API, because if the maximum count is reached, + * the device will be wiped immediately, and your callback will not be invoked. + * + * @param admin Which {@link DeviceAdminReceiver} this request is associated with. * @param num The number of failed password attempts at which point the * device will wipe its data. */ @@ -369,7 +375,9 @@ public class DevicePolicyManager { } /** - * Force a new password on the user. This takes effect immediately. + * Force a new device unlock password (the password needed to access the + * entire device, not for individual accounts) on the user. This takes + * effect immediately. * The given password must be sufficient for the * current password quality and length constraints as returned by * {@link #getPasswordQuality(ComponentName)} and @@ -404,10 +412,10 @@ public class DevicePolicyManager { * the length that the user can set. It takes effect immediately. * * <p>The calling device admin must have requested - * {@link DeviceAdminInfo#USES_POLICY_LIMIT_UNLOCK} to be able to call + * {@link DeviceAdminInfo#USES_POLICY_FORCE_LOCK} to be able to call * this method; if it has not, a security exception will be thrown. * - * @param admin Which {@link DeviceAdmin} this request is associated with. + * @param admin Which {@link DeviceAdminReceiver} this request is associated with. * @param timeMs The new desired maximum time to lock in milliseconds. * A value of 0 means there is no restriction. */ diff --git a/core/java/android/app/Dialog.java b/core/java/android/app/Dialog.java index ed38240..4598bb5 100644 --- a/core/java/android/app/Dialog.java +++ b/core/java/android/app/Dialog.java @@ -822,11 +822,6 @@ public class Dialog implements DialogInterface, Window.Callback, final SearchManager searchManager = (SearchManager) mContext .getSystemService(Context.SEARCH_SERVICE); - // can't start search without an associated activity (e.g a system dialog) - if (!searchManager.hasIdent()) { - return false; - } - // associate search with owner activity if possible (otherwise it will default to // global search). final ComponentName appName = getAssociatedActivity(); diff --git a/core/java/android/app/FullBackupAgent.java b/core/java/android/app/FullBackupAgent.java index d89db96..db198ad 100644 --- a/core/java/android/app/FullBackupAgent.java +++ b/core/java/android/app/FullBackupAgent.java @@ -48,7 +48,8 @@ public class FullBackupAgent extends BackupAgent { } // That's the file set; now back it all up - FileBackupHelper helper = new FileBackupHelper(this, (String[])allFiles.toArray()); + FileBackupHelper helper = new FileBackupHelper(this, + allFiles.toArray(new String[allFiles.size()])); helper.performBackup(oldState, data, newState); } diff --git a/core/java/android/app/IAlarmManager.aidl b/core/java/android/app/IAlarmManager.aidl index cb42236..edb40ed 100755 --- a/core/java/android/app/IAlarmManager.aidl +++ b/core/java/android/app/IAlarmManager.aidl @@ -27,6 +27,7 @@ interface IAlarmManager { void set(int type, long triggerAtTime, in PendingIntent operation); void setRepeating(int type, long triggerAtTime, long interval, in PendingIntent operation); void setInexactRepeating(int type, long triggerAtTime, long interval, in PendingIntent operation); + void setTime(long millis); void setTimeZone(String zone); void remove(in PendingIntent operation); } diff --git a/core/java/android/app/IUiModeManager.aidl b/core/java/android/app/IUiModeManager.aidl new file mode 100644 index 0000000..6ac8a2a --- /dev/null +++ b/core/java/android/app/IUiModeManager.aidl @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app; + +/** + * Interface used to control special UI modes. + * @hide + */ +interface IUiModeManager { + /** + * Enables the car mode. Only the system can do this. + * @hide + */ + void enableCarMode(); + + /** + * Disables the car mode. + */ + void disableCarMode(); + + /** + * Sets the night mode. + * The mode can be one of: + * 1 - notnight mode + * 2 - night mode + * 3 - automatic mode switching + */ + void setNightMode(int mode); + + /** + * Gets the currently configured night mode. Return 1 for notnight, + * 2 for night, and 3 for automatic mode switching. + */ + int getNightMode(); +} diff --git a/core/java/android/app/IntentService.java b/core/java/android/app/IntentService.java index 804c8eb..3fd36a3 100644 --- a/core/java/android/app/IntentService.java +++ b/core/java/android/app/IntentService.java @@ -42,7 +42,7 @@ public abstract class IntentService extends Service { * {@link #onStartCommand(Intent, int, int)} will return * {@link Service#START_REDELIVER_INTENT} instead of * {@link Service#START_NOT_STICKY}, so that if this service's process - * is called while it is executing the Intent in + * is killed while it is executing the Intent in * {@link #onHandleIntent(Intent)}, then when later restarted the same Intent * will be re-delivered to it, to retry its execution. */ diff --git a/core/java/android/app/SearchDialog.java b/core/java/android/app/SearchDialog.java index 7fa5b08..581b436 100644 --- a/core/java/android/app/SearchDialog.java +++ b/core/java/android/app/SearchDialog.java @@ -16,6 +16,9 @@ package android.app; +import com.android.common.Patterns; +import com.android.common.speech.Recognition; + import static android.app.SuggestionsAdapter.getColumnString; import android.content.ActivityNotFoundException; @@ -67,8 +70,6 @@ import android.widget.TextView; import android.widget.AdapterView.OnItemClickListener; import android.widget.AdapterView.OnItemSelectedListener; -import com.android.common.Patterns; - import java.util.ArrayList; import java.util.WeakHashMap; import java.util.concurrent.atomic.AtomicLong; @@ -88,15 +89,9 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS private static final String INSTANCE_KEY_COMPONENT = "comp"; private static final String INSTANCE_KEY_APPDATA = "data"; - private static final String INSTANCE_KEY_GLOBALSEARCH = "glob"; - private static final String INSTANCE_KEY_STORED_COMPONENT = "sComp"; private static final String INSTANCE_KEY_STORED_APPDATA = "sData"; - private static final String INSTANCE_KEY_PREVIOUS_COMPONENTS = "sPrev"; private static final String INSTANCE_KEY_USER_QUERY = "uQry"; - // The extra key used in an intent to the speech recognizer for in-app voice search. - private static final String EXTRA_CALLING_PACKAGE = "calling_package"; - // The string used for privateImeOptions to identify to the IME that it should not show // a microphone button since one already exists in the search dialog. private static final String IME_OPTION_NO_MICROPHONE = "nm"; @@ -117,19 +112,8 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS private SearchableInfo mSearchable; private ComponentName mLaunchComponent; private Bundle mAppSearchData; - private boolean mGlobalSearchMode; private Context mActivityContext; private SearchManager mSearchManager; - - // Values we store to allow user to toggle between in-app search and global search. - private ComponentName mStoredComponentName; - private Bundle mStoredAppSearchData; - - // stack of previous searchables, to support the BACK key after - // SearchManager.INTENT_ACTION_CHANGE_SEARCH_SOURCE. - // The top of the stack (= previous searchable) is the last element of the list, - // since adding and removing is efficient at the end of an ArrayList. - private ArrayList<ComponentName> mPreviousComponents; // For voice searching private final Intent mVoiceWebSearchIntent; @@ -160,7 +144,7 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS * @param context Application Context we can use for system acess */ public SearchDialog(Context context, SearchManager searchManager) { - super(context, com.android.internal.R.style.Theme_GlobalSearchBar); + super(context, com.android.internal.R.style.Theme_SearchBar); // Save voice intent for later queries/launching mVoiceWebSearchIntent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH); @@ -243,14 +227,8 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS * @return true if search dialog launched, false if not */ public boolean show(String initialQuery, boolean selectInitialQuery, - ComponentName componentName, Bundle appSearchData, boolean globalSearch) { - - // Reset any stored values from last time dialog was shown. - mStoredComponentName = null; - mStoredAppSearchData = null; - - boolean success = doShow(initialQuery, selectInitialQuery, componentName, appSearchData, - globalSearch); + ComponentName componentName, Bundle appSearchData) { + boolean success = doShow(initialQuery, selectInitialQuery, componentName, appSearchData); if (success) { // Display the drop down as soon as possible instead of waiting for the rest of the // pending UI stuff to get done, so that things appear faster to the user. @@ -259,67 +237,16 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS return success; } - private boolean isInRealAppSearch() { - return !mGlobalSearchMode - && (mPreviousComponents == null || mPreviousComponents.isEmpty()); - } - /** - * Called in response to a press of the hard search button in - * {@link #onKeyDown(int, KeyEvent)}, this method toggles between in-app - * search and global search when relevant. - * - * If pressed within an in-app search context, this switches the search dialog out to - * global search. If pressed within a global search context that was originally an in-app - * search context, this switches back to the in-app search context. If pressed within a - * global search context that has no original in-app search context (e.g., global search - * from Home), this does nothing. - * - * @return false if we wanted to toggle context but could not do so successfully, true - * in all other cases - */ - private boolean toggleGlobalSearch() { - String currentSearchText = mSearchAutoComplete.getText().toString(); - if (!mGlobalSearchMode) { - mStoredComponentName = mLaunchComponent; - mStoredAppSearchData = mAppSearchData; - - // If this is the browser, we have a special case to not show the icon to the left - // of the text field, for extra space for url entry (this should be reconciled in - // Eclair). So special case a second tap of the search button to remove any - // already-entered text so that we can be sure to show the "Quick Search Box" hint - // text to still make it clear to the user that we've jumped out to global search. - // - // TODO: When the browser icon issue is reconciled in Eclair, remove this special case. - if (isBrowserSearch()) currentSearchText = ""; - - cancel(); - mSearchManager.startGlobalSearch(currentSearchText, false, mStoredAppSearchData); - return true; - } else { - if (mStoredComponentName != null) { - // This means we should toggle *back* to an in-app search context from - // global search. - return doShow(currentSearchText, false, mStoredComponentName, - mStoredAppSearchData, false); - } else { - return true; - } - } - } - - /** - * Does the rest of the work required to show the search dialog. Called by both - * {@link #show(String, boolean, ComponentName, Bundle, boolean)} and - * {@link #toggleGlobalSearch()}. - * + * Does the rest of the work required to show the search dialog. Called by + * {@link #show(String, boolean, ComponentName, Bundle)} and + * * @return true if search dialog showed, false if not */ private boolean doShow(String initialQuery, boolean selectInitialQuery, - ComponentName componentName, Bundle appSearchData, - boolean globalSearch) { + ComponentName componentName, Bundle appSearchData) { // set up the searchable and show the dialog - if (!show(componentName, appSearchData, globalSearch)) { + if (!show(componentName, appSearchData)) { return false; } @@ -337,38 +264,24 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS * * @return <code>true</code> if search dialog launched */ - private boolean show(ComponentName componentName, Bundle appSearchData, - boolean globalSearch) { + private boolean show(ComponentName componentName, Bundle appSearchData) { if (DBG) { Log.d(LOG_TAG, "show(" + componentName + ", " - + appSearchData + ", " + globalSearch + ")"); + + appSearchData + ")"); } SearchManager searchManager = (SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE); - // Try to get the searchable info for the provided component (or for global search, - // if globalSearch == true). - mSearchable = searchManager.getSearchableInfo(componentName, globalSearch); - - // If we got back nothing, and it wasn't a request for global search, then try again - // for global search, as we'll try to launch that in lieu of any component-specific search. - if (!globalSearch && mSearchable == null) { - globalSearch = true; - mSearchable = searchManager.getSearchableInfo(componentName, globalSearch); - } + // Try to get the searchable info for the provided component. + mSearchable = searchManager.getSearchableInfo(componentName, false); - // If there's not even a searchable info available for global search, then really give up. if (mSearchable == null) { - Log.w(LOG_TAG, "No global search provider."); return false; } mLaunchComponent = componentName; mAppSearchData = appSearchData; - // Using globalSearch here is just an optimization, just calling - // isDefaultSearchable() should always give the same result. - mGlobalSearchMode = globalSearch || searchManager.isDefaultSearchable(mSearchable); mActivityContext = mSearchable.getActivityContext(getContext()); // show the dialog. this will call onStart(). @@ -377,20 +290,6 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS // of any bad state in the AutoCompleteTextView etc createContentView(); - // The Dialog uses a ContextThemeWrapper for the context; use this to change the - // theme out from underneath us, between the global search theme and the in-app - // search theme. They are identical except that the global search theme does not - // dim the background of the window (because global search is full screen so it's - // not needed and this should save a little bit of time on global search invocation). - Object context = getContext(); - if (context instanceof ContextThemeWrapper) { - ContextThemeWrapper wrapper = (ContextThemeWrapper) context; - if (globalSearch) { - wrapper.setTheme(com.android.internal.R.style.Theme_GlobalSearchBar); - } else { - wrapper.setTheme(com.android.internal.R.style.Theme_SearchBar); - } - } show(); } updateUI(); @@ -416,7 +315,6 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS mSearchable = null; mActivityContext = null; mUserQuery = null; - mPreviousComponents = null; } /** @@ -430,7 +328,7 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS mWorkingSpinner.setVisible(working, false); mWorkingSpinner.invalidateSelf(); } - + /** * Closes and gets rid of the suggestions adapter. */ @@ -444,7 +342,7 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS } mSuggestionsAdapter = null; } - + /** * Save the minimal set of data necessary to recreate the search * @@ -460,10 +358,6 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS // setup info so I can recreate this particular search bundle.putParcelable(INSTANCE_KEY_COMPONENT, mLaunchComponent); bundle.putBundle(INSTANCE_KEY_APPDATA, mAppSearchData); - bundle.putBoolean(INSTANCE_KEY_GLOBALSEARCH, mGlobalSearchMode); - bundle.putParcelable(INSTANCE_KEY_STORED_COMPONENT, mStoredComponentName); - bundle.putBundle(INSTANCE_KEY_STORED_APPDATA, mStoredAppSearchData); - bundle.putParcelableArrayList(INSTANCE_KEY_PREVIOUS_COMPONENTS, mPreviousComponents); bundle.putString(INSTANCE_KEY_USER_QUERY, mUserQuery); return bundle; @@ -483,22 +377,10 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS ComponentName launchComponent = savedInstanceState.getParcelable(INSTANCE_KEY_COMPONENT); Bundle appSearchData = savedInstanceState.getBundle(INSTANCE_KEY_APPDATA); - boolean globalSearch = savedInstanceState.getBoolean(INSTANCE_KEY_GLOBALSEARCH); - ComponentName storedComponentName = - savedInstanceState.getParcelable(INSTANCE_KEY_STORED_COMPONENT); - Bundle storedAppSearchData = - savedInstanceState.getBundle(INSTANCE_KEY_STORED_APPDATA); - ArrayList<ComponentName> previousComponents = - savedInstanceState.getParcelableArrayList(INSTANCE_KEY_PREVIOUS_COMPONENTS); String userQuery = savedInstanceState.getString(INSTANCE_KEY_USER_QUERY); - // Set stored state - mStoredComponentName = storedComponentName; - mStoredAppSearchData = storedAppSearchData; - mPreviousComponents = previousComponents; - // show the dialog. - if (!doShow(userQuery, false, launchComponent, appSearchData, globalSearch)) { + if (!doShow(userQuery, false, launchComponent, appSearchData)) { // for some reason, we couldn't re-instantiate return; } @@ -508,7 +390,7 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS * Called after resources have changed, e.g. after screen rotation or locale change. */ public void onConfigurationChanged() { - if (isShowing()) { + if (mSearchable != null && isShowing()) { // Redraw (resources may have changed) updateSearchButton(); updateSearchAppIcon(); @@ -573,19 +455,13 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS // we dismiss the entire dialog instead mSearchAutoComplete.setDropDownDismissedOnCompletion(false); - if (!isInRealAppSearch()) { - mSearchAutoComplete.setDropDownAlwaysVisible(true); // fill space until results come in - } else { - mSearchAutoComplete.setDropDownAlwaysVisible(false); - } - mSearchAutoComplete.setForceIgnoreOutsideTouch(true); // attach the suggestions adapter, if suggestions are available // The existence of a suggestions authority is the proxy for "suggestions available here" if (mSearchable.getSuggestAuthority() != null) { mSuggestionsAdapter = new SuggestionsAdapter(getContext(), this, mSearchable, - mOutsideDrawablesCache, mGlobalSearchMode); + mOutsideDrawablesCache); mSearchAutoComplete.setAdapter(mSuggestionsAdapter); } } @@ -613,7 +489,7 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS // global search, for extra space for url entry. // // TODO: Remove this special case once the issue has been reconciled in Eclair. - if (mGlobalSearchMode || isBrowserSearch()) { + if (isBrowserSearch()) { mAppIcon.setImageResource(0); mAppIcon.setVisibility(View.GONE); mSearchPlate.setPadding(SEARCH_PLATE_LEFT_PADDING_GLOBAL, @@ -707,15 +583,13 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS /** * Hack to determine whether this is the browser, so we can remove the browser icon - * to the left of the search field, as a special requirement for Donut. - * - * TODO: For Eclair, reconcile this with the rest of the global search UI. + * to the left of the search field. */ private boolean isBrowserSearch() { return mLaunchComponent.flattenToShortString().startsWith("com.android.browser/"); } - /** + /* * Listeners of various types */ @@ -761,12 +635,6 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS return false; } - if (keyCode == KeyEvent.KEYCODE_SEARCH && event.getRepeatCount() == 0) { - event.startTracking(); - // Consume search key for later use. - return true; - } - // if it's an action specified by the searchable activity, launch the // entered query with the action key SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode); @@ -777,25 +645,7 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS return super.onKeyDown(keyCode, event); } - - @Override - public boolean onKeyUp(int keyCode, KeyEvent event) { - if (DBG) Log.d(LOG_TAG, "onKeyUp(" + keyCode + "," + event + ")"); - if (mSearchable == null) { - return false; - } - if (keyCode == KeyEvent.KEYCODE_SEARCH && event.isTracking() - && !event.isCanceled()) { - // If the search key is pressed, toggle between global and in-app search. If we are - // currently doing global search and there is no in-app search context to toggle to, - // just don't do anything. - return toggleGlobalSearch(); - } - - return super.onKeyUp(keyCode, event); - } - /** * Callback to watch the textedit field for empty/non-empty */ @@ -825,12 +675,8 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS if (mSearchable.autoUrlDetect() && !mSearchAutoComplete.isPerformingCompletion()) { // The user changed the query, check if it is a URL and if so change the search // button in the soft keyboard to the 'Go' button. - int options = (mSearchAutoComplete.getImeOptions() & (~EditorInfo.IME_MASK_ACTION)); - if (Patterns.WEB_URL.matcher(mUserQuery).matches()) { - options = options | EditorInfo.IME_ACTION_GO; - } else { - options = options | EditorInfo.IME_ACTION_SEARCH; - } + int options = (mSearchAutoComplete.getImeOptions() & (~EditorInfo.IME_MASK_ACTION)) + | EditorInfo.IME_ACTION_GO; if (options != mSearchAutoCompleteImeOptions) { mSearchAutoCompleteImeOptions = options; mSearchAutoComplete.setImeOptions(options); @@ -907,7 +753,8 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS if (searchable.getVoiceSearchLaunchWebSearch()) { getContext().startActivity(mVoiceWebSearchIntent); } else if (searchable.getVoiceSearchLaunchRecognizer()) { - Intent appSearchIntent = createVoiceAppSearchIntent(mVoiceAppSearchIntent); + Intent appSearchIntent = createVoiceAppSearchIntent(mVoiceAppSearchIntent, + searchable); getContext().startActivity(appSearchIntent); } } catch (ActivityNotFoundException e) { @@ -925,8 +772,8 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS * @param baseIntent The voice app search intent to start from * @return A completely-configured intent ready to send to the voice search activity */ - private Intent createVoiceAppSearchIntent(Intent baseIntent) { - ComponentName searchActivity = mSearchable.getSearchActivity(); + private Intent createVoiceAppSearchIntent(Intent baseIntent, SearchableInfo searchable) { + ComponentName searchActivity = searchable.getSearchActivity(); // create the necessary intent to set up a search-and-forward operation // in the voice search system. We have to keep the bundle separate, @@ -956,23 +803,23 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS String language = null; int maxResults = 1; Resources resources = mActivityContext.getResources(); - if (mSearchable.getVoiceLanguageModeId() != 0) { - languageModel = resources.getString(mSearchable.getVoiceLanguageModeId()); + if (searchable.getVoiceLanguageModeId() != 0) { + languageModel = resources.getString(searchable.getVoiceLanguageModeId()); } - if (mSearchable.getVoicePromptTextId() != 0) { - prompt = resources.getString(mSearchable.getVoicePromptTextId()); + if (searchable.getVoicePromptTextId() != 0) { + prompt = resources.getString(searchable.getVoicePromptTextId()); } - if (mSearchable.getVoiceLanguageId() != 0) { - language = resources.getString(mSearchable.getVoiceLanguageId()); + if (searchable.getVoiceLanguageId() != 0) { + language = resources.getString(searchable.getVoiceLanguageId()); } - if (mSearchable.getVoiceMaxResults() != 0) { - maxResults = mSearchable.getVoiceMaxResults(); + if (searchable.getVoiceMaxResults() != 0) { + maxResults = searchable.getVoiceMaxResults(); } voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, languageModel); voiceIntent.putExtra(RecognizerIntent.EXTRA_PROMPT, prompt); voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, language); voiceIntent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, maxResults); - voiceIntent.putExtra(EXTRA_CALLING_PACKAGE, + voiceIntent.putExtra(Recognition.EXTRA_CALLING_PACKAGE, searchActivity == null ? null : searchActivity.toShortString()); // Add the values that configure forwarding the results @@ -1166,14 +1013,9 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS */ protected void launchQuerySearch(int actionKey, String actionMsg) { String query = mSearchAutoComplete.getText().toString(); - String action = mGlobalSearchMode ? Intent.ACTION_WEB_SEARCH : Intent.ACTION_SEARCH; + String action = Intent.ACTION_SEARCH; Intent intent = createIntent(action, null, null, query, null, - actionKey, actionMsg, null); - // Allow GlobalSearch to log and create shortcut for searches launched by - // the search button, enter key or an action key. - if (mGlobalSearchMode) { - mSuggestionsAdapter.reportSearch(query); - } + actionKey, actionMsg); launchIntent(intent); } @@ -1186,7 +1028,7 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS protected boolean launchSuggestion(int position) { return launchSuggestion(position, KeyEvent.KEYCODE_UNKNOWN, null); } - + /** * Launches an intent based on a suggestion. * @@ -1203,20 +1045,7 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS Intent intent = createIntentFromSuggestion(c, actionKey, actionMsg); - // report back about the click - if (mGlobalSearchMode) { - // in global search mode, do it via cursor - mSuggestionsAdapter.callCursorOnClick(c, position, actionKey, actionMsg); - } else if (intent != null - && mPreviousComponents != null - && !mPreviousComponents.isEmpty()) { - // in-app search (and we have pivoted in as told by mPreviousComponents, - // which is used for keeping track of what we pop back to when we are pivoting into - // in app search.) - reportInAppClickToGlobalSearch(c, intent); - } - - // launch the intent + // launch the intent launchIntent(intent); return true; @@ -1225,163 +1054,28 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS } /** - * Report a click from an in app search result back to global search for shortcutting porpoises. - * - * @param c The cursor that is pointing to the clicked position. - * @param intent The intent that will be launched for the click. - */ - private void reportInAppClickToGlobalSearch(Cursor c, Intent intent) { - // for in app search, still tell global search via content provider - Uri uri = getClickReportingUri(); - final ContentValues cv = new ContentValues(); - cv.put(SearchManager.SEARCH_CLICK_REPORT_COLUMN_QUERY, mUserQuery); - final ComponentName source = mSearchable.getSearchActivity(); - cv.put(SearchManager.SEARCH_CLICK_REPORT_COLUMN_COMPONENT, source.flattenToShortString()); - - // grab the intent columns from the intent we created since it has additional - // logic for falling back on the searchable default - cv.put(SearchManager.SUGGEST_COLUMN_INTENT_ACTION, intent.getAction()); - cv.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA, intent.getDataString()); - cv.put(SearchManager.SUGGEST_COLUMN_INTENT_COMPONENT_NAME, - intent.getComponent().flattenToShortString()); - - // ensure the icons will work for global search - cv.put(SearchManager.SUGGEST_COLUMN_ICON_1, - wrapIconForPackage( - mSearchable.getSuggestPackage(), - getColumnString(c, SearchManager.SUGGEST_COLUMN_ICON_1))); - cv.put(SearchManager.SUGGEST_COLUMN_ICON_2, - wrapIconForPackage( - mSearchable.getSuggestPackage(), - getColumnString(c, SearchManager.SUGGEST_COLUMN_ICON_2))); - - // the rest can be passed through directly - cv.put(SearchManager.SUGGEST_COLUMN_FORMAT, - getColumnString(c, SearchManager.SUGGEST_COLUMN_FORMAT)); - cv.put(SearchManager.SUGGEST_COLUMN_TEXT_1, - getColumnString(c, SearchManager.SUGGEST_COLUMN_TEXT_1)); - cv.put(SearchManager.SUGGEST_COLUMN_TEXT_2, - getColumnString(c, SearchManager.SUGGEST_COLUMN_TEXT_2)); - cv.put(SearchManager.SUGGEST_COLUMN_QUERY, - getColumnString(c, SearchManager.SUGGEST_COLUMN_QUERY)); - cv.put(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID, - getColumnString(c, SearchManager.SUGGEST_COLUMN_SHORTCUT_ID)); - // note: deliberately omitting background color since it is only for global search - // "more results" entries - mContext.getContentResolver().insert(uri, cv); - } - - /** - * @return A URI appropriate for reporting a click. - */ - private Uri getClickReportingUri() { - Uri.Builder uriBuilder = new Uri.Builder() - .scheme(ContentResolver.SCHEME_CONTENT) - .authority(SearchManager.SEARCH_CLICK_REPORT_AUTHORITY); - - uriBuilder.appendPath(SearchManager.SEARCH_CLICK_REPORT_URI_PATH); - - return uriBuilder - .query("") // TODO: Remove, workaround for a bug in Uri.writeToParcel() - .fragment("") // TODO: Remove, workaround for a bug in Uri.writeToParcel() - .build(); - } - - /** - * Wraps an icon for a particular package. If the icon is a resource id, it is converted into - * an android.resource:// URI. - * - * @param packageName The source of the icon - * @param icon The icon retrieved from a suggestion column - * @return An icon string appropriate for the package. - */ - private String wrapIconForPackage(String packageName, String icon) { - if (icon == null || icon.length() == 0 || "0".equals(icon)) { - // SearchManager specifies that null or zero can be returned to indicate - // no icon. We also allow empty string. - return null; - } else if (!Character.isDigit(icon.charAt(0))){ - return icon; - } else { - return new Uri.Builder() - .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) - .authority(packageName) - .encodedPath(icon) - .toString(); - } - } - - /** * Launches an intent, including any special intent handling. */ private void launchIntent(Intent intent) { if (intent == null) { return; } - if (handleSpecialIntent(intent)){ - return; - } Log.d(LOG_TAG, "launching " + intent); try { - // in global search mode, we send the activity straight to the original suggestion - // source. this is because GlobalSearch may not have permission to launch the - // intent, and to avoid the extra step of going through GlobalSearch. - if (mGlobalSearchMode) { - launchGlobalSearchIntent(intent); - if (mStoredComponentName != null) { - // If we're embedded in an application, dismiss the dialog. - // This ensures that if the intent is handled by the current - // activity, it's not obscured by the dialog. - dismiss(); - } - } else { - // If the intent was created from a suggestion, it will always have an explicit - // component here. - Log.i(LOG_TAG, "Starting (as ourselves) " + intent.toURI()); - getContext().startActivity(intent); - // If the search switches to a different activity, - // SearchDialogWrapper#performActivityResuming - // will handle hiding the dialog when the next activity starts, but for - // real in-app search, we still need to dismiss the dialog. - if (isInRealAppSearch()) { - dismiss(); - } - } + // If the intent was created from a suggestion, it will always have an explicit + // component here. + Log.i(LOG_TAG, "Starting (as ourselves) " + intent.toURI()); + getContext().startActivity(intent); + // If the search switches to a different activity, + // SearchDialogWrapper#performActivityResuming + // will handle hiding the dialog when the next activity starts, but for + // real in-app search, we still need to dismiss the dialog. + dismiss(); } catch (RuntimeException ex) { Log.e(LOG_TAG, "Failed launch activity: " + intent, ex); } } - private void launchGlobalSearchIntent(Intent intent) { - final String packageName; - // GlobalSearch puts the original source of the suggestion in the - // 'component name' column. If set, we send the intent to that activity. - // We trust GlobalSearch to always set this to the suggestion source. - String intentComponent = intent.getStringExtra(SearchManager.COMPONENT_NAME_KEY); - if (intentComponent != null) { - ComponentName componentName = ComponentName.unflattenFromString(intentComponent); - intent.setComponent(componentName); - intent.removeExtra(SearchManager.COMPONENT_NAME_KEY); - // Launch the intent as the suggestion source. - // This prevents sources from using the search dialog to launch - // intents that they don't have permission for themselves. - packageName = componentName.getPackageName(); - } else { - // If there is no component in the suggestion, it must be a built-in suggestion - // from GlobalSearch (e.g. "Search the web for") or the intent - // launched when pressing the search/go button in the search dialog. - // Launch the intent with the permissions of GlobalSearch. - packageName = mSearchable.getSearchActivity().getPackageName(); - } - - // Launch all global search suggestions as new tasks, since they don't relate - // to the current task. - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - setBrowserApplicationId(intent); - - startActivityInPackage(intent, packageName); - } - /** * If the intent is to open an HTTP or HTTPS URL, we set * {@link Browser#EXTRA_APPLICATION_ID} so that any existing browser window that @@ -1398,106 +1092,6 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS } /** - * Starts an activity as if it had been started by the given package. - * - * @param intent The description of the activity to start. - * @param packageName - * @throws ActivityNotFoundException If the intent could not be resolved to - * and existing activity. - * @throws SecurityException If the package does not have permission to start - * start the activity. - * @throws AndroidRuntimeException If some other error occurs. - */ - private void startActivityInPackage(Intent intent, String packageName) { - try { - int uid = ActivityThread.getPackageManager().getPackageUid(packageName); - if (uid < 0) { - throw new AndroidRuntimeException("Package UID not found " + packageName); - } - String resolvedType = intent.resolveTypeIfNeeded(getContext().getContentResolver()); - IBinder resultTo = null; - String resultWho = null; - int requestCode = -1; - boolean onlyIfNeeded = false; - Log.i(LOG_TAG, "Starting (uid " + uid + ", " + packageName + ") " + intent.toURI()); - int result = ActivityManagerNative.getDefault().startActivityInPackage( - uid, intent, resolvedType, resultTo, resultWho, requestCode, onlyIfNeeded); - checkStartActivityResult(result, intent); - } catch (RemoteException ex) { - throw new AndroidRuntimeException(ex); - } - } - - // Stolen from Instrumentation.checkStartActivityResult() - private static void checkStartActivityResult(int res, Intent intent) { - if (res >= IActivityManager.START_SUCCESS) { - return; - } - switch (res) { - case IActivityManager.START_INTENT_NOT_RESOLVED: - case IActivityManager.START_CLASS_NOT_FOUND: - if (intent.getComponent() != null) - throw new ActivityNotFoundException( - "Unable to find explicit activity class " - + intent.getComponent().toShortString() - + "; have you declared this activity in your AndroidManifest.xml?"); - throw new ActivityNotFoundException( - "No Activity found to handle " + intent); - case IActivityManager.START_PERMISSION_DENIED: - throw new SecurityException("Not allowed to start activity " - + intent); - case IActivityManager.START_FORWARD_AND_REQUEST_CONFLICT: - throw new AndroidRuntimeException( - "FORWARD_RESULT_FLAG used while also requesting a result"); - default: - throw new AndroidRuntimeException("Unknown error code " - + res + " when starting " + intent); - } - } - - /** - * Handles the special intent actions declared in {@link SearchManager}. - * - * @return <code>true</code> if the intent was handled. - */ - private boolean handleSpecialIntent(Intent intent) { - String action = intent.getAction(); - if (SearchManager.INTENT_ACTION_CHANGE_SEARCH_SOURCE.equals(action)) { - handleChangeSourceIntent(intent); - return true; - } - return false; - } - - /** - * Handles {@link SearchManager#INTENT_ACTION_CHANGE_SEARCH_SOURCE}. - */ - private void handleChangeSourceIntent(Intent intent) { - Uri dataUri = intent.getData(); - if (dataUri == null) { - Log.w(LOG_TAG, "SearchManager.INTENT_ACTION_CHANGE_SOURCE without intent data."); - return; - } - ComponentName componentName = ComponentName.unflattenFromString(dataUri.toString()); - if (componentName == null) { - Log.w(LOG_TAG, "Invalid ComponentName: " + dataUri); - return; - } - if (DBG) Log.d(LOG_TAG, "Switching to " + componentName); - - pushPreviousComponent(mLaunchComponent); - if (!show(componentName, mAppSearchData, false)) { - Log.w(LOG_TAG, "Failed to switch to source " + componentName); - popPreviousComponent(); - return; - } - - String query = intent.getStringExtra(SearchManager.QUERY); - setUserQuery(query); - mSearchAutoComplete.showDropDown(); - } - - /** * Sets the list item selection in the AutoCompleteTextView's ListView. */ public void setListSelection(int index) { @@ -1505,61 +1099,6 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS } /** - * Checks if there are any previous searchable components in the history stack. - */ - private boolean hasPreviousComponent() { - return mPreviousComponents != null && !mPreviousComponents.isEmpty(); - } - - /** - * Saves the previous component that was searched, so that we can go - * back to it. - */ - private void pushPreviousComponent(ComponentName componentName) { - if (mPreviousComponents == null) { - mPreviousComponents = new ArrayList<ComponentName>(); - } - mPreviousComponents.add(componentName); - } - - /** - * Pops the previous component off the stack and returns it. - * - * @return The component name, or <code>null</code> if there was - * no previous component. - */ - private ComponentName popPreviousComponent() { - if (!hasPreviousComponent()) { - return null; - } - return mPreviousComponents.remove(mPreviousComponents.size() - 1); - } - - /** - * Goes back to the previous component that was searched, if any. - * - * @return <code>true</code> if there was a previous component that we could go back to. - */ - private boolean backToPreviousComponent() { - ComponentName previous = popPreviousComponent(); - if (previous == null) { - return false; - } - - if (!show(previous, mAppSearchData, false)) { - Log.w(LOG_TAG, "Failed to switch to source " + previous); - return false; - } - - // must touch text to trigger suggestions - // TODO: should this be the text as it was when the user left - // the source that we are now going back to? - String query = mSearchAutoComplete.getText().toString(); - setUserQuery(query); - return true; - } - - /** * When a particular suggestion has been selected, perform the various lookups required * to use the suggestion. This includes checking the cursor for suggestion-specific data, * and/or falling back to the XML for defaults; It also creates REST style Uri data when @@ -1608,10 +1147,9 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS String query = getColumnString(c, SearchManager.SUGGEST_COLUMN_QUERY); String extraData = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA); - String mode = mGlobalSearchMode ? SearchManager.MODE_GLOBAL_SEARCH_SUGGESTION : null; return createIntent(action, dataUri, extraData, query, componentName, actionKey, - actionMsg, mode); + actionMsg); } catch (RuntimeException e ) { int rowNum; try { // be really paranoid now @@ -1642,16 +1180,13 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS * @return The intent. */ private Intent createIntent(String action, Uri data, String extraData, String query, - String componentName, int actionKey, String actionMsg, String mode) { + String componentName, int actionKey, String actionMsg) { // Now build the Intent Intent intent = new Intent(action); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); // We need CLEAR_TOP to avoid reusing an old task that has other activities // on top of the one we want. We don't want to do this in in-app search though, // as it can be destructive to the activity stack. - if (mGlobalSearchMode) { - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - } if (data != null) { intent.setData(data); } @@ -1662,9 +1197,6 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS if (extraData != null) { intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData); } - if (componentName != null) { - intent.putExtra(SearchManager.COMPONENT_NAME_KEY, componentName); - } if (mAppSearchData != null) { intent.putExtra(SearchManager.APP_DATA, mAppSearchData); } @@ -1672,16 +1204,10 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS intent.putExtra(SearchManager.ACTION_KEY, actionKey); intent.putExtra(SearchManager.ACTION_MSG, actionMsg); } - if (mode != null) { - intent.putExtra(SearchManager.SEARCH_MODE, mode); - } - // Only allow 3rd-party intents from GlobalSearch - if (!mGlobalSearchMode) { - intent.setComponent(mSearchable.getSearchActivity()); - } + intent.setComponent(mSearchable.getSearchActivity()); return intent; } - + /** * For a given suggestion and a given cursor row, get the action message. If not provided * by the specific row/column, also check for a single definition (for the action key). @@ -1838,12 +1364,8 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS imm.hideSoftInputFromWindow(getWindow().getDecorView().getWindowToken(), 0)) { return; } - // Otherwise, go back to any previous source (e.g. back to QSB when - // pivoted into a source. - if (!backToPreviousComponent()) { - // If no previous source, close search dialog - cancel(); - } + // Close search dialog + cancel(); } /** diff --git a/core/java/android/app/SearchManager.java b/core/java/android/app/SearchManager.java index 3046a2c..ce5f1bf 100644 --- a/core/java/android/app/SearchManager.java +++ b/core/java/android/app/SearchManager.java @@ -350,7 +350,7 @@ import java.util.List; * <p><b>Configuring your Content Provider to Receive Suggestion Queries.</b> The basic job of * a search suggestions {@link android.content.ContentProvider Content Provider} is to provide * "live" (while-you-type) conversion of the user's query text into a set of zero or more - * suggestions. Each application is free to define the conversion, and as described above there are + * suggestions. Each application is free to define the conversion, and as described above there are * many possible solutions. This section simply defines how to communicate with the suggestion * provider. * @@ -360,7 +360,8 @@ import java.util.List; * * <p>Every query includes a Uri, and the Search Manager will format the Uri as shown: * <p><pre class="prettyprint"> - * content:// your.suggest.authority / your.suggest.path / SearchManager.SUGGEST_URI_PATH_QUERY</pre> + * content:// your.suggest.authority / your.suggest.path / SearchManager.SUGGEST_URI_PATH_QUERY + * </pre> * * <p>Your Content Provider can receive the query text in one of two ways. * <ul> @@ -379,7 +380,7 @@ import java.util.List; * <p><b>Providing access to Content Providers that require permissions.</b> If your content * provider declares an android:readPermission in your application's manifest, you must provide * access to the search infrastructure to the search suggestion path by including a path-permission - * that grants android:readPermission access to "android.permission.GLOBAL_SEARCH". Granting access + * that grants android:readPermission access to "android.permission.GLOBAL_SEARCH". Granting access * explicitly to the search infrastructure ensures it will be able to access the search suggestions * without needing to know ahead of time any other details of the permissions protecting your * provider. Content providers that require no permissions are already available to the search @@ -546,7 +547,8 @@ import java.util.List; * taken directly to a specific result. * <ul> * <li><b>Action:</b> {@link android.content.Intent#ACTION_VIEW ACTION_VIEW}</li> - * <li><b>Data:</b> a complete Uri, supplied by the cursor, that identifies the desired data.</li> + * <li><b>Data:</b> a complete Uri, supplied by the cursor, that identifies the desired data. + * </li> * <li><b>Query:</b> query text supplied with the suggestion (probably ignored)</li> * </ul> * </li> @@ -570,7 +572,7 @@ import java.util.List; * * <p><b>Suggestion Rewriting.</b> If the user navigates through the suggestions list, the UI * may temporarily rewrite the user's query with a query that matches the currently selected - * suggestion. This enables the user to see what query is being suggested, and also allows the user + * suggestion. This enables the user to see what query is being suggested, and also allows the user * to click or touch in the entry EditText element and make further edits to the query before * dispatching it. In order to perform this correctly, the Search UI needs to know exactly what * text to rewrite the query with. @@ -1279,16 +1281,6 @@ public class SearchManager public final static String APP_DATA = "app_data"; /** - * Intent app_data bundle key: Use this key with the bundle from - * {@link android.content.Intent#getBundleExtra - * content.Intent.getBundleExtra(APP_DATA)} to obtain the source identifier - * set by the activity that launched the search. - * - * @hide - */ - public final static String SOURCE = "source"; - - /** * Intent extra data key: Use {@link android.content.Intent#getBundleExtra * content.Intent.getBundleExtra(SEARCH_MODE)} to get the search mode used * to launch the intent. @@ -1299,15 +1291,6 @@ public class SearchManager public final static String SEARCH_MODE = "search_mode"; /** - * Value for the {@link #SEARCH_MODE} key. - * This is used if the intent was launched by clicking a suggestion in global search - * mode (Quick Search Box). - * - * @hide - */ - public static final String MODE_GLOBAL_SEARCH_SUGGESTION = "global_search_suggestion"; - - /** * Intent extra data key: Use this key with Intent.ACTION_SEARCH and * {@link android.content.Intent#getIntExtra content.Intent.getIntExtra()} * to obtain the keycode that the user used to trigger this query. It will be zero if the @@ -1318,14 +1301,6 @@ public class SearchManager public final static String ACTION_KEY = "action_key"; /** - * Intent component name key: This key will be used for the extra populated by the - * {@link #SUGGEST_COLUMN_INTENT_COMPONENT_NAME} column. - * - * {@hide} - */ - public final static String COMPONENT_NAME_KEY = "intent_component_name_key"; - - /** * Intent extra data key: This key will be used for the extra populated by the * {@link #SUGGEST_COLUMN_INTENT_EXTRA_DATA} column. */ @@ -1339,58 +1314,6 @@ public class SearchManager public final static String EXTRA_SELECT_QUERY = "select_query"; /** - * Defines the constants used in the communication between {@link android.app.SearchDialog} and - * the global search provider via {@link Cursor#respond(android.os.Bundle)}. - * - * @hide - */ - public static class DialogCursorProtocol { - - /** - * The sent bundle will contain this integer key, with a value set to one of the events - * below. - */ - public final static String METHOD = "DialogCursorProtocol.method"; - - /** - * After data has been refreshed. - */ - public final static int POST_REFRESH = 0; - public final static String POST_REFRESH_RECEIVE_ISPENDING - = "DialogCursorProtocol.POST_REFRESH.isPending"; - public final static String POST_REFRESH_RECEIVE_DISPLAY_NOTIFY - = "DialogCursorProtocol.POST_REFRESH.displayNotify"; - - /** - * When a position has been clicked. - */ - public final static int CLICK = 2; - public final static String CLICK_SEND_POSITION - = "DialogCursorProtocol.CLICK.sendPosition"; - public final static String CLICK_SEND_MAX_DISPLAY_POS - = "DialogCursorProtocol.CLICK.sendDisplayPosition"; - public final static String CLICK_SEND_ACTION_KEY - = "DialogCursorProtocol.CLICK.sendActionKey"; - public final static String CLICK_SEND_ACTION_MSG - = "DialogCursorProtocol.CLICK.sendActionMsg"; - public final static String CLICK_RECEIVE_SELECTED_POS - = "DialogCursorProtocol.CLICK.receiveSelectedPosition"; - - /** - * When the threshold received in {@link #POST_REFRESH_RECEIVE_DISPLAY_NOTIFY} is displayed. - */ - public final static int THRESH_HIT = 3; - - /** - * When a search is started without using a suggestion. - */ - public final static int SEARCH = 4; - public final static String SEARCH_SEND_MAX_DISPLAY_POS - = "DialogCursorProtocol.SEARCH.sendDisplayPosition"; - public final static String SEARCH_SEND_QUERY = "DialogCursorProtocol.SEARCH.query"; - } - - /** * Intent extra data key: Use this key with Intent.ACTION_SEARCH and * {@link android.content.Intent#getStringExtra content.Intent.getStringExtra()} * to obtain the action message that was defined for a particular search action key and/or @@ -1431,40 +1354,6 @@ public class SearchManager public final static String SHORTCUT_MIME_TYPE = "vnd.android.cursor.item/vnd.android.search.suggest"; - - /** - * The authority of the provider to report clicks to when a click is detected after pivoting - * into a specific app's search from global search. - * - * In addition to the columns below, the suggestion columns are used to pass along the full - * suggestion so it can be shortcutted. - * - * @hide - */ - public final static String SEARCH_CLICK_REPORT_AUTHORITY = - "com.android.globalsearch.stats"; - - /** - * The path the write goes to. - * - * @hide - */ - public final static String SEARCH_CLICK_REPORT_URI_PATH = "click"; - - /** - * The column storing the query for the click. - * - * @hide - */ - public final static String SEARCH_CLICK_REPORT_COLUMN_QUERY = "query"; - - /** - * The column storing the component name of the application that was pivoted into. - * - * @hide - */ - public final static String SEARCH_CLICK_REPORT_COLUMN_COMPONENT = "component"; - /** * Column name for suggestions cursor. <i>Unused - can be null or column can be omitted.</i> */ @@ -1541,10 +1430,7 @@ public class SearchManager */ public final static String SUGGEST_COLUMN_INTENT_EXTRA_DATA = "suggest_intent_extra_data"; /** - * Column name for suggestions cursor. <i>Optional.</i> This column allows suggestions - * to provide additional arbitrary data which will be included as an extra under the key - * {@link #COMPONENT_NAME_KEY}. For use by the global search system only - if other providers - * attempt to use this column, the value will be overwritten by global search. + * TODO: Remove * * @hide */ @@ -1604,27 +1490,6 @@ public class SearchManager public final static String SUGGEST_PARAMETER_LIMIT = "limit"; /** - * If a suggestion has this value in {@link #SUGGEST_COLUMN_INTENT_ACTION}, - * the search dialog will switch to a different suggestion source when the - * suggestion is clicked. - * - * {@link #SUGGEST_COLUMN_INTENT_DATA} must contain - * the flattened {@link ComponentName} of the activity which is to be searched. - * - * TODO: Should {@link #SUGGEST_COLUMN_INTENT_DATA} instead contain a URI in the format - * used by {@link android.provider.Applications}? - * - * TODO: This intent should be protected by the same permission that we use - * for replacing the global search provider. - * - * The query text field will be set to the value of {@link #SUGGEST_COLUMN_QUERY}. - * - * @hide Pending API council approval. - */ - public final static String INTENT_ACTION_CHANGE_SEARCH_SOURCE - = "android.search.action.CHANGE_SEARCH_SOURCE"; - - /** * Intent action for starting the global search activity. * The global search provider should handle this intent. * @@ -1638,8 +1503,6 @@ public class SearchManager /** * Intent action for starting the global search settings activity. * The global search provider should handle this intent. - * - * @hide Pending API council approval. */ public final static String INTENT_ACTION_SEARCH_SETTINGS = "android.search.action.SEARCH_SETTINGS"; @@ -1675,7 +1538,7 @@ public class SearchManager * @hide */ public final static String INTENT_ACTION_NONE = "android.search.action.ZILCH"; - + /** * Reference to the shared system search service. */ @@ -1684,13 +1547,6 @@ public class SearchManager private final Context mContext; /** - * compact representation of the activity associated with this search manager so - * we can say who we are when starting search. the search managerservice, in turn, - * uses this to properly handle the back stack. - */ - private int mIdent; - - /** * The package associated with this seach manager. */ private String mAssociatedPackage; @@ -1708,21 +1564,6 @@ public class SearchManager mService = ISearchManager.Stub.asInterface( ServiceManager.getService(Context.SEARCH_SERVICE)); } - - /*package*/ boolean hasIdent() { - return mIdent != 0; - } - - /*package*/ void setIdent(int ident, ComponentName component) { - if (mIdent != 0) { - throw new IllegalStateException("mIdent already set"); - } - if (component == null) { - throw new IllegalArgumentException("component must be non-null"); - } - mIdent = ident; - mAssociatedPackage = component.getPackageName(); - } /** * Launch search UI. @@ -1769,15 +1610,14 @@ public class SearchManager ComponentName launchActivity, Bundle appSearchData, boolean globalSearch) { - ensureSearchDialog(); - if (globalSearch) { startGlobalSearch(initialQuery, selectInitialQuery, appSearchData); return; } - mSearchDialog.show(initialQuery, selectInitialQuery, launchActivity, appSearchData, - globalSearch); + ensureSearchDialog(); + + mSearchDialog.show(initialQuery, selectInitialQuery, launchActivity, appSearchData); } private void ensureSearchDialog() { @@ -1850,6 +1690,25 @@ public class SearchManager } /** + * Gets the name of the web search activity. + * + * @return The name of the default activity for web searches. This activity + * can be used to get web search suggestions. Returns {@code null} if + * there is no default web search activity. + * + * @hide + */ + public ComponentName getWebSearchActivity() { + ComponentName globalSearch = getGlobalSearchActivity(); + if (globalSearch == null) { + return null; + } + Intent intent = new Intent(Intent.ACTION_WEB_SEARCH); + intent.setPackage(globalSearch.getPackageName()); + return intent.resolveActivity(mContext.getPackageManager()); + } + + /** * Similar to {@link #startSearch} but actually fires off the search query after invoking * the search dialog. Made available for testing purposes. * @@ -2006,17 +1865,6 @@ public class SearchManager return null; } } - - /** - * Checks whether the given searchable is the default searchable. - * - * @hide because SearchableInfo is not part of the API. - */ - public boolean isDefaultSearchable(SearchableInfo searchable) { - SearchableInfo defaultSearchable = getSearchableInfo(null, true); - return defaultSearchable != null - && defaultSearchable.getSearchActivity().equals(searchable.getSearchActivity()); - } /** * Gets a cursor with search suggestions. @@ -2103,56 +1951,4 @@ public class SearchManager } } - /** - * Returns a list of the searchable activities that handle web searches. - * - * @return a list of all searchable activities that handle - * {@link android.content.Intent#ACTION_WEB_SEARCH}. - * - * @hide because SearchableInfo is not part of the API. - */ - public List<SearchableInfo> getSearchablesForWebSearch() { - try { - return mService.getSearchablesForWebSearch(); - } catch (RemoteException e) { - Log.e(TAG, "getSearchablesForWebSearch() failed: " + e); - return null; - } - } - - /** - * Returns the default searchable activity for web searches. - * - * @return searchable information for the activity handling web searches by default. - * - * @hide because SearchableInfo is not part of the API. - */ - public SearchableInfo getDefaultSearchableForWebSearch() { - try { - return mService.getDefaultSearchableForWebSearch(); - } catch (RemoteException e) { - Log.e(TAG, "getDefaultSearchableForWebSearch() failed: " + e); - return null; - } - } - - /** - * Sets the default searchable activity for web searches. - * - * @param component Name of the component to set as default activity for web searches. - * - * @hide - */ - public void setDefaultWebSearch(ComponentName component) { - try { - mService.setDefaultWebSearch(component); - } catch (RemoteException e) { - Log.e(TAG, "setDefaultWebSearch() failed: " + e); - } - } - - private static void debug(String msg) { - Thread thread = Thread.currentThread(); - Log.d(TAG, msg + " (" + thread.getName() + "-" + thread.getId() + ")"); - } } diff --git a/core/java/android/app/SearchableInfo.java b/core/java/android/app/SearchableInfo.java index 9897742..4496354 100644 --- a/core/java/android/app/SearchableInfo.java +++ b/core/java/android/app/SearchableInfo.java @@ -73,7 +73,7 @@ public final class SearchableInfo implements Parcelable { private final boolean mIncludeInGlobalSearch; private final boolean mQueryAfterZeroResults; private final boolean mAutoUrlDetect; - private final String mSettingsDescription; + private final int mSettingsDescriptionId; private final String mSuggestAuthority; private final String mSuggestPath; private final String mSuggestSelection; @@ -155,11 +155,11 @@ public final class SearchableInfo implements Parcelable { } /** - * Gets the description to use for this source in system search settings, or null if - * none has been specified. + * Gets the resource ID of the description string to use for this source in system search + * settings, or {@code 0} if none has been specified. */ - public String getSettingsDescription() { - return mSettingsDescription; + public int getSettingsDescriptionId() { + return mSettingsDescriptionId; } /** @@ -311,8 +311,8 @@ public final class SearchableInfo implements Parcelable { mAutoUrlDetect = a.getBoolean( com.android.internal.R.styleable.Searchable_autoUrlDetect, false); - mSettingsDescription = a.getString( - com.android.internal.R.styleable.Searchable_searchSettingsDescription); + mSettingsDescriptionId = a.getResourceId( + com.android.internal.R.styleable.Searchable_searchSettingsDescription, 0); mSuggestAuthority = a.getString( com.android.internal.R.styleable.Searchable_searchSuggestAuthority); mSuggestPath = a.getString( @@ -495,7 +495,7 @@ public final class SearchableInfo implements Parcelable { + ",suggestAuthority=" + searchable.getSuggestAuthority() + ",target=" + searchable.getSearchActivity().getClassName() + ",global=" + searchable.shouldIncludeInGlobalSearch() - + ",settingsDescription=" + searchable.getSettingsDescription() + + ",settingsDescription=" + searchable.getSettingsDescriptionId() + ",threshold=" + searchable.getSuggestThreshold()); } else { Log.d(LOG_TAG, "Checked " + activityInfo.name + ", no searchable meta-data"); @@ -746,7 +746,7 @@ public final class SearchableInfo implements Parcelable { mQueryAfterZeroResults = in.readInt() != 0; mAutoUrlDetect = in.readInt() != 0; - mSettingsDescription = in.readString(); + mSettingsDescriptionId = in.readInt(); mSuggestAuthority = in.readString(); mSuggestPath = in.readString(); mSuggestSelection = in.readString(); @@ -784,7 +784,7 @@ public final class SearchableInfo implements Parcelable { dest.writeInt(mQueryAfterZeroResults ? 1 : 0); dest.writeInt(mAutoUrlDetect ? 1 : 0); - dest.writeString(mSettingsDescription); + dest.writeInt(mSettingsDescriptionId); dest.writeString(mSuggestAuthority); dest.writeString(mSuggestPath); dest.writeString(mSuggestSelection); diff --git a/core/java/android/app/Service.java b/core/java/android/app/Service.java index 8ec5bd4..6767332 100644 --- a/core/java/android/app/Service.java +++ b/core/java/android/app/Service.java @@ -56,6 +56,8 @@ import java.io.PrintWriter; * <li><a href="#ServiceLifecycle">Service Lifecycle</a> * <li><a href="#Permissions">Permissions</a> * <li><a href="#ProcessLifecycle">Process Lifecycle</a> + * <li><a href="#LocalServiceSample">Local Service Sample</a> + * <li><a href="#RemoteMessengerServiceSample">Remote Messenger Service Sample</a> * </ol> * * <a name="ServiceLifecycle"></a> @@ -166,6 +168,64 @@ import java.io.PrintWriter; * (such as an {@link android.app.Activity}) can, of course, increase the * importance of the overall * process beyond just the importance of the service itself. + * + * <a name="LocalServiceSample"></a> + * <h3>Local Service Sample</h3> + * + * <p>One of the most common uses of a Service is as a secondary component + * running alongside other parts of an application, in the same process as + * the rest of the components. All components of an .apk run in the same + * process unless explicitly stated otherwise, so this is a typical situation. + * + * <p>When used in this way, by assuming the + * components are in the same process, you can greatly simplify the interaction + * between them: clients of the service can simply cast the IBinder they + * receive from it to a concrete class published by the service. + * + * <p>An example of this use of a Service is shown here. First is the Service + * itself, publishing a custom class when bound: + * + * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/LocalService.java + * service} + * + * <p>With that done, one can now write client code that directly accesses the + * running service, such as: + * + * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/LocalServiceActivities.java + * bind} + * + * <a name="RemoteMessengerServiceSample"></a> + * <h3>Remote Messenger Service Sample</h3> + * + * <p>If you need to be able to write a Service that can perform complicated + * communication with clients in remote processes (beyond simply the use of + * {@link Context#startService(Intent) Context.startService} to send + * commands to it), then you can use the {@link android.os.Messenger} class + * instead of writing full AIDL files. + * + * <p>An example of a Service that uses Messenger as its client interface + * is shown here. First is the Service itself, publishing a Messenger to + * an internal Handler when bound: + * + * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/MessengerService.java + * service} + * + * <p>If we want to make this service run in a remote process (instead of the + * standard one for its .apk), we can use <code>android:process</code> in its + * manifest tag to specify one: + * + * {@sample development/samples/ApiDemos/AndroidManifest.xml remote_service_declaration} + * + * <p>Note that the name "remote" chosen here is arbitrary, and you can use + * other names if you want additional processes. The ':' prefix appends the + * name to your package's standard process name. + * + * <p>With that done, clients can now bind to the service and send messages + * to it. Note that this allows clients to register with it to receive + * messages back as well: + * + * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/MessengerServiceActivities.java + * bind} */ public abstract class Service extends ContextWrapper implements ComponentCallbacks { private static final String TAG = "Service"; diff --git a/core/java/android/app/SuggestionsAdapter.java b/core/java/android/app/SuggestionsAdapter.java index 173c3e1..57795d1 100644 --- a/core/java/android/app/SuggestionsAdapter.java +++ b/core/java/android/app/SuggestionsAdapter.java @@ -16,7 +16,6 @@ package android.app; -import android.app.SearchManager.DialogCursorProtocol; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; @@ -30,12 +29,10 @@ import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.StateListDrawable; import android.net.Uri; -import android.os.Bundle; import android.text.Html; import android.text.TextUtils; import android.util.Log; import android.util.SparseArray; -import android.view.KeyEvent; import android.view.View; import android.view.ViewGroup; import android.widget.Filter; @@ -65,7 +62,6 @@ class SuggestionsAdapter extends ResourceCursorAdapter { private Context mProviderContext; private WeakHashMap<String, Drawable.ConstantState> mOutsideDrawablesCache; private SparseArray<Drawable.ConstantState> mBackgroundsCache; - private boolean mGlobalSearchMode; private boolean mClosed = false; // Cached column indexes, updated when the cursor changes. @@ -76,29 +72,8 @@ class SuggestionsAdapter extends ResourceCursorAdapter { private int mIconName2Col; private int mBackgroundColorCol; - // The extra used to tell a cursor to close itself. This is a hack, see the description by - // its use later in this file. - private static final String EXTRA_CURSOR_RESPOND_CLOSE_CURSOR = "cursor_respond_close_cursor"; - - // The bundle which contains {EXTRA_CURSOR_RESPOND_CLOSE_CURSOR=true}, just cached once - // so we don't bother recreating it a bunch. - private final Bundle mCursorRespondCloseCursorBundle; - - // This value is stored in SuggestionsAdapter by the SearchDialog to indicate whether - // a particular list item should be selected upon the next call to notifyDataSetChanged. - // This is used to indicate the index of the "More results..." list item so that when - // the data set changes after a click of "More results...", we can correctly tell the - // ListView to scroll to the right line item. It gets reset to NONE every time it - // is consumed. - private int mListItemToSelect = NONE; static final int NONE = -1; - // holds the maximum position that has been displayed to the user - int mMaxDisplayed = NONE; - - // holds the position that, when displayed, should result in notifying the cursor - int mDisplayNotifyPos = NONE; - private final Runnable mStartSpinnerRunnable; private final Runnable mStopSpinnerRunnable; @@ -110,8 +85,7 @@ class SuggestionsAdapter extends ResourceCursorAdapter { public SuggestionsAdapter(Context context, SearchDialog searchDialog, SearchableInfo searchable, - WeakHashMap<String, Drawable.ConstantState> outsideDrawablesCache, - boolean globalSearchMode) { + WeakHashMap<String, Drawable.ConstantState> outsideDrawablesCache) { super(context, com.android.internal.R.layout.search_dropdown_item_icons_2line, null, // no initial cursor @@ -126,7 +100,6 @@ class SuggestionsAdapter extends ResourceCursorAdapter { mOutsideDrawablesCache = outsideDrawablesCache; mBackgroundsCache = new SparseArray<Drawable.ConstantState>(); - mGlobalSearchMode = globalSearchMode; mStartSpinnerRunnable = new Runnable() { public void run() { @@ -140,10 +113,6 @@ class SuggestionsAdapter extends ResourceCursorAdapter { } }; - // Create this once because we'll reuse it a bunch. - mCursorRespondCloseCursorBundle = new Bundle(); - mCursorRespondCloseCursorBundle.putBoolean(EXTRA_CURSOR_RESPOND_CLOSE_CURSOR, true); - // delay 500ms when deleting getFilter().setDelayer(new Filter.Delayer() { @@ -177,27 +146,22 @@ class SuggestionsAdapter extends ResourceCursorAdapter { public Cursor runQueryOnBackgroundThread(CharSequence constraint) { if (DBG) Log.d(LOG_TAG, "runQueryOnBackgroundThread(" + constraint + ")"); String query = (constraint == null) ? "" : constraint.toString(); - if (!mGlobalSearchMode) { - /** - * for in app search we show the progress spinner until the cursor is returned with - * the results. for global search we manage the progress bar using - * {@link DialogCursorProtocol#POST_REFRESH_RECEIVE_ISPENDING}. - */ - mSearchDialog.getWindow().getDecorView().post(mStartSpinnerRunnable); - } + /** + * for in app search we show the progress spinner until the cursor is returned with + * the results. + */ + mSearchDialog.getWindow().getDecorView().post(mStartSpinnerRunnable); try { final Cursor cursor = mSearchManager.getSuggestions(mSearchable, query, QUERY_LIMIT); // trigger fill window so the spinner stays up until the results are copied over and // closer to being ready - if (!mGlobalSearchMode && cursor != null) cursor.getCount(); + if (cursor != null) cursor.getCount(); return cursor; } catch (RuntimeException e) { Log.w(LOG_TAG, "Search suggestions query threw an exception.", e); return null; } finally { - if (!mGlobalSearchMode) { - mSearchDialog.getWindow().getDecorView().post(mStopSpinnerRunnable); - } + mSearchDialog.getWindow().getDecorView().post(mStopSpinnerRunnable); } } @@ -221,21 +185,8 @@ class SuggestionsAdapter extends ResourceCursorAdapter { } try { - Cursor oldCursor = getCursor(); super.changeCursor(c); - - // We send a special respond to the cursor to tell it to close itself directly because - // it may not happen correctly for some cursors currently. This was originally - // included as a fix to http://b/2036290, in which the search dialog was holding - // on to references to the web search provider unnecessarily. This is being caused by - // the fact that the cursor is not being correctly closed in - // BulkCursorToCursorAdapter#close, which remains unfixed (see http://b/2015069). - // - // TODO: Remove this hack once http://b/2015069 is fixed. - if (oldCursor != null && oldCursor != c) { - oldCursor.respond(mCursorRespondCloseCursorBundle); - } - + if (c != null) { mFormatCol = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_FORMAT); mText1Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1); @@ -249,79 +200,6 @@ class SuggestionsAdapter extends ResourceCursorAdapter { } } - @Override - public void notifyDataSetChanged() { - if (DBG) Log.d(LOG_TAG, "notifyDataSetChanged"); - super.notifyDataSetChanged(); - - callCursorPostRefresh(mCursor); - - // look out for the pending item we are supposed to scroll to - if (mListItemToSelect != NONE) { - mSearchDialog.setListSelection(mListItemToSelect); - mListItemToSelect = NONE; - } - } - - /** - * Handle sending and receiving information associated with - * {@link DialogCursorProtocol#POST_REFRESH}. - * - * @param cursor The cursor to call. - */ - private void callCursorPostRefresh(Cursor cursor) { - if (!mGlobalSearchMode) return; - final Bundle request = new Bundle(); - request.putInt(DialogCursorProtocol.METHOD, DialogCursorProtocol.POST_REFRESH); - final Bundle response = cursor.respond(request); - - mSearchDialog.setWorking( - response.getBoolean(DialogCursorProtocol.POST_REFRESH_RECEIVE_ISPENDING, false)); - - mDisplayNotifyPos = - response.getInt(DialogCursorProtocol.POST_REFRESH_RECEIVE_DISPLAY_NOTIFY, -1); - } - - /** - * Tell the cursor which position was clicked, handling sending and receiving information - * associated with {@link DialogCursorProtocol#CLICK}. - * - * @param cursor The cursor - * @param position The position that was clicked. - */ - void callCursorOnClick(Cursor cursor, int position, int actionKey, String actionMsg) { - if (!mGlobalSearchMode) return; - final Bundle request = new Bundle(5); - request.putInt(DialogCursorProtocol.METHOD, DialogCursorProtocol.CLICK); - request.putInt(DialogCursorProtocol.CLICK_SEND_POSITION, position); - request.putInt(DialogCursorProtocol.CLICK_SEND_MAX_DISPLAY_POS, mMaxDisplayed); - if (actionKey != KeyEvent.KEYCODE_UNKNOWN) { - request.putInt(DialogCursorProtocol.CLICK_SEND_ACTION_KEY, actionKey); - request.putString(DialogCursorProtocol.CLICK_SEND_ACTION_MSG, actionMsg); - } - final Bundle response = cursor.respond(request); - mMaxDisplayed = -1; - mListItemToSelect = response.getInt( - DialogCursorProtocol.CLICK_RECEIVE_SELECTED_POS, SuggestionsAdapter.NONE); - } - - /** - * Tell the cursor that a search was started without using a suggestion. - * - * @param query The search query. - */ - void reportSearch(String query) { - if (!mGlobalSearchMode) return; - Cursor cursor = getCursor(); - if (cursor == null) return; - final Bundle request = new Bundle(3); - request.putInt(DialogCursorProtocol.METHOD, DialogCursorProtocol.SEARCH); - request.putString(DialogCursorProtocol.SEARCH_SEND_QUERY, query); - request.putInt(DialogCursorProtocol.SEARCH_SEND_MAX_DISPLAY_POS, mMaxDisplayed); - // the response is always empty - cursor.respond(request); - } - /** * Tags the view with cached child view look-ups. */ @@ -353,20 +231,6 @@ class SuggestionsAdapter extends ResourceCursorAdapter { @Override public void bindView(View view, Context context, Cursor cursor) { ChildViewCache views = (ChildViewCache) view.getTag(); - final int pos = cursor.getPosition(); - - // update the maximum position displayed since last refresh - if (pos > mMaxDisplayed) { - mMaxDisplayed = pos; - } - - // if the cursor wishes to be notified about this position, send it - if (mGlobalSearchMode && mDisplayNotifyPos != NONE && pos == mDisplayNotifyPos) { - final Bundle request = new Bundle(); - request.putInt(DialogCursorProtocol.METHOD, DialogCursorProtocol.THRESH_HIT); - mCursor.respond(request); - mDisplayNotifyPos = NONE; // only notify the first time - } int backgroundColor = 0; if (mBackgroundColorCol != -1) { diff --git a/core/java/android/backup/AbsoluteFileBackupHelper.java b/core/java/android/backup/AbsoluteFileBackupHelper.java index 1dbccc9..6bf848f 100644 --- a/core/java/android/backup/AbsoluteFileBackupHelper.java +++ b/core/java/android/backup/AbsoluteFileBackupHelper.java @@ -27,7 +27,7 @@ import java.io.FileDescriptor; * Like FileBackupHelper, but takes absolute paths for the files instead of * subpaths of getFilesDir() * - * @hide + * STOPSHIP: document! */ public class AbsoluteFileBackupHelper extends FileBackupHelperBase implements BackupHelper { private static final String TAG = "AbsoluteFileBackupHelper"; @@ -36,6 +36,13 @@ public class AbsoluteFileBackupHelper extends FileBackupHelperBase implements Ba Context mContext; String[] mFiles; + /** + * Construct a helper for backing up / restoring the files at the given absolute locations + * within the file system. + * + * @param context + * @param files + */ public AbsoluteFileBackupHelper(Context context, String... files) { super(context); @@ -54,6 +61,9 @@ public class AbsoluteFileBackupHelper extends FileBackupHelperBase implements Ba performBackup_checked(oldState, data, newState, mFiles, mFiles); } + /** + * Restore one absolute file entity from the restore stream + */ public void restoreEntity(BackupDataInputStream data) { if (DEBUG) Log.d(TAG, "got entity '" + data.getKey() + "' size=" + data.size()); String key = data.getKey(); diff --git a/core/java/android/backup/BackupDataInput.java b/core/java/android/backup/BackupDataInput.java index e67b0be..295dc66 100644 --- a/core/java/android/backup/BackupDataInput.java +++ b/core/java/android/backup/BackupDataInput.java @@ -21,7 +21,9 @@ import android.content.Context; import java.io.FileDescriptor; import java.io.IOException; -/** @hide */ +/** + * STOPSHIP: document! + */ public class BackupDataInput { int mBackupReader; @@ -33,6 +35,7 @@ public class BackupDataInput { int dataSize; } + /** @hide */ public BackupDataInput(FileDescriptor fd) { if (fd == null) throw new NullPointerException(); mBackupReader = ctor(fd); @@ -41,6 +44,7 @@ public class BackupDataInput { } } + /** @hide */ protected void finalize() throws Throwable { try { dtor(mBackupReader); @@ -49,6 +53,13 @@ public class BackupDataInput { } } + /** + * Consumes the next header from the restore stream. + * + * @return true when there is an entity ready for consumption from the restore stream, + * false if the restore stream has been fully consumed. + * @throws IOException if an error occurred while reading the restore stream + */ public boolean readNextHeader() throws IOException { int result = readNextHeader_native(mBackupReader, mHeader); if (result == 0) { @@ -66,6 +77,11 @@ public class BackupDataInput { } } + /** + * Report the key associated with the current record in the restore stream + * @return the current record's key string + * @throws IllegalStateException if the next record header has not yet been read + */ public String getKey() { if (mHeaderReady) { return mHeader.key; @@ -74,6 +90,13 @@ public class BackupDataInput { } } + /** + * Report the size in bytes of the data associated with the current record in the + * restore stream. + * + * @return The size of the record's raw data, in bytes + * @throws IllegalStateException if the next record header has not yet been read + */ public int getDataSize() { if (mHeaderReady) { return mHeader.dataSize; @@ -82,6 +105,19 @@ public class BackupDataInput { } } + /** + * Read a record's raw data from the restore stream. The record's header must first + * have been processed by the {@link #readNextHeader()} method. Multiple calls to + * this method may be made in order to process the data in chunks; not all of it + * must be read in a single call. + * + * @param data An allocated byte array of at least 'size' bytes + * @param offset Offset within the 'data' array at which the data will be placed + * when read from the stream. + * @param size The number of bytes to read in this pass. + * @return The number of bytes of data read + * @throws IOException if an error occurred when trying to read the restore data stream + */ public int readEntityData(byte[] data, int offset, int size) throws IOException { if (mHeaderReady) { int result = readEntityData_native(mBackupReader, data, offset, size); @@ -95,6 +131,14 @@ public class BackupDataInput { } } + /** + * Consume the current record's data without actually reading it into a buffer + * for further processing. This allows a {@link android.app.BackupAgent} to + * efficiently discard obsolete or otherwise uninteresting records during the + * restore operation. + * + * @throws IOException if an error occurred when trying to read the restore data stream + */ public void skipEntityData() throws IOException { if (mHeaderReady) { skipEntityData_native(mBackupReader); diff --git a/core/java/android/backup/BackupDataInputStream.java b/core/java/android/backup/BackupDataInputStream.java index b705c4c..503b3c1 100644 --- a/core/java/android/backup/BackupDataInputStream.java +++ b/core/java/android/backup/BackupDataInputStream.java @@ -16,12 +16,11 @@ package android.backup; -import android.util.Log; - import java.io.InputStream; import java.io.IOException; -/** @hide */ +/** + * STOPSHIP: document */ public class BackupDataInputStream extends InputStream { String key; @@ -30,6 +29,7 @@ public class BackupDataInputStream extends InputStream { BackupDataInput mData; byte[] mOneByte; + /** @hide */ BackupDataInputStream(BackupDataInput data) { mData = data; } diff --git a/core/java/android/backup/BackupDataOutput.java b/core/java/android/backup/BackupDataOutput.java index d29c5ba..672d01f 100644 --- a/core/java/android/backup/BackupDataOutput.java +++ b/core/java/android/backup/BackupDataOutput.java @@ -21,13 +21,16 @@ import android.content.Context; import java.io.FileDescriptor; import java.io.IOException; -/** @hide */ +/** + * STOPSHIP: document + */ public class BackupDataOutput { int mBackupWriter; public static final int OP_UPDATE = 1; public static final int OP_DELETE = 2; + /** @hide */ public BackupDataOutput(FileDescriptor fd) { if (fd == null) throw new NullPointerException(); mBackupWriter = ctor(fd); @@ -36,7 +39,15 @@ public class BackupDataOutput { } } - // A dataSize of -1 indicates that the record under this key should be deleted + /** + * Mark the beginning of one record in the backup data stream. + * + * @param key + * @param dataSize The size in bytes of this record's data. Passing a dataSize + * of -1 indicates that the record under this key should be deleted. + * @return The number of bytes written to the backup stream + * @throws IOException if the write failed + */ public int writeEntityHeader(String key, int dataSize) throws IOException { int result = writeEntityHeader_native(mBackupWriter, key, dataSize); if (result >= 0) { @@ -46,6 +57,13 @@ public class BackupDataOutput { } } + /** + * Write a chunk of data under the current entity to the backup transport. + * @param data A raw data buffer to send + * @param size The number of bytes to be sent in this chunk + * @return the number of bytes written + * @throws IOException if the write failed + */ public int writeEntityData(byte[] data, int size) throws IOException { int result = writeEntityData_native(mBackupWriter, data, size); if (result >= 0) { @@ -59,6 +77,7 @@ public class BackupDataOutput { setKeyPrefix_native(mBackupWriter, keyPrefix); } + /** @hide */ protected void finalize() throws Throwable { try { dtor(mBackupWriter); diff --git a/core/java/android/backup/BackupHelper.java b/core/java/android/backup/BackupHelper.java index 3983e28..fc48cf2 100644 --- a/core/java/android/backup/BackupHelper.java +++ b/core/java/android/backup/BackupHelper.java @@ -20,7 +20,9 @@ import android.os.ParcelFileDescriptor; import java.io.InputStream; -/** @hide */ +/** + * STOPSHIP: document! + */ public interface BackupHelper { /** * Based on oldState, determine which of the files from the application's data directory @@ -31,16 +33,18 @@ public interface BackupHelper { ParcelFileDescriptor newState); /** - * Called by BackupHelperDispatcher to dispatch one entity of data. + * Called by BackupHelperAgent to restore one entity from the restore dataset. * <p class=note> * Do not close the <code>data</code> stream. Do not read more than - * <code>dataSize</code> bytes from <code>data</code>. + * <code>data.size()</code> bytes from <code>data</code>. */ public void restoreEntity(BackupDataInputStream data); /** - * + * Called by BackupHelperAgent to write the new backup state file corresponding to + * the current state of the app's data at the time the backup operation was + * performed. */ - public void writeRestoreSnapshot(ParcelFileDescriptor fd); + public void writeNewStateDescription(ParcelFileDescriptor fd); } diff --git a/core/java/android/backup/BackupHelperAgent.java b/core/java/android/backup/BackupHelperAgent.java index 5d0c4a2..dc17154f 100644 --- a/core/java/android/backup/BackupHelperAgent.java +++ b/core/java/android/backup/BackupHelperAgent.java @@ -26,28 +26,54 @@ import android.util.Log; import java.io.IOException; -/** @hide */ +/** + * A convenient BackupAgent wrapper class that automatically manages heterogeneous + * data sets within the backup data, each identified by a unique key prefix. An + * application will typically extend this class in their own backup agent. Then, + * within the agent's onBackup() and onRestore() methods, it will call + * {@link #addHelper(String, BackupHelper)} one or more times to specify the data + * sets, then invoke super.onBackup() or super.onRestore() to have the BackupHelperAgent + * implementation process the data. + * + * STOPSHIP: document! + */ public class BackupHelperAgent extends BackupAgent { static final String TAG = "BackupHelperAgent"; BackupHelperDispatcher mDispatcher = new BackupHelperDispatcher(); + /** + * Run the backup process on each of the configured handlers. + */ @Override public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState) throws IOException { mDispatcher.performBackup(oldState, data, newState); } + /** + * Run the restore process on each of the configured handlers. + */ @Override public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState) throws IOException { mDispatcher.performRestore(data, appVersionCode, newState); } + /** @hide */ public BackupHelperDispatcher getDispatcher() { return mDispatcher; } + /** + * Add a helper for a given data subset to the agent's configuration. Each helper + * must have a prefix string that is unique within this backup agent's set of + * helpers. + * + * @param keyPrefix A string used to disambiguate the various helpers within this agent + * @param helper A backup/restore helper object to be invoked during backup and restore + * operations. + */ public void addHelper(String keyPrefix, BackupHelper helper) { mDispatcher.addHelper(keyPrefix, helper); } diff --git a/core/java/android/backup/BackupHelperDispatcher.java b/core/java/android/backup/BackupHelperDispatcher.java index 6ccb83e..bf2c44d 100644 --- a/core/java/android/backup/BackupHelperDispatcher.java +++ b/core/java/android/backup/BackupHelperDispatcher.java @@ -138,7 +138,7 @@ public class BackupHelperDispatcher { // Write out the state files -- mHelpers is a TreeMap, so the order is well defined. for (BackupHelper helper: mHelpers.values()) { - helper.writeRestoreSnapshot(newState); + helper.writeNewStateDescription(newState); } } diff --git a/core/java/android/backup/BackupManager.java b/core/java/android/backup/BackupManager.java index 0b27117..4bf59eb 100644 --- a/core/java/android/backup/BackupManager.java +++ b/core/java/android/backup/BackupManager.java @@ -38,7 +38,11 @@ import android.util.Log; * documentation for {@link android.app.BackupAgent} for a detailed description * of how the backup then proceeds. * - * @hide pending API solidification + * <p>STOPSHIP more documentation here! Include the attributes: + * android:backupAgent + * android:allowBackup + * android:restoreNeedsApplication + * android:killAfterRestore */ public class BackupManager { private static final String TAG = "BackupManager"; @@ -110,11 +114,8 @@ public class BackupManager { } /** - * Begin the process of restoring system data from backup. This method requires - * that the application hold the "android.permission.BACKUP" permission, and is - * not public. - * - * {@hide} + * Begin the process of restoring data from backup. See the + * {@link android.backup.RestoreSession} class for documentation on that process. */ public RestoreSession beginRestoreSession() { if (!EVEN_THINK_ABOUT_DOING_RESTORE) { @@ -128,7 +129,7 @@ public class BackupManager { IRestoreSession binder = sService.beginRestoreSession(transport); session = new RestoreSession(mContext, binder); } catch (RemoteException e) { - Log.d(TAG, "beginRestoreSession() couldn't connect"); + Log.w(TAG, "beginRestoreSession() couldn't connect"); } } return session; diff --git a/core/java/android/backup/FileBackupHelper.java b/core/java/android/backup/FileBackupHelper.java index dacfc8f..68b4d42 100644 --- a/core/java/android/backup/FileBackupHelper.java +++ b/core/java/android/backup/FileBackupHelper.java @@ -23,7 +23,9 @@ import android.util.Log; import java.io.File; import java.io.FileDescriptor; -/** @hide */ +/** + * STOPSHIP: document! [manages backup of a set of files; restore is totally opaque] + */ public class FileBackupHelper extends FileBackupHelperBase implements BackupHelper { private static final String TAG = "FileBackupHelper"; private static final boolean DEBUG = false; @@ -32,6 +34,13 @@ public class FileBackupHelper extends FileBackupHelperBase implements BackupHelp File mFilesDir; String[] mFiles; + /** + * Construct a helper to manage backup/restore of entire files within the + * application's data directory hierarchy. + * + * @param context The backup agent's Context object + * @param files A list of the files to be backed up or restored. + */ public FileBackupHelper(Context context, String... files) { super(context); @@ -60,6 +69,9 @@ public class FileBackupHelper extends FileBackupHelperBase implements BackupHelp performBackup_checked(oldState, data, newState, fullPaths, files); } + /** + * Restore one record [representing a single file] from the restore dataset. + */ public void restoreEntity(BackupDataInputStream data) { if (DEBUG) Log.d(TAG, "got entity '" + data.getKey() + "' size=" + data.size()); String key = data.getKey(); diff --git a/core/java/android/backup/FileBackupHelperBase.java b/core/java/android/backup/FileBackupHelperBase.java index 03ae476..a0ff38b 100644 --- a/core/java/android/backup/FileBackupHelperBase.java +++ b/core/java/android/backup/FileBackupHelperBase.java @@ -20,7 +20,6 @@ import android.content.Context; import android.os.ParcelFileDescriptor; import android.util.Log; -import java.io.InputStream; import java.io.File; import java.io.FileDescriptor; import java.io.FileOutputStream; @@ -80,18 +79,14 @@ class FileBackupHelperBase { } } - void writeFile(File f, InputStream in) { - if (!(in instanceof BackupDataInputStream)) { - throw new IllegalStateException("input stream must be a BackupDataInputStream"); - } + void writeFile(File f, BackupDataInputStream in) { int result = -1; // Create the enclosing directory. File parent = f.getParentFile(); parent.mkdirs(); - result = writeFile_native(mPtr, f.getAbsolutePath(), - ((BackupDataInputStream)in).mData.mBackupReader); + result = writeFile_native(mPtr, f.getAbsolutePath(), in.mData.mBackupReader); if (result != 0) { // Bail on this entity. Only log one failure per helper object. if (!mExceptionLogged) { @@ -103,7 +98,7 @@ class FileBackupHelperBase { } } - public void writeRestoreSnapshot(ParcelFileDescriptor fd) { + public void writeNewStateDescription(ParcelFileDescriptor fd) { int result = writeSnapshot_native(mPtr, fd.getFileDescriptor()); // TODO: Do something with the error. } diff --git a/core/java/android/backup/RestoreObserver.java b/core/java/android/backup/RestoreObserver.java index 3be8c08..e4182750 100644 --- a/core/java/android/backup/RestoreObserver.java +++ b/core/java/android/backup/RestoreObserver.java @@ -19,7 +19,6 @@ package android.backup; /** * Callback class for receiving progress reports during a restore operation. These * methods will all be called on your application's main thread. - * @hide */ public abstract class RestoreObserver { /** diff --git a/core/java/android/backup/RestoreSession.java b/core/java/android/backup/RestoreSession.java index d10831e..bc410c4 100644 --- a/core/java/android/backup/RestoreSession.java +++ b/core/java/android/backup/RestoreSession.java @@ -27,7 +27,6 @@ import android.util.Log; /** * Interface for applications to use when managing a restore session. - * @hide */ public class RestoreSession { static final String TAG = "RestoreSession"; @@ -44,6 +43,8 @@ public class RestoreSession { * and a String array under the key "names" whose entries are the user-meaningful * text corresponding to the backup sets at each index in the tokens array. * On error, returns null. + * + * {@hide} */ public RestoreSet[] getAvailableRestoreSets() { try { @@ -62,10 +63,12 @@ public class RestoreSession { * * @return Zero on success; nonzero on error. The observer will only receive * progress callbacks if this method returned zero. - * @param token The token from {@link getAvailableRestoreSets()} corresponding to + * @param token The token from {@link #getAvailableRestoreSets()} corresponding to * the restore set that should be used. * @param observer If non-null, this binder points to an object that will receive * progress callbacks during the restore operation. + * + * {@hide} */ public int restoreAll(long token, RestoreObserver observer) { int err = -1; @@ -117,8 +120,7 @@ public class RestoreSession { * object is no longer valid. * * <p><b>Note:</b> The caller <i>must</i> invoke this method to end the restore session, - * even if {@link #getAvailableRestoreSets()} or - * {@link #restorePackage(long, String, RestoreObserver)} failed. + * even if {@link #restorePackage(String, RestoreObserver)} failed. */ public void endRestoreSession() { try { diff --git a/core/java/android/backup/SharedPreferencesBackupHelper.java b/core/java/android/backup/SharedPreferencesBackupHelper.java index 6a0bc96..f9c97a3 100644 --- a/core/java/android/backup/SharedPreferencesBackupHelper.java +++ b/core/java/android/backup/SharedPreferencesBackupHelper.java @@ -23,7 +23,9 @@ import android.util.Log; import java.io.File; import java.io.FileDescriptor; -/** @hide */ +/** + * STOPSHIP: document! + */ public class SharedPreferencesBackupHelper extends FileBackupHelperBase implements BackupHelper { private static final String TAG = "SharedPreferencesBackupHelper"; private static final boolean DEBUG = false; @@ -31,6 +33,13 @@ public class SharedPreferencesBackupHelper extends FileBackupHelperBase implemen private Context mContext; private String[] mPrefGroups; + /** + * Construct a helper for backing up and restoring the + * {@link android.content.SharedPreferences} under the given names. + * + * @param context + * @param prefGroups + */ public SharedPreferencesBackupHelper(Context context, String... prefGroups) { super(context); @@ -38,6 +47,9 @@ public class SharedPreferencesBackupHelper extends FileBackupHelperBase implemen mPrefGroups = prefGroups; } + /** + * Backs up the configured SharedPreferences groups + */ public void performBackup(ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState) { Context context = mContext; @@ -54,6 +66,10 @@ public class SharedPreferencesBackupHelper extends FileBackupHelperBase implemen performBackup_checked(oldState, data, newState, files, prefGroups); } + /** + * Restores one entity from the restore data stream to its proper shared + * preferences file store. + */ public void restoreEntity(BackupDataInputStream data) { Context context = mContext; diff --git a/core/java/android/content/ComponentName.java b/core/java/android/content/ComponentName.java index 0455202..c4ba05d 100644 --- a/core/java/android/content/ComponentName.java +++ b/core/java/android/content/ComponentName.java @@ -30,7 +30,7 @@ import java.lang.Comparable; * name inside of that package. * */ -public final class ComponentName implements Parcelable, Comparable<ComponentName> { +public final class ComponentName implements Parcelable, Cloneable, Comparable<ComponentName> { private final String mPackage; private final String mClass; @@ -76,6 +76,10 @@ public final class ComponentName implements Parcelable, Comparable<ComponentName mClass = cls.getName(); } + public ComponentName clone() { + return new ComponentName(mPackage, mClass); + } + /** * Return the package name of this component. */ diff --git a/core/java/android/content/ContentResolver.java b/core/java/android/content/ContentResolver.java index eb2d7b1..45f361e 100644 --- a/core/java/android/content/ContentResolver.java +++ b/core/java/android/content/ContentResolver.java @@ -16,6 +16,8 @@ package android.content; +import android.accounts.Account; +import android.app.ActivityThread; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.AssetFileDescriptor; import android.content.res.Resources; @@ -29,9 +31,10 @@ import android.os.IBinder; import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.os.ServiceManager; +import android.os.SystemClock; import android.text.TextUtils; -import android.accounts.Account; import android.util.Config; +import android.util.EventLog; import android.util.Log; import java.io.File; @@ -41,6 +44,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.List; +import java.util.Random; import java.util.ArrayList; @@ -61,7 +65,31 @@ public abstract class ContentResolver { */ @Deprecated public static final String SYNC_EXTRAS_FORCE = "force"; + + /** + * If this extra is set to true then the sync settings (like getSyncAutomatically()) + * are ignored by the sync scheduler. + */ + public static final String SYNC_EXTRAS_IGNORE_SETTINGS = "ignore_settings"; + + /** + * If this extra is set to true then any backoffs for the initial attempt (e.g. due to retries) + * are ignored by the sync scheduler. If this request fails and gets rescheduled then the + * retries will still honor the backoff. + */ + public static final String SYNC_EXTRAS_IGNORE_BACKOFF = "ignore_backoff"; + + /** + * If this extra is set to true then the request will not be retried if it fails. + */ + public static final String SYNC_EXTRAS_DO_NOT_RETRY = "do_not_retry"; + + /** + * Setting this extra is the equivalent of setting both {@link #SYNC_EXTRAS_IGNORE_SETTINGS} + * and {@link #SYNC_EXTRAS_IGNORE_BACKOFF} + */ public static final String SYNC_EXTRAS_MANUAL = "force"; + public static final String SYNC_EXTRAS_UPLOAD = "upload"; public static final String SYNC_EXTRAS_OVERRIDE_TOO_MANY_DELETIONS = "deletions_override"; public static final String SYNC_EXTRAS_DISCARD_LOCAL_DELETIONS = "discard_deletions"; @@ -139,6 +167,11 @@ public abstract class ContentResolver { /** @hide */ public static final int SYNC_OBSERVER_TYPE_ALL = 0x7fffffff; + // Always log queries which take 100ms+; shorter queries are + // sampled accordingly. + private static final int SLOW_THRESHOLD_MILLIS = 100; + private final Random mRandom = new Random(); // guarded by itself + public ContentResolver(Context context) { mContext = context; } @@ -211,12 +244,15 @@ public abstract class ContentResolver { return null; } try { + long startTime = SystemClock.uptimeMillis(); Cursor qCursor = provider.query(uri, projection, selection, selectionArgs, sortOrder); - if(qCursor == null) { + if (qCursor == null) { releaseProvider(provider); return null; } - //Wrap the cursor object into CursorWrapperInner object + long durationMillis = SystemClock.uptimeMillis() - startTime; + maybeLogQueryToEventLog(durationMillis, uri, projection, selection, sortOrder); + // Wrap the cursor object into CursorWrapperInner object return new CursorWrapperInner(qCursor, provider); } catch (RemoteException e) { releaseProvider(provider); @@ -548,7 +584,11 @@ public abstract class ContentResolver { throw new IllegalArgumentException("Unknown URL " + url); } try { - return provider.insert(url, values); + long startTime = SystemClock.uptimeMillis(); + Uri createdRow = provider.insert(url, values); + long durationMillis = SystemClock.uptimeMillis() - startTime; + maybeLogUpdateToEventLog(durationMillis, url, "insert", null /* where */); + return createdRow; } catch (RemoteException e) { return null; } finally { @@ -603,7 +643,11 @@ public abstract class ContentResolver { throw new IllegalArgumentException("Unknown URL " + url); } try { - return provider.bulkInsert(url, values); + long startTime = SystemClock.uptimeMillis(); + int rowsCreated = provider.bulkInsert(url, values); + long durationMillis = SystemClock.uptimeMillis() - startTime; + maybeLogUpdateToEventLog(durationMillis, url, "bulkinsert", null /* where */); + return rowsCreated; } catch (RemoteException e) { return 0; } finally { @@ -628,7 +672,11 @@ public abstract class ContentResolver { throw new IllegalArgumentException("Unknown URL " + url); } try { - return provider.delete(url, where, selectionArgs); + long startTime = SystemClock.uptimeMillis(); + int rowsDeleted = provider.delete(url, where, selectionArgs); + long durationMillis = SystemClock.uptimeMillis() - startTime; + maybeLogUpdateToEventLog(durationMillis, url, "delete", where); + return rowsDeleted; } catch (RemoteException e) { return -1; } finally { @@ -656,7 +704,11 @@ public abstract class ContentResolver { throw new IllegalArgumentException("Unknown URI " + uri); } try { - return provider.update(uri, values, where, selectionArgs); + long startTime = SystemClock.uptimeMillis(); + int rowsUpdated = provider.update(uri, values, where, selectionArgs); + long durationMillis = SystemClock.uptimeMillis() - startTime; + maybeLogUpdateToEventLog(durationMillis, uri, "update", where); + return rowsUpdated; } catch (RemoteException e) { return -1; } finally { @@ -966,6 +1018,100 @@ public abstract class ContentResolver { } /** + * Specifies that a sync should be requested with the specified the account, authority, + * and extras at the given frequency. If there is already another periodic sync scheduled + * with the account, authority and extras then a new periodic sync won't be added, instead + * the frequency of the previous one will be updated. + * <p> + * These periodic syncs honor the "syncAutomatically" and "masterSyncAutomatically" settings. + * Although these sync are scheduled at the specified frequency, it may take longer for it to + * actually be started if other syncs are ahead of it in the sync operation queue. This means + * that the actual start time may drift. + * <p> + * Periodic syncs are not allowed to have any of {@link #SYNC_EXTRAS_DO_NOT_RETRY}, + * {@link #SYNC_EXTRAS_IGNORE_BACKOFF}, {@link #SYNC_EXTRAS_IGNORE_SETTINGS}, + * {@link #SYNC_EXTRAS_INITIALIZE}, {@link #SYNC_EXTRAS_FORCE}, + * {@link #SYNC_EXTRAS_EXPEDITED}, {@link #SYNC_EXTRAS_MANUAL} set to true. + * If any are supplied then an {@link IllegalArgumentException} will be thrown. + * + * @param account the account to specify in the sync + * @param authority the provider to specify in the sync request + * @param extras extra parameters to go along with the sync request + * @param pollFrequency how frequently the sync should be performed, in seconds. + * @throws IllegalArgumentException if an illegal extra was set or if any of the parameters + * are null. + */ + public static void addPeriodicSync(Account account, String authority, Bundle extras, + long pollFrequency) { + validateSyncExtrasBundle(extras); + if (account == null) { + throw new IllegalArgumentException("account must not be null"); + } + if (authority == null) { + throw new IllegalArgumentException("authority must not be null"); + } + if (extras.getBoolean(SYNC_EXTRAS_MANUAL, false) + || extras.getBoolean(SYNC_EXTRAS_DO_NOT_RETRY, false) + || extras.getBoolean(SYNC_EXTRAS_IGNORE_BACKOFF, false) + || extras.getBoolean(SYNC_EXTRAS_IGNORE_SETTINGS, false) + || extras.getBoolean(SYNC_EXTRAS_INITIALIZE, false) + || extras.getBoolean(SYNC_EXTRAS_FORCE, false) + || extras.getBoolean(SYNC_EXTRAS_EXPEDITED, false)) { + throw new IllegalArgumentException("illegal extras were set"); + } + try { + getContentService().addPeriodicSync(account, authority, extras, pollFrequency); + } catch (RemoteException e) { + // exception ignored; if this is thrown then it means the runtime is in the midst of + // being restarted + } + } + + /** + * Remove a periodic sync. Has no affect if account, authority and extras don't match + * an existing periodic sync. + * + * @param account the account of the periodic sync to remove + * @param authority the provider of the periodic sync to remove + * @param extras the extras of the periodic sync to remove + */ + public static void removePeriodicSync(Account account, String authority, Bundle extras) { + validateSyncExtrasBundle(extras); + if (account == null) { + throw new IllegalArgumentException("account must not be null"); + } + if (authority == null) { + throw new IllegalArgumentException("authority must not be null"); + } + try { + getContentService().removePeriodicSync(account, authority, extras); + } catch (RemoteException e) { + throw new RuntimeException("the ContentService should always be reachable", e); + } + } + + /** + * Get the list of information about the periodic syncs for the given account and authority. + * + * @param account the account whose periodic syncs we are querying + * @param authority the provider whose periodic syncs we are querying + * @return a list of PeriodicSync objects. This list may be empty but will never be null. + */ + public static List<PeriodicSync> getPeriodicSyncs(Account account, String authority) { + if (account == null) { + throw new IllegalArgumentException("account must not be null"); + } + if (authority == null) { + throw new IllegalArgumentException("authority must not be null"); + } + try { + return getContentService().getPeriodicSyncs(account, authority); + } catch (RemoteException e) { + throw new RuntimeException("the ContentService should always be reachable", e); + } + } + + /** * Check if this account/provider is syncable. * @return >0 if it is syncable, 0 if not, and <0 if the state isn't known yet. */ @@ -1099,6 +1245,78 @@ public abstract class ContentResolver { } } + /** + * Returns sampling percentage for a given duration. + * + * Always returns at least 1%. + */ + private int samplePercentForDuration(long durationMillis) { + if (durationMillis >= SLOW_THRESHOLD_MILLIS) { + return 100; + } + return (int) (100 * durationMillis / SLOW_THRESHOLD_MILLIS) + 1; + } + + private void maybeLogQueryToEventLog(long durationMillis, + Uri uri, String[] projection, + String selection, String sortOrder) { + int samplePercent = samplePercentForDuration(durationMillis); + if (samplePercent < 100) { + synchronized (mRandom) { + if (mRandom.nextInt(100) >= samplePercent) { + return; + } + } + } + + StringBuilder projectionBuffer = new StringBuilder(100); + if (projection != null) { + for (int i = 0; i < projection.length; ++i) { + // Note: not using a comma delimiter here, as the + // multiple arguments to EventLog.writeEvent later + // stringify with a comma delimiter, which would make + // parsing uglier later. + if (i != 0) projectionBuffer.append('/'); + projectionBuffer.append(projection[i]); + } + } + + // ActivityThread.currentPackageName() only returns non-null if the + // current thread is an application main thread. This parameter tells + // us whether an event loop is blocked, and if so, which app it is. + String blockingPackage = ActivityThread.currentPackageName(); + + EventLog.writeEvent( + EventLogTags.CONTENT_QUERY_OPERATION, + uri.toString(), + projectionBuffer.toString(), + selection != null ? selection : "", + sortOrder != null ? sortOrder : "", + durationMillis, + blockingPackage != null ? blockingPackage : "", + samplePercent); + } + + private void maybeLogUpdateToEventLog( + long durationMillis, Uri uri, String operation, String selection) { + int samplePercent = samplePercentForDuration(durationMillis); + if (samplePercent < 100) { + synchronized (mRandom) { + if (mRandom.nextInt(100) >= samplePercent) { + return; + } + } + } + String blockingPackage = ActivityThread.currentPackageName(); + EventLog.writeEvent( + EventLogTags.CONTENT_UPDATE_OPERATION, + uri.toString(), + operation, + selection != null ? selection : "", + durationMillis, + blockingPackage != null ? blockingPackage : "", + samplePercent); + } private final class CursorWrapperInner extends CursorWrapper { private IContentProvider mContentProvider; diff --git a/core/java/android/content/ContentService.java b/core/java/android/content/ContentService.java index 974a667..e0dfab5 100644 --- a/core/java/android/content/ContentService.java +++ b/core/java/android/content/ContentService.java @@ -32,6 +32,8 @@ import android.Manifest; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.Collection; +import java.util.List; /** * {@hide} @@ -273,6 +275,42 @@ public final class ContentService extends IContentService.Stub { } } + public void addPeriodicSync(Account account, String authority, Bundle extras, + long pollFrequency) { + mContext.enforceCallingOrSelfPermission(Manifest.permission.WRITE_SYNC_SETTINGS, + "no permission to write the sync settings"); + long identityToken = clearCallingIdentity(); + try { + getSyncManager().getSyncStorageEngine().addPeriodicSync( + account, authority, extras, pollFrequency); + } finally { + restoreCallingIdentity(identityToken); + } + } + + public void removePeriodicSync(Account account, String authority, Bundle extras) { + mContext.enforceCallingOrSelfPermission(Manifest.permission.WRITE_SYNC_SETTINGS, + "no permission to write the sync settings"); + long identityToken = clearCallingIdentity(); + try { + getSyncManager().getSyncStorageEngine().removePeriodicSync(account, authority, extras); + } finally { + restoreCallingIdentity(identityToken); + } + } + + public List<PeriodicSync> getPeriodicSyncs(Account account, String providerName) { + mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_SETTINGS, + "no permission to read the sync settings"); + long identityToken = clearCallingIdentity(); + try { + return getSyncManager().getSyncStorageEngine().getPeriodicSyncs( + account, providerName); + } finally { + restoreCallingIdentity(identityToken); + } + } + public int getIsSyncable(Account account, String providerName) { mContext.enforceCallingOrSelfPermission(Manifest.permission.READ_SYNC_SETTINGS, "no permission to read the sync settings"); diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 5aefe4c..672e5f7 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -25,6 +25,7 @@ import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase.CursorFactory; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; +import android.media.MediaScannerConnection.ScanResultListener; import android.net.Uri; import android.os.Bundle; import android.os.Handler; @@ -137,7 +138,28 @@ public abstract class Context { /** * Return the context of the single, global Application object of the - * current process. + * current process. This generally should only be used if you need a + * Context whose lifecycle is separate from the current context, that is + * tied to the lifetime of the process rather than the current component. + * + * <p>Consider for example how this interacts with + * {@ #registerReceiver(BroadcastReceiver, IntentFilter)}: + * <ul> + * <li> <p>If used from an Activity context, the receiver is being registered + * within that activity. This means that you are expected to unregister + * before the activity is done being destroyed; in fact if you do not do + * so, the framework will clean up your leaked registration as it removes + * the activity and log an error. Thus, if you use the Activity context + * to register a receiver that is static (global to the process, not + * associated with an Activity instance) then that registration will be + * removed on you at whatever point the activity you used is destroyed. + * <li> <p>If used from the Context returned here, the receiver is being + * registered with the global state associated with your application. Thus + * it will never be unregistered for you. This is necessary if the receiver + * is associated with static data, not a particular component. However + * using the ApplicationContext elsewhere can easily lead to serious leaks + * if you forget to unregister, unbind, etc. + * </ul> */ public abstract Context getApplicationContext(); @@ -393,11 +415,84 @@ public abstract class Context { public abstract File getFilesDir(); /** + * Returns the absolute path to the directory on the external filesystem + * (that is somewhere on {@link android.os.Environment#getExternalStorageDirectory() + * Environment.getExternalStorageDirectory()} where the application can + * place persistent files it owns. These files are private to the + * applications, and not typically visible to the user as media. + * + * <p>This is like {@link #getFilesDir()} in that these + * files will be deleted when the application is uninstalled, however there + * are some important differences: + * + * <ul> + * <li>External files are not always available: they will disappear if the + * user mounts the external storage on a computer or removes it. See the + * APIs on {@link android.os.Environment} for information in the storage state. + * <li>There is no security enforced with these files. All applications + * can read and write files placed here. + * </ul> + * + * <p>Here is an example of typical code to manipulate a file in + * an application's private storage:</p> + * + * {@sample development/samples/ApiDemos/src/com/example/android/apis/content/ExternalStorage.java + * private_file} + * + * <p>If you install a non-null <var>type</var> to this function, the returned + * file will be a path to a sub-directory of the given type. Though these files + * are not automatically scanned by the media scanner, you can explicitly + * add them to the media database with + * {@link android.media.MediaScannerConnection#scanFile(Context, String[], String[], + * ScanResultListener) MediaScannerConnection.scanFile}. + * Note that this is not the same as + * {@link android.os.Environment#getExternalStoragePublicDirectory + * Environment.getExternalStoragePublicDirectory()}, which provides + * directories of media shared by all applications. The + * directories returned here are + * owned by the application, and its contents will be removed when the + * application is uninstalled. Unlike + * {@link android.os.Environment#getExternalStoragePublicDirectory + * Environment.getExternalStoragePublicDirectory()}, the directory + * returned here will be automatically created for you. + * + * <p>Here is an example of typical code to manipulate a picture in + * an application's private storage and add it to the media database:</p> + * + * {@sample development/samples/ApiDemos/src/com/example/android/apis/content/ExternalStorage.java + * private_picture} + * + * @param type The type of files directory to return. May be null for + * the root of the files directory or one of + * the following Environment constants for a subdirectory: + * {@link android.os.Environment#DIRECTORY_MUSIC}, + * {@link android.os.Environment#DIRECTORY_PODCASTS}, + * {@link android.os.Environment#DIRECTORY_RINGTONES}, + * {@link android.os.Environment#DIRECTORY_ALARMS}, + * {@link android.os.Environment#DIRECTORY_NOTIFICATIONS}, + * {@link android.os.Environment#DIRECTORY_PICTURES}, or + * {@link android.os.Environment#DIRECTORY_MOVIES}. + * + * @return Returns the path of the directory holding application files + * on external storage. Returns null if external storage is not currently + * mounted so it could not ensure the path exists; you will need to call + * this method again when it is available. + * + * @see #getFilesDir + */ + public abstract File getExternalFilesDir(String type); + + /** * Returns the absolute path to the application specific cache directory * on the filesystem. These files will be ones that get deleted first when the - * device runs low on storage + * device runs low on storage. * There is no guarantee when these files will be deleted. - * + * + * <strong>Note: you should not <em>rely</em> on the system deleting these + * files for you; you should always have a reasonable maximum, such as 1 MB, + * for the amount of space you consume with cache files, and prune those + * files when exceeding that space.</strong> + * * @return Returns the path of the directory holding application cache files. * * @see #openFileOutput @@ -407,6 +502,37 @@ public abstract class Context { public abstract File getCacheDir(); /** + * Returns the absolute path to the directory on the external filesystem + * (that is somewhere on {@link android.os.Environment#getExternalStorageDirectory() + * Environment.getExternalStorageDirectory()} where the application can + * place cache files it owns. + * + * <p>This is like {@link #getCacheDir()} in that these + * files will be deleted when the application is uninstalled, however there + * are some important differences: + * + * <ul> + * <li>The platform does not monitor the space available in external storage, + * and thus will not automatically delete these files. Note that you should + * be managing the maximum space you will use for these anyway, just like + * with {@link #getCacheDir()}. + * <li>External files are not always available: they will disappear if the + * user mounts the external storage on a computer or removes it. See the + * APIs on {@link android.os.Environment} for information in the storage state. + * <li>There is no security enforced with these files. All applications + * can read and write files placed here. + * </ul> + * + * @return Returns the path of the directory holding application cache files + * on external storage. Returns null if external storage is not currently + * mounted so it could not ensure the path exists; you will need to call + * this method again when it is available. + * + * @see #getCacheDir + */ + public abstract File getExternalCacheDir(); + + /** * Returns an array of strings naming the private files associated with * this Context's application package. * @@ -1110,7 +1236,7 @@ public abstract class Context { * @see #SENSOR_SERVICE * @see android.hardware.SensorManager * @see #STORAGE_SERVICE - * @see android.storage.StorageManager + * @see android.os.storage.StorageManager * @see #VIBRATOR_SERVICE * @see android.os.Vibrator * @see #CONNECTIVITY_SERVICE @@ -1243,11 +1369,11 @@ public abstract class Context { /** * Use with {@link #getSystemService} to retrieve a {@link - * android.storage.StorageManager} for accesssing system storage + * android.os.storage.StorageManager} for accesssing system storage * functions. * * @see #getSystemService - * @see android.storage.StorageManager + * @see android.os.storage.StorageManager */ public static final String STORAGE_SERVICE = "storage"; diff --git a/core/java/android/content/ContextWrapper.java b/core/java/android/content/ContextWrapper.java index 1b34320..a447108 100644 --- a/core/java/android/content/ContextWrapper.java +++ b/core/java/android/content/ContextWrapper.java @@ -179,11 +179,21 @@ public class ContextWrapper extends Context { } @Override + public File getExternalFilesDir(String type) { + return mBase.getExternalFilesDir(type); + } + + @Override public File getCacheDir() { return mBase.getCacheDir(); } @Override + public File getExternalCacheDir() { + return mBase.getExternalCacheDir(); + } + + @Override public File getDir(String name, int mode) { return mBase.getDir(name, mode); } diff --git a/core/java/android/content/EventLogTags.logtags b/core/java/android/content/EventLogTags.logtags new file mode 100644 index 0000000..a815b95 --- /dev/null +++ b/core/java/android/content/EventLogTags.logtags @@ -0,0 +1,6 @@ +# See system/core/logcat/event.logtags for a description of the format of this file. + +option java_package android.content; + +52002 content_query_operation (uri|3),(projection|3),(selection|3),(sortorder|3),(time|1|3),(blocking_package|3),(sample_percent|1|6) +52003 content_update_operation (uri|3),(operation|3),(selection|3),(time|1|3),(blocking_package|3),(sample_percent|1|6) diff --git a/core/java/android/content/IContentService.aidl b/core/java/android/content/IContentService.aidl index b0f14c1..2d906ed 100644 --- a/core/java/android/content/IContentService.aidl +++ b/core/java/android/content/IContentService.aidl @@ -21,6 +21,7 @@ import android.content.ActiveSyncInfo; import android.content.ISyncStatusObserver; import android.content.SyncAdapterType; import android.content.SyncStatusInfo; +import android.content.PeriodicSync; import android.net.Uri; import android.os.Bundle; import android.database.IContentObserver; @@ -38,11 +39,11 @@ interface IContentService { void requestSync(in Account account, String authority, in Bundle extras); void cancelSync(in Account account, String authority); - + /** * 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 + * @return true if the provider should be synced when a network tickle is received */ boolean getSyncAutomatically(in Account account, String providerName); @@ -55,6 +56,33 @@ interface IContentService { void setSyncAutomatically(in Account account, String providerName, boolean sync); /** + * Get the frequency of the periodic poll, if any. + * @param providerName the provider whose setting we are querying + * @return the frequency of the periodic sync in seconds. If 0 then no periodic syncs + * will take place. + */ + List<PeriodicSync> getPeriodicSyncs(in Account account, String providerName); + + /** + * Set whether or not the provider is to be synced on a periodic basis. + * + * @param providerName the provider whose behavior is being controlled + * @param pollFrequency the period that a sync should be performed, in seconds. If this is + * zero or less then no periodic syncs will be performed. + */ + void addPeriodicSync(in Account account, String providerName, in Bundle extras, + long pollFrequency); + + /** + * Set whether or not the provider is to be synced on a periodic basis. + * + * @param providerName the provider whose behavior is being controlled + * @param pollFrequency the period that a sync should be performed, in seconds. If this is + * zero or less then no periodic syncs will be performed. + */ + void removePeriodicSync(in Account account, String providerName, in Bundle extras); + + /** * Check if this account/provider is syncable. * @return >0 if it is syncable, 0 if not, and <0 if the state isn't known yet. */ @@ -69,15 +97,15 @@ interface IContentService { void setMasterSyncAutomatically(boolean flag); boolean getMasterSyncAutomatically(); - + /** * 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(in Account account, String authority); - + ActiveSyncInfo getActiveSync(); - + /** * Returns the types of the SyncAdapters that are registered with the system. * @return Returns the types of the SyncAdapters that are registered with the system. @@ -96,8 +124,8 @@ interface IContentService { * Return true if the pending status is true of any matching authorities. */ boolean isSyncPending(in Account account, String authority); - + void addStatusChangeListener(int mask, ISyncStatusObserver callback); - + void removeStatusChangeListener(ISyncStatusObserver callback); } diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index e957e20..1b0437c 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -1381,8 +1381,8 @@ public class Intent implements Parcelable, Cloneable { * @hide */ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) - public static final String ACTION_MEDIA_RESOURCES_AVAILABLE = - "android.intent.action.MEDIA_RESOURCES_AVAILABILE"; + public static final String ACTION_EXTERNAL_APPLICATIONS_AVAILABLE = + "android.intent.action.EXTERNAL_APPLICATIONS_AVAILABLE"; /** * Broadcast Action: Resources for a set of packages are currently @@ -1406,8 +1406,8 @@ public class Intent implements Parcelable, Cloneable { * @hide */ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) - public static final String ACTION_MEDIA_RESOURCES_UNAVAILABLE = - "android.intent.action.MEDIA_RESOURCES_UNAVAILABILE"; + public static final String ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE = + "android.intent.action.EXTERNAL_APPLICATIONS_UNAVAILABILE"; /** * Broadcast Action: The current system wallpaper has changed. See @@ -1815,9 +1815,18 @@ public class Intent implements Parcelable, Cloneable { /** * Broadcast Action: A sticky broadcast indicating the phone was docked - * or undocked. Includes the extra - * field {@link #EXTRA_DOCK_STATE}, containing the current dock state. - * This is intended for monitoring the current dock state. + * or undocked. + * + * <p>The intent will have the following extra values: + * <ul> + * <li><em>{@link #EXTRA_DOCK_STATE}</em> - the current dock + * state, which depends on the state of the car mode.</li> + * <li><em>{@link #EXTRA_PHYSICAL_DOCK_STATE}</em> - the physical dock + * state.</li> + * <li><em>{@link #EXTRA_CAR_MODE_ENABLED}</em> - a boolean indicating the + * state of the car mode.</li> + * </ul> + * <p>This is intended for monitoring the current dock state. * To launch an activity from a dock state change, use {@link #CATEGORY_CAR_DOCK} * or {@link #CATEGORY_DESK_DOCK} instead. */ @@ -1838,7 +1847,7 @@ public class Intent implements Parcelable, Cloneable { * @hide */ public static final String ACTION_REMOTE_INTENT = - "android.intent.action.REMOTE_INTENT"; + "com.google.android.pushmessaging.intent.RECEIVE"; /** * Broadcast Action: hook for permforming cleanup after a system update. @@ -2152,6 +2161,22 @@ public class Intent implements Parcelable, Cloneable { public static final int EXTRA_DOCK_STATE_CAR = 2; /** + * Used as an int extra field in {@link android.content.Intent#ACTION_DOCK_EVENT} + * intents to request the physical dock state. Possible values are + * {@link android.content.Intent#EXTRA_DOCK_STATE_UNDOCKED}, + * {@link android.content.Intent#EXTRA_DOCK_STATE_DESK}, or + * {@link android.content.Intent#EXTRA_DOCK_STATE_CAR}. + */ + public static final String EXTRA_PHYSICAL_DOCK_STATE = + "android.intent.extra.PHYSICAL_DOCK_STATE"; + + /** + * Used as an boolean extra field in {@link android.content.Intent#ACTION_DOCK_EVENT} + * intents to indicate that the car mode is enabled or not. + */ + public static final String EXTRA_CAR_MODE_ENABLED = "android.intent.extra.CAR_MODE_ENABLED"; + + /** * Boolean that can be supplied as meta-data with a dock activity, to * indicate that the dock should take over the home key when it is active. */ @@ -2198,8 +2223,8 @@ public class Intent implements Parcelable, Cloneable { /** * This field is part of - * {@link android.content.Intent#ACTION_MEDIA_RESOURCES_AVAILABLE}, - * {@link android.content.Intent#ACTION_MEDIA_RESOURCES_UNAVAILABLE} + * {@link android.content.Intent#ACTION_EXTERNAL_APPLICATIONS_AVAILABLE}, + * {@link android.content.Intent#ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE} * and contains a string array of all of the components that have changed. * @hide */ @@ -2208,8 +2233,8 @@ public class Intent implements Parcelable, Cloneable { /** * This field is part of - * {@link android.content.Intent#ACTION_MEDIA_RESOURCES_AVAILABLE}, - * {@link android.content.Intent#ACTION_MEDIA_RESOURCES_UNAVAILABLE} + * {@link android.content.Intent#ACTION_EXTERNAL_APPLICATIONS_AVAILABLE}, + * {@link android.content.Intent#ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE} * and contains an integer array of uids of all of the components * that have changed. * @hide diff --git a/core/java/com/google/android/net/ParentalControlState.aidl b/core/java/android/content/PeriodicSync.aidl index ed1326a..4530591 100644 --- a/core/java/com/google/android/net/ParentalControlState.aidl +++ b/core/java/android/content/PeriodicSync.aidl @@ -1,11 +1,11 @@ -/** - * Copyright (c) 2008, The Android Open Source Project +/* + * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -14,5 +14,6 @@ * limitations under the License. */ -package com.google.android.net; -parcelable ParentalControlState; +package android.content; + +parcelable PeriodicSync; diff --git a/core/java/android/content/PeriodicSync.java b/core/java/android/content/PeriodicSync.java new file mode 100644 index 0000000..17813ec --- /dev/null +++ b/core/java/android/content/PeriodicSync.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content; + +import android.os.Parcelable; +import android.os.Bundle; +import android.os.Parcel; +import android.accounts.Account; + +/** + * Value type that contains information about a periodic sync. Is parcelable, making it suitable + * for passing in an IPC. + */ +public class PeriodicSync implements Parcelable { + /** The account to be synced */ + public final Account account; + /** The authority of the sync */ + public final String authority; + /** Any extras that parameters that are to be passed to the sync adapter. */ + public final Bundle extras; + /** How frequently the sync should be scheduled, in seconds. */ + public final long period; + + /** Creates a new PeriodicSync, copying the Bundle */ + public PeriodicSync(Account account, String authority, Bundle extras, long period) { + this.account = account; + this.authority = authority; + this.extras = new Bundle(extras); + this.period = period; + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + account.writeToParcel(dest, flags); + dest.writeString(authority); + dest.writeBundle(extras); + dest.writeLong(period); + } + + public static final Creator<PeriodicSync> CREATOR = new Creator<PeriodicSync>() { + public PeriodicSync createFromParcel(Parcel source) { + return new PeriodicSync(Account.CREATOR.createFromParcel(source), + source.readString(), source.readBundle(), source.readLong()); + } + + public PeriodicSync[] newArray(int size) { + return new PeriodicSync[size]; + } + }; + + public boolean equals(Object o) { + if (o == this) { + return true; + } + + if (!(o instanceof PeriodicSync)) { + return false; + } + + final PeriodicSync other = (PeriodicSync) o; + + return account.equals(other.account) + && authority.equals(other.authority) + && period == other.period + && SyncStorageEngine.equals(extras, other.extras); + } +} diff --git a/core/java/android/content/SyncManager.java b/core/java/android/content/SyncManager.java index 699b61d..317e5a9 100644 --- a/core/java/android/content/SyncManager.java +++ b/core/java/android/content/SyncManager.java @@ -52,14 +52,7 @@ import android.util.EventLog; import android.util.Log; import android.util.Pair; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.File; import java.io.FileDescriptor; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; import java.util.HashSet; @@ -74,12 +67,6 @@ import java.util.concurrent.CountDownLatch; public class SyncManager implements OnAccountsUpdateListener { private static final String TAG = "SyncManager"; - // used during dumping of the Sync history - private static final long MILLIS_IN_HOUR = 1000 * 60 * 60; - private static final long MILLIS_IN_DAY = MILLIS_IN_HOUR * 24; - private static final long MILLIS_IN_WEEK = MILLIS_IN_DAY * 7; - private static final long MILLIS_IN_4WEEKS = MILLIS_IN_WEEK * 4; - /** Delay a sync due to local changes this long. In milliseconds */ private static final long LOCAL_SYNC_DELAY; @@ -128,6 +115,11 @@ public class SyncManager implements OnAccountsUpdateListener { private static final long DEFAULT_MAX_SYNC_RETRY_TIME_IN_SECONDS = 60 * 60; // one hour /** + * How long to wait before retrying a sync that failed due to one already being in progress. + */ + private static final int DELAY_RETRY_SYNC_IN_PROGRESS_IN_SECONDS = 10; + + /** * An error notification is sent if sync of any of the providers has been failing for this long. */ private static final long ERROR_NOTIFICATION_DELAY_MS = 1000 * 60 * 10; // 10 minutes @@ -137,7 +129,7 @@ public class SyncManager implements OnAccountsUpdateListener { private Context mContext; - private volatile Account[] mAccounts = null; + private volatile Account[] mAccounts = INITIAL_ACCOUNTS_ARRAY; volatile private PowerManager.WakeLock mSyncWakeLock; volatile private PowerManager.WakeLock mHandleAlarmWakeLock; @@ -157,9 +149,7 @@ public class SyncManager implements OnAccountsUpdateListener { // set if the sync active indicator should be reported private boolean mNeedSyncActiveNotification = false; - private volatile boolean mSyncPollInitialized; private final PendingIntent mSyncAlarmIntent; - private final PendingIntent mSyncPollAlarmIntent; // Synchronized on "this". Instead of using this directly one should instead call // its accessor, getConnManager(). private ConnectivityManager mConnManagerDoNotUseDirectly; @@ -201,9 +191,11 @@ public class SyncManager implements OnAccountsUpdateListener { } }; + private static final Account[] INITIAL_ACCOUNTS_ARRAY = new Account[0]; + public void onAccountsUpdated(Account[] accounts) { // remember if this was the first time this was called after an update - final boolean justBootedUp = mAccounts == null; + final boolean justBootedUp = mAccounts == INITIAL_ACCOUNTS_ARRAY; mAccounts = accounts; // if a sync is in progress yet it is no longer in the accounts list, @@ -276,7 +268,6 @@ public class SyncManager implements OnAccountsUpdateListener { // ignore the rest of the states -- leave our boolean alone. } if (mDataConnectionIsConnected) { - initializeSyncPoll(); sendCheckAlarmsMessage(); } } @@ -291,14 +282,8 @@ public class SyncManager implements OnAccountsUpdateListener { }; private static final String ACTION_SYNC_ALARM = "android.content.syncmanager.SYNC_ALARM"; - private static final String SYNC_POLL_ALARM = "android.content.syncmanager.SYNC_POLL_ALARM"; private final SyncHandler mSyncHandler; - private static final int MAX_SYNC_POLL_DELAY_SECONDS = 36 * 60 * 60; // 36 hours - private static final int MIN_SYNC_POLL_DELAY_SECONDS = 24 * 60 * 60; // 24 hours - - private static final String SYNCMANAGER_PREFS_FILENAME = "/data/system/syncmanager.prefs"; - private volatile boolean mBootCompleted = false; private ConnectivityManager getConnectivityManager() { @@ -338,9 +323,6 @@ public class SyncManager implements OnAccountsUpdateListener { mSyncAlarmIntent = PendingIntent.getBroadcast( mContext, 0 /* ignored */, new Intent(ACTION_SYNC_ALARM), 0); - mSyncPollAlarmIntent = PendingIntent.getBroadcast( - mContext, 0 /* ignored */, new Intent(SYNC_POLL_ALARM), 0); - IntentFilter intentFilter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION); context.registerReceiver(mConnectivityIntentReceiver, intentFilter); @@ -396,49 +378,6 @@ public class SyncManager implements OnAccountsUpdateListener { } } - private synchronized void initializeSyncPoll() { - if (mSyncPollInitialized) return; - mSyncPollInitialized = true; - - mContext.registerReceiver(new SyncPollAlarmReceiver(), new IntentFilter(SYNC_POLL_ALARM)); - - // load the next poll time from shared preferences - long absoluteAlarmTime = readSyncPollTime(); - - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "initializeSyncPoll: absoluteAlarmTime is " + absoluteAlarmTime); - } - - // Convert absoluteAlarmTime to elapsed realtime. If this time was in the past then - // schedule the poll immediately, if it is too far in the future then cap it at - // MAX_SYNC_POLL_DELAY_SECONDS. - long absoluteNow = System.currentTimeMillis(); - long relativeNow = SystemClock.elapsedRealtime(); - long relativeAlarmTime = relativeNow; - if (absoluteAlarmTime > absoluteNow) { - long delayInMs = absoluteAlarmTime - absoluteNow; - final int maxDelayInMs = MAX_SYNC_POLL_DELAY_SECONDS * 1000; - if (delayInMs > maxDelayInMs) { - delayInMs = MAX_SYNC_POLL_DELAY_SECONDS * 1000; - } - relativeAlarmTime += delayInMs; - } - - // schedule an alarm for the next poll time - scheduleSyncPollAlarm(relativeAlarmTime); - } - - private void scheduleSyncPollAlarm(long relativeAlarmTime) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "scheduleSyncPollAlarm: relativeAlarmTime is " + relativeAlarmTime - + ", now is " + SystemClock.elapsedRealtime() - + ", delay is " + (relativeAlarmTime - SystemClock.elapsedRealtime())); - } - ensureAlarmService(); - mAlarmService.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, relativeAlarmTime, - mSyncPollAlarmIntent); - } - /** * Return a random value v that satisfies minValue <= v < maxValue. The difference between * maxValue and minValue must be less than Integer.MAX_VALUE. @@ -453,68 +392,6 @@ public class SyncManager implements OnAccountsUpdateListener { return minValue + random.nextInt((int)spread); } - private void handleSyncPollAlarm() { - // determine the next poll time - long delayMs = jitterize(MIN_SYNC_POLL_DELAY_SECONDS, MAX_SYNC_POLL_DELAY_SECONDS) * 1000; - long nextRelativePollTimeMs = SystemClock.elapsedRealtime() + delayMs; - - if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, "handleSyncPollAlarm: delay " + delayMs); - - // write the absolute time to shared preferences - writeSyncPollTime(System.currentTimeMillis() + delayMs); - - // schedule an alarm for the next poll time - scheduleSyncPollAlarm(nextRelativePollTimeMs); - - // perform a poll - scheduleSync(null /* sync all syncable accounts */, null /* sync all syncable providers */, - new Bundle(), 0 /* no delay */, false /* onlyThoseWithUnkownSyncableState */); - } - - private void writeSyncPollTime(long when) { - File f = new File(SYNCMANAGER_PREFS_FILENAME); - DataOutputStream str = null; - try { - str = new DataOutputStream(new FileOutputStream(f)); - str.writeLong(when); - } catch (FileNotFoundException e) { - Log.w(TAG, "error writing to file " + f, e); - } catch (IOException e) { - Log.w(TAG, "error writing to file " + f, e); - } finally { - if (str != null) { - try { - str.close(); - } catch (IOException e) { - Log.w(TAG, "error closing file " + f, e); - } - } - } - } - - private long readSyncPollTime() { - File f = new File(SYNCMANAGER_PREFS_FILENAME); - - DataInputStream str = null; - try { - str = new DataInputStream(new FileInputStream(f)); - return str.readLong(); - } catch (FileNotFoundException e) { - writeSyncPollTime(0); - } catch (IOException e) { - Log.w(TAG, "error reading file " + f, e); - } finally { - if (str != null) { - try { - str.close(); - } catch (IOException e) { - Log.w(TAG, "error closing file " + f, e); - } - } - } - return 0; - } - public ActiveSyncContext getActiveSyncContext() { return mActiveSyncContext; } @@ -575,17 +452,6 @@ public class SyncManager implements OnAccountsUpdateListener { } /** - * Returns whether or not sync is enabled. Sync can be enabled by - * setting the system property "ro.config.sync" to the value "yes". - * This is normally done at boot time on builds that support sync. - * @return true if sync is enabled - */ - private boolean isSyncEnabled() { - // Require the precise value "yes" to discourage accidental activation. - return "yes".equals(SystemProperties.get("ro.config.sync")); - } - - /** * Initiate a sync. This can start a sync for all providers * (pass null to url, set onlyTicklable to false), only those * providers that are marked as ticklable (pass null to url, @@ -616,18 +482,6 @@ public class SyncManager implements OnAccountsUpdateListener { Bundle extras, long delay, boolean onlyThoseWithUnkownSyncableState) { boolean isLoggable = Log.isLoggable(TAG, Log.VERBOSE); - if (mAccounts == null) { - Log.e(TAG, "scheduleSync: the accounts aren't known yet, this should never happen"); - return; - } - - if (!isSyncEnabled()) { - if (isLoggable) { - Log.v(TAG, "not syncing because sync is disabled"); - } - return; - } - final boolean backgroundDataUsageAllowed = !mBootCompleted || getConnectivityManager().getBackgroundDataSetting(); @@ -645,13 +499,6 @@ public class SyncManager implements OnAccountsUpdateListener { // if the accounts aren't configured yet then we can't support an account-less // sync request accounts = mAccounts; - if (accounts == null) { - // not ready yet - if (isLoggable) { - Log.v(TAG, "scheduleSync: no accounts yet, dropping"); - } - return; - } if (accounts.length == 0) { if (isLoggable) { Log.v(TAG, "scheduleSync: no accounts configured, dropping"); @@ -662,6 +509,12 @@ public class SyncManager implements OnAccountsUpdateListener { final boolean uploadOnly = extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, false); final boolean manualSync = extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false); + if (manualSync) { + extras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, true); + extras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, true); + } + final boolean ignoreSettings = + extras.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, false); int source; if (uploadOnly) { @@ -720,7 +573,7 @@ public class SyncManager implements OnAccountsUpdateListener { final boolean syncAutomatically = masterSyncAutomatically && mSyncStorageEngine.getSyncAutomatically(account, authority); boolean syncAllowed = - manualSync || (backgroundDataUsageAllowed && syncAutomatically); + ignoreSettings || (backgroundDataUsageAllowed && syncAutomatically); if (!syncAllowed) { if (isLoggable) { Log.d(TAG, "scheduleSync: sync of " + account + ", " + authority @@ -799,12 +652,6 @@ public class SyncManager implements OnAccountsUpdateListener { } } - class SyncPollAlarmReceiver extends BroadcastReceiver { - public void onReceive(Context context, Intent intent) { - handleSyncPollAlarm(); - } - } - private void clearBackoffSetting(SyncOperation op) { mSyncStorageEngine.setBackoff(op.account, op.authority, SyncStorageEngine.NOT_IN_BACKOFF_MODE, SyncStorageEngine.NOT_IN_BACKOFF_MODE); @@ -923,7 +770,7 @@ public class SyncManager implements OnAccountsUpdateListener { mSyncStorageEngine.setBackoff(account, authority, SyncStorageEngine.NOT_IN_BACKOFF_MODE, SyncStorageEngine.NOT_IN_BACKOFF_MODE); synchronized (mSyncQueue) { - mSyncQueue.clear(account, authority); + mSyncQueue.remove(account, authority); } } @@ -933,21 +780,29 @@ public class SyncManager implements OnAccountsUpdateListener { Log.d(TAG, "encountered error(s) during the sync: " + syncResult + ", " + operation); } + operation = new SyncOperation(operation); + + // The SYNC_EXTRAS_IGNORE_BACKOFF only applies to the first attempt to sync a given + // request. Retries of the request will always honor the backoff, so clear the + // flag in case we retry this request. + if (operation.extras.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, false)) { + operation.extras.remove(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF); + } + // If this sync aborted because the internal sync loop retried too many times then // don't reschedule. Otherwise we risk getting into a retry loop. // If the operation succeeded to some extent then retry immediately. // If this was a two-way sync then retry soft errors with an exponential backoff. // If this was an upward sync then schedule a two-way sync immediately. // Otherwise do not reschedule. - if (operation.extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false)) { - Log.d(TAG, "not retrying sync operation because it is a manual sync: " + if (operation.extras.getBoolean(ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY, false)) { + Log.d(TAG, "not retrying sync operation because SYNC_EXTRAS_DO_NOT_RETRY was specified " + operation); } else if (operation.extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD, false)) { - final SyncOperation newSyncOperation = new SyncOperation(operation); - newSyncOperation.extras.remove(ContentResolver.SYNC_EXTRAS_UPLOAD); + operation.extras.remove(ContentResolver.SYNC_EXTRAS_UPLOAD); Log.d(TAG, "retrying sync operation as a two-way sync because an upload-only sync " + "encountered an error: " + operation); - scheduleSyncOperation(newSyncOperation); + scheduleSyncOperation(operation); } else if (syncResult.tooManyRetries) { Log.d(TAG, "not retrying sync operation because it retried too many times: " + operation); @@ -956,13 +811,21 @@ public class SyncManager implements OnAccountsUpdateListener { Log.d(TAG, "retrying sync operation because even though it had an error " + "it achieved some success"); } - scheduleSyncOperation(new SyncOperation(operation)); + scheduleSyncOperation(operation); + } else if (syncResult.syncAlreadyInProgress) { + if (isLoggable) { + Log.d(TAG, "retrying sync operation that failed because there was already a " + + "sync in progress: " + operation); + } + scheduleSyncOperation(new SyncOperation(operation.account, operation.syncSource, + operation.authority, operation.extras, + DELAY_RETRY_SYNC_IN_PROGRESS_IN_SECONDS)); } else if (syncResult.hasSoftError()) { if (isLoggable) { Log.d(TAG, "retrying sync operation because it encountered a soft error: " + operation); } - scheduleSyncOperation(new SyncOperation(operation)); + scheduleSyncOperation(operation); } else { Log.d(TAG, "not retrying sync operation because the error is a hard error: " + operation); @@ -1054,9 +917,7 @@ public class SyncManager implements OnAccountsUpdateListener { protected void dump(FileDescriptor fd, PrintWriter pw) { StringBuilder sb = new StringBuilder(); dumpSyncState(pw, sb); - if (isSyncEnabled()) { - dumpSyncHistory(pw, sb); - } + dumpSyncHistory(pw, sb); pw.println(); pw.println("SyncAdapters:"); @@ -1072,19 +933,19 @@ public class SyncManager implements OnAccountsUpdateListener { } protected void dumpSyncState(PrintWriter pw, StringBuilder sb) { - pw.print("sync enabled: "); pw.println(isSyncEnabled()); pw.print("data connected: "); pw.println(mDataConnectionIsConnected); pw.print("memory low: "); pw.println(mStorageIsLow); final Account[] accounts = mAccounts; pw.print("accounts: "); - if (accounts != null) { + if (accounts != INITIAL_ACCOUNTS_ARRAY) { pw.println(accounts.length); } else { - pw.println("none"); + pw.println("not known yet"); } final long now = SystemClock.elapsedRealtime(); - pw.print("now: "); pw.println(now); + pw.print("now: "); pw.print(now); + pw.println(" (" + formatTime(System.currentTimeMillis()) + ")"); pw.print("uptime: "); pw.print(DateUtils.formatElapsedTime(now/1000)); pw.println(" (HH:MM:SS)"); pw.print("time spent syncing: "); @@ -1102,7 +963,9 @@ public class SyncManager implements OnAccountsUpdateListener { pw.println("no alarm is scheduled (there had better not be any pending syncs)"); } - pw.print("active sync: "); pw.println(mActiveSyncContext); + final SyncManager.ActiveSyncContext activeSyncContext = mActiveSyncContext; + + pw.print("active sync: "); pw.println(activeSyncContext); pw.print("notification info: "); sb.setLength(0); @@ -1125,6 +988,11 @@ public class SyncManager implements OnAccountsUpdateListener { pw.print(authority != null ? authority.account : "<no account>"); pw.print(" "); pw.print(authority != null ? authority.authority : "<no account>"); + if (activeSyncContext != null) { + pw.print(" "); + pw.print(SyncStorageEngine.SOURCES[ + activeSyncContext.mSyncOperation.syncSource]); + } pw.print(", duration is "); pw.println(DateUtils.formatElapsedTime(durationInSeconds)); } else { @@ -1152,80 +1020,76 @@ public class SyncManager implements OnAccountsUpdateListener { } } - HashSet<Account> processedAccounts = new HashSet<Account>(); - ArrayList<SyncStatusInfo> statuses - = mSyncStorageEngine.getSyncStatus(); - if (statuses != null && statuses.size() > 0) { - pw.println(); - pw.println("Sync Status"); - final int N = statuses.size(); - for (int i=0; i<N; i++) { - SyncStatusInfo status = statuses.get(i); - SyncStorageEngine.AuthorityInfo authority - = mSyncStorageEngine.getAuthority(status.authorityId); - if (authority != null) { - Account curAccount = authority.account; - - if (processedAccounts.contains(curAccount)) { - continue; - } - - processedAccounts.add(curAccount); + // join the installed sync adapter with the accounts list and emit for everything + pw.println(); + pw.println("Sync Status"); + for (Account account : accounts) { + pw.print(" Account "); pw.print(account.name); + pw.print(" "); pw.print(account.type); + pw.println(":"); + for (RegisteredServicesCache.ServiceInfo<SyncAdapterType> syncAdapterType : + mSyncAdapters.getAllServices()) { + if (!syncAdapterType.type.accountType.equals(account.type)) { + continue; + } - pw.print(" Account "); pw.print(authority.account.name); - pw.print(" "); pw.print(authority.account.type); - pw.println(":"); - for (int j=i; j<N; j++) { - status = statuses.get(j); - authority = mSyncStorageEngine.getAuthority(status.authorityId); - if (!curAccount.equals(authority.account)) { - continue; - } - pw.print(" "); pw.print(authority.authority); - pw.println(":"); - final String syncable = authority.syncable > 0 - ? "syncable" - : (authority.syncable == 0 ? "not syncable" : "not initialized"); - final String enabled = authority.enabled ? "enabled" : "disabled"; - final String delayUntil = authority.delayUntil > now - ? "delay for " + ((authority.delayUntil - now) / 1000) + " sec" - : "no delay required"; - final String backoff = authority.backoffTime > now - ? "backoff for " + ((authority.backoffTime - now) / 1000) - + " sec" - : "no backoff required"; - final String backoffDelay = authority.backoffDelay > 0 - ? ("the backoff increment is " + authority.backoffDelay / 1000 - + " sec") - : "no backoff increment"; - pw.println(String.format( - " settings: %s, %s, %s, %s, %s", - enabled, syncable, backoff, backoffDelay, delayUntil)); - pw.print(" count: local="); pw.print(status.numSourceLocal); - pw.print(" poll="); pw.print(status.numSourcePoll); - pw.print(" server="); pw.print(status.numSourceServer); - pw.print(" user="); pw.print(status.numSourceUser); - pw.print(" total="); pw.println(status.numSyncs); - pw.print(" total duration: "); - pw.println(DateUtils.formatElapsedTime( - status.totalElapsedTime/1000)); - if (status.lastSuccessTime != 0) { - pw.print(" SUCCESS: source="); - pw.print(SyncStorageEngine.SOURCES[ - status.lastSuccessSource]); - pw.print(" time="); - pw.println(formatTime(status.lastSuccessTime)); - } else { - pw.print(" FAILURE: source="); - pw.print(SyncStorageEngine.SOURCES[ - status.lastFailureSource]); - pw.print(" initialTime="); - pw.print(formatTime(status.initialFailureTime)); - pw.print(" lastTime="); - pw.println(formatTime(status.lastFailureTime)); - pw.print(" message: "); pw.println(status.lastFailureMesg); - } - } + SyncStorageEngine.AuthorityInfo settings = mSyncStorageEngine.getOrCreateAuthority( + account, syncAdapterType.type.authority); + SyncStatusInfo status = mSyncStorageEngine.getOrCreateSyncStatus(settings); + pw.print(" "); pw.print(settings.authority); + pw.println(":"); + pw.print(" settings:"); + pw.print(" " + (settings.syncable > 0 + ? "syncable" + : (settings.syncable == 0 ? "not syncable" : "not initialized"))); + pw.print(", " + (settings.enabled ? "enabled" : "disabled")); + if (settings.delayUntil > now) { + pw.print(", delay for " + + ((settings.delayUntil - now) / 1000) + " sec"); + } + if (settings.backoffTime > now) { + pw.print(", backoff for " + + ((settings.backoffTime - now) / 1000) + " sec"); + } + if (settings.backoffDelay > 0) { + pw.print(", the backoff increment is " + settings.backoffDelay / 1000 + + " sec"); + } + pw.println(); + for (int periodicIndex = 0; + periodicIndex < settings.periodicSyncs.size(); + periodicIndex++) { + Pair<Bundle, Long> info = settings.periodicSyncs.get(periodicIndex); + long lastPeriodicTime = status.getPeriodicSyncTime(periodicIndex); + long nextPeriodicTime = lastPeriodicTime + info.second * 1000; + pw.println(" periodic period=" + info.second + + ", extras=" + info.first + + ", next=" + formatTime(nextPeriodicTime)); + } + pw.print(" count: local="); pw.print(status.numSourceLocal); + pw.print(" poll="); pw.print(status.numSourcePoll); + pw.print(" periodic="); pw.print(status.numSourcePeriodic); + pw.print(" server="); pw.print(status.numSourceServer); + pw.print(" user="); pw.print(status.numSourceUser); + pw.print(" total="); pw.print(status.numSyncs); + pw.println(); + pw.print(" total duration: "); + pw.println(DateUtils.formatElapsedTime(status.totalElapsedTime/1000)); + if (status.lastSuccessTime != 0) { + pw.print(" SUCCESS: source="); + pw.print(SyncStorageEngine.SOURCES[status.lastSuccessSource]); + pw.print(" time="); + pw.println(formatTime(status.lastSuccessTime)); + } + if (status.lastFailureTime != 0) { + pw.print(" FAILURE: source="); + pw.print(SyncStorageEngine.SOURCES[ + status.lastFailureSource]); + pw.print(" initialTime="); + pw.print(formatTime(status.initialFailureTime)); + pw.print(" lastTime="); + pw.println(formatTime(status.lastFailureTime)); + pw.print(" message: "); pw.println(status.lastFailureMesg); } } } @@ -1580,6 +1444,31 @@ public class SyncManager implements OnAccountsUpdateListener { } } + private boolean isSyncAllowed(Account account, String authority, boolean ignoreSettings, + boolean backgroundDataUsageAllowed) { + Account[] accounts = mAccounts; + + // skip the sync if the account of this operation no longer exists + if (!ArrayUtils.contains(accounts, account)) { + return false; + } + + // skip the sync if it isn't manual and auto sync is disabled + final boolean syncAutomatically = + mSyncStorageEngine.getSyncAutomatically(account, authority) + && mSyncStorageEngine.getMasterSyncAutomatically(); + if (!(ignoreSettings || (backgroundDataUsageAllowed && syncAutomatically))) { + return false; + } + + if (mSyncStorageEngine.getIsSyncable(account, authority) <= 0) { + // if not syncable or if the syncable is unknown (< 0), don't allow + return false; + } + + return true; + } + private void runStateSyncing() { // if the sync timeout has been reached then cancel it @@ -1589,7 +1478,7 @@ public class SyncManager implements OnAccountsUpdateListener { if (now > activeSyncContext.mTimeoutStartTime + MAX_TIME_PER_SYNC) { SyncOperation nextSyncOperation; synchronized (mSyncQueue) { - nextSyncOperation = mSyncQueue.nextReadyToRun(now); + nextSyncOperation = getNextReadyToRunSyncOperation(now); } if (nextSyncOperation != null) { Log.d(TAG, "canceling and rescheduling sync because it ran too long: " @@ -1627,7 +1516,7 @@ public class SyncManager implements OnAccountsUpdateListener { // 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. Account[] accounts = mAccounts; - if (accounts == null) { + if (accounts == INITIAL_ACCOUNTS_ARRAY) { if (isLoggable) { Log.v(TAG, "runStateIdle: accounts not known, skipping"); } @@ -1643,7 +1532,7 @@ public class SyncManager implements OnAccountsUpdateListener { synchronized (mSyncQueue) { final long now = SystemClock.elapsedRealtime(); while (true) { - op = mSyncQueue.nextReadyToRun(now); + op = getNextReadyToRunSyncOperation(now); if (op == null) { if (isLoggable) { Log.v(TAG, "runStateIdle: no more sync operations, returning"); @@ -1655,42 +1544,10 @@ public class SyncManager implements OnAccountsUpdateListener { // from the queue now mSyncQueue.remove(op); - // Sync is disabled, drop this operation. - if (!isSyncEnabled()) { - if (isLoggable) { - Log.v(TAG, "runStateIdle: sync disabled, dropping " + op); - } - continue; - } - - // skip the sync if the account of this operation no longer exists - if (!ArrayUtils.contains(accounts, op.account)) { - if (isLoggable) { - Log.v(TAG, "runStateIdle: account not present, dropping " + op); - } - continue; - } - - // skip the sync if it isn't manual and auto sync is disabled - final boolean manualSync = - op.extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false); - final boolean syncAutomatically = - mSyncStorageEngine.getSyncAutomatically(op.account, op.authority) - && mSyncStorageEngine.getMasterSyncAutomatically(); - if (!(manualSync || (backgroundDataUsageAllowed && syncAutomatically))) { - if (isLoggable) { - Log.v(TAG, "runStateIdle: sync of this operation is not allowed, " - + "dropping " + op); - } - continue; - } - - if (mSyncStorageEngine.getIsSyncable(op.account, op.authority) <= 0) { - // if not syncable or if the syncable is unknown (< 0), don't allow - if (isLoggable) { - Log.v(TAG, "runStateIdle: sync of this operation is not allowed, " - + "dropping " + op); - } + final boolean ignoreSettings = op.extras + .getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, false); + if (!isSyncAllowed(op.account, op.authority, ignoreSettings, + backgroundDataUsageAllowed)) { continue; } @@ -1736,6 +1593,74 @@ public class SyncManager implements OnAccountsUpdateListener { // MESSAGE_SERVICE_CONNECTED or MESSAGE_SERVICE_DISCONNECTED message } + private SyncOperation getNextPeriodicSyncOperation() { + final boolean backgroundDataUsageAllowed = + getConnectivityManager().getBackgroundDataSetting(); + SyncStorageEngine.AuthorityInfo best = null; + long bestPollTimeAbsolute = Long.MAX_VALUE; + Bundle bestExtras = null; + ArrayList<SyncStorageEngine.AuthorityInfo> infos = mSyncStorageEngine.getAuthorities(); + for (SyncStorageEngine.AuthorityInfo info : infos) { + if (!isSyncAllowed(info.account, info.authority, false /* ignoreSettings */, + backgroundDataUsageAllowed)) { + continue; + } + SyncStatusInfo status = mSyncStorageEngine.getStatusByAccountAndAuthority( + info.account, info.authority); + int i = 0; + for (Pair<Bundle, Long> periodicSync : info.periodicSyncs) { + long lastPollTimeAbsolute = status != null ? status.getPeriodicSyncTime(i) : 0; + final Bundle extras = periodicSync.first; + final Long periodInSeconds = periodicSync.second; + long nextPollTimeAbsolute = lastPollTimeAbsolute + periodInSeconds * 1000; + if (nextPollTimeAbsolute < bestPollTimeAbsolute) { + best = info; + bestPollTimeAbsolute = nextPollTimeAbsolute; + bestExtras = extras; + } + i++; + } + } + + if (best == null) { + return null; + } + + final long nowAbsolute = System.currentTimeMillis(); + final SyncOperation syncOperation = new SyncOperation(best.account, + SyncStorageEngine.SOURCE_PERIODIC, + best.authority, bestExtras, 0 /* delay */); + syncOperation.earliestRunTime = SystemClock.elapsedRealtime() + + (bestPollTimeAbsolute - nowAbsolute); + if (syncOperation.earliestRunTime < 0) { + syncOperation.earliestRunTime = 0; + } + return syncOperation; + } + + public Pair<SyncOperation, Long> bestSyncOperationCandidate() { + Pair<SyncOperation, Long> nextOpAndRunTime = mSyncQueue.nextOperation(); + SyncOperation nextOp = nextOpAndRunTime != null ? nextOpAndRunTime.first : null; + Long nextRunTime = nextOpAndRunTime != null ? nextOpAndRunTime.second : null; + SyncOperation pollOp = getNextPeriodicSyncOperation(); + if (nextOp != null + && (pollOp == null || nextOp.expedited + || nextRunTime <= pollOp.earliestRunTime)) { + return nextOpAndRunTime; + } else if (pollOp != null) { + return Pair.create(pollOp, pollOp.earliestRunTime); + } else { + return null; + } + } + + private SyncOperation getNextReadyToRunSyncOperation(long now) { + Pair<SyncOperation, Long> nextOpAndRunTime = bestSyncOperationCandidate(); + return nextOpAndRunTime != null && nextOpAndRunTime.second <= now + ? nextOpAndRunTime.first + : null; + } + private void runBoundToSyncAdapter(ISyncAdapter syncAdapter) { mActiveSyncContext.mSyncAdapter = syncAdapter; final SyncOperation syncOperation = mActiveSyncContext.mSyncOperation; @@ -1948,7 +1873,6 @@ public class SyncManager implements OnAccountsUpdateListener { // in each of these cases the sync loop will be kicked, which will cause this // method to be called again if (!mDataConnectionIsConnected) return; - if (mAccounts == null) return; if (mStorageIsLow) return; final long now = SystemClock.elapsedRealtime(); @@ -1961,7 +1885,8 @@ public class SyncManager implements OnAccountsUpdateListener { ActiveSyncContext activeSyncContext = mActiveSyncContext; if (activeSyncContext == null) { synchronized (mSyncQueue) { - alarmTime = mSyncQueue.nextRunTime(now); + Pair<SyncOperation, Long> candidate = bestSyncOperationCandidate(); + alarmTime = candidate != null ? candidate.second : null; } } else { final long notificationTime = @@ -2102,9 +2027,8 @@ public class SyncManager implements OnAccountsUpdateListener { SyncStorageEngine.EVENT_STOP, syncOperation.syncSource, syncOperation.account.name.hashCode()); - mSyncStorageEngine.stopSyncEvent(rowId, elapsedTime, resultMessage, - downstreamActivity, upstreamActivity); + mSyncStorageEngine.stopSyncEvent(rowId, syncOperation.extras, elapsedTime, + resultMessage, downstreamActivity, upstreamActivity); } } - } diff --git a/core/java/android/content/SyncOperation.java b/core/java/android/content/SyncOperation.java index 2d6e833..4599165 100644 --- a/core/java/android/content/SyncOperation.java +++ b/core/java/android/content/SyncOperation.java @@ -26,6 +26,9 @@ public class SyncOperation implements Comparable { this.extras = new Bundle(extras); removeFalseExtra(ContentResolver.SYNC_EXTRAS_UPLOAD); removeFalseExtra(ContentResolver.SYNC_EXTRAS_MANUAL); + removeFalseExtra(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS); + removeFalseExtra(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF); + removeFalseExtra(ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY); removeFalseExtra(ContentResolver.SYNC_EXTRAS_DISCARD_LOCAL_DELETIONS); removeFalseExtra(ContentResolver.SYNC_EXTRAS_EXPEDITED); removeFalseExtra(ContentResolver.SYNC_EXTRAS_OVERRIDE_TOO_MANY_DELETIONS); diff --git a/core/java/android/content/SyncQueue.java b/core/java/android/content/SyncQueue.java index a9f15d9..bb21488 100644 --- a/core/java/android/content/SyncQueue.java +++ b/core/java/android/content/SyncQueue.java @@ -2,8 +2,6 @@ package android.content; import com.google.android.collect.Maps; -import android.os.Bundle; -import android.os.SystemClock; import android.util.Pair; import android.util.Log; import android.accounts.Account; @@ -32,10 +30,9 @@ public class SyncQueue { final int N = ops.size(); for (int i=0; i<N; i++) { SyncStorageEngine.PendingOperation op = ops.get(i); - // -1 is a special value that means expedited - final int delay = op.expedited ? -1 : 0; SyncOperation syncOperation = new SyncOperation( - op.account, op.syncSource, op.authority, op.extras, delay); + op.account, op.syncSource, op.authority, op.extras, 0 /* delay */); + syncOperation.expedited = op.expedited; syncOperation.pendingOperation = op; add(syncOperation, op); } @@ -90,8 +87,15 @@ public class SyncQueue { return true; } + /** + * Remove the specified operation if it is in the queue. + * @param operation the operation to remove + */ public void remove(SyncOperation operation) { SyncOperation operationToRemove = mOperationsMap.remove(operation.key); + if (operationToRemove == null) { + return; + } if (!mSyncStorageEngine.deleteFromPending(operationToRemove.pendingOperation)) { final String errorMessage = "unable to find pending row for " + operationToRemove; Log.e(TAG, errorMessage, new IllegalStateException(errorMessage)); @@ -102,54 +106,34 @@ public class SyncQueue { * Find the operation that should run next. Operations are sorted by their earliestRunTime, * prioritizing expedited operations. The earliestRunTime is adjusted by the sync adapter's * backoff and delayUntil times, if any. - * @param now the current {@link android.os.SystemClock#elapsedRealtime()} * @return the operation that should run next and when it should run. The time may be in * the future. It is expressed in milliseconds since boot. */ - private Pair<SyncOperation, Long> nextOperation(long now) { - SyncOperation lowestOp = null; - long lowestOpRunTime = 0; + public Pair<SyncOperation, Long> nextOperation() { + SyncOperation best = null; + long bestRunTime = 0; for (SyncOperation op : mOperationsMap.values()) { - // effectiveRunTime: - // - backoffTime > currentTime : backoffTime - // - backoffTime <= currentTime : op.runTime - Pair<Long, Long> backoff = null; - long delayUntilTime = 0; - final boolean isManualSync = - op.extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false); - if (!isManualSync) { - backoff = mSyncStorageEngine.getBackoff(op.account, op.authority); - delayUntilTime = mSyncStorageEngine.getDelayUntilTime(op.account, op.authority); + long opRunTime = op.earliestRunTime; + if (!op.extras.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, false)) { + Pair<Long, Long> backoff = mSyncStorageEngine.getBackoff(op.account, op.authority); + long delayUntil = mSyncStorageEngine.getDelayUntilTime(op.account, op.authority); + opRunTime = Math.max( + Math.max(opRunTime, delayUntil), + backoff != null ? backoff.first : 0); } - long backoffTime = Math.max(backoff != null ? backoff.first : 0, delayUntilTime); - long opRunTime = backoffTime > now ? backoffTime : op.earliestRunTime; - if (lowestOp == null - || (lowestOp.expedited == op.expedited - ? opRunTime < lowestOpRunTime - : op.expedited)) { - lowestOp = op; - lowestOpRunTime = opRunTime; + // if the expedited state of both ops are the same then compare their runtime. + // Otherwise the candidate is only better than the current best if the candidate + // is expedited. + if (best == null + || (best.expedited == op.expedited ? opRunTime < bestRunTime : op.expedited)) { + best = op; + bestRunTime = opRunTime; } } - if (lowestOp == null) { + if (best == null) { return null; } - return Pair.create(lowestOp, lowestOpRunTime); - } - - /** - * Return when the next SyncOperation will be ready to run or null if there are - * none. - * @param now the current {@link android.os.SystemClock#elapsedRealtime()}, used to - * decide if the sync operation is ready to run - * @return when the next SyncOperation will be ready to run, expressed in elapsedRealtime() - */ - public Long nextRunTime(long now) { - Pair<SyncOperation, Long> nextOpAndRunTime = nextOperation(now); - if (nextOpAndRunTime == null) { - return null; - } - return nextOpAndRunTime.second; + return Pair.create(best, bestRunTime); } /** @@ -158,21 +142,25 @@ public class SyncQueue { * decide if the sync operation is ready to run * @return the SyncOperation that should be run next and is ready to run. */ - public SyncOperation nextReadyToRun(long now) { - Pair<SyncOperation, Long> nextOpAndRunTime = nextOperation(now); + public Pair<SyncOperation, Long> nextReadyToRun(long now) { + Pair<SyncOperation, Long> nextOpAndRunTime = nextOperation(); if (nextOpAndRunTime == null || nextOpAndRunTime.second > now) { return null; } - return nextOpAndRunTime.first; + return nextOpAndRunTime; } - public void clear(Account account, String authority) { + public void remove(Account account, String authority) { Iterator<Map.Entry<String, SyncOperation>> entries = mOperationsMap.entrySet().iterator(); while (entries.hasNext()) { Map.Entry<String, SyncOperation> entry = entries.next(); SyncOperation syncOperation = entry.getValue(); - if (account != null && !syncOperation.account.equals(account)) continue; - if (authority != null && !syncOperation.authority.equals(authority)) continue; + if (account != null && !syncOperation.account.equals(account)) { + continue; + } + if (authority != null && !syncOperation.authority.equals(authority)) { + continue; + } entries.remove(); if (!mSyncStorageEngine.deleteFromPending(syncOperation.pendingOperation)) { final String errorMessage = "unable to find pending row for " + syncOperation; diff --git a/core/java/android/content/SyncStatusInfo.java b/core/java/android/content/SyncStatusInfo.java index b8fda03..bb2b2da 100644 --- a/core/java/android/content/SyncStatusInfo.java +++ b/core/java/android/content/SyncStatusInfo.java @@ -20,10 +20,12 @@ import android.os.Parcel; import android.os.Parcelable; import android.util.Log; +import java.util.ArrayList; + /** @hide */ public class SyncStatusInfo implements Parcelable { - static final int VERSION = 1; - + static final int VERSION = 2; + public final int authorityId; public long totalElapsedTime; public int numSyncs; @@ -31,6 +33,7 @@ public class SyncStatusInfo implements Parcelable { public int numSourceServer; public int numSourceLocal; public int numSourceUser; + public int numSourcePeriodic; public long lastSuccessTime; public int lastSuccessSource; public long lastFailureTime; @@ -39,7 +42,10 @@ public class SyncStatusInfo implements Parcelable { public long initialFailureTime; public boolean pending; public boolean initialize; - + public ArrayList<Long> periodicSyncTimes; + + private static final String TAG = "Sync"; + SyncStatusInfo(int authorityId) { this.authorityId = authorityId; } @@ -50,10 +56,11 @@ public class SyncStatusInfo implements Parcelable { return Integer.parseInt(lastFailureMesg); } } catch (NumberFormatException e) { + Log.d(TAG, "error parsing lastFailureMesg of " + lastFailureMesg, e); } return def; } - + public int describeContents() { return 0; } @@ -75,11 +82,19 @@ public class SyncStatusInfo implements Parcelable { parcel.writeLong(initialFailureTime); parcel.writeInt(pending ? 1 : 0); parcel.writeInt(initialize ? 1 : 0); + if (periodicSyncTimes != null) { + parcel.writeInt(periodicSyncTimes.size()); + for (long periodicSyncTime : periodicSyncTimes) { + parcel.writeLong(periodicSyncTime); + } + } else { + parcel.writeInt(-1); + } } SyncStatusInfo(Parcel parcel) { int version = parcel.readInt(); - if (version != VERSION) { + if (version != VERSION && version != 1) { Log.w("SyncStatusInfo", "Unknown version: " + version); } authorityId = parcel.readInt(); @@ -97,8 +112,51 @@ public class SyncStatusInfo implements Parcelable { initialFailureTime = parcel.readLong(); pending = parcel.readInt() != 0; initialize = parcel.readInt() != 0; + if (version == 1) { + periodicSyncTimes = null; + } else { + int N = parcel.readInt(); + if (N < 0) { + periodicSyncTimes = null; + } else { + periodicSyncTimes = new ArrayList<Long>(); + for (int i=0; i<N; i++) { + periodicSyncTimes.add(parcel.readLong()); + } + } + } } - + + public void setPeriodicSyncTime(int index, long when) { + ensurePeriodicSyncTimeSize(index); + periodicSyncTimes.set(index, when); + } + + private void ensurePeriodicSyncTimeSize(int index) { + if (periodicSyncTimes == null) { + periodicSyncTimes = new ArrayList<Long>(0); + } + + final int requiredSize = index + 1; + if (periodicSyncTimes.size() < requiredSize) { + for (int i = periodicSyncTimes.size(); i < requiredSize; i++) { + periodicSyncTimes.add((long) 0); + } + } + } + + public long getPeriodicSyncTime(int index) { + if (periodicSyncTimes == null || periodicSyncTimes.size() < (index + 1)) { + return 0; + } + return periodicSyncTimes.get(index); + } + + public void removePeriodicSyncTime(int index) { + ensurePeriodicSyncTimeSize(index); + periodicSyncTimes.remove(index); + } + public static final Creator<SyncStatusInfo> CREATOR = new Creator<SyncStatusInfo>() { public SyncStatusInfo createFromParcel(Parcel in) { return new SyncStatusInfo(in); diff --git a/core/java/android/content/SyncStorageEngine.java b/core/java/android/content/SyncStorageEngine.java index db70096..fcb910d 100644 --- a/core/java/android/content/SyncStorageEngine.java +++ b/core/java/android/content/SyncStorageEngine.java @@ -36,7 +36,6 @@ import android.os.Message; import android.os.Parcel; import android.os.RemoteCallbackList; import android.os.RemoteException; -import android.os.SystemClock; import android.util.Log; import android.util.SparseArray; import android.util.Xml; @@ -50,6 +49,7 @@ import java.util.Calendar; import java.util.HashMap; import java.util.Iterator; import java.util.TimeZone; +import java.util.List; /** * Singleton that tracks the sync data and overall sync @@ -62,6 +62,8 @@ public class SyncStorageEngine extends Handler { private static final boolean DEBUG = false; private static final boolean DEBUG_FILE = false; + private static final long DEFAULT_POLL_FREQUENCY_SECONDS = 60 * 60 * 24; // One day + // @VisibleForTesting static final long MILLIS_IN_4WEEKS = 1000L * 60 * 60 * 24 * 7 * 4; @@ -89,6 +91,9 @@ public class SyncStorageEngine extends Handler { /** Enum value for a user-initiated sync. */ public static final int SOURCE_USER = 3; + /** Enum value for a periodic sync. */ + public static final int SOURCE_PERIODIC = 4; + public static final long NOT_IN_BACKOFF_MODE = -1; private static final Intent SYNC_CONNECTION_SETTING_CHANGED_INTENT = @@ -99,7 +104,8 @@ public class SyncStorageEngine extends Handler { public static final String[] SOURCES = { "SERVER", "LOCAL", "POLL", - "USER" }; + "USER", + "PERIODIC" }; // The MESG column will contain one of these or one of the Error types. public static final String MESG_SUCCESS = "success"; @@ -164,6 +170,7 @@ public class SyncStorageEngine extends Handler { long backoffTime; long backoffDelay; long delayUntil; + final ArrayList<Pair<Bundle, Long>> periodicSyncs; AuthorityInfo(Account account, String authority, int ident) { this.account = account; @@ -173,6 +180,8 @@ public class SyncStorageEngine extends Handler { syncable = -1; // default to "unknown" backoffTime = -1; // if < 0 then we aren't in backoff mode backoffDelay = -1; // if < 0 then we aren't in backoff mode + periodicSyncs = new ArrayList<Pair<Bundle, Long>>(); + periodicSyncs.add(Pair.create(new Bundle(), DEFAULT_POLL_FREQUENCY_SECONDS)); } } @@ -228,6 +237,7 @@ public class SyncStorageEngine extends Handler { private int mYearInDays; private final Context mContext; + private static volatile SyncStorageEngine sSyncStorageEngine = null; /** @@ -262,17 +272,15 @@ public class SyncStorageEngine extends Handler { private int mNextHistoryId = 0; private boolean mMasterSyncAutomatically = true; - private SyncStorageEngine(Context context) { + private SyncStorageEngine(Context context, File dataDir) { mContext = context; sSyncStorageEngine = this; mCal = Calendar.getInstance(TimeZone.getTimeZone("GMT+0")); - // This call will return the correct directory whether Encrypted File Systems is - // enabled or not. - File dataDir = Environment.getSecureDataDirectory(); File systemDir = new File(dataDir, "system"); File syncDir = new File(systemDir, "sync"); + syncDir.mkdirs(); mAccountInfoFile = new AtomicFile(new File(syncDir, "accounts.xml")); mStatusFile = new AtomicFile(new File(syncDir, "status.bin")); mPendingFile = new AtomicFile(new File(syncDir, "pending.bin")); @@ -286,14 +294,17 @@ public class SyncStorageEngine extends Handler { } public static SyncStorageEngine newTestInstance(Context context) { - return new SyncStorageEngine(context); + return new SyncStorageEngine(context, context.getFilesDir()); } public static void init(Context context) { if (sSyncStorageEngine != null) { return; } - sSyncStorageEngine = new SyncStorageEngine(context); + // This call will return the correct directory whether Encrypted File Systems is + // enabled or not. + File dataDir = Environment.getSecureDataDirectory(); + sSyncStorageEngine = new SyncStorageEngine(context, dataDir); } public static SyncStorageEngine getSingleton() { @@ -475,7 +486,7 @@ public class SyncStorageEngine extends Handler { } } else { AuthorityInfo authority = - getOrCreateAuthorityLocked(account, providerName, -1, false); + getOrCreateAuthorityLocked(account, providerName, -1 /* ident */, true); if (authority.backoffTime == nextSyncTime && authority.backoffDelay == nextDelay) { return; } @@ -483,9 +494,6 @@ public class SyncStorageEngine extends Handler { authority.backoffDelay = nextDelay; changed = true; } - if (changed) { - writeAccountInfoLocked(); - } } if (changed) { @@ -499,12 +507,12 @@ public class SyncStorageEngine extends Handler { + " -> delayUntil " + delayUntil); } synchronized (mAuthorities) { - AuthorityInfo authority = getOrCreateAuthorityLocked(account, providerName, -1, false); + AuthorityInfo authority = getOrCreateAuthorityLocked( + account, providerName, -1 /* ident */, true); if (authority.delayUntil == delayUntil) { return; } authority.delayUntil = delayUntil; - writeAccountInfoLocked(); } reportChange(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS); @@ -520,6 +528,90 @@ public class SyncStorageEngine extends Handler { } } + private void updateOrRemovePeriodicSync(Account account, String providerName, Bundle extras, + long period, boolean add) { + if (period <= 0) { + period = 0; + } + if (extras == null) { + extras = new Bundle(); + } + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "addOrRemovePeriodicSync: " + account + ", provider " + providerName + + " -> period " + period + ", extras " + extras); + } + synchronized (mAuthorities) { + AuthorityInfo authority = getOrCreateAuthorityLocked(account, providerName, -1, false); + if (add) { + boolean alreadyPresent = false; + for (int i = 0, N = authority.periodicSyncs.size(); i < N; i++) { + Pair<Bundle, Long> syncInfo = authority.periodicSyncs.get(i); + final Bundle existingExtras = syncInfo.first; + if (equals(existingExtras, extras)) { + if (syncInfo.second == period) { + return; + } + authority.periodicSyncs.set(i, Pair.create(extras, period)); + alreadyPresent = true; + break; + } + } + if (!alreadyPresent) { + authority.periodicSyncs.add(Pair.create(extras, period)); + SyncStatusInfo status = getOrCreateSyncStatusLocked(authority.ident); + status.setPeriodicSyncTime(authority.periodicSyncs.size() - 1, 0); + } + } else { + SyncStatusInfo status = mSyncStatus.get(authority.ident); + boolean changed = false; + Iterator<Pair<Bundle, Long>> iterator = authority.periodicSyncs.iterator(); + int i = 0; + while (iterator.hasNext()) { + Pair<Bundle, Long> syncInfo = iterator.next(); + if (equals(syncInfo.first, extras)) { + iterator.remove(); + changed = true; + if (status != null) { + status.removePeriodicSyncTime(i); + } + } else { + i++; + } + } + if (!changed) { + return; + } + } + writeAccountInfoLocked(); + writeStatusLocked(); + } + + reportChange(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS); + } + + public void addPeriodicSync(Account account, String providerName, Bundle extras, + long pollFrequency) { + updateOrRemovePeriodicSync(account, providerName, extras, pollFrequency, true /* add */); + } + + public void removePeriodicSync(Account account, String providerName, Bundle extras) { + updateOrRemovePeriodicSync(account, providerName, extras, 0 /* period, ignored */, + false /* remove */); + } + + public List<PeriodicSync> getPeriodicSyncs(Account account, String providerName) { + ArrayList<PeriodicSync> syncs = new ArrayList<PeriodicSync>(); + synchronized (mAuthorities) { + AuthorityInfo authority = getAuthorityLocked(account, providerName, "getPeriodicSyncs"); + if (authority != null) { + for (Pair<Bundle, Long> item : authority.periodicSyncs) { + syncs.add(new PeriodicSync(account, providerName, item.first, item.second)); + } + } + } + return syncs; + } + public void setMasterSyncAutomatically(boolean flag) { boolean old; synchronized (mAuthorities) { @@ -540,9 +632,11 @@ public class SyncStorageEngine extends Handler { } } - public AuthorityInfo getAuthority(Account account, String authority) { + public AuthorityInfo getOrCreateAuthority(Account account, String authority) { synchronized (mAuthorities) { - return getAuthorityLocked(account, authority, null); + return getOrCreateAuthorityLocked(account, authority, + -1 /* assign a new identifier if creating a new authority */, + true /* write to storage if this results in a change */); } } @@ -817,7 +911,25 @@ public class SyncStorageEngine extends Handler { return id; } - public void stopSyncEvent(long historyId, long elapsedTime, String resultMessage, + public static boolean equals(Bundle b1, Bundle b2) { + if (b1.size() != b2.size()) { + return false; + } + if (b1.isEmpty()) { + return true; + } + for (String key : b1.keySet()) { + if (!b2.containsKey(key)) { + return false; + } + if (!b1.get(key).equals(b2.get(key))) { + return false; + } + } + return true; + } + + public void stopSyncEvent(long historyId, Bundle extras, long elapsedTime, String resultMessage, long downstreamActivity, long upstreamActivity) { synchronized (mAuthorities) { if (DEBUG) Log.v(TAG, "stopSyncEvent: historyId=" + historyId); @@ -860,6 +972,17 @@ public class SyncStorageEngine extends Handler { case SOURCE_SERVER: status.numSourceServer++; break; + case SOURCE_PERIODIC: + status.numSourcePeriodic++; + AuthorityInfo authority = mAuthorities.get(item.authorityId); + for (int periodicSyncIndex = 0; + periodicSyncIndex < authority.periodicSyncs.size(); + periodicSyncIndex++) { + if (equals(extras, authority.periodicSyncs.get(periodicSyncIndex).first)) { + status.setPeriodicSyncTime(periodicSyncIndex, item.eventTime); + } + } + break; } boolean writeStatisticsNow = false; @@ -948,11 +1071,27 @@ public class SyncStorageEngine extends Handler { } /** + * Return an array of the current authorities. Note + * that the objects inside the array are the real, live objects, + * so be careful what you do with them. + */ + public ArrayList<AuthorityInfo> getAuthorities() { + synchronized (mAuthorities) { + final int N = mAuthorities.size(); + ArrayList<AuthorityInfo> infos = new ArrayList<AuthorityInfo>(N); + for (int i=0; i<N; i++) { + infos.add(mAuthorities.valueAt(i)); + } + return infos; + } + } + + /** * Returns the status that matches the authority and account. * * @param account the account we want to check * @param authority the authority whose row should be selected - * @return the SyncStatusInfo for the authority, or null if none exists + * @return the SyncStatusInfo for the authority */ public SyncStatusInfo getStatusByAccountAndAuthority(Account account, String authority) { if (account == null || authority == null) { @@ -1130,6 +1269,12 @@ public class SyncStorageEngine extends Handler { return authority; } + public SyncStatusInfo getOrCreateSyncStatus(AuthorityInfo authority) { + synchronized (mAuthorities) { + return getOrCreateSyncStatusLocked(authority.ident); + } + } + private SyncStatusInfo getOrCreateSyncStatusLocked(int authorityId) { SyncStatusInfo status = mSyncStatus.get(authorityId); if (status == null) { @@ -1155,6 +1300,25 @@ public class SyncStorageEngine extends Handler { } /** + * public for testing + */ + public void clearAndReadState() { + synchronized (mAuthorities) { + mAuthorities.clear(); + mAccounts.clear(); + mPendingOperations.clear(); + mSyncStatus.clear(); + mSyncHistory.clear(); + + readAccountInfoLocked(); + readStatusLocked(); + readPendingOperationsLocked(); + readStatisticsLocked(); + readLegacyAccountInfoLocked(); + } + } + + /** * Read all account information back in to the initial engine state. */ private void readAccountInfoLocked() { @@ -1175,59 +1339,23 @@ public class SyncStorageEngine extends Handler { mMasterSyncAutomatically = listen == null || Boolean.parseBoolean(listen); eventType = parser.next(); + AuthorityInfo authority = null; + Pair<Bundle, Long> periodicSync = null; do { - if (eventType == XmlPullParser.START_TAG - && parser.getDepth() == 2) { + if (eventType == XmlPullParser.START_TAG) { tagName = parser.getName(); - if ("authority".equals(tagName)) { - int id = -1; - try { - id = Integer.parseInt(parser.getAttributeValue( - null, "id")); - } catch (NumberFormatException e) { - } catch (NullPointerException e) { + if (parser.getDepth() == 2) { + if ("authority".equals(tagName)) { + authority = parseAuthority(parser); + periodicSync = null; + } + } else if (parser.getDepth() == 3) { + if ("periodicSync".equals(tagName) && authority != null) { + periodicSync = parsePeriodicSync(parser, authority); } - if (id >= 0) { - String accountName = parser.getAttributeValue( - null, "account"); - String accountType = parser.getAttributeValue( - null, "type"); - if (accountType == null) { - accountType = "com.google"; - } - String authorityName = parser.getAttributeValue( - null, "authority"); - String enabled = parser.getAttributeValue( - null, "enabled"); - String syncable = parser.getAttributeValue(null, "syncable"); - AuthorityInfo authority = mAuthorities.get(id); - if (DEBUG_FILE) Log.v(TAG, "Adding authority: account=" - + accountName + " auth=" + authorityName - + " enabled=" + enabled - + " syncable=" + syncable); - if (authority == null) { - if (DEBUG_FILE) Log.v(TAG, "Creating entry"); - authority = getOrCreateAuthorityLocked( - new Account(accountName, accountType), - authorityName, id, false); - } - if (authority != null) { - authority.enabled = enabled == null - || Boolean.parseBoolean(enabled); - if ("unknown".equals(syncable)) { - authority.syncable = -1; - } else { - authority.syncable = - (syncable == null || Boolean.parseBoolean(enabled)) - ? 1 - : 0; - } - } else { - Log.w(TAG, "Failure adding authority: account=" - + accountName + " auth=" + authorityName - + " enabled=" + enabled - + " syncable=" + syncable); - } + } else if (parser.getDepth() == 4 && periodicSync != null) { + if ("extra".equals(tagName)) { + parseExtra(parser, periodicSync); } } } @@ -1249,6 +1377,105 @@ public class SyncStorageEngine extends Handler { } } + private AuthorityInfo parseAuthority(XmlPullParser parser) { + AuthorityInfo authority = null; + int id = -1; + try { + id = Integer.parseInt(parser.getAttributeValue( + null, "id")); + } catch (NumberFormatException e) { + Log.e(TAG, "error parsing the id of the authority", e); + } catch (NullPointerException e) { + Log.e(TAG, "the id of the authority is null", e); + } + if (id >= 0) { + String accountName = parser.getAttributeValue(null, "account"); + String accountType = parser.getAttributeValue(null, "type"); + if (accountType == null) { + accountType = "com.google"; + } + String authorityName = parser.getAttributeValue(null, "authority"); + String enabled = parser.getAttributeValue(null, "enabled"); + String syncable = parser.getAttributeValue(null, "syncable"); + authority = mAuthorities.get(id); + if (DEBUG_FILE) Log.v(TAG, "Adding authority: account=" + + accountName + " auth=" + authorityName + + " enabled=" + enabled + + " syncable=" + syncable); + if (authority == null) { + if (DEBUG_FILE) Log.v(TAG, "Creating entry"); + authority = getOrCreateAuthorityLocked( + new Account(accountName, accountType), authorityName, id, false); + // clear this since we will read these later on + authority.periodicSyncs.clear(); + } + if (authority != null) { + authority.enabled = enabled == null || Boolean.parseBoolean(enabled); + if ("unknown".equals(syncable)) { + authority.syncable = -1; + } else { + authority.syncable = + (syncable == null || Boolean.parseBoolean(enabled)) ? 1 : 0; + } + } else { + Log.w(TAG, "Failure adding authority: account=" + + accountName + " auth=" + authorityName + + " enabled=" + enabled + + " syncable=" + syncable); + } + } + + return authority; + } + + private Pair<Bundle, Long> parsePeriodicSync(XmlPullParser parser, AuthorityInfo authority) { + Bundle extras = new Bundle(); + String periodValue = parser.getAttributeValue(null, "period"); + final long period; + try { + period = Long.parseLong(periodValue); + } catch (NumberFormatException e) { + Log.e(TAG, "error parsing the period of a periodic sync", e); + return null; + } catch (NullPointerException e) { + Log.e(TAG, "the period of a periodic sync is null", e); + return null; + } + final Pair<Bundle, Long> periodicSync = Pair.create(extras, period); + authority.periodicSyncs.add(periodicSync); + return periodicSync; + } + + private void parseExtra(XmlPullParser parser, Pair<Bundle, Long> periodicSync) { + final Bundle extras = periodicSync.first; + String name = parser.getAttributeValue(null, "name"); + String type = parser.getAttributeValue(null, "type"); + String value1 = parser.getAttributeValue(null, "value1"); + String value2 = parser.getAttributeValue(null, "value2"); + + try { + if ("long".equals(type)) { + extras.putLong(name, Long.parseLong(value1)); + } else if ("integer".equals(type)) { + extras.putInt(name, Integer.parseInt(value1)); + } else if ("double".equals(type)) { + extras.putDouble(name, Double.parseDouble(value1)); + } else if ("float".equals(type)) { + extras.putFloat(name, Float.parseFloat(value1)); + } else if ("boolean".equals(type)) { + extras.putBoolean(name, Boolean.parseBoolean(value1)); + } else if ("string".equals(type)) { + extras.putString(name, value1); + } else if ("account".equals(type)) { + extras.putParcelable(name, new Account(value1, value2)); + } + } catch (NumberFormatException e) { + Log.e(TAG, "error parsing bundle value", e); + } catch (NullPointerException e) { + Log.e(TAG, "error parsing bundle value", e); + } + } + /** * Write all account information to the account file. */ @@ -1284,6 +1511,41 @@ public class SyncStorageEngine extends Handler { } else if (authority.syncable == 0) { out.attribute(null, "syncable", "false"); } + for (Pair<Bundle, Long> periodicSync : authority.periodicSyncs) { + out.startTag(null, "periodicSync"); + out.attribute(null, "period", Long.toString(periodicSync.second)); + final Bundle extras = periodicSync.first; + for (String key : extras.keySet()) { + out.startTag(null, "extra"); + out.attribute(null, "name", key); + final Object value = extras.get(key); + if (value instanceof Long) { + out.attribute(null, "type", "long"); + out.attribute(null, "value1", value.toString()); + } else if (value instanceof Integer) { + out.attribute(null, "type", "integer"); + out.attribute(null, "value1", value.toString()); + } else if (value instanceof Boolean) { + out.attribute(null, "type", "boolean"); + out.attribute(null, "value1", value.toString()); + } else if (value instanceof Float) { + out.attribute(null, "type", "float"); + out.attribute(null, "value1", value.toString()); + } else if (value instanceof Double) { + out.attribute(null, "type", "double"); + out.attribute(null, "value1", value.toString()); + } else if (value instanceof String) { + out.attribute(null, "type", "string"); + out.attribute(null, "value1", value.toString()); + } else if (value instanceof Account) { + out.attribute(null, "type", "account"); + out.attribute(null, "value1", ((Account)value).name); + out.attribute(null, "value2", ((Account)value).type); + } + out.endTag(null, "extra"); + } + out.endTag(null, "periodicSync"); + } out.endTag(null, "authority"); } @@ -1389,6 +1651,7 @@ public class SyncStorageEngine extends Handler { st.numSourcePoll = getIntColumn(c, "numSourcePoll"); st.numSourceServer = getIntColumn(c, "numSourceServer"); st.numSourceUser = getIntColumn(c, "numSourceUser"); + st.numSourcePeriodic = 0; st.lastSuccessSource = getIntColumn(c, "lastSuccessSource"); st.lastSuccessTime = getLongColumn(c, "lastSuccessTime"); st.lastFailureSource = getIntColumn(c, "lastFailureSource"); diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java index b94bb51..a13f7f9 100644 --- a/core/java/android/content/pm/ActivityInfo.java +++ b/core/java/android/content/pm/ActivityInfo.java @@ -251,6 +251,13 @@ public class ActivityInfo extends ComponentInfo public static final int CONFIG_SCREEN_LAYOUT = 0x0100; /** * Bit in {@link #configChanges} that indicates that the activity + * can itself handle the ui mode. Set from the + * {@link android.R.attr#configChanges} attribute. + * @hide (UIMODE) Pending API council approval + */ + public static final int CONFIG_UI_MODE = 0x0200; + /** + * Bit in {@link #configChanges} that indicates that the activity * can itself handle changes to the font scaling factor. Set from the * {@link android.R.attr#configChanges} attribute. This is * not a core resource configutation, but a higher-level value, so its diff --git a/core/java/android/content/pm/ApplicationInfo.java b/core/java/android/content/pm/ApplicationInfo.java index 808c839..123d9b7 100644 --- a/core/java/android/content/pm/ApplicationInfo.java +++ b/core/java/android/content/pm/ApplicationInfo.java @@ -178,12 +178,20 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { public static final int FLAG_SUPPORTS_SCREEN_DENSITIES = 1<<13; /** + * Value for {@link #flags}: set to true if this application would like to + * request the VM to operate under the safe mode. Comes from + * {@link android.R.styleable#AndroidManifestApplication_safeMode + * android:safeMode} of the <application> tag. + */ + public static final int FLAG_VM_SAFE_MODE = 1<<14; + + /** * Value for {@link #flags}: this is false if the application has set * its android:allowBackup to false, true otherwise. * * {@hide} */ - public static final int FLAG_ALLOW_BACKUP = 1<<14; + public static final int FLAG_ALLOW_BACKUP = 1<<15; /** * Value for {@link #flags}: this is false if the application has set @@ -194,7 +202,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { * * {@hide} */ - public static final int FLAG_KILL_AFTER_RESTORE = 1<<15; + public static final int FLAG_KILL_AFTER_RESTORE = 1<<16; /** * Value for {@link #flags}: this is true if the application has set @@ -205,7 +213,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { * * {@hide} */ - public static final int FLAG_RESTORE_NEEDS_APPLICATION = 1<<16; + public static final int FLAG_RESTORE_NEEDS_APPLICATION = 1<<17; /** * Value for {@link #flags}: this is true if the application has set @@ -215,7 +223,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { * * {@hide} */ - public static final int FLAG_NEVER_ENCRYPT = 1<<17; + public static final int FLAG_NEVER_ENCRYPT = 1<<18; /** * Value for {@link #flags}: Set to true if the application has been @@ -223,7 +231,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { * * {@hide} */ - public static final int FLAG_FORWARD_LOCK = 1<<18; + public static final int FLAG_FORWARD_LOCK = 1<<19; /** * Value for {@link #flags}: Set to true if the application is @@ -231,7 +239,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { * * {@hide} */ - public static final int FLAG_ON_SDCARD = 1<<19; + public static final int FLAG_ON_SDCARD = 1<<20; /** * Value for {@link #flags}: Set to true if the application is @@ -239,7 +247,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { * * {@hide} */ - public static final int FLAG_NATIVE_DEBUGGABLE = 1<<20; + public static final int FLAG_NATIVE_DEBUGGABLE = 1<<21; /** * Flags associated with the application. Any combination of @@ -250,7 +258,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { * {@link #FLAG_TEST_ONLY}, {@link #FLAG_SUPPORTS_SMALL_SCREENS}, * {@link #FLAG_SUPPORTS_NORMAL_SCREENS}, * {@link #FLAG_SUPPORTS_LARGE_SCREENS}, {@link #FLAG_RESIZEABLE_FOR_SCREENS}, - * {@link #FLAG_SUPPORTS_SCREEN_DENSITIES} + * {@link #FLAG_SUPPORTS_SCREEN_DENSITIES}, {@link #FLAG_VM_SAFE_MODE} */ public int flags = 0; @@ -270,6 +278,8 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { * Full paths to the locations of extra resource packages this application * uses. This field is only used if there are extra resource packages, * otherwise it is null. + * + * {@hide} */ public String[] resourceDirs; diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl index 54db5e0..47789a5 100644 --- a/core/java/android/content/pm/IPackageManager.aidl +++ b/core/java/android/content/pm/IPackageManager.aidl @@ -47,6 +47,9 @@ interface IPackageManager { PackageInfo getPackageInfo(String packageName, int flags); int getPackageUid(String packageName); int[] getPackageGids(String packageName); + + String[] currentToCanonicalPackageNames(in String[] names); + String[] canonicalToCurrentPackageNames(in String[] names); PermissionInfo getPermissionInfo(String name, int flags); @@ -305,4 +308,5 @@ interface IPackageManager { */ void updateExternalMediaStatus(boolean mounted); + String nextPackageToClean(String lastPackage); } diff --git a/core/java/android/content/pm/PackageInfo.java b/core/java/android/content/pm/PackageInfo.java index a8ce889..c003355 100644 --- a/core/java/android/content/pm/PackageInfo.java +++ b/core/java/android/content/pm/PackageInfo.java @@ -131,6 +131,34 @@ public class PackageInfo implements Parcelable { * The features that this application has said it requires. */ public FeatureInfo[] reqFeatures; + + /** + * Constant corresponding to <code>auto</code> in + * the {@link android.R.attr#installLocation} attribute. + * @hide + */ + public static final int INSTALL_LOCATION_AUTO = 0; + /** + * Constant corresponding to <code>internalOnly</code> in + * the {@link android.R.attr#installLocation} attribute. + * @hide + */ + public static final int INSTALL_LOCATION_INTERNAL_ONLY = 1; + /** + * Constant corresponding to <code>preferExternal</code> in + * the {@link android.R.attr#installLocation} attribute. + * @hide + */ + public static final int INSTALL_LOCATION_PREFER_EXTERNAL = 2; + /** + * The launch mode style requested by the activity. From the + * {@link android.R.attr#installLocation} attribute, one of + * {@link #INSTALL_LOCATION_AUTO}, + * {@link #INSTALL_LOCATION_INTERNAL_ONLY}, + * {@link #INSTALL_LOCATION_PREFER_EXTERNAL} + * @hide + */ + public int installLocation = INSTALL_LOCATION_INTERNAL_ONLY; public PackageInfo() { } @@ -168,6 +196,7 @@ public class PackageInfo implements Parcelable { dest.writeTypedArray(signatures, parcelableFlags); dest.writeTypedArray(configPreferences, parcelableFlags); dest.writeTypedArray(reqFeatures, parcelableFlags); + dest.writeInt(installLocation); } public static final Parcelable.Creator<PackageInfo> CREATOR @@ -202,5 +231,6 @@ public class PackageInfo implements Parcelable { signatures = source.createTypedArray(Signature.CREATOR); configPreferences = source.createTypedArray(ConfigurationInfo.CREATOR); reqFeatures = source.createTypedArray(FeatureInfo.CREATOR); + installLocation = source.readInt(); } } diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index 745628a..8576de2 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -258,14 +258,7 @@ public abstract class PackageManager { * package has to be installed on the sdcard. * @hide */ - public static final int INSTALL_ON_SDCARD = 0x00000008; - - /** - * Convenience flag parameter to indicate that this package has to be installed - * on internal flash. - * @hide - */ - public static final int INSTALL_ON_INTERNAL_FLASH = 0x00000000; + public static final int INSTALL_EXTERNAL = 0x00000008; /** * Flag parameter for @@ -437,6 +430,15 @@ public abstract class PackageManager { public static final int INSTALL_FAILED_CONTAINER_ERROR = -18; /** + * Installation return code: this is passed to the {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if + * the new package couldn't be installed in the specified install + * location. + * @hide + */ + public static final int INSTALL_FAILED_INVALID_INSTALL_LOCATION = -19; + + /** * Installation parse return code: this is passed to the {@link IPackageInstallObserver} by * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} * if the parser was given a path that is not a file, or does not end with the expected @@ -520,6 +522,14 @@ public abstract class PackageManager { public static final int INSTALL_PARSE_FAILED_MANIFEST_EMPTY = -109; /** + * Installation failed return code: this is passed to the {@link IPackageInstallObserver} by + * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} + * if the system failed to install the package because of system issues. + * @hide + */ + public static final int INSTALL_FAILED_INTERNAL_ERROR = -110; + + /** * Indicates the state of installation. Used by PackageManager to * figure out incomplete installations. Say a package is being installed * (the state is set to PKG_INSTALL_INCOMPLETE) and remains so till @@ -613,20 +623,11 @@ public abstract class PackageManager { public static final String FEATURE_LIVE_WALLPAPER = "android.software.live_wallpaper"; /** - * Determines best place to install an application: either SD or internal FLASH. - * Tweak the algorithm for best results. - * @param appInfo ApplicationInfo object of the package to install. - * Call utility method to obtain. - * @param packageURI URI identifying the package's APK file. - * @return {@link INSTALL_ON_INTERNAL_FLASH} if it is best to install package on internal - * storage, {@link INSTALL_ON_SDCARD} if it is best to install package on SD card, - * and {@link INSTALL_FAILED_INSUFFICIENT_STORAGE} if insufficient space to safely install - * the application. {@link INSTALL_PARSE_FAILED_NOT_APK} Is returned if any input - * parameter is <code>null</code>. - * This recommendation does take into account the package's own flags. + * Action to external storage service to clean out removed apps. * @hide */ - public abstract int recommendAppInstallLocation(ApplicationInfo appInfo, Uri packageURI); + public static final String ACTION_CLEAN_EXTERNAL_STORAGE + = "android.content.pm.CLEAN_EXTERNAL_STORAGE"; /** * Retrieve overall information about an application package that is @@ -674,6 +675,23 @@ public abstract class PackageManager { throws NameNotFoundException; /** + * Map from the current package names in use on the device to whatever + * the current canonical name of that package is. + * @param names Array of current names to be mapped. + * @return Returns an array of the same size as the original, containing + * the canonical name for each package. + */ + public abstract String[] currentToCanonicalPackageNames(String[] names); + + /** + * Map from a packages canonical name to the current name in use on the device. + * @param names Array of new names to be mapped. + * @return Returns an array of the same size as the original, containing + * the current name for each package. + */ + public abstract String[] canonicalToCurrentPackageNames(String[] names); + + /** * Return a "good" intent to launch a front-door activity in a package, * for use for example to implement an "open" button when browsing through * packages. The current implementation will look first for a main @@ -1135,7 +1153,9 @@ public abstract class PackageManager { * * @return Returns a ResolveInfo containing the final activity intent that * was determined to be the best action. Returns null if no - * matching activity was found. + * matching activity was found. If multiple matching activities are + * found and there is no default set, returns a ResolveInfo + * containing something else, such as the activity resolver. * * @see #MATCH_DEFAULT_ONLY * @see #GET_INTENT_FILTERS diff --git a/core/java/android/content/pm/PackageParser.java b/core/java/android/content/pm/PackageParser.java index bac55cc..5823560 100644 --- a/core/java/android/content/pm/PackageParser.java +++ b/core/java/android/content/pm/PackageParser.java @@ -177,6 +177,7 @@ public class PackageParser { pi.sharedUserId = p.mSharedUserId; pi.sharedUserLabel = p.mSharedUserLabel; pi.applicationInfo = p.applicationInfo; + pi.installLocation = p.installLocation; if ((flags&PackageManager.GET_GIDS) != 0) { pi.gids = gids; } @@ -709,6 +710,9 @@ public class PackageParser { com.android.internal.R.styleable.AndroidManifest_sharedUserLabel, 0); } sa.recycle(); + pkg.installLocation = sa.getInteger( + com.android.internal.R.styleable.AndroidManifest_installLocation, + PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY); // Resource boolean are -1, so 1 means we don't know the value. int supportsSmallScreens = 1; @@ -957,15 +961,15 @@ public class PackageParser { sa = res.obtainAttributes(attrs, com.android.internal.R.styleable.AndroidManifestOriginalPackage); - String name = sa.getNonResourceString( + String orig =sa.getNonResourceString( com.android.internal.R.styleable.AndroidManifestOriginalPackage_name); + if (!pkg.packageName.equals(orig)) { + pkg.mOriginalPackage = orig; + pkg.mRealPackage = pkg.packageName; + } sa.recycle(); - if (name != null && (flags&PARSE_IS_SYSTEM) != 0) { - pkg.mOriginalPackage = name; - } - XmlUtils.skipCurrentTag(parser); } else if (tagName.equals("adopt-permissions")) { @@ -977,7 +981,7 @@ public class PackageParser { sa.recycle(); - if (name != null && (flags&PARSE_IS_SYSTEM) != 0) { + if (name != null) { if (pkg.mAdoptPermissions == null) { pkg.mAdoptPermissions = new ArrayList<String>(); } @@ -1425,6 +1429,12 @@ public class PackageParser { } if (sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestApplication_safeMode, + false)) { + ai.flags |= ApplicationInfo.FLAG_VM_SAFE_MODE; + } + + if (sa.getBoolean( com.android.internal.R.styleable.AndroidManifestApplication_hasCode, true)) { ai.flags |= ApplicationInfo.FLAG_HAS_CODE; @@ -2138,7 +2148,7 @@ public class PackageParser { havePerm = true; } if (writePermission != null) { - writePermission = readPermission.intern(); + writePermission = writePermission.intern(); havePerm = true; } @@ -2540,7 +2550,7 @@ public class PackageParser { } public final static class Package { - public final String packageName; + public String packageName; // For now we only support one application per package. public final ApplicationInfo applicationInfo = new ApplicationInfo(); @@ -2562,6 +2572,7 @@ public class PackageParser { public String[] usesLibraryFiles = null; public String mOriginalPackage = null; + public String mRealPackage = null; public ArrayList<String> mAdoptPermissions = null; // We store the application meta-data independently to avoid multiple unwanted references @@ -2610,12 +2621,40 @@ public class PackageParser { */ public ArrayList<FeatureInfo> reqFeatures = null; + public int installLocation; + public Package(String _name) { packageName = _name; applicationInfo.packageName = _name; applicationInfo.uid = -1; } + public void setPackageName(String newName) { + packageName = newName; + applicationInfo.packageName = newName; + for (int i=permissions.size()-1; i>=0; i--) { + permissions.get(i).setPackageName(newName); + } + for (int i=permissionGroups.size()-1; i>=0; i--) { + permissionGroups.get(i).setPackageName(newName); + } + for (int i=activities.size()-1; i>=0; i--) { + activities.get(i).setPackageName(newName); + } + for (int i=receivers.size()-1; i>=0; i--) { + receivers.get(i).setPackageName(newName); + } + for (int i=providers.size()-1; i>=0; i--) { + providers.get(i).setPackageName(newName); + } + for (int i=services.size()-1; i>=0; i--) { + services.get(i).setPackageName(newName); + } + for (int i=instrumentation.size()-1; i>=0; i--) { + instrumentation.get(i).setPackageName(newName); + } + } + public String toString() { return "Package{" + Integer.toHexString(System.identityHashCode(this)) @@ -2626,15 +2665,16 @@ public class PackageParser { public static class Component<II extends IntentInfo> { public final Package owner; public final ArrayList<II> intents; - public final ComponentName component; - public final String componentShortName; + public final String className; public Bundle metaData; + ComponentName componentName; + String componentShortName; + public Component(Package _owner) { owner = _owner; intents = null; - component = null; - componentShortName = null; + className = null; } public Component(final ParsePackageItemArgs args, final PackageItemInfo outInfo) { @@ -2642,8 +2682,7 @@ public class PackageParser { intents = new ArrayList<II>(0); String name = args.sa.getNonResourceString(args.nameRes); if (name == null) { - component = null; - componentShortName = null; + className = null; args.outError[0] = args.tag + " does not specify android:name"; return; } @@ -2651,15 +2690,12 @@ public class PackageParser { outInfo.name = buildClassName(owner.applicationInfo.packageName, name, args.outError); if (outInfo.name == null) { - component = null; - componentShortName = null; + className = null; args.outError[0] = args.tag + " does not have valid android:name"; return; } - component = new ComponentName(owner.applicationInfo.packageName, - outInfo.name); - componentShortName = component.flattenToShortString(); + className = outInfo.name; int iconVal = args.sa.getResourceId(args.iconRes, 0); if (iconVal != 0) { @@ -2697,9 +2733,36 @@ public class PackageParser { public Component(Component<II> clone) { owner = clone.owner; intents = clone.intents; - component = clone.component; + className = clone.className; + componentName = clone.componentName; componentShortName = clone.componentShortName; - metaData = clone.metaData; + } + + public ComponentName getComponentName() { + if (componentName != null) { + return componentName; + } + if (className != null) { + componentName = new ComponentName(owner.applicationInfo.packageName, + className); + } + return componentName; + } + + public String getComponentShortName() { + if (componentShortName != null) { + return componentShortName; + } + ComponentName component = getComponentName(); + if (component != null) { + componentShortName = component.flattenToShortString(); + } + return componentShortName; + } + + public void setPackageName(String packageName) { + componentName = null; + componentShortName = null; } } @@ -2717,6 +2780,11 @@ public class PackageParser { super(_owner); info = _info; } + + public void setPackageName(String packageName) { + super.setPackageName(packageName); + info.packageName = packageName; + } public String toString() { return "Permission{" @@ -2738,6 +2806,11 @@ public class PackageParser { info = _info; } + public void setPackageName(String packageName) { + super.setPackageName(packageName); + info.packageName = packageName; + } + public String toString() { return "PermissionGroup{" + Integer.toHexString(System.identityHashCode(this)) @@ -2813,10 +2886,15 @@ public class PackageParser { info.applicationInfo = args.owner.applicationInfo; } + public void setPackageName(String packageName) { + super.setPackageName(packageName); + info.packageName = packageName; + } + public String toString() { return "Activity{" + Integer.toHexString(System.identityHashCode(this)) - + " " + component.flattenToString() + "}"; + + " " + getComponentShortName() + "}"; } } @@ -2842,10 +2920,15 @@ public class PackageParser { info.applicationInfo = args.owner.applicationInfo; } + public void setPackageName(String packageName) { + super.setPackageName(packageName); + info.packageName = packageName; + } + public String toString() { return "Service{" + Integer.toHexString(System.identityHashCode(this)) - + " " + component.flattenToString() + "}"; + + " " + getComponentShortName() + "}"; } } @@ -2878,6 +2961,11 @@ public class PackageParser { this.syncable = existingProvider.syncable; } + public void setPackageName(String packageName) { + super.setPackageName(packageName); + info.packageName = packageName; + } + public String toString() { return "Provider{" + Integer.toHexString(System.identityHashCode(this)) @@ -2911,10 +2999,15 @@ public class PackageParser { info = _info; } + public void setPackageName(String packageName) { + super.setPackageName(packageName); + info.packageName = packageName; + } + public String toString() { return "Instrumentation{" + Integer.toHexString(System.identityHashCode(this)) - + " " + component.flattenToString() + "}"; + + " " + getComponentShortName() + "}"; } } diff --git a/core/java/android/content/pm/RegisteredServicesCache.java b/core/java/android/content/pm/RegisteredServicesCache.java index 7362394..a885820 100644 --- a/core/java/android/content/pm/RegisteredServicesCache.java +++ b/core/java/android/content/pm/RegisteredServicesCache.java @@ -117,8 +117,8 @@ public abstract class RegisteredServicesCache<V> { mContext.registerReceiver(receiver, intentFilter); // Register for events related to sdcard installation. IntentFilter sdFilter = new IntentFilter(); - sdFilter.addAction(Intent.ACTION_MEDIA_RESOURCES_AVAILABLE); - sdFilter.addAction(Intent.ACTION_MEDIA_RESOURCES_UNAVAILABLE); + sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE); + sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE); mContext.registerReceiver(receiver, sdFilter); } diff --git a/core/java/android/content/res/AssetManager.java b/core/java/android/content/res/AssetManager.java index 23a408b..7f9a5c6 100644 --- a/core/java/android/content/res/AssetManager.java +++ b/core/java/android/content/res/AssetManager.java @@ -619,7 +619,7 @@ public final class AssetManager { public native final void setConfiguration(int mcc, int mnc, String locale, int orientation, int touchscreen, int density, int keyboard, int keyboardHidden, int navigation, int screenWidth, int screenHeight, - int screenLayout, int majorVersion); + int screenLayout, int uiMode, int majorVersion); /** * Retrieve the resource identifier for the given resource name. diff --git a/core/java/android/content/res/Configuration.java b/core/java/android/content/res/Configuration.java index 1fe34b5..6490b65 100644 --- a/core/java/android/content/res/Configuration.java +++ b/core/java/android/content/res/Configuration.java @@ -161,6 +161,41 @@ public final class Configuration implements Parcelable, Comparable<Configuration * or {@link #ORIENTATION_SQUARE}. */ public int orientation; + + /** @hide (UIMODE) Pending API council approval */ + public static final int UI_MODE_TYPE_MASK = 0x0f; + /** @hide (UIMODE) Pending API council approval */ + public static final int UI_MODE_TYPE_NORMAL = 0x00; + /** @hide (UIMODE) Pending API council approval */ + public static final int UI_MODE_TYPE_CAR = 0x01; + + /** @hide (UIMODE) Pending API council approval */ + public static final int UI_MODE_NIGHT_MASK = 0x30; + /** @hide (UIMODE) Pending API council approval */ + public static final int UI_MODE_NIGHT_UNDEFINED = 0x00; + /** @hide (UIMODE) Pending API council approval */ + public static final int UI_MODE_NIGHT_NO = 0x10; + /** @hide (UIMODE) Pending API council approval */ + public static final int UI_MODE_NIGHT_YES = 0x20; + + /** + * Bit mask of the ui mode. Currently there are two fields: + * <p>The {@link #UI_MODE_TYPE_MASK} bits define the overall ui mode of the + * device. They may be one of + * {@link #UI_MODE_TYPE_NORMAL} or {@link #UI_MODE_TYPE_CAR}. + * + * <p>The {@link #UI_MODE_NIGHT_MASK} defines whether the screen + * is in a special mode. They may be one of + * {@link #UI_MODE_NIGHT_NO} or {@link #UI_MODE_NIGHT_YES}. + * + * @hide (UIMODE) Pending API council approval + */ + public int uiMode; + + /** + * @hide Internal book-keeping. + */ + public int seq; /** * Construct an invalid Configuration. You must call {@link #setToDefaults} @@ -189,6 +224,8 @@ public final class Configuration implements Parcelable, Comparable<Configuration navigationHidden = o.navigationHidden; orientation = o.orientation; screenLayout = o.screenLayout; + uiMode = o.uiMode; + seq = o.seq; } public String toString() { @@ -217,6 +254,12 @@ public final class Configuration implements Parcelable, Comparable<Configuration sb.append(orientation); sb.append(" layout="); sb.append(screenLayout); + sb.append(" uiMode="); + sb.append(uiMode); + if (seq != 0) { + sb.append(" seq="); + sb.append(seq); + } sb.append('}'); return sb.toString(); } @@ -227,7 +270,7 @@ public final class Configuration implements Parcelable, Comparable<Configuration public void setToDefaults() { fontScale = 1; mcc = mnc = 0; - locale = Locale.getDefault(); + locale = null; userSetLocale = false; touchscreen = TOUCHSCREEN_UNDEFINED; keyboard = KEYBOARD_UNDEFINED; @@ -237,6 +280,8 @@ public final class Configuration implements Parcelable, Comparable<Configuration navigationHidden = NAVIGATIONHIDDEN_UNDEFINED; orientation = ORIENTATION_UNDEFINED; screenLayout = SCREENLAYOUT_SIZE_UNDEFINED; + uiMode = UI_MODE_TYPE_NORMAL; + seq = 0; } /** {@hide} */ @@ -317,6 +362,15 @@ public final class Configuration implements Parcelable, Comparable<Configuration changed |= ActivityInfo.CONFIG_SCREEN_LAYOUT; screenLayout = delta.screenLayout; } + if (delta.uiMode != UI_MODE_TYPE_NORMAL + && uiMode != delta.uiMode) { + changed |= ActivityInfo.CONFIG_UI_MODE; + uiMode = delta.uiMode; + } + + if (delta.seq != 0) { + seq = delta.seq; + } return changed; } @@ -393,6 +447,10 @@ public final class Configuration implements Parcelable, Comparable<Configuration && screenLayout != delta.screenLayout) { changed |= ActivityInfo.CONFIG_SCREEN_LAYOUT; } + if (delta.uiMode != UI_MODE_TYPE_NORMAL + && uiMode != delta.uiMode) { + changed |= ActivityInfo.CONFIG_UI_MODE; + } return changed; } @@ -413,6 +471,35 @@ public final class Configuration implements Parcelable, Comparable<Configuration } /** + * @hide Return true if the sequence of 'other' is better than this. Assumes + * that 'this' is your current sequence and 'other' is a new one you have + * received some how and want to compare with what you have. + */ + public boolean isOtherSeqNewer(Configuration other) { + if (other == null) { + // Sanity check. + return false; + } + if (other.seq == 0) { + // If the other sequence is not specified, then we must assume + // it is newer since we don't know any better. + return true; + } + if (seq == 0) { + // If this sequence is not specified, then we also consider the + // other is better. Yes we have a preference for other. Sue us. + return true; + } + int diff = other.seq - seq; + if (diff > 0x10000) { + // If there has been a sufficiently large jump, assume the + // sequence has wrapped around. + return false; + } + return diff > 0; + } + + /** * Parcelable methods */ public int describeContents() { @@ -444,6 +531,8 @@ public final class Configuration implements Parcelable, Comparable<Configuration dest.writeInt(navigationHidden); dest.writeInt(orientation); dest.writeInt(screenLayout); + dest.writeInt(uiMode); + dest.writeInt(seq); } public static final Parcelable.Creator<Configuration> CREATOR @@ -477,6 +566,8 @@ public final class Configuration implements Parcelable, Comparable<Configuration navigationHidden = source.readInt(); orientation = source.readInt(); screenLayout = source.readInt(); + uiMode = source.readInt(); + seq = source.readInt(); } public int compareTo(Configuration that) { @@ -510,6 +601,8 @@ public final class Configuration implements Parcelable, Comparable<Configuration n = this.orientation - that.orientation; if (n != 0) return n; n = this.screenLayout - that.screenLayout; + if (n != 0) return n; + n = this.uiMode - that.uiMode; //if (n != 0) return n; return n; } @@ -533,6 +626,6 @@ public final class Configuration implements Parcelable, Comparable<Configuration + this.locale.hashCode() + this.touchscreen + this.keyboard + this.keyboardHidden + this.hardKeyboardHidden + this.navigation + this.navigationHidden - + this.orientation + this.screenLayout; + + this.orientation + this.screenLayout + this.uiMode; } } diff --git a/core/java/android/content/res/Resources.java b/core/java/android/content/res/Resources.java index e4fc259..a5e39d4 100644 --- a/core/java/android/content/res/Resources.java +++ b/core/java/android/content/res/Resources.java @@ -39,6 +39,7 @@ import android.view.Display; import java.io.IOException; import java.io.InputStream; import java.lang.ref.WeakReference; +import java.util.Locale; /** * Class for accessing an application's resources. This sits on top of the @@ -1259,6 +1260,9 @@ public class Resources { if (config != null) { configChanges = mConfiguration.updateFrom(config); } + if (mConfiguration.locale == null) { + mConfiguration.locale = Locale.getDefault(); + } if (metrics != null) { mMetrics.setTo(metrics); mMetrics.updateMetrics(mCompatibilityInfo, @@ -1294,7 +1298,7 @@ public class Resources { mConfiguration.touchscreen, (int)(mMetrics.density*160), mConfiguration.keyboard, keyboardHidden, mConfiguration.navigation, width, height, - mConfiguration.screenLayout, sSdkVersion); + mConfiguration.screenLayout, mConfiguration.uiMode, sSdkVersion); int N = mDrawableCache.size(); if (DEBUG_CONFIG) { Log.d(TAG, "Cleaning up drawables config changes: 0x" diff --git a/core/java/android/database/sqlite/SQLiteClosable.java b/core/java/android/database/sqlite/SQLiteClosable.java index f64261c..e589f34 100644 --- a/core/java/android/database/sqlite/SQLiteClosable.java +++ b/core/java/android/database/sqlite/SQLiteClosable.java @@ -16,6 +16,8 @@ package android.database.sqlite; +import android.database.CursorWindow; + /** * An object create from a SQLiteDatabase that can be closed. */ @@ -29,9 +31,9 @@ public abstract class SQLiteClosable { synchronized(mLock) { if (mReferenceCount <= 0) { throw new IllegalStateException( - "attempt to acquire a reference on a close SQLiteClosable"); + "attempt to acquire a reference on an already-closed " + getObjInfo()); } - mReferenceCount++; + mReferenceCount++; } } @@ -52,4 +54,24 @@ public abstract class SQLiteClosable { } } } + + private String getObjInfo() { + StringBuilder buff = new StringBuilder(); + buff.append(this.getClass().getName()); + buff.append(" Obj"); + buff.append(" ("); + if (this instanceof SQLiteDatabase) { + buff.append("database = "); + buff.append(((SQLiteDatabase)this).getPath()); + } else if (this instanceof SQLiteProgram || this instanceof SQLiteStatement || + this instanceof SQLiteQuery) { + buff.append("mSql = "); + buff.append(((SQLiteProgram)this).mSql); + } else if (this instanceof CursorWindow) { + buff.append("mStartPos = "); + buff.append(((CursorWindow)this).getStartPosition()); + } + buff.append(") "); + return buff.toString(); + } } diff --git a/core/java/android/database/sqlite/SQLiteCompiledSql.java b/core/java/android/database/sqlite/SQLiteCompiledSql.java index 79527b4..eb85822 100644 --- a/core/java/android/database/sqlite/SQLiteCompiledSql.java +++ b/core/java/android/database/sqlite/SQLiteCompiledSql.java @@ -44,6 +44,9 @@ import android.util.Log; */ /* package */ int nStatement = 0; + /** when in cache and is in use, this member is set */ + private boolean mInUse = false; + /* package */ SQLiteCompiledSql(SQLiteDatabase db, String sql) { mDatabase = db; this.nHandle = db.mNativeHandle; @@ -92,6 +95,18 @@ import android.util.Log; } } + /* package */ synchronized boolean isInUse() { + return mInUse; + } + + /* package */ synchronized void acquire() { + mInUse = true; + } + + /* package */ synchronized void release() { + mInUse = false; + } + /** * Make sure that the native resource is cleaned up. */ diff --git a/core/java/android/database/sqlite/SQLiteDatabase.java b/core/java/android/database/sqlite/SQLiteDatabase.java index e4b0191..9ac8a4d 100644 --- a/core/java/android/database/sqlite/SQLiteDatabase.java +++ b/core/java/android/database/sqlite/SQLiteDatabase.java @@ -66,64 +66,60 @@ public class SQLiteDatabase extends SQLiteClosable { * Algorithms used in ON CONFLICT clause * http://www.sqlite.org/lang_conflict.html */ - public static final class ConflictAlgorithm { - /** - * When a constraint violation occurs, an immediate ROLLBACK occurs, - * thus ending the current transaction, and the command aborts with a - * return code of SQLITE_CONSTRAINT. If no transaction is active - * (other than the implied transaction that is created on every command) - * then this algorithm works the same as ABORT. - */ - public static final int ROLLBACK = 1; - - /** - * When a constraint violation occurs,no ROLLBACK is executed - * so changes from prior commands within the same transaction - * are preserved. This is the default behavior. - */ - public static final int ABORT = 2; + /** + * When a constraint violation occurs, an immediate ROLLBACK occurs, + * thus ending the current transaction, and the command aborts with a + * return code of SQLITE_CONSTRAINT. If no transaction is active + * (other than the implied transaction that is created on every command) + * then this algorithm works the same as ABORT. + */ + public static final int CONFLICT_ROLLBACK = 1; - /** - * When a constraint violation occurs, the command aborts with a return - * code SQLITE_CONSTRAINT. But any changes to the database that - * the command made prior to encountering the constraint violation - * are preserved and are not backed out. - */ - public static final int FAIL = 3; + /** + * When a constraint violation occurs,no ROLLBACK is executed + * so changes from prior commands within the same transaction + * are preserved. This is the default behavior. + */ + public static final int CONFLICT_ABORT = 2; - /** - * When a constraint violation occurs, the one row that contains - * the constraint violation is not inserted or changed. - * But the command continues executing normally. Other rows before and - * after the row that contained the constraint violation continue to be - * inserted or updated normally. No error is returned. - */ - public static final int IGNORE = 4; + /** + * When a constraint violation occurs, the command aborts with a return + * code SQLITE_CONSTRAINT. But any changes to the database that + * the command made prior to encountering the constraint violation + * are preserved and are not backed out. + */ + public static final int CONFLICT_FAIL = 3; - /** - * When a UNIQUE constraint violation occurs, the pre-existing rows that - * are causing the constraint violation are removed prior to inserting - * or updating the current row. Thus the insert or update always occurs. - * The command continues executing normally. No error is returned. - * If a NOT NULL constraint violation occurs, the NULL value is replaced - * by the default value for that column. If the column has no default - * value, then the ABORT algorithm is used. If a CHECK constraint - * violation occurs then the IGNORE algorithm is used. When this conflict - * resolution strategy deletes rows in order to satisfy a constraint, - * it does not invoke delete triggers on those rows. - * This behavior might change in a future release. - */ - public static final int REPLACE = 5; + /** + * When a constraint violation occurs, the one row that contains + * the constraint violation is not inserted or changed. + * But the command continues executing normally. Other rows before and + * after the row that contained the constraint violation continue to be + * inserted or updated normally. No error is returned. + */ + public static final int CONFLICT_IGNORE = 4; - /** - * use the following when no conflict action is specified. - */ - public static final int NONE = 0; - private static final String[] VALUES = new String[] - {"", " OR ROLLBACK ", " OR ABORT ", " OR FAIL ", " OR IGNORE ", " OR REPLACE "}; + /** + * When a UNIQUE constraint violation occurs, the pre-existing rows that + * are causing the constraint violation are removed prior to inserting + * or updating the current row. Thus the insert or update always occurs. + * The command continues executing normally. No error is returned. + * If a NOT NULL constraint violation occurs, the NULL value is replaced + * by the default value for that column. If the column has no default + * value, then the ABORT algorithm is used. If a CHECK constraint + * violation occurs then the IGNORE algorithm is used. When this conflict + * resolution strategy deletes rows in order to satisfy a constraint, + * it does not invoke delete triggers on those rows. + * This behavior might change in a future release. + */ + public static final int CONFLICT_REPLACE = 5; - private ConflictAlgorithm() {} // disable instantiation of this class - } + /** + * use the following when no conflict action is specified. + */ + public static final int CONFLICT_NONE = 0; + private static final String[] CONFLICT_VALUES = new String[] + {"", " OR ROLLBACK ", " OR ABORT ", " OR FAIL ", " OR IGNORE ", " OR REPLACE "}; /** * Maximum Length Of A LIKE Or GLOB Pattern @@ -205,10 +201,17 @@ public class SQLiteDatabase extends SQLiteClosable { private long mLastLockMessageTime = 0L; - // always log queries which take 100ms+; shorter queries are sampled accordingly - private static final int QUERY_LOG_TIME_IN_NANOS = 100 * 1000000; + // Things related to query logging/sampling for debugging + // slow/frequent queries during development. Always log queries + // which take 100ms+; shorter queries are sampled accordingly. + // Commit statements, which are typically slow, are logged + // together with the most recently executed SQL statement, for + // disambiguation. + private static final int QUERY_LOG_TIME_IN_MILLIS = 100; private static final int QUERY_LOG_SQL_LENGTH = 64; + private static final String COMMIT_SQL = "COMMIT;"; private final Random mRandom = new Random(); + private String mLastSqlStatement = null; /** Used by native code, do not rename */ /* package */ int mNativeHandle = 0; @@ -290,10 +293,6 @@ public class SQLiteDatabase extends SQLiteClosable { @Override protected void onAllReferencesReleased() { if (isOpen()) { - if (SQLiteDebug.DEBUG_CAPTURE_SQL) { - Log.d(TAG, "captured_sql|" + mPath + "|DETACH DATABASE " + - getDatabaseName(mPath) + ";"); - } if (SQLiteDebug.DEBUG_SQL_CACHE) { mTimeClosed = getTime(); } @@ -548,7 +547,7 @@ public class SQLiteDatabase extends SQLiteClosable { } } if (mTransactionIsSuccessful) { - execSQL("COMMIT;"); + execSQL(COMMIT_SQL); } else { try { execSQL("ROLLBACK;"); @@ -782,7 +781,14 @@ public class SQLiteDatabase extends SQLiteClosable { SQLiteDatabase db = null; try { // Open the database. - return new SQLiteDatabase(path, factory, flags); + SQLiteDatabase sqliteDatabase = new SQLiteDatabase(path, factory, flags); + if (SQLiteDebug.DEBUG_SQL_STATEMENTS) { + sqliteDatabase.enableSqlTracing(path); + } + if (SQLiteDebug.DEBUG_SQL_TIME) { + sqliteDatabase.enableSqlProfiling(path); + } + return sqliteDatabase; } catch (SQLiteDatabaseCorruptException e) { // Try to recover from this, if we can. // TODO: should we do this for other open failures? @@ -1338,7 +1344,7 @@ public class SQLiteDatabase extends SQLiteClosable { */ public long insert(String table, String nullColumnHack, ContentValues values) { try { - return insertWithOnConflict(table, nullColumnHack, values, ConflictAlgorithm.NONE); + return insertWithOnConflict(table, nullColumnHack, values, CONFLICT_NONE); } catch (SQLException e) { Log.e(TAG, "Error inserting " + values, e); return -1; @@ -1360,7 +1366,7 @@ public class SQLiteDatabase extends SQLiteClosable { */ public long insertOrThrow(String table, String nullColumnHack, ContentValues values) throws SQLException { - return insertWithOnConflict(table, nullColumnHack, values, ConflictAlgorithm.NONE); + return insertWithOnConflict(table, nullColumnHack, values, CONFLICT_NONE); } /** @@ -1377,7 +1383,7 @@ public class SQLiteDatabase extends SQLiteClosable { public long replace(String table, String nullColumnHack, ContentValues initialValues) { try { return insertWithOnConflict(table, nullColumnHack, initialValues, - ConflictAlgorithm.REPLACE); + CONFLICT_REPLACE); } catch (SQLException e) { Log.e(TAG, "Error inserting " + initialValues, e); return -1; @@ -1399,7 +1405,7 @@ public class SQLiteDatabase extends SQLiteClosable { public long replaceOrThrow(String table, String nullColumnHack, ContentValues initialValues) throws SQLException { return insertWithOnConflict(table, nullColumnHack, initialValues, - ConflictAlgorithm.REPLACE); + CONFLICT_REPLACE); } /** @@ -1412,10 +1418,10 @@ public class SQLiteDatabase extends SQLiteClosable { * @param initialValues this map contains the initial column values for the * row. The keys should be the column names and the values the * column values - * @param conflictAlgorithm {@link ConflictAlgorithm} for insert conflict resolver + * @param conflictAlgorithm for insert conflict resolver * @return the row ID of the newly inserted row * OR the primary key of the existing row if the input param 'conflictAlgorithm' = - * {@link ConflictAlgorithm#IGNORE} + * {@link #CONFLICT_IGNORE} * OR -1 if any error */ public long insertWithOnConflict(String table, String nullColumnHack, @@ -1427,7 +1433,7 @@ public class SQLiteDatabase extends SQLiteClosable { // Measurements show most sql lengths <= 152 StringBuilder sql = new StringBuilder(152); sql.append("INSERT"); - sql.append(ConflictAlgorithm.VALUES[conflictAlgorithm]); + sql.append(CONFLICT_VALUES[conflictAlgorithm]); sql.append(" INTO "); sql.append(table); // Measurements show most values lengths < 40 @@ -1551,7 +1557,7 @@ public class SQLiteDatabase extends SQLiteClosable { * @return the number of rows affected */ public int update(String table, ContentValues values, String whereClause, String[] whereArgs) { - return updateWithOnConflict(table, values, whereClause, whereArgs, ConflictAlgorithm.NONE); + return updateWithOnConflict(table, values, whereClause, whereArgs, CONFLICT_NONE); } /** @@ -1562,7 +1568,7 @@ public class SQLiteDatabase extends SQLiteClosable { * valid value that will be translated to NULL. * @param whereClause the optional WHERE clause to apply when updating. * Passing null will update all rows. - * @param conflictAlgorithm {@link ConflictAlgorithm} for update conflict resolver + * @param conflictAlgorithm for update conflict resolver * @return the number of rows affected */ public int updateWithOnConflict(String table, ContentValues values, @@ -1577,7 +1583,7 @@ public class SQLiteDatabase extends SQLiteClosable { StringBuilder sql = new StringBuilder(120); sql.append("UPDATE "); - sql.append(ConflictAlgorithm.VALUES[conflictAlgorithm]); + sql.append(CONFLICT_VALUES[conflictAlgorithm]); sql.append(table); sql.append(" SET "); @@ -1651,10 +1657,7 @@ public class SQLiteDatabase extends SQLiteClosable { * @throws SQLException If the SQL string is invalid for some reason */ public void execSQL(String sql) throws SQLException { - long timeStart = Debug.threadCpuTimeNanos(); - if (SQLiteDebug.DEBUG_CAPTURE_SQL) { - Log.v(TAG, SQLiteDebug.captureSql(this.getPath(), sql, null)); - } + long timeStart = SystemClock.uptimeMillis(); lock(); try { native_execSQL(sql); @@ -1664,7 +1667,15 @@ public class SQLiteDatabase extends SQLiteClosable { } finally { unlock(); } - logTimeStat(sql, timeStart); + + // Log commit statements along with the most recently executed + // SQL statement for disambiguation. Note that instance + // equality to COMMIT_SQL is safe here. + if (sql == COMMIT_SQL) { + logTimeStat(sql + mLastSqlStatement, timeStart); + } else { + logTimeStat(sql, timeStart); + } } /** @@ -1680,10 +1691,7 @@ public class SQLiteDatabase extends SQLiteClosable { if (bindArgs == null) { throw new IllegalArgumentException("Empty bindArgs"); } - if (SQLiteDebug.DEBUG_CAPTURE_SQL) { - Log.v(TAG, SQLiteDebug.captureSql(this.getPath(), sql, bindArgs)); - } - long timeStart = Debug.threadCpuTimeNanos(); + long timeStart = SystemClock.uptimeMillis(); lock(); SQLiteStatement statement = null; try { @@ -1741,10 +1749,6 @@ public class SQLiteDatabase extends SQLiteClosable { mLeakedException = new IllegalStateException(path + " SQLiteDatabase created and never closed"); mFactory = factory; - if (SQLiteDebug.DEBUG_CAPTURE_SQL) { - Log.d(TAG, "captured_sql|" + mPath + "|ATTACH DATABASE '" + mPath + - "' as " + getDatabaseName(mPath) + ";"); - } dbopen(mPath, mFlags); if (SQLiteDebug.DEBUG_SQL_CACHE) { mTimeOpened = getTime(); @@ -1754,10 +1758,6 @@ public class SQLiteDatabase extends SQLiteClosable { setLocale(Locale.getDefault()); } catch (RuntimeException e) { Log.e(TAG, "Failed to setLocale() when constructing, closing the database", e); - if (SQLiteDebug.DEBUG_CAPTURE_SQL) { - Log.d(TAG, "captured_sql|" + mPath + "|DETACH DATABASE " + - getDatabaseName(mPath) + ";"); - } dbclose(); if (SQLiteDebug.DEBUG_SQL_CACHE) { mTimeClosed = getTime(); @@ -1770,20 +1770,6 @@ public class SQLiteDatabase extends SQLiteClosable { return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS ").format(System.currentTimeMillis()); } - private String getDatabaseName(String path) { - if (path == null || path.trim().length() == 0) { - return "db not specified?"; - } - - if (path.equalsIgnoreCase(":memory:")) { - return "memorydb"; - } - String[] tokens = path.split("/"); - String[] lastNodeTokens = tokens[tokens.length - 1].split("\\.", 2); - return (lastNodeTokens.length == 1) ? lastNodeTokens[0] - : lastNodeTokens[0] + lastNodeTokens[1]; - } - /** * return whether the DB is opened as read only. * @return true if DB is opened as read only @@ -1814,17 +1800,22 @@ public class SQLiteDatabase extends SQLiteClosable { - /* package */ void logTimeStat(String sql, long beginNanos) { + /* package */ void logTimeStat(String sql, long beginMillis) { + // Keep track of the last statement executed here, as this is + // the common funnel through which all methods of hitting + // libsqlite eventually flow. + mLastSqlStatement = sql; + // Sample fast queries in proportion to the time taken. // Quantize the % first, so the logged sampling probability // exactly equals the actual sampling rate for this query. int samplePercent; - long nanos = Debug.threadCpuTimeNanos() - beginNanos; - if (nanos >= QUERY_LOG_TIME_IN_NANOS) { + long durationMillis = SystemClock.uptimeMillis() - beginMillis; + if (durationMillis >= QUERY_LOG_TIME_IN_MILLIS) { samplePercent = 100; } else { - samplePercent = (int) (100 * nanos / QUERY_LOG_TIME_IN_NANOS) + 1; + samplePercent = (int) (100 * durationMillis / QUERY_LOG_TIME_IN_MILLIS) + 1; if (mRandom.nextInt(100) >= samplePercent) return; } @@ -1841,8 +1832,8 @@ public class SQLiteDatabase extends SQLiteClosable { String blockingPackage = ActivityThread.currentPackageName(); if (blockingPackage == null) blockingPackage = ""; - int millis = (int) (nanos / 1000000); - EventLog.writeEvent(EVENT_DB_OPERATION, mPath, sql, millis, blockingPackage, samplePercent); + EventLog.writeEvent( + EVENT_DB_OPERATION, mPath, sql, durationMillis, blockingPackage, samplePercent); } /** @@ -1909,8 +1900,7 @@ public class SQLiteDatabase extends SQLiteClosable { */ Log.w(TAG, "Reached MAX size for compiled-sql statement cache for database " + getPath() + "; i.e., NO space for this sql statement in cache: " + - sql + ". Make sure your sql " + - "statements are using prepared-sql-statement syntax with '?' for " + + sql + ". Please change your sql statements to use '?' for " + "bindargs, instead of using actual values"); /* increment the number of times this warnings has been printed. @@ -1922,8 +1912,9 @@ public class SQLiteDatabase extends SQLiteClosable { mCacheFullWarnings = 0; // clear the cache mCompiledQueries.clear(); - Log.w(TAG, "compiled-sql statement cache cleared for the database " + - getPath()); + Log.w(TAG, "Compiled-sql statement cache for database: " + + getPath() + " hit MAX size-limit too many times. " + + "Removing all compiled-sql statements from the cache."); } else { // clear just a single entry from cache Set<String> keySet = mCompiledQueries.keySet(); @@ -2061,6 +2052,23 @@ public class SQLiteDatabase extends SQLiteClosable { private native void dbopen(String path, int flags); /** + * Native call to setup tracing of all sql statements + * + * @param path the full path to the database + */ + private native void enableSqlTracing(String path); + + /** + * Native call to setup profiling of all sql statements. + * currently, sqlite's profiling = printing of execution-time + * (wall-clock time) of each of the sql statements, as they + * are executed. + * + * @param path the full path to the database + */ + private native void enableSqlProfiling(String path); + + /** * Native call to execute a raw SQL statement. {@link #lock} must be held * when calling this method. * diff --git a/core/java/android/database/sqlite/SQLiteDebug.java b/core/java/android/database/sqlite/SQLiteDebug.java index b12034a..4ea680e 100644 --- a/core/java/android/database/sqlite/SQLiteDebug.java +++ b/core/java/android/database/sqlite/SQLiteDebug.java @@ -31,17 +31,17 @@ public final class SQLiteDebug { Log.isLoggable("SQLiteStatements", Log.VERBOSE); /** - * Controls the printing of compiled-sql-statement cache stats. + * Controls the printing of wall-clock time taken to execute SQL statements + * as they are executed. */ - public static final boolean DEBUG_SQL_CACHE = - Log.isLoggable("SQLiteCompiledSql", Log.VERBOSE); + public static final boolean DEBUG_SQL_TIME = + Log.isLoggable("SQLiteTime", Log.VERBOSE); /** - * Controls the capturing and printing of complete sql statement including the bind args and - * the database name. + * Controls the printing of compiled-sql-statement cache stats. */ - public static final boolean DEBUG_CAPTURE_SQL = - Log.isLoggable("SQLiteCaptureSql", Log.VERBOSE); + public static final boolean DEBUG_SQL_CACHE = + Log.isLoggable("SQLiteCompiledSql", Log.VERBOSE); /** * Controls the stack trace reporting of active cursors being @@ -121,62 +121,4 @@ public final class SQLiteDebug { static synchronized void notifyActiveCursorFinalized() { sNumActiveCursorsFinalized++; } - - /** - * returns a message containing the given database name (path) and the string built by - * replacing "?" characters in the given sql string with the corresponding - * positional values from the given param bindArgs. - * - * @param path the database name - * @param sql sql string with possibly "?" for bindargs - * @param bindArgs args for "?"s in the above string - * @return the String to be logged - */ - /* package */ static String captureSql(String path, String sql, Object[] bindArgs) { - // how many bindargs in sql - sql = sql.trim(); - String args[] = sql.split("\\?"); - // how many "?"s in the given sql string? - int varArgsInSql = (sql.endsWith("?")) ? args.length : args.length - 1; - - // how many bind args do we have in the given input param bindArgs - int bindArgsLen = (bindArgs == null) ? 0 : bindArgs.length; - if (varArgsInSql < bindArgsLen) { - return "too many bindArgs provided. " + - "# of bindArgs = " + bindArgsLen + ", # of varargs = " + varArgsInSql + - "; sql = " + sql; - } - - // if there are no bindArgs, we are done. log the sql as is. - if (bindArgsLen == 0 && varArgsInSql == 0) { - return logSql(path, sql); - } - - StringBuilder buf = new StringBuilder(); - - // take the supplied bindArgs and plug them into sql - for (int i = 0; i < bindArgsLen; i++) { - buf.append(args[i]); - buf.append(bindArgs[i]); - } - - // does given sql have more varArgs than the supplied bindArgs - // if so, assign nulls to the extra varArgs in sql - for (int i = bindArgsLen; i < varArgsInSql; i ++) { - buf.append(args[i]); - buf.append("null"); - } - - // if there are any characters left in the given sql string AFTER the last "?" - // log them also. for example, if the given sql = "select * from test where a=? and b=1 - // then the following code appends " and b=1" string to buf. - if (varArgsInSql < args.length) { - buf.append(args[varArgsInSql]); - } - return logSql(path, buf.toString()); - } - - private static String logSql(String path, String sql) { - return "captured_sql|" + path + "|" + sql + ";"; - } } diff --git a/core/java/android/database/sqlite/SQLiteProgram.java b/core/java/android/database/sqlite/SQLiteProgram.java index 1159c1d..2bb2f5d 100644 --- a/core/java/android/database/sqlite/SQLiteProgram.java +++ b/core/java/android/database/sqlite/SQLiteProgram.java @@ -16,19 +16,10 @@ package android.database.sqlite; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; - -import android.util.Log; - /** * A base class for compiled SQLite programs. */ public abstract class SQLiteProgram extends SQLiteClosable { - private static final String TAG = "SQLiteProgram"; /** The database this program is compiled against. */ protected SQLiteDatabase mDatabase; @@ -53,16 +44,7 @@ public abstract class SQLiteProgram extends SQLiteClosable { */ protected int nStatement = 0; - /** - * stores all bindargs for debugging purposes - */ - private Map<Integer, String> mBindArgs = null; - /* package */ SQLiteProgram(SQLiteDatabase db, String sql) { - if (SQLiteDebug.DEBUG_SQL_STATEMENTS) { - Log.d(TAG, "processing sql: " + sql); - } - mDatabase = db; mSql = sql; db.acquireReference(); @@ -76,32 +58,51 @@ public abstract class SQLiteProgram extends SQLiteClosable { // add it to the cache of compiled-sqls db.addToCompiledQueries(sql, mCompiledSql); + mCompiledSql.acquire(); + } else { + // it is already in compiled-sql cache. + if (mCompiledSql.isInUse()) { + // but the CompiledSql in cache is in use by some other SQLiteProgram object. + // we can't have two different SQLiteProgam objects can't share the same + // CompiledSql object. create a new one. + // finalize it when I am done with it in "this" object. + mCompiledSql = new SQLiteCompiledSql(db, sql); + } else { + // the CompiledSql in cache is NOT in use by any other SQLiteProgram object. + // it is safe to give it to this SQLIteProgram Object. + mCompiledSql.acquire(); + } } nStatement = mCompiledSql.nStatement; } @Override protected void onAllReferencesReleased() { - releaseCompiledSqlIfInCache(); + releaseCompiledSqlIfNotInCache(); mDatabase.releaseReference(); mDatabase.removeSQLiteClosable(this); } @Override protected void onAllReferencesReleasedFromContainer() { - releaseCompiledSqlIfInCache(); + releaseCompiledSqlIfNotInCache(); mDatabase.releaseReference(); } - private void releaseCompiledSqlIfInCache() { + private void releaseCompiledSqlIfNotInCache() { if (mCompiledSql == null) { return; } synchronized(mDatabase.mCompiledQueries) { if (!mDatabase.mCompiledQueries.containsValue(mCompiledSql)) { + // it is NOT in compiled-sql cache. i.e., responsibility of + // release this statement is on me. mCompiledSql.releaseSqlStatement(); mCompiledSql = null; // so that GC doesn't call finalize() on it nStatement = 0; + } else { + // it is in compiled-sql cache. reset its CompiledSql#mInUse flag + mCompiledSql.release(); } } } @@ -120,7 +121,7 @@ public abstract class SQLiteProgram extends SQLiteClosable { } /** - * @deprecated use this.compiledStatement.compile instead + * @deprecated This method is deprecated and must not be used. * * @param sql the SQL string to compile * @param forceCompilation forces the SQL to be recompiled in the event that there is an @@ -138,9 +139,6 @@ public abstract class SQLiteProgram extends SQLiteClosable { * @param index The 1-based index to the parameter to bind null to */ public void bindNull(int index) { - if (SQLiteDebug.DEBUG_CAPTURE_SQL) { - addToBindArgs(index, "null"); - } acquireReference(); try { native_bind_null(index); @@ -157,9 +155,6 @@ public abstract class SQLiteProgram extends SQLiteClosable { * @param value The value to bind */ public void bindLong(int index, long value) { - if (SQLiteDebug.DEBUG_CAPTURE_SQL) { - addToBindArgs(index, value + ""); - } acquireReference(); try { native_bind_long(index, value); @@ -176,9 +171,6 @@ public abstract class SQLiteProgram extends SQLiteClosable { * @param value The value to bind */ public void bindDouble(int index, double value) { - if (SQLiteDebug.DEBUG_CAPTURE_SQL) { - addToBindArgs(index, value + ""); - } acquireReference(); try { native_bind_double(index, value); @@ -195,9 +187,6 @@ public abstract class SQLiteProgram extends SQLiteClosable { * @param value The value to bind */ public void bindString(int index, String value) { - if (SQLiteDebug.DEBUG_CAPTURE_SQL) { - addToBindArgs(index, "'" + value + "'"); - } if (value == null) { throw new IllegalArgumentException("the bind value at index " + index + " is null"); } @@ -217,9 +206,6 @@ public abstract class SQLiteProgram extends SQLiteClosable { * @param value The value to bind */ public void bindBlob(int index, byte[] value) { - if (SQLiteDebug.DEBUG_CAPTURE_SQL) { - addToBindArgs(index, "blob"); - } if (value == null) { throw new IllegalArgumentException("the bind value at index " + index + " is null"); } @@ -235,9 +221,6 @@ public abstract class SQLiteProgram extends SQLiteClosable { * Clears all existing bindings. Unset bindings are treated as NULL. */ public void clearBindings() { - if (SQLiteDebug.DEBUG_CAPTURE_SQL) { - mBindArgs = null; - } acquireReference(); try { native_clear_bindings(); @@ -259,39 +242,7 @@ public abstract class SQLiteProgram extends SQLiteClosable { } /** - * this method is called under the debug flag {@link SQLiteDebug.DEBUG_CAPTURE_SQL} only. - * it collects the bindargs as they are called by the callers the bind... methods in this - * class. - */ - private void addToBindArgs(int index, String obj) { - if (mBindArgs == null) { - mBindArgs = new HashMap<Integer, String>(); - } - mBindArgs.put(index, obj); - } - - /** - * constructs all the bindargs in sequence and returns a String Array of the values. - * it uses the HashMap built up by the above method. - * - * @return the string array of bindArgs with the args arranged in sequence - */ - /* package */ String[] getBindArgs() { - if (mBindArgs == null) { - return null; - } - Set<Integer> indexSet = mBindArgs.keySet(); - ArrayList<Integer> indexList = new ArrayList<Integer>(indexSet); - Collections.sort(indexList); - int len = indexList.size(); - String[] bindObjs = new String[len]; - for (int i = 0; i < len; i++) { - bindObjs[i] = mBindArgs.get(indexList.get(i)); - } - return bindObjs; - } - - /** + * @deprecated This method is deprecated and must not be used. * Compiles SQL into a SQLite program. * * <P>The database lock must be held when calling this method. @@ -299,6 +250,10 @@ public abstract class SQLiteProgram extends SQLiteClosable { */ @Deprecated protected final native void native_compile(String sql); + + /** + * @deprecated This method is deprecated and must not be used. + */ @Deprecated protected final native void native_finalize(); diff --git a/core/java/android/database/sqlite/SQLiteQuery.java b/core/java/android/database/sqlite/SQLiteQuery.java index c34661d..7cd9561 100644 --- a/core/java/android/database/sqlite/SQLiteQuery.java +++ b/core/java/android/database/sqlite/SQLiteQuery.java @@ -17,7 +17,6 @@ package android.database.sqlite; import android.database.CursorWindow; -import android.os.Debug; import android.os.SystemClock; import android.util.Log; @@ -58,7 +57,7 @@ public class SQLiteQuery extends SQLiteProgram { */ /* package */ int fillWindow(CursorWindow window, int maxRead, int lastPos) { - long timeStart = Debug.threadCpuTimeNanos(); + long timeStart = SystemClock.uptimeMillis(); mDatabase.lock(); try { diff --git a/core/java/android/database/sqlite/SQLiteStatement.java b/core/java/android/database/sqlite/SQLiteStatement.java index 0cee3c5..f29b69f 100644 --- a/core/java/android/database/sqlite/SQLiteStatement.java +++ b/core/java/android/database/sqlite/SQLiteStatement.java @@ -16,8 +16,7 @@ package android.database.sqlite; -import android.os.Debug; -import android.util.Log; +import android.os.SystemClock; /** * A pre-compiled statement against a {@link SQLiteDatabase} that can be reused. @@ -27,8 +26,6 @@ import android.util.Log; */ public class SQLiteStatement extends SQLiteProgram { - private static final String TAG = "SQLiteStatement"; - /** * Don't use SQLiteStatement constructor directly, please use * {@link SQLiteDatabase#compileStatement(String)} @@ -47,17 +44,11 @@ public class SQLiteStatement extends SQLiteProgram * some reason */ public void execute() { - long timeStart = Debug.threadCpuTimeNanos(); + long timeStart = SystemClock.uptimeMillis(); mDatabase.lock(); acquireReference(); try { - if (SQLiteDebug.DEBUG_SQL_STATEMENTS) { - Log.v(TAG, "execute() for [" + mSql + "]"); - } - if (SQLiteDebug.DEBUG_CAPTURE_SQL) { - Log.v(TAG, SQLiteDebug.captureSql(mDatabase.getPath(), mSql, getBindArgs())); - } native_execute(); mDatabase.logTimeStat(mSql, timeStart); } finally { @@ -77,17 +68,11 @@ public class SQLiteStatement extends SQLiteProgram * some reason */ public long executeInsert() { - long timeStart = Debug.threadCpuTimeNanos(); + long timeStart = SystemClock.uptimeMillis(); mDatabase.lock(); acquireReference(); try { - if (SQLiteDebug.DEBUG_SQL_STATEMENTS) { - Log.v(TAG, "executeInsert() for [" + mSql + "]"); - } - if (SQLiteDebug.DEBUG_CAPTURE_SQL) { - Log.v(TAG, SQLiteDebug.captureSql(mDatabase.getPath(), mSql, getBindArgs())); - } native_execute(); mDatabase.logTimeStat(mSql, timeStart); return mDatabase.lastInsertRow(); @@ -106,17 +91,11 @@ public class SQLiteStatement extends SQLiteProgram * @throws android.database.sqlite.SQLiteDoneException if the query returns zero rows */ public long simpleQueryForLong() { - long timeStart = Debug.threadCpuTimeNanos(); + long timeStart = SystemClock.uptimeMillis(); mDatabase.lock(); acquireReference(); try { - if (SQLiteDebug.DEBUG_SQL_STATEMENTS) { - Log.v(TAG, "simpleQueryForLong() for [" + mSql + "]"); - } - if (SQLiteDebug.DEBUG_CAPTURE_SQL) { - Log.v(TAG, SQLiteDebug.captureSql(mDatabase.getPath(), mSql, getBindArgs())); - } long retValue = native_1x1_long(); mDatabase.logTimeStat(mSql, timeStart); return retValue; @@ -135,17 +114,11 @@ public class SQLiteStatement extends SQLiteProgram * @throws android.database.sqlite.SQLiteDoneException if the query returns zero rows */ public String simpleQueryForString() { - long timeStart = Debug.threadCpuTimeNanos(); + long timeStart = SystemClock.uptimeMillis(); mDatabase.lock(); acquireReference(); try { - if (SQLiteDebug.DEBUG_SQL_STATEMENTS) { - Log.v(TAG, "simpleQueryForString() for [" + mSql + "]"); - } - if (SQLiteDebug.DEBUG_CAPTURE_SQL) { - Log.v(TAG, SQLiteDebug.captureSql(mDatabase.getPath(), mSql, getBindArgs())); - } String retValue = native_1x1_string(); mDatabase.logTimeStat(mSql, timeStart); return retValue; diff --git a/core/java/android/gesture/Learner.java b/core/java/android/gesture/Learner.java index 60997e0..a105652 100755 --- a/core/java/android/gesture/Learner.java +++ b/core/java/android/gesture/Learner.java @@ -72,7 +72,8 @@ abstract class Learner { for (int i = 0; i < count; i++) { final Instance instance = instances.get(i); // the label can be null, as specified in Instance - if ((instance.label == null && name == null) || instance.label.equals(name)) { + if ((instance.label == null && name == null) + || (instance.label != null && instance.label.equals(name))) { toDelete.add(instance); } } diff --git a/core/java/android/hardware/Camera.java b/core/java/android/hardware/Camera.java index 47c2cac..6dba94d 100644 --- a/core/java/android/hardware/Camera.java +++ b/core/java/android/hardware/Camera.java @@ -26,7 +26,7 @@ import java.io.IOException; import android.util.Log; import android.view.Surface; import android.view.SurfaceHolder; -import android.graphics.PixelFormat; +import android.graphics.ImageFormat; import android.os.Handler; import android.os.Looper; import android.os.Message; @@ -192,7 +192,7 @@ public class Camera { * The callback that delivers the preview frames. * * @param data The contents of the preview frame in the format defined - * by {@link android.graphics.PixelFormat}, which can be queried + * by {@link android.graphics.ImageFormat}, which can be queried * with {@link android.hardware.Camera.Parameters#getPreviewFormat()}. * If {@link android.hardware.Camera.Parameters#setPreviewFormat(int)} * is never called, the default will be the YCbCr_420_SP @@ -276,7 +276,7 @@ public class Camera { * Adds a pre-allocated buffer to the callback buffer queue. * Preview width and height can be determined from getPreviewSize, and bitsPerPixel can be * found from from {@link android.hardware.Camera.Parameters#getPreviewFormat()} and - * {@link android.graphics.PixelFormat#getPixelFormatInfo(int, PixelFormat)} + * {@link android.graphics.ImageFormat#getBitsPerPixel(int)} * * Alternatively, a buffer from a previous callback may be passed in or used * to determine the size of new preview frame buffers. @@ -723,6 +723,7 @@ public class Camera { private static final String KEY_FOCAL_LENGTH = "focal-length"; private static final String KEY_HORIZONTAL_VIEW_ANGLE = "horizontal-view-angle"; private static final String KEY_VERTICAL_VIEW_ANGLE = "vertical-view-angle"; + private static final String KEY_EXPOSURE_COMPENSATION = "exposure-compensation"; // Parameter key suffix for supported values. private static final String SUPPORTED_VALUES_SUFFIX = "-values"; @@ -1051,7 +1052,8 @@ public class Camera { } /** - * Sets the rate at which preview frames are received. + * Sets the rate at which preview frames are received. This is the + * target frame rate. The actual frame rate depends on the driver. * * @param fps the frame rate (frames per second) */ @@ -1060,8 +1062,9 @@ public class Camera { } /** - * Returns the setting for the rate at which preview frames - * are received. + * Returns the setting for the rate at which preview frames are + * received. This is the target frame rate. The actual frame rate + * depends on the driver. * * @return the frame rate setting (frames per second) */ @@ -1083,15 +1086,15 @@ public class Camera { /** * Sets the image format for preview pictures. * <p>If this is never called, the default format will be - * {@link android.graphics.PixelFormat#YCbCr_420_SP}, which + * {@link android.graphics.ImageFormat#NV21}, which * uses the NV21 encoding format.</p> * * @param pixel_format the desired preview picture format, defined - * by one of the {@link android.graphics.PixelFormat} constants. - * (E.g., <var>PixelFormat.YCbCr_420_SP</var> (default), - * <var>PixelFormat.RGB_565</var>, or - * <var>PixelFormat.JPEG</var>) - * @see android.graphics.PixelFormat + * by one of the {@link android.graphics.ImageFormat} constants. + * (E.g., <var>ImageFormat.NV21</var> (default), + * <var>ImageFormat.RGB_565</var>, or + * <var>ImageFormat.JPEG</var>) + * @see android.graphics.ImageFormat */ public void setPreviewFormat(int pixel_format) { String s = cameraFormatForPixelFormat(pixel_format); @@ -1107,7 +1110,7 @@ public class Camera { * Returns the image format for preview pictures got from * {@link PreviewCallback}. * - * @return the {@link android.graphics.PixelFormat} int representing + * @return the {@link android.graphics.ImageFormat} int representing * the preview picture format. */ public int getPreviewFormat() { @@ -1125,7 +1128,7 @@ public class Camera { ArrayList<Integer> formats = new ArrayList<Integer>(); for (String s : split(str)) { int f = pixelFormatForCameraFormat(s); - if (f == PixelFormat.UNKNOWN) continue; + if (f == ImageFormat.UNKNOWN) continue; formats.add(f); } return formats; @@ -1168,10 +1171,10 @@ public class Camera { * Sets the image format for pictures. * * @param pixel_format the desired picture format - * (<var>PixelFormat.YCbCr_420_SP (NV21)</var>, - * <var>PixelFormat.RGB_565</var>, or - * <var>PixelFormat.JPEG</var>) - * @see android.graphics.PixelFormat + * (<var>ImageFormat.NV21</var>, + * <var>ImageFormat.RGB_565</var>, or + * <var>ImageFormat.JPEG</var>) + * @see android.graphics.ImageFormat */ public void setPictureFormat(int pixel_format) { String s = cameraFormatForPixelFormat(pixel_format); @@ -1186,7 +1189,7 @@ public class Camera { /** * Returns the image format for pictures. * - * @return the PixelFormat int representing the picture format + * @return the ImageFormat int representing the picture format */ public int getPictureFormat() { return pixelFormatForCameraFormat(get(KEY_PICTURE_FORMAT)); @@ -1195,7 +1198,7 @@ public class Camera { /** * Gets the supported picture formats. * - * @return a List of Integer objects (values are PixelFormat.XXX). This + * @return a List of Integer objects (values are ImageFormat.XXX). This * method will always return a list with at least one element. */ public List<Integer> getSupportedPictureFormats() { @@ -1203,7 +1206,7 @@ public class Camera { ArrayList<Integer> formats = new ArrayList<Integer>(); for (String s : split(str)) { int f = pixelFormatForCameraFormat(s); - if (f == PixelFormat.UNKNOWN) continue; + if (f == ImageFormat.UNKNOWN) continue; formats.add(f); } return formats; @@ -1211,35 +1214,35 @@ public class Camera { private String cameraFormatForPixelFormat(int pixel_format) { switch(pixel_format) { - case PixelFormat.YCbCr_422_SP: return PIXEL_FORMAT_YUV422SP; - case PixelFormat.YCbCr_420_SP: return PIXEL_FORMAT_YUV420SP; - case PixelFormat.YCbCr_422_I: return PIXEL_FORMAT_YUV422I; - case PixelFormat.RGB_565: return PIXEL_FORMAT_RGB565; - case PixelFormat.JPEG: return PIXEL_FORMAT_JPEG; - default: return null; + case ImageFormat.NV16: return PIXEL_FORMAT_YUV422SP; + case ImageFormat.NV21: return PIXEL_FORMAT_YUV420SP; + case ImageFormat.YUY2: return PIXEL_FORMAT_YUV422I; + case ImageFormat.RGB_565: return PIXEL_FORMAT_RGB565; + case ImageFormat.JPEG: return PIXEL_FORMAT_JPEG; + default: return null; } } private int pixelFormatForCameraFormat(String format) { if (format == null) - return PixelFormat.UNKNOWN; + return ImageFormat.UNKNOWN; if (format.equals(PIXEL_FORMAT_YUV422SP)) - return PixelFormat.YCbCr_422_SP; + return ImageFormat.NV16; if (format.equals(PIXEL_FORMAT_YUV420SP)) - return PixelFormat.YCbCr_420_SP; + return ImageFormat.NV21; if (format.equals(PIXEL_FORMAT_YUV422I)) - return PixelFormat.YCbCr_422_I; + return ImageFormat.YUY2; if (format.equals(PIXEL_FORMAT_RGB565)) - return PixelFormat.RGB_565; + return ImageFormat.RGB_565; if (format.equals(PIXEL_FORMAT_JPEG)) - return PixelFormat.JPEG; + return ImageFormat.JPEG; - return PixelFormat.UNKNOWN; + return ImageFormat.UNKNOWN; } /** @@ -1540,6 +1543,41 @@ public class Camera { } /** + * Gets the current exposure compensation setting. + * + * @return the current exposure compensation value multiplied by 100. + * null if exposure compensation is not supported. Ex: -100 + * means -1 EV. 130 means +1.3 EV. + * @hide + */ + public int getExposureCompensation() { + return getInt(KEY_EXPOSURE_COMPENSATION); + } + + /** + * Sets the exposure compensation. + * + * @param value exposure compensation multiplied by 100. Ex: -100 means + * -1 EV. 130 means +1.3 EV. + * @hide + */ + public void setExposureCompensation(int value) { + set(KEY_EXPOSURE_COMPENSATION, value); + } + + /** + * Gets the supported exposure compensation. + * + * @return a List of Integer constants. null if exposure compensation is + * not supported. The list is sorted from small to large. Ex: + * -100, -66, -33, 0, 33, 66, 100. + * @hide + */ + public List<Integer> getSupportedExposureCompensation() { + return splitInt(get(KEY_EXPOSURE_COMPENSATION + SUPPORTED_VALUES_SUFFIX)); + } + + /** * Gets current zoom value. This also works when smooth zoom is in * progress. * diff --git a/core/java/android/inputmethodservice/KeyboardView.java b/core/java/android/inputmethodservice/KeyboardView.java index b0c3909..9201e3b 100755..100644 --- a/core/java/android/inputmethodservice/KeyboardView.java +++ b/core/java/android/inputmethodservice/KeyboardView.java @@ -165,6 +165,7 @@ public class KeyboardView extends View implements View.OnClickListener { private static final int DELAY_BEFORE_PREVIEW = 0; private static final int DELAY_AFTER_PREVIEW = 70; + private static final int DEBOUNCE_TIME = 70; private int mVerticalCorrection; private int mProximityThreshold; @@ -620,7 +621,10 @@ public class KeyboardView extends View implements View.OnClickListener { if (mBuffer == null || mKeyboardChanged) { if (mBuffer == null || mKeyboardChanged && (mBuffer.getWidth() != getWidth() || mBuffer.getHeight() != getHeight())) { - mBuffer = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888); + // Make sure our bitmap is at least 1x1 + final int width = Math.max(1, getWidth()); + final int height = Math.max(1, getHeight()); + mBuffer = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); mCanvas = new Canvas(mBuffer); } invalidateAllKeys(); @@ -739,6 +743,10 @@ public class KeyboardView extends View implements View.OnClickListener { final Key key = keys[nearestKeyIndices[i]]; int dist = 0; boolean isInside = key.isInside(x,y); + if (isInside) { + primaryIndex = nearestKeyIndices[i]; + } + if (((mProximityCorrectOn && (dist = key.squaredDistanceFrom(x, y)) < mProximityThreshold) || isInside) @@ -767,10 +775,6 @@ public class KeyboardView extends View implements View.OnClickListener { } } } - - if (isInside) { - primaryIndex = nearestKeyIndices[i]; - } } if (primaryIndex == NOT_A_KEY) { primaryIndex = closestKey; @@ -1119,7 +1123,8 @@ public class KeyboardView extends View implements View.OnClickListener { mSwipeTracker.addMovement(me); // Ignore all motion events until a DOWN. - if (mAbortKey && action != MotionEvent.ACTION_DOWN) { + if (mAbortKey + && action != MotionEvent.ACTION_DOWN && action != MotionEvent.ACTION_CANCEL) { return true; } @@ -1203,6 +1208,7 @@ public class KeyboardView extends View implements View.OnClickListener { } } showPreview(mCurrentKey); + mLastMoveTime = eventTime; break; case MotionEvent.ACTION_UP: @@ -1216,7 +1222,8 @@ public class KeyboardView extends View implements View.OnClickListener { mCurrentKey = keyIndex; mCurrentKeyTime = 0; } - if (mCurrentKeyTime < mLastKeyTime && mLastKey != NOT_A_KEY) { + if (mCurrentKeyTime < mLastKeyTime && mCurrentKeyTime < DEBOUNCE_TIME + && mLastKey != NOT_A_KEY) { mCurrentKey = mLastKey; touchX = mLastCodeX; touchY = mLastCodeY; diff --git a/core/java/android/net/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java index d435df5..badb767 100644 --- a/core/java/android/net/ConnectivityManager.java +++ b/core/java/android/net/ConnectivityManager.java @@ -125,13 +125,21 @@ public class ConnectivityManager /** * @hide + * gives a String[] */ - public static final String EXTRA_AVAILABLE_TETHER_COUNT = "availableCount"; + public static final String EXTRA_AVAILABLE_TETHER = "availableArray"; /** * @hide + * gives a String[] */ - public static final String EXTRA_ACTIVE_TETHER_COUNT = "activeCount"; + public static final String EXTRA_ACTIVE_TETHER = "activeArray"; + + /** + * @hide + * gives a String[] + */ + public static final String EXTRA_ERRORED_TETHER = "erroredArray"; /** * The Default Mobile data connection. When active, all data traffic @@ -400,4 +408,37 @@ public class ConnectivityManager return false; } } + + /** + * {@hide} + */ + public boolean isTetheringSupported() { + try { + return mService.isTetheringSupported(); + } catch (RemoteException e) { + return false; + } + } + + /** + * {@hide} + */ + public String[] getTetherableUsbRegexs() { + try { + return mService.getTetherableUsbRegexs(); + } catch (RemoteException e) { + return new String[0]; + } + } + + /** + * {@hide} + */ + public String[] getTetherableWifiRegexs() { + try { + return mService.getTetherableWifiRegexs(); + } catch (RemoteException e) { + return new String[0]; + } + } } diff --git a/core/java/android/net/Downloads.java b/core/java/android/net/Downloads.java new file mode 100644 index 0000000..72106c8 --- /dev/null +++ b/core/java/android/net/Downloads.java @@ -0,0 +1,645 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net; + +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.os.ParcelFileDescriptor; +import android.os.SystemClock; +import android.provider.BaseColumns; +import android.util.Log; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.File; +import java.io.InputStream; + +/** + * The Download Manager + * + * + */ +public final class Downloads { + + + /** + * Download status codes + */ + + /** + * This download hasn't started yet + */ + public static final int STATUS_PENDING = 190; + + /** + * This download has started + */ + public static final int STATUS_RUNNING = 192; + + /** + * This download has successfully completed. + * Warning: there might be other status values that indicate success + * in the future. + * Use isSucccess() to capture the entire category. + */ + public static final int STATUS_SUCCESS = 200; + + /** + * This download can't be performed because the content type cannot be + * handled. + */ + public static final int STATUS_NOT_ACCEPTABLE = 406; + + /** + * This download has completed with an error. + * Warning: there will be other status values that indicate errors in + * the future. Use isStatusError() to capture the entire category. + */ + public static final int STATUS_UNKNOWN_ERROR = 491; + + /** + * This download couldn't be completed because of an HTTP + * redirect response that the download manager couldn't + * handle. + */ + public static final int STATUS_UNHANDLED_REDIRECT = 493; + + /** + * This download couldn't be completed due to insufficient storage + * space. Typically, this is because the SD card is full. + */ + public static final int STATUS_INSUFFICIENT_SPACE_ERROR = 498; + + /** + * This download couldn't be completed because no external storage + * device was found. Typically, this is because the SD card is not + * mounted. + */ + public static final int STATUS_DEVICE_NOT_FOUND_ERROR = 499; + + /** + * Returns whether the status is a success (i.e. 2xx). + */ + public static boolean isStatusSuccess(int status) { + return (status >= 200 && status < 300); + } + + /** + * Returns whether the status is an error (i.e. 4xx or 5xx). + */ + public static boolean isStatusError(int status) { + return (status >= 400 && status < 600); + } + + /** + * Download destinations + */ + + /** + * This download will be saved to the external storage. This is the + * default behavior, and should be used for any file that the user + * can freely access, copy, delete. Even with that destination, + * unencrypted DRM files are saved in secure internal storage. + * Downloads to the external destination only write files for which + * there is a registered handler. The resulting files are accessible + * by filename to all applications. + */ + public static final int DOWNLOAD_DESTINATION_EXTERNAL = 1; + + /** + * This download will be saved to the download manager's private + * partition. This is the behavior used by applications that want to + * download private files that are used and deleted soon after they + * get downloaded. All file types are allowed, and only the initiating + * application can access the file (indirectly through a content + * provider). This requires the + * android.permission.ACCESS_DOWNLOAD_MANAGER_ADVANCED permission. + */ + public static final int DOWNLOAD_DESTINATION_CACHE = 2; + + /** + * This download will be saved to the download manager's private + * partition and will be purged as necessary to make space. This is + * for private files (similar to CACHE_PARTITION) that aren't deleted + * immediately after they are used, and are kept around by the download + * manager as long as space is available. + */ + public static final int DOWNLOAD_DESTINATION_CACHE_PURGEABLE = 3; + + + /** + * An invalid download id + */ + public static final long DOWNLOAD_ID_INVALID = -1; + + + /** + * Broadcast Action: this is sent by the download manager to the app + * 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 ACTION_DOWNLOAD_COMPLETED = + "android.intent.action.DOWNLOAD_COMPLETED"; + + /** + * If extras are specified when requesting a download they will be provided in the intent that + * is sent to the specified class and package when a download has finished. + * <P>Type: TEXT</P> + * <P>Owner can Init</P> + */ + public static final String COLUMN_NOTIFICATION_EXTRAS = "notificationextras"; + + + /** + * Status class for a download + */ + public static final class StatusInfo { + public boolean completed = false; + /** The filename of the active download. */ + public String filename = null; + /** An opaque id for the download */ + public long id = DOWNLOAD_ID_INVALID; + /** An opaque status code for the download */ + public int statusCode = -1; + /** Approximate number of bytes downloaded so far, for debugging purposes. */ + public long bytesSoFar = -1; + + /** + * Returns whether the download is completed + * @return a boolean whether the download is complete. + */ + public boolean isComplete() { + return android.provider.Downloads.Impl.isStatusCompleted(statusCode); + } + + /** + * Returns whether the download is successful + * @return a boolean whether the download is successful. + */ + public boolean isSuccessful() { + return android.provider.Downloads.Impl.isStatusCompleted(statusCode); + } + } + + /** + * Class to access initiate and query download by server uri + */ + public static final class ByUri extends DownloadBase { + /** @hide */ + private ByUri() {} + + /** + * Query where clause by app data. + * @hide + */ + private static final String QUERY_WHERE_APP_DATA_CLAUSE = + android.provider.Downloads.Impl.COLUMN_APP_DATA + "=?"; + + /** + * Gets a Cursor pointing to the download(s) of the current system update. + * @hide + */ + private static final Cursor getCurrentOtaDownloads(Context context, String url) { + return context.getContentResolver().query( + android.provider.Downloads.Impl.CONTENT_URI, + DOWNLOADS_PROJECTION, + QUERY_WHERE_APP_DATA_CLAUSE, + new String[] {url}, + null); + } + + /** + * Returns a StatusInfo with the result of trying to download the + * given URL. Returns null if no attempts have been made. + */ + public static final StatusInfo getStatus( + Context context, + String url, + long redownload_threshold) { + StatusInfo result = null; + boolean hasFailedDownload = false; + long failedDownloadModificationTime = 0; + Cursor c = getCurrentOtaDownloads(context, url); + try { + while (c != null && c.moveToNext()) { + if (result == null) { + result = new StatusInfo(); + } + int status = getStatusOfDownload(c, redownload_threshold); + if (status == STATUS_DOWNLOADING_UPDATE || + status == STATUS_DOWNLOADED_UPDATE) { + result.completed = (status == STATUS_DOWNLOADED_UPDATE); + result.filename = c.getString(DOWNLOADS_COLUMN_FILENAME); + result.id = c.getLong(DOWNLOADS_COLUMN_ID); + result.statusCode = c.getInt(DOWNLOADS_COLUMN_STATUS); + result.bytesSoFar = c.getLong(DOWNLOADS_COLUMN_CURRENT_BYTES); + return result; + } + + long modTime = c.getLong(DOWNLOADS_COLUMN_LAST_MODIFICATION); + if (hasFailedDownload && + modTime < failedDownloadModificationTime) { + // older than the one already in result; skip it. + continue; + } + + hasFailedDownload = true; + failedDownloadModificationTime = modTime; + result.statusCode = c.getInt(DOWNLOADS_COLUMN_STATUS); + result.bytesSoFar = c.getLong(DOWNLOADS_COLUMN_CURRENT_BYTES); + } + } finally { + if (c != null) { + c.close(); + } + } + return result; + } + + /** + * Query where clause for general querying. + */ + private static final String QUERY_WHERE_CLAUSE = + android.provider.Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE + "=? AND " + + android.provider.Downloads.Impl.COLUMN_NOTIFICATION_CLASS + "=?"; + + /** + * Delete all the downloads for a package/class pair. + */ + public static final void removeAllDownloadsByPackage( + Context context, + String notification_package, + String notification_class) { + context.getContentResolver().delete( + android.provider.Downloads.Impl.CONTENT_URI, + QUERY_WHERE_CLAUSE, + new String[] { notification_package, notification_class }); + } + + /** + * The column for the id in the Cursor returned by + * getProgressCursor() + */ + public static final int getProgressColumnId() { + return 0; + } + + /** + * The column for the current byte count in the Cursor returned by + * getProgressCursor() + */ + public static final int getProgressColumnCurrentBytes() { + return 1; + } + + /** + * The column for the total byte count in the Cursor returned by + * getProgressCursor() + */ + public static final int getProgressColumnTotalBytes() { + return 2; + } + + /** @hide */ + private static final String[] PROJECTION = { + BaseColumns._ID, + android.provider.Downloads.Impl.COLUMN_CURRENT_BYTES, + android.provider.Downloads.Impl.COLUMN_TOTAL_BYTES + }; + + /** + * Returns a Cursor representing the progress of the download identified by the ID. + */ + public static final Cursor getProgressCursor(Context context, long id) { + Uri downloadUri = Uri.withAppendedPath(android.provider.Downloads.Impl.CONTENT_URI, + String.valueOf(id)); + return context.getContentResolver().query(downloadUri, PROJECTION, null, null, null); + } + } + + /** + * Class to access downloads by opaque download id + */ + public static final class ById extends DownloadBase { + /** @hide */ + private ById() {} + + /** + * Get the mime tupe of the download specified by the download id + */ + public static String getMimeTypeForId(Context context, long downloadId) { + ContentResolver cr = context.getContentResolver(); + + String mimeType = null; + Cursor downloadCursor = null; + + try { + Uri downloadUri = getDownloadUri(downloadId); + + downloadCursor = cr.query( + downloadUri, new String[]{android.provider.Downloads.Impl.COLUMN_MIME_TYPE}, + null, null, null); + if (downloadCursor.moveToNext()) { + mimeType = downloadCursor.getString(0); + } + } finally { + if (downloadCursor != null) downloadCursor.close(); + } + return mimeType; + } + + /** + * Delete a download by Id + */ + public static void deleteDownload(Context context, long downloadId) { + ContentResolver cr = context.getContentResolver(); + + String mimeType = null; + + Uri downloadUri = getDownloadUri(downloadId); + + cr.delete(downloadUri, null, null); + } + + /** + * Open a filedescriptor to a particular download + */ + public static ParcelFileDescriptor openDownload( + Context context, long downloadId, String mode) + throws FileNotFoundException + { + ContentResolver cr = context.getContentResolver(); + + String mimeType = null; + + Uri downloadUri = getDownloadUri(downloadId); + + return cr.openFileDescriptor(downloadUri, mode); + } + + /** + * Open a stream to a particular download + */ + public static InputStream openDownloadStream(Context context, long downloadId) + throws FileNotFoundException, IOException + { + ContentResolver cr = context.getContentResolver(); + + String mimeType = null; + + Uri downloadUri = getDownloadUri(downloadId); + + return cr.openInputStream(downloadUri); + } + + private static Uri getDownloadUri(long downloadId) { + return Uri.parse(android.provider.Downloads.Impl.CONTENT_URI + "/" + downloadId); + } + + /** + * Returns a StatusInfo with the result of trying to download the + * given URL. Returns null if no attempts have been made. + */ + public static final StatusInfo getStatus( + Context context, + long downloadId) { + StatusInfo result = null; + boolean hasFailedDownload = false; + long failedDownloadModificationTime = 0; + + Uri downloadUri = getDownloadUri(downloadId); + + ContentResolver cr = context.getContentResolver(); + + Cursor c = cr.query( + downloadUri, DOWNLOADS_PROJECTION, null /* selection */, null /* selection args */, + null /* sort order */); + try { + if (!c.moveToNext()) { + return result; + } + + if (result == null) { + result = new StatusInfo(); + } + int status = getStatusOfDownload(c,0); + if (status == STATUS_DOWNLOADING_UPDATE || + status == STATUS_DOWNLOADED_UPDATE) { + result.completed = (status == STATUS_DOWNLOADED_UPDATE); + result.filename = c.getString(DOWNLOADS_COLUMN_FILENAME); + result.id = c.getLong(DOWNLOADS_COLUMN_ID); + result.statusCode = c.getInt(DOWNLOADS_COLUMN_STATUS); + result.bytesSoFar = c.getLong(DOWNLOADS_COLUMN_CURRENT_BYTES); + return result; + } + + long modTime = c.getLong(DOWNLOADS_COLUMN_LAST_MODIFICATION); + + result.statusCode = c.getInt(DOWNLOADS_COLUMN_STATUS); + result.bytesSoFar = c.getLong(DOWNLOADS_COLUMN_CURRENT_BYTES); + } finally { + if (c != null) { + c.close(); + } + } + return result; + } + } + + + /** + * Base class with common functionality for the various download classes + */ + private static class DownloadBase { + /** @hide */ + DownloadBase() {} + + /** + * Initiate a download where the download will be tracked by its URI. + */ + public static long startDownloadByUri( + Context context, + String url, + String cookieData, + boolean showDownload, + int downloadDestination, + boolean allowRoaming, + boolean skipIntegrityCheck, + String title, + String notification_package, + String notification_class, + String notification_extras) { + ContentResolver cr = context.getContentResolver(); + + // Tell download manager to start downloading update. + ContentValues values = new ContentValues(); + values.put(android.provider.Downloads.Impl.COLUMN_URI, url); + values.put(android.provider.Downloads.Impl.COLUMN_COOKIE_DATA, cookieData); + values.put(android.provider.Downloads.Impl.COLUMN_VISIBILITY, + showDownload ? android.provider.Downloads.Impl.VISIBILITY_VISIBLE + : android.provider.Downloads.Impl.VISIBILITY_HIDDEN); + if (title != null) { + values.put(android.provider.Downloads.Impl.COLUMN_TITLE, title); + } + values.put(android.provider.Downloads.Impl.COLUMN_APP_DATA, url); + + + // NOTE: destination should be seperated from whether the download + // can happen when roaming + int destination = android.provider.Downloads.Impl.DESTINATION_EXTERNAL; + switch (downloadDestination) { + case DOWNLOAD_DESTINATION_EXTERNAL: + destination = android.provider.Downloads.Impl.DESTINATION_EXTERNAL; + break; + case DOWNLOAD_DESTINATION_CACHE: + if (allowRoaming) { + destination = android.provider.Downloads.Impl.DESTINATION_CACHE_PARTITION; + } else { + destination = + android.provider.Downloads.Impl.DESTINATION_CACHE_PARTITION_NOROAMING; + } + break; + case DOWNLOAD_DESTINATION_CACHE_PURGEABLE: + destination = + android.provider.Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE; + break; + } + values.put(android.provider.Downloads.Impl.COLUMN_DESTINATION, destination); + values.put(android.provider.Downloads.Impl.COLUMN_NO_INTEGRITY, + skipIntegrityCheck); // Don't check ETag + if (notification_package != null && notification_class != null) { + values.put(android.provider.Downloads.Impl.COLUMN_NOTIFICATION_PACKAGE, + notification_package); + values.put(android.provider.Downloads.Impl.COLUMN_NOTIFICATION_CLASS, + notification_class); + + if (notification_extras != null) { + values.put(android.provider.Downloads.Impl.COLUMN_NOTIFICATION_EXTRAS, + notification_extras); + } + } + + Uri downloadUri = cr.insert(android.provider.Downloads.Impl.CONTENT_URI, values); + + long downloadId = DOWNLOAD_ID_INVALID; + if (downloadUri != null) { + downloadId = Long.parseLong(downloadUri.getLastPathSegment()); + } + return downloadId; + } + } + + /** @hide */ + private static final int STATUS_INVALID = 0; + /** @hide */ + private static final int STATUS_DOWNLOADING_UPDATE = 3; + /** @hide */ + private static final int STATUS_DOWNLOADED_UPDATE = 4; + + /** + * Column projection for the query to the download manager. This must match + * with the constants DOWNLOADS_COLUMN_*. + * @hide + */ + private static final String[] DOWNLOADS_PROJECTION = { + BaseColumns._ID, + android.provider.Downloads.Impl.COLUMN_APP_DATA, + android.provider.Downloads.Impl.COLUMN_STATUS, + android.provider.Downloads.Impl._DATA, + android.provider.Downloads.Impl.COLUMN_LAST_MODIFICATION, + android.provider.Downloads.Impl.COLUMN_CURRENT_BYTES, + }; + + /** + * The column index for the ID. + * @hide + */ + private static final int DOWNLOADS_COLUMN_ID = 0; + /** + * The column index for the URI. + * @hide + */ + private static final int DOWNLOADS_COLUMN_URI = 1; + /** + * The column index for the status code. + * @hide + */ + private static final int DOWNLOADS_COLUMN_STATUS = 2; + /** + * The column index for the filename. + * @hide + */ + private static final int DOWNLOADS_COLUMN_FILENAME = 3; + /** + * The column index for the last modification time. + * @hide + */ + private static final int DOWNLOADS_COLUMN_LAST_MODIFICATION = 4; + /** + * The column index for the number of bytes downloaded so far. + * @hide + */ + private static final int DOWNLOADS_COLUMN_CURRENT_BYTES = 5; + + /** + * Gets the status of a download. + * + * @param c A Cursor pointing to a download. The URL column is assumed to be valid. + * @return The status of the download. + * @hide + */ + private static final int getStatusOfDownload( Cursor c, long redownload_threshold) { + int status = c.getInt(DOWNLOADS_COLUMN_STATUS); + long realtime = SystemClock.elapsedRealtime(); + + // TODO(dougz): special handling of 503, 404? (eg, special + // explanatory messages to user) + + if (!android.provider.Downloads.Impl.isStatusCompleted(status)) { + // Check if it's stuck + long modified = c.getLong(DOWNLOADS_COLUMN_LAST_MODIFICATION); + long now = System.currentTimeMillis(); + if (now < modified || now - modified > redownload_threshold) { + return STATUS_INVALID; + } + + return STATUS_DOWNLOADING_UPDATE; + } + + if (android.provider.Downloads.Impl.isStatusError(status)) { + return STATUS_INVALID; + } + + String filename = c.getString(DOWNLOADS_COLUMN_FILENAME); + if (filename == null) { + return STATUS_INVALID; + } + + return STATUS_DOWNLOADED_UPDATE; + } + + + /** + * @hide + */ + private Downloads() {} +} diff --git a/core/java/android/net/IConnectivityManager.aidl b/core/java/android/net/IConnectivityManager.aidl index caa3f2b..508e9c3 100644 --- a/core/java/android/net/IConnectivityManager.aidl +++ b/core/java/android/net/IConnectivityManager.aidl @@ -55,7 +55,13 @@ interface IConnectivityManager boolean untether(String iface); + boolean isTetheringSupported(); + String[] getTetherableIfaces(); String[] getTetheredIfaces(); + + String[] getTetherableUsbRegexs(); + + String[] getTetherableWifiRegexs(); } diff --git a/core/java/android/net/SSLCertificateSocketFactory.java b/core/java/android/net/SSLCertificateSocketFactory.java index e40f1b8..f959fee 100644 --- a/core/java/android/net/SSLCertificateSocketFactory.java +++ b/core/java/android/net/SSLCertificateSocketFactory.java @@ -40,224 +40,195 @@ import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; +import org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl; import org.apache.harmony.xnet.provider.jsse.SSLClientSessionCache; import org.apache.harmony.xnet.provider.jsse.SSLContextImpl; import org.apache.harmony.xnet.provider.jsse.SSLParameters; /** - * SSLSocketFactory that provides optional (on debug devices, only) skipping of ssl certificfate - * chain validation and custom read timeouts used just when connecting to the server/negotiating - * an ssl session. - * - * You can skip the ssl certificate checking at runtime by setting socket.relaxsslcheck=yes on - * devices that do not have have ro.secure set. + * SSLSocketFactory implementation with several extra features: + * <ul> + * <li>Timeout specification for SSL handshake operations + * <li>Optional SSL session caching with {@link SSLSessionCache} + * <li>Optionally bypass all SSL certificate checks + * </ul> + * Note that the handshake timeout does not apply to actual connection. + * If you want a connection timeout as well, use {@link #createSocket()} and + * {@link Socket#connect(SocketAddress, int)}. + * <p> + * On development devices, "setprop socket.relaxsslcheck yes" bypasses all + * SSL certificate checks, for testing with development servers. */ public class SSLCertificateSocketFactory extends SSLSocketFactory { + private static final String TAG = "SSLCertificateSocketFactory"; - private static final String LOG_TAG = "SSLCertificateSocketFactory"; - - private static final TrustManager[] TRUST_MANAGER = new TrustManager[] { + private static final TrustManager[] INSECURE_TRUST_MANAGER = new TrustManager[] { new X509TrustManager() { - public X509Certificate[] getAcceptedIssuers() { - return null; - } - - public void checkClientTrusted(X509Certificate[] certs, - String authType) { } - - public void checkServerTrusted(X509Certificate[] certs, - String authType) { } + public X509Certificate[] getAcceptedIssuers() { return null; } + public void checkClientTrusted(X509Certificate[] certs, String authType) { } + public void checkServerTrusted(X509Certificate[] certs, String authType) { } } }; - private final SSLSocketFactory mFactory; + private SSLSocketFactory mInsecureFactory = null; + private SSLSocketFactory mSecureFactory = null; + + private final int mHandshakeTimeoutMillis; + private final SSLClientSessionCache mSessionCache; + private final boolean mSecure; - private final int mSocketReadTimeoutForSslHandshake; + /** @deprecated Use {@link #getDefault(int)} instead. */ + public SSLCertificateSocketFactory(int handshakeTimeoutMillis) { + this(handshakeTimeoutMillis, null, true); + } + + private SSLCertificateSocketFactory( + int handshakeTimeoutMillis, SSLSessionCache cache, boolean secure) { + mHandshakeTimeoutMillis = handshakeTimeoutMillis; + mSessionCache = cache == null ? null : cache.mSessionCache; + mSecure = secure; + } /** - * Do not use this constructor (will be deprecated). Use {@link #getDefault(int)} instead. + * Returns a new socket factory instance with an optional handshake timeout. + * + * @param handshakeTimeoutMillis to use for SSL connection handshake, or 0 + * for none. The socket timeout is reset to 0 after the handshake. + * @return a new SocketFactory with the specified parameters */ - public SSLCertificateSocketFactory(int socketReadTimeoutForSslHandshake) - throws NoSuchAlgorithmException, KeyManagementException { - this(socketReadTimeoutForSslHandshake, null /* cache */); + public static SocketFactory getDefault(int handshakeTimeoutMillis) { + return new SSLCertificateSocketFactory(handshakeTimeoutMillis, null, true); } - private SSLCertificateSocketFactory(int socketReadTimeoutForSslHandshake, - SSLClientSessionCache cache) throws NoSuchAlgorithmException, KeyManagementException { - SSLContextImpl sslContext = new SSLContextImpl(); - sslContext.engineInit(null /* kms */, - TRUST_MANAGER, new java.security.SecureRandom(), - cache /* client cache */, null /* server cache */); - this.mFactory = sslContext.engineGetSocketFactory(); - this.mSocketReadTimeoutForSslHandshake = socketReadTimeoutForSslHandshake; + /** + * Returns a new socket factory instance with an optional handshake timeout + * and SSL session cache. + * + * @param handshakeTimeoutMillis to use for SSL connection handshake, or 0 + * for none. The socket timeout is reset to 0 after the handshake. + * @param cache The {@link SSLClientSessionCache} to use, or null for no cache. + * @return a new SocketFactory with the specified parameters + */ + public static SSLSocketFactory getDefault(int handshakeTimeoutMillis, SSLSessionCache cache) { + return new SSLCertificateSocketFactory(handshakeTimeoutMillis, cache, true); } /** - * Returns a new instance of a socket factory using the specified socket read - * timeout while connecting with the server/negotiating an ssl session. + * Returns a new instance of a socket factory with all SSL security checks + * disabled, using an optional handshake timeout and SSL session cache. + * Sockets created using this factory are vulnerable to man-in-the-middle + * attacks! * - * @param socketReadTimeoutForSslHandshake the socket read timeout used for performing - * ssl handshake. The socket read timeout is set back to 0 after the handshake. - * @return a new SocketFactory, or null on error + * @param handshakeTimeoutMillis to use for SSL connection handshake, or 0 + * for none. The socket timeout is reset to 0 after the handshake. + * @param cache The {@link SSLClientSessionCache} to use, or null for no cache. + * @return an insecure SocketFactory with the specified parameters */ - public static SocketFactory getDefault(int socketReadTimeoutForSslHandshake) { - return getDefault(socketReadTimeoutForSslHandshake, null /* cache */); + public static SSLSocketFactory getInsecure(int handshakeTimeoutMillis, SSLSessionCache cache) { + return new SSLCertificateSocketFactory(handshakeTimeoutMillis, cache, false); } /** - * Returns a new instance of a socket factory using the specified socket read - * timeout while connecting with the server/negotiating an ssl session. - * Persists ssl sessions using the provided {@link SSLClientSessionCache}. - * - * @param socketReadTimeoutForSslHandshake the socket read timeout used for performing - * ssl handshake. The socket read timeout is set back to 0 after the handshake. - * @param cache The {@link SSLClientSessionCache} to use, if any. - * @return a new SocketFactory, or null on error + * Returns a socket factory (also named SSLSocketFactory, but in a different + * namespace) for use with the Apache HTTP stack. * - * @hide - */ - public static SocketFactory getDefault(int socketReadTimeoutForSslHandshake, - SSLClientSessionCache cache) { + * @param handshakeTimeoutMillis to use for SSL connection handshake, or 0 + * for none. The socket timeout is reset to 0 after the handshake. + * @param cache The {@link SSLClientSessionCache} to use, or null for no cache. + * @return a new SocketFactory with the specified parameters + */ + public static org.apache.http.conn.ssl.SSLSocketFactory getHttpSocketFactory( + int handshakeTimeoutMillis, + SSLSessionCache cache) { + return new org.apache.http.conn.ssl.SSLSocketFactory( + new SSLCertificateSocketFactory(handshakeTimeoutMillis, cache, true)); + } + + private SSLSocketFactory makeSocketFactory(TrustManager[] trustManagers) { try { - return new SSLCertificateSocketFactory(socketReadTimeoutForSslHandshake, cache); - } catch (NoSuchAlgorithmException e) { - Log.e(LOG_TAG, - "SSLCertifcateSocketFactory.getDefault" + - " NoSuchAlgorithmException " , e); - return null; + SSLContextImpl sslContext = new SSLContextImpl(); + sslContext.engineInit(null, trustManagers, null, mSessionCache, null); + return sslContext.engineGetSocketFactory(); } catch (KeyManagementException e) { - Log.e(LOG_TAG, - "SSLCertifcateSocketFactory.getDefault" + - " KeyManagementException " , e); - return null; + Log.wtf(TAG, e); + return (SSLSocketFactory) SSLSocketFactory.getDefault(); // Fallback } } - private boolean hasValidCertificateChain(Certificate[] certs) - throws IOException { - boolean trusted = (certs != null && (certs.length > 0)); - - if (trusted) { - try { - // the authtype we pass in doesn't actually matter - SSLParameters.getDefaultTrustManager() - .checkServerTrusted((X509Certificate[]) certs, "RSA"); - } catch (GeneralSecurityException e) { - String exceptionMessage = e != null ? e.getMessage() : "none"; - if (Config.LOGD) { - Log.d(LOG_TAG,"hasValidCertificateChain(): sec. exception: " - + exceptionMessage); + private synchronized SSLSocketFactory getDelegate() { + // Relax the SSL check if instructed (for this factory, or systemwide) + if (!mSecure || ("0".equals(SystemProperties.get("ro.secure")) && + "yes".equals(SystemProperties.get("socket.relaxsslcheck")))) { + if (mInsecureFactory == null) { + if (mSecure) { + Log.w(TAG, "*** BYPASSING SSL SECURITY CHECKS (socket.relaxsslcheck=yes) ***"); + } else { + Log.w(TAG, "Bypassing SSL security checks at caller's request"); } - trusted = false; - } - } - - return trusted; - } - - private void validateSocket(SSLSocket sslSock, String destHost) - throws IOException - { - if (Config.LOGV) { - Log.v(LOG_TAG,"validateSocket() to host "+destHost); - } - - String relaxSslCheck = SystemProperties.get("socket.relaxsslcheck"); - String secure = SystemProperties.get("ro.secure"); - - // only allow relaxing the ssl check on non-secure builds where the relaxation is - // specifically requested. - if ("0".equals(secure) && "yes".equals(relaxSslCheck)) { - if (Config.LOGD) { - Log.d(LOG_TAG,"sys prop socket.relaxsslcheck is set," + - " ignoring invalid certs"); - } - return; - } - - Certificate[] certs = null; - sslSock.setUseClientMode(true); - sslSock.startHandshake(); - certs = sslSock.getSession().getPeerCertificates(); - - // check that the root certificate in the chain belongs to - // a CA we trust - if (certs == null) { - Log.e(LOG_TAG, - "[SSLCertificateSocketFactory] no trusted root CA"); - throw new IOException("no trusted root CA"); - } - - if (Config.LOGV) { - Log.v(LOG_TAG,"validateSocket # certs = " +certs.length); - } - - if (!hasValidCertificateChain(certs)) { - if (Config.LOGD) { - Log.d(LOG_TAG,"validateSocket(): certificate untrusted!"); + mInsecureFactory = makeSocketFactory(INSECURE_TRUST_MANAGER); } - throw new IOException("Certificate untrusted"); - } - - X509Certificate lastChainCert = (X509Certificate) certs[0]; - - if (!DomainNameValidator.match(lastChainCert, destHost)) { - if (Config.LOGD) { - Log.d(LOG_TAG,"validateSocket(): domain name check failed"); + return mInsecureFactory; + } else { + if (mSecureFactory == null) { + mSecureFactory = makeSocketFactory(null); } - throw new IOException("Domain Name check failed"); + return mSecureFactory; } } - public Socket createSocket(Socket socket, String s, int i, boolean flag) - throws IOException - { - throw new IOException("Cannot validate certification without a hostname"); + @Override + public Socket createSocket(Socket k, String host, int port, boolean close) throws IOException { + OpenSSLSocketImpl s = (OpenSSLSocketImpl) getDelegate().createSocket(k, host, port, close); + s.setHandshakeTimeout(mHandshakeTimeoutMillis); + return s; } - public Socket createSocket(InetAddress inaddr, int i, InetAddress inaddr2, int j) - throws IOException - { - throw new IOException("Cannot validate certification without a hostname"); + @Override + public Socket createSocket() throws IOException { + OpenSSLSocketImpl s = (OpenSSLSocketImpl) getDelegate().createSocket(); + s.setHandshakeTimeout(mHandshakeTimeoutMillis); + return s; } - public Socket createSocket(InetAddress inaddr, int i) throws IOException { - throw new IOException("Cannot validate certification without a hostname"); + @Override + public Socket createSocket(InetAddress addr, int port, InetAddress localAddr, int localPort) + throws IOException { + OpenSSLSocketImpl s = (OpenSSLSocketImpl) getDelegate().createSocket( + addr, port, localAddr, localPort); + s.setHandshakeTimeout(mHandshakeTimeoutMillis); + return s; } - public Socket createSocket(String s, int i, InetAddress inaddr, int j) throws IOException { - SSLSocket sslSock = (SSLSocket) mFactory.createSocket(s, i, inaddr, j); - - if (mSocketReadTimeoutForSslHandshake >= 0) { - sslSock.setSoTimeout(mSocketReadTimeoutForSslHandshake); - } - - validateSocket(sslSock,s); - sslSock.setSoTimeout(0); - - return sslSock; + @Override + public Socket createSocket(InetAddress addr, int port) throws IOException { + OpenSSLSocketImpl s = (OpenSSLSocketImpl) getDelegate().createSocket(addr, port); + s.setHandshakeTimeout(mHandshakeTimeoutMillis); + return s; } - public Socket createSocket(String s, int i) throws IOException { - SSLSocket sslSock = (SSLSocket) mFactory.createSocket(s, i); - - if (mSocketReadTimeoutForSslHandshake >= 0) { - sslSock.setSoTimeout(mSocketReadTimeoutForSslHandshake); - } - - validateSocket(sslSock,s); - sslSock.setSoTimeout(0); + @Override + public Socket createSocket(String host, int port, InetAddress localAddr, int localPort) + throws IOException { + OpenSSLSocketImpl s = (OpenSSLSocketImpl) getDelegate().createSocket( + host, port, localAddr, localPort); + s.setHandshakeTimeout(mHandshakeTimeoutMillis); + return s; + } - return sslSock; + @Override + public Socket createSocket(String host, int port) throws IOException { + OpenSSLSocketImpl s = (OpenSSLSocketImpl) getDelegate().createSocket(host, port); + s.setHandshakeTimeout(mHandshakeTimeoutMillis); + return s; } + @Override public String[] getDefaultCipherSuites() { - return mFactory.getSupportedCipherSuites(); + return getDelegate().getSupportedCipherSuites(); } + @Override public String[] getSupportedCipherSuites() { - return mFactory.getSupportedCipherSuites(); + return getDelegate().getSupportedCipherSuites(); } } - - diff --git a/core/java/android/net/SSLSessionCache.java b/core/java/android/net/SSLSessionCache.java new file mode 100644 index 0000000..4cbeb94 --- /dev/null +++ b/core/java/android/net/SSLSessionCache.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net; + +import org.apache.harmony.xnet.provider.jsse.FileClientSessionCache; +import org.apache.harmony.xnet.provider.jsse.SSLClientSessionCache; + +import android.content.Context; +import android.util.Log; + +import java.io.File; +import java.io.IOException; + +/** + * File-based cache of established SSL sessions. When re-establishing a + * connection to the same server, using an SSL session cache can save some time, + * power, and bandwidth by skipping directly to an encrypted stream. + * This is a persistent cache which can span executions of the application. + * + * @see SSLCertificateSocketFactory + */ +public final class SSLSessionCache { + private static final String TAG = "SSLSessionCache"; + /* package */ final SSLClientSessionCache mSessionCache; + + /** + * Create a session cache using the specified directory. + * Individual session entries will be files within the directory. + * Multiple instances for the same directory share data internally. + * + * @param dir to store session files in (created if necessary) + * @throws IOException if the cache can't be opened + */ + public SSLSessionCache(File dir) throws IOException { + mSessionCache = FileClientSessionCache.usingDirectory(dir); + } + + /** + * Create a session cache at the default location for this app. + * Multiple instances share data internally. + * + * @param context for the application + */ + public SSLSessionCache(Context context) { + File dir = context.getDir("sslcache", Context.MODE_PRIVATE); + SSLClientSessionCache cache = null; + try { + cache = FileClientSessionCache.usingDirectory(dir); + } catch (IOException e) { + Log.w(TAG, "Unable to create SSL session cache in " + dir, e); + } + mSessionCache = cache; + } +} diff --git a/core/java/android/net/WebAddress.java b/core/java/android/net/WebAddress.java index f4ae66a..fa13894 100644 --- a/core/java/android/net/WebAddress.java +++ b/core/java/android/net/WebAddress.java @@ -16,6 +16,8 @@ package android.net; +import static com.android.common.Patterns.GOOD_IRI_CHAR; + import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -54,9 +56,10 @@ 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%_]+)*|\\[[0-9a-fA-F:\\.]+\\])?" + + /* host */ "([-" + GOOD_IRI_CHAR + "%_]+(?:\\.[-" + GOOD_IRI_CHAR + "%_]+)*|\\[[0-9a-fA-F:\\.]+\\])?" + /* port */ "(?:\\:([0-9]+))?" + - /* path */ "(\\/?.*)?"); + /* path */ "(\\/?[^#]*)?" + + /* anchor */ ".*"); /** parses given uriString. */ public WebAddress(String address) throws ParseException { diff --git a/core/java/android/os/Base64Utils.java b/core/java/android/os/Base64Utils.java deleted file mode 100644 index 684a469..0000000 --- a/core/java/android/os/Base64Utils.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (C) 2006 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.os; - -/** - * {@hide} - */ -public class Base64Utils -{ - // TODO add encode api here if possible - - public static byte [] decodeBase64(String data) { - return decodeBase64Native(data); - } - private static native byte[] decodeBase64Native(String data); -} - diff --git a/core/java/android/os/Environment.java b/core/java/android/os/Environment.java index 9491bd4..a9831aa 100644 --- a/core/java/android/os/Environment.java +++ b/core/java/android/os/Environment.java @@ -18,7 +18,7 @@ package android.os; import java.io.File; -import android.os.IMountService; +import android.os.storage.IMountService; /** * Provides access to environment variables. @@ -91,6 +91,14 @@ public class Environment { private static final File EXTERNAL_STORAGE_DIRECTORY = getDirectory("EXTERNAL_STORAGE", "/sdcard"); + private static final File EXTERNAL_STORAGE_ANDROID_DATA_DIRECTORY + = new File (new File(getDirectory("EXTERNAL_STORAGE", "/sdcard"), + "Android"), "data"); + + private static final File EXTERNAL_STORAGE_ANDROID_MEDIA_DIRECTORY + = new File (new File(getDirectory("EXTERNAL_STORAGE", "/sdcard"), + "Android"), "media"); + private static final File DOWNLOAD_CACHE_DIRECTORY = getDirectory("DOWNLOAD_CACHE", "/cache"); @@ -102,13 +110,183 @@ public class Environment { } /** - * Gets the Android external storage directory. + * Gets the Android external storage directory. This directory may not + * currently be accessible if it has been mounted by the user on their + * computer, has been removed from the device, or some other problem has + * happened. You can determine its current state with + * {@link #getExternalStorageState()}. + * + * <p>Here is an example of typical code to monitor the state of + * external storage:</p> + * + * {@sample development/samples/ApiDemos/src/com/example/android/apis/content/ExternalStorage.java + * monitor_storage} */ public static File getExternalStorageDirectory() { return EXTERNAL_STORAGE_DIRECTORY; } /** + * Standard directory in which to place any audio files that should be + * in the regular list of music for the user. + * This may be combined with + * {@link #DIRECTORY_PODCASTS}, {@link #DIRECTORY_NOTIFICATIONS}, + * {@link #DIRECTORY_ALARMS}, and {@link #DIRECTORY_RINGTONES} as a series + * of directories to categories a particular audio file as more than one + * type. + */ + public static String DIRECTORY_MUSIC = "Music"; + + /** + * Standard directory in which to place any audio files that should be + * in the list of podcasts that the user can select (not as regular + * music). + * This may be combined with {@link #DIRECTORY_MUSIC}, + * {@link #DIRECTORY_NOTIFICATIONS}, + * {@link #DIRECTORY_ALARMS}, and {@link #DIRECTORY_RINGTONES} as a series + * of directories to categories a particular audio file as more than one + * type. + */ + public static String DIRECTORY_PODCASTS = "Podcasts"; + + /** + * Standard directory in which to place any audio files that should be + * in the list of ringtones that the user can select (not as regular + * music). + * This may be combined with {@link #DIRECTORY_MUSIC}, + * {@link #DIRECTORY_PODCASTS}, {@link #DIRECTORY_NOTIFICATIONS}, and + * {@link #DIRECTORY_ALARMS} as a series + * of directories to categories a particular audio file as more than one + * type. + */ + public static String DIRECTORY_RINGTONES = "Ringtones"; + + /** + * Standard directory in which to place any audio files that should be + * in the list of alarms that the user can select (not as regular + * music). + * This may be combined with {@link #DIRECTORY_MUSIC}, + * {@link #DIRECTORY_PODCASTS}, {@link #DIRECTORY_NOTIFICATIONS}, + * and {@link #DIRECTORY_RINGTONES} as a series + * of directories to categories a particular audio file as more than one + * type. + */ + public static String DIRECTORY_ALARMS = "Alarms"; + + /** + * Standard directory in which to place any audio files that should be + * in the list of notifications that the user can select (not as regular + * music). + * This may be combined with {@link #DIRECTORY_MUSIC}, + * {@link #DIRECTORY_PODCASTS}, + * {@link #DIRECTORY_ALARMS}, and {@link #DIRECTORY_RINGTONES} as a series + * of directories to categories a particular audio file as more than one + * type. + */ + public static String DIRECTORY_NOTIFICATIONS = "Notifications"; + + /** + * Standard directory in which to place pictures that are available to + * the user. Note that this is primarily a convention for the top-level + * public directory, as the media scanner will find and collect pictures + * in any directory. + */ + public static String DIRECTORY_PICTURES = "Pictures"; + + /** + * Standard directory in which to place movies that are available to + * the user. Note that this is primarily a convention for the top-level + * public directory, as the media scanner will find and collect movies + * in any directory. + */ + public static String DIRECTORY_MOVIES = "Movies"; + + /** + * Standard directory in which to place files that have been downloaded by + * the user. Note that this is primarily a convention for the top-level + * public directory, you are free to download files anywhere in your own + * private directories. + */ + public static String DIRECTORY_DOWNLOADS = "Downloads"; + + /** + * The traditional location for pictures and videos when mounting the + * device as a camera. Note that this is primarily a convention for the + * top-level public directory, as this convention makes no sense elsewhere. + */ + public static String DIRECTORY_DCIM = "DCIM"; + + /** + * Get a top-level public external storage directory for placing files of + * a particular type. This is where the user will typically place and + * manage their own files, so you should be careful about what you put here + * to ensure you don't erase their files or get in the way of their own + * organization. + * + * <p>Here is an example of typical code to manipulate a picture on + * the public external storage:</p> + * + * {@sample development/samples/ApiDemos/src/com/example/android/apis/content/ExternalStorage.java + * public_picture} + * + * @param type The type of storage directory to return. Should be one of + * {@link #DIRECTORY_MUSIC}, {@link #DIRECTORY_PODCASTS}, + * {@link #DIRECTORY_RINGTONES}, {@link #DIRECTORY_ALARMS}, + * {@link #DIRECTORY_NOTIFICATIONS}, {@link #DIRECTORY_PICTURES}, + * {@link #DIRECTORY_MOVIES}, {@link #DIRECTORY_DOWNLOADS}, or + * {@link #DIRECTORY_DCIM}. May not be null. + * + * @return Returns the File path for the directory. Note that this + * directory may not yet exist, so you must make sure it exists before + * using it such as with {@link File#mkdirs File.mkdirs()}. + */ + public static File getExternalStoragePublicDirectory(String type) { + return new File(getExternalStorageDirectory(), type); + } + + /** + * Returns the path for android-specific data on the SD card. + * @hide + */ + public static File getExternalStorageAndroidDataDir() { + return EXTERNAL_STORAGE_ANDROID_DATA_DIRECTORY; + } + + /** + * Generates the raw path to an application's data + * @hide + */ + public static File getExternalStorageAppDataDirectory(String packageName) { + return new File(EXTERNAL_STORAGE_ANDROID_DATA_DIRECTORY, packageName); + } + + /** + * Generates the raw path to an application's media + * @hide + */ + public static File getExternalStorageAppMediaDirectory(String packageName) { + return new File(EXTERNAL_STORAGE_ANDROID_MEDIA_DIRECTORY, packageName); + } + + /** + * Generates the path to an application's files. + * @hide + */ + public static File getExternalStorageAppFilesDirectory(String packageName) { + return new File(new File(EXTERNAL_STORAGE_ANDROID_DATA_DIRECTORY, + packageName), "files"); + } + + /** + * Generates the path to an application's cache. + * @hide + */ + public static File getExternalStorageAppCacheDirectory(String packageName) { + return new File(new File(EXTERNAL_STORAGE_ANDROID_DATA_DIRECTORY, + packageName), "cache"); + } + + /** * Gets the Android Download/Cache content directory. */ public static File getDownloadCacheDirectory() { @@ -173,6 +351,8 @@ public class Environment { * Gets the current state of the external storage device. * Note: This call should be deprecated as it doesn't support * multiple volumes. + * + * <p>See {@link #getExternalStorageDirectory()} for an example of its use. */ public static String getExternalStorageState() { try { diff --git a/core/java/android/os/FileObserver.java b/core/java/android/os/FileObserver.java index 3457815..7e99f38 100644 --- a/core/java/android/os/FileObserver.java +++ b/core/java/android/os/FileObserver.java @@ -52,73 +52,75 @@ public abstract class FileObserver { public static final int ALL_EVENTS = ACCESS | MODIFY | ATTRIB | CLOSE_WRITE | CLOSE_NOWRITE | OPEN | MOVED_FROM | MOVED_TO | DELETE | CREATE - | DELETE_SELF | MOVE_SELF; + | DELETE_SELF | MOVE_SELF; private static final String LOG_TAG = "FileObserver"; private static class ObserverThread extends Thread { - private HashMap<Integer, WeakReference> m_observers = new HashMap<Integer, WeakReference>(); - private int m_fd; - - public ObserverThread() { - super("FileObserver"); - m_fd = init(); - } - - public void run() { - observe(m_fd); - } - - public int startWatching(String path, int mask, FileObserver observer) { - int wfd = startWatching(m_fd, path, mask); - - Integer i = new Integer(wfd); - if (wfd >= 0) { - synchronized (m_observers) { - m_observers.put(i, new WeakReference(observer)); - } - } - - return i; - } - - public void stopWatching(int descriptor) { - stopWatching(m_fd, descriptor); - } - - public void onEvent(int wfd, int mask, String path) { - // look up our observer, fixing up the map if necessary... - FileObserver observer; - - synchronized (m_observers) { - WeakReference weak = m_observers.get(wfd); - observer = (FileObserver) weak.get(); - if (observer == null) { - m_observers.remove(wfd); + private HashMap<Integer, WeakReference> m_observers = new HashMap<Integer, WeakReference>(); + private int m_fd; + + public ObserverThread() { + super("FileObserver"); + m_fd = init(); + } + + public void run() { + observe(m_fd); + } + + public int startWatching(String path, int mask, FileObserver observer) { + int wfd = startWatching(m_fd, path, mask); + + Integer i = new Integer(wfd); + if (wfd >= 0) { + synchronized (m_observers) { + m_observers.put(i, new WeakReference(observer)); + } } + + return i; } - // ...then call out to the observer without the sync lock held - if (observer != null) { - try { - observer.onEvent(mask, path); - } catch (Throwable throwable) { - Log.wtf(LOG_TAG, "Unhandled exception in FileObserver " + observer, throwable); + public void stopWatching(int descriptor) { + stopWatching(m_fd, descriptor); + } + + public void onEvent(int wfd, int mask, String path) { + // look up our observer, fixing up the map if necessary... + FileObserver observer = null; + + synchronized (m_observers) { + WeakReference weak = m_observers.get(wfd); + if (weak != null) { // can happen with lots of events from a dead wfd + observer = (FileObserver) weak.get(); + if (observer == null) { + m_observers.remove(wfd); + } + } + } + + // ...then call out to the observer without the sync lock held + if (observer != null) { + try { + observer.onEvent(mask, path); + } catch (Throwable throwable) { + Log.wtf(LOG_TAG, "Unhandled exception in FileObserver " + observer, throwable); + } } } - } - private native int init(); - private native void observe(int fd); - private native int startWatching(int fd, String path, int mask); - private native void stopWatching(int fd, int wfd); + private native int init(); + private native void observe(int fd); + private native int startWatching(int fd, String path, int mask); + private native void stopWatching(int fd, int wfd); } private static ObserverThread s_observerThread; static { - s_observerThread = new ObserverThread(); - s_observerThread.start(); + s_observerThread = new ObserverThread(); + s_observerThread.start(); } // instance @@ -127,30 +129,30 @@ public abstract class FileObserver { private int m_mask; public FileObserver(String path) { - this(path, ALL_EVENTS); + this(path, ALL_EVENTS); } public FileObserver(String path, int mask) { - m_path = path; - m_mask = mask; - m_descriptor = -1; + m_path = path; + m_mask = mask; + m_descriptor = -1; } protected void finalize() { - stopWatching(); + stopWatching(); } public void startWatching() { - if (m_descriptor < 0) { - m_descriptor = s_observerThread.startWatching(m_path, m_mask, this); - } + if (m_descriptor < 0) { + m_descriptor = s_observerThread.startWatching(m_path, m_mask, this); + } } public void stopWatching() { - if (m_descriptor >= 0) { - s_observerThread.stopWatching(m_descriptor); - m_descriptor = -1; - } + if (m_descriptor >= 0) { + s_observerThread.stopWatching(m_descriptor); + m_descriptor = -1; + } } public abstract void onEvent(int event, String path); diff --git a/core/java/android/os/HandlerThread.java b/core/java/android/os/HandlerThread.java index 65301e4..911439a 100644 --- a/core/java/android/os/HandlerThread.java +++ b/core/java/android/os/HandlerThread.java @@ -53,9 +53,9 @@ public class HandlerThread extends Thread { Looper.prepare(); synchronized (this) { mLooper = Looper.myLooper(); - Process.setThreadPriority(mPriority); notifyAll(); } + Process.setThreadPriority(mPriority); onLooperPrepared(); Looper.loop(); mTid = -1; diff --git a/core/java/android/os/ICheckinService.aidl b/core/java/android/os/ICheckinService.aidl deleted file mode 100644 index e5609b0..0000000 --- a/core/java/android/os/ICheckinService.aidl +++ /dev/null @@ -1,41 +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 android.os.IParentalControlCallback; - -/** - * System private API for direct access to the checkin service. - * Users should use the content provider instead. - * - * @see android.provider.Checkin - * {@hide} - */ -interface ICheckinService { - /** Reboot into the recovery system and wipe all user data. */ - void masterClear(); - - /** Reboot into the recovery system, wipe all user data and enable Encrypted File Systems. */ - void masterClearAndToggleEFS(boolean efsEnabled); - - /** - * Determine if the device is under parental control. Return null if - * we are unable to check the parental control status. - */ - void getParentalControlState(IParentalControlCallback p, - String requestingApp); -} diff --git a/core/java/android/os/IMountServiceListener.aidl b/core/java/android/os/IMountServiceListener.aidl deleted file mode 100644 index 3df64b2..0000000 --- a/core/java/android/os/IMountServiceListener.aidl +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (C) 2009 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.os; - -/** - * Callback class for receiving events from MountService. - * - * @hide - */ -interface IMountServiceListener { - /** - * A sharing method has changed availability state. - * - * @param method The share method which has changed. - * @param available The share availability state. - */ - void onShareAvailabilityChanged(String method, boolean available); - - /** - * Media has been inserted - * - * @param label The volume label. - * @param path The volume mount path. - * @param major The backing device major number. - * @param minor The backing device minor number. - */ - void onMediaInserted(String label, String path, int major, int minor); - - /** - * Media has been removed - * - * @param label The volume label. - * @param path The volume mount path. - * @param major The backing device major number. - * @param minor The backing device minor number. - * @param clean Indicates if the removal was clean (unmounted first). - */ - void onMediaRemoved(String label, String path, int major, int minor, boolean clean); - - /** - * Volume state has changed. - * - * @param label The volume label. - * @param path The volume mount path. - * @param oldState The old state of the volume. - * @param newState The new state of the volume. - * - * Note: State is one of the values returned by Environment.getExternalStorageState() - */ - void onVolumeStateChanged(String label, String path, String oldState, String newState); - -} diff --git a/core/java/android/os/INetworkManagementService.aidl b/core/java/android/os/INetworkManagementService.aidl index f48f45f..92041d8 100644 --- a/core/java/android/os/INetworkManagementService.aidl +++ b/core/java/android/os/INetworkManagementService.aidl @@ -148,4 +148,18 @@ interface INetworkManagementService */ void detachPppd(String tty); + /** + * Turn on USB RNDIS support - this will turn off thinks like adb/mass-storage + */ + void startUsbRNDIS(); + + /** + * Turn off USB RNDIS support + */ + void stopUsbRNDIS(); + + /** + * Check the status of USB RNDIS support + */ + boolean isUsbRNDISStarted(); } diff --git a/core/java/android/os/IParentalControlCallback.aidl b/core/java/android/os/IParentalControlCallback.aidl deleted file mode 100644 index 2f1a563..0000000 --- a/core/java/android/os/IParentalControlCallback.aidl +++ /dev/null @@ -1,27 +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.os; - -import com.google.android.net.ParentalControlState; - -/** - * This callback interface is used to deliver the parental control state to the calling application. - * {@hide} - */ -oneway interface IParentalControlCallback { - void onResult(in ParentalControlState state); -} diff --git a/core/java/android/os/Message.java b/core/java/android/os/Message.java index 4130109..476da1d 100644 --- a/core/java/android/os/Message.java +++ b/core/java/android/os/Message.java @@ -40,20 +40,36 @@ public final class Message implements Parcelable { */ public int what; - // Use these fields instead of using the class's Bundle if you can. - /** arg1 and arg2 are lower-cost alternatives to using {@link #setData(Bundle) setData()} - if you only need to store a few integer values. */ + /** + * arg1 and arg2 are lower-cost alternatives to using + * {@link #setData(Bundle) setData()} if you only need to store a + * few integer values. + */ public int arg1; - /** arg1 and arg2 are lower-cost alternatives to using {@link #setData(Bundle) setData()} - if you only need to store a few integer values.*/ + /** + * arg1 and arg2 are lower-cost alternatives to using + * {@link #setData(Bundle) setData()} if you only need to store a + * few integer values. + */ public int arg2; - /** An arbitrary object to send to the recipient. This must be null when - * sending messages across processes. */ + /** + * An arbitrary object to send to the recipient. When using + * {@link Messenger} to send the message across processes this can only + * be non-null if it contains a Parcelable of a framework class (not one + * implemented by the application). For other data transfer use + * {@link #setData}. + * + * <p>Note that Parcelable objects here are not supported prior to + * the {@link android.os.Build.VERSION_CODES#FROYO} release. + */ public Object obj; - /** Optional Messenger where replies to this message can be sent. + /** + * Optional Messenger where replies to this message can be sent. The + * semantics of exactly how this is used are up to the sender and + * receiver. */ public Messenger replyTo; @@ -278,14 +294,22 @@ public final class Message implements Parcelable { * the <em>target</em> {@link Handler} that is receiving this Message to * dispatch it. If * not set, the message will be dispatched to the receiving Handler's - * {@link Handler#handleMessage(Message Handler.handleMessage())}. */ + * {@link Handler#handleMessage(Message Handler.handleMessage())}. + */ public Runnable getCallback() { return callback; } /** * Obtains a Bundle of arbitrary data associated with this - * event, lazily creating it if necessary. Set this value by calling {@link #setData(Bundle)}. + * event, lazily creating it if necessary. Set this value by calling + * {@link #setData(Bundle)}. Note that when transferring data across + * processes via {@link Messenger}, you will need to set your ClassLoader + * on the Bundle via {@link Bundle#setClassLoader(ClassLoader) + * Bundle.setClassLoader()} so that it can instantiate your objects when + * you retrieve them. + * @see #peekData() + * @see #setData(Bundle) */ public Bundle getData() { if (data == null) { @@ -297,14 +321,21 @@ public final class Message implements Parcelable { /** * Like getData(), but does not lazily create the Bundle. A null - * is returned if the Bundle does not already exist. + * is returned if the Bundle does not already exist. See + * {@link #getData} for further information on this. + * @see #getData() + * @see #setData(Bundle) */ public Bundle peekData() { return data; } - /** Sets a Bundle of arbitrary data values. Use arg1 and arg1 members - * as a lower cost way to send a few simple integer values, if you can. */ + /** + * Sets a Bundle of arbitrary data values. Use arg1 and arg1 members + * as a lower cost way to send a few simple integer values, if you can. + * @see #getData() + * @see #peekData() + */ public void setData(Bundle data) { this.data = data; } @@ -381,13 +412,25 @@ public final class Message implements Parcelable { } public void writeToParcel(Parcel dest, int flags) { - if (obj != null || callback != null) { + if (callback != null) { throw new RuntimeException( - "Can't marshal objects across processes."); + "Can't marshal callbacks across processes."); } dest.writeInt(what); dest.writeInt(arg1); dest.writeInt(arg2); + if (obj != null) { + try { + Parcelable p = (Parcelable)obj; + dest.writeInt(1); + dest.writeParcelable(p, flags); + } catch (ClassCastException e) { + throw new RuntimeException( + "Can't marshal non-Parcelable objects across processes."); + } + } else { + dest.writeInt(0); + } dest.writeLong(when); dest.writeBundle(data); Messenger.writeMessengerOrNullToParcel(replyTo, dest); @@ -397,6 +440,9 @@ public final class Message implements Parcelable { what = source.readInt(); arg1 = source.readInt(); arg2 = source.readInt(); + if (source.readInt() != 0) { + obj = source.readParcelable(getClass().getClassLoader()); + } when = source.readLong(); data = source.readBundle(); replyTo = Messenger.readMessengerOrNullFromParcel(source); diff --git a/core/java/android/os/MountServiceListener.java b/core/java/android/os/MountServiceListener.java deleted file mode 100644 index a68f464..0000000 --- a/core/java/android/os/MountServiceListener.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.os; - -/** - * Callback class for receiving progress reports during a restore operation. These - * methods will all be called on your application's main thread. - * @hide - */ -public abstract class MountServiceListener { - /** - * A sharing method has changed availability state. - * - * @param method The share method which has changed. - * @param available The share availability state. - */ - void shareAvailabilityChange(String method, boolean available) { - } - - /** - * Media has been inserted - * - * @param label The volume label. - * @param path The volume mount path. - * @param major The backing device major number. - * @param minor The backing device minor number. - */ - void mediaInserted(String label, String path, int major, int minor) { - } - - /** - * Media has been removed - * - * @param label The volume label. - * @param path The volume mount path. - * @param major The backing device major number. - * @param minor The backing device minor number. - * @param clean Indicates if the removal was clean (unmounted first). - */ - void mediaRemoved(String label, String path, int major, int minor, boolean clean) { - } - - /** - * Volume state has changed. - * - * @param label The volume label. - * @param path The volume mount path. - * @param oldState The old state of the volume. - * @param newState The new state of the volume. - * - * Note: State is one of the values returned by Environment.getExternalStorageState() - */ - void volumeStateChange(String label, String path, String oldState, String newState) { - } -} diff --git a/core/java/android/os/MountServiceResultCode.java b/core/java/android/os/MountServiceResultCode.java deleted file mode 100644 index e71dbf4..0000000 --- a/core/java/android/os/MountServiceResultCode.java +++ /dev/null @@ -1,34 +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.IOException; - -/** - * Class that provides access to constants returned from MountService APIs - * - * {@hide} - */ -public class MountServiceResultCode -{ - public static final int OperationSucceeded = 0; - public static final int OperationFailedInternalError = -1; - public static final int OperationFailedNoMedia = -2; - public static final int OperationFailedMediaBlank = -3; - public static final int OperationFailedMediaCorrupt = -4; - public static final int OperationFailedVolumeNotMounted = -5; -} diff --git a/core/java/android/os/Power.java b/core/java/android/os/Power.java index bc76180..b3df522 100644 --- a/core/java/android/os/Power.java +++ b/core/java/android/os/Power.java @@ -18,7 +18,7 @@ package android.os; import java.io.IOException; import android.os.ServiceManager; -import android.os.IMountService; +import android.os.storage.IMountService; /** * Class that provides access to some of the power management functions. diff --git a/core/java/android/os/Process.java b/core/java/android/os/Process.java index 699ddb2..4887783 100644 --- a/core/java/android/os/Process.java +++ b/core/java/android/os/Process.java @@ -504,6 +504,9 @@ public class Process { argsForZygote.add("--runtime-init"); argsForZygote.add("--setuid=" + uid); argsForZygote.add("--setgid=" + gid); + if ((debugFlags & Zygote.DEBUG_ENABLE_SAFEMODE) != 0) { + argsForZygote.add("--enable-safemode"); + } if ((debugFlags & Zygote.DEBUG_ENABLE_DEBUGGER) != 0) { argsForZygote.add("--enable-debugger"); } diff --git a/core/java/android/os/IMountService.aidl b/core/java/android/os/storage/IMountService.aidl index a5828f6..79a6cfe 100644 --- a/core/java/android/os/IMountService.aidl +++ b/core/java/android/os/storage/IMountService.aidl @@ -15,14 +15,15 @@ ** limitations under the License. */ -package android.os; +package android.os.storage; -import android.os.IMountServiceListener; +import android.os.storage.IMountServiceListener; /** WARNING! Update IMountService.h and IMountService.cpp if you change this file. * In particular, the ordering of the methods below must match the * _TRANSACTION enum in IMountService.cpp - * @hide + * @hide - Applications should use android.os.storage.StorageManager to access + * storage functions. */ interface IMountService { @@ -38,31 +39,19 @@ interface IMountService void unregisterListener(IMountServiceListener listener); /** - * Gets an Array of supported share methods + * Returns true if a USB mass storage host is connected */ - String[] getShareMethodList(); + boolean isUsbMassStorageConnected(); /** - * Returns true if the share method is available + * Enables / disables USB mass storage. */ - boolean getShareMethodAvailable(String method); + int setUsbMassStorageEnabled(boolean enable); /** - * Shares a volume via the specified method - * Returns an int consistent with MountServiceResultCode - */ - int shareVolume(String path, String method); - - /** - * Unshares a volume via the specified method - * Returns an int consistent with MountServiceResultCode - */ - int unshareVolume(String path, String method); - - /** - * Returns true if the volume is shared via the specified method. + * Returns true if a USB mass storage host is enabled (media is shared) */ - boolean getVolumeShared(String path, String method); + boolean isUsbMassStorageEnabled(); /** * Mount external storage at given mount point. @@ -74,7 +63,7 @@ interface IMountService * Safely unmount external storage at given mount point. * Returns an int consistent with MountServiceResultCode */ - int unmountVolume(String mountPoint); + int unmountVolume(String mountPoint, boolean force); /** * Format external storage given a mount point. @@ -83,6 +72,12 @@ interface IMountService int formatVolume(String mountPoint); /** + * Returns an array of pids with open files on + * the specified path. + */ + int[] getStorageUsers(String path); + + /** * Gets the state of an volume via it's mountpoint. */ String getVolumeState(String mountPoint); @@ -105,7 +100,7 @@ interface IMountService * NOTE: Ensure all references are released prior to deleting. * Returns an int consistent with MountServiceResultCode */ - int destroySecureContainer(String id); + int destroySecureContainer(String id, boolean force); /* * Mount a secure container with the specified key and owner UID. @@ -117,7 +112,12 @@ interface IMountService * Unount a secure container. * Returns an int consistent with MountServiceResultCode */ - int unmountSecureContainer(String id); + int unmountSecureContainer(String id, boolean force); + + /* + * Returns true if the specified container is mounted + */ + boolean isSecureContainerMounted(String id); /* * Rename an unmounted secure container. diff --git a/core/java/android/os/storage/IMountServiceListener.aidl b/core/java/android/os/storage/IMountServiceListener.aidl new file mode 100644 index 0000000..883413a --- /dev/null +++ b/core/java/android/os/storage/IMountServiceListener.aidl @@ -0,0 +1,43 @@ +/* + * 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.storage; + +/** + * Callback class for receiving events from MountService. + * + * @hide - Applications should use android.os.storage.IStorageEventListener + * for storage event callbacks. + */ +interface IMountServiceListener { + /** + * Detection state of USB Mass Storage has changed + * + * @param available true if a UMS host is connected. + */ + void onUsbMassStorageConnectionChanged(boolean connected); + + /** + * Storage state has changed. + * + * @param path The volume mount path. + * @param oldState The old state of the volume. + * @param newState The new state of the volume. + * + * Note: State is one of the values returned by Environment.getExternalStorageState() + */ + void onStorageStateChanged(String path, String oldState, String newState); +} diff --git a/core/java/android/os/storage/MountServiceListener.java b/core/java/android/os/storage/MountServiceListener.java new file mode 100644 index 0000000..bebb3f6 --- /dev/null +++ b/core/java/android/os/storage/MountServiceListener.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.os.storage; + +/** + * Callback class for receiving progress reports during a restore operation. These + * methods will all be called on your application's main thread. + * @hide + */ +public abstract class MountServiceListener { + /** + * USB Mass storage connection state has changed. + * + * @param connected True if UMS is connected. + */ + void onUsbMassStorageConnectionChanged(boolean connected) { + } + + /** + * Storage state has changed. + * + * @param path The volume mount path. + * @param oldState The old state of the volume. + * @param newState The new state of the volume. + * + * @Note: State is one of the values returned by Environment.getExternalStorageState() + */ + void onStorageStateChange(String path, String oldState, String newState) { + } +} diff --git a/core/java/android/os/storage/StorageEventListener.java b/core/java/android/os/storage/StorageEventListener.java new file mode 100644 index 0000000..d3d39d6 --- /dev/null +++ b/core/java/android/os/storage/StorageEventListener.java @@ -0,0 +1,38 @@ +/* + * 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.os.storage; + +/** + * Used for receiving notifications from the StorageManager + */ +public abstract class StorageEventListener { + /** + * Called when the detection state of a USB Mass Storage host has changed. + * @param connected true if the USB mass storage is connected. + */ + public void onUsbMassStorageConnectionChanged(boolean connected) { + } + + /** + * Called when storage has changed state + * @param path the filesystem path for the storage + * @param oldState the old state as returned by {@link android.os.Environment#getExternalStorageState()}. + * @param newState the old state as returned by {@link android.os.Environment#getExternalStorageState()}. + */ + public void onStorageStateChanged(String path, String oldState, String newState) { + } +} diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java new file mode 100644 index 0000000..e421ea5 --- /dev/null +++ b/core/java/android/os/storage/StorageManager.java @@ -0,0 +1,297 @@ +/* + * 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.os.storage; + +import android.content.Context; +import android.os.Binder; +import android.os.Bundle; +import android.os.Looper; +import android.os.Parcelable; +import android.os.ParcelFileDescriptor; +import android.os.Process; +import android.os.RemoteException; +import android.os.Handler; +import android.os.Message; +import android.os.ServiceManager; +import android.os.storage.IMountService; +import android.os.storage.IMountServiceListener; +import android.util.Log; +import android.util.SparseArray; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; + +/** + * StorageManager is the interface to the systems storage service. + * Get an instance of this class by calling + * {@link android.content.Context#getSystemService(java.lang.String)} with an argument + * of {@link android.content.Context#STORAGE_SERVICE}. + * + */ + +public class StorageManager +{ + private static final String TAG = "StorageManager"; + + /* + * Our internal MountService binder reference + */ + private IMountService mMountService; + + /* + * The looper target for callbacks + */ + Looper mTgtLooper; + + /* + * Target listener for binder callbacks + */ + private MountServiceBinderListener mBinderListener; + + /* + * List of our listeners + */ + private ArrayList<ListenerDelegate> mListeners = new ArrayList<ListenerDelegate>(); + + private class MountServiceBinderListener extends IMountServiceListener.Stub { + public void onUsbMassStorageConnectionChanged(boolean available) { + final int size = mListeners.size(); + for (int i = 0; i < size; i++) { + mListeners.get(i).sendShareAvailabilityChanged(available); + } + } + + public void onStorageStateChanged(String path, String oldState, String newState) { + final int size = mListeners.size(); + for (int i = 0; i < size; i++) { + mListeners.get(i).sendStorageStateChanged(path, oldState, newState); + } + } + } + + /** + * Private base class for messages sent between the callback thread + * and the target looper handler. + */ + private class StorageEvent { + public static final int EVENT_UMS_CONNECTION_CHANGED = 1; + public static final int EVENT_STORAGE_STATE_CHANGED = 2; + + private Message mMessage; + + public StorageEvent(int what) { + mMessage = Message.obtain(); + mMessage.what = what; + mMessage.obj = this; + } + + public Message getMessage() { + return mMessage; + } + } + + /** + * Message sent on a USB mass storage connection change. + */ + private class UmsConnectionChangedStorageEvent extends StorageEvent { + public boolean available; + + public UmsConnectionChangedStorageEvent(boolean a) { + super(EVENT_UMS_CONNECTION_CHANGED); + available = a; + } + } + + /** + * Message sent on volume state change. + */ + private class StorageStateChangedStorageEvent extends StorageEvent { + public String path; + public String oldState; + public String newState; + + public StorageStateChangedStorageEvent(String p, String oldS, String newS) { + super(EVENT_STORAGE_STATE_CHANGED); + path = p; + oldState = oldS; + newState = newS; + } + } + + /** + * Private class containing sender and receiver code for StorageEvents. + */ + private class ListenerDelegate { + final StorageEventListener mStorageEventListener; + private final Handler mHandler; + + ListenerDelegate(StorageEventListener listener) { + mStorageEventListener = listener; + mHandler = new Handler(mTgtLooper) { + @Override + public void handleMessage(Message msg) { + StorageEvent e = (StorageEvent) msg.obj; + + if (msg.what == StorageEvent.EVENT_UMS_CONNECTION_CHANGED) { + UmsConnectionChangedStorageEvent ev = (UmsConnectionChangedStorageEvent) e; + mStorageEventListener.onUsbMassStorageConnectionChanged(ev.available); + } else if (msg.what == StorageEvent.EVENT_STORAGE_STATE_CHANGED) { + StorageStateChangedStorageEvent ev = (StorageStateChangedStorageEvent) e; + mStorageEventListener.onStorageStateChanged(ev.path, ev.oldState, ev.newState); + } else { + Log.e(TAG, "Unsupported event " + msg.what); + } + } + }; + } + + StorageEventListener getListener() { + return mStorageEventListener; + } + + void sendShareAvailabilityChanged(boolean available) { + UmsConnectionChangedStorageEvent e = new UmsConnectionChangedStorageEvent(available); + mHandler.sendMessage(e.getMessage()); + } + + void sendStorageStateChanged(String path, String oldState, String newState) { + StorageStateChangedStorageEvent e = new StorageStateChangedStorageEvent(path, oldState, newState); + mHandler.sendMessage(e.getMessage()); + } + } + + /** + * Constructs a StorageManager object through which an application can + * can communicate with the systems mount service. + * + * @param tgtLooper The {@android.os.Looper} which events will be received on. + * + * <p>Applications can get instance of this class by calling + * {@link android.content.Context#getSystemService(java.lang.String)} with an argument + * of {@link android.content.Context#STORAGE_SERVICE}. + * + * @hide + */ + public StorageManager(Looper tgtLooper) throws RemoteException { + mMountService = IMountService.Stub.asInterface(ServiceManager.getService("mount")); + if (mMountService == null) { + Log.e(TAG, "Unable to connect to mount service! - is it running yet?"); + return; + } + mTgtLooper = tgtLooper; + mBinderListener = new MountServiceBinderListener(); + mMountService.registerListener(mBinderListener); + } + + + /** + * Registers a {@link android.os.storage.StorageEventListener StorageEventListener}. + * + * @param listener A {@link android.os.storage.StorageEventListener StorageEventListener} object. + * + */ + public void registerListener(StorageEventListener listener) { + if (listener == null) { + return; + } + + synchronized (mListeners) { + mListeners.add(new ListenerDelegate(listener)); + } + } + + /** + * Unregisters a {@link android.os.storage.StorageEventListener StorageEventListener}. + * + * @param listener A {@link android.os.storage.StorageEventListener StorageEventListener} object. + * + */ + public void unregisterListener(StorageEventListener listener) { + if (listener == null) { + return; + } + + synchronized (mListeners) { + final int size = mListeners.size(); + for (int i=0 ; i<size ; i++) { + ListenerDelegate l = mListeners.get(i); + if (l.getListener() == listener) { + mListeners.remove(i); + break; + } + } + } + } + + /** + * Enables USB Mass Storage (UMS) on the device. + * @return an integer value representing the outcome of the operation. + * @see android.os.storage.StorageResultCode + */ + public int enableUsbMassStorage() { + try { + return mMountService.setUsbMassStorageEnabled(true); + } catch (Exception ex) { + Log.e(TAG, "Failed to enable UMS", ex); + } + return StorageResultCode.OperationFailedInternalError; + } + + /** + * Disables USB Mass Storage (UMS) on the device. + * @return an integer value representing the outcome of the operation. + * @see android.os.storage.StorageResultCode + */ + public int disableUsbMassStorage() { + try { + return mMountService.setUsbMassStorageEnabled(false); + } catch (Exception ex) { + Log.e(TAG, "Failed to disable UMS", ex); + } + return StorageResultCode.OperationFailedInternalError; + } + + /** + * Query if a USB Mass Storage (UMS) host is connected. + * @return true if UMS host is connected. + */ + public boolean isUsbMassStorageConnected() { + try { + return mMountService.isUsbMassStorageConnected(); + } catch (Exception ex) { + Log.e(TAG, "Failed to get UMS connection state", ex); + } + return false; + } + + /** + * Query if a USB Mass Storage (UMS) is enabled on the device. + * @return true if UMS host is enabled. + */ + public boolean isUsbMassStorageEnabled() { + try { + return mMountService.isUsbMassStorageEnabled(); + } catch (RemoteException rex) { + Log.e(TAG, "Failed to get UMS enable state", rex); + } + return false; + } +} diff --git a/core/java/android/os/storage/StorageResultCode.java b/core/java/android/os/storage/StorageResultCode.java new file mode 100644 index 0000000..07d95df --- /dev/null +++ b/core/java/android/os/storage/StorageResultCode.java @@ -0,0 +1,73 @@ +/* + * 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.storage; + +/** + * Class that provides access to constants returned from StorageManager + * and lower level MountService APIs. + */ +public class StorageResultCode +{ + /** + * Operation succeeded. + * @see android.os.storage.StorageManager + */ + public static final int OperationSucceeded = 0; + + /** + * Operation failed: Internal error. + * @see android.os.storage.StorageManager + */ + public static final int OperationFailedInternalError = -1; + + /** + * Operation failed: Missing media. + * @see android.os.storage.StorageManager + */ + public static final int OperationFailedNoMedia = -2; + + /** + * Operation failed: Media is blank. + * @see android.os.storage.StorageManager + */ + public static final int OperationFailedMediaBlank = -3; + + /** + * Operation failed: Media is corrupt. + * @see android.os.storage.StorageManager + */ + public static final int OperationFailedMediaCorrupt = -4; + + /** + * Operation failed: Storage not mounted. + * @see android.os.storage.StorageManager + */ + public static final int OperationFailedStorageNotMounted = -5; + + /** + * Operation failed: Storage is mounted. + * @see android.os.storage.StorageManager + */ + public static final int OperationFailedStorageMounted = -6; + + /** + * Operation failed: Storage is busy. + * @see android.os.storage.StorageManager + */ + public static final int OperationFailedStorageBusy = -7; + +} diff --git a/core/java/android/pim/RecurrenceSet.java b/core/java/android/pim/RecurrenceSet.java index bd7924a..5d09fb5 100644 --- a/core/java/android/pim/RecurrenceSet.java +++ b/core/java/android/pim/RecurrenceSet.java @@ -48,7 +48,8 @@ public class RecurrenceSet { * events table in the CalendarProvider. * @param values The values retrieved from the Events table. */ - public RecurrenceSet(ContentValues values) { + public RecurrenceSet(ContentValues values) + throws EventRecurrence.InvalidFormatException { String rruleStr = values.getAsString(Calendar.Events.RRULE); String rdateStr = values.getAsString(Calendar.Events.RDATE); String exruleStr = values.getAsString(Calendar.Events.EXRULE); @@ -65,7 +66,8 @@ public class RecurrenceSet { * @param cursor The cursor containing the RRULE, RDATE, EXRULE, and EXDATE * columns. */ - public RecurrenceSet(Cursor cursor) { + public RecurrenceSet(Cursor cursor) + throws EventRecurrence.InvalidFormatException { int rruleColumn = cursor.getColumnIndex(Calendar.Events.RRULE); int rdateColumn = cursor.getColumnIndex(Calendar.Events.RDATE); int exruleColumn = cursor.getColumnIndex(Calendar.Events.EXRULE); @@ -78,12 +80,14 @@ public class RecurrenceSet { } public RecurrenceSet(String rruleStr, String rdateStr, - String exruleStr, String exdateStr) { + String exruleStr, String exdateStr) + throws EventRecurrence.InvalidFormatException { init(rruleStr, rdateStr, exruleStr, exdateStr); } private void init(String rruleStr, String rdateStr, - String exruleStr, String exdateStr) { + String exruleStr, String exdateStr) + throws EventRecurrence.InvalidFormatException { if (!TextUtils.isEmpty(rruleStr) || !TextUtils.isEmpty(rdateStr)) { if (!TextUtils.isEmpty(rruleStr)) { diff --git a/core/java/android/pim/vcard/VCardComposer.java b/core/java/android/pim/vcard/VCardComposer.java index 389c9f4..2eb25954 100644 --- a/core/java/android/pim/vcard/VCardComposer.java +++ b/core/java/android/pim/vcard/VCardComposer.java @@ -25,11 +25,13 @@ import android.database.Cursor; import android.database.sqlite.SQLiteException; import android.net.Uri; import android.os.RemoteException; +import android.pim.vcard.exception.VCardException; import android.provider.CallLog; import android.provider.CallLog.Calls; import android.provider.ContactsContract.Contacts; import android.provider.ContactsContract.Data; import android.provider.ContactsContract.RawContacts; +import android.provider.ContactsContract.RawContactsEntity; import android.provider.ContactsContract.CommonDataKinds.Email; import android.provider.ContactsContract.CommonDataKinds.Event; import android.provider.ContactsContract.CommonDataKinds.Im; @@ -54,6 +56,7 @@ import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.UnsupportedEncodingException; import java.io.Writer; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.nio.charset.UnsupportedCharsetException; import java.util.ArrayList; @@ -199,6 +202,10 @@ public class VCardComposer { try { // Create one empty entry. mWriter.write(createOneEntryInternal("-1", null)); + } catch (VCardException e) { + Log.e(LOG_TAG, "VCardException has been thrown during on Init(): " + + e.getMessage()); + return false; } catch (IOException e) { Log.e(LOG_TAG, "IOException occurred during exportOneContactData: " @@ -455,6 +462,9 @@ public class VCardComposer { return true; } } + } catch (VCardException e) { + Log.e(LOG_TAG, "VCardException has been thrown: " + e.getMessage()); + return false; } catch (OutOfMemoryError error) { // Maybe some data (e.g. photo) is too big to have in memory. But it // should be rare. @@ -486,36 +496,42 @@ public class VCardComposer { } private String createOneEntryInternal(final String contactId, - Method getEntityIteratorMethod) { + Method getEntityIteratorMethod) throws VCardException { final Map<String, List<ContentValues>> contentValuesListMap = new HashMap<String, List<ContentValues>>(); - // The resolver may return the entity iterator with no data. It is possiible. + // The resolver may return the entity iterator with no data. It is possible. // e.g. If all the data in the contact of the given contact id are not exportable ones, // they are hidden from the view of this method, though contact id itself exists. - boolean dataExists = false; EntityIterator entityIterator = null; try { - + final Uri uri = RawContactsEntity.CONTENT_URI.buildUpon() + .appendQueryParameter(Data.FOR_EXPORT_ONLY, "1") + .build(); + final String selection = Data.CONTACT_ID + "=?"; + final String[] selectionArgs = new String[] {contactId}; if (getEntityIteratorMethod != null) { + // Please note that this branch is executed by some tests only try { - final Uri uri = RawContacts.CONTENT_URI.buildUpon() - .appendQueryParameter(Data.FOR_EXPORT_ONLY, "1") - .build(); - final String selection = Data.CONTACT_ID + "=?"; - final String[] selectionArgs = new String[] {contactId}; entityIterator = (EntityIterator)getEntityIteratorMethod.invoke(null, mContentResolver, uri, selection, selectionArgs, null); - } catch (Exception e) { - e.printStackTrace(); + } catch (IllegalArgumentException e) { + Log.e(LOG_TAG, "IllegalArgumentException has been thrown: " + + e.getMessage()); + } catch (IllegalAccessException e) { + Log.e(LOG_TAG, "IllegalAccessException has been thrown: " + + e.getMessage()); + } catch (InvocationTargetException e) { + Log.e(LOG_TAG, "InvocationTargetException has been thrown: "); + StackTraceElement[] stackTraceElements = e.getCause().getStackTrace(); + for (StackTraceElement element : stackTraceElements) { + Log.e(LOG_TAG, " at " + element.toString()); + } + throw new VCardException("InvocationTargetException has been thrown: " + + e.getCause().getMessage()); } } else { - final Uri uri = RawContacts.CONTENT_URI.buildUpon() - .appendEncodedPath(contactId) - .appendEncodedPath(RawContacts.Entity.CONTENT_DIRECTORY) - .appendQueryParameter(Data.FOR_EXPORT_ONLY, "1") - .build(); entityIterator = RawContacts.newEntityIterator(mContentResolver.query( - uri, null, null, null, null)); + uri, null, selection, selectionArgs, null)); } if (entityIterator == null) { @@ -523,7 +539,11 @@ public class VCardComposer { return ""; } - dataExists = entityIterator.hasNext(); + if (!entityIterator.hasNext()) { + Log.w(LOG_TAG, "Data does not exist. contactId: " + contactId); + return ""; + } + while (entityIterator.hasNext()) { Entity entity = entityIterator.next(); for (NamedContentValues namedContentValues : entity.getSubValues()) { @@ -550,10 +570,6 @@ public class VCardComposer { } } - if (!dataExists) { - return ""; - } - final VCardBuilder builder = new VCardBuilder(mVCardType); builder.appendNameProperties(contentValuesListMap.get(StructuredName.CONTENT_ITEM_TYPE)) .appendNickNames(contentValuesListMap.get(Nickname.CONTENT_ITEM_TYPE)) diff --git a/core/java/android/pim/vcard/VCardEntry.java b/core/java/android/pim/vcard/VCardEntry.java index 20eee84..1cf3144 100644 --- a/core/java/android/pim/vcard/VCardEntry.java +++ b/core/java/android/pim/vcard/VCardEntry.java @@ -281,6 +281,29 @@ public class VCardEntry { isPrimary == organization.isPrimary); } + public String getFormattedString() { + final StringBuilder builder = new StringBuilder(); + if (!TextUtils.isEmpty(companyName)) { + builder.append(companyName); + } + + if (!TextUtils.isEmpty(departmentName)) { + if (builder.length() > 0) { + builder.append(", "); + } + builder.append(departmentName); + } + + if (!TextUtils.isEmpty(titleName)) { + if (builder.length() > 0) { + builder.append(", "); + } + builder.append(titleName); + } + + return builder.toString(); + } + @Override public String toString() { return String.format( @@ -1008,6 +1031,8 @@ public class VCardEntry { mDisplayName = mPhoneList.get(0).data; } else if (mPostalList != null && mPostalList.size() > 0) { mDisplayName = mPostalList.get(0).getFormattedAddress(mVCardType); + } else if (mOrganizationList != null && mOrganizationList.size() > 0) { + mDisplayName = mOrganizationList.get(0).getFormattedString(); } if (mDisplayName == null) { diff --git a/core/java/android/provider/Browser.java b/core/java/android/provider/Browser.java index 36255d0..f7c3148 100644 --- a/core/java/android/provider/Browser.java +++ b/core/java/android/provider/Browser.java @@ -120,6 +120,15 @@ public class Browser { private static final int MAX_HISTORY_COUNT = 250; /** + * URI for writing geolocation permissions. This requires the + * {@link android.Manifest.permission#WRITE_GEOLOCATION_PERMISSIONS}. + */ + public static final Uri GEOLOCATION_URI = + Uri.parse("content://browser/geolocation"); + + private static final String GEOLOCATION_WHERE_CLAUSE = GeolocationColumns.ORIGIN + " = ?"; + + /** * Open the AddBookmark activity to save a bookmark. Launch with * and/or url, which can be edited by the user before saving. * @param c Context used to launch the AddBookmark activity. @@ -553,6 +562,42 @@ public class Browser { } } + /** + * Allows geolocation for the specified origin. + * This requires the {@link android.Manifest.permission#WRITE_GEOLOCATION_PERMISSIONS} + * permission. + * + * @param origin The origin to allow geolocation for, e.g. "http://www.google.com". The string + * should not include a trailing slash. + */ + public static void allowGeolocation(ContentResolver cr, String origin) { + try { + ContentValues map = new ContentValues(); + map.put(GeolocationColumns.ORIGIN, origin); + cr.insert(GEOLOCATION_URI, map); + } catch (IllegalStateException e) { + Log.e(LOGTAG, "allowGeolocation", e); + return; + } + } + + /** + * Clears the geolocation permission state for the specified origin. + * This requires the {@link android.Manifest.permission#WRITE_GEOLOCATION_PERMISSIONS} + * permission. + * + * @param origin The origin to allow geolocation for, e.g. "http://www.google.com". The string + * should not include a trailing slash. + */ + public static void clearGeolocation(ContentResolver cr, String origin) { + try { + String[] whereArgs = { origin }; + cr.delete(GEOLOCATION_URI, GEOLOCATION_WHERE_CLAUSE, whereArgs); + } catch (IllegalStateException e) { + Log.e(LOGTAG, "clearGeolocation", e); + } + } + public static class BookmarkColumns implements BaseColumns { public static final String URL = "url"; public static final String VISITS = "visits"; @@ -580,4 +625,8 @@ public class Browser { public static final String SEARCH = "search"; public static final String DATE = "date"; } + + public static class GeolocationColumns { + public static final String ORIGIN = "origin"; + } } diff --git a/core/java/android/provider/Calendar.java b/core/java/android/provider/Calendar.java index f89ba91..cb42d73 100644 --- a/core/java/android/provider/Calendar.java +++ b/core/java/android/provider/Calendar.java @@ -184,6 +184,20 @@ public final class Calendar { * <P>Type: INTEGER (long)</P> */ public static final String _SYNC_DIRTY = "_sync_dirty"; + + /** + * The name of the account instance to which this row belongs, which when paired with + * {@link #ACCOUNT_TYPE} identifies a specific account. + * <P>Type: TEXT</P> + */ + public static final String ACCOUNT_NAME = "account_name"; + + /** + * The type of account to which this row belongs, which when paired with + * {@link #ACCOUNT_NAME} identifies a specific account. + * <P>Type: TEXT</P> + */ + public static final String ACCOUNT_TYPE = "account_type"; } /** @@ -579,20 +593,6 @@ public final class Calendar { public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/event_entities"); - /** - * The name of the account instance to which this row belongs, which when paired with - * {@link #ACCOUNT_TYPE} identifies a specific account. - * <P>Type: TEXT</P> - */ - public static final String ACCOUNT_NAME = "_sync_account"; - - /** - * The type of account to which this row belongs, which when paired with - * {@link #ACCOUNT_NAME} identifies a specific account. - * <P>Type: TEXT</P> - */ - public static final String ACCOUNT_TYPE = "_sync_account_type"; - public static EntityIterator newEntityIterator(Cursor cursor, ContentResolver resolver) { return new EntityIteratorImpl(cursor, resolver); } diff --git a/core/java/android/provider/Checkin.java b/core/java/android/provider/Checkin.java deleted file mode 100644 index 75936a1..0000000 --- a/core/java/android/provider/Checkin.java +++ /dev/null @@ -1,269 +0,0 @@ -/* - * Copyright (C) 2006 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.provider; - -import org.apache.commons.codec.binary.Base64; - -import android.content.ContentResolver; -import android.content.ContentValues; -import android.database.SQLException; -import android.net.Uri; -import android.os.SystemClock; -import android.util.Log; - -import java.io.ByteArrayOutputStream; -import java.io.DataOutputStream; - -/** - * Contract class for the checkin provider, used to store events and - * statistics that will be uploaded to a checkin server eventually. - * Describes the exposed database schema, and offers methods to add - * events and statistics to be uploaded. - * - * TODO: Move this to vendor/google when we have a home for it. - * - * @hide - */ -public final class Checkin { - public static final String AUTHORITY = "android.server.checkin"; - - /** - * The events table is a log of important timestamped occurrences. - * Each event has a type tag and an optional string value. - * If too many events are added before they can be reported, the - * content provider will erase older events to limit the table size. - */ - public interface Events extends BaseColumns { - public static final String TABLE_NAME = "events"; - public static final Uri CONTENT_URI = - Uri.parse("content://" + AUTHORITY + "/" + TABLE_NAME); - - public static final String TAG = "tag"; // TEXT - public static final String VALUE = "value"; // TEXT - public static final String DATE = "date"; // INTEGER - - /** Valid tag values. Extend as necessary for your needs. */ - public enum Tag { - APANIC_CONSOLE, - APANIC_THREADS, - AUTOTEST_FAILURE, - AUTOTEST_SEQUENCE_BEGIN, - AUTOTEST_SUITE_BEGIN, - AUTOTEST_TCPDUMP_BEGIN, - AUTOTEST_TCPDUMP_DATA, - AUTOTEST_TCPDUMP_END, - AUTOTEST_TEST_BEGIN, - AUTOTEST_TEST_FAILURE, - AUTOTEST_TEST_SUCCESS, - BROWSER_BUG_REPORT, - CARRIER_BUG_REPORT, - CHECKIN_FAILURE, - CHECKIN_SUCCESS, - FOTA_BEGIN, - FOTA_FAILURE, - FOTA_INSTALL, - FOTA_PROMPT, - FOTA_PROMPT_ACCEPT, - FOTA_PROMPT_REJECT, - FOTA_PROMPT_SKIPPED, - GSERVICES_ERROR, - GSERVICES_UPDATE, - LOGIN_SERVICE_ACCOUNT_TRIED, - LOGIN_SERVICE_ACCOUNT_SAVED, - LOGIN_SERVICE_AUTHENTICATE, - LOGIN_SERVICE_CAPTCHA_ANSWERED, - LOGIN_SERVICE_CAPTCHA_SHOWN, - LOGIN_SERVICE_PASSWORD_ENTERED, - LOGIN_SERVICE_SWITCH_GOOGLE_MAIL, - NETWORK_DOWN, - NETWORK_UP, - PHONE_UI, - RADIO_BUG_REPORT, - SETUP_COMPLETED, - SETUP_INITIATED, - SETUP_IO_ERROR, - SETUP_NETWORK_ERROR, - SETUP_REQUIRED_CAPTCHA, - SETUP_RETRIES_EXHAUSTED, - SETUP_SERVER_ERROR, - SETUP_SERVER_TIMEOUT, - SETUP_NO_DATA_NETWORK, - SYSTEM_BOOT, - SYSTEM_LAST_KMSG, - SYSTEM_RECOVERY_LOG, - SYSTEM_RESTART, - SYSTEM_SERVICE_LOOPING, - SYSTEM_TOMBSTONE, - TEST, - BATTERY_DISCHARGE_INFO, - MARKET_DOWNLOAD, - MARKET_INSTALL, - MARKET_REMOVE, - MARKET_REFUND, - MARKET_UNINSTALL, - } - } - - /** - * The stats table is a list of counter values indexed by a tag name. - * Each statistic has a count and sum fields, so it can track averages. - * When multiple statistics are inserted with the same tag, the count - * and sum fields are added together into a single entry in the database. - */ - public interface Stats extends BaseColumns { - public static final String TABLE_NAME = "stats"; - public static final Uri CONTENT_URI = - Uri.parse("content://" + AUTHORITY + "/" + TABLE_NAME); - - public static final String TAG = "tag"; // TEXT UNIQUE - public static final String COUNT = "count"; // INTEGER - public static final String SUM = "sum"; // REAL - - /** Valid tag values. Extend as necessary for your needs. */ - public enum Tag { - BROWSER_SNAP_CENTER, - BROWSER_TEXT_SIZE_CHANGE, - BROWSER_ZOOM_OVERVIEW, - - CRASHES_REPORTED, - CRASHES_TRUNCATED, - ELAPSED_REALTIME_SEC, - ELAPSED_UPTIME_SEC, - HTTP_REQUEST, - HTTP_REUSED, - HTTP_STATUS, - PHONE_GSM_REGISTERED, - PHONE_GPRS_ATTEMPTED, - PHONE_GPRS_CONNECTED, - PHONE_RADIO_RESETS, - TEST, - NETWORK_RX_MOBILE, - NETWORK_TX_MOBILE, - PHONE_CDMA_REGISTERED, - PHONE_CDMA_DATA_ATTEMPTED, - PHONE_CDMA_DATA_CONNECTED, - } - } - - /** - * The properties table is a set of tagged values sent with every checkin. - * Unlike statistics or events, they are not cleared after being uploaded. - * Multiple properties inserted with the same tag overwrite each other. - */ - public interface Properties extends BaseColumns { - public static final String TABLE_NAME = "properties"; - public static final Uri CONTENT_URI = - Uri.parse("content://" + AUTHORITY + "/" + TABLE_NAME); - - public static final String TAG = "tag"; // TEXT UNIQUE - public static final String VALUE = "value"; // TEXT - - /** Valid tag values, to be extended as necessary. */ - public enum Tag { - DESIRED_BUILD, - MARKET_CHECKIN, - } - } - - /** - * The crashes table is a log of crash reports, kept separate from the - * general event log because crashes are large, important, and bursty. - * Like the events table, the crashes table is pruned on insert. - */ - public interface Crashes extends BaseColumns { - public static final String TABLE_NAME = "crashes"; - public static final Uri CONTENT_URI = - Uri.parse("content://" + AUTHORITY + "/" + TABLE_NAME); - - // TODO: one or both of these should be a file attachment, not a column - public static final String DATA = "data"; // TEXT - public static final String LOGS = "logs"; // TEXT - } - - /** - * Intents with this action cause a checkin attempt. Normally triggered by - * a periodic alarm, these may be sent directly to force immediate checkin. - */ - public interface TriggerIntent { - public static final String ACTION = "android.server.checkin.CHECKIN"; - - // The category is used for GTalk service messages - public static final String CATEGORY = "android.server.checkin.CHECKIN"; - - // If true indicates that the checkin should only transfer market related data - public static final String EXTRA_MARKET_ONLY = "market_only"; - } - - private static final String TAG = "Checkin"; - - /** - * Helper function to log an event to the database. - * - * @param resolver from {@link android.content.Context#getContentResolver} - * @param tag identifying the type of event being recorded - * @param value associated with event, if any - * @return URI of the event that was added - */ - static public Uri logEvent(ContentResolver resolver, - Events.Tag tag, String value) { - try { - // Don't specify the date column; the content provider will add that. - ContentValues values = new ContentValues(); - values.put(Events.TAG, tag.toString()); - if (value != null) values.put(Events.VALUE, value); - return resolver.insert(Events.CONTENT_URI, values); - } catch (IllegalArgumentException e) { // thrown when provider is unavailable. - Log.w(TAG, "Can't log event " + tag + ": " + e); - return null; - } catch (SQLException e) { - Log.e(TAG, "Can't log event " + tag, e); // Database errors are not fatal. - return null; - } - } - - /** - * Helper function to update statistics in the database. - * Note that multiple updates to the same tag will be combined. - * - * @param tag identifying what is being observed - * @param count of occurrences - * @param sum of some value over these occurrences - * @return URI of the statistic that was returned - */ - static public Uri updateStats(ContentResolver resolver, - Stats.Tag tag, int count, double sum) { - try { - ContentValues values = new ContentValues(); - values.put(Stats.TAG, tag.toString()); - if (count != 0) values.put(Stats.COUNT, count); - if (sum != 0.0) values.put(Stats.SUM, sum); - return resolver.insert(Stats.CONTENT_URI, values); - } catch (IllegalArgumentException e) { // thrown when provider is unavailable. - Log.w(TAG, "Can't update stat " + tag + ": " + e); - return null; - } catch (SQLException e) { - Log.e(TAG, "Can't update stat " + tag, e); // Database errors are not fatal. - return null; - } - } - - /** Minimum time to wait after a crash failure before trying again. */ - static private final long MIN_CRASH_FAILURE_RETRY = 10000; // 10 seconds - - /** {@link SystemClock#elapsedRealtime} of the last time a crash report failed. */ - static private volatile long sLastCrashFailureRealtime = -MIN_CRASH_FAILURE_RETRY; -} diff --git a/core/java/android/provider/ContactsContract.java b/core/java/android/provider/ContactsContract.java index 544e399..acb8473 100644 --- a/core/java/android/provider/ContactsContract.java +++ b/core/java/android/provider/ContactsContract.java @@ -608,6 +608,45 @@ public final class ContactsContract { } /** + * URI parameter and cursor extras that return counts of rows grouped by the + * address book index, which is usually the first letter of the sort key. + * When this parameter is supplied, the row counts are returned in the + * cursor extras bundle. + * + * @hide + */ + public final static class ContactCounts { + + /** + * Add this query parameter to a URI to get back row counts grouped by + * the address book index as cursor extras. For most languages it is the + * first letter of the sort key. This parameter does not affect the main + * content of the cursor. + * + * @hide + */ + public static final String ADDRESS_BOOK_INDEX_EXTRAS = "address_book_index_extras"; + + /** + * The array of address book index titles, which are returned in the + * same order as the data in the cursor. + * <p>TYPE: String[]</p> + * + * @hide + */ + public static final String EXTRA_ADDRESS_BOOK_INDEX_TITLES = "address_book_index_titles"; + + /** + * The array of group counts for the corresponding group. Contains the same number + * of elements as the EXTRA_ADDRESS_BOOK_INDEX_TITLES array. + * <p>TYPE: int[]</p> + * + * @hide + */ + public static final String EXTRA_ADDRESS_BOOK_INDEX_COUNTS = "address_book_index_counts"; + } + + /** * Constants for the contacts table, which contains a record per aggregate * of raw contacts representing the same person. * <h3>Operations</h3> @@ -5695,5 +5734,4 @@ public final class ContactsContract { public static final String IM_ISPRIMARY = "im_isprimary"; } } - } diff --git a/core/java/android/provider/MediaStore.java b/core/java/android/provider/MediaStore.java index 211bc0a..74a03da 100644 --- a/core/java/android/provider/MediaStore.java +++ b/core/java/android/provider/MediaStore.java @@ -383,10 +383,10 @@ public final class MediaStore { if (isVideo) { bitmap = ThumbnailUtils.createVideoThumbnail(filePath); if (kind == MICRO_KIND && bitmap != null) { - bitmap = ThumbnailUtils.extractMiniThumb(bitmap, - ThumbnailUtils.MINI_THUMB_TARGET_SIZE, - ThumbnailUtils.MINI_THUMB_TARGET_SIZE, - ThumbnailUtils.RECYCLE_INPUT); + bitmap = ThumbnailUtils.extractThumbnail(bitmap, + ThumbnailUtils.TARGET_SIZE_MICRO_THUMBNAIL, + ThumbnailUtils.TARGET_SIZE_MICRO_THUMBNAIL, + ThumbnailUtils.OPTIONS_RECYCLE_INPUT); } } else { bitmap = ThumbnailUtils.createImageThumbnail(cr, filePath, uri, origId, diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index bacaf43..14e27eb 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -22,6 +22,7 @@ import org.apache.commons.codec.binary.Base64; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; +import android.content.ComponentName; import android.content.ContentQueryMap; import android.content.ContentResolver; import android.content.ContentValues; @@ -1473,6 +1474,89 @@ public final class Settings { public static final String NOTIFICATION_LIGHT_PULSE = "notification_light_pulse"; /** + * Let user pick default install location. + * @hide + */ + public static final String SET_INSTALL_LOCATION = "set_install_location"; + + /** + * Default install location value. + * 0 = auto, let system decide + * 1 = internal + * 2 = sdcard + * @hide + */ + public static final String DEFAULT_INSTALL_LOCATION = "default_install_location"; + + /** + * Show pointer location on screen? + * 0 = no + * 1 = yes + * @hide + */ + public static final String POINTER_LOCATION = "pointer_location"; + + /** + * Whether to play a sound for low-battery alerts. + * @hide + */ + public static final String POWER_SOUNDS_ENABLED = "power_sounds_enabled"; + + /** + * Whether to play a sound for dock events. + * @hide + */ + public static final String DOCK_SOUNDS_ENABLED = "dock_sounds_enabled"; + + /** + * Whether to play sounds when the keyguard is shown and dismissed. + * @hide + */ + public static final String LOCKSCREEN_SOUNDS_ENABLED = "lockscreen_sounds_enabled"; + + /** + * URI for the low battery sound file. + * @hide + */ + public static final String LOW_BATTERY_SOUND = "low_battery_sound"; + + /** + * URI for the desk dock "in" event sound. + * @hide + */ + public static final String DESK_DOCK_SOUND = "desk_dock_sound"; + + /** + * URI for the desk dock "out" event sound. + * @hide + */ + public static final String DESK_UNDOCK_SOUND = "desk_undock_sound"; + + /** + * URI for the car dock "in" event sound. + * @hide + */ + public static final String CAR_DOCK_SOUND = "car_dock_sound"; + + /** + * URI for the car dock "out" event sound. + * @hide + */ + public static final String CAR_UNDOCK_SOUND = "car_undock_sound"; + + /** + * URI for the "device locked" (keyguard shown) sound. + * @hide + */ + public static final String LOCK_SOUND = "lock_sound"; + + /** + * URI for the "device unlocked" (keyguard dismissed) sound. + * @hide + */ + public static final String UNLOCK_SOUND = "unlock_sound"; + + /** * Settings to backup. This is here so that it's in the same place as the settings * keys and easy to update. * @hide @@ -2126,14 +2210,23 @@ public final class Settings { public static final String NETWORK_PREFERENCE = "network_preference"; /** + * Used to disable Tethering on a device - defaults to true + * @hide + */ + public static final String TETHER_SUPPORTED = "tether_supported"; + + /** + * No longer supported. */ public static final String PARENTAL_CONTROL_ENABLED = "parental_control_enabled"; /** + * No longer supported. */ public static final String PARENTAL_CONTROL_LAST_UPDATE = "parental_control_last_update"; /** + * No longer supported. */ public static final String PARENTAL_CONTROL_REDIRECT_URL = "parental_control_redirect_url"; @@ -2799,22 +2892,6 @@ public final class Settings { "sms_outgoing_check_max_count"; /** - * Enable use of ssl session caching. - * 'db' - save each session in a (per process) database - * 'file' - save each session in a (per process) file - * not set or any other value - normal java in-memory caching - * @hide - */ - public static final String SSL_SESSION_CACHE = "ssl_session_cache"; - - /** - * How many bytes long a message has to be, in order to be gzipped. - * @hide - */ - public static final String SYNC_MIN_GZIP_BYTES = - "sync_min_gzip_bytes"; - - /** * The number of promoted sources in GlobalSearch. * @hide */ @@ -2956,6 +3033,14 @@ public final class Settings { * @hide */ public static final String ANR_SHOW_BACKGROUND = "anr_show_background"; + + /** + * The {@link ComponentName} string of the service to be used as the voice recognition + * service. + * + * @hide + */ + public static final String VOICE_RECOGNITION_SERVICE = "voice_recognition_service"; /** * @hide diff --git a/core/java/android/server/BluetoothA2dpService.java b/core/java/android/server/BluetoothA2dpService.java index 22bb43c..096ad39 100644 --- a/core/java/android/server/BluetoothA2dpService.java +++ b/core/java/android/server/BluetoothA2dpService.java @@ -121,10 +121,44 @@ public class BluetoothA2dpService extends IBluetoothA2dp.Stub { handleSinkStateChange(device, state, BluetoothA2dp.STATE_DISCONNECTED); } } + } else if (action.equals(AudioManager.VOLUME_CHANGED_ACTION)) { + int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1); + if (streamType == AudioManager.STREAM_MUSIC) { + BluetoothDevice sinks[] = getConnectedSinks(); + if (sinks.length != 0 && isPhoneDocked(sinks[0])) { + String address = sinks[0].getAddress(); + int newVolLevel = + intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, 0); + int oldVolLevel = + intent.getIntExtra(AudioManager.EXTRA_PREV_VOLUME_STREAM_VALUE, 0); + String path = mBluetoothService.getObjectPathFromAddress(address); + if (newVolLevel > oldVolLevel) { + avrcpVolumeUpNative(path); + } else if (newVolLevel < oldVolLevel) { + avrcpVolumeDownNative(path); + } + } + } } } }; + + private boolean isPhoneDocked(BluetoothDevice device) { + // This works only because these broadcast intents are "sticky" + Intent i = mContext.registerReceiver(null, new IntentFilter(Intent.ACTION_DOCK_EVENT)); + if (i != null) { + int state = i.getIntExtra(Intent.EXTRA_DOCK_STATE, Intent.EXTRA_DOCK_STATE_UNDOCKED); + if (state != Intent.EXTRA_DOCK_STATE_UNDOCKED) { + BluetoothDevice dockDevice = i.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); + if (dockDevice != null && device.equals(dockDevice)) { + return true; + } + } + } + return false; + } + public BluetoothA2dpService(Context context, BluetoothService bluetoothService) { mContext = context; @@ -145,6 +179,7 @@ public class BluetoothA2dpService extends IBluetoothA2dp.Stub { mIntentFilter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED); mIntentFilter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED); mIntentFilter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED); + mIntentFilter.addAction(AudioManager.VOLUME_CHANGED_ACTION); mContext.registerReceiver(mReceiver, mIntentFilter); mAudioDevices = new HashMap<BluetoothDevice, Integer>(); @@ -551,4 +586,6 @@ public class BluetoothA2dpService extends IBluetoothA2dp.Stub { private synchronized native boolean suspendSinkNative(String path); private synchronized native boolean resumeSinkNative(String path); private synchronized native Object []getSinkPropertiesNative(String path); + private synchronized native boolean avrcpVolumeUpNative(String path); + private synchronized native boolean avrcpVolumeDownNative(String path); } diff --git a/core/java/android/server/BluetoothService.java b/core/java/android/server/BluetoothService.java index dfb775f..aa20ac4 100644 --- a/core/java/android/server/BluetoothService.java +++ b/core/java/android/server/BluetoothService.java @@ -136,6 +136,14 @@ public class BluetoothService extends IBluetooth.Stub { } return false; } + + @Override + public int hashCode() { + int hash = 1; + hash = hash * 31 + (address == null ? 0 : address.hashCode()); + hash = hash * 31 + (uuid == null ? 0 : uuid.hashCode()); + return hash; + } } static { diff --git a/core/java/android/server/search/SearchManagerService.java b/core/java/android/server/search/SearchManagerService.java index 324fbaa..fbc4a81 100644 --- a/core/java/android/server/search/SearchManagerService.java +++ b/core/java/android/server/search/SearchManagerService.java @@ -75,8 +75,8 @@ public class SearchManagerService extends ISearchManager.Stub { mContext.registerReceiver(mPackageChangedReceiver, packageFilter); // Register for events related to sdcard installation. IntentFilter sdFilter = new IntentFilter(); - sdFilter.addAction(Intent.ACTION_MEDIA_RESOURCES_AVAILABLE); - sdFilter.addAction(Intent.ACTION_MEDIA_RESOURCES_UNAVAILABLE); + sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE); + sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE); mContext.registerReceiver(mPackageChangedReceiver, sdFilter); } @@ -96,8 +96,8 @@ public class SearchManagerService extends ISearchManager.Stub { if (Intent.ACTION_PACKAGE_ADDED.equals(action) || Intent.ACTION_PACKAGE_REMOVED.equals(action) || Intent.ACTION_PACKAGE_CHANGED.equals(action) || - Intent.ACTION_MEDIA_RESOURCES_AVAILABLE.equals(action) || - Intent.ACTION_MEDIA_RESOURCES_UNAVAILABLE.equals(action)) { + Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE.equals(action) || + Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE.equals(action)) { if (DBG) Log.d(TAG, "Got " + action); // Update list of searchable activities getSearchables().buildSearchableList(); diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java index eb48a0c..52de64c 100644 --- a/core/java/android/service/wallpaper/WallpaperService.java +++ b/core/java/android/service/wallpaper/WallpaperService.java @@ -28,6 +28,7 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.res.Configuration; import android.graphics.Rect; import android.os.Bundle; import android.os.IBinder; @@ -226,7 +227,7 @@ public abstract class WallpaperService extends Service { @Override public void resized(int w, int h, Rect coveredInsets, - Rect visibleInsets, boolean reportDraw) { + Rect visibleInsets, boolean reportDraw, Configuration newConfig) { Message msg = mCaller.obtainMessageI(MSG_WINDOW_RESIZED, reportDraw ? 1 : 0); mCaller.sendMessage(msg); diff --git a/core/java/android/speech/RecognitionListener.java b/core/java/android/speech/RecognitionListener.java index 5434887..2f5bcc3 100644 --- a/core/java/android/speech/RecognitionListener.java +++ b/core/java/android/speech/RecognitionListener.java @@ -79,7 +79,8 @@ public interface RecognitionListener { * time between {@link #onBeginningOfSpeech()} and {@link #onResults(Bundle)} when partial * results are ready. This method may be called zero, one or multiple times for each call to * {@link RecognitionManager#startListening(Intent)}, depending on the speech recognition - * service implementation. + * service implementation. To request partial results, use + * {@link RecognizerIntent#EXTRA_PARTIAL_RESULTS} * * @param partialResults the returned results. To retrieve the results in * ArrayList<String> format use {@link Bundle#getStringArrayList(String)} with diff --git a/core/java/android/speech/RecognitionManager.java b/core/java/android/speech/RecognitionManager.java index 0d25b2f..16b1f89 100644 --- a/core/java/android/speech/RecognitionManager.java +++ b/core/java/android/speech/RecognitionManager.java @@ -27,6 +27,8 @@ import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.os.RemoteException; +import android.provider.Settings; +import android.text.TextUtils; import android.util.Log; import java.util.LinkedList; @@ -96,6 +98,9 @@ public class RecognitionManager { /** Context with which the manager was created */ private final Context mContext; + + /** Component to direct service intent to */ + private final ComponentName mServiceComponent; /** Handler that will execute the main tasks */ private Handler mHandler = new Handler() { @@ -131,8 +136,9 @@ public class RecognitionManager { * The right way to create a {@code RecognitionManager} is by using * {@link #createRecognitionManager} static factory method */ - private RecognitionManager(final Context context) { + private RecognitionManager(final Context context, final ComponentName serviceComponent) { mContext = context; + mServiceComponent = serviceComponent; } /** @@ -169,7 +175,7 @@ public class RecognitionManager { */ public static boolean isRecognitionAvailable(final Context context) { final List<ResolveInfo> list = context.getPackageManager().queryIntentServices( - new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH), 0); + new Intent(RecognitionService.SERVICE_INTERFACE), 0); return list != null && list.size() != 0; } @@ -182,11 +188,31 @@ public class RecognitionManager { * @return a new {@code RecognitionManager} */ public static RecognitionManager createRecognitionManager(final Context context) { + return createRecognitionManager(context, null); + } + + /** + * Factory method to create a new {@code RecognitionManager}, please note that + * {@link #setRecognitionListener(RecognitionListener)} must be called before dispatching any + * command to the created {@code RecognitionManager}. + * + * Use this version of the method to specify a specific service to direct this + * {@link RecognitionManager} to. Normally you would not use this; use + * {@link #createRecognitionManager(Context)} instead to use the system default + * recognition service. + * + * @param context in which to create {@code RecognitionManager} + * @param serviceComponent the {@link ComponentName} of a specific service to direct this + * {@code RecognitionManager} to + * @return a new {@code RecognitionManager} + */ + public static RecognitionManager createRecognitionManager(final Context context, + final ComponentName serviceComponent) { if (context == null) { throw new IllegalArgumentException("Context cannot be null)"); } checkIsCalledFromMainThread(); - return new RecognitionManager(context); + return new RecognitionManager(context, serviceComponent); } /** @@ -216,11 +242,27 @@ public class RecognitionManager { throw new IllegalArgumentException("intent must not be null"); } checkIsCalledFromMainThread(); - checkIsCommandAllowed(); if (mConnection == null) { // first time connection mConnection = new Connection(); - if (!mContext.bindService(new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH), - mConnection, Context.BIND_AUTO_CREATE)) { + + Intent serviceIntent = new Intent(RecognitionService.SERVICE_INTERFACE); + + if (mServiceComponent == null) { + String serviceComponent = Settings.Secure.getString(mContext.getContentResolver(), + Settings.Secure.VOICE_RECOGNITION_SERVICE); + + if (TextUtils.isEmpty(serviceComponent)) { + Log.e(TAG, "no selected voice recognition service"); + mListener.onError(ERROR_CLIENT); + return; + } + + serviceIntent.setComponent(ComponentName.unflattenFromString(serviceComponent)); + } else { + serviceIntent.setComponent(mServiceComponent); + } + + if (!mContext.bindService(serviceIntent, mConnection, Context.BIND_AUTO_CREATE)) { Log.e(TAG, "bind to recognition service failed"); mConnection = null; mService = null; @@ -243,7 +285,6 @@ public class RecognitionManager { */ public void stopListening() { checkIsCalledFromMainThread(); - checkIsCommandAllowed(); putMessage(Message.obtain(mHandler, MSG_STOP)); } @@ -254,7 +295,6 @@ public class RecognitionManager { */ public void cancel() { checkIsCalledFromMainThread(); - checkIsCommandAllowed(); putMessage(Message.obtain(mHandler, MSG_CANCEL)); } @@ -265,12 +305,6 @@ public class RecognitionManager { } } - private void checkIsCommandAllowed() { - if (mService == null && mPendingTasks.isEmpty()) { // setListener message must be there - throw new IllegalStateException("Listener must be set before any command is called"); - } - } - private void putMessage(Message msg) { if (mService == null) { mPendingTasks.offer(msg); @@ -281,6 +315,9 @@ public class RecognitionManager { /** sends the actual message to the service */ private void handleStartListening(Intent recognizerIntent) { + if (!checkOpenConnection()) { + return; + } try { mService.startListening(recognizerIntent, mListener); if (DBG) Log.d(TAG, "service start listening command succeded"); @@ -292,6 +329,9 @@ public class RecognitionManager { /** sends the actual message to the service */ private void handleStopMessage() { + if (!checkOpenConnection()) { + return; + } try { mService.stopListening(mListener); if (DBG) Log.d(TAG, "service stop listening command succeded"); @@ -303,6 +343,9 @@ public class RecognitionManager { /** sends the actual message to the service */ private void handleCancelMessage() { + if (!checkOpenConnection()) { + return; + } try { mService.cancel(mListener); if (DBG) Log.d(TAG, "service cancel command succeded"); @@ -311,6 +354,15 @@ public class RecognitionManager { mListener.onError(ERROR_CLIENT); } } + + private boolean checkOpenConnection() { + if (mService != null) { + return true; + } + mListener.onError(ERROR_CLIENT); + Log.e(TAG, "not connected to the recognition service"); + return false; + } /** changes the listener */ private void handleChangeListener(RecognitionListener listener) { diff --git a/core/java/android/speech/RecognitionService.java b/core/java/android/speech/RecognitionService.java index 0d74960..941b70c 100644 --- a/core/java/android/speech/RecognitionService.java +++ b/core/java/android/speech/RecognitionService.java @@ -16,6 +16,8 @@ package android.speech; +import android.annotation.SdkConstant; +import android.annotation.SdkConstant.SdkConstantType; import android.app.Service; import android.content.Intent; import android.content.pm.PackageManager; @@ -28,10 +30,22 @@ import android.util.Log; /** * This class provides a base class for recognition service implementations. This class should be - * extended only in case you wish to implement a new speech recognizer. Please not that the - * implementation of this service is state-less. + * extended only in case you wish to implement a new speech recognizer. Please note that the + * implementation of this service is stateless. */ public abstract class RecognitionService extends Service { + /** + * The {@link Intent} that must be declared as handled by the service. + */ + @SdkConstant(SdkConstantType.SERVICE_ACTION) + public static final String SERVICE_INTERFACE = "android.speech.RecognitionService"; + + /** + * Name under which a RecognitionService component publishes information about itself. + * This meta-data should reference an XML resource containing a + * <code><{@link android.R.styleable#RecognitionService recognition-service}></code> tag. + */ + public static final String SERVICE_META_DATA = "android.speech"; /** Log messages identifier */ private static final String TAG = "RecognitionService"; diff --git a/core/java/android/speech/RecognizerIntent.java b/core/java/android/speech/RecognizerIntent.java index 334b049..7c15cec 100644 --- a/core/java/android/speech/RecognizerIntent.java +++ b/core/java/android/speech/RecognizerIntent.java @@ -16,9 +16,17 @@ package android.speech; +import java.util.ArrayList; + import android.app.Activity; import android.content.ActivityNotFoundException; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.os.Bundle; /** * Constants for supporting speech recognition through starting an {@link Intent} @@ -77,6 +85,7 @@ public class RecognizerIntent { * <li>{@link #EXTRA_PROMPT} * <li>{@link #EXTRA_LANGUAGE} * <li>{@link #EXTRA_MAX_RESULTS} + * <li>{@link #EXTRA_PARTIAL_RESULTS} * </ul> * * <p> Result extras (returned in the result, not to be specified in the request): @@ -166,6 +175,13 @@ public class RecognizerIntent { public static final String EXTRA_MAX_RESULTS = "android.speech.extra.MAX_RESULTS"; /** + * Optional boolean to indicate whether partial results should be returned by the recognizer + * as the user speaks (default is false). The server may ignore a request for partial + * results in some or all cases. + */ + public static final String EXTRA_PARTIAL_RESULTS = "android.speech.extra.PARTIAL_RESULTS"; + + /** * When the intent is {@link #ACTION_RECOGNIZE_SPEECH}, the speech input activity will * return results to you via the activity results mechanism. Alternatively, if you use this * extra to supply a PendingIntent, the results will be added to its bundle and the @@ -202,8 +218,90 @@ public class RecognizerIntent { public static final String EXTRA_RESULTS = "android.speech.extra.RESULTS"; /** - * Triggers the voice search settings activity. + * Returns the broadcast intent to fire with + * {@link Context#sendOrderedBroadcast(Intent, String, BroadcastReceiver, android.os.Handler, int, String, Bundle)} + * to receive details from the package that implements voice search. + * <p> + * This is based on the value specified by the voice search {@link Activity} in + * {@link #DETAILS_META_DATA}, and if this is not specified, will return null. Also if there + * is no chosen default to resolve for {@link #ACTION_WEB_SEARCH}, this will return null. + * <p> + * If an intent is returned and is fired, a {@link Bundle} of extras will be returned to the + * provided result receiver, and should ideally contain values for + * {@link #EXTRA_LANGUAGE_PREFERENCE} and {@link #EXTRA_SUPPORTED_LANGUAGES}. + * <p> + * (Whether these are actually provided is up to the particular implementation. It is + * recommended that {@link Activity}s implementing {@link #ACTION_WEB_SEARCH} provide this + * information, but it is not required.) + * + * @param context a context object + * @return the broadcast intent to fire or null if not available + */ + public static final Intent getVoiceDetailsIntent(Context context) { + Intent voiceSearchIntent = new Intent(ACTION_WEB_SEARCH); + ResolveInfo ri = context.getPackageManager().resolveActivity( + voiceSearchIntent, PackageManager.GET_META_DATA); + if (ri == null || ri.activityInfo == null || ri.activityInfo.metaData == null) return null; + + String className = ri.activityInfo.metaData.getString(DETAILS_META_DATA); + if (className == null) return null; + + Intent detailsIntent = new Intent(ACTION_GET_LANGUAGE_DETAILS); + detailsIntent.setComponent(new ComponentName(ri.activityInfo.packageName, className)); + return detailsIntent; + } + + /** + * Meta-data name under which an {@link Activity} implementing {@link #ACTION_WEB_SEARCH} can + * use to expose the class name of a {@link BroadcastReceiver} which can respond to request for + * more information, from any of the broadcast intents specified in this class. + * <p> + * Broadcast intents can be directed to the class name specified in the meta-data by creating + * an {@link Intent}, setting the component with + * {@link Intent#setComponent(android.content.ComponentName)}, and using + * {@link Context#sendOrderedBroadcast(Intent, String, BroadcastReceiver, android.os.Handler, int, String, android.os.Bundle)} + * with another {@link BroadcastReceiver} which can receive the results. + * <p> + * The {@link #getVoiceDetailsIntent(Context)} method is provided as a convenience to create + * a broadcast intent based on the value of this meta-data, if available. + * <p> + * This is optional and not all {@link Activity}s which implement {@link #ACTION_WEB_SEARCH} + * are required to implement this. Thus retrieving this meta-data may be null. + */ + public static final String DETAILS_META_DATA = "android.speech.DETAILS"; + + /** + * A broadcast intent which can be fired to the {@link BroadcastReceiver} component specified + * in the meta-data defined in the {@link #DETAILS_META_DATA} meta-data of an + * {@link Activity} satisfying {@link #ACTION_WEB_SEARCH}. + * <p> + * When fired with + * {@link Context#sendOrderedBroadcast(Intent, String, BroadcastReceiver, android.os.Handler, int, String, android.os.Bundle)}, + * a {@link Bundle} of extras will be returned to the provided result receiver, and should + * ideally contain values for {@link #EXTRA_LANGUAGE_PREFERENCE} and + * {@link #EXTRA_SUPPORTED_LANGUAGES}. + * <p> + * (Whether these are actually provided is up to the particular implementation. It is + * recommended that {@link Activity}s implementing {@link #ACTION_WEB_SEARCH} provide this + * information, but it is not required.) + */ + public static final String ACTION_GET_LANGUAGE_DETAILS = + "android.speech.action.GET_LANGUAGE_DETAILS"; + + /** + * The key to the extra in the {@link Bundle} returned by {@link #ACTION_GET_LANGUAGE_DETAILS} + * which is a {@link String} that represents the current language preference this user has + * specified - a locale string like "en-US". + */ + public static final String EXTRA_LANGUAGE_PREFERENCE = + "android.speech.extra.LANGUAGE_PREFERENCE"; + + /** + * The key to the extra in the {@link Bundle} returned by {@link #ACTION_GET_LANGUAGE_DETAILS} + * which is an {@link ArrayList} of {@link String}s that represents the languages supported by + * this implementation of voice recognition - a list of strings like "en-US", "cmn-Hans-CN", + * etc. */ - public static final String ACTION_VOICE_SEARCH_SETTINGS = - "android.speech.action.VOICE_SEARCH_SETTINGS"; + public static final String EXTRA_SUPPORTED_LANGUAGES = + "android.speech.extra.SUPPORTED_LANGUAGES"; } diff --git a/core/java/android/speech/tts/TextToSpeech.java b/core/java/android/speech/tts/TextToSpeech.java index bbbeb3f..0db2198 100755 --- a/core/java/android/speech/tts/TextToSpeech.java +++ b/core/java/android/speech/tts/TextToSpeech.java @@ -256,6 +256,31 @@ public class TextToSpeech { * the TextToSpeech engine specifies the locale associated with each resource file. */ public static final String EXTRA_VOICE_DATA_FILES_INFO = "dataFilesInfo"; + /** + * Extra information received with the {@link #ACTION_CHECK_TTS_DATA} intent where + * the TextToSpeech engine returns an ArrayList<String> of all the available voices. + * The format of each voice is: lang-COUNTRY-variant where COUNTRY and variant are + * optional (ie, "eng" or "eng-USA" or "eng-USA-FEMALE"). + * {@hide} + */ + public static final String EXTRA_AVAILABLE_VOICES = "availableVoices"; + /** + * Extra information received with the {@link #ACTION_CHECK_TTS_DATA} intent where + * the TextToSpeech engine returns an ArrayList<String> of all the unavailable voices. + * The format of each voice is: lang-COUNTRY-variant where COUNTRY and variant are + * optional (ie, "eng" or "eng-USA" or "eng-USA-FEMALE"). + * {@hide} + */ + public static final String EXTRA_UNAVAILABLE_VOICES = "unavailableVoices"; + /** + * Extra information sent with the {@link #ACTION_CHECK_TTS_DATA} intent where the + * caller indicates to the TextToSpeech engine which specific sets of voice data to + * check for by sending an ArrayList<String> of the voices that are of interest. + * The format of each voice is: lang-COUNTRY-variant where COUNTRY and variant are + * optional (ie, "eng" or "eng-USA" or "eng-USA-FEMALE"). + * {@hide} + */ + public static final String EXTRA_CHECK_VOICE_DATA_FOR = "checkVoiceDataFor"; // extras for a TTS engine's data installation /** diff --git a/core/java/android/storage/StorageEventListener.java b/core/java/android/storage/StorageEventListener.java deleted file mode 100644 index cd71090..0000000 --- a/core/java/android/storage/StorageEventListener.java +++ /dev/null @@ -1,57 +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.storage; - -/** - * Used for receiving notifications from the StorageManager - */ -public interface StorageEventListener { - /** - * Called when the ability to share a volume has changed. - * @param method the share-method which has changed. - * @param available true if the share is available. - */ - public void onShareAvailabilityChanged(String method, boolean available); - - /** - * Called when media has been inserted - * @param label the system defined label for the volume. - * @param path the filesystem path for the volume. - * @param major the major number of the device. - * @param minor the minor number of the device. - */ - public void onMediaInserted(String label, String path, int major, int minor); - - /** - * Called when media has been removed - * @param label the system defined label for the volume. - * @param path the filesystem path for the volume. - * @param major the major number of the device. - * @param minor the minor number of the device. - * @param clean the media was removed cleanly. - */ - public void onMediaRemoved(String label, String path, int major, int minor, boolean clean); - - /** - * Called when a volume has changed state - * @param label the system defined label for the volume. - * @param path the filesystem path for the volume. - * @param oldState the old state as returned by {@link android.os.Environment#getExternalStorageState()}. - * @param newState the old state as returned by {@link android.os.Environment#getExternalStorageState()}. - */ - public void onVolumeStateChanged(String label, String path, String oldState, String newState); -} diff --git a/core/java/android/storage/StorageManager.java b/core/java/android/storage/StorageManager.java deleted file mode 100644 index dd8bd63..0000000 --- a/core/java/android/storage/StorageManager.java +++ /dev/null @@ -1,300 +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.storage; - -import android.content.Context; -import android.os.Binder; -import android.os.Bundle; -import android.os.Looper; -import android.os.Parcelable; -import android.os.ParcelFileDescriptor; -import android.os.Process; -import android.os.RemoteException; -import android.os.Handler; -import android.os.Message; -import android.os.ServiceManager; -import android.os.IMountService; -import android.os.IMountServiceListener; -import android.util.Log; -import android.util.SparseArray; - -import java.io.FileDescriptor; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; - -/** - * Class that lets you access the device's storage management functions. Get an instance of this - * class by calling {@link android.content.Context#getSystemService(java.lang.String) - * Context.getSystemService()} with an argument of {@link android.content.Context#STORAGE_SERVICE}. - */ -public class StorageManager -{ - private static final String TAG = "StorageManager"; - - /* - * Our internal MountService binder reference - */ - private IMountService mMountService; - - /* - * The looper target for callbacks - */ - Looper mTgtLooper; - - /* - * Target listener for binder callbacks - */ - private MountServiceBinderListener mBinderListener; - - /* - * *static* list of our listeners - */ - static final ArrayList<ListenerDelegate> sListeners = new ArrayList<ListenerDelegate>(); - - private class MountServiceBinderListener extends IMountServiceListener.Stub { - public void onShareAvailabilityChanged(String method, boolean available) { - final int size = sListeners.size(); - for (int i = 0; i < size; i++) { - sListeners.get(i).sendShareAvailabilityChanged(method, available); - } - } - - public void onMediaInserted(String label, String path, int major, int minor) { - final int size = sListeners.size(); - for (int i = 0; i < size; i++) { - sListeners.get(i).sendMediaInserted(label, path, major, minor); - } - } - - public void onMediaRemoved(String label, String path, int major, int minor, boolean clean) { - final int size = sListeners.size(); - for (int i = 0; i < size; i++) { - sListeners.get(i).sendMediaRemoved(label, path, major, minor, clean); - } - } - - public void onVolumeStateChanged(String label, String path, String oldState, String newState) { - final int size = sListeners.size(); - for (int i = 0; i < size; i++) { - sListeners.get(i).sendVolumeStateChanged(label, path, oldState, newState); - } - } - } - - /** - * Private base class for messages sent between the callback thread - * and the target looper handler - */ - private class StorageEvent { - public static final int EVENT_SHARE_AVAILABILITY_CHANGED = 1; - public static final int EVENT_MEDIA_INSERTED = 2; - public static final int EVENT_MEDIA_REMOVED = 3; - public static final int EVENT_VOLUME_STATE_CHANGED = 4; - - private Message mMessage; - - public StorageEvent(int what) { - mMessage = Message.obtain(); - mMessage.what = what; - mMessage.obj = this; - } - - public Message getMessage() { - return mMessage; - } - } - - /** - * Message sent on a share availability change. - */ - private class ShareAvailabilityChangedStorageEvent extends StorageEvent { - public String method; - public boolean available; - - public ShareAvailabilityChangedStorageEvent(String m, boolean a) { - super(EVENT_SHARE_AVAILABILITY_CHANGED); - method = m; - available = a; - } - } - - /** - * Message sent on media insertion - */ - private class MediaInsertedStorageEvent extends StorageEvent { - public String label; - public String path; - public int major; - public int minor; - - public MediaInsertedStorageEvent(String l, String p, int maj, int min) { - super(EVENT_MEDIA_INSERTED); - label = l; - path = p; - major = maj; - minor = min; - } - } - - /** - * Message sent on media removal - */ - private class MediaRemovedStorageEvent extends StorageEvent { - public String label; - public String path; - public int major; - public int minor; - public boolean clean; - - public MediaRemovedStorageEvent(String l, String p, int maj, int min, boolean c) { - super(EVENT_MEDIA_REMOVED); - label = l; - path = p; - major = maj; - minor = min; - clean = c; - } - } - - /** - * Message sent on volume state change - */ - private class VolumeStateChangedStorageEvent extends StorageEvent { - public String label; - public String path; - public String oldState; - public String newState; - - public VolumeStateChangedStorageEvent(String l, String p, String oldS, String newS) { - super(EVENT_VOLUME_STATE_CHANGED); - label = l; - path = p; - oldState = oldS; - newState = newS; - } - } - - /** - * Private class containing sender and receiver code for StorageEvents - */ - private class ListenerDelegate { - final StorageEventListener mStorageEventListener; - private final Handler mHandler; - - ListenerDelegate(StorageEventListener listener) { - mStorageEventListener = listener; - mHandler = new Handler(mTgtLooper) { - @Override - public void handleMessage(Message msg) { - StorageEvent e = (StorageEvent) msg.obj; - - if (msg.what == StorageEvent.EVENT_SHARE_AVAILABILITY_CHANGED) { - ShareAvailabilityChangedStorageEvent ev = (ShareAvailabilityChangedStorageEvent) e; - mStorageEventListener.onShareAvailabilityChanged(ev.method, ev.available); - } else if (msg.what == StorageEvent.EVENT_MEDIA_INSERTED) { - MediaInsertedStorageEvent ev = (MediaInsertedStorageEvent) e; - mStorageEventListener.onMediaInserted(ev.label, ev.path, ev.major, ev.minor); - } else if (msg.what == StorageEvent.EVENT_MEDIA_REMOVED) { - MediaRemovedStorageEvent ev = (MediaRemovedStorageEvent) e; - mStorageEventListener.onMediaRemoved(ev.label, ev.path, ev.major, ev.minor, ev.clean); - } else if (msg.what == StorageEvent.EVENT_VOLUME_STATE_CHANGED) { - VolumeStateChangedStorageEvent ev = (VolumeStateChangedStorageEvent) e; - mStorageEventListener.onVolumeStateChanged(ev.label, ev.path, ev.oldState, ev.newState); - } else { - Log.e(TAG, "Unsupported event " + msg.what); - } - } - }; - } - - StorageEventListener getListener() { - return mStorageEventListener; - } - - void sendShareAvailabilityChanged(String method, boolean available) { - ShareAvailabilityChangedStorageEvent e = new ShareAvailabilityChangedStorageEvent(method, available); - mHandler.sendMessage(e.getMessage()); - } - - void sendMediaInserted(String label, String path, int major, int minor) { - MediaInsertedStorageEvent e = new MediaInsertedStorageEvent(label, path, major, minor); - mHandler.sendMessage(e.getMessage()); - } - - void sendMediaRemoved(String label, String path, int major, int minor, boolean clean) { - MediaRemovedStorageEvent e = new MediaRemovedStorageEvent(label, path, major, minor, clean); - mHandler.sendMessage(e.getMessage()); - } - - void sendVolumeStateChanged(String label, String path, String oldState, String newState) { - VolumeStateChangedStorageEvent e = new VolumeStateChangedStorageEvent(label, path, oldState, newState); - mHandler.sendMessage(e.getMessage()); - } - } - - /** - * {@hide} - */ - public StorageManager(Looper tgtLooper) throws RemoteException { - mMountService = IMountService.Stub.asInterface(ServiceManager.getService("mount")); - mTgtLooper = tgtLooper; - mBinderListener = new MountServiceBinderListener(); - mMountService.registerListener(mBinderListener); - } - - - /** - * Registers a {@link android.storage.StorageEventListener StorageEventListener}. - * - * @param listener A {@link android.storage.StorageEventListener StorageEventListener} object. - * - */ - public void registerListener(StorageEventListener listener) { - if (listener == null) { - return; - } - - synchronized (sListeners) { - sListeners.add(new ListenerDelegate(listener)); - } - } - - /** - * Unregisters a {@link android.storage.StorageEventListener StorageEventListener}. - * - * @param listener A {@link android.storage.StorageEventListener StorageEventListener} object. - * - */ - public void unregisterListener(StorageEventListener listener) { - if (listener == null) { - return; - } - synchronized (sListeners) { - final int size = sListeners.size(); - for (int i=0 ; i<size ; i++) { - ListenerDelegate l = sListeners.get(i); - if (l.getListener() == listener) { - sListeners.remove(i); - break; - } - } - } - } -} diff --git a/core/java/android/text/AndroidCharacter.java b/core/java/android/text/AndroidCharacter.java index 6dfd64d..05887c5 100644 --- a/core/java/android/text/AndroidCharacter.java +++ b/core/java/android/text/AndroidCharacter.java @@ -22,6 +22,13 @@ package android.text; */ public class AndroidCharacter { + public static final int EAST_ASIAN_WIDTH_NEUTRAL = 0; + public static final int EAST_ASIAN_WIDTH_AMBIGUOUS = 1; + public static final int EAST_ASIAN_WIDTH_HALF_WIDTH = 2; + public static final int EAST_ASIAN_WIDTH_FULL_WIDTH = 3; + public static final int EAST_ASIAN_WIDTH_NARROW = 4; + public static final int EAST_ASIAN_WIDTH_WIDE = 5; + /** * Fill in the first <code>count</code> bytes of <code>dest</code> with the * directionalities from the first <code>count</code> chars of <code>src</code>. @@ -30,10 +37,47 @@ public class AndroidCharacter */ public native static void getDirectionalities(char[] src, byte[] dest, int count); + + /** + * Calculate the East Asian Width of a character according to + * <a href="http://unicode.org/reports/tr11/">Unicode TR#11</a>. The return + * will be one of {@link #EAST_ASIAN_WIDTH_NEUTRAL}, + * {@link #EAST_ASIAN_WIDTH_AMBIGUOUS}, {@link #EAST_ASIAN_WIDTH_HALF_WIDTH}, + * {@link #EAST_ASIAN_WIDTH_FULL_WIDTH}, {@link #EAST_ASIAN_WIDTH_NARROW}, + * or {@link #EAST_ASIAN_WIDTH_WIDE}. + * + * @param input the character to measure + * @return the East Asian Width for input + */ + public native static int getEastAsianWidth(char input); + + /** + * Fill the first <code>count</code> bytes of <code>dest</code> with the + * East Asian Width from the first <code>count</code> chars of + * <code>src</code>. East Asian Width is calculated based on + * <a href="http://unicode.org/reports/tr11/">Unicode TR#11</a>. Each entry + * in <code>dest> will be one of {@link #EAST_ASIAN_WIDTH_NEUTRAL}, + * {@link #EAST_ASIAN_WIDTH_AMBIGUOUS}, {@link #EAST_ASIAN_WIDTH_HALF_WIDTH}, + * {@link #EAST_ASIAN_WIDTH_FULL_WIDTH}, {@link #EAST_ASIAN_WIDTH_NARROW}, + * or {@link #EAST_ASIAN_WIDTH_WIDE}. + * + * @param src character array of input to measure + * @param start first character in array to measure + * @param count maximum number of characters to measure + * @param dest byte array of results for each character in src + */ + public native static void getEastAsianWidths(char[] src, int start, + int count, byte[] dest); + /** * Replace the specified slice of <code>text</code> with the chars' * right-to-left mirrors (if any), returning true if any * replacements were made. + * + * @param text array of characters to apply mirror operation + * @param start first character in array to mirror + * @param count maximum number of characters to mirror + * @return true if replacements were made */ public native static boolean mirror(char[] text, int start, int count); diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java index afc6864..1023036 100644 --- a/core/java/android/text/Layout.java +++ b/core/java/android/text/Layout.java @@ -39,6 +39,8 @@ import android.view.KeyEvent; */ public abstract class Layout { private static final boolean DEBUG = false; + private static final ParagraphStyle[] NO_PARA_SPANS = + ArrayUtils.emptyArray(ParagraphStyle.class); /* package */ static final EmojiFactory EMOJI_FACTORY = EmojiFactory.newAvailableInstance(); @@ -57,7 +59,7 @@ public abstract class Layout { private RectF mEmojiRect; /** - * Return how wide a layout would be necessary to display the + * Return how wide a layout must be in order to display the * specified text with one line per paragraph. */ public static float getDesiredWidth(CharSequence source, @@ -66,7 +68,7 @@ public abstract class Layout { } /** - * Return how wide a layout would be necessary to display the + * Return how wide a layout must be in order to display the * specified text slice with one line per paragraph. */ public static float getDesiredWidth(CharSequence source, @@ -82,6 +84,7 @@ public abstract class Layout { if (next < 0) next = end; + // note, omits trailing paragraph char float w = measureText(paint, workPaint, source, i, next, null, true, null); @@ -97,10 +100,19 @@ public abstract class Layout { /** * Subclasses of Layout use this constructor to set the display text, * width, and other standard properties. + * @param text the text to render + * @param paint the default paint for the layout. Styles can override + * various attributes of the paint. + * @param width the wrapping width for the text. + * @param align whether to left, right, or center the text. Styles can + * override the alignment. + * @param spacingMult factor by which to scale the font size to get the + * default line spacing + * @param spacingAdd amount to add to the default line spacing */ protected Layout(CharSequence text, TextPaint paint, int width, Alignment align, - float spacingmult, float spacingadd) { + float spacingMult, float spacingAdd) { if (width < 0) throw new IllegalArgumentException("Layout: " + width + " < 0"); @@ -109,8 +121,8 @@ public abstract class Layout { mWorkPaint = new TextPaint(); mWidth = width; mAlignment = align; - mSpacingMult = spacingmult; - mSpacingAdd = spacingadd; + mSpacingMult = spacingMult; + mSpacingAdd = spacingAdd; mSpannedText = text instanceof Spanned; } @@ -141,10 +153,16 @@ public abstract class Layout { } /** - * Draw the specified rectangle from this Layout on the specified Canvas, - * with the specified path drawn between the background and the text. + * Draw this Layout on the specified canvas, with the highlight path drawn + * between the background and the text. + * + * @param c the canvas + * @param highlight the path of the highlight or cursor; can be null + * @param highlightPaint the paint for the highlight + * @param cursorOffsetVertical the amount to temporarily translate the + * canvas while rendering the highlight */ - public void draw(Canvas c, Path highlight, Paint highlightpaint, + public void draw(Canvas c, Path highlight, Paint highlightPaint, int cursorOffsetVertical) { int dtop, dbottom; @@ -157,13 +175,10 @@ public abstract class Layout { dbottom = sTempRect.bottom; } - TextPaint paint = mPaint; int top = 0; - // getLineBottom(getLineCount() -1) just calls getLineTop(getLineCount) int bottom = getLineTop(getLineCount()); - if (dtop > top) { top = dtop; } @@ -177,16 +192,19 @@ public abstract class Layout { int previousLineBottom = getLineTop(first); int previousLineEnd = getLineStart(first); + TextPaint paint = mPaint; CharSequence buf = mText; + int width = mWidth; + boolean spannedText = mSpannedText; - ParagraphStyle[] nospans = ArrayUtils.emptyArray(ParagraphStyle.class); - ParagraphStyle[] spans = nospans; + ParagraphStyle[] spans = NO_PARA_SPANS; int spanend = 0; int textLength = 0; - boolean spannedText = mSpannedText; + // First, draw LineBackgroundSpans. + // LineBackgroundSpans know nothing about the alignment or direction of + // the layout or line. XXX: Should they? if (spannedText) { - spanend = 0; textLength = buf.length(); for (int i = first; i <= last; i++) { int start = previousLineEnd; @@ -209,7 +227,7 @@ public abstract class Layout { for (int n = 0; n < spans.length; n++) { LineBackgroundSpan back = (LineBackgroundSpan) spans[n]; - back.drawBackground(c, paint, 0, mWidth, + back.drawBackground(c, paint, 0, width, ltop, lbaseline, lbottom, buf, start, end, i); @@ -219,7 +237,7 @@ public abstract class Layout { spanend = 0; previousLineBottom = getLineTop(first); previousLineEnd = getLineStart(first); - spans = nospans; + spans = NO_PARA_SPANS; } // There can be a highlight even without spans if we are drawing @@ -229,7 +247,7 @@ public abstract class Layout { c.translate(0, cursorOffsetVertical); } - c.drawPath(highlight, highlightpaint); + c.drawPath(highlight, highlightPaint); if (cursorOffsetVertical != 0) { c.translate(0, -cursorOffsetVertical); @@ -238,6 +256,9 @@ public abstract class Layout { Alignment align = mAlignment; + // Next draw the lines, one at a time. + // the baseline is the top of the following line minus the current + // line's descent. for (int i = first; i <= last; i++) { int start = previousLineEnd; @@ -249,21 +270,20 @@ public abstract class Layout { previousLineBottom = lbottom; int lbaseline = lbottom - getLineDescent(i); - boolean par = false; + boolean isFirstParaLine = false; if (spannedText) { if (start == 0 || buf.charAt(start - 1) == '\n') { - par = true; + isFirstParaLine = true; } + // New batch of paragraph styles, compute the alignment. + // Last alignment style wins. if (start >= spanend) { - Spanned sp = (Spanned) buf; - spanend = sp.nextSpanTransition(start, textLength, ParagraphStyle.class); spans = sp.getSpans(start, spanend, ParagraphStyle.class); align = mAlignment; - for (int n = spans.length-1; n >= 0; n--) { if (spans[n] instanceof AlignmentSpan) { align = ((AlignmentSpan) spans[n]).getAlignment(); @@ -277,6 +297,8 @@ public abstract class Layout { int left = 0; int right = mWidth; + // Draw all leading margin spans. Adjust left or right according + // to the paragraph direction of the line. if (spannedText) { final int length = spans.length; for (int n = 0; n < length; n++) { @@ -286,15 +308,15 @@ public abstract class Layout { if (dir == DIR_RIGHT_TO_LEFT) { margin.drawLeadingMargin(c, paint, right, dir, ltop, lbaseline, lbottom, buf, - start, end, par, this); + start, end, isFirstParaLine, this); - right -= margin.getLeadingMargin(par); + right -= margin.getLeadingMargin(isFirstParaLine); } else { margin.drawLeadingMargin(c, paint, left, dir, ltop, lbaseline, lbottom, buf, - start, end, par, this); + start, end, isFirstParaLine, this); - boolean useMargin = par; + boolean useMargin = isFirstParaLine; if (margin instanceof LeadingMarginSpan.LeadingMarginSpan2) { int count = ((LeadingMarginSpan.LeadingMarginSpan2)margin).getLeadingMarginLineCount(); useMargin = count > i; @@ -305,6 +327,8 @@ public abstract class Layout { } } + // Adjust the point at which to start rendering depending on the + // alignment of the paragraph. int x; if (align == Alignment.ALIGN_NORMAL) { if (dir == DIR_LEFT_TO_RIGHT) { @@ -340,6 +364,7 @@ public abstract class Layout { Assert.assertTrue(dir == DIR_LEFT_TO_RIGHT); Assert.assertNotNull(c); } + // XXX: assumes there's nothing additional to be done c.drawText(buf, start, end, x, lbaseline, paint); } else { drawText(c, buf, start, end, dir, directions, @@ -382,7 +407,7 @@ public abstract class Layout { /** * Increase the width of this layout to the specified width. - * Be careful to use this only when you know it is appropriate -- + * Be careful to use this only when you know it is appropriate— * it does not cause the text to reflow to use the full new width. */ public final void increaseWidthTo(int wid) { @@ -397,7 +422,7 @@ public abstract class Layout { * Return the total height of this layout. */ public int getHeight() { - return getLineTop(getLineCount()); // same as getLineBottom(getLineCount() - 1); + return getLineTop(getLineCount()); } /** @@ -439,33 +464,35 @@ public abstract class Layout { bounds.left = 0; // ??? bounds.top = getLineTop(line); bounds.right = mWidth; // ??? - bounds.bottom = getLineBottom(line); + bounds.bottom = getLineTop(line + 1); } return getLineBaseline(line); } /** - * Return the vertical position of the top of the specified line. - * If the specified line is one beyond the last line, returns the + * Return the vertical position of the top of the specified line + * (0…getLineCount()). + * If the specified line is equal to the line count, returns the * bottom of the last line. */ public abstract int getLineTop(int line); /** - * Return the descent of the specified line. + * Return the descent of the specified line(0…getLineCount() - 1). */ public abstract int getLineDescent(int line); /** - * Return the text offset of the beginning of the specified line. - * If the specified line is one beyond the last line, returns the - * end of the last line. + * Return the text offset of the beginning of the specified line ( + * 0…getLineCount()). If the specified line is equal to the line + * count, returns the length of the text. */ public abstract int getLineStart(int line); /** - * Returns the primary directionality of the paragraph containing - * the specified line. + * Returns the primary directionality of the paragraph containing the + * specified line, either 1 for left-to-right lines, or -1 for right-to-left + * lines (see {@link #DIR_LEFT_TO_RIGHT}, {@link #DIR_RIGHT_TO_LEFT}). */ public abstract int getParagraphDirection(int line); @@ -477,9 +504,11 @@ public abstract class Layout { public abstract boolean getLineContainsTab(int line); /** - * Returns an array of directionalities for the specified line. + * Returns the directional run information for the specified line. * The array alternates counts of characters in left-to-right * and right-to-left segments of the line. + * + * <p>NOTE: this is inadequate to support bidirectional text, and will change. */ public abstract Directions getLineDirections(int line); @@ -1565,6 +1594,21 @@ public abstract class Layout { return h; } + /** + * Measure width of a run of text on a single line that is known to all be + * in the same direction as the paragraph base direction. Returns the width, + * and the line metrics in fm if fm is not null. + * + * @param paint the paint for the text; will not be modified + * @param workPaint paint available for modification + * @param text text + * @param start start of the line + * @param end limit of the line + * @param fm object to return integer metrics in, can be null + * @param hasTabs true if it is known that the line has tabs + * @param tabs tab position information + * @return the width of the text from start to end + */ /* package */ static float measureText(TextPaint paint, TextPaint workPaint, CharSequence text, @@ -1580,37 +1624,36 @@ public abstract class Layout { int len = end - start; - int here = 0; - float h = 0; - int ab = 0, be = 0; - int top = 0, bot = 0; + int lastPos = 0; + float width = 0; + int ascent = 0, descent = 0, top = 0, bottom = 0; if (fm != null) { fm.ascent = 0; fm.descent = 0; } - for (int i = hasTabs ? 0 : len; i <= len; i++) { + for (int pos = hasTabs ? 0 : len; pos <= len; pos++) { int codept = 0; Bitmap bm = null; - if (hasTabs && i < len) { - codept = buf[i]; + if (hasTabs && pos < len) { + codept = buf[pos]; } - if (codept >= 0xD800 && codept <= 0xDFFF && i < len) { - codept = Character.codePointAt(buf, i); + if (codept >= 0xD800 && codept <= 0xDFFF && pos < len) { + codept = Character.codePointAt(buf, pos); if (codept >= MIN_EMOJI && codept <= MAX_EMOJI) { bm = EMOJI_FACTORY.getBitmapFromAndroidPua(codept); } } - if (i == len || codept == '\t' || bm != null) { + if (pos == len || codept == '\t' || bm != null) { workPaint.baselineShift = 0; - h += Styled.measureText(paint, workPaint, text, - start + here, start + i, + width += Styled.measureText(paint, workPaint, text, + start + lastPos, start + pos, fm); if (fm != null) { @@ -1623,60 +1666,80 @@ public abstract class Layout { } } - if (i != len) { + if (pos != len) { if (bm == null) { - h = nextTab(text, start, end, h, tabs); + // no emoji, must have hit a tab + width = nextTab(text, start, end, width, tabs); } else { + // This sets up workPaint with the font on the emoji + // text, so that we can extract the ascent and scale. + + // We can't use the result of the previous call to + // measureText because the emoji might have its own style. + // We have to initialize workPaint here because if the + // text is unstyled measureText might not use workPaint + // at all. workPaint.set(paint); Styled.measureText(paint, workPaint, text, - start + i, start + i + 1, null); + start + pos, start + pos + 1, null); - float wid = (float) bm.getWidth() * + width += (float) bm.getWidth() * -workPaint.ascent() / bm.getHeight(); - h += wid; - i++; + // Since we had an emoji, we bump past the second half + // of the surrogate pair. + pos++; } } if (fm != null) { - if (fm.ascent < ab) { - ab = fm.ascent; + if (fm.ascent < ascent) { + ascent = fm.ascent; } - if (fm.descent > be) { - be = fm.descent; + if (fm.descent > descent) { + descent = fm.descent; } if (fm.top < top) { top = fm.top; } - if (fm.bottom > bot) { - bot = fm.bottom; + if (fm.bottom > bottom) { + bottom = fm.bottom; } - /* - * No need to take bitmap height into account here, - * since it is scaled to match the text height. - */ + // No need to take bitmap height into account here, + // since it is scaled to match the text height. } - here = i + 1; + lastPos = pos + 1; } } if (fm != null) { - fm.ascent = ab; - fm.descent = be; + fm.ascent = ascent; + fm.descent = descent; fm.top = top; - fm.bottom = bot; + fm.bottom = bottom; } if (hasTabs) TextUtils.recycle(buf); - return h; + return width; } + /** + * Returns the position of the next tab stop after h on the line. + * + * @param text the text + * @param start start of the line + * @param end limit of the line + * @param h the current horizontal offset + * @param tabs the tabs, can be null. If it is null, any tabs in effect + * on the line will be used. If there are no tabs, a default offset + * will be used to compute the tab stop. + * @return the offset of the next tab stop. + */ /* package */ static float nextTab(CharSequence text, int start, int end, float h, Object[] tabs) { float nh = Float.MAX_VALUE; @@ -1747,6 +1810,16 @@ public abstract class Layout { public static class Directions { private short[] mDirections; + // The values in mDirections are the offsets from the first character + // in the line to the next flip in direction. Runs at even indices + // are left-to-right, the others are right-to-left. So, for example, + // a line that starts with a right-to-left run has 0 at mDirections[0], + // since the 'first' (ltr) run is zero length. + // + // The code currently assumes that each run is adjacent to the previous + // one, progressing in the base line direction. This isn't sufficient + // to handle nested runs, for example numeric text in an rtl context + // in an ltr paragraph. /* package */ Directions(short[] dirs) { mDirections = dirs; } diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java index fbf1261..6c89f92 100644 --- a/core/java/android/text/StaticLayout.java +++ b/core/java/android/text/StaticLayout.java @@ -1012,6 +1012,10 @@ extends Layout int extra; if (needMultiply) { + // XXX: this looks like it is using the +0.5 and the cast to int + // to do rounding, but this I expect this isn't doing the intended + // thing when spacingmult < 1. An intended extra of, say, -1.2 + // will get 'rounded' to -.7 and then truncated to 0. extra = (int) ((below - above) * (spacingmult - 1) + spacingadd + 0.5); } else { diff --git a/core/java/android/text/Styled.java b/core/java/android/text/Styled.java index 0aa2004..513b2cd 100644 --- a/core/java/android/text/Styled.java +++ b/core/java/android/text/Styled.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package android.text; import android.graphics.Canvas; @@ -23,27 +22,49 @@ import android.text.style.MetricAffectingSpan; import android.text.style.ReplacementSpan; /** - * This class provides static methods for drawing and measuring styled texts, like - * {@link android.text.Spanned} object with {@link android.text.style.ReplacementSpan}. + * This class provides static methods for drawing and measuring styled text, + * like {@link android.text.Spanned} object with + * {@link android.text.style.ReplacementSpan}. + * * @hide */ public class Styled { - private static float each(Canvas canvas, + /** + * Draws and/or measures a uniform run of text on a single line. No span of + * interest should start or end in the middle of this run (if not + * drawing, character spans that don't affect metrics can be ignored). + * Neither should the run direction change in the middle of the run. + * + * <p>The x position is the leading edge of the text. In a right-to-left + * paragraph, this will be to the right of the text to be drawn. Paint + * should not have an Align value other than LEFT or positioning will get + * confused. + * + * <p>On return, workPaint will reflect the original paint plus any + * modifications made by character styles on the run. + * + * <p>The returned width is signed and will be < 0 if the paragraph + * direction is right-to-left. + */ + private static float drawUniformRun(Canvas canvas, Spanned text, int start, int end, - int dir, boolean reverse, + int dir, boolean runIsRtl, float x, int top, int y, int bottom, Paint.FontMetricsInt fmi, TextPaint paint, TextPaint workPaint, - boolean needwid) { + boolean needWidth) { - boolean havewid = false; + boolean haveWidth = false; float ret = 0; CharacterStyle[] spans = text.getSpans(start, end, CharacterStyle.class); ReplacementSpan replacement = null; + // XXX: This shouldn't be modifying paint, only workPaint. + // However, the members belonging to TextPaint should have default + // values anyway. Better to ensure this in the Layout constructor. paint.bgColor = 0; paint.baselineShift = 0; workPaint.set(paint); @@ -65,9 +86,10 @@ public class Styled CharSequence tmp; int tmpstart, tmpend; - if (reverse) { + if (runIsRtl) { tmp = TextUtils.getReverse(text, start, end); tmpstart = 0; + // XXX: assumes getReverse doesn't change the length of the text tmpend = end - start; } else { tmp = text; @@ -86,9 +108,9 @@ public class Styled workPaint.setColor(workPaint.bgColor); workPaint.setStyle(Paint.Style.FILL); - if (!havewid) { + if (!haveWidth) { ret = workPaint.measureText(tmp, tmpstart, tmpend); - havewid = true; + haveWidth = true; } if (dir == Layout.DIR_RIGHT_TO_LEFT) @@ -101,18 +123,18 @@ public class Styled } if (dir == Layout.DIR_RIGHT_TO_LEFT) { - if (!havewid) { + if (!haveWidth) { ret = workPaint.measureText(tmp, tmpstart, tmpend); - havewid = true; + haveWidth = true; } canvas.drawText(tmp, tmpstart, tmpend, x - ret, y + workPaint.baselineShift, workPaint); } else { - if (needwid) { - if (!havewid) { + if (needWidth) { + if (!haveWidth) { ret = workPaint.measureText(tmp, tmpstart, tmpend); - havewid = true; + haveWidth = true; } } @@ -120,9 +142,9 @@ public class Styled x, y + workPaint.baselineShift, workPaint); } } else { - if (needwid && !havewid) { + if (needWidth && !haveWidth) { ret = workPaint.measureText(tmp, tmpstart, tmpend); - havewid = true; + haveWidth = true; } } } else { @@ -145,25 +167,28 @@ public class Styled } /** - * Return the advance widths for the characters in the string. - * See also {@link android.graphics.Paint#getTextWidths(CharSequence, int, int, float[])}. + * Returns the advance widths for a uniform left-to-right run of text with + * no style changes in the middle of the run. If any style is replacement + * text, the first character will get the width of the replacement and the + * remaining characters will get a width of 0. * - * @param paint The main {@link TextPaint} object. - * @param workPaint The {@link TextPaint} object used for temporal workspace. - * @param text The text to measure - * @param start The index of the first char to to measure - * @param end The end of the text slice to measure - * @param widths Array to receive the advance widths of the characters. - * Must be at least a large as (end - start). - * @param fmi FontMetrics information. Can be null. - * @return The actual number of widths returned. + * @param paint the paint, will not be modified + * @param workPaint a paint to modify; on return will reflect the original + * paint plus the effect of all spans on the run + * @param text the text + * @param start the start of the run + * @param end the limit of the run + * @param widths array to receive the advance widths of the characters. Must + * be at least a large as (end - start). + * @param fmi FontMetrics information; can be null + * @return the actual number of widths returned */ public static int getTextWidths(TextPaint paint, TextPaint workPaint, Spanned text, int start, int end, float[] widths, Paint.FontMetricsInt fmi) { - // Keep workPaint as is so that developers reuse the workspace. - MetricAffectingSpan[] spans = text.getSpans(start, end, MetricAffectingSpan.class); + MetricAffectingSpan[] spans = + text.getSpans(start, end, MetricAffectingSpan.class); ReplacementSpan replacement = null; workPaint.set(paint); @@ -186,7 +211,6 @@ public class Styled if (end > start) { widths[0] = wid; - for (int i = start + 1; i < end; i++) widths[i - start] = 0; } @@ -194,19 +218,42 @@ public class Styled return end - start; } - private static float foreach(Canvas canvas, + /** + * Renders and/or measures a directional run of text on a single line. + * Unlike {@link #drawUniformRun}, this can render runs that cross style + * boundaries. Returns the signed advance width, if requested. + * + * <p>The x position is the leading edge of the text. In a right-to-left + * paragraph, this will be to the right of the text to be drawn. Paint + * should not have an Align value other than LEFT or positioning will get + * confused. + * + * <p>This optimizes for unstyled text and so workPaint might not be + * modified by this call. + * + * <p>The returned advance width will be < 0 if the paragraph + * direction is right-to-left. + */ + private static float drawDirectionalRun(Canvas canvas, CharSequence text, int start, int end, - int dir, boolean reverse, + int dir, boolean runIsRtl, float x, int top, int y, int bottom, Paint.FontMetricsInt fmi, TextPaint paint, TextPaint workPaint, boolean needWidth) { - if (! (text instanceof Spanned)) { + + // XXX: It looks like all calls to this API match dir and runIsRtl, so + // having both parameters is redundant and confusing. + + // fast path for unstyled text + if (!(text instanceof Spanned)) { float ret = 0; - if (reverse) { + if (runIsRtl) { CharSequence tmp = TextUtils.getReverse(text, start, end); + // XXX: this assumes getReverse doesn't tweak the length of + // the text int tmpend = end - start; if (canvas != null || needWidth) @@ -227,15 +274,14 @@ public class Styled paint.getFontMetricsInt(fmi); } - return ret * dir; //Layout.DIR_RIGHT_TO_LEFT == -1 + return ret * dir; // Layout.DIR_RIGHT_TO_LEFT == -1 } float ox = x; - int asc = 0, desc = 0; - int ftop = 0, fbot = 0; + int minAscent = 0, maxDescent = 0, minTop = 0, maxBottom = 0; Spanned sp = (Spanned) text; - Class division; + Class<?> division; if (canvas == null) division = MetricAffectingSpan.class; @@ -246,20 +292,23 @@ public class Styled for (int i = start; i < end; i = next) { next = sp.nextSpanTransition(i, end, division); - x += each(canvas, sp, i, next, dir, reverse, + // XXX: if dir and runIsRtl were not the same, this would draw + // spans in the wrong order, but no one appears to call it this + // way. + x += drawUniformRun(canvas, sp, i, next, dir, runIsRtl, x, top, y, bottom, fmi, paint, workPaint, needWidth || next != end); if (fmi != null) { - if (fmi.ascent < asc) - asc = fmi.ascent; - if (fmi.descent > desc) - desc = fmi.descent; - - if (fmi.top < ftop) - ftop = fmi.top; - if (fmi.bottom > fbot) - fbot = fmi.bottom; + if (fmi.ascent < minAscent) + minAscent = fmi.ascent; + if (fmi.descent > maxDescent) + maxDescent = fmi.descent; + + if (fmi.top < minTop) + minTop = fmi.top; + if (fmi.bottom > maxBottom) + maxBottom = fmi.bottom; } } @@ -267,71 +316,78 @@ public class Styled if (start == end) { paint.getFontMetricsInt(fmi); } else { - fmi.ascent = asc; - fmi.descent = desc; - fmi.top = ftop; - fmi.bottom = fbot; + fmi.ascent = minAscent; + fmi.descent = maxDescent; + fmi.top = minTop; + fmi.bottom = maxBottom; } } return x - ox; } - + /** + * Draws a unidirectional run of text on a single line, and optionally + * returns the signed advance. Unlike drawDirectionalRun, the paragraph + * direction and run direction can be different. + */ /* package */ static float drawText(Canvas canvas, CharSequence text, int start, int end, - int direction, boolean reverse, + int dir, boolean runIsRtl, float x, int top, int y, int bottom, TextPaint paint, TextPaint workPaint, boolean needWidth) { - if ((direction == Layout.DIR_RIGHT_TO_LEFT && !reverse) || - (reverse && direction == Layout.DIR_LEFT_TO_RIGHT)) { - float ch = foreach(null, text, start, end, Layout.DIR_LEFT_TO_RIGHT, - false, 0, 0, 0, 0, null, paint, workPaint, - true); - - ch *= direction; // DIR_RIGHT_TO_LEFT == -1 - foreach(canvas, text, start, end, -direction, - reverse, x + ch, top, y, bottom, null, paint, + // XXX this logic is (dir == DIR_LEFT_TO_RIGHT) == runIsRtl + if ((dir == Layout.DIR_RIGHT_TO_LEFT && !runIsRtl) || + (runIsRtl && dir == Layout.DIR_LEFT_TO_RIGHT)) { + // TODO: this needs the real direction + float ch = drawDirectionalRun(null, text, start, end, + Layout.DIR_LEFT_TO_RIGHT, false, 0, 0, 0, 0, null, paint, + workPaint, true); + + ch *= dir; // DIR_RIGHT_TO_LEFT == -1 + drawDirectionalRun(canvas, text, start, end, -dir, + runIsRtl, x + ch, top, y, bottom, null, paint, workPaint, true); return ch; } - return foreach(canvas, text, start, end, direction, reverse, + return drawDirectionalRun(canvas, text, start, end, dir, runIsRtl, x, top, y, bottom, null, paint, workPaint, needWidth); } /** - * Draw the specified range of text, specified by start/end, with its origin at (x,y), - * in the specified Paint. The origin is interpreted based on the Align setting in the - * Paint. - * - * This method considers style information in the text - * (e.g. Even when text is an instance of {@link android.text.Spanned}, this method - * correctly draws the text). - * See also - * {@link android.graphics.Canvas#drawText(CharSequence, int, int, float, float, Paint)} - * and - * {@link android.graphics.Canvas#drawRect(float, float, float, float, Paint)}. + * Draws a run of text on a single line, with its + * origin at (x,y), in the specified Paint. The origin is interpreted based + * on the Align setting in the Paint. + * + * This method considers style information in the text (e.g. even when text + * is an instance of {@link android.text.Spanned}, this method correctly + * draws the text). See also + * {@link android.graphics.Canvas#drawText(CharSequence, int, int, float, + * float, Paint)} and + * {@link android.graphics.Canvas#drawRect(float, float, float, float, + * Paint)}. * - * @param canvas The target canvas. + * @param canvas The target canvas * @param text The text to be drawn * @param start The index of the first character in text to draw * @param end (end - 1) is the index of the last character in text to draw * @param direction The direction of the text. This must be - * {@link android.text.Layout#DIR_LEFT_TO_RIGHT} or - * {@link android.text.Layout#DIR_RIGHT_TO_LEFT}. + * {@link android.text.Layout#DIR_LEFT_TO_RIGHT} or + * {@link android.text.Layout#DIR_RIGHT_TO_LEFT}. * @param x The x-coordinate of origin for where to draw the text * @param top The top side of the rectangle to be drawn * @param y The y-coordinate of origin for where to draw the text * @param bottom The bottom side of the rectangle to be drawn * @param paint The main {@link TextPaint} object. - * @param workPaint The {@link TextPaint} object used for temporal workspace. - * @param needWidth If true, this method returns the width of drawn text. - * @return Width of the drawn text if needWidth is true. + * @param workPaint The {@link TextPaint} object used for temporal + * workspace. + * @param needWidth If true, this method returns the width of drawn text + * @return Width of the drawn text if needWidth is true */ public static float drawText(Canvas canvas, CharSequence text, int start, int end, @@ -341,34 +397,37 @@ public class Styled TextPaint workPaint, boolean needWidth) { // For safety. - direction = direction >= 0 ? Layout.DIR_LEFT_TO_RIGHT : Layout.DIR_RIGHT_TO_LEFT; - /* - * Hided "reverse" parameter since it is meaningless for external developers. - * Kept workPaint as is so that developers reuse the workspace. - */ + direction = direction >= 0 ? Layout.DIR_LEFT_TO_RIGHT + : Layout.DIR_RIGHT_TO_LEFT; + + // Hide runIsRtl parameter since it is meaningless for external + // developers. + // XXX: the runIsRtl probably ought to be the same as direction, then + // this could draw rtl text. return drawText(canvas, text, start, end, direction, false, x, top, y, bottom, paint, workPaint, needWidth); } /** - * Return the width of the text, considering style information in the text - * (e.g. Even when text is an instance of {@link android.text.Spanned}, this method - * correctly mesures the width of the text). + * Returns the width of a run of left-to-right text on a single line, + * considering style information in the text (e.g. even when text is an + * instance of {@link android.text.Spanned}, this method correctly measures + * the width of the text). * - * @param paint The main {@link TextPaint} object. - * @param workPaint The {@link TextPaint} object used for temporal workspace. - * @param text The text to measure - * @param start The index of the first character to start measuring + * @param paint the main {@link TextPaint} object; will not be modified + * @param workPaint the {@link TextPaint} object available for modification; + * will not necessarily be used + * @param text the text to measure + * @param start the index of the first character to start measuring * @param end 1 beyond the index of the last character to measure - * @param fmi FontMetrics information. Can be null - * @return The width of the text + * @param fmi FontMetrics information; can be null + * @return The width of the text */ public static float measureText(TextPaint paint, TextPaint workPaint, CharSequence text, int start, int end, Paint.FontMetricsInt fmi) { - // Keep workPaint as is so that developers reuse the workspace. - return foreach(null, text, start, end, + return drawDirectionalRun(null, text, start, end, Layout.DIR_LEFT_TO_RIGHT, false, 0, 0, 0, 0, fmi, paint, workPaint, true); } diff --git a/core/java/android/text/style/LeadingMarginSpan.java b/core/java/android/text/style/LeadingMarginSpan.java index cb55329..f320701 100644 --- a/core/java/android/text/style/LeadingMarginSpan.java +++ b/core/java/android/text/style/LeadingMarginSpan.java @@ -23,10 +23,44 @@ import android.text.Layout; import android.text.ParcelableSpan; import android.text.TextUtils; +/** + * A paragraph style affecting the leading margin. There can be multiple leading + * margin spans on a single paragraph; they will be rendered in order, each + * adding its margin to the ones before it. The leading margin is on the right + * for lines in a right-to-left paragraph. + */ public interface LeadingMarginSpan extends ParagraphStyle { + /** + * Returns the amount by which to adjust the leading margin. Positive values + * move away from the leading edge of the paragraph, negative values move + * towards it. + * + * @param first true if the request is for the first line of a paragraph, + * false for subsequent lines + * @return the offset for the margin. + */ public int getLeadingMargin(boolean first); + + /** + * Renders the leading margin. This is called before the margin has been + * adjusted by the value returned by {@link #getLeadingMargin(boolean)}. + * + * @param c the canvas + * @param p the paint. The this should be left unchanged on exit. + * @param x the current position of the margin + * @param dir the base direction of the paragraph; if negative, the margin + * is to the right of the text, otherwise it is to the left. + * @param top the top of the line + * @param baseline the baseline of the line + * @param bottom the bottom of the line + * @param text the text + * @param start the start of the line + * @param end the end of the line + * @param first true if this is the first line of its paragraph + * @param layout the layout containing this line + */ public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, int top, int baseline, int bottom, @@ -38,14 +72,29 @@ extends ParagraphStyle public int getLeadingMarginLineCount(); }; + /** + * The standard implementation of LeadingMarginSpan, which adjusts the + * margin but does not do any rendering. + */ public static class Standard implements LeadingMarginSpan, ParcelableSpan { private final int mFirst, mRest; + /** + * Constructor taking separate indents for the first and subsequent + * lines. + * + * @param first the indent for the first line of the paragraph + * @param rest the indent for the remaining lines of the paragraph + */ public Standard(int first, int rest) { mFirst = first; mRest = rest; } + /** + * Constructor taking an indent for all lines. + * @param every the indent of each line + */ public Standard(int every) { this(every, every); } diff --git a/core/java/android/text/style/TabStopSpan.java b/core/java/android/text/style/TabStopSpan.java index e5b7644..0566428 100644 --- a/core/java/android/text/style/TabStopSpan.java +++ b/core/java/android/text/style/TabStopSpan.java @@ -16,14 +16,31 @@ package android.text.style; +/** + * Represents a single tab stop on a line. + */ public interface TabStopSpan extends ParagraphStyle { + /** + * Returns the offset of the tab stop from the leading margin of the + * line. + * @return the offset + */ public int getTabStop(); + /** + * The default implementation of TabStopSpan. + */ public static class Standard implements TabStopSpan { + /** + * Constructor. + * + * @param where the offset of the tab stop from the leading margin of + * the line + */ public Standard(int where) { mTab = where; } diff --git a/core/java/android/text/util/Rfc822Tokenizer.java b/core/java/android/text/util/Rfc822Tokenizer.java index 9d8bfd9..69d745d 100644 --- a/core/java/android/text/util/Rfc822Tokenizer.java +++ b/core/java/android/text/util/Rfc822Tokenizer.java @@ -84,7 +84,7 @@ public class Rfc822Tokenizer implements MultiAutoCompleteTextView.Tokenizer { if (c == '"') { i++; break; - } else if (c == '\\') { + } else if (c == '\\' && i + 1 < cursor) { name.append(text.charAt(i + 1)); i += 2; } else { @@ -110,7 +110,7 @@ public class Rfc822Tokenizer implements MultiAutoCompleteTextView.Tokenizer { comment.append(c); level++; i++; - } else if (c == '\\') { + } else if (c == '\\' && i + 1 < cursor) { comment.append(text.charAt(i + 1)); i += 2; } else { diff --git a/core/java/android/util/base64/Base64.java b/core/java/android/util/base64/Base64.java new file mode 100644 index 0000000..f6d3905 --- /dev/null +++ b/core/java/android/util/base64/Base64.java @@ -0,0 +1,741 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.util.base64; + +import java.io.UnsupportedEncodingException; + +/** + * Utilities for encoding and decoding the Base64 representation of + * binary data. See RFCs <a + * href="http://www.ietf.org/rfc/rfc2045.txt">2045</a> and <a + * href="http://www.ietf.org/rfc/rfc3548.txt">3548</a>. + */ +public class Base64 { + /** + * Default values for encoder/decoder flags. + */ + public static final int DEFAULT = 0; + + /** + * Encoder flag bit to omit the padding '=' characters at the end + * of the output (if any). + */ + public static final int NO_PADDING = 1; + + /** + * Encoder flag bit to omit all line terminators (i.e., the output + * will be on one long line). + */ + public static final int NO_WRAP = 2; + + /** + * Encoder flag bit to indicate lines should be terminated with a + * CRLF pair instead of just an LF. Has no effect if {@code + * NO_WRAP} is specified as well. + */ + public static final int CRLF = 4; + + /** + * Encoder/decoder flag bit to indicate using the "URL and + * filename safe" variant of Base64 (see RFC 3548 section 4) where + * {@code -} and {@code _} are used in place of {@code +} and + * {@code /}. + */ + public static final int URL_SAFE = 8; + + /** + * Flag to pass to {@link Base64OutputStream} to indicate that it + * should not close the output stream it is wrapping when it + * itself is closed. + */ + public static final int NO_CLOSE = 16; + + // -------------------------------------------------------- + // shared code + // -------------------------------------------------------- + + /* package */ static abstract class Coder { + public byte[] output; + public int op; + + /** + * Encode/decode another block of input data. this.output is + * provided by the caller, and must be big enough to hold all + * the coded data. On exit, this.opwill be set to the length + * of the coded data. + * + * @param finish true if this is the final call to process for + * this object. Will finalize the coder state and + * include any final bytes in the output. + * + * @return true if the input so far is good; false if some + * error has been detected in the input stream.. + */ + public abstract boolean process(byte[] input, int offset, int len, boolean finish); + + /** + * @return the maximum number of bytes a call to process() + * could produce for the given number of input bytes. This may + * be an overestimate. + */ + public abstract int maxOutputSize(int len); + } + + // -------------------------------------------------------- + // decoding + // -------------------------------------------------------- + + /** + * Decode the Base64-encoded data in input and return the data in + * a new byte array. + * + * <p>The padding '=' characters at the end are considered optional, but + * if any are present, there must be the correct number of them. + * + * @param str the input String to decode, which is converted to + * bytes using the default charset + * @param flags controls certain features of the decoded output. + * Pass {@code DEFAULT} to decode standard Base64. + * + * @throws IllegalArgumentException if the input contains + * incorrect padding + */ + public static byte[] decode(String str, int flags) { + return decode(str.getBytes(), flags); + } + + /** + * Decode the Base64-encoded data in input and return the data in + * a new byte array. + * + * <p>The padding '=' characters at the end are considered optional, but + * if any are present, there must be the correct number of them. + * + * @param input the input array to decode + * @param flags controls certain features of the decoded output. + * Pass {@code DEFAULT} to decode standard Base64. + * + * @throws IllegalArgumentException if the input contains + * incorrect padding + */ + public static byte[] decode(byte[] input, int flags) { + return decode(input, 0, input.length, flags); + } + + /** + * Decode the Base64-encoded data in input and return the data in + * a new byte array. + * + * <p>The padding '=' characters at the end are considered optional, but + * if any are present, there must be the correct number of them. + * + * @param input the data to decode + * @param offset the position within the input array at which to start + * @param len the number of bytes of input to decode + * @param flags controls certain features of the decoded output. + * Pass {@code DEFAULT} to decode standard Base64. + * + * @throws IllegalArgumentException if the input contains + * incorrect padding + */ + public static byte[] decode(byte[] input, int offset, int len, int flags) { + // Allocate space for the most data the input could represent. + // (It could contain less if it contains whitespace, etc.) + Decoder decoder = new Decoder(flags, new byte[len*3/4]); + + if (!decoder.process(input, offset, len, true)) { + throw new IllegalArgumentException("bad base-64"); + } + + // Maybe we got lucky and allocated exactly enough output space. + if (decoder.op == decoder.output.length) { + return decoder.output; + } + + // Need to shorten the array, so allocate a new one of the + // right size and copy. + byte[] temp = new byte[decoder.op]; + System.arraycopy(decoder.output, 0, temp, 0, decoder.op); + return temp; + } + + /* package */ static class Decoder extends Coder { + /** + * Lookup table for turning bytes into their position in the + * Base64 alphabet. + */ + private static final int DECODE[] = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1, + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, + -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + }; + + /** + * Decode lookup table for the "web safe" variant (RFC 3548 + * sec. 4) where - and _ replace + and /. + */ + private static final int DECODE_WEBSAFE[] = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1, + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, 63, + -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + }; + + /** Non-data values in the DECODE arrays. */ + private static final int SKIP = -1; + private static final int EQUALS = -2; + + /** + * States 0-3 are reading through the next input tuple. + * State 4 is having read one '=' and expecting exactly + * one more. + * State 5 is expecting no more data or padding characters + * in the input. + * State 6 is the error state; an error has been detected + * in the input and no future input can "fix" it. + */ + private int state; // state number (0 to 6) + private int value; + + final private int[] alphabet; + + public Decoder(int flags, byte[] output) { + this.output = output; + + alphabet = ((flags & URL_SAFE) == 0) ? DECODE : DECODE_WEBSAFE; + state = 0; + value = 0; + } + + /** + * @return an overestimate for the number of bytes {@code + * len} bytes could decode to. + */ + public int maxOutputSize(int len) { + return len * 3/4 + 10; + } + + /** + * Decode another block of input data. + * + * @return true if the state machine is still healthy. false if + * bad base-64 data has been detected in the input stream. + */ + public boolean process(byte[] input, int offset, int len, boolean finish) { + if (this.state == 6) return false; + + int p = offset; + len += offset; + + // Using local variables makes the decoder about 12% + // faster than if we manipulate the member variables in + // the loop. (Even alphabet makes a measurable + // difference, which is somewhat surprising to me since + // the member variable is final.) + int state = this.state; + int value = this.value; + int op = 0; + final byte[] output = this.output; + final int[] alphabet = this.alphabet; + + while (p < len) { + // Try the fast path: we're starting a new tuple and the + // next four bytes of the input stream are all data + // bytes. This corresponds to going through states + // 0-1-2-3-0. We expect to use this method for most of + // the data. + // + // If any of the next four bytes of input are non-data + // (whitespace, etc.), value will end up negative. (All + // the non-data values in decode are small negative + // numbers, so shifting any of them up and or'ing them + // together will result in a value with its top bit set.) + // + // You can remove this whole block and the output should + // be the same, just slower. + if (state == 0) { + while (p+4 <= len && + (value = ((alphabet[input[p] & 0xff] << 18) | + (alphabet[input[p+1] & 0xff] << 12) | + (alphabet[input[p+2] & 0xff] << 6) | + (alphabet[input[p+3] & 0xff]))) >= 0) { + output[op+2] = (byte) value; + output[op+1] = (byte) (value >> 8); + output[op] = (byte) (value >> 16); + op += 3; + p += 4; + } + if (p >= len) break; + } + + // The fast path isn't available -- either we've read a + // partial tuple, or the next four input bytes aren't all + // data, or whatever. Fall back to the slower state + // machine implementation. + + int d = alphabet[input[p++] & 0xff]; + + switch (state) { + case 0: + if (d >= 0) { + value = d; + ++state; + } else if (d != SKIP) { + this.state = 6; + return false; + } + break; + + case 1: + if (d >= 0) { + value = (value << 6) | d; + ++state; + } else if (d != SKIP) { + this.state = 6; + return false; + } + break; + + case 2: + if (d >= 0) { + value = (value << 6) | d; + ++state; + } else if (d == EQUALS) { + // Emit the last (partial) output tuple; + // expect exactly one more padding character. + output[op++] = (byte) (value >> 4); + state = 4; + } else if (d != SKIP) { + this.state = 6; + return false; + } + break; + + case 3: + if (d >= 0) { + // Emit the output triple and return to state 0. + value = (value << 6) | d; + output[op+2] = (byte) value; + output[op+1] = (byte) (value >> 8); + output[op] = (byte) (value >> 16); + op += 3; + state = 0; + } else if (d == EQUALS) { + // Emit the last (partial) output tuple; + // expect no further data or padding characters. + output[op+1] = (byte) (value >> 2); + output[op] = (byte) (value >> 10); + op += 2; + state = 5; + } else if (d != SKIP) { + this.state = 6; + return false; + } + break; + + case 4: + if (d == EQUALS) { + ++state; + } else if (d != SKIP) { + this.state = 6; + return false; + } + break; + + case 5: + if (d != SKIP) { + this.state = 6; + return false; + } + break; + } + } + + if (!finish) { + // We're out of input, but a future call could provide + // more. + this.state = state; + this.value = value; + this.op = op; + return true; + } + + // Done reading input. Now figure out where we are left in + // the state machine and finish up. + + switch (state) { + case 0: + // Output length is a multiple of three. Fine. + break; + case 1: + // Read one extra input byte, which isn't enough to + // make another output byte. Illegal. + this.state = 6; + return false; + case 2: + // Read two extra input bytes, enough to emit 1 more + // output byte. Fine. + output[op++] = (byte) (value >> 4); + break; + case 3: + // Read three extra input bytes, enough to emit 2 more + // output bytes. Fine. + output[op++] = (byte) (value >> 10); + output[op++] = (byte) (value >> 2); + break; + case 4: + // Read one padding '=' when we expected 2. Illegal. + this.state = 6; + return false; + case 5: + // Read all the padding '='s we expected and no more. + // Fine. + break; + } + + this.state = state; + this.op = op; + return true; + } + } + + // -------------------------------------------------------- + // encoding + // -------------------------------------------------------- + + /** + * Base64-encode the given data and return a newly allocated + * String with the result. + * + * @param input the data to encode + * @param flags controls certain features of the encoded output. + * Passing {@code DEFAULT} results in output that + * adheres to RFC 2045. + */ + public static String encodeToString(byte[] input, int flags) { + try { + return new String(encode(input, flags), "US-ASCII"); + } catch (UnsupportedEncodingException e) { + // US-ASCII is guaranteed to be available. + throw new AssertionError(e); + } + } + + /** + * Base64-encode the given data and return a newly allocated + * String with the result. + * + * @param input the data to encode + * @param offset the position within the input array at which to + * start + * @param len the number of bytes of input to encode + * @param flags controls certain features of the encoded output. + * Passing {@code DEFAULT} results in output that + * adheres to RFC 2045. + */ + public static String encodeToString(byte[] input, int offset, int len, int flags) { + try { + return new String(encode(input, offset, len, flags), "US-ASCII"); + } catch (UnsupportedEncodingException e) { + // US-ASCII is guaranteed to be available. + throw new AssertionError(e); + } + } + + /** + * Base64-encode the given data and return a newly allocated + * byte[] with the result. + * + * @param input the data to encode + * @param flags controls certain features of the encoded output. + * Passing {@code DEFAULT} results in output that + * adheres to RFC 2045. + */ + public static byte[] encode(byte[] input, int flags) { + return encode(input, 0, input.length, flags); + } + + /** + * Base64-encode the given data and return a newly allocated + * byte[] with the result. + * + * @param input the data to encode + * @param offset the position within the input array at which to + * start + * @param len the number of bytes of input to encode + * @param flags controls certain features of the encoded output. + * Passing {@code DEFAULT} results in output that + * adheres to RFC 2045. + */ + public static byte[] encode(byte[] input, int offset, int len, int flags) { + Encoder encoder = new Encoder(flags, null); + + // Compute the exact length of the array we will produce. + int output_len = len / 3 * 4; + + // Account for the tail of the data and the padding bytes, if any. + if (encoder.do_padding) { + if (len % 3 > 0) { + output_len += 4; + } + } else { + switch (len % 3) { + case 0: break; + case 1: output_len += 2; break; + case 2: output_len += 3; break; + } + } + + // Account for the newlines, if any. + if (encoder.do_newline && len > 0) { + output_len += (((len-1) / (3 * Encoder.LINE_GROUPS)) + 1) * + (encoder.do_cr ? 2 : 1); + } + + encoder.output = new byte[output_len]; + encoder.process(input, offset, len, true); + + assert encoder.op == output_len; + + return encoder.output; + } + + /* package */ static class Encoder extends Coder { + /** + * Emit a new line every this many output tuples. Corresponds to + * a 76-character line length (the maximum allowable according to + * <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045</a>). + */ + public static final int LINE_GROUPS = 19; + + /** + * Lookup table for turning Base64 alphabet positions (6 bits) + * into output bytes. + */ + private static final byte ENCODE[] = { + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', + 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', + 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', + 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/', + }; + + /** + * Lookup table for turning Base64 alphabet positions (6 bits) + * into output bytes. + */ + private static final byte ENCODE_WEBSAFE[] = { + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', + 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', + 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', + 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_', + }; + + final private byte[] tail; + /* package */ int tailLen; + private int count; + + final public boolean do_padding; + final public boolean do_newline; + final public boolean do_cr; + final private byte[] alphabet; + + public Encoder(int flags, byte[] output) { + this.output = output; + + do_padding = (flags & NO_PADDING) == 0; + do_newline = (flags & NO_WRAP) == 0; + do_cr = (flags & CRLF) != 0; + alphabet = ((flags & URL_SAFE) == 0) ? ENCODE : ENCODE_WEBSAFE; + + tail = new byte[2]; + tailLen = 0; + + count = do_newline ? LINE_GROUPS : -1; + } + + /** + * @return an overestimate for the number of bytes {@code + * len} bytes could encode to. + */ + public int maxOutputSize(int len) { + return len * 8/5 + 10; + } + + public boolean process(byte[] input, int offset, int len, boolean finish) { + // Using local variables makes the encoder about 9% faster. + final byte[] alphabet = this.alphabet; + final byte[] output = this.output; + int op = 0; + int count = this.count; + + int p = offset; + len += offset; + int v = -1; + + // First we need to concatenate the tail of the previous call + // with any input bytes available now and see if we can empty + // the tail. + + switch (tailLen) { + case 0: + // There was no tail. + break; + + case 1: + if (p+2 <= len) { + // A 1-byte tail with at least 2 bytes of + // input available now. + v = ((tail[0] & 0xff) << 16) | + ((input[p++] & 0xff) << 8) | + (input[p++] & 0xff); + tailLen = 0; + }; + break; + + case 2: + if (p+1 <= len) { + // A 2-byte tail with at least 1 byte of input. + v = ((tail[0] & 0xff) << 16) | + ((tail[1] & 0xff) << 8) | + (input[p++] & 0xff); + tailLen = 0; + } + break; + } + + if (v != -1) { + output[op++] = alphabet[(v >> 18) & 0x3f]; + output[op++] = alphabet[(v >> 12) & 0x3f]; + output[op++] = alphabet[(v >> 6) & 0x3f]; + output[op++] = alphabet[v & 0x3f]; + if (--count == 0) { + if (do_cr) output[op++] = '\r'; + output[op++] = '\n'; + count = LINE_GROUPS; + } + } + + // At this point either there is no tail, or there are fewer + // than 3 bytes of input available. + + // The main loop, turning 3 input bytes into 4 output bytes on + // each iteration. + while (p+3 <= len) { + v = ((input[p] & 0xff) << 16) | + ((input[p+1] & 0xff) << 8) | + (input[p+2] & 0xff); + output[op] = alphabet[(v >> 18) & 0x3f]; + output[op+1] = alphabet[(v >> 12) & 0x3f]; + output[op+2] = alphabet[(v >> 6) & 0x3f]; + output[op+3] = alphabet[v & 0x3f]; + p += 3; + op += 4; + if (--count == 0) { + if (do_cr) output[op++] = '\r'; + output[op++] = '\n'; + count = LINE_GROUPS; + } + } + + if (finish) { + // Finish up the tail of the input. Note that we need to + // consume any bytes in tail before any bytes + // remaining in input; there should be at most two bytes + // total. + + if (p-tailLen == len-1) { + int t = 0; + v = ((tailLen > 0 ? tail[t++] : input[p++]) & 0xff) << 4; + tailLen -= t; + output[op++] = alphabet[(v >> 6) & 0x3f]; + output[op++] = alphabet[v & 0x3f]; + if (do_padding) { + output[op++] = '='; + output[op++] = '='; + } + if (do_newline) { + if (do_cr) output[op++] = '\r'; + output[op++] = '\n'; + } + } else if (p-tailLen == len-2) { + int t = 0; + v = (((tailLen > 1 ? tail[t++] : input[p++]) & 0xff) << 10) | + (((tailLen > 0 ? tail[t++] : input[p++]) & 0xff) << 2); + tailLen -= t; + output[op++] = alphabet[(v >> 12) & 0x3f]; + output[op++] = alphabet[(v >> 6) & 0x3f]; + output[op++] = alphabet[v & 0x3f]; + if (do_padding) { + output[op++] = '='; + } + if (do_newline) { + if (do_cr) output[op++] = '\r'; + output[op++] = '\n'; + } + } else if (do_newline && op > 0 && count != LINE_GROUPS) { + if (do_cr) output[op++] = '\r'; + output[op++] = '\n'; + } + + assert tailLen == 0; + assert p == len; + } else { + // Save the leftovers in tail to be consumed on the next + // call to encodeInternal. + + if (p == len-1) { + tail[tailLen++] = input[p]; + } else if (p == len-2) { + tail[tailLen++] = input[p]; + tail[tailLen++] = input[p+1]; + } + } + + this.op = op; + this.count = count; + + return true; + } + } + + private Base64() { } // don't instantiate +} diff --git a/core/java/android/util/base64/Base64InputStream.java b/core/java/android/util/base64/Base64InputStream.java new file mode 100644 index 0000000..935939e --- /dev/null +++ b/core/java/android/util/base64/Base64InputStream.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.util.base64; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * An InputStream that does Base64 decoding on the data read through + * it. + */ +public class Base64InputStream extends FilterInputStream { + private final Base64.Coder coder; + + private static byte[] EMPTY = new byte[0]; + + private static final int BUFFER_SIZE = 2048; + private boolean eof; + private byte[] inputBuffer; + private int outputStart; + private int outputEnd; + + /** + * An InputStream that performs Base64 decoding on the data read + * from the wrapped stream. + * + * @param in the InputStream to read the source data from + * @param flags bit flags for controlling the decoder; see the + * constants in {@link Base64} + */ + public Base64InputStream(InputStream in, int flags) { + this(in, flags, false); + } + + /** + * Performs Base64 encoding or decoding on the data read from the + * wrapped InputStream. + * + * @param in the InputStream to read the source data from + * @param flags bit flags for controlling the decoder; see the + * constants in {@link Base64} + * @param encode true to encode, false to decode + * + * @hide + */ + public Base64InputStream(InputStream in, int flags, boolean encode) { + super(in); + eof = false; + inputBuffer = new byte[BUFFER_SIZE]; + if (encode) { + coder = new Base64.Encoder(flags, null); + } else { + coder = new Base64.Decoder(flags, null); + } + coder.output = new byte[coder.maxOutputSize(BUFFER_SIZE)]; + outputStart = 0; + outputEnd = 0; + } + + public boolean markSupported() { + return false; + } + + public void mark(int readlimit) { + throw new UnsupportedOperationException(); + } + + public void reset() { + throw new UnsupportedOperationException(); + } + + public void close() throws IOException { + in.close(); + inputBuffer = null; + } + + public int available() { + return outputEnd - outputStart; + } + + public long skip(long n) throws IOException { + if (outputStart >= outputEnd) { + refill(); + } + if (outputStart >= outputEnd) { + return 0; + } + long bytes = Math.min(n, outputEnd-outputStart); + outputStart += bytes; + return bytes; + } + + public int read() throws IOException { + if (outputStart >= outputEnd) { + refill(); + } + if (outputStart >= outputEnd) { + return -1; + } else { + return coder.output[outputStart++]; + } + } + + public int read(byte[] b, int off, int len) throws IOException { + if (outputStart >= outputEnd) { + refill(); + } + if (outputStart >= outputEnd) { + return -1; + } + int bytes = Math.min(len, outputEnd-outputStart); + System.arraycopy(coder.output, outputStart, b, off, bytes); + outputStart += bytes; + return bytes; + } + + /** + * Read data from the input stream into inputBuffer, then + * decode/encode it into the empty coder.output, and reset the + * outputStart and outputEnd pointers. + */ + private void refill() throws IOException { + if (eof) return; + int bytesRead = in.read(inputBuffer); + boolean success; + if (bytesRead == -1) { + eof = true; + success = coder.process(EMPTY, 0, 0, true); + } else { + success = coder.process(inputBuffer, 0, bytesRead, false); + } + if (!success) { + throw new IOException("bad base-64"); + } + outputEnd = coder.op; + outputStart = 0; + } +} diff --git a/core/java/android/util/base64/Base64OutputStream.java b/core/java/android/util/base64/Base64OutputStream.java new file mode 100644 index 0000000..35e9a2b --- /dev/null +++ b/core/java/android/util/base64/Base64OutputStream.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.util.base64; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +/** + * An OutputStream that does Base64 encoding on the data written to + * it, writing the resulting data to another OutputStream. + */ +public class Base64OutputStream extends FilterOutputStream { + private final Base64.Coder coder; + private final int flags; + + private byte[] buffer = null; + private int bpos = 0; + + private static byte[] EMPTY = new byte[0]; + + /** + * Performs Base64 encoding on the data written to the stream, + * writing the encoded data to another OutputStream. + * + * @param out the OutputStream to write the encoded data to + * @param flags bit flags for controlling the encoder; see the + * constants in {@link Base64} + */ + public Base64OutputStream(OutputStream out, int flags) { + this(out, flags, true); + } + + /** + * Performs Base64 encoding or decoding on the data written to the + * stream, writing the encoded/decoded data to another + * OutputStream. + * + * @param out the OutputStream to write the encoded data to + * @param flags bit flags for controlling the encoder; see the + * constants in {@link Base64} + * @param encode true to encode, false to decode + * + * @hide + */ + public Base64OutputStream(OutputStream out, int flags, boolean encode) { + super(out); + this.flags = flags; + if (encode) { + coder = new Base64.Encoder(flags, null); + } else { + coder = new Base64.Decoder(flags, null); + } + } + + public void write(int b) throws IOException { + // To avoid invoking the encoder/decoder routines for single + // bytes, we buffer up calls to write(int) in an internal + // byte array to transform them into writes of decently-sized + // arrays. + + if (buffer == null) { + buffer = new byte[1024]; + } + if (bpos >= buffer.length) { + // internal buffer full; write it out. + internalWrite(buffer, 0, bpos, false); + bpos = 0; + } + buffer[bpos++] = (byte) b; + } + + /** + * Flush any buffered data from calls to write(int). Needed + * before doing a write(byte[], int, int) or a close(). + */ + private void flushBuffer() throws IOException { + if (bpos > 0) { + internalWrite(buffer, 0, bpos, false); + bpos = 0; + } + } + + public void write(byte[] b, int off, int len) throws IOException { + if (len <= 0) return; + flushBuffer(); + internalWrite(b, off, len, false); + } + + public void close() throws IOException { + IOException thrown = null; + try { + flushBuffer(); + internalWrite(EMPTY, 0, 0, true); + } catch (IOException e) { + thrown = e; + } + + try { + if ((flags & Base64.NO_CLOSE) == 0) { + out.close(); + } else { + out.flush(); + } + } catch (IOException e) { + if (thrown != null) { + thrown = e; + } + } + + if (thrown != null) { + throw thrown; + } + } + + /** + * Write the given bytes to the encoder/decoder. + * + * @param finish true if this is the last batch of input, to cause + * encoder/decoder state to be finalized. + */ + private void internalWrite(byte[] b, int off, int len, boolean finish) throws IOException { + coder.output = embiggen(coder.output, coder.maxOutputSize(len)); + if (!coder.process(b, off, len, finish)) { + throw new IOException("bad base-64"); + } + out.write(coder.output, 0, coder.op); + } + + /** + * If b.length is at least len, return b. Otherwise return a new + * byte array of length len. + */ + private byte[] embiggen(byte[] b, int len) { + if (b == null || b.length < len) { + return new byte[len]; + } else { + return b; + } + } +} diff --git a/core/java/android/view/GestureDetector.java b/core/java/android/view/GestureDetector.java index 3c79200..58f998e 100644 --- a/core/java/android/view/GestureDetector.java +++ b/core/java/android/view/GestureDetector.java @@ -465,10 +465,10 @@ public class GestureDetector { case MotionEvent.ACTION_POINTER_UP: // Ending a multitouch gesture and going back to 1 finger if (mIgnoreMultitouch && ev.getPointerCount() == 2) { - int id = (((action & MotionEvent.ACTION_POINTER_ID_MASK) - >> MotionEvent.ACTION_POINTER_ID_SHIFT) == 0) ? 1 : 0; - mLastMotionX = ev.getX(id); - mLastMotionY = ev.getY(id); + int index = (((action & MotionEvent.ACTION_POINTER_INDEX_MASK) + >> MotionEvent.ACTION_POINTER_INDEX_SHIFT) == 0) ? 1 : 0; + mLastMotionX = ev.getX(index); + mLastMotionY = ev.getY(index); mVelocityTracker.recycle(); mVelocityTracker = VelocityTracker.obtain(); } diff --git a/core/java/android/view/HapticFeedbackConstants.java b/core/java/android/view/HapticFeedbackConstants.java index e1f2823..d31c8dc 100644 --- a/core/java/android/view/HapticFeedbackConstants.java +++ b/core/java/android/view/HapticFeedbackConstants.java @@ -34,8 +34,18 @@ public class HapticFeedbackConstants { * The user has pressed on a virtual on-screen key. */ public static final int VIRTUAL_KEY = 1; + + /** + * The user has hit the barrier point while scrolling a view. + */ + public static final int SCROLL_BARRIER = 2; /** + * The user has pressed a soft keyboard key. + */ + public static final int KEYBOARD_TAP = 3; + + /** * This is a private constant. Feel free to renumber as desired. * @hide */ diff --git a/core/java/android/view/IWindow.aidl b/core/java/android/view/IWindow.aidl index 71302cb..3b09808 100644 --- a/core/java/android/view/IWindow.aidl +++ b/core/java/android/view/IWindow.aidl @@ -17,6 +17,7 @@ package android.view; +import android.content.res.Configuration; import android.graphics.Rect; import android.os.Bundle; import android.os.ParcelFileDescriptor; @@ -44,7 +45,7 @@ oneway interface IWindow { void executeCommand(String command, String parameters, in ParcelFileDescriptor descriptor); void resized(int w, int h, in Rect coveredInsets, in Rect visibleInsets, - boolean reportDraw); + boolean reportDraw, in Configuration newConfig); void dispatchKey(in KeyEvent event); void dispatchPointer(in MotionEvent event, long eventTime, boolean callWhenDone); void dispatchTrackball(in MotionEvent event, long eventTime, boolean callWhenDone); diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl index 0ebe360..9b7b2f4 100644 --- a/core/java/android/view/IWindowManager.aidl +++ b/core/java/android/view/IWindowManager.aidl @@ -64,8 +64,6 @@ interface IWindowManager void addAppToken(int addPos, IApplicationToken token, int groupId, int requestedOrientation, boolean fullscreen); void setAppGroupId(IBinder token, int groupId); - Configuration updateOrientationFromAppTokens(in Configuration currentConfig, - IBinder freezeThisOneIfNeeded); void setAppOrientation(IApplicationToken token, int requestedOrientation); int getAppOrientation(IApplicationToken token); void setFocusedApp(IBinder token, boolean moveFocusNow); @@ -85,6 +83,13 @@ interface IWindowManager void moveAppTokensToTop(in List<IBinder> tokens); void moveAppTokensToBottom(in List<IBinder> tokens); + // Re-evaluate the current orientation from the caller's state. + // If there is a change, the new Configuration is returned and the + // caller must call setNewConfiguration() sometime later. + Configuration updateOrientationFromAppTokens(in Configuration currentConfig, + IBinder freezeThisOneIfNeeded); + void setNewConfiguration(in Configuration config); + // these require DISABLE_KEYGUARD permission void disableKeyguard(IBinder token, String tag); void reenableKeyguard(IBinder token); diff --git a/core/java/android/view/MotionEvent.java b/core/java/android/view/MotionEvent.java index ca907af..d648e96 100644 --- a/core/java/android/view/MotionEvent.java +++ b/core/java/android/view/MotionEvent.java @@ -76,61 +76,81 @@ public final class MotionEvent implements Parcelable { public static final int ACTION_POINTER_DOWN = 5; /** - * Synonym for {@link #ACTION_POINTER_DOWN} with - * {@link #ACTION_POINTER_ID_MASK} of 0: the primary pointer has gone done. + * A non-primary pointer has gone up. The bits in + * {@link #ACTION_POINTER_ID_MASK} indicate which pointer changed. */ - public static final int ACTION_POINTER_1_DOWN = ACTION_POINTER_DOWN | 0x0000; + public static final int ACTION_POINTER_UP = 6; /** - * Synonym for {@link #ACTION_POINTER_DOWN} with - * {@link #ACTION_POINTER_ID_MASK} of 1: the secondary pointer has gone done. + * Bits in the action code that represent a pointer index, used with + * {@link #ACTION_POINTER_DOWN} and {@link #ACTION_POINTER_UP}. Shifting + * down by {@link #ACTION_POINTER_INDEX_SHIFT} provides the actual pointer + * index where the data for the pointer going up or down can be found; you can + * get its identifier with {@link #getPointerId(int)} and the actual + * data with {@link #getX(int)} etc. */ - public static final int ACTION_POINTER_2_DOWN = ACTION_POINTER_DOWN | 0x0100; + public static final int ACTION_POINTER_INDEX_MASK = 0xff00; /** - * Synonym for {@link #ACTION_POINTER_DOWN} with - * {@link #ACTION_POINTER_ID_MASK} of 2: the tertiary pointer has gone done. + * Bit shift for the action bits holding the pointer index as + * defined by {@link #ACTION_POINTER_INDEX_MASK}. */ - public static final int ACTION_POINTER_3_DOWN = ACTION_POINTER_DOWN | 0x0200; + public static final int ACTION_POINTER_INDEX_SHIFT = 8; /** - * A non-primary pointer has gone up. The bits in - * {@link #ACTION_POINTER_ID_MASK} indicate which pointer changed. + * @deprecated Use {@link #ACTION_POINTER_INDEX_MASK} to retrieve the + * data index associated with {@link #ACTION_POINTER_DOWN}. */ - public static final int ACTION_POINTER_UP = 6; + @Deprecated + public static final int ACTION_POINTER_1_DOWN = ACTION_POINTER_DOWN | 0x0000; + + /** + * @deprecated Use {@link #ACTION_POINTER_INDEX_MASK} to retrieve the + * data index associated with {@link #ACTION_POINTER_DOWN}. + */ + @Deprecated + public static final int ACTION_POINTER_2_DOWN = ACTION_POINTER_DOWN | 0x0100; /** - * Synonym for {@link #ACTION_POINTER_UP} with - * {@link #ACTION_POINTER_ID_MASK} of 0: the primary pointer has gone up. + * @deprecated Use {@link #ACTION_POINTER_INDEX_MASK} to retrieve the + * data index associated with {@link #ACTION_POINTER_DOWN}. */ + @Deprecated + public static final int ACTION_POINTER_3_DOWN = ACTION_POINTER_DOWN | 0x0200; + + /** + * @deprecated Use {@link #ACTION_POINTER_INDEX_MASK} to retrieve the + * data index associated with {@link #ACTION_POINTER_UP}. + */ + @Deprecated public static final int ACTION_POINTER_1_UP = ACTION_POINTER_UP | 0x0000; /** - * Synonym for {@link #ACTION_POINTER_UP} with - * {@link #ACTION_POINTER_ID_MASK} of 1: the secondary pointer has gone up. + * @deprecated Use {@link #ACTION_POINTER_INDEX_MASK} to retrieve the + * data index associated with {@link #ACTION_POINTER_UP}. */ + @Deprecated public static final int ACTION_POINTER_2_UP = ACTION_POINTER_UP | 0x0100; /** - * Synonym for {@link #ACTION_POINTER_UP} with - * {@link #ACTION_POINTER_ID_MASK} of 2: the tertiary pointer has gone up. + * @deprecated Use {@link #ACTION_POINTER_INDEX_MASK} to retrieve the + * data index associated with {@link #ACTION_POINTER_UP}. */ + @Deprecated public static final int ACTION_POINTER_3_UP = ACTION_POINTER_UP | 0x0200; /** - * Bits in the action code that represent a pointer ID, used with - * {@link #ACTION_POINTER_DOWN} and {@link #ACTION_POINTER_UP}. Pointer IDs - * start at 0, with 0 being the primary (first) pointer in the motion. Note - * that this not <em>not</em> an index into the array of pointer values, - * which is compacted to only contain pointers that are down; the pointer - * ID for a particular index can be found with {@link #findPointerIndex}. + * @deprecated Renamed to {@link #ACTION_POINTER_INDEX_MASK} to match + * the actual data contained in these bits. */ + @Deprecated public static final int ACTION_POINTER_ID_MASK = 0xff00; /** - * Bit shift for the action bits holding the pointer identifier as - * defined by {@link #ACTION_POINTER_ID_MASK}. + * @deprecated Renamed to {@link #ACTION_POINTER_INDEX_SHIFT} to match + * the actual data contained in these bits. */ + @Deprecated public static final int ACTION_POINTER_ID_SHIFT = 8; private static final boolean TRACK_RECYCLED_LOCATION = false; @@ -618,13 +638,39 @@ public final class MotionEvent implements Parcelable { /** * Return the kind of action being performed -- one of either * {@link #ACTION_DOWN}, {@link #ACTION_MOVE}, {@link #ACTION_UP}, or - * {@link #ACTION_CANCEL}. + * {@link #ACTION_CANCEL}. Consider using {@link #getActionMasked} + * and {@link #getActionIndex} to retrieve the separate masked action + * and pointer index. */ public final int getAction() { return mAction; } /** + * Return the masked action being performed, without pointer index + * information. May be any of the actions: {@link #ACTION_DOWN}, + * {@link #ACTION_MOVE}, {@link #ACTION_UP}, {@link #ACTION_CANCEL}, + * {@link #ACTION_POINTER_DOWN}, or {@link #ACTION_POINTER_UP}. + * Use {@link #getActionIndex} to return the index associated with + * pointer actions. + */ + public final int getActionMasked() { + return mAction & ACTION_MASK; + } + + /** + * For {@link #ACTION_POINTER_DOWN} or {@link #ACTION_POINTER_UP} + * as returned by {@link #getActionMasked}, this returns the associated + * pointer index. The index may be used with {@link #getPointerId(int)}, + * {@link #getX(int)}, {@link #getY(int)}, {@link #getPressure(int)}, + * and {@link #getSize(int)} to get information about the pointer that has + * gone down or up. + */ + public final int getActionIndex() { + return (mAction & ACTION_POINTER_INDEX_MASK) >> ACTION_POINTER_INDEX_SHIFT; + } + + /** * Returns the time (in ms) when the user originally pressed down to start * a stream of position events. */ diff --git a/core/java/android/view/SurfaceView.java b/core/java/android/view/SurfaceView.java index ca5e1de..d7f2539 100644 --- a/core/java/android/view/SurfaceView.java +++ b/core/java/android/view/SurfaceView.java @@ -19,6 +19,7 @@ package android.view; import com.android.internal.view.BaseIWindow; import android.content.Context; +import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.CompatibilityInfo.Translator; import android.graphics.Canvas; @@ -504,7 +505,7 @@ public class SurfaceView extends View { } public void resized(int w, int h, Rect coveredInsets, - Rect visibleInsets, boolean reportDraw) { + Rect visibleInsets, boolean reportDraw, Configuration newConfig) { SurfaceView surfaceView = mSurfaceView.get(); if (surfaceView != null) { if (localLOGV) Log.v( diff --git a/core/java/android/view/VelocityTracker.java b/core/java/android/view/VelocityTracker.java index 9581080..91fd6f1 100644 --- a/core/java/android/view/VelocityTracker.java +++ b/core/java/android/view/VelocityTracker.java @@ -61,6 +61,7 @@ public final class VelocityTracker implements Poolable<VelocityTracker> { float mYVelocity[] = new float[MotionEvent.BASE_AVAIL_POINTERS]; float mXVelocity[] = new float[MotionEvent.BASE_AVAIL_POINTERS]; + int mLastTouch; private VelocityTracker mNext; @@ -105,8 +106,11 @@ public final class VelocityTracker implements Poolable<VelocityTracker> { * Reset the velocity tracker back to its initial state. */ public void clear() { - for (int i = 0; i < MotionEvent.BASE_AVAIL_POINTERS; i++) { - mPastTime[i][0] = 0; + final long[][] pastTime = mPastTime; + for (int p = 0; p < MotionEvent.BASE_AVAIL_POINTERS; p++) { + for (int i = 0; i < NUM_PAST; i++) { + pastTime[p][i] = 0; + } } } @@ -120,55 +124,37 @@ public final class VelocityTracker implements Poolable<VelocityTracker> { * @param ev The MotionEvent you received and would like to track. */ public void addMovement(MotionEvent ev) { - long time = ev.getEventTime(); final int N = ev.getHistorySize(); final int pointerCount = ev.getPointerCount(); - for (int p = 0; p < pointerCount; p++) { - for (int i=0; i<N; i++) { - addPoint(p, ev.getHistoricalX(p, i), ev.getHistoricalY(p, i), - ev.getHistoricalEventTime(i)); + int touchIndex = (mLastTouch + 1) % NUM_PAST; + for (int i=0; i<N; i++) { + for (int id = 0; id < MotionEvent.BASE_AVAIL_POINTERS; id++) { + mPastTime[id][touchIndex] = 0; } - addPoint(p, ev.getX(p), ev.getY(p), time); - } - } - - private void addPoint(int pos, float x, float y, long time) { - int drop = -1; - int i; - if (localLOGV) Log.v(TAG, "Adding past y=" + y + " time=" + time); - final long[] pastTime = mPastTime[pos]; - for (i=0; i<NUM_PAST; i++) { - if (pastTime[i] == 0) { - break; - } else if (pastTime[i] < time-LONGEST_PAST_TIME) { - if (localLOGV) Log.v(TAG, "Dropping past too old at " - + i + " time=" + pastTime[i]); - drop = i; + for (int p = 0; p < pointerCount; p++) { + int id = ev.getPointerId(p); + mPastX[id][touchIndex] = ev.getHistoricalX(p, i); + mPastY[id][touchIndex] = ev.getHistoricalY(p, i); + mPastTime[id][touchIndex] = ev.getHistoricalEventTime(i); } + + touchIndex = (touchIndex + 1) % NUM_PAST; } - if (localLOGV) Log.v(TAG, "Add index: " + i); - if (i == NUM_PAST && drop < 0) { - drop = 0; - } - if (drop == i) drop--; - final float[] pastX = mPastX[pos]; - final float[] pastY = mPastY[pos]; - if (drop >= 0) { - if (localLOGV) Log.v(TAG, "Dropping up to #" + drop); - final int start = drop+1; - final int count = NUM_PAST-drop-1; - System.arraycopy(pastX, start, pastX, 0, count); - System.arraycopy(pastY, start, pastY, 0, count); - System.arraycopy(pastTime, start, pastTime, 0, count); - i -= (drop+1); + + // During calculation any pointer values with a time of 0 are treated + // as a break in input. Initialize all to 0 for each new touch index. + for (int id = 0; id < MotionEvent.BASE_AVAIL_POINTERS; id++) { + mPastTime[id][touchIndex] = 0; } - pastX[i] = x; - pastY[i] = y; - pastTime[i] = time; - i++; - if (i < NUM_PAST) { - pastTime[i] = 0; + final long time = ev.getEventTime(); + for (int p = 0; p < pointerCount; p++) { + int id = ev.getPointerId(p); + mPastX[id][touchIndex] = ev.getX(p); + mPastY[id][touchIndex] = ev.getY(p); + mPastTime[id][touchIndex] = time; } + + mLastTouch = touchIndex; } /** @@ -199,36 +185,43 @@ public final class VelocityTracker implements Poolable<VelocityTracker> { final float[] pastX = mPastX[pos]; final float[] pastY = mPastY[pos]; final long[] pastTime = mPastTime[pos]; - + final int lastTouch = mLastTouch; + + // find oldest acceptable time + int oldestTouch = lastTouch; + if (pastTime[lastTouch] > 0) { // cleared ? + final float acceptableTime = pastTime[lastTouch] - LONGEST_PAST_TIME; + int nextOldestTouch = (NUM_PAST + oldestTouch - 1) % NUM_PAST; + while (pastTime[nextOldestTouch] >= acceptableTime && + nextOldestTouch != lastTouch) { + oldestTouch = nextOldestTouch; + nextOldestTouch = (NUM_PAST + oldestTouch - 1) % NUM_PAST; + } + } + // Kind-of stupid. - final float oldestX = pastX[0]; - final float oldestY = pastY[0]; - final long oldestTime = pastTime[0]; + final float oldestX = pastX[oldestTouch]; + final float oldestY = pastY[oldestTouch]; + final long oldestTime = pastTime[oldestTouch]; float accumX = 0; float accumY = 0; - int N=0; - while (N < NUM_PAST) { - if (pastTime[N] == 0) { - break; - } - N++; - } + float N = (lastTouch - oldestTouch + NUM_PAST) % NUM_PAST + 1; // Skip the last received event, since it is probably pretty noisy. if (N > 3) N--; for (int i=1; i < N; i++) { - final int dur = (int)(pastTime[i] - oldestTime); + final int j = (oldestTouch + i) % NUM_PAST; + final int dur = (int)(pastTime[j] - oldestTime); if (dur == 0) continue; - float dist = pastX[i] - oldestX; + float dist = pastX[j] - oldestX; float vel = (dist/dur) * units; // pixels/frame. - if (accumX == 0) accumX = vel; - else accumX = (accumX + vel) * .5f; - - dist = pastY[i] - oldestY; + accumX = (accumX == 0) ? vel : (accumX + vel) * .5f; + + dist = pastY[j] - oldestY; vel = (dist/dur) * units; // pixels/frame. - if (accumY == 0) accumY = vel; - else accumY = (accumY + vel) * .5f; + accumY = (accumY == 0) ? vel : (accumY + vel) * .5f; } + mXVelocity[pos] = accumX < 0.0f ? Math.max(accumX, -maxVelocity) : Math.min(accumX, maxVelocity); mYVelocity[pos] = accumY < 0.0f ? Math.max(accumY, -maxVelocity) @@ -263,25 +256,25 @@ public final class VelocityTracker implements Poolable<VelocityTracker> { * Retrieve the last computed X velocity. You must first call * {@link #computeCurrentVelocity(int)} before calling this function. * - * @param pos Which pointer's velocity to return. + * @param id Which pointer's velocity to return. * @return The previously computed X velocity. * * @hide Pending API approval */ - public float getXVelocity(int pos) { - return mXVelocity[pos]; + public float getXVelocity(int id) { + return mXVelocity[id]; } /** * Retrieve the last computed Y velocity. You must first call * {@link #computeCurrentVelocity(int)} before calling this function. * - * @param pos Which pointer's velocity to return. + * @param id Which pointer's velocity to return. * @return The previously computed Y velocity. * * @hide Pending API approval */ - public float getYVelocity(int pos) { - return mYVelocity[pos]; + public float getYVelocity(int id) { + return mYVelocity[id]; } } diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index f5c465e..2eb633f 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -20,6 +20,7 @@ import com.android.internal.R; import com.android.internal.view.menu.MenuBuilder; import android.content.Context; +import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Bitmap; @@ -1504,6 +1505,31 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * @hide */ private static final int PREPRESSED = 0x02000000; + + /** + * Always allow a user to overscroll this view, provided it is a + * view that can scroll. + */ + private static final int OVERSCROLL_ALWAYS = 0; + + /** + * Allow a user to overscroll this view only if the content is large + * enough to meaningfully scroll, provided it is a view that can scroll. + */ + private static final int OVERSCROLL_IF_CONTENT_SCROLLS = 1; + + /** + * Never allow a user to overscroll this view. + */ + private static final int OVERSCROLL_NEVER = 2; + + /** + * Controls the overscroll mode for this view. + * See {@link #overscrollBy(int, int, int, int, int, int, int, int)}, + * {@link #OVERSCROLL_ALWAYS}, {@link #OVERSCROLL_IF_CONTENT_SCROLLS}, + * and {@link #OVERSCROLL_NEVER}. + */ + private int mOverscrollMode = OVERSCROLL_ALWAYS; /** * The parent this view is attached to. @@ -2053,6 +2079,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility }); } break; + case R.styleable.View_overscrollMode: + mOverscrollMode = a.getInt(attr, OVERSCROLL_ALWAYS); + break; } } @@ -3022,6 +3051,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * * @param enabled True if this view is enabled, false otherwise. */ + @RemotableViewMethod public void setEnabled(boolean enabled) { if (enabled == isEnabled()) return; @@ -3904,6 +3934,32 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility } /** + * Dispatch a notification about a resource configuration change down + * the view hierarchy. + * ViewGroups should override to route to their children. + * + * @param newConfig The new resource configuration. + * + * @see #onConfigurationChanged + */ + public void dispatchConfigurationChanged(Configuration newConfig) { + onConfigurationChanged(newConfig); + } + + /** + * Called when the current configuration of the resources being used + * by the application have changed. You can use this to decide when + * to reload resources that can changed based on orientation and other + * configuration characterstics. You only need to use this if you are + * not relying on the normal {@link android.app.Activity} mechanism of + * recreating the activity instance upon a configuration change. + * + * @param newConfig The new resource configuration. + */ + protected void onConfigurationChanged(Configuration newConfig) { + } + + /** * Private function to aggregate all per-view attributes in to the view * root. */ @@ -8281,6 +8337,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility * Cancels any animations for this view. */ public void clearAnimation() { + if (mCurrentAnimation != null) { + mCurrentAnimation.detach(); + } mCurrentAnimation = null; } @@ -8544,7 +8603,135 @@ public class View implements Drawable.Callback, KeyEvent.Callback, Accessibility LayoutInflater factory = LayoutInflater.from(context); return factory.inflate(resource, root); } + + /** + * Scroll the view with standard behavior for scrolling beyond the normal + * content boundaries. Views that call this method should override + * {@link #onOverscrolled(int, int, boolean, boolean)} to respond to the + * results of an overscroll operation. + * + * Views can use this method to handle any touch or fling-based scrolling. + * + * @param deltaX Change in X in pixels + * @param deltaY Change in Y in pixels + * @param scrollX Current X scroll value in pixels before applying deltaX + * @param scrollY Current Y scroll value in pixels before applying deltaY + * @param scrollRangeX Maximum content scroll range along the X axis + * @param scrollRangeY Maximum content scroll range along the Y axis + * @param maxOverscrollX Number of pixels to overscroll by in either direction + * along the X axis. + * @param maxOverscrollY Number of pixels to overscroll by in either direction + * along the Y axis. + * @return true if scrolling was clamped to an overscroll boundary along either + * axis, false otherwise. + */ + protected boolean overscrollBy(int deltaX, int deltaY, + int scrollX, int scrollY, + int scrollRangeX, int scrollRangeY, + int maxOverscrollX, int maxOverscrollY) { + final int overscrollMode = mOverscrollMode; + final boolean canScrollHorizontal = + computeHorizontalScrollRange() > computeHorizontalScrollExtent(); + final boolean canScrollVertical = + computeVerticalScrollRange() > computeVerticalScrollExtent(); + final boolean overscrollHorizontal = overscrollMode == OVERSCROLL_ALWAYS || + (overscrollMode == OVERSCROLL_IF_CONTENT_SCROLLS && canScrollHorizontal); + final boolean overscrollVertical = overscrollMode == OVERSCROLL_ALWAYS || + (overscrollMode == OVERSCROLL_IF_CONTENT_SCROLLS && canScrollVertical); + + int newScrollX = scrollX + deltaX; + if (overscrollHorizontal) { + // Scale the scroll amount if we're in the dropoff zone + final int dropoffX = maxOverscrollX / 2; + final int dropoffLeft = -dropoffX; + final int dropoffRight = dropoffX + scrollRangeX; + if ((scrollX < dropoffLeft && deltaX < 0) || + (scrollX > dropoffRight && deltaX > 0)) { + newScrollX = scrollX + deltaX / 2; + } else { + if (newScrollX > dropoffRight && deltaX > 0) { + int extra = newScrollX - dropoffRight; + newScrollX = dropoffRight + extra / 2; + } else if (newScrollX < dropoffLeft && deltaX < 0) { + int extra = newScrollX - dropoffLeft; + newScrollX = dropoffLeft + extra / 2; + } + } + } else { + maxOverscrollX = 0; + } + + int newScrollY = scrollY + deltaY; + if (overscrollVertical) { + final int dropoffY = maxOverscrollY / 2; + final int dropoffTop = -dropoffY; + final int dropoffBottom = dropoffY + scrollRangeY; + if ((scrollY < dropoffTop && deltaY < 0) || + (scrollY > dropoffBottom && deltaY > 0)) { + newScrollY = scrollY + deltaY / 2; + } else { + if (newScrollY > dropoffBottom && deltaY > 0) { + int extra = newScrollY - dropoffBottom; + newScrollY = dropoffBottom + extra / 2; + } else if (newScrollY < dropoffTop && deltaY < 0) { + int extra = newScrollY - dropoffTop; + newScrollY = dropoffTop + extra / 2; + } + } + } else { + maxOverscrollY = 0; + } + + // Clamp values if at the limits and record + final int left = -maxOverscrollX; + final int right = maxOverscrollX + scrollRangeX; + final int top = -maxOverscrollY; + final int bottom = maxOverscrollY + scrollRangeY; + + boolean clampedX = false; + if (newScrollX > right) { + newScrollX = right; + clampedX = true; + } else if (newScrollX < left) { + newScrollX = left; + clampedX = true; + } + + boolean clampedY = false; + if (newScrollY > bottom) { + newScrollY = bottom; + clampedY = true; + } else if (newScrollY < top) { + newScrollY = top; + clampedY = true; + } + + // Bump the device with some haptic feedback if we're at the edge + // and didn't start there. + if ((overscrollHorizontal && clampedX && scrollX != left && scrollX != right) || + (overscrollVertical && clampedY && scrollY != top && scrollY != bottom)) { + performHapticFeedback(HapticFeedbackConstants.SCROLL_BARRIER); + } + onOverscrolled(newScrollX, newScrollY, clampedX, clampedY); + + return clampedX || clampedY; + } + + /** + * Called by {@link #overscrollBy(int, int, int, int, int, int, int, int)} to + * respond to the results of an overscroll operation. + * + * @param scrollX New X scroll value in pixels + * @param scrollY New Y scroll value in pixels + * @param clampedX True if scrollX was clamped to an overscroll boundary + * @param clampedY True if scrollY was clamped to an overscroll boundary + */ + protected void onOverscrolled(int scrollX, int scrollY, + boolean clampedX, boolean clampedY) { + // Intentionally empty. + } + /** * A MeasureSpec encapsulates the layout requirements passed from parent to child. * Each MeasureSpec represents a requirement for either the width or the height. diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index cdf9eb0..0663215 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -19,6 +19,7 @@ package android.view; import com.android.internal.R; import android.content.Context; +import android.content.res.Configuration; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Canvas; @@ -722,6 +723,19 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager /** * {@inheritDoc} */ + @Override + public void dispatchConfigurationChanged(Configuration newConfig) { + super.dispatchConfigurationChanged(newConfig); + final int count = mChildrenCount; + final View[] children = mChildren; + for (int i = 0; i < count; i++) { + children[i].dispatchConfigurationChanged(newConfig); + } + } + + /** + * {@inheritDoc} + */ public void recomputeViewAttributes(View child) { ViewParent parent = mParent; if (parent != null) parent.recomputeViewAttributes(this); @@ -3398,7 +3412,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager /** * Special value for the height or width requested by a View. - * MATCH_PARENT means that the view wants to be as bigas its parent, + * MATCH_PARENT means that the view wants to be as big as its parent, * minus the parent's padding, if any. */ public static final int MATCH_PARENT = -1; diff --git a/core/java/android/view/ViewRoot.java b/core/java/android/view/ViewRoot.java index 07b2d1c..264b8c9 100644 --- a/core/java/android/view/ViewRoot.java +++ b/core/java/android/view/ViewRoot.java @@ -41,7 +41,9 @@ import android.view.inputmethod.InputMethodManager; import android.widget.Scroller; import android.content.pm.PackageManager; import android.content.res.CompatibilityInfo; +import android.content.res.Configuration; import android.content.res.Resources; +import android.content.ComponentCallbacks; import android.content.Context; import android.app.ActivityManagerNative; import android.Manifest; @@ -101,6 +103,9 @@ public final class ViewRoot extends Handler implements ViewParent, static final ArrayList<Runnable> sFirstDrawHandlers = new ArrayList<Runnable>(); static boolean sFirstDrawComplete = false; + static final ArrayList<ComponentCallbacks> sConfigCallbacks + = new ArrayList<ComponentCallbacks>(); + private static int sDrawTime; long mLastTrackballTime = 0; @@ -171,6 +176,12 @@ public final class ViewRoot extends Handler implements ViewParent, final ViewTreeObserver.InternalInsetsInfo mLastGivenInsets = new ViewTreeObserver.InternalInsetsInfo(); + class ResizedInfo { + Rect coveredInsets; + Rect visibleInsets; + Configuration newConfig; + } + boolean mScrollMayChange; int mSoftInputMode; View mLastScrolledFocus; @@ -265,6 +276,12 @@ public final class ViewRoot extends Handler implements ViewParent, } } + public static void addConfigCallback(ComponentCallbacks callback) { + synchronized (sConfigCallbacks) { + sConfigCallbacks.add(callback); + } + } + // FIXME for perf testing only private boolean mProfile = false; @@ -1782,23 +1799,33 @@ public final class ViewRoot extends Handler implements ViewParent, handleGetNewSurface(); break; case RESIZED: - Rect coveredInsets = ((Rect[])msg.obj)[0]; - Rect visibleInsets = ((Rect[])msg.obj)[1]; + ResizedInfo ri = (ResizedInfo)msg.obj; if (mWinFrame.width() == msg.arg1 && mWinFrame.height() == msg.arg2 - && mPendingContentInsets.equals(coveredInsets) - && mPendingVisibleInsets.equals(visibleInsets)) { + && mPendingContentInsets.equals(ri.coveredInsets) + && mPendingVisibleInsets.equals(ri.visibleInsets)) { break; } // fall through... case RESIZED_REPORT: if (mAdded) { + Configuration config = ((ResizedInfo)msg.obj).newConfig; + if (config != null) { + synchronized (sConfigCallbacks) { + for (int i=sConfigCallbacks.size()-1; i>=0; i--) { + sConfigCallbacks.get(i).onConfigurationChanged(config); + } + } + if (mView != null) { + mView.dispatchConfigurationChanged(config); + } + } mWinFrame.left = 0; mWinFrame.right = msg.arg1; mWinFrame.top = 0; mWinFrame.bottom = msg.arg2; - mPendingContentInsets.set(((Rect[])msg.obj)[0]); - mPendingVisibleInsets.set(((Rect[])msg.obj)[1]); + mPendingContentInsets.set(((ResizedInfo)msg.obj).coveredInsets); + mPendingVisibleInsets.set(((ResizedInfo)msg.obj).visibleInsets); if (msg.what == RESIZED_REPORT) { mReportNextDraw = true; } @@ -2587,7 +2614,7 @@ public final class ViewRoot extends Handler implements ViewParent, } public void dispatchResized(int w, int h, Rect coveredInsets, - Rect visibleInsets, boolean reportDraw) { + Rect visibleInsets, boolean reportDraw, Configuration newConfig) { if (DEBUG_LAYOUT) Log.v(TAG, "Resizing " + this + ": w=" + w + " h=" + h + " coveredInsets=" + coveredInsets.toShortString() + " visibleInsets=" + visibleInsets.toShortString() @@ -2601,7 +2628,11 @@ public final class ViewRoot extends Handler implements ViewParent, } msg.arg1 = w; msg.arg2 = h; - msg.obj = new Rect[] { new Rect(coveredInsets), new Rect(visibleInsets) }; + ResizedInfo ri = new ResizedInfo(); + ri.coveredInsets = new Rect(coveredInsets); + ri.visibleInsets = new Rect(visibleInsets); + ri.newConfig = newConfig; + msg.obj = ri; sendMessage(msg); } @@ -2802,11 +2833,11 @@ public final class ViewRoot extends Handler implements ViewParent, } public void resized(int w, int h, Rect coveredInsets, - Rect visibleInsets, boolean reportDraw) { + Rect visibleInsets, boolean reportDraw, Configuration newConfig) { final ViewRoot viewRoot = mViewRoot.get(); if (viewRoot != null) { viewRoot.dispatchResized(w, h, coveredInsets, - visibleInsets, reportDraw); + visibleInsets, reportDraw, newConfig); } } diff --git a/core/java/android/view/WindowManagerPolicy.java b/core/java/android/view/WindowManagerPolicy.java index 7bc5cce..ab3260e 100644 --- a/core/java/android/view/WindowManagerPolicy.java +++ b/core/java/android/view/WindowManagerPolicy.java @@ -851,6 +851,11 @@ public interface WindowManagerPolicy { */ public boolean isCheekPressedAgainstScreen(MotionEvent ev); + /** + * Called every time the window manager is dispatching a pointer event. + */ + public void dispatchedPointerEventLw(MotionEvent ev, int targetX, int targetY); + public void setCurrentOrientationLw(int newOrientation); /** diff --git a/core/java/android/view/animation/Animation.java b/core/java/android/view/animation/Animation.java index c8396c4..349b7e5 100644 --- a/core/java/android/view/animation/Animation.java +++ b/core/java/android/view/animation/Animation.java @@ -256,6 +256,37 @@ public abstract class Animation implements Cloneable { } /** + * Cancel the animation. Cancelling an animation invokes the animation + * listener, if set, to notify the end of the animation. + * + * If you cancel an animation manually, you must call {@link #reset()} + * before starting the animation again. + * + * @see #reset() + * @see #start() + * @see #startNow() + */ + public void cancel() { + if (mStarted && !mEnded) { + if (mListener != null) mListener.onAnimationEnd(this); + mEnded = true; + } + // Make sure we move the animation to the end + mStartTime = Long.MIN_VALUE; + mMore = mOneMoreTime = false; + } + + /** + * @hide + */ + public void detach() { + if (mStarted && !mEnded) { + mEnded = true; + if (mListener != null) mListener.onAnimationEnd(this); + } + } + + /** * Whether or not the animation has been initialized. * * @return Has this animation been initialized. @@ -745,10 +776,10 @@ public abstract class Animation implements Cloneable { if (expired) { if (mRepeatCount == mRepeated) { if (!mEnded) { + mEnded = true; if (mListener != null) { mListener.onAnimationEnd(this); } - mEnded = true; } } else { if (mRepeatCount > 0) { @@ -880,7 +911,7 @@ public abstract class Animation implements Cloneable { region.inset(-1.0f, -1.0f); if (mFillBefore) { final Transformation previousTransformation = mPreviousTransformation; - applyTransformation(0.0f, previousTransformation); + applyTransformation(mInterpolator.getInterpolation(0.0f), previousTransformation); } } diff --git a/core/java/android/view/animation/AnimationSet.java b/core/java/android/view/animation/AnimationSet.java index 98b2594..1546dcd 100644 --- a/core/java/android/view/animation/AnimationSet.java +++ b/core/java/android/view/animation/AnimationSet.java @@ -282,7 +282,9 @@ public class AnimationSet extends Animation { final Animation a = animations.get(i); temp.clear(); - a.applyTransformation(0.0f, temp); + final Interpolator interpolator = a.mInterpolator; + a.applyTransformation(interpolator != null ? interpolator.getInterpolation(0.0f) + : 0.0f, temp); previousTransformation.compose(temp); } } diff --git a/core/java/android/webkit/DateSorter.java b/core/java/android/webkit/DateSorter.java index 16feaa9..bc13504 100644 --- a/core/java/android/webkit/DateSorter.java +++ b/core/java/android/webkit/DateSorter.java @@ -26,7 +26,7 @@ import java.util.Date; * Sorts dates into the following groups: * Today * Yesterday - * five days ago + * seven days ago * one month ago * older than a month ago */ @@ -41,7 +41,7 @@ public class DateSorter { private long [] mBins = new long[DAY_COUNT-1]; private String [] mLabels = new String[DAY_COUNT]; - private static final int NUM_DAYS_AGO = 5; + private static final int NUM_DAYS_AGO = 7; /** * @param context Application context diff --git a/core/java/android/webkit/EventLogTags.logtags b/core/java/android/webkit/EventLogTags.logtags new file mode 100644 index 0000000..082a437 --- /dev/null +++ b/core/java/android/webkit/EventLogTags.logtags @@ -0,0 +1,11 @@ +# See system/core/logcat/event.logtags for a description of the format of this file. + +option java_package android.webkit; + +# browser stats for diary study +70101 browser_zoom_level_change (start level|1|5),(end level|1|5),(time|2|3) +70102 browser_double_tap_duration (duration|1|3),(time|2|3) +# 70103- used by the browser app itself + +70150 browser_snap_center +70151 browser_text_size_change (oldSize|1|5), (newSize|1|5) diff --git a/core/java/android/webkit/GeolocationPermissions.java b/core/java/android/webkit/GeolocationPermissions.java index d12d828..4565b75 100755 --- a/core/java/android/webkit/GeolocationPermissions.java +++ b/core/java/android/webkit/GeolocationPermissions.java @@ -23,6 +23,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; +import java.util.Vector; /** @@ -61,11 +62,8 @@ public final class GeolocationPermissions { private Handler mHandler; private Handler mUIHandler; - // Members used to transfer the origins and permissions between threads. - private Set<String> mOrigins; - private boolean mAllowed; - private Set<String> mOriginsToClear; - private Set<String> mOriginsToAllow; + // A queue to store messages until the handler is ready. + private Vector<Message> mQueuedMessages; // Message ids static final int GET_ORIGINS = 0; @@ -126,7 +124,7 @@ public final class GeolocationPermissions { * Creates the message handler. Must be called on the WebKit thread. * @hide */ - public void createHandler() { + public synchronized void createHandler() { if (mHandler == null) { mHandler = new Handler() { @Override @@ -134,21 +132,21 @@ public final class GeolocationPermissions { // Runs on the WebKit thread. switch (msg.what) { case GET_ORIGINS: { - getOriginsImpl(); + Set origins = nativeGetOrigins(); ValueCallback callback = (ValueCallback) msg.obj; Map values = new HashMap<String, Object>(); values.put(CALLBACK, callback); - values.put(ORIGINS, mOrigins); + values.put(ORIGINS, origins); postUIMessage(Message.obtain(null, RETURN_ORIGINS, values)); } break; case GET_ALLOWED: { Map values = (Map) msg.obj; String origin = (String) values.get(ORIGIN); ValueCallback callback = (ValueCallback) values.get(CALLBACK); - getAllowedImpl(origin); + boolean allowed = nativeGetAllowed(origin); Map retValues = new HashMap<String, Object>(); retValues.put(CALLBACK, callback); - retValues.put(ALLOWED, new Boolean(mAllowed)); + retValues.put(ALLOWED, new Boolean(allowed)); postUIMessage(Message.obtain(null, RETURN_ALLOWED, retValues)); } break; case CLEAR: @@ -164,15 +162,12 @@ public final class GeolocationPermissions { } }; - if (mOriginsToClear != null) { - for (String origin : mOriginsToClear) { - nativeClear(origin); - } - } - if (mOriginsToAllow != null) { - for (String origin : mOriginsToAllow) { - nativeAllow(origin); + // Handle the queued messages + if (mQueuedMessages != null) { + while (!mQueuedMessages.isEmpty()) { + mHandler.sendMessage(mQueuedMessages.remove(0)); } + mQueuedMessages = null; } } } @@ -180,9 +175,15 @@ public final class GeolocationPermissions { /** * Utility function to send a message to our handler. */ - private void postMessage(Message msg) { - assert(mHandler != null); - mHandler.sendMessage(msg); + private synchronized void postMessage(Message msg) { + if (mHandler == null) { + if (mQueuedMessages == null) { + mQueuedMessages = new Vector<Message>(); + } + mQueuedMessages.add(msg); + } else { + mHandler.sendMessage(msg); + } } /** @@ -207,8 +208,8 @@ public final class GeolocationPermissions { public void getOrigins(ValueCallback<Set<String> > callback) { if (callback != null) { if (WebViewCore.THREAD_NAME.equals(Thread.currentThread().getName())) { - getOriginsImpl(); - callback.onReceiveValue(mOrigins); + Set origins = nativeGetOrigins(); + callback.onReceiveValue(origins); } else { postMessage(Message.obtain(null, GET_ORIGINS, callback)); } @@ -216,14 +217,6 @@ public final class GeolocationPermissions { } /** - * Helper method to get the set of origins. - */ - private void getOriginsImpl() { - // Called on the WebKit thread. - mOrigins = nativeGetOrigins(); - } - - /** * Gets the permission state for the specified origin. * * Callback is a ValueCallback object whose onReceiveValue method will be @@ -238,8 +231,8 @@ public final class GeolocationPermissions { return; } if (WebViewCore.THREAD_NAME.equals(Thread.currentThread().getName())) { - getAllowedImpl(origin); - callback.onReceiveValue(new Boolean(mAllowed)); + boolean allowed = nativeGetAllowed(origin); + callback.onReceiveValue(new Boolean(allowed)); } else { Map values = new HashMap<String, Object>(); values.put(ORIGIN, origin); @@ -249,31 +242,13 @@ public final class GeolocationPermissions { } /** - * Helper method to get the permission state for the specified origin. - */ - private void getAllowedImpl(String origin) { - // Called on the WebKit thread. - mAllowed = nativeGetAllowed(origin); - } - - /** * Clears the permission state for the specified origin. This method may be * called before the WebKit thread has intialized the message handler. * Messages will be queued until this time. */ public void clear(String origin) { // Called on the UI thread. - if (mHandler == null) { - if (mOriginsToClear == null) { - mOriginsToClear = new HashSet<String>(); - } - mOriginsToClear.add(origin); - if (mOriginsToAllow != null) { - mOriginsToAllow.remove(origin); - } - } else { - postMessage(Message.obtain(null, CLEAR, origin)); - } + postMessage(Message.obtain(null, CLEAR, origin)); } /** @@ -283,17 +258,7 @@ public final class GeolocationPermissions { */ public void allow(String origin) { // Called on the UI thread. - if (mHandler == null) { - if (mOriginsToAllow == null) { - mOriginsToAllow = new HashSet<String>(); - } - mOriginsToAllow.add(origin); - if (mOriginsToClear != null) { - mOriginsToClear.remove(origin); - } - } else { - postMessage(Message.obtain(null, ALLOW, origin)); - } + postMessage(Message.obtain(null, ALLOW, origin)); } /** diff --git a/core/java/android/webkit/GoogleLocationSettingManager.java b/core/java/android/webkit/GoogleLocationSettingManager.java deleted file mode 100644 index ecac70a..0000000 --- a/core/java/android/webkit/GoogleLocationSettingManager.java +++ /dev/null @@ -1,209 +0,0 @@ -/* - * Copyright (C) 2009 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.webkit; - -import android.content.ContentResolver; -import android.content.Context; -import android.content.SharedPreferences; -import android.content.SharedPreferences.Editor; -import android.database.ContentObserver; -import android.os.Handler; -import android.preference.PreferenceManager; -import android.provider.Settings; - -import java.util.HashSet; - -/** - * A class to manage the interaction between the system setting 'Location & - * Security - Share with Google' and the browser. When this setting is set - * to true, we allow Geolocation for Google origins. When this setting is - * set to false, we clear Geolocation permissions for Google origins. - */ -class GoogleLocationSettingManager { - // The observer used to listen to the system setting. - private GoogleLocationSettingObserver mSettingObserver; - - // The value of the system setting that indicates true. - private final static int sSystemSettingTrue = 1; - // The value of the system setting that indicates false. - private final static int sSystemSettingFalse = 0; - // The value of the USE_LOCATION_FOR_SERVICES system setting last read - // by the browser. - private final static String LAST_READ_USE_LOCATION_FOR_SERVICES = - "lastReadUseLocationForServices"; - // The Browser package name. - private static final String BROWSER_PACKAGE_NAME = "com.android.browser"; - // The Google origins we consider. - private static HashSet<String> sGoogleOrigins; - static { - sGoogleOrigins = new HashSet<String>(); - // NOTE: DO NOT ADD A "/" AT THE END! - sGoogleOrigins.add("http://www.google.com"); - sGoogleOrigins.add("http://www.google.co.uk"); - } - - private static GoogleLocationSettingManager sGoogleLocationSettingManager = null; - private static int sRefCount = 0; - - static GoogleLocationSettingManager getInstance() { - if (sGoogleLocationSettingManager == null) { - sGoogleLocationSettingManager = new GoogleLocationSettingManager(); - } - return sGoogleLocationSettingManager; - } - - private GoogleLocationSettingManager() {} - - /** - * Starts the manager. Checks whether the setting has changed and - * installs an observer to listen for future changes. - */ - public void start(Context context) { - // Are we running in the browser? - if (context == null || !BROWSER_PACKAGE_NAME.equals(context.getPackageName())) { - return; - } - // Increase the refCount - sRefCount++; - // Are we already registered? - if (mSettingObserver != null) { - return; - } - // Read and apply the settings if needed. - maybeApplySetting(context); - // Register to receive notifications when the system settings change. - mSettingObserver = new GoogleLocationSettingObserver(); - mSettingObserver.observe(context); - } - - /** - * Stops the manager. - */ - public void stop() { - // Are we already registered? - if (mSettingObserver == null) { - return; - } - if (--sRefCount == 0) { - mSettingObserver.doNotObserve(); - mSettingObserver = null; - } - } - /** - * Checks to see if the system setting has changed and if so, - * updates the Geolocation permissions accordingly. - * @param the Application context - */ - private void maybeApplySetting(Context context) { - int setting = getSystemSetting(context); - if (settingChanged(setting, context)) { - applySetting(setting); - } - } - - /** - * Gets the current system setting for 'Use location for Google services'. - * @param the Application context - * @return The system setting. - */ - private int getSystemSetting(Context context) { - return Settings.Secure.getInt(context.getContentResolver(), - Settings.Secure.USE_LOCATION_FOR_SERVICES, - sSystemSettingFalse); - } - - /** - * Determines whether the supplied setting has changed from the last - * value read by the browser. - * @param setting The setting. - * @param the Application context - * @return Whether the setting has changed from the last value read - * by the browser. - */ - private boolean settingChanged(int setting, Context context) { - SharedPreferences preferences = - PreferenceManager.getDefaultSharedPreferences(context); - // Default to false. If the system setting is false the first time it is ever read by the - // browser, there's nothing to do. - int lastReadSetting = sSystemSettingFalse; - lastReadSetting = preferences.getInt(LAST_READ_USE_LOCATION_FOR_SERVICES, - lastReadSetting); - - if (lastReadSetting == setting) { - return false; - } - - Editor editor = preferences.edit(); - editor.putInt(LAST_READ_USE_LOCATION_FOR_SERVICES, setting); - editor.commit(); - return true; - } - - /** - * Applies the supplied setting to the Geolocation permissions. - * @param setting The setting. - */ - private void applySetting(int setting) { - for (String origin : sGoogleOrigins) { - if (setting == sSystemSettingTrue) { - GeolocationPermissions.getInstance().allow(origin); - } else { - GeolocationPermissions.getInstance().clear(origin); - } - } - } - - /** - * This class implements an observer to listen for changes to the - * system setting. - */ - private class GoogleLocationSettingObserver extends ContentObserver { - private Context mContext; - - GoogleLocationSettingObserver() { - super(new Handler()); - } - - void observe(Context context) { - if (mContext != null) { - return; - } - ContentResolver resolver = context.getContentResolver(); - resolver.registerContentObserver(Settings.Secure.getUriFor( - Settings.Secure.USE_LOCATION_FOR_SERVICES), false, this); - mContext = context; - } - - void doNotObserve() { - if (mContext == null) { - return; - } - ContentResolver resolver = mContext.getContentResolver(); - resolver.unregisterContentObserver(this); - mContext = null; - } - - @Override - public void onChange(boolean selfChange) { - // This may come after the call to doNotObserve() above, - // so mContext may be null. - if (mContext != null) { - maybeApplySetting(mContext); - } - } - } -} diff --git a/core/java/android/webkit/JWebCoreJavaBridge.java b/core/java/android/webkit/JWebCoreJavaBridge.java index e496d97..9dc7079 100644 --- a/core/java/android/webkit/JWebCoreJavaBridge.java +++ b/core/java/android/webkit/JWebCoreJavaBridge.java @@ -21,6 +21,8 @@ import android.os.Handler; import android.os.Message; import android.util.Log; +import java.util.Set; + final class JWebCoreJavaBridge extends Handler { // Identifier for the timer message. private static final int TIMER_MESSAGE = 1; @@ -248,4 +250,7 @@ final class JWebCoreJavaBridge extends Handler { boolean reload); public native void setNetworkOnLine(boolean online); public native void setNetworkType(String type, String subtype); + public native void addPackageNames(Set<String> packageNames); + public native void addPackageName(String packageName); + public native void removePackageName(String packageName); } diff --git a/core/java/android/webkit/MimeTypeMap.java b/core/java/android/webkit/MimeTypeMap.java index a9d6ff6..3ed9851 100644 --- a/core/java/android/webkit/MimeTypeMap.java +++ b/core/java/android/webkit/MimeTypeMap.java @@ -368,6 +368,7 @@ public class MimeTypeMap { sMimeTypeMap.loadEntry("application/x-xcf", "xcf"); sMimeTypeMap.loadEntry("application/x-xfig", "fig"); sMimeTypeMap.loadEntry("application/xhtml+xml", "xhtml"); + sMimeTypeMap.loadEntry("audio/3gpp", "3gpp"); sMimeTypeMap.loadEntry("audio/basic", "snd"); sMimeTypeMap.loadEntry("audio/midi", "mid"); sMimeTypeMap.loadEntry("audio/midi", "midi"); diff --git a/core/java/android/webkit/WebSettings.java b/core/java/android/webkit/WebSettings.java index 35f1ac6..662be95 100644 --- a/core/java/android/webkit/WebSettings.java +++ b/core/java/android/webkit/WebSettings.java @@ -22,7 +22,7 @@ import android.content.pm.PackageManager; import android.os.Build; import android.os.Handler; import android.os.Message; -import android.provider.Checkin; +import android.util.EventLog; import java.lang.SecurityException; import java.util.Locale; @@ -501,8 +501,8 @@ public class WebSettings { */ public synchronized void setTextSize(TextSize t) { if (WebView.mLogEvent && mTextSize != t ) { - Checkin.updateStats(mContext.getContentResolver(), - Checkin.Stats.Tag.BROWSER_TEXT_SIZE_CHANGE, 1, 0.0); + EventLog.writeEvent(EventLogTags.BROWSER_TEXT_SIZE_CHANGE, + mTextSize.value, t.value); } mTextSize = t; postSync(); @@ -916,9 +916,12 @@ public class WebSettings { } /** - * Tell the WebView to block network image. This is only checked when - * getLoadsImagesAutomatically() is true. - * @param flag True if the WebView should block network image + * Tell the WebView to block network images. This is only checked when + * {@link #getLoadsImagesAutomatically} is true. If you set the value to + * false, images will automatically be loaded. Use this api to reduce + * bandwidth only. Use {@link #setBlockNetworkLoads} if possible. + * @param flag True if the WebView should block network images. + * @see #setBlockNetworkLoads */ public synchronized void setBlockNetworkImage(boolean flag) { if (mBlockNetworkImage != flag) { @@ -928,17 +931,21 @@ public class WebSettings { } /** - * Return true if the WebView will block network image. The default is false. - * @return True if the WebView blocks network image. + * Return true if the WebView will block network images. The default is + * false. + * @return True if the WebView blocks network images. */ public synchronized boolean getBlockNetworkImage() { return mBlockNetworkImage; } /** - * @hide - * Tell the WebView to block all network load requests. - * @param flag True if the WebView should block all network loads + * Tell the WebView to block all network load requests. If you set the + * value to false, you must call {@link android.webkit.WebView#reload} to + * fetch remote resources. This flag supercedes the value passed to + * {@link #setBlockNetworkImage}. + * @param flag True if the WebView should block all network loads. + * @see android.webkit.WebView#reload */ public synchronized void setBlockNetworkLoads(boolean flag) { if (mBlockNetworkLoads != flag) { @@ -948,9 +955,8 @@ public class WebSettings { } /** - * @hide - * Return true if the WebView will block all network loads. - * The default is false. + * 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() { @@ -1355,8 +1361,6 @@ public class WebSettings { junit.framework.Assert.assertTrue(frame.mNativeFrame != 0); } - GoogleLocationSettingManager.getInstance().start(mContext); - SharedPreferences sp = mContext.getSharedPreferences(PREF_FILE, Context.MODE_PRIVATE); if (mDoubleTapToastCount > 0) { @@ -1373,7 +1377,6 @@ public class WebSettings { */ /*package*/ synchronized void onDestroyed() { - GoogleLocationSettingManager.getInstance().stop(); } private int pin(int size) { diff --git a/core/java/android/webkit/WebStorage.java b/core/java/android/webkit/WebStorage.java index cf71a84..9314d7b 100644 --- a/core/java/android/webkit/WebStorage.java +++ b/core/java/android/webkit/WebStorage.java @@ -146,7 +146,7 @@ public final class WebStorage { * @hide * Message handler, webcore side */ - public void createHandler() { + public synchronized void createHandler() { if (mHandler == null) { mHandler = new Handler() { @Override @@ -342,7 +342,7 @@ public final class WebStorage { /** * Utility function to send a message to our handler */ - private void postMessage(Message msg) { + private synchronized void postMessage(Message msg) { if (mHandler != null) { mHandler.sendMessage(msg); } diff --git a/core/java/android/webkit/WebTextView.java b/core/java/android/webkit/WebTextView.java index 6d0be43..d1ad61f 100644 --- a/core/java/android/webkit/WebTextView.java +++ b/core/java/android/webkit/WebTextView.java @@ -116,7 +116,7 @@ import java.util.ArrayList; * @param webView The WebView that created this. */ /* package */ WebTextView(Context context, WebView webView) { - super(context); + super(context, null, com.android.internal.R.attr.webTextViewStyle); mWebView = webView; mMaxLength = -1; } @@ -145,6 +145,12 @@ import java.util.ArrayList; break; } + if (KeyEvent.KEYCODE_TAB == keyCode) { + if (down) { + onEditorAction(EditorInfo.IME_ACTION_NEXT); + } + return true; + } Spannable text = (Spannable) getText(); int oldLength = text.length(); // Normally the delete key's dom events are sent via onTextChanged. @@ -810,6 +816,9 @@ import java.util.ArrayList; int imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI | EditorInfo.IME_FLAG_NO_FULLSCREEN; switch (type) { + case 0: // NORMAL_TEXT_FIELD + imeOptions |= EditorInfo.IME_ACTION_GO; + break; case 1: // TEXT_AREA single = false; inputType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE @@ -819,6 +828,7 @@ import java.util.ArrayList; break; case 2: // PASSWORD inPassword = true; + imeOptions |= EditorInfo.IME_ACTION_GO; break; case 3: // SEARCH imeOptions |= EditorInfo.IME_ACTION_SEARCH; @@ -826,22 +836,25 @@ import java.util.ArrayList; case 4: // EMAIL // TYPE_TEXT_VARIATION_WEB_EDIT_TEXT prevents EMAIL_ADDRESS // from working, so exclude it for now. - inputType = EditorInfo.TYPE_CLASS_TEXT - | EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS; + imeOptions |= EditorInfo.IME_ACTION_GO; break; case 5: // NUMBER - inputType = EditorInfo.TYPE_CLASS_NUMBER; + inputType |= EditorInfo.TYPE_CLASS_NUMBER; + // Number and telephone do not have both a Tab key and an + // action, so set the action to NEXT + imeOptions |= EditorInfo.IME_ACTION_NEXT; break; case 6: // TELEPHONE - inputType = EditorInfo.TYPE_CLASS_PHONE; + inputType |= EditorInfo.TYPE_CLASS_PHONE; + imeOptions |= EditorInfo.IME_ACTION_NEXT; break; case 7: // URL - // TYPE_TEXT_VARIATION_WEB_EDIT_TEXT prevents URI - // from working, so exclude it for now. - inputType = EditorInfo.TYPE_CLASS_TEXT - | EditorInfo.TYPE_TEXT_VARIATION_URI; + // TYPE_TEXT_VARIATION_URI prevents Tab key from showing, so + // exclude it for now. + imeOptions |= EditorInfo.IME_ACTION_GO; break; default: + imeOptions |= EditorInfo.IME_ACTION_GO; break; } setHint(null); @@ -855,22 +868,6 @@ import java.util.ArrayList; mWebView.requestFormData(name, mNodePointer); } } - if (type != 3 /* SEARCH */) { - int action = mWebView.nativeTextFieldAction(); - switch (action) { - // Keep in sync with CachedRoot::ImeAction - case 0: // NEXT - imeOptions |= EditorInfo.IME_ACTION_NEXT; - break; - case 1: // GO - imeOptions |= EditorInfo.IME_ACTION_GO; - break; - case -1: // FAILURE - case 2: // DONE - imeOptions |= EditorInfo.IME_ACTION_DONE; - break; - } - } } mSingle = single; setMaxLength(maxLength); diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java index 6627973..adae0cb 100644 --- a/core/java/android/webkit/WebView.java +++ b/core/java/android/webkit/WebView.java @@ -26,6 +26,7 @@ import android.database.DataSetObserver; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; +import android.graphics.Interpolator; import android.graphics.Picture; import android.graphics.Point; import android.graphics.Rect; @@ -37,7 +38,6 @@ import android.os.Handler; import android.os.Message; import android.os.ServiceManager; import android.os.SystemClock; -import android.provider.Checkin; import android.text.IClipboard; import android.text.Selection; import android.text.Spannable; @@ -86,6 +86,7 @@ import java.util.ArrayList; import java.util.List; import java.util.HashMap; import java.util.Map; +import java.util.Set; import junit.framework.Assert; @@ -213,8 +214,6 @@ public class WebView extends AbsoluteLayout // true means redraw the screen all-the-time. Only with AUTO_REDRAW_HACK private boolean mAutoRedraw; private int mRootLayer; // C++ pointer to the root layer - private boolean mLayersHaveAnimations; - private EvaluateLayersAnimations mEvaluateThread; static final String LOGTAG = "webview"; @@ -622,8 +621,6 @@ public class WebView extends AbsoluteLayout private boolean mGotKeyDown; /* package */ static boolean mLogEvent = true; - private static final int EVENT_LOG_ZOOM_LEVEL_CHANGE = 70101; - private static final int EVENT_LOG_DOUBLE_TAP_DURATION = 70102; // for event log private long mLastTouchUpTime = 0; @@ -1105,7 +1102,7 @@ public class WebView extends AbsoluteLayout * methods may be called on a WebView after destroy. */ public void destroy() { - clearTextEntry(); + clearTextEntry(false); if (mWebViewCore != null) { // Set the handlers to null before destroying WebViewCore so no // more messages will be posted. @@ -1391,7 +1388,7 @@ public class WebView extends AbsoluteLayout arg.mUrl = url; arg.mExtraHeaders = extraHeaders; mWebViewCore.sendMessage(EventHub.LOAD_URL, arg); - clearTextEntry(); + clearTextEntry(false); } /** @@ -1420,7 +1417,7 @@ public class WebView extends AbsoluteLayout arg.mUrl = url; arg.mPostData = postData; mWebViewCore.sendMessage(EventHub.POST_URL, arg); - clearTextEntry(); + clearTextEntry(false); } else { loadUrl(url); } @@ -1476,7 +1473,7 @@ public class WebView extends AbsoluteLayout arg.mEncoding = encoding; arg.mFailUrl = failUrl; mWebViewCore.sendMessage(EventHub.LOAD_DATA, arg); - clearTextEntry(); + clearTextEntry(false); } /** @@ -1493,7 +1490,7 @@ public class WebView extends AbsoluteLayout * Reload the current url. */ public void reload() { - clearTextEntry(); + clearTextEntry(false); switchOutDrawHistory(); mWebViewCore.sendMessage(EventHub.RELOAD); } @@ -1580,7 +1577,7 @@ public class WebView extends AbsoluteLayout // null, and that will be the case mCertificate = null; if (steps != 0) { - clearTextEntry(); + clearTextEntry(false); mWebViewCore.sendMessage(EventHub.GO_BACK_FORWARD, steps, ignoreSnapshot ? 1 : 0); } @@ -1680,9 +1677,17 @@ public class WebView extends AbsoluteLayout && mWebTextView.hasFocus(); } - private void clearTextEntry() { + /** + * Remove the WebTextView. + * @param disableFocusController If true, send a message to webkit + * disabling the focus controller, so the caret stops blinking. + */ + private void clearTextEntry(boolean disableFocusController) { if (inEditingMode()) { mWebTextView.remove(); + if (disableFocusController) { + setFocusControllerInactive(); + } } } @@ -1716,7 +1721,7 @@ public class WebView extends AbsoluteLayout Log.w(LOGTAG, "This WebView doesn't support zoom."); return; } - clearTextEntry(); + clearTextEntry(false); if (getSettings().getBuiltInZoomControls()) { mZoomButtonsController.setVisible(true); } else { @@ -2906,6 +2911,45 @@ public class WebView extends AbsoluteLayout return mWebViewCore.getSettings(); } + /** + * Use this method to inform the webview about packages that are installed + * in the system. This information will be used by the + * navigator.isApplicationInstalled() API. + * @param packageNames is a set of package names that are known to be + * installed in the system. + * + * @hide not a public API + */ + public void addPackageNames(Set<String> packageNames) { + mWebViewCore.sendMessage(EventHub.ADD_PACKAGE_NAMES, packageNames); + } + + /** + * Use this method to inform the webview about single packages that are + * installed in the system. This information will be used by the + * navigator.isApplicationInstalled() API. + * @param packageName is the name of a package that is known to be + * installed in the system. + * + * @hide not a public API + */ + public void addPackageName(String packageName) { + mWebViewCore.sendMessage(EventHub.ADD_PACKAGE_NAME, packageName); + } + + /** + * Use this method to inform the webview about packages that are uninstalled + * in the system. This information will be used by the + * navigator.isApplicationInstalled() API. + * @param packageName is the name of a package that has been uninstalled in + * the system. + * + * @hide not a public API + */ + public void removePackageName(String packageName) { + mWebViewCore.sendMessage(EventHub.REMOVE_PACKAGE_NAME, packageName); + } + /** * Return the list of currently loaded plugins. * @return The list of currently loaded plugins. @@ -2972,8 +3016,16 @@ public class WebView extends AbsoluteLayout if (mTitleBar != null) { canvas.translate(0, (int) mTitleBar.getHeight()); } - if (mDragTrackerHandler == null || !mDragTrackerHandler.draw(canvas)) { + if (mDragTrackerHandler == null) { drawContent(canvas); + } else { + if (!mDragTrackerHandler.draw(canvas)) { + // sometimes the tracker doesn't draw, even though its active + drawContent(canvas); + } + if (mDragTrackerHandler.isFinished()) { + mDragTrackerHandler = null; + } } canvas.restoreToCount(saveCount); @@ -3036,14 +3088,11 @@ public class WebView extends AbsoluteLayout Rect vBox = contentToViewRect(contentBounds); Rect visibleRect = new Rect(); calcOurVisibleRect(visibleRect); - // The IME may have shown, resulting in the textfield being offscreen. - // If so, the textfield will be scrolled on screen, so treat it as - // though it is on screen. If it is on screen, place the WebTextView in - // its new place, accounting for our new scroll/zoom values. - InputMethodManager imm = InputMethodManager.peekInstance(); - if ((imm != null && imm.isActive(mWebTextView)) - || (allowIntersect ? Rect.intersects(visibleRect, vBox) - : visibleRect.contains(vBox))) { + // If the textfield is on screen, place the WebTextView in + // its new place, accounting for our new scroll/zoom values, + // and adjust its textsize. + if (allowIntersect ? Rect.intersects(visibleRect, vBox) + : visibleRect.contains(vBox)) { mWebTextView.setRect(vBox.left, vBox.top, vBox.width(), vBox.height()); mWebTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, @@ -3061,14 +3110,36 @@ public class WebView extends AbsoluteLayout } } + private static class Metrics { + int mScrollX; + int mScrollY; + int mWidth; + int mHeight; + float mScale; + } + + private Metrics getViewMetrics() { + Metrics metrics = new Metrics(); + metrics.mScrollX = mScrollX; + metrics.mScrollY = computeVerticalScrollOffset(); + metrics.mWidth = getWidth(); + metrics.mHeight = getHeight() - getVisibleTitleHeight(); + metrics.mScale = mActualScale; + return metrics; + } + private void drawLayers(Canvas canvas) { if (mRootLayer != 0) { - int scrollY = computeVerticalScrollOffset(); - int viewHeight = getHeight() - getVisibleTitleHeight(); + // Currently for each draw we compute the animation values; + // We may in the future decide to do that independently. + if (nativeEvaluateLayersAnimations(mRootLayer)) { + // If we have unfinished (or unstarted) animations, + // we ask for a repaint. + invalidate(); + } - nativeDrawLayers(mRootLayer, mScrollX, scrollY, - getWidth(), viewHeight, - mActualScale, canvas); + // We can now draw the layers. + nativeDrawLayers(mRootLayer, canvas); } } @@ -3156,7 +3227,15 @@ public class WebView extends AbsoluteLayout mWebViewCore.drawContentPicture(canvas, color, (animateZoom || mPreviewZoomOnly), animateScroll); - + boolean cursorIsInLayer = nativeCursorIsInLayer(); + if (drawCursorRing && !cursorIsInLayer) { + nativeDrawCursorRing(canvas); + } + // When the FindDialog is up, only draw the matches if we are not in + // the process of scrolling them into view. + if (mFindIsUp && !animateScroll) { + nativeDrawMatches(canvas); + } drawLayers(canvas); if (mNativeClass == 0) return; @@ -3179,12 +3258,7 @@ public class WebView extends AbsoluteLayout LONG_PRESS_TIMEOUT); } } - nativeDrawCursorRing(canvas); - } - // When the FindDialog is up, only draw the matches if we are not in - // the process of scrolling them into view. - if (mFindIsUp && !animateScroll) { - nativeDrawMatches(canvas); + if (cursorIsInLayer) nativeDrawCursorRing(canvas); } if (mFocusSizeChanged) { mFocusSizeChanged = false; @@ -3370,6 +3444,10 @@ public class WebView extends AbsoluteLayout text = ""; } mWebTextView.setTextAndKeepSelection(text); + InputMethodManager imm = InputMethodManager.peekInstance(); + if (imm != null && imm.isActive(mWebTextView)) { + imm.restartInput(mWebTextView); + } } mWebTextView.requestFocus(); } @@ -3405,40 +3483,6 @@ public class WebView extends AbsoluteLayout } /* - * This class runs the layers animations in their own thread, - * so that we do not slow down the UI. - */ - private class EvaluateLayersAnimations extends Thread { - boolean mRunning = true; - // delay corresponds to 40fps, no need to go faster. - int mDelay = 25; // in ms - public void run() { - while (mRunning) { - if (mLayersHaveAnimations && mRootLayer != 0) { - // updates is a C++ pointer to a Vector of AnimationValues - int updates = nativeEvaluateLayersAnimations(mRootLayer); - if (updates == 0) { - mRunning = false; - } - Message.obtain(mPrivateHandler, - WebView.IMMEDIATE_REPAINT_MSG_ID, - updates, 0).sendToTarget(); - } else { - mRunning = false; - } - try { - Thread.currentThread().sleep(mDelay); - } catch (InterruptedException e) { - mRunning = false; - } - } - } - public void cancel() { - mRunning = false; - } - } - - /* * 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. @@ -3720,6 +3764,7 @@ public class WebView extends AbsoluteLayout } return true; } + clearTextEntry(true); nativeSetFollowedLink(true); if (!mCallbackProxy.uiOverrideUrlLoading(nativeCursorText())) { mWebViewCore.sendMessage(EventHub.CLICK, data.mFrame, @@ -3800,7 +3845,7 @@ public class WebView extends AbsoluteLayout @Override protected void onDetachedFromWindow() { - clearTextEntry(); + clearTextEntry(false); super.onDetachedFromWindow(); // Clean up the zoom controller mZoomButtonsController.setVisible(false); @@ -4066,6 +4111,14 @@ public class WebView extends AbsoluteLayout private final float mMaxDY, mMaxDX; private float mCurrStretchY, mCurrStretchX; private int mSX, mSY; + private Interpolator mInterp; + private float[] mXY = new float[2]; + + // inner (non-state) classes can't have enums :( + private static final int DRAGGING_STATE = 0; + private static final int ANIMATING_STATE = 1; + private static final int FINISHED_STATE = 2; + private int mState; public DragTrackerHandler(float x, float y, DragTracker proxy) { mProxy = proxy; @@ -4090,6 +4143,7 @@ public class WebView extends AbsoluteLayout mMinDX = -viewLeft; mMaxDX = docRight - viewRight; + mState = DRAGGING_STATE; mProxy.onStartDrag(x, y); // ensure we buildBitmap at least once @@ -4112,6 +4166,12 @@ public class WebView extends AbsoluteLayout float sy = computeStretch(mStartY - y, mMinDY, mMaxDY); float sx = computeStretch(mStartX - x, mMinDX, mMaxDX); + if ((mSnapScrollMode & SNAP_X) != 0) { + sy = 0; + } else if ((mSnapScrollMode & SNAP_Y) != 0) { + sx = 0; + } + if (mCurrStretchX != sx || mCurrStretchY != sy) { mCurrStretchX = sx; mCurrStretchY = sy; @@ -4126,10 +4186,26 @@ public class WebView extends AbsoluteLayout } public void stopDrag() { + final int DURATION = 200; + int now = (int)SystemClock.uptimeMillis(); + mInterp = new Interpolator(2); + mXY[0] = mCurrStretchX; + mXY[1] = mCurrStretchY; + // float[] blend = new float[] { 0.5f, 0, 0.75f, 1 }; + float[] blend = new float[] { 0, 0.5f, 0.75f, 1 }; + mInterp.setKeyFrame(0, now, mXY, blend); + float[] zerozero = new float[] { 0, 0 }; + mInterp.setKeyFrame(1, now + DURATION, zerozero, null); + mState = ANIMATING_STATE; + if (DebugFlags.DRAG_TRACKER || DEBUG_DRAG_TRACKER) { - Log.d(DebugFlags.DRAG_TRACKER_LOGTAG, "----- stopDrag"); + Log.d(DebugFlags.DRAG_TRACKER_LOGTAG, "----- stopDrag, starting animation"); } - mProxy.onStopDrag(); + } + + // Call this after each draw. If it ruturns null, the tracker is done + public boolean isFinished() { + return mState == FINISHED_STATE; } private int hiddenHeightOfTitleBar() { @@ -4150,13 +4226,23 @@ public class WebView extends AbsoluteLayout if (mCurrStretchX != 0 || mCurrStretchY != 0) { int sx = getScrollX(); int sy = getScrollY() - hiddenHeightOfTitleBar(); - if (mSX != sx || mSY != sy) { buildBitmap(sx, sy); mSX = sx; mSY = sy; } + if (mState == ANIMATING_STATE) { + Interpolator.Result result = mInterp.timeToValues(mXY); + if (result == Interpolator.Result.FREEZE_END) { + mState = FINISHED_STATE; + return false; + } else { + mProxy.onStretchChange(mXY[0], mXY[1]); + invalidate(); + // fall through to the draw + } + } int count = canvas.save(Canvas.MATRIX_SAVE_FLAG); canvas.translate(sx, sy); mProxy.onDraw(canvas); @@ -4350,6 +4436,7 @@ public class WebView extends AbsoluteLayout ted.mX = viewToContentX((int) x + mScrollX); ted.mY = viewToContentY((int) y + mScrollY); ted.mEventTime = eventTime; + ted.mMetaState = ev.getMetaState(); mWebViewCore.sendMessage(EventHub.TOUCH_EVENT, ted); mLastSentTouchTime = eventTime; } @@ -4399,7 +4486,7 @@ public class WebView extends AbsoluteLayout mWebViewCore.sendMessage( EventHub.UPDATE_FRAME_CACHE_IF_LOADING); if (mLogEvent && eventTime - mLastTouchUpTime < 1000) { - EventLog.writeEvent(EVENT_LOG_DOUBLE_TAP_DURATION, + EventLog.writeEvent(EventLogTags.BROWSER_DOUBLE_TAP_DURATION, (eventTime - mLastTouchUpTime), eventTime); } } @@ -4600,7 +4687,6 @@ public class WebView extends AbsoluteLayout case MotionEvent.ACTION_UP: { if (mDragTrackerHandler != null) { mDragTrackerHandler.stopDrag(); - mDragTrackerHandler = null; } mLastTouchUpTime = eventTime; switch (mTouchMode) { @@ -4614,6 +4700,7 @@ public class WebView extends AbsoluteLayout ted.mX = viewToContentX((int) x + mScrollX); ted.mY = viewToContentY((int) y + mScrollY); ted.mEventTime = eventTime; + ted.mMetaState = ev.getMetaState(); mWebViewCore.sendMessage(EventHub.TOUCH_EVENT, ted); } else if (mFullScreenHolder == null) { doDoubleTap(); @@ -4712,7 +4799,6 @@ public class WebView extends AbsoluteLayout private void cancelTouch() { if (mDragTrackerHandler != null) { mDragTrackerHandler.stopDrag(); - mDragTrackerHandler = null; } // we also use mVelocityTracker == null to tell us that we are // not "moving around", so we can take the slower/prettier @@ -5337,11 +5423,8 @@ public class WebView extends AbsoluteLayout } private void doMotionUp(int contentX, int contentY) { - if (nativeMotionUp(contentX, contentY, mNavSlop)) { - if (mLogEvent) { - Checkin.updateStats(mContext.getContentResolver(), - Checkin.Stats.Tag.BROWSER_SNAP_CENTER, 1, 0.0); - } + if (mLogEvent && nativeMotionUp(contentX, contentY, mNavSlop)) { + EventLog.writeEvent(EventLogTags.BROWSER_SNAP_CENTER); } if (nativeHasCursorNode() && !nativeCursorIsTextInput()) { playSoundEffect(SoundEffectConstants.CLICK); @@ -5654,6 +5737,11 @@ public class WebView extends AbsoluteLayout ted.mX = viewToContentX((int) mLastTouchX + mScrollX); ted.mY = viewToContentY((int) mLastTouchY + mScrollY); ted.mEventTime = SystemClock.uptimeMillis(); + // metaState for long press is tricky. Should it be the state + // when the press started or when the press was released? Or + // some intermediary key state? For simplicity for now, we + // don't set it. + ted.mMetaState = 0; mWebViewCore.sendMessage(EventHub.TOUCH_EVENT, ted); } else if (mPreventDrag == PREVENT_DRAG_NO) { mTouchMode = TOUCH_DONE_MODE; @@ -5751,7 +5839,7 @@ public class WebView extends AbsoluteLayout // is necessary for page loads driven by webkit, and in // particular when the user was on a password field, so // the WebTextView was visible. - clearTextEntry(); + clearTextEntry(false); // update the zoom buttons as the scale can be changed if (getSettings().getBuiltInZoomControls()) { updateZoomButtonsEnabled(); @@ -5834,6 +5922,10 @@ public class WebView extends AbsoluteLayout } break; case UPDATE_TEXT_SELECTION_MSG_ID: + // If no textfield was in focus, and the user touched one, + // causing it to send this message, then WebTextView has not + // been set up yet. Rebuild it so it can set its selection. + rebuildWebTextView(); if (inEditingMode() && mWebTextView.isSameTextField(msg.arg1) && msg.arg2 == mTextGeneration) { @@ -5869,7 +5961,7 @@ public class WebView extends AbsoluteLayout } break; case CLEAR_TEXT_ENTRY: - clearTextEntry(); + clearTextEntry(false); break; case INVAL_RECT_MSG_ID: { Rect r = (Rect)msg.obj; @@ -5883,34 +5975,16 @@ public class WebView extends AbsoluteLayout break; } case IMMEDIATE_REPAINT_MSG_ID: { - int updates = msg.arg1; - if (updates != 0) { - // updates is a C++ pointer to a Vector of - // AnimationValues that we apply to the layers. - // The Vector is deallocated in nativeUpdateLayers(). - nativeUpdateLayers(updates); - } invalidate(); break; } case SET_ROOT_LAYER_MSG_ID: { int oldLayer = mRootLayer; mRootLayer = msg.arg1; + nativeSetRootLayer(mRootLayer); if (oldLayer > 0) { nativeDestroyLayer(oldLayer); } - if (mRootLayer == 0) { - mLayersHaveAnimations = false; - } - if (mEvaluateThread != null) { - mEvaluateThread.cancel(); - mEvaluateThread = null; - } - if (nativeLayersHaveAnimations(mRootLayer)) { - mLayersHaveAnimations = true; - mEvaluateThread = new EvaluateLayersAnimations(); - mEvaluateThread.start(); - } invalidate(); break; } @@ -6124,6 +6198,9 @@ public class WebView extends AbsoluteLayout // mContentHeight may not be updated yet y = Math.max(0, (Math.min(maxHeight, y + viewHeight) - viewHeight)); + // We need to take into account the visible title height + // when scrolling since y is an absolute view position. + y = Math.max(0, y - getVisibleTitleHeight()); scrollTo(x, y); } break; @@ -6491,8 +6568,7 @@ public class WebView extends AbsoluteLayout */ private void sendMoveMouseIfLatest(boolean removeFocus) { if (removeFocus) { - clearTextEntry(); - setFocusControllerInactive(); + clearTextEntry(true); } mWebViewCore.sendMessage(EventHub.SET_MOVE_MOUSE_IF_LATEST, cursorData()); @@ -6668,6 +6744,7 @@ public class WebView extends AbsoluteLayout /* package */ native boolean nativeCursorMatchesFocus(); private native boolean nativeCursorIntersects(Rect visibleRect); private native boolean nativeCursorIsAnchor(); + private native boolean nativeCursorIsInLayer(); private native boolean nativeCursorIsTextInput(); private native Point nativeCursorPosition(); private native String nativeCursorText(); @@ -6680,13 +6757,8 @@ public class WebView extends AbsoluteLayout private native void nativeDestroy(); private native void nativeDrawCursorRing(Canvas content); private native void nativeDestroyLayer(int layer); - private native int nativeEvaluateLayersAnimations(int layer); - private native boolean nativeLayersHaveAnimations(int layer); - private native void nativeUpdateLayers(int updates); - private native void nativeDrawLayers(int layer, - int scrollX, int scrollY, - int width, int height, - float scale, Canvas canvas); + private native boolean nativeEvaluateLayersAnimations(int layer); + private native void nativeDrawLayers(int layer, Canvas canvas); private native void nativeDrawMatches(Canvas canvas); private native void nativeDrawSelectionPointer(Canvas content, float scale, int x, int y, boolean extendSelection); @@ -6736,8 +6808,7 @@ public class WebView extends AbsoluteLayout private native void nativeSetFindIsUp(); private native void nativeSetFollowedLink(boolean followed); private native void nativeSetHeightCanMeasure(boolean measure); - // Returns a value corresponding to CachedFrame::ImeAction - /* package */ native int nativeTextFieldAction(); + private native void nativeSetRootLayer(int layer); private native int nativeTextGeneration(); // Never call this version except by updateCachedTextfield(String) - // we always want to pass in our generation number. diff --git a/core/java/android/webkit/WebViewCore.java b/core/java/android/webkit/WebViewCore.java index 9c91919..361ec56 100644 --- a/core/java/android/webkit/WebViewCore.java +++ b/core/java/android/webkit/WebViewCore.java @@ -515,7 +515,7 @@ final class WebViewCore { private native void nativeTouchUp(int touchGeneration, int framePtr, int nodePtr, int x, int y); - private native int nativeHandleTouchEvent(int action, int x, int y, long time); + private native int nativeHandleTouchEvent(int action, int x, int y, long time, int metaState); private native void nativeUpdateFrameCache(); @@ -735,6 +735,7 @@ final class WebViewCore { int mX; int mY; long mEventTime; + int mMetaState; } static class GeolocationPermissionsData { @@ -892,6 +893,11 @@ final class WebViewCore { static final int SET_NETWORK_TYPE = 183; + // navigator.isApplicationInstalled() + static final int ADD_PACKAGE_NAMES = 184; + static final int ADD_PACKAGE_NAME = 185; + static final int REMOVE_PACKAGE_NAME = 186; + // private message ids private static final int DESTROY = 200; @@ -1193,7 +1199,7 @@ final class WebViewCore { mWebView.mPrivateHandler, WebView.PREVENT_TOUCH_ID, ted.mAction, nativeHandleTouchEvent(ted.mAction, ted.mX, - ted.mY, ted.mEventTime)).sendToTarget(); + ted.mY, ted.mEventTime, ted.mMetaState)).sendToTarget(); break; } @@ -1363,6 +1369,33 @@ final class WebViewCore { case HIDE_FULLSCREEN: nativeFullScreenPluginHidden(msg.arg1); break; + + case ADD_PACKAGE_NAMES: + if (BrowserFrame.sJavaBridge == null) { + throw new IllegalStateException("No WebView " + + "has been created in this process!"); + } + BrowserFrame.sJavaBridge.addPackageNames( + (Set<String>) msg.obj); + break; + + case ADD_PACKAGE_NAME: + if (BrowserFrame.sJavaBridge == null) { + throw new IllegalStateException("No WebView " + + "has been created in this process!"); + } + BrowserFrame.sJavaBridge.addPackageName( + (String) msg.obj); + break; + + case REMOVE_PACKAGE_NAME: + if (BrowserFrame.sJavaBridge == null) { + throw new IllegalStateException("No WebView " + + "has been created in this process!"); + } + BrowserFrame.sJavaBridge.removePackageName( + (String) msg.obj); + break; } } }; diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java index 66a7631..a79bbee 100644 --- a/core/java/android/widget/AbsListView.java +++ b/core/java/android/widget/AbsListView.java @@ -32,7 +32,6 @@ import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; import android.util.AttributeSet; -import android.util.Log; import android.view.Gravity; import android.view.HapticFeedbackConstants; import android.view.KeyEvent; @@ -307,6 +306,11 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te * Handles one frame of a fling */ private FlingRunnable mFlingRunnable; + + /** + * Handles scrolling between positions within the list. + */ + private PositionScroller mPositionScroller; /** * The offset in pixels form the top of the AdapterView to the top @@ -373,6 +377,16 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te int mResurrectToPosition = INVALID_POSITION; private ContextMenuInfo mContextMenuInfo = null; + + /** + * Maximum distance to overscroll by + */ + private int mOverscrollMax; + + /** + * Content height divided by this is the overscroll limit. + */ + private static final int OVERSCROLL_LIMIT_DIVISOR = 3; /** * Used to request a layout when we changed touch mode @@ -1044,7 +1058,8 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te final int top = view.getTop(); int height = view.getHeight(); if (height > 0) { - return Math.max(firstPosition * 100 - (top * 100) / height, 0); + return Math.max(firstPosition * 100 - (top * 100) / height + + (int)((float)mScrollY / getHeight() * mItemCount * 100), 0); } } else { int index; @@ -1064,7 +1079,17 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te @Override protected int computeVerticalScrollRange() { - return mSmoothScrollbarEnabled ? Math.max(mItemCount * 100, 0) : mItemCount; + int result; + if (mSmoothScrollbarEnabled) { + result = Math.max(mItemCount * 100, 0); + if (mScrollY != 0) { + // Compensate for overscroll + result += Math.abs((int) ((float) mScrollY / getHeight() * mItemCount * 100)); + } + } else { + result = mItemCount; + } + return result; } @Override @@ -1125,6 +1150,8 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te mInLayout = true; layoutChildren(); mInLayout = false; + + mOverscrollMax = (b - t) / OVERSCROLL_LIMIT_DIVISOR; } /** @@ -1588,6 +1615,10 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te // let the fling runnable report it's new state which // should be idle mFlingRunnable.endFling(); + if (mScrollY != 0) { + mScrollY = 0; + invalidate(); + } } // Always hide the type filter dismissPopup(); @@ -1935,12 +1966,17 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } else { int touchMode = mTouchMode; if (touchMode == TOUCH_MODE_OVERSCROLL || touchMode == TOUCH_MODE_OVERFLING) { - mScrollY = 0; if (mFlingRunnable != null) { mFlingRunnable.endFling(); + + if (mScrollY != 0) { + mScrollY = 0; + invalidate(); + } } } } + mLastTouchMode = isInTouchMode ? TOUCH_MODE_ON : TOUCH_MODE_OFF; } @Override @@ -2052,9 +2088,11 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te if (motionView != null) { motionViewPrevTop = motionView.getTop(); } + // No need to do all this work if we're not going to move anyway + boolean atEdge = false; if (incrementalDeltaY != 0) { - trackMotionScroll(deltaY, incrementalDeltaY); + atEdge = trackMotionScroll(deltaY, incrementalDeltaY); } // Check to see if we have bumped into the scroll limit @@ -2063,11 +2101,13 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te // Check if the top of the motion view is where it is // supposed to be final int motionViewRealTop = motionView.getTop(); - final int motionViewNewTop = mMotionViewNewTop; - if (motionViewRealTop != motionViewNewTop) { + if (atEdge) { // Apply overscroll - mScrollY -= incrementalDeltaY - (motionViewRealTop - motionViewPrevTop); + int overscroll = -incrementalDeltaY - + (motionViewRealTop - motionViewPrevTop); + overscrollBy(0, overscroll, 0, mScrollY, 0, 0, + 0, getOverscrollMax()); mTouchMode = TOUCH_MODE_OVERSCROLL; invalidate(); } @@ -2111,7 +2151,8 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te mMotionPosition = motionPosition; } } else { - mScrollY -= incrementalDeltaY; + overscrollBy(0, -incrementalDeltaY, 0, mScrollY, 0, 0, + 0, getOverscrollMax()); invalidate(); } mLastY = y; @@ -2296,12 +2337,43 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te return true; } + + @Override + protected void onOverscrolled(int scrollX, int scrollY, + boolean clampedX, boolean clampedY) { + mScrollY = scrollY; + if (clampedY) { + // Velocity is broken by hitting the limit; don't start a fling off of this. + if (mVelocityTracker != null) { + mVelocityTracker.clear(); + } + } + } + + private int getOverscrollMax() { + final int childCount = getChildCount(); + if (childCount > 0) { + return Math.min(mOverscrollMax, + getChildAt(childCount - 1).getBottom() / OVERSCROLL_LIMIT_DIVISOR); + } else { + return mOverscrollMax; + } + } @Override public void draw(Canvas canvas) { super.draw(canvas); if (mFastScroller != null) { - mFastScroller.draw(canvas); + final int scrollY = mScrollY; + if (scrollY != 0) { + // Pin the fast scroll thumb to the top/bottom during overscroll. + int restoreCount = canvas.save(); + canvas.translate(0, (float) scrollY); + mFastScroller.draw(canvas); + canvas.restoreToCount(restoreCount); + } else { + mFastScroller.draw(canvas); + } } } @@ -2440,7 +2512,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } } } - + void startSpringback() { if (mScroller.springback(0, mScrollY, 0, 0, 0, 0)) { mTouchMode = TOUCH_MODE_OVERFLING; @@ -2448,19 +2520,33 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te post(this); } } - + void startOverfling(int initialVelocity) { mScroller.fling(0, mScrollY, 0, initialVelocity, 0, 0, 0, 0, 0, getHeight()); mTouchMode = TOUCH_MODE_OVERFLING; invalidate(); post(this); } - + + void startScroll(int distance, int duration) { + int initialY = distance < 0 ? Integer.MAX_VALUE : 0; + mLastFlingY = initialY; + mScroller.startScroll(0, initialY, 0, distance, duration); + mTouchMode = TOUCH_MODE_FLING; + post(this); + } + private void endFling() { mTouchMode = TOUCH_MODE_REST; + reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); clearScrollingCache(); + removeCallbacks(this); + + if (mPositionScroller != null) { + removeCallbacks(mPositionScroller); + } } public void run() { @@ -2503,22 +2589,24 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te delta = Math.max(-(getHeight() - mPaddingBottom - mPaddingTop - 1), delta); } - // Do something different on overscroll - offsetChildrenTopAndBottom() - trackMotionScroll(delta, delta); - // Check to see if we have bumped into the scroll limit View motionView = getChildAt(mMotionPosition - mFirstPosition); + int oldTop = 0; if (motionView != null) { - // Check if the top of the motion view is where it is - // supposed to be - if (motionView.getTop() != mMotionViewNewTop) { - float vel = scroller.getCurrVelocity(); - if (delta > 0) { - vel = -vel; - } - startOverfling(Math.round(vel)); - break; + oldTop = motionView.getTop(); + } + if (trackMotionScroll(delta, delta)) { + if (motionView != null) { + // Tweak the scroll for how far we overshot + int overshoot = -(delta - (motionView.getTop() - oldTop)); + overscrollBy(0, overshoot, 0, mScrollY, 0, 0, 0, getOverscrollMax()); } + float vel = scroller.getCurrVelocity(); + if (delta > 0) { + vel = -vel; + } + startOverfling(Math.round(vel)); + break; } if (more) { @@ -2541,9 +2629,14 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te case TOUCH_MODE_OVERFLING: { final OverScroller scroller = mScroller; if (scroller.computeScrollOffset()) { - mScrollY = scroller.getCurrY(); - invalidate(); - post(this); + final int scrollY = mScrollY; + final int deltaY = scroller.getCurrY() - scrollY; + if (overscrollBy(0, deltaY, 0, scrollY, 0, 0, 0, getOverscrollMax())) { + startSpringback(); + } else { + invalidate(); + post(this); + } } else { endFling(); } @@ -2553,6 +2646,285 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } } + + + class PositionScroller implements Runnable { + private static final int SCROLL_DURATION = 400; + + private static final int MOVE_DOWN_POS = 1; + private static final int MOVE_UP_POS = 2; + private static final int MOVE_DOWN_BOUND = 3; + private static final int MOVE_UP_BOUND = 4; + + private int mMode; + private int mTargetPos; + private int mBoundPos; + private int mLastSeenPos; + private int mScrollDuration; + private int mExtraScroll; + + PositionScroller() { + mExtraScroll = ViewConfiguration.get(mContext).getScaledFadingEdgeLength(); + } + + void start(int position) { + final int firstPos = mFirstPosition; + final int lastPos = firstPos + getChildCount() - 1; + + int viewTravelCount = 0; + if (position <= firstPos) { + viewTravelCount = firstPos - position + 1; + mMode = MOVE_UP_POS; + } else if (position >= lastPos) { + viewTravelCount = position - lastPos + 1; + mMode = MOVE_DOWN_POS; + } else { + // Already on screen, nothing to do + return; + } + + if (viewTravelCount > 0) { + mScrollDuration = SCROLL_DURATION / viewTravelCount; + } else { + mScrollDuration = SCROLL_DURATION; + } + mTargetPos = position; + mBoundPos = INVALID_POSITION; + mLastSeenPos = INVALID_POSITION; + + post(this); + } + + void start(int position, int boundPosition) { + if (boundPosition == INVALID_POSITION) { + start(position); + return; + } + + final int firstPos = mFirstPosition; + final int lastPos = firstPos + getChildCount() - 1; + + int viewTravelCount = 0; + if (position < firstPos) { + final int boundPosFromLast = lastPos - boundPosition; + if (boundPosFromLast < 1) { + // Moving would shift our bound position off the screen. Abort. + return; + } + + final int posTravel = firstPos - position + 1; + final int boundTravel = boundPosFromLast - 1; + if (boundTravel < posTravel) { + viewTravelCount = boundTravel; + mMode = MOVE_UP_BOUND; + } else { + viewTravelCount = posTravel; + mMode = MOVE_UP_POS; + } + } else if (position > lastPos) { + final int boundPosFromFirst = boundPosition - firstPos; + if (boundPosFromFirst < 1) { + // Moving would shift our bound position off the screen. Abort. + return; + } + + final int posTravel = position - lastPos + 1; + final int boundTravel = boundPosFromFirst - 1; + if (boundTravel < posTravel) { + viewTravelCount = boundTravel; + mMode = MOVE_DOWN_BOUND; + } else { + viewTravelCount = posTravel; + mMode = MOVE_DOWN_POS; + } + } else { + // Already on screen, nothing to do + return; + } + + if (viewTravelCount > 0) { + mScrollDuration = SCROLL_DURATION / viewTravelCount; + } else { + mScrollDuration = SCROLL_DURATION; + } + mTargetPos = position; + mBoundPos = boundPosition; + mLastSeenPos = INVALID_POSITION; + + post(this); + } + + void stop() { + removeCallbacks(this); + } + + public void run() { + final int listHeight = getHeight(); + final int firstPos = mFirstPosition; + + switch (mMode) { + case MOVE_DOWN_POS: { + final int lastViewIndex = getChildCount() - 1; + final int lastPos = firstPos + lastViewIndex; + + if (lastViewIndex < 0) { + return; + } + + if (lastPos == mLastSeenPos) { + // No new views, let things keep going. + post(this); + return; + } + + final View lastView = getChildAt(lastViewIndex); + final int lastViewHeight = lastView.getHeight(); + final int lastViewTop = lastView.getTop(); + final int lastViewPixelsShowing = listHeight - lastViewTop; + + smoothScrollBy(lastViewHeight - lastViewPixelsShowing + mExtraScroll, + mScrollDuration); + + mLastSeenPos = lastPos; + if (lastPos != mTargetPos) { + post(this); + } + break; + } + + case MOVE_DOWN_BOUND: { + final int nextViewIndex = 1; + if (firstPos == mBoundPos || getChildCount() <= nextViewIndex) { + return; + } + final int nextPos = firstPos + nextViewIndex; + + if (nextPos == mLastSeenPos) { + // No new views, let things keep going. + post(this); + return; + } + + final View nextView = getChildAt(nextViewIndex); + final int nextViewHeight = nextView.getHeight(); + final int nextViewTop = nextView.getTop(); + final int extraScroll = mExtraScroll; + if (nextPos != mBoundPos) { + smoothScrollBy(Math.max(0, nextViewHeight + nextViewTop - extraScroll), + mScrollDuration); + + mLastSeenPos = nextPos; + + post(this); + } else { + if (nextViewTop > extraScroll) { + smoothScrollBy(nextViewTop - extraScroll, mScrollDuration); + } + } + break; + } + + case MOVE_UP_POS: { + if (firstPos == mLastSeenPos) { + // No new views, let things keep going. + post(this); + return; + } + + final View firstView = getChildAt(0); + if (firstView == null) { + return; + } + final int firstViewTop = firstView.getTop(); + + smoothScrollBy(firstViewTop - mExtraScroll, mScrollDuration); + + mLastSeenPos = firstPos; + + if (firstPos != mTargetPos) { + post(this); + } + break; + } + + case MOVE_UP_BOUND: { + final int lastViewIndex = getChildCount() - 2; + if (lastViewIndex < 0) { + return; + } + final int lastPos = firstPos + lastViewIndex; + + if (lastPos == mLastSeenPos) { + // No new views, let things keep going. + post(this); + return; + } + + final View lastView = getChildAt(lastViewIndex); + final int lastViewHeight = lastView.getHeight(); + final int lastViewTop = lastView.getTop(); + final int lastViewPixelsShowing = listHeight - lastViewTop; + mLastSeenPos = lastPos; + if (lastPos != mBoundPos) { + smoothScrollBy(-(lastViewPixelsShowing - mExtraScroll), mScrollDuration); + post(this); + } else { + final int bottom = listHeight - mExtraScroll; + final int lastViewBottom = lastViewTop + lastViewHeight; + if (bottom > lastViewBottom) { + smoothScrollBy(-(bottom - lastViewBottom), mScrollDuration); + } + } + break; + } + + default: + break; + } + } + } + + /** + * Smoothly scroll to the specified adapter position. The view will + * scroll such that the indicated position is displayed. + * @param position Scroll to this adapter position. + */ + public void smoothScrollToPosition(int position) { + if (mPositionScroller == null) { + mPositionScroller = new PositionScroller(); + } + mPositionScroller.start(position); + } + + /** + * Smoothly scroll to the specified adapter position. The view will + * scroll such that the indicated position is displayed, but it will + * stop early if scrolling further would scroll boundPosition out of + * view. + * @param position Scroll to this adapter position. + * @param boundPosition Do not scroll if it would move this adapter + * position out of view. + */ + public void smoothScrollToPosition(int position, int boundPosition) { + if (mPositionScroller == null) { + mPositionScroller = new PositionScroller(); + } + mPositionScroller.start(position, boundPosition); + } + + /** + * Smoothly scroll by distance pixels over duration milliseconds. + * @param distance Distance to scroll in pixels. + * @param duration Duration of the scroll animation in milliseconds. + */ + public void smoothScrollBy(int distance, int duration) { + if (mFlingRunnable == null) { + mFlingRunnable = new FlingRunnable(); + } else { + mFlingRunnable.endFling(); + } + mFlingRunnable.startScroll(distance, duration); + } private void createScrollingCache() { if (mScrollingCacheEnabled && !mCachingStarted) { @@ -2588,11 +2960,12 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te * @param deltaY Amount to offset mMotionView. This is the accumulated delta since the motion * began. Positive numbers mean the user's finger is moving down the screen. * @param incrementalDeltaY Change in deltaY from the previous event. + * @return true if we're already at the beginning/end of the list and have nothing to do. */ - void trackMotionScroll(int deltaY, int incrementalDeltaY) { + boolean trackMotionScroll(int deltaY, int incrementalDeltaY) { final int childCount = getChildCount(); if (childCount == 0) { - return; + return true; } final int firstTop = getChildAt(0).getTop(); @@ -2618,98 +2991,99 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te incrementalDeltaY = Math.min(height - 1, incrementalDeltaY); } - final int absIncrementalDeltaY = Math.abs(incrementalDeltaY); - - if (spaceAbove >= absIncrementalDeltaY && spaceBelow >= absIncrementalDeltaY) { - hideSelector(); - offsetChildrenTopAndBottom(incrementalDeltaY); - if (!awakenScrollBars()) { - invalidate(); - } - mMotionViewNewTop = mMotionViewOriginalTop + deltaY; - } else { - final int firstPosition = mFirstPosition; + final int firstPosition = mFirstPosition; - if (firstPosition == 0 && firstTop >= listPadding.top && deltaY > 0) { - // Don't need to move views down if the top of the first position is already visible - return; - } + if (firstPosition == 0 && firstTop >= listPadding.top && deltaY > 0) { + // Don't need to move views down if the top of the first position + // is already visible + return true; + } - if (firstPosition + childCount == mItemCount && lastBottom <= end && deltaY < 0) { - // Don't need to move views up if the bottom of the last position is already visible - return; - } + if (firstPosition + childCount == mItemCount && lastBottom <= end && deltaY < 0) { + // Don't need to move views up if the bottom of the last position + // is already visible + return true; + } - final boolean down = incrementalDeltaY < 0; + final boolean down = incrementalDeltaY < 0; - hideSelector(); + hideSelector(); - final int headerViewsCount = getHeaderViewsCount(); - final int footerViewsStart = mItemCount - getFooterViewsCount(); + final int headerViewsCount = getHeaderViewsCount(); + final int footerViewsStart = mItemCount - getFooterViewsCount(); - int start = 0; - int count = 0; + int start = 0; + int count = 0; - if (down) { - final int top = listPadding.top - incrementalDeltaY; - for (int i = 0; i < childCount; i++) { - final View child = getChildAt(i); - if (child.getBottom() >= top) { - break; - } else { - count++; - int position = firstPosition + i; - if (position >= headerViewsCount && position < footerViewsStart) { - mRecycler.addScrapView(child); - - if (ViewDebug.TRACE_RECYCLER) { - ViewDebug.trace(child, - ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP, - firstPosition + i, -1); - } + if (down) { + final int top = listPadding.top - incrementalDeltaY; + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + if (child.getBottom() >= top) { + break; + } else { + count++; + int position = firstPosition + i; + if (position >= headerViewsCount && position < footerViewsStart) { + mRecycler.addScrapView(child); + + if (ViewDebug.TRACE_RECYCLER) { + ViewDebug.trace(child, + ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP, + firstPosition + i, -1); } } } - } else { - final int bottom = getHeight() - listPadding.bottom - incrementalDeltaY; - for (int i = childCount - 1; i >= 0; i--) { - final View child = getChildAt(i); - if (child.getTop() <= bottom) { - break; - } else { - start = i; - count++; - int position = firstPosition + i; - if (position >= headerViewsCount && position < footerViewsStart) { - mRecycler.addScrapView(child); - - if (ViewDebug.TRACE_RECYCLER) { - ViewDebug.trace(child, - ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP, - firstPosition + i, -1); - } + } + } else { + final int bottom = getHeight() - listPadding.bottom - incrementalDeltaY; + for (int i = childCount - 1; i >= 0; i--) { + final View child = getChildAt(i); + if (child.getTop() <= bottom) { + break; + } else { + start = i; + count++; + int position = firstPosition + i; + if (position >= headerViewsCount && position < footerViewsStart) { + mRecycler.addScrapView(child); + + if (ViewDebug.TRACE_RECYCLER) { + ViewDebug.trace(child, + ViewDebug.RecyclerTraceType.MOVE_TO_SCRAP_HEAP, + firstPosition + i, -1); } } } } + } + + mMotionViewNewTop = mMotionViewOriginalTop + deltaY; - mMotionViewNewTop = mMotionViewOriginalTop + deltaY; + mBlockLayoutRequests = true; - mBlockLayoutRequests = true; + if (count > 0) { detachViewsFromParent(start, count); - offsetChildrenTopAndBottom(incrementalDeltaY); + } + offsetChildrenTopAndBottom(incrementalDeltaY); - if (down) { - mFirstPosition += count; - } + if (down) { + mFirstPosition += count; + } - invalidate(); - fillGap(down); - mBlockLayoutRequests = false; + invalidate(); - invokeOnItemScrollListener(); - awakenScrollBars(); + final int absIncrementalDeltaY = Math.abs(incrementalDeltaY); + if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) { + fillGap(down); } + + mBlockLayoutRequests = false; + + invokeOnItemScrollListener(); + awakenScrollBars(); + + return false; } /** diff --git a/core/java/android/widget/DatePicker.java b/core/java/android/widget/DatePicker.java index 299ed8a..9ef5e0b 100644 --- a/core/java/android/widget/DatePicker.java +++ b/core/java/android/widget/DatePicker.java @@ -103,6 +103,18 @@ public class DatePicker extends FrameLayout { mMonthPicker.setFormatter(NumberPicker.TWO_DIGIT_FORMATTER); DateFormatSymbols dfs = new DateFormatSymbols(); String[] months = dfs.getShortMonths(); + + /* + * If the user is in a locale where the month names are numeric, + * use just the number instead of the "month" character for + * consistency with the other fields. + */ + if (months[0].startsWith("1")) { + for (int i = 0; i < months.length; i++) { + months[i] = String.valueOf(i + 1); + } + } + mMonthPicker.setRange(1, 12, months); mMonthPicker.setSpeed(200); mMonthPicker.setOnChangeListener(new OnChangedListener() { diff --git a/core/java/android/widget/EditText.java b/core/java/android/widget/EditText.java index 57aca24..1532db1 100644 --- a/core/java/android/widget/EditText.java +++ b/core/java/android/widget/EditText.java @@ -16,9 +16,13 @@ package android.widget; -import android.text.*; -import android.text.method.*; import android.content.Context; +import android.text.Editable; +import android.text.Selection; +import android.text.Spannable; +import android.text.TextUtils; +import android.text.method.ArrowKeyMovementMethod; +import android.text.method.MovementMethod; import android.util.AttributeSet; diff --git a/core/java/android/widget/ExpandableListView.java b/core/java/android/widget/ExpandableListView.java index 405461a..a4b20da 100644 --- a/core/java/android/widget/ExpandableListView.java +++ b/core/java/android/widget/ExpandableListView.java @@ -18,14 +18,12 @@ package android.widget; import com.android.internal.R; -import java.util.ArrayList; - import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Rect; -import android.graphics.drawable.Drawable; import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; import android.os.Parcel; import android.os.Parcelable; import android.util.AttributeSet; @@ -35,6 +33,8 @@ import android.view.View; import android.view.ContextMenu.ContextMenuInfo; import android.widget.ExpandableListConnector.PositionMetadata; +import java.util.ArrayList; + /** * A view that shows items in a vertically scrolling two-level list. This * differs from the {@link ListView} by allowing two levels: groups which can @@ -541,6 +541,12 @@ public class ExpandableListView extends ListView { if (mOnGroupExpandListener != null) { mOnGroupExpandListener.onGroupExpand(posMetadata.position.groupPos); } + + final int groupPos = posMetadata.position.groupPos; + final int groupFlatPos = posMetadata.position.flatListPos; + + smoothScrollToPosition(groupFlatPos + mAdapter.getChildrenCount(groupPos), + groupFlatPos); } returnValue = true; diff --git a/core/java/android/widget/GridView.java b/core/java/android/widget/GridView.java index 30a38df..b9acf5e 100644 --- a/core/java/android/widget/GridView.java +++ b/core/java/android/widget/GridView.java @@ -1856,8 +1856,11 @@ public class GridView extends AbsListView { final int top = view.getTop(); int height = view.getHeight(); if (height > 0) { - final int whichRow = mFirstPosition / mNumColumns; - return Math.max(whichRow * 100 - (top * 100) / height, 0); + final int numColumns = mNumColumns; + final int whichRow = mFirstPosition / numColumns; + final int rowCount = (mItemCount + numColumns - 1) / numColumns; + return Math.max(whichRow * 100 - (top * 100) / height + + (int) ((float) mScrollY / getHeight() * rowCount * 100), 0); } } return 0; @@ -1868,7 +1871,12 @@ public class GridView extends AbsListView { // TODO: Account for vertical spacing too final int numColumns = mNumColumns; final int rowCount = (mItemCount + numColumns - 1) / numColumns; - return Math.max(rowCount * 100, 0); + int result = Math.max(rowCount * 100, 0); + if (mScrollY != 0) { + // Compensate for overscroll + result += Math.abs((int) ((float) mScrollY / getHeight() * rowCount * 100)); + } + return result; } } diff --git a/core/java/android/widget/HorizontalScrollView.java b/core/java/android/widget/HorizontalScrollView.java index 0078fec..a7b819a 100644 --- a/core/java/android/widget/HorizontalScrollView.java +++ b/core/java/android/widget/HorizontalScrollView.java @@ -461,7 +461,8 @@ public class HorizontalScrollView extends FrameLayout { final int deltaX = (int) (mLastMotionX - x); mLastMotionX = x; - super.scrollTo(mScrollX + deltaX, mScrollY); + overscrollBy(deltaX, 0, mScrollX, 0, getScrollRange(), 0, + getOverscrollMax(), 0); break; case MotionEvent.ACTION_UP: final VelocityTracker velocityTracker = mVelocityTracker; @@ -472,8 +473,7 @@ public class HorizontalScrollView extends FrameLayout { if ((Math.abs(initialVelocity) > mMinimumVelocity)) { fling(-initialVelocity); } else { - final int right = Math.max(0, getChildAt(0).getHeight() - - (getHeight() - mPaddingRight - mPaddingLeft)); + final int right = getScrollRange(); if (mScroller.springback(mScrollX, mScrollY, 0, 0, right, 0)) { invalidate(); } @@ -487,6 +487,41 @@ public class HorizontalScrollView extends FrameLayout { } return true; } + + @Override + protected void onOverscrolled(int scrollX, int scrollY, + boolean clampedX, boolean clampedY) { + // Treat animating scrolls differently; see #computeScroll() for why. + if (!mScroller.isFinished()) { + mScrollX = scrollX; + mScrollY = scrollY; + if (clampedX) { + mScroller.springback(mScrollX, mScrollY, 0, getScrollRange(), 0, 0); + } + } else { + super.scrollTo(scrollX, scrollY); + } + } + + private int getOverscrollMax() { + int childCount = getChildCount(); + int containerOverscroll = (getWidth() - mPaddingLeft - mPaddingRight) / 3; + if (childCount > 0) { + return Math.min(containerOverscroll, getChildAt(0).getWidth() / 3); + } else { + return containerOverscroll; + } + } + + private int getScrollRange() { + int scrollRange = 0; + if (getChildCount() > 0) { + View child = getChildAt(0); + scrollRange = Math.max(0, + child.getWidth() - getWidth() - mPaddingLeft - mPaddingRight); + } + return scrollRange; + } /** * <p> @@ -855,10 +890,28 @@ public class HorizontalScrollView extends FrameLayout { */ @Override protected int computeHorizontalScrollRange() { - int count = getChildCount(); - return count == 0 ? getWidth() : getChildAt(0).getRight(); + final int count = getChildCount(); + final int contentWidth = getWidth() - mPaddingLeft - mPaddingRight; + if (count == 0) { + return contentWidth; + } + + int scrollRange = getChildAt(0).getRight(); + final int scrollX = mScrollX; + final int overscrollRight = Math.max(0, scrollRange - contentWidth); + if (scrollX < 0) { + scrollRange -= scrollX; + } else if (scrollX > overscrollRight) { + scrollRange += scrollX - overscrollRight; + } + + return scrollRange; + } + + @Override + protected int computeHorizontalScrollOffset() { + return Math.max(0, super.computeHorizontalScrollOffset()); } - @Override protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) { @@ -913,10 +966,9 @@ public class HorizontalScrollView extends FrameLayout { int x = mScroller.getCurrX(); int y = mScroller.getCurrY(); - mScrollX = x; - mScrollY = y; - - if (oldX != mScrollX || oldY != mScrollY) { + if (oldX != x || oldY != y) { + overscrollBy(x - oldX, y - oldY, oldX, oldY, getScrollRange(), 0, + getOverscrollMax(), 0); onScrollChanged(mScrollX, mScrollY, oldX, oldY); } diff --git a/core/java/android/widget/ImageView.java b/core/java/android/widget/ImageView.java index 3853359..233ce30 100644 --- a/core/java/android/widget/ImageView.java +++ b/core/java/android/widget/ImageView.java @@ -32,6 +32,7 @@ import android.graphics.drawable.Drawable; import android.net.Uri; import android.util.AttributeSet; import android.util.Log; +import android.view.RemotableViewMethod; import android.view.View; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; @@ -69,6 +70,7 @@ public class ImageView extends View { private ColorFilter mColorFilter; private int mAlpha = 255; private int mViewAlphaScale = 256; + private boolean mColorMod = false; private Drawable mDrawable = null; private int[] mState = null; @@ -138,7 +140,7 @@ public class ImageView extends View { int tint = a.getInt(com.android.internal.R.styleable.ImageView_tint, 0); if (tint != 0) { - setColorFilter(tint, PorterDuff.Mode.SRC_ATOP); + setColorFilter(tint); } mCropToPadding = a.getBoolean( @@ -877,6 +879,18 @@ public class ImageView extends View { setColorFilter(new PorterDuffColorFilter(color, mode)); } + /** + * Set a tinting option for the image. Assumes + * {@link PorterDuff.Mode#SRC_ATOP} blending mode. + * + * @param color Color tint to apply. + * @attr ref android.R.styleable#ImageView_tint + */ + @RemotableViewMethod + public final void setColorFilter(int color) { + setColorFilter(color, PorterDuff.Mode.SRC_ATOP); + } + public final void clearColorFilter() { setColorFilter(null); } @@ -889,22 +903,29 @@ public class ImageView extends View { public void setColorFilter(ColorFilter cf) { if (mColorFilter != cf) { mColorFilter = cf; + mColorMod = true; applyColorMod(); invalidate(); } } + @RemotableViewMethod public void setAlpha(int alpha) { alpha &= 0xFF; // keep it legal if (mAlpha != alpha) { mAlpha = alpha; + mColorMod = true; applyColorMod(); invalidate(); } } private void applyColorMod() { - if (mDrawable != null) { + // Only mutate and apply when modifications have occurred. This should + // not reset the mColorMod flag, since these filters need to be + // re-applied if the Drawable is changed. + if (mDrawable != null && mColorMod) { + mDrawable = mDrawable.mutate(); mDrawable.setColorFilter(mColorFilter); mDrawable.setAlpha(mAlpha * mViewAlphaScale >> 8); } diff --git a/core/java/android/widget/LinearLayout.java b/core/java/android/widget/LinearLayout.java index b4e2790..9fcb829 100644 --- a/core/java/android/widget/LinearLayout.java +++ b/core/java/android/widget/LinearLayout.java @@ -16,6 +16,8 @@ package android.widget; +import com.android.internal.R; + import android.content.Context; import android.content.res.TypedArray; import android.util.AttributeSet; @@ -25,8 +27,6 @@ import android.view.ViewDebug; import android.view.ViewGroup; import android.widget.RemoteViews.RemoteView; -import com.android.internal.R; - /** * A Layout that arranges its children in a single column or a single row. The direction of @@ -360,19 +360,21 @@ public class LinearLayout extends ViewGroup { // Optimization: don't bother measuring children who are going to use // leftover space. These views will get measured again down below if // there is any leftover space. - mTotalLength += lp.topMargin + lp.bottomMargin; + final int totalLength = mTotalLength; + mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin); } else { int oldHeight = Integer.MIN_VALUE; if (lp.height == 0 && lp.weight > 0) { - // heightMode is either UNSPECIFIED OR AT_MOST, and this child - // wanted to stretch to fill available space. Translate that to - // WRAP_CONTENT so that it does not end up with a height of 0 - oldHeight = 0; - lp.height = LayoutParams.WRAP_CONTENT; + // heightMode is either UNSPECIFIED or AT_MOST, and this + // child wanted to stretch to fill available space. + // Translate that to WRAP_CONTENT so that it does not end up + // with a height of 0 + oldHeight = 0; + lp.height = LayoutParams.WRAP_CONTENT; } - // Determine how big this child would like to. If this or + // Determine how big this child would like to be. If this or // previous children have given a weight, then we allow it to // use all available space (and we will shrink things later // if needed). @@ -385,8 +387,9 @@ public class LinearLayout extends ViewGroup { } final int childHeight = child.getMeasuredHeight(); - mTotalLength += childHeight + lp.topMargin + - lp.bottomMargin + getNextLocationOffset(child); + final int totalLength = mTotalLength; + mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin + + lp.bottomMargin + getNextLocationOffset(child)); if (useLargestChild) { largestChildHeight = Math.max(childHeight, largestChildHeight); @@ -459,8 +462,10 @@ public class LinearLayout extends ViewGroup { final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams(); - mTotalLength += largestChildHeight + lp.topMargin+ lp.bottomMargin + - getNextLocationOffset(child); + // Account for negative margins + final int totalLength = mTotalLength; + mTotalLength = Math.max(totalLength, totalLength + largestChildHeight + + lp.topMargin + lp.bottomMargin + getNextLocationOffset(child)); } } @@ -536,12 +541,14 @@ public class LinearLayout extends ViewGroup { allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT; - mTotalLength += child.getMeasuredHeight() + lp.topMargin + - lp.bottomMargin + getNextLocationOffset(child); + final int totalLength = mTotalLength; + mTotalLength = Math.max(totalLength, totalLength + child.getMeasuredHeight() + + lp.topMargin + lp.bottomMargin + getNextLocationOffset(child)); } // Add in our padding - mTotalLength += mPaddingTop + mPaddingBottom; + mTotalLength += mPaddingTop + mPaddingBottom; + // TODO: Should we recompute the heightSpec based on the new total length? } else { alternativeMaxWidth = Math.max(alternativeMaxWidth, weightedMaxWidth); @@ -651,7 +658,8 @@ public class LinearLayout extends ViewGroup { // Optimization: don't bother measuring children who are going to use // leftover space. These views will get measured again down below if // there is any leftover space. - mTotalLength += lp.leftMargin + lp.rightMargin; + final int totalLength = mTotalLength; + mTotalLength = Math.max(totalLength, totalLength + lp.leftMargin + lp.rightMargin); // Baseline alignment requires to measure widgets to obtain the // baseline offset (in particular for TextViews). @@ -666,7 +674,8 @@ public class LinearLayout extends ViewGroup { int oldWidth = Integer.MIN_VALUE; if (lp.width == 0 && lp.weight > 0) { - // widthMode is either UNSPECIFIED OR AT_MOST, and this child + // widthMode is either UNSPECIFIED or AT_MOST, and this + // child // wanted to stretch to fill available space. Translate that to // WRAP_CONTENT so that it does not end up with a width of 0 oldWidth = 0; @@ -686,8 +695,9 @@ public class LinearLayout extends ViewGroup { } final int childWidth = child.getMeasuredWidth(); - mTotalLength += childWidth + lp.leftMargin + lp.rightMargin + - getNextLocationOffset(child); + final int totalLength = mTotalLength; + mTotalLength = Math.max(totalLength, totalLength + childWidth + lp.leftMargin + + lp.rightMargin + getNextLocationOffset(child)); if (useLargestChild) { largestChildWidth = Math.max(childWidth, largestChildWidth); @@ -772,8 +782,9 @@ public class LinearLayout extends ViewGroup { final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams(); - mTotalLength += largestChildWidth + lp.leftMargin + lp.rightMargin + - getNextLocationOffset(child); + final int totalLength = mTotalLength; + mTotalLength = Math.max(totalLength, totalLength + largestChildWidth + + lp.leftMargin + lp.rightMargin + getNextLocationOffset(child)); } } @@ -843,8 +854,9 @@ public class LinearLayout extends ViewGroup { } } - mTotalLength += child.getMeasuredWidth() + lp.leftMargin + - lp.rightMargin + getNextLocationOffset(child); + final int totalLength = mTotalLength; + mTotalLength = Math.max(totalLength, totalLength + child.getMeasuredWidth() + + lp.leftMargin + lp.rightMargin + getNextLocationOffset(child)); boolean matchHeightLocally = heightMode != MeasureSpec.EXACTLY && lp.height == LayoutParams.MATCH_PARENT; @@ -875,6 +887,7 @@ public class LinearLayout extends ViewGroup { // Add in our padding mTotalLength += mPaddingLeft + mPaddingRight; + // TODO: Should we update widthSize with the new total length? // Check mMaxAscent[INDEX_TOP] first because it maps to Gravity.TOP, // the most common case diff --git a/core/java/android/widget/ListView.java b/core/java/android/widget/ListView.java index 401e7ff..2feed03 100644 --- a/core/java/android/widget/ListView.java +++ b/core/java/android/widget/ListView.java @@ -16,14 +16,17 @@ package android.widget; +import com.android.internal.R; +import com.google.android.collect.Lists; + import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; -import android.graphics.Rect; -import android.graphics.PixelFormat; import android.graphics.Paint; -import android.graphics.drawable.Drawable; +import android.graphics.PixelFormat; +import android.graphics.Rect; import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; import android.os.Parcel; import android.os.Parcelable; import android.util.AttributeSet; @@ -31,16 +34,13 @@ import android.util.SparseBooleanArray; import android.view.FocusFinder; import android.view.KeyEvent; import android.view.MotionEvent; +import android.view.SoundEffectConstants; import android.view.View; import android.view.ViewDebug; import android.view.ViewGroup; import android.view.ViewParent; -import android.view.SoundEffectConstants; import android.view.accessibility.AccessibilityEvent; -import com.google.android.collect.Lists; -import com.android.internal.R; - import java.util.ArrayList; /* @@ -2722,7 +2722,8 @@ public class ListView extends AbsListView { /** * Determine the distance to the nearest edge of a view in a particular - * direciton. + * direction. + * * @param descendant A descendant of this list. * @return The distance, or 0 if the nearest edge is already on screen. */ @@ -2916,7 +2917,14 @@ public class ListView extends AbsListView { if (!mStackFromBottom) { int bottom; - int listBottom = mBottom - mTop - mListPadding.bottom; + int listBottom = mBottom - mTop - mListPadding.bottom + mScrollY; + + // Draw top divider for overscroll + if (count > 0 && mScrollY < 0) { + bounds.bottom = 0; + bounds.top = -dividerHeight; + drawDivider(canvas, bounds, -1); + } for (int i = 0; i < count; i++) { if ((headerDividers || first + i >= headerCount) && @@ -3084,9 +3092,9 @@ public class ListView extends AbsListView { previouslyFocusedRect.offset(mScrollX, mScrollY); final ListAdapter adapter = mAdapter; - final int firstPosition = mFirstPosition; - // Don't cache the result of getChildCount here, it could change in layoutChildren. - if (adapter.getCount() < getChildCount() + firstPosition) { + // Don't cache the result of getChildCount or mFirstPosition here, + // it could change in layoutChildren. + if (adapter.getCount() < getChildCount() + mFirstPosition) { mLayoutMode = LAYOUT_NORMAL; layoutChildren(); } @@ -3096,6 +3104,7 @@ public class ListView extends AbsListView { Rect otherRect = mTempRect; int minDistance = Integer.MAX_VALUE; final int childCount = getChildCount(); + final int firstPosition = mFirstPosition; for (int i = 0; i < childCount; i++) { // only consider selectable views @@ -3300,9 +3309,9 @@ public class ListView extends AbsListView { * Sets the checked state of the specified position. The is only valid if * the choice mode has been set to {@link #CHOICE_MODE_SINGLE} or * {@link #CHOICE_MODE_MULTIPLE}. - * + * * @param position The item whose checked state is to be checked - * @param value The new checked sate for the item + * @param value The new checked state for the item */ public void setItemChecked(int position, boolean value) { if (mChoiceMode == CHOICE_MODE_NONE) { @@ -3385,10 +3394,11 @@ public class ListView extends AbsListView { } /** - * Returns the set of checked items ids. The result is only valid if - * the choice mode has not been set to {@link #CHOICE_MODE_SINGLE}. - * - * @return A new array which contains the id of each checked item in the list. + * Returns the set of checked items ids. The result is only valid if the + * choice mode has not been set to {@link #CHOICE_MODE_SINGLE}. + * + * @return A new array which contains the id of each checked item in the + * list. */ public long[] getCheckItemIds() { if (mChoiceMode != CHOICE_MODE_NONE && mCheckStates != null && mAdapter != null) { @@ -3397,11 +3407,23 @@ public class ListView extends AbsListView { final long[] ids = new long[count]; final ListAdapter adapter = mAdapter; + int checkedCount = 0; for (int i = 0; i < count; i++) { - ids[i]= adapter.getItemId(states.keyAt(i)); + if (states.valueAt(i)) { + ids[checkedCount++] = adapter.getItemId(states.keyAt(i)); + } } - return ids; + // Trim array if needed. mCheckStates may contain false values + // resulting in checkedCount being smaller than count. + if (checkedCount == count) { + return ids; + } else { + final long[] result = new long[checkedCount]; + System.arraycopy(ids, 0, result, 0, checkedCount); + + return result; + } } return new long[0]; diff --git a/core/java/android/widget/NumberPicker.java b/core/java/android/widget/NumberPicker.java index 2d36bc8..fd18db4 100644 --- a/core/java/android/widget/NumberPicker.java +++ b/core/java/android/widget/NumberPicker.java @@ -39,6 +39,7 @@ import com.android.internal.R; * A view for selecting a number * * For a dialog using this view, see {@link android.app.TimePickerDialog}. + * @hide */ @Widget public class NumberPicker extends LinearLayout { diff --git a/core/java/android/widget/ScrollView.java b/core/java/android/widget/ScrollView.java index 4a1d871..52ed11d 100644 --- a/core/java/android/widget/ScrollView.java +++ b/core/java/android/widget/ScrollView.java @@ -459,7 +459,8 @@ public class ScrollView extends FrameLayout { final int deltaY = (int) (mLastMotionY - y); mLastMotionY = y; - super.scrollTo(mScrollX, mScrollY + deltaY); + overscrollBy(0, deltaY, 0, mScrollY, 0, getScrollRange(), + 0, getOverscrollMax()); break; case MotionEvent.ACTION_UP: final VelocityTracker velocityTracker = mVelocityTracker; @@ -470,8 +471,7 @@ public class ScrollView extends FrameLayout { if ((Math.abs(initialVelocity) > mMinimumVelocity)) { fling(-initialVelocity); } else { - final int bottom = Math.max(0, getChildAt(0).getHeight() - - (getHeight() - mPaddingBottom - mPaddingTop)); + final int bottom = getScrollRange(); if (mScroller.springback(mScrollX, mScrollY, 0, 0, 0, bottom)) { invalidate(); } @@ -485,6 +485,41 @@ public class ScrollView extends FrameLayout { } return true; } + + @Override + protected void onOverscrolled(int scrollX, int scrollY, + boolean clampedX, boolean clampedY) { + // Treat animating scrolls differently; see #computeScroll() for why. + if (!mScroller.isFinished()) { + mScrollX = scrollX; + mScrollY = scrollY; + if (clampedY) { + mScroller.springback(mScrollX, mScrollY, 0, 0, 0, getScrollRange()); + } + } else { + super.scrollTo(scrollX, scrollY); + } + } + + private int getOverscrollMax() { + int childCount = getChildCount(); + int containerOverscroll = (getHeight() - mPaddingBottom - mPaddingTop) / 3; + if (childCount > 0) { + return Math.min(containerOverscroll, getChildAt(0).getHeight() / 3); + } else { + return containerOverscroll; + } + } + + private int getScrollRange() { + int scrollRange = 0; + if (getChildCount() > 0) { + View child = getChildAt(0); + scrollRange = Math.max(0, + child.getHeight() - getHeight() - mPaddingBottom - mPaddingTop); + } + return scrollRange; + } /** * <p> @@ -857,10 +892,28 @@ public class ScrollView extends FrameLayout { */ @Override protected int computeVerticalScrollRange() { - int count = getChildCount(); - return count == 0 ? getHeight() : (getChildAt(0)).getBottom(); + final int count = getChildCount(); + final int contentHeight = getHeight() - mPaddingBottom - mPaddingTop; + if (count == 0) { + return contentHeight; + } + + int scrollRange = getChildAt(0).getBottom(); + final int scrollY = mScrollY; + final int overscrollBottom = Math.max(0, scrollRange - contentHeight); + if (scrollY < 0) { + scrollRange -= scrollY; + } else if (scrollY > overscrollBottom) { + scrollRange += scrollY - overscrollBottom; + } + + return scrollRange; } + @Override + protected int computeVerticalScrollOffset() { + return Math.max(0, super.computeVerticalScrollOffset()); + } @Override protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) { @@ -915,10 +968,9 @@ public class ScrollView extends FrameLayout { int x = mScroller.getCurrX(); int y = mScroller.getCurrY(); - mScrollX = x; - mScrollY = y; - - if (oldX != mScrollX || oldY != mScrollY) { + if (oldX != x || oldY != y) { + overscrollBy(x - oldX, y - oldY, oldX, oldY, 0, getScrollRange(), + 0, getOverscrollMax()); onScrollChanged(mScrollX, mScrollY, oldX, oldY); } diff --git a/core/java/android/widget/TabHost.java b/core/java/android/widget/TabHost.java index 78e2fee..d4d9063 100644 --- a/core/java/android/widget/TabHost.java +++ b/core/java/android/widget/TabHost.java @@ -16,6 +16,8 @@ package android.widget; +import com.android.internal.R; + import android.app.LocalActivityManager; import android.content.Context; import android.content.Intent; @@ -33,8 +35,6 @@ import android.view.Window; import java.util.ArrayList; import java.util.List; -import com.android.internal.R; - /** * Container for a tabbed window view. This object holds two children: a set of tab labels that the * user clicks to select a specific tab, and a FrameLayout object that displays the contents of that @@ -624,7 +624,7 @@ mTabHost.addTab(TAB_TAG_1, "Hello, world!", "Tab 1"); } public void tabClosed() { - mTabContent.setVisibility(View.INVISIBLE); + mTabContent.setVisibility(View.GONE); } } diff --git a/core/java/android/widget/TableLayout.java b/core/java/android/widget/TableLayout.java index 66500a3..73760ac 100644 --- a/core/java/android/widget/TableLayout.java +++ b/core/java/android/widget/TableLayout.java @@ -575,6 +575,16 @@ public class TableLayout extends LinearLayout { final int totalExtraSpace = size - totalWidth; int extraSpace = totalExtraSpace / count; + // Column's widths are changed: force child table rows to re-measure. + // (done by super.measureVertical after shrinkAndStretchColumns.) + final int nbChildren = getChildCount(); + for (int i = 0; i < nbChildren; i++) { + View child = getChildAt(i); + if (child instanceof TableRow) { + child.forceLayout(); + } + } + if (!allColumns) { for (int i = 0; i < count; i++) { int column = columns.keyAt(i); diff --git a/core/java/android/widget/TableRow.java b/core/java/android/widget/TableRow.java index abf08bf..48d12df 100644 --- a/core/java/android/widget/TableRow.java +++ b/core/java/android/widget/TableRow.java @@ -22,8 +22,8 @@ import android.util.AttributeSet; import android.util.SparseIntArray; import android.view.Gravity; import android.view.View; -import android.view.ViewGroup; import android.view.ViewDebug; +import android.view.ViewGroup; /** diff --git a/core/java/android/widget/ViewFlipper.java b/core/java/android/widget/ViewFlipper.java index aee25b0..8034961 100644 --- a/core/java/android/widget/ViewFlipper.java +++ b/core/java/android/widget/ViewFlipper.java @@ -38,7 +38,7 @@ import android.widget.RemoteViews.RemoteView; @RemoteView public class ViewFlipper extends ViewAnimator { private static final String TAG = "ViewFlipper"; - private static final boolean LOGD = true; + private static final boolean LOGD = false; private static final int DEFAULT_INTERVAL = 3000; diff --git a/core/java/com/android/internal/app/DisableCarModeActivity.java b/core/java/com/android/internal/app/DisableCarModeActivity.java new file mode 100644 index 0000000..95dc1f9 --- /dev/null +++ b/core/java/com/android/internal/app/DisableCarModeActivity.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.app; + +import android.app.Activity; +import android.app.IUiModeManager; +import android.os.Bundle; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.util.Log; + +public class DisableCarModeActivity extends Activity { + private static final String TAG = "DisableCarModeActivity"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + try { + IUiModeManager uiModeManager = IUiModeManager.Stub.asInterface( + ServiceManager.getService("uimode")); + uiModeManager.disableCarMode(); + } catch (RemoteException e) { + Log.e(TAG, "Failed to disable car mode", e); + } + finish(); + } + +} diff --git a/core/java/com/android/internal/app/ExternalMediaFormatActivity.java b/core/java/com/android/internal/app/ExternalMediaFormatActivity.java index 2b07ae6..7e9bbd1 100644 --- a/core/java/com/android/internal/app/ExternalMediaFormatActivity.java +++ b/core/java/com/android/internal/app/ExternalMediaFormatActivity.java @@ -24,7 +24,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.os.Bundle; import android.os.Handler; -import android.os.IMountService; +import android.os.storage.IMountService; import android.os.Message; import android.os.RemoteException; import android.os.ServiceManager; diff --git a/core/java/com/android/internal/app/IMediaContainerService.aidl b/core/java/com/android/internal/app/IMediaContainerService.aidl index 726e28f..c0e9587 100755 --- a/core/java/com/android/internal/app/IMediaContainerService.aidl +++ b/core/java/com/android/internal/app/IMediaContainerService.aidl @@ -25,4 +25,5 @@ interface IMediaContainerService { String key, String resFileName); boolean copyResource(in Uri packageURI, in ParcelFileDescriptor outStream); + int getRecommendedInstallLocation(in Uri fileUri); }
\ No newline at end of file diff --git a/core/java/com/android/internal/app/NetInitiatedActivity.java b/core/java/com/android/internal/app/NetInitiatedActivity.java index 98fb236..24818a8 100755 --- a/core/java/com/android/internal/app/NetInitiatedActivity.java +++ b/core/java/com/android/internal/app/NetInitiatedActivity.java @@ -24,7 +24,6 @@ import android.content.Intent; import android.content.IntentFilter; import android.os.Bundle; import android.os.Handler; -import android.os.IMountService; import android.os.Message; import android.os.RemoteException; import android.os.ServiceManager; diff --git a/core/java/com/android/internal/app/ShutdownThread.java b/core/java/com/android/internal/app/ShutdownThread.java index c110f95..2f48499 100644 --- a/core/java/com/android/internal/app/ShutdownThread.java +++ b/core/java/com/android/internal/app/ShutdownThread.java @@ -32,7 +32,7 @@ import android.os.RemoteException; import android.os.Power; import android.os.ServiceManager; import android.os.SystemClock; -import android.os.IMountService; +import android.os.storage.IMountService; import com.android.internal.telephony.ITelephony; import android.util.Log; diff --git a/core/java/com/android/internal/app/TetherActivity.java b/core/java/com/android/internal/app/TetherActivity.java index 2b93dbc..a48ccf9 100644 --- a/core/java/com/android/internal/app/TetherActivity.java +++ b/core/java/com/android/internal/app/TetherActivity.java @@ -25,7 +25,6 @@ import android.content.IntentFilter; import android.net.ConnectivityManager; import android.os.Bundle; import android.os.Handler; -import android.os.IMountService; import android.os.Message; import android.os.RemoteException; import android.os.ServiceManager; @@ -33,16 +32,19 @@ import android.widget.Toast; import android.util.Log; /** - * This activity is shown to the user for him/her to connect/disconnect a Tether - * connection. It will display notification when a suitable connection is made - * to allow the tether to be setup. A second notification will be show when a - * tether is active, allowing the user to manage tethered connections. + * This activity is shown to the user in two cases: when a connection is possible via + * a usb tether and when any type of tether is connected. In the connecting case + * It allows them to start a USB tether. In the Tethered/disconnecting case it + * will disconnect all tethers. */ public class TetherActivity extends AlertActivity implements DialogInterface.OnClickListener { private static final int POSITIVE_BUTTON = AlertDialog.BUTTON1; + // count of the number of tethered connections at activity create time. + private int mTethered; + /* Used to detect when the USB cable is unplugged, so we can call finish() */ private BroadcastReceiver mTetherReceiver = new BroadcastReceiver() { @Override @@ -53,8 +55,6 @@ public class TetherActivity extends AlertActivity implements } }; - private boolean mWantTethering; - @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -62,17 +62,18 @@ public class TetherActivity extends AlertActivity implements // determine if we advertise tethering or untethering ConnectivityManager cm = (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE); - if (cm.getTetheredIfaces().length > 0) { - mWantTethering = false; - } else if (cm.getTetherableIfaces().length > 0) { - mWantTethering = true; - } else { + mTethered = cm.getTetheredIfaces().length; + int tetherable = cm.getTetherableIfaces().length; + if ((mTethered == 0) && (tetherable == 0)) { finish(); return; } - // Set up the "dialog" - if (mWantTethering == true) { + // Set up the dialog + // if we have a tethered connection we put up a "Do you want to Disconect" dialog + // otherwise we must have a tetherable interface (else we'd return above) + // and so we want to put up the "do you want to connect" dialog + if (mTethered == 0) { mAlertParams.mIconId = com.android.internal.R.drawable.ic_dialog_usb; mAlertParams.mTitle = getString(com.android.internal.R.string.tether_title); mAlertParams.mMessage = getString(com.android.internal.R.string.tether_message); @@ -115,17 +116,36 @@ public class TetherActivity extends AlertActivity implements * {@inheritDoc} */ public void onClick(DialogInterface dialog, int which) { + boolean error = false; if (which == POSITIVE_BUTTON) { - ConnectivityManager connManager = + ConnectivityManager cm = (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE); // start/stop tethering - if (mWantTethering) { - if (!connManager.tether("ppp0")) { + String[] tethered = cm.getTetheredIfaces(); + + if (tethered.length == 0) { + String[] tetherable = cm.getTetherableIfaces(); + String[] usbRegexs = cm.getTetherableUsbRegexs(); + for (String t : tetherable) { + for (String r : usbRegexs) { + if (t.matches(r)) { + if (!cm.tether(t)) + error = true; + break; + } + } + } + if (error) { showTetheringError(); } } else { - if (!connManager.untether("ppp0")) { + for (String t : tethered) { + if (!cm.untether("ppp0")) { + error = true; + } + } + if (error) { showUnTetheringError(); } } @@ -135,7 +155,12 @@ public class TetherActivity extends AlertActivity implements } private void handleTetherStateChanged(Intent intent) { - finish(); + // determine if we advertise tethering or untethering + ConnectivityManager cm = + (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE); + if (mTethered != cm.getTetheredIfaces().length) { + finish(); + } } private void showTetheringError() { diff --git a/core/java/com/android/internal/app/UsbStorageActivity.java b/core/java/com/android/internal/app/UsbStorageActivity.java deleted file mode 100644 index 34ae2b4..0000000 --- a/core/java/com/android/internal/app/UsbStorageActivity.java +++ /dev/null @@ -1,191 +0,0 @@ -/* - * Copyright (C) 2007 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.internal.app; - -import android.app.Activity; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.IntentFilter; -import android.os.Bundle; -import android.os.Handler; -import android.os.Environment; -import android.os.IMountService; -import android.os.MountServiceResultCode; -import android.os.Message; -import android.os.RemoteException; -import android.os.ServiceManager; -import android.widget.ImageView; -import android.widget.Button; -import android.widget.TextView; -import android.widget.Toast; -import android.view.View; - -/** - * This activity is shown to the user for him/her to enable USB mass storage - * on-demand (that is, when the USB cable is connected). It uses the alert - * dialog style. It will be launched from a notification. - */ -public class UsbStorageActivity extends Activity { - private Button mMountButton; - private Button mUnmountButton; - private TextView mBanner; - private TextView mMessage; - private ImageView mIcon; - - /** Used to detect when the USB cable is unplugged, so we can call finish() */ - private BroadcastReceiver mBatteryReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (intent.getAction() == Intent.ACTION_BATTERY_CHANGED) { - handleBatteryChanged(intent); - } - } - }; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setTitle(getString(com.android.internal.R.string.usb_storage_activity_title)); - - setContentView(com.android.internal.R.layout.usb_storage_activity); - - mIcon = (ImageView) findViewById(com.android.internal.R.id.icon); - mBanner = (TextView) findViewById(com.android.internal.R.id.banner); - mMessage = (TextView) findViewById(com.android.internal.R.id.message); - - mMountButton = (Button) findViewById(com.android.internal.R.id.mount_button); - mMountButton.setOnClickListener( - new View.OnClickListener() { - public void onClick(View v) { - mountAsUsbStorage(); - // TODO: replace with forthcoming MountService callbacks - switchDisplay(true); - } - }); - - mUnmountButton = (Button) findViewById(com.android.internal.R.id.unmount_button); - mUnmountButton.setOnClickListener( - new View.OnClickListener() { - public void onClick(View v) { - stopUsbStorage(); - // TODO: replace with forthcoming MountService callbacks - switchDisplay(false); - } - }); - } - - private void switchDisplay(boolean usbStorageInUse) { - if (usbStorageInUse) { - mUnmountButton.setVisibility(View.VISIBLE); - mMountButton.setVisibility(View.GONE); - mIcon.setImageResource(com.android.internal.R.drawable.usb_android_connected); - mBanner.setText(com.android.internal.R.string.usb_storage_stop_title); - mMessage.setText(com.android.internal.R.string.usb_storage_stop_message); - } else { - mUnmountButton.setVisibility(View.GONE); - mMountButton.setVisibility(View.VISIBLE); - mIcon.setImageResource(com.android.internal.R.drawable.usb_android); - mBanner.setText(com.android.internal.R.string.usb_storage_title); - mMessage.setText(com.android.internal.R.string.usb_storage_message); - } - } - - @Override - protected void onResume() { - super.onResume(); - - registerReceiver(mBatteryReceiver, new IntentFilter(Intent.ACTION_BATTERY_CHANGED)); - - boolean umsOn = false; - try { - IMountService mountService = IMountService.Stub.asInterface(ServiceManager - .getService("mount")); - if (mountService != null) { - umsOn = mountService.getVolumeShared( - Environment.getExternalStorageDirectory().getPath(), "ums"); - } - } catch (android.os.RemoteException exc) { - // pass - } - switchDisplay(umsOn); - } - - @Override - protected void onPause() { - super.onPause(); - - unregisterReceiver(mBatteryReceiver); - } - - private void mountAsUsbStorage() { - IMountService mountService = IMountService.Stub.asInterface(ServiceManager - .getService("mount")); - if (mountService == null) { - showSharingError(); - return; - } - - try { - if (mountService.shareVolume( - Environment.getExternalStorageDirectory().getPath(), "ums") != - MountServiceResultCode.OperationSucceeded) { - showSharingError(); - } - } catch (RemoteException e) { - showSharingError(); - } - } - - private void stopUsbStorage() { - IMountService mountService = IMountService.Stub.asInterface(ServiceManager - .getService("mount")); - if (mountService == null) { - showStoppingError(); - return; - } - - try { - mountService.unshareVolume( - Environment.getExternalStorageDirectory().getPath(), "ums"); - } catch (RemoteException e) { - showStoppingError(); - return; - } - } - - private void handleBatteryChanged(Intent intent) { - int pluggedType = intent.getIntExtra("plugged", 0); - if (pluggedType == 0) { - // It was disconnected from the plug, so finish - finish(); - } - } - - private void showSharingError() { - Toast.makeText(this, com.android.internal.R.string.usb_storage_error_message, - Toast.LENGTH_LONG).show(); - } - - private void showStoppingError() { - Toast.makeText(this, com.android.internal.R.string.usb_storage_stop_error_message, - Toast.LENGTH_LONG).show(); - } - -} diff --git a/core/java/com/android/internal/content/PackageHelper.java b/core/java/com/android/internal/content/PackageHelper.java new file mode 100644 index 0000000..bc7dbf4 --- /dev/null +++ b/core/java/com/android/internal/content/PackageHelper.java @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.content; + +import android.os.storage.IMountService; + +import android.os.IBinder; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.storage.StorageResultCode; +import android.util.Log; + +import java.io.File; + +/** + * Constants used internally between the PackageManager + * and media container service transports. + * Some utility methods to invoke MountService api. + */ +public class PackageHelper { + public static final int RECOMMEND_INSTALL_INTERNAL = 1; + public static final int RECOMMEND_INSTALL_EXTERNAL = 2; + public static final int RECOMMEND_FAILED_INSUFFICIENT_STORAGE = -1; + public static final int RECOMMEND_FAILED_INVALID_APK = -2; + private static final boolean DEBUG_SD_INSTALL = true; + private static final String TAG = "PackageHelper"; + + public static IMountService getMountService() { + IBinder service = ServiceManager.getService("mount"); + if (service != null) { + return IMountService.Stub.asInterface(service); + } else { + Log.e(TAG, "Can't get mount service"); + } + return null; + } + + public static String createSdDir(File tmpPackageFile, String cid, + String sdEncKey, int uid) { + // Create mount point via MountService + IMountService mountService = getMountService(); + long len = tmpPackageFile.length(); + int mbLen = (int) (len/(1024*1024)); + if ((len - (mbLen * 1024 * 1024)) > 0) { + mbLen++; + } + if (DEBUG_SD_INSTALL) Log.i(TAG, "Size of resource " + mbLen); + + try { + int rc = mountService.createSecureContainer( + cid, mbLen, "vfat", sdEncKey, uid); + if (rc != StorageResultCode.OperationSucceeded) { + Log.e(TAG, "Failed to create secure container " + cid); + return null; + } + String cachePath = mountService.getSecureContainerPath(cid); + if (DEBUG_SD_INSTALL) Log.i(TAG, "Created secure container " + cid + + " at " + cachePath); + return cachePath; + } catch (RemoteException e) { + Log.e(TAG, "MountService running?"); + } + return null; + } + + public static String mountSdDir(String cid, String key, int ownerUid) { + try { + int rc = getMountService().mountSecureContainer(cid, key, ownerUid); + if (rc != StorageResultCode.OperationSucceeded) { + Log.i(TAG, "Failed to mount container " + cid + " rc : " + rc); + return null; + } + return getMountService().getSecureContainerPath(cid); + } catch (RemoteException e) { + Log.e(TAG, "MountService running?"); + } + return null; + } + + public static boolean unMountSdDir(String cid) { + try { + int rc = getMountService().unmountSecureContainer(cid, false); + if (rc != StorageResultCode.OperationSucceeded) { + Log.e(TAG, "Failed to unmount " + cid + " with rc " + rc); + return false; + } + return true; + } catch (RemoteException e) { + Log.e(TAG, "MountService running?"); + } + return false; + } + + public static boolean renameSdDir(String oldId, String newId) { + try { + int rc = getMountService().renameSecureContainer(oldId, newId); + if (rc != StorageResultCode.OperationSucceeded) { + Log.e(TAG, "Failed to rename " + oldId + " to " + + newId + "with rc " + rc); + return false; + } + return true; + } catch (RemoteException e) { + Log.i(TAG, "Failed ot rename " + oldId + " to " + newId + + " with exception : " + e); + } + return false; + } + + public static String getSdDir(String cid) { + try { + return getMountService().getSecureContainerPath(cid); + } catch (RemoteException e) { + Log.e(TAG, "Failed to get container path for " + cid + + " with exception " + e); + } + return null; + } + + public static boolean finalizeSdDir(String cid) { + try { + int rc = getMountService().finalizeSecureContainer(cid); + if (rc != StorageResultCode.OperationSucceeded) { + Log.i(TAG, "Failed to finalize container " + cid); + return false; + } + return true; + } catch (RemoteException e) { + Log.e(TAG, "Failed to finalize container " + cid + + " with exception " + e); + } + return false; + } + + public static boolean destroySdDir(String cid) { + try { + int rc = getMountService().destroySecureContainer(cid, false); + if (rc != StorageResultCode.OperationSucceeded) { + Log.i(TAG, "Failed to destroy container " + cid); + return false; + } + return true; + } catch (RemoteException e) { + Log.e(TAG, "Failed to destroy container " + cid + + " with exception " + e); + } + return false; + } + + public static String[] getSecureContainerList() { + try { + return getMountService().getSecureContainerList(); + } catch (RemoteException e) { + Log.e(TAG, "Failed to get secure container list with exception" + + e); + } + return null; + } + + public static boolean isContainerMounted(String cid) { + try { + return getMountService().isSecureContainerMounted(cid); + } catch (RemoteException e) { + Log.e(TAG, "Failed to find out if container " + cid + " mounted"); + } + return false; + } +} diff --git a/core/java/com/android/internal/os/RuntimeInit.java b/core/java/com/android/internal/os/RuntimeInit.java index 57a28e6..c134d88 100644 --- a/core/java/com/android/internal/os/RuntimeInit.java +++ b/core/java/com/android/internal/os/RuntimeInit.java @@ -22,7 +22,6 @@ import android.app.IActivityManager; import android.os.Build; import android.os.Debug; import android.os.IBinder; -import android.os.ICheckinService; import android.os.Process; import android.os.RemoteException; import android.os.ServiceManager; diff --git a/core/java/com/android/internal/os/ZygoteConnection.java b/core/java/com/android/internal/os/ZygoteConnection.java index 631e7d8..da0c5a2 100644 --- a/core/java/com/android/internal/os/ZygoteConnection.java +++ b/core/java/com/android/internal/os/ZygoteConnection.java @@ -295,7 +295,10 @@ class ZygoteConnection { /** from --peer-wait */ boolean peerWait; - /** from --enable-debugger, --enable-checkjni, --enable-assert */ + /** + * From --enable-debugger, --enable-checkjni, --enable-assert, and + * --enable-safemode + */ int debugFlags; /** from --classpath */ @@ -363,6 +366,8 @@ class ZygoteConnection { arg.substring(arg.indexOf('=') + 1)); } else if (arg.equals("--enable-debugger")) { debugFlags |= Zygote.DEBUG_ENABLE_DEBUGGER; + } else if (arg.equals("--enable-safemode")) { + debugFlags |= Zygote.DEBUG_ENABLE_SAFEMODE; } else if (arg.equals("--enable-checkjni")) { debugFlags |= Zygote.DEBUG_ENABLE_CHECKJNI; } else if (arg.equals("--enable-assert")) { diff --git a/core/java/com/android/internal/util/HierarchicalStateMachine.java b/core/java/com/android/internal/util/HierarchicalStateMachine.java index a1c5078..b4af79c 100644 --- a/core/java/com/android/internal/util/HierarchicalStateMachine.java +++ b/core/java/com/android/internal/util/HierarchicalStateMachine.java @@ -1021,7 +1021,7 @@ public class HierarchicalStateMachine { * @param msg that couldn't be handled. */ protected void unhandledMessage(Message msg) { - Log.e(TAG, "unhandledMessage: msg.what=" + msg.what); + Log.e(TAG, mName + " - unhandledMessage: msg.what=" + msg.what); } /** diff --git a/core/java/com/android/internal/view/BaseIWindow.java b/core/java/com/android/internal/view/BaseIWindow.java index 15dcbd6..22c6e79 100644 --- a/core/java/com/android/internal/view/BaseIWindow.java +++ b/core/java/com/android/internal/view/BaseIWindow.java @@ -1,5 +1,6 @@ package com.android.internal.view; +import android.content.res.Configuration; import android.graphics.Rect; import android.os.Bundle; import android.os.ParcelFileDescriptor; @@ -17,7 +18,7 @@ public class BaseIWindow extends IWindow.Stub { } public void resized(int w, int h, Rect coveredInsets, - Rect visibleInsets, boolean reportDraw) { + Rect visibleInsets, boolean reportDraw, Configuration newConfig) { if (reportDraw) { try { mSession.finishDrawing(this); diff --git a/core/java/com/android/internal/widget/PasswordEntryKeyboard.java b/core/java/com/android/internal/widget/PasswordEntryKeyboard.java new file mode 100644 index 0000000..e1a6737 --- /dev/null +++ b/core/java/com/android/internal/widget/PasswordEntryKeyboard.java @@ -0,0 +1,268 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.android.internal.widget; + +import java.util.Locale; +import android.content.Context; +import android.content.res.Resources; +import android.content.res.XmlResourceParser; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.Paint.Align; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.inputmethodservice.Keyboard; +import android.inputmethodservice.KeyboardView; +import android.util.Log; +import com.android.internal.R; + +/** + * A basic, embed-able keyboard designed for password entry. Allows entry of all Latin-1 characters. + * + * It has two modes: alpha and numeric. In alpha mode, it allows all Latin-1 characters and enables + * an additional keyboard with symbols. In numeric mode, it shows a 12-key DTMF dialer-like + * keypad with alpha characters hints. + */ +public class PasswordEntryKeyboard extends Keyboard { + private static final String TAG = "PasswordEntryKeyboard"; + private static final int SHIFT_OFF = 0; + private static final int SHIFT_ON = 1; + private static final int SHIFT_LOCKED = 2; + public static final int KEYCODE_SPACE = ' '; + + private Drawable mShiftIcon; + private Drawable mShiftLockIcon; + private Drawable mShiftLockPreviewIcon; + private Drawable mOldShiftIcon; + private Drawable mOldShiftPreviewIcon; + private Drawable mSpaceIcon; + private Key mShiftKey; + private Key mEnterKey; + private Key mF1Key; + private Key mSpaceKey; + private Locale mLocale; + private Resources mRes; + private int mExtensionResId; + private int mShiftState = SHIFT_OFF; + + static int sSpacebarVerticalCorrection; + + public PasswordEntryKeyboard(Context context, int xmlLayoutResId) { + this(context, xmlLayoutResId, 0); + } + + public PasswordEntryKeyboard(Context context, int xmlLayoutResId, int mode) { + super(context, xmlLayoutResId, mode); + final Resources res = context.getResources(); + mRes = res; + mShiftIcon = res.getDrawable(R.drawable.sym_keyboard_shift); + mShiftLockIcon = res.getDrawable(R.drawable.sym_keyboard_shift_locked); + mShiftLockPreviewIcon = res.getDrawable(R.drawable.sym_keyboard_feedback_shift_locked); + mShiftLockPreviewIcon.setBounds(0, 0, + mShiftLockPreviewIcon.getIntrinsicWidth(), + mShiftLockPreviewIcon.getIntrinsicHeight()); + mSpaceIcon = res.getDrawable(R.drawable.sym_keyboard_space); + sSpacebarVerticalCorrection = res.getDimensionPixelOffset( + R.dimen.password_keyboard_spacebar_vertical_correction); + } + + public PasswordEntryKeyboard(Context context, int layoutTemplateResId, + CharSequence characters, int columns, int horizontalPadding) { + super(context, layoutTemplateResId, characters, columns, horizontalPadding); + } + + @Override + protected Key createKeyFromXml(Resources res, Row parent, int x, int y, + XmlResourceParser parser) { + LatinKey key = new LatinKey(res, parent, x, y, parser); + final int code = key.codes[0]; + if (code >=0 && code != '\n' && (code < 32 || code > 127)) { + // Log.w(TAG, "Key code for " + key.label + " is not latin-1"); + key.label = " "; + key.setEnabled(false); + } + switch (key.codes[0]) { + case 10: + mEnterKey = key; + break; + case PasswordEntryKeyboardView.KEYCODE_F1: + mF1Key = key; + break; + case 32: + mSpaceKey = key; + break; + } + return key; + } + + /** + * Allows enter key resources to be overridden + * @param res resources to grab given items from + * @param previewId preview drawable shown on enter key + * @param iconId normal drawable shown on enter key + * @param labelId string shown on enter key + */ + void setEnterKeyResources(Resources res, int previewId, int iconId, int labelId) { + if (mEnterKey != null) { + // Reset some of the rarely used attributes. + mEnterKey.popupCharacters = null; + mEnterKey.popupResId = 0; + mEnterKey.text = null; + + mEnterKey.iconPreview = res.getDrawable(previewId); + mEnterKey.icon = res.getDrawable(iconId); + mEnterKey.label = res.getText(labelId); + + // Set the initial size of the preview icon + if (mEnterKey.iconPreview != null) { + mEnterKey.iconPreview.setBounds(0, 0, + mEnterKey.iconPreview.getIntrinsicWidth(), + mEnterKey.iconPreview.getIntrinsicHeight()); + } + } + } + + /** + * Allows shiftlock to be turned on. See {@link #setShiftLocked(boolean)} + * + */ + void enableShiftLock() { + int index = getShiftKeyIndex(); + if (index >= 0) { + mShiftKey = getKeys().get(index); + if (mShiftKey instanceof LatinKey) { + ((LatinKey)mShiftKey).enableShiftLock(); + } + mOldShiftIcon = mShiftKey.icon; + mOldShiftPreviewIcon = mShiftKey.iconPreview; + } + } + + /** + * Turn on shift lock. This turns on the LED for this key, if it has one. + * It should be followed by a call to {@link KeyboardView#invalidateKey(int)} + * or {@link KeyboardView#invalidateAllKeys()} + * + * @param shiftLocked + */ + void setShiftLocked(boolean shiftLocked) { + if (mShiftKey != null) { + if (shiftLocked) { + mShiftKey.on = true; + mShiftKey.icon = mShiftLockIcon; + mShiftState = SHIFT_LOCKED; + } else { + mShiftKey.on = false; + mShiftKey.icon = mShiftLockIcon; + mShiftState = SHIFT_ON; + } + } + } + + /** + * Turn on shift mode. Sets shift mode and turns on icon for shift key. + * It should be followed by a call to {@link KeyboardView#invalidateKey(int)} + * or {@link KeyboardView#invalidateAllKeys()} + * + * @param shiftLocked + */ + @Override + public boolean setShifted(boolean shiftState) { + boolean shiftChanged = false; + if (mShiftKey != null) { + if (shiftState == false) { + shiftChanged = mShiftState != SHIFT_OFF; + mShiftState = SHIFT_OFF; + mShiftKey.on = false; + mShiftKey.icon = mOldShiftIcon; + } else if (mShiftState == SHIFT_OFF) { + shiftChanged = mShiftState == SHIFT_OFF; + mShiftState = SHIFT_ON; + mShiftKey.on = false; + mShiftKey.icon = mShiftIcon; + } + } else { + return super.setShifted(shiftState); + } + return shiftChanged; + } + + /** + * Whether or not keyboard is shifted. + * @return true if keyboard state is shifted. + */ + @Override + public boolean isShifted() { + if (mShiftKey != null) { + return mShiftState != SHIFT_OFF; + } else { + return super.isShifted(); + } + } + + static class LatinKey extends Keyboard.Key { + private boolean mShiftLockEnabled; + private boolean mEnabled = true; + + public LatinKey(Resources res, Keyboard.Row parent, int x, int y, + XmlResourceParser parser) { + super(res, parent, x, y, parser); + if (popupCharacters != null && popupCharacters.length() == 0) { + // If there is a keyboard with no keys specified in popupCharacters + popupResId = 0; + } + } + + void setEnabled(boolean enabled) { + mEnabled = enabled; + } + + void enableShiftLock() { + mShiftLockEnabled = true; + } + + @Override + public void onReleased(boolean inside) { + if (!mShiftLockEnabled) { + super.onReleased(inside); + } else { + pressed = !pressed; + } + } + + /** + * Overriding this method so that we can reduce the target area for certain keys. + */ + @Override + public boolean isInside(int x, int y) { + if (!mEnabled) { + return false; + } + final int code = codes[0]; + if (code == KEYCODE_SHIFT || code == KEYCODE_DELETE) { + y -= height / 10; + if (code == KEYCODE_SHIFT) x += width / 6; + if (code == KEYCODE_DELETE) x -= width / 6; + } else if (code == KEYCODE_SPACE) { + y += PasswordEntryKeyboard.sSpacebarVerticalCorrection; + } + return super.isInside(x, y); + } + } +} diff --git a/core/java/com/android/internal/widget/PasswordEntryKeyboardHelper.java b/core/java/com/android/internal/widget/PasswordEntryKeyboardHelper.java new file mode 100644 index 0000000..c2862b0 --- /dev/null +++ b/core/java/com/android/internal/widget/PasswordEntryKeyboardHelper.java @@ -0,0 +1,229 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.android.internal.widget; + +import android.content.Context; +import android.inputmethodservice.Keyboard; +import android.inputmethodservice.KeyboardView; +import android.inputmethodservice.KeyboardView.OnKeyboardActionListener; +import android.os.Handler; +import android.os.SystemClock; +import android.provider.Settings; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; +import android.view.View; +import android.view.ViewRoot; +import com.android.internal.R; + +public class PasswordEntryKeyboardHelper implements OnKeyboardActionListener { + + public static final int KEYBOARD_MODE_ALPHA = 0; + public static final int KEYBOARD_MODE_NUMERIC = 1; + private static final int KEYBOARD_STATE_NORMAL = 0; + private static final int KEYBOARD_STATE_SHIFTED = 1; + private static final int KEYBOARD_STATE_CAPSLOCK = 2; + private int mKeyboardMode = KEYBOARD_MODE_ALPHA; + private int mKeyboardState = KEYBOARD_STATE_NORMAL; + private PasswordEntryKeyboard mQwertyKeyboard; + private PasswordEntryKeyboard mQwertyKeyboardShifted; + private PasswordEntryKeyboard mSymbolsKeyboard; + private PasswordEntryKeyboard mSymbolsKeyboardShifted; + private PasswordEntryKeyboard mNumericKeyboard; + private Context mContext; + private View mTargetView; + private KeyboardView mKeyboardView; + + public PasswordEntryKeyboardHelper(Context context, KeyboardView keyboardView, View targetView) { + mContext = context; + mTargetView = targetView; + mKeyboardView = keyboardView; + createKeyboards(); + mKeyboardView.setOnKeyboardActionListener(this); + } + + public boolean isAlpha() { + return mKeyboardMode == KEYBOARD_MODE_ALPHA; + } + + private void createKeyboards() { + mNumericKeyboard = new PasswordEntryKeyboard(mContext, R.xml.password_kbd_numeric); + mQwertyKeyboard = new PasswordEntryKeyboard(mContext, + R.xml.password_kbd_qwerty, R.id.mode_normal); + mQwertyKeyboard.enableShiftLock(); + + mQwertyKeyboardShifted = new PasswordEntryKeyboard(mContext, + R.xml.password_kbd_qwerty_shifted, + R.id.mode_normal); + mQwertyKeyboardShifted.enableShiftLock(); + mQwertyKeyboardShifted.setShifted(true); // always shifted. + + mSymbolsKeyboard = new PasswordEntryKeyboard(mContext, R.xml.password_kbd_symbols); + mSymbolsKeyboard.enableShiftLock(); + + mSymbolsKeyboardShifted = new PasswordEntryKeyboard(mContext, + R.xml.password_kbd_symbols_shift); + mSymbolsKeyboardShifted.enableShiftLock(); + mSymbolsKeyboardShifted.setShifted(true); // always shifted + } + + public void setKeyboardMode(int mode) { + switch (mode) { + case KEYBOARD_MODE_ALPHA: + mKeyboardView.setKeyboard(mQwertyKeyboard); + mKeyboardState = KEYBOARD_STATE_NORMAL; + final boolean visiblePassword = Settings.System.getInt( + mContext.getContentResolver(), + Settings.System.TEXT_SHOW_PASSWORD, 1) != 0; + mKeyboardView.setPreviewEnabled(visiblePassword); + break; + case KEYBOARD_MODE_NUMERIC: + mKeyboardView.setKeyboard(mNumericKeyboard); + mKeyboardState = KEYBOARD_STATE_NORMAL; + mKeyboardView.setPreviewEnabled(false); // never show popup for numeric keypad + break; + } + mKeyboardMode = mode; + } + + private void sendKeyEventsToTarget(int keyEventCode) { + Handler handler = mTargetView.getHandler(); + KeyEvent[] events = KeyCharacterMap.load(KeyCharacterMap.ALPHA).getEvents( + new char[] { (char) keyEventCode }); + if (events != null) { + for (KeyEvent event : events) { + handler.sendMessage(handler.obtainMessage(ViewRoot.DISPATCH_KEY, event)); + } + } + } + + public void sendDownUpKeyEvents(int keyEventCode) { + long eventTime = SystemClock.uptimeMillis(); + Handler handler = mTargetView.getHandler(); + handler.sendMessage(handler.obtainMessage(ViewRoot.DISPATCH_KEY_FROM_IME, + new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN, keyEventCode, 0, 0, 0, 0, + KeyEvent.FLAG_SOFT_KEYBOARD|KeyEvent.FLAG_KEEP_TOUCH_MODE))); + handler.sendMessage(handler.obtainMessage(ViewRoot.DISPATCH_KEY_FROM_IME, + new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP, keyEventCode, 0, 0, 0, 0, + KeyEvent.FLAG_SOFT_KEYBOARD|KeyEvent.FLAG_KEEP_TOUCH_MODE))); + } + + public void onKey(int primaryCode, int[] keyCodes) { + if (primaryCode == Keyboard.KEYCODE_DELETE) { + handleBackspace(); + } else if (primaryCode == Keyboard.KEYCODE_SHIFT) { + handleShift(); + } else if (primaryCode == Keyboard.KEYCODE_CANCEL) { + handleClose(); + return; + } else if (primaryCode == Keyboard.KEYCODE_MODE_CHANGE && mKeyboardView != null) { + handleModeChange(); + } else { + handleCharacter(primaryCode, keyCodes); + // Switch back to old keyboard if we're not in capslock mode + if (mKeyboardState == KEYBOARD_STATE_SHIFTED) { + // skip to the unlocked state + mKeyboardState = KEYBOARD_STATE_CAPSLOCK; + handleShift(); + } + } + } + + private void handleModeChange() { + final Keyboard current = mKeyboardView.getKeyboard(); + Keyboard next = null; + if (current == mQwertyKeyboard || current == mQwertyKeyboardShifted) { + next = mSymbolsKeyboard; + } else if (current == mSymbolsKeyboard || current == mSymbolsKeyboardShifted) { + next = mQwertyKeyboard; + } + if (next != null) { + mKeyboardView.setKeyboard(next); + mKeyboardState = KEYBOARD_STATE_NORMAL; + } + } + + private void handleBackspace() { + sendDownUpKeyEvents(KeyEvent.KEYCODE_DEL); + } + + private void handleShift() { + if (mKeyboardView == null) { + return; + } + Keyboard current = mKeyboardView.getKeyboard(); + PasswordEntryKeyboard next = null; + final boolean isAlphaMode = current == mQwertyKeyboard + || current == mQwertyKeyboardShifted; + if (mKeyboardState == KEYBOARD_STATE_NORMAL) { + mKeyboardState = isAlphaMode ? KEYBOARD_STATE_SHIFTED : KEYBOARD_STATE_CAPSLOCK; + next = isAlphaMode ? mQwertyKeyboardShifted : mSymbolsKeyboardShifted; + } else if (mKeyboardState == KEYBOARD_STATE_SHIFTED) { + mKeyboardState = KEYBOARD_STATE_CAPSLOCK; + next = isAlphaMode ? mQwertyKeyboardShifted : mSymbolsKeyboardShifted; + } else if (mKeyboardState == KEYBOARD_STATE_CAPSLOCK) { + mKeyboardState = KEYBOARD_STATE_NORMAL; + next = isAlphaMode ? mQwertyKeyboard : mSymbolsKeyboard; + } + if (next != null) { + if (next != current) { + mKeyboardView.setKeyboard(next); + } + next.setShiftLocked(mKeyboardState == KEYBOARD_STATE_CAPSLOCK); + mKeyboardView.setShifted(mKeyboardState != KEYBOARD_STATE_NORMAL); + } + } + + private void handleCharacter(int primaryCode, int[] keyCodes) { + // Maybe turn off shift if not in capslock mode. + if (mKeyboardView.isShifted() && primaryCode != ' ' && primaryCode != '\n') { + primaryCode = Character.toUpperCase(primaryCode); + } + sendKeyEventsToTarget(primaryCode); + } + + private void handleClose() { + + } + + public void onPress(int primaryCode) { + // TODO: vibration support. + } + + public void onRelease(int primaryCode) { + + } + + public void onText(CharSequence text) { + + } + + public void swipeDown() { + + } + + public void swipeLeft() { + + } + + public void swipeRight() { + + } + + public void swipeUp() { + + } +}; diff --git a/core/java/com/android/internal/widget/PasswordEntryKeyboardView.java b/core/java/com/android/internal/widget/PasswordEntryKeyboardView.java new file mode 100644 index 0000000..3e6f6f3 --- /dev/null +++ b/core/java/com/android/internal/widget/PasswordEntryKeyboardView.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.android.internal.widget; + +import android.content.Context; +import android.inputmethodservice.KeyboardView; +import android.util.AttributeSet; + +public class PasswordEntryKeyboardView extends KeyboardView { + + static final int KEYCODE_OPTIONS = -100; + static final int KEYCODE_SHIFT_LONGPRESS = -101; + static final int KEYCODE_VOICE = -102; + static final int KEYCODE_F1 = -103; + static final int KEYCODE_NEXT_LANGUAGE = -104; + + public PasswordEntryKeyboardView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public PasswordEntryKeyboardView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + +} diff --git a/core/java/com/android/internal/widget/WeightedLinearLayout.java b/core/java/com/android/internal/widget/WeightedLinearLayout.java index b90204e..3d09f08 100644 --- a/core/java/com/android/internal/widget/WeightedLinearLayout.java +++ b/core/java/com/android/internal/widget/WeightedLinearLayout.java @@ -52,7 +52,8 @@ public class WeightedLinearLayout extends LinearLayout { @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { final DisplayMetrics metrics = getContext().getResources().getDisplayMetrics(); - final boolean isPortrait = metrics.widthPixels < metrics.heightPixels; + final int screenWidth = metrics.widthPixels; + final boolean isPortrait = screenWidth < metrics.heightPixels; final int widthMode = getMode(widthMeasureSpec); @@ -62,14 +63,13 @@ public class WeightedLinearLayout extends LinearLayout { int height = getMeasuredHeight(); boolean measure = false; - final int widthSize = getSize(widthMeasureSpec); widthMeasureSpec = MeasureSpec.makeMeasureSpec(width, EXACTLY); heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, EXACTLY); final float widthWeight = isPortrait ? mMinorWeight : mMajorWeight; if (widthMode == AT_MOST && widthWeight > 0.0f) { - if (width < (widthSize * widthWeight)) { - widthMeasureSpec = MeasureSpec.makeMeasureSpec((int) (widthSize * widthWeight), + if (width < (screenWidth * widthWeight)) { + widthMeasureSpec = MeasureSpec.makeMeasureSpec((int) (screenWidth * widthWeight), EXACTLY); measure = true; } diff --git a/core/java/com/google/android/net/ParentalControl.java b/core/java/com/google/android/net/ParentalControl.java deleted file mode 100644 index 71a3958..0000000 --- a/core/java/com/google/android/net/ParentalControl.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright (C) 2008 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.android.net; - -import android.os.ICheckinService; -import android.os.IParentalControlCallback; -import android.os.RemoteException; -import android.os.ServiceManager; -import android.util.Log; - -public class ParentalControl { - /** - * Strings to identify your app. To enable parental control checking for - * new apps, please add it here, and configure GServices accordingly. - */ - public static final String VENDING = "vending"; - public static final String YOUTUBE = "youtube"; - - /** - * This interface is supplied to getParentalControlState and is callback upon with - * the state of parental control. - */ - public interface Callback { - /** - * This method will be called when the state of parental control is known. If state is - * null, then the state of parental control is unknown. - * @param state The state of parental control. - */ - void onResult(ParentalControlState state); - } - - private static class RemoteCallback extends IParentalControlCallback.Stub { - private Callback mCallback; - - public RemoteCallback(Callback callback) { - mCallback = callback; - } - - public void onResult(ParentalControlState state) { - if (mCallback != null) { - mCallback.onResult(state); - } - } - }; - - public static void getParentalControlState(Callback callback, - String requestingApp) { - ICheckinService service = - ICheckinService.Stub.asInterface(ServiceManager.getService("checkin")); - - RemoteCallback remoteCallback = new RemoteCallback(callback); - try { - service.getParentalControlState(remoteCallback, requestingApp); - } catch (RemoteException e) { - // This should never happen. - Log.e("ParentalControl", "Failed to talk to the checkin service."); - } - } -} diff --git a/core/java/com/google/android/net/ParentalControlState.java b/core/java/com/google/android/net/ParentalControlState.java deleted file mode 100644 index 162a1f6..0000000 --- a/core/java/com/google/android/net/ParentalControlState.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (C) 2008 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.android.net; - -import android.os.Parcel; -import android.os.Parcelable; - -public class ParentalControlState implements Parcelable { - public boolean isEnabled; - public String redirectUrl; - - /** - * Used to read a ParentalControlStatus from a Parcel. - */ - public static final Parcelable.Creator<ParentalControlState> CREATOR = - new Parcelable.Creator<ParentalControlState>() { - public ParentalControlState createFromParcel(Parcel source) { - ParentalControlState status = new ParentalControlState(); - status.isEnabled = (source.readInt() == 1); - status.redirectUrl = source.readString(); - return status; - } - - public ParentalControlState[] newArray(int size) { - return new ParentalControlState[size]; - } - }; - - public int describeContents() { - return 0; - } - - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(isEnabled ? 1 : 0); - dest.writeString(redirectUrl); - } - - @Override - public String toString() { - return isEnabled + ", " + redirectUrl; - } -}; |