diff options
Diffstat (limited to 'core/java/android')
18 files changed, 1452 insertions, 759 deletions
diff --git a/core/java/android/content/ContentResolver.java b/core/java/android/content/ContentResolver.java index e914604..840fd4a 100644 --- a/core/java/android/content/ContentResolver.java +++ b/core/java/android/content/ContentResolver.java @@ -141,7 +141,7 @@ public abstract class ContentResolver { public static final String SYNC_EXTRAS_PRIORITY = "sync_priority"; /** {@hide} Flag to allow sync to occur on metered network. */ - public static final String SYNC_EXTRAS_DISALLOW_METERED = "disallow_metered"; + public static final String SYNC_EXTRAS_DISALLOW_METERED = "allow_metered"; /** * Set by the SyncManager to request that the SyncAdapter initialize itself for @@ -1752,7 +1752,7 @@ public abstract class ContentResolver { new SyncRequest.Builder() .setSyncAdapter(account, authority) .setExtras(extras) - .syncOnce() + .syncOnce(0, 0) // Immediate sync. .build(); requestSync(request); } @@ -1760,9 +1760,6 @@ public abstract class ContentResolver { /** * Register a sync with the SyncManager. These requests are built using the * {@link SyncRequest.Builder}. - * - * @param request The immutable SyncRequest object containing the sync parameters. Use - * {@link SyncRequest.Builder} to construct these. */ public static void requestSync(SyncRequest request) { try { @@ -1830,8 +1827,21 @@ public abstract class ContentResolver { */ public static void cancelSync(Account account, String authority) { try { - getContentService().cancelSync(account, authority); + getContentService().cancelSync(account, authority, null); + } catch (RemoteException e) { + } + } + + /** + * Cancel any active or pending syncs that are running on this service. + * + * @param cname the service for which to cancel all active/pending operations. + */ + public static void cancelSync(ComponentName cname) { + try { + getContentService().cancelSync(null, null, cname); } catch (RemoteException e) { + } } @@ -1898,12 +1908,13 @@ public abstract class ContentResolver { * {@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. - * <p>As of API level 19 this function introduces a default flexibility of ~4% (up to a maximum - * of one hour in the day) into the requested period. Use - * {@link SyncRequest.Builder#syncPeriodic(long, long)} to set this flexibility manually. * * <p>This method requires the caller to hold the permission * {@link android.Manifest.permission#WRITE_SYNC_SETTINGS}. + * <p>The bundle for a periodic sync can be queried by applications with the correct + * permissions using + * {@link ContentResolver#getPeriodicSyncs(Account account, String provider)}, so no + * sensitive data should be transferred here. * * @param account the account to specify in the sync * @param authority the provider to specify in the sync request @@ -1921,13 +1932,7 @@ public abstract class ContentResolver { 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)) { + if (invalidPeriodicExtras(extras)) { throw new IllegalArgumentException("illegal extras were set"); } try { @@ -1939,6 +1944,26 @@ public abstract class ContentResolver { } /** + * {@hide} + * Helper function to throw an <code>IllegalArgumentException</code> if any illegal + * extras were set for a periodic sync. + * + * @param extras bundle to validate. + */ + public static boolean invalidPeriodicExtras(Bundle extras) { + if (extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false) + || extras.getBoolean(ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY, false) + || extras.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, false) + || extras.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, false) + || extras.getBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, false) + || extras.getBoolean(ContentResolver.SYNC_EXTRAS_FORCE, false) + || extras.getBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, false)) { + return true; + } + return false; + } + + /** * Remove a periodic sync. Has no affect if account, authority and extras don't match * an existing periodic sync. * <p>This method requires the caller to hold the permission @@ -1964,6 +1989,31 @@ public abstract class ContentResolver { } /** + * Remove the specified sync. This will cancel any pending or active syncs. If the request is + * for a periodic sync, this call will remove any future occurrences. + * <p>If a periodic sync is specified, the caller must hold the permission + * {@link android.Manifest.permission#WRITE_SYNC_SETTINGS}. If this SyncRequest targets a + * SyncService adapter,the calling application must be signed with the same certificate as the + * adapter. + *</p>It is possible to cancel a sync using a SyncRequest object that is not the same object + * with which you requested the sync. Do so by building a SyncRequest with the same + * service/adapter, frequency, <b>and</b> extras bundle. + * + * @param request SyncRequest object containing information about sync to cancel. + */ + public static void cancelSync(SyncRequest request) { + if (request == null) { + throw new IllegalArgumentException("request cannot be null"); + } + try { + getContentService().cancelRequest(request); + } catch (RemoteException e) { + // exception ignored; if this is thrown then it means the runtime is in the midst of + // being restarted + } + } + + /** * Get the list of information about the periodic syncs for the given account and authority. * <p>This method requires the caller to hold the permission * {@link android.Manifest.permission#READ_SYNC_SETTINGS}. @@ -1980,7 +2030,23 @@ public abstract class ContentResolver { throw new IllegalArgumentException("authority must not be null"); } try { - return getContentService().getPeriodicSyncs(account, authority); + return getContentService().getPeriodicSyncs(account, authority, null); + } catch (RemoteException e) { + throw new RuntimeException("the ContentService should always be reachable", e); + } + } + + /** + * Return periodic syncs associated with the provided component. + * <p>The calling application must be signed with the same certificate as the target component, + * otherwise this call will fail. + */ + public static List<PeriodicSync> getPeriodicSyncs(ComponentName cname) { + if (cname == null) { + throw new IllegalArgumentException("Component must not be null"); + } + try { + return getContentService().getPeriodicSyncs(null, null, cname); } catch (RemoteException e) { throw new RuntimeException("the ContentService should always be reachable", e); } @@ -2016,6 +2082,38 @@ public abstract class ContentResolver { } /** + * Set whether the provided {@link SyncService} is available to process work. + * <p>This method requires the caller to hold the permission + * {@link android.Manifest.permission#WRITE_SYNC_SETTINGS}. + * <p>The calling application must be signed with the same certificate as the target component, + * otherwise this call will fail. + */ + public static void setServiceActive(ComponentName cname, boolean active) { + try { + getContentService().setServiceActive(cname, active); + } catch (RemoteException e) { + // exception ignored; if this is thrown then it means the runtime is in the midst of + // being restarted + } + } + + /** + * Query the state of this sync service. + * <p>Set with {@link #setServiceActive(ComponentName cname, boolean active)}. + * <p>The calling application must be signed with the same certificate as the target component, + * otherwise this call will fail. + * @param cname ComponentName referring to a {@link SyncService} + * @return true if jobs will be run on this service, false otherwise. + */ + public static boolean isServiceActive(ComponentName cname) { + try { + return getContentService().isServiceActive(cname); + } catch (RemoteException e) { + throw new RuntimeException("the ContentService should always be reachable", e); + } + } + + /** * Gets the master auto-sync setting that applies to all the providers and accounts. * If this is false then the per-provider auto-sync setting is ignored. * <p>This method requires the caller to hold the permission @@ -2049,8 +2147,8 @@ public abstract class ContentResolver { } /** - * Returns true if there is currently a sync operation for the given - * account or authority in the pending list, or actively being processed. + * Returns true if there is currently a sync operation for the given account or authority + * actively being processed. * <p>This method requires the caller to hold the permission * {@link android.Manifest.permission#READ_SYNC_STATS}. * @param account the account whose setting we are querying @@ -2058,8 +2156,26 @@ public abstract class ContentResolver { * @return true if a sync is active for the given account or authority. */ public static boolean isSyncActive(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().isSyncActive(account, authority, null); + } catch (RemoteException e) { + throw new RuntimeException("the ContentService should always be reachable", e); + } + } + + public static boolean isSyncActive(ComponentName cname) { + if (cname == null) { + throw new IllegalArgumentException("component name must not be null"); + } try { - return getContentService().isSyncActive(account, authority); + return getContentService().isSyncActive(null, null, cname); } catch (RemoteException e) { throw new RuntimeException("the ContentService should always be reachable", e); } @@ -2117,7 +2233,7 @@ public abstract class ContentResolver { */ public static SyncStatusInfo getSyncStatus(Account account, String authority) { try { - return getContentService().getSyncStatus(account, authority); + return getContentService().getSyncStatus(account, authority, null); } catch (RemoteException e) { throw new RuntimeException("the ContentService should always be reachable", e); } @@ -2133,7 +2249,15 @@ public abstract class ContentResolver { */ public static boolean isSyncPending(Account account, String authority) { try { - return getContentService().isSyncPending(account, authority); + return getContentService().isSyncPending(account, authority, null); + } catch (RemoteException e) { + throw new RuntimeException("the ContentService should always be reachable", e); + } + } + + public static boolean isSyncPending(ComponentName cname) { + try { + return getContentService().isSyncPending(null, null, cname); } catch (RemoteException e) { throw new RuntimeException("the ContentService should always be reachable", e); } diff --git a/core/java/android/content/IContentService.aidl b/core/java/android/content/IContentService.aidl index 9ad5a19..73a76e8 100644 --- a/core/java/android/content/IContentService.aidl +++ b/core/java/android/content/IContentService.aidl @@ -17,6 +17,7 @@ package android.content; import android.accounts.Account; +import android.content.ComponentName; import android.content.SyncInfo; import android.content.ISyncStatusObserver; import android.content.SyncAdapterType; @@ -55,8 +56,14 @@ interface IContentService { int userHandle); void requestSync(in Account account, String authority, in Bundle extras); + /** + * Start a sync given a request. + */ void sync(in SyncRequest request); - void cancelSync(in Account account, String authority); + void cancelSync(in Account account, String authority, in ComponentName cname); + + /** Cancel a sync, providing information about the sync to be cancelled. */ + void cancelRequest(in SyncRequest request); /** * Check if the provider should be synced when a network tickle is received @@ -74,12 +81,14 @@ 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. + * Get a list of periodic operations for a specified authority, or service. + * @param account account for authority, must be null if cname is non-null. + * @param providerName name of provider, must be null if cname is non-null. + * @param cname component to identify sync service, must be null if account/providerName are + * non-null. */ - List<PeriodicSync> getPeriodicSyncs(in Account account, String providerName); + List<PeriodicSync> getPeriodicSyncs(in Account account, String providerName, + in ComponentName cname); /** * Set whether or not the provider is to be synced on a periodic basis. @@ -112,15 +121,22 @@ interface IContentService { */ void setIsSyncable(in Account account, String providerName, int syncable); - void setMasterSyncAutomatically(boolean flag); - - boolean getMasterSyncAutomatically(); + /** + * Corresponds roughly to setIsSyncable(String account, String provider) for syncs that bind + * to a SyncService. + */ + void setServiceActive(in ComponentName cname, boolean active); /** - * Returns true if there is currently a sync operation for the given - * account or authority in the pending list, or actively being processed. + * Corresponds roughly to getIsSyncable(String account, String provider) for syncs that bind + * to a SyncService. + * @return 0 if this SyncService is not enabled, 1 if enabled, <0 if unknown. */ - boolean isSyncActive(in Account account, String authority); + boolean isServiceActive(in ComponentName cname); + + void setMasterSyncAutomatically(boolean flag); + + boolean getMasterSyncAutomatically(); List<SyncInfo> getCurrentSyncs(); @@ -131,17 +147,33 @@ interface IContentService { SyncAdapterType[] getSyncAdapterTypes(); /** + * Returns true if there is currently a operation for the given account/authority or service + * actively being processed. + * @param account account for authority, must be null if cname is non-null. + * @param providerName name of provider, must be null if cname is non-null. + * @param cname component to identify sync service, must be null if account/providerName are + * non-null. + */ + boolean isSyncActive(in Account account, String authority, in ComponentName cname); + + /** * Returns the status that matches the authority. If there are multiples accounts for * the authority, the one with the latest "lastSuccessTime" status is returned. - * @param authority the authority whose row should be selected - * @return the SyncStatusInfo for the authority, or null if none exists + * @param account account for authority, must be null if cname is non-null. + * @param providerName name of provider, must be null if cname is non-null. + * @param cname component to identify sync service, must be null if account/providerName are + * non-null. */ - SyncStatusInfo getSyncStatus(in Account account, String authority); + SyncStatusInfo getSyncStatus(in Account account, String authority, in ComponentName cname); /** * Return true if the pending status is true of any matching authorities. + * @param account account for authority, must be null if cname is non-null. + * @param providerName name of provider, must be null if cname is non-null. + * @param cname component to identify sync service, must be null if account/providerName are + * non-null. */ - boolean isSyncPending(in Account account, String authority); + boolean isSyncPending(in Account account, String authority, in ComponentName cname); void addStatusChangeListener(int mask, ISyncStatusObserver callback); diff --git a/core/java/android/content/IAnonymousSyncAdapter.aidl b/core/java/android/content/ISyncServiceAdapter.aidl index a80cea3..d419307 100644 --- a/core/java/android/content/IAnonymousSyncAdapter.aidl +++ b/core/java/android/content/ISyncServiceAdapter.aidl @@ -24,7 +24,7 @@ import android.content.ISyncContext; * Provider specified). See {@link android.content.AbstractThreadedSyncAdapter}. * {@hide} */ -oneway interface IAnonymousSyncAdapter { +oneway interface ISyncServiceAdapter { /** * Initiate a sync. SyncAdapter-specific parameters may be specified in diff --git a/core/java/android/content/PeriodicSync.java b/core/java/android/content/PeriodicSync.java index b586eec..836c6f8 100644 --- a/core/java/android/content/PeriodicSync.java +++ b/core/java/android/content/PeriodicSync.java @@ -29,13 +29,17 @@ public class PeriodicSync implements Parcelable { public final Account account; /** The authority of the sync. Can be null. */ public final String authority; + /** The service for syncing, if this is an anonymous sync. Can be null.*/ + public final ComponentName service; /** 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. Kept around for API purposes. */ public final long period; + /** Whether this periodic sync runs on a {@link SyncService}. */ + public final boolean isService; /** - * {@hide} * How much flexibility can be taken in scheduling the sync, in seconds. + * {@hide} */ public final long flexTime; @@ -48,44 +52,74 @@ public class PeriodicSync implements Parcelable { public PeriodicSync(Account account, String authority, Bundle extras, long periodInSeconds) { this.account = account; this.authority = authority; + this.service = null; + this.isService = false; if (extras == null) { this.extras = new Bundle(); } else { this.extras = new Bundle(extras); } this.period = periodInSeconds; - // Initialise to a sane value. + // Old API uses default flex time. No-one should be using this ctor anyway. this.flexTime = 0L; } /** - * {@hide} * Create a copy of a periodic sync. + * {@hide} */ public PeriodicSync(PeriodicSync other) { this.account = other.account; this.authority = other.authority; + this.service = other.service; + this.isService = other.isService; this.extras = new Bundle(other.extras); this.period = other.period; this.flexTime = other.flexTime; } /** - * {@hide} * A PeriodicSync for a sync with a specified provider. + * {@hide} */ public PeriodicSync(Account account, String authority, Bundle extras, long period, long flexTime) { this.account = account; this.authority = authority; + this.service = null; + this.isService = false; + this.extras = new Bundle(extras); + this.period = period; + this.flexTime = flexTime; + } + + /** + * A PeriodicSync for a sync with a specified SyncService. + * {@hide} + */ + public PeriodicSync(ComponentName service, Bundle extras, + long period, + long flexTime) { + this.account = null; + this.authority = null; + this.service = service; + this.isService = true; this.extras = new Bundle(extras); this.period = period; this.flexTime = flexTime; } private PeriodicSync(Parcel in) { - this.account = in.readParcelable(null); - this.authority = in.readString(); + this.isService = (in.readInt() != 0); + if (this.isService) { + this.service = in.readParcelable(null); + this.account = null; + this.authority = null; + } else { + this.account = in.readParcelable(null); + this.authority = in.readString(); + this.service = null; + } this.extras = in.readBundle(); this.period = in.readLong(); this.flexTime = in.readLong(); @@ -98,8 +132,13 @@ public class PeriodicSync implements Parcelable { @Override public void writeToParcel(Parcel dest, int flags) { - dest.writeParcelable(account, flags); - dest.writeString(authority); + dest.writeInt(isService ? 1 : 0); + if (account == null && authority == null) { + dest.writeParcelable(service, flags); + } else { + dest.writeParcelable(account, flags); + dest.writeString(authority); + } dest.writeBundle(extras); dest.writeLong(period); dest.writeLong(flexTime); @@ -126,14 +165,24 @@ public class PeriodicSync implements Parcelable { return false; } final PeriodicSync other = (PeriodicSync) o; - return account.equals(other.account) - && authority.equals(other.authority) + if (this.isService != other.isService) { + return false; + } + boolean equal = false; + if (this.isService) { + equal = service.equals(other.service); + } else { + equal = account.equals(other.account) + && authority.equals(other.authority); + } + return equal && period == other.period && syncExtrasEquals(extras, other.extras); } /** - * Periodic sync extra comparison function. + * Periodic sync extra comparison function. Duplicated from + * {@link com.android.server.content.SyncManager#syncExtrasEquals(Bundle b1, Bundle b2)} * {@hide} */ public static boolean syncExtrasEquals(Bundle b1, Bundle b2) { @@ -158,6 +207,7 @@ public class PeriodicSync implements Parcelable { public String toString() { return "account: " + account + ", authority: " + authority + + ", service: " + service + ". period: " + period + "s " + ", flex: " + flexTime; } diff --git a/core/java/android/content/SyncInfo.java b/core/java/android/content/SyncInfo.java index 0284882..61b11c2 100644 --- a/core/java/android/content/SyncInfo.java +++ b/core/java/android/content/SyncInfo.java @@ -17,6 +17,7 @@ package android.content; import android.accounts.Account; +import android.content.pm.RegisteredServicesCache; import android.os.Parcel; import android.os.Parcelable; import android.os.Parcelable.Creator; @@ -29,16 +30,24 @@ public class SyncInfo implements Parcelable { public final int authorityId; /** - * The {@link Account} that is currently being synced. + * The {@link Account} that is currently being synced. Will be null if this sync is running via + * a {@link SyncService}. */ public final Account account; /** - * The authority of the provider that is currently being synced. + * The authority of the provider that is currently being synced. Will be null if this sync + * is running via a {@link SyncService}. */ public final String authority; /** + * The {@link SyncService} that is targeted by this operation. Null if this sync is running via + * a {@link AbstractThreadedSyncAdapter}. + */ + public final ComponentName service; + + /** * The start time of the current sync operation in milliseconds since boot. * This is represented in elapsed real time. * See {@link android.os.SystemClock#elapsedRealtime()}. @@ -46,12 +55,13 @@ public class SyncInfo implements Parcelable { public final long startTime; /** @hide */ - public SyncInfo(int authorityId, Account account, String authority, + public SyncInfo(int authorityId, Account account, String authority, ComponentName service, long startTime) { this.authorityId = authorityId; this.account = account; this.authority = authority; this.startTime = startTime; + this.service = service; } /** @hide */ @@ -62,17 +72,20 @@ public class SyncInfo implements Parcelable { /** @hide */ public void writeToParcel(Parcel parcel, int flags) { parcel.writeInt(authorityId); - account.writeToParcel(parcel, 0); + parcel.writeParcelable(account, flags); parcel.writeString(authority); parcel.writeLong(startTime); + parcel.writeParcelable(service, flags); + } /** @hide */ SyncInfo(Parcel parcel) { authorityId = parcel.readInt(); - account = new Account(parcel); + account = parcel.readParcelable(Account.class.getClassLoader()); authority = parcel.readString(); startTime = parcel.readLong(); + service = parcel.readParcelable(ComponentName.class.getClassLoader()); } /** @hide */ diff --git a/core/java/android/content/SyncRequest.java b/core/java/android/content/SyncRequest.java index d4e0c2a..201a8b3 100644 --- a/core/java/android/content/SyncRequest.java +++ b/core/java/android/content/SyncRequest.java @@ -20,18 +20,19 @@ import android.accounts.Account; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; +import android.util.Pair; public class SyncRequest implements Parcelable { private static final String TAG = "SyncRequest"; - /** Account to pass to the sync adapter. May be null. */ + /** Account to pass to the sync adapter. Can be null. */ private final Account mAccountToSync; /** Authority string that corresponds to a ContentProvider. */ private final String mAuthority; - /** Sync service identifier. May be null.*/ + /** {@link SyncService} identifier. */ private final ComponentName mComponentInfo; /** Bundle containing user info as well as sync settings. */ private final Bundle mExtras; - /** Disallow this sync request on metered networks. */ + /** Don't allow this sync request on metered networks. */ private final boolean mDisallowMetered; /** * Anticipated upload size in bytes. @@ -69,18 +70,14 @@ public class SyncRequest implements Parcelable { return mIsPeriodic; } - /** - * {@hide} - * @return whether this is an expedited sync. - */ public boolean isExpedited() { return mIsExpedited; } /** * {@hide} - * @return true if this sync uses an account/authority pair, or false if this sync is bound to - * a Sync Service. + * @return true if this sync uses an account/authority pair, or false if + * this is an anonymous sync bound to an @link AnonymousSyncService. */ public boolean hasAuthority() { return mIsAuthority; @@ -88,34 +85,51 @@ public class SyncRequest implements Parcelable { /** * {@hide} + * * @return account object for this sync. - * @throws IllegalArgumentException if this function is called for a request that does not - * specify an account/provider authority. + * @throws IllegalArgumentException if this function is called for a request that targets a + * sync service. */ public Account getAccount() { if (!hasAuthority()) { - throw new IllegalArgumentException("Cannot getAccount() for a sync that does not" - + "specify an authority."); + throw new IllegalArgumentException("Cannot getAccount() for a sync that targets a sync" + + "service."); } return mAccountToSync; } /** * {@hide} + * * @return provider for this sync. - * @throws IllegalArgumentException if this function is called for a request that does not - * specify an account/provider authority. + * @throws IllegalArgumentException if this function is called for a request that targets a + * sync service. */ public String getProvider() { if (!hasAuthority()) { - throw new IllegalArgumentException("Cannot getProvider() for a sync that does not" - + "specify a provider."); + throw new IllegalArgumentException("Cannot getProvider() for a sync that targets a" + + "sync service."); } return mAuthority; } /** * {@hide} + * Throws a runtime IllegalArgumentException if this function is called for a + * SyncRequest that is bound to an account/provider. + * + * @return ComponentName for the service that this sync will bind to. + */ + public ComponentName getService() { + if (hasAuthority()) { + throw new IllegalArgumentException( + "Cannot getAnonymousService() for a sync that has specified a provider."); + } + return mComponentInfo; + } + + /** + * {@hide} * Retrieve bundle for this SyncRequest. Will not be null. */ public Bundle getBundle() { @@ -129,7 +143,6 @@ public class SyncRequest implements Parcelable { public long getSyncFlexTime() { return mSyncFlexTimeSecs; } - /** * {@hide} * @return the last point in time at which this sync must scheduled. @@ -216,7 +229,7 @@ public class SyncRequest implements Parcelable { } /** - * Builder class for a {@link SyncRequest}. As you build your SyncRequest this class will also + * Builder class for a @link SyncRequest. As you build your SyncRequest this class will also * perform validation. */ public static class Builder { @@ -232,9 +245,12 @@ public class SyncRequest implements Parcelable { private static final int SYNC_TARGET_SERVICE = 1; /** Specify that this is a sync with a provider. */ private static final int SYNC_TARGET_ADAPTER = 2; - /** Earliest point of displacement into the future at which this sync can occur. */ + /** + * Earliest point of displacement into the future at which this sync can + * occur. + */ private long mSyncFlexTimeSecs; - /** Latest point of displacement into the future at which this sync must occur. */ + /** Displacement into the future at which this sync must occur. */ private long mSyncRunTimeSecs; /** * Sync configuration information - custom user data explicitly provided by the developer. @@ -283,8 +299,9 @@ public class SyncRequest implements Parcelable { private boolean mExpedited; /** - * The sync component that contains the sync logic if this is a provider-less sync, - * otherwise null. + * The {@link SyncService} component that + * contains the sync logic if this is a provider-less sync, otherwise + * null. */ private ComponentName mComponentName; /** @@ -302,29 +319,51 @@ public class SyncRequest implements Parcelable { } /** - * Request that a sync occur immediately. + * Developer can define timing constraints for this one-shot request. + * These values are elapsed real-time. + * + * @param whenSeconds The time in seconds at which you want this + * sync to occur. + * @param beforeSeconds The amount of time in advance of whenSeconds that this + * sync may be permitted to occur. This is rounded up to a minimum of 5 + * seconds, for any sync for which whenSeconds > 5. * * Example * <pre> - * SyncRequest.Builder builder = (new SyncRequest.Builder()).syncOnce(); + * Perform an immediate sync. + * SyncRequest.Builder builder = (new SyncRequest.Builder()).syncOnce(0, 0); + * That is, a sync 0 seconds from now with 0 seconds of flex. + * + * Perform a sync in exactly 5 minutes. + * SyncRequest.Builder builder = + * new SyncRequest.Builder().syncOnce(5 * MIN_IN_SECS, 0); + * + * Perform a sync in 5 minutes, with one minute of leeway (between 4 and 5 minutes from + * now). + * SyncRequest.Builder builder = + * new SyncRequest.Builder().syncOnce(5 * MIN_IN_SECS, 1 * MIN_IN_SECS); * </pre> */ - public Builder syncOnce() { + public Builder syncOnce(long whenSeconds, long beforeSeconds) { if (mSyncType != SYNC_TYPE_UNKNOWN) { throw new IllegalArgumentException("Sync type has already been defined."); } mSyncType = SYNC_TYPE_ONCE; - setupInterval(0, 0); + setupInterval(whenSeconds, beforeSeconds); return this; } /** * Build a periodic sync. Either this or syncOnce() <b>must</b> be called for this builder. - * Syncs are identified by target {@link android.provider}/{@link android.accounts.Account} - * and by the contents of the extras bundle. - * You cannot reuse the same builder for one-time syncs (by calling this function) after - * having specified a periodic sync. If you do, an <code>IllegalArgumentException</code> + * Syncs are identified by target {@link SyncService}/{@link android.provider} and by the + * contents of the extras bundle. + * You cannot reuse the same builder for one-time syncs after having specified a periodic + * sync (by calling this function). If you do, an <code>IllegalArgumentException</code> * will be thrown. + * <p>The bundle for a periodic sync can be queried by applications with the correct + * permissions using + * {@link ContentResolver#getPeriodicSyncs(Account account, String provider)}, so no + * sensitive data should be transferred here. * * Example usage. * @@ -375,7 +414,6 @@ public class SyncRequest implements Parcelable { } /** - * {@hide} * Developer can provide insight into their payload size; optional. -1 specifies unknown, * so that you are not restricted to defining both fields. * @@ -389,20 +427,28 @@ public class SyncRequest implements Parcelable { } /** - * @see android.net.ConnectivityManager#isActiveNetworkMetered() - * @param disallow true to enforce that this transfer not occur on metered networks. - * Default false. + * Will throw an <code>IllegalArgumentException</code> if called and + * {@link #setIgnoreSettings(boolean ignoreSettings)} has already been called. + * @param disallow true to allow this transfer on metered networks. Default false. + * */ public Builder setDisallowMetered(boolean disallow) { + if (mIgnoreSettings && disallow) { + throw new IllegalArgumentException("setDisallowMetered(true) after having" + + "specified that settings are ignored."); + } mDisallowMetered = disallow; return this; } /** - * Specify an authority and account for this transfer. + * Specify an authority and account for this transfer. Cannot be used with + * {@link #setSyncAdapter(ComponentName cname)}. * - * @param authority String identifying which content provider to sync. - * @param account Account to sync. Can be null unless this is a periodic sync. + * @param authority + * @param account Account to sync. Can be null unless this is a periodic + * sync, for which verification by the ContentResolver will + * fail. If a sync is performed without an account, the */ public Builder setSyncAdapter(Account account, String authority) { if (mSyncTarget != SYNC_TARGET_UNKNOWN) { @@ -416,10 +462,26 @@ public class SyncRequest implements Parcelable { } /** - * Optional developer-provided extras handed back in - * {@link AbstractThreadedSyncAdapter#onPerformSync(Account, Bundle, String, - * ContentProviderClient, SyncResult)} occurs. This bundle is copied into the SyncRequest - * returned by {@link #build()}. + * Specify the {@link SyncService} component for this sync. This is not validated until + * sync time so providing an incorrect component name here will not fail. Cannot be used + * with {@link #setSyncAdapter(Account account, String authority)}. + * + * @param cname ComponentName to identify your Anonymous service + */ + public Builder setSyncAdapter(ComponentName cname) { + if (mSyncTarget != SYNC_TARGET_UNKNOWN) { + throw new IllegalArgumentException("Sync target has already been defined."); + } + mSyncTarget = SYNC_TARGET_SERVICE; + mComponentName = cname; + mAccount = null; + mAuthority = null; + return this; + } + + /** + * Developer-provided extras handed back when sync actually occurs. This bundle is copied + * into the SyncRequest returned by {@link #build()}. * * Example: * <pre> @@ -433,7 +495,7 @@ public class SyncRequest implements Parcelable { * Bundle extras = new Bundle(); * extras.setString("data", syncData); * builder.setExtras(extras); - * ContentResolver.sync(builder.build()); // Each sync() request is for a unique sync. + * ContentResolver.sync(builder.build()); // Each sync() request creates a unique sync. * } * </pre> * Only values of the following types may be used in the extras bundle: @@ -474,13 +536,19 @@ public class SyncRequest implements Parcelable { /** * Convenience function for setting {@link ContentResolver#SYNC_EXTRAS_IGNORE_SETTINGS}. * - * A sync can specify that system sync settings be ignored (user has turned sync off). Not - * valid for periodic sync and will throw an <code>IllegalArgumentException</code> in + * Not valid for periodic sync and will throw an <code>IllegalArgumentException</code> in * {@link #build()}. + * <p>Throws <code>IllegalArgumentException</code> if called and + * {@link #setDisallowMetered(boolean)} has been set. + * * * @param ignoreSettings true to ignore the sync automatically settings. Default false. */ public Builder setIgnoreSettings(boolean ignoreSettings) { + if (mDisallowMetered && ignoreSettings) { + throw new IllegalArgumentException("setIgnoreSettings(true) after having specified" + + " sync settings with this builder."); + } mIgnoreSettings = ignoreSettings; return this; } @@ -488,13 +556,13 @@ public class SyncRequest implements Parcelable { /** * Convenience function for setting {@link ContentResolver#SYNC_EXTRAS_IGNORE_BACKOFF}. * - * Force the sync scheduling process to ignore any back-off that was the result of a failed - * sync, as well as to invalidate any {@link SyncResult#delayUntil} value that may have - * been set by the adapter. Successive failures will not honor this flag. Not valid for - * periodic sync and will throw an <code>IllegalArgumentException</code> in - * {@link #build()}. + * Ignoring back-off will force the sync scheduling process to ignore any back-off that was + * the result of a failed sync, as well as to invalidate any {@link SyncResult#delayUntil} + * value that may have been set by the adapter. Successive failures will not honor this + * flag. Not valid for periodic sync and will throw an <code>IllegalArgumentException</code> + * in {@link #build()}. * - * @param ignoreBackoff ignore back-off settings. Default false. + * @param ignoreBackoff ignore back off settings. Default false. */ public Builder setIgnoreBackoff(boolean ignoreBackoff) { mIgnoreBackoff = ignoreBackoff; @@ -504,9 +572,8 @@ public class SyncRequest implements Parcelable { /** * Convenience function for setting {@link ContentResolver#SYNC_EXTRAS_MANUAL}. * - * A manual sync is functionally equivalent to calling {@link #setIgnoreBackoff(boolean)} - * and {@link #setIgnoreSettings(boolean)}. Not valid for periodic sync and will throw an - * <code>IllegalArgumentException</code> in {@link #build()}. + * Not valid for periodic sync and will throw an <code>IllegalArgumentException</code> in + * {@link #build()}. * * @param isManual User-initiated sync or not. Default false. */ @@ -516,7 +583,7 @@ public class SyncRequest implements Parcelable { } /** - * An expedited sync runs immediately and will preempt another non-expedited running sync. + * An expedited sync runs immediately and can preempt other non-expedited running syncs. * * Not valid for periodic sync and will throw an <code>IllegalArgumentException</code> in * {@link #build()}. @@ -529,7 +596,6 @@ public class SyncRequest implements Parcelable { } /** - * {@hide} * @param priority the priority of this request among all requests from the calling app. * Range of [-2,2] similar to how this is done with notifications. */ @@ -549,11 +615,11 @@ public class SyncRequest implements Parcelable { * builder. */ public SyncRequest build() { + // Validate the extras bundle + ContentResolver.validateSyncExtrasBundle(mCustomExtras); if (mCustomExtras == null) { mCustomExtras = new Bundle(); } - // Validate the extras bundle - ContentResolver.validateSyncExtrasBundle(mCustomExtras); // Combine builder extra flags into the config bundle. mSyncConfigExtras = new Bundle(); if (mIgnoreBackoff) { @@ -572,51 +638,33 @@ public class SyncRequest implements Parcelable { mSyncConfigExtras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true); } if (mIsManual) { - mSyncConfigExtras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); + mSyncConfigExtras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, true); + mSyncConfigExtras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, true); } mSyncConfigExtras.putLong(ContentResolver.SYNC_EXTRAS_EXPECTED_UPLOAD, mTxBytes); mSyncConfigExtras.putLong(ContentResolver.SYNC_EXTRAS_EXPECTED_DOWNLOAD, mRxBytes); mSyncConfigExtras.putInt(ContentResolver.SYNC_EXTRAS_PRIORITY, mPriority); if (mSyncType == SYNC_TYPE_PERIODIC) { // If this is a periodic sync ensure than invalid extras were not set. - validatePeriodicExtras(mCustomExtras); - validatePeriodicExtras(mSyncConfigExtras); - // Verify that account and provider are not null. - if (mAccount == null) { - throw new IllegalArgumentException("Account must not be null for periodic" - + " sync."); - } - if (mAuthority == null) { - throw new IllegalArgumentException("Authority must not be null for periodic" - + " sync."); + if (ContentResolver.invalidPeriodicExtras(mCustomExtras) || + ContentResolver.invalidPeriodicExtras(mSyncConfigExtras)) { + throw new IllegalArgumentException("Illegal extras were set"); } } else if (mSyncType == SYNC_TYPE_UNKNOWN) { throw new IllegalArgumentException("Must call either syncOnce() or syncPeriodic()"); } + if (mSyncTarget == SYNC_TARGET_SERVICE) { + if (mSyncConfigExtras.getBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, false)) { + throw new IllegalArgumentException("Cannot specify an initialisation sync" + + " that targets a service."); + } + } // Ensure that a target for the sync has been set. if (mSyncTarget == SYNC_TARGET_UNKNOWN) { - throw new IllegalArgumentException("Must specify an adapter with " - + "setSyncAdapter(Account, String"); + throw new IllegalArgumentException("Must specify an adapter with one of" + + "setSyncAdapter(ComponentName) or setSyncAdapter(Account, String"); } return new SyncRequest(this); } - - /** - * Helper function to throw an <code>IllegalArgumentException</code> if any illegal - * extras were set for a periodic sync. - * - * @param extras bundle to validate. - */ - private void validatePeriodicExtras(Bundle extras) { - if (extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false) - || extras.getBoolean(ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY, false) - || extras.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, false) - || extras.getBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, false) - || extras.getBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, false) - || extras.getBoolean(ContentResolver.SYNC_EXTRAS_FORCE, false) - || extras.getBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, false)) { - throw new IllegalArgumentException("Illegal extras were set"); - } - } - } + } } diff --git a/core/java/android/content/SyncService.java b/core/java/android/content/SyncService.java new file mode 100644 index 0000000..3f99ce1 --- /dev/null +++ b/core/java/android/content/SyncService.java @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2013 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.app.Service; +import android.os.Bundle; +import android.os.IBinder; +import android.os.Process; +import android.os.Trace; +import android.util.ArrayMap; +import android.util.SparseArray; +import android.util.Log; + +import com.android.internal.annotations.GuardedBy; + +/** + * Simplified @link android.content.AbstractThreadedSyncAdapter. Folds that + * behaviour into a service to which the system can bind when requesting an + * anonymous (providerless/accountless) sync. + * <p> + * In order to perform an anonymous sync operation you must extend this service, implementing the + * abstract methods. This service must be declared in the application's manifest as usual. You + * can use this service for other work, however you <b> must not </b> override the onBind() method + * unless you know what you're doing, which limits the usefulness of this service for other work. + * <p>A {@link SyncService} can either be active or inactive. Different to an + * {@link AbstractThreadedSyncAdapter}, there is no + * {@link ContentResolver#setSyncAutomatically(android.accounts.Account account, String provider, boolean sync)}, + * as well as no concept of initialisation (you can handle your own if needed). + * + * <pre> + * <service android:name=".MySyncService"/> + * </pre> + * Like @link android.content.AbstractThreadedSyncAdapter this service supports + * multiple syncs at the same time. Each incoming startSync() with a unique tag + * will spawn a thread to do the work of that sync. If startSync() is called + * with a tag that already exists, a SyncResult.ALREADY_IN_PROGRESS is returned. + * Remember that your service will spawn multiple threads if you schedule multiple syncs + * at once, so if you mutate local objects you must ensure synchronization. + */ +public abstract class SyncService extends Service { + private static final String TAG = "SyncService"; + + private final SyncAdapterImpl mSyncAdapter = new SyncAdapterImpl(); + + /** Keep track of on-going syncs, keyed by bundle. */ + @GuardedBy("mSyncThreadLock") + private final SparseArray<SyncThread> + mSyncThreads = new SparseArray<SyncThread>(); + /** Lock object for accessing the SyncThreads HashMap. */ + private final Object mSyncThreadLock = new Object(); + /** + * Default key for if this sync service does not support parallel operations. Currently not + * sure if null keys will make it into the ArrayMap for KLP, so keeping our default for now. + */ + private static final int KEY_DEFAULT = 0; + /** Identifier for this sync service. */ + private ComponentName mServiceComponent; + + /** {@hide} */ + public IBinder onBind(Intent intent) { + mServiceComponent = new ComponentName(this, getClass()); + return mSyncAdapter.asBinder(); + } + + /** {@hide} */ + private class SyncAdapterImpl extends ISyncServiceAdapter.Stub { + @Override + public void startSync(ISyncContext syncContext, Bundle extras) { + // Wrap the provided Sync Context because it may go away by the time + // we call it. + final SyncContext syncContextClient = new SyncContext(syncContext); + boolean alreadyInProgress = false; + final int extrasAsKey = extrasToKey(extras); + synchronized (mSyncThreadLock) { + if (mSyncThreads.get(extrasAsKey) != null) { + Log.e(TAG, "starting sync for : " + mServiceComponent); + // Start sync. + SyncThread syncThread = new SyncThread(syncContextClient, extras); + mSyncThreads.put(extrasAsKey, syncThread); + syncThread.start(); + } else { + // Don't want to call back to SyncManager while still + // holding lock. + alreadyInProgress = true; + } + } + if (alreadyInProgress) { + syncContextClient.onFinished(SyncResult.ALREADY_IN_PROGRESS); + } + } + + /** + * Used by the SM to cancel a specific sync using the + * com.android.server.content.SyncManager.ActiveSyncContext as a handle. + */ + @Override + public void cancelSync(ISyncContext syncContext) { + SyncThread runningSync = null; + synchronized (mSyncThreadLock) { + for (int i = 0; i < mSyncThreads.size(); i++) { + SyncThread thread = mSyncThreads.valueAt(i); + if (thread.mSyncContext.getSyncContextBinder() == syncContext.asBinder()) { + runningSync = thread; + break; + } + } + } + if (runningSync != null) { + runningSync.interrupt(); + } + } + } + + /** + * + * @param extras Bundle for which to compute hash + * @return an integer hash that is equal to that of another bundle if they both contain the + * same key -> value mappings, however, not necessarily in order. + * Based on the toString() representation of the value mapped. + */ + private int extrasToKey(Bundle extras) { + int hash = KEY_DEFAULT; // Empty bundle, or no parallel operations enabled. + if (parallelSyncsEnabled()) { + for (String key : extras.keySet()) { + String mapping = key + " " + extras.get(key).toString(); + hash += mapping.hashCode(); + } + } + return hash; + } + + /** + * {@hide} + * Similar to {@link android.content.AbstractThreadedSyncAdapter.SyncThread}. However while + * the ATSA considers an already in-progress sync to be if the account provided is currently + * syncing, this anonymous sync has no notion of account and considers a sync unique if the + * provided bundle is different. + */ + private class SyncThread extends Thread { + private final SyncContext mSyncContext; + private final Bundle mExtras; + private final int mThreadsKey; + + public SyncThread(SyncContext syncContext, Bundle extras) { + mSyncContext = syncContext; + mExtras = extras; + mThreadsKey = extrasToKey(extras); + } + + @Override + public void run() { + Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); + + Trace.traceBegin(Trace.TRACE_TAG_SYNC_MANAGER, getApplication().getPackageName()); + + SyncResult syncResult = new SyncResult(); + try { + if (isCancelled()) return; + // Run the sync. + SyncService.this.onPerformSync(mExtras, syncResult); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_SYNC_MANAGER); + if (!isCancelled()) { + mSyncContext.onFinished(syncResult); + } + // Synchronize so that the assignment will be seen by other + // threads that also synchronize accesses to mSyncThreads. + synchronized (mSyncThreadLock) { + mSyncThreads.remove(mThreadsKey); + } + } + } + + private boolean isCancelled() { + return Thread.currentThread().isInterrupted(); + } + } + + /** + * Initiate an anonymous sync using this service. SyncAdapter-specific + * parameters may be specified in extras, which is guaranteed to not be + * null. + */ + public abstract void onPerformSync(Bundle extras, SyncResult syncResult); + + /** + * Override this function to indicated whether you want to support parallel syncs. + * <p>If you override and return true multiple threads will be spawned within your Service to + * handle each concurrent sync request. + * + * @return false to indicate that this service does not support parallel operations by default. + */ + protected boolean parallelSyncsEnabled() { + return false; + } +} diff --git a/core/java/android/os/BatteryProperty.aidl b/core/java/android/os/BatteryProperty.aidl new file mode 100644 index 0000000..b3f2ec3 --- /dev/null +++ b/core/java/android/os/BatteryProperty.aidl @@ -0,0 +1,19 @@ +/* +** Copyright 2013, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ + +package android.os; + +parcelable BatteryProperty; diff --git a/core/java/android/os/BatteryProperty.java b/core/java/android/os/BatteryProperty.java new file mode 100644 index 0000000..346f5de --- /dev/null +++ b/core/java/android/os/BatteryProperty.java @@ -0,0 +1,70 @@ +/* Copyright 2013, 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.Parcel; +import android.os.Parcelable; + +/** + * {@hide} + */ +public class BatteryProperty implements Parcelable { + /* + * Battery property identifiers. These must match the values in + * frameworks/native/include/batteryservice/BatteryService.h + */ + public static final int BATTERY_PROP_CHARGE_COUNTER = 1; + public static final int BATTERY_PROP_CURRENT_NOW = 2; + public static final int BATTERY_PROP_CURRENT_AVG = 3; + + public int valueInt; + + public BatteryProperty() { + valueInt = Integer.MIN_VALUE; + } + + /* + * Parcel read/write code must be kept in sync with + * frameworks/native/services/batteryservice/BatteryProperty.cpp + */ + + private BatteryProperty(Parcel p) { + readFromParcel(p); + } + + public void readFromParcel(Parcel p) { + valueInt = p.readInt(); + } + + public void writeToParcel(Parcel p, int flags) { + p.writeInt(valueInt); + } + + public static final Parcelable.Creator<BatteryProperty> CREATOR + = new Parcelable.Creator<BatteryProperty>() { + public BatteryProperty createFromParcel(Parcel p) { + return new BatteryProperty(p); + } + + public BatteryProperty[] newArray(int size) { + return new BatteryProperty[size]; + } + }; + + public int describeContents() { + return 0; + } +} diff --git a/core/java/android/os/IBatteryPropertiesRegistrar.aidl b/core/java/android/os/IBatteryPropertiesRegistrar.aidl index 376f6c9..fd01802 100644 --- a/core/java/android/os/IBatteryPropertiesRegistrar.aidl +++ b/core/java/android/os/IBatteryPropertiesRegistrar.aidl @@ -17,6 +17,7 @@ package android.os; import android.os.IBatteryPropertiesListener; +import android.os.BatteryProperty; /** * {@hide} @@ -25,4 +26,5 @@ import android.os.IBatteryPropertiesListener; interface IBatteryPropertiesRegistrar { void registerListener(IBatteryPropertiesListener listener); void unregisterListener(IBatteryPropertiesListener listener); + int getProperty(in int id, out BatteryProperty prop); } diff --git a/core/java/android/os/ParcelFileDescriptor.java b/core/java/android/os/ParcelFileDescriptor.java index 579971d..d4cadd3 100644 --- a/core/java/android/os/ParcelFileDescriptor.java +++ b/core/java/android/os/ParcelFileDescriptor.java @@ -821,6 +821,8 @@ public class ParcelFileDescriptor implements Parcelable, Closeable { */ @Override public void writeToParcel(Parcel out, int flags) { + // WARNING: This must stay in sync with Parcel::readParcelFileDescriptor() + // in frameworks/native/libs/binder/Parcel.cpp if (mWrapped != null) { mWrapped.writeToParcel(out, flags); } else { @@ -845,6 +847,8 @@ public class ParcelFileDescriptor implements Parcelable, Closeable { = new Parcelable.Creator<ParcelFileDescriptor>() { @Override public ParcelFileDescriptor createFromParcel(Parcel in) { + // WARNING: This must stay in sync with Parcel::writeParcelFileDescriptor() + // in frameworks/native/libs/binder/Parcel.cpp final FileDescriptor fd = in.readRawFileDescriptor(); FileDescriptor commChannel = null; if (in.readInt() != 0) { diff --git a/core/java/android/os/Process.java b/core/java/android/os/Process.java index cf9ddb3..f3af4be 100644 --- a/core/java/android/os/Process.java +++ b/core/java/android/os/Process.java @@ -892,19 +892,6 @@ public class Process { } /** - * Set the out-of-memory badness adjustment for a process. - * - * @param pid The process identifier to set. - * @param amt Adjustment value -- linux allows -16 to +15. - * - * @return Returns true if the underlying system supports this - * feature, else false. - * - * {@hide} - */ - public static final native boolean setOomAdj(int pid, int amt); - - /** * Adjust the swappiness level for a process. * * @param pid The process identifier to set. diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java index 8fc3ce3..bcd26f8 100644 --- a/core/java/android/webkit/WebView.java +++ b/core/java/android/webkit/WebView.java @@ -28,12 +28,9 @@ import android.graphics.drawable.Drawable; import android.net.http.SslCertificate; import android.os.Build; import android.os.Bundle; -import android.os.CancellationSignal; import android.os.Looper; import android.os.Message; -import android.os.ParcelFileDescriptor; import android.os.StrictMode; -import android.print.PrintAttributes; import android.util.AttributeSet; import android.util.Log; import android.view.KeyEvent; @@ -51,6 +48,7 @@ import android.widget.AbsoluteLayout; import java.io.BufferedWriter; import java.io.File; +import java.io.OutputStream; import java.util.Map; /** @@ -249,7 +247,7 @@ public class WebView extends AbsoluteLayout // Throwing an exception for incorrect thread usage if the // build target is JB MR2 or newer. Defaults to false, and is // set in the WebView constructor. - private static Boolean sEnforceThreadChecking = false; + private static volatile boolean sEnforceThreadChecking = false; /** * Transportation object for returning WebView across thread boundaries. @@ -1072,38 +1070,29 @@ public class WebView extends AbsoluteLayout * Exports the contents of this Webview as PDF. Only supported for API levels * {@link android.os.Build.VERSION_CODES#KITKAT} and above. * - * TODO(sgurun) the parameter list is stale. Fix it before unhiding. - * - * @param fd The FileDescriptor to export the PDF contents to. Cannot be null. + * @param out The stream to export the PDF contents to. Cannot be null. * @param width The page width. Should be larger than 0. * @param height The page height. Should be larger than 0. * @param resultCallback A callback to be invoked when the PDF content is exported. - * A true indicates success, and a false failure. Cannot be null. - * @param cancellationSignal Signal for cancelling the PDF conversion request. Must not - * be null. - * - * The PDF conversion is done asynchronously and the PDF output is written to the provided - * file descriptor. The caller should not close the file descriptor until the resultCallback - * is called, indicating PDF conversion is complete. Webview will never close the file - * descriptor. - * Limitations: Webview cannot be drawn during the PDF export so the application is - * recommended to take it offscreen, or putting in a layer with an overlaid progress - * UI / spinner. - * - * If the caller cancels the task using the cancellationSignal, the cancellation will be - * acked using the resultCallback signal. - * - * Throws an exception if an IO error occurs accessing the file descriptor. - * - * TODO(sgurun) margins, explain the units, make it public. + * A true indicates success, and a false failure. + * + * TODO: explain method parameters, margins, consider making the callback + * return more meaningful information, explain any threading concerns, HW + * draw limitations, and make it public. + * TODO: at the moment we are asking app to provide paper size information (width + * and height). This is likely not ideal (I think need margin info too). + * Another approach would be using PrintAttributes. This is to be clarified later. + * + * TODO: explain this webview will not draw during export (onDraw will clear to + * background color) so recommend taking it offscreen, or putting in a layer with an + * overlaid progress UI / spinner. * @hide */ - public void exportToPdf(ParcelFileDescriptor fd, PrintAttributes attributes, - ValueCallback<Boolean> resultCallback, CancellationSignal cancellationSignal) - throws java.io.IOException { + public void exportToPdf(OutputStream out, int width, int height, + ValueCallback<Boolean> resultCallback) { checkThread(); if (DebugFlags.TRACE_API) Log.d(LOGTAG, "exportToPdf"); - mProvider.exportToPdf(fd, attributes, resultCallback, cancellationSignal); + mProvider.exportToPdf(out, width, height, resultCallback); } /** diff --git a/core/java/android/webkit/WebViewClassic.java b/core/java/android/webkit/WebViewClassic.java index 3f22d53..4811ca5 100644 --- a/core/java/android/webkit/WebViewClassic.java +++ b/core/java/android/webkit/WebViewClassic.java @@ -57,12 +57,10 @@ import android.net.http.SslCertificate; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; -import android.os.CancellationSignal; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.SystemClock; -import android.print.PrintAttributes; import android.security.KeyChain; import android.text.Editable; import android.text.InputType; @@ -2897,11 +2895,11 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc * See {@link WebView#exportToPdf()} */ @Override - public void exportToPdf(android.os.ParcelFileDescriptor fd, PrintAttributes attributes, - ValueCallback<Boolean> resultCallback, CancellationSignal cancellationSignal) - throws java.io.IOException { + public void exportToPdf(java.io.OutputStream out, int width, int height, + ValueCallback<Boolean> resultCallback) { // K-only API not implemented in WebViewClassic. throw new IllegalStateException("This API not supported on Android 4.3 and earlier"); + } /** diff --git a/core/java/android/webkit/WebViewProvider.java b/core/java/android/webkit/WebViewProvider.java index d625d8a..17b4061 100644 --- a/core/java/android/webkit/WebViewProvider.java +++ b/core/java/android/webkit/WebViewProvider.java @@ -25,10 +25,7 @@ import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.net.http.SslCertificate; import android.os.Bundle; -import android.os.CancellationSignal; import android.os.Message; -import android.os.ParcelFileDescriptor; -import android.print.PrintAttributes; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; @@ -43,6 +40,7 @@ import android.webkit.WebView.PictureListener; import java.io.BufferedWriter; import java.io.File; +import java.io.OutputStream; import java.util.Map; /** @@ -149,9 +147,8 @@ public interface WebViewProvider { public Picture capturePicture(); - public void exportToPdf(ParcelFileDescriptor fd, PrintAttributes attributes, - ValueCallback<Boolean> resultCallback, CancellationSignal cancellationSignal) - throws java.io.IOException; + public void exportToPdf(OutputStream out, int width, int height, + ValueCallback<Boolean> resultCallback); public float getScale(); diff --git a/core/java/android/widget/OverScroller.java b/core/java/android/widget/OverScroller.java index f218199..7b3dd31 100644 --- a/core/java/android/widget/OverScroller.java +++ b/core/java/android/widget/OverScroller.java @@ -70,7 +70,11 @@ public class OverScroller { * @hide */ public OverScroller(Context context, Interpolator interpolator, boolean flywheel) { - mInterpolator = interpolator; + if (interpolator == null) { + mInterpolator = new Scroller.ViscousFluidInterpolator(); + } else { + mInterpolator = interpolator; + } mFlywheel = flywheel; mScrollerX = new SplineOverScroller(context); mScrollerY = new SplineOverScroller(context); @@ -112,7 +116,11 @@ public class OverScroller { } void setInterpolator(Interpolator interpolator) { - mInterpolator = interpolator; + if (interpolator == null) { + mInterpolator = new Scroller.ViscousFluidInterpolator(); + } else { + mInterpolator = interpolator; + } } /** @@ -302,14 +310,7 @@ public class OverScroller { final int duration = mScrollerX.mDuration; if (elapsedTime < duration) { - float q = (float) (elapsedTime) / duration; - - if (mInterpolator == null) { - q = Scroller.viscousFluid(q); - } else { - q = mInterpolator.getInterpolation(q); - } - + final float q = mInterpolator.getInterpolation(elapsedTime / (float) duration); mScrollerX.updateScroll(q); mScrollerY.updateScroll(q); } else { diff --git a/core/java/android/widget/Scroller.java b/core/java/android/widget/Scroller.java index 3bfd39d..1a0ce9c 100644 --- a/core/java/android/widget/Scroller.java +++ b/core/java/android/widget/Scroller.java @@ -61,6 +61,8 @@ import android.view.animation.Interpolator; * }</pre> */ public class Scroller { + private final Interpolator mInterpolator; + private int mMode; private int mStartX; @@ -81,7 +83,6 @@ public class Scroller { private float mDeltaX; private float mDeltaY; private boolean mFinished; - private Interpolator mInterpolator; private boolean mFlywheel; private float mVelocity; @@ -142,18 +143,8 @@ public class Scroller { SPLINE_TIME[i] = coef * ((1.0f - y) * P1 + y * P2) + y * y * y; } SPLINE_POSITION[NB_SAMPLES] = SPLINE_TIME[NB_SAMPLES] = 1.0f; - - // This controls the viscous fluid effect (how much of it) - sViscousFluidScale = 8.0f; - // must be set to 1.0 (used in viscousFluid()) - sViscousFluidNormalize = 1.0f; - sViscousFluidNormalize = 1.0f / viscousFluid(1.0f); - } - private static float sViscousFluidScale; - private static float sViscousFluidNormalize; - /** * Create a Scroller with the default duration and interpolator. */ @@ -178,7 +169,11 @@ public class Scroller { */ public Scroller(Context context, Interpolator interpolator, boolean flywheel) { mFinished = true; - mInterpolator = interpolator; + if (interpolator == null) { + mInterpolator = new ViscousFluidInterpolator(); + } else { + mInterpolator = interpolator; + } mPpi = context.getResources().getDisplayMetrics().density * 160.0f; mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction()); mFlywheel = flywheel; @@ -312,13 +307,7 @@ public class Scroller { if (timePassed < mDuration) { switch (mMode) { case SCROLL_MODE: - float x = timePassed * mDurationReciprocal; - - if (mInterpolator == null) - x = viscousFluid(x); - else - x = mInterpolator.getInterpolation(x); - + final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal); mCurrX = mStartX + Math.round(x * mDeltaX); mCurrY = mStartY + Math.round(x * mDeltaY); break; @@ -499,20 +488,6 @@ public class Scroller { return mFlingFriction * mPhysicalCoeff * Math.exp(DECELERATION_RATE / decelMinusOne * l); } - static float viscousFluid(float x) - { - x *= sViscousFluidScale; - if (x < 1.0f) { - x -= (1.0f - (float)Math.exp(-x)); - } else { - float start = 0.36787944117f; // 1/e == exp(-1) - x = 1.0f - (float)Math.exp(1.0f - x); - x = start + x * (1.0f - start); - } - x *= sViscousFluidNormalize; - return x; - } - /** * Stops the animation. Contrary to {@link #forceFinished(boolean)}, * aborting the animating cause the scroller to move to the final x and y @@ -583,4 +558,41 @@ public class Scroller { return !mFinished && Math.signum(xvel) == Math.signum(mFinalX - mStartX) && Math.signum(yvel) == Math.signum(mFinalY - mStartY); } + + static class ViscousFluidInterpolator implements Interpolator { + /** Controls the viscous fluid effect (how much of it). */ + private static final float VISCOUS_FLUID_SCALE = 8.0f; + + private static final float VISCOUS_FLUID_NORMALIZE; + private static final float VISCOUS_FLUID_OFFSET; + + static { + + // must be set to 1.0 (used in viscousFluid()) + VISCOUS_FLUID_NORMALIZE = 1.0f / viscousFluid(1.0f); + // account for very small floating-point error + VISCOUS_FLUID_OFFSET = 1.0f - VISCOUS_FLUID_NORMALIZE * viscousFluid(1.0f); + } + + private static float viscousFluid(float x) { + x *= VISCOUS_FLUID_SCALE; + if (x < 1.0f) { + x -= (1.0f - (float)Math.exp(-x)); + } else { + float start = 0.36787944117f; // 1/e == exp(-1) + x = 1.0f - (float)Math.exp(1.0f - x); + x = start + x * (1.0f - start); + } + return x; + } + + @Override + public float getInterpolation(float input) { + final float interpolated = VISCOUS_FLUID_NORMALIZE * viscousFluid(input); + if (input > 0) { + return input + VISCOUS_FLUID_OFFSET; + } + return input; + } + } } diff --git a/core/java/android/widget/TimePicker.java b/core/java/android/widget/TimePicker.java index c26cb24..cea2b8f 100644 --- a/core/java/android/widget/TimePicker.java +++ b/core/java/android/widget/TimePicker.java @@ -57,58 +57,7 @@ import java.util.Locale; @Widget public class TimePicker extends FrameLayout { - private static final boolean DEFAULT_ENABLED_STATE = true; - - private static final int HOURS_IN_HALF_DAY = 12; - - /** - * A no-op callback used in the constructor to avoid null checks later in - * the code. - */ - private static final OnTimeChangedListener NO_OP_CHANGE_LISTENER = new OnTimeChangedListener() { - public void onTimeChanged(TimePicker view, int hourOfDay, int minute) { - } - }; - - // state - private boolean mIs24HourView; - - private boolean mIsAm; - - // ui components - private final NumberPicker mHourSpinner; - - private final NumberPicker mMinuteSpinner; - - private final NumberPicker mAmPmSpinner; - - private final EditText mHourSpinnerInput; - - private final EditText mMinuteSpinnerInput; - - private final EditText mAmPmSpinnerInput; - - private final TextView mDivider; - - // Note that the legacy implementation of the TimePicker is - // using a button for toggling between AM/PM while the new - // version uses a NumberPicker spinner. Therefore the code - // accommodates these two cases to be backwards compatible. - private final Button mAmPmButton; - - private final String[] mAmPmStrings; - - private boolean mIsEnabled = DEFAULT_ENABLED_STATE; - - // callbacks - private OnTimeChangedListener mOnTimeChangedListener; - - private Calendar mTempCalendar; - - private Locale mCurrentLocale; - - private boolean mHourWithTwoDigit; - private char mHourFormat; + private TimePickerDelegate mDelegate; /** * The callback interface used to indicate the time has been adjusted. @@ -133,568 +82,756 @@ public class TimePicker extends FrameLayout { public TimePicker(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); + mDelegate = new LegacyTimePickerDelegate(this, context, attrs, defStyle); + } - // initialization based on locale - setCurrentLocale(Locale.getDefault()); - - // process style attributes - TypedArray attributesArray = context.obtainStyledAttributes( - attrs, R.styleable.TimePicker, defStyle, 0); - int layoutResourceId = attributesArray.getResourceId( - R.styleable.TimePicker_internalLayout, R.layout.time_picker); - attributesArray.recycle(); - - LayoutInflater inflater = (LayoutInflater) context.getSystemService( - Context.LAYOUT_INFLATER_SERVICE); - inflater.inflate(layoutResourceId, this, true); - - // hour - mHourSpinner = (NumberPicker) findViewById(R.id.hour); - mHourSpinner.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() { - public void onValueChange(NumberPicker spinner, int oldVal, int newVal) { - updateInputState(); - if (!is24HourView()) { - if ((oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY) - || (oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1)) { - mIsAm = !mIsAm; - updateAmPmControl(); - } - } - onTimeChanged(); - } - }); - mHourSpinnerInput = (EditText) mHourSpinner.findViewById(R.id.numberpicker_input); - mHourSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT); - - // divider (only for the new widget style) - mDivider = (TextView) findViewById(R.id.divider); - if (mDivider != null) { - setDividerText(); - } - - // minute - mMinuteSpinner = (NumberPicker) findViewById(R.id.minute); - mMinuteSpinner.setMinValue(0); - mMinuteSpinner.setMaxValue(59); - mMinuteSpinner.setOnLongPressUpdateInterval(100); - mMinuteSpinner.setFormatter(NumberPicker.getTwoDigitFormatter()); - mMinuteSpinner.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() { - public void onValueChange(NumberPicker spinner, int oldVal, int newVal) { - updateInputState(); - int minValue = mMinuteSpinner.getMinValue(); - int maxValue = mMinuteSpinner.getMaxValue(); - if (oldVal == maxValue && newVal == minValue) { - int newHour = mHourSpinner.getValue() + 1; - if (!is24HourView() && newHour == HOURS_IN_HALF_DAY) { - mIsAm = !mIsAm; - updateAmPmControl(); - } - mHourSpinner.setValue(newHour); - } else if (oldVal == minValue && newVal == maxValue) { - int newHour = mHourSpinner.getValue() - 1; - if (!is24HourView() && newHour == HOURS_IN_HALF_DAY - 1) { - mIsAm = !mIsAm; - updateAmPmControl(); - } - mHourSpinner.setValue(newHour); - } - onTimeChanged(); - } - }); - mMinuteSpinnerInput = (EditText) mMinuteSpinner.findViewById(R.id.numberpicker_input); - mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT); - - /* Get the localized am/pm strings and use them in the spinner */ - mAmPmStrings = new DateFormatSymbols().getAmPmStrings(); - - // am/pm - View amPmView = findViewById(R.id.amPm); - if (amPmView instanceof Button) { - mAmPmSpinner = null; - mAmPmSpinnerInput = null; - mAmPmButton = (Button) amPmView; - mAmPmButton.setOnClickListener(new OnClickListener() { - public void onClick(View button) { - button.requestFocus(); - mIsAm = !mIsAm; - updateAmPmControl(); - onTimeChanged(); - } - }); - } else { - mAmPmButton = null; - mAmPmSpinner = (NumberPicker) amPmView; - mAmPmSpinner.setMinValue(0); - mAmPmSpinner.setMaxValue(1); - mAmPmSpinner.setDisplayedValues(mAmPmStrings); - mAmPmSpinner.setOnValueChangedListener(new OnValueChangeListener() { - public void onValueChange(NumberPicker picker, int oldVal, int newVal) { - updateInputState(); - picker.requestFocus(); - mIsAm = !mIsAm; - updateAmPmControl(); - onTimeChanged(); - } - }); - mAmPmSpinnerInput = (EditText) mAmPmSpinner.findViewById(R.id.numberpicker_input); - mAmPmSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_DONE); - } - - if (isAmPmAtStart()) { - // Move the am/pm view to the beginning - ViewGroup amPmParent = (ViewGroup) findViewById(R.id.timePickerLayout); - amPmParent.removeView(amPmView); - amPmParent.addView(amPmView, 0); - // Swap layout margins if needed. They may be not symmetrical (Old Standard Theme for - // example and not for Holo Theme) - ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) amPmView.getLayoutParams(); - final int startMargin = lp.getMarginStart(); - final int endMargin = lp.getMarginEnd(); - if (startMargin != endMargin) { - lp.setMarginStart(endMargin); - lp.setMarginEnd(startMargin); - } - } - - getHourFormatData(); - - // update controls to initial state - updateHourControl(); - updateMinuteControl(); - updateAmPmControl(); - - setOnTimeChangedListener(NO_OP_CHANGE_LISTENER); - - // set to current time - setCurrentHour(mTempCalendar.get(Calendar.HOUR_OF_DAY)); - setCurrentMinute(mTempCalendar.get(Calendar.MINUTE)); + /** + * Set the current hour. + */ + public void setCurrentHour(Integer currentHour) { + mDelegate.setCurrentHour(currentHour); + } - if (!isEnabled()) { - setEnabled(false); - } + /** + * @return The current hour in the range (0-23). + */ + public Integer getCurrentHour() { + return mDelegate.getCurrentHour(); + } - // set the content descriptions - setContentDescriptions(); + /** + * Set the current minute (0-59). + */ + public void setCurrentMinute(Integer currentMinute) { + mDelegate.setCurrentMinute(currentMinute); + } - // If not explicitly specified this view is important for accessibility. - if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { - setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); - } + /** + * @return The current minute. + */ + public Integer getCurrentMinute() { + return mDelegate.getCurrentMinute(); } - private void getHourFormatData() { - final Locale defaultLocale = Locale.getDefault(); - final String bestDateTimePattern = DateFormat.getBestDateTimePattern(defaultLocale, - (mIs24HourView) ? "Hm" : "hm"); - final int lengthPattern = bestDateTimePattern.length(); - mHourWithTwoDigit = false; - char hourFormat = '\0'; - // Check if the returned pattern is single or double 'H', 'h', 'K', 'k'. We also save - // the hour format that we found. - for (int i = 0; i < lengthPattern; i++) { - final char c = bestDateTimePattern.charAt(i); - if (c == 'H' || c == 'h' || c == 'K' || c == 'k') { - mHourFormat = c; - if (i + 1 < lengthPattern && c == bestDateTimePattern.charAt(i + 1)) { - mHourWithTwoDigit = true; - } - break; - } - } + /** + * Set whether in 24 hour or AM/PM mode. + * + * @param is24HourView True = 24 hour mode. False = AM/PM. + */ + public void setIs24HourView(Boolean is24HourView) { + mDelegate.setIs24HourView(is24HourView); } - private boolean isAmPmAtStart() { - final Locale defaultLocale = Locale.getDefault(); - final String bestDateTimePattern = DateFormat.getBestDateTimePattern(defaultLocale, - "hm" /* skeleton */); + /** + * @return true if this is in 24 hour view else false. + */ + public boolean is24HourView() { + return mDelegate.is24HourView(); + } - return bestDateTimePattern.startsWith("a"); + /** + * Set the callback that indicates the time has been adjusted by the user. + * + * @param onTimeChangedListener the callback, should not be null. + */ + public void setOnTimeChangedListener(OnTimeChangedListener onTimeChangedListener) { + mDelegate.setOnTimeChangedListener(onTimeChangedListener); } @Override public void setEnabled(boolean enabled) { - if (mIsEnabled == enabled) { + if (mDelegate.isEnabled() == enabled) { return; } super.setEnabled(enabled); - mMinuteSpinner.setEnabled(enabled); - if (mDivider != null) { - mDivider.setEnabled(enabled); - } - mHourSpinner.setEnabled(enabled); - if (mAmPmSpinner != null) { - mAmPmSpinner.setEnabled(enabled); - } else { - mAmPmButton.setEnabled(enabled); - } - mIsEnabled = enabled; + mDelegate.setEnabled(enabled); } @Override public boolean isEnabled() { - return mIsEnabled; + return mDelegate.isEnabled(); + } + + @Override + public int getBaseline() { + return mDelegate.getBaseline(); } @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); - setCurrentLocale(newConfig.locale); + mDelegate.onConfigurationChanged(newConfig); + } + + @Override + protected Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + return mDelegate.onSaveInstanceState(superState); + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + SavedState ss = (SavedState) state; + super.onRestoreInstanceState(ss.getSuperState()); + mDelegate.onRestoreInstanceState(ss); + } + + @Override + public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + return mDelegate.dispatchPopulateAccessibilityEvent(event); + } + + @Override + public void onPopulateAccessibilityEvent(AccessibilityEvent event) { + super.onPopulateAccessibilityEvent(event); + mDelegate.onPopulateAccessibilityEvent(event); + } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + mDelegate.onInitializeAccessibilityEvent(event); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + mDelegate.onInitializeAccessibilityNodeInfo(info); } /** - * Sets the current locale. - * - * @param locale The current locale. + * A delegate interface that defined the public API of the TimePicker. Allows different + * TimePicker implementations. This would need to be implemented by the TimePicker delegates + * for the real behavior. */ - private void setCurrentLocale(Locale locale) { - if (locale.equals(mCurrentLocale)) { - return; - } - mCurrentLocale = locale; - mTempCalendar = Calendar.getInstance(locale); + private interface TimePickerDelegate { + void setCurrentHour(Integer currentHour); + Integer getCurrentHour(); + + void setCurrentMinute(Integer currentMinute); + Integer getCurrentMinute(); + + void setIs24HourView(Boolean is24HourView); + boolean is24HourView(); + + void setOnTimeChangedListener(OnTimeChangedListener onTimeChangedListener); + + void setEnabled(boolean enabled); + boolean isEnabled(); + + int getBaseline(); + + void onConfigurationChanged(Configuration newConfig); + + Parcelable onSaveInstanceState(Parcelable superState); + void onRestoreInstanceState(Parcelable state); + + boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event); + void onPopulateAccessibilityEvent(AccessibilityEvent event); + void onInitializeAccessibilityEvent(AccessibilityEvent event); + void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info); } /** - * Used to save / restore state of time picker + * A delegate implementing the basic TimePicker */ - private static class SavedState extends BaseSavedState { + private static class LegacyTimePickerDelegate implements TimePickerDelegate { + // the delegator + private TimePicker mDelegator; - private final int mHour; + private static final boolean DEFAULT_ENABLED_STATE = true; - private final int mMinute; + private static final int HOURS_IN_HALF_DAY = 12; - private SavedState(Parcelable superState, int hour, int minute) { - super(superState); - mHour = hour; - mMinute = minute; - } + // state + private boolean mIs24HourView; - private SavedState(Parcel in) { - super(in); - mHour = in.readInt(); - mMinute = in.readInt(); - } + private boolean mIsAm; - public int getHour() { - return mHour; - } + // ui components + private final NumberPicker mHourSpinner; - public int getMinute() { - return mMinute; - } + private final NumberPicker mMinuteSpinner; - @Override - public void writeToParcel(Parcel dest, int flags) { - super.writeToParcel(dest, flags); - dest.writeInt(mHour); - dest.writeInt(mMinute); - } + private final NumberPicker mAmPmSpinner; - @SuppressWarnings({"unused", "hiding"}) - public static final Parcelable.Creator<SavedState> CREATOR = new Creator<SavedState>() { - public SavedState createFromParcel(Parcel in) { - return new SavedState(in); + private final EditText mHourSpinnerInput; + + private final EditText mMinuteSpinnerInput; + + private final EditText mAmPmSpinnerInput; + + private final TextView mDivider; + + // Note that the legacy implementation of the TimePicker is + // using a button for toggling between AM/PM while the new + // version uses a NumberPicker spinner. Therefore the code + // accommodates these two cases to be backwards compatible. + private final Button mAmPmButton; + + private final String[] mAmPmStrings; + + private boolean mIsEnabled = DEFAULT_ENABLED_STATE; + + // callbacks + private OnTimeChangedListener mOnTimeChangedListener; + + private Calendar mTempCalendar; + + private Locale mCurrentLocale; + + private boolean mHourWithTwoDigit; + private char mHourFormat; + + /** + * A no-op callback used in the constructor to avoid null checks later in + * the code. + */ + private static final OnTimeChangedListener NO_OP_CHANGE_LISTENER = + new OnTimeChangedListener() { + public void onTimeChanged(TimePicker view, int hourOfDay, int minute) { + } + }; + + public LegacyTimePickerDelegate(TimePicker delegator, Context context, AttributeSet attrs, + int defStyle) { + mDelegator = delegator; + + // initialization based on locale + setCurrentLocale(Locale.getDefault()); + + // process style attributes + TypedArray attributesArray = context.obtainStyledAttributes( + attrs, R.styleable.TimePicker, defStyle, 0); + int layoutResourceId = attributesArray.getResourceId( + R.styleable.TimePicker_internalLayout, R.layout.time_picker); + attributesArray.recycle(); + + LayoutInflater inflater = (LayoutInflater) context.getSystemService( + Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(layoutResourceId, mDelegator, true); + + // hour + mHourSpinner = (NumberPicker) delegator.findViewById(R.id.hour); + mHourSpinner.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() { + public void onValueChange(NumberPicker spinner, int oldVal, int newVal) { + updateInputState(); + if (!is24HourView()) { + if ((oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY) || + (oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1)) { + mIsAm = !mIsAm; + updateAmPmControl(); + } + } + onTimeChanged(); + } + }); + mHourSpinnerInput = (EditText) mHourSpinner.findViewById(R.id.numberpicker_input); + mHourSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT); + + // divider (only for the new widget style) + mDivider = (TextView) mDelegator.findViewById(R.id.divider); + if (mDivider != null) { + setDividerText(); } - public SavedState[] newArray(int size) { - return new SavedState[size]; + // minute + mMinuteSpinner = (NumberPicker) mDelegator.findViewById(R.id.minute); + mMinuteSpinner.setMinValue(0); + mMinuteSpinner.setMaxValue(59); + mMinuteSpinner.setOnLongPressUpdateInterval(100); + mMinuteSpinner.setFormatter(NumberPicker.getTwoDigitFormatter()); + mMinuteSpinner.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() { + public void onValueChange(NumberPicker spinner, int oldVal, int newVal) { + updateInputState(); + int minValue = mMinuteSpinner.getMinValue(); + int maxValue = mMinuteSpinner.getMaxValue(); + if (oldVal == maxValue && newVal == minValue) { + int newHour = mHourSpinner.getValue() + 1; + if (!is24HourView() && newHour == HOURS_IN_HALF_DAY) { + mIsAm = !mIsAm; + updateAmPmControl(); + } + mHourSpinner.setValue(newHour); + } else if (oldVal == minValue && newVal == maxValue) { + int newHour = mHourSpinner.getValue() - 1; + if (!is24HourView() && newHour == HOURS_IN_HALF_DAY - 1) { + mIsAm = !mIsAm; + updateAmPmControl(); + } + mHourSpinner.setValue(newHour); + } + onTimeChanged(); + } + }); + mMinuteSpinnerInput = (EditText) mMinuteSpinner.findViewById(R.id.numberpicker_input); + mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT); + + /* Get the localized am/pm strings and use them in the spinner */ + mAmPmStrings = new DateFormatSymbols().getAmPmStrings(); + + // am/pm + View amPmView = mDelegator.findViewById(R.id.amPm); + if (amPmView instanceof Button) { + mAmPmSpinner = null; + mAmPmSpinnerInput = null; + mAmPmButton = (Button) amPmView; + mAmPmButton.setOnClickListener(new OnClickListener() { + public void onClick(View button) { + button.requestFocus(); + mIsAm = !mIsAm; + updateAmPmControl(); + onTimeChanged(); + } + }); + } else { + mAmPmButton = null; + mAmPmSpinner = (NumberPicker) amPmView; + mAmPmSpinner.setMinValue(0); + mAmPmSpinner.setMaxValue(1); + mAmPmSpinner.setDisplayedValues(mAmPmStrings); + mAmPmSpinner.setOnValueChangedListener(new OnValueChangeListener() { + public void onValueChange(NumberPicker picker, int oldVal, int newVal) { + updateInputState(); + picker.requestFocus(); + mIsAm = !mIsAm; + updateAmPmControl(); + onTimeChanged(); + } + }); + mAmPmSpinnerInput = (EditText) mAmPmSpinner.findViewById(R.id.numberpicker_input); + mAmPmSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_DONE); } - }; - } - @Override - protected Parcelable onSaveInstanceState() { - Parcelable superState = super.onSaveInstanceState(); - return new SavedState(superState, getCurrentHour(), getCurrentMinute()); - } + if (isAmPmAtStart()) { + // Move the am/pm view to the beginning + ViewGroup amPmParent = (ViewGroup) delegator.findViewById(R.id.timePickerLayout); + amPmParent.removeView(amPmView); + amPmParent.addView(amPmView, 0); + // Swap layout margins if needed. They may be not symmetrical (Old Standard Theme + // for example and not for Holo Theme) + ViewGroup.MarginLayoutParams lp = + (ViewGroup.MarginLayoutParams) amPmView.getLayoutParams(); + final int startMargin = lp.getMarginStart(); + final int endMargin = lp.getMarginEnd(); + if (startMargin != endMargin) { + lp.setMarginStart(endMargin); + lp.setMarginEnd(startMargin); + } + } - @Override - protected void onRestoreInstanceState(Parcelable state) { - SavedState ss = (SavedState) state; - super.onRestoreInstanceState(ss.getSuperState()); - setCurrentHour(ss.getHour()); - setCurrentMinute(ss.getMinute()); - } + getHourFormatData(); - /** - * Set the callback that indicates the time has been adjusted by the user. - * - * @param onTimeChangedListener the callback, should not be null. - */ - public void setOnTimeChangedListener(OnTimeChangedListener onTimeChangedListener) { - mOnTimeChangedListener = onTimeChangedListener; - } + // update controls to initial state + updateHourControl(); + updateMinuteControl(); + updateAmPmControl(); - /** - * @return The current hour in the range (0-23). - */ - public Integer getCurrentHour() { - int currentHour = mHourSpinner.getValue(); - if (is24HourView()) { - return currentHour; - } else if (mIsAm) { - return currentHour % HOURS_IN_HALF_DAY; - } else { - return (currentHour % HOURS_IN_HALF_DAY) + HOURS_IN_HALF_DAY; - } - } + setOnTimeChangedListener(NO_OP_CHANGE_LISTENER); - /** - * Set the current hour. - */ - public void setCurrentHour(Integer currentHour) { - setCurrentHour(currentHour, true); - } + // set to current time + setCurrentHour(mTempCalendar.get(Calendar.HOUR_OF_DAY)); + setCurrentMinute(mTempCalendar.get(Calendar.MINUTE)); - private void setCurrentHour(Integer currentHour, boolean notifyTimeChanged) { - // why was Integer used in the first place? - if (currentHour == null || currentHour == getCurrentHour()) { - return; + if (!isEnabled()) { + setEnabled(false); + } + + // set the content descriptions + setContentDescriptions(); + + // If not explicitly specified this view is important for accessibility. + if (mDelegator.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + mDelegator.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); + } } - if (!is24HourView()) { - // convert [0,23] ordinal to wall clock display - if (currentHour >= HOURS_IN_HALF_DAY) { - mIsAm = false; - if (currentHour > HOURS_IN_HALF_DAY) { - currentHour = currentHour - HOURS_IN_HALF_DAY; + + private void getHourFormatData() { + final Locale defaultLocale = Locale.getDefault(); + final String bestDateTimePattern = DateFormat.getBestDateTimePattern(defaultLocale, + (mIs24HourView) ? "Hm" : "hm"); + final int lengthPattern = bestDateTimePattern.length(); + mHourWithTwoDigit = false; + char hourFormat = '\0'; + // Check if the returned pattern is single or double 'H', 'h', 'K', 'k'. We also save + // the hour format that we found. + for (int i = 0; i < lengthPattern; i++) { + final char c = bestDateTimePattern.charAt(i); + if (c == 'H' || c == 'h' || c == 'K' || c == 'k') { + mHourFormat = c; + if (i + 1 < lengthPattern && c == bestDateTimePattern.charAt(i + 1)) { + mHourWithTwoDigit = true; + } + break; } + } + } + + private boolean isAmPmAtStart() { + final Locale defaultLocale = Locale.getDefault(); + final String bestDateTimePattern = DateFormat.getBestDateTimePattern(defaultLocale, + "hm" /* skeleton */); + + return bestDateTimePattern.startsWith("a"); + } + + /** + * The time separator is defined in the Unicode CLDR and cannot be supposed to be ":". + * + * See http://unicode.org/cldr/trac/browser/trunk/common/main + * + * We pass the correct "skeleton" depending on 12 or 24 hours view and then extract the + * separator as the character which is just after the hour marker in the returned pattern. + */ + private void setDividerText() { + final Locale defaultLocale = Locale.getDefault(); + final String skeleton = (mIs24HourView) ? "Hm" : "hm"; + final String bestDateTimePattern = DateFormat.getBestDateTimePattern(defaultLocale, + skeleton); + final String separatorText; + int hourIndex = bestDateTimePattern.lastIndexOf('H'); + if (hourIndex == -1) { + hourIndex = bestDateTimePattern.lastIndexOf('h'); + } + if (hourIndex == -1) { + // Default case + separatorText = ":"; } else { - mIsAm = true; - if (currentHour == 0) { - currentHour = HOURS_IN_HALF_DAY; + int minuteIndex = bestDateTimePattern.indexOf('m', hourIndex + 1); + if (minuteIndex == -1) { + separatorText = Character.toString(bestDateTimePattern.charAt(hourIndex + 1)); + } else { + separatorText = bestDateTimePattern.substring(hourIndex + 1, minuteIndex); } } - updateAmPmControl(); + mDivider.setText(separatorText); + } + + @Override + public void setCurrentHour(Integer currentHour) { + setCurrentHour(currentHour, true); } - mHourSpinner.setValue(currentHour); - if (notifyTimeChanged) { + + private void setCurrentHour(Integer currentHour, boolean notifyTimeChanged) { + // why was Integer used in the first place? + if (currentHour == null || currentHour == getCurrentHour()) { + return; + } + if (!is24HourView()) { + // convert [0,23] ordinal to wall clock display + if (currentHour >= HOURS_IN_HALF_DAY) { + mIsAm = false; + if (currentHour > HOURS_IN_HALF_DAY) { + currentHour = currentHour - HOURS_IN_HALF_DAY; + } + } else { + mIsAm = true; + if (currentHour == 0) { + currentHour = HOURS_IN_HALF_DAY; + } + } + updateAmPmControl(); + } + mHourSpinner.setValue(currentHour); + if (notifyTimeChanged) { + onTimeChanged(); + } + } + + @Override + public Integer getCurrentHour() { + int currentHour = mHourSpinner.getValue(); + if (is24HourView()) { + return currentHour; + } else if (mIsAm) { + return currentHour % HOURS_IN_HALF_DAY; + } else { + return (currentHour % HOURS_IN_HALF_DAY) + HOURS_IN_HALF_DAY; + } + } + + @Override + public void setCurrentMinute(Integer currentMinute) { + if (currentMinute == getCurrentMinute()) { + return; + } + mMinuteSpinner.setValue(currentMinute); onTimeChanged(); } - } - /** - * Set whether in 24 hour or AM/PM mode. - * - * @param is24HourView True = 24 hour mode. False = AM/PM. - */ - public void setIs24HourView(Boolean is24HourView) { - if (mIs24HourView == is24HourView) { - return; + @Override + public Integer getCurrentMinute() { + return mMinuteSpinner.getValue(); } - // cache the current hour since spinner range changes and BEFORE changing mIs24HourView!! - int currentHour = getCurrentHour(); - // Order is important here. - mIs24HourView = is24HourView; - getHourFormatData(); - updateHourControl(); - // set value after spinner range is updated - be aware that because mIs24HourView has - // changed then getCurrentHour() is not equal to the currentHour we cached before so - // explicitly ask for *not* propagating any onTimeChanged() - setCurrentHour(currentHour, false /* no onTimeChanged() */); - updateMinuteControl(); - updateAmPmControl(); - } - /** - * @return true if this is in 24 hour view else false. - */ - public boolean is24HourView() { - return mIs24HourView; - } + @Override + public void setIs24HourView(Boolean is24HourView) { + if (mIs24HourView == is24HourView) { + return; + } + // cache the current hour since spinner range changes and BEFORE changing mIs24HourView!! + int currentHour = getCurrentHour(); + // Order is important here. + mIs24HourView = is24HourView; + getHourFormatData(); + updateHourControl(); + // set value after spinner range is updated - be aware that because mIs24HourView has + // changed then getCurrentHour() is not equal to the currentHour we cached before so + // explicitly ask for *not* propagating any onTimeChanged() + setCurrentHour(currentHour, false /* no onTimeChanged() */); + updateMinuteControl(); + updateAmPmControl(); + } - /** - * @return The current minute. - */ - public Integer getCurrentMinute() { - return mMinuteSpinner.getValue(); - } + @Override + public boolean is24HourView() { + return mIs24HourView; + } - /** - * Set the current minute (0-59). - */ - public void setCurrentMinute(Integer currentMinute) { - if (currentMinute == getCurrentMinute()) { - return; + @Override + public void setOnTimeChangedListener(OnTimeChangedListener onTimeChangedListener) { + mOnTimeChangedListener = onTimeChangedListener; } - mMinuteSpinner.setValue(currentMinute); - onTimeChanged(); - } - /** - * The time separator is defined in the Unicode CLDR and cannot be supposed to be ":". - * - * See http://unicode.org/cldr/trac/browser/trunk/common/main - * - * We pass the correct "skeleton" depending on 12 or 24 hours view and then extract the - * separator as the character which is just after the hour marker in the returned pattern. - */ - private void setDividerText() { - final Locale defaultLocale = Locale.getDefault(); - final String skeleton = (mIs24HourView) ? "Hm" : "hm"; - final String bestDateTimePattern = DateFormat.getBestDateTimePattern(defaultLocale, - skeleton); - final String separatorText; - int hourIndex = bestDateTimePattern.lastIndexOf('H'); - if (hourIndex == -1) { - hourIndex = bestDateTimePattern.lastIndexOf('h'); - } - if (hourIndex == -1) { - // Default case - separatorText = ":"; - } else { - int minuteIndex = bestDateTimePattern.indexOf('m', hourIndex + 1); - if (minuteIndex == -1) { - separatorText = Character.toString(bestDateTimePattern.charAt(hourIndex + 1)); + @Override + public void setEnabled(boolean enabled) { + mMinuteSpinner.setEnabled(enabled); + if (mDivider != null) { + mDivider.setEnabled(enabled); + } + mHourSpinner.setEnabled(enabled); + if (mAmPmSpinner != null) { + mAmPmSpinner.setEnabled(enabled); } else { - separatorText = bestDateTimePattern.substring(hourIndex + 1, minuteIndex); + mAmPmButton.setEnabled(enabled); } + mIsEnabled = enabled; } - mDivider.setText(separatorText); - } - @Override - public int getBaseline() { - return mHourSpinner.getBaseline(); - } + @Override + public boolean isEnabled() { + return mIsEnabled; + } - @Override - public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { - onPopulateAccessibilityEvent(event); - return true; - } + @Override + public int getBaseline() { + return mHourSpinner.getBaseline(); + } - @Override - public void onPopulateAccessibilityEvent(AccessibilityEvent event) { - super.onPopulateAccessibilityEvent(event); + @Override + public void onConfigurationChanged(Configuration newConfig) { + setCurrentLocale(newConfig.locale); + } - int flags = DateUtils.FORMAT_SHOW_TIME; - if (mIs24HourView) { - flags |= DateUtils.FORMAT_24HOUR; - } else { - flags |= DateUtils.FORMAT_12HOUR; - } - mTempCalendar.set(Calendar.HOUR_OF_DAY, getCurrentHour()); - mTempCalendar.set(Calendar.MINUTE, getCurrentMinute()); - String selectedDateUtterance = DateUtils.formatDateTime(mContext, - mTempCalendar.getTimeInMillis(), flags); - event.getText().add(selectedDateUtterance); - } + @Override + public Parcelable onSaveInstanceState(Parcelable superState) { + return new SavedState(superState, getCurrentHour(), getCurrentMinute()); + } - @Override - public void onInitializeAccessibilityEvent(AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(event); - event.setClassName(TimePicker.class.getName()); - } + @Override + public void onRestoreInstanceState(Parcelable state) { + SavedState ss = (SavedState) state; + setCurrentHour(ss.getHour()); + setCurrentMinute(ss.getMinute()); + } - @Override - public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(info); - info.setClassName(TimePicker.class.getName()); - } + @Override + public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + onPopulateAccessibilityEvent(event); + return true; + } - private void updateHourControl() { - if (is24HourView()) { - // 'k' means 1-24 hour - if (mHourFormat == 'k') { - mHourSpinner.setMinValue(1); - mHourSpinner.setMaxValue(24); + @Override + public void onPopulateAccessibilityEvent(AccessibilityEvent event) { + int flags = DateUtils.FORMAT_SHOW_TIME; + if (mIs24HourView) { + flags |= DateUtils.FORMAT_24HOUR; } else { - mHourSpinner.setMinValue(0); - mHourSpinner.setMaxValue(23); + flags |= DateUtils.FORMAT_12HOUR; + } + mTempCalendar.set(Calendar.HOUR_OF_DAY, getCurrentHour()); + mTempCalendar.set(Calendar.MINUTE, getCurrentMinute()); + String selectedDateUtterance = DateUtils.formatDateTime(mDelegator.getContext(), + mTempCalendar.getTimeInMillis(), flags); + event.getText().add(selectedDateUtterance); + } + + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + event.setClassName(TimePicker.class.getName()); + } + + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + info.setClassName(TimePicker.class.getName()); + } + + private void updateInputState() { + // Make sure that if the user changes the value and the IME is active + // for one of the inputs if this widget, the IME is closed. If the user + // changed the value via the IME and there is a next input the IME will + // be shown, otherwise the user chose another means of changing the + // value and having the IME up makes no sense. + InputMethodManager inputMethodManager = InputMethodManager.peekInstance(); + if (inputMethodManager != null) { + if (inputMethodManager.isActive(mHourSpinnerInput)) { + mHourSpinnerInput.clearFocus(); + inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0); + } else if (inputMethodManager.isActive(mMinuteSpinnerInput)) { + mMinuteSpinnerInput.clearFocus(); + inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0); + } else if (inputMethodManager.isActive(mAmPmSpinnerInput)) { + mAmPmSpinnerInput.clearFocus(); + inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0); + } } - } else { - // 'K' means 0-11 hour - if (mHourFormat == 'K') { - mHourSpinner.setMinValue(0); - mHourSpinner.setMaxValue(11); + } + + private void updateAmPmControl() { + if (is24HourView()) { + if (mAmPmSpinner != null) { + mAmPmSpinner.setVisibility(View.GONE); + } else { + mAmPmButton.setVisibility(View.GONE); + } } else { - mHourSpinner.setMinValue(1); - mHourSpinner.setMaxValue(12); + int index = mIsAm ? Calendar.AM : Calendar.PM; + if (mAmPmSpinner != null) { + mAmPmSpinner.setValue(index); + mAmPmSpinner.setVisibility(View.VISIBLE); + } else { + mAmPmButton.setText(mAmPmStrings[index]); + mAmPmButton.setVisibility(View.VISIBLE); + } } + mDelegator.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); } - mHourSpinner.setFormatter(mHourWithTwoDigit ? NumberPicker.getTwoDigitFormatter() : null); - } - private void updateMinuteControl() { - if (is24HourView()) { - mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_DONE); - } else { - mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT); + /** + * Sets the current locale. + * + * @param locale The current locale. + */ + private void setCurrentLocale(Locale locale) { + if (locale.equals(mCurrentLocale)) { + return; + } + mCurrentLocale = locale; + mTempCalendar = Calendar.getInstance(locale); } - } - private void updateAmPmControl() { - if (is24HourView()) { - if (mAmPmSpinner != null) { - mAmPmSpinner.setVisibility(View.GONE); + private void onTimeChanged() { + mDelegator.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); + if (mOnTimeChangedListener != null) { + mOnTimeChangedListener.onTimeChanged(mDelegator, getCurrentHour(), + getCurrentMinute()); + } + } + + private void updateHourControl() { + if (is24HourView()) { + // 'k' means 1-24 hour + if (mHourFormat == 'k') { + mHourSpinner.setMinValue(1); + mHourSpinner.setMaxValue(24); + } else { + mHourSpinner.setMinValue(0); + mHourSpinner.setMaxValue(23); + } } else { - mAmPmButton.setVisibility(View.GONE); + // 'K' means 0-11 hour + if (mHourFormat == 'K') { + mHourSpinner.setMinValue(0); + mHourSpinner.setMaxValue(11); + } else { + mHourSpinner.setMinValue(1); + mHourSpinner.setMaxValue(12); + } } - } else { - int index = mIsAm ? Calendar.AM : Calendar.PM; - if (mAmPmSpinner != null) { - mAmPmSpinner.setValue(index); - mAmPmSpinner.setVisibility(View.VISIBLE); + mHourSpinner.setFormatter(mHourWithTwoDigit ? NumberPicker.getTwoDigitFormatter() : null); + } + + private void updateMinuteControl() { + if (is24HourView()) { + mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_DONE); } else { - mAmPmButton.setText(mAmPmStrings[index]); - mAmPmButton.setVisibility(View.VISIBLE); + mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT); } } - sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); - } - private void onTimeChanged() { - sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); - if (mOnTimeChangedListener != null) { - mOnTimeChangedListener.onTimeChanged(this, getCurrentHour(), getCurrentMinute()); + private void setContentDescriptions() { + // Minute + trySetContentDescription(mMinuteSpinner, R.id.increment, + R.string.time_picker_increment_minute_button); + trySetContentDescription(mMinuteSpinner, R.id.decrement, + R.string.time_picker_decrement_minute_button); + // Hour + trySetContentDescription(mHourSpinner, R.id.increment, + R.string.time_picker_increment_hour_button); + trySetContentDescription(mHourSpinner, R.id.decrement, + R.string.time_picker_decrement_hour_button); + // AM/PM + if (mAmPmSpinner != null) { + trySetContentDescription(mAmPmSpinner, R.id.increment, + R.string.time_picker_increment_set_pm_button); + trySetContentDescription(mAmPmSpinner, R.id.decrement, + R.string.time_picker_decrement_set_am_button); + } } - } - private void setContentDescriptions() { - // Minute - trySetContentDescription(mMinuteSpinner, R.id.increment, - R.string.time_picker_increment_minute_button); - trySetContentDescription(mMinuteSpinner, R.id.decrement, - R.string.time_picker_decrement_minute_button); - // Hour - trySetContentDescription(mHourSpinner, R.id.increment, - R.string.time_picker_increment_hour_button); - trySetContentDescription(mHourSpinner, R.id.decrement, - R.string.time_picker_decrement_hour_button); - // AM/PM - if (mAmPmSpinner != null) { - trySetContentDescription(mAmPmSpinner, R.id.increment, - R.string.time_picker_increment_set_pm_button); - trySetContentDescription(mAmPmSpinner, R.id.decrement, - R.string.time_picker_decrement_set_am_button); + private void trySetContentDescription(View root, int viewId, int contDescResId) { + View target = root.findViewById(viewId); + if (target != null) { + target.setContentDescription(mDelegator.getContext().getString(contDescResId)); + } } } - private void trySetContentDescription(View root, int viewId, int contDescResId) { - View target = root.findViewById(viewId); - if (target != null) { - target.setContentDescription(mContext.getString(contDescResId)); + /** + * Used to save / restore state of time picker + */ + private static class SavedState extends BaseSavedState { + + private final int mHour; + + private final int mMinute; + + private SavedState(Parcelable superState, int hour, int minute) { + super(superState); + mHour = hour; + mMinute = minute; } - } - private void updateInputState() { - // Make sure that if the user changes the value and the IME is active - // for one of the inputs if this widget, the IME is closed. If the user - // changed the value via the IME and there is a next input the IME will - // be shown, otherwise the user chose another means of changing the - // value and having the IME up makes no sense. - InputMethodManager inputMethodManager = InputMethodManager.peekInstance(); - if (inputMethodManager != null) { - if (inputMethodManager.isActive(mHourSpinnerInput)) { - mHourSpinnerInput.clearFocus(); - inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0); - } else if (inputMethodManager.isActive(mMinuteSpinnerInput)) { - mMinuteSpinnerInput.clearFocus(); - inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0); - } else if (inputMethodManager.isActive(mAmPmSpinnerInput)) { - mAmPmSpinnerInput.clearFocus(); - inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0); - } + private SavedState(Parcel in) { + super(in); + mHour = in.readInt(); + mMinute = in.readInt(); } + + public int getHour() { + return mHour; + } + + public int getMinute() { + return mMinute; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeInt(mHour); + dest.writeInt(mMinute); + } + + @SuppressWarnings({"unused", "hiding"}) + public static final Parcelable.Creator<SavedState> CREATOR = new Creator<SavedState>() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; } } |