diff options
Diffstat (limited to 'core/java')
96 files changed, 4550 insertions, 1619 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; diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java index 3ceea9d..6b35f3f 100644 --- a/core/java/com/android/internal/app/ResolverActivity.java +++ b/core/java/com/android/internal/app/ResolverActivity.java @@ -604,9 +604,10 @@ public class ResolverActivity extends Activity implements AdapterView.OnItemClic if ((mAlwaysUseOption || mAdapter.hasFilteredItem()) && mAdapter.mOrigResolveList != null) { // Build a reasonable intent filter, based on what matched. IntentFilter filter = new IntentFilter(); + String action = intent.getAction(); - if (intent.getAction() != null) { - filter.addAction(intent.getAction()); + if (action != null) { + filter.addAction(action); } Set<String> categories = intent.getCategories(); if (categories != null) { @@ -688,8 +689,30 @@ public class ResolverActivity extends Activity implements AdapterView.OnItemClic if (r.match > bestMatch) bestMatch = r.match; } if (alwaysCheck) { - getPackageManager().addPreferredActivity(filter, bestMatch, set, - intent.getComponent()); + PackageManager pm = getPackageManager(); + + // Set the preferred Activity + pm.addPreferredActivity(filter, bestMatch, set, intent.getComponent()); + + // Update Domain Verification status + int userId = getUserId(); + ComponentName cn = intent.getComponent(); + String packageName = cn.getPackageName(); + String dataScheme = (data != null) ? data.getScheme() : null; + + boolean isHttpOrHttps = (dataScheme != null) && + (dataScheme.equals(IntentFilter.SCHEME_HTTP) || + dataScheme.equals(IntentFilter.SCHEME_HTTPS)); + + boolean isViewAction = (action != null) && action.equals(Intent.ACTION_VIEW); + boolean hasCategoryBrowsable = (categories != null) && + categories.contains(Intent.CATEGORY_BROWSABLE); + + if (isHttpOrHttps && isViewAction && hasCategoryBrowsable) { + pm.updateIntentVerificationStatus(packageName, + PackageManager.INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ALWAYS, + userId); + } } else { try { AppGlobals.getPackageManager().setLastChosenActivity(intent, diff --git a/core/java/com/android/internal/backup/IBackupTransport.aidl b/core/java/com/android/internal/backup/IBackupTransport.aidl index 6158a7b..083d6c7 100644 --- a/core/java/com/android/internal/backup/IBackupTransport.aidl +++ b/core/java/com/android/internal/backup/IBackupTransport.aidl @@ -238,6 +238,7 @@ interface IBackupTransport { long requestFullBackupTime(); int performFullBackup(in PackageInfo targetPackage, in ParcelFileDescriptor socket); + int checkFullBackupSize(long size); int sendBackupData(int numBytes); void cancelFullBackup(); diff --git a/core/java/com/android/internal/logging/EventLogTags.logtags b/core/java/com/android/internal/logging/EventLogTags.logtags new file mode 100644 index 0000000..870d20d --- /dev/null +++ b/core/java/com/android/internal/logging/EventLogTags.logtags @@ -0,0 +1,7 @@ +# See system/core/logcat/event.logtags for a description of the format of this file. + +option java_package com.android.internal.logging; + +# interaction logs +524287 sysui_view_visibility (category|1|5),(visible|1|6) +524288 sysui_action (category|1|5) diff --git a/core/java/com/android/internal/logging/MetricsConstants.java b/core/java/com/android/internal/logging/MetricsConstants.java new file mode 100644 index 0000000..e5cba84 --- /dev/null +++ b/core/java/com/android/internal/logging/MetricsConstants.java @@ -0,0 +1,153 @@ +/* + * 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 com.android.internal.logging; + +/** + * Constants for mestrics logs. + * + * @hide + */ +public interface MetricsConstants { + // These constants must match those in the analytic pipeline. + public static final int ACCESSIBILITY = 2; + public static final int ACCESSIBILITY_CAPTION_PROPERTIES = 3; + public static final int ACCESSIBILITY_SERVICE = 4; + public static final int ACCESSIBILITY_TOGGLE_DALTONIZER = 5; + public static final int ACCESSIBILITY_TOGGLE_GLOBAL_GESTURE = 6; + public static final int ACCESSIBILITY_TOGGLE_SCREEN_MAGNIFICATION = 7; + public static final int ACCOUNT = 8; + public static final int ACCOUNTS_ACCOUNT_SYNC = 9; + public static final int ACCOUNTS_CHOOSE_ACCOUNT_ACTIVITY = 10; + public static final int ACCOUNTS_MANAGE_ACCOUNTS = 11; + public static final int APN = 12; + public static final int APN_EDITOR = 13; + public static final int APPLICATION = 16; + public static final int APPLICATIONS_APP_LAUNCH = 17; + public static final int APPLICATIONS_APP_PERMISSION = 18; + public static final int APPLICATIONS_APP_STORAGE = 19; + public static final int APPLICATIONS_INSTALLED_APP_DETAILS = 20; + public static final int APPLICATIONS_PROCESS_STATS_DETAIL = 21; + public static final int APPLICATIONS_PROCESS_STATS_MEM_DETAIL = 22; + public static final int APPLICATIONS_PROCESS_STATS_UI = 23; + public static final int APP_OPS_DETAILS = 14; + public static final int APP_OPS_SUMMARY = 15; + public static final int BLUETOOTH = 24; + public static final int BLUETOOTH_DEVICE_PICKER = 25; + public static final int BLUETOOTH_DEVICE_PROFILES = 26; + public static final int CHOOSE_LOCK_GENERIC = 27; + public static final int CHOOSE_LOCK_PASSWORD = 28; + public static final int CHOOSE_LOCK_PATTERN = 29; + public static final int CONFIRM_LOCK_PASSWORD = 30; + public static final int CONFIRM_LOCK_PATTERN = 31; + public static final int CRYPT_KEEPER = 32; + public static final int CRYPT_KEEPER_CONFIRM = 33; + public static final int DASHBOARD_SEARCH_RESULTS = 34; + public static final int DASHBOARD_SUMMARY = 35; + public static final int DATA_USAGE = 36; + public static final int DATA_USAGE_SUMMARY = 37; + public static final int DATE_TIME = 38; + public static final int DEVELOPMENT = 39; + public static final int DEVICEINFO = 40; + public static final int DEVICEINFO_IMEI_INFORMATION = 41; + public static final int DEVICEINFO_MEMORY = 42; + public static final int DEVICEINFO_SIM_STATUS = 43; + public static final int DEVICEINFO_STATUS = 44; + public static final int DEVICEINFO_USB = 45; + public static final int DISPLAY = 46; + public static final int DREAM = 47; + public static final int ENCRYPTION = 48; + public static final int FINGERPRINT = 49; + public static final int FINGERPRINT_ENROLL = 50; + public static final int FUELGAUGE_BATTERY_HISTORY_DETAIL = 51; + public static final int FUELGAUGE_BATTERY_SAVER = 52; + public static final int FUELGAUGE_POWER_USAGE_DETAIL = 53; + public static final int FUELGAUGE_POWER_USAGE_SUMMARY = 54; + public static final int HOME = 55; + public static final int ICC_LOCK = 56; + public static final int INPUTMETHOD_KEYBOARD = 58; + public static final int INPUTMETHOD_LANGUAGE = 57; + public static final int INPUTMETHOD_SPELL_CHECKERS = 59; + public static final int INPUTMETHOD_SUBTYPE_ENABLER = 60; + public static final int INPUTMETHOD_USER_DICTIONARY = 61; + public static final int INPUTMETHOD_USER_DICTIONARY_ADD_WORD = 62; + public static final int LOCATION = 63; + public static final int LOCATION_MODE = 64; + public static final int MAIN_SETTINGS = 1; + public static final int MANAGE_APPLICATIONS = 65; + public static final int MASTER_CLEAR = 66; + public static final int MASTER_CLEAR_CONFIRM = 67; + public static final int NET_DATA_USAGE_METERED = 68; + public static final int NFC_BEAM = 69; + public static final int NFC_PAYMENT = 70; + public static final int NOTIFICATION = 71; + public static final int NOTIFICATION_APP_NOTIFICATION = 72; + public static final int NOTIFICATION_OTHER_SOUND = 73; + public static final int NOTIFICATION_REDACTION = 74; + public static final int NOTIFICATION_STATION = 75; + public static final int NOTIFICATION_ZEN_MODE = 76; + public static final int OWNER_INFO = 77; + public static final int PRINT_JOB_SETTINGS = 78; + public static final int PRINT_SERVICE_SETTINGS = 79; + public static final int PRINT_SETTINGS = 80; + public static final int PRIVACY = 81; + public static final int PROXY_SELECTOR = 82; + public static final int QS_AIRPLANEMODE = 112; + public static final int QS_BLUETOOTH = 113; + public static final int QS_CAST = 114; + public static final int QS_CELLULAR = 115; + public static final int QS_COLORINVERSION = 116; + public static final int QS_DATAUSAGEDETAIL = 117; + public static final int QS_DND = 118; + public static final int QS_FLASHLIGHT = 119; + public static final int QS_HOTSPOT = 120; + public static final int QS_INTENT = 121; + public static final int QS_LOCATION = 122; + public static final int QS_PANEL = 111; + public static final int QS_ROTATIONLOCK = 123; + public static final int QS_USERDETAIL = 125; + public static final int QS_USERDETAILITE = 124; + public static final int QS_WIFI = 126; + public static final int RESET_NETWORK = 83; + public static final int RESET_NETWORK_CONFIRM = 84; + public static final int RUNNING_SERVICE_DETAILS = 85; + public static final int SCREEN_PINNING = 86; + public static final int SECURITY = 87; + public static final int SIM = 88; + public static final int TESTING = 89; + public static final int TETHER = 90; + public static final int TRUSTED_CREDENTIALS = 92; + public static final int TRUST_AGENT = 91; + public static final int TTS_ENGINE_SETTINGS = 93; + public static final int TTS_TEXT_TO_SPEECH = 94; + public static final int TYPE_UNKNOWN = 0; + public static final int USAGE_ACCESS = 95; + public static final int USER = 96; + public static final int USERS_APP_RESTRICTIONS = 97; + public static final int USER_DETAILS = 98; + public static final int VIEW_UNKNOWN = 0; + public static final int VOICE_INPUT = 99; + public static final int VPN = 100; + public static final int WALLPAPER_TYPE = 101; + public static final int WFD_WIFI_DISPLAY = 102; + public static final int WIFI = 103; + public static final int WIFI_ADVANCED = 104; + public static final int WIFI_APITEST = 107; + public static final int WIFI_CALLING = 105; + public static final int WIFI_INFO = 108; + public static final int WIFI_P2P = 109; + public static final int WIFI_SAVED_ACCESS_POINTS = 106; + public static final int WIRELESS = 110; +} diff --git a/core/java/com/android/internal/logging/MetricsLogger.java b/core/java/com/android/internal/logging/MetricsLogger.java new file mode 100644 index 0000000..9b45e34 --- /dev/null +++ b/core/java/com/android/internal/logging/MetricsLogger.java @@ -0,0 +1,63 @@ +/* + * 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 com.android.internal.logging; + + +import android.content.Context; +import android.os.Build; + +/** + * Log all the things. + * + * @hide + */ +public class MetricsLogger implements MetricsConstants { + // These constants are temporary, they should migrate to MetricsConstants. + public static final int APPLICATIONS_ADVANCED = 132; + public static final int LOCATION_SCANNING = 133; + public static final int MANAGE_APPLICATIONS_ALL = 134; + public static final int MANAGE_APPLICATIONS_NOTIFICATIONS = 135; + + public static final int ACTION_WIFI_ADD_NETWORK = 136; + public static final int ACTION_WIFI_CONNECT = 137; + public static final int ACTION_WIFI_FORCE_SCAN = 138; + public static final int ACTION_WIFI_FORGET = 139; + public static final int ACTION_WIFI_OFF = 140; + public static final int ACTION_WIFI_ON = 141; + + public static final int MANAGE_PERMISSIONS = 142; + + public static void visible(Context context, int category) throws IllegalArgumentException { + if (Build.IS_DEBUGGABLE && category == VIEW_UNKNOWN) { + throw new IllegalArgumentException("Must define metric category"); + } + EventLogTags.writeSysuiViewVisibility(category, 100); + } + + public static void hidden(Context context, int category) { + if (Build.IS_DEBUGGABLE && category == VIEW_UNKNOWN) { + throw new IllegalArgumentException("Must define metric category"); + } + EventLogTags.writeSysuiViewVisibility(category, 0); + } + + public static void action(Context context, int category) { + if (Build.IS_DEBUGGABLE && category == VIEW_UNKNOWN) { + throw new IllegalArgumentException("Must define metric category"); + } + EventLogTags.writeSysuiAction(category); + } +} diff --git a/core/java/com/android/internal/midi/EventScheduler.java b/core/java/com/android/internal/midi/EventScheduler.java new file mode 100644 index 0000000..7526609 --- /dev/null +++ b/core/java/com/android/internal/midi/EventScheduler.java @@ -0,0 +1,236 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.midi; + +import java.util.SortedMap; +import java.util.TreeMap; + +/** + * Store arbitrary timestamped events using a Long timestamp. + * Only one Thread can write into the buffer. + * And only one Thread can read from the buffer. + */ +public class EventScheduler { + private static final long NANOS_PER_MILLI = 1000000; + + private final Object lock = new Object(); + private SortedMap<Long, FastEventQueue> mEventBuffer; + private FastEventQueue mEventPool = null; + private int mMaxPoolSize = 200; + + public EventScheduler() { + mEventBuffer = new TreeMap<Long, FastEventQueue>(); + } + + // If we keep at least one node in the list then it can be atomic + // and non-blocking. + private class FastEventQueue { + // One thread takes from the beginning of the list. + volatile SchedulableEvent mFirst; + // A second thread returns events to the end of the list. + volatile SchedulableEvent mLast; + volatile long mEventsAdded; + volatile long mEventsRemoved; + + FastEventQueue(SchedulableEvent event) { + mFirst = event; + mLast = mFirst; + mEventsAdded = 1; + mEventsRemoved = 0; + } + + int size() { + return (int)(mEventsAdded - mEventsRemoved); + } + + /** + * Do not call this unless there is more than one event + * in the list. + * @return first event in the list + */ + public SchedulableEvent remove() { + // Take first event. + mEventsRemoved++; + SchedulableEvent event = mFirst; + mFirst = event.mNext; + return event; + } + + /** + * @param event + */ + public void add(SchedulableEvent event) { + event.mNext = null; + mLast.mNext = event; + mLast = event; + mEventsAdded++; + } + } + + /** + * Base class for events that can be stored in the EventScheduler. + */ + public static class SchedulableEvent { + private long mTimestamp; + private SchedulableEvent mNext = null; + + /** + * @param timestamp + */ + public SchedulableEvent(long timestamp) { + mTimestamp = timestamp; + } + + /** + * @return timestamp + */ + public long getTimestamp() { + return mTimestamp; + } + + /** + * The timestamp should not be modified when the event is in the + * scheduling buffer. + */ + public void setTimestamp(long timestamp) { + mTimestamp = timestamp; + } + } + + /** + * Get an event from the pool. + * Always leave at least one event in the pool. + * @return event or null + */ + public SchedulableEvent removeEventfromPool() { + SchedulableEvent event = null; + if (mEventPool != null && (mEventPool.size() > 1)) { + event = mEventPool.remove(); + } + return event; + } + + /** + * Return events to a pool so they can be reused. + * + * @param event + */ + public void addEventToPool(SchedulableEvent event) { + if (mEventPool == null) { + mEventPool = new FastEventQueue(event); + // If we already have enough items in the pool then just + // drop the event. This prevents unbounded memory leaks. + } else if (mEventPool.size() < mMaxPoolSize) { + mEventPool.add(event); + } + } + + /** + * Add an event to the scheduler. Events with the same time will be + * processed in order. + * + * @param event + */ + public void add(SchedulableEvent event) { + synchronized (lock) { + FastEventQueue list = mEventBuffer.get(event.getTimestamp()); + if (list == null) { + long lowestTime = mEventBuffer.isEmpty() ? Long.MAX_VALUE + : mEventBuffer.firstKey(); + list = new FastEventQueue(event); + mEventBuffer.put(event.getTimestamp(), list); + // If the event we added is earlier than the previous earliest + // event then notify any threads waiting for the next event. + if (event.getTimestamp() < lowestTime) { + lock.notify(); + } + } else { + list.add(event); + } + } + } + + private SchedulableEvent removeNextEventLocked(long lowestTime) { + SchedulableEvent event; + FastEventQueue list = mEventBuffer.get(lowestTime); + // Remove list from tree if this is the last node. + if ((list.size() == 1)) { + mEventBuffer.remove(lowestTime); + } + event = list.remove(); + return event; + } + + /** + * Check to see if any scheduled events are ready to be processed. + * + * @param timestamp + * @return next event or null if none ready + */ + public SchedulableEvent getNextEvent(long time) { + SchedulableEvent event = null; + synchronized (lock) { + if (!mEventBuffer.isEmpty()) { + long lowestTime = mEventBuffer.firstKey(); + // Is it time for this list to be processed? + if (lowestTime <= time) { + event = removeNextEventLocked(lowestTime); + } + } + } + // Log.i(TAG, "getNextEvent: event = " + event); + return event; + } + + /** + * Return the next available event or wait until there is an event ready to + * be processed. This method assumes that the timestamps are in nanoseconds + * and that the current time is System.nanoTime(). + * + * @return event + * @throws InterruptedException + */ + public SchedulableEvent waitNextEvent() throws InterruptedException { + SchedulableEvent event = null; + while (true) { + long millisToWait = Integer.MAX_VALUE; + synchronized (lock) { + if (!mEventBuffer.isEmpty()) { + long now = System.nanoTime(); + long lowestTime = mEventBuffer.firstKey(); + // Is it time for the earliest list to be processed? + if (lowestTime <= now) { + event = removeNextEventLocked(lowestTime); + break; + } else { + // Figure out how long to sleep until next event. + long nanosToWait = lowestTime - now; + // Add 1 millisecond so we don't wake up before it is + // ready. + millisToWait = 1 + (nanosToWait / NANOS_PER_MILLI); + // Clip 64-bit value to 32-bit max. + if (millisToWait > Integer.MAX_VALUE) { + millisToWait = Integer.MAX_VALUE; + } + } + } + lock.wait((int) millisToWait); + } + } + return event; + } +} diff --git a/core/java/com/android/internal/midi/MidiConstants.java b/core/java/com/android/internal/midi/MidiConstants.java new file mode 100644 index 0000000..87552e4 --- /dev/null +++ b/core/java/com/android/internal/midi/MidiConstants.java @@ -0,0 +1,88 @@ +/* + * 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 com.android.internal.midi; + +/** + * MIDI related constants and static methods. + */ +public class MidiConstants { + public static final byte STATUS_COMMAND_MASK = (byte) 0xF0; + public static final byte STATUS_CHANNEL_MASK = (byte) 0x0F; + + // Channel voice messages. + public static final byte STATUS_NOTE_OFF = (byte) 0x80; + public static final byte STATUS_NOTE_ON = (byte) 0x90; + public static final byte STATUS_POLYPHONIC_AFTERTOUCH = (byte) 0xA0; + public static final byte STATUS_CONTROL_CHANGE = (byte) 0xB0; + public static final byte STATUS_PROGRAM_CHANGE = (byte) 0xC0; + public static final byte STATUS_CHANNEL_PRESSURE = (byte) 0xD0; + public static final byte STATUS_PITCH_BEND = (byte) 0xE0; + + // System Common Messages. + public static final byte STATUS_SYSTEM_EXCLUSIVE = (byte) 0xF0; + public static final byte STATUS_MIDI_TIME_CODE = (byte) 0xF1; + public static final byte STATUS_SONG_POSITION = (byte) 0xF2; + public static final byte STATUS_SONG_SELECT = (byte) 0xF3; + public static final byte STATUS_TUNE_REQUEST = (byte) 0xF6; + public static final byte STATUS_END_SYSEX = (byte) 0xF7; + + // System Real-Time Messages + public static final byte STATUS_TIMING_CLOCK = (byte) 0xF8; + public static final byte STATUS_START = (byte) 0xFA; + public static final byte STATUS_CONTINUE = (byte) 0xFB; + public static final byte STATUS_STOP = (byte) 0xFC; + public static final byte STATUS_ACTIVE_SENSING = (byte) 0xFE; + public static final byte STATUS_RESET = (byte) 0xFF; + + /** Number of bytes in a message nc from 8c to Ec */ + public final static int CHANNEL_BYTE_LENGTHS[] = { 3, 3, 3, 3, 2, 2, 3 }; + + /** Number of bytes in a message Fn from F0 to FF */ + public final static int SYSTEM_BYTE_LENGTHS[] = { 1, 2, 3, 2, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1 }; + + /********************************************************************/ + + public static int getBytesPerMessage(int command) { + if ((command < 0x80) || (command > 0xFF)) { + return 0; + } else if (command >= 0xF0) { + return SYSTEM_BYTE_LENGTHS[command & 0x0F]; + } else { + return CHANNEL_BYTE_LENGTHS[(command >> 4) - 8]; + } + } + + /** + * @param msg + * @param offset + * @param count + * @return true if the entire message is ActiveSensing commands + */ + public static boolean isAllActiveSensing(byte[] msg, int offset, + int count) { + // Count bytes that are not active sensing. + int goodBytes = 0; + for (int i = 0; i < count; i++) { + byte b = msg[offset + i]; + if (b != MidiConstants.STATUS_ACTIVE_SENSING) { + goodBytes++; + } + } + return (goodBytes == 0); + } +} diff --git a/core/java/com/android/internal/midi/MidiDispatcher.java b/core/java/com/android/internal/midi/MidiDispatcher.java new file mode 100644 index 0000000..377bc68 --- /dev/null +++ b/core/java/com/android/internal/midi/MidiDispatcher.java @@ -0,0 +1,86 @@ +/* + * 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 com.android.internal.midi; + +import android.media.midi.MidiReceiver; +import android.media.midi.MidiSender; + +import java.io.IOException; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * Utility class for dispatching MIDI data to a list of {@link android.media.midi.MidiReceiver}s. + * This class subclasses {@link android.media.midi.MidiReceiver} and dispatches any data it receives + * to its receiver list. Any receivers that throw an exception upon receiving data will + * be automatically removed from the receiver list, but no IOException will be returned + * from the dispatcher's {@link android.media.midi.MidiReceiver#onReceive} in that case. + */ +public final class MidiDispatcher extends MidiReceiver { + + private final CopyOnWriteArrayList<MidiReceiver> mReceivers + = new CopyOnWriteArrayList<MidiReceiver>(); + + private final MidiSender mSender = new MidiSender() { + /** + * Called to connect a {@link android.media.midi.MidiReceiver} to the sender + * + * @param receiver the receiver to connect + */ + public void connect(MidiReceiver receiver) { + mReceivers.add(receiver); + } + + /** + * Called to disconnect a {@link android.media.midi.MidiReceiver} from the sender + * + * @param receiver the receiver to disconnect + */ + public void disconnect(MidiReceiver receiver) { + mReceivers.remove(receiver); + } + }; + + /** + * Returns the number of {@link android.media.midi.MidiReceiver}s this dispatcher contains. + * @return the number of receivers + */ + public int getReceiverCount() { + return mReceivers.size(); + } + + /** + * Returns a {@link android.media.midi.MidiSender} which is used to add and remove + * {@link android.media.midi.MidiReceiver}s + * to the dispatcher's receiver list. + * @return the dispatcher's MidiSender + */ + public MidiSender getSender() { + return mSender; + } + + @Override + public void onReceive(byte[] msg, int offset, int count, long timestamp) throws IOException { + for (MidiReceiver receiver : mReceivers) { + try { + receiver.sendWithTimestamp(msg, offset, count, timestamp); + } catch (IOException e) { + // if the receiver fails we remove the receiver but do not propagate the exception + mReceivers.remove(receiver); + } + } + } +} diff --git a/core/java/com/android/internal/midi/MidiEventScheduler.java b/core/java/com/android/internal/midi/MidiEventScheduler.java new file mode 100644 index 0000000..3a1d3fc --- /dev/null +++ b/core/java/com/android/internal/midi/MidiEventScheduler.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.midi; + +import android.media.midi.MidiReceiver; + +import java.io.IOException; + +/** + * Add MIDI Events to an EventScheduler + */ +public class MidiEventScheduler extends EventScheduler { + private static final String TAG = "MidiEventScheduler"; + // Maintain a pool of scheduled events to reduce memory allocation. + // This pool increases performance by about 14%. + private final static int POOL_EVENT_SIZE = 16; + private MidiReceiver mReceiver = new SchedulingReceiver(); + + private class SchedulingReceiver extends MidiReceiver + { + /** + * Store these bytes in the EventScheduler to be delivered at the specified + * time. + */ + @Override + public void onReceive(byte[] msg, int offset, int count, long timestamp) + throws IOException { + MidiEvent event = createScheduledEvent(msg, offset, count, timestamp); + if (event != null) { + add(event); + } + } + } + + public static class MidiEvent extends SchedulableEvent { + public int count = 0; + public byte[] data; + + private MidiEvent(int count) { + super(0); + data = new byte[count]; + } + + private MidiEvent(byte[] msg, int offset, int count, long timestamp) { + super(timestamp); + data = new byte[count]; + System.arraycopy(msg, offset, data, 0, count); + this.count = count; + } + + @Override + public String toString() { + String text = "Event: "; + for (int i = 0; i < count; i++) { + text += data[i] + ", "; + } + return text; + } + } + + /** + * Create an event that contains the message. + */ + private MidiEvent createScheduledEvent(byte[] msg, int offset, int count, + long timestamp) { + MidiEvent event; + if (count > POOL_EVENT_SIZE) { + event = new MidiEvent(msg, offset, count, timestamp); + } else { + event = (MidiEvent) removeEventfromPool(); + if (event == null) { + event = new MidiEvent(POOL_EVENT_SIZE); + } + System.arraycopy(msg, offset, event.data, 0, count); + event.count = count; + event.setTimestamp(timestamp); + } + return event; + } + + /** + * Return events to a pool so they can be reused. + * + * @param event + */ + @Override + public void addEventToPool(SchedulableEvent event) { + // Make sure the event is suitable for the pool. + if (event instanceof MidiEvent) { + MidiEvent midiEvent = (MidiEvent) event; + if (midiEvent.data.length == POOL_EVENT_SIZE) { + super.addEventToPool(event); + } + } + } + + /** + * This MidiReceiver will write date to the scheduling buffer. + * @return the MidiReceiver + */ + public MidiReceiver getReceiver() { + return mReceiver; + } + +} diff --git a/core/java/com/android/internal/midi/MidiFramer.java b/core/java/com/android/internal/midi/MidiFramer.java new file mode 100644 index 0000000..53d71bb --- /dev/null +++ b/core/java/com/android/internal/midi/MidiFramer.java @@ -0,0 +1,92 @@ +/* + * 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 com.android.internal.midi; + +import android.media.midi.MidiReceiver; + +import java.io.IOException; + +/** + * Convert stream of bytes to discrete messages. + * + * Parses the incoming bytes and then posts individual messages to the receiver + * specified in the constructor. Short messages of 1-3 bytes will be complete. + * System Exclusive messages may be posted in pieces. + * + * Resolves Running Status and + * interleaved System Real-Time messages. + */ +public class MidiFramer extends MidiReceiver { + + public String TAG = "MidiFramer"; + private MidiReceiver mReceiver; + private byte[] mBuffer = new byte[3]; + private int mCount; + private int mRunningStatus; + private int mNeeded; + + public MidiFramer(MidiReceiver receiver) { + mReceiver = receiver; + } + + public static String formatMidiData(byte[] data, int offset, int count) { + String text = "MIDI+" + offset + " : "; + for (int i = 0; i < count; i++) { + text += String.format("0x%02X, ", data[offset + i]); + } + return text; + } + + /* + * @see android.midi.MidiReceiver#onPost(byte[], int, int, long) + */ + @Override + public void onReceive(byte[] data, int offset, int count, long timestamp) + throws IOException { + // Log.i(TAG, formatMidiData(data, offset, count)); + for (int i = 0; i < count; i++) { + int b = data[offset] & 0xFF; + if (b >= 0x80) { // status byte? + if (b < 0xF0) { // channel message? + mRunningStatus = (byte) b; + mCount = 1; + mNeeded = MidiConstants.getBytesPerMessage(b) - 1; + } else if (b < 0xF8) { // system common? + mBuffer[0] = (byte) b; + mRunningStatus = 0; + mCount = 1; + mNeeded = MidiConstants.getBytesPerMessage(b) - 1; + } else { // real-time? + // Single byte message interleaved with other data. + mReceiver.sendWithTimestamp(data, offset, 1, timestamp); + } + } else { // data byte + mBuffer[mCount++] = (byte) b; + if (--mNeeded == 0) { + if (mRunningStatus != 0) { + mBuffer[0] = (byte) mRunningStatus; + } + mReceiver.sendWithTimestamp(mBuffer, 0, mCount, timestamp); + mNeeded = MidiConstants.getBytesPerMessage(mBuffer[0]) - 1; + mCount = 1; + } + } + ++offset; + } + } + +} diff --git a/core/java/com/android/internal/os/BatteryStatsImpl.java b/core/java/com/android/internal/os/BatteryStatsImpl.java index 2c34ded..93dc995 100644 --- a/core/java/com/android/internal/os/BatteryStatsImpl.java +++ b/core/java/com/android/internal/os/BatteryStatsImpl.java @@ -16,18 +16,14 @@ package com.android.internal.os; -import static android.net.NetworkStats.UID_ALL; -import static com.android.server.NetworkManagementSocketTagger.PROP_QTAGUID_ENABLED; - +import android.annotation.Nullable; import android.app.ActivityManager; import android.bluetooth.BluetoothActivityEnergyInfo; -import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothHeadset; import android.content.Context; import android.net.ConnectivityManager; import android.net.NetworkStats; -import android.net.wifi.IWifiManager; import android.net.wifi.WifiActivityEnergyInfo; import android.net.wifi.WifiManager; import android.os.BadParcelableException; @@ -42,8 +38,6 @@ import android.os.Parcel; import android.os.ParcelFormatException; import android.os.Parcelable; import android.os.Process; -import android.os.RemoteException; -import android.os.ServiceManager; import android.os.SystemClock; import android.os.SystemProperties; import android.os.WorkSource; @@ -65,13 +59,14 @@ import android.util.TimeUtils; import android.util.Xml; import android.view.Display; -import com.android.internal.annotations.GuardedBy; import com.android.internal.net.NetworkStatsFactory; import com.android.internal.util.ArrayUtils; import com.android.internal.util.FastPrintWriter; import com.android.internal.util.FastXmlSerializer; import com.android.internal.util.JournaledFile; import com.android.internal.util.XmlUtils; +import com.android.server.NetworkManagementSocketTagger; +import libcore.util.EmptyArray; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlSerializer; @@ -132,6 +127,9 @@ public final class BatteryStatsImpl extends BatteryStats { static final int MSG_REPORT_POWER_CHANGE = 2; static final long DELAY_UPDATE_WAKELOCKS = 5*1000; + private final KernelWakelockReader mKernelWakelockReader = new KernelWakelockReader(); + private final KernelWakelockStats mTmpWakelockStats = new KernelWakelockStats(); + public interface BatteryCallback { public void batteryNeedsCpuUpdate(); public void batteryPowerChanged(boolean onBattery); @@ -160,7 +158,12 @@ public final class BatteryStatsImpl extends BatteryStats { } } + public interface ExternalStatsSync { + void scheduleSync(); + } + public final MyHandler mHandler; + private final ExternalStatsSync mExternalSync; private BatteryCallback mCallback; @@ -330,7 +333,7 @@ public final class BatteryStatsImpl extends BatteryStats { int mPhoneSignalStrengthBin = -1; int mPhoneSignalStrengthBinRaw = -1; - final StopwatchTimer[] mPhoneSignalStrengthsTimer = + final StopwatchTimer[] mPhoneSignalStrengthsTimer = new StopwatchTimer[SignalStrength.NUM_SIGNAL_STRENGTH_BINS]; StopwatchTimer mPhoneSignalScanningTimer; @@ -445,18 +448,17 @@ public final class BatteryStatsImpl extends BatteryStats { private int mLoadedNumConnectivityChange; private int mUnpluggedNumConnectivityChange; + private final NetworkStats.Entry mTmpNetworkStatsEntry = new NetworkStats.Entry(); + /* * Holds a SamplingTimer associated with each kernel wakelock name being tracked. */ - private final HashMap<String, SamplingTimer> mKernelWakelockStats = - new HashMap<String, SamplingTimer>(); + private final HashMap<String, SamplingTimer> mKernelWakelockStats = new HashMap<>(); public Map<String, ? extends Timer> getKernelWakelockStats() { return mKernelWakelockStats; } - private static int sKernelWakelockUpdateVersion = 0; - String mLastWakeupReason = null; long mLastWakeupUptimeMs = 0; private final HashMap<String, SamplingTimer> mWakeupReasonStats = new HashMap<>(); @@ -465,55 +467,12 @@ public final class BatteryStatsImpl extends BatteryStats { return mWakeupReasonStats; } - private static final int[] PROC_WAKELOCKS_FORMAT = new int[] { - Process.PROC_TAB_TERM|Process.PROC_OUT_STRING| // 0: name - Process.PROC_QUOTES, - Process.PROC_TAB_TERM|Process.PROC_OUT_LONG, // 1: count - Process.PROC_TAB_TERM, - Process.PROC_TAB_TERM, - Process.PROC_TAB_TERM, - Process.PROC_TAB_TERM|Process.PROC_OUT_LONG, // 5: totalTime - }; - - private static final int[] WAKEUP_SOURCES_FORMAT = new int[] { - Process.PROC_TAB_TERM|Process.PROC_OUT_STRING, // 0: name - Process.PROC_TAB_TERM|Process.PROC_COMBINE| - Process.PROC_OUT_LONG, // 1: count - Process.PROC_TAB_TERM|Process.PROC_COMBINE, - Process.PROC_TAB_TERM|Process.PROC_COMBINE, - Process.PROC_TAB_TERM|Process.PROC_COMBINE, - Process.PROC_TAB_TERM|Process.PROC_COMBINE, - Process.PROC_TAB_TERM|Process.PROC_COMBINE - |Process.PROC_OUT_LONG, // 6: totalTime - }; - - private final String[] mProcWakelocksName = new String[3]; - private final long[] mProcWakelocksData = new long[3]; - - /* - * Used as a buffer for reading in data from /proc/wakelocks before it is processed and added - * to mKernelWakelockStats. - */ - private final Map<String, KernelWakelockStats> mProcWakelockFileStats = new HashMap<>(); - - private final NetworkStatsFactory mNetworkStatsFactory = new NetworkStatsFactory(); - private NetworkStats mCurMobileSnapshot = new NetworkStats(SystemClock.elapsedRealtime(), 50); - private NetworkStats mLastMobileSnapshot = new NetworkStats(SystemClock.elapsedRealtime(), 50); - private NetworkStats mCurWifiSnapshot = new NetworkStats(SystemClock.elapsedRealtime(), 50); - private NetworkStats mLastWifiSnapshot = new NetworkStats(SystemClock.elapsedRealtime(), 50); - private NetworkStats mTmpNetworkStats; - private final NetworkStats.Entry mTmpNetworkStatsEntry = new NetworkStats.Entry(); - - @GuardedBy("this") - private String[] mMobileIfaces = new String[0]; - @GuardedBy("this") - private String[] mWifiIfaces = new String[0]; - public BatteryStatsImpl() { mFile = null; mCheckinFile = null; mDailyFile = null; mHandler = null; + mExternalSync = null; clearHistoryLocked(); } @@ -523,7 +482,7 @@ public final class BatteryStatsImpl extends BatteryStats { } static class TimeBase { - private final ArrayList<TimeBaseObs> mObservers = new ArrayList<TimeBaseObs>(); + private final ArrayList<TimeBaseObs> mObservers = new ArrayList<>(); private long mUptime; private long mRealtime; @@ -1778,147 +1737,6 @@ public final class BatteryStatsImpl extends BatteryStats { return timer; } - private final Map<String, KernelWakelockStats> readKernelWakelockStats() { - - FileInputStream is; - byte[] buffer = new byte[32*1024]; - int len; - boolean wakeup_sources; - - try { - try { - is = new FileInputStream("/d/wakeup_sources"); - wakeup_sources = true; - } catch (java.io.FileNotFoundException e) { - try { - is = new FileInputStream("/proc/wakelocks"); - wakeup_sources = false; - } catch (java.io.FileNotFoundException e2) { - return null; - } - } - - len = is.read(buffer); - is.close(); - } catch (java.io.IOException e) { - return null; - } - - if (len > 0) { - if (len >= buffer.length) { - Slog.wtf(TAG, "Kernel wake locks exceeded buffer size " + buffer.length); - } - int i; - for (i=0; i<len; i++) { - if (buffer[i] == '\0') { - len = i; - break; - } - } - } - - return parseProcWakelocks(buffer, len, wakeup_sources); - } - - private final Map<String, KernelWakelockStats> parseProcWakelocks( - byte[] wlBuffer, int len, boolean wakeup_sources) { - String name; - int count; - long totalTime; - int startIndex; - int endIndex; - int numUpdatedWlNames = 0; - - // Advance past the first line. - int i; - for (i = 0; i < len && wlBuffer[i] != '\n' && wlBuffer[i] != '\0'; i++); - startIndex = endIndex = i + 1; - - synchronized(this) { - Map<String, KernelWakelockStats> m = mProcWakelockFileStats; - - sKernelWakelockUpdateVersion++; - while (endIndex < len) { - for (endIndex=startIndex; - endIndex < len && wlBuffer[endIndex] != '\n' && wlBuffer[endIndex] != '\0'; - endIndex++); - endIndex++; // endIndex is an exclusive upper bound. - // Don't go over the end of the buffer, Process.parseProcLine might - // write to wlBuffer[endIndex] - if (endIndex >= (len - 1) ) { - return m; - } - - String[] nameStringArray = mProcWakelocksName; - long[] wlData = mProcWakelocksData; - // Stomp out any bad characters since this is from a circular buffer - // A corruption is seen sometimes that results in the vm crashing - // This should prevent crashes and the line will probably fail to parse - for (int j = startIndex; j < endIndex; j++) { - if ((wlBuffer[j] & 0x80) != 0) wlBuffer[j] = (byte) '?'; - } - boolean parsed = Process.parseProcLine(wlBuffer, startIndex, endIndex, - wakeup_sources ? WAKEUP_SOURCES_FORMAT : - PROC_WAKELOCKS_FORMAT, - nameStringArray, wlData, null); - - name = nameStringArray[0]; - count = (int) wlData[1]; - - if (wakeup_sources) { - // convert milliseconds to microseconds - totalTime = wlData[2] * 1000; - } else { - // convert nanoseconds to microseconds with rounding. - totalTime = (wlData[2] + 500) / 1000; - } - - if (parsed && name.length() > 0) { - if (!m.containsKey(name)) { - m.put(name, new KernelWakelockStats(count, totalTime, - sKernelWakelockUpdateVersion)); - numUpdatedWlNames++; - } else { - KernelWakelockStats kwlStats = m.get(name); - if (kwlStats.mVersion == sKernelWakelockUpdateVersion) { - kwlStats.mCount += count; - kwlStats.mTotalTime += totalTime; - } else { - kwlStats.mCount = count; - kwlStats.mTotalTime = totalTime; - kwlStats.mVersion = sKernelWakelockUpdateVersion; - numUpdatedWlNames++; - } - } - } - startIndex = endIndex; - } - - if (m.size() != numUpdatedWlNames) { - // Don't report old data. - Iterator<KernelWakelockStats> itr = m.values().iterator(); - while (itr.hasNext()) { - if (itr.next().mVersion != sKernelWakelockUpdateVersion) { - itr.remove(); - } - } - } - return m; - } - } - - private class KernelWakelockStats { - public int mCount; - public long mTotalTime; - public int mVersion; - - KernelWakelockStats(int count, long totalTime, int version) { - mCount = count; - mTotalTime = totalTime; - mVersion = version; - } - } - /* * Get the KernelWakelockTimer associated with name, and create a new one if one * doesn't already exist. @@ -3390,7 +3208,7 @@ public final class BatteryStatsImpl extends BatteryStats { mMobileRadioActivePerAppTimer.startRunningLocked(elapsedRealtime); } else { mMobileRadioActiveTimer.stopRunningLocked(realElapsedRealtimeMs); - updateNetworkActivityLocked(NET_UPDATE_MOBILE, realElapsedRealtimeMs); + updateMobileRadioStateLocked(realElapsedRealtimeMs); mMobileRadioActivePerAppTimer.stopRunningLocked(realElapsedRealtimeMs); } } @@ -3728,6 +3546,7 @@ public final class BatteryStatsImpl extends BatteryStats { addHistoryRecordLocked(elapsedRealtime, uptime); mWifiOn = true; mWifiOnTimer.startRunningLocked(elapsedRealtime); + scheduleSyncExternalStatsLocked(); } } @@ -3741,6 +3560,7 @@ public final class BatteryStatsImpl extends BatteryStats { addHistoryRecordLocked(elapsedRealtime, uptime); mWifiOn = false; mWifiOnTimer.stopRunningLocked(elapsedRealtime); + scheduleSyncExternalStatsLocked(); } } @@ -3903,6 +3723,7 @@ public final class BatteryStatsImpl extends BatteryStats { int uid = mapUid(ws.get(i)); getUidStatsLocked(uid).noteWifiRunningLocked(elapsedRealtime); } + scheduleSyncExternalStatsLocked(); } else { Log.w(TAG, "noteWifiRunningLocked -- called while WIFI running"); } @@ -3941,6 +3762,7 @@ public final class BatteryStatsImpl extends BatteryStats { int uid = mapUid(ws.get(i)); getUidStatsLocked(uid).noteWifiStoppedLocked(elapsedRealtime); } + scheduleSyncExternalStatsLocked(); } else { Log.w(TAG, "noteWifiStoppedLocked -- called while WIFI not running"); } @@ -3955,6 +3777,7 @@ public final class BatteryStatsImpl extends BatteryStats { } mWifiState = wifiState; mWifiStateTimer[wifiState].startRunningLocked(elapsedRealtime); + scheduleSyncExternalStatsLocked(); } } @@ -4026,6 +3849,7 @@ public final class BatteryStatsImpl extends BatteryStats { addHistoryRecordLocked(elapsedRealtime, uptime); mBluetoothOn = true; mBluetoothOnTimer.startRunningLocked(elapsedRealtime); + scheduleSyncExternalStatsLocked(); } } @@ -4039,6 +3863,7 @@ public final class BatteryStatsImpl extends BatteryStats { addHistoryRecordLocked(elapsedRealtime, uptime); mBluetoothOn = false; mBluetoothOnTimer.stopRunningLocked(elapsedRealtime); + scheduleSyncExternalStatsLocked(); } } @@ -4065,6 +3890,7 @@ public final class BatteryStatsImpl extends BatteryStats { if (DEBUG_HISTORY) Slog.v(TAG, "WIFI full lock on to: " + Integer.toHexString(mHistoryCur.states)); addHistoryRecordLocked(elapsedRealtime, uptime); + scheduleSyncExternalStatsLocked(); } mWifiFullLockNesting++; getUidStatsLocked(uid).noteFullWifiLockAcquiredLocked(elapsedRealtime); @@ -4080,6 +3906,7 @@ public final class BatteryStatsImpl extends BatteryStats { if (DEBUG_HISTORY) Slog.v(TAG, "WIFI full lock off to: " + Integer.toHexString(mHistoryCur.states)); addHistoryRecordLocked(elapsedRealtime, uptime); + scheduleSyncExternalStatsLocked(); } getUidStatsLocked(uid).noteFullWifiLockReleasedLocked(elapsedRealtime); } @@ -4137,6 +3964,7 @@ public final class BatteryStatsImpl extends BatteryStats { if (DEBUG_HISTORY) Slog.v(TAG, "WIFI multicast on to: " + Integer.toHexString(mHistoryCur.states)); addHistoryRecordLocked(elapsedRealtime, uptime); + scheduleSyncExternalStatsLocked(); } mWifiMulticastNesting++; getUidStatsLocked(uid).noteWifiMulticastEnabledLocked(elapsedRealtime); @@ -4152,6 +3980,7 @@ public final class BatteryStatsImpl extends BatteryStats { if (DEBUG_HISTORY) Slog.v(TAG, "WIFI multicast off to: " + Integer.toHexString(mHistoryCur.states)); addHistoryRecordLocked(elapsedRealtime, uptime); + scheduleSyncExternalStatsLocked(); } getUidStatsLocked(uid).noteWifiMulticastDisabledLocked(elapsedRealtime); } @@ -4259,7 +4088,8 @@ public final class BatteryStatsImpl extends BatteryStats { // During device boot, qtaguid isn't enabled until after the inital // loading of battery stats. Now that they're enabled, take our initial // snapshot for future delta calculation. - updateNetworkActivityLocked(NET_UPDATE_ALL, SystemClock.elapsedRealtime()); + updateMobileRadioStateLocked(SystemClock.elapsedRealtime()); + updateWifiStateLocked(null); } @Override public long getScreenOnTime(long elapsedRealtimeUs, int which) { @@ -5949,7 +5779,7 @@ public final class BatteryStatsImpl extends BatteryStats { Slog.w(TAG, "File corrupt: too many excessive power entries " + N); return false; } - + mExcessivePower = new ArrayList<ExcessivePower>(); for (int i=0; i<N; i++) { ExcessivePower ew = new ExcessivePower(); @@ -6727,7 +6557,7 @@ public final class BatteryStatsImpl extends BatteryStats { } } - public BatteryStatsImpl(File systemDir, Handler handler) { + public BatteryStatsImpl(File systemDir, Handler handler, ExternalStatsSync externalSync) { if (systemDir != null) { mFile = new JournaledFile(new File(systemDir, "batterystats.bin"), new File(systemDir, "batterystats.bin.tmp")); @@ -6736,6 +6566,7 @@ public final class BatteryStatsImpl extends BatteryStats { } mCheckinFile = new AtomicFile(new File(systemDir, "batterystats-checkin.bin")); mDailyFile = new AtomicFile(new File(systemDir, "batterystats-daily.xml")); + mExternalSync = externalSync; mHandler = new MyHandler(handler.getLooper()); mStartCount++; mScreenOnTimer = new StopwatchTimer(null, -1, null, mOnBatteryTimeBase); @@ -6808,6 +6639,7 @@ public final class BatteryStatsImpl extends BatteryStats { mCheckinFile = null; mDailyFile = null; mHandler = null; + mExternalSync = null; clearHistoryLocked(); readFromParcel(p); } @@ -7479,21 +7311,233 @@ public final class BatteryStatsImpl extends BatteryStats { mDischargeScreenOffUnplugLevel = mDischargeCurrentLevel; } } - + public void pullPendingStateUpdatesLocked() { - updateKernelWakelocksLocked(); - updateNetworkActivityLocked(NET_UPDATE_ALL, SystemClock.elapsedRealtime()); - // TODO(adamlesinski): enable when bluedroid stops deadlocking. b/19248786 - // updateBluetoothControllerActivityLocked(); - // TODO(adamlesinski): disabled to avoid deadlock. Need to change how external - // data is pulled/accessed from BatteryStats. b/19729960 - // updateWifiControllerActivityLocked(); if (mOnBatteryInternal) { final boolean screenOn = mScreenState == Display.STATE_ON; updateDischargeScreenLevelsLocked(screenOn, screenOn); } } + private String[] mMobileIfaces = EmptyArray.STRING; + private String[] mWifiIfaces = EmptyArray.STRING; + + private final NetworkStatsFactory mNetworkStatsFactory = new NetworkStatsFactory(); + + private static final int NETWORK_STATS_LAST = 0; + private static final int NETWORK_STATS_NEXT = 1; + private static final int NETWORK_STATS_DELTA = 2; + + private final NetworkStats[] mMobileNetworkStats = new NetworkStats[] { + new NetworkStats(SystemClock.elapsedRealtime(), 50), + new NetworkStats(SystemClock.elapsedRealtime(), 50), + new NetworkStats(SystemClock.elapsedRealtime(), 50) + }; + + private final NetworkStats[] mWifiNetworkStats = new NetworkStats[] { + new NetworkStats(SystemClock.elapsedRealtime(), 50), + new NetworkStats(SystemClock.elapsedRealtime(), 50), + new NetworkStats(SystemClock.elapsedRealtime(), 50) + }; + + /** + * Retrieves the delta of network stats for the given network ifaces. Uses networkStatsBuffer + * as a buffer of NetworkStats objects to cycle through when computing deltas. + */ + private NetworkStats getNetworkStatsDeltaLocked(String[] ifaces, + NetworkStats[] networkStatsBuffer) + throws IOException { + if (!SystemProperties.getBoolean(NetworkManagementSocketTagger.PROP_QTAGUID_ENABLED, + false)) { + return null; + } + + final NetworkStats stats = mNetworkStatsFactory.readNetworkStatsDetail(NetworkStats.UID_ALL, + ifaces, NetworkStats.TAG_NONE, networkStatsBuffer[NETWORK_STATS_NEXT]); + networkStatsBuffer[NETWORK_STATS_DELTA] = NetworkStats.subtract(stats, + networkStatsBuffer[NETWORK_STATS_LAST], null, null, + networkStatsBuffer[NETWORK_STATS_DELTA]); + networkStatsBuffer[NETWORK_STATS_NEXT] = networkStatsBuffer[NETWORK_STATS_LAST]; + networkStatsBuffer[NETWORK_STATS_LAST] = stats; + return networkStatsBuffer[NETWORK_STATS_DELTA]; + } + + /** + * Distribute WiFi energy info and network traffic to apps. + * @param info The energy information from the WiFi controller. + */ + public void updateWifiStateLocked(@Nullable final WifiActivityEnergyInfo info) { + final NetworkStats delta; + try { + delta = getNetworkStatsDeltaLocked(mWifiIfaces, mWifiNetworkStats); + } catch (IOException e) { + Slog.wtf(TAG, "Failed to get wifi network stats", e); + return; + } + + if (!mOnBatteryInternal) { + return; + } + + if (delta != null) { + final int size = delta.size(); + for (int i = 0; i < size; i++) { + final NetworkStats.Entry entry = delta.getValues(i, mTmpNetworkStatsEntry); + + if (DEBUG) { + Slog.d(TAG, "Wifi uid " + entry.uid + ": delta rx=" + entry.rxBytes + + " tx=" + entry.txBytes); + } + + if (entry.rxBytes == 0 || entry.txBytes == 0) { + continue; + } + + final Uid u = getUidStatsLocked(mapUid(entry.uid)); + u.noteNetworkActivityLocked(NETWORK_WIFI_RX_DATA, entry.rxBytes, + entry.rxPackets); + u.noteNetworkActivityLocked(NETWORK_WIFI_TX_DATA, entry.txBytes, + entry.txPackets); + + mNetworkByteActivityCounters[NETWORK_WIFI_RX_DATA].addCountLocked( + entry.rxBytes); + mNetworkByteActivityCounters[NETWORK_WIFI_TX_DATA].addCountLocked( + entry.txBytes); + mNetworkPacketActivityCounters[NETWORK_WIFI_RX_DATA].addCountLocked( + entry.rxPackets); + mNetworkPacketActivityCounters[NETWORK_WIFI_TX_DATA].addCountLocked( + entry.txPackets); + } + } + + if (info != null) { + // Update WiFi controller stats. + mWifiActivityCounters[CONTROLLER_RX_TIME].addCountLocked( + info.getControllerRxTimeMillis()); + mWifiActivityCounters[CONTROLLER_TX_TIME].addCountLocked( + info.getControllerTxTimeMillis()); + mWifiActivityCounters[CONTROLLER_IDLE_TIME].addCountLocked( + info.getControllerIdleTimeMillis()); + mWifiActivityCounters[CONTROLLER_ENERGY].addCountLocked( + info.getControllerEnergyUsed()); + } + } + + /** + * Distribute Cell radio energy info and network traffic to apps. + */ + public void updateMobileRadioStateLocked(long elapsedRealtimeMs) { + final NetworkStats delta; + + try { + delta = getNetworkStatsDeltaLocked(mMobileIfaces, mMobileNetworkStats); + } catch (IOException e) { + Slog.wtf(TAG, "Failed to get mobile network stats", e); + return; + } + + if (delta == null || !mOnBatteryInternal) { + return; + } + + long radioTime = mMobileRadioActivePerAppTimer.checkpointRunningLocked(elapsedRealtimeMs); + long totalPackets = delta.getTotalPackets(); + + final int size = delta.size(); + for (int i = 0; i < size; i++) { + final NetworkStats.Entry entry = delta.getValues(i, mTmpNetworkStatsEntry); + + if (entry.rxBytes == 0 || entry.txBytes == 0) continue; + + final Uid u = getUidStatsLocked(mapUid(entry.uid)); + u.noteNetworkActivityLocked(NETWORK_MOBILE_RX_DATA, entry.rxBytes, + entry.rxPackets); + u.noteNetworkActivityLocked(NETWORK_MOBILE_TX_DATA, entry.txBytes, + entry.txPackets); + + if (radioTime > 0) { + // Distribute total radio active time in to this app. + long appPackets = entry.rxPackets + entry.txPackets; + long appRadioTime = (radioTime*appPackets)/totalPackets; + u.noteMobileRadioActiveTimeLocked(appRadioTime); + // Remove this app from the totals, so that we don't lose any time + // due to rounding. + radioTime -= appRadioTime; + totalPackets -= appPackets; + } + + mNetworkByteActivityCounters[NETWORK_MOBILE_RX_DATA].addCountLocked( + entry.rxBytes); + mNetworkByteActivityCounters[NETWORK_MOBILE_TX_DATA].addCountLocked( + entry.txBytes); + mNetworkPacketActivityCounters[NETWORK_MOBILE_RX_DATA].addCountLocked( + entry.rxPackets); + mNetworkPacketActivityCounters[NETWORK_MOBILE_TX_DATA].addCountLocked( + entry.txPackets); + } + + if (radioTime > 0) { + // Whoops, there is some radio time we can't blame on an app! + mMobileRadioActiveUnknownTime.addCountLocked(radioTime); + mMobileRadioActiveUnknownCount.addCountLocked(1); + } + } + + /** + * Distribute Bluetooth energy info and network traffic to apps. + * @param info The energy information from the bluetooth controller. + */ + public void updateBluetoothStateLocked(@Nullable final BluetoothActivityEnergyInfo info) { + if (info != null && mOnBatteryInternal) { + mBluetoothActivityCounters[CONTROLLER_RX_TIME].addCountLocked( + info.getControllerRxTimeMillis()); + mBluetoothActivityCounters[CONTROLLER_TX_TIME].addCountLocked( + info.getControllerTxTimeMillis()); + mBluetoothActivityCounters[CONTROLLER_IDLE_TIME].addCountLocked( + info.getControllerIdleTimeMillis()); + mBluetoothActivityCounters[CONTROLLER_ENERGY].addCountLocked( + info.getControllerEnergyUsed()); + } + } + + /** + * Read and distribute kernel wake lock use across apps. + */ + public void updateKernelWakelocksLocked() { + final KernelWakelockStats wakelockStats = mKernelWakelockReader.readKernelWakelockStats( + mTmpWakelockStats); + if (wakelockStats == null) { + // Not crashing might make board bringup easier. + Slog.w(TAG, "Couldn't get kernel wake lock stats"); + return; + } + + for (Map.Entry<String, KernelWakelockStats.Entry> ent : wakelockStats.entrySet()) { + String name = ent.getKey(); + KernelWakelockStats.Entry kws = ent.getValue(); + + SamplingTimer kwlt = mKernelWakelockStats.get(name); + if (kwlt == null) { + kwlt = new SamplingTimer(mOnBatteryScreenOffTimeBase, + true /* track reported val */); + mKernelWakelockStats.put(name, kwlt); + } + kwlt.updateCurrentReportedCount(kws.mCount); + kwlt.updateCurrentReportedTotalTime(kws.mTotalTime); + kwlt.setUpdateVersion(kws.mVersion); + } + + if (wakelockStats.size() != mKernelWakelockStats.size()) { + // Set timers to stale if they didn't appear in /proc/wakelocks this time. + for (Map.Entry<String, SamplingTimer> ent : mKernelWakelockStats.entrySet()) { + SamplingTimer st = ent.getValue(); + if (st.getUpdateVersion() != wakelockStats.kernelWakelockVersion) { + st.setStale(); + } + } + } + } + void setOnBatteryLocked(final long mSecRealtime, final long mSecUptime, final boolean onBattery, final int oldStatus, final int level) { boolean doWrite = false; @@ -7647,340 +7691,132 @@ public final class BatteryStatsImpl extends BatteryStats { } } + private void scheduleSyncExternalStatsLocked() { + if (mExternalSync != null) { + mExternalSync.scheduleSync(); + } + } + // This should probably be exposed in the API, though it's not critical - private static final int BATTERY_PLUGGED_NONE = 0; + public static final int BATTERY_PLUGGED_NONE = 0; - public void setBatteryState(int status, int health, int plugType, int level, + public void setBatteryStateLocked(int status, int health, int plugType, int level, int temp, int volt) { - synchronized(this) { - final boolean onBattery = plugType == BATTERY_PLUGGED_NONE; - final long uptime = SystemClock.uptimeMillis(); - final long elapsedRealtime = SystemClock.elapsedRealtime(); - if (!mHaveBatteryLevel) { - mHaveBatteryLevel = true; - // We start out assuming that the device is plugged in (not - // on battery). If our first report is now that we are indeed - // plugged in, then twiddle our state to correctly reflect that - // since we won't be going through the full setOnBattery(). - if (onBattery == mOnBattery) { - if (onBattery) { - mHistoryCur.states &= ~HistoryItem.STATE_BATTERY_PLUGGED_FLAG; - } else { - mHistoryCur.states |= HistoryItem.STATE_BATTERY_PLUGGED_FLAG; - } - } - mHistoryCur.batteryStatus = (byte)status; - mHistoryCur.batteryLevel = (byte)level; - mMaxChargeStepLevel = mMinDischargeStepLevel = - mLastChargeStepLevel = mLastDischargeStepLevel = level; - } else if (mCurrentBatteryLevel != level || mOnBattery != onBattery) { - recordDailyStatsIfNeededLocked(level >= 100 && onBattery); - } - int oldStatus = mHistoryCur.batteryStatus; - if (onBattery) { - mDischargeCurrentLevel = level; - if (!mRecordingHistory) { - mRecordingHistory = true; - startRecordingHistory(elapsedRealtime, uptime, true); - } - } else if (level < 96) { - if (!mRecordingHistory) { - mRecordingHistory = true; - startRecordingHistory(elapsedRealtime, uptime, true); - } - } - mCurrentBatteryLevel = level; - if (mDischargePlugLevel < 0) { - mDischargePlugLevel = level; - } - if (onBattery != mOnBattery) { - mHistoryCur.batteryLevel = (byte)level; - mHistoryCur.batteryStatus = (byte)status; - mHistoryCur.batteryHealth = (byte)health; - mHistoryCur.batteryPlugType = (byte)plugType; - mHistoryCur.batteryTemperature = (short)temp; - mHistoryCur.batteryVoltage = (char)volt; - setOnBatteryLocked(elapsedRealtime, uptime, onBattery, oldStatus, level); - } else { - boolean changed = false; - if (mHistoryCur.batteryLevel != level) { - mHistoryCur.batteryLevel = (byte)level; - changed = true; - } - if (mHistoryCur.batteryStatus != status) { - mHistoryCur.batteryStatus = (byte)status; - changed = true; - } - if (mHistoryCur.batteryHealth != health) { - mHistoryCur.batteryHealth = (byte)health; - changed = true; - } - if (mHistoryCur.batteryPlugType != plugType) { - mHistoryCur.batteryPlugType = (byte)plugType; - changed = true; - } - if (temp >= (mHistoryCur.batteryTemperature+10) - || temp <= (mHistoryCur.batteryTemperature-10)) { - mHistoryCur.batteryTemperature = (short)temp; - changed = true; - } - if (volt > (mHistoryCur.batteryVoltage+20) - || volt < (mHistoryCur.batteryVoltage-20)) { - mHistoryCur.batteryVoltage = (char)volt; - changed = true; - } - if (changed) { - addHistoryRecordLocked(elapsedRealtime, uptime); - } - long modeBits = (((long)mInitStepMode) << STEP_LEVEL_INITIAL_MODE_SHIFT) - | (((long)mModStepMode) << STEP_LEVEL_MODIFIED_MODE_SHIFT) - | (((long)(level&0xff)) << STEP_LEVEL_LEVEL_SHIFT); + final boolean onBattery = plugType == BATTERY_PLUGGED_NONE; + final long uptime = SystemClock.uptimeMillis(); + final long elapsedRealtime = SystemClock.elapsedRealtime(); + if (!mHaveBatteryLevel) { + mHaveBatteryLevel = true; + // We start out assuming that the device is plugged in (not + // on battery). If our first report is now that we are indeed + // plugged in, then twiddle our state to correctly reflect that + // since we won't be going through the full setOnBattery(). + if (onBattery == mOnBattery) { if (onBattery) { - if (mLastDischargeStepLevel != level && mMinDischargeStepLevel > level) { - mDischargeStepTracker.addLevelSteps(mLastDischargeStepLevel - level, - modeBits, elapsedRealtime); - mDailyDischargeStepTracker.addLevelSteps(mLastDischargeStepLevel - level, - modeBits, elapsedRealtime); - mLastDischargeStepLevel = level; - mMinDischargeStepLevel = level; - mInitStepMode = mCurStepMode; - mModStepMode = 0; - } + mHistoryCur.states &= ~HistoryItem.STATE_BATTERY_PLUGGED_FLAG; } else { - if (mLastChargeStepLevel != level && mMaxChargeStepLevel < level) { - mChargeStepTracker.addLevelSteps(level - mLastChargeStepLevel, - modeBits, elapsedRealtime); - mDailyChargeStepTracker.addLevelSteps(level - mLastChargeStepLevel, - modeBits, elapsedRealtime); - mLastChargeStepLevel = level; - mMaxChargeStepLevel = level; - mInitStepMode = mCurStepMode; - mModStepMode = 0; - } + mHistoryCur.states |= HistoryItem.STATE_BATTERY_PLUGGED_FLAG; } } - if (!onBattery && status == BatteryManager.BATTERY_STATUS_FULL) { - // We don't record history while we are plugged in and fully charged. - // The next time we are unplugged, history will be cleared. - mRecordingHistory = DEBUG; + mHistoryCur.batteryStatus = (byte)status; + mHistoryCur.batteryLevel = (byte)level; + mMaxChargeStepLevel = mMinDischargeStepLevel = + mLastChargeStepLevel = mLastDischargeStepLevel = level; + } else if (mCurrentBatteryLevel != level || mOnBattery != onBattery) { + recordDailyStatsIfNeededLocked(level >= 100 && onBattery); + } + int oldStatus = mHistoryCur.batteryStatus; + if (onBattery) { + mDischargeCurrentLevel = level; + if (!mRecordingHistory) { + mRecordingHistory = true; + startRecordingHistory(elapsedRealtime, uptime, true); + } + } else if (level < 96) { + if (!mRecordingHistory) { + mRecordingHistory = true; + startRecordingHistory(elapsedRealtime, uptime, true); } } - } - - public void updateKernelWakelocksLocked() { - Map<String, KernelWakelockStats> m = readKernelWakelockStats(); - - if (m == null) { - // Not crashing might make board bringup easier. - Slog.w(TAG, "Couldn't get kernel wake lock stats"); - return; + mCurrentBatteryLevel = level; + if (mDischargePlugLevel < 0) { + mDischargePlugLevel = level; } + if (onBattery != mOnBattery) { + mHistoryCur.batteryLevel = (byte)level; + mHistoryCur.batteryStatus = (byte)status; + mHistoryCur.batteryHealth = (byte)health; + mHistoryCur.batteryPlugType = (byte)plugType; + mHistoryCur.batteryTemperature = (short)temp; + mHistoryCur.batteryVoltage = (char)volt; + setOnBatteryLocked(elapsedRealtime, uptime, onBattery, oldStatus, level); + } else { + boolean changed = false; + if (mHistoryCur.batteryLevel != level) { + mHistoryCur.batteryLevel = (byte)level; + changed = true; - for (Map.Entry<String, KernelWakelockStats> ent : m.entrySet()) { - String name = ent.getKey(); - KernelWakelockStats kws = ent.getValue(); - - SamplingTimer kwlt = mKernelWakelockStats.get(name); - if (kwlt == null) { - kwlt = new SamplingTimer(mOnBatteryScreenOffTimeBase, - true /* track reported val */); - mKernelWakelockStats.put(name, kwlt); + // TODO(adamlesinski): Schedule the creation of a HistoryStepDetails record + // which will pull external stats. + scheduleSyncExternalStatsLocked(); } - kwlt.updateCurrentReportedCount(kws.mCount); - kwlt.updateCurrentReportedTotalTime(kws.mTotalTime); - kwlt.setUpdateVersion(sKernelWakelockUpdateVersion); - } - - if (m.size() != mKernelWakelockStats.size()) { - // Set timers to stale if they didn't appear in /proc/wakelocks this time. - for (Map.Entry<String, SamplingTimer> ent : mKernelWakelockStats.entrySet()) { - SamplingTimer st = ent.getValue(); - if (st.getUpdateVersion() != sKernelWakelockUpdateVersion) { - st.setStale(); - } + if (mHistoryCur.batteryStatus != status) { + mHistoryCur.batteryStatus = (byte)status; + changed = true; } - } - } - - static final int NET_UPDATE_MOBILE = 1<<0; - static final int NET_UPDATE_WIFI = 1<<1; - static final int NET_UPDATE_ALL = 0xffff; - - private void updateNetworkActivityLocked(int which, long elapsedRealtimeMs) { - if (!SystemProperties.getBoolean(PROP_QTAGUID_ENABLED, false)) return; - - if ((which&NET_UPDATE_MOBILE) != 0 && mMobileIfaces.length > 0) { - final NetworkStats snapshot; - final NetworkStats last = mCurMobileSnapshot; - try { - snapshot = mNetworkStatsFactory.readNetworkStatsDetail(UID_ALL, - mMobileIfaces, NetworkStats.TAG_NONE, mLastMobileSnapshot); - } catch (IOException e) { - Log.wtf(TAG, "Failed to read mobile network stats", e); - return; + if (mHistoryCur.batteryHealth != health) { + mHistoryCur.batteryHealth = (byte)health; + changed = true; } - - mCurMobileSnapshot = snapshot; - mLastMobileSnapshot = last; - - if (mOnBatteryInternal) { - final NetworkStats delta = NetworkStats.subtract(snapshot, last, - null, null, mTmpNetworkStats); - mTmpNetworkStats = delta; - - long radioTime = mMobileRadioActivePerAppTimer.checkpointRunningLocked( - elapsedRealtimeMs); - long totalPackets = delta.getTotalPackets(); - - final int size = delta.size(); - for (int i = 0; i < size; i++) { - final NetworkStats.Entry entry = delta.getValues(i, mTmpNetworkStatsEntry); - - if (entry.rxBytes == 0 || entry.txBytes == 0) continue; - - final Uid u = getUidStatsLocked(mapUid(entry.uid)); - u.noteNetworkActivityLocked(NETWORK_MOBILE_RX_DATA, entry.rxBytes, - entry.rxPackets); - u.noteNetworkActivityLocked(NETWORK_MOBILE_TX_DATA, entry.txBytes, - entry.txPackets); - - if (radioTime > 0) { - // Distribute total radio active time in to this app. - long appPackets = entry.rxPackets + entry.txPackets; - long appRadioTime = (radioTime*appPackets)/totalPackets; - u.noteMobileRadioActiveTimeLocked(appRadioTime); - // Remove this app from the totals, so that we don't lose any time - // due to rounding. - radioTime -= appRadioTime; - totalPackets -= appPackets; - } - - mNetworkByteActivityCounters[NETWORK_MOBILE_RX_DATA].addCountLocked( - entry.rxBytes); - mNetworkByteActivityCounters[NETWORK_MOBILE_TX_DATA].addCountLocked( - entry.txBytes); - mNetworkPacketActivityCounters[NETWORK_MOBILE_RX_DATA].addCountLocked( - entry.rxPackets); - mNetworkPacketActivityCounters[NETWORK_MOBILE_TX_DATA].addCountLocked( - entry.txPackets); - } - - if (radioTime > 0) { - // Whoops, there is some radio time we can't blame on an app! - mMobileRadioActiveUnknownTime.addCountLocked(radioTime); - mMobileRadioActiveUnknownCount.addCountLocked(1); - } + if (mHistoryCur.batteryPlugType != plugType) { + mHistoryCur.batteryPlugType = (byte)plugType; + changed = true; } - } - - if ((which&NET_UPDATE_WIFI) != 0 && mWifiIfaces.length > 0) { - final NetworkStats snapshot; - final NetworkStats last = mCurWifiSnapshot; - try { - snapshot = mNetworkStatsFactory.readNetworkStatsDetail(UID_ALL, - mWifiIfaces, NetworkStats.TAG_NONE, mLastWifiSnapshot); - } catch (IOException e) { - Log.wtf(TAG, "Failed to read wifi network stats", e); - return; + if (temp >= (mHistoryCur.batteryTemperature+10) + || temp <= (mHistoryCur.batteryTemperature-10)) { + mHistoryCur.batteryTemperature = (short)temp; + changed = true; } - - mCurWifiSnapshot = snapshot; - mLastWifiSnapshot = last; - - if (mOnBatteryInternal) { - final NetworkStats delta = NetworkStats.subtract(snapshot, last, - null, null, mTmpNetworkStats); - mTmpNetworkStats = delta; - - final int size = delta.size(); - for (int i = 0; i < size; i++) { - final NetworkStats.Entry entry = delta.getValues(i, mTmpNetworkStatsEntry); - - if (DEBUG) { - final NetworkStats.Entry cur = snapshot.getValues(i, null); - Slog.d(TAG, "Wifi uid " + entry.uid + ": delta rx=" + entry.rxBytes - + " tx=" + entry.txBytes + ", cur rx=" + cur.rxBytes - + " tx=" + cur.txBytes); - } - - if (entry.rxBytes == 0 || entry.txBytes == 0) continue; - - final Uid u = getUidStatsLocked(mapUid(entry.uid)); - u.noteNetworkActivityLocked(NETWORK_WIFI_RX_DATA, entry.rxBytes, - entry.rxPackets); - u.noteNetworkActivityLocked(NETWORK_WIFI_TX_DATA, entry.txBytes, - entry.txPackets); - - mNetworkByteActivityCounters[NETWORK_WIFI_RX_DATA].addCountLocked( - entry.rxBytes); - mNetworkByteActivityCounters[NETWORK_WIFI_TX_DATA].addCountLocked( - entry.txBytes); - mNetworkPacketActivityCounters[NETWORK_WIFI_RX_DATA].addCountLocked( - entry.rxPackets); - mNetworkPacketActivityCounters[NETWORK_WIFI_TX_DATA].addCountLocked( - entry.txPackets); + if (volt > (mHistoryCur.batteryVoltage+20) + || volt < (mHistoryCur.batteryVoltage-20)) { + mHistoryCur.batteryVoltage = (char)volt; + changed = true; + } + if (changed) { + addHistoryRecordLocked(elapsedRealtime, uptime); + } + long modeBits = (((long)mInitStepMode) << STEP_LEVEL_INITIAL_MODE_SHIFT) + | (((long)mModStepMode) << STEP_LEVEL_MODIFIED_MODE_SHIFT) + | (((long)(level&0xff)) << STEP_LEVEL_LEVEL_SHIFT); + if (onBattery) { + if (mLastDischargeStepLevel != level && mMinDischargeStepLevel > level) { + mDischargeStepTracker.addLevelSteps(mLastDischargeStepLevel - level, + modeBits, elapsedRealtime); + mDailyDischargeStepTracker.addLevelSteps(mLastDischargeStepLevel - level, + modeBits, elapsedRealtime); + mLastDischargeStepLevel = level; + mMinDischargeStepLevel = level; + mInitStepMode = mCurStepMode; + mModStepMode = 0; + } + } else { + if (mLastChargeStepLevel != level && mMaxChargeStepLevel < level) { + mChargeStepTracker.addLevelSteps(level - mLastChargeStepLevel, + modeBits, elapsedRealtime); + mDailyChargeStepTracker.addLevelSteps(level - mLastChargeStepLevel, + modeBits, elapsedRealtime); + mLastChargeStepLevel = level; + mMaxChargeStepLevel = level; + mInitStepMode = mCurStepMode; + mModStepMode = 0; } } } - } - - private void updateBluetoothControllerActivityLocked() { - BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); - if (adapter == null) { - return; - } - - // We read the data even if we are not on battery. Each read clears - // the previous data, so we must always read to make sure the - // data is for the current interval. - BluetoothActivityEnergyInfo info = adapter.getControllerActivityEnergyInfo( - BluetoothAdapter.ACTIVITY_ENERGY_INFO_REFRESHED); - if (info == null || !info.isValid() || !mOnBatteryInternal) { - // Bad info or we are not on battery. - return; - } - - mBluetoothActivityCounters[CONTROLLER_RX_TIME].addCountLocked( - info.getControllerRxTimeMillis()); - mBluetoothActivityCounters[CONTROLLER_TX_TIME].addCountLocked( - info.getControllerTxTimeMillis()); - mBluetoothActivityCounters[CONTROLLER_IDLE_TIME].addCountLocked( - info.getControllerIdleTimeMillis()); - mBluetoothActivityCounters[CONTROLLER_ENERGY].addCountLocked( - info.getControllerEnergyUsed()); - } - - private void updateWifiControllerActivityLocked() { - IWifiManager wifiManager = IWifiManager.Stub.asInterface( - ServiceManager.getService(Context.WIFI_SERVICE)); - if (wifiManager == null) { - return; - } - - WifiActivityEnergyInfo info; - try { - // We read the data even if we are not on battery. Each read clears - // the previous data, so we must always read to make sure the - // data is for the current interval. - info = wifiManager.reportActivityInfo(); - } catch (RemoteException e) { - // Nothing to report, WiFi is dead. - return; + if (!onBattery && status == BatteryManager.BATTERY_STATUS_FULL) { + // We don't record history while we are plugged in and fully charged. + // The next time we are unplugged, history will be cleared. + mRecordingHistory = DEBUG; } - - if (info == null || !info.isValid() || !mOnBatteryInternal) { - // Bad info or we are not on battery. - return; - } - - mWifiActivityCounters[CONTROLLER_RX_TIME].addCountLocked( - info.getControllerRxTimeMillis()); - mWifiActivityCounters[CONTROLLER_TX_TIME].addCountLocked( - info.getControllerTxTimeMillis()); - mWifiActivityCounters[CONTROLLER_IDLE_TIME].addCountLocked( - info.getControllerIdleTimeMillis()); - mWifiActivityCounters[CONTROLLER_ENERGY].addCountLocked( - info.getControllerEnergyUsed()); } public long getAwakeTimeBattery() { diff --git a/core/java/com/android/internal/os/KernelWakelockReader.java b/core/java/com/android/internal/os/KernelWakelockReader.java new file mode 100644 index 0000000..768d586 --- /dev/null +++ b/core/java/com/android/internal/os/KernelWakelockReader.java @@ -0,0 +1,192 @@ +/* + * 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 com.android.internal.os; + +import android.os.Process; +import android.util.Slog; + +import java.io.FileInputStream; +import java.util.Iterator; + +/** + * Reads and parses wakelock stats from the kernel (/proc/wakelocks). + */ +public class KernelWakelockReader { + private static final String TAG = "KernelWakelockReader"; + private static int sKernelWakelockUpdateVersion = 0; + private static final String sWakelockFile = "/proc/wakelocks"; + private static final String sWakeupSourceFile = "/d/wakeup_sources"; + + private static final int[] PROC_WAKELOCKS_FORMAT = new int[] { + Process.PROC_TAB_TERM|Process.PROC_OUT_STRING| // 0: name + Process.PROC_QUOTES, + Process.PROC_TAB_TERM|Process.PROC_OUT_LONG, // 1: count + Process.PROC_TAB_TERM, + Process.PROC_TAB_TERM, + Process.PROC_TAB_TERM, + Process.PROC_TAB_TERM|Process.PROC_OUT_LONG, // 5: totalTime + }; + + private static final int[] WAKEUP_SOURCES_FORMAT = new int[] { + Process.PROC_TAB_TERM|Process.PROC_OUT_STRING, // 0: name + Process.PROC_TAB_TERM|Process.PROC_COMBINE| + Process.PROC_OUT_LONG, // 1: count + Process.PROC_TAB_TERM|Process.PROC_COMBINE, + Process.PROC_TAB_TERM|Process.PROC_COMBINE, + Process.PROC_TAB_TERM|Process.PROC_COMBINE, + Process.PROC_TAB_TERM|Process.PROC_COMBINE, + Process.PROC_TAB_TERM|Process.PROC_COMBINE + |Process.PROC_OUT_LONG, // 6: totalTime + }; + + private final String[] mProcWakelocksName = new String[3]; + private final long[] mProcWakelocksData = new long[3]; + + /** + * Reads kernel wakelock stats and updates the staleStats with the new information. + * @param staleStats Existing object to update. + * @return the updated data. + */ + public final KernelWakelockStats readKernelWakelockStats(KernelWakelockStats staleStats) { + byte[] buffer = new byte[32*1024]; + int len; + boolean wakeup_sources; + + try { + FileInputStream is; + try { + is = new FileInputStream(sWakeupSourceFile); + wakeup_sources = true; + } catch (java.io.FileNotFoundException e) { + try { + is = new FileInputStream(sWakelockFile); + wakeup_sources = false; + } catch (java.io.FileNotFoundException e2) { + return null; + } + } + + len = is.read(buffer); + is.close(); + } catch (java.io.IOException e) { + return null; + } + + if (len > 0) { + if (len >= buffer.length) { + Slog.wtf(TAG, "Kernel wake locks exceeded buffer size " + buffer.length); + } + int i; + for (i=0; i<len; i++) { + if (buffer[i] == '\0') { + len = i; + break; + } + } + } + return parseProcWakelocks(buffer, len, wakeup_sources, staleStats); + } + + /** + * Reads the wakelocks and updates the staleStats with the new information. + */ + private KernelWakelockStats parseProcWakelocks(byte[] wlBuffer, int len, boolean wakeup_sources, + final KernelWakelockStats staleStats) { + String name; + int count; + long totalTime; + int startIndex; + int endIndex; + int numUpdatedWlNames = 0; + + // Advance past the first line. + int i; + for (i = 0; i < len && wlBuffer[i] != '\n' && wlBuffer[i] != '\0'; i++); + startIndex = endIndex = i + 1; + + synchronized(this) { + sKernelWakelockUpdateVersion++; + while (endIndex < len) { + for (endIndex=startIndex; + endIndex < len && wlBuffer[endIndex] != '\n' && wlBuffer[endIndex] != '\0'; + endIndex++); + endIndex++; // endIndex is an exclusive upper bound. + // Don't go over the end of the buffer, Process.parseProcLine might + // write to wlBuffer[endIndex] + if (endIndex >= (len - 1) ) { + return staleStats; + } + + String[] nameStringArray = mProcWakelocksName; + long[] wlData = mProcWakelocksData; + // Stomp out any bad characters since this is from a circular buffer + // A corruption is seen sometimes that results in the vm crashing + // This should prevent crashes and the line will probably fail to parse + for (int j = startIndex; j < endIndex; j++) { + if ((wlBuffer[j] & 0x80) != 0) wlBuffer[j] = (byte) '?'; + } + boolean parsed = Process.parseProcLine(wlBuffer, startIndex, endIndex, + wakeup_sources ? WAKEUP_SOURCES_FORMAT : + PROC_WAKELOCKS_FORMAT, + nameStringArray, wlData, null); + + name = nameStringArray[0]; + count = (int) wlData[1]; + + if (wakeup_sources) { + // convert milliseconds to microseconds + totalTime = wlData[2] * 1000; + } else { + // convert nanoseconds to microseconds with rounding. + totalTime = (wlData[2] + 500) / 1000; + } + + if (parsed && name.length() > 0) { + if (!staleStats.containsKey(name)) { + staleStats.put(name, new KernelWakelockStats.Entry(count, totalTime, + sKernelWakelockUpdateVersion)); + numUpdatedWlNames++; + } else { + KernelWakelockStats.Entry kwlStats = staleStats.get(name); + if (kwlStats.mVersion == sKernelWakelockUpdateVersion) { + kwlStats.mCount += count; + kwlStats.mTotalTime += totalTime; + } else { + kwlStats.mCount = count; + kwlStats.mTotalTime = totalTime; + kwlStats.mVersion = sKernelWakelockUpdateVersion; + numUpdatedWlNames++; + } + } + } + startIndex = endIndex; + } + + if (staleStats.size() != numUpdatedWlNames) { + // Don't report old data. + Iterator<KernelWakelockStats.Entry> itr = staleStats.values().iterator(); + while (itr.hasNext()) { + if (itr.next().mVersion != sKernelWakelockUpdateVersion) { + itr.remove(); + } + } + } + + staleStats.kernelWakelockVersion = sKernelWakelockUpdateVersion; + return staleStats; + } + } +} diff --git a/core/java/com/android/internal/os/KernelWakelockStats.java b/core/java/com/android/internal/os/KernelWakelockStats.java new file mode 100644 index 0000000..144ea00 --- /dev/null +++ b/core/java/com/android/internal/os/KernelWakelockStats.java @@ -0,0 +1,37 @@ +/* + * 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 com.android.internal.os; + +import java.util.HashMap; + +/** + * Kernel wakelock stats object. + */ +public class KernelWakelockStats extends HashMap<String, KernelWakelockStats.Entry> { + public static class Entry { + public int mCount; + public long mTotalTime; + public int mVersion; + + Entry(int count, long totalTime, int version) { + mCount = count; + mTotalTime = totalTime; + mVersion = version; + } + } + + int kernelWakelockVersion; +} diff --git a/core/java/com/android/internal/os/Zygote.java b/core/java/com/android/internal/os/Zygote.java index 8674a21..75b6446 100644 --- a/core/java/com/android/internal/os/Zygote.java +++ b/core/java/com/android/internal/os/Zygote.java @@ -41,15 +41,10 @@ public final class Zygote { /** enable the JIT compiler */ public static final int DEBUG_ENABLE_JIT = 1 << 5; - /** No external storage should be mounted. */ public static final int MOUNT_EXTERNAL_NONE = 0; - /** Single-user external storage should be mounted. */ - public static final int MOUNT_EXTERNAL_SINGLEUSER = 1; - /** Multi-user external storage should be mounted. */ - public static final int MOUNT_EXTERNAL_MULTIUSER = 2; - /** All multi-user external storage should be mounted. */ - public static final int MOUNT_EXTERNAL_MULTIUSER_ALL = 3; + /** Default user-specific external storage should be mounted. */ + public static final int MOUNT_EXTERNAL_DEFAULT = 1; private static final ZygoteHooks VM_HOOKS = new ZygoteHooks(); diff --git a/core/java/com/android/internal/os/ZygoteConnection.java b/core/java/com/android/internal/os/ZygoteConnection.java index 4d405b2..9106ccd 100644 --- a/core/java/com/android/internal/os/ZygoteConnection.java +++ b/core/java/com/android/internal/os/ZygoteConnection.java @@ -514,10 +514,8 @@ class ZygoteConnection { "Duplicate arg specified"); } niceName = arg.substring(arg.indexOf('=') + 1); - } else if (arg.equals("--mount-external-multiuser")) { - mountExternal = Zygote.MOUNT_EXTERNAL_MULTIUSER; - } else if (arg.equals("--mount-external-multiuser-all")) { - mountExternal = Zygote.MOUNT_EXTERNAL_MULTIUSER_ALL; + } else if (arg.equals("--mount-external-default")) { + mountExternal = Zygote.MOUNT_EXTERNAL_DEFAULT; } else if (arg.equals("--query-abi-list")) { abiListQuery = true; } else if (arg.startsWith("--instruction-set=")) { diff --git a/core/java/com/android/internal/widget/FloatingToolbar.java b/core/java/com/android/internal/widget/FloatingToolbar.java new file mode 100644 index 0000000..be9945d --- /dev/null +++ b/core/java/com/android/internal/widget/FloatingToolbar.java @@ -0,0 +1,596 @@ +/* + * 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 com.android.internal.widget; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.content.Context; +import android.graphics.Color; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.drawable.ColorDrawable; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.View.MeasureSpec; +import android.view.ViewGroup; +import android.view.Window; +import android.widget.Button; +import android.widget.ImageButton; +import android.widget.LinearLayout; +import android.widget.PopupWindow; + +import com.android.internal.R; +import com.android.internal.util.Preconditions; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +/** + * A floating toolbar for showing contextual menu items. + * This view shows as many menu item buttons as can fit in the horizontal toolbar and the + * the remaining menu items in a vertical overflow view when the overflow button is clicked. + * The horizontal toolbar morphs into the vertical overflow view. + */ +public final class FloatingToolbar { + + private static final MenuItem.OnMenuItemClickListener NO_OP_MENUITEM_CLICK_LISTENER = + new MenuItem.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + return false; + } + }; + + private final Context mContext; + private final FloatingToolbarPopup mPopup; + private final ViewGroup mMenuItemButtonsContainer; + private final View.OnClickListener mMenuItemButtonOnClickListener = + new View.OnClickListener() { + @Override + public void onClick(View v) { + if (v.getTag() instanceof MenuItem) { + mMenuItemClickListener.onMenuItemClick((MenuItem) v.getTag()); + mPopup.dismiss(); + } + } + }; + + private final Rect mContentRect = new Rect(); + private final Point mCoordinates = new Point(); + + private Menu mMenu; + private List<CharSequence> mShowingTitles = new ArrayList<CharSequence>(); + private MenuItem.OnMenuItemClickListener mMenuItemClickListener = NO_OP_MENUITEM_CLICK_LISTENER; + private View mOpenOverflowButton; + + private int mSuggestedWidth; + + /** + * Initializes a floating toolbar. + */ + public FloatingToolbar(Context context, Window window) { + mContext = Preconditions.checkNotNull(context); + mPopup = new FloatingToolbarPopup(Preconditions.checkNotNull(window.getDecorView())); + mMenuItemButtonsContainer = createMenuButtonsContainer(context); + } + + /** + * Sets the menu to be shown in this floating toolbar. + * NOTE: Call {@link #updateLayout()} or {@link #show()} to effect visual changes to the + * toolbar. + */ + public FloatingToolbar setMenu(Menu menu) { + mMenu = Preconditions.checkNotNull(menu); + return this; + } + + /** + * Sets the custom listener for invocation of menu items in this floating + * toolbar. + */ + public FloatingToolbar setOnMenuItemClickListener( + MenuItem.OnMenuItemClickListener menuItemClickListener) { + if (menuItemClickListener != null) { + mMenuItemClickListener = menuItemClickListener; + } else { + mMenuItemClickListener = NO_OP_MENUITEM_CLICK_LISTENER; + } + return this; + } + + /** + * Sets the content rectangle. This is the area of the interesting content that this toolbar + * should avoid obstructing. + * NOTE: Call {@link #updateLayout()} or {@link #show()} to effect visual changes to the + * toolbar. + */ + public FloatingToolbar setContentRect(Rect rect) { + mContentRect.set(Preconditions.checkNotNull(rect)); + return this; + } + + /** + * Sets the suggested width of this floating toolbar. + * The actual width will be about this size but there are no guarantees that it will be exactly + * the suggested width. + * NOTE: Call {@link #updateLayout()} or {@link #show()} to effect visual changes to the + * toolbar. + */ + public FloatingToolbar setSuggestedWidth(int suggestedWidth) { + mSuggestedWidth = suggestedWidth; + return this; + } + + /** + * Shows this floating toolbar. + */ + public FloatingToolbar show() { + List<MenuItem> menuItems = getVisibleAndEnabledMenuItems(mMenu); + if (hasContentChanged(menuItems) || hasWidthChanged()) { + mPopup.dismiss(); + layoutMenuItemButtons(menuItems); + mShowingTitles = getMenuItemTitles(menuItems); + } + refreshCoordinates(); + mPopup.updateCoordinates(mCoordinates.x, mCoordinates.y); + if (!mPopup.isShowing()) { + mPopup.show(mCoordinates.x, mCoordinates.y); + } + return this; + } + + /** + * Updates this floating toolbar to reflect recent position and view updates. + * NOTE: This method is a no-op if the toolbar isn't showing. + */ + public FloatingToolbar updateLayout() { + if (mPopup.isShowing()) { + // show() performs all the logic we need here. + show(); + } + return this; + } + + /** + * Dismisses this floating toolbar. + */ + public void dismiss() { + mPopup.dismiss(); + } + + /** + * Returns {@code true} if this popup is currently showing. {@code false} otherwise. + */ + public boolean isShowing() { + return mPopup.isShowing(); + } + + /** + * Refreshes {@link #mCoordinates} with values based on {@link #mContentRect}. + */ + private void refreshCoordinates() { + int popupWidth = mPopup.getWidth(); + int popupHeight = mPopup.getHeight(); + if (!mPopup.isShowing()) { + // Popup isn't yet shown, get estimated size from the menu item buttons container. + mMenuItemButtonsContainer.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); + popupWidth = mMenuItemButtonsContainer.getMeasuredWidth(); + popupHeight = mMenuItemButtonsContainer.getMeasuredHeight(); + } + int x = mContentRect.centerX() - popupWidth / 2; + int y; + if (shouldDisplayAtTopOfContent()) { + y = mContentRect.top - popupHeight; + } else { + y = mContentRect.bottom; + } + mCoordinates.set(x, y); + } + + /** + * Returns true if this floating toolbar's menu items have been reordered or changed. + */ + private boolean hasContentChanged(List<MenuItem> menuItems) { + return !mShowingTitles.equals(getMenuItemTitles(menuItems)); + } + + /** + * Returns true if there is a significant change in width of the toolbar. + */ + private boolean hasWidthChanged() { + int actualWidth = mPopup.getWidth(); + int difference = Math.abs(actualWidth - mSuggestedWidth); + return difference > (actualWidth * 0.2); + } + + /** + * Returns true if the preferred positioning of the toolbar is above the content rect. + */ + private boolean shouldDisplayAtTopOfContent() { + return mContentRect.top - getMinimumOverflowHeight(mContext) > 0; + } + + /** + * Returns the visible and enabled menu items in the specified menu. + * This method is recursive. + */ + private List<MenuItem> getVisibleAndEnabledMenuItems(Menu menu) { + List<MenuItem> menuItems = new ArrayList<MenuItem>(); + for (int i = 0; (menu != null) && (i < menu.size()); i++) { + MenuItem menuItem = menu.getItem(i); + if (menuItem.isVisible() && menuItem.isEnabled()) { + Menu subMenu = menuItem.getSubMenu(); + if (subMenu != null) { + menuItems.addAll(getVisibleAndEnabledMenuItems(subMenu)); + } else { + menuItems.add(menuItem); + } + } + } + return menuItems; + } + + private List<CharSequence> getMenuItemTitles(List<MenuItem> menuItems) { + List<CharSequence> titles = new ArrayList<CharSequence>(); + for (MenuItem menuItem : menuItems) { + titles.add(menuItem.getTitle()); + } + return titles; + } + + private void layoutMenuItemButtons(List<MenuItem> menuItems) { + final int toolbarWidth = getAdjustedToolbarWidth(mContext, mSuggestedWidth) + // Reserve space for the "open overflow" button. + - getEstimatedOpenOverflowButtonWidth(mContext); + + int availableWidth = toolbarWidth; + LinkedList<MenuItem> remainingMenuItems = new LinkedList<MenuItem>(menuItems); + + mMenuItemButtonsContainer.removeAllViews(); + + boolean isFirstItem = true; + while (!remainingMenuItems.isEmpty()) { + final MenuItem menuItem = remainingMenuItems.peek(); + Button menuItemButton = createMenuItemButton(mContext, menuItem); + + // Adding additional left padding for the first button to even out button spacing. + if (isFirstItem) { + menuItemButton.setPadding( + 2 * menuItemButton.getPaddingLeft(), + menuItemButton.getPaddingTop(), + menuItemButton.getPaddingRight(), + menuItemButton.getPaddingBottom()); + isFirstItem = false; + } + + // Adding additional right padding for the last button to even out button spacing. + if (remainingMenuItems.size() == 1) { + menuItemButton.setPadding( + menuItemButton.getPaddingLeft(), + menuItemButton.getPaddingTop(), + 2 * menuItemButton.getPaddingRight(), + menuItemButton.getPaddingBottom()); + } + + menuItemButton.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); + int menuItemButtonWidth = Math.min(menuItemButton.getMeasuredWidth(), toolbarWidth); + if (menuItemButtonWidth <= availableWidth) { + menuItemButton.setTag(menuItem); + menuItemButton.setOnClickListener(mMenuItemButtonOnClickListener); + mMenuItemButtonsContainer.addView(menuItemButton); + menuItemButton.getLayoutParams().width = menuItemButtonWidth; + availableWidth -= menuItemButtonWidth; + remainingMenuItems.pop(); + } else { + // The "open overflow" button launches the vertical overflow from the + // floating toolbar. + createOpenOverflowButtonIfNotExists(); + mMenuItemButtonsContainer.addView(mOpenOverflowButton); + break; + } + } + mPopup.setContentView(mMenuItemButtonsContainer); + } + + /** + * Creates and returns the button that opens the vertical overflow. + */ + private void createOpenOverflowButtonIfNotExists() { + mOpenOverflowButton = (ImageButton) LayoutInflater.from(mContext) + .inflate(R.layout.floating_popup_open_overflow_button, null); + mOpenOverflowButton.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View v) { + // Open the overflow. + } + }); + } + + /** + * Creates and returns a floating toolbar menu buttons container. + */ + private static ViewGroup createMenuButtonsContainer(Context context) { + return (ViewGroup) LayoutInflater.from(context) + .inflate(R.layout.floating_popup_container, null); + } + + /** + * Creates and returns a menu button for the specified menu item. + */ + private static Button createMenuItemButton(Context context, MenuItem menuItem) { + Button menuItemButton = (Button) LayoutInflater.from(context) + .inflate(R.layout.floating_popup_menu_button, null); + menuItemButton.setText(menuItem.getTitle()); + menuItemButton.setContentDescription(menuItem.getTitle()); + return menuItemButton; + } + + private static int getMinimumOverflowHeight(Context context) { + return context.getResources(). + getDimensionPixelSize(R.dimen.floating_toolbar_minimum_overflow_height); + } + + private static int getEstimatedOpenOverflowButtonWidth(Context context) { + return context.getResources() + .getDimensionPixelSize(R.dimen.floating_toolbar_menu_button_minimum_width); + } + + private static int getAdjustedToolbarWidth(Context context, int width) { + if (width <= 0 || width > getScreenWidth(context)) { + width = context.getResources() + .getDimensionPixelSize(R.dimen.floating_toolbar_default_width); + } + return width; + } + + /** + * Returns the device's screen width. + */ + public static int getScreenWidth(Context context) { + return context.getResources().getDisplayMetrics().widthPixels; + } + + /** + * Returns the device's screen height. + */ + public static int getScreenHeight(Context context) { + return context.getResources().getDisplayMetrics().heightPixels; + } + + + /** + * A popup window used by the floating toolbar. + */ + private static final class FloatingToolbarPopup { + + private final View mParent; + private final PopupWindow mPopupWindow; + private final ViewGroup mContentContainer; + private final Animator.AnimatorListener mOnDismissEnd = + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mPopupWindow.dismiss(); + mDismissAnimating = false; + } + }; + private final AnimatorSet mGrowFadeInFromBottomAnimation; + private final AnimatorSet mShrinkFadeOutFromBottomAnimation; + + private boolean mDismissAnimating; + + /** + * Initializes a new floating bar popup. + * + * @param parent A parent view to get the {@link View#getWindowToken()} token from. + */ + public FloatingToolbarPopup(View parent) { + mParent = Preconditions.checkNotNull(parent); + mContentContainer = createContentContainer(parent.getContext()); + mPopupWindow = createPopupWindow(mContentContainer); + mGrowFadeInFromBottomAnimation = createGrowFadeInFromBottom(mContentContainer); + mShrinkFadeOutFromBottomAnimation = + createShrinkFadeOutFromBottomAnimation(mContentContainer, mOnDismissEnd); + } + + /** + * Shows this popup at the specified coordinates. + * The specified coordinates may be adjusted to make sure the popup is entirely on-screen. + * If this popup is already showing, this will be a no-op. + */ + public void show(int x, int y) { + if (isShowing()) { + updateCoordinates(x, y); + return; + } + + mPopupWindow.showAtLocation(mParent, Gravity.NO_GRAVITY, 0, 0); + positionOnScreen(x, y); + growFadeInFromBottom(); + + mDismissAnimating = false; + } + + /** + * Gets rid of this popup. If the popup isn't currently showing, this will be a no-op. + */ + public void dismiss() { + if (!isShowing()) { + return; + } + + if (mDismissAnimating) { + // This window is already dismissing. Don't restart the animation. + return; + } + mDismissAnimating = true; + shrinkFadeOutFromBottom(); + } + + /** + * Returns {@code true} if this popup is currently showing. {@code false} otherwise. + */ + public boolean isShowing() { + return mPopupWindow.isShowing() && !mDismissAnimating; + } + + /** + * Updates the coordinates of this popup. + * The specified coordinates may be adjusted to make sure the popup is entirely on-screen. + */ + public void updateCoordinates(int x, int y) { + if (isShowing()) { + positionOnScreen(x, y); + } + } + + /** + * Sets the content of this popup. + */ + public void setContentView(View view) { + Preconditions.checkNotNull(view); + mContentContainer.removeAllViews(); + mContentContainer.addView(view); + } + + /** + * Returns the width of this popup. + */ + public int getWidth() { + return mContentContainer.getWidth(); + } + + /** + * Returns the height of this popup. + */ + public int getHeight() { + return mContentContainer.getHeight(); + } + + /** + * Returns the context this popup is running in. + */ + public Context getContext() { + return mContentContainer.getContext(); + } + + private void positionOnScreen(int x, int y) { + if (getWidth() == 0) { + // content size is yet to be measured. + mContentContainer.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); + } + x = clamp(x, 0, getScreenWidth(getContext()) - getWidth()); + y = clamp(y, 0, getScreenHeight(getContext()) - getHeight()); + + // Position the view w.r.t. the window. + mContentContainer.setX(x); + mContentContainer.setY(y); + } + + /** + * Performs the "grow and fade in from the bottom" animation on the floating popup. + */ + private void growFadeInFromBottom() { + setPivot(); + mGrowFadeInFromBottomAnimation.start(); + } + + /** + * Performs the "shrink and fade out from bottom" animation on the floating popup. + */ + private void shrinkFadeOutFromBottom() { + setPivot(); + mShrinkFadeOutFromBottomAnimation.start(); + } + + /** + * Sets the popup content container's pivot. + */ + private void setPivot() { + mContentContainer.setPivotX(mContentContainer.getMeasuredWidth() / 2); + mContentContainer.setPivotY(mContentContainer.getMeasuredHeight()); + } + + private static ViewGroup createContentContainer(Context context) { + return (ViewGroup) LayoutInflater.from(context) + .inflate(R.layout.floating_popup_container, null); + } + + private static PopupWindow createPopupWindow(View content) { + ViewGroup popupContentHolder = new LinearLayout(content.getContext()); + PopupWindow popupWindow = new PopupWindow(popupContentHolder); + popupWindow.setAnimationStyle(0); + popupWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); + popupWindow.setWidth(getScreenWidth(content.getContext())); + popupWindow.setHeight(getScreenHeight(content.getContext())); + content.setLayoutParams(new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + popupContentHolder.addView(content); + return popupWindow; + } + + /** + * Creates a "grow and fade in from the bottom" animation for the specified view. + * + * @param view The view to animate + */ + private static AnimatorSet createGrowFadeInFromBottom(View view) { + AnimatorSet growFadeInFromBottomAnimation = new AnimatorSet(); + growFadeInFromBottomAnimation.playTogether( + ObjectAnimator.ofFloat(view, View.SCALE_X, 0.5f, 1).setDuration(125), + ObjectAnimator.ofFloat(view, View.SCALE_Y, 0.5f, 1).setDuration(125), + ObjectAnimator.ofFloat(view, View.ALPHA, 0, 1).setDuration(75)); + return growFadeInFromBottomAnimation; + } + + /** + * Creates a "shrink and fade out from bottom" animation for the specified view. + * + * @param view The view to animate + * @param listener The animation listener + */ + private static AnimatorSet createShrinkFadeOutFromBottomAnimation( + View view, Animator.AnimatorListener listener) { + AnimatorSet shrinkFadeOutFromBottomAnimation = new AnimatorSet(); + shrinkFadeOutFromBottomAnimation.playTogether( + ObjectAnimator.ofFloat(view, View.SCALE_Y, 1, 0.5f).setDuration(125), + ObjectAnimator.ofFloat(view, View.ALPHA, 1, 0).setDuration(75)); + shrinkFadeOutFromBottomAnimation.setStartDelay(150); + shrinkFadeOutFromBottomAnimation.addListener(listener); + return shrinkFadeOutFromBottomAnimation; + } + + /** + * Returns value, restricted to the range min->max (inclusive). + * If maximum is less than minimum, the result is undefined. + * + * @param value The value to clamp. + * @param minimum The minimum value in the range. + * @param maximum The maximum value in the range. Must not be less than minimum. + */ + private static int clamp(int value, int minimum, int maximum) { + return Math.max(minimum, Math.min(value, maximum)); + } + } +} diff --git a/core/java/com/android/internal/widget/ViewPager.java b/core/java/com/android/internal/widget/ViewPager.java index 8018942..8d66191 100644 --- a/core/java/com/android/internal/widget/ViewPager.java +++ b/core/java/com/android/internal/widget/ViewPager.java @@ -42,6 +42,7 @@ import android.view.ViewGroup; import android.view.ViewParent; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; import android.view.accessibility.AccessibilityRecord; import android.view.animation.Interpolator; import android.widget.EdgeEffect; @@ -371,8 +372,6 @@ public class ViewPager extends ViewGroup { mCloseEnough = (int) (CLOSE_ENOUGH * density); mDefaultGutterSize = (int) (DEFAULT_GUTTER_SIZE * density); - setAccessibilityDelegate(new MyAccessibilityDelegate()); - if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); } @@ -2695,29 +2694,6 @@ public class ViewPager extends ViewGroup { } @Override - public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { - // Dispatch scroll events from this ViewPager. - if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED) { - return super.dispatchPopulateAccessibilityEvent(event); - } - - // Dispatch all other accessibility events from the current page. - final int childCount = getChildCount(); - for (int i = 0; i < childCount; i++) { - final View child = getChildAt(i); - if (child.getVisibility() == VISIBLE) { - final ItemInfo ii = infoForChild(child); - if (ii != null && ii.position == mCurItem && - child.dispatchPopulateAccessibilityEvent(event)) { - return true; - } - } - } - - return false; - } - - @Override protected ViewGroup.LayoutParams generateDefaultLayoutParams() { return new LayoutParams(); } @@ -2737,60 +2713,63 @@ public class ViewPager extends ViewGroup { return new LayoutParams(getContext(), attrs); } - class MyAccessibilityDelegate extends AccessibilityDelegate { - @Override - public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { - super.onInitializeAccessibilityEvent(host, event); - event.setClassName(ViewPager.class.getName()); - final AccessibilityRecord record = AccessibilityRecord.obtain(); - record.setScrollable(canScroll()); - if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED - && mAdapter != null) { - record.setItemCount(mAdapter.getCount()); - record.setFromIndex(mCurItem); - record.setToIndex(mCurItem); - } + @Override + public void onInitializeAccessibilityEvent(AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(event); + + event.setClassName(ViewPager.class.getName()); + event.setScrollable(canScroll()); + + if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED && mAdapter != null) { + event.setItemCount(mAdapter.getCount()); + event.setFromIndex(mCurItem); + event.setToIndex(mCurItem); } + } - @Override - public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(host, info); - info.setClassName(ViewPager.class.getName()); - info.setScrollable(canScroll()); - if (canScrollHorizontally(1)) { - info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); - } - if (canScrollHorizontally(-1)) { - info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); - } + @Override + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(info); + + info.setClassName(ViewPager.class.getName()); + info.setScrollable(canScroll()); + + if (canScrollHorizontally(1)) { + info.addAction(AccessibilityAction.ACTION_SCROLL_FORWARD); } - @Override - public boolean performAccessibilityAction(View host, int action, Bundle args) { - if (super.performAccessibilityAction(host, action, args)) { - return true; - } - switch (action) { - case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: { - if (canScrollHorizontally(1)) { - setCurrentItem(mCurItem + 1); - return true; - } - } return false; - case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: { - if (canScrollHorizontally(-1)) { - setCurrentItem(mCurItem - 1); - return true; - } - } return false; - } - return false; + if (canScrollHorizontally(-1)) { + info.addAction(AccessibilityAction.ACTION_SCROLL_BACKWARD); } + } - private boolean canScroll() { - return (mAdapter != null) && (mAdapter.getCount() > 1); + @Override + public boolean performAccessibilityAction(int action, Bundle args) { + if (super.performAccessibilityAction(action, args)) { + return true; } + + switch (action) { + case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: + if (canScrollHorizontally(1)) { + setCurrentItem(mCurItem + 1); + return true; + } + return false; + case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: + if (canScrollHorizontally(-1)) { + setCurrentItem(mCurItem - 1); + return true; + } + return false; + } + + return false; + } + + private boolean canScroll() { + return mAdapter != null && mAdapter.getCount() > 1; } private class PagerObserver extends DataSetObserver { diff --git a/core/java/com/android/server/backup/SystemBackupAgent.java b/core/java/com/android/server/backup/SystemBackupAgent.java index b5f2f37..037fd66 100644 --- a/core/java/com/android/server/backup/SystemBackupAgent.java +++ b/core/java/com/android/server/backup/SystemBackupAgent.java @@ -104,9 +104,9 @@ public class SystemBackupAgent extends BackupAgentHelper { // steps during restore; the restore will happen properly when the individual // files are restored piecemeal. FullBackup.backupToTar(getPackageName(), FullBackup.ROOT_TREE_TOKEN, null, - WALLPAPER_INFO_DIR, WALLPAPER_INFO, output.getData()); + WALLPAPER_INFO_DIR, WALLPAPER_INFO, output); FullBackup.backupToTar(getPackageName(), FullBackup.ROOT_TREE_TOKEN, null, - WALLPAPER_IMAGE_DIR, WALLPAPER_IMAGE, output.getData()); + WALLPAPER_IMAGE_DIR, WALLPAPER_IMAGE, output); } @Override |
