diff options
Diffstat (limited to 'core/java/android')
78 files changed, 2436 insertions, 1006 deletions
diff --git a/core/java/android/animation/FloatKeyframeSet.java b/core/java/android/animation/FloatKeyframeSet.java index 56da940..0173079 100644 --- a/core/java/android/animation/FloatKeyframeSet.java +++ b/core/java/android/animation/FloatKeyframeSet.java @@ -118,13 +118,14 @@ class FloatKeyframeSet extends KeyframeSet implements Keyframes.FloatKeyframes { FloatKeyframe nextKeyframe = (FloatKeyframe) mKeyframes.get(i); if (fraction < nextKeyframe.getFraction()) { final TimeInterpolator interpolator = nextKeyframe.getInterpolator(); - if (interpolator != null) { - fraction = interpolator.getInterpolation(fraction); - } float intervalFraction = (fraction - prevKeyframe.getFraction()) / (nextKeyframe.getFraction() - prevKeyframe.getFraction()); float prevValue = prevKeyframe.getFloatValue(); float nextValue = nextKeyframe.getFloatValue(); + // Apply interpolator on the proportional duration. + if (interpolator != null) { + intervalFraction = interpolator.getInterpolation(intervalFraction); + } return mEvaluator == null ? prevValue + intervalFraction * (nextValue - prevValue) : ((Number)mEvaluator.evaluate(intervalFraction, prevValue, nextValue)). diff --git a/core/java/android/animation/IntKeyframeSet.java b/core/java/android/animation/IntKeyframeSet.java index 12a4bf9..73f9af1 100644 --- a/core/java/android/animation/IntKeyframeSet.java +++ b/core/java/android/animation/IntKeyframeSet.java @@ -117,13 +117,14 @@ class IntKeyframeSet extends KeyframeSet implements Keyframes.IntKeyframes { IntKeyframe nextKeyframe = (IntKeyframe) mKeyframes.get(i); if (fraction < nextKeyframe.getFraction()) { final TimeInterpolator interpolator = nextKeyframe.getInterpolator(); - if (interpolator != null) { - fraction = interpolator.getInterpolation(fraction); - } float intervalFraction = (fraction - prevKeyframe.getFraction()) / (nextKeyframe.getFraction() - prevKeyframe.getFraction()); int prevValue = prevKeyframe.getIntValue(); int nextValue = nextKeyframe.getIntValue(); + // Apply interpolator on the proportional duration. + if (interpolator != null) { + intervalFraction = interpolator.getInterpolation(intervalFraction); + } return mEvaluator == null ? prevValue + (int)(intervalFraction * (nextValue - prevValue)) : ((Number)mEvaluator.evaluate(intervalFraction, prevValue, nextValue)). diff --git a/core/java/android/animation/KeyframeSet.java b/core/java/android/animation/KeyframeSet.java index c80e162..32edd4d 100644 --- a/core/java/android/animation/KeyframeSet.java +++ b/core/java/android/animation/KeyframeSet.java @@ -201,7 +201,6 @@ class KeyframeSet implements Keyframes { * @return The animated value. */ public Object getValue(float fraction) { - // Special-case optimization for the common case of only two keyframes if (mNumKeyframes == 2) { if (mInterpolator != null) { @@ -238,12 +237,13 @@ class KeyframeSet implements Keyframes { Keyframe nextKeyframe = mKeyframes.get(i); if (fraction < nextKeyframe.getFraction()) { final TimeInterpolator interpolator = nextKeyframe.getInterpolator(); - if (interpolator != null) { - fraction = interpolator.getInterpolation(fraction); - } final float prevFraction = prevKeyframe.getFraction(); float intervalFraction = (fraction - prevFraction) / (nextKeyframe.getFraction() - prevFraction); + // Apply interpolator on the proportional duration. + if (interpolator != null) { + intervalFraction = interpolator.getInterpolation(intervalFraction); + } return mEvaluator.evaluate(intervalFraction, prevKeyframe.getValue(), nextKeyframe.getValue()); } diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java index 6d74905..6ec5457 100644 --- a/core/java/android/app/ApplicationPackageManager.java +++ b/core/java/android/app/ApplicationPackageManager.java @@ -36,6 +36,7 @@ import android.content.pm.IPackageManager; import android.content.pm.IPackageMoveObserver; import android.content.pm.IPackageStatsObserver; import android.content.pm.InstrumentationInfo; +import android.content.pm.IntentFilterVerificationInfo; import android.content.pm.KeySet; import android.content.pm.ManifestDigest; import android.content.pm.PackageInfo; @@ -1309,6 +1310,45 @@ final class ApplicationPackageManager extends PackageManager { } @Override + public void verifyIntentFilter(int id, int verificationCode, List<String> outFailedDomains) { + try { + mPM.verifyIntentFilter(id, verificationCode, outFailedDomains); + } catch (RemoteException e) { + // Should never happen! + } + } + + @Override + public int getIntentVerificationStatus(String packageName, int userId) { + try { + return mPM.getIntentVerificationStatus(packageName, userId); + } catch (RemoteException e) { + // Should never happen! + return PackageManager.INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_UNDEFINED; + } + } + + @Override + public boolean updateIntentVerificationStatus(String packageName, int status, int userId) { + try { + return mPM.updateIntentVerificationStatus(packageName, status, userId); + } catch (RemoteException e) { + // Should never happen! + return false; + } + } + + @Override + public List<IntentFilterVerificationInfo> getIntentFilterVerifications(String packageName) { + try { + return mPM.getIntentFilterVerifications(packageName); + } catch (RemoteException e) { + // Should never happen! + return null; + } + } + + @Override public void setInstallerPackageName(String targetPackage, String installerPackageName) { try { diff --git a/core/java/android/app/IBackupAgent.aidl b/core/java/android/app/IBackupAgent.aidl index 451af99..fe8e228 100644 --- a/core/java/android/app/IBackupAgent.aidl +++ b/core/java/android/app/IBackupAgent.aidl @@ -100,6 +100,11 @@ oneway interface IBackupAgent { void doFullBackup(in ParcelFileDescriptor data, int token, IBackupManager callbackBinder); /** + * Estimate how much data a full backup will deliver + */ + void doMeasureFullBackup(int token, IBackupManager callbackBinder); + + /** * Restore a single "file" to the application. The file was typically obtained from * a full-backup dataset. The agent reads 'size' bytes of file content * from the provided file descriptor. diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java index 59fe490..a0f40f6 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -402,13 +402,7 @@ final class SystemServiceRegistry { new CachedServiceFetcher<StorageManager>() { @Override public StorageManager createService(ContextImpl ctx) { - try { - return new StorageManager( - ctx.getContentResolver(), ctx.mMainThread.getHandler().getLooper()); - } catch (RemoteException rex) { - Log.e(TAG, "Failed to create StorageManager", rex); - return null; - } + return new StorageManager(ctx, ctx.mMainThread.getHandler().getLooper()); }}); registerService(Context.TELEPHONY_SERVICE, TelephonyManager.class, diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index ea48b61..cbb0f51 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -175,7 +175,8 @@ public class DevicePolicyManager { * * <p>This component is set as device owner and active admin when device owner provisioning is * started by an NFC message containing an NFC record with MIME type - * {@link #MIME_TYPE_PROVISIONING_NFC_V2}. + * {@link #MIME_TYPE_PROVISIONING_NFC_V2}. For the NFC record, the component name should be + * flattened to a string, via {@link ComponentName#flattenToShortString()}. * * @see DeviceAdminReceiver */ @@ -398,14 +399,16 @@ public class DevicePolicyManager { "android.app.extra.PROVISIONING_SKIP_ENCRYPTION"; /** - * On devices managed by a device owner app, a String representation of a Component name extra - * indicating the component of the application that is temporarily granted device owner - * privileges during device initialization and profile owner privileges during secondary user - * initialization. + * On devices managed by a device owner app, a {@link ComponentName} extra indicating the + * component of the application that is temporarily granted device owner privileges during + * device initialization and profile owner privileges during secondary user initialization. * - * <p>Use in an NFC record with {@link #MIME_TYPE_PROVISIONING_NFC_V2} that starts device owner - * provisioning via an NFC bump. - * @see ComponentName#unflattenFromString() + * <p> + * It can also be used in an NFC record with {@link #MIME_TYPE_PROVISIONING_NFC_V2} that starts + * device owner provisioning via an NFC bump. For the NFC record, it should be flattened to a + * string first. + * + * @see ComponentName#flattenToShortString() */ public static final String EXTRA_PROVISIONING_DEVICE_INITIALIZER_COMPONENT_NAME = "android.app.extra.PROVISIONING_DEVICE_INITIALIZER_COMPONENT_NAME"; @@ -3162,6 +3165,22 @@ public class DevicePolicyManager { } /** + * Start Quick Contact on the managed profile for the current user, if the policy allows. + * @hide + */ + public void startManagedQuickContact(String actualLookupKey, long actualContactId, + Intent originalIntent) { + if (mService != null) { + try { + mService.startManagedQuickContact( + actualLookupKey, actualContactId, originalIntent); + } catch (RemoteException e) { + Log.w(TAG, "Failed talking with device policy service", e); + } + } + } + + /** * Called by the profile owner of a managed profile so that some intents sent in the managed * profile can also be resolved in the parent, or vice versa. * Only activity intents are supported. diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl index 9ca52e5..73b0684 100644 --- a/core/java/android/app/admin/IDevicePolicyManager.aidl +++ b/core/java/android/app/admin/IDevicePolicyManager.aidl @@ -189,6 +189,7 @@ interface IDevicePolicyManager { void setCrossProfileCallerIdDisabled(in ComponentName who, boolean disabled); boolean getCrossProfileCallerIdDisabled(in ComponentName who); boolean getCrossProfileCallerIdDisabledForUser(int userId); + void startManagedQuickContact(String lookupKey, long contactId, in Intent originalIntent); void setTrustAgentConfiguration(in ComponentName admin, in ComponentName agent, in PersistableBundle args); diff --git a/core/java/android/app/backup/BackupAgent.java b/core/java/android/app/backup/BackupAgent.java index 7f89100..2bf267a 100644 --- a/core/java/android/app/backup/BackupAgent.java +++ b/core/java/android/app/backup/BackupAgent.java @@ -424,10 +424,12 @@ public abstract class BackupAgent extends ContextWrapper { } // And now that we know where it lives, semantically, back it up appropriately - Log.i(TAG, "backupFile() of " + filePath + " => domain=" + domain + // In the measurement case, backupToTar() updates the size in output and returns + // without transmitting any file data. + if (DEBUG) Log.i(TAG, "backupFile() of " + filePath + " => domain=" + domain + " rootpath=" + rootpath); - FullBackup.backupToTar(getPackageName(), domain, null, rootpath, filePath, - output.getData()); + + FullBackup.backupToTar(getPackageName(), domain, null, rootpath, filePath, output); } /** @@ -477,9 +479,8 @@ public abstract class BackupAgent extends ContextWrapper { continue; } - // Finally, back this file up before proceeding - FullBackup.backupToTar(packageName, domain, null, rootPath, filePath, - output.getData()); + // Finally, back this file up (or measure it) before proceeding + FullBackup.backupToTar(packageName, domain, null, rootPath, filePath, output); } } } @@ -640,7 +641,7 @@ public abstract class BackupAgent extends ContextWrapper { Binder.restoreCallingIdentity(ident); try { - callbackBinder.opComplete(token); + callbackBinder.opComplete(token, 0); } catch (RemoteException e) { // we'll time out anyway, so we're safe } @@ -670,7 +671,7 @@ public abstract class BackupAgent extends ContextWrapper { Binder.restoreCallingIdentity(ident); try { - callbackBinder.opComplete(token); + callbackBinder.opComplete(token, 0); } catch (RemoteException e) { // we'll time out anyway, so we're safe } @@ -692,10 +693,10 @@ public abstract class BackupAgent extends ContextWrapper { try { BackupAgent.this.onFullBackup(new FullBackupDataOutput(data)); } catch (IOException ex) { - Log.d(TAG, "onBackup (" + BackupAgent.this.getClass().getName() + ") threw", ex); + Log.d(TAG, "onFullBackup (" + BackupAgent.this.getClass().getName() + ") threw", ex); throw new RuntimeException(ex); } catch (RuntimeException ex) { - Log.d(TAG, "onBackup (" + BackupAgent.this.getClass().getName() + ") threw", ex); + Log.d(TAG, "onFullBackup (" + BackupAgent.this.getClass().getName() + ") threw", ex); throw ex; } finally { // ... and then again after, as in the doBackup() case @@ -713,13 +714,37 @@ public abstract class BackupAgent extends ContextWrapper { Binder.restoreCallingIdentity(ident); try { - callbackBinder.opComplete(token); + callbackBinder.opComplete(token, 0); } catch (RemoteException e) { // we'll time out anyway, so we're safe } } } + public void doMeasureFullBackup(int token, IBackupManager callbackBinder) { + // Ensure that we're running with the app's normal permission level + final long ident = Binder.clearCallingIdentity(); + FullBackupDataOutput measureOutput = new FullBackupDataOutput(); + + waitForSharedPrefs(); + try { + BackupAgent.this.onFullBackup(measureOutput); + } catch (IOException ex) { + Log.d(TAG, "onFullBackup[M] (" + BackupAgent.this.getClass().getName() + ") threw", ex); + throw new RuntimeException(ex); + } catch (RuntimeException ex) { + Log.d(TAG, "onFullBackup[M] (" + BackupAgent.this.getClass().getName() + ") threw", ex); + throw ex; + } finally { + Binder.restoreCallingIdentity(ident); + try { + callbackBinder.opComplete(token, measureOutput.getSize()); + } catch (RemoteException e) { + // timeout, so we're safe + } + } + } + @Override public void doRestoreFile(ParcelFileDescriptor data, long size, int type, String domain, String path, long mode, long mtime, @@ -728,6 +753,7 @@ public abstract class BackupAgent extends ContextWrapper { try { BackupAgent.this.onRestoreFile(data, size, type, domain, path, mode, mtime); } catch (IOException e) { + Log.d(TAG, "onRestoreFile (" + BackupAgent.this.getClass().getName() + ") threw", e); throw new RuntimeException(e); } finally { // Ensure that any side-effect SharedPreferences writes have landed @@ -735,7 +761,7 @@ public abstract class BackupAgent extends ContextWrapper { Binder.restoreCallingIdentity(ident); try { - callbackBinder.opComplete(token); + callbackBinder.opComplete(token, 0); } catch (RemoteException e) { // we'll time out anyway, so we're safe } @@ -747,13 +773,16 @@ public abstract class BackupAgent extends ContextWrapper { long ident = Binder.clearCallingIdentity(); try { BackupAgent.this.onRestoreFinished(); + } catch (Exception e) { + Log.d(TAG, "onRestoreFinished (" + BackupAgent.this.getClass().getName() + ") threw", e); + throw e; } finally { // Ensure that any side-effect SharedPreferences writes have landed waitForSharedPrefs(); Binder.restoreCallingIdentity(ident); try { - callbackBinder.opComplete(token); + callbackBinder.opComplete(token, 0); } catch (RemoteException e) { // we'll time out anyway, so we're safe } diff --git a/core/java/android/app/backup/BackupTransport.java b/core/java/android/app/backup/BackupTransport.java index e853540..ca6dc69 100644 --- a/core/java/android/app/backup/BackupTransport.java +++ b/core/java/android/app/backup/BackupTransport.java @@ -393,6 +393,26 @@ public class BackupTransport { } /** + * Called after {@link #performFullBackup} to make sure that the transport is willing to + * handle a full-data backup operation of the specified size on the current package. + * If the transport returns anything other than TRANSPORT_OK, the package's backup + * operation will be skipped (and {@link #finishBackup() invoked} with no data for that + * package being passed to {@link #sendBackupData}. + * + * Added in MNC (API 23). + * + * @param size The estimated size of the full-data payload for this app. This includes + * manifest and archive format overhead, but is not guaranteed to be precise. + * @return TRANSPORT_OK if the platform is to proceed with the full-data backup, + * TRANSPORT_PACKAGE_REJECTED if the proposed payload size is too large for + * the transport to handle, or TRANSPORT_ERROR to indicate a fatal error + * condition that means the platform cannot perform a backup at this time. + */ + public int checkFullBackupSize(long size) { + return BackupTransport.TRANSPORT_OK; + } + + /** * Tells the transport to read {@code numBytes} bytes of data from the socket file * descriptor provided in the {@link #performFullBackup(PackageInfo, ParcelFileDescriptor)} * call, and deliver those bytes to the datastore. @@ -588,6 +608,11 @@ public class BackupTransport { } @Override + public int checkFullBackupSize(long size) { + return BackupTransport.this.checkFullBackupSize(size); + } + + @Override public int sendBackupData(int numBytes) throws RemoteException { return BackupTransport.this.sendBackupData(numBytes); } diff --git a/core/java/android/app/backup/FullBackup.java b/core/java/android/app/backup/FullBackup.java index e5b47c6..259884e 100644 --- a/core/java/android/app/backup/FullBackup.java +++ b/core/java/android/app/backup/FullBackup.java @@ -58,7 +58,7 @@ public class FullBackup { * @hide */ static public native int backupToTar(String packageName, String domain, - String linkdomain, String rootpath, String path, BackupDataOutput output); + String linkdomain, String rootpath, String path, FullBackupDataOutput output); /** * Copy data from a socket to the given File location on permanent storage. The diff --git a/core/java/android/app/backup/FullBackupDataOutput.java b/core/java/android/app/backup/FullBackupDataOutput.java index 99dab1f..94704b9 100644 --- a/core/java/android/app/backup/FullBackupDataOutput.java +++ b/core/java/android/app/backup/FullBackupDataOutput.java @@ -9,7 +9,14 @@ import android.os.ParcelFileDescriptor; */ public class FullBackupDataOutput { // Currently a name-scoping shim around BackupDataOutput - private BackupDataOutput mData; + private final BackupDataOutput mData; + private long mSize; + + /** @hide - used only in measure operation */ + public FullBackupDataOutput() { + mData = null; + mSize = 0; + } /** @hide */ public FullBackupDataOutput(ParcelFileDescriptor fd) { @@ -18,4 +25,14 @@ public class FullBackupDataOutput { /** @hide */ public BackupDataOutput getData() { return mData; } + + /** @hide - used for measurement pass */ + public void addSize(long size) { + if (size > 0) { + mSize += size; + } + } + + /** @hide - used for measurement pass */ + public long getSize() { return mSize; } } diff --git a/core/java/android/app/backup/IBackupManager.aidl b/core/java/android/app/backup/IBackupManager.aidl index 41ad936..8f36dc4 100644 --- a/core/java/android/app/backup/IBackupManager.aidl +++ b/core/java/android/app/backup/IBackupManager.aidl @@ -286,11 +286,14 @@ interface IBackupManager { * Notify the backup manager that a BackupAgent has completed the operation * corresponding to the given token. * - * @param token The transaction token passed to a BackupAgent's doBackup() or - * doRestore() method. + * @param token The transaction token passed to the BackupAgent method being + * invoked. + * @param result In the case of a full backup measure operation, the estimated + * total file size that would result from the operation. Unused in all other + * cases. * {@hide} */ - void opComplete(int token); + void opComplete(int token, long result); /** * Make the device's backup and restore machinery (in)active. When it is inactive, diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index 030b770..7a99a79 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -1911,6 +1911,19 @@ public class Intent implements Parcelable, Cloneable { public static final String ACTION_PACKAGE_VERIFIED = "android.intent.action.PACKAGE_VERIFIED"; /** + * Broadcast Action: Sent to the system intent filter verifier when an intent filter + * needs to be verified. The data contains the filter data hosts to be verified against. + * <p class="note"> + * This is a protected intent that can only be sent by the system. + * </p> + * + * @hide + */ + @SystemApi + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_INTENT_FILTER_NEEDS_VERIFICATION = "android.intent.action.INTENT_FILTER_NEEDS_VERIFICATION"; + + /** * Broadcast Action: Resources for a set of packages (which were * previously unavailable) are currently * available since the media on which they exist is available. diff --git a/core/java/android/content/IntentFilter.java b/core/java/android/content/IntentFilter.java index 1240a23..590d791 100644 --- a/core/java/android/content/IntentFilter.java +++ b/core/java/android/content/IntentFilter.java @@ -16,10 +16,12 @@ package android.content; +import android.content.pm.PackageParser; import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; import android.os.PatternMatcher; +import android.text.TextUtils; import android.util.AndroidException; import android.util.Log; import android.util.Printer; @@ -150,6 +152,7 @@ public class IntentFilter implements Parcelable { private static final String CAT_STR = "cat"; private static final String NAME_STR = "name"; private static final String ACTION_STR = "action"; + private static final String AUTO_VERIFY_STR = "autoVerify"; /** * The filter {@link #setPriority} value at which system high-priority @@ -247,6 +250,19 @@ public class IntentFilter implements Parcelable { */ public static final int NO_MATCH_CATEGORY = -4; + /** + * HTTP scheme. + * + * @see #addDataScheme(String) + */ + public static final String SCHEME_HTTP = "http"; + /** + * HTTPS scheme. + * + * @see #addDataScheme(String) + */ + public static final String SCHEME_HTTPS = "https"; + private int mPriority; private final ArrayList<String> mActions; private ArrayList<String> mCategories = null; @@ -257,6 +273,13 @@ public class IntentFilter implements Parcelable { private ArrayList<String> mDataTypes = null; private boolean mHasPartialTypes = false; + private static final int STATE_VERIFY_AUTO = 0x00000001; + private static final int STATE_NEED_VERIFY = 0x00000010; + private static final int STATE_NEED_VERIFY_CHECKED = 0x00000100; + private static final int STATE_VERIFIED = 0x00001000; + + private int mVerifyState; + // These functions are the start of more optimized code for managing // the string sets... not yet implemented. @@ -326,7 +349,7 @@ public class IntentFilter implements Parcelable { public MalformedMimeTypeException(String name) { super(name); } - }; + } /** * Create a new IntentFilter instance with a specified action and MIME @@ -421,6 +444,7 @@ public class IntentFilter implements Parcelable { mDataPaths = new ArrayList<PatternMatcher>(o.mDataPaths); } mHasPartialTypes = o.mHasPartialTypes; + mVerifyState = o.mVerifyState; } /** @@ -452,6 +476,94 @@ public class IntentFilter implements Parcelable { } /** + * Set whether this filter will needs to be automatically verified against its data URIs or not. + * The default is false. + * + * The verification would need to happen only and only if the Intent action is + * {@link android.content.Intent#ACTION_VIEW} and the Intent category is + * {@link android.content.Intent#CATEGORY_BROWSABLE} and the Intent data scheme + * is "http" or "https". + * + * True means that the filter will need to use its data URIs to be verified. + * + * @param autoVerify The new autoVerify value. + * + * @see #getAutoVerify() + * @see #addAction(String) + * @see #getAction(int) + * @see #addCategory(String) + * @see #getCategory(int) + * @see #addDataScheme(String) + * @see #getDataScheme(int) + * + * @hide + */ + public final void setAutoVerify(boolean autoVerify) { + mVerifyState &= ~STATE_VERIFY_AUTO; + if (autoVerify) mVerifyState |= STATE_VERIFY_AUTO; + } + + /** + * Return if this filter will needs to be automatically verified again its data URIs or not. + * + * @return True if the filter will needs to be automatically verified. False otherwise. + * + * @see #setAutoVerify(boolean) + * + * @hide + */ + public final boolean getAutoVerify() { + return ((mVerifyState & STATE_VERIFY_AUTO) == 1); + } + + /** + * Return if this filter needs to be automatically verified again its data URIs or not. + * + * @return True if the filter needs to be automatically verified. False otherwise. + * + * This will check if if the Intent action is {@link android.content.Intent#ACTION_VIEW} and + * the Intent category is {@link android.content.Intent#CATEGORY_BROWSABLE} and the Intent + * data scheme is "http" or "https". + * + * @see #setAutoVerify(boolean) + * + * @hide + */ + public final boolean needsVerification() { + return hasAction(Intent.ACTION_VIEW) && + hasCategory(Intent.CATEGORY_BROWSABLE) && + (hasDataScheme(SCHEME_HTTP) || hasDataScheme(SCHEME_HTTPS)) && + getAutoVerify(); + } + + /** + * Return if this filter has been verified + * + * @return true if the filter has been verified or if autoVerify is false. + * + * @hide + */ + public final boolean isVerified() { + if ((mVerifyState & STATE_NEED_VERIFY_CHECKED) == STATE_NEED_VERIFY_CHECKED) { + return ((mVerifyState & STATE_NEED_VERIFY) == STATE_NEED_VERIFY); + } + return false; + } + + /** + * Set if this filter has been verified + * + * @param verified true if this filter has been verified. False otherwise. + * + * @hide + */ + public void setVerified(boolean verified) { + mVerifyState |= STATE_NEED_VERIFY_CHECKED; + mVerifyState &= ~STATE_VERIFIED; + if (verified) mVerifyState |= STATE_VERIFIED; + } + + /** * Add a new Intent action to match against. If any actions are included * in the filter, then an Intent's action must be one of those values for * it to match. If no actions are included, the Intent action is ignored. @@ -1333,6 +1445,7 @@ public class IntentFilter implements Parcelable { * Write the contents of the IntentFilter as an XML stream. */ public void writeToXml(XmlSerializer serializer) throws IOException { + serializer.attribute(null, AUTO_VERIFY_STR, Boolean.toString(getAutoVerify())); int N = countActions(); for (int i=0; i<N; i++) { serializer.startTag(null, ACTION_STR); @@ -1407,6 +1520,9 @@ public class IntentFilter implements Parcelable { public void readFromXml(XmlPullParser parser) throws XmlPullParserException, IOException { + String autoVerify = parser.getAttributeValue(null, AUTO_VERIFY_STR); + setAutoVerify(TextUtils.isEmpty(autoVerify) ? false : Boolean.getBoolean(autoVerify)); + int outerDepth = parser.getDepth(); int type; while ((type=parser.next()) != XmlPullParser.END_DOCUMENT @@ -1548,6 +1664,11 @@ public class IntentFilter implements Parcelable { sb.append(", mHasPartialTypes="); sb.append(mHasPartialTypes); du.println(sb.toString()); } + { + sb.setLength(0); + sb.append(prefix); sb.append("AutoVerify="); sb.append(getAutoVerify()); + du.println(sb.toString()); + } } public static final Parcelable.Creator<IntentFilter> CREATOR @@ -1614,6 +1735,7 @@ public class IntentFilter implements Parcelable { } dest.writeInt(mPriority); dest.writeInt(mHasPartialTypes ? 1 : 0); + dest.writeInt(getAutoVerify() ? 1 : 0); } /** @@ -1680,6 +1802,7 @@ public class IntentFilter implements Parcelable { } mPriority = source.readInt(); mHasPartialTypes = source.readInt() > 0; + setAutoVerify(source.readInt() > 0); } private final boolean findMimeType(String type) { @@ -1724,4 +1847,27 @@ public class IntentFilter implements Parcelable { return false; } + + /** + * @hide + */ + public ArrayList<String> getHostsList() { + ArrayList<String> result = new ArrayList<>(); + Iterator<IntentFilter.AuthorityEntry> it = authoritiesIterator(); + if (it != null) { + while (it.hasNext()) { + IntentFilter.AuthorityEntry entry = it.next(); + result.add(entry.getHost()); + } + } + return result; + } + + /** + * @hide + */ + public String[] getHosts() { + ArrayList<String> list = getHostsList(); + return list.toArray(new String[list.size()]); + } } diff --git a/core/java/android/content/pm/ApplicationInfo.java b/core/java/android/content/pm/ApplicationInfo.java index 8f17845..2496e45 100644 --- a/core/java/android/content/pm/ApplicationInfo.java +++ b/core/java/android/content/pm/ApplicationInfo.java @@ -340,8 +340,6 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { * cleartext network traffic, in which case platform components (e.g., HTTP stacks, * {@code WebView}, {@code MediaPlayer}) will refuse app's requests to use cleartext traffic. * Third-party libraries are encouraged to honor this flag as well. - * - * @hide */ public static final int FLAG_USES_CLEARTEXT_TRAFFIC = 1<<27; @@ -379,7 +377,8 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { * {@link #FLAG_LARGE_HEAP}, {@link #FLAG_STOPPED}, * {@link #FLAG_SUPPORTS_RTL}, {@link #FLAG_INSTALLED}, * {@link #FLAG_IS_DATA_ONLY}, {@link #FLAG_IS_GAME}, - * {@link #FLAG_FULL_BACKUP_ONLY}, {@link #FLAG_MULTIARCH}. + * {@link #FLAG_FULL_BACKUP_ONLY}, {@link #FLAG_USES_CLEARTEXT_TRAFFIC}, + * {@link #FLAG_MULTIARCH}. */ public int flags = 0; @@ -655,7 +654,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { } pw.println(prefix + "dataDir=" + dataDir); if (sharedLibraryFiles != null) { - pw.println(prefix + "sharedLibraryFiles=" + sharedLibraryFiles); + pw.println(prefix + "sharedLibraryFiles=" + Arrays.toString(sharedLibraryFiles)); } pw.println(prefix + "enabled=" + enabled + " targetSdkVersion=" + targetSdkVersion + " versionCode=" + versionCode); diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl index c6d97f1..66b0d1a 100644 --- a/core/java/android/content/pm/IPackageManager.aidl +++ b/core/java/android/content/pm/IPackageManager.aidl @@ -31,6 +31,7 @@ import android.content.pm.IPackageDeleteObserver2; import android.content.pm.IPackageDataObserver; import android.content.pm.IPackageMoveObserver; import android.content.pm.IPackageStatsObserver; +import android.content.pm.IntentFilterVerificationInfo; import android.content.pm.InstrumentationInfo; import android.content.pm.KeySet; import android.content.pm.PackageInfo; @@ -436,6 +437,11 @@ interface IPackageManager { void verifyPendingInstall(int id, int verificationCode); void extendVerificationTimeout(int id, int verificationCodeAtTimeout, long millisecondsToDelay); + void verifyIntentFilter(int id, int verificationCode, in List<String> outFailedDomains); + int getIntentVerificationStatus(String packageName, int userId); + boolean updateIntentVerificationStatus(String packageName, int status, int userId); + List<IntentFilterVerificationInfo> getIntentFilterVerifications(String packageName); + VerifierDeviceIdentity getVerifierDeviceIdentity(); boolean isFirstBoot(); diff --git a/core/java/android/content/pm/IntentFilterVerificationInfo.aidl b/core/java/android/content/pm/IntentFilterVerificationInfo.aidl new file mode 100644 index 0000000..00220e5 --- /dev/null +++ b/core/java/android/content/pm/IntentFilterVerificationInfo.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content.pm; + +parcelable IntentFilterVerificationInfo; diff --git a/core/java/android/content/pm/IntentFilterVerificationInfo.java b/core/java/android/content/pm/IntentFilterVerificationInfo.java new file mode 100644 index 0000000..60cb4a8 --- /dev/null +++ b/core/java/android/content/pm/IntentFilterVerificationInfo.java @@ -0,0 +1,224 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content.pm; + +import static android.content.pm.PackageManager.INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_UNDEFINED; +import static android.content.pm.PackageManager.INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ASK; +import static android.content.pm.PackageManager.INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ALWAYS; +import static android.content.pm.PackageManager.INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_NEVER; + +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; +import android.util.Log; +import com.android.internal.util.XmlUtils; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +import java.io.IOException; +import java.util.ArrayList; + +/** + * The {@link com.android.server.pm.PackageManagerService} maintains some + * {@link IntentFilterVerificationInfo}s for each domain / package / class name per user. + * + * @hide + */ +public final class IntentFilterVerificationInfo implements Parcelable { + private static final String TAG = IntentFilterVerificationInfo.class.getName(); + + private static final String TAG_DOMAIN = "domain"; + private static final String ATTR_DOMAIN_NAME = "name"; + private static final String ATTR_PACKAGE_NAME = "packageName"; + private static final String ATTR_STATUS = "status"; + + private String[] mDomains; + private String mPackageName; + private int mMainStatus; + + public IntentFilterVerificationInfo() { + mPackageName = null; + mDomains = new String[0]; + mMainStatus = INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_UNDEFINED; + } + + public IntentFilterVerificationInfo(String packageName, String[] domains) { + mPackageName = packageName; + mDomains = domains; + mMainStatus = INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_UNDEFINED; + } + + public IntentFilterVerificationInfo(XmlPullParser parser) + throws IOException, XmlPullParserException { + readFromXml(parser); + } + + public IntentFilterVerificationInfo(Parcel source) { + readFromParcel(source); + } + + public String[] getDomains() { + return mDomains; + } + + public String getPackageName() { + return mPackageName; + } + + public int getStatus() { + return mMainStatus; + } + + public void setStatus(int s) { + if (s >= INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_UNDEFINED && + s <= INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_NEVER) { + mMainStatus = s; + } else { + Log.w(TAG, "Trying to set a non supported status: " + s); + } + } + + public String getDomainsString() { + StringBuilder sb = new StringBuilder(); + for (String str : mDomains) { + if (sb.length() > 0) { + sb.append(" "); + } + sb.append(str); + } + return sb.toString(); + } + + String getStringFromXml(XmlPullParser parser, String attribute, String defaultValue) { + String value = parser.getAttributeValue(null, attribute); + if (value == null) { + String msg = "Missing element under " + TAG +": " + attribute + " at " + + parser.getPositionDescription(); + Log.w(TAG, msg); + return defaultValue; + } else { + return value; + } + } + + int getIntFromXml(XmlPullParser parser, String attribute, int defaultValue) { + String value = parser.getAttributeValue(null, attribute); + if (TextUtils.isEmpty(value)) { + String msg = "Missing element under " + TAG +": " + attribute + " at " + + parser.getPositionDescription(); + Log.w(TAG, msg); + return defaultValue; + } else { + return Integer.parseInt(value); + } + } + + public void readFromXml(XmlPullParser parser) throws XmlPullParserException, + IOException { + mPackageName = getStringFromXml(parser, ATTR_PACKAGE_NAME, null); + if (mPackageName == null) { + Log.e(TAG, "Package name cannot be null!"); + } + int status = getIntFromXml(parser, ATTR_STATUS, -1); + if (status == -1) { + Log.e(TAG, "Unknown status value: " + status); + } + mMainStatus = status; + + ArrayList<String> list = new ArrayList<>(); + int outerDepth = parser.getDepth(); + int type; + while ((type=parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG + || parser.getDepth() > outerDepth)) { + if (type == XmlPullParser.END_TAG + || type == XmlPullParser.TEXT) { + continue; + } + + String tagName = parser.getName(); + if (tagName.equals(TAG_DOMAIN)) { + String name = getStringFromXml(parser, ATTR_DOMAIN_NAME, null); + if (!TextUtils.isEmpty(name)) { + if (list == null) { + list = new ArrayList<>(); + } + list.add(name); + } + } else { + Log.w(TAG, "Unknown tag parsing IntentFilter: " + tagName); + } + XmlUtils.skipCurrentTag(parser); + } + + mDomains = list.toArray(new String[list.size()]); + } + + public void writeToXml(XmlSerializer serializer) throws IOException { + serializer.attribute(null, ATTR_PACKAGE_NAME, mPackageName); + serializer.attribute(null, ATTR_STATUS, String.valueOf(mMainStatus)); + for (String str : mDomains) { + serializer.startTag(null, TAG_DOMAIN); + serializer.attribute(null, ATTR_DOMAIN_NAME, str); + serializer.endTag(null, TAG_DOMAIN); + } + } + + public String getStatusString() { + return getStatusStringFromValue(mMainStatus); + } + + public static String getStatusStringFromValue(int val) { + switch (val) { + case INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ASK : return "ask"; + case INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ALWAYS : return "always"; + case INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_NEVER : return "never"; + default: + case INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_UNDEFINED : return "undefined"; + } + } + + @Override + public int describeContents() { + return 0; + } + + private void readFromParcel(Parcel source) { + mPackageName = source.readString(); + mMainStatus = source.readInt(); + mDomains = source.readStringArray(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mPackageName); + dest.writeInt(mMainStatus); + dest.writeStringArray(mDomains); + } + + public static final Creator<IntentFilterVerificationInfo> CREATOR = + new Creator<IntentFilterVerificationInfo>() { + public IntentFilterVerificationInfo createFromParcel(Parcel source) { + return new IntentFilterVerificationInfo(source); + } + public IntentFilterVerificationInfo[] newArray(int size) { + return new IntentFilterVerificationInfo[size]; + } + }; + +} diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index 59a16da..46d6ffb3 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -969,6 +969,60 @@ public abstract class PackageManager { public static final int VERIFICATION_REJECT = -1; /** + * Used as the {@code verificationCode} argument for + * {@link PackageManager#verifyIntentFilter} to indicate that the calling + * IntentFilter Verifier confirms that the IntentFilter is verified. + * + * @hide + */ + public static final int INTENT_FILTER_VERIFICATION_SUCCESS = 1; + + /** + * Used as the {@code verificationCode} argument for + * {@link PackageManager#verifyIntentFilter} to indicate that the calling + * IntentFilter Verifier confirms that the IntentFilter is NOT verified. + * + * @hide + */ + public static final int INTENT_FILTER_VERIFICATION_FAILURE = -1; + + /** + * Internal status code to indicate that an IntentFilter verification result is not specified. + * + * @hide + */ + public static final int INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_UNDEFINED = 0; + + /** + * Used as the {@code status} argument for {@link PackageManager#updateIntentVerificationStatus} + * to indicate that the User will always be prompted the Intent Disambiguation Dialog if there + * are two or more Intent resolved for the IntentFilter's domain(s). + * + * @hide + */ + public static final int INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ASK = 1; + + /** + * Used as the {@code status} argument for {@link PackageManager#updateIntentVerificationStatus} + * to indicate that the User will never be prompted the Intent Disambiguation Dialog if there + * are two or more resolution of the Intent. The default App for the domain(s) specified in the + * IntentFilter will also ALWAYS be used. + * + * @hide + */ + public static final int INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ALWAYS = 2; + + /** + * Used as the {@code status} argument for {@link PackageManager#updateIntentVerificationStatus} + * to indicate that the User may be prompted the Intent Disambiguation Dialog if there + * are two or more Intent resolved. The default App for the domain(s) specified in the + * IntentFilter will also NEVER be presented to the User. + * + * @hide + */ + public static final int INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_NEVER = 3; + + /** * Can be used as the {@code millisecondsToDelay} argument for * {@link PackageManager#extendVerificationTimeout}. This is the * maximum time {@code PackageManager} waits for the verification @@ -1600,6 +1654,12 @@ public abstract class PackageManager { @SdkConstant(SdkConstantType.FEATURE) public static final String FEATURE_GAMEPAD = "android.hardware.gamepad"; + /** + * Feature for {@link #getSystemAvailableFeatures} and {@link #hasSystemFeature}: + * The device has a full implementation of the android.media.midi.* APIs. + */ + @SdkConstant(SdkConstantType.FEATURE) + public static final String FEATURE_MIDI = "android.software.midi"; /** * Action to external storage service to clean out removed apps. @@ -1674,8 +1734,52 @@ public abstract class PackageManager { = "android.content.pm.extra.VERIFICATION_VERSION_CODE"; /** - * The action used to request that the user approve a grant permissions - * request from the application. + * Extra field name for the ID of a intent filter pending verification. Passed to + * an intent filter verifier and is used to call back to + * {@link PackageManager#verifyIntentFilter(int, int)} + * + * @hide + */ + public static final String EXTRA_INTENT_FILTER_VERIFICATION_ID + = "android.content.pm.extra.INTENT_FILTER_VERIFICATION_ID"; + + /** + * Extra field name for the scheme used for an intent filter pending verification. Passed to + * an intent filter verifier and is used to construct the URI to verify against. + * + * Usually this is "https" + * + * @hide + */ + public static final String EXTRA_INTENT_FILTER_VERIFICATION_URI_SCHEME + = "android.content.pm.extra.INTENT_FILTER_VERIFICATION_URI_SCHEME"; + + /** + * Extra field name for the host names to be used for an intent filter pending verification. + * Passed to an intent filter verifier and is used to construct the URI to verify the + * intent filter. + * + * This is a space delimited list of hosts. + * + * @hide + */ + public static final String EXTRA_INTENT_FILTER_VERIFICATION_HOSTS + = "android.content.pm.extra.INTENT_FILTER_VERIFICATION_HOSTS"; + + /** + * Extra field name for the package name to be used for an intent filter pending verification. + * Passed to an intent filter verifier and is used to check the verification responses coming + * from the hosts. Each host response will need to include the package name of APK containing + * the intent filter. + * + * @hide + */ + public static final String EXTRA_INTENT_FILTER_VERIFICATION_PACKAGE_NAME + = "android.content.pm.extra.INTENT_FILTER_VERIFICATION_PACKAGE_NAME"; + + /** + * The action used to request that the user approve a permission request + * from the application. * * @hide */ @@ -3455,6 +3559,85 @@ public abstract class PackageManager { int verificationCodeAtTimeout, long millisecondsToDelay); /** + * Allows a package listening to the + * {@link Intent#ACTION_INTENT_FILTER_NEEDS_VERIFICATION intent filter verification + * broadcast} to respond to the package manager. The response must include + * the {@code verificationCode} which is one of + * {@link PackageManager#INTENT_FILTER_VERIFICATION_SUCCESS} or + * {@link PackageManager#INTENT_FILTER_VERIFICATION_FAILURE}. + * + * @param verificationId pending package identifier as passed via the + * {@link PackageManager#EXTRA_VERIFICATION_ID} Intent extra. + * @param verificationCode either {@link PackageManager#INTENT_FILTER_VERIFICATION_SUCCESS} + * or {@link PackageManager#INTENT_FILTER_VERIFICATION_FAILURE}. + * @param outFailedDomains a list of failed domains if the verificationCode is + * {@link PackageManager#INTENT_FILTER_VERIFICATION_FAILURE}, otherwise null; + * @throws SecurityException if the caller does not have the + * INTENT_FILTER_VERIFICATION_AGENT permission. + * + * @hide + */ + public abstract void verifyIntentFilter(int verificationId, int verificationCode, + List<String> outFailedDomains); + + /** + * Get the status of a Domain Verification Result for an IntentFilter. This is + * related to the {@link android.content.IntentFilter#setAutoVerify(boolean)} and + * {@link android.content.IntentFilter#getAutoVerify()} + * + * This is used by the ResolverActivity to change the status depending on what the User select + * in the Disambiguation Dialog and also used by the Settings App for changing the default App + * for a domain. + * + * @param packageName The package name of the Activity associated with the IntentFilter. + * @param userId The user id. + * + * @return The status to set to. This can be + * {@link #INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ASK} or + * {@link #INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ALWAYS} or + * {@link #INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_NEVER} or + * {@link #INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_UNDEFINED} + * + * @hide + */ + public abstract int getIntentVerificationStatus(String packageName, int userId); + + /** + * Allow to change the status of a Intent Verification status for all IntentFilter of an App. + * This is related to the {@link android.content.IntentFilter#setAutoVerify(boolean)} and + * {@link android.content.IntentFilter#getAutoVerify()} + * + * This is used by the ResolverActivity to change the status depending on what the User select + * in the Disambiguation Dialog and also used by the Settings App for changing the default App + * for a domain. + * + * @param packageName The package name of the Activity associated with the IntentFilter. + * @param status The status to set to. This can be + * {@link #INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ASK} or + * {@link #INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ALWAYS} or + * {@link #INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_NEVER} + * @param userId The user id. + * + * @return true if the status has been set. False otherwise. + * + * @hide + */ + public abstract boolean updateIntentVerificationStatus(String packageName, int status, + int userId); + + /** + * Get the list of IntentFilterVerificationInfo for a specific package and User. + * + * @param packageName the package name. When this parameter is set to a non null value, + * the results will be filtered by the package name provided. + * Otherwise, there will be no filtering and it will return a list + * corresponding for all packages for the provided userId. + * @return a list of IntentFilterVerificationInfo for a specific package and User. + */ + public abstract List<IntentFilterVerificationInfo> getIntentFilterVerifications( + String packageName); + + /** * Change the installer associated with a given package. There are limitations * on how the installer package can be changed; in particular: * <ul> diff --git a/core/java/android/content/pm/PackageParser.java b/core/java/android/content/pm/PackageParser.java index 212cf6d..4b81fd4 100644 --- a/core/java/android/content/pm/PackageParser.java +++ b/core/java/android/content/pm/PackageParser.java @@ -2750,15 +2750,21 @@ public class PackageParser { } } - addSharedLibrariesForBackwardCompatibility(owner); + modifySharedLibrariesForBackwardCompatibility(owner); return true; } - private static void addSharedLibrariesForBackwardCompatibility(Package owner) { - if (owner.applicationInfo.targetSdkVersion <= Build.VERSION_CODES.LOLLIPOP_MR1) { - owner.usesLibraries = ArrayUtils.add(owner.usesLibraries, "org.apache.http.legacy"); - } + private static void modifySharedLibrariesForBackwardCompatibility(Package owner) { + // "org.apache.http.legacy" is now a part of the boot classpath so it doesn't need + // to be an explicit dependency. + // + // A future change will remove this library from the boot classpath, at which point + // all apps that target SDK 21 and earlier will have it automatically added to their + // dependency lists. + owner.usesLibraries = ArrayUtils.remove(owner.usesLibraries, "org.apache.http.legacy"); + owner.usesOptionalLibraries = ArrayUtils.remove(owner.usesOptionalLibraries, + "org.apache.http.legacy"); } /** @@ -3149,7 +3155,7 @@ public class PackageParser { if (parser.getName().equals("intent-filter")) { ActivityIntentInfo intent = new ActivityIntentInfo(a); - if (!parseIntent(res, parser, attrs, true, intent, outError)) { + if (!parseIntent(res, parser, attrs, true, true, intent, outError)) { return null; } if (intent.countActions() == 0) { @@ -3161,7 +3167,7 @@ public class PackageParser { } } else if (!receiver && parser.getName().equals("preferred")) { ActivityIntentInfo intent = new ActivityIntentInfo(a); - if (!parseIntent(res, parser, attrs, false, intent, outError)) { + if (!parseIntent(res, parser, attrs, false, false, intent, outError)) { return null; } if (intent.countActions() == 0) { @@ -3341,7 +3347,7 @@ public class PackageParser { if (parser.getName().equals("intent-filter")) { ActivityIntentInfo intent = new ActivityIntentInfo(a); - if (!parseIntent(res, parser, attrs, true, intent, outError)) { + if (!parseIntent(res, parser, attrs, true, true, intent, outError)) { return null; } if (intent.countActions() == 0) { @@ -3521,7 +3527,7 @@ public class PackageParser { if (parser.getName().equals("intent-filter")) { ProviderIntentInfo intent = new ProviderIntentInfo(outInfo); - if (!parseIntent(res, parser, attrs, true, intent, outError)) { + if (!parseIntent(res, parser, attrs, true, false, intent, outError)) { return false; } outInfo.intents.add(intent); @@ -3780,7 +3786,7 @@ public class PackageParser { if (parser.getName().equals("intent-filter")) { ServiceIntentInfo intent = new ServiceIntentInfo(s); - if (!parseIntent(res, parser, attrs, true, intent, outError)) { + if (!parseIntent(res, parser, attrs, true, false, intent, outError)) { return null; } @@ -3981,7 +3987,7 @@ public class PackageParser { = "http://schemas.android.com/apk/res/android"; private boolean parseIntent(Resources res, XmlPullParser parser, AttributeSet attrs, - boolean allowGlobs, IntentInfo outInfo, String[] outError) + boolean allowGlobs, boolean allowAutoVerify, IntentInfo outInfo, String[] outError) throws XmlPullParserException, IOException { TypedArray sa = res.obtainAttributes(attrs, @@ -4006,6 +4012,12 @@ public class PackageParser { outInfo.banner = sa.getResourceId( com.android.internal.R.styleable.AndroidManifestIntentFilter_banner, 0); + if (allowAutoVerify) { + outInfo.setAutoVerify(sa.getBoolean( + com.android.internal.R.styleable.AndroidManifestIntentFilter_autoVerify, + false)); + } + sa.recycle(); int outerDepth = parser.getDepth(); diff --git a/core/java/android/content/pm/PackageUserState.java b/core/java/android/content/pm/PackageUserState.java index a9c7be3..92b8055 100644 --- a/core/java/android/content/pm/PackageUserState.java +++ b/core/java/android/content/pm/PackageUserState.java @@ -37,10 +37,14 @@ public class PackageUserState { public ArraySet<String> disabledComponents; public ArraySet<String> enabledComponents; + public int domainVerificationStatus; + public PackageUserState() { installed = true; hidden = false; enabled = COMPONENT_ENABLED_STATE_DEFAULT; + domainVerificationStatus = + PackageManager.INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_UNDEFINED; } public PackageUserState(PackageUserState o) { @@ -51,9 +55,10 @@ public class PackageUserState { hidden = o.hidden; lastDisableAppCaller = o.lastDisableAppCaller; disabledComponents = o.disabledComponents != null - ? new ArraySet<String>(o.disabledComponents) : null; + ? new ArraySet<>(o.disabledComponents) : null; enabledComponents = o.enabledComponents != null - ? new ArraySet<String>(o.enabledComponents) : null; + ? new ArraySet<>(o.enabledComponents) : null; blockUninstall = o.blockUninstall; + domainVerificationStatus = o.domainVerificationStatus; } } diff --git a/core/java/android/content/pm/ResolveInfo.java b/core/java/android/content/pm/ResolveInfo.java index fe3aec9..7b141f0 100644 --- a/core/java/android/content/pm/ResolveInfo.java +++ b/core/java/android/content/pm/ResolveInfo.java @@ -143,6 +143,11 @@ public class ResolveInfo implements Parcelable { */ public boolean system; + /** + * @hide Does the associated IntentFilter needs verification ? + */ + public boolean filterNeedsVerification; + private ComponentInfo getComponentInfo() { if (activityInfo != null) return activityInfo; if (serviceInfo != null) return serviceInfo; @@ -283,6 +288,7 @@ public class ResolveInfo implements Parcelable { resolvePackageName = orig.resolvePackageName; system = orig.system; targetUserId = orig.targetUserId; + filterNeedsVerification = orig.filterNeedsVerification; } public String toString() { @@ -344,6 +350,7 @@ public class ResolveInfo implements Parcelable { dest.writeInt(targetUserId); dest.writeInt(system ? 1 : 0); dest.writeInt(noResourceId ? 1 : 0); + dest.writeInt(filterNeedsVerification ? 1 : 0); } public static final Creator<ResolveInfo> CREATOR @@ -389,6 +396,7 @@ public class ResolveInfo implements Parcelable { targetUserId = source.readInt(); system = source.readInt() != 0; noResourceId = source.readInt() != 0; + filterNeedsVerification = source.readInt() != 0; } public static class DisplayNameComparator diff --git a/core/java/android/content/res/Configuration.java b/core/java/android/content/res/Configuration.java index 14af584..b5eeb30 100644 --- a/core/java/android/content/res/Configuration.java +++ b/core/java/android/content/res/Configuration.java @@ -1314,7 +1314,8 @@ public final class Configuration implements Parcelable, Comparable<Configuration * {@link View#LAYOUT_DIRECTION_LTR}. If not null will set it to the layout direction * corresponding to the Locale. * - * @see {@link View#LAYOUT_DIRECTION_LTR} and {@link View#LAYOUT_DIRECTION_RTL} + * @see View#LAYOUT_DIRECTION_LTR + * @see View#LAYOUT_DIRECTION_RTL */ public void setLayoutDirection(Locale locale) { // There is a "1" difference between the configuration values for diff --git a/core/java/android/content/res/Resources.java b/core/java/android/content/res/Resources.java index 95ad57e..44018ff 100644 --- a/core/java/android/content/res/Resources.java +++ b/core/java/android/content/res/Resources.java @@ -111,12 +111,12 @@ public class Resources { // single-threaded, and after that these are immutable. private static final LongSparseArray<ConstantState>[] sPreloadedDrawables; private static final LongSparseArray<ConstantState> sPreloadedColorDrawables - = new LongSparseArray<ConstantState>(); + = new LongSparseArray<>(); private static final LongSparseArray<ColorStateListFactory> sPreloadedColorStateLists - = new LongSparseArray<ColorStateListFactory>(); + = new LongSparseArray<>(); // Pool of TypedArrays targeted to this Resources object. - final SynchronizedPool<TypedArray> mTypedArrayPool = new SynchronizedPool<TypedArray>(5); + final SynchronizedPool<TypedArray> mTypedArrayPool = new SynchronizedPool<>(5); // Used by BridgeResources in layoutlib static Resources mSystem = null; @@ -128,21 +128,19 @@ public class Resources { private final Object mAccessLock = new Object(); private final Configuration mTmpConfig = new Configuration(); private final ArrayMap<String, LongSparseArray<WeakReference<ConstantState>>> mDrawableCache = - new ArrayMap<String, LongSparseArray<WeakReference<ConstantState>>>(); + new ArrayMap<>(); private final ArrayMap<String, LongSparseArray<WeakReference<ConstantState>>> mColorDrawableCache = - new ArrayMap<String, LongSparseArray<WeakReference<ConstantState>>>(); + new ArrayMap<>(); private final ConfigurationBoundResourceCache<ColorStateList> mColorStateListCache = - new ConfigurationBoundResourceCache<ColorStateList>(this); + new ConfigurationBoundResourceCache<>(this); private final ConfigurationBoundResourceCache<Animator> mAnimatorCache = - new ConfigurationBoundResourceCache<Animator>(this); + new ConfigurationBoundResourceCache<>(this); private final ConfigurationBoundResourceCache<StateListAnimator> mStateListAnimatorCache = - new ConfigurationBoundResourceCache<StateListAnimator>(this); + new ConfigurationBoundResourceCache<>(this); private TypedValue mTmpValue = new TypedValue(); private boolean mPreloading; - private TypedArray mCachedStyledAttributes = null; - private int mLastCachedXmlBlockIndex = -1; private final int[] mCachedXmlBlockIds = { 0, 0, 0, 0 }; private final XmlBlock[] mCachedXmlBlocks = new XmlBlock[4]; @@ -157,8 +155,8 @@ public class Resources { static { sPreloadedDrawables = new LongSparseArray[2]; - sPreloadedDrawables[0] = new LongSparseArray<ConstantState>(); - sPreloadedDrawables[1] = new LongSparseArray<ConstantState>(); + sPreloadedDrawables[0] = new LongSparseArray<>(); + sPreloadedDrawables[1] = new LongSparseArray<>(); } /** @@ -1876,7 +1874,7 @@ public class Resources { // the framework. mCompatibilityInfo.applyToDisplayMetrics(mMetrics); - int configChanges = calcConfigChanges(config); + final int configChanges = calcConfigChanges(config); if (mConfiguration.locale == null) { mConfiguration.locale = Locale.getDefault(); mConfiguration.setLayoutDirection(mConfiguration.locale); @@ -1891,7 +1889,8 @@ public class Resources { if (mConfiguration.locale != null) { locale = adjustLanguageTag(mConfiguration.locale.toLanguageTag()); } - int width, height; + + final int width, height; if (mMetrics.widthPixels >= mMetrics.heightPixels) { width = mMetrics.widthPixels; height = mMetrics.heightPixels; @@ -1901,12 +1900,15 @@ public class Resources { //noinspection SuspiciousNameCombination height = mMetrics.widthPixels; } - int keyboardHidden = mConfiguration.keyboardHidden; - if (keyboardHidden == Configuration.KEYBOARDHIDDEN_NO - && mConfiguration.hardKeyboardHidden - == Configuration.HARDKEYBOARDHIDDEN_YES) { + + final int keyboardHidden; + if (mConfiguration.keyboardHidden == Configuration.KEYBOARDHIDDEN_NO + && mConfiguration.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_YES) { keyboardHidden = Configuration.KEYBOARDHIDDEN_SOFT; + } else { + keyboardHidden = mConfiguration.keyboardHidden; } + mAssets.setConfiguration(mConfiguration.mcc, mConfiguration.mnc, locale, mConfiguration.orientation, mConfiguration.touchscreen, @@ -2508,10 +2510,10 @@ public class Resources { // Clean out the caches before we add more. This shouldn't // happen very often. pruneCaches(caches); - themedCache = new LongSparseArray<WeakReference<ConstantState>>(1); + themedCache = new LongSparseArray<>(1); caches.put(themeKey, themedCache); } - themedCache.put(key, new WeakReference<ConstantState>(cs)); + themedCache.put(key, new WeakReference<>(cs)); } } } @@ -2830,15 +2832,6 @@ public class Resources { + Integer.toHexString(id)); } - /*package*/ void recycleCachedStyledAttributes(TypedArray attrs) { - synchronized (mAccessLock) { - final TypedArray cached = mCachedStyledAttributes; - if (cached == null || cached.mData.length < attrs.mData.length) { - mCachedStyledAttributes = attrs; - } - } - } - /** * Obtains styled attributes from the theme, if available, or unstyled * resources if the theme is null. diff --git a/core/java/android/database/DatabaseErrorHandler.java b/core/java/android/database/DatabaseErrorHandler.java index f0c5452..55ad921 100644 --- a/core/java/android/database/DatabaseErrorHandler.java +++ b/core/java/android/database/DatabaseErrorHandler.java @@ -19,13 +19,12 @@ package android.database; import android.database.sqlite.SQLiteDatabase; /** - * An interface to let the apps define the actions to take when the following errors are detected - * database corruption + * An interface to let apps define an action to take when database corruption is detected. */ public interface DatabaseErrorHandler { /** - * defines the method to be invoked when database corruption is detected. + * The method invoked when database corruption is detected. * @param dbObj the {@link SQLiteDatabase} object representing the database on which corruption * is detected. */ diff --git a/core/java/android/database/DefaultDatabaseErrorHandler.java b/core/java/android/database/DefaultDatabaseErrorHandler.java index b234e34..7fa2b40 100755 --- a/core/java/android/database/DefaultDatabaseErrorHandler.java +++ b/core/java/android/database/DefaultDatabaseErrorHandler.java @@ -24,7 +24,7 @@ import android.util.Log; import android.util.Pair; /** - * Default class used to define the actions to take when the database corruption is reported + * Default class used to define the action to take when database corruption is reported * by sqlite. * <p> * An application can specify an implementation of {@link DatabaseErrorHandler} on the @@ -38,7 +38,7 @@ import android.util.Pair; * The specified {@link DatabaseErrorHandler} is used to handle database corruption errors, if they * occur. * <p> - * If null is specified for DatabaeErrorHandler param in the above calls, then this class is used + * If null is specified for the DatabaseErrorHandler param in the above calls, this class is used * as the default {@link DatabaseErrorHandler}. */ public final class DefaultDatabaseErrorHandler implements DatabaseErrorHandler { diff --git a/core/java/android/hardware/SystemSensorManager.java b/core/java/android/hardware/SystemSensorManager.java index a6c3ea4..88fa339 100644 --- a/core/java/android/hardware/SystemSensorManager.java +++ b/core/java/android/hardware/SystemSensorManager.java @@ -54,11 +54,13 @@ public class SystemSensorManager extends SensorManager { // Looper associated with the context in which this instance was created. private final Looper mMainLooper; private final int mTargetSdkLevel; + private final String mPackageName; /** {@hide} */ public SystemSensorManager(Context context, Looper mainLooper) { mMainLooper = mainLooper; mTargetSdkLevel = context.getApplicationInfo().targetSdkVersion; + mPackageName = context.getPackageName(); synchronized(sSensorModuleLock) { if (!sSensorModuleInitialized) { sSensorModuleInitialized = true; @@ -117,14 +119,14 @@ public class SystemSensorManager extends SensorManager { if (queue == null) { Looper looper = (handler != null) ? handler.getLooper() : mMainLooper; queue = new SensorEventQueue(listener, looper, this); - if (!queue.addSensor(sensor, delayUs, maxBatchReportLatencyUs, reservedFlags)) { + if (!queue.addSensor(sensor, delayUs, maxBatchReportLatencyUs)) { queue.dispose(); return false; } mSensorListeners.put(listener, queue); return true; } else { - return queue.addSensor(sensor, delayUs, maxBatchReportLatencyUs, reservedFlags); + return queue.addSensor(sensor, delayUs, maxBatchReportLatencyUs); } } } @@ -165,14 +167,14 @@ public class SystemSensorManager extends SensorManager { TriggerEventQueue queue = mTriggerListeners.get(listener); if (queue == null) { queue = new TriggerEventQueue(listener, mMainLooper, this); - if (!queue.addSensor(sensor, 0, 0, 0)) { + if (!queue.addSensor(sensor, 0, 0)) { queue.dispose(); return false; } mTriggerListeners.put(listener, queue); return true; } else { - return queue.addSensor(sensor, 0, 0, 0); + return queue.addSensor(sensor, 0, 0); } } } @@ -223,9 +225,9 @@ public class SystemSensorManager extends SensorManager { */ private static abstract class BaseEventQueue { private native long nativeInitBaseEventQueue(BaseEventQueue eventQ, MessageQueue msgQ, - float[] scratch); + float[] scratch, String packageName); private static native int nativeEnableSensor(long eventQ, int handle, int rateUs, - int maxBatchReportLatencyUs, int reservedFlags); + int maxBatchReportLatencyUs); private static native int nativeDisableSensor(long eventQ, int handle); private static native void nativeDestroySensorEventQueue(long eventQ); private static native int nativeFlushSensor(long eventQ); @@ -238,7 +240,8 @@ public class SystemSensorManager extends SensorManager { protected final SystemSensorManager mManager; BaseEventQueue(Looper looper, SystemSensorManager manager) { - nSensorEventQueue = nativeInitBaseEventQueue(this, looper.getQueue(), mScratch); + nSensorEventQueue = nativeInitBaseEventQueue(this, looper.getQueue(), mScratch, + manager.mPackageName); mCloseGuard.open("dispose"); mManager = manager; } @@ -248,7 +251,7 @@ public class SystemSensorManager extends SensorManager { } public boolean addSensor( - Sensor sensor, int delayUs, int maxBatchReportLatencyUs, int reservedFlags) { + Sensor sensor, int delayUs, int maxBatchReportLatencyUs) { // Check if already present. int handle = sensor.getHandle(); if (mActiveSensors.get(handle)) return false; @@ -256,10 +259,10 @@ public class SystemSensorManager extends SensorManager { // Get ready to receive events before calling enable. mActiveSensors.put(handle, true); addSensorEvent(sensor); - if (enableSensor(sensor, delayUs, maxBatchReportLatencyUs, reservedFlags) != 0) { + if (enableSensor(sensor, delayUs, maxBatchReportLatencyUs) != 0) { // Try continuous mode if batching fails. if (maxBatchReportLatencyUs == 0 || - maxBatchReportLatencyUs > 0 && enableSensor(sensor, delayUs, 0, 0) != 0) { + maxBatchReportLatencyUs > 0 && enableSensor(sensor, delayUs, 0) != 0) { removeSensor(sensor, false); return false; } @@ -328,11 +331,11 @@ public class SystemSensorManager extends SensorManager { } private int enableSensor( - Sensor sensor, int rateUs, int maxBatchReportLatencyUs, int reservedFlags) { + Sensor sensor, int rateUs, int maxBatchReportLatencyUs) { if (nSensorEventQueue == 0) throw new NullPointerException(); if (sensor == null) throw new NullPointerException(); return nativeEnableSensor(nSensorEventQueue, sensor.getHandle(), rateUs, - maxBatchReportLatencyUs, reservedFlags); + maxBatchReportLatencyUs); } private int disableSensor(Sensor sensor) { diff --git a/core/java/android/hardware/camera2/CaptureRequest.java b/core/java/android/hardware/camera2/CaptureRequest.java index 7569ea5..b8fb8e7 100644 --- a/core/java/android/hardware/camera2/CaptureRequest.java +++ b/core/java/android/hardware/camera2/CaptureRequest.java @@ -1541,7 +1541,8 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>> * to the camera, that the JPEG picture needs to be rotated by, to be viewed * upright.</p> * <p>Camera devices may either encode this value into the JPEG EXIF header, or - * rotate the image data to match this orientation.</p> + * rotate the image data to match this orientation. When the image data is rotated, + * the thumbnail data will also be rotated.</p> * <p>Note that this orientation is relative to the orientation of the camera sensor, given * by {@link CameraCharacteristics#SENSOR_ORIENTATION android.sensor.orientation}.</p> * <p>To translate from the device orientation given by the Android sensor APIs, the following diff --git a/core/java/android/hardware/camera2/CaptureResult.java b/core/java/android/hardware/camera2/CaptureResult.java index b84dc2e..e346dc2 100644 --- a/core/java/android/hardware/camera2/CaptureResult.java +++ b/core/java/android/hardware/camera2/CaptureResult.java @@ -2230,7 +2230,8 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { * to the camera, that the JPEG picture needs to be rotated by, to be viewed * upright.</p> * <p>Camera devices may either encode this value into the JPEG EXIF header, or - * rotate the image data to match this orientation.</p> + * rotate the image data to match this orientation. When the image data is rotated, + * the thumbnail data will also be rotated.</p> * <p>Note that this orientation is relative to the orientation of the camera sensor, given * by {@link CameraCharacteristics#SENSOR_ORIENTATION android.sensor.orientation}.</p> * <p>To translate from the device orientation given by the Android sensor APIs, the following diff --git a/core/java/android/hardware/hdmi/HdmiTvClient.java b/core/java/android/hardware/hdmi/HdmiTvClient.java index a94c1da..a336e5c 100644 --- a/core/java/android/hardware/hdmi/HdmiTvClient.java +++ b/core/java/android/hardware/hdmi/HdmiTvClient.java @@ -168,7 +168,22 @@ public final class HdmiTvClient extends HdmiClient { } /** - * Sets system audio volume + * Sets system audio mode. + * + * @param enabled set to {@code true} to enable the mode; otherwise {@code false} + * @param callback callback to get the result with + * @throws {@link IllegalArgumentException} if the {@code callback} is null + */ + public void setSystemAudioMode(boolean enabled, SelectCallback callback) { + try { + mService.setSystemAudioMode(enabled, getCallbackWrapper(callback)); + } catch (RemoteException e) { + Log.e(TAG, "failed to set system audio mode:", e); + } + } + + /** + * Sets system audio volume. * * @param oldIndex current volume index * @param newIndex volume index to be set @@ -183,7 +198,7 @@ public final class HdmiTvClient extends HdmiClient { } /** - * Sets system audio mute status + * Sets system audio mute status. * * @param mute {@code true} if muted; otherwise, {@code false} */ @@ -196,7 +211,7 @@ public final class HdmiTvClient extends HdmiClient { } /** - * Sets record listener + * Sets record listener. * * @param listener */ diff --git a/core/java/android/net/NetworkStats.java b/core/java/android/net/NetworkStats.java index 0766253..77d7e0c 100644 --- a/core/java/android/net/NetworkStats.java +++ b/core/java/android/net/NetworkStats.java @@ -50,12 +50,19 @@ public class NetworkStats implements Parcelable { public static final int UID_ALL = -1; /** {@link #tag} value matching any tag. */ public static final int TAG_ALL = -1; - /** {@link #set} value when all sets combined. */ + /** {@link #set} value when all sets combined, not including debug sets. */ public static final int SET_ALL = -1; /** {@link #set} value where background data is accounted. */ public static final int SET_DEFAULT = 0; /** {@link #set} value where foreground data is accounted. */ public static final int SET_FOREGROUND = 1; + /** All {@link #set} value greater than SET_DEBUG_START are debug {@link #set} values. */ + public static final int SET_DEBUG_START = 1000; + /** Debug {@link #set} value when the VPN stats are moved in. */ + public static final int SET_DBG_VPN_IN = 1001; + /** Debug {@link #set} value when the VPN stats are moved out of a vpn UID. */ + public static final int SET_DBG_VPN_OUT = 1002; + /** {@link #tag} value for total data across all tags. */ public static final int TAG_NONE = 0; @@ -729,6 +736,10 @@ public class NetworkStats implements Parcelable { return "DEFAULT"; case SET_FOREGROUND: return "FOREGROUND"; + case SET_DBG_VPN_IN: + return "DBG_VPN_IN"; + case SET_DBG_VPN_OUT: + return "DBG_VPN_OUT"; default: return "UNKNOWN"; } @@ -745,12 +756,27 @@ public class NetworkStats implements Parcelable { return "def"; case SET_FOREGROUND: return "fg"; + case SET_DBG_VPN_IN: + return "vpnin"; + case SET_DBG_VPN_OUT: + return "vpnout"; default: return "unk"; } } /** + * @return true if the querySet matches the dataSet. + */ + public static boolean setMatches(int querySet, int dataSet) { + if (querySet == dataSet) { + return true; + } + // SET_ALL matches all non-debugging sets. + return querySet == SET_ALL && dataSet < SET_DEBUG_START; + } + + /** * Return text description of {@link #tag} value. */ public static String tagToString(int tag) { @@ -843,6 +869,9 @@ public class NetworkStats implements Parcelable { if (recycle.uid == UID_ALL) { throw new IllegalStateException( "Cannot adjust VPN accounting on an iface aggregated NetworkStats."); + } if (recycle.set == SET_DBG_VPN_IN || recycle.set == SET_DBG_VPN_OUT) { + throw new IllegalStateException( + "Cannot adjust VPN accounting on a NetworkStats containing SET_DBG_VPN_*"); } if (recycle.uid == tunUid && recycle.tag == TAG_NONE @@ -906,6 +935,9 @@ public class NetworkStats implements Parcelable { combineValues(tmpEntry); if (tag[i] == TAG_NONE) { moved.add(tmpEntry); + // Add debug info + tmpEntry.set = SET_DBG_VPN_IN; + combineValues(tmpEntry); } } } @@ -913,6 +945,13 @@ public class NetworkStats implements Parcelable { } private void deductTrafficFromVpnApp(int tunUid, String underlyingIface, Entry moved) { + // Add debug info + moved.uid = tunUid; + moved.set = SET_DBG_VPN_OUT; + moved.tag = TAG_NONE; + moved.iface = underlyingIface; + combineValues(moved); + // Caveat: if the vpn software uses tag, the total tagged traffic may be greater than // the TAG_NONE traffic. int idxVpnBackground = findIndex(underlyingIface, tunUid, SET_DEFAULT, TAG_NONE); diff --git a/core/java/android/net/TrafficStats.java b/core/java/android/net/TrafficStats.java index 3f18519..ff3de2b 100644 --- a/core/java/android/net/TrafficStats.java +++ b/core/java/android/net/TrafficStats.java @@ -51,6 +51,8 @@ public class TrafficStats { public static final long MB_IN_BYTES = KB_IN_BYTES * 1024; /** @hide */ public static final long GB_IN_BYTES = MB_IN_BYTES * 1024; + /** @hide */ + public static final long TB_IN_BYTES = GB_IN_BYTES * 1024; /** * Special UID value used when collecting {@link NetworkStatsHistory} for diff --git a/core/java/android/os/BatteryStats.java b/core/java/android/os/BatteryStats.java index 1a06e0a..8b3ecae 100644 --- a/core/java/android/os/BatteryStats.java +++ b/core/java/android/os/BatteryStats.java @@ -3233,9 +3233,9 @@ public abstract class BatteryStats implements Parcelable { if (!didOne) sb.append(" (no activity)"); pw.println(sb.toString()); - final long wifiIdleTimeMs = getBluetoothControllerActivity(CONTROLLER_IDLE_TIME, which); - final long wifiRxTimeMs = getBluetoothControllerActivity(CONTROLLER_RX_TIME, which); - final long wifiTxTimeMs = getBluetoothControllerActivity(CONTROLLER_TX_TIME, which); + final long wifiIdleTimeMs = getWifiControllerActivity(CONTROLLER_IDLE_TIME, which); + final long wifiRxTimeMs = getWifiControllerActivity(CONTROLLER_RX_TIME, which); + final long wifiTxTimeMs = getWifiControllerActivity(CONTROLLER_TX_TIME, which); final long wifiTotalTimeMs = wifiIdleTimeMs + wifiRxTimeMs + wifiTxTimeMs; sb.setLength(0); diff --git a/core/java/android/os/Environment.java b/core/java/android/os/Environment.java index 975bfc2..2db976e 100644 --- a/core/java/android/os/Environment.java +++ b/core/java/android/os/Environment.java @@ -18,16 +18,11 @@ package android.os; import android.app.admin.DevicePolicyManager; import android.content.Context; -import android.os.storage.IMountService; +import android.os.storage.StorageManager; import android.os.storage.StorageVolume; -import android.text.TextUtils; import android.util.Log; -import com.google.android.collect.Lists; - import java.io.File; -import java.io.IOException; -import java.util.ArrayList; /** * Provides access to environment variables. @@ -36,11 +31,9 @@ public class Environment { private static final String TAG = "Environment"; private static final String ENV_EXTERNAL_STORAGE = "EXTERNAL_STORAGE"; - private static final String ENV_EMULATED_STORAGE_SOURCE = "EMULATED_STORAGE_SOURCE"; - private static final String ENV_EMULATED_STORAGE_TARGET = "EMULATED_STORAGE_TARGET"; - private static final String ENV_MEDIA_STORAGE = "MEDIA_STORAGE"; - private static final String ENV_SECONDARY_STORAGE = "SECONDARY_STORAGE"; private static final String ENV_ANDROID_ROOT = "ANDROID_ROOT"; + private static final String ENV_ANDROID_DATA = "ANDROID_DATA"; + private static final String ENV_ANDROID_STORAGE = "ANDROID_STORAGE"; private static final String ENV_OEM_ROOT = "OEM_ROOT"; private static final String ENV_VENDOR_ROOT = "VENDOR_ROOT"; @@ -57,12 +50,10 @@ public class Environment { public static final String DIRECTORY_ANDROID = DIR_ANDROID; private static final File DIR_ANDROID_ROOT = getDirectory(ENV_ANDROID_ROOT, "/system"); + private static final File DIR_ANDROID_DATA = getDirectory(ENV_ANDROID_DATA, "/data"); + private static final File DIR_ANDROID_STORAGE = getDirectory(ENV_ANDROID_STORAGE, "/storage"); private static final File DIR_OEM_ROOT = getDirectory(ENV_OEM_ROOT, "/oem"); private static final File DIR_VENDOR_ROOT = getDirectory(ENV_VENDOR_ROOT, "/vendor"); - private static final File DIR_MEDIA_STORAGE = getDirectory(ENV_MEDIA_STORAGE, "/data/media"); - - private static final String CANONCIAL_EMULATED_STORAGE_TARGET = getCanonicalPathOrNull( - ENV_EMULATED_STORAGE_TARGET); private static final String SYSTEM_PROPERTY_EFS_ENABLED = "persist.security.efs.enabled"; @@ -81,73 +72,24 @@ public class Environment { /** {@hide} */ public static class UserEnvironment { - // TODO: generalize further to create package-specific environment - - /** External storage dirs, as visible to vold */ - private final File[] mExternalDirsForVold; - /** External storage dirs, as visible to apps */ - private final File[] mExternalDirsForApp; - /** Primary emulated storage dir for direct access */ - private final File mEmulatedDirForDirect; + private final int mUserId; public UserEnvironment(int userId) { - // See storage config details at http://source.android.com/tech/storage/ - String rawExternalStorage = System.getenv(ENV_EXTERNAL_STORAGE); - String rawEmulatedSource = System.getenv(ENV_EMULATED_STORAGE_SOURCE); - String rawEmulatedTarget = System.getenv(ENV_EMULATED_STORAGE_TARGET); - - String rawMediaStorage = System.getenv(ENV_MEDIA_STORAGE); - if (TextUtils.isEmpty(rawMediaStorage)) { - rawMediaStorage = "/data/media"; - } - - ArrayList<File> externalForVold = Lists.newArrayList(); - ArrayList<File> externalForApp = Lists.newArrayList(); - - if (!TextUtils.isEmpty(rawEmulatedTarget)) { - // Device has emulated storage; external storage paths should have - // userId burned into them. - final String rawUserId = Integer.toString(userId); - final File emulatedSourceBase = new File(rawEmulatedSource); - final File emulatedTargetBase = new File(rawEmulatedTarget); - final File mediaBase = new File(rawMediaStorage); - - // /storage/emulated/0 - externalForVold.add(buildPath(emulatedSourceBase, rawUserId)); - externalForApp.add(buildPath(emulatedTargetBase, rawUserId)); - // /data/media/0 - mEmulatedDirForDirect = buildPath(mediaBase, rawUserId); - - } else { - // Device has physical external storage; use plain paths. - if (TextUtils.isEmpty(rawExternalStorage)) { - Log.w(TAG, "EXTERNAL_STORAGE undefined; falling back to default"); - rawExternalStorage = "/storage/sdcard0"; - } - - // /storage/sdcard0 - externalForVold.add(new File(rawExternalStorage)); - externalForApp.add(new File(rawExternalStorage)); - // /data/media - mEmulatedDirForDirect = new File(rawMediaStorage); - } + mUserId = userId; + } - // Splice in any secondary storage paths, but only for owner - final String rawSecondaryStorage = System.getenv(ENV_SECONDARY_STORAGE); - if (!TextUtils.isEmpty(rawSecondaryStorage) && userId == UserHandle.USER_OWNER) { - for (String secondaryPath : rawSecondaryStorage.split(":")) { - externalForVold.add(new File(secondaryPath)); - externalForApp.add(new File(secondaryPath)); - } + public File[] getExternalDirs() { + final StorageVolume[] volumes = StorageManager.getVolumeList(mUserId); + final File[] dirs = new File[volumes.length]; + for (int i = 0; i < volumes.length; i++) { + dirs[i] = volumes[i].getPathFile(); } - - mExternalDirsForVold = externalForVold.toArray(new File[externalForVold.size()]); - mExternalDirsForApp = externalForApp.toArray(new File[externalForApp.size()]); + return dirs; } @Deprecated public File getExternalStorageDirectory() { - return mExternalDirsForApp[0]; + return getExternalDirs()[0]; } @Deprecated @@ -155,60 +97,36 @@ public class Environment { return buildExternalStoragePublicDirs(type)[0]; } - public File[] getExternalDirsForVold() { - return mExternalDirsForVold; - } - - public File[] getExternalDirsForApp() { - return mExternalDirsForApp; - } - - public File getMediaDir() { - return mEmulatedDirForDirect; - } - public File[] buildExternalStoragePublicDirs(String type) { - return buildPaths(mExternalDirsForApp, type); + return buildPaths(getExternalDirs(), type); } public File[] buildExternalStorageAndroidDataDirs() { - return buildPaths(mExternalDirsForApp, DIR_ANDROID, DIR_DATA); + return buildPaths(getExternalDirs(), DIR_ANDROID, DIR_DATA); } public File[] buildExternalStorageAndroidObbDirs() { - return buildPaths(mExternalDirsForApp, DIR_ANDROID, DIR_OBB); + return buildPaths(getExternalDirs(), DIR_ANDROID, DIR_OBB); } public File[] buildExternalStorageAppDataDirs(String packageName) { - return buildPaths(mExternalDirsForApp, DIR_ANDROID, DIR_DATA, packageName); - } - - public File[] buildExternalStorageAppDataDirsForVold(String packageName) { - return buildPaths(mExternalDirsForVold, DIR_ANDROID, DIR_DATA, packageName); + return buildPaths(getExternalDirs(), DIR_ANDROID, DIR_DATA, packageName); } public File[] buildExternalStorageAppMediaDirs(String packageName) { - return buildPaths(mExternalDirsForApp, DIR_ANDROID, DIR_MEDIA, packageName); - } - - public File[] buildExternalStorageAppMediaDirsForVold(String packageName) { - return buildPaths(mExternalDirsForVold, DIR_ANDROID, DIR_MEDIA, packageName); + return buildPaths(getExternalDirs(), DIR_ANDROID, DIR_MEDIA, packageName); } public File[] buildExternalStorageAppObbDirs(String packageName) { - return buildPaths(mExternalDirsForApp, DIR_ANDROID, DIR_OBB, packageName); - } - - public File[] buildExternalStorageAppObbDirsForVold(String packageName) { - return buildPaths(mExternalDirsForVold, DIR_ANDROID, DIR_OBB, packageName); + return buildPaths(getExternalDirs(), DIR_ANDROID, DIR_OBB, packageName); } public File[] buildExternalStorageAppFilesDirs(String packageName) { - return buildPaths(mExternalDirsForApp, DIR_ANDROID, DIR_DATA, packageName, DIR_FILES); + return buildPaths(getExternalDirs(), DIR_ANDROID, DIR_DATA, packageName, DIR_FILES); } public File[] buildExternalStorageAppCacheDirs(String packageName) { - return buildPaths(mExternalDirsForApp, DIR_ANDROID, DIR_DATA, packageName, DIR_CACHE); + return buildPaths(getExternalDirs(), DIR_ANDROID, DIR_DATA, packageName, DIR_CACHE); } } @@ -220,6 +138,11 @@ public class Environment { return DIR_ANDROID_ROOT; } + /** {@hide} */ + public static File getStorageDirectory() { + return DIR_ANDROID_STORAGE; + } + /** * Return root directory of the "oem" partition holding OEM customizations, * if any. If present, the partition is mounted read-only. @@ -270,17 +193,6 @@ public class Environment { } /** - * Return directory used for internal media storage, which is protected by - * {@link android.Manifest.permission#WRITE_MEDIA_STORAGE}. - * - * @hide - */ - public static File getMediaStorageDirectory() { - throwIfUserRequired(); - return sCurrentUser.getMediaDir(); - } - - /** * Return the system directory for a user. This is for use by system services to store * files relating to the user. This directory will be automatically deleted when the user * is removed. @@ -389,7 +301,7 @@ public class Environment { */ public static File getExternalStorageDirectory() { throwIfUserRequired(); - return sCurrentUser.getExternalDirsForApp()[0]; + return sCurrentUser.getExternalDirs()[0]; } /** {@hide} */ @@ -402,18 +314,6 @@ public class Environment { return buildPath(getLegacyExternalStorageDirectory(), DIR_ANDROID, DIR_OBB); } - /** {@hide} */ - public static File getEmulatedStorageSource(int userId) { - // /mnt/shell/emulated/0 - return new File(System.getenv(ENV_EMULATED_STORAGE_SOURCE), String.valueOf(userId)); - } - - /** {@hide} */ - public static File getEmulatedStorageObbSource() { - // /mnt/shell/emulated/obb - return new File(System.getenv(ENV_EMULATED_STORAGE_SOURCE), DIR_OBB); - } - /** * Standard directory in which to place any audio files that should be * in the regular list of music for the user. @@ -683,6 +583,13 @@ public class Environment { public static final String MEDIA_UNMOUNTABLE = "unmountable"; /** + * Storage state if the media is in the process of being ejected. + * + * @see #getExternalStorageState(File) + */ + public static final String MEDIA_EJECTING = "ejecting"; + + /** * Returns the current state of the primary "external" storage device. * * @see #getExternalStorageDirectory() @@ -693,7 +600,7 @@ public class Environment { * {@link #MEDIA_BAD_REMOVAL}, or {@link #MEDIA_UNMOUNTABLE}. */ public static String getExternalStorageState() { - final File externalDir = sCurrentUser.getExternalDirsForApp()[0]; + final File externalDir = sCurrentUser.getExternalDirs()[0]; return getExternalStorageState(externalDir); } @@ -716,17 +623,12 @@ public class Environment { * {@link #MEDIA_BAD_REMOVAL}, or {@link #MEDIA_UNMOUNTABLE}. */ public static String getExternalStorageState(File path) { - final StorageVolume volume = getStorageVolume(path); + final StorageVolume volume = StorageManager.getStorageVolume(path, UserHandle.myUserId()); if (volume != null) { - final IMountService mountService = IMountService.Stub.asInterface( - ServiceManager.getService("mount")); - try { - return mountService.getVolumeState(volume.getPath()); - } catch (RemoteException e) { - } + return volume.getState(); + } else { + return MEDIA_UNKNOWN; } - - return Environment.MEDIA_UNKNOWN; } /** @@ -738,7 +640,7 @@ public class Environment { */ public static boolean isExternalStorageRemovable() { if (isStorageDisabled()) return false; - final File externalDir = sCurrentUser.getExternalDirsForApp()[0]; + final File externalDir = sCurrentUser.getExternalDirs()[0]; return isExternalStorageRemovable(externalDir); } @@ -753,7 +655,7 @@ public class Environment { * device. */ public static boolean isExternalStorageRemovable(File path) { - final StorageVolume volume = getStorageVolume(path); + final StorageVolume volume = StorageManager.getStorageVolume(path, UserHandle.myUserId()); if (volume != null) { return volume.isRemovable(); } else { @@ -771,7 +673,7 @@ public class Environment { */ public static boolean isExternalStorageEmulated() { if (isStorageDisabled()) return false; - final File externalDir = sCurrentUser.getExternalDirsForApp()[0]; + final File externalDir = sCurrentUser.getExternalDirs()[0]; return isExternalStorageEmulated(externalDir); } @@ -784,7 +686,7 @@ public class Environment { * device. */ public static boolean isExternalStorageEmulated(File path) { - final StorageVolume volume = getStorageVolume(path); + final StorageVolume volume = StorageManager.getStorageVolume(path, UserHandle.myUserId()); if (volume != null) { return volume.isEmulated(); } else { @@ -797,19 +699,6 @@ public class Environment { return path == null ? new File(defaultPath) : new File(path); } - private static String getCanonicalPathOrNull(String variableName) { - String path = System.getenv(variableName); - if (path == null) { - return null; - } - try { - return new File(path).getCanonicalPath(); - } catch (IOException e) { - Log.w(TAG, "Unable to resolve canonical path for " + path); - return null; - } - } - /** {@hide} */ public static void setUserRequired(boolean userRequired) { sUserRequired = userRequired; @@ -856,28 +745,6 @@ public class Environment { return SystemProperties.getBoolean("config.disable_storage", false); } - private static StorageVolume getStorageVolume(File path) { - try { - path = path.getCanonicalFile(); - } catch (IOException e) { - return null; - } - - try { - final IMountService mountService = IMountService.Stub.asInterface( - ServiceManager.getService("mount")); - final StorageVolume[] volumes = mountService.getVolumeList(); - for (StorageVolume volume : volumes) { - if (FileUtils.contains(volume.getPathFile(), path)) { - return volume; - } - } - } catch (RemoteException e) { - } - - return null; - } - /** * If the given path exists on emulated external storage, return the * translated backing path hosted on internal storage. This bypasses any @@ -891,26 +758,7 @@ public class Environment { * @hide */ public static File maybeTranslateEmulatedPathToInternal(File path) { - // Fast return if not emulated, or missing variables - if (!Environment.isExternalStorageEmulated() - || CANONCIAL_EMULATED_STORAGE_TARGET == null) { - return path; - } - - try { - final String rawPath = path.getCanonicalPath(); - if (rawPath.startsWith(CANONCIAL_EMULATED_STORAGE_TARGET)) { - final File internalPath = new File(DIR_MEDIA_STORAGE, - rawPath.substring(CANONCIAL_EMULATED_STORAGE_TARGET.length())); - if (internalPath.exists()) { - return internalPath; - } - } - } catch (IOException e) { - Log.w(TAG, "Failed to resolve canonical path for " + path); - } - - // Unable to translate to internal path; use original + // TODO: bring back this optimization return path; } } diff --git a/core/java/android/os/FileUtils.java b/core/java/android/os/FileUtils.java index 0a724a1..b302f95 100644 --- a/core/java/android/os/FileUtils.java +++ b/core/java/android/os/FileUtils.java @@ -369,6 +369,23 @@ public class FileUtils { * {@link File#getCanonicalFile()} to avoid symlink or path traversal * attacks. */ + public static boolean contains(File[] dirs, File file) { + for (File dir : dirs) { + if (contains(dir, file)) { + return true; + } + } + return false; + } + + /** + * Test if a file lives under the given directory, either as a direct child + * or a distant grandchild. + * <p> + * Both files <em>must</em> have been resolved using + * {@link File#getCanonicalFile()} to avoid symlink or path traversal + * attacks. + */ public static boolean contains(File dir, File file) { if (file == null) return false; diff --git a/core/java/android/os/Process.java b/core/java/android/os/Process.java index 0de9c70..355ec8c 100644 --- a/core/java/android/os/Process.java +++ b/core/java/android/os/Process.java @@ -637,10 +637,8 @@ public class Process { if ((debugFlags & Zygote.DEBUG_ENABLE_ASSERT) != 0) { argsForZygote.add("--enable-assert"); } - if (mountExternal == Zygote.MOUNT_EXTERNAL_MULTIUSER) { - argsForZygote.add("--mount-external-multiuser"); - } else if (mountExternal == Zygote.MOUNT_EXTERNAL_MULTIUSER_ALL) { - argsForZygote.add("--mount-external-multiuser-all"); + if (mountExternal == Zygote.MOUNT_EXTERNAL_DEFAULT) { + argsForZygote.add("--mount-external-default"); } argsForZygote.add("--target-sdk-version=" + targetSdkVersion); diff --git a/core/java/android/os/storage/IMountService.java b/core/java/android/os/storage/IMountService.java index 6209c2a..fef12d1 100644 --- a/core/java/android/os/storage/IMountService.java +++ b/core/java/android/os/storage/IMountService.java @@ -757,12 +757,13 @@ public interface IMountService extends IInterface { return _result; } - public StorageVolume[] getVolumeList() throws RemoteException { + public StorageVolume[] getVolumeList(int userId) throws RemoteException { Parcel _data = Parcel.obtain(); Parcel _reply = Parcel.obtain(); StorageVolume[] _result; try { _data.writeInterfaceToken(DESCRIPTOR); + _data.writeInt(userId); mRemote.transact(Stub.TRANSACTION_getVolumeList, _data, _reply, 0); _reply.readException(); _result = _reply.createTypedArray(StorageVolume.CREATOR); @@ -1308,7 +1309,8 @@ public interface IMountService extends IInterface { } case TRANSACTION_getVolumeList: { data.enforceInterface(DESCRIPTOR); - StorageVolume[] result = getVolumeList(); + int userId = data.readInt(); + StorageVolume[] result = getVolumeList(userId); reply.writeNoException(); reply.writeTypedArray(result, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE); return true; @@ -1630,7 +1632,7 @@ public interface IMountService extends IInterface { /** * Returns list of all mountable volumes. */ - public StorageVolume[] getVolumeList() throws RemoteException; + public StorageVolume[] getVolumeList(int userId) throws RemoteException; /** * Gets the path on the filesystem for the ASEC container itself. diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java index 2785ee8..532bf2c 100644 --- a/core/java/android/os/storage/StorageManager.java +++ b/core/java/android/os/storage/StorageManager.java @@ -18,19 +18,23 @@ package android.os.storage; import static android.net.TrafficStats.MB_IN_BYTES; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.ContentResolver; import android.content.Context; import android.os.Environment; +import android.os.FileUtils; import android.os.Handler; import android.os.Looper; import android.os.Message; -import android.os.Parcelable; import android.os.RemoteException; import android.os.ServiceManager; import android.provider.Settings; import android.util.Log; import android.util.SparseArray; +import libcore.util.EmptyArray; + import com.android.internal.util.Preconditions; import java.io.File; @@ -60,6 +64,7 @@ import java.util.concurrent.atomic.AtomicInteger; public class StorageManager { private static final String TAG = "StorageManager"; + private final Context mContext; private final ContentResolver mResolver; /* @@ -311,8 +316,9 @@ public class StorageManager { * * @hide */ - public StorageManager(ContentResolver resolver, Looper tgtLooper) throws RemoteException { - mResolver = resolver; + public StorageManager(Context context, Looper tgtLooper) { + mContext = context; + mResolver = context.getContentResolver(); mTgtLooper = tgtLooper; mMountService = IMountService.Stub.asInterface(ServiceManager.getService("mount")); if (mMountService == null) { @@ -548,17 +554,46 @@ public class StorageManager { return null; } + /** {@hide} */ + public @Nullable StorageVolume getStorageVolume(File file) { + return getStorageVolume(getVolumeList(), file); + } + + /** {@hide} */ + public static @Nullable StorageVolume getStorageVolume(File file, int userId) { + return getStorageVolume(getVolumeList(userId), file); + } + + /** {@hide} */ + private static @Nullable StorageVolume getStorageVolume(StorageVolume[] volumes, File file) { + File canonicalFile = null; + try { + canonicalFile = file.getCanonicalFile(); + } catch (IOException ignored) { + canonicalFile = null; + } + for (StorageVolume volume : volumes) { + if (volume.getPathFile().equals(file)) { + return volume; + } + if (FileUtils.contains(volume.getPathFile(), canonicalFile)) { + return volume; + } + } + return null; + } + /** * Gets the state of a volume via its mountpoint. * @hide */ - public String getVolumeState(String mountPoint) { - if (mMountService == null) return Environment.MEDIA_REMOVED; - try { - return mMountService.getVolumeState(mountPoint); - } catch (RemoteException e) { - Log.e(TAG, "Failed to get volume state", e); - return null; + @Deprecated + public @NonNull String getVolumeState(String mountPoint) { + final StorageVolume vol = getStorageVolume(new File(mountPoint)); + if (vol != null) { + return vol.getState(); + } else { + return Environment.MEDIA_UNKNOWN; } } @@ -566,20 +601,22 @@ public class StorageManager { * Returns list of all mountable volumes. * @hide */ - public StorageVolume[] getVolumeList() { - if (mMountService == null) return new StorageVolume[0]; + public @NonNull StorageVolume[] getVolumeList() { try { - Parcelable[] list = mMountService.getVolumeList(); - if (list == null) return new StorageVolume[0]; - int length = list.length; - StorageVolume[] result = new StorageVolume[length]; - for (int i = 0; i < length; i++) { - result[i] = (StorageVolume)list[i]; - } - return result; + return mMountService.getVolumeList(mContext.getUserId()); } catch (RemoteException e) { - Log.e(TAG, "Failed to get volume list", e); - return null; + throw e.rethrowAsRuntimeException(); + } + } + + /** {@hide} */ + public static @NonNull StorageVolume[] getVolumeList(int userId) { + final IMountService mountService = IMountService.Stub.asInterface( + ServiceManager.getService("mount")); + try { + return mountService.getVolumeList(userId); + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); } } @@ -587,9 +624,9 @@ public class StorageManager { * Returns list of paths for all mountable volumes. * @hide */ - public String[] getVolumePaths() { + @Deprecated + public @NonNull String[] getVolumePaths() { StorageVolume[] volumes = getVolumeList(); - if (volumes == null) return null; int count = volumes.length; String[] paths = new String[count]; for (int i = 0; i < count; i++) { @@ -599,21 +636,21 @@ public class StorageManager { } /** {@hide} */ - public StorageVolume getPrimaryVolume() { + public @NonNull StorageVolume getPrimaryVolume() { return getPrimaryVolume(getVolumeList()); } /** {@hide} */ - public static StorageVolume getPrimaryVolume(StorageVolume[] volumes) { + public static @NonNull StorageVolume getPrimaryVolume(StorageVolume[] volumes) { for (StorageVolume volume : volumes) { if (volume.isPrimary()) { return volume; } } - Log.w(TAG, "No primary storage defined"); - return null; + throw new IllegalStateException("Missing primary storage"); } + /** {@hide} */ private static final int DEFAULT_THRESHOLD_PERCENTAGE = 10; private static final long DEFAULT_THRESHOLD_MAX_BYTES = 500 * MB_IN_BYTES; private static final long DEFAULT_FULL_THRESHOLD_BYTES = MB_IN_BYTES; diff --git a/core/java/android/os/storage/StorageVolume.java b/core/java/android/os/storage/StorageVolume.java index 06565f1..0c391ca 100644 --- a/core/java/android/os/storage/StorageVolume.java +++ b/core/java/android/os/storage/StorageVolume.java @@ -17,6 +17,7 @@ package android.os.storage; import android.content.Context; +import android.net.TrafficStats; import android.os.Parcel; import android.os.Parcelable; import android.os.UserHandle; @@ -34,52 +35,58 @@ import java.io.File; */ public class StorageVolume implements Parcelable { - // TODO: switch to more durable token - private int mStorageId; + private final String mId; + private final int mStorageId; private final File mPath; private final int mDescriptionId; private final boolean mPrimary; private final boolean mRemovable; private final boolean mEmulated; - private final int mMtpReserveSpace; + private final long mMtpReserveSize; private final boolean mAllowMassStorage; /** Maximum file size for the storage, or zero for no limit */ private final long mMaxFileSize; /** When set, indicates exclusive ownership of this volume */ private final UserHandle mOwner; - private String mUuid; - private String mUserLabel; - private String mState; + private final String mUuid; + private final String mUserLabel; + private final String mState; // StorageVolume extra for ACTION_MEDIA_REMOVED, ACTION_MEDIA_UNMOUNTED, ACTION_MEDIA_CHECKING, // ACTION_MEDIA_NOFS, ACTION_MEDIA_MOUNTED, ACTION_MEDIA_SHARED, ACTION_MEDIA_UNSHARED, // ACTION_MEDIA_BAD_REMOVAL, ACTION_MEDIA_UNMOUNTABLE and ACTION_MEDIA_EJECT broadcasts. public static final String EXTRA_STORAGE_VOLUME = "storage_volume"; - public StorageVolume(File path, int descriptionId, boolean primary, boolean removable, - boolean emulated, int mtpReserveSpace, boolean allowMassStorage, long maxFileSize, - UserHandle owner) { + public StorageVolume(String id, int storageId, File path, int descriptionId, boolean primary, + boolean removable, boolean emulated, long mtpReserveSize, boolean allowMassStorage, + long maxFileSize, UserHandle owner, String uuid, String userLabel, String state) { + mId = id; + mStorageId = storageId; mPath = path; mDescriptionId = descriptionId; mPrimary = primary; mRemovable = removable; mEmulated = emulated; - mMtpReserveSpace = mtpReserveSpace; + mMtpReserveSize = mtpReserveSize; mAllowMassStorage = allowMassStorage; mMaxFileSize = maxFileSize; mOwner = owner; + mUuid = uuid; + mUserLabel = userLabel; + mState = state; } private StorageVolume(Parcel in) { + mId = in.readString(); mStorageId = in.readInt(); mPath = new File(in.readString()); mDescriptionId = in.readInt(); mPrimary = in.readInt() != 0; mRemovable = in.readInt() != 0; mEmulated = in.readInt() != 0; - mMtpReserveSpace = in.readInt(); + mMtpReserveSize = in.readLong(); mAllowMassStorage = in.readInt() != 0; mMaxFileSize = in.readLong(); mOwner = in.readParcelable(null); @@ -88,10 +95,8 @@ public class StorageVolume implements Parcelable { mState = in.readString(); } - public static StorageVolume fromTemplate(StorageVolume template, File path, UserHandle owner) { - return new StorageVolume(path, template.mDescriptionId, template.mPrimary, - template.mRemovable, template.mEmulated, template.mMtpReserveSpace, - template.mAllowMassStorage, template.mMaxFileSize, owner); + public String getId() { + return mId; } /** @@ -153,15 +158,6 @@ public class StorageVolume implements Parcelable { } /** - * Do not call this unless you are MountService - */ - public void setStorageId(int index) { - // storage ID is 0x00010001 for primary storage, - // then 0x00020001, 0x00030001, etc. for secondary storages - mStorageId = ((index + 1) << 16) + 1; - } - - /** * Number of megabytes of space to leave unallocated by MTP. * MTP will subtract this value from the free space it reports back * to the host via GetStorageInfo, and will not allow new files to @@ -174,7 +170,7 @@ public class StorageVolume implements Parcelable { * @return MTP reserve space */ public int getMtpReserveSpace() { - return mMtpReserveSpace; + return (int) (mMtpReserveSize / TrafficStats.MB_IN_BYTES); } /** @@ -199,10 +195,6 @@ public class StorageVolume implements Parcelable { return mOwner; } - public void setUuid(String uuid) { - mUuid = uuid; - } - public String getUuid() { return mUuid; } @@ -222,18 +214,10 @@ public class StorageVolume implements Parcelable { } } - public void setUserLabel(String userLabel) { - mUserLabel = userLabel; - } - public String getUserLabel() { return mUserLabel; } - public void setState(String state) { - mState = state; - } - public String getState() { return mState; } @@ -262,13 +246,14 @@ public class StorageVolume implements Parcelable { public void dump(IndentingPrintWriter pw) { pw.println("StorageVolume:"); pw.increaseIndent(); + pw.printPair("mId", mId); pw.printPair("mStorageId", mStorageId); pw.printPair("mPath", mPath); pw.printPair("mDescriptionId", mDescriptionId); pw.printPair("mPrimary", mPrimary); pw.printPair("mRemovable", mRemovable); pw.printPair("mEmulated", mEmulated); - pw.printPair("mMtpReserveSpace", mMtpReserveSpace); + pw.printPair("mMtpReserveSize", mMtpReserveSize); pw.printPair("mAllowMassStorage", mAllowMassStorage); pw.printPair("mMaxFileSize", mMaxFileSize); pw.printPair("mOwner", mOwner); @@ -297,13 +282,14 @@ public class StorageVolume implements Parcelable { @Override public void writeToParcel(Parcel parcel, int flags) { + parcel.writeString(mId); parcel.writeInt(mStorageId); parcel.writeString(mPath.toString()); parcel.writeInt(mDescriptionId); parcel.writeInt(mPrimary ? 1 : 0); parcel.writeInt(mRemovable ? 1 : 0); parcel.writeInt(mEmulated ? 1 : 0); - parcel.writeInt(mMtpReserveSpace); + parcel.writeLong(mMtpReserveSize); parcel.writeInt(mAllowMassStorage ? 1 : 0); parcel.writeLong(mMaxFileSize); parcel.writeParcelable(mOwner, flags); diff --git a/core/java/android/preference/Preference.java b/core/java/android/preference/Preference.java index f32e8cf..3b482eb 100644 --- a/core/java/android/preference/Preference.java +++ b/core/java/android/preference/Preference.java @@ -1438,7 +1438,7 @@ public class Preference implements Comparable<Preference> { protected boolean persistString(String value) { if (shouldPersist()) { // Shouldn't store null - if (value == getPersistedString(null)) { + if (TextUtils.equals(value, getPersistedString(null))) { // It's already there, so the same as persisting return true; } diff --git a/core/java/android/provider/ContactsContract.java b/core/java/android/provider/ContactsContract.java index 74b0a1c..bf7f3cb 100644 --- a/core/java/android/provider/ContactsContract.java +++ b/core/java/android/provider/ContactsContract.java @@ -18,6 +18,7 @@ package android.provider; import android.accounts.Account; import android.app.Activity; +import android.app.admin.DevicePolicyManager; import android.content.ActivityNotFoundException; import android.content.ContentProviderClient; import android.content.ContentProviderOperation; @@ -1628,7 +1629,6 @@ public final class ContactsContract { */ public static final String CONTENT_VCARD_TYPE = "text/x-vcard"; - /** * Mimimal ID for corp contacts returned from * {@link PhoneLookup#ENTERPRISE_CONTENT_FILTER_URI}. @@ -1638,6 +1638,14 @@ public final class ContactsContract { public static long ENTERPRISE_CONTACT_ID_BASE = 1000000000; // slightly smaller than 2 ** 30 /** + * Prefix for corp contacts returned from + * {@link PhoneLookup#ENTERPRISE_CONTENT_FILTER_URI}. + * + * @hide + */ + public static String ENTERPRISE_CONTACT_LOOKUP_PREFIX = "c-"; + + /** * Return TRUE if a contact ID is from the contacts provider on the enterprise profile. * * {@link PhoneLookup#ENTERPRISE_CONTENT_FILTER_URI} may return such a contact. @@ -4809,6 +4817,14 @@ public final class ContactsContract { Uri.withAppendedPath(AUTHORITY_URI, "raw_contact_entities"); /** + * The content:// style URI for this table in corp profile + * + * @hide + */ + public static final Uri CORP_CONTENT_URI = + Uri.withAppendedPath(AUTHORITY_URI, "raw_contact_entities_corp"); + + /** * The content:// style URI for this table, specific to the user's profile. */ public static final Uri PROFILE_CONTENT_URI = @@ -5024,9 +5040,17 @@ public final class ContactsContract { * is from the corp profile, use * {@link ContactsContract.Contacts#isEnterpriseContactId(long)}. * </li> + * <li> + * Corp contacts will get artificial {@link #LOOKUP_KEY}s too. + * </li> * </ul> * <p> - * This URI does NOT support selection nor order-by. + * A contact lookup URL built by + * {@link ContactsContract.Contacts#getLookupUri(long, String)} + * with an {@link #_ID} and a {@link #LOOKUP_KEY} returned by this API can be passed to + * {@link ContactsContract.QuickContact#showQuickContact} even if a contact is from the + * corp profile. + * </p> * * <pre> * Uri lookupUri = Uri.withAppendedPath(PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI, @@ -6017,10 +6041,18 @@ public final class ContactsContract { * a contact * is from the corp profile, use * {@link ContactsContract.Contacts#isEnterpriseContactId(long)}. - * </li> - * </ul> - * <p> - * This URI does NOT support selection nor order-by. + * </li> + * <li> + * Corp contacts will get artificial {@link #LOOKUP_KEY}s too. + * </li> + * </ul> + * <p> + * A contact lookup URL built by + * {@link ContactsContract.Contacts#getLookupUri(long, String)} + * with an {@link #_ID} and a {@link #LOOKUP_KEY} returned by this API can be passed to + * {@link ContactsContract.QuickContact#showQuickContact} even if a contact is from the + * corp profile. + * </p> * * <pre> * Uri lookupUri = Uri.withAppendedPath(Email.ENTERPRISE_CONTENT_LOOKUP_URI, @@ -8174,6 +8206,9 @@ public final class ContactsContract { */ public static final int MODE_LARGE = 3; + /** @hide */ + public static final int MODE_DEFAULT = MODE_LARGE; + /** * Constructs the QuickContacts intent with a view's rect. * @hide @@ -8216,6 +8251,7 @@ public final class ContactsContract { // Launch pivot dialog through intent for now final Intent intent = new Intent(ACTION_QUICK_CONTACT).addFlags(intentFlags); + // NOTE: This logic and rebuildManagedQuickContactsIntent() must be in sync. intent.setData(lookupUri); intent.setSourceBounds(target); intent.putExtra(EXTRA_MODE, mode); @@ -8224,6 +8260,30 @@ public final class ContactsContract { } /** + * Constructs a QuickContacts intent based on an incoming intent for DevicePolicyManager + * to strip off anything not necessary. + * + * @hide + */ + public static Intent rebuildManagedQuickContactsIntent(String lookupKey, long contactId, + Intent originalIntent) { + final Intent intent = new Intent(ACTION_QUICK_CONTACT); + // Rebuild the URI from a lookup key and a contact ID. + intent.setData(Contacts.getLookupUri(contactId, lookupKey)); + + // Copy flags and always set NEW_TASK because it won't have a parent activity. + intent.setFlags(originalIntent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK); + + // Copy extras. + intent.setSourceBounds(originalIntent.getSourceBounds()); + intent.putExtra(EXTRA_MODE, originalIntent.getIntExtra(EXTRA_MODE, MODE_DEFAULT)); + intent.putExtra(EXTRA_EXCLUDE_MIMES, + originalIntent.getStringArrayExtra(EXTRA_EXCLUDE_MIMES)); + return intent; + } + + + /** * Trigger a dialog that lists the various methods of interacting with * the requested {@link Contacts} entry. This may be based on available * {@link ContactsContract.Data} rows under that contact, and may also @@ -8251,7 +8311,7 @@ public final class ContactsContract { // Trigger with obtained rectangle Intent intent = composeQuickContactsIntent(context, target, lookupUri, mode, excludeMimes); - startActivityWithErrorToast(context, intent); + ContactsInternal.startQuickContactWithErrorToast(context, intent); } /** @@ -8284,7 +8344,7 @@ public final class ContactsContract { String[] excludeMimes) { Intent intent = composeQuickContactsIntent(context, target, lookupUri, mode, excludeMimes); - startActivityWithErrorToast(context, intent); + ContactsInternal.startQuickContactWithErrorToast(context, intent); } /** @@ -8317,10 +8377,10 @@ public final class ContactsContract { // Use MODE_LARGE instead of accepting mode as a parameter. The different mode // values defined in ContactsContract only affect very old implementations // of QuickContacts. - Intent intent = composeQuickContactsIntent(context, target, lookupUri, MODE_LARGE, + Intent intent = composeQuickContactsIntent(context, target, lookupUri, MODE_DEFAULT, excludeMimes); intent.putExtra(EXTRA_PRIORITIZED_MIMETYPE, prioritizedMimeType); - startActivityWithErrorToast(context, intent); + ContactsInternal.startQuickContactWithErrorToast(context, intent); } /** @@ -8355,19 +8415,10 @@ public final class ContactsContract { // Use MODE_LARGE instead of accepting mode as a parameter. The different mode // values defined in ContactsContract only affect very old implementations // of QuickContacts. - Intent intent = composeQuickContactsIntent(context, target, lookupUri, MODE_LARGE, + Intent intent = composeQuickContactsIntent(context, target, lookupUri, MODE_DEFAULT, excludeMimes); intent.putExtra(EXTRA_PRIORITIZED_MIMETYPE, prioritizedMimeType); - startActivityWithErrorToast(context, intent); - } - - private static void startActivityWithErrorToast(Context context, Intent intent) { - try { - context.startActivity(intent); - } catch (ActivityNotFoundException e) { - Toast.makeText(context, com.android.internal.R.string.quick_contacts_not_available, - Toast.LENGTH_SHORT).show(); - } + ContactsInternal.startQuickContactWithErrorToast(context, intent); } } diff --git a/core/java/android/provider/ContactsInternal.java b/core/java/android/provider/ContactsInternal.java new file mode 100644 index 0000000..059a603 --- /dev/null +++ b/core/java/android/provider/ContactsInternal.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package android.provider; + +import android.app.admin.DevicePolicyManager; +import android.content.ActivityNotFoundException; +import android.content.ContentUris; +import android.content.Context; +import android.content.Intent; +import android.content.UriMatcher; +import android.net.Uri; +import android.os.Process; +import android.os.UserHandle; +import android.text.TextUtils; +import android.widget.Toast; + +import java.util.List; + +/** + * Contacts related internal methods. + * + * @hide + */ +public class ContactsInternal { + private ContactsInternal() { + } + + /** URI matcher used to parse contact URIs. */ + private static final UriMatcher sContactsUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); + + private static final int CONTACTS_URI_LOOKUP_ID = 1000; + + static { + // Contacts URI matching table + final UriMatcher matcher = sContactsUriMatcher; + matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#", CONTACTS_URI_LOOKUP_ID); + } + + /** + * Called by {@link ContactsContract} to star Quick Contact, possibly on the managed profile. + */ + public static void startQuickContactWithErrorToast(Context context, Intent intent) { + final Uri uri = intent.getData(); + + final int match = sContactsUriMatcher.match(uri); + switch (match) { + case CONTACTS_URI_LOOKUP_ID: { + if (maybeStartManagedQuickContact(context, intent)) { + return; // Request handled by DPM. Just return here. + } + break; + } + } + // Launch on the current profile. + startQuickContactWithErrorToastForUser(context, intent, Process.myUserHandle()); + } + + public static void startQuickContactWithErrorToastForUser(Context context, Intent intent, + UserHandle user) { + try { + context.startActivityAsUser(intent, user); + } catch (ActivityNotFoundException e) { + Toast.makeText(context, com.android.internal.R.string.quick_contacts_not_available, + Toast.LENGTH_SHORT).show(); + } + } + + /** + * If the URI in {@code intent} is of a corp contact, launch quick contact on the managed + * profile. + * + * @return the URI in {@code intent} is of a corp contact thus launched on the managed profile. + */ + private static boolean maybeStartManagedQuickContact(Context context, Intent originalIntent) { + final Uri uri = originalIntent.getData(); + + // Decompose into an ID and a lookup key. + final List<String> pathSegments = uri.getPathSegments(); + final long contactId = ContentUris.parseId(uri); + final String lookupKey = pathSegments.get(2); + + // See if it has a corp lookupkey. + if (TextUtils.isEmpty(lookupKey) + || !lookupKey.startsWith( + ContactsContract.Contacts.ENTERPRISE_CONTACT_LOOKUP_PREFIX)) { + return false; // It's not a corp lookup key. + } + + // Launch Quick Contact on the managed profile, if the policy allows. + final DevicePolicyManager dpm = context.getSystemService(DevicePolicyManager.class); + final String actualLookupKey = lookupKey.substring( + ContactsContract.Contacts.ENTERPRISE_CONTACT_LOOKUP_PREFIX.length()); + final long actualContactId = + (contactId - ContactsContract.Contacts.ENTERPRISE_CONTACT_ID_BASE); + + dpm.startManagedQuickContact(actualLookupKey, actualContactId, originalIntent); + return true; + } +} diff --git a/core/java/android/provider/SearchIndexableData.java b/core/java/android/provider/SearchIndexableData.java index 7b9d1ea..5e0a76d 100644 --- a/core/java/android/provider/SearchIndexableData.java +++ b/core/java/android/provider/SearchIndexableData.java @@ -16,6 +16,7 @@ package android.provider; +import android.annotation.SystemApi; import android.content.Context; import java.util.Locale; @@ -27,6 +28,7 @@ import java.util.Locale; * * @hide */ +@SystemApi public abstract class SearchIndexableData { /** diff --git a/core/java/android/provider/SearchIndexableResource.java b/core/java/android/provider/SearchIndexableResource.java index c807df2..1eb1734 100644 --- a/core/java/android/provider/SearchIndexableResource.java +++ b/core/java/android/provider/SearchIndexableResource.java @@ -16,6 +16,7 @@ package android.provider; +import android.annotation.SystemApi; import android.content.Context; /** @@ -31,6 +32,7 @@ import android.content.Context; * * @hide */ +@SystemApi public class SearchIndexableResource extends SearchIndexableData { /** diff --git a/core/java/android/provider/SearchIndexablesContract.java b/core/java/android/provider/SearchIndexablesContract.java index 1b5f72a..93ac7f6 100644 --- a/core/java/android/provider/SearchIndexablesContract.java +++ b/core/java/android/provider/SearchIndexablesContract.java @@ -16,6 +16,7 @@ package android.provider; +import android.annotation.SystemApi; import android.content.ContentResolver; /** @@ -23,6 +24,7 @@ import android.content.ContentResolver; * * @hide */ +@SystemApi public class SearchIndexablesContract { /** @@ -234,7 +236,7 @@ public class SearchIndexablesContract { /** * The base columns. */ - private static class BaseColumns { + public static class BaseColumns { private BaseColumns() { } diff --git a/core/java/android/provider/SearchIndexablesProvider.java b/core/java/android/provider/SearchIndexablesProvider.java index 9c8f6d0..3120e54 100644 --- a/core/java/android/provider/SearchIndexablesProvider.java +++ b/core/java/android/provider/SearchIndexablesProvider.java @@ -16,6 +16,7 @@ package android.provider; +import android.annotation.SystemApi; import android.content.ContentProvider; import android.content.ContentValues; import android.content.Context; @@ -61,6 +62,7 @@ import android.net.Uri; * * @hide */ +@SystemApi public abstract class SearchIndexablesProvider extends ContentProvider { private static final String TAG = "IndexablesProvider"; diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index de536bd..8e5d245 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -5255,6 +5255,15 @@ public final class Settings { public static final String SMS_DEFAULT_APPLICATION = "sms_default_application"; /** + * Specifies the package name currently configured to be the emergency assistance application + * + * @see android.telephony.TelephonyManager#ACTION_EMERGENCY_ASSISTANCE + * + * @hide + */ + public static final String EMERGENCY_ASSISTANCE_APPLICATION = "emergency_assistance_application"; + + /** * Names of the packages that the current user has explicitly allowed to * see all of the user's notifications, separated by ':'. * @@ -6088,7 +6097,7 @@ public final class Settings { public static final String PACKAGE_VERIFIER_SETTING_VISIBLE = "verifier_setting_visible"; /** - * Run package verificaiton on apps installed through ADB/ADT/USB + * Run package verification on apps installed through ADB/ADT/USB * 1 = perform package verification on ADB installs (default) * 0 = bypass package verification on ADB installs * @hide diff --git a/core/java/android/security/IKeystoreService.aidl b/core/java/android/security/IKeystoreService.aidl index d24bc13..579cdbe 100644 --- a/core/java/android/security/IKeystoreService.aidl +++ b/core/java/android/security/IKeystoreService.aidl @@ -60,8 +60,8 @@ interface IKeystoreService { // Keymaster 0.4 methods int addRngEntropy(in byte[] data); - int generateKey(String alias, in KeymasterArguments arguments, int uid, int flags, - out KeyCharacteristics characteristics); + int generateKey(String alias, in KeymasterArguments arguments, in byte[] entropy, int uid, + int flags, out KeyCharacteristics characteristics); int getKeyCharacteristics(String alias, in KeymasterBlob clientId, in KeymasterBlob appId, out KeyCharacteristics characteristics); int importKey(String alias, in KeymasterArguments arguments, int format, @@ -69,8 +69,10 @@ interface IKeystoreService { ExportResult exportKey(String alias, int format, in KeymasterBlob clientId, in KeymasterBlob appId); OperationResult begin(IBinder appToken, String alias, int purpose, boolean pruneable, - in KeymasterArguments params, out KeymasterArguments operationParams); + in KeymasterArguments params, in byte[] entropy, out KeymasterArguments operationParams); OperationResult update(IBinder token, in KeymasterArguments params, in byte[] input); OperationResult finish(IBinder token, in KeymasterArguments params, in byte[] signature); int abort(IBinder handle); + boolean isOperationAuthorized(IBinder token); + int addAuthToken(in byte[] authToken); } diff --git a/core/java/android/security/NetworkSecurityPolicy.java b/core/java/android/security/NetworkSecurityPolicy.java index 0626bbc..0b3bf45 100644 --- a/core/java/android/security/NetworkSecurityPolicy.java +++ b/core/java/android/security/NetworkSecurityPolicy.java @@ -24,8 +24,6 @@ package android.security; * * <p>The policy currently consists of a single flag: whether cleartext network traffic is * permitted. See {@link #isCleartextTrafficPermitted()}. - * - * @hide */ public class NetworkSecurityPolicy { @@ -48,9 +46,9 @@ public class NetworkSecurityPolicy { * without TLS or STARTTLS) is permitted for this process. * * <p>When cleartext network traffic is not permitted, the platform's components (e.g. HTTP and - * FTP stacks, {@code WebView}, {@code MediaPlayer}) will refuse this process's requests to use - * cleartext traffic. Third-party libraries are strongly encouraged to honor this setting as - * well. + * FTP stacks, {@link android.webkit.WebView}, {@link android.media.MediaPlayer}) will refuse + * this process's requests to use cleartext traffic. Third-party libraries are strongly + * encouraged to honor this setting as well. * * <p>This flag is honored on a best effort basis because it's impossible to prevent all * cleartext traffic from Android applications given the level of access provided to them. For diff --git a/core/java/android/security/keymaster/KeymasterDefs.java b/core/java/android/security/keymaster/KeymasterDefs.java index e653b74..c2ebbc6 100644 --- a/core/java/android/security/keymaster/KeymasterDefs.java +++ b/core/java/android/security/keymaster/KeymasterDefs.java @@ -16,6 +16,9 @@ package android.security.keymaster; +import java.util.HashMap; +import java.util.Map; + /** * Class tracking all the keymaster enum values needed for the binder API to keystore. * This must be kept in sync with hardware/libhardware/include/hardware/keymaster_defs.h @@ -224,7 +227,53 @@ public final class KeymasterDefs { public static final int KM_ERROR_VERSION_MISMATCH = -101; public static final int KM_ERROR_UNKNOWN_ERROR = -1000; + public static final Map<Integer, String> sErrorCodeToString = new HashMap<Integer, String>(); + static { + sErrorCodeToString.put(KM_ERROR_OK, "OK"); + sErrorCodeToString.put(KM_ERROR_UNSUPPORTED_PURPOSE, "Unsupported purpose"); + sErrorCodeToString.put(KM_ERROR_INCOMPATIBLE_PURPOSE, "Incompatible purpose"); + sErrorCodeToString.put(KM_ERROR_UNSUPPORTED_ALGORITHM, "Unsupported algorithm"); + sErrorCodeToString.put(KM_ERROR_INCOMPATIBLE_ALGORITHM, "Incompatible algorithm"); + sErrorCodeToString.put(KM_ERROR_UNSUPPORTED_KEY_SIZE, "Unsupported key size"); + sErrorCodeToString.put(KM_ERROR_UNSUPPORTED_BLOCK_MODE, "Unsupported block mode"); + sErrorCodeToString.put(KM_ERROR_INCOMPATIBLE_BLOCK_MODE, "Incompatible block mode"); + sErrorCodeToString.put(KM_ERROR_UNSUPPORTED_TAG_LENGTH, + "Unsupported authentication tag length"); + sErrorCodeToString.put(KM_ERROR_UNSUPPORTED_PADDING_MODE, "Unsupported padding mode"); + sErrorCodeToString.put(KM_ERROR_INCOMPATIBLE_PADDING_MODE, "Incompatible padding mode"); + sErrorCodeToString.put(KM_ERROR_UNSUPPORTED_DIGEST, "Unsupported digest"); + sErrorCodeToString.put(KM_ERROR_INCOMPATIBLE_DIGEST, "Incompatible digest"); + sErrorCodeToString.put(KM_ERROR_INVALID_EXPIRATION_TIME, "Invalid expiration time"); + sErrorCodeToString.put(KM_ERROR_INVALID_USER_ID, "Invalid user ID"); + sErrorCodeToString.put(KM_ERROR_INVALID_AUTHORIZATION_TIMEOUT, + "Invalid user authorization timeout"); + sErrorCodeToString.put(KM_ERROR_UNSUPPORTED_KEY_FORMAT, "Unsupported key format"); + sErrorCodeToString.put(KM_ERROR_INCOMPATIBLE_KEY_FORMAT, "Incompatible key format"); + sErrorCodeToString.put(KM_ERROR_INVALID_INPUT_LENGTH, "Invalid input length"); + sErrorCodeToString.put(KM_ERROR_KEY_NOT_YET_VALID, "Key not yet valid"); + sErrorCodeToString.put(KM_ERROR_KEY_EXPIRED, "Key expired"); + sErrorCodeToString.put(KM_ERROR_KEY_USER_NOT_AUTHENTICATED, "Key user not authenticated"); + sErrorCodeToString.put(KM_ERROR_INVALID_OPERATION_HANDLE, "Invalid operation handle"); + sErrorCodeToString.put(KM_ERROR_VERIFICATION_FAILED, "Signature/MAC verification failed"); + sErrorCodeToString.put(KM_ERROR_TOO_MANY_OPERATIONS, "Too many operations"); + sErrorCodeToString.put(KM_ERROR_INVALID_KEY_BLOB, "Invalid key blob"); + sErrorCodeToString.put(KM_ERROR_INVALID_ARGUMENT, "Invalid argument"); + sErrorCodeToString.put(KM_ERROR_UNSUPPORTED_TAG, "Unsupported tag"); + sErrorCodeToString.put(KM_ERROR_INVALID_TAG, "Invalid tag"); + sErrorCodeToString.put(KM_ERROR_MEMORY_ALLOCATION_FAILED, "Memory allocation failed"); + sErrorCodeToString.put(KM_ERROR_UNIMPLEMENTED, "Not implemented"); + sErrorCodeToString.put(KM_ERROR_UNKNOWN_ERROR, "Unknown error"); + } + public static int getTagType(int tag) { return tag & (0xF << 28); } + + public static String getErrorMessage(int errorCode) { + String result = sErrorCodeToString.get(errorCode); + if (result != null) { + return result; + } + return String.valueOf(errorCode); + } } diff --git a/core/java/android/service/voice/VoiceInteractionSession.java b/core/java/android/service/voice/VoiceInteractionSession.java index 7a5bb90..3245f55 100644 --- a/core/java/android/service/voice/VoiceInteractionSession.java +++ b/core/java/android/service/voice/VoiceInteractionSession.java @@ -512,11 +512,7 @@ public abstract class VoiceInteractionSession implements KeyEvent.Callback { Request removeRequest(IBinder reqInterface) { synchronized (this) { - Request req = mActiveRequests.get(reqInterface); - if (req != null) { - mActiveRequests.remove(req); - } - return req; + return mActiveRequests.remove(reqInterface); } } diff --git a/core/java/android/text/DynamicLayout.java b/core/java/android/text/DynamicLayout.java index 1bdaef0..7d2e1ef 100644 --- a/core/java/android/text/DynamicLayout.java +++ b/core/java/android/text/DynamicLayout.java @@ -356,6 +356,8 @@ public class DynamicLayout extends Layout ints[DESCENT] = desc; objects[0] = reflowed.getLineDirections(i); + ints[HYPHEN] = reflowed.getHyphen(i); + if (mEllipsize) { ints[ELLIPSIS_START] = reflowed.getEllipsisStart(i); ints[ELLIPSIS_COUNT] = reflowed.getEllipsisCount(i); @@ -631,6 +633,14 @@ public class DynamicLayout extends Layout return mBottomPadding; } + /** + * @hide + */ + @Override + public int getHyphen(int line) { + return mInts.getValue(line, HYPHEN); + } + @Override public int getEllipsizedWidth() { return mEllipsizedWidth; @@ -739,11 +749,12 @@ public class DynamicLayout extends Layout private static final int TAB = START; private static final int TOP = 1; private static final int DESCENT = 2; - private static final int COLUMNS_NORMAL = 3; + private static final int HYPHEN = 3; + private static final int COLUMNS_NORMAL = 4; - private static final int ELLIPSIS_START = 3; - private static final int ELLIPSIS_COUNT = 4; - private static final int COLUMNS_ELLIPSIZE = 5; + private static final int ELLIPSIS_START = 4; + private static final int ELLIPSIS_COUNT = 5; + private static final int COLUMNS_ELLIPSIZE = 6; private static final int START_MASK = 0x1FFFFFFF; private static final int DIR_SHIFT = 30; diff --git a/core/java/android/text/Hyphenator.java b/core/java/android/text/Hyphenator.java new file mode 100644 index 0000000..f4dff9b --- /dev/null +++ b/core/java/android/text/Hyphenator.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2015 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.text; + +import android.util.Log; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.util.HashMap; +import java.util.Locale; + +/** + * Hyphenator is a wrapper class for a native implementation of automatic hyphenation, + * in essence finding valid hyphenation opportunities in a word. + * + * @hide + */ +/* package */ class Hyphenator { + // This class has deliberately simple lifetime management (no finalizer) because in + // the common case a process will use a very small number of locales. + + private static String TAG = "Hyphenator"; + + static HashMap<Locale, Hyphenator> sMap = new HashMap<Locale, Hyphenator>(); + + private long mNativePtr; + + private Hyphenator(long nativePtr) { + mNativePtr = nativePtr; + } + + public static long get(Locale locale) { + synchronized (sMap) { + Hyphenator result = sMap.get(locale); + if (result == null) { + result = loadHyphenator(locale); + sMap.put(locale, result); + } + return result == null ? 0 : result.mNativePtr; + } + } + + private static Hyphenator loadHyphenator(Locale locale) { + // TODO: find pattern dictionary (from system location) that best matches locale + if (Locale.US.equals(locale)) { + File f = new File("/data/local/tmp/hyph-en-us.pat.txt"); + try { + RandomAccessFile rf = new RandomAccessFile(f, "r"); + byte[] buf = new byte[(int)rf.length()]; + rf.read(buf); + rf.close(); + String patternData = new String(buf); + long nativePtr = StaticLayout.nLoadHyphenator(patternData); + return new Hyphenator(nativePtr); + } catch (IOException e) { + Log.e(TAG, "error loading hyphenation " + f); + } + } + return null; + } +} diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java index fcf1828..22abb18 100644 --- a/core/java/android/text/Layout.java +++ b/core/java/android/text/Layout.java @@ -225,17 +225,17 @@ public abstract class Layout { // Draw the lines, one at a time. // The baseline is the top of the following line minus the current line's descent. - for (int i = firstLine; i <= lastLine; i++) { + for (int lineNum = firstLine; lineNum <= lastLine; lineNum++) { int start = previousLineEnd; - previousLineEnd = getLineStart(i + 1); - int end = getLineVisibleEnd(i, start, previousLineEnd); + previousLineEnd = getLineStart(lineNum + 1); + int end = getLineVisibleEnd(lineNum, start, previousLineEnd); int ltop = previousLineBottom; - int lbottom = getLineTop(i+1); + int lbottom = getLineTop(lineNum + 1); previousLineBottom = lbottom; - int lbaseline = lbottom - getLineDescent(i); + int lbaseline = lbottom - getLineDescent(lineNum); - int dir = getParagraphDirection(i); + int dir = getParagraphDirection(lineNum); int left = 0; int right = mWidth; @@ -254,7 +254,7 @@ public abstract class Layout { // just collect the ones present at the start of the paragraph. // If spanEnd is before the end of the paragraph, that's not // our problem. - if (start >= spanEnd && (i == firstLine || isFirstParaLine)) { + if (start >= spanEnd && (lineNum == firstLine || isFirstParaLine)) { spanEnd = sp.nextSpanTransition(start, textLength, ParagraphStyle.class); spans = getParagraphSpans(sp, start, spanEnd, ParagraphStyle.class); @@ -280,7 +280,7 @@ public abstract class Layout { int startLine = getLineForOffset(sp.getSpanStart(spans[n])); // if there is more than one LeadingMarginSpan2, use // the count that is greatest - if (i < startLine + count) { + if (lineNum < startLine + count) { useFirstLineMargin = true; break; } @@ -304,7 +304,7 @@ public abstract class Layout { } } - boolean hasTabOrEmoji = getLineContainsTab(i); + boolean hasTabOrEmoji = getLineContainsTab(lineNum); // Can't tell if we have tabs for sure, currently if (hasTabOrEmoji && !tabStopsIsInitialized) { if (tabStops == null) { @@ -333,7 +333,7 @@ public abstract class Layout { x = right; } } else { - int max = (int)getLineExtent(i, tabStops, false); + int max = (int)getLineExtent(lineNum, tabStops, false); if (align == Alignment.ALIGN_OPPOSITE) { if (dir == DIR_LEFT_TO_RIGHT) { x = right - max; @@ -346,7 +346,8 @@ public abstract class Layout { } } - Directions directions = getLineDirections(i); + paint.setHyphenEdit(getHyphen(lineNum)); + Directions directions = getLineDirections(lineNum); if (directions == DIRS_ALL_LEFT_TO_RIGHT && !mSpannedText && !hasTabOrEmoji) { // XXX: assumes there's nothing additional to be done canvas.drawText(buf, start, end, x, lbaseline, paint); @@ -677,6 +678,15 @@ public abstract class Layout { */ public abstract int getBottomPadding(); + /** + * Returns the hyphen edit for a line. + * + * @hide + */ + public int getHyphen(int line) { + return 0; + } + /** * Returns true if the character at offset and the preceding character @@ -1153,7 +1163,10 @@ public abstract class Layout { return end - 1; } - if (ch != ' ' && ch != '\t') { + // Note: keep this in sync with Minikin LineBreaker::isLineEndSpace() + if (!(ch == ' ' || ch == '\t' || ch == 0x1680 || + (0x2000 <= ch && ch <= 0x200A && ch != 0x2007) || + ch == 0x205F || ch == 0x3000)) { break; } diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java index ee39e27..4174df0 100644 --- a/core/java/android/text/StaticLayout.java +++ b/core/java/android/text/StaticLayout.java @@ -170,8 +170,10 @@ public class StaticLayout extends Layout { * Measurement and break iteration is done in native code. The protocol for using * the native code is as follows. * - * For each paragraph, do a nSetText of the paragraph text. Then, for each run within the - * paragraph: + * For each paragraph, do a nSetupParagraph, which sets paragraph text, line width, tab + * stops, break strategy (and possibly other parameters in the future). + * + * Then, for each run within the paragraph: * - setLocale (this must be done at least for the first run, optional afterwards) * - one of the following, depending on the type of run: * + addStyleRun (a text run, to be measured in native code) @@ -186,7 +188,7 @@ public class StaticLayout extends Layout { private void setLocale(Locale locale) { if (!locale.equals(mLocale)) { - nSetLocale(mNativePtr, locale.toLanguageTag()); + nSetLocale(mNativePtr, locale.toLanguageTag(), Hyphenator.get(locale)); mLocale = locale; } } @@ -459,7 +461,26 @@ public class StaticLayout extends Layout { byte[] chdirs = measured.mLevels; int dir = measured.mDir; boolean easy = measured.mEasy; - nSetText(b.mNativePtr, chs, paraEnd - paraStart); + + // tab stop locations + int[] variableTabStops = null; + if (spanned != null) { + TabStopSpan[] spans = getParagraphSpans(spanned, paraStart, + paraEnd, TabStopSpan.class); + if (spans.length > 0) { + int[] stops = new int[spans.length]; + for (int i = 0; i < spans.length; i++) { + stops[i] = spans[i].getTabStop(); + } + Arrays.sort(stops, 0, stops.length); + variableTabStops = stops; + } + } + + int breakStrategy = 0; // 0 = kBreakStrategy_Greedy + nSetupParagraph(b.mNativePtr, chs, paraEnd - paraStart, + firstWidth, firstWidthLineCount, restWidth, + variableTabStops, TAB_INCREMENT, breakStrategy); // measurement has to be done before performing line breaking // but we don't want to recompute fontmetrics or span ranges the @@ -505,29 +526,13 @@ public class StaticLayout extends Layout { spanEndCacheCount++; } - // tab stop locations - int[] variableTabStops = null; - if (spanned != null) { - TabStopSpan[] spans = getParagraphSpans(spanned, paraStart, - paraEnd, TabStopSpan.class); - if (spans.length > 0) { - int[] stops = new int[spans.length]; - for (int i = 0; i < spans.length; i++) { - stops[i] = spans[i].getTabStop(); - } - Arrays.sort(stops, 0, stops.length); - variableTabStops = stops; - } - } - nGetWidths(b.mNativePtr, widths); - int breakCount = nComputeLineBreaks(b.mNativePtr, paraEnd - paraStart, firstWidth, - firstWidthLineCount, restWidth, variableTabStops, TAB_INCREMENT, false, lineBreaks, - lineBreaks.breaks, lineBreaks.widths, lineBreaks.flags, lineBreaks.breaks.length); + int breakCount = nComputeLineBreaks(b.mNativePtr, lineBreaks, lineBreaks.breaks, + lineBreaks.widths, lineBreaks.flags, lineBreaks.breaks.length); int[] breaks = lineBreaks.breaks; float[] lineWidths = lineBreaks.widths; - boolean[] flags = lineBreaks.flags; + int[] flags = lineBreaks.flags; // here is the offset of the starting character of the line we are currently measuring int here = paraStart; @@ -613,7 +618,7 @@ public class StaticLayout extends Layout { fm.top, fm.bottom, v, spacingmult, spacingadd, null, - null, fm, false, + null, fm, 0, needMultiply, measured.mLevels, measured.mDir, measured.mEasy, bufEnd, includepad, trackpad, null, null, bufStart, ellipsize, @@ -625,7 +630,7 @@ public class StaticLayout extends Layout { int above, int below, int top, int bottom, int v, float spacingmult, float spacingadd, LineHeightSpan[] chooseHt, int[] chooseHtv, - Paint.FontMetricsInt fm, boolean hasTabOrEmoji, + Paint.FontMetricsInt fm, int flags, boolean needMultiply, byte[] chdirs, int dir, boolean easy, int bufEnd, boolean includePad, boolean trackPad, char[] chs, @@ -718,8 +723,10 @@ public class StaticLayout extends Layout { lines[off + mColumns + START] = end; lines[off + mColumns + TOP] = v; - if (hasTabOrEmoji) - lines[off + TAB] |= TAB_MASK; + // TODO: could move TAB to share same column as HYPHEN, simplifying this code and gaining + // one bit for start field + lines[off + TAB] |= flags & TAB_MASK; + lines[off + HYPHEN] = flags; lines[off + DIR] |= dir << DIR_SHIFT; Directions linedirs = DIRS_ALL_LEFT_TO_RIGHT; @@ -938,6 +945,14 @@ public class StaticLayout extends Layout { return mBottomPadding; } + /** + * @hide + */ + @Override + public int getHyphen(int line) { + return mLines[mColumns * line + HYPHEN] & 0xff; + } + @Override public int getEllipsisCount(int line) { if (mColumns < COLUMNS_ELLIPSIZE) { @@ -964,9 +979,15 @@ public class StaticLayout extends Layout { private static native long nNewBuilder(); private static native void nFreeBuilder(long nativePtr); private static native void nFinishBuilder(long nativePtr); - private static native void nSetLocale(long nativePtr, String locale); - private static native void nSetText(long nativePtr, char[] text, int length); + /* package */ static native long nLoadHyphenator(String patternData); + + private static native void nSetLocale(long nativePtr, String locale, long nativeHyphenator); + + // Set up paragraph text and settings; done as one big method to minimize jni crossings + private static native void nSetupParagraph(long nativePtr, char[] text, int length, + float firstWidth, int firstWidthLineCount, float restWidth, + int[] variableTabStops, int defaultTabStop, int breakStrategy); private static native float nAddStyleRun(long nativePtr, long nativePaint, long nativeTypeface, int start, int end, boolean isRtl); @@ -983,25 +1004,24 @@ public class StaticLayout extends Layout { // the arrays inside the LineBreaks objects are passed in as well // to reduce the number of JNI calls in the common case where the // arrays do not have to be resized - private static native int nComputeLineBreaks(long nativePtr, - int length, float firstWidth, int firstWidthLineCount, float restWidth, - int[] variableTabStops, int defaultTabStop, boolean optimize, LineBreaks recycle, - int[] recycleBreaks, float[] recycleWidths, boolean[] recycleFlags, int recycleLength); + private static native int nComputeLineBreaks(long nativePtr, LineBreaks recycle, + int[] recycleBreaks, float[] recycleWidths, int[] recycleFlags, int recycleLength); private int mLineCount; private int mTopPadding, mBottomPadding; private int mColumns; private int mEllipsizedWidth; - private static final int COLUMNS_NORMAL = 3; - private static final int COLUMNS_ELLIPSIZE = 5; + private static final int COLUMNS_NORMAL = 4; + private static final int COLUMNS_ELLIPSIZE = 6; private static final int START = 0; private static final int DIR = START; private static final int TAB = START; private static final int TOP = 1; private static final int DESCENT = 2; - private static final int ELLIPSIS_START = 3; - private static final int ELLIPSIS_COUNT = 4; + private static final int HYPHEN = 3; + private static final int ELLIPSIS_START = 4; + private static final int ELLIPSIS_COUNT = 5; private int[] mLines; private Directions[] mLineDirections; @@ -1023,7 +1043,7 @@ public class StaticLayout extends Layout { private static final int INITIAL_SIZE = 16; public int[] breaks = new int[INITIAL_SIZE]; public float[] widths = new float[INITIAL_SIZE]; - public boolean[] flags = new boolean[INITIAL_SIZE]; // hasTabOrEmoji + public int[] flags = new int[INITIAL_SIZE]; // hasTabOrEmoji // breaks, widths, and flags should all have the same length } diff --git a/core/java/android/text/TextLine.java b/core/java/android/text/TextLine.java index 4725581..479242c 100644 --- a/core/java/android/text/TextLine.java +++ b/core/java/android/text/TextLine.java @@ -955,6 +955,10 @@ class TextLine { span.updateDrawState(wp); } + // Only draw hyphen on last run in line + if (jnext < mLen) { + wp.setHyphenEdit(0); + } x += handleText(wp, j, jnext, i, inext, runIsRtl, c, x, top, y, bottom, fmi, needWidth || jnext < measureLimit); } diff --git a/core/java/android/text/format/Time.java b/core/java/android/text/format/Time.java index 0c66709..d567d90 100644 --- a/core/java/android/text/format/Time.java +++ b/core/java/android/text/format/Time.java @@ -47,6 +47,7 @@ import libcore.util.ZoneInfoDB; * before 1st Jan 1970 UTC).</li> * <li>Much of the formatting / parsing assumes ASCII text and is therefore not suitable for * use with non-ASCII scripts.</li> + * <li>No support for pseudo-zones like "GMT-07:00".</li> * </ul> * * @deprecated Use {@link java.util.GregorianCalendar} instead. diff --git a/core/java/android/util/DebugUtils.java b/core/java/android/util/DebugUtils.java index 84d9ce8..c44f42b 100644 --- a/core/java/android/util/DebugUtils.java +++ b/core/java/android/util/DebugUtils.java @@ -17,8 +17,10 @@ package android.util; import java.io.PrintWriter; -import java.lang.reflect.Method; +import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.util.Locale; /** @@ -203,4 +205,57 @@ public class DebugUtils { outBuilder.append(suffix); return outBuilder.toString(); } + + /** + * Use prefixed constants (static final values) on given class to turn value + * into human-readable string. + * + * @hide + */ + public static String valueToString(Class<?> clazz, String prefix, int value) { + for (Field field : clazz.getDeclaredFields()) { + final int modifiers = field.getModifiers(); + if (Modifier.isStatic(modifiers) && Modifier.isFinal(modifiers) + && field.getType().equals(int.class) && field.getName().startsWith(prefix)) { + try { + if (value == field.getInt(null)) { + return field.getName().substring(prefix.length()); + } + } catch (IllegalAccessException ignored) { + } + } + } + return Integer.toString(value); + } + + /** + * Use prefixed constants (static final values) on given class to turn flags + * into human-readable string. + * + * @hide + */ + public static String flagsToString(Class<?> clazz, String prefix, int flags) { + final StringBuilder res = new StringBuilder(); + + for (Field field : clazz.getDeclaredFields()) { + final int modifiers = field.getModifiers(); + if (Modifier.isStatic(modifiers) && Modifier.isFinal(modifiers) + && field.getType().equals(int.class) && field.getName().startsWith(prefix)) { + try { + final int value = field.getInt(null); + if ((flags & value) != 0) { + flags &= ~value; + res.append(field.getName().substring(prefix.length())).append('|'); + } + } catch (IllegalAccessException ignored) { + } + } + } + if (flags != 0 || res.length() == 0) { + res.append(Integer.toHexString(flags)); + } else { + res.deleteCharAt(res.length() - 1); + } + return res.toString(); + } } diff --git a/core/java/android/view/IGraphicsStats.aidl b/core/java/android/view/IGraphicsStats.aidl new file mode 100644 index 0000000..c235eb2 --- /dev/null +++ b/core/java/android/view/IGraphicsStats.aidl @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2015, 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.view; + +import android.os.ParcelFileDescriptor; + +/** + * @hide + */ +interface IGraphicsStats { + ParcelFileDescriptor requestBufferForProcess(String packageName, IBinder token); +} diff --git a/core/java/android/view/ThreadedRenderer.java b/core/java/android/view/ThreadedRenderer.java index 031be07..87d5d9a 100644 --- a/core/java/android/view/ThreadedRenderer.java +++ b/core/java/android/view/ThreadedRenderer.java @@ -23,7 +23,9 @@ import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Rect; import android.graphics.drawable.Drawable; +import android.os.Binder; import android.os.IBinder; +import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.os.ServiceManager; import android.os.Trace; @@ -124,7 +126,7 @@ public class ThreadedRenderer extends HardwareRenderer { mRootNode.setClipToBounds(false); mNativeProxy = nCreateProxy(translucent, rootNodePtr); - AtlasInitializer.sInstance.init(context, mNativeProxy); + ProcessInitializer.sInstance.init(context, mNativeProxy); loadSystemProperties(); } @@ -410,15 +412,44 @@ public class ThreadedRenderer extends HardwareRenderer { nTrimMemory(level); } - private static class AtlasInitializer { - static AtlasInitializer sInstance = new AtlasInitializer(); + public static void dumpProfileData(byte[] data, FileDescriptor fd) { + nDumpProfileData(data, fd); + } + + private static class ProcessInitializer { + static ProcessInitializer sInstance = new ProcessInitializer(); + static IGraphicsStats sGraphicsStatsService; + private static IBinder sProcToken; private boolean mInitialized = false; - private AtlasInitializer() {} + private ProcessInitializer() {} synchronized void init(Context context, long renderProxy) { if (mInitialized) return; + mInitialized = true; + initGraphicsStats(context, renderProxy); + initAssetAtlas(context, renderProxy); + } + + private static void initGraphicsStats(Context context, long renderProxy) { + IBinder binder = ServiceManager.getService("graphicsstats"); + if (binder == null) return; + + sGraphicsStatsService = IGraphicsStats.Stub.asInterface(binder); + sProcToken = new Binder(); + try { + final String pkg = context.getApplicationInfo().packageName; + ParcelFileDescriptor pfd = sGraphicsStatsService. + requestBufferForProcess(pkg, sProcToken); + nSetProcessStatsBuffer(renderProxy, pfd.getFd()); + pfd.close(); + } catch (Exception e) { + Log.w(LOG_TAG, "Could not acquire gfx stats buffer", e); + } + } + + private static void initAssetAtlas(Context context, long renderProxy) { IBinder binder = ServiceManager.getService("assetatlas"); if (binder == null) return; @@ -432,7 +463,6 @@ public class ThreadedRenderer extends HardwareRenderer { // TODO Remove after fixing b/15425820 validateMap(context, map); nSetAtlas(renderProxy, buffer, map); - mInitialized = true; } // If IAssetAtlas is not the same class as the IBinder // we are using a remote service and we can safely @@ -477,6 +507,7 @@ public class ThreadedRenderer extends HardwareRenderer { static native void setupShadersDiskCache(String cacheFile); private static native void nSetAtlas(long nativeProxy, GraphicBuffer buffer, long[] map); + private static native void nSetProcessStatsBuffer(long nativeProxy, int fd); private static native long nCreateRootRenderNode(); private static native long nCreateProxy(boolean translucent, long rootRenderNode); @@ -514,4 +545,5 @@ public class ThreadedRenderer extends HardwareRenderer { private static native void nDumpProfileInfo(long nativeProxy, FileDescriptor fd, @DumpFlags int dumpFlags); + private static native void nDumpProfileData(byte[] data, FileDescriptor fd); } diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index db8109f..a69384a 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -3196,9 +3196,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, private static class ForegroundInfo { private Drawable mDrawable; private TintInfo mTintInfo; - private int mGravity = Gravity.START | Gravity.TOP; + private int mGravity = Gravity.FILL; private boolean mInsidePadding = true; - private boolean mBoundsChanged; + private boolean mBoundsChanged = true; private final Rect mSelfBounds = new Rect(); private final Rect mOverlayBounds = new Rect(); } @@ -9959,6 +9959,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @param oldt Previous vertical scroll origin. */ protected void onScrollChanged(int l, int t, int oldl, int oldt) { + notifySubtreeAccessibilityStateChangedIfNeeded(); + if (AccessibilityManager.getInstance(mContext).isEnabled()) { postSendViewScrolledAccessibilityEventCallback(); } @@ -11162,6 +11164,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, invalidateViewProperty(false, true); invalidateParentIfNeededAndWasQuickRejected(); + notifySubtreeAccessibilityStateChangedIfNeeded(); } } @@ -16674,6 +16677,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } mForegroundInfo.mDrawable = foreground; + mForegroundInfo.mBoundsChanged = true; if (foreground != null) { setWillNotDraw(false); foreground.setCallback(this); diff --git a/core/java/android/view/ViewDebug.java b/core/java/android/view/ViewDebug.java index 50e64c6..a237afd 100644 --- a/core/java/android/view/ViewDebug.java +++ b/core/java/android/view/ViewDebug.java @@ -1005,31 +1005,23 @@ public class ViewDebug { return fields; } - final ArrayList<Field> declaredFields = new ArrayList(); - klass.getDeclaredFieldsUnchecked(false, declaredFields); - - final ArrayList<Field> foundFields = new ArrayList<Field>(); - final int count = declaredFields.size(); - for (int i = 0; i < count; i++) { - final Field field = declaredFields.get(i); - - // Ensure the field type can be resolved. - try { - field.getType(); - } catch (NoClassDefFoundError e) { - continue; - } - - if (field.isAnnotationPresent(ExportedProperty.class)) { - field.setAccessible(true); - foundFields.add(field); - sAnnotations.put(field, field.getAnnotation(ExportedProperty.class)); + try { + final Field[] declaredFields = klass.getDeclaredFieldsUnchecked(false); + final ArrayList<Field> foundFields = new ArrayList<Field>(); + for (final Field field : declaredFields) { + // Fields which can't be resolved have a null type. + if (field.getType() != null && field.isAnnotationPresent(ExportedProperty.class)) { + field.setAccessible(true); + foundFields.add(field); + sAnnotations.put(field, field.getAnnotation(ExportedProperty.class)); + } } + fields = foundFields.toArray(new Field[foundFields.size()]); + map.put(klass, fields); + } catch (NoClassDefFoundError e) { + throw new AssertionError(e); } - fields = foundFields.toArray(new Field[foundFields.size()]); - map.put(klass, fields); - return fields; } @@ -1651,4 +1643,4 @@ public class ViewDebug { } }); } -}
\ No newline at end of file +} diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 294174a..4158340 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -6268,41 +6268,79 @@ public final class ViewRootImpl implements ViewParent, case AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED: { - if (mAccessibilityFocusedHost != null && mAccessibilityFocusedVirtualView != null) { - // We care only for changes rooted in the focused host. - final long eventSourceId = event.getSourceNodeId(); - final int hostViewId = AccessibilityNodeInfo.getAccessibilityViewId( - eventSourceId); - if (hostViewId != mAccessibilityFocusedHost.getAccessibilityViewId()) { - break; - } - - // We only care about changes that may change the virtual focused view bounds. - final int changes = event.getContentChangeTypes(); - if ((changes & AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE) != 0 - || changes == AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED) { - AccessibilityNodeProvider provider = mAccessibilityFocusedHost - .getAccessibilityNodeProvider(); - if (provider != null) { - final int virtualChildId = AccessibilityNodeInfo.getVirtualDescendantId( - mAccessibilityFocusedVirtualView.getSourceNodeId()); - if (virtualChildId == AccessibilityNodeInfo.UNDEFINED_ITEM_ID) { - mAccessibilityFocusedVirtualView = provider - .createAccessibilityNodeInfo( - AccessibilityNodeProvider.HOST_VIEW_ID); - } else { - mAccessibilityFocusedVirtualView = provider - .createAccessibilityNodeInfo(virtualChildId); - } - } - } - } + handleWindowContentChangedEvent(event); } break; } mAccessibilityManager.sendAccessibilityEvent(event); return true; } + /** + * Updates the focused virtual view, when necessary, in response to a + * content changed event. + * <p> + * This is necessary to get updated bounds after a position change. + * + * @param event an accessibility event of type + * {@link AccessibilityEvent#TYPE_WINDOW_CONTENT_CHANGED} + */ + private void handleWindowContentChangedEvent(AccessibilityEvent event) { + // No virtual view focused, nothing to do here. + if (mAccessibilityFocusedHost == null || mAccessibilityFocusedVirtualView == null) { + return; + } + + // If we have a node but no provider, abort. + final AccessibilityNodeProvider provider = + mAccessibilityFocusedHost.getAccessibilityNodeProvider(); + if (provider == null) { + // TODO: Should we clear the focused virtual view? + return; + } + + // We only care about change types that may affect the bounds of the + // focused virtual view. + final int changes = event.getContentChangeTypes(); + if ((changes & AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE) == 0 + && changes != AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED) { + return; + } + + final long eventSourceNodeId = event.getSourceNodeId(); + final int changedViewId = AccessibilityNodeInfo.getAccessibilityViewId(eventSourceNodeId); + + // Search up the tree for subtree containment. + boolean hostInSubtree = false; + View root = mAccessibilityFocusedHost; + while (root != null && !hostInSubtree) { + if (changedViewId == root.getAccessibilityViewId()) { + hostInSubtree = true; + } else { + final ViewParent parent = root.getParent(); + if (parent instanceof View) { + root = (View) parent; + } else { + root = null; + } + } + } + + // We care only about changes in subtrees containing the host view. + if (!hostInSubtree) { + return; + } + + final long focusedSourceNodeId = mAccessibilityFocusedVirtualView.getSourceNodeId(); + int focusedChildId = AccessibilityNodeInfo.getVirtualDescendantId(focusedSourceNodeId); + if (focusedChildId == AccessibilityNodeInfo.UNDEFINED_ITEM_ID) { + // TODO: Should we clear the focused virtual view? + focusedChildId = AccessibilityNodeProvider.HOST_VIEW_ID; + } + + // Refresh the node for the focused virtual view. + mAccessibilityFocusedVirtualView = provider.createAccessibilityNodeInfo(focusedChildId); + } + @Override public void notifySubtreeAccessibilityStateChanged(View child, View source, int changeType) { postSendWindowContentChangedCallback(source, changeType); diff --git a/core/java/android/view/Window.java b/core/java/android/view/Window.java index 9a92932..36f047e 100644 --- a/core/java/android/view/Window.java +++ b/core/java/android/view/Window.java @@ -1856,14 +1856,14 @@ public abstract class Window { public abstract int getStatusBarColor(); /** - * Sets the color of the status bar to {@param color}. + * Sets the color of the status bar to {@code color}. * * For this to take effect, * the window must be drawing the system bar backgrounds with * {@link android.view.WindowManager.LayoutParams#FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS} and * {@link android.view.WindowManager.LayoutParams#FLAG_TRANSLUCENT_STATUS} must not be set. * - * If {@param color} is not opaque, consider setting + * If {@code color} is not opaque, consider setting * {@link android.view.View#SYSTEM_UI_FLAG_LAYOUT_STABLE} and * {@link android.view.View#SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN}. * <p> diff --git a/core/java/android/webkit/WebChromeClient.java b/core/java/android/webkit/WebChromeClient.java index 4737e9b..0b18bb8 100644 --- a/core/java/android/webkit/WebChromeClient.java +++ b/core/java/android/webkit/WebChromeClient.java @@ -284,13 +284,19 @@ public class WebChromeClient { * currently set for that origin. The host application should invoke the * specified callback with the desired permission state. See * {@link GeolocationPermissions} for details. + * + * If this method isn't overridden, the callback is invoked with permission + * denied state. + * * @param origin The origin of the web content attempting to use the * Geolocation API. * @param callback The callback to use to set the permission state for the * origin. */ public void onGeolocationPermissionsShowPrompt(String origin, - GeolocationPermissions.Callback callback) {} + GeolocationPermissions.Callback callback) { + callback.invoke(origin, false, false); + } /** * Notify the host application that a request for Geolocation permissions, diff --git a/core/java/android/webkit/WebViewClient.java b/core/java/android/webkit/WebViewClient.java index 53c7e04..8a2b3fa 100644 --- a/core/java/android/webkit/WebViewClient.java +++ b/core/java/android/webkit/WebViewClient.java @@ -200,8 +200,6 @@ public class WebViewClient { public static final int ERROR_FILE_NOT_FOUND = -14; /** Too many requests during this load */ public static final int ERROR_TOO_MANY_REQUESTS = -15; - /** Request blocked by the browser */ - public static final int ERROR_BLOCKED = -16; /** * Report an error to the host application. These errors are unrecoverable diff --git a/core/java/android/widget/AnalogClock.java b/core/java/android/widget/AnalogClock.java index 1716dbd..45eee34 100644 --- a/core/java/android/widget/AnalogClock.java +++ b/core/java/android/widget/AnalogClock.java @@ -40,8 +40,10 @@ import java.util.TimeZone; * @attr ref android.R.styleable#AnalogClock_dial * @attr ref android.R.styleable#AnalogClock_hand_hour * @attr ref android.R.styleable#AnalogClock_hand_minute + * @deprecated This widget is no longer supported. */ @RemoteView +@Deprecated public class AnalogClock extends View { private Time mCalendar; diff --git a/core/java/android/widget/DatePickerCalendarDelegate.java b/core/java/android/widget/DatePickerCalendarDelegate.java index 7b8a979..a157087 100755 --- a/core/java/android/widget/DatePickerCalendarDelegate.java +++ b/core/java/android/widget/DatePickerCalendarDelegate.java @@ -49,7 +49,6 @@ import java.util.Locale; * A delegate for picking up a date (day / month / year). */ class DatePickerCalendarDelegate extends DatePicker.AbstractDatePickerDelegate { - private static final int USE_LOCALE = 0; private static final int UNINITIALIZED = -1; @@ -61,9 +60,9 @@ class DatePickerCalendarDelegate extends DatePicker.AbstractDatePickerDelegate { private static final int ANIMATION_DURATION = 300; - public static final int[] ATTRS_TEXT_COLOR = new int[]{com.android.internal.R.attr.textColor}; - - public static final int[] ATTRS_DISABLED_ALPHA = new int[]{ + private static final int[] ATTRS_TEXT_COLOR = new int[] { + com.android.internal.R.attr.textColor}; + private static final int[] ATTRS_DISABLED_ALPHA = new int[] { com.android.internal.R.attr.disabledAlpha}; private SimpleDateFormat mYearFormat; @@ -157,6 +156,8 @@ class DatePickerCalendarDelegate extends DatePicker.AbstractDatePickerDelegate { header.setBackground(a.getDrawable(R.styleable.DatePicker_headerBackground)); } + a.recycle(); + // Set up picker container. mAnimator = (ViewAnimator) mContainer.findViewById(R.id.animator); @@ -174,32 +175,10 @@ class DatePickerCalendarDelegate extends DatePicker.AbstractDatePickerDelegate { mYearPickerView.setDate(mCurrentDate.getTimeInMillis()); mYearPickerView.setOnYearSelectedListener(mOnYearSelectedListener); - final int yearTextAppearanceResId = a.getResourceId( - R.styleable.DatePicker_yearListItemTextAppearance, 0); - if (yearTextAppearanceResId != 0) { - mYearPickerView.setYearTextAppearance(yearTextAppearanceResId); - } - - final int yearActivatedTextAppearanceResId = a.getResourceId( - R.styleable.DatePicker_yearListItemActivatedTextAppearance, 0); - if (yearActivatedTextAppearanceResId != 0) { - mYearPickerView.setYearActivatedTextAppearance(yearActivatedTextAppearanceResId); - } - - a.recycle(); - // Set up content descriptions. mSelectDay = res.getString(R.string.select_day); mSelectYear = res.getString(R.string.select_year); - final Animation inAnim = new AlphaAnimation(0, 1); - inAnim.setDuration(ANIMATION_DURATION); - mAnimator.setInAnimation(inAnim); - - final Animation outAnim = new AlphaAnimation(1, 0); - outAnim.setDuration(ANIMATION_DURATION); - mAnimator.setOutAnimation(outAnim); - // Initialize for current locale. This also initializes the date, so no // need to call onDateChanged. onLocaleChanged(mCurrentLocale); diff --git a/core/java/android/widget/DayPickerAdapter.java b/core/java/android/widget/DayPickerAdapter.java index 4f9f09e..9a4b6f5 100644 --- a/core/java/android/widget/DayPickerAdapter.java +++ b/core/java/android/widget/DayPickerAdapter.java @@ -18,10 +18,15 @@ package android.widget; import com.android.internal.widget.PagerAdapter; +import android.annotation.IdRes; +import android.annotation.LayoutRes; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.TypedArray; import android.util.SparseArray; +import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.SimpleMonthView.OnDayClickListener; @@ -37,9 +42,13 @@ class DayPickerAdapter extends PagerAdapter { private final Calendar mMinDate = Calendar.getInstance(); private final Calendar mMaxDate = Calendar.getInstance(); - private final SparseArray<SimpleMonthView> mItems = new SparseArray<>(); + private final SparseArray<ViewHolder> mItems = new SparseArray<>(); - private Calendar mSelectedDay = Calendar.getInstance(); + private final LayoutInflater mInflater; + private final int mLayoutResId; + private final int mCalendarViewId; + + private Calendar mSelectedDay = null; private int mMonthTextAppearance; private int mDayOfWeekTextAppearance; @@ -51,19 +60,29 @@ class DayPickerAdapter extends PagerAdapter { private OnDaySelectedListener mOnDaySelectedListener; + private int mCount; private int mFirstDayOfWeek; - public DayPickerAdapter(Context context) { + public DayPickerAdapter(@NonNull Context context, @LayoutRes int layoutResId, + @IdRes int calendarViewId) { + mInflater = LayoutInflater.from(context); + mLayoutResId = layoutResId; + mCalendarViewId = calendarViewId; + final TypedArray ta = context.obtainStyledAttributes(new int[] { com.android.internal.R.attr.colorControlHighlight}); mDayHighlightColor = ta.getColorStateList(0); ta.recycle(); } - public void setRange(Calendar min, Calendar max) { + public void setRange(@NonNull Calendar min, @NonNull Calendar max) { mMinDate.setTimeInMillis(min.getTimeInMillis()); mMaxDate.setTimeInMillis(max.getTimeInMillis()); + final int diffYear = mMaxDate.get(Calendar.YEAR) - mMinDate.get(Calendar.YEAR); + final int diffMonth = mMaxDate.get(Calendar.MONTH) - mMinDate.get(Calendar.MONTH); + mCount = diffMonth + MONTHS_IN_YEAR * diffYear + 1; + // Positions are now invalid, clear everything and start over. notifyDataSetChanged(); } @@ -80,7 +99,7 @@ class DayPickerAdapter extends PagerAdapter { // Update displayed views. final int count = mItems.size(); for (int i = 0; i < count; i++) { - final SimpleMonthView monthView = mItems.valueAt(i); + final SimpleMonthView monthView = mItems.valueAt(i).calendar; monthView.setFirstDayOfWeek(weekStart); } } @@ -94,23 +113,25 @@ class DayPickerAdapter extends PagerAdapter { * * @param day the selected day */ - public void setSelectedDay(Calendar day) { + public void setSelectedDay(@Nullable Calendar day) { final int oldPosition = getPositionForDay(mSelectedDay); final int newPosition = getPositionForDay(day); // Clear the old position if necessary. - if (oldPosition != newPosition) { - final SimpleMonthView oldMonthView = mItems.get(oldPosition, null); + if (oldPosition != newPosition && oldPosition >= 0) { + final ViewHolder oldMonthView = mItems.get(oldPosition, null); if (oldMonthView != null) { - oldMonthView.setSelectedDay(-1); + oldMonthView.calendar.setSelectedDay(-1); } } // Set the new position. - final SimpleMonthView newMonthView = mItems.get(newPosition, null); - if (newMonthView != null) { - final int dayOfMonth = day.get(Calendar.DAY_OF_MONTH); - newMonthView.setSelectedDay(dayOfMonth); + if (newPosition >= 0) { + final ViewHolder newMonthView = mItems.get(newPosition, null); + if (newMonthView != null) { + final int dayOfMonth = day.get(Calendar.DAY_OF_MONTH); + newMonthView.calendar.setSelectedDay(dayOfMonth); + } } mSelectedDay = day; @@ -155,14 +176,13 @@ class DayPickerAdapter extends PagerAdapter { @Override public int getCount() { - final int diffYear = mMaxDate.get(Calendar.YEAR) - mMinDate.get(Calendar.YEAR); - final int diffMonth = mMaxDate.get(Calendar.MONTH) - mMinDate.get(Calendar.MONTH); - return diffMonth + MONTHS_IN_YEAR * diffYear + 1; + return mCount; } @Override public boolean isViewFromObject(View view, Object object) { - return view == object; + final ViewHolder holder = (ViewHolder) object; + return view == holder.container; } private int getMonthForPosition(int position) { @@ -173,7 +193,11 @@ class DayPickerAdapter extends PagerAdapter { return position / MONTHS_IN_YEAR + mMinDate.get(Calendar.YEAR); } - private int getPositionForDay(Calendar day) { + private int getPositionForDay(@Nullable Calendar day) { + if (day == null) { + return -1; + } + final int yearOffset = (day.get(Calendar.YEAR) - mMinDate.get(Calendar.YEAR)); final int monthOffset = (day.get(Calendar.MONTH) - mMinDate.get(Calendar.MONTH)); return yearOffset * MONTHS_IN_YEAR + monthOffset; @@ -181,7 +205,9 @@ class DayPickerAdapter extends PagerAdapter { @Override public Object instantiateItem(ViewGroup container, int position) { - final SimpleMonthView v = new SimpleMonthView(container.getContext()); + final View itemView = mInflater.inflate(mLayoutResId, container, false); + + final SimpleMonthView v = (SimpleMonthView) itemView.findViewById(mCalendarViewId); v.setOnDayClickListener(mOnDayClickListener); v.setMonthTextAppearance(mMonthTextAppearance); v.setDayOfWeekTextAppearance(mDayOfWeekTextAppearance); @@ -205,7 +231,7 @@ class DayPickerAdapter extends PagerAdapter { final int year = getYearForPosition(position); final int selectedDay; - if (mSelectedDay.get(Calendar.MONTH) == month) { + if (mSelectedDay != null && mSelectedDay.get(Calendar.MONTH) == month) { selectedDay = mSelectedDay.get(Calendar.DAY_OF_MONTH); } else { selectedDay = -1; @@ -227,33 +253,34 @@ class DayPickerAdapter extends PagerAdapter { v.setMonthParams(selectedDay, month, year, mFirstDayOfWeek, enabledDayRangeStart, enabledDayRangeEnd); + v.setPrevEnabled(position > 0); + v.setNextEnabled(position < mCount - 1); - mItems.put(position, v); + final ViewHolder holder = new ViewHolder(position, itemView, v); + mItems.put(position, holder); - container.addView(v); + container.addView(itemView); - return v; + return holder; } @Override public void destroyItem(ViewGroup container, int position, Object object) { - container.removeView(mItems.get(position)); + final ViewHolder holder = (ViewHolder) object; + container.removeView(holder.container); mItems.remove(position); } @Override public int getItemPosition(Object object) { - final int index = mItems.indexOfValue((SimpleMonthView) object); - if (index < 0) { - return mItems.keyAt(index); - } - return -1; + final ViewHolder holder = (ViewHolder) object; + return holder.position; } @Override public CharSequence getPageTitle(int position) { - final SimpleMonthView v = mItems.get(position); + final SimpleMonthView v = mItems.get(position).calendar; if (v != null) { return v.getTitle(); } @@ -275,9 +302,29 @@ class DayPickerAdapter extends PagerAdapter { } } } + + @Override + public void onNavigationClick(SimpleMonthView view, int direction, boolean animate) { + if (mOnDaySelectedListener != null) { + mOnDaySelectedListener.onNavigationClick(DayPickerAdapter.this, direction, animate); + } + } }; + private static class ViewHolder { + public final int position; + public final View container; + public final SimpleMonthView calendar; + + public ViewHolder(int position, View container, SimpleMonthView calendar) { + this.position = position; + this.container = container; + this.calendar = calendar; + } + } + public interface OnDaySelectedListener { public void onDaySelected(DayPickerAdapter view, Calendar day); + public void onNavigationClick(DayPickerAdapter view, int direction, boolean animate); } } diff --git a/core/java/android/widget/DayPickerView.java b/core/java/android/widget/DayPickerView.java index a7ae926..e2f8efc 100644 --- a/core/java/android/widget/DayPickerView.java +++ b/core/java/android/widget/DayPickerView.java @@ -88,7 +88,8 @@ class DayPickerView extends ViewPager { a.recycle(); // Set up adapter. - mAdapter = new DayPickerAdapter(context); + mAdapter = new DayPickerAdapter(context, + R.layout.date_picker_month_item_material, R.id.month_view); mAdapter.setMonthTextAppearance(monthTextAppearanceResId); mAdapter.setDayOfWeekTextAppearance(dayOfWeekTextAppearanceResId); mAdapter.setDayTextAppearance(dayTextAppearanceResId); @@ -128,6 +129,14 @@ class DayPickerView extends ViewPager { mOnDaySelectedListener.onDaySelected(DayPickerView.this, day); } } + + @Override + public void onNavigationClick(DayPickerAdapter view, int direction, boolean animate) { + // ViewPager clamps input values, so we don't need to worry + // about passing invalid indices. + final int nextItem = getCurrentItem() + direction; + setCurrentItem(nextItem, animate); + } }); } diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java index 87fcd81..82e36c3 100644 --- a/core/java/android/widget/Editor.java +++ b/core/java/android/widget/Editor.java @@ -16,18 +16,6 @@ package android.widget; -import android.content.UndoManager; -import android.content.UndoOperation; -import android.content.UndoOwner; -import android.os.Build; -import android.os.Parcel; -import android.os.Parcelable; -import android.text.InputFilter; -import com.android.internal.util.ArrayUtils; -import com.android.internal.util.GrowingArrayUtils; -import com.android.internal.view.menu.MenuBuilder; -import com.android.internal.widget.EditableInputConnection; - import android.R; import android.app.PendingIntent; import android.app.PendingIntent.CanceledException; @@ -35,6 +23,9 @@ import android.content.ClipData; import android.content.ClipData.Item; import android.content.Context; import android.content.Intent; +import android.content.UndoManager; +import android.content.UndoOperation; +import android.content.UndoOwner; import android.content.pm.PackageManager; import android.content.res.TypedArray; import android.graphics.Canvas; @@ -46,13 +37,17 @@ import android.graphics.Rect; import android.graphics.RectF; import android.graphics.drawable.Drawable; import android.inputmethodservice.ExtractEditText; +import android.os.Build; import android.os.Bundle; import android.os.Handler; +import android.os.Parcel; +import android.os.Parcelable; import android.os.ParcelableParcel; import android.os.SystemClock; import android.provider.Settings; import android.text.DynamicLayout; import android.text.Editable; +import android.text.InputFilter; import android.text.InputType; import android.text.Layout; import android.text.ParcelableSpan; @@ -105,6 +100,11 @@ import android.widget.AdapterView.OnItemClickListener; import android.widget.TextView.Drawables; import android.widget.TextView.OnEditorActionListener; +import com.android.internal.util.ArrayUtils; +import com.android.internal.util.GrowingArrayUtils; +import com.android.internal.view.menu.MenuBuilder; +import com.android.internal.widget.EditableInputConnection; + import java.text.BreakIterator; import java.util.Arrays; import java.util.Comparator; @@ -2939,7 +2939,24 @@ public class Editor { * The default callback provides a subset of Select All, Cut, Copy and Paste actions, depending * on which of these this TextView supports. */ - private class SelectionActionModeCallback implements ActionMode.Callback { + private class SelectionActionModeCallback extends ActionMode.Callback2 { + private final Path mSelectionPath = new Path(); + private final RectF mSelectionBounds = new RectF(); + + private int mSelectionHandleHeight; + private int mInsertionHandleHeight; + + public SelectionActionModeCallback() { + SelectionModifierCursorController selectionController = getSelectionController(); + if (selectionController.mStartHandle == null) { + selectionController.initDrawables(); + selectionController.initHandles(); + } + mSelectionHandleHeight = Math.max( + mSelectHandleLeft.getMinimumHeight(), mSelectHandleRight.getMinimumHeight()); + getInsertionController().getHandle(); + mInsertionHandleHeight = mSelectHandleCenter.getMinimumHeight(); + } @Override public boolean onCreateActionMode(ActionMode mode, Menu menu) { @@ -2956,13 +2973,6 @@ public class Editor { mode.setSubtitle(null); mode.setTitleOptionalHint(true); - menu.add(0, TextView.ID_SELECT_ALL, 0, com.android.internal.R.string.selectAll). - setIcon(styledAttributes.getResourceId( - R.styleable.SelectionModeDrawables_actionModeSelectAllDrawable, 0)). - setAlphabeticShortcut('a'). - setShowAsAction( - MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT); - if (mTextView.canCut()) { menu.add(0, TextView.ID_CUT, 0, com.android.internal.R.string.cut). setIcon(styledAttributes.getResourceId( @@ -2990,6 +3000,13 @@ public class Editor { MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT); } + menu.add(0, TextView.ID_SELECT_ALL, 0, com.android.internal.R.string.selectAll). + setIcon(styledAttributes.getResourceId( + R.styleable.SelectionModeDrawables_actionModeSelectAllDrawable, 0)). + setAlphabeticShortcut('a'). + setShowAsAction( + MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT); + if (mTextView.isSuggestionsEnabled() && isCursorInsideSuggestionSpan()) { menu.add(0, TextView.ID_REPLACE, 0, com.android.internal.R.string.replace). setShowAsAction( @@ -3057,6 +3074,40 @@ public class Editor { mSelectionActionMode = null; } + + @Override + public void onGetContentRect(ActionMode mode, View view, Rect outRect) { + if (!view.equals(mTextView) || mTextView.getLayout() == null) { + super.onGetContentRect(mode, view, outRect); + return; + } + if (mTextView.getSelectionStart() != mTextView.getSelectionEnd()) { + // We have a selection. + mSelectionPath.reset(); + mTextView.getLayout().getSelectionPath( + mTextView.getSelectionStart(), mTextView.getSelectionEnd(), mSelectionPath); + mSelectionPath.computeBounds(mSelectionBounds, true); + mSelectionBounds.bottom += mSelectionHandleHeight; + } else { + // We have a single cursor. + int line = mTextView.getLayout().getLineForOffset(mTextView.getSelectionStart()); + float primaryHorizontal = + mTextView.getLayout().getPrimaryHorizontal(mTextView.getSelectionStart()); + mSelectionBounds.set( + primaryHorizontal, + mTextView.getLayout().getLineTop(line), + primaryHorizontal + 1, + mTextView.getLayout().getLineTop(line + 1) + mInsertionHandleHeight); + } + // Take TextView's padding and scroll into account. + int textHorizontalOffset = mTextView.viewportToContentHorizontalOffset(); + int textVerticalOffset = mTextView.viewportToContentVerticalOffset(); + outRect.set( + (int) Math.floor(mSelectionBounds.left + textHorizontalOffset), + (int) Math.floor(mSelectionBounds.top + textVerticalOffset), + (int) Math.ceil(mSelectionBounds.right + textHorizontalOffset), + (int) Math.ceil(mSelectionBounds.bottom + textVerticalOffset)); + } } private void onReplace() { @@ -3066,97 +3117,6 @@ public class Editor { showSuggestions(); } - private class ActionPopupWindow extends PinnedPopupWindow implements OnClickListener { - private static final int POPUP_TEXT_LAYOUT = - com.android.internal.R.layout.text_edit_action_popup_text; - private TextView mPasteTextView; - private TextView mReplaceTextView; - - @Override - protected void createPopupWindow() { - mPopupWindow = new PopupWindow(mTextView.getContext(), null, - com.android.internal.R.attr.textSelectHandleWindowStyle); - mPopupWindow.setClippingEnabled(true); - } - - @Override - protected void initContentView() { - LinearLayout linearLayout = new LinearLayout(mTextView.getContext()); - linearLayout.setOrientation(LinearLayout.HORIZONTAL); - mContentView = linearLayout; - mContentView.setBackgroundResource( - com.android.internal.R.drawable.text_edit_paste_window); - - LayoutInflater inflater = (LayoutInflater) mTextView.getContext(). - getSystemService(Context.LAYOUT_INFLATER_SERVICE); - - LayoutParams wrapContent = new LayoutParams( - ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); - - mPasteTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null); - mPasteTextView.setLayoutParams(wrapContent); - mContentView.addView(mPasteTextView); - mPasteTextView.setText(com.android.internal.R.string.paste); - mPasteTextView.setOnClickListener(this); - - mReplaceTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null); - mReplaceTextView.setLayoutParams(wrapContent); - mContentView.addView(mReplaceTextView); - mReplaceTextView.setText(com.android.internal.R.string.replace); - mReplaceTextView.setOnClickListener(this); - } - - @Override - public void show() { - boolean canPaste = mTextView.canPaste(); - boolean canSuggest = mTextView.isSuggestionsEnabled() && isCursorInsideSuggestionSpan(); - mPasteTextView.setVisibility(canPaste ? View.VISIBLE : View.GONE); - mReplaceTextView.setVisibility(canSuggest ? View.VISIBLE : View.GONE); - - if (!canPaste && !canSuggest) return; - - super.show(); - } - - @Override - public void onClick(View view) { - if (view == mPasteTextView && mTextView.canPaste()) { - mTextView.onTextContextMenuItem(TextView.ID_PASTE); - hide(); - } else if (view == mReplaceTextView) { - onReplace(); - } - } - - @Override - protected int getTextOffset() { - return (mTextView.getSelectionStart() + mTextView.getSelectionEnd()) / 2; - } - - @Override - protected int getVerticalLocalPosition(int line) { - return mTextView.getLayout().getLineTop(line) - mContentView.getMeasuredHeight(); - } - - @Override - protected int clipVertically(int positionY) { - if (positionY < 0) { - final int offset = getTextOffset(); - final Layout layout = mTextView.getLayout(); - final int line = layout.getLineForOffset(offset); - positionY += layout.getLineBottom(line) - layout.getLineTop(line); - positionY += mContentView.getMeasuredHeight(); - - // Assumes insertion and selection handles share the same height - final Drawable handle = mTextView.getContext().getDrawable( - mTextView.mTextSelectHandleRes); - positionY += handle.getIntrinsicHeight(); - } - - return positionY; - } - } - /** * A listener to call {@link InputMethodManager#updateCursorAnchorInfo(View, CursorAnchorInfo)} * while the input method is requesting the cursor/anchor position. Does nothing as long as @@ -4102,7 +4062,6 @@ public class Editor { } class SelectionModifierCursorController implements CursorController { - private static final int DELAY_BEFORE_REPLACE_ACTION = 200; // milliseconds // The cursor controller handles, lazily created when shown. private SelectionStartHandleView mStartHandle; private SelectionEndHandleView mEndHandle; @@ -4664,8 +4623,8 @@ public class Editor { // Otherwise the user inserted the composition. String newText = TextUtils.substring(source, start, end); - EditOperation edit = new EditOperation(mEditor, false, "", dstart, newText); - recordEdit(edit); + EditOperation edit = new EditOperation(mEditor, "", dstart, newText); + recordEdit(edit, false /* forceMerge */); return true; } @@ -4684,11 +4643,15 @@ public class Editor { // Build a new operation with all the information from this edit. String newText = TextUtils.substring(source, start, end); String oldText = TextUtils.substring(dest, dstart, dend); - EditOperation edit = new EditOperation(mEditor, forceMerge, oldText, dstart, newText); - recordEdit(edit); + EditOperation edit = new EditOperation(mEditor, oldText, dstart, newText); + recordEdit(edit, forceMerge); } - private void recordEdit(EditOperation edit) { + /** + * Fetches the last undo operation and checks to see if a new edit should be merged into it. + * If forceMerge is true then the new edit is always merged. + */ + private void recordEdit(EditOperation edit, boolean forceMerge) { // Fetch the last edit operation and attempt to merge in the new edit. final UndoManager um = mEditor.mUndoManager; um.beginUpdate("Edit text"); @@ -4698,6 +4661,11 @@ public class Editor { // Add this as the first edit. if (DEBUG_UNDO) Log.d(TAG, "filter: adding first op " + edit); um.addOperation(edit, UndoManager.MERGE_MODE_NONE); + } else if (forceMerge) { + // Forced merges take priority because they could be the result of a non-user-edit + // change and this case should not create a new undo operation. + if (DEBUG_UNDO) Log.d(TAG, "filter: force merge " + edit); + lastEdit.forceMergeWith(edit); } else if (!mIsUserEdit) { // An application directly modified the Editable outside of a text edit. Treat this // as a new change and don't attempt to merge. @@ -4773,7 +4741,6 @@ public class Editor { private static final int TYPE_REPLACE = 2; private int mType; - private boolean mForceMerge; private String mOldText; private int mOldTextStart; private String mNewText; @@ -4784,13 +4751,10 @@ public class Editor { /** * Constructs an edit operation from a text input operation on editor that replaces the - * oldText starting at dstart with newText. If forceMerge is true then always forcibly - * merge this operation with any previous one. + * oldText starting at dstart with newText. */ - public EditOperation(Editor editor, boolean forceMerge, String oldText, int dstart, - String newText) { + public EditOperation(Editor editor, String oldText, int dstart, String newText) { super(editor.mUndoOwner); - mForceMerge = forceMerge; mOldText = oldText; mNewText = newText; @@ -4817,7 +4781,6 @@ public class Editor { public EditOperation(Parcel src, ClassLoader loader) { super(src, loader); mType = src.readInt(); - mForceMerge = src.readInt() != 0; mOldText = src.readString(); mOldTextStart = src.readInt(); mNewText = src.readString(); @@ -4829,7 +4792,6 @@ public class Editor { @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(mType); - dest.writeInt(mForceMerge ? 1 : 0); dest.writeString(mOldText); dest.writeInt(mOldTextStart); dest.writeString(mNewText); @@ -4881,10 +4843,6 @@ public class Editor { Log.d(TAG, "mergeWith old " + this); Log.d(TAG, "mergeWith new " + edit); } - if (edit.mForceMerge) { - forceMergeWith(edit); - return true; - } switch (mType) { case TYPE_INSERT: return mergeInsertWith(edit); @@ -4942,7 +4900,7 @@ public class Editor { * Forcibly creates a single merged edit operation by simulating the entire text * contents being replaced. */ - private void forceMergeWith(EditOperation edit) { + public void forceMergeWith(EditOperation edit) { if (DEBUG_UNDO) Log.d(TAG, "forceMerge"); Editor editor = getOwnerData(); diff --git a/core/java/android/widget/RadialTimePickerView.java b/core/java/android/widget/RadialTimePickerView.java index 20aa972..143dea4 100644 --- a/core/java/android/widget/RadialTimePickerView.java +++ b/core/java/android/widget/RadialTimePickerView.java @@ -122,8 +122,9 @@ public class RadialTimePickerView extends View { private final Paint mPaintCenter = new Paint(); private final Paint[][] mPaintSelector = new Paint[2][3]; - private final int[][] mColorSelector = new int[2][3]; - private final IntHolder[][] mAlphaSelector = new IntHolder[2][3]; + + private final int mSelectorColor; + private final int mSelectorDotColor; private final Paint mPaintBackground = new Paint(); @@ -147,6 +148,8 @@ public class RadialTimePickerView extends View { private final RadialPickerTouchHelper mTouchHelper; + private final Path mSelectorPath = new Path(); + private boolean mIs24HourMode; private boolean mShowHours; @@ -316,11 +319,6 @@ public class RadialTimePickerView extends View { for (int i = 0; i < mAlpha.length; i++) { mAlpha[i] = new IntHolder(ALPHA_OPAQUE); } - for (int i = 0; i < mAlphaSelector.length; i++) { - for (int j = 0; j < mAlphaSelector[i].length; j++) { - mAlphaSelector[i][j] = new IntHolder(ALPHA_OPAQUE); - } - } mTextColor[HOURS] = a.getColorStateList(R.styleable.TimePicker_numbersTextColor); mTextColor[HOURS_INNER] = a.getColorStateList(R.styleable.TimePicker_numbersInnerTextColor); @@ -345,33 +343,28 @@ public class RadialTimePickerView extends View { final int[] activatedStateSet = StateSet.get( StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_ACTIVATED); + mSelectorColor = selectorActivatedColor; + mSelectorDotColor = mTextColor[HOURS].getColorForState(activatedStateSet, 0); + mPaintSelector[HOURS][SELECTOR_CIRCLE] = new Paint(); mPaintSelector[HOURS][SELECTOR_CIRCLE].setAntiAlias(true); - mColorSelector[HOURS][SELECTOR_CIRCLE] = selectorActivatedColor; mPaintSelector[HOURS][SELECTOR_DOT] = new Paint(); mPaintSelector[HOURS][SELECTOR_DOT].setAntiAlias(true); - mColorSelector[HOURS][SELECTOR_DOT] = - mTextColor[HOURS].getColorForState(activatedStateSet, 0); mPaintSelector[HOURS][SELECTOR_LINE] = new Paint(); mPaintSelector[HOURS][SELECTOR_LINE].setAntiAlias(true); mPaintSelector[HOURS][SELECTOR_LINE].setStrokeWidth(2); - mColorSelector[HOURS][SELECTOR_LINE] = selectorActivatedColor; mPaintSelector[MINUTES][SELECTOR_CIRCLE] = new Paint(); mPaintSelector[MINUTES][SELECTOR_CIRCLE].setAntiAlias(true); - mColorSelector[MINUTES][SELECTOR_CIRCLE] = selectorActivatedColor; mPaintSelector[MINUTES][SELECTOR_DOT] = new Paint(); mPaintSelector[MINUTES][SELECTOR_DOT].setAntiAlias(true); - mColorSelector[MINUTES][SELECTOR_DOT] = - mTextColor[MINUTES].getColorForState(activatedStateSet, 0); mPaintSelector[MINUTES][SELECTOR_LINE] = new Paint(); mPaintSelector[MINUTES][SELECTOR_LINE].setAntiAlias(true); mPaintSelector[MINUTES][SELECTOR_LINE].setStrokeWidth(2); - mColorSelector[MINUTES][SELECTOR_LINE] = selectorActivatedColor; mPaintBackground.setColor(a.getColor(R.styleable.TimePicker_numbersBackgroundColor, context.getColor(R.color.timepicker_default_numbers_background_color_material))); @@ -600,8 +593,8 @@ public class RadialTimePickerView extends View { // Initialize the hours and minutes numbers. for (int i = 0; i < 12; i++) { mHours12Texts[i] = String.format("%d", HOURS_NUMBERS[i]); - mOuterHours24Texts[i] = String.format("%02d", HOURS_NUMBERS_24[i]); - mInnerHours24Texts[i] = String.format("%d", HOURS_NUMBERS[i]); + mInnerHours24Texts[i] = String.format("%02d", HOURS_NUMBERS_24[i]); + mOuterHours24Texts[i] = String.format("%d", HOURS_NUMBERS[i]); mMinutesTexts[i] = String.format("%02d", MINUTES_NUMBERS[i]); } } @@ -612,22 +605,16 @@ public class RadialTimePickerView extends View { mInnerTextHours = mInnerHours24Texts; } else { mOuterTextHours = mHours12Texts; - mInnerTextHours = null; + mInnerTextHours = mHours12Texts; } mOuterTextMinutes = mMinutesTexts; final int hoursAlpha = mShowHours ? ALPHA_OPAQUE : ALPHA_TRANSPARENT; mAlpha[HOURS].setValue(hoursAlpha); - mAlphaSelector[HOURS][SELECTOR_CIRCLE].setValue(hoursAlpha); - mAlphaSelector[HOURS][SELECTOR_DOT].setValue(hoursAlpha); - mAlphaSelector[HOURS][SELECTOR_LINE].setValue(hoursAlpha); final int minutesAlpha = mShowHours ? ALPHA_TRANSPARENT : ALPHA_OPAQUE; mAlpha[MINUTES].setValue(minutesAlpha); - mAlphaSelector[MINUTES][SELECTOR_CIRCLE].setValue(minutesAlpha); - mAlphaSelector[MINUTES][SELECTOR_DOT].setValue(minutesAlpha); - mAlphaSelector[MINUTES][SELECTOR_LINE].setValue(minutesAlpha); } @Override @@ -675,7 +662,7 @@ public class RadialTimePickerView extends View { mOuterTextHours, mOuterTextX[HOURS], mOuterTextY[HOURS], mPaint[HOURS], hoursAlpha, !mIsOnInnerCircle, mSelectionDegrees[HOURS], false); - // Draw inner hours (12-23) for 24-hour time. + // Draw inner hours (13-00) for 24-hour time. if (mIs24HourMode && mInnerTextHours != null) { drawTextElements(canvas, mTextSize[HOURS_INNER], mTypeface, mTextColor[HOURS_INNER], mInnerTextHours, mInnerTextX, mInnerTextY, mPaint[HOURS], hoursAlpha, @@ -714,69 +701,61 @@ public class RadialTimePickerView extends View { canvas.drawCircle(mXCenter, mYCenter, mCenterDotRadius, mPaintCenter); } + private int applyAlpha(int argb, int alpha) { + final int srcAlpha = (argb >> 24) & 0xFF; + final int dstAlpha = (int) (srcAlpha * (alpha / 255.0) + 0.5f); + return (0xFFFFFF & argb) | (dstAlpha << 24); + } + private int getMultipliedAlpha(int argb, int alpha) { return (int) (Color.alpha(argb) * (alpha / 255.0) + 0.5); } - private final Path mSelectorPath = new Path(); - private void drawSelector(Canvas canvas, int index, Path selectorPath, float alphaMod) { - // Calculate the current radius at which to place the selection circle. - mLineLength[index] = mCircleRadius - mTextInset[index]; - - final double selectionRadians = Math.toRadians(mSelectionDegrees[index]); - - float pointX = mXCenter + (int) (mLineLength[index] * Math.sin(selectionRadians)); - float pointY = mYCenter - (int) (mLineLength[index] * Math.cos(selectionRadians)); + final int alpha = (int) (mAlpha[index % 2].getValue() * alphaMod + 0.5f); + final int color = applyAlpha(mSelectorColor, alpha); - int color; - int alpha; - Paint paint; - - // Draw the selection circle - color = mColorSelector[index % 2][SELECTOR_CIRCLE]; - alpha = (int) (mAlphaSelector[index % 2][SELECTOR_CIRCLE].getValue() * alphaMod + 0.5f); - paint = mPaintSelector[index % 2][SELECTOR_CIRCLE]; + // Calculate the current radius at which to place the selection circle. + final int selRadius = mSelectorRadius; + final int selLength = mCircleRadius - mTextInset[index]; + final double selAngleRad = Math.toRadians(mSelectionDegrees[index]); + final float selCenterX = mXCenter + selLength * (float) Math.sin(selAngleRad); + final float selCenterY = mYCenter - selLength * (float) Math.cos(selAngleRad); + + // Draw the selection circle. + final Paint paint = mPaintSelector[index % 2][SELECTOR_CIRCLE]; paint.setColor(color); - paint.setAlpha(getMultipliedAlpha(color, alpha)); - canvas.drawCircle(pointX, pointY, mSelectorRadius, paint); + canvas.drawCircle(selCenterX, selCenterY, selRadius, paint); // If needed, set up the clip path for later. if (selectorPath != null) { - mSelectorPath.reset(); - mSelectorPath.addCircle(pointX, pointY, mSelectorRadius, Path.Direction.CCW); + selectorPath.reset(); + selectorPath.addCircle(selCenterX, selCenterY, selRadius, Path.Direction.CCW); } - // Draw the dot if needed. + // Draw the dot if we're between two items. final boolean shouldDrawDot = mSelectionDegrees[index] % 30 != 0; if (shouldDrawDot) { - // We're not on a direct tick - color = mColorSelector[index % 2][SELECTOR_DOT]; - alpha = (int) (mAlphaSelector[index % 2][SELECTOR_DOT].getValue() * alphaMod + 0.5f); - paint = mPaintSelector[index % 2][SELECTOR_DOT]; - paint.setColor(color); - paint.setAlpha(getMultipliedAlpha(color, alpha)); - canvas.drawCircle(pointX, pointY, mSelectorDotRadius, paint); + final Paint dotPaint = mPaintSelector[index % 2][SELECTOR_DOT]; + dotPaint.setColor(color); + canvas.drawCircle(selCenterX, selCenterY, mSelectorDotRadius, dotPaint); } // Shorten the line to only go from the edge of the center dot to the // edge of the selection circle. - final double sin = Math.sin(selectionRadians); - final double cos = Math.cos(selectionRadians); - final int lineLength = mLineLength[index] - mSelectorRadius; + final double sin = Math.sin(selAngleRad); + final double cos = Math.cos(selAngleRad); + final int lineLength = selLength - selRadius; final int centerX = mXCenter + (int) (mCenterDotRadius * sin); final int centerY = mYCenter - (int) (mCenterDotRadius * cos); - pointX = centerX + (int) (lineLength * sin); - pointY = centerY - (int) (lineLength * cos); - - // Draw the line - color = mColorSelector[index % 2][SELECTOR_LINE]; - alpha = (int) (mAlphaSelector[index % 2][SELECTOR_LINE].getValue() * alphaMod + 0.5f); - paint = mPaintSelector[index % 2][SELECTOR_LINE]; - paint.setColor(color); - paint.setStrokeWidth(mSelectorStroke); - paint.setAlpha(getMultipliedAlpha(color, alpha)); - canvas.drawLine(mXCenter, mYCenter, pointX, pointY, paint); + final float linePointX = centerX + (int) (lineLength * sin); + final float linePointY = centerY - (int) (lineLength * cos); + + // Draw the line. + final Paint linePaint = mPaintSelector[index % 2][SELECTOR_LINE]; + linePaint.setColor(color); + linePaint.setStrokeWidth(mSelectorStroke); + canvas.drawLine(mXCenter, mYCenter, linePointX, linePointY, linePaint); } private void calculatePositionsHours() { @@ -890,21 +869,8 @@ public class RadialTimePickerView extends View { if (mHoursToMinutesAnims.size() == 0) { mHoursToMinutesAnims.add(getFadeOutAnimator(mAlpha[HOURS], ALPHA_OPAQUE, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); - mHoursToMinutesAnims.add(getFadeOutAnimator(mAlphaSelector[HOURS][SELECTOR_CIRCLE], - ALPHA_OPAQUE, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); - mHoursToMinutesAnims.add(getFadeOutAnimator(mAlphaSelector[HOURS][SELECTOR_DOT], - ALPHA_OPAQUE, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); - mHoursToMinutesAnims.add(getFadeOutAnimator(mAlphaSelector[HOURS][SELECTOR_LINE], - ALPHA_OPAQUE, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); - mHoursToMinutesAnims.add(getFadeInAnimator(mAlpha[MINUTES], ALPHA_TRANSPARENT, ALPHA_OPAQUE, mInvalidateUpdateListener)); - mHoursToMinutesAnims.add(getFadeInAnimator(mAlphaSelector[MINUTES][SELECTOR_CIRCLE], - ALPHA_TRANSPARENT, ALPHA_OPAQUE, mInvalidateUpdateListener)); - mHoursToMinutesAnims.add(getFadeInAnimator(mAlphaSelector[MINUTES][SELECTOR_DOT], - ALPHA_TRANSPARENT, ALPHA_OPAQUE, mInvalidateUpdateListener)); - mHoursToMinutesAnims.add(getFadeInAnimator(mAlphaSelector[MINUTES][SELECTOR_LINE], - ALPHA_TRANSPARENT, ALPHA_OPAQUE, mInvalidateUpdateListener)); } if (mTransition != null && mTransition.isRunning()) { @@ -919,21 +885,8 @@ public class RadialTimePickerView extends View { if (mMinuteToHoursAnims.size() == 0) { mMinuteToHoursAnims.add(getFadeOutAnimator(mAlpha[MINUTES], ALPHA_OPAQUE, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); - mMinuteToHoursAnims.add(getFadeOutAnimator(mAlphaSelector[MINUTES][SELECTOR_CIRCLE], - ALPHA_OPAQUE, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); - mMinuteToHoursAnims.add(getFadeOutAnimator(mAlphaSelector[MINUTES][SELECTOR_DOT], - ALPHA_OPAQUE, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); - mMinuteToHoursAnims.add(getFadeOutAnimator(mAlphaSelector[MINUTES][SELECTOR_LINE], - ALPHA_OPAQUE, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); - mMinuteToHoursAnims.add(getFadeInAnimator(mAlpha[HOURS], ALPHA_TRANSPARENT, ALPHA_OPAQUE, mInvalidateUpdateListener)); - mMinuteToHoursAnims.add(getFadeInAnimator(mAlphaSelector[HOURS][SELECTOR_CIRCLE], - ALPHA_TRANSPARENT, ALPHA_OPAQUE, mInvalidateUpdateListener)); - mMinuteToHoursAnims.add(getFadeInAnimator(mAlphaSelector[HOURS][SELECTOR_DOT], - ALPHA_TRANSPARENT, ALPHA_OPAQUE, mInvalidateUpdateListener)); - mMinuteToHoursAnims.add(getFadeInAnimator(mAlphaSelector[HOURS][SELECTOR_LINE], - ALPHA_TRANSPARENT, ALPHA_OPAQUE, mInvalidateUpdateListener)); } if (mTransition != null && mTransition.isRunning()) { diff --git a/core/java/android/widget/SimpleMonthView.java b/core/java/android/widget/SimpleMonthView.java index 4e5a39a..3fb096c 100644 --- a/core/java/android/widget/SimpleMonthView.java +++ b/core/java/android/widget/SimpleMonthView.java @@ -26,6 +26,7 @@ import android.graphics.Paint.Align; import android.graphics.Paint.Style; import android.graphics.Rect; import android.graphics.Typeface; +import android.graphics.drawable.Drawable; import android.os.Bundle; import android.text.TextPaint; import android.text.format.DateFormat; @@ -59,6 +60,12 @@ class SimpleMonthView extends View { private static final String DEFAULT_TITLE_FORMAT = "MMMMy"; private static final String DAY_OF_WEEK_FORMAT = "EEEEE"; + /** Virtual view ID for previous button. */ + private static final int ITEM_ID_PREV = 0x101; + + /** Virtual view ID for next button. */ + private static final int ITEM_ID_NEXT = 0x100; + private final TextPaint mMonthPaint = new TextPaint(); private final TextPaint mDayOfWeekPaint = new TextPaint(); private final TextPaint mDayPaint = new TextPaint(); @@ -66,13 +73,27 @@ class SimpleMonthView extends View { private final Paint mDayHighlightPaint = new Paint(); private final Calendar mCalendar = Calendar.getInstance(); - private final Calendar mDayLabelCalendar = Calendar.getInstance(); + private final Calendar mDayOfWeekLabelCalendar = Calendar.getInstance(); private final MonthViewTouchHelper mTouchHelper; private final SimpleDateFormat mTitleFormatter; private final SimpleDateFormat mDayOfWeekFormatter; + private final int mMonthHeight; + private final int mDayOfWeekHeight; + private final int mDayHeight; + private final int mCellWidth; + private final int mDaySelectorRadius; + + // Next/previous drawables. + private final Drawable mPrevDrawable; + private final Drawable mNextDrawable; + private final Rect mPrevHitArea; + private final Rect mNextHitArea; + private final CharSequence mPrevContentDesc; + private final CharSequence mNextContentDesc; + private CharSequence mTitle; private int mMonth; @@ -81,12 +102,6 @@ class SimpleMonthView extends View { private int mPaddedWidth; private int mPaddedHeight; - private final int mMonthHeight; - private final int mDayOfWeekHeight; - private final int mDayHeight; - private final int mCellWidth; - private final int mDaySelectorRadius; - /** The day of month for the selected day, or -1 if no day is selected. */ private int mActivatedDay = -1; @@ -122,7 +137,10 @@ class SimpleMonthView extends View { private ColorStateList mDayTextColor; - private int mTouchedDay = -1; + private int mTouchedItem = -1; + + private boolean mPrevEnabled; + private boolean mNextEnabled; public SimpleMonthView(Context context) { this(context, null); @@ -146,6 +164,13 @@ class SimpleMonthView extends View { mCellWidth = res.getDimensionPixelSize(R.dimen.date_picker_day_width); mDaySelectorRadius = res.getDimensionPixelSize(R.dimen.date_picker_day_selector_radius); + mPrevDrawable = context.getDrawable(R.drawable.ic_chevron_left); + mNextDrawable = context.getDrawable(R.drawable.ic_chevron_right); + mPrevHitArea = mPrevDrawable != null ? new Rect() : null; + mNextHitArea = mNextDrawable != null ? new Rect() : null; + mPrevContentDesc = res.getText(R.string.date_picker_prev_month_button); + mNextContentDesc = res.getText(R.string.date_picker_next_month_button); + // Set up accessibility components. mTouchHelper = new MonthViewTouchHelper(this); setAccessibilityDelegate(mTouchHelper); @@ -160,6 +185,18 @@ class SimpleMonthView extends View { initPaints(res); } + public void setNextEnabled(boolean enabled) { + mNextEnabled = enabled; + mTouchHelper.invalidateRoot(); + invalidate(); + } + + public void setPrevEnabled(boolean enabled) { + mPrevEnabled = enabled; + mTouchHelper.invalidateRoot(); + invalidate(); + } + /** * Applies the specified text appearance resource to a paint, returning the * text color if one is set in the text appearance. @@ -192,7 +229,16 @@ class SimpleMonthView extends View { } public void setMonthTextAppearance(int resId) { - applyTextAppearance(mMonthPaint, resId); + final ColorStateList monthColor = applyTextAppearance(mMonthPaint, resId); + if (monthColor != null) { + if (mPrevDrawable != null) { + mPrevDrawable.setTintList(monthColor); + } + if (mNextDrawable != null) { + mNextDrawable.setTintList(monthColor); + } + } + invalidate(); } @@ -300,23 +346,26 @@ class SimpleMonthView extends View { @Override public boolean onTouchEvent(MotionEvent event) { + final int x = (int) (event.getX() + 0.5f); + final int y = (int) (event.getY() + 0.5f); + switch (event.getAction()) { case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_MOVE: - final int touchedDay = getDayAtLocation(event.getX(), event.getY()); - if (mTouchedDay != touchedDay) { - mTouchedDay = touchedDay; + final int touchedItem = getItemAtLocation(x, y); + if (mTouchedItem != touchedItem) { + mTouchedItem = touchedItem; invalidate(); } break; case MotionEvent.ACTION_UP: - final int clickedDay = getDayAtLocation(event.getX(), event.getY()); - onDayClicked(clickedDay); + final int clickedItem = getItemAtLocation(x, y); + onItemClicked(clickedItem, true); // Fall through. case MotionEvent.ACTION_CANCEL: // Reset touched day on stream end. - mTouchedDay = -1; + mTouchedItem = -1; invalidate(); break; } @@ -332,6 +381,7 @@ class SimpleMonthView extends View { drawMonth(canvas); drawDaysOfWeek(canvas); drawDays(canvas); + drawButtons(canvas); canvas.translate(-paddingLeft, -paddingTop); } @@ -347,34 +397,43 @@ class SimpleMonthView extends View { } private void drawDaysOfWeek(Canvas canvas) { - final float cellWidthHalf = mPaddedWidth / (DAYS_IN_WEEK * 2); - - // Vertically centered within the cell height. - final float lineHeight = mDayOfWeekPaint.ascent() + mDayOfWeekPaint.descent(); - final float y = mMonthHeight + (mDayOfWeekHeight - lineHeight) / 2f; - - for (int i = 0; i < DAYS_IN_WEEK; i++) { - final int calendarDay = (i + mWeekStart) % DAYS_IN_WEEK; - mDayLabelCalendar.set(Calendar.DAY_OF_WEEK, calendarDay); - - final String dayLabel = mDayOfWeekFormatter.format(mDayLabelCalendar.getTime()); - final float x = (2 * i + 1) * cellWidthHalf; - canvas.drawText(dayLabel, x, y, mDayOfWeekPaint); + final TextPaint p = mDayOfWeekPaint; + final int headerHeight = mMonthHeight; + final int rowHeight = mDayOfWeekHeight; + final int colWidth = mPaddedWidth / DAYS_IN_WEEK; + + // Text is vertically centered within the day of week height. + final float halfLineHeight = (p.ascent() + p.descent()) / 2f; + final int rowCenter = headerHeight + rowHeight / 2; + + for (int col = 0; col < DAYS_IN_WEEK; col++) { + final int colCenter = colWidth * col + colWidth / 2; + final int dayOfWeek = (col + mWeekStart) % DAYS_IN_WEEK; + final String label = getDayOfWeekLabel(dayOfWeek); + canvas.drawText(label, colCenter, rowCenter - halfLineHeight, p); } } + private String getDayOfWeekLabel(int dayOfWeek) { + mDayOfWeekLabelCalendar.set(Calendar.DAY_OF_WEEK, dayOfWeek); + return mDayOfWeekFormatter.format(mDayOfWeekLabelCalendar.getTime()); + } + /** * Draws the month days. */ private void drawDays(Canvas canvas) { - final int cellWidthHalf = mPaddedWidth / (DAYS_IN_WEEK * 2); + final TextPaint p = mDayPaint; + final int headerHeight = mMonthHeight + mDayOfWeekHeight; + final int rowHeight = mDayHeight; + final int colWidth = mPaddedWidth / DAYS_IN_WEEK; - // Vertically centered within the cell height. - final float halfLineHeight = (mDayPaint.ascent() + mDayPaint.descent()) / 2; - float centerY = mMonthHeight + mDayOfWeekHeight + mDayHeight / 2f; + // Text is vertically centered within the row height. + final float halfLineHeight = (p.ascent() + p.descent()) / 2f; + int rowCenter = headerHeight + rowHeight / 2; - for (int day = 1, j = findDayOffset(); day <= mDaysInMonth; day++) { - final int x = (2 * j + 1) * cellWidthHalf; + for (int day = 1, col = findDayOffset(); day <= mDaysInMonth; day++) { + final int colCenter = colWidth * col + colWidth / 2; int stateMask = 0; if (day >= mEnabledDayStart && day <= mEnabledDayEnd) { @@ -386,12 +445,12 @@ class SimpleMonthView extends View { stateMask |= StateSet.VIEW_STATE_ACTIVATED; // Adjust the circle to be centered on the row. - canvas.drawCircle(x, centerY, mDaySelectorRadius, mDaySelectorPaint); - } else if (mTouchedDay == day) { + canvas.drawCircle(colCenter, rowCenter, mDaySelectorRadius, mDaySelectorPaint); + } else if (mTouchedItem == day) { stateMask |= StateSet.VIEW_STATE_PRESSED; // Adjust the circle to be centered on the row. - canvas.drawCircle(x, centerY, mDaySelectorRadius, mDayHighlightPaint); + canvas.drawCircle(colCenter, rowCenter, mDaySelectorRadius, mDayHighlightPaint); } final boolean isDayToday = mToday == day; @@ -402,19 +461,29 @@ class SimpleMonthView extends View { final int[] stateSet = StateSet.get(stateMask); dayTextColor = mDayTextColor.getColorForState(stateSet, 0); } - mDayPaint.setColor(dayTextColor); + p.setColor(dayTextColor); - canvas.drawText("" + day, x, centerY - halfLineHeight, mDayPaint); + canvas.drawText(Integer.toString(day), colCenter, rowCenter - halfLineHeight, p); - j++; + col++; - if (j == DAYS_IN_WEEK) { - j = 0; - centerY += mDayHeight; + if (col == DAYS_IN_WEEK) { + col = 0; + rowCenter += rowHeight; } } } + private void drawButtons(Canvas canvas) { + if (mPrevEnabled && mPrevDrawable != null) { + mPrevDrawable.draw(canvas); + } + + if (mNextEnabled && mNextDrawable != null) { + mNextDrawable.draw(canvas); + } + } + private static boolean isValidDayOfWeek(int day) { return day >= Calendar.SUNDAY && day <= Calendar.SATURDAY; } @@ -569,8 +638,42 @@ class SimpleMonthView extends View { @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { - mPaddedWidth = w - getPaddingLeft() - getPaddingRight(); - mPaddedHeight = w - getPaddingTop() - getPaddingBottom(); + final int paddedLeft = getPaddingLeft(); + final int paddedTop = getPaddingTop(); + final int paddedRight = w - getPaddingRight(); + final int paddedBottom = h - getPaddingBottom(); + mPaddedWidth = paddedRight - paddedLeft; + mPaddedHeight = paddedBottom - paddedTop; + + final int monthHeight = mMonthHeight; + final int cellWidth = mPaddedWidth / DAYS_IN_WEEK; + + // Vertically center the previous/next drawables within the month + // header, horizontally center within the day cell, then expand the + // hit area to ensure it's at least 48x48dp. + final Drawable prevDrawable = mPrevDrawable; + if (prevDrawable != null) { + final int dW = prevDrawable.getIntrinsicWidth(); + final int dH = prevDrawable.getIntrinsicHeight(); + final int iconTop = (monthHeight - dH) / 2; + final int iconLeft = (cellWidth - dW) / 2; + + // Button bounds don't include padding, but hit area does. + prevDrawable.setBounds(iconLeft, iconTop, iconLeft + dW, iconTop + dH); + mPrevHitArea.set(0, 0, paddedLeft + cellWidth, paddedTop + monthHeight); + } + + final Drawable nextDrawable = mNextDrawable; + if (nextDrawable != null) { + final int dW = nextDrawable.getIntrinsicWidth(); + final int dH = nextDrawable.getIntrinsicHeight(); + final int iconTop = (monthHeight - dH) / 2; + final int iconRight = mPaddedWidth - (cellWidth - dW) / 2; + + // Button bounds don't include padding, but hit area does. + nextDrawable.setBounds(iconRight - dW, iconTop, iconRight, iconTop + dH); + mNextHitArea.set(paddedRight - cellWidth, 0, w, paddedTop + monthHeight); + } // Invalidate cached accessibility information. mTouchHelper.invalidateRoot(); @@ -585,22 +688,29 @@ class SimpleMonthView extends View { } /** - * Calculates the day of the month at the specified touch position. Returns - * the day of the month or -1 if the position wasn't in a valid day. + * Calculates the day of the month or item identifier at the specified + * touch position. Returns the day of the month or -1 if the position + * wasn't in a valid day. * * @param x the x position of the touch event * @param y the y position of the touch event - * @return the day of the month at (x, y) or -1 if the position wasn't in a - * valid day + * @return the day of the month at (x, y), an item identifier, or -1 if the + * position wasn't in a valid day or item */ - private int getDayAtLocation(float x, float y) { - final int paddedX = (int) (x - getPaddingLeft() + 0.5f); + private int getItemAtLocation(int x, int y) { + if (mNextEnabled && mNextDrawable != null && mNextHitArea.contains(x, y)) { + return ITEM_ID_NEXT; + } else if (mPrevEnabled && mPrevDrawable != null && mPrevHitArea.contains(x, y)) { + return ITEM_ID_PREV; + } + + final int paddedX = x - getPaddingLeft(); if (paddedX < 0 || paddedX >= mPaddedWidth) { return -1; } final int headerHeight = mMonthHeight + mDayOfWeekHeight; - final int paddedY = (int) (y - getPaddingTop() + 0.5f); + final int paddedY = y - getPaddingTop(); if (paddedY < headerHeight || paddedY >= mPaddedHeight) { return -1; } @@ -619,47 +729,97 @@ class SimpleMonthView extends View { /** * Calculates the bounds of the specified day. * - * @param day the day of the month + * @param id the day of the month, or an item identifier * @param outBounds the rect to populate with bounds */ - private boolean getBoundsForDay(int day, Rect outBounds) { - if (day < 1 || day > mDaysInMonth) { + private boolean getBoundsForItem(int id, Rect outBounds) { + if (mNextEnabled && id == ITEM_ID_NEXT) { + if (mNextDrawable != null) { + outBounds.set(mNextHitArea); + return true; + } + } else if (mPrevEnabled && id == ITEM_ID_PREV) { + if (mPrevDrawable != null) { + outBounds.set(mPrevHitArea); + return true; + } + } + + if (id < 1 || id > mDaysInMonth) { return false; } - final int index = day - 1 + findDayOffset(); - final int row = index / DAYS_IN_WEEK; + final int index = id - 1 + findDayOffset(); + + // Compute left edge. final int col = index % DAYS_IN_WEEK; + final int colWidth = mPaddedWidth / DAYS_IN_WEEK; + final int left = getPaddingLeft() + col * colWidth; + // Compute top edge. + final int row = index / DAYS_IN_WEEK; + final int rowHeight = mDayHeight; final int headerHeight = mMonthHeight + mDayOfWeekHeight; - final int paddedY = row * mDayHeight + headerHeight; - final int paddedX = col * mPaddedWidth; - - final int y = paddedY + getPaddingTop(); - final int x = paddedX + getPaddingLeft(); - - final int cellHeight = mDayHeight; - final int cellWidth = mPaddedWidth / DAYS_IN_WEEK; - outBounds.set(x, y, (x + cellWidth), (y + cellHeight)); + final int top = getPaddingTop() + headerHeight + row * rowHeight; + outBounds.set(left, top, left + colWidth, top + rowHeight); return true; } /** + * Called when an item is clicked. + * + * @param id the day number or item identifier + */ + private boolean onItemClicked(int id, boolean animate) { + return onNavigationClicked(id, animate) || onDayClicked(id); + } + + /** * Called when the user clicks on a day. Handles callbacks to the * {@link OnDayClickListener} if one is set. * * @param day the day that was clicked */ - private void onDayClicked(int day) { + private boolean onDayClicked(int day) { + if (day < 0 || day > mDaysInMonth) { + return false; + } + if (mOnDayClickListener != null) { - Calendar date = Calendar.getInstance(); + final Calendar date = Calendar.getInstance(); date.set(mYear, mMonth, day); mOnDayClickListener.onDayClick(this, date); } // This is a no-op if accessibility is turned off. mTouchHelper.sendEventForVirtualView(day, AccessibilityEvent.TYPE_VIEW_CLICKED); + return true; + } + + /** + * Called when the user clicks on a navigation button. Handles callbacks to + * the {@link OnDayClickListener} if one is set. + * + * @param id the item identifier + */ + private boolean onNavigationClicked(int id, boolean animate) { + final int direction; + if (id == ITEM_ID_NEXT) { + direction = 1; + } else if (id == ITEM_ID_PREV) { + direction = -1; + } else { + return false; + } + + if (mOnDayClickListener != null) { + mOnDayClickListener.onNavigationClick(this, direction, animate); + } + + // This is a no-op if accessibility is turned off. + mTouchHelper.sendEventForVirtualView(id, AccessibilityEvent.TYPE_VIEW_CLICKED); + return true; } /** @@ -678,7 +838,7 @@ class SimpleMonthView extends View { @Override protected int getVirtualViewAt(float x, float y) { - final int day = getDayAtLocation(x, y); + final int day = getItemAtLocation((int) (x + 0.5f), (int) (y + 0.5f)); if (day >= 0) { return day; } @@ -687,6 +847,14 @@ class SimpleMonthView extends View { @Override protected void getVisibleVirtualViews(IntArray virtualViewIds) { + if (mNextEnabled && mNextDrawable != null) { + virtualViewIds.add(ITEM_ID_PREV); + } + + if (mPrevEnabled && mPrevDrawable != null) { + virtualViewIds.add(ITEM_ID_NEXT); + } + for (int day = 1; day <= mDaysInMonth; day++) { virtualViewIds.add(day); } @@ -699,7 +867,7 @@ class SimpleMonthView extends View { @Override protected void onPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfo node) { - final boolean hasBounds = getBoundsForDay(virtualViewId, mTempRect); + final boolean hasBounds = getBoundsForItem(virtualViewId, mTempRect); if (!hasBounds) { // The day is invalid, kill the node. @@ -710,12 +878,14 @@ class SimpleMonthView extends View { return; } + node.setText(getItemText(virtualViewId)); node.setContentDescription(getItemDescription(virtualViewId)); node.setBoundsInParent(mTempRect); node.addAction(AccessibilityAction.ACTION_CLICK); if (virtualViewId == mActivatedDay) { - node.setSelected(true); + // TODO: This should use activated once that's supported. + node.setChecked(true); } } @@ -725,31 +895,45 @@ class SimpleMonthView extends View { Bundle arguments) { switch (action) { case AccessibilityNodeInfo.ACTION_CLICK: - onDayClicked(virtualViewId); - return true; + return onItemClicked(virtualViewId, false); } return false; } /** - * Generates a description for a given time object. Since this - * description will be spoken, the components are ordered by descending - * specificity as DAY MONTH YEAR. + * Generates a description for a given virtual view. * - * @param day The day to generate a description for - * @return A description of the time object + * @param id the day or item identifier to generate a description for + * @return a description of the virtual view */ - private CharSequence getItemDescription(int day) { - mTempCalendar.set(mYear, mMonth, day); - final CharSequence date = DateFormat.format(DATE_FORMAT, - mTempCalendar.getTimeInMillis()); + private CharSequence getItemDescription(int id) { + if (id == ITEM_ID_NEXT) { + return mNextContentDesc; + } else if (id == ITEM_ID_PREV) { + return mPrevContentDesc; + } else if (id >= 1 && id <= mDaysInMonth) { + mTempCalendar.set(mYear, mMonth, id); + return DateFormat.format(DATE_FORMAT, mTempCalendar.getTimeInMillis()); + } - if (day == mActivatedDay) { - return getContext().getString(R.string.item_is_selected, date); + return ""; + } + + /** + * Generates displayed text for a given virtual view. + * + * @param id the day or item identifier to generate text for + * @return the visible text of the virtual view + */ + private CharSequence getItemText(int id) { + if (id == ITEM_ID_NEXT || id == ITEM_ID_PREV) { + return null; + } else if (id >= 1 && id <= mDaysInMonth) { + return Integer.toString(id); } - return date; + return null; } } @@ -758,5 +942,6 @@ class SimpleMonthView extends View { */ public interface OnDayClickListener { public void onDayClick(SimpleMonthView view, Calendar day); + public void onNavigationClick(SimpleMonthView view, int direction, boolean animate); } } diff --git a/core/java/android/widget/TimePicker.java b/core/java/android/widget/TimePicker.java index 944b491..986c0f8 100644 --- a/core/java/android/widget/TimePicker.java +++ b/core/java/android/widget/TimePicker.java @@ -16,6 +16,7 @@ package android.widget; +import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.Widget; import android.content.Context; @@ -29,18 +30,13 @@ import com.android.internal.R; import java.util.Locale; /** - * A view for selecting the time of day, in either 24 hour or AM/PM mode. The - * hour, each minute digit, and AM/PM (if applicable) can be conrolled by - * vertical spinners. The hour can be entered by keyboard input. Entering in two - * digit hours can be accomplished by hitting two digits within a timeout of - * about a second (e.g. '1' then '2' to select 12). The minutes can be entered - * by entering single digits. Under AM/PM mode, the user can hit 'a', 'A", 'p' - * or 'P' to pick. For a dialog using this view, see - * {@link android.app.TimePickerDialog}. + * A widget for selecting the time of day, in either 24-hour or AM/PM mode. * <p> - * See the <a href="{@docRoot}guide/topics/ui/controls/pickers.html">Pickers</a> - * guide. - * </p> + * For a dialog using this view, see {@link android.app.TimePickerDialog}. See + * the <a href="{@docRoot}guide/topics/ui/controls/pickers.html">Pickers</a> + * guide for more information. + * + * @attr ref android.R.styleable#TimePicker_timePickerMode */ @Widget public class TimePicker extends FrameLayout { @@ -96,44 +92,105 @@ public class TimePicker extends FrameLayout { } /** - * Set the current hour. + * Sets the currently selected hour using 24-hour time. + * + * @param hour the hour to set, in the range (0-23) + * @see #getHour() + */ + public void setHour(int hour) { + mDelegate.setCurrentHour(hour); + } + + /** + * Returns the currently selected hour using 24-hour time. + * + * @return the currently selected hour, in the range (0-23) + * @see #setHour(int) + */ + public int getHour() { + return mDelegate.getCurrentHour(); + } + + /** + * Sets the currently selected minute.. + * + * @param minute the minute to set, in the range (0-59) + * @see #getMinute() */ - public void setCurrentHour(Integer currentHour) { - mDelegate.setCurrentHour(currentHour); + public void setMinute(int minute) { + mDelegate.setCurrentMinute(minute); } /** - * @return The current hour in the range (0-23). + * Returns the currently selected minute. + * + * @return the currently selected minute, in the range (0-59) + * @see #setMinute(int) */ + public int getMinute() { + return mDelegate.getCurrentMinute(); + } + + /** + * Sets the current hour. + * + * @deprecated Use {@link #setHour(int)} + */ + @Deprecated + public void setCurrentHour(@NonNull Integer currentHour) { + setHour(currentHour); + } + + /** + * @return the current hour in the range (0-23) + * @deprecated Use {@link #getHour()} + */ + @NonNull + @Deprecated public Integer getCurrentHour() { return mDelegate.getCurrentHour(); } /** * Set the current minute (0-59). + * + * @deprecated Use {@link #setMinute(int)} */ - public void setCurrentMinute(Integer currentMinute) { + @Deprecated + public void setCurrentMinute(@NonNull Integer currentMinute) { mDelegate.setCurrentMinute(currentMinute); } /** - * @return The current minute. + * @return the current minute + * @deprecated Use {@link #getMinute()} */ + @NonNull + @Deprecated public Integer getCurrentMinute() { return mDelegate.getCurrentMinute(); } /** - * Set whether in 24 hour or AM/PM mode. + * Sets whether this widget displays time in 24-hour mode or 12-hour mode + * with an AM/PM picker. * - * @param is24HourView True = 24 hour mode. False = AM/PM. + * @param is24HourView {@code true} to display in 24-hour mode, + * {@code false} for 12-hour mode with AM/PM + * @see #is24HourView() */ - public void setIs24HourView(Boolean is24HourView) { + public void setIs24HourView(@NonNull Boolean is24HourView) { + if (is24HourView == null) { + return; + } + mDelegate.setIs24HourView(is24HourView); } /** - * @return true if this is in 24 hour view else false. + * @return {@code true} if this widget displays time in 24-hour mode, + * {@code false} otherwise} + * @see #setIs24HourView(Boolean) */ public boolean is24HourView() { return mDelegate.is24HourView(); @@ -210,13 +267,13 @@ public class TimePicker extends FrameLayout { * for the real behavior. */ interface TimePickerDelegate { - void setCurrentHour(Integer currentHour); - Integer getCurrentHour(); + void setCurrentHour(int currentHour); + int getCurrentHour(); - void setCurrentMinute(Integer currentMinute); - Integer getCurrentMinute(); + void setCurrentMinute(int currentMinute); + int getCurrentMinute(); - void setIs24HourView(Boolean is24HourView); + void setIs24HourView(boolean is24HourView); boolean is24HourView(); void setOnTimeChangedListener(OnTimeChangedListener onTimeChangedListener); diff --git a/core/java/android/widget/TimePickerClockDelegate.java b/core/java/android/widget/TimePickerClockDelegate.java index 9fdd718..c58d5cb 100644 --- a/core/java/android/widget/TimePickerClockDelegate.java +++ b/core/java/android/widget/TimePickerClockDelegate.java @@ -16,16 +16,20 @@ package android.widget; +import android.annotation.Nullable; import android.content.Context; +import android.content.res.ColorStateList; import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; import android.os.Parcel; import android.os.Parcelable; import android.text.format.DateFormat; import android.text.format.DateUtils; import android.util.AttributeSet; import android.util.Log; +import android.util.StateSet; import android.util.TypedValue; import android.view.HapticFeedbackConstants; import android.view.KeyCharacterMap; @@ -48,7 +52,6 @@ import java.util.Locale; */ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate implements RadialTimePickerView.OnValueSelectedListener { - private static final String TAG = "TimePickerClockDelegate"; // Index used by RadialPickerLayout @@ -61,14 +64,16 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate impl // Also NOT a real index, just used for keyboard mode. private static final int ENABLE_PICKER_INDEX = 3; + private static final int[] ATTRS_TEXT_COLOR = new int[] { + com.android.internal.R.attr.textColor}; + private static final int[] ATTRS_DISABLED_ALPHA = new int[] { + com.android.internal.R.attr.disabledAlpha}; + // LayoutLib relies on these constants. Change TimePickerClockDelegate_Delegate if // modifying these. static final int AM = 0; static final int PM = 1; - private static final boolean DEFAULT_ENABLED_STATE = true; - private boolean mIsEnabled = DEFAULT_ENABLED_STATE; - private static final int HOURS_IN_HALF_DAY = 12; private final View mHeaderView; @@ -83,8 +88,7 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate impl private final String mAmText; private final String mPmText; - private final float mDisabledAlpha; - + private boolean mIsEnabled = true; private boolean mAllowAutoAdvance; private int mInitialHourOfDay; private int mInitialMinute; @@ -134,7 +138,6 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate impl final View mainView = inflater.inflate(layoutResourceId, delegator); mHeaderView = mainView.findViewById(R.id.time_header); - mHeaderView.setBackground(a.getDrawable(R.styleable.TimePicker_headerBackground)); // Set up hour/minute labels. mHourView = (TextView) mainView.findViewById(R.id.hours); @@ -147,14 +150,6 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate impl mMinuteView.setAccessibilityDelegate( new ClickActionDelegate(context, R.string.select_minutes)); - final int headerTimeTextAppearance = a.getResourceId( - R.styleable.TimePicker_headerTimeTextAppearance, 0); - if (headerTimeTextAppearance != 0) { - mHourView.setTextAppearance(context, headerTimeTextAppearance); - mSeparatorView.setTextAppearance(context, headerTimeTextAppearance); - mMinuteView.setTextAppearance(context, headerTimeTextAppearance); - } - // Now that we have text appearances out of the way, make sure the hour // and minute views are correctly sized. mHourView.setMinWidth(computeStableWidth(mHourView, 24)); @@ -169,19 +164,40 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate impl mPmLabel.setText(amPmStrings[1]); mPmLabel.setOnClickListener(mClickListener); - final int headerAmPmTextAppearance = a.getResourceId( - R.styleable.TimePicker_headerAmPmTextAppearance, 0); - if (headerAmPmTextAppearance != 0) { - mAmLabel.setTextAppearance(context, headerAmPmTextAppearance); - mPmLabel.setTextAppearance(context, headerAmPmTextAppearance); + // For the sake of backwards compatibility, attempt to extract the text + // color from the header time text appearance. If it's set, we'll let + // that override the "real" header text color. + ColorStateList headerTextColor = null; + + @SuppressWarnings("deprecation") + final int timeHeaderTextAppearance = a.getResourceId( + R.styleable.TimePicker_headerTimeTextAppearance, 0); + if (timeHeaderTextAppearance != 0) { + final TypedArray textAppearance = mContext.obtainStyledAttributes(null, + ATTRS_TEXT_COLOR, 0, timeHeaderTextAppearance); + final ColorStateList legacyHeaderTextColor = textAppearance.getColorStateList(0); + headerTextColor = applyLegacyColorFixes(legacyHeaderTextColor); + textAppearance.recycle(); } - a.recycle(); + if (headerTextColor == null) { + headerTextColor = a.getColorStateList(R.styleable.TimePicker_headerTextColor); + } + + if (headerTextColor != null) { + mHourView.setTextColor(headerTextColor); + mSeparatorView.setTextColor(headerTextColor); + mMinuteView.setTextColor(headerTextColor); + mAmLabel.setTextColor(headerTextColor); + mPmLabel.setTextColor(headerTextColor); + } + + // Set up header background, if available. + if (a.hasValueOrEmpty(R.styleable.TimePicker_headerBackground)) { + mHeaderView.setBackground(a.getDrawable(R.styleable.TimePicker_headerBackground)); + } - // Pull disabled alpha from theme. - final TypedValue outValue = new TypedValue(); - context.getTheme().resolveAttribute(android.R.attr.disabledAlpha, outValue, true); - mDisabledAlpha = outValue.getFloat(); + a.recycle(); mRadialTimePickerView = (RadialTimePickerView) mainView.findViewById( R.id.radial_picker); @@ -204,6 +220,54 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate impl initialize(currentHour, currentMinute, false /* 12h */, HOUR_INDEX); } + /** + * The legacy text color might have been poorly defined. Ensures that it + * has an appropriate activated state, using the selected state if one + * exists or modifying the default text color otherwise. + * + * @param color a legacy text color, or {@code null} + * @return a color state list with an appropriate activated state, or + * {@code null} if a valid activated state could not be generated + */ + @Nullable + private ColorStateList applyLegacyColorFixes(@Nullable ColorStateList color) { + if (color == null || color.hasState(R.attr.state_activated)) { + return color; + } + + final int activatedColor; + final int defaultColor; + if (color.hasState(R.attr.state_selected)) { + activatedColor = color.getColorForState(StateSet.get( + StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_SELECTED), 0); + defaultColor = color.getColorForState(StateSet.get( + StateSet.VIEW_STATE_ENABLED), 0); + } else { + activatedColor = color.getDefaultColor(); + + // Generate a non-activated color using the disabled alpha. + final TypedArray ta = mContext.obtainStyledAttributes(ATTRS_DISABLED_ALPHA); + final float disabledAlpha = ta.getFloat(0, 0.30f); + defaultColor = multiplyAlphaComponent(activatedColor, disabledAlpha); + } + + if (activatedColor == 0 || defaultColor == 0) { + // We somehow failed to obtain the colors. + return null; + } + + final int[][] stateSet = new int[][] {{ R.attr.state_activated }, {}}; + final int[] colors = new int[] { activatedColor, defaultColor }; + return new ColorStateList(stateSet, colors); + } + + private int multiplyAlphaComponent(int color, float alphaMod) { + final int srcRgb = color & 0xFFFFFF; + final int srcAlpha = (color >> 24) & 0xFF; + final int dstAlpha = (int) (srcAlpha * alphaMod + 0.5f); + return srcRgb | (dstAlpha << 24); + } + private static class ClickActionDelegate extends AccessibilityDelegate { private final AccessibilityAction mClickAction; @@ -312,7 +376,7 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate impl * Set the current hour. */ @Override - public void setCurrentHour(Integer currentHour) { + public void setCurrentHour(int currentHour) { if (mInitialHourOfDay == currentHour) { return; } @@ -329,7 +393,7 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate impl * @return The current hour in the range (0-23). */ @Override - public Integer getCurrentHour() { + public int getCurrentHour() { int currentHour = mRadialTimePickerView.getCurrentHour(); if (mIs24HourView) { return currentHour; @@ -348,7 +412,7 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate impl * Set the current minute (0-59). */ @Override - public void setCurrentMinute(Integer currentMinute) { + public void setCurrentMinute(int currentMinute) { if (mInitialMinute == currentMinute) { return; } @@ -363,7 +427,7 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate impl * @return The current minute. */ @Override - public Integer getCurrentMinute() { + public int getCurrentMinute() { return mRadialTimePickerView.getCurrentMinute(); } @@ -373,7 +437,7 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate impl * @param is24HourView True = 24 hour mode. False = AM/PM. */ @Override - public void setIs24HourView(Boolean is24HourView) { + public void setIs24HourView(boolean is24HourView) { if (is24HourView == mIs24HourView) { return; } @@ -604,12 +668,12 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate impl private void updateAmPmLabelStates(int amOrPm) { final boolean isAm = amOrPm == AM; + mAmLabel.setActivated(isAm); mAmLabel.setChecked(isAm); - mAmLabel.setSelected(isAm); final boolean isPm = amOrPm == PM; + mPmLabel.setActivated(isPm); mPmLabel.setChecked(isPm); - mPmLabel.setSelected(isPm); } /** @@ -769,8 +833,8 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate impl } } - mHourView.setSelected(index == HOUR_INDEX); - mMinuteView.setSelected(index == MINUTE_INDEX); + mHourView.setActivated(index == HOUR_INDEX); + mMinuteView.setActivated(index == MINUTE_INDEX); } private void setAmOrPm(int amOrPm) { @@ -960,9 +1024,9 @@ class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate impl String minuteStr = (values[1] == -1) ? mDoublePlaceholderText : String.format(minuteFormat, values[1]).replace(' ', mPlaceholderText); mHourView.setText(hourStr); - mHourView.setSelected(false); + mHourView.setActivated(false); mMinuteView.setText(minuteStr); - mMinuteView.setSelected(false); + mMinuteView.setActivated(false); if (!mIs24HourView) { updateAmPmLabelStates(values[2]); } diff --git a/core/java/android/widget/TimePickerSpinnerDelegate.java b/core/java/android/widget/TimePickerSpinnerDelegate.java index 513c55b..df6b0a9 100644 --- a/core/java/android/widget/TimePickerSpinnerDelegate.java +++ b/core/java/android/widget/TimePickerSpinnerDelegate.java @@ -279,13 +279,13 @@ class TimePickerSpinnerDelegate extends TimePicker.AbstractTimePickerDelegate { } @Override - public void setCurrentHour(Integer currentHour) { + public void setCurrentHour(int currentHour) { setCurrentHour(currentHour, true); } - private void setCurrentHour(Integer currentHour, boolean notifyTimeChanged) { + private void setCurrentHour(int currentHour, boolean notifyTimeChanged) { // why was Integer used in the first place? - if (currentHour == null || currentHour == getCurrentHour()) { + if (currentHour == getCurrentHour()) { return; } if (!is24HourView()) { @@ -310,7 +310,7 @@ class TimePickerSpinnerDelegate extends TimePicker.AbstractTimePickerDelegate { } @Override - public Integer getCurrentHour() { + public int getCurrentHour() { int currentHour = mHourSpinner.getValue(); if (is24HourView()) { return currentHour; @@ -322,7 +322,7 @@ class TimePickerSpinnerDelegate extends TimePicker.AbstractTimePickerDelegate { } @Override - public void setCurrentMinute(Integer currentMinute) { + public void setCurrentMinute(int currentMinute) { if (currentMinute == getCurrentMinute()) { return; } @@ -331,12 +331,12 @@ class TimePickerSpinnerDelegate extends TimePicker.AbstractTimePickerDelegate { } @Override - public Integer getCurrentMinute() { + public int getCurrentMinute() { return mMinuteSpinner.getValue(); } @Override - public void setIs24HourView(Boolean is24HourView) { + public void setIs24HourView(boolean is24HourView) { if (mIs24HourView == is24HourView) { return; } diff --git a/core/java/android/widget/YearPickerView.java b/core/java/android/widget/YearPickerView.java index 7bd502e..7182414 100644 --- a/core/java/android/widget/YearPickerView.java +++ b/core/java/android/widget/YearPickerView.java @@ -111,16 +111,12 @@ class YearPickerView extends ListView { mAdapter.setRange(min, max); } - public void setYearTextAppearance(int resId) { - mAdapter.setItemTextAppearance(resId); - } - - public void setYearActivatedTextAppearance(int resId) { - mAdapter.setItemActivatedTextAppearance(resId); - } - private static class YearAdapter extends BaseAdapter { private static final int ITEM_LAYOUT = R.layout.year_label_text_view; + private static final int ITEM_TEXT_APPEARANCE = + R.style.TextAppearance_Material_DatePicker_List_YearLabel; + private static final int ITEM_TEXT_ACTIVATED_APPEARANCE = + R.style.TextAppearance_Material_DatePicker_List_YearLabel_Activated; private final LayoutInflater mInflater; @@ -128,9 +124,6 @@ class YearPickerView extends ListView { private int mMinYear; private int mCount; - private int mItemTextAppearanceResId; - private int mItemActivatedTextAppearanceResId; - public YearAdapter(Context context) { mInflater = LayoutInflater.from(context); } @@ -155,16 +148,6 @@ class YearPickerView extends ListView { return false; } - public void setItemTextAppearance(int resId) { - mItemTextAppearanceResId = resId; - notifyDataSetChanged(); - } - - public void setItemActivatedTextAppearance(int resId) { - mItemActivatedTextAppearanceResId = resId; - notifyDataSetChanged(); - } - @Override public int getCount() { return mCount; @@ -203,10 +186,10 @@ class YearPickerView extends ListView { final boolean activated = mActivatedYear == year; final int textAppearanceResId; - if (activated && mItemActivatedTextAppearanceResId != 0) { - textAppearanceResId = mItemActivatedTextAppearanceResId; + if (activated && ITEM_TEXT_ACTIVATED_APPEARANCE != 0) { + textAppearanceResId = ITEM_TEXT_ACTIVATED_APPEARANCE; } else { - textAppearanceResId = mItemTextAppearanceResId; + textAppearanceResId = ITEM_TEXT_APPEARANCE; } final TextView v = (TextView) convertView; |
