summaryrefslogtreecommitdiffstats
path: root/core/java/android
diff options
context:
space:
mode:
Diffstat (limited to 'core/java/android')
-rw-r--r--core/java/android/content/ContentResolver.java168
-rw-r--r--core/java/android/content/IContentService.aidl64
-rw-r--r--core/java/android/content/ISyncServiceAdapter.aidl (renamed from core/java/android/content/IAnonymousSyncAdapter.aidl)2
-rw-r--r--core/java/android/content/PeriodicSync.java72
-rw-r--r--core/java/android/content/SyncInfo.java23
-rw-r--r--core/java/android/content/SyncRequest.java228
-rw-r--r--core/java/android/content/SyncService.java210
-rw-r--r--core/java/android/os/BatteryProperty.aidl19
-rw-r--r--core/java/android/os/BatteryProperty.java70
-rw-r--r--core/java/android/os/IBatteryPropertiesRegistrar.aidl2
-rw-r--r--core/java/android/os/ParcelFileDescriptor.java4
-rw-r--r--core/java/android/os/Process.java13
-rw-r--r--core/java/android/webkit/WebView.java47
-rw-r--r--core/java/android/webkit/WebViewClassic.java8
-rw-r--r--core/java/android/webkit/WebViewProvider.java9
-rw-r--r--core/java/android/widget/OverScroller.java21
-rw-r--r--core/java/android/widget/Scroller.java78
-rw-r--r--core/java/android/widget/TimePicker.java1173
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>
+ * &lt;service android:name=".MySyncService"/&gt;
+ * </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];
+ }
+ };
}
}