Merge "Make data RAT icon test pass"
diff --git a/api/system-current.txt b/api/system-current.txt
index 50a8ea4..16404e4 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -4332,6 +4332,7 @@
public final class StatsManager {
method public boolean addConfiguration(java.lang.String, byte[], java.lang.String, java.lang.String);
method public byte[] getData(java.lang.String);
+ method public byte[] getMetadata();
method public boolean removeConfiguration(java.lang.String);
}
diff --git a/api/test-current.txt b/api/test-current.txt
index b181538..de0945b 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -457,19 +457,12 @@
method public void apply(android.service.autofill.ValueFinder, android.widget.RemoteViews, int) throws java.lang.Exception;
}
- public final class FieldsDetection implements android.os.Parcelable {
- ctor public FieldsDetection(android.view.autofill.AutofillId, java.lang.String, java.lang.String);
- method public int describeContents();
- method public void writeToParcel(android.os.Parcel, int);
- field public static final android.os.Parcelable.Creator<android.service.autofill.FieldsDetection> CREATOR;
- }
-
public static final class FillEventHistory.Event {
- method public java.util.Map<java.lang.String, java.lang.Integer> getDetectedFields();
+ method public java.util.Map<java.lang.String, java.lang.Integer> getFieldsClassification();
}
public static final class FillResponse.Builder {
- method public android.service.autofill.FillResponse.Builder setFieldsDetection(android.service.autofill.FieldsDetection);
+ method public android.service.autofill.FillResponse.Builder setFieldClassificationIds(android.view.autofill.AutofillId...);
}
public final class ImageTransformation extends android.service.autofill.InternalTransformation implements android.os.Parcelable android.service.autofill.Transformation {
@@ -501,6 +494,22 @@
method public android.view.autofill.AutofillValue sanitize(android.view.autofill.AutofillValue);
}
+ public final class UserData implements android.os.Parcelable {
+ method public int describeContents();
+ method public static int getMaxFieldClassificationIdsSize();
+ method public static int getMaxUserDataSize();
+ method public static int getMaxValueLength();
+ method public static int getMinValueLength();
+ method public void writeToParcel(android.os.Parcel, int);
+ field public static final android.os.Parcelable.Creator<android.service.autofill.UserData> CREATOR;
+ }
+
+ public static final class UserData.Builder {
+ ctor public UserData.Builder(java.lang.String, java.lang.String);
+ method public android.service.autofill.UserData.Builder add(java.lang.String, java.lang.String);
+ method public android.service.autofill.UserData build();
+ }
+
public abstract interface ValueFinder {
method public abstract java.lang.String findByAutofillId(android.view.autofill.AutofillId);
}
@@ -983,6 +992,11 @@
ctor public AutofillId(int);
}
+ public final class AutofillManager {
+ method public android.service.autofill.UserData getUserData();
+ method public void setUserData(android.service.autofill.UserData);
+ }
+
}
package android.widget {
diff --git a/cmds/statsd/src/StatsService.cpp b/cmds/statsd/src/StatsService.cpp
index eb3ea0b..0e9cd3b 100644
--- a/cmds/statsd/src/StatsService.cpp
+++ b/cmds/statsd/src/StatsService.cpp
@@ -715,6 +715,18 @@
}
}
+Status StatsService::getMetadata(vector<uint8_t>* output) {
+ IPCThreadState* ipc = IPCThreadState::self();
+ VLOG("StatsService::getMetadata with Pid %i, Uid %i", ipc->getCallingPid(),
+ ipc->getCallingUid());
+ if (checkCallingPermission(String16(kPermissionDump))) {
+ StatsdStats::getInstance().dumpStats(output, false); // Don't reset the counters.
+ return Status::ok();
+ } else {
+ return Status::fromExceptionCode(binder::Status::EX_SECURITY);
+ }
+}
+
Status StatsService::addConfiguration(const String16& key,
const vector <uint8_t>& config,
const String16& package, const String16& cls,
diff --git a/cmds/statsd/src/StatsService.h b/cmds/statsd/src/StatsService.h
index 007227e..03bc6d9 100644
--- a/cmds/statsd/src/StatsService.h
+++ b/cmds/statsd/src/StatsService.h
@@ -76,6 +76,11 @@
virtual Status getData(const String16& key, vector<uint8_t>* output) override;
/**
+ * Binder call for clients to get metadata across all configs in statsd.
+ */
+ virtual Status getMetadata(vector<uint8_t>* output) override;
+
+ /**
* Binder call to let clients send a configuration and indicate they're interested when they
* should requestData for this configuration.
*/
diff --git a/core/java/android/os/IStatsManager.aidl b/core/java/android/os/IStatsManager.aidl
index b814b46..c8c428e 100644
--- a/core/java/android/os/IStatsManager.aidl
+++ b/core/java/android/os/IStatsManager.aidl
@@ -69,11 +69,16 @@
/**
* Fetches data for the specified configuration key. Returns a byte array representing proto
- * wire-encoded of ConfigMetricsReport.
+ * wire-encoded of ConfigMetricsReportList.
*/
byte[] getData(in String key);
/**
+ * Fetches metadata across statsd. Returns byte array representing wire-encoded proto.
+ */
+ byte[] getMetadata();
+
+ /**
* Sets a configuration with the specified config key and subscribes to updates for this
* configuration key. Broadcasts will be sent if this configuration needs to be collected.
* The configuration must be a wire-encoded StatsDConfig. The caller specifies the name of the
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index c4c6798..53832c7 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -5333,6 +5333,42 @@
public static final String AUTOFILL_FEATURE_FIELD_DETECTION = "autofill_field_detection";
/**
+ * Experimental autofill feature.
+ *
+ * <p>TODO(b/67867469): document (or remove) once feature is finished
+ * @hide
+ */
+ public static final String AUTOFILL_USER_DATA_MAX_USER_DATA_SIZE =
+ "autofill_user_data_max_user_data_size";
+
+ /**
+ * Experimental autofill feature.
+ *
+ * <p>TODO(b/67867469): document (or remove) once feature is finished
+ * @hide
+ */
+ public static final String AUTOFILL_USER_DATA_MAX_FIELD_CLASSIFICATION_IDS_SIZE =
+ "autofill_user_data_max_field_classification_size";
+
+ /**
+ * Experimental autofill feature.
+ *
+ * <p>TODO(b/67867469): document (or remove) once feature is finished
+ * @hide
+ */
+ public static final String AUTOFILL_USER_DATA_MAX_VALUE_LENGTH =
+ "autofill_user_data_max_value_length";
+
+ /**
+ * Experimental autofill feature.
+ *
+ * <p>TODO(b/67867469): document (or remove) once feature is finished
+ * @hide
+ */
+ public static final String AUTOFILL_USER_DATA_MIN_VALUE_LENGTH =
+ "autofill_user_data_min_value_length";
+
+ /**
* @deprecated Use {@link android.provider.Settings.Global#DEVICE_PROVISIONED} instead
*/
@Deprecated
diff --git a/core/java/android/service/autofill/AutofillService.java b/core/java/android/service/autofill/AutofillService.java
index cd362c7..1afa8b3 100644
--- a/core/java/android/service/autofill/AutofillService.java
+++ b/core/java/android/service/autofill/AutofillService.java
@@ -440,7 +440,6 @@
* save(username, password);
* </pre>
*
- *
* <a name="Privacy"></a>
* <h3>Privacy</h3>
*
@@ -453,6 +452,13 @@
* <p>Because this data could contain PII (Personally Identifiable Information, such as username or
* email address), the service should only use it locally (i.e., in the app's process) for
* heuristics purposes, but it should not be sent to external servers.
+ *
+ * <a name="FieldsClassification"></a>
+ * <h3>Metrics and fields classification</h3
+ *
+ * <p>TODO(b/67867469): document it or remove this section; in particular, document the relationship
+ * between set/getUserData(), FillResponse.setFieldClassificationIds(), and
+ * FillEventHistory.getFieldsClassification.
*/
public abstract class AutofillService extends Service {
private static final String TAG = "AutofillService";
diff --git a/core/java/android/service/autofill/FieldsDetection.java b/core/java/android/service/autofill/FieldsDetection.java
deleted file mode 100644
index 550ecf6..0000000
--- a/core/java/android/service/autofill/FieldsDetection.java
+++ /dev/null
@@ -1,127 +0,0 @@
-/*
- * Copyright 2017 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.service.autofill;
-
-import android.annotation.TestApi;
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.view.autofill.AutofillId;
-
-/**
- * Class by service to improve autofillable fields detection by tracking the meaning of fields
- * manually edited by the user (when they match values provided by the service).
- *
- * TODO(b/67867469):
- * - proper javadoc
- * - unhide / remove testApi
- * - add FieldsDetection management so service can set it just once and reference it in further
- * calls to improve performance (and also API to refresh it)
- * - rename to FieldsDetectionInfo or FieldClassification? (same for CTS tests)
- * - add FieldsDetectionUnitTest once API is well-defined
- * @hide
- */
-@TestApi
-public final class FieldsDetection implements Parcelable {
-
- private final AutofillId mFieldId;
- private final String mRemoteId;
- private final String mValue;
-
- /**
- * Creates a field detection for just one field / value pair.
- *
- * @param fieldId autofill id of the field in the screen.
- * @param remoteId id used by the service to identify the field later.
- * @param value field value known to the service.
- *
- * TODO(b/67867469):
- * - proper javadoc
- * - change signature to allow more fields / values / match methods
- * - might also need to use a builder, where the constructor is the id for the fieldsdetector
- * - might need id for values as well
- * - add @NonNull / check it / add unit tests
- * - make 'value' input more generic so it can accept distance-based match and other matches
- * - throw exception if field value is less than X characters (somewhere between 7-10)
- * - make sure to limit total number of fields to around 10 or so
- * - use AutofillValue instead of String (so it can compare dates, for example)
- */
- public FieldsDetection(AutofillId fieldId, String remoteId, String value) {
- mFieldId = fieldId;
- mRemoteId = remoteId;
- mValue = value;
- }
-
- /** @hide */
- public AutofillId getFieldId() {
- return mFieldId;
- }
-
- /** @hide */
- public String getRemoteId() {
- return mRemoteId;
- }
-
- /** @hide */
- public String getValue() {
- return mValue;
- }
-
- /////////////////////////////////////
- // Object "contract" methods. //
- /////////////////////////////////////
- @Override
- public String toString() {
- // Cannot disclose remoteId or value because they could contain PII
- return new StringBuilder("FieldsDetection: [field=").append(mFieldId)
- .append(", remoteId_length=").append(mRemoteId.length())
- .append(", value_length=").append(mValue.length())
- .append("]").toString();
- }
-
- /////////////////////////////////////
- // Parcelable "contract" methods. //
- /////////////////////////////////////
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(Parcel parcel, int flags) {
- parcel.writeParcelable(mFieldId, flags);
- parcel.writeString(mRemoteId);
- parcel.writeString(mValue);
- }
-
- public static final Parcelable.Creator<FieldsDetection> CREATOR =
- new Parcelable.Creator<FieldsDetection>() {
- @Override
- public FieldsDetection createFromParcel(Parcel parcel) {
- // TODO(b/67867469): remove comment below if it does not use a builder at the end
- // Always go through the builder to ensure the data ingested by
- // the system obeys the contract of the builder to avoid attacks
- // using specially crafted parcels.
- return new FieldsDetection(parcel.readParcelable(null), parcel.readString(),
- parcel.readString());
- }
-
- @Override
- public FieldsDetection[] newArray(int size) {
- return new FieldsDetection[size];
- }
- };
-}
diff --git a/core/java/android/service/autofill/FillEventHistory.java b/core/java/android/service/autofill/FillEventHistory.java
index 736d9ef..eedb972 100644
--- a/core/java/android/service/autofill/FillEventHistory.java
+++ b/core/java/android/service/autofill/FillEventHistory.java
@@ -58,11 +58,6 @@
*/
public final class FillEventHistory implements Parcelable {
/**
- * Not in parcel. The UID of the {@link AutofillService} that created the {@link FillResponse}.
- */
- private final int mServiceUid;
-
- /**
* Not in parcel. The ID of the autofill session that created the {@link FillResponse}.
*/
private final int mSessionId;
@@ -70,17 +65,6 @@
@Nullable private final Bundle mClientState;
@Nullable List<Event> mEvents;
- /**
- * Gets the UID of the {@link AutofillService} that created the {@link FillResponse}.
- *
- * @return The UID of the {@link AutofillService}
- *
- * @hide
- */
- public int getServiceUid() {
- return mServiceUid;
- }
-
/** @hide */
public int getSessionId() {
return mSessionId;
@@ -123,9 +107,8 @@
/**
* @hide
*/
- public FillEventHistory(int serviceUid, int sessionId, @Nullable Bundle clientState) {
+ public FillEventHistory(int sessionId, @Nullable Bundle clientState) {
mClientState = clientState;
- mServiceUid = serviceUid;
mSessionId = sessionId;
}
@@ -364,16 +347,17 @@
}
/**
- * Gets the results of the last {@link FieldsDetection} request.
+ * Gets the results of the last fields classification request.
*
* @return map of edit-distance match ({@code 0} means full match,
- * {@code 1} means 1 character different, etc...) by remote id (as set in the
- * {@link FieldsDetection} constructor), or {@code null} if none of the user-input values
+ * {@code 1} means 1 character different, etc...) by remote id (as set on
+ * {@link UserData.Builder#add(String, android.view.autofill.AutofillValue)}),
+ * or {@code null} if none of the user-input values
* matched the requested detection.
*
* <p><b>Note: </b>Only set on events of type {@link #TYPE_CONTEXT_COMMITTED}, when the
- * service requested {@link FillResponse.Builder#setFieldsDetection(FieldsDetection) fields
- * detection}.
+ * service requested {@link FillResponse.Builder#setFieldClassificationIds(AutofillId...)
+ * fields detection}.
*
* TODO(b/67867469):
* - improve javadoc
@@ -382,11 +366,12 @@
* - unhide
* - unhide / remove testApi
* - add @NonNull / check it / add unit tests
+ * - add link to AutofillService #FieldsClassification anchor
*
* @hide
*/
@TestApi
- @NonNull public Map<String, Integer> getDetectedFields() {
+ @NonNull public Map<String, Integer> getFieldsClassification() {
if (mDetectedRemoteId == null || mDetectedFieldScore == -1) {
return Collections.emptyMap();
}
@@ -534,7 +519,7 @@
new Parcelable.Creator<FillEventHistory>() {
@Override
public FillEventHistory createFromParcel(Parcel parcel) {
- FillEventHistory selection = new FillEventHistory(0, 0, parcel.readBundle());
+ FillEventHistory selection = new FillEventHistory(0, parcel.readBundle());
final int numEvents = parcel.readInt();
for (int i = 0; i < numEvents; i++) {
diff --git a/core/java/android/service/autofill/FillResponse.java b/core/java/android/service/autofill/FillResponse.java
index 84a0974..dff40ff 100644
--- a/core/java/android/service/autofill/FillResponse.java
+++ b/core/java/android/service/autofill/FillResponse.java
@@ -76,7 +76,7 @@
private final @Nullable AutofillId[] mAuthenticationIds;
private final @Nullable AutofillId[] mIgnoredIds;
private final long mDisableDuration;
- private final @Nullable FieldsDetection mFieldsDetection;
+ private final @Nullable AutofillId[] mFieldClassificationIds;
private final int mFlags;
private int mRequestId;
@@ -89,7 +89,7 @@
mAuthenticationIds = builder.mAuthenticationIds;
mIgnoredIds = builder.mIgnoredIds;
mDisableDuration = builder.mDisableDuration;
- mFieldsDetection = builder.mFieldsDetection;
+ mFieldClassificationIds = builder.mFieldClassificationIds;
mFlags = builder.mFlags;
mRequestId = INVALID_REQUEST_ID;
}
@@ -135,8 +135,8 @@
}
/** @hide */
- public @Nullable FieldsDetection getFieldsDetection() {
- return mFieldsDetection;
+ public @Nullable AutofillId[] getFieldClassificationIds() {
+ return mFieldClassificationIds;
}
/** @hide */
@@ -175,7 +175,7 @@
private AutofillId[] mAuthenticationIds;
private AutofillId[] mIgnoredIds;
private long mDisableDuration;
- private FieldsDetection mFieldsDetection;
+ private AutofillId[] mFieldClassificationIds;
private int mFlags;
private boolean mDestroyed;
@@ -329,21 +329,29 @@
}
/**
+ * Sets which fields are used for <a href="#FieldsClassification">fields classification</a>
+ *
+ * @throws IllegalArgumentException is length of {@code ids} args is more than
+ * {@link UserData#getMaxFieldClassificationIdsSize()}.
+ * @throws IllegalStateException if {@link #build()} or {@link #disableAutofill(long)} was
+ * already called.
+ * @throws NullPointerException if {@code ids} or any element on it is {@code null}.
+ *
* TODO(b/67867469):
- * - javadoc it
- * - javadoc how to check results
- * - unhide
+ * - improve javadoc: explain relationship with UserData and how to check results
* - unhide / remove testApi
- * - throw exception (and document) if response has datasets or saveinfo
- * - throw exception (and document) if id on fieldsDetection is ignored
+ * - implement multiple ids
*
* @hide
*/
@TestApi
- public Builder setFieldsDetection(@NonNull FieldsDetection fieldsDetection) {
+ public Builder setFieldClassificationIds(@NonNull AutofillId... ids) {
throwIfDestroyed();
throwIfDisableAutofillCalled();
- mFieldsDetection = Preconditions.checkNotNull(fieldsDetection);
+ Preconditions.checkArrayElementsNotNull(ids, "ids");
+ Preconditions.checkArgumentInRange(ids.length, 1,
+ UserData.getMaxFieldClassificationIdsSize(), "ids length");
+ mFieldClassificationIds = ids;
return this;
}
@@ -391,16 +399,17 @@
* @throws IllegalArgumentException if {@code duration} is not a positive number.
* @throws IllegalStateException if either {@link #addDataset(Dataset)},
* {@link #setAuthentication(AutofillId[], IntentSender, RemoteViews)},
- * {@link #setSaveInfo(SaveInfo)}, or {@link #setClientState(Bundle)}
- * was already called.
+ * {@link #setSaveInfo(SaveInfo)}, {@link #setClientState(Bundle)}, or
+ * {link #setFieldClassificationIds(AutofillId...)} was already called.
*/
+ // TODO(b/67867469): add @ to {link setFieldClassificationIds} once it's public
public Builder disableAutofill(long duration) {
throwIfDestroyed();
if (duration <= 0) {
throw new IllegalArgumentException("duration must be greater than 0");
}
if (mAuthentication != null || mDatasets != null || mSaveInfo != null
- || mFieldsDetection != null || mClientState != null) {
+ || mFieldClassificationIds != null || mClientState != null) {
throw new IllegalStateException("disableAutofill() must be the only method called");
}
@@ -417,15 +426,18 @@
* <li>No call was made to {@link #addDataset(Dataset)},
* {@link #setAuthentication(AutofillId[], IntentSender, RemoteViews)},
* {@link #setSaveInfo(SaveInfo)}, {@link #disableAutofill(long)},
- * or {@link #setClientState(Bundle)}.
+ * {@link #setClientState(Bundle)},
+ * or {link #setFieldClassificationIds(AutofillId...)}.
* </ol>
*
* @return A built response.
*/
+ // TODO(b/67867469): add @ to {link setFieldClassificationIds} once it's public
public FillResponse build() {
throwIfDestroyed();
if (mAuthentication == null && mDatasets == null && mSaveInfo == null
- && mDisableDuration == 0 && mFieldsDetection == null && mClientState == null) {
+ && mDisableDuration == 0 && mFieldClassificationIds == null
+ && mClientState == null) {
throw new IllegalStateException("need to provide: at least one DataSet, or a "
+ "SaveInfo, or an authentication with a presentation, "
+ "or a FieldsDetection, or a client state, or disable autofill");
@@ -466,7 +478,8 @@
.append(", ignoredIds=").append(Arrays.toString(mIgnoredIds))
.append(", disableDuration=").append(mDisableDuration)
.append(", flags=").append(mFlags)
- .append(", fieldDetection=").append(mFieldsDetection)
+ .append(", fieldClassificationIds=")
+ .append(Arrays.toString(mFieldClassificationIds))
.append("]")
.toString();
}
@@ -490,7 +503,7 @@
parcel.writeParcelable(mPresentation, flags);
parcel.writeParcelableArray(mIgnoredIds, flags);
parcel.writeLong(mDisableDuration);
- parcel.writeParcelable(mFieldsDetection, flags);
+ parcel.writeParcelableArray(mFieldClassificationIds, flags);
parcel.writeInt(mFlags);
parcel.writeInt(mRequestId);
}
@@ -526,9 +539,10 @@
if (disableDuration > 0) {
builder.disableAutofill(disableDuration);
}
- final FieldsDetection fieldsDetection = parcel.readParcelable(null);
- if (fieldsDetection != null) {
- builder.setFieldsDetection(fieldsDetection);
+ final AutofillId[] fieldClassifactionIds =
+ parcel.readParcelableArray(null, AutofillId.class);
+ if (fieldClassifactionIds != null) {
+ builder.setFieldClassificationIds(fieldClassifactionIds);
}
builder.setFlags(parcel.readInt());
diff --git a/core/java/android/service/autofill/UserData.aidl b/core/java/android/service/autofill/UserData.aidl
new file mode 100644
index 0000000..76016de
--- /dev/null
+++ b/core/java/android/service/autofill/UserData.aidl
@@ -0,0 +1,20 @@
+/**
+ * Copyright (c) 2017, 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.service.autofill;
+
+parcelable UserData;
+parcelable UserData.Constraints;
diff --git a/core/java/android/service/autofill/UserData.java b/core/java/android/service/autofill/UserData.java
new file mode 100644
index 0000000..16d8d4a
--- /dev/null
+++ b/core/java/android/service/autofill/UserData.java
@@ -0,0 +1,288 @@
+/*
+ * Copyright 2017 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.service.autofill;
+
+import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MAX_FIELD_CLASSIFICATION_IDS_SIZE;
+import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MAX_USER_DATA_SIZE;
+import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MAX_VALUE_LENGTH;
+import static android.provider.Settings.Secure.AUTOFILL_USER_DATA_MIN_VALUE_LENGTH;
+import static android.view.autofill.Helper.sDebug;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.TestApi;
+import android.app.ActivityThread;
+import android.content.ContentResolver;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.provider.Settings;
+import android.util.ArraySet;
+import android.util.Log;
+import android.view.autofill.Helper;
+
+import com.android.internal.util.Preconditions;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+
+/**
+ * Class used by service to improve autofillable fields detection by tracking the meaning of fields
+ * manually edited by the user (when they match values provided by the service).
+ *
+ * TODO(b/67867469):
+ * - improve javadoc / add link to section on AutofillService
+ * - unhide / remove testApi
+ * @hide
+ */
+@TestApi
+public final class UserData implements Parcelable {
+
+ private static final String TAG = "UserData";
+
+ private static final int DEFAULT_MAX_USER_DATA_SIZE = 10;
+ private static final int DEFAULT_MAX_FIELD_CLASSIFICATION_IDS_SIZE = 10;
+ private static final int DEFAULT_MIN_VALUE_LENGTH = 5;
+ private static final int DEFAULT_MAX_VALUE_LENGTH = 100;
+
+ private final String[] mRemoteIds;
+ private final String[] mValues;
+
+ private UserData(Builder builder) {
+ mRemoteIds = new String[builder.mRemoteIds.size()];
+ builder.mRemoteIds.toArray(mRemoteIds);
+ mValues = new String[builder.mValues.size()];
+ builder.mValues.toArray(mValues);
+ }
+
+ /** @hide */
+ public String[] getRemoteIds() {
+ return mRemoteIds;
+ }
+
+ /** @hide */
+ public String[] getValues() {
+ return mValues;
+ }
+
+ /** @hide */
+ public void dump(String prefix, PrintWriter pw) {
+ // Cannot disclose remote ids because they could contain PII
+ pw.print(prefix); pw.print("Remote ids size: "); pw.println(mRemoteIds.length);
+ for (int i = 0; i < mValues.length; i++) {
+ pw.print(prefix); pw.print(prefix); pw.print(i); pw.print(": "); pw.println(mValues[i]);
+ }
+ }
+
+ /** @hide */
+ public static void dumpConstraints(String prefix, PrintWriter pw) {
+ pw.print(prefix); pw.print("maxUserDataSize: "); pw.println(getMaxUserDataSize());
+ pw.print(prefix); pw.print("maxFieldClassificationIdsSize: ");
+ pw.println(getMaxFieldClassificationIdsSize());
+ pw.print(prefix); pw.print("minValueLength: "); pw.println(getMinValueLength());
+ pw.print(prefix); pw.print("maxValueLength: "); pw.println(getMaxValueLength());
+ }
+
+ /**
+ * A builder for {@link UserData} objects.
+ *
+ * TODO(b/67867469): unhide / remove testApi
+ *
+ * @hide
+ */
+ @TestApi
+ public static final class Builder {
+ private final ArraySet<String> mRemoteIds;
+ private final ArrayList<String> mValues;
+ private boolean mDestroyed;
+
+ /**
+ * Creates a new builder for the user data used for <a href="#FieldsClassification">fields
+ * classification</a>.
+ *
+ * @throws IllegalArgumentException if {@code remoteId} or {@code value} are empty or if the
+ * length of {@code value} is lower than {@link UserData#getMinValueLength()}
+ * or higher than {@link UserData#getMaxValueLength()}.
+ */
+ public Builder(@NonNull String remoteId, @NonNull String value) {
+ checkValidRemoteId(remoteId);
+ checkValidValue(value);
+ final int capacity = getMaxUserDataSize();
+ mRemoteIds = new ArraySet<>(capacity);
+ mValues = new ArrayList<>(capacity);
+ mRemoteIds.add(remoteId);
+ mValues.add(value);
+ }
+
+ /**
+ * Adds a new value for user data.
+ *
+ * @param remoteId unique string used to identify the user data.
+ * @param value value of the user data.
+ *
+ * @throws IllegalStateException if {@link #build()} or
+ * {@link #add(String, String)} with the same {@code remoteId} has already
+ * been called, or if the number of values add (i.e., calls made to this method plus
+ * constructor) is more than {@link UserData#getMaxUserDataSize()}.
+ *
+ * @throws IllegalArgumentException if {@code remoteId} or {@code value} are empty or if the
+ * length of {@code value} is lower than {@link UserData#getMinValueLength()}
+ * or higher than {@link UserData#getMaxValueLength()}.
+ */
+ public Builder add(@NonNull String remoteId, @NonNull String value) {
+ throwIfDestroyed();
+ checkValidRemoteId(remoteId);
+ checkValidValue(value);
+
+ Preconditions.checkState(!mRemoteIds.contains(remoteId),
+ // Don't include remoteId on message because it could contain PII
+ "already has entry with same remoteId");
+ Preconditions.checkState(mRemoteIds.size() < getMaxUserDataSize(),
+ "already added " + mRemoteIds.size() + " elements");
+ mRemoteIds.add(remoteId);
+ mValues.add(value);
+ return this;
+ }
+
+ private void checkValidRemoteId(@Nullable String remoteId) {
+ Preconditions.checkNotNull(remoteId);
+ Preconditions.checkArgument(!remoteId.isEmpty(), "remoteId cannot be empty");
+ }
+
+ private void checkValidValue(@Nullable String value) {
+ Preconditions.checkNotNull(value);
+ final int length = value.length();
+ Preconditions.checkArgumentInRange(length, getMinValueLength(),
+ getMaxValueLength(), "value length (" + length + ")");
+ }
+
+ /**
+ * Creates a new {@link UserData} instance.
+ *
+ * <p>You should not interact with this builder once this method is called.
+ *
+ * @throws IllegalStateException if {@link #build()} was already called.
+ *
+ * @return The built dataset.
+ */
+ public UserData build() {
+ throwIfDestroyed();
+ mDestroyed = true;
+ return new UserData(this);
+ }
+
+ private void throwIfDestroyed() {
+ if (mDestroyed) {
+ throw new IllegalStateException("Already called #build()");
+ }
+ }
+ }
+
+ /////////////////////////////////////
+ // Object "contract" methods. //
+ /////////////////////////////////////
+ @Override
+ public String toString() {
+ if (!sDebug) return super.toString();
+
+ // Cannot disclose keys or values because they could contain PII
+ final StringBuilder builder = new StringBuilder("UserData: [remoteIds=");
+ Helper.appendRedacted(builder, mRemoteIds);
+ builder.append(", values=");
+ Helper.appendRedacted(builder, mValues);
+ return builder.append("]").toString();
+ }
+
+ /////////////////////////////////////
+ // Parcelable "contract" methods. //
+ /////////////////////////////////////
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeStringArray(mRemoteIds);
+ parcel.writeStringArray(mValues);
+ }
+
+ public static final Parcelable.Creator<UserData> CREATOR =
+ new Parcelable.Creator<UserData>() {
+ @Override
+ public UserData createFromParcel(Parcel parcel) {
+ // Always go through the builder to ensure the data ingested by
+ // the system obeys the contract of the builder to avoid attacks
+ // using specially crafted parcels.
+ final String[] remoteIds = parcel.readStringArray();
+ final String[] values = parcel.readStringArray();
+ final Builder builder = new Builder(remoteIds[0], values[0]);
+ for (int i = 1; i < remoteIds.length; i++) {
+ builder.add(remoteIds[i], values[i]);
+ }
+ return builder.build();
+ }
+
+ @Override
+ public UserData[] newArray(int size) {
+ return new UserData[size];
+ }
+ };
+
+ /**
+ * Gets the maximum number of values that can be added to a {@link UserData}.
+ */
+ public static int getMaxUserDataSize() {
+ return getInt(AUTOFILL_USER_DATA_MAX_USER_DATA_SIZE, DEFAULT_MAX_USER_DATA_SIZE);
+ }
+
+ /**
+ * Gets the maximum number of ids that can be passed to {@link
+ * FillResponse.Builder#setFieldClassificationIds(android.view.autofill.AutofillId...)}.
+ */
+ public static int getMaxFieldClassificationIdsSize() {
+ return getInt(AUTOFILL_USER_DATA_MAX_FIELD_CLASSIFICATION_IDS_SIZE,
+ DEFAULT_MAX_FIELD_CLASSIFICATION_IDS_SIZE);
+ }
+
+ /**
+ * Gets the minimum length of values passed to {@link Builder#Builder(String, String)}.
+ */
+ public static int getMinValueLength() {
+ return getInt(AUTOFILL_USER_DATA_MIN_VALUE_LENGTH, DEFAULT_MIN_VALUE_LENGTH);
+ }
+
+ /**
+ * Gets the maximum length of values passed to {@link Builder#Builder(String, String)}.
+ */
+ public static int getMaxValueLength() {
+ return getInt(AUTOFILL_USER_DATA_MAX_VALUE_LENGTH, DEFAULT_MAX_VALUE_LENGTH);
+ }
+
+ private static int getInt(String settings, int defaultValue) {
+ ContentResolver cr = null;
+ final ActivityThread at = ActivityThread.currentActivityThread();
+ if (at != null) {
+ cr = at.getApplication().getContentResolver();
+ }
+
+ if (cr == null) {
+ Log.w(TAG, "Could not read from " + settings + "; hardcoding " + defaultValue);
+ return defaultValue;
+ }
+ return Settings.Secure.getInt(cr, settings, defaultValue);
+ }
+}
diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java
index 735b822..1ec2406 100644
--- a/core/java/android/service/notification/ZenModeConfig.java
+++ b/core/java/android/service/notification/ZenModeConfig.java
@@ -890,7 +890,17 @@
}
public static boolean isValidScheduleConditionId(Uri conditionId) {
- return tryParseScheduleConditionId(conditionId) != null;
+ ScheduleInfo info;
+ try {
+ info = tryParseScheduleConditionId(conditionId);
+ } catch (NullPointerException | ArrayIndexOutOfBoundsException e) {
+ return false;
+ }
+
+ if (info == null || info.days == null || info.days.length == 0) {
+ return false;
+ }
+ return true;
}
public static ScheduleInfo tryParseScheduleConditionId(Uri conditionId) {
diff --git a/core/java/android/text/AutoGrowArray.java b/core/java/android/text/AutoGrowArray.java
deleted file mode 100644
index e428377..0000000
--- a/core/java/android/text/AutoGrowArray.java
+++ /dev/null
@@ -1,374 +0,0 @@
-/*
- * Copyright (C) 2017 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.annotation.IntRange;
-import android.annotation.NonNull;
-
-import com.android.internal.util.ArrayUtils;
-
-import libcore.util.EmptyArray;
-
-/**
- * Implements a growing array of int primitives.
- *
- * These arrays are NOT thread safe.
- *
- * @hide
- */
-public final class AutoGrowArray {
- private static final int MIN_CAPACITY_INCREMENT = 12;
- private static final int MAX_CAPACITY_TO_BE_KEPT = 10000;
-
- /**
- * Returns next capacity size.
- *
- * The returned capacity is larger than requested capacity.
- */
- private static int computeNewCapacity(int currentSize, int requested) {
- final int targetCapacity = currentSize + (currentSize < (MIN_CAPACITY_INCREMENT / 2)
- ? MIN_CAPACITY_INCREMENT : currentSize >> 1);
- return targetCapacity > requested ? targetCapacity : requested;
- }
-
- /**
- * An auto growing byte array.
- */
- public static class ByteArray {
-
- private @NonNull byte[] mValues;
- private @IntRange(from = 0) int mSize;
-
- /**
- * Creates an empty ByteArray with the default initial capacity.
- */
- public ByteArray() {
- this(10);
- }
-
- /**
- * Creates an empty ByteArray with the specified initial capacity.
- */
- public ByteArray(@IntRange(from = 0) int initialCapacity) {
- if (initialCapacity == 0) {
- mValues = EmptyArray.BYTE;
- } else {
- mValues = ArrayUtils.newUnpaddedByteArray(initialCapacity);
- }
- mSize = 0;
- }
-
- /**
- * Changes the size of this ByteArray. If this ByteArray is shrinked, the backing array
- * capacity is unchanged.
- */
- public void resize(@IntRange(from = 0) int newSize) {
- if (newSize > mValues.length) {
- ensureCapacity(newSize - mSize);
- }
- mSize = newSize;
- }
-
- /**
- * Appends the specified value to the end of this array.
- */
- public void append(byte value) {
- ensureCapacity(1);
- mValues[mSize++] = value;
- }
-
- /**
- * Ensures capacity to append at least <code>count</code> values.
- */
- private void ensureCapacity(@IntRange int count) {
- final int requestedSize = mSize + count;
- if (requestedSize >= mValues.length) {
- final int newCapacity = computeNewCapacity(mSize, requestedSize);
- final byte[] newValues = ArrayUtils.newUnpaddedByteArray(newCapacity);
- System.arraycopy(mValues, 0, newValues, 0, mSize);
- mValues = newValues;
- }
- }
-
- /**
- * Removes all values from this array.
- */
- public void clear() {
- mSize = 0;
- }
-
- /**
- * Removes all values from this array and release the internal array object if it is too
- * large.
- */
- public void clearWithReleasingLargeArray() {
- clear();
- if (mValues.length > MAX_CAPACITY_TO_BE_KEPT) {
- mValues = EmptyArray.BYTE;
- }
- }
-
- /**
- * Returns the value at the specified position in this array.
- */
- public byte get(@IntRange(from = 0) int index) {
- return mValues[index];
- }
-
- /**
- * Sets the value at the specified position in this array.
- */
- public void set(@IntRange(from = 0) int index, byte value) {
- mValues[index] = value;
- }
-
- /**
- * Returns the number of values in this array.
- */
- public @IntRange(from = 0) int size() {
- return mSize;
- }
-
- /**
- * Returns internal raw array.
- *
- * Note that this array may have larger size than you requested.
- * Use size() instead for getting the actual array size.
- */
- public @NonNull byte[] getRawArray() {
- return mValues;
- }
- }
-
- /**
- * An auto growing int array.
- */
- public static class IntArray {
-
- private @NonNull int[] mValues;
- private @IntRange(from = 0) int mSize;
-
- /**
- * Creates an empty IntArray with the default initial capacity.
- */
- public IntArray() {
- this(10);
- }
-
- /**
- * Creates an empty IntArray with the specified initial capacity.
- */
- public IntArray(@IntRange(from = 0) int initialCapacity) {
- if (initialCapacity == 0) {
- mValues = EmptyArray.INT;
- } else {
- mValues = ArrayUtils.newUnpaddedIntArray(initialCapacity);
- }
- mSize = 0;
- }
-
- /**
- * Changes the size of this IntArray. If this IntArray is shrinked, the backing array
- * capacity is unchanged.
- */
- public void resize(@IntRange(from = 0) int newSize) {
- if (newSize > mValues.length) {
- ensureCapacity(newSize - mSize);
- }
- mSize = newSize;
- }
-
- /**
- * Appends the specified value to the end of this array.
- */
- public void append(int value) {
- ensureCapacity(1);
- mValues[mSize++] = value;
- }
-
- /**
- * Ensures capacity to append at least <code>count</code> values.
- */
- private void ensureCapacity(@IntRange(from = 0) int count) {
- final int requestedSize = mSize + count;
- if (requestedSize >= mValues.length) {
- final int newCapacity = computeNewCapacity(mSize, requestedSize);
- final int[] newValues = ArrayUtils.newUnpaddedIntArray(newCapacity);
- System.arraycopy(mValues, 0, newValues, 0, mSize);
- mValues = newValues;
- }
- }
-
- /**
- * Removes all values from this array.
- */
- public void clear() {
- mSize = 0;
- }
-
- /**
- * Removes all values from this array and release the internal array object if it is too
- * large.
- */
- public void clearWithReleasingLargeArray() {
- clear();
- if (mValues.length > MAX_CAPACITY_TO_BE_KEPT) {
- mValues = EmptyArray.INT;
- }
- }
-
- /**
- * Returns the value at the specified position in this array.
- */
- public int get(@IntRange(from = 0) int index) {
- return mValues[index];
- }
-
- /**
- * Sets the value at the specified position in this array.
- */
- public void set(@IntRange(from = 0) int index, int value) {
- mValues[index] = value;
- }
-
- /**
- * Returns the number of values in this array.
- */
- public @IntRange(from = 0) int size() {
- return mSize;
- }
-
- /**
- * Returns internal raw array.
- *
- * Note that this array may have larger size than you requested.
- * Use size() instead for getting the actual array size.
- */
- public @NonNull int[] getRawArray() {
- return mValues;
- }
- }
-
- /**
- * An auto growing float array.
- */
- public static class FloatArray {
-
- private @NonNull float[] mValues;
- private @IntRange(from = 0) int mSize;
-
- /**
- * Creates an empty FloatArray with the default initial capacity.
- */
- public FloatArray() {
- this(10);
- }
-
- /**
- * Creates an empty FloatArray with the specified initial capacity.
- */
- public FloatArray(@IntRange(from = 0) int initialCapacity) {
- if (initialCapacity == 0) {
- mValues = EmptyArray.FLOAT;
- } else {
- mValues = ArrayUtils.newUnpaddedFloatArray(initialCapacity);
- }
- mSize = 0;
- }
-
- /**
- * Changes the size of this FloatArray. If this FloatArray is shrinked, the backing array
- * capacity is unchanged.
- */
- public void resize(@IntRange(from = 0) int newSize) {
- if (newSize > mValues.length) {
- ensureCapacity(newSize - mSize);
- }
- mSize = newSize;
- }
-
- /**
- * Appends the specified value to the end of this array.
- */
- public void append(float value) {
- ensureCapacity(1);
- mValues[mSize++] = value;
- }
-
- /**
- * Ensures capacity to append at least <code>count</code> values.
- */
- private void ensureCapacity(int count) {
- final int requestedSize = mSize + count;
- if (requestedSize >= mValues.length) {
- final int newCapacity = computeNewCapacity(mSize, requestedSize);
- final float[] newValues = ArrayUtils.newUnpaddedFloatArray(newCapacity);
- System.arraycopy(mValues, 0, newValues, 0, mSize);
- mValues = newValues;
- }
- }
-
- /**
- * Removes all values from this array.
- */
- public void clear() {
- mSize = 0;
- }
-
- /**
- * Removes all values from this array and release the internal array object if it is too
- * large.
- */
- public void clearWithReleasingLargeArray() {
- clear();
- if (mValues.length > MAX_CAPACITY_TO_BE_KEPT) {
- mValues = EmptyArray.FLOAT;
- }
- }
-
- /**
- * Returns the value at the specified position in this array.
- */
- public float get(@IntRange(from = 0) int index) {
- return mValues[index];
- }
-
- /**
- * Sets the value at the specified position in this array.
- */
- public void set(@IntRange(from = 0) int index, float value) {
- mValues[index] = value;
- }
-
- /**
- * Returns the number of values in this array.
- */
- public @IntRange(from = 0) int size() {
- return mSize;
- }
-
- /**
- * Returns internal raw array.
- *
- * Note that this array may have larger size than you requested.
- * Use size() instead for getting the actual array size.
- */
- public @NonNull float[] getRawArray() {
- return mValues;
- }
- }
-}
diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java
index 2a693a1..4d2a962 100644
--- a/core/java/android/text/Layout.java
+++ b/core/java/android/text/Layout.java
@@ -1907,14 +1907,22 @@
private static float measurePara(TextPaint paint, CharSequence text, int start, int end,
TextDirectionHeuristic textDir) {
- MeasuredText mt = null;
+ MeasuredText mt = MeasuredText.obtain();
TextLine tl = TextLine.obtain();
try {
- mt = MeasuredText.buildForBidi(text, start, end, textDir, mt);
- final char[] chars = mt.getChars();
- final int len = chars.length;
- final Directions directions = mt.getDirections(0, len);
- final int dir = mt.getParagraphDir();
+ mt.setPara(text, start, end, textDir);
+ Directions directions;
+ int dir;
+ if (mt.mEasy) {
+ directions = DIRS_ALL_LEFT_TO_RIGHT;
+ dir = Layout.DIR_LEFT_TO_RIGHT;
+ } else {
+ directions = AndroidBidi.directions(mt.mDir, mt.mLevels,
+ 0, mt.mChars, 0, mt.mLen);
+ dir = mt.mDir;
+ }
+ char[] chars = mt.mChars;
+ int len = mt.mLen;
boolean hasTabs = false;
TabStops tabStops = null;
// leading margins should be taken into account when measuring a paragraph
@@ -1947,9 +1955,7 @@
return margin + Math.abs(tl.metrics(null));
} finally {
TextLine.recycle(tl);
- if (mt != null) {
- mt.recycle();
- }
+ MeasuredText.recycle(mt);
}
}
@@ -2266,11 +2272,6 @@
private SpanSet<LineBackgroundSpan> mLineBackgroundSpans;
private int mJustificationMode;
- /** @hide */
- @IntDef({DIR_LEFT_TO_RIGHT, DIR_RIGHT_TO_LEFT})
- @Retention(RetentionPolicy.SOURCE)
- public @interface Direction {}
-
public static final int DIR_LEFT_TO_RIGHT = 1;
public static final int DIR_RIGHT_TO_LEFT = -1;
diff --git a/core/java/android/text/MeasuredText.java b/core/java/android/text/MeasuredText.java
index ca31176..3d9fba7 100644
--- a/core/java/android/text/MeasuredText.java
+++ b/core/java/android/text/MeasuredText.java
@@ -16,384 +16,125 @@
package android.text;
-import android.annotation.FloatRange;
-import android.annotation.IntRange;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
import android.graphics.Paint;
-import android.text.AutoGrowArray.ByteArray;
-import android.text.AutoGrowArray.FloatArray;
-import android.text.AutoGrowArray.IntArray;
-import android.text.Layout.Directions;
import android.text.style.MetricAffectingSpan;
import android.text.style.ReplacementSpan;
-import android.util.Pools.SynchronizedPool;
+import android.util.Log;
-import java.util.Arrays;
+import com.android.internal.util.ArrayUtils;
/**
- * MeasuredText provides text information for rendering purpose.
- *
- * The first motivation of this class is identify the text directions and retrieving individual
- * character widths. However retrieving character widths is slower than identifying text directions.
- * Thus, this class provides several builder methods for specific purposes.
- *
- * - buildForBidi:
- * Compute only text directions.
- * - buildForMeasurement:
- * Compute text direction and all character widths.
- * - buildForStaticLayout:
- * This is bit special. StaticLayout also needs to know text direction and character widths for
- * line breaking, but all things are done in native code. Similarly, text measurement is done
- * in native code. So instead of storing result to Java array, this keeps the result in native
- * code since there is no good reason to move the results to Java layer.
- *
- * In addition to the character widths, some additional information is computed for each purposes,
- * e.g. whole text length for measurement or font metrics for static layout.
- *
- * MeasuredText is NOT a thread safe object.
* @hide
*/
class MeasuredText {
- private static final char OBJECT_REPLACEMENT_CHARACTER = '\uFFFC';
+ private static final boolean localLOGV = false;
+ CharSequence mText;
+ int mTextStart;
+ float[] mWidths;
+ char[] mChars;
+ byte[] mLevels;
+ int mDir;
+ boolean mEasy;
+ int mLen;
- private MeasuredText() {} // Use build static functions instead.
+ private int mPos;
+ private TextPaint mWorkPaint;
- private static final SynchronizedPool<MeasuredText> sPool = new SynchronizedPool<>(1);
-
- private static @NonNull MeasuredText obtain() { // Use build static functions instead.
- final MeasuredText mt = sPool.acquire();
- return mt != null ? mt : new MeasuredText();
+ private MeasuredText() {
+ mWorkPaint = new TextPaint();
}
- /**
- * Recycle the MeasuredText.
- *
- * Do not call any methods after you call this method.
- */
- public void recycle() {
- release();
- sPool.release(this);
- }
+ private static final Object[] sLock = new Object[0];
+ private static final MeasuredText[] sCached = new MeasuredText[3];
- // The casted original text.
- //
- // This may be null if the passed text is not a Spanned.
- private @Nullable Spanned mSpanned;
-
- // The start offset of the target range in the original text (mSpanned);
- private @IntRange(from = 0) int mTextStart;
-
- // The length of the target range in the original text.
- private @IntRange(from = 0) int mTextLength;
-
- // The copied character buffer for measuring text.
- //
- // The length of this array is mTextLength.
- private @Nullable char[] mCopiedBuffer;
-
- // The whole paragraph direction.
- private @Layout.Direction int mParaDir;
-
- // True if the text is LTR direction and doesn't contain any bidi characters.
- private boolean mLtrWithoutBidi;
-
- // The bidi level for individual characters.
- //
- // This is empty if mLtrWithoutBidi is true.
- private @NonNull ByteArray mLevels = new ByteArray();
-
- // The whole width of the text.
- // See getWholeWidth comments.
- private @FloatRange(from = 0.0f) float mWholeWidth;
-
- // Individual characters' widths.
- // See getWidths comments.
- private @Nullable FloatArray mWidths = new FloatArray();
-
- // The span end positions.
- // See getSpanEndCache comments.
- private @Nullable IntArray mSpanEndCache = new IntArray(4);
-
- // The font metrics.
- // See getFontMetrics comments.
- private @Nullable IntArray mFontMetrics = new IntArray(4 * 4);
-
- // Following two objects are for avoiding object allocation.
- private @NonNull TextPaint mCachedPaint = new TextPaint();
- private @Nullable Paint.FontMetricsInt mCachedFm;
-
- /**
- * Releases internal buffers.
- */
- public void release() {
- reset();
- mLevels.clearWithReleasingLargeArray();
- mWidths.clearWithReleasingLargeArray();
- mFontMetrics.clearWithReleasingLargeArray();
- mSpanEndCache.clearWithReleasingLargeArray();
- }
-
- /**
- * Resets the internal state for starting new text.
- */
- private void reset() {
- mSpanned = null;
- mCopiedBuffer = null;
- mWholeWidth = 0;
- mLevels.clear();
- mWidths.clear();
- mFontMetrics.clear();
- mSpanEndCache.clear();
- }
-
- /**
- * Returns the characters to be measured.
- *
- * This is always available.
- */
- public @NonNull char[] getChars() {
- return mCopiedBuffer;
- }
-
- /**
- * Returns the paragraph direction.
- *
- * This is always available.
- */
- public @Layout.Direction int getParagraphDir() {
- return mParaDir;
- }
-
- /**
- * Returns the directions.
- *
- * This is always available.
- */
- public Directions getDirections(@IntRange(from = 0) int start, // inclusive
- @IntRange(from = 0) int end) { // exclusive
- if (mLtrWithoutBidi) {
- return Layout.DIRS_ALL_LEFT_TO_RIGHT;
- }
-
- final int length = end - start;
- return AndroidBidi.directions(mParaDir, mLevels.getRawArray(), start, mCopiedBuffer, start,
- length);
- }
-
- /**
- * Returns the whole text width.
- *
- * This is available only if the MeasureText is computed with computeForMeasurement.
- * Returns 0 in other cases.
- */
- public @FloatRange(from = 0.0f) float getWholeWidth() {
- return mWholeWidth;
- }
-
- /**
- * Returns the individual character's width.
- *
- * This is available only if the MeasureText is computed with computeForMeasurement.
- * Returns empty array in other cases.
- */
- public @NonNull FloatArray getWidths() {
- return mWidths;
- }
-
- /**
- * Returns the MetricsAffectingSpan end indices.
- *
- * If the input text is not a spanned string, this has one value that is the length of the text.
- *
- * This is available only if the MeasureText is computed with computeForStaticLayout.
- * Returns empty array in other cases.
- */
- public @NonNull IntArray getSpanEndCache() {
- return mSpanEndCache;
- }
-
- /**
- * Returns the int array which holds FontMetrics.
- *
- * This array holds the repeat of top, bottom, ascent, descent of font metrics value.
- *
- * This is available only if the MeasureText is computed with computeForStaticLayout.
- * Returns empty array in other cases.
- */
- public @NonNull IntArray getFontMetrics() {
- return mFontMetrics;
- }
-
- /**
- * Generates new MeasuredText for Bidi computation.
- *
- * If recycle is null, this returns new instance. If recycle is not null, this fills computed
- * result to recycle and returns recycle.
- *
- * @param text the character sequence to be measured
- * @param start the inclusive start offset of the target region in the text
- * @param end the exclusive end offset of the target region in the text
- * @param textDir the text direction
- * @param recycle pass existing MeasuredText if you want to recycle it.
- *
- * @return measured text
- */
- public static @NonNull MeasuredText buildForBidi(@NonNull CharSequence text,
- @IntRange(from = 0) int start,
- @IntRange(from = 0) int end,
- @NonNull TextDirectionHeuristic textDir,
- @Nullable MeasuredText recycle) {
- final MeasuredText mt = recycle == null ? obtain() : recycle;
- mt.resetAndAnalyzeBidi(text, start, end, textDir);
- return mt;
- }
-
- /**
- * Generates new MeasuredText for measuring texts.
- *
- * If recycle is null, this returns new instance. If recycle is not null, this fills computed
- * result to recycle and returns recycle.
- *
- * @param paint the paint to be used for rendering the text.
- * @param text the character sequence to be measured
- * @param start the inclusive start offset of the target region in the text
- * @param end the exclusive end offset of the target region in the text
- * @param textDir the text direction
- * @param recycle pass existing MeasuredText if you want to recycle it.
- *
- * @return measured text
- */
- public static @NonNull MeasuredText buildForMeasurement(@NonNull TextPaint paint,
- @NonNull CharSequence text,
- @IntRange(from = 0) int start,
- @IntRange(from = 0) int end,
- @NonNull TextDirectionHeuristic textDir,
- @Nullable MeasuredText recycle) {
- final MeasuredText mt = recycle == null ? obtain() : recycle;
- mt.resetAndAnalyzeBidi(text, start, end, textDir);
-
- mt.mWidths.resize(mt.mTextLength);
- if (mt.mTextLength == 0) {
- return mt;
- }
-
- if (mt.mSpanned == null) {
- // No style change by MetricsAffectingSpan. Just measure all text.
- mt.applyMetricsAffectingSpan(
- paint, null /* spans */, start, end, 0 /* native static layout ptr */);
- } else {
- // There may be a MetricsAffectingSpan. Split into span transitions and apply styles.
- int spanEnd;
- for (int spanStart = start; spanStart < end; spanStart = spanEnd) {
- spanEnd = mt.mSpanned.nextSpanTransition(spanStart, end, MetricAffectingSpan.class);
- MetricAffectingSpan[] spans = mt.mSpanned.getSpans(spanStart, spanEnd,
- MetricAffectingSpan.class);
- spans = TextUtils.removeEmptySpans(spans, mt.mSpanned, MetricAffectingSpan.class);
- mt.applyMetricsAffectingSpan(
- paint, spans, spanStart, spanEnd, 0 /* native static layout ptr */);
+ static MeasuredText obtain() {
+ MeasuredText mt;
+ synchronized (sLock) {
+ for (int i = sCached.length; --i >= 0;) {
+ if (sCached[i] != null) {
+ mt = sCached[i];
+ sCached[i] = null;
+ return mt;
+ }
}
}
+ mt = new MeasuredText();
+ if (localLOGV) {
+ Log.v("MEAS", "new: " + mt);
+ }
return mt;
}
- /**
- * Generates new MeasuredText for StaticLayout.
- *
- * If recycle is null, this returns new instance. If recycle is not null, this fills computed
- * result to recycle and returns recycle.
- *
- * @param paint the paint to be used for rendering the text.
- * @param text the character sequence to be measured
- * @param start the inclusive start offset of the target region in the text
- * @param end the exclusive end offset of the target region in the text
- * @param textDir the text direction
- * @param nativeStaticLayoutPtr the pointer to the native static layout object
- * @param recycle pass existing MeasuredText if you want to recycle it.
- *
- * @return measured text
- */
- public static @NonNull MeasuredText buildForStaticLayout(
- @NonNull TextPaint paint,
- @NonNull CharSequence text,
- @IntRange(from = 0) int start,
- @IntRange(from = 0) int end,
- @NonNull TextDirectionHeuristic textDir,
- /* Non-Zero */ long nativeStaticLayoutPtr,
- @Nullable MeasuredText recycle) {
- final MeasuredText mt = recycle == null ? obtain() : recycle;
- mt.resetAndAnalyzeBidi(text, start, end, textDir);
- if (mt.mTextLength == 0) {
- return mt;
- }
-
- if (mt.mSpanned == null) {
- // No style change by MetricsAffectingSpan. Just measure all text.
- mt.applyMetricsAffectingSpan(paint, null /* spans */, start, end,
- nativeStaticLayoutPtr);
- mt.mSpanEndCache.append(end);
- } else {
- // There may be a MetricsAffectingSpan. Split into span transitions and apply styles.
- int spanEnd;
- for (int spanStart = start; spanStart < end; spanStart = spanEnd) {
- spanEnd = mt.mSpanned.nextSpanTransition(spanStart, end, MetricAffectingSpan.class);
- MetricAffectingSpan[] spans = mt.mSpanned.getSpans(spanStart, spanEnd,
- MetricAffectingSpan.class);
- spans = TextUtils.removeEmptySpans(spans, mt.mSpanned, MetricAffectingSpan.class);
- mt.applyMetricsAffectingSpan(paint, spans, spanStart, spanEnd,
- nativeStaticLayoutPtr);
- mt.mSpanEndCache.append(spanEnd);
+ static MeasuredText recycle(MeasuredText mt) {
+ mt.finish();
+ synchronized(sLock) {
+ for (int i = 0; i < sCached.length; ++i) {
+ if (sCached[i] == null) {
+ sCached[i] = mt;
+ mt.mText = null;
+ break;
+ }
}
}
+ return null;
+ }
- return mt;
+ void finish() {
+ mText = null;
+ if (mLen > 1000) {
+ mWidths = null;
+ mChars = null;
+ mLevels = null;
+ }
}
/**
- * Reset internal state and analyzes text for bidirectional runs.
- *
- * @param text the character sequence to be measured
- * @param start the inclusive start offset of the target region in the text
- * @param end the exclusive end offset of the target region in the text
- * @param textDir the text direction
+ * Analyzes text for bidirectional runs. Allocates working buffers.
*/
- private void resetAndAnalyzeBidi(@NonNull CharSequence text,
- @IntRange(from = 0) int start, // inclusive
- @IntRange(from = 0) int end, // exclusive
- @NonNull TextDirectionHeuristic textDir) {
- reset();
- mSpanned = text instanceof Spanned ? (Spanned) text : null;
+ void setPara(CharSequence text, int start, int end, TextDirectionHeuristic textDir) {
+ mText = text;
mTextStart = start;
- mTextLength = end - start;
- if (mCopiedBuffer == null || mCopiedBuffer.length != mTextLength) {
- mCopiedBuffer = new char[mTextLength];
+ int len = end - start;
+ mLen = len;
+ mPos = 0;
+
+ if (mWidths == null || mWidths.length < len) {
+ mWidths = ArrayUtils.newUnpaddedFloatArray(len);
}
- TextUtils.getChars(text, start, end, mCopiedBuffer, 0);
+ if (mChars == null || mChars.length != len) {
+ mChars = new char[len];
+ }
+ TextUtils.getChars(text, start, end, mChars, 0);
- // Replace characters associated with ReplacementSpan to U+FFFC.
- if (mSpanned != null) {
- ReplacementSpan[] spans = mSpanned.getSpans(start, end, ReplacementSpan.class);
+ if (text instanceof Spanned) {
+ Spanned spanned = (Spanned) text;
+ ReplacementSpan[] spans = spanned.getSpans(start, end,
+ ReplacementSpan.class);
for (int i = 0; i < spans.length; i++) {
- int startInPara = mSpanned.getSpanStart(spans[i]) - start;
- int endInPara = mSpanned.getSpanEnd(spans[i]) - start;
- // The span interval may be larger and must be restricted to [start, end)
+ int startInPara = spanned.getSpanStart(spans[i]) - start;
+ int endInPara = spanned.getSpanEnd(spans[i]) - start;
+ // The span interval may be larger and must be restricted to [start, end[
if (startInPara < 0) startInPara = 0;
- if (endInPara > mTextLength) endInPara = mTextLength;
- Arrays.fill(mCopiedBuffer, startInPara, endInPara, OBJECT_REPLACEMENT_CHARACTER);
+ if (endInPara > len) endInPara = len;
+ for (int j = startInPara; j < endInPara; j++) {
+ mChars[j] = '\uFFFC'; // object replacement character
+ }
}
}
if ((textDir == TextDirectionHeuristics.LTR ||
textDir == TextDirectionHeuristics.FIRSTSTRONG_LTR ||
textDir == TextDirectionHeuristics.ANYRTL_LTR) &&
- TextUtils.doesNotNeedBidi(mCopiedBuffer, 0, mTextLength)) {
- mLevels.clear();
- mParaDir = Layout.DIR_LEFT_TO_RIGHT;
- mLtrWithoutBidi = true;
+ TextUtils.doesNotNeedBidi(mChars, 0, len)) {
+ mDir = Layout.DIR_LEFT_TO_RIGHT;
+ mEasy = true;
} else {
- final int bidiRequest;
+ if (mLevels == null || mLevels.length < len) {
+ mLevels = ArrayUtils.newUnpaddedByteArray(len);
+ }
+ int bidiRequest;
if (textDir == TextDirectionHeuristics.LTR) {
bidiRequest = Layout.DIR_REQUEST_LTR;
} else if (textDir == TextDirectionHeuristics.RTL) {
@@ -403,146 +144,122 @@
} else if (textDir == TextDirectionHeuristics.FIRSTSTRONG_RTL) {
bidiRequest = Layout.DIR_REQUEST_DEFAULT_RTL;
} else {
- final boolean isRtl = textDir.isRtl(mCopiedBuffer, 0, mTextLength);
+ boolean isRtl = textDir.isRtl(mChars, 0, len);
bidiRequest = isRtl ? Layout.DIR_REQUEST_RTL : Layout.DIR_REQUEST_LTR;
}
- mLevels.resize(mTextLength);
- mParaDir = AndroidBidi.bidi(bidiRequest, mCopiedBuffer, mLevels.getRawArray());
- mLtrWithoutBidi = false;
- }
- }
-
- private void applyReplacementRun(@NonNull ReplacementSpan replacement,
- @IntRange(from = 0) int start, // inclusive, in copied buffer
- @IntRange(from = 0) int end, // exclusive, in copied buffer
- /* Maybe Zero */ long nativeStaticLayoutPtr) {
- // Use original text. Shouldn't matter.
- // TODO: passing uninitizlied FontMetrics to developers. Do we need to keep this for
- // backward compatibility? or Should we initialize them for getFontMetricsInt?
- final float width = replacement.getSize(
- mCachedPaint, mSpanned, start + mTextStart, end + mTextStart, mCachedFm);
- if (nativeStaticLayoutPtr == 0) {
- // Assigns all width to the first character. This is the same behavior as minikin.
- mWidths.set(start, width);
- if (end > start + 1) {
- Arrays.fill(mWidths.getRawArray(), start + 1, end, 0.0f);
- }
- mWholeWidth += width;
- } else {
- StaticLayout.addReplacementRun(nativeStaticLayoutPtr, mCachedPaint, start, end, width);
- }
- }
-
- private void applyStyleRun(@IntRange(from = 0) int start, // inclusive, in copied buffer
- @IntRange(from = 0) int end, // exclusive, in copied buffer
- /* Maybe Zero */ long nativeStaticLayoutPtr) {
- if (nativeStaticLayoutPtr != 0) {
- mCachedPaint.getFontMetricsInt(mCachedFm);
- }
-
- if (mLtrWithoutBidi) {
- // If the whole text is LTR direction, just apply whole region.
- if (nativeStaticLayoutPtr == 0) {
- mWholeWidth += mCachedPaint.getTextRunAdvances(
- mCopiedBuffer, start, end - start, start, end - start, false /* isRtl */,
- mWidths.getRawArray(), start);
- } else {
- StaticLayout.addStyleRun(nativeStaticLayoutPtr, mCachedPaint, start, end,
- false /* isRtl */);
- }
- } else {
- // If there is multiple bidi levels, split into individual bidi level and apply style.
- byte level = mLevels.get(start);
- // Note that the empty text or empty range won't reach this method.
- // Safe to search from start + 1.
- for (int levelStart = start, levelEnd = start + 1;; ++levelEnd) {
- if (levelEnd == end || mLevels.get(levelEnd) != level) { // transition point
- final boolean isRtl = (level & 0x1) != 0;
- if (nativeStaticLayoutPtr == 0) {
- final int levelLength = levelEnd - levelStart;
- mWholeWidth += mCachedPaint.getTextRunAdvances(
- mCopiedBuffer, levelStart, levelLength, levelStart, levelEnd, isRtl,
- mWidths.getRawArray(), levelStart);
- } else {
- StaticLayout.addStyleRun(
- nativeStaticLayoutPtr, mCachedPaint, levelStart, levelEnd, isRtl);
- }
- if (levelEnd == end) {
- break;
- }
- levelStart = levelEnd;
- level = mLevels.get(levelEnd);
- }
- }
- }
- }
-
- private void applyMetricsAffectingSpan(
- @NonNull TextPaint paint,
- @Nullable MetricAffectingSpan[] spans,
- @IntRange(from = 0) int start, // inclusive, in original text buffer
- @IntRange(from = 0) int end, // exclusive, in original text buffer
- /* Maybe Zero */ long nativeStaticLayoutPtr) {
- mCachedPaint.set(paint);
- // XXX paint should not have a baseline shift, but...
- mCachedPaint.baselineShift = 0;
-
- final boolean needFontMetrics = nativeStaticLayoutPtr != 0;
-
- if (needFontMetrics && mCachedFm == null) {
- mCachedFm = new Paint.FontMetricsInt();
- }
-
- ReplacementSpan replacement = null;
- if (spans != null) {
- for (int i = 0; i < spans.length; i++) {
- MetricAffectingSpan span = spans[i];
- if (span instanceof ReplacementSpan) {
- // The last ReplacementSpan is effective for backward compatibility reasons.
- replacement = (ReplacementSpan) span;
- } else {
- // TODO: No need to call updateMeasureState for ReplacementSpan as well?
- span.updateMeasureState(mCachedPaint);
- }
- }
- }
-
- final int startInCopiedBuffer = start - mTextStart;
- final int endInCopiedBuffer = end - mTextStart;
-
- if (replacement != null) {
- applyReplacementRun(replacement, startInCopiedBuffer, endInCopiedBuffer,
- nativeStaticLayoutPtr);
- } else {
- applyStyleRun(startInCopiedBuffer, endInCopiedBuffer, nativeStaticLayoutPtr);
- }
-
- if (needFontMetrics) {
- if (mCachedPaint.baselineShift < 0) {
- mCachedFm.ascent += mCachedPaint.baselineShift;
- mCachedFm.top += mCachedPaint.baselineShift;
- } else {
- mCachedFm.descent += mCachedPaint.baselineShift;
- mCachedFm.bottom += mCachedPaint.baselineShift;
- }
-
- mFontMetrics.append(mCachedFm.top);
- mFontMetrics.append(mCachedFm.bottom);
- mFontMetrics.append(mCachedFm.ascent);
- mFontMetrics.append(mCachedFm.descent);
+ mDir = AndroidBidi.bidi(bidiRequest, mChars, mLevels);
+ mEasy = false;
}
}
/**
- * Returns the maximum index that the accumulated width not exceeds the width.
+ * Apply the style.
*
- * If forward=false is passed, returns the minimum index from the end instead.
- *
- * This only works if the MeasuredText is computed with computeForMeasurement.
- * Undefined behavior in other case.
+ * If nativeStaticLayoutPtr is 0, this method measures the styled text width.
+ * If nativeStaticLayoutPtr is not 0, this method just passes the style information to native
+ * code by calling StaticLayout.addstyleRun() and returns 0.
*/
- @IntRange(from = 0) int breakText(int limit, boolean forwards, float width) {
- float[] w = mWidths.getRawArray();
+ float addStyleRun(TextPaint paint, int len, Paint.FontMetricsInt fm,
+ long nativeStaticLayoutPtr) {
+ if (fm != null) {
+ paint.getFontMetricsInt(fm);
+ }
+
+ final int p = mPos;
+ mPos = p + len;
+
+ if (mEasy) {
+ final boolean isRtl = mDir != Layout.DIR_LEFT_TO_RIGHT;
+ if (nativeStaticLayoutPtr == 0) {
+ return paint.getTextRunAdvances(mChars, p, len, p, len, isRtl, mWidths, p);
+ } else {
+ StaticLayout.addStyleRun(nativeStaticLayoutPtr, paint, p, p + len, isRtl);
+ return 0.0f; // Builder.addStyleRun doesn't return the width.
+ }
+ }
+
+ float totalAdvance = 0;
+ int level = mLevels[p];
+ for (int q = p, i = p + 1, e = p + len;; ++i) {
+ if (i == e || mLevels[i] != level) {
+ final boolean isRtl = (level & 0x1) != 0;
+ if (nativeStaticLayoutPtr == 0) {
+ totalAdvance +=
+ paint.getTextRunAdvances(mChars, q, i - q, q, i - q, isRtl, mWidths, q);
+ } else {
+ // Builder.addStyleRun doesn't return the width.
+ StaticLayout.addStyleRun(nativeStaticLayoutPtr, paint, q, i, isRtl);
+ }
+ if (i == e) {
+ break;
+ }
+ q = i;
+ level = mLevels[i];
+ }
+ }
+ return totalAdvance; // If nativeStaticLayoutPtr is 0, the result is zero.
+ }
+
+ float addStyleRun(TextPaint paint, int len, Paint.FontMetricsInt fm) {
+ return addStyleRun(paint, len, fm, 0 /* native ptr */);
+ }
+
+ float addStyleRun(TextPaint paint, MetricAffectingSpan[] spans, int len,
+ Paint.FontMetricsInt fm, long nativeStaticLayoutPtr) {
+
+ TextPaint workPaint = mWorkPaint;
+ workPaint.set(paint);
+ // XXX paint should not have a baseline shift, but...
+ workPaint.baselineShift = 0;
+
+ ReplacementSpan replacement = null;
+ for (int i = 0; i < spans.length; i++) {
+ MetricAffectingSpan span = spans[i];
+ if (span instanceof ReplacementSpan) {
+ replacement = (ReplacementSpan)span;
+ } else {
+ span.updateMeasureState(workPaint);
+ }
+ }
+
+ float wid;
+ if (replacement == null) {
+ wid = addStyleRun(workPaint, len, fm, nativeStaticLayoutPtr);
+ } else {
+ // Use original text. Shouldn't matter.
+ wid = replacement.getSize(workPaint, mText, mTextStart + mPos,
+ mTextStart + mPos + len, fm);
+ if (nativeStaticLayoutPtr == 0) {
+ float[] w = mWidths;
+ w[mPos] = wid;
+ for (int i = mPos + 1, e = mPos + len; i < e; i++)
+ w[i] = 0;
+ } else {
+ StaticLayout.addReplacementRun(nativeStaticLayoutPtr, paint, mPos, mPos + len, wid);
+ }
+ mPos += len;
+ }
+
+ if (fm != null) {
+ if (workPaint.baselineShift < 0) {
+ fm.ascent += workPaint.baselineShift;
+ fm.top += workPaint.baselineShift;
+ } else {
+ fm.descent += workPaint.baselineShift;
+ fm.bottom += workPaint.baselineShift;
+ }
+ }
+
+ return wid;
+ }
+
+ float addStyleRun(TextPaint paint, MetricAffectingSpan[] spans, int len,
+ Paint.FontMetricsInt fm) {
+ return addStyleRun(paint, spans, len, fm, 0 /* native ptr */);
+ }
+
+ int breakText(int limit, boolean forwards, float width) {
+ float[] w = mWidths;
if (forwards) {
int i = 0;
while (i < limit) {
@@ -550,7 +267,7 @@
if (width < 0.0f) break;
i++;
}
- while (i > 0 && mCopiedBuffer[i - 1] == ' ') i--;
+ while (i > 0 && mChars[i - 1] == ' ') i--;
return i;
} else {
int i = limit - 1;
@@ -559,22 +276,16 @@
if (width < 0.0f) break;
i--;
}
- while (i < limit - 1 && (mCopiedBuffer[i + 1] == ' ' || w[i + 1] == 0.0f)) {
+ while (i < limit - 1 && (mChars[i + 1] == ' ' || w[i + 1] == 0.0f)) {
i++;
}
return limit - i - 1;
}
}
- /**
- * Returns the length of the substring.
- *
- * This only works if the MeasuredText is computed with computeForMeasurement.
- * Undefined behavior in other case.
- */
- @FloatRange(from = 0.0f) float measure(int start, int limit) {
+ float measure(int start, int limit) {
float width = 0;
- float[] w = mWidths.getRawArray();
+ float[] w = mWidths;
for (int i = start; i < limit; ++i) {
width += w[i];
}
diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java
index 81c82c9..c0fc44f 100644
--- a/core/java/android/text/StaticLayout.java
+++ b/core/java/android/text/StaticLayout.java
@@ -21,10 +21,10 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.graphics.Paint;
-import android.text.AutoGrowArray.FloatArray;
import android.text.style.LeadingMarginSpan;
import android.text.style.LeadingMarginSpan.LeadingMarginSpan2;
import android.text.style.LineHeightSpan;
+import android.text.style.MetricAffectingSpan;
import android.text.style.TabStopSpan;
import android.util.Log;
import android.util.Pools.SynchronizedPool;
@@ -99,6 +99,8 @@
b.mBreakStrategy = Layout.BREAK_STRATEGY_SIMPLE;
b.mHyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NONE;
b.mJustificationMode = Layout.JUSTIFICATION_MODE_NONE;
+
+ b.mMeasuredText = MeasuredText.obtain();
return b;
}
@@ -109,6 +111,8 @@
private static void recycle(@NonNull Builder b) {
b.mPaint = null;
b.mText = null;
+ MeasuredText.recycle(b.mMeasuredText);
+ b.mMeasuredText = null;
b.mLeftIndents = null;
b.mRightIndents = null;
b.mLeftPaddings = null;
@@ -124,6 +128,7 @@
mRightIndents = null;
mLeftPaddings = null;
mRightPaddings = null;
+ mMeasuredText.finish();
}
public Builder setText(CharSequence source) {
@@ -439,6 +444,9 @@
private final Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt();
+ // This will go away and be subsumed by native builder code
+ private MeasuredText mMeasuredText;
+
private static final SynchronizedPool<Builder> sPool = new SynchronizedPool<>(3);
}
@@ -610,7 +618,11 @@
TextUtils.TruncateAt ellipsize = b.mEllipsize;
final boolean addLastLineSpacing = b.mAddLastLineLineSpacing;
LineBreaks lineBreaks = new LineBreaks(); // TODO: move to builder to avoid allocation costs
- FloatArray widths = new FloatArray();
+ // store span end locations
+ int[] spanEndCache = new int[4];
+ // store fontMetrics per span range
+ // must be a multiple of 4 (and > 0) (store top, bottom, ascent, and descent per range)
+ int[] fmCache = new int[4 * 4];
mLineCount = 0;
mEllipsized = false;
@@ -622,6 +634,8 @@
Paint.FontMetricsInt fm = b.mFontMetricsInt;
int[] chooseHtv = null;
+ MeasuredText measured = b.mMeasuredText;
+
Spanned spanned = null;
if (source instanceof Spanned)
spanned = (Spanned) source;
@@ -648,7 +662,6 @@
b.mJustificationMode != Layout.JUSTIFICATION_MODE_NONE,
indents, mLeftPaddings, mRightPaddings);
- MeasuredText measured = null;
try {
int paraEnd;
for (int paraStart = bufStart; paraStart <= bufEnd; paraStart = paraEnd) {
@@ -708,6 +721,13 @@
}
}
+ measured.setPara(source, paraStart, paraEnd, textDir);
+ char[] chs = measured.mChars;
+ float[] widths = measured.mWidths;
+ byte[] chdirs = measured.mLevels;
+ int dir = measured.mDir;
+ boolean easy = measured.mEasy;
+
// tab stop locations
int[] variableTabStops = null;
if (spanned != null) {
@@ -723,16 +743,50 @@
}
}
- measured = MeasuredText.buildForStaticLayout(
- paint, source, paraStart, paraEnd, textDir, nativePtr, measured);
- final char[] chs = measured.getChars();
- final int[] spanEndCache = measured.getSpanEndCache().getRawArray();
- final int[] fmCache = measured.getFontMetrics().getRawArray();
- widths.resize(chs.length);
-
// measurement has to be done before performing line breaking
// but we don't want to recompute fontmetrics or span ranges the
// second time, so we cache those and then use those stored values
+ int fmCacheCount = 0;
+ int spanEndCacheCount = 0;
+ for (int spanStart = paraStart, spanEnd; spanStart < paraEnd; spanStart = spanEnd) {
+ if (fmCacheCount * 4 >= fmCache.length) {
+ int[] grow = new int[fmCacheCount * 4 * 2];
+ System.arraycopy(fmCache, 0, grow, 0, fmCacheCount * 4);
+ fmCache = grow;
+ }
+
+ if (spanEndCacheCount >= spanEndCache.length) {
+ int[] grow = new int[spanEndCacheCount * 2];
+ System.arraycopy(spanEndCache, 0, grow, 0, spanEndCacheCount);
+ spanEndCache = grow;
+ }
+
+ if (spanned == null) {
+ spanEnd = paraEnd;
+ int spanLen = spanEnd - spanStart;
+ measured.addStyleRun(paint, spanLen, fm, nativePtr);
+ } else {
+ spanEnd = spanned.nextSpanTransition(spanStart, paraEnd,
+ MetricAffectingSpan.class);
+ int spanLen = spanEnd - spanStart;
+ MetricAffectingSpan[] spans =
+ spanned.getSpans(spanStart, spanEnd, MetricAffectingSpan.class);
+ spans = TextUtils.removeEmptySpans(spans, spanned,
+ MetricAffectingSpan.class);
+ measured.addStyleRun(paint, spans, spanLen, fm, nativePtr);
+ }
+
+ // the order of storage here (top, bottom, ascent, descent) has to match the
+ // code below where these values are retrieved
+ fmCache[fmCacheCount * 4 + 0] = fm.top;
+ fmCache[fmCacheCount * 4 + 1] = fm.bottom;
+ fmCache[fmCacheCount * 4 + 2] = fm.ascent;
+ fmCache[fmCacheCount * 4 + 3] = fm.descent;
+ fmCacheCount++;
+
+ spanEndCache[spanEndCacheCount] = spanEnd;
+ spanEndCacheCount++;
+ }
int breakCount = nComputeLineBreaks(
nativePtr,
@@ -755,7 +809,7 @@
lineBreaks.ascents,
lineBreaks.descents,
lineBreaks.flags,
- widths.getRawArray());
+ widths);
final int[] breaks = lineBreaks.breaks;
final float[] lineWidths = lineBreaks.widths;
@@ -778,7 +832,7 @@
width += lineWidths[i];
} else {
for (int j = (i == 0 ? 0 : breaks[i - 1]); j < breaks[i]; j++) {
- width += widths.get(j);
+ width += widths[j];
}
}
flag |= flags[i] & TAB_MASK;
@@ -842,10 +896,10 @@
v = out(source, here, endPos,
ascent, descent, fmTop, fmBottom,
v, spacingmult, spacingadd, chooseHt, chooseHtv, fm,
- flags[breakIndex], needMultiply, measured, bufEnd,
- includepad, trackpad, addLastLineSpacing, chs, widths.getRawArray(),
- paraStart, ellipsize, ellipsizedWidth, lineWidths[breakIndex],
- paint, moreChars);
+ flags[breakIndex], needMultiply, chdirs, dir, easy, bufEnd,
+ includepad, trackpad, addLastLineSpacing, chs, widths, paraStart,
+ ellipsize, ellipsizedWidth, lineWidths[breakIndex], paint,
+ moreChars);
if (endPos < spanEnd) {
// preserve metrics for current span
@@ -873,8 +927,7 @@
if ((bufEnd == bufStart || source.charAt(bufEnd - 1) == CHAR_NEW_LINE)
&& mLineCount < mMaximumVisibleLineCount) {
- measured = MeasuredText.buildForStaticLayout(
- paint, source, bufEnd, bufEnd, textDir, nativePtr, measured);
+ measured.setPara(source, bufEnd, bufEnd, textDir);
paint.getFontMetricsInt(fm);
@@ -884,15 +937,12 @@
v,
spacingmult, spacingadd, null,
null, fm, 0,
- needMultiply, measured, bufEnd,
+ needMultiply, measured.mLevels, measured.mDir, measured.mEasy, bufEnd,
includepad, trackpad, addLastLineSpacing, null,
null, bufStart, ellipsize,
ellipsizedWidth, 0, paint, false);
}
} finally {
- if (measured != null) {
- measured.recycle();
- }
nFinish(nativePtr);
}
}
@@ -902,8 +952,8 @@
private int out(final CharSequence text, final int start, final int end, int above, int below,
int top, int bottom, int v, final float spacingmult, final float spacingadd,
final LineHeightSpan[] chooseHt, final int[] chooseHtv, final Paint.FontMetricsInt fm,
- final int flags, final boolean needMultiply, final MeasuredText measured,
- final int bufEnd, final boolean includePad, final boolean trackPad,
+ final int flags, final boolean needMultiply, final byte[] chdirs, final int dir,
+ final boolean easy, final int bufEnd, final boolean includePad, final boolean trackPad,
final boolean addLastLineLineSpacing, final char[] chs, final float[] widths,
final int widthStart, final TextUtils.TruncateAt ellipsize, final float ellipsisWidth,
final float textWidth, final TextPaint paint, final boolean moreChars) {
@@ -911,7 +961,6 @@
final int off = j * mColumns;
final int want = off + mColumns + TOP;
int[] lines = mLines;
- final int dir = measured.getParagraphDir();
if (want >= lines.length) {
final int[] grow = ArrayUtils.newUnpaddedIntArray(GrowingArrayUtils.growSize(want));
@@ -937,8 +986,17 @@
// one bit for start field
lines[off + TAB] |= flags & TAB_MASK;
lines[off + HYPHEN] = flags;
+
lines[off + DIR] |= dir << DIR_SHIFT;
- mLineDirections[j] = measured.getDirections(start - widthStart, end - widthStart);
+ // easy means all chars < the first RTL, so no emoji, no nothing
+ // XXX a run with no text or all spaces is easy but might be an empty
+ // RTL paragraph. Make sure easy is false if this is the case.
+ if (easy) {
+ mLineDirections[j] = DIRS_ALL_LEFT_TO_RIGHT;
+ } else {
+ mLineDirections[j] = AndroidBidi.directions(dir, chdirs, start - widthStart, chs,
+ start - widthStart, end - start);
+ }
final boolean firstLine = (j == 0);
final boolean currentLineIsTheLastVisibleOne = (j + 1 == mMaximumVisibleLineCount);
diff --git a/core/java/android/text/TextUtils.java b/core/java/android/text/TextUtils.java
index af66157..cbdaa69 100644
--- a/core/java/android/text/TextUtils.java
+++ b/core/java/android/text/TextUtils.java
@@ -42,6 +42,7 @@
import android.text.style.ForegroundColorSpan;
import android.text.style.LeadingMarginSpan;
import android.text.style.LocaleSpan;
+import android.text.style.MetricAffectingSpan;
import android.text.style.ParagraphStyle;
import android.text.style.QuoteSpan;
import android.text.style.RelativeSizeSpan;
@@ -1250,11 +1251,10 @@
@NonNull String ellipsis) {
final int len = text.length();
- MeasuredText mt = null;
+ final MeasuredText mt = MeasuredText.obtain();
MeasuredText resultMt = null;
try {
- mt = MeasuredText.buildForMeasurement(paint, text, 0, text.length(), textDir, mt);
- float width = mt.getWholeWidth();
+ float width = setPara(mt, paint, text, 0, text.length(), textDir);
if (width <= avail) {
if (callback != null) {
@@ -1263,6 +1263,7 @@
return text;
}
+ resultMt = MeasuredText.obtain();
// First estimate of effective width of ellipsis.
float ellipsisWidth = paint.measureText(ellipsis);
int numberOfTries = 0;
@@ -1289,7 +1290,7 @@
}
}
- final char[] buf = mt.getChars();
+ final char[] buf = mt.mChars;
final Spanned sp = text instanceof Spanned ? (Spanned) text : null;
final int removed = end - start;
@@ -1332,9 +1333,7 @@
if (remaining == 0) { // All text is gone.
textFits = true;
} else {
- resultMt = MeasuredText.buildForMeasurement(
- paint, result, 0, result.length(), textDir, resultMt);
- width = resultMt.getWholeWidth();
+ width = setPara(resultMt, paint, result, 0, result.length(), textDir);
if (width <= avail) {
textFits = true;
} else {
@@ -1358,11 +1357,9 @@
}
return result;
} finally {
- if (mt != null) {
- mt.recycle();
- }
+ MeasuredText.recycle(mt);
if (resultMt != null) {
- resultMt.recycle();
+ MeasuredText.recycle(resultMt);
}
}
}
@@ -1479,17 +1476,15 @@
public static CharSequence commaEllipsize(CharSequence text, TextPaint p,
float avail, String oneMore, String more, TextDirectionHeuristic textDir) {
- MeasuredText mt = null;
- MeasuredText tempMt = null;
+ MeasuredText mt = MeasuredText.obtain();
try {
int len = text.length();
- mt = MeasuredText.buildForMeasurement(p, text, 0, len, textDir, mt);
- float width = mt.getWholeWidth();
+ float width = setPara(mt, p, text, 0, len, textDir);
if (width <= avail) {
return text;
}
- char[] buf = mt.getChars();
+ char[] buf = mt.mChars;
int commaCount = 0;
for (int i = 0; i < len; i++) {
@@ -1505,8 +1500,9 @@
int w = 0;
int count = 0;
- float[] widths = mt.getWidths().getRawArray();
+ float[] widths = mt.mWidths;
+ MeasuredText tempMt = MeasuredText.obtain();
for (int i = 0; i < len; i++) {
w += widths[i];
@@ -1523,9 +1519,8 @@
}
// XXX this is probably ok, but need to look at it more
- tempMt = MeasuredText.buildForMeasurement(
- p, format, 0, format.length(), textDir, tempMt);
- float moreWid = tempMt.getWholeWidth();
+ tempMt.setPara(format, 0, format.length(), textDir);
+ float moreWid = tempMt.addStyleRun(p, tempMt.mLen, null);
if (w + moreWid <= avail) {
ok = i + 1;
@@ -1533,18 +1528,40 @@
}
}
}
+ MeasuredText.recycle(tempMt);
SpannableStringBuilder out = new SpannableStringBuilder(okFormat);
out.insert(0, text, 0, ok);
return out;
} finally {
- if (mt != null) {
- mt.recycle();
- }
- if (tempMt != null) {
- tempMt.recycle();
+ MeasuredText.recycle(mt);
+ }
+ }
+
+ private static float setPara(MeasuredText mt, TextPaint paint,
+ CharSequence text, int start, int end, TextDirectionHeuristic textDir) {
+
+ mt.setPara(text, start, end, textDir);
+
+ float width;
+ Spanned sp = text instanceof Spanned ? (Spanned) text : null;
+ int len = end - start;
+ if (sp == null) {
+ width = mt.addStyleRun(paint, len, null);
+ } else {
+ width = 0;
+ int spanEnd;
+ for (int spanStart = 0; spanStart < len; spanStart = spanEnd) {
+ spanEnd = sp.nextSpanTransition(spanStart, len,
+ MetricAffectingSpan.class);
+ MetricAffectingSpan[] spans = sp.getSpans(
+ spanStart, spanEnd, MetricAffectingSpan.class);
+ spans = TextUtils.removeEmptySpans(spans, sp, MetricAffectingSpan.class);
+ width += mt.addStyleRun(paint, spans, spanEnd - spanStart, null);
}
}
+
+ return width;
}
// Returns true if the character's presence could affect RTL layout.
diff --git a/core/java/android/util/StatsManager.java b/core/java/android/util/StatsManager.java
index 2bcd863..26a3c36 100644
--- a/core/java/android/util/StatsManager.java
+++ b/core/java/android/util/StatsManager.java
@@ -93,10 +93,11 @@
}
/**
- * Clients can request data with a binder call.
+ * Clients can request data with a binder call. This getter is destructive and also clears
+ * the retrieved metrics from statsd memory.
*
* @param configKey Configuration key to retrieve data from.
- * @return Serialized ConfigMetricsReport proto. Returns null on failure.
+ * @return Serialized ConfigMetricsReportList proto. Returns null on failure.
*/
@RequiresPermission(Manifest.permission.DUMP)
public byte[] getData(String configKey) {
@@ -115,6 +116,30 @@
}
}
+ /**
+ * Clients can request metadata for statsd. Will contain stats across all configurations but not
+ * the actual metrics themselves (metrics must be collected via {@link #getData(String)}.
+ * This getter is not destructive and will not reset any metrics/counters.
+ *
+ * @return Serialized StatsdStatsReport proto. Returns null on failure.
+ */
+ @RequiresPermission(Manifest.permission.DUMP)
+ public byte[] getMetadata() {
+ synchronized (this) {
+ try {
+ IStatsManager service = getIStatsManagerLocked();
+ if (service == null) {
+ Slog.d(TAG, "Failed to find statsd when getting metadata");
+ return null;
+ }
+ return service.getMetadata();
+ } catch (RemoteException e) {
+ Slog.d(TAG, "Failed to connecto statsd when getting metadata");
+ return null;
+ }
+ }
+ }
+
private class StatsdDeathRecipient implements IBinder.DeathRecipient {
@Override
public void binderDied() {
diff --git a/core/java/android/view/autofill/AutofillManager.java b/core/java/android/view/autofill/AutofillManager.java
index 547e0db..9a99e53 100644
--- a/core/java/android/view/autofill/AutofillManager.java
+++ b/core/java/android/view/autofill/AutofillManager.java
@@ -24,6 +24,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemService;
+import android.annotation.TestApi;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
@@ -36,6 +37,7 @@
import android.os.RemoteException;
import android.service.autofill.AutofillService;
import android.service.autofill.FillEventHistory;
+import android.service.autofill.UserData;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
@@ -1007,6 +1009,54 @@
}
/**
+ * Gets the user data used for <a href="#FieldsClassification">fields classification</a>.
+ *
+ * <p><b>Note:</b> This method should only be called by an app providing an autofill service.
+ *
+ * TODO(b/67867469):
+ * - proper javadoc
+ * - unhide / remove testApi
+ *
+ * @return value previously set by {@link #setUserData(UserData)} or {@code null} if it was
+ * reset or if the caller currently does not have an enabled autofill service for the user.
+ *
+ * @hide
+ */
+ @TestApi
+ @Nullable public UserData getUserData() {
+ try {
+ return mService.getUserData();
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ return null;
+ }
+ }
+
+ /**
+ * Sets the user data used for <a href="#FieldsClassification">fields classification</a>.
+ *
+ * <p><b>Note:</b> This method should only be called by an app providing an autofill service,
+ * and it's ignored if the caller currently doesn't have an enabled autofill service for
+ * the user.
+ *
+ * TODO(b/67867469):
+ * - proper javadoc
+ * - unhide / remove testApi
+ * - add unit tests:
+ * - call set / get / verify
+ *
+ * @hide
+ */
+ @TestApi
+ public void setUserData(@Nullable UserData userData) {
+ try {
+ mService.setUserData(userData);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Returns {@code true} if autofill is supported by the current device and
* is supported for this user.
*
diff --git a/core/java/android/view/autofill/AutofillValue.java b/core/java/android/view/autofill/AutofillValue.java
index 3beae11..8e649de 100644
--- a/core/java/android/view/autofill/AutofillValue.java
+++ b/core/java/android/view/autofill/AutofillValue.java
@@ -177,7 +177,7 @@
.append("[type=").append(mType)
.append(", value=");
if (isText()) {
- string.append(((CharSequence) mValue).length()).append("_chars");
+ Helper.appendRedacted(string, (CharSequence) mValue);
} else {
string.append(mValue);
}
diff --git a/core/java/android/view/autofill/Helper.java b/core/java/android/view/autofill/Helper.java
index 829e7f3..b95704a 100644
--- a/core/java/android/view/autofill/Helper.java
+++ b/core/java/android/view/autofill/Helper.java
@@ -16,6 +16,8 @@
package android.view.autofill;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.os.Bundle;
import java.util.Arrays;
@@ -50,6 +52,35 @@
return builder;
}
+ /**
+ * Appends {@code value} to the {@code builder} redacting its contents.
+ */
+ public static void appendRedacted(@NonNull StringBuilder builder,
+ @Nullable CharSequence value) {
+ if (value == null) {
+ builder.append("null");
+ } else {
+ builder.append(value.length()).append("_chars");
+ }
+ }
+
+ /**
+ * Appends {@code values} to the {@code builder} redacting its contents.
+ */
+ public static void appendRedacted(@NonNull StringBuilder builder, @Nullable String[] values) {
+ if (values == null) {
+ builder.append("N/A");
+ return;
+ }
+ builder.append("[");
+ for (String value : values) {
+ builder.append(" '");
+ appendRedacted(builder, value);
+ builder.append("'");
+ }
+ builder.append(" ]");
+ }
+
private Helper() {
throw new UnsupportedOperationException("contains static members only");
}
diff --git a/core/java/android/view/autofill/IAutoFillManager.aidl b/core/java/android/view/autofill/IAutoFillManager.aidl
index d6db3fe..7d6a19f 100644
--- a/core/java/android/view/autofill/IAutoFillManager.aidl
+++ b/core/java/android/view/autofill/IAutoFillManager.aidl
@@ -21,6 +21,7 @@
import android.os.Bundle;
import android.os.IBinder;
import android.service.autofill.FillEventHistory;
+import android.service.autofill.UserData;
import android.view.autofill.AutofillId;
import android.view.autofill.AutofillValue;
import android.view.autofill.IAutoFillManagerClient;
@@ -53,4 +54,6 @@
boolean isServiceSupported(int userId);
boolean isServiceEnabled(int userId, String packageName);
void onPendingSaveUi(int operation, IBinder token);
+ UserData getUserData();
+ void setUserData(in UserData userData);
}
diff --git a/core/jni/android/graphics/YuvToJpegEncoder.cpp b/core/jni/android/graphics/YuvToJpegEncoder.cpp
index 31567f7..5eecd9c 100644
--- a/core/jni/android/graphics/YuvToJpegEncoder.cpp
+++ b/core/jni/android/graphics/YuvToJpegEncoder.cpp
@@ -26,12 +26,13 @@
bool YuvToJpegEncoder::encode(SkWStream* stream, void* inYuv, int width,
int height, int* offsets, int jpegQuality) {
jpeg_compress_struct cinfo;
- skjpeg_error_mgr sk_err;
+ jpeg_error_mgr err;
skjpeg_destination_mgr sk_wstream(stream);
- cinfo.err = jpeg_std_error(&sk_err);
- sk_err.error_exit = skjpeg_error_exit;
- if (setjmp(sk_err.fJmpBuf)) {
+ cinfo.err = jpeg_std_error(&err);
+ err.error_exit = skjpeg_error_exit;
+ jmp_buf jmp;
+ if (setjmp(jmp)) {
return false;
}
jpeg_create_compress(&cinfo);
diff --git a/core/jni/android_view_ThreadedRenderer.cpp b/core/jni/android_view_ThreadedRenderer.cpp
index 870a0c2..519a885 100644
--- a/core/jni/android_view_ThreadedRenderer.cpp
+++ b/core/jni/android_view_ThreadedRenderer.cpp
@@ -1043,7 +1043,22 @@
{ "nSetHighContrastText", "(Z)V", (void*)android_view_ThreadedRenderer_setHighContrastText },
};
+static JavaVM* mJvm = nullptr;
+
+static void attachRenderThreadToJvm() {
+ LOG_ALWAYS_FATAL_IF(!mJvm, "No jvm but we set the hook??");
+
+ JavaVMAttachArgs args;
+ args.version = JNI_VERSION_1_4;
+ args.name = (char*) "RenderThread";
+ args.group = NULL;
+ JNIEnv* env;
+ mJvm->AttachCurrentThreadAsDaemon(&env, (void*) &args);
+}
+
int register_android_view_ThreadedRenderer(JNIEnv* env) {
+ env->GetJavaVM(&mJvm);
+ RenderThread::setOnStartHook(&attachRenderThreadToJvm);
jclass observerClass = FindClassOrDie(env, "android/view/FrameMetricsObserver");
gFrameMetricsObserverClassInfo.frameMetrics = GetFieldIDOrDie(
env, observerClass, "mFrameMetrics", "Landroid/view/FrameMetrics;");
diff --git a/core/tests/coretests/src/android/provider/SettingsBackupTest.java b/core/tests/coretests/src/android/provider/SettingsBackupTest.java
index 4e0e5ed..0367275 100644
--- a/core/tests/coretests/src/android/provider/SettingsBackupTest.java
+++ b/core/tests/coretests/src/android/provider/SettingsBackupTest.java
@@ -418,7 +418,13 @@
private static final Set<String> BACKUP_BLACKLISTED_SECURE_SETTINGS =
newHashSet(
Settings.Secure.ACCESSIBILITY_SOFT_KEYBOARD_MODE,
+ // TODO(b/67867469): Move autofill settings below to
+ // BACKUP_BLACKLISTED_SYSTEM_SETTINGS once feature is moved out of experimental
Settings.Secure.AUTOFILL_FEATURE_FIELD_DETECTION,
+ Settings.Secure.AUTOFILL_USER_DATA_MAX_FIELD_CLASSIFICATION_IDS_SIZE,
+ Settings.Secure.AUTOFILL_USER_DATA_MAX_USER_DATA_SIZE,
+ Settings.Secure.AUTOFILL_USER_DATA_MAX_VALUE_LENGTH,
+ Settings.Secure.AUTOFILL_USER_DATA_MIN_VALUE_LENGTH,
Settings.Secure.ALLOWED_GEOLOCATION_ORIGINS,
Settings.Secure.ALWAYS_ON_VPN_APP,
Settings.Secure.ALWAYS_ON_VPN_LOCKDOWN,
diff --git a/libs/hwui/renderthread/RenderThread.cpp b/libs/hwui/renderthread/RenderThread.cpp
index 05a9b75..20443ec 100644
--- a/libs/hwui/renderthread/RenderThread.cpp
+++ b/libs/hwui/renderthread/RenderThread.cpp
@@ -51,10 +51,17 @@
static bool gHasRenderThreadInstance = false;
+static void (*gOnStartHook)() = nullptr;
+
bool RenderThread::hasInstance() {
return gHasRenderThreadInstance;
}
+void RenderThread::setOnStartHook(void (*onStartHook)()) {
+ LOG_ALWAYS_FATAL_IF(hasInstance(), "can't set an onStartHook after we've started...");
+ gOnStartHook = onStartHook;
+}
+
RenderThread& RenderThread::getInstance() {
// This is a pointer because otherwise __cxa_finalize
// will try to delete it like a Good Citizen but that causes us to crash
@@ -256,6 +263,9 @@
bool RenderThread::threadLoop() {
setpriority(PRIO_PROCESS, 0, PRIORITY_DISPLAY);
+ if (gOnStartHook) {
+ gOnStartHook();
+ }
initThreadLocals();
while (true) {
diff --git a/libs/hwui/renderthread/RenderThread.h b/libs/hwui/renderthread/RenderThread.h
index d17a509..970537b 100644
--- a/libs/hwui/renderthread/RenderThread.h
+++ b/libs/hwui/renderthread/RenderThread.h
@@ -67,6 +67,9 @@
PREVENT_COPY_AND_ASSIGN(RenderThread);
public:
+ // Sets a callback that fires before any RenderThread setup has occured.
+ ANDROID_API static void setOnStartHook(void (*onStartHook)());
+
WorkQueue& queue() { return ThreadBase::queue(); }
// Mimics android.view.Choreographer
diff --git a/libs/hwui/thread/ThreadBase.h b/libs/hwui/thread/ThreadBase.h
index b3fec1f..8068121 100644
--- a/libs/hwui/thread/ThreadBase.h
+++ b/libs/hwui/thread/ThreadBase.h
@@ -31,7 +31,10 @@
PREVENT_COPY_AND_ASSIGN(ThreadBase);
public:
- ThreadBase() : mLooper(new Looper(false)), mQueue([this]() { mLooper->wake(); }, mLock) {}
+ ThreadBase()
+ : Thread(false)
+ , mLooper(new Looper(false))
+ , mQueue([this]() { mLooper->wake(); }, mLock) {}
WorkQueue& queue() { return mQueue; }
diff --git a/packages/SystemUI/res/layout/volume_dialog.xml b/packages/SystemUI/res/layout/volume_dialog.xml
index fac254a..863f17b 100644
--- a/packages/SystemUI/res/layout/volume_dialog.xml
+++ b/packages/SystemUI/res/layout/volume_dialog.xml
@@ -13,7 +13,7 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<com.android.systemui.HardwareUiLayout
+<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
@@ -45,4 +45,4 @@
</LinearLayout>
</RelativeLayout>
-</com.android.systemui.HardwareUiLayout>
+</RelativeLayout>
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
index 383d327..f16d7b8 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
@@ -19,6 +19,9 @@
import static android.accessibilityservice.AccessibilityServiceInfo.FEEDBACK_ALL_MASK;
import static android.accessibilityservice.AccessibilityServiceInfo.FEEDBACK_GENERIC;
+import static com.android.systemui.util.leak.RotationUtils.ROTATION_LANDSCAPE;
+import static com.android.systemui.util.leak.RotationUtils.ROTATION_NONE;
+import static com.android.systemui.util.leak.RotationUtils.ROTATION_SEASCAPE;
import static com.android.systemui.volume.Events.DISMISS_REASON_TOUCH_OUTSIDE;
import android.accessibilityservice.AccessibilityServiceInfo;
@@ -42,6 +45,7 @@
import android.os.Looper;
import android.os.Message;
import android.os.SystemClock;
+import android.provider.Settings;
import android.provider.Settings.Global;
import android.util.Log;
import android.util.Slog;
@@ -67,6 +71,7 @@
import com.android.settingslib.Utils;
import com.android.systemui.Dependency;
+import com.android.systemui.HardwareBgDrawable;
import com.android.systemui.HardwareUiLayout;
import com.android.systemui.Interpolators;
import com.android.systemui.R;
@@ -74,6 +79,7 @@
import com.android.systemui.plugins.VolumeDialogController;
import com.android.systemui.plugins.VolumeDialogController.State;
import com.android.systemui.plugins.VolumeDialogController.StreamState;
+import com.android.systemui.util.leak.RotationUtils;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -97,9 +103,13 @@
private final VolumeDialogController mController;
private Window mWindow;
- private HardwareUiLayout mHardwareLayout;
+ //private HardwareUiLayout mHardwareLayout;
private CustomDialog mDialog;
private ViewGroup mDialogView;
+ private boolean mEdgeBleed;
+ private boolean mRoundedDivider;
+ private HardwareBgDrawable mBackground;
+ private int mRotation = ROTATION_NONE;
private ViewGroup mDialogRowsView;
private ViewGroup mDialogContentView;
private final List<VolumeRow> mRows = new ArrayList<>();
@@ -111,6 +121,8 @@
private final Accessibility mAccessibility = new Accessibility();
private final ColorStateList mActiveSliderTint;
private final ColorStateList mInactiveSliderTint;
+ private static final String EDGE_BLEED = "sysui_hwui_edge_bleed";
+ private static final String ROUNDED_DIVIDER = "sysui_hwui_rounded_divider";
private boolean mShowing;
private boolean mShowA11yStream;
@@ -181,8 +193,16 @@
return true;
}
});
- mHardwareLayout = HardwareUiLayout.get(mDialogView);
- mHardwareLayout.setOutsideTouchListener(view -> dismiss(DISMISS_REASON_TOUCH_OUTSIDE));
+
+ mEdgeBleed = Settings.Secure.getInt(mContext.getContentResolver(),
+ EDGE_BLEED, 0) != 0;
+ mRoundedDivider = Settings.Secure.getInt(mContext.getContentResolver(),
+ ROUNDED_DIVIDER, 1) != 0;
+ updateEdgeMargin(mEdgeBleed ? 0 : getEdgePadding());
+ mBackground = new HardwareBgDrawable(mRoundedDivider, !mEdgeBleed, mContext);
+ mDialogView.setBackground(mBackground);
+ //mHardwareLayout = HardwareUiLayout.get(mDialogView);
+ //mHardwareLayout.setOutsideTouchListener(view -> dismiss(DISMISS_REASON_TOUCH_OUTSIDE));
mDialogContentView = mDialog.findViewById(R.id.volume_dialog_content);
mDialogRowsView = mDialogContentView.findViewById(R.id.volume_dialog_rows);
@@ -210,6 +230,25 @@
updateRowsH(getActiveRow());
}
+ private int getEdgePadding() {
+ return mContext.getResources().getDimensionPixelSize(R.dimen.edge_margin);
+ }
+
+ private void updateEdgeMargin(int edge) {
+ if (mDialogView != null) {
+ mRotation = RotationUtils.getRotation(mContext);
+ ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) mDialogView.getLayoutParams();
+ if (mRotation == ROTATION_LANDSCAPE) {
+ params.topMargin = edge;
+ } else if (mRotation == ROTATION_SEASCAPE) {
+ params.bottomMargin = edge;
+ } else {
+ params.rightMargin = edge;
+ }
+ mDialogView.setLayoutParams(params);
+ }
+ }
+
private ColorStateList loadColorStateList(int colorResId) {
return ColorStateList.valueOf(mContext.getColor(colorResId));
}
@@ -389,11 +428,11 @@
rescheduleTimeoutH();
if (mShowing) return;
mShowing = true;
- mHardwareLayout.setTranslationX(getAnimTranslation());
- mHardwareLayout.setAlpha(0);
- mHardwareLayout.animate()
+ mDialogView.setTranslationY(getAnimTranslation());
+ mDialogView.setAlpha(0);
+ mDialogView.animate()
.alpha(1)
- .translationX(0)
+ .translationY(0)
.setDuration(300)
.setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
.withEndAction(() -> {
@@ -432,9 +471,9 @@
mHandler.removeMessages(H.SHOW);
if (!mShowing) return;
mShowing = false;
- mHardwareLayout.setTranslationX(0);
- mHardwareLayout.setAlpha(1);
- mHardwareLayout.animate()
+ mDialogView.setTranslationX(0);
+ mDialogView.setAlpha(1);
+ mDialogView.animate()
.alpha(0)
.translationX(getAnimTranslation())
.setDuration(300)
diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerService.java b/services/autofill/java/com/android/server/autofill/AutofillManagerService.java
index 23e4f50..0291276 100644
--- a/services/autofill/java/com/android/server/autofill/AutofillManagerService.java
+++ b/services/autofill/java/com/android/server/autofill/AutofillManagerService.java
@@ -52,6 +52,7 @@
import android.os.UserManagerInternal;
import android.provider.Settings;
import android.service.autofill.FillEventHistory;
+import android.service.autofill.UserData;
import android.util.LocalLog;
import android.util.Slog;
import android.util.SparseArray;
@@ -581,6 +582,34 @@
}
@Override
+ public UserData getUserData() throws RemoteException {
+ UserHandle user = getCallingUserHandle();
+ int uid = getCallingUid();
+
+ synchronized (mLock) {
+ AutofillManagerServiceImpl service = peekServiceForUserLocked(user.getIdentifier());
+ if (service != null) {
+ return service.getUserData(uid);
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ public void setUserData(UserData userData) throws RemoteException {
+ UserHandle user = getCallingUserHandle();
+ int uid = getCallingUid();
+
+ synchronized (mLock) {
+ AutofillManagerServiceImpl service = peekServiceForUserLocked(user.getIdentifier());
+ if (service != null) {
+ service.setUserData(uid, userData);
+ }
+ }
+ }
+
+ @Override
public boolean restoreSession(int sessionId, IBinder activityToken, IBinder appCallback)
throws RemoteException {
activityToken = Preconditions.checkNotNull(activityToken, "activityToken");
@@ -723,6 +752,7 @@
}
boolean oldDebug = sDebug;
+ final String prefix = " ";
try {
synchronized (mLock) {
oldDebug = sDebug;
@@ -731,6 +761,7 @@
pw.print("Verbose mode: "); pw.println(sVerbose);
pw.print("Disabled users: "); pw.println(mDisabledUsers);
pw.print("Max partitions per session: "); pw.println(sPartitionMaxCount);
+ pw.println("User data constraints: "); UserData.dumpConstraints(prefix, pw);
final int size = mServicesCache.size();
pw.print("Cached services: ");
if (size == 0) {
@@ -740,7 +771,7 @@
for (int i = 0; i < size; i++) {
pw.print("\nService at index "); pw.println(i);
final AutofillManagerServiceImpl impl = mServicesCache.valueAt(i);
- impl.dumpLocked(" ", pw);
+ impl.dumpLocked(prefix, pw);
}
}
mUi.dump(pw);
diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
index 21e2722..8b6dc20 100644
--- a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
+++ b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
@@ -51,6 +51,7 @@
import android.service.autofill.FillEventHistory.Event;
import android.service.autofill.FillResponse;
import android.service.autofill.IAutoFillService;
+import android.service.autofill.UserData;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
@@ -121,6 +122,11 @@
private boolean mDisabled;
/**
+ * Data used for field classification.
+ */
+ private UserData mUserData;
+
+ /**
* Caches whether the setup completed for the current user.
*/
@GuardedBy("mLock")
@@ -183,6 +189,14 @@
}
}
+ private int getServiceUidLocked() {
+ if (mInfo == null) {
+ Slog.w(TAG, "getServiceUidLocked(): no mInfo");
+ return -1;
+ }
+ return mInfo.getServiceInfo().applicationInfo.uid;
+ }
+
@Nullable
String getServicePackageName() {
final ComponentName serviceComponent = getServiceComponentName();
@@ -574,9 +588,9 @@
* Initializes the last fill selection after an autofill service returned a new
* {@link FillResponse}.
*/
- void setLastResponse(int serviceUid, int sessionId, @NonNull FillResponse response) {
+ void setLastResponse(int sessionId, @NonNull FillResponse response) {
synchronized (mLock) {
- mEventHistory = new FillEventHistory(serviceUid, sessionId, response.getClientState());
+ mEventHistory = new FillEventHistory(sessionId, response.getClientState());
}
}
@@ -688,18 +702,54 @@
*/
FillEventHistory getFillEventHistory(int callingUid) {
synchronized (mLock) {
- if (mEventHistory != null && mEventHistory.getServiceUid() == callingUid) {
+ if (mEventHistory != null
+ && isCalledByServiceLocked("getFillEventHistory", callingUid)) {
return mEventHistory;
}
}
-
return null;
}
+ // Called by Session - does not need to check uid
+ UserData getUserData() {
+ synchronized (mLock) {
+ return mUserData;
+ }
+ }
+
+ // Called by AutofillManager
+ UserData getUserData(int callingUid) {
+ synchronized (mLock) {
+ if (isCalledByServiceLocked("getUserData", callingUid)) {
+ return mUserData;
+ }
+ }
+ return null;
+ }
+
+ // Called by AutofillManager
+ void setUserData(int callingUid, UserData userData) {
+ synchronized (mLock) {
+ if (isCalledByServiceLocked("setUserData", callingUid)) {
+ mUserData = userData;
+ }
+ }
+ }
+
+ private boolean isCalledByServiceLocked(String methodName, int callingUid) {
+ if (getServiceUidLocked() != callingUid) {
+ Slog.w(TAG, methodName + "() called by UID " + callingUid
+ + ", but service UID is " + getServiceUidLocked());
+ return false;
+ }
+ return true;
+ }
+
void dumpLocked(String prefix, PrintWriter pw) {
final String prefix2 = prefix + " ";
pw.print(prefix); pw.print("User: "); pw.println(mUserId);
+ pw.print(prefix); pw.print("UID: "); pw.println(getServiceUidLocked());
pw.print(prefix); pw.print("Component: "); pw.println(mInfo != null
? mInfo.getServiceInfo().getComponentName() : null);
pw.print(prefix); pw.print("Component from settings: ");
@@ -762,8 +812,13 @@
}
}
- pw.print(prefix); pw.println("Clients");
- mClients.dump(pw, prefix2);
+ pw.print(prefix); pw.print("Clients: ");
+ if (mClients == null) {
+ pw.println("N/A");
+ } else {
+ pw.println();
+ mClients.dump(pw, prefix2);
+ }
if (mEventHistory == null || mEventHistory.getEvents() == null
|| mEventHistory.getEvents().size() == 0) {
@@ -779,6 +834,14 @@
+ event.getDatasetId());
}
}
+
+ pw.print(prefix); pw.print("User data: ");
+ if (mUserData == null) {
+ pw.println("N/A");
+ } else {
+ pw.println();
+ mUserData.dump(prefix2, pw);
+ }
}
void destroySessionsLocked() {
diff --git a/services/autofill/java/com/android/server/autofill/RemoteFillService.java b/services/autofill/java/com/android/server/autofill/RemoteFillService.java
index 831c488..aea9ad0 100644
--- a/services/autofill/java/com/android/server/autofill/RemoteFillService.java
+++ b/services/autofill/java/com/android/server/autofill/RemoteFillService.java
@@ -97,7 +97,7 @@
private PendingRequest mPendingRequest;
public interface FillServiceCallbacks {
- void onFillRequestSuccess(int requestFlags, @Nullable FillResponse response, int serviceUid,
+ void onFillRequestSuccess(int requestFlags, @Nullable FillResponse response,
@NonNull String servicePackageName);
void onFillRequestFailure(@Nullable CharSequence message,
@NonNull String servicePackageName);
@@ -281,11 +281,11 @@
mContext.unbindService(mServiceConnection);
}
- private void dispatchOnFillRequestSuccess(PendingRequest pendingRequest,
- int callingUid, int requestFlags, FillResponse response) {
+ private void dispatchOnFillRequestSuccess(PendingRequest pendingRequest, int requestFlags,
+ FillResponse response) {
mHandler.getHandler().post(() -> {
if (handleResponseCallbackCommon(pendingRequest)) {
- mCallbacks.onFillRequestSuccess(requestFlags, response, callingUid,
+ mCallbacks.onFillRequestSuccess(requestFlags, response,
mComponentName.getPackageName());
}
});
@@ -546,7 +546,7 @@
final RemoteFillService remoteService = getService();
if (remoteService != null) {
remoteService.dispatchOnFillRequestSuccess(PendingFillRequest.this,
- getCallingUid(), request.getFlags(), response);
+ request.getFlags(), response);
}
}
diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java
index e3db1b1..3615bca 100644
--- a/services/autofill/java/com/android/server/autofill/Session.java
+++ b/services/autofill/java/com/android/server/autofill/Session.java
@@ -55,7 +55,6 @@
import android.os.SystemClock;
import android.service.autofill.AutofillService;
import android.service.autofill.Dataset;
-import android.service.autofill.FieldsDetection;
import android.service.autofill.FillContext;
import android.service.autofill.FillRequest;
import android.service.autofill.FillResponse;
@@ -63,6 +62,7 @@
import android.service.autofill.InternalValidator;
import android.service.autofill.SaveInfo;
import android.service.autofill.SaveRequest;
+import android.service.autofill.UserData;
import android.service.autofill.ValueFinder;
import android.util.ArrayMap;
import android.util.ArraySet;
@@ -485,7 +485,7 @@
// FillServiceCallbacks
@Override
public void onFillRequestSuccess(int requestFlags, @Nullable FillResponse response,
- int serviceUid, @NonNull String servicePackageName) {
+ @NonNull String servicePackageName) {
synchronized (mLock) {
if (mDestroyed) {
Slog.w(TAG, "Call to Session#onFillRequestSuccess() rejected - session: "
@@ -499,13 +499,13 @@
}
// TODO(b/67867469): remove once feature is finished
- if (response.getFieldsDetection() != null && !mService.isFieldDetectionEnabled()) {
+ if (response.getFieldClassificationIds() != null && !mService.isFieldDetectionEnabled()) {
Slog.w(TAG, "Ignoring " + response + " because field detection is disabled");
processNullResponseLocked(requestFlags);
return;
}
- mService.setLastResponse(serviceUid, id, response);
+ mService.setLastResponse(id, response);
int sessionFinishedState = 0;
final long disableDuration = response.getDisableDuration();
@@ -908,7 +908,7 @@
final FillResponse response = mResponses.valueAt(i);
final List<Dataset> datasets = response.getDatasets();
if (datasets == null || datasets.isEmpty()) {
- if (sVerbose) Slog.v(TAG, "logContextCommitted() no datasets at " + i);
+ if (sVerbose) Slog.v(TAG, "logContextCommitted() no datasets at " + i);
} else {
for (int j = 0; j < datasets.size(); j++) {
final Dataset dataset = datasets.get(j);
@@ -931,25 +931,27 @@
}
}
}
- final FieldsDetection fieldsDetection = lastResponse.getFieldsDetection();
+ final AutofillId[] fieldClassificationIds = lastResponse.getFieldClassificationIds();
- if (!hasAtLeastOneDataset && fieldsDetection == null) {
+ if (!hasAtLeastOneDataset && fieldClassificationIds == null) {
if (sVerbose) {
Slog.v(TAG, "logContextCommittedLocked(): skipped (no datasets nor fields "
- + "detection)");
+ + "classification ids)");
}
return;
}
+ final UserData userData = mService.getUserData();
final AutofillId detectableFieldId;
final String detectableRemoteId;
String detectedRemoteId = null;
- if (fieldsDetection == null) {
+ if (userData == null) {
detectableFieldId = null;
detectableRemoteId = null;
} else {
- detectableFieldId = fieldsDetection.getFieldId();
- detectableRemoteId = fieldsDetection.getRemoteId();
+ // TODO(b/67867469): hardcoded to just first entry on initial refactoring.
+ detectableFieldId = fieldClassificationIds[0];
+ detectableRemoteId = userData.getRemoteIds()[0];
}
int detectedFieldScore = -1;
@@ -1062,7 +1064,8 @@
if (detectableFieldId != null && detectableFieldId.equals(viewState.id)
&& currentValue.isText() && currentValue.getTextValue() != null) {
final String actualValue = currentValue.getTextValue().toString();
- final String expectedValue = fieldsDetection.getValue();
+ // TODO(b/67867469): hardcoded to just first entry on initial refactoring.
+ final String expectedValue = userData.getValues()[0];
if (actualValue.equalsIgnoreCase(expectedValue)) {
detectedRemoteId = detectableRemoteId;
detectedFieldScore = 0;
diff --git a/services/core/java/com/android/server/EventLogTags.logtags b/services/core/java/com/android/server/EventLogTags.logtags
index d3ce306..6174aec 100644
--- a/services/core/java/com/android/server/EventLogTags.logtags
+++ b/services/core/java/com/android/server/EventLogTags.logtags
@@ -32,9 +32,11 @@
# The device is being asked to go into a soft sleep (typically by the ungaze gesture).
# It logs the time remaining before the device would've normally gone to sleep without the request.
2731 power_soft_sleep_requested (savedwaketimems|2)
+# Power save state has changed. See BatterySaverController.java for the details.
+2739 battery_saver_mode (prevOffOrOn|1|5),(nowOffOrOn|1|5),(interactive|1|5),(features|3|5)
#
-# Leave IDs through 2739 for more power logs (2730 used by battery_discharge above)
+# Leave IDs through 2740 for more power logs (2730 used by battery_discharge above)
#
diff --git a/services/core/java/com/android/server/notification/ScheduleCalendar.java b/services/core/java/com/android/server/notification/ScheduleCalendar.java
index 40230bd..5ff0e21 100644
--- a/services/core/java/com/android/server/notification/ScheduleCalendar.java
+++ b/services/core/java/com/android/server/notification/ScheduleCalendar.java
@@ -18,6 +18,7 @@
import android.service.notification.ZenModeConfig.ScheduleInfo;
import android.util.ArraySet;
+import android.util.Log;
import java.util.Calendar;
import java.util.Objects;
@@ -41,10 +42,25 @@
}
public void maybeSetNextAlarm(long now, long nextAlarm) {
- if (mSchedule != null) {
- if (mSchedule.exitAtAlarm
- && (now > mSchedule.nextAlarm || nextAlarm < mSchedule.nextAlarm)) {
- mSchedule.nextAlarm = nextAlarm;
+ if (mSchedule != null && mSchedule.exitAtAlarm) {
+ // alarm canceled
+ if (nextAlarm == 0) {
+ mSchedule.nextAlarm = 0;
+ }
+ // only allow alarms in the future
+ if (nextAlarm > now) {
+ // store earliest alarm
+ if (mSchedule.nextAlarm == 0) {
+ mSchedule.nextAlarm = nextAlarm;
+ } else {
+ mSchedule.nextAlarm = Math.min(mSchedule.nextAlarm, nextAlarm);
+ }
+ } else if (mSchedule.nextAlarm < now) {
+ if (ScheduleConditionProvider.DEBUG) {
+ Log.d(ScheduleConditionProvider.TAG,
+ "All alarms are in the past " + mSchedule.nextAlarm);
+ }
+ mSchedule.nextAlarm = 0;
}
}
}
@@ -87,6 +103,9 @@
}
public boolean shouldExitForAlarm(long time) {
+ if (mSchedule == null) {
+ return false;
+ }
return mSchedule.exitAtAlarm
&& mSchedule.nextAlarm != 0
&& time >= mSchedule.nextAlarm;
diff --git a/services/core/java/com/android/server/notification/ScheduleConditionProvider.java b/services/core/java/com/android/server/notification/ScheduleConditionProvider.java
index 50a51b2..c5f80bb 100644
--- a/services/core/java/com/android/server/notification/ScheduleConditionProvider.java
+++ b/services/core/java/com/android/server/notification/ScheduleConditionProvider.java
@@ -37,6 +37,8 @@
import android.util.Log;
import android.util.Slog;
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.server.notification.NotificationManagerService.DumpFilter;
import java.io.PrintWriter;
@@ -62,10 +64,9 @@
private static final String SEPARATOR = ";";
private static final String SCP_SETTING = "snoozed_schedule_condition_provider";
-
private final Context mContext = this;
private final ArrayMap<Uri, ScheduleCalendar> mSubscriptions = new ArrayMap<>();
- private ArraySet<Uri> mSnoozed = new ArraySet<>();
+ private ArraySet<Uri> mSnoozedForAlarm = new ArraySet<>();
private AlarmManager mAlarmManager;
private boolean mConnected;
@@ -102,7 +103,7 @@
pw.println(mSubscriptions.get(conditionId).toString());
}
}
- pw.println(" snoozed due to alarm: " + TextUtils.join(SEPARATOR, mSnoozed));
+ pw.println(" snoozed due to alarm: " + TextUtils.join(SEPARATOR, mSnoozedForAlarm));
dumpUpcomingTime(pw, "mNextAlarmTime", mNextAlarmTime, now);
}
@@ -129,7 +130,7 @@
public void onSubscribe(Uri conditionId) {
if (DEBUG) Slog.d(TAG, "onSubscribe " + conditionId);
if (!ZenModeConfig.isValidScheduleConditionId(conditionId)) {
- notifyCondition(createCondition(conditionId, Condition.STATE_FALSE, "badCondition"));
+ notifyCondition(createCondition(conditionId, Condition.STATE_ERROR, "invalidId"));
return;
}
synchronized (mSubscriptions) {
@@ -169,32 +170,11 @@
synchronized (mSubscriptions) {
setRegistered(!mSubscriptions.isEmpty());
for (Uri conditionId : mSubscriptions.keySet()) {
- final ScheduleCalendar cal = mSubscriptions.get(conditionId);
- if (cal != null && cal.isInSchedule(now)) {
- if (conditionSnoozed(conditionId) || cal.shouldExitForAlarm(now)) {
- conditionsToNotify.add(createCondition(
- conditionId, Condition.STATE_FALSE, "alarmCanceled"));
- addSnoozed(conditionId);
- } else {
- conditionsToNotify.add(createCondition(
- conditionId, Condition.STATE_TRUE, "meetsSchedule"));
- }
- cal.maybeSetNextAlarm(now, nextUserAlarmTime);
- } else {
- conditionsToNotify.add(createCondition(
- conditionId, Condition.STATE_FALSE, "!meetsSchedule"));
- removeSnoozed(conditionId);
- if (cal != null && nextUserAlarmTime == 0) {
- cal.maybeSetNextAlarm(now, nextUserAlarmTime);
- }
- }
- if (cal != null) {
- final long nextChangeTime = cal.getNextChangeTime(now);
- if (nextChangeTime > 0 && nextChangeTime > now) {
- if (mNextAlarmTime == 0 || nextChangeTime < mNextAlarmTime) {
- mNextAlarmTime = nextChangeTime;
- }
- }
+ Condition condition =
+ evaluateSubscriptionLocked(conditionId, mSubscriptions.get(conditionId),
+ now, nextUserAlarmTime);
+ if (condition != null) {
+ conditionsToNotify.add(condition);
}
}
}
@@ -202,6 +182,39 @@
updateAlarm(now, mNextAlarmTime);
}
+ @VisibleForTesting
+ @GuardedBy("mSubscriptions")
+ Condition evaluateSubscriptionLocked(Uri conditionId, ScheduleCalendar cal,
+ long now, long nextUserAlarmTime) {
+ Condition condition;
+ if (cal == null) {
+ condition = createCondition(conditionId, Condition.STATE_ERROR, "!invalidId");
+ removeSnoozed(conditionId);
+ return condition;
+ }
+ if (cal.isInSchedule(now)) {
+ if (conditionSnoozed(conditionId)) {
+ condition = createCondition(conditionId, Condition.STATE_FALSE, "snoozed");
+ } else if (cal.shouldExitForAlarm(now)) {
+ condition = createCondition(conditionId, Condition.STATE_FALSE, "alarmCanceled");
+ addSnoozed(conditionId);
+ } else {
+ condition = createCondition(conditionId, Condition.STATE_TRUE, "meetsSchedule");
+ }
+ } else {
+ condition = createCondition(conditionId, Condition.STATE_FALSE, "!meetsSchedule");
+ removeSnoozed(conditionId);
+ }
+ cal.maybeSetNextAlarm(now, nextUserAlarmTime);
+ final long nextChangeTime = cal.getNextChangeTime(now);
+ if (nextChangeTime > 0 && nextChangeTime > now) {
+ if (mNextAlarmTime == 0 || nextChangeTime < mNextAlarmTime) {
+ mNextAlarmTime = nextChangeTime;
+ }
+ }
+ return condition;
+ }
+
private void updateAlarm(long now, long time) {
final AlarmManager alarms = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
final PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext,
@@ -266,27 +279,28 @@
}
private boolean conditionSnoozed(Uri conditionId) {
- synchronized (mSnoozed) {
- return mSnoozed.contains(conditionId);
+ synchronized (mSnoozedForAlarm) {
+ return mSnoozedForAlarm.contains(conditionId);
}
}
- private void addSnoozed(Uri conditionId) {
- synchronized (mSnoozed) {
- mSnoozed.add(conditionId);
+ @VisibleForTesting
+ void addSnoozed(Uri conditionId) {
+ synchronized (mSnoozedForAlarm) {
+ mSnoozedForAlarm.add(conditionId);
saveSnoozedLocked();
}
}
private void removeSnoozed(Uri conditionId) {
- synchronized (mSnoozed) {
- mSnoozed.remove(conditionId);
+ synchronized (mSnoozedForAlarm) {
+ mSnoozedForAlarm.remove(conditionId);
saveSnoozedLocked();
}
}
- public void saveSnoozedLocked() {
- final String setting = TextUtils.join(SEPARATOR, mSnoozed);
+ private void saveSnoozedLocked() {
+ final String setting = TextUtils.join(SEPARATOR, mSnoozedForAlarm);
final int currentUser = ActivityManager.getCurrentUser();
Settings.Secure.putStringForUser(mContext.getContentResolver(),
SCP_SETTING,
@@ -294,8 +308,8 @@
currentUser);
}
- public void readSnoozed() {
- synchronized (mSnoozed) {
+ private void readSnoozed() {
+ synchronized (mSnoozedForAlarm) {
long identity = Binder.clearCallingIdentity();
try {
final String setting = Settings.Secure.getStringForUser(
@@ -312,7 +326,7 @@
if (TextUtils.isEmpty(token)) {
continue;
}
- mSnoozed.add(Uri.parse(token));
+ mSnoozedForAlarm.add(Uri.parse(token));
}
}
} finally {
diff --git a/services/core/java/com/android/server/power/BatterySaverPolicy.java b/services/core/java/com/android/server/power/BatterySaverPolicy.java
index 3ccf17e..3810192 100644
--- a/services/core/java/com/android/server/power/BatterySaverPolicy.java
+++ b/services/core/java/com/android/server/power/BatterySaverPolicy.java
@@ -87,6 +87,12 @@
private String mDeviceSpecificSettingsSource; // For dump() only.
/**
+ * A short string describing which battery saver is now enabled, which we dump in the eventlog.
+ */
+ @GuardedBy("mLock")
+ private String mEventLogKeys;
+
+ /**
* {@code true} if vibration is disabled in battery saver mode.
*
* @see Settings.Global#BATTERY_SAVER_CONSTANTS
@@ -354,6 +360,27 @@
mFilesForNoninteractive = (new CpuFrequencies()).parseString(
parser.getString(KEY_CPU_FREQ_NONINTERACTIVE, "")).toSysFileMap();
+
+ final StringBuilder sb = new StringBuilder();
+
+ if (mForceAllAppsStandby) sb.append("A");
+ if (mForceBackgroundCheck) sb.append("B");
+
+ if (mVibrationDisabled) sb.append("v");
+ if (mAnimationDisabled) sb.append("a");
+ if (mSoundTriggerDisabled) sb.append("s");
+ if (mFullBackupDeferred) sb.append("F");
+ if (mKeyValueBackupDeferred) sb.append("K");
+ if (!mFireWallDisabled) sb.append("f");
+ if (!mDataSaverDisabled) sb.append("d");
+ if (!mAdjustBrightnessDisabled) sb.append("b");
+
+ if (mLaunchBoostDisabled) sb.append("l");
+ if (mOptionalSensorsDisabled) sb.append("S");
+
+ sb.append(mGpsMode);
+
+ mEventLogKeys = sb.toString();
}
/**
@@ -431,6 +458,12 @@
}
}
+ public String toEventLogString() {
+ synchronized (mLock) {
+ return mEventLogKeys;
+ }
+ }
+
public void dump(PrintWriter pw) {
synchronized (mLock) {
pw.println();
diff --git a/services/core/java/com/android/server/power/batterysaver/BatterySaverController.java b/services/core/java/com/android/server/power/batterysaver/BatterySaverController.java
index 1b19c33..a6bca0b 100644
--- a/services/core/java/com/android/server/power/batterysaver/BatterySaverController.java
+++ b/services/core/java/com/android/server/power/batterysaver/BatterySaverController.java
@@ -45,6 +45,7 @@
import com.android.internal.notification.SystemNotificationChannels;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.Preconditions;
+import com.android.server.EventLogTags;
import com.android.server.LocalServices;
import com.android.server.power.BatterySaverPolicy;
import com.android.server.power.BatterySaverPolicy.BatterySaverPolicyListener;
@@ -77,6 +78,12 @@
@GuardedBy("mLock")
private boolean mEnabled;
+ /**
+ * Previously enabled or not; only for the event logging. Only use it from
+ * {@link #handleBatterySaverStateChanged}.
+ */
+ private boolean mPreviouslyEnabled;
+
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
@@ -213,12 +220,18 @@
final ArrayMap<String, String> fileValues;
synchronized (mLock) {
- Slog.i(TAG, "Battery saver " + (mEnabled ? "enabled" : "disabled")
- + ": isInteractive=" + isInteractive);
+ EventLogTags.writeBatterySaverMode(
+ mPreviouslyEnabled ? 1 : 0, // Previously off or on.
+ mEnabled ? 1 : 0, // Now off or on.
+ isInteractive ? 1 : 0, // Device interactive state.
+ mEnabled ? mBatterySaverPolicy.toEventLogString() : "");
+ mPreviouslyEnabled = mEnabled;
listeners = mListeners.toArray(new LowPowerModeListener[mListeners.size()]);
+
enabled = mEnabled;
+
if (enabled) {
fileValues = mBatterySaverPolicy.getFileValues(isInteractive);
} else {
diff --git a/services/tests/notification/src/com/android/server/notification/ScheduleCalendarTest.java b/services/tests/notification/src/com/android/server/notification/ScheduleCalendarTest.java
new file mode 100644
index 0000000..cbda12d
--- /dev/null
+++ b/services/tests/notification/src/com/android/server/notification/ScheduleCalendarTest.java
@@ -0,0 +1,327 @@
+/*
+ * Copyright (C) 2017 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.server.notification;
+
+import static junit.framework.Assert.assertFalse;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.service.notification.ZenModeConfig;
+import android.support.test.runner.AndroidJUnit4;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.util.Slog;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ScheduleCalendarTest extends NotificationTestCase {
+
+ private ScheduleCalendar mScheduleCalendar;
+ private ZenModeConfig.ScheduleInfo mScheduleInfo;
+
+ @Before
+ public void setUp() throws Exception {
+ mScheduleCalendar = new ScheduleCalendar();
+ mScheduleInfo = new ZenModeConfig.ScheduleInfo();
+ mScheduleInfo.days = new int[] {1, 2, 3, 4, 5};
+ mScheduleCalendar.setSchedule(mScheduleInfo);
+ }
+
+ @Test
+ public void testNullScheduleInfo() throws Exception {
+ mScheduleCalendar.setSchedule(null);
+
+ mScheduleCalendar.maybeSetNextAlarm(1000, 1999);
+ assertEquals(0, mScheduleCalendar.getNextChangeTime(1000));
+ assertFalse(mScheduleCalendar.isInSchedule(100));
+ assertFalse(mScheduleCalendar.shouldExitForAlarm(100));
+ }
+
+ @Test
+ public void testGetNextChangeTime_startToday() throws Exception {
+ Calendar cal = new GregorianCalendar();
+ cal.set(Calendar.HOUR_OF_DAY, 1);
+ cal.set(Calendar.MINUTE, 15);
+ cal.set(Calendar.SECOND, 0);
+ cal.set(Calendar.MILLISECOND, 0);
+ mScheduleInfo.days = new int[] {getTodayDay()};
+ mScheduleInfo.startHour = cal.get(Calendar.HOUR_OF_DAY) + 1;
+ mScheduleInfo.endHour = cal.get(Calendar.HOUR_OF_DAY) + 3;
+ mScheduleInfo.startMinute = 15;
+ mScheduleInfo.endMinute = 15;
+ mScheduleInfo.exitAtAlarm = false;
+ mScheduleCalendar.setSchedule(mScheduleInfo);
+
+ Calendar expected = new GregorianCalendar();
+ expected.setTimeInMillis(cal.getTimeInMillis());
+ expected.set(Calendar.HOUR_OF_DAY, mScheduleInfo.startHour);
+
+ long actualMs = mScheduleCalendar.getNextChangeTime(cal.getTimeInMillis());
+ GregorianCalendar actual = new GregorianCalendar();
+ actual.setTimeInMillis(actualMs);
+ assertEquals("Expected " + expected + " was " + actual, expected.getTimeInMillis(),
+ actualMs);
+ }
+
+ @Test
+ public void testGetNextChangeTime_endToday() throws Exception {
+ Calendar cal = new GregorianCalendar();
+ cal.set(Calendar.HOUR_OF_DAY, 2);
+ cal.set(Calendar.MINUTE, 15);
+ cal.set(Calendar.SECOND, 0);
+ cal.set(Calendar.MILLISECOND, 0);
+ mScheduleInfo.days = new int[] {getTodayDay()};
+ mScheduleInfo.startHour = cal.get(Calendar.HOUR_OF_DAY) - 1;
+ mScheduleInfo.endHour = cal.get(Calendar.HOUR_OF_DAY) + 3;
+ mScheduleInfo.startMinute = 15;
+ mScheduleInfo.endMinute = 15;
+ mScheduleInfo.exitAtAlarm = false;
+
+ Calendar expected = new GregorianCalendar();
+ expected.setTimeInMillis(cal.getTimeInMillis());
+ expected.set(Calendar.HOUR_OF_DAY, mScheduleInfo.endHour);
+ expected.set(Calendar.MINUTE, mScheduleInfo.endMinute);
+
+ long actualMs = mScheduleCalendar.getNextChangeTime(cal.getTimeInMillis());
+ GregorianCalendar actual = new GregorianCalendar();
+ actual.setTimeInMillis(actualMs);
+ assertEquals("Expected " + expected + " was " + actual, expected.getTimeInMillis(),
+ actualMs);
+ }
+
+ @Test
+ public void testGetNextChangeTime_startTomorrow() throws Exception {
+ Calendar cal = new GregorianCalendar();
+ cal.set(Calendar.HOUR_OF_DAY, 23);
+ cal.set(Calendar.MINUTE, 15);
+ cal.set(Calendar.SECOND, 0);
+ cal.set(Calendar.MILLISECOND, 0);
+ mScheduleInfo.days = new int[] {getTodayDay(), getTodayDay() + 1};
+ mScheduleInfo.startHour = 1;
+ mScheduleInfo.endHour = 3;
+ mScheduleInfo.startMinute = 15;
+ mScheduleInfo.endMinute = 15;
+ mScheduleInfo.exitAtAlarm = false;
+
+ Calendar expected = new GregorianCalendar();
+ expected.setTimeInMillis(cal.getTimeInMillis());
+ expected.add(Calendar.DATE, 1);
+ expected.set(Calendar.HOUR_OF_DAY, mScheduleInfo.startHour);
+ expected.set(Calendar.MINUTE, mScheduleInfo.startMinute);
+
+ long actualMs = mScheduleCalendar.getNextChangeTime(cal.getTimeInMillis());
+ GregorianCalendar actual = new GregorianCalendar();
+ actual.setTimeInMillis(actualMs);
+ assertEquals("Expected " + expected + " was " + actual, expected.getTimeInMillis(),
+ actualMs);
+ }
+
+ @Test
+ public void testGetNextChangeTime_endTomorrow() throws Exception {
+ Calendar cal = new GregorianCalendar();
+ cal.set(Calendar.HOUR_OF_DAY, 23);
+ cal.set(Calendar.MINUTE, 15);
+ cal.set(Calendar.SECOND, 0);
+ cal.set(Calendar.MILLISECOND, 0);
+ mScheduleInfo.days = new int[] {getTodayDay(), getTodayDay() + 1};
+ mScheduleInfo.startHour = 22;
+ mScheduleInfo.endHour = 3;
+ mScheduleInfo.startMinute = 15;
+ mScheduleInfo.endMinute = 15;
+ mScheduleInfo.exitAtAlarm = false;
+
+ Calendar expected = new GregorianCalendar();
+ expected.setTimeInMillis(cal.getTimeInMillis());
+ expected.add(Calendar.DATE, 1);
+ expected.set(Calendar.HOUR_OF_DAY, mScheduleInfo.endHour);
+ expected.set(Calendar.MINUTE, mScheduleInfo.endMinute);
+
+ long actualMs = mScheduleCalendar.getNextChangeTime(cal.getTimeInMillis());
+ GregorianCalendar actual = new GregorianCalendar();
+ actual.setTimeInMillis(actualMs);
+ assertEquals("Expected " + expected + " was " + actual, expected.getTimeInMillis(),
+ actualMs);
+ }
+
+ @Test
+ public void testShouldExitForAlarm_settingOff() {
+ mScheduleInfo.exitAtAlarm = false;
+ mScheduleInfo.nextAlarm = 1000;
+
+ assertFalse(mScheduleCalendar.shouldExitForAlarm(1000));
+ }
+
+ @Test
+ public void testShouldExitForAlarm_beforeAlarm() {
+ mScheduleInfo.exitAtAlarm = true;
+ mScheduleInfo.nextAlarm = 1000;
+
+ assertFalse(mScheduleCalendar.shouldExitForAlarm(999));
+ }
+
+ @Test
+ public void testShouldExitForAlarm_noAlarm() {
+ mScheduleInfo.exitAtAlarm = true;
+ mScheduleInfo.nextAlarm = 0;
+
+ assertFalse(mScheduleCalendar.shouldExitForAlarm(999));
+ }
+
+ @Test
+ public void testShouldExitForAlarm() {
+ mScheduleInfo.exitAtAlarm = true;
+ mScheduleInfo.nextAlarm = 1000;
+
+ assertTrue(mScheduleCalendar.shouldExitForAlarm(1000));
+ }
+
+ @Test
+ public void testMaybeSetNextAlarm_settingOff() {
+ mScheduleInfo.exitAtAlarm = false;
+ mScheduleInfo.nextAlarm = 0;
+
+ mScheduleCalendar.maybeSetNextAlarm(1000, 2000);
+
+ assertEquals(0, mScheduleInfo.nextAlarm);
+ }
+
+ @Test
+ public void testMaybeSetNextAlarm_settingOn() {
+ mScheduleInfo.exitAtAlarm = true;
+ mScheduleInfo.nextAlarm = 0;
+
+ mScheduleCalendar.maybeSetNextAlarm(1000, 2000);
+
+ assertEquals(2000, mScheduleInfo.nextAlarm);
+ }
+
+ @Test
+ public void testMaybeSetNextAlarm_alarmCanceled() {
+ mScheduleInfo.exitAtAlarm = true;
+ mScheduleInfo.nextAlarm = 10000;
+
+ mScheduleCalendar.maybeSetNextAlarm(1000, 0);
+
+ assertEquals(0, mScheduleInfo.nextAlarm);
+ }
+
+ @Test
+ public void testMaybeSetNextAlarm_earlierAlarm() {
+ mScheduleInfo.exitAtAlarm = true;
+ mScheduleInfo.nextAlarm = 2000;
+
+ mScheduleCalendar.maybeSetNextAlarm(1000, 1500);
+
+ assertEquals(1500, mScheduleInfo.nextAlarm);
+ }
+
+ @Test
+ public void testMaybeSetNextAlarm_laterAlarm() {
+ mScheduleInfo.exitAtAlarm = true;
+ mScheduleInfo.nextAlarm = 2000;
+
+ mScheduleCalendar.maybeSetNextAlarm(1000, 3000);
+
+ assertEquals(2000, mScheduleInfo.nextAlarm);
+ }
+
+ @Test
+ public void testMaybeSetNextAlarm_expiredAlarm() {
+ mScheduleInfo.exitAtAlarm = true;
+ mScheduleInfo.nextAlarm = 998;
+
+ mScheduleCalendar.maybeSetNextAlarm(1000, 999);
+
+ assertEquals(0, mScheduleInfo.nextAlarm);
+ }
+
+ @Test
+ public void testIsInSchedule_inScheduleOvernight() {
+ Calendar cal = new GregorianCalendar();
+ cal.set(Calendar.HOUR_OF_DAY, 23);
+ cal.set(Calendar.MINUTE, 15);
+ cal.set(Calendar.SECOND, 0);
+ cal.set(Calendar.MILLISECOND, 0);
+ mScheduleInfo.days = new int[] {getTodayDay()};
+ mScheduleInfo.startHour = 22;
+ mScheduleInfo.endHour = 3;
+ mScheduleInfo.startMinute = 15;
+ mScheduleInfo.endMinute = 15;
+
+ assertTrue(mScheduleCalendar.isInSchedule(cal.getTimeInMillis()));
+ }
+
+ @Test
+ public void testIsInSchedule_inScheduleSingleDay() {
+ Calendar cal = new GregorianCalendar();
+ cal.set(Calendar.HOUR_OF_DAY, 14);
+ cal.set(Calendar.MINUTE, 15);
+ cal.set(Calendar.SECOND, 0);
+ cal.set(Calendar.MILLISECOND, 0);
+ mScheduleInfo.days = new int[] {getTodayDay()};
+ mScheduleInfo.startHour = 12;
+ mScheduleInfo.endHour = 3;
+ mScheduleInfo.startMinute = 16;
+ mScheduleInfo.endMinute = 15;
+
+ assertTrue(mScheduleCalendar.isInSchedule(cal.getTimeInMillis()));
+ }
+
+ @Test
+ public void testIsInSchedule_notToday() {
+ Calendar cal = new GregorianCalendar();
+ cal.set(Calendar.HOUR_OF_DAY, 14);
+ cal.set(Calendar.MINUTE, 15);
+ cal.set(Calendar.SECOND, 0);
+ cal.set(Calendar.MILLISECOND, 0);
+ cal.set(Calendar.DAY_OF_WEEK, Calendar.SATURDAY);
+ mScheduleInfo.days = new int[] {Calendar.FRIDAY, Calendar.SUNDAY};
+ mScheduleInfo.startHour = 12;
+ mScheduleInfo.startMinute = 16;
+ mScheduleInfo.endHour = 15;
+ mScheduleInfo.endMinute = 15;
+
+ assertFalse(mScheduleCalendar.isInSchedule(cal.getTimeInMillis()));
+ }
+
+ @Test
+ public void testIsInSchedule_startingSoon() {
+ Calendar cal = new GregorianCalendar();
+ cal.set(Calendar.HOUR_OF_DAY, 14);
+ cal.set(Calendar.MINUTE, 15);
+ cal.set(Calendar.SECOND, 59);
+ cal.set(Calendar.MILLISECOND, 0);
+ mScheduleInfo.days = new int[] {getTodayDay()};
+ mScheduleInfo.startHour = 14;
+ mScheduleInfo.endHour = 3;
+ mScheduleInfo.startMinute = 16;
+ mScheduleInfo.endMinute = 15;
+
+ assertFalse(mScheduleCalendar.isInSchedule(cal.getTimeInMillis()));
+ }
+
+ private int getTodayDay() {
+ return new GregorianCalendar().get(Calendar.DAY_OF_WEEK);
+ }
+}
diff --git a/services/tests/notification/src/com/android/server/notification/ScheduleConditionProviderTest.java b/services/tests/notification/src/com/android/server/notification/ScheduleConditionProviderTest.java
new file mode 100644
index 0000000..ddf46a0
--- /dev/null
+++ b/services/tests/notification/src/com/android/server/notification/ScheduleConditionProviderTest.java
@@ -0,0 +1,337 @@
+package com.android.server.notification;
+
+import static org.mockito.Mockito.spy;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Looper;
+import android.service.notification.Condition;
+import android.service.notification.ZenModeConfig;
+import android.support.test.InstrumentationRegistry;
+import android.test.ServiceTestCase;
+import android.testing.TestableContext;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+
+public class ScheduleConditionProviderTest extends ServiceTestCase<ScheduleConditionProvider> {
+
+ ScheduleConditionProvider mService;
+
+ @Rule
+ public final TestableContext mContext =
+ new TestableContext(InstrumentationRegistry.getContext(), null);
+
+ public ScheduleConditionProviderTest() {
+ super(ScheduleConditionProvider.class);
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ Looper.prepare();
+
+ Intent startIntent =
+ new Intent("com.android.server.notification.ScheduleConditionProvider");
+ startIntent.setPackage("android");
+ bindService(startIntent);
+ mService = spy(getService());
+ }
+
+ @Test
+ public void testIsValidConditionId_incomplete() throws Exception {
+ Uri badConditionId = Uri.EMPTY;
+ assertFalse(mService.isValidConditionId(badConditionId));
+ assertEquals(Condition.STATE_ERROR,
+ mService.evaluateSubscriptionLocked(badConditionId, null, 0, 1000).state);
+ }
+
+ @Test
+ public void testIsValidConditionId() throws Exception {
+ ZenModeConfig.ScheduleInfo info = new ZenModeConfig.ScheduleInfo();
+ info.days = new int[] {1, 2, 4};
+ info.startHour = 8;
+ info.startMinute = 56;
+ info.nextAlarm = 1000;
+ info.exitAtAlarm = true;
+ info.endHour = 12;
+ info.endMinute = 9;
+ Uri conditionId = ZenModeConfig.toScheduleConditionId(info);
+ assertTrue(mService.isValidConditionId(conditionId));
+ }
+
+ @Test
+ public void testEvaluateSubscription_noAlarmExit_InSchedule() {
+ Calendar now = getNow();
+
+ // Schedule - 1 hour long; starts now
+ ZenModeConfig.ScheduleInfo info = new ZenModeConfig.ScheduleInfo();
+ info.days = new int[] {Calendar.FRIDAY};
+ info.startHour = now.get(Calendar.HOUR_OF_DAY);
+ info.startMinute = now.get(Calendar.MINUTE);
+ info.nextAlarm = 0;
+ info.exitAtAlarm = false;
+ info.endHour = now.get(Calendar.HOUR_OF_DAY) + 1;
+ info.endMinute = info.startMinute;
+ Uri conditionId = ZenModeConfig.toScheduleConditionId(info);
+ ScheduleCalendar cal = new ScheduleCalendar();
+ cal.setSchedule(info);
+ assertTrue(cal.isInSchedule(now.getTimeInMillis()));
+
+ Condition condition = mService.evaluateSubscriptionLocked(
+ conditionId, cal, now.getTimeInMillis(), now.getTimeInMillis() + 1000);
+
+ assertEquals(Condition.STATE_TRUE, condition.state);
+ }
+
+ @Test
+ public void testEvaluateSubscription_noAlarmExit_InScheduleSnoozed() {
+ Calendar now = getNow();
+
+ // Schedule - 1 hour long; starts now
+ ZenModeConfig.ScheduleInfo info = new ZenModeConfig.ScheduleInfo();
+ info.days = new int[] {Calendar.FRIDAY};
+ info.startHour = now.get(Calendar.HOUR_OF_DAY);
+ info.startMinute = now.get(Calendar.MINUTE);
+ info.nextAlarm = 0;
+ info.exitAtAlarm = false;
+ info.endHour = now.get(Calendar.HOUR_OF_DAY) + 1;
+ info.endMinute = info.startMinute;
+ Uri conditionId = ZenModeConfig.toScheduleConditionId(info);
+ ScheduleCalendar cal = new ScheduleCalendar();
+ cal.setSchedule(info);
+ assertTrue(cal.isInSchedule(now.getTimeInMillis()));
+
+ mService.addSnoozed(conditionId);
+
+ Condition condition = mService.evaluateSubscriptionLocked(
+ conditionId, cal, now.getTimeInMillis(), now.getTimeInMillis() + 1000);
+
+ assertEquals(Condition.STATE_FALSE, condition.state);
+ }
+
+ @Test
+ public void testEvaluateSubscription_noAlarmExit_beforeSchedule() {
+ Calendar now = new GregorianCalendar();
+ now.set(Calendar.HOUR_OF_DAY, 14);
+ now.set(Calendar.MINUTE, 15);
+ now.set(Calendar.SECOND, 59);
+ now.set(Calendar.MILLISECOND, 0);
+ now.set(Calendar.DAY_OF_WEEK, Calendar.FRIDAY);
+
+ // Schedule - 1 hour long; starts in 1 second
+ ZenModeConfig.ScheduleInfo info = new ZenModeConfig.ScheduleInfo();
+ info.days = new int[] {Calendar.FRIDAY};
+ info.startHour = now.get(Calendar.HOUR_OF_DAY);
+ info.startMinute = now.get(Calendar.MINUTE) + 1;
+ info.nextAlarm = 0;
+ info.exitAtAlarm = false;
+ info.endHour = now.get(Calendar.HOUR_OF_DAY) + 1;
+ info.endMinute = info.startMinute;
+ Uri conditionId = ZenModeConfig.toScheduleConditionId(info);
+ ScheduleCalendar cal = new ScheduleCalendar();
+ cal.setSchedule(info);
+
+ Condition condition = mService.evaluateSubscriptionLocked(
+ conditionId, cal, now.getTimeInMillis(), now.getTimeInMillis() + 1000);
+
+ assertEquals(Condition.STATE_FALSE, condition.state);
+ }
+
+ @Test
+ public void testEvaluateSubscription_noAlarmExit_endSchedule() {
+ Calendar now = getNow();
+
+ // Schedule - 1 hour long; ends now
+ ZenModeConfig.ScheduleInfo info = new ZenModeConfig.ScheduleInfo();
+ info.days = new int[] {Calendar.FRIDAY};
+ info.startHour = now.get(Calendar.HOUR_OF_DAY) - 1;
+ info.startMinute = now.get(Calendar.MINUTE);
+ info.nextAlarm = 0;
+ info.exitAtAlarm = false;
+ info.endHour = now.get(Calendar.HOUR_OF_DAY);
+ info.endMinute = now.get(Calendar.MINUTE);
+ Uri conditionId = ZenModeConfig.toScheduleConditionId(info);
+ ScheduleCalendar cal = new ScheduleCalendar();
+ cal.setSchedule(info);
+
+ Condition condition = mService.evaluateSubscriptionLocked(
+ conditionId, cal, now.getTimeInMillis(), now.getTimeInMillis() + 1000);
+
+ assertEquals(Condition.STATE_FALSE, condition.state);
+ }
+
+ @Test
+ public void testEvaluateSubscription_alarmSetBeforeInSchedule() {
+ Calendar now = getNow();
+
+ // Schedule - 1 hour long; starts now, ends with alarm
+ ZenModeConfig.ScheduleInfo info = getScheduleEndsInHour(now);
+ Uri conditionId = ZenModeConfig.toScheduleConditionId(info);
+ ScheduleCalendar cal = new ScheduleCalendar();
+ cal.setSchedule(info);
+
+ // an hour before start, update with an alarm that will fire during the schedule
+ mService.evaluateSubscriptionLocked(
+ conditionId, cal, now.getTimeInMillis() - 1000, now.getTimeInMillis() + 1000);
+
+ // at start, should be in dnd
+ Condition condition = mService.evaluateSubscriptionLocked(
+ conditionId, cal, now.getTimeInMillis(), now.getTimeInMillis() + 1000);
+ assertEquals(Condition.STATE_TRUE, condition.state);
+
+ // at alarm fire time, should exit dnd
+ assertTrue(cal.isInSchedule(now.getTimeInMillis() + 1000));
+ assertTrue("" + info.nextAlarm + " " + now.getTimeInMillis(),
+ cal.shouldExitForAlarm(now.getTimeInMillis() + 1000));
+ condition = mService.evaluateSubscriptionLocked(
+ conditionId, cal, now.getTimeInMillis() + 1000, 0);
+ assertEquals(Condition.STATE_FALSE, condition.state);
+ }
+
+ @Test
+ public void testEvaluateSubscription_alarmSetInSchedule() {
+ Calendar now = getNow();
+
+ // Schedule - 1 hour long; starts now, ends with alarm
+ ZenModeConfig.ScheduleInfo info = getScheduleEndsInHour(now);
+ Uri conditionId = ZenModeConfig.toScheduleConditionId(info);
+ ScheduleCalendar cal = new ScheduleCalendar();
+ cal.setSchedule(info);
+
+ // at start, should be in dnd
+ Condition condition = mService.evaluateSubscriptionLocked(
+ conditionId, cal, now.getTimeInMillis(), 0);
+ assertEquals(Condition.STATE_TRUE, condition.state);
+
+ // in schedule, update with alarm time, should be in dnd
+ condition = mService.evaluateSubscriptionLocked(
+ conditionId, cal, now.getTimeInMillis() + 500, now.getTimeInMillis() + 1000);
+ assertEquals(Condition.STATE_TRUE, condition.state);
+
+ // at alarm fire time, should exit dnd
+ assertTrue(cal.isInSchedule(now.getTimeInMillis() + 1000));
+ assertTrue("" + info.nextAlarm + " " + now.getTimeInMillis(),
+ cal.shouldExitForAlarm(now.getTimeInMillis() + 1000));
+ condition = mService.evaluateSubscriptionLocked(
+ conditionId, cal, now.getTimeInMillis() + 1000, 0);
+ assertEquals(Condition.STATE_FALSE, condition.state);
+ }
+
+ @Test
+ public void testEvaluateSubscription_earlierAlarmSet() {
+ Calendar now = getNow();
+
+ // Schedule - 1 hour long; starts now, ends with alarm
+ ZenModeConfig.ScheduleInfo info = getScheduleEndsInHour(now);
+ Uri conditionId = ZenModeConfig.toScheduleConditionId(info);
+ ScheduleCalendar cal = new ScheduleCalendar();
+ cal.setSchedule(info);
+
+ // at start, should be in dnd, alarm in 2000 ms
+ Condition condition = mService.evaluateSubscriptionLocked(
+ conditionId, cal, now.getTimeInMillis(), now.getTimeInMillis() + 2000);
+ assertEquals(Condition.STATE_TRUE, condition.state);
+
+ // in schedule, update with earlier alarm time, should be in dnd
+ condition = mService.evaluateSubscriptionLocked(
+ conditionId, cal, now.getTimeInMillis() + 500, now.getTimeInMillis() + 1000);
+ assertEquals(Condition.STATE_TRUE, condition.state);
+
+ // at earliest alarm fire time, should exit dnd
+ assertTrue(cal.isInSchedule(now.getTimeInMillis() + 1000));
+ assertTrue("" + info.nextAlarm + " " + now.getTimeInMillis(),
+ cal.shouldExitForAlarm(now.getTimeInMillis() + 1000));
+ condition = mService.evaluateSubscriptionLocked(
+ conditionId, cal, now.getTimeInMillis() + 1000, 0);
+ assertEquals(Condition.STATE_FALSE, condition.state);
+ }
+
+ @Test
+ public void testEvaluateSubscription_laterAlarmSet() {
+ Calendar now = getNow();
+
+ // Schedule - 1 hour long; starts now, ends with alarm
+ ZenModeConfig.ScheduleInfo info = getScheduleEndsInHour(now);
+ Uri conditionId = ZenModeConfig.toScheduleConditionId(info);
+ ScheduleCalendar cal = new ScheduleCalendar();
+ cal.setSchedule(info);
+
+ // at start, should be in dnd, alarm in 500 ms
+ Condition condition = mService.evaluateSubscriptionLocked(
+ conditionId, cal, now.getTimeInMillis(), now.getTimeInMillis() + 500);
+ assertEquals(Condition.STATE_TRUE, condition.state);
+
+ // in schedule, update with later alarm time, should be in dnd
+ condition = mService.evaluateSubscriptionLocked(
+ conditionId, cal, now.getTimeInMillis() + 250, now.getTimeInMillis() + 1000);
+ assertEquals(Condition.STATE_TRUE, condition.state);
+
+ // at earliest alarm fire time, should exit dnd
+ assertTrue(cal.isInSchedule(now.getTimeInMillis() + 500));
+ assertTrue("" + info.nextAlarm + " " + now.getTimeInMillis(),
+ cal.shouldExitForAlarm(now.getTimeInMillis() + 500));
+ condition = mService.evaluateSubscriptionLocked(
+ conditionId, cal, now.getTimeInMillis() + 500, 0);
+ assertEquals(Condition.STATE_FALSE, condition.state);
+ }
+
+ @Test
+ public void testEvaluateSubscription_alarmCanceled() {
+ Calendar now = getNow();
+
+ // Schedule - 1 hour long; starts now, ends with alarm
+ ZenModeConfig.ScheduleInfo info = getScheduleEndsInHour(now);
+ Uri conditionId = ZenModeConfig.toScheduleConditionId(info);
+ ScheduleCalendar cal = new ScheduleCalendar();
+ cal.setSchedule(info);
+
+ // at start, should be in dnd, alarm in 500 ms
+ Condition condition = mService.evaluateSubscriptionLocked(
+ conditionId, cal, now.getTimeInMillis(), now.getTimeInMillis() + 500);
+ assertEquals(Condition.STATE_TRUE, condition.state);
+
+ // in schedule, cancel alarm
+ condition = mService.evaluateSubscriptionLocked(
+ conditionId, cal, now.getTimeInMillis() + 250, 0);
+ assertEquals(Condition.STATE_TRUE, condition.state);
+
+ // at previous alarm time, should not exit DND
+ assertTrue(cal.isInSchedule(now.getTimeInMillis() + 500));
+ assertFalse(cal.shouldExitForAlarm(now.getTimeInMillis() + 500));
+ condition = mService.evaluateSubscriptionLocked(
+ conditionId, cal, now.getTimeInMillis() + 500, 0);
+ assertEquals(Condition.STATE_TRUE, condition.state);
+
+ // end of schedule, exit DND
+ now.add(Calendar.HOUR_OF_DAY, 1);
+ condition = mService.evaluateSubscriptionLocked(conditionId, cal, now.getTimeInMillis(), 0);
+ assertEquals(Condition.STATE_FALSE, condition.state);
+ }
+
+ private Calendar getNow() {
+ Calendar now = new GregorianCalendar();
+ now.set(Calendar.HOUR_OF_DAY, 14);
+ now.set(Calendar.MINUTE, 16);
+ now.set(Calendar.SECOND, 0);
+ now.set(Calendar.MILLISECOND, 0);
+ now.set(Calendar.DAY_OF_WEEK, Calendar.FRIDAY);
+ return now;
+ }
+
+ private ZenModeConfig.ScheduleInfo getScheduleEndsInHour(Calendar now) {
+ ZenModeConfig.ScheduleInfo info = new ZenModeConfig.ScheduleInfo();
+ info.days = new int[] {Calendar.FRIDAY};
+ info.startHour = now.get(Calendar.HOUR_OF_DAY);
+ info.startMinute = now.get(Calendar.MINUTE);
+ info.exitAtAlarm = true;
+ info.endHour = now.get(Calendar.HOUR_OF_DAY) + 1;
+ info.endMinute = now.get(Calendar.MINUTE);
+ return info;
+ }
+}
diff --git a/wifi/java/android/net/wifi/rtt/ResponderConfig.java b/wifi/java/android/net/wifi/rtt/ResponderConfig.java
index b503769..1090bfa 100644
--- a/wifi/java/android/net/wifi/rtt/ResponderConfig.java
+++ b/wifi/java/android/net/wifi/rtt/ResponderConfig.java
@@ -248,7 +248,10 @@
* Point (AP), which can be obtained from {@link android.net.wifi.WifiManager#getScanResults()}.
*/
public static ResponderConfig fromScanResult(ScanResult scanResult) {
- byte[] macAddress = new MacAddress(scanResult.BSSID).toByteArray();
+ byte[] macAddress = null;
+ if (scanResult.BSSID != null) {
+ macAddress = MacAddress.byteAddrFromStringAddr(scanResult.BSSID);
+ }
int responderType = RESPONDER_AP;
boolean supports80211mc = scanResult.is80211mcResponder();
int channelWidth = translcateScanResultChannelWidth(scanResult.channelWidth);