Merge "Pass in event time when notifying listeners of a state change"
diff --git a/apex/jobscheduler/framework/java/android/os/DeviceIdleManager.java b/apex/jobscheduler/framework/java/android/os/DeviceIdleManager.java
index 8019d4f..4c44334 100644
--- a/apex/jobscheduler/framework/java/android/os/DeviceIdleManager.java
+++ b/apex/jobscheduler/framework/java/android/os/DeviceIdleManager.java
@@ -17,13 +17,10 @@
package android.os;
import android.annotation.NonNull;
-import android.annotation.RequiresPermission;
import android.annotation.SystemService;
import android.annotation.TestApi;
import android.content.Context;
-import java.util.List;
-
/**
* Access to the service that keeps track of device idleness and drives low power mode based on
* that.
@@ -75,21 +72,6 @@
}
/**
- * Add the specified packages to the power save whitelist.
- *
- * @return the number of packages that were successfully added to the whitelist
- */
- @RequiresPermission(android.Manifest.permission.DEVICE_POWER)
- public int addPowerSaveWhitelistApps(@NonNull List<String> packageNames) {
- try {
- return mService.addPowerSaveWhitelistApps(packageNames);
- } catch (RemoteException e) {
- e.rethrowFromSystemServer();
- return 0;
- }
- }
-
- /**
* Return whether a given package is in the power-save whitelist or not.
* @hide
*/
diff --git a/apex/jobscheduler/framework/java/android/os/PowerWhitelistManager.java b/apex/jobscheduler/framework/java/android/os/PowerWhitelistManager.java
index 7a3ed92..4ffcf8a 100644
--- a/apex/jobscheduler/framework/java/android/os/PowerWhitelistManager.java
+++ b/apex/jobscheduler/framework/java/android/os/PowerWhitelistManager.java
@@ -26,6 +26,8 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.util.Collections;
+import java.util.List;
/**
* Interface to access and modify the power save whitelist.
@@ -78,6 +80,31 @@
}
/**
+ * Add the specified package to the power save whitelist.
+ *
+ * @return true if the package was successfully added to the whitelist
+ */
+ @RequiresPermission(android.Manifest.permission.DEVICE_POWER)
+ public boolean addToWhitelist(@NonNull String packageName) {
+ return addToWhitelist(Collections.singletonList(packageName)) == 1;
+ }
+
+ /**
+ * Add the specified packages to the power save whitelist.
+ *
+ * @return the number of packages that were successfully added to the whitelist
+ */
+ @RequiresPermission(android.Manifest.permission.DEVICE_POWER)
+ public int addToWhitelist(@NonNull List<String> packageNames) {
+ try {
+ return mService.addPowerSaveWhitelistApps(packageNames);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ return 0;
+ }
+ }
+
+ /**
* Add an app to the temporary whitelist for a short amount of time.
*
* @param packageName The package to add to the temp whitelist
diff --git a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
index bcd8be7..2f8b513 100644
--- a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
+++ b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
@@ -1171,7 +1171,8 @@
private void fetchCarrierPrivilegedAppsLocked() {
TelephonyManager telephonyManager =
mContext.getSystemService(TelephonyManager.class);
- mCarrierPrivilegedApps = telephonyManager.getPackagesWithCarrierPrivilegesForAllPhones();
+ mCarrierPrivilegedApps =
+ telephonyManager.getCarrierPrivilegedPackagesForAllActiveSubscriptions();
mHaveCarrierPrivilegedApps = true;
if (DEBUG) {
Slog.d(TAG, "apps with carrier privilege " + mCarrierPrivilegedApps);
diff --git a/apex/sdkext/Android.bp b/apex/sdkext/Android.bp
index b8dcb90..40f3c45 100644
--- a/apex/sdkext/Android.bp
+++ b/apex/sdkext/Android.bp
@@ -15,7 +15,13 @@
apex {
name: "com.android.sdkext",
manifest: "manifest.json",
+ binaries: [ "derive_sdk" ],
java_libs: [ "framework-sdkext" ],
+ prebuilts: [
+ "com.android.sdkext.ldconfig",
+ "cur_sdkinfo",
+ "derive_sdk.rc",
+ ],
key: "com.android.sdkext.key",
certificate: ":com.android.sdkext.certificate",
}
@@ -30,3 +36,35 @@
name: "com.android.sdkext.certificate",
certificate: "com.android.sdkext",
}
+
+prebuilt_etc {
+ name: "com.android.sdkext.ldconfig",
+ src: "ld.config.txt",
+ filename: "ld.config.txt",
+ installable: false,
+}
+
+python_binary_host {
+ name: "gen_sdkinfo",
+ srcs: [
+ "derive_sdk/sdk.proto",
+ "gen_sdkinfo.py",
+ ],
+ proto: {
+ canonical_path_from_root: false,
+ },
+}
+
+gensrcs {
+ name: "cur_sdkinfo_src",
+ srcs: [""],
+ tools: [ "gen_sdkinfo" ],
+ cmd: "$(location) -v 0 -o $(out)",
+}
+
+prebuilt_etc {
+ name: "cur_sdkinfo",
+ src: ":cur_sdkinfo_src",
+ filename: "sdkinfo.binarypb",
+ installable: false,
+}
diff --git a/apex/sdkext/derive_sdk/Android.bp b/apex/sdkext/derive_sdk/Android.bp
new file mode 100644
index 0000000..c4e3c29
--- /dev/null
+++ b/apex/sdkext/derive_sdk/Android.bp
@@ -0,0 +1,37 @@
+// Copyright (C) 2019 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.
+
+cc_binary {
+ name: "derive_sdk",
+ srcs: [
+ "derive_sdk.cpp",
+ "sdk.proto",
+ ],
+ proto: {
+ type: "lite",
+ },
+ sdk_version: "current",
+ stl: "c++_static",
+ shared_libs: [ "liblog" ],
+ static_libs: [
+ "libbase_ndk",
+ "libprotobuf-cpp-lite-ndk",
+ ],
+}
+
+prebuilt_etc {
+ name: "derive_sdk.rc",
+ src: "derive_sdk.rc",
+ installable: false,
+}
diff --git a/apex/sdkext/derive_sdk/derive_sdk.cpp b/apex/sdkext/derive_sdk/derive_sdk.cpp
new file mode 100644
index 0000000..0aacebe
--- /dev/null
+++ b/apex/sdkext/derive_sdk/derive_sdk.cpp
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+
+#define LOG_TAG "derive_sdk"
+
+#include <algorithm>
+#include <dirent.h>
+#include <iostream>
+#include <sys/stat.h>
+#include <vector>
+
+#include <android-base/file.h>
+#include <android-base/logging.h>
+#include <android-base/properties.h>
+
+#include "frameworks/base/apex/sdkext/derive_sdk/sdk.pb.h"
+
+using com::android::sdkext::proto::SdkVersion;
+
+int main(int, char**) {
+ std::unique_ptr<DIR, decltype(&closedir)> apex(opendir("/apex"), closedir);
+ if (!apex) {
+ LOG(ERROR) << "Could not read /apex";
+ return EXIT_FAILURE;
+ }
+ struct dirent* de;
+ std::vector<std::string> paths;
+ while ((de = readdir(apex.get()))) {
+ std::string name = de->d_name;
+ if (name[0] == '.' || name.find('@') != std::string::npos) {
+ // Skip <name>@<ver> dirs, as they are bind-mounted to <name>
+ continue;
+ }
+ std::string path = "/apex/" + name + "/etc/sdkinfo.binarypb";
+ struct stat statbuf;
+ if (stat(path.c_str(), &statbuf) == 0) {
+ paths.push_back(path);
+ }
+ }
+
+ std::vector<int> versions;
+ for (const auto& path : paths) {
+ std::string contents;
+ if (!android::base::ReadFileToString(path, &contents, true)) {
+ LOG(ERROR) << "failed to read " << path;
+ continue;
+ }
+ SdkVersion sdk_version;
+ if (!sdk_version.ParseFromString(contents)) {
+ LOG(ERROR) << "failed to parse " << path;
+ continue;
+ }
+ versions.push_back(sdk_version.version());
+ }
+ auto itr = std::min_element(versions.begin(), versions.end());
+ std::string prop_value = itr == versions.end() ? "0" : std::to_string(*itr);
+
+ if (!android::base::SetProperty("persist.com.android.sdkext.sdk_info", prop_value)) {
+ LOG(ERROR) << "failed to set sdk_info prop";
+ return EXIT_FAILURE;
+ }
+
+ return EXIT_SUCCESS;
+}
diff --git a/apex/sdkext/derive_sdk/derive_sdk.rc b/apex/sdkext/derive_sdk/derive_sdk.rc
new file mode 100644
index 0000000..1b66794
--- /dev/null
+++ b/apex/sdkext/derive_sdk/derive_sdk.rc
@@ -0,0 +1,3 @@
+service derive_sdk /apex/com.android.sdkext/bin/derive_sdk
+ oneshot
+ disabled
diff --git a/apex/sdkext/derive_sdk/sdk.proto b/apex/sdkext/derive_sdk/sdk.proto
new file mode 100644
index 0000000..d15b935
--- /dev/null
+++ b/apex/sdkext/derive_sdk/sdk.proto
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+
+syntax = "proto3";
+package com.android.sdkext.proto;
+
+option java_outer_classname = "SdkProto";
+option optimize_for = LITE_RUNTIME;
+
+message SdkVersion {
+ int32 version = 1;
+}
diff --git a/apex/sdkext/gen_sdkinfo.py b/apex/sdkext/gen_sdkinfo.py
new file mode 100644
index 0000000..5af478b
--- /dev/null
+++ b/apex/sdkext/gen_sdkinfo.py
@@ -0,0 +1,19 @@
+import sdk_pb2
+import sys
+
+if __name__ == '__main__':
+ argv = sys.argv[1:]
+ if not len(argv) == 4 or sorted([argv[0], argv[2]]) != ['-o', '-v']:
+ print('usage: gen_sdkinfo -v <version> -o <output-file>')
+ sys.exit(1)
+
+ for i in range(len(argv)):
+ if sys.argv[i] == '-o':
+ filename = sys.argv[i+1]
+ if sys.argv[i] == '-v':
+ version = int(sys.argv[i+1])
+
+ proto = sdk_pb2.SdkVersion()
+ proto.version = version
+ with open(filename, 'wb') as f:
+ f.write(proto.SerializeToString())
diff --git a/apex/sdkext/ld.config.txt b/apex/sdkext/ld.config.txt
new file mode 100644
index 0000000..b447068
--- /dev/null
+++ b/apex/sdkext/ld.config.txt
@@ -0,0 +1,31 @@
+# Copyright (C) 2019 The Android Open Source Project
+#
+# Bionic loader config file for the sdkext apex.
+
+dir.sdkext = /apex/com.android.sdkext/bin/
+
+[sdkext]
+additional.namespaces = platform
+
+namespace.default.isolated = true
+namespace.default.links = platform
+namespace.default.link.platform.allow_all_shared_libs = true
+
+###############################################################################
+# "platform" namespace: used for NDK libraries
+###############################################################################
+namespace.platform.isolated = true
+namespace.platform.search.paths = /system/${LIB}
+namespace.platform.asan.search.paths = /data/asan/system/${LIB}
+
+# /system/lib/libc.so, etc are symlinks to /apex/com.android.lib/lib/bionic/libc.so, etc.
+# Add /apex/... path to the permitted paths because linker uses realpath(3)
+# to check the accessibility of the lib. We could add this to search.paths
+# instead but that makes the resolution of bionic libs be dependent on
+# the order of /system/lib and /apex/... in search.paths. If /apex/...
+# is after /system/lib, then /apex/... is never tried because libc.so
+# is always found in /system/lib but fails to pass the accessibility test
+# because of its realpath. It's better to not depend on the ordering if
+# possible.
+namespace.platform.permitted.paths = /apex/com.android.runtime/${LIB}/bionic
+namespace.platform.asan.permitted.paths = /apex/com.android.runtime/${LIB}/bionic
diff --git a/api/current.txt b/api/current.txt
index 8533ab1..eb506e8 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -29323,12 +29323,14 @@
method public static long getMobileRxPackets();
method public static long getMobileTxBytes();
method public static long getMobileTxPackets();
+ method public static long getRxPackets(@NonNull String);
method public static int getThreadStatsTag();
method public static int getThreadStatsUid();
method public static long getTotalRxBytes();
method public static long getTotalRxPackets();
method public static long getTotalTxBytes();
method public static long getTotalTxPackets();
+ method public static long getTxPackets(@NonNull String);
method public static long getUidRxBytes(int);
method public static long getUidRxPackets(int);
method @Deprecated public static long getUidTcpRxBytes(int);
@@ -43137,6 +43139,7 @@
field public static final int MS_INVALIDATE;
field public static final int MS_SYNC;
field public static final int NETLINK_INET_DIAG;
+ field public static final int NETLINK_NETFILTER;
field public static final int NETLINK_ROUTE;
field public static final int NI_DGRAM;
field public static final int NI_NAMEREQD;
diff --git a/api/system-current.txt b/api/system-current.txt
index fe9c0d1..c7bde93 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -6379,6 +6379,8 @@
}
public class PowerWhitelistManager {
+ method @RequiresPermission(android.Manifest.permission.DEVICE_POWER) public boolean addToWhitelist(@NonNull String);
+ method @RequiresPermission(android.Manifest.permission.DEVICE_POWER) public int addToWhitelist(@NonNull java.util.List<java.lang.String>);
method @RequiresPermission(android.Manifest.permission.CHANGE_DEVICE_IDLE_TEMP_WHITELIST) public void whitelistAppTemporarily(@NonNull String, long);
method @RequiresPermission(android.Manifest.permission.CHANGE_DEVICE_IDLE_TEMP_WHITELIST) public long whitelistAppTemporarilyForEvent(@NonNull String, int, @NonNull String);
field public static final int EVENT_MMS = 2; // 0x2
@@ -9425,6 +9427,7 @@
method @Nullable @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public android.telephony.ImsiEncryptionInfo getCarrierInfoForImsiEncryption(int);
method public java.util.List<java.lang.String> getCarrierPackageNamesForIntent(android.content.Intent);
method public java.util.List<java.lang.String> getCarrierPackageNamesForIntentAndPhone(android.content.Intent, int);
+ method @NonNull @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public java.util.List<java.lang.String> getCarrierPrivilegedPackagesForAllActiveSubscriptions();
method @Nullable @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public android.telephony.CarrierRestrictionRules getCarrierRestrictionRules();
method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public String getCdmaMdn();
method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public String getCdmaMdn(int);
diff --git a/api/test-current.txt b/api/test-current.txt
index 3ddbbf8..3ce8c5e 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -1796,7 +1796,6 @@
}
public class DeviceIdleManager {
- method @RequiresPermission("android.permission.DEVICE_POWER") public int addPowerSaveWhitelistApps(@NonNull java.util.List<java.lang.String>);
method @NonNull public String[] getSystemPowerWhitelist();
method @NonNull public String[] getSystemPowerWhitelistExceptIdle();
}
@@ -2045,6 +2044,8 @@
}
public class PowerWhitelistManager {
+ method @RequiresPermission("android.permission.DEVICE_POWER") public boolean addToWhitelist(@NonNull String);
+ method @RequiresPermission("android.permission.DEVICE_POWER") public int addToWhitelist(@NonNull java.util.List<java.lang.String>);
method @RequiresPermission("android.permission.CHANGE_DEVICE_IDLE_TEMP_WHITELIST") public void whitelistAppTemporarily(@NonNull String, long);
method @RequiresPermission("android.permission.CHANGE_DEVICE_IDLE_TEMP_WHITELIST") public long whitelistAppTemporarilyForEvent(@NonNull String, int, @NonNull String);
field public static final int EVENT_MMS = 2; // 0x2
diff --git a/cmds/statsd/src/StatsService.cpp b/cmds/statsd/src/StatsService.cpp
index e8c0e15..d10a661 100644
--- a/cmds/statsd/src/StatsService.cpp
+++ b/cmds/statsd/src/StatsService.cpp
@@ -1295,7 +1295,18 @@
const sp<android::os::IPullAtomCallback>& pullerCallback) {
ENFORCE_UID(AID_SYSTEM);
- VLOG("StatsService::registerPuller called.");
+ VLOG("StatsService::registerPullAtomCallback called.");
+ mPullerManager->RegisterPullAtomCallback(uid, atomTag, coolDownNs, timeoutNs, additiveFields,
+ pullerCallback);
+ return Status::ok();
+}
+
+Status StatsService::registerNativePullAtomCallback(int32_t atomTag, int64_t coolDownNs,
+ int64_t timeoutNs, const std::vector<int32_t>& additiveFields,
+ const sp<android::os::IPullAtomCallback>& pullerCallback) {
+
+ VLOG("StatsService::registerNativePullAtomCallback called.");
+ int32_t uid = IPCThreadState::self()->getCallingUid();
mPullerManager->RegisterPullAtomCallback(uid, atomTag, coolDownNs, timeoutNs, additiveFields,
pullerCallback);
return Status::ok();
diff --git a/cmds/statsd/src/StatsService.h b/cmds/statsd/src/StatsService.h
index 6d40007..8c98e7b 100644
--- a/cmds/statsd/src/StatsService.h
+++ b/cmds/statsd/src/StatsService.h
@@ -187,6 +187,13 @@
const sp<android::os::IPullAtomCallback>& pullerCallback) override;
/**
+ * Binder call to register a callback function for a pulled atom.
+ */
+ virtual Status registerNativePullAtomCallback(int32_t atomTag, int64_t coolDownNs,
+ int64_t timeoutNs, const std::vector<int32_t>& additiveFields,
+ const sp<android::os::IPullAtomCallback>& pullerCallback) override;
+
+ /**
* Binder call to unregister any existing callback function for a vendor pulled atom.
*/
virtual Status unregisterPullerCallback(int32_t atomTag, const String16& packageName) override;
diff --git a/cmds/statsd/src/condition/CombinationConditionTracker.cpp b/cmds/statsd/src/condition/CombinationConditionTracker.cpp
index 52a1269..69aae3d 100644
--- a/cmds/statsd/src/condition/CombinationConditionTracker.cpp
+++ b/cmds/statsd/src/condition/CombinationConditionTracker.cpp
@@ -86,7 +86,7 @@
ALOGW("Child initialization failed %lld ", (long long)child);
return false;
} else {
- ALOGW("Child initialization success %lld ", (long long)child);
+ VLOG("Child initialization success %lld ", (long long)child);
}
if (allConditionTrackers[childIndex]->isSliced()) {
diff --git a/core/java/android/app/timedetector/ManualTimeSuggestion.java b/core/java/android/app/timedetector/ManualTimeSuggestion.java
index e7d619a..471606da 100644
--- a/core/java/android/app/timedetector/ManualTimeSuggestion.java
+++ b/core/java/android/app/timedetector/ManualTimeSuggestion.java
@@ -85,7 +85,8 @@
@NonNull
public List<String> getDebugInfo() {
- return Collections.unmodifiableList(mDebugInfo);
+ return mDebugInfo == null
+ ? Collections.emptyList() : Collections.unmodifiableList(mDebugInfo);
}
/**
diff --git a/core/java/android/app/timedetector/PhoneTimeSuggestion.java b/core/java/android/app/timedetector/PhoneTimeSuggestion.java
index 233dbbc..dd02af7 100644
--- a/core/java/android/app/timedetector/PhoneTimeSuggestion.java
+++ b/core/java/android/app/timedetector/PhoneTimeSuggestion.java
@@ -23,7 +23,6 @@
import android.util.TimestampedValue;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
@@ -52,20 +51,25 @@
};
private final int mPhoneId;
- @Nullable private TimestampedValue<Long> mUtcTime;
+ @Nullable private final TimestampedValue<Long> mUtcTime;
@Nullable private ArrayList<String> mDebugInfo;
- public PhoneTimeSuggestion(int phoneId) {
- mPhoneId = phoneId;
+ private PhoneTimeSuggestion(Builder builder) {
+ mPhoneId = builder.mPhoneId;
+ mUtcTime = builder.mUtcTime;
+ mDebugInfo = builder.mDebugInfo != null ? new ArrayList<>(builder.mDebugInfo) : null;
}
private static PhoneTimeSuggestion createFromParcel(Parcel in) {
int phoneId = in.readInt();
- PhoneTimeSuggestion suggestion = new PhoneTimeSuggestion(phoneId);
- suggestion.setUtcTime(in.readParcelable(null /* classLoader */));
+ PhoneTimeSuggestion suggestion = new PhoneTimeSuggestion.Builder(phoneId)
+ .setUtcTime(in.readParcelable(null /* classLoader */))
+ .build();
@SuppressWarnings("unchecked")
ArrayList<String> debugInfo = (ArrayList<String>) in.readArrayList(null /* classLoader */);
- suggestion.mDebugInfo = debugInfo;
+ if (debugInfo != null) {
+ suggestion.addDebugInfo(debugInfo);
+ }
return suggestion;
}
@@ -85,10 +89,6 @@
return mPhoneId;
}
- public void setUtcTime(@Nullable TimestampedValue<Long> utcTime) {
- mUtcTime = utcTime;
- }
-
@Nullable
public TimestampedValue<Long> getUtcTime() {
return mUtcTime;
@@ -96,7 +96,8 @@
@NonNull
public List<String> getDebugInfo() {
- return Collections.unmodifiableList(mDebugInfo);
+ return mDebugInfo == null
+ ? Collections.emptyList() : Collections.unmodifiableList(mDebugInfo);
}
/**
@@ -104,11 +105,23 @@
* information is present in {@link #toString()} but is not considered for
* {@link #equals(Object)} and {@link #hashCode()}.
*/
- public void addDebugInfo(String... debugInfos) {
+ public void addDebugInfo(String debugInfo) {
if (mDebugInfo == null) {
mDebugInfo = new ArrayList<>();
}
- mDebugInfo.addAll(Arrays.asList(debugInfos));
+ mDebugInfo.add(debugInfo);
+ }
+
+ /**
+ * Associates information with the instance that can be useful for debugging / logging. The
+ * information is present in {@link #toString()} but is not considered for
+ * {@link #equals(Object)} and {@link #hashCode()}.
+ */
+ public void addDebugInfo(@NonNull List<String> debugInfo) {
+ if (mDebugInfo == null) {
+ mDebugInfo = new ArrayList<>(debugInfo.size());
+ }
+ mDebugInfo.addAll(debugInfo);
}
@Override
@@ -137,4 +150,39 @@
+ ", mDebugInfo=" + mDebugInfo
+ '}';
}
+
+ /**
+ * Builds {@link PhoneTimeSuggestion} instances.
+ *
+ * @hide
+ */
+ public static class Builder {
+ private final int mPhoneId;
+ private TimestampedValue<Long> mUtcTime;
+ private List<String> mDebugInfo;
+
+ public Builder(int phoneId) {
+ mPhoneId = phoneId;
+ }
+
+ /** Returns the builder for call chaining. */
+ public Builder setUtcTime(TimestampedValue<Long> utcTime) {
+ mUtcTime = utcTime;
+ return this;
+ }
+
+ /** Returns the builder for call chaining. */
+ public Builder addDebugInfo(@NonNull String debugInfo) {
+ if (mDebugInfo == null) {
+ mDebugInfo = new ArrayList<>();
+ }
+ mDebugInfo.add(debugInfo);
+ return this;
+ }
+
+ /** Returns the {@link PhoneTimeSuggestion}. */
+ public PhoneTimeSuggestion build() {
+ return new PhoneTimeSuggestion(this);
+ }
+ }
}
diff --git a/core/java/android/content/res/loader/ResourceLoaderManager.java b/core/java/android/content/res/loader/ResourceLoaderManager.java
index ddbfa81..592ec09 100644
--- a/core/java/android/content/res/loader/ResourceLoaderManager.java
+++ b/core/java/android/content/res/loader/ResourceLoaderManager.java
@@ -151,6 +151,7 @@
public void onImplUpdate(ResourcesImpl resourcesImpl) {
synchronized (mLock) {
this.mResourcesImpl = resourcesImpl;
+ this.mResourcesImpl.getAssets().setResourceLoaderManager(this);
updateLoaders();
}
}
diff --git a/core/java/android/net/TrafficStats.java b/core/java/android/net/TrafficStats.java
index 1c6a484..bf4884a 100644
--- a/core/java/android/net/TrafficStats.java
+++ b/core/java/android/net/TrafficStats.java
@@ -16,6 +16,7 @@
package android.net;
+import android.annotation.NonNull;
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.annotation.TestApi;
@@ -595,8 +596,15 @@
return total;
}
- /** {@hide} */
- public static long getTxPackets(String iface) {
+ /**
+ * Return the number of packets transmitted on the specified interface since
+ * device boot. Statistics are measured at the network layer, so both TCP and
+ * UDP usage are included.
+ *
+ * @param iface The name of the interface.
+ * @return The number of transmitted packets.
+ */
+ public static long getTxPackets(@NonNull String iface) {
try {
return getStatsService().getIfaceStats(iface, TYPE_TX_PACKETS);
} catch (RemoteException e) {
@@ -604,8 +612,15 @@
}
}
- /** {@hide} */
- public static long getRxPackets(String iface) {
+ /**
+ * Return the number of packets received on the specified interface since
+ * device boot. Statistics are measured at the network layer, so both TCP
+ * and UDP usage are included.
+ *
+ * @param iface The name of the interface.
+ * @return The number of received packets.
+ */
+ public static long getRxPackets(@NonNull String iface) {
try {
return getStatsService().getIfaceStats(iface, TYPE_RX_PACKETS);
} catch (RemoteException e) {
diff --git a/core/java/android/os/IStatsManager.aidl b/core/java/android/os/IStatsManager.aidl
index 29871b6..5ebb9f2 100644
--- a/core/java/android/os/IStatsManager.aidl
+++ b/core/java/android/os/IStatsManager.aidl
@@ -202,6 +202,13 @@
in int[] additiveFields, IPullAtomCallback pullerCallback);
/**
+ * Registers a puller callback function that, when invoked, pulls the data
+ * for the specified atom tag.
+ */
+ oneway void registerNativePullAtomCallback(int atomTag, long coolDownNs, long timeoutNs,
+ in int[] additiveFields, IPullAtomCallback pullerCallback);
+
+ /**
* Unregisters a puller callback function for the given vendor atom.
*
* Requires Manifest.permission.DUMP and Manifest.permission.PACKAGE_USAGE_STATS
diff --git a/core/java/android/service/autofill/augmented/FillWindow.java b/core/java/android/service/autofill/augmented/FillWindow.java
index 6a29d48..5d00370 100644
--- a/core/java/android/service/autofill/augmented/FillWindow.java
+++ b/core/java/android/service/autofill/augmented/FillWindow.java
@@ -242,6 +242,7 @@
synchronized (mLock) {
if (mDestroyed) return;
if (mUpdateCalled) {
+ mFillView.setOnClickListener(null);
hide();
mProxy.report(AutofillProxy.REPORT_EVENT_UI_DESTROYED);
}
diff --git a/core/java/android/view/autofill/AutofillManager.java b/core/java/android/view/autofill/AutofillManager.java
index 54446e1..9c04b39 100644
--- a/core/java/android/view/autofill/AutofillManager.java
+++ b/core/java/android/view/autofill/AutofillManager.java
@@ -47,6 +47,7 @@
import android.service.autofill.AutofillService;
import android.service.autofill.FillEventHistory;
import android.service.autofill.UserData;
+import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.DebugUtils;
@@ -61,6 +62,7 @@
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityNodeProvider;
import android.view.accessibility.AccessibilityWindowInfo;
+import android.widget.EditText;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.logging.MetricsLogger;
@@ -1073,6 +1075,8 @@
} else if (sVerbose) {
Log.v(TAG, "Ignoring visibility change on " + id + ": no tracked views");
}
+ } else if (!virtual && isVisible) {
+ startAutofillIfNeededLocked(view);
}
}
}
@@ -1238,9 +1242,11 @@
return;
}
if (!mEnabled || !isActiveLocked()) {
- if (sVerbose) {
- Log.v(TAG, "notifyValueChanged(" + view.getAutofillId()
- + "): ignoring on state " + getStateAsStringLocked());
+ if (!startAutofillIfNeededLocked(view)) {
+ if (sVerbose) {
+ Log.v(TAG, "notifyValueChanged(" + view.getAutofillId()
+ + "): ignoring on state " + getStateAsStringLocked());
+ }
}
return;
}
@@ -1879,6 +1885,37 @@
}
}
+ @GuardedBy("mLock")
+ private boolean startAutofillIfNeededLocked(View view) {
+ if (mState == STATE_UNKNOWN
+ && mSessionId == NO_SESSION
+ && view instanceof EditText
+ && !TextUtils.isEmpty(((EditText) view).getText())
+ && !view.isFocused()
+ && view.isImportantForAutofill()
+ && view.isLaidOut()
+ && view.isVisibleToUser()) {
+
+ ensureServiceClientAddedIfNeededLocked();
+
+ if (sVerbose) {
+ Log.v(TAG, "startAutofillIfNeededLocked(): enabled=" + mEnabled);
+ }
+ if (mEnabled && !isClientDisablingEnterExitEvent()) {
+ final AutofillId id = view.getAutofillId();
+ final AutofillValue value = view.getAutofillValue();
+ // Starts new session.
+ startSessionLocked(id, /* bounds= */ null, /* value= */ null, /* flags= */ 0);
+ // Updates value.
+ updateSessionLocked(id, /* bounds= */ null, value, ACTION_VALUE_CHANGED,
+ /* flags= */ 0);
+ addEnteredIdLocked(id);
+ return true;
+ }
+ }
+ return false;
+ }
+
/**
* Registers a {@link AutofillCallback} to receive autofill events.
*
diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java
index 187ab46..eb0d9bf 100644
--- a/core/java/android/widget/Editor.java
+++ b/core/java/android/widget/Editor.java
@@ -152,6 +152,9 @@
// handles.
private static final boolean FLAG_USE_MAGNIFIER = true;
+ private static final int DELAY_BEFORE_HANDLE_FADES_OUT = 4000;
+ private static final int RECENT_CUT_COPY_DURATION_MS = 15 * 1000; // 15 seconds in millis
+
static final int BLINK = 500;
private static final int DRAG_SHADOW_MAX_TEXT_LENGTH = 20;
private static final float LINE_SLOP_MULTIPLIER_FOR_HANDLEVIEWS = 0.5f;
@@ -326,8 +329,6 @@
// Global listener that detects changes in the global position of the TextView
private PositionListener mPositionListener;
- private float mLastDownPositionX, mLastDownPositionY;
- private float mLastUpPositionX, mLastUpPositionY;
private float mContextMenuAnchorX, mContextMenuAnchorY;
Callback mCustomSelectionActionModeCallback;
Callback mCustomInsertionActionModeCallback;
@@ -336,18 +337,11 @@
@UnsupportedAppUsage
boolean mCreatedWithASelection;
- // Indicates the current tap state (first tap, double tap, or triple click).
- private int mTapState = TAP_STATE_INITIAL;
- private long mLastTouchUpTime = 0;
- private static final int TAP_STATE_INITIAL = 0;
- private static final int TAP_STATE_FIRST_TAP = 1;
- private static final int TAP_STATE_DOUBLE_TAP = 2;
- // Only for mouse input.
- private static final int TAP_STATE_TRIPLE_CLICK = 3;
-
// The button state as of the last time #onTouchEvent is called.
private int mLastButtonState;
+ private final EditorTouchState mTouchState = new EditorTouchState();
+
private Runnable mInsertionActionModeRunnable;
// The span controller helps monitoring the changes to which the Editor needs to react:
@@ -1193,10 +1187,10 @@
logCursor("performLongClick", "handled=%s", handled);
}
// Long press in empty space moves cursor and starts the insertion action mode.
- if (!handled && !isPositionOnText(mLastDownPositionX, mLastDownPositionY)
+ if (!handled && !isPositionOnText(mTouchState.getLastDownX(), mTouchState.getLastDownY())
&& mInsertionControllerEnabled) {
- final int offset = mTextView.getOffsetForPosition(mLastDownPositionX,
- mLastDownPositionY);
+ final int offset = mTextView.getOffsetForPosition(mTouchState.getLastDownX(),
+ mTouchState.getLastDownY());
Selection.setSelection((Spannable) mTextView.getText(), offset);
getInsertionController().show();
mIsInsertionActionModeStartPending = true;
@@ -1240,11 +1234,11 @@
}
float getLastUpPositionX() {
- return mLastUpPositionX;
+ return mTouchState.getLastUpX();
}
float getLastUpPositionY() {
- return mLastUpPositionY;
+ return mTouchState.getLastUpY();
}
private long getLastTouchOffsets() {
@@ -1279,6 +1273,9 @@
// Has to be done before onTakeFocus, which can be overloaded.
final int lastTapPosition = getLastTapPosition();
if (lastTapPosition >= 0) {
+ if (TextView.DEBUG_CURSOR) {
+ logCursor("onFocusChanged", "setting cursor position: %d", lastTapPosition);
+ }
Selection.setSelection((Spannable) mTextView.getText(), lastTapPosition);
}
@@ -1443,39 +1440,6 @@
}
}
- private void updateTapState(MotionEvent event) {
- final int action = event.getActionMasked();
- if (action == MotionEvent.ACTION_DOWN) {
- final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);
- // Detect double tap and triple click.
- if (((mTapState == TAP_STATE_FIRST_TAP)
- || ((mTapState == TAP_STATE_DOUBLE_TAP) && isMouse))
- && (SystemClock.uptimeMillis() - mLastTouchUpTime)
- <= ViewConfiguration.getDoubleTapTimeout()) {
- if (mTapState == TAP_STATE_FIRST_TAP) {
- mTapState = TAP_STATE_DOUBLE_TAP;
- } else {
- mTapState = TAP_STATE_TRIPLE_CLICK;
- }
- if (TextView.DEBUG_CURSOR) {
- logCursor("updateTapState", "ACTION_DOWN: %s tap detected",
- (mTapState == TAP_STATE_DOUBLE_TAP ? "double" : "triple"));
- }
- } else {
- mTapState = TAP_STATE_FIRST_TAP;
- if (TextView.DEBUG_CURSOR) {
- logCursor("updateTapState", "ACTION_DOWN: first tap detected");
- }
- }
- }
- if (action == MotionEvent.ACTION_UP) {
- mLastTouchUpTime = SystemClock.uptimeMillis();
- if (TextView.DEBUG_CURSOR) {
- logCursor("updateTapState", "ACTION_UP");
- }
- }
- }
-
private boolean shouldFilterOutTouchEvent(MotionEvent event) {
if (!event.isFromSource(InputDevice.SOURCE_MOUSE)) {
return false;
@@ -1503,7 +1467,8 @@
}
return;
}
- updateTapState(event);
+ ViewConfiguration viewConfiguration = ViewConfiguration.get(mTextView.getContext());
+ mTouchState.update(event, viewConfiguration);
updateFloatingToolbarVisibility(event);
if (hasSelectionController()) {
@@ -1515,15 +1480,7 @@
mShowSuggestionRunnable = null;
}
- if (event.getActionMasked() == MotionEvent.ACTION_UP) {
- mLastUpPositionX = event.getX();
- mLastUpPositionY = event.getY();
- }
-
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
- mLastDownPositionX = event.getX();
- mLastDownPositionY = event.getY();
-
// Reset this state; it will be re-set if super.onTouchEvent
// causes focus to move to the view.
mTouchFocusSelected = false;
@@ -5067,7 +5024,10 @@
public boolean onTouchEvent(MotionEvent ev) {
if (TextView.DEBUG_CURSOR) {
logCursor(this.getClass().getSimpleName() + ": HandleView: onTouchEvent",
- MotionEvent.actionToString(ev.getActionMasked()));
+ "%d: %s (%f,%f)",
+ ev.getSequenceNumber(),
+ MotionEvent.actionToString(ev.getActionMasked()),
+ ev.getX(), ev.getY());
}
updateFloatingToolbarVisibility(ev);
@@ -5145,56 +5105,14 @@
}
private class InsertionHandleView extends HandleView {
- private static final int DELAY_BEFORE_HANDLE_FADES_OUT = 4000;
- private static final int RECENT_CUT_COPY_DURATION = 15 * 1000; // seconds
-
// Used to detect taps on the insertion handle, which will affect the insertion action mode
- private float mDownPositionX, mDownPositionY;
+ private float mLastDownRawX, mLastDownRawY;
private Runnable mHider;
public InsertionHandleView(Drawable drawable) {
super(drawable, drawable, com.android.internal.R.id.insertion_handle);
}
- @Override
- public void show() {
- super.show();
-
- final long durationSinceCutOrCopy =
- SystemClock.uptimeMillis() - TextView.sLastCutCopyOrTextChangedTime;
-
- // Cancel the single tap delayed runnable.
- if (mInsertionActionModeRunnable != null
- && ((mTapState == TAP_STATE_DOUBLE_TAP)
- || (mTapState == TAP_STATE_TRIPLE_CLICK)
- || isCursorInsideEasyCorrectionSpan())) {
- mTextView.removeCallbacks(mInsertionActionModeRunnable);
- }
-
- // Prepare and schedule the single tap runnable to run exactly after the double tap
- // timeout has passed.
- if ((mTapState != TAP_STATE_DOUBLE_TAP) && (mTapState != TAP_STATE_TRIPLE_CLICK)
- && !isCursorInsideEasyCorrectionSpan()
- && (durationSinceCutOrCopy < RECENT_CUT_COPY_DURATION)) {
- if (mTextActionMode == null) {
- if (mInsertionActionModeRunnable == null) {
- mInsertionActionModeRunnable = new Runnable() {
- @Override
- public void run() {
- startInsertionActionMode();
- }
- };
- }
- mTextView.postDelayed(
- mInsertionActionModeRunnable,
- ViewConfiguration.getDoubleTapTimeout() + 1);
- }
-
- }
-
- hideAfterDelay();
- }
-
private void hideAfterDelay() {
if (mHider == null) {
mHider = new Runnable() {
@@ -5250,8 +5168,8 @@
switch (ev.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
- mDownPositionX = ev.getRawX();
- mDownPositionY = ev.getRawY();
+ mLastDownRawX = ev.getRawX();
+ mLastDownRawY = ev.getRawY();
updateMagnifier(ev);
break;
@@ -5261,8 +5179,8 @@
case MotionEvent.ACTION_UP:
if (!offsetHasBeenChanged()) {
- final float deltaX = mDownPositionX - ev.getRawX();
- final float deltaY = mDownPositionY - ev.getRawY();
+ final float deltaX = mLastDownRawX - ev.getRawX();
+ final float deltaY = mLastDownRawY - ev.getRawY();
final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
final ViewConfiguration viewConfiguration = ViewConfiguration.get(
@@ -5804,6 +5722,37 @@
public void show() {
getHandle().show();
+ final long durationSinceCutOrCopy =
+ SystemClock.uptimeMillis() - TextView.sLastCutCopyOrTextChangedTime;
+
+ // Cancel the single tap delayed runnable.
+ if (mInsertionActionModeRunnable != null
+ && (mTouchState.isMultiTap() || isCursorInsideEasyCorrectionSpan())) {
+ mTextView.removeCallbacks(mInsertionActionModeRunnable);
+ }
+
+ // Prepare and schedule the single tap runnable to run exactly after the double tap
+ // timeout has passed.
+ if (!mTouchState.isMultiTap()
+ && !isCursorInsideEasyCorrectionSpan()
+ && (durationSinceCutOrCopy < RECENT_CUT_COPY_DURATION_MS)) {
+ if (mTextActionMode == null) {
+ if (mInsertionActionModeRunnable == null) {
+ mInsertionActionModeRunnable = new Runnable() {
+ @Override
+ public void run() {
+ startInsertionActionMode();
+ }
+ };
+ }
+ mTextView.postDelayed(
+ mInsertionActionModeRunnable,
+ ViewConfiguration.getDoubleTapTimeout() + 1);
+ }
+ }
+
+ getHandle().hideAfterDelay();
+
if (mSelectionModifierCursorController != null) {
mSelectionModifierCursorController.hide();
}
@@ -5870,7 +5819,6 @@
// The offsets of that last touch down event. Remembered to start selection there.
private int mMinTouchOffset, mMaxTouchOffset;
- private float mDownPositionX, mDownPositionY;
private boolean mGestureStayedInTapRegion;
// Where the user first starts the drag motion.
@@ -5940,13 +5888,18 @@
}
public void enterDrag(int dragAcceleratorMode) {
+ if (TextView.DEBUG_CURSOR) {
+ logCursor("SelectionModifierCursorController: enterDrag",
+ "starting selection drag: mode=%s", dragAcceleratorMode);
+ }
+
// Just need to init the handles / hide insertion cursor.
show();
mDragAcceleratorMode = dragAcceleratorMode;
// Start location of selection.
- mStartOffset = mTextView.getOffsetForPosition(mLastDownPositionX,
- mLastDownPositionY);
- mLineSelectionIsOn = mTextView.getLineAtCoordinate(mLastDownPositionY);
+ mStartOffset = mTextView.getOffsetForPosition(mTouchState.getLastDownX(),
+ mTouchState.getLastDownY());
+ mLineSelectionIsOn = mTextView.getLineAtCoordinate(mTouchState.getLastDownY());
// Don't show the handles until user has lifted finger.
hide();
@@ -5974,36 +5927,20 @@
eventX, eventY);
// Double tap detection
- if (mGestureStayedInTapRegion) {
- if (mTapState == TAP_STATE_DOUBLE_TAP
- || mTapState == TAP_STATE_TRIPLE_CLICK) {
- final float deltaX = eventX - mDownPositionX;
- final float deltaY = eventY - mDownPositionY;
- final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
-
- ViewConfiguration viewConfiguration = ViewConfiguration.get(
- mTextView.getContext());
- int doubleTapSlop = viewConfiguration.getScaledDoubleTapSlop();
- boolean stayedInArea =
- distanceSquared < doubleTapSlop * doubleTapSlop;
-
- if (stayedInArea && (isMouse || isPositionOnText(eventX, eventY))) {
- if (TextView.DEBUG_CURSOR) {
- logCursor("SelectionModifierCursorController: onTouchEvent",
- "ACTION_DOWN: select and start drag");
- }
- if (mTapState == TAP_STATE_DOUBLE_TAP) {
- selectCurrentWordAndStartDrag();
- } else if (mTapState == TAP_STATE_TRIPLE_CLICK) {
- selectCurrentParagraphAndStartDrag();
- }
- mDiscardNextActionUp = true;
- }
+ if (mGestureStayedInTapRegion
+ && mTouchState.isMultiTapInSameArea()
+ && (isMouse || isPositionOnText(eventX, eventY))) {
+ if (TextView.DEBUG_CURSOR) {
+ logCursor("SelectionModifierCursorController: onTouchEvent",
+ "ACTION_DOWN: select and start drag");
}
+ if (mTouchState.isDoubleTap()) {
+ selectCurrentWordAndStartDrag();
+ } else if (mTouchState.isTripleClick()) {
+ selectCurrentParagraphAndStartDrag();
+ }
+ mDiscardNextActionUp = true;
}
-
- mDownPositionX = eventX;
- mDownPositionY = eventY;
mGestureStayedInTapRegion = true;
mHaventMovedEnoughToStartDrag = true;
}
@@ -6025,8 +5962,8 @@
final int touchSlop = viewConfig.getScaledTouchSlop();
if (mGestureStayedInTapRegion || mHaventMovedEnoughToStartDrag) {
- final float deltaX = eventX - mDownPositionX;
- final float deltaY = eventY - mDownPositionY;
+ final float deltaX = eventX - mTouchState.getLastDownX();
+ final float deltaY = eventY - mTouchState.getLastDownY();
final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
if (mGestureStayedInTapRegion) {
@@ -7164,7 +7101,7 @@
}
}
- private static void logCursor(String location, @Nullable String msgFormat, Object ... msgArgs) {
+ static void logCursor(String location, @Nullable String msgFormat, Object ... msgArgs) {
if (msgFormat == null) {
Log.d(TAG, location);
} else {
diff --git a/core/java/android/widget/EditorTouchState.java b/core/java/android/widget/EditorTouchState.java
new file mode 100644
index 0000000..f880939
--- /dev/null
+++ b/core/java/android/widget/EditorTouchState.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2019 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.widget;
+
+import static android.widget.Editor.logCursor;
+
+import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
+
+import android.annotation.IntDef;
+import android.view.InputDevice;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Helper class used by {@link Editor} to track state for touch events.
+ *
+ * @hide
+ */
+@VisibleForTesting(visibility = PACKAGE)
+public class EditorTouchState {
+ private float mLastDownX, mLastDownY;
+ private float mLastUpX, mLastUpY;
+ private long mLastUpMillis;
+
+ @IntDef({MultiTapStatus.NONE, MultiTapStatus.FIRST_TAP, MultiTapStatus.DOUBLE_TAP,
+ MultiTapStatus.TRIPLE_CLICK})
+ @Retention(RetentionPolicy.SOURCE)
+ @VisibleForTesting
+ public @interface MultiTapStatus {
+ int NONE = 0;
+ int FIRST_TAP = 1;
+ int DOUBLE_TAP = 2;
+ int TRIPLE_CLICK = 3; // Only for mouse input.
+ }
+ @MultiTapStatus
+ private int mMultiTapStatus = MultiTapStatus.NONE;
+ private boolean mMultiTapInSameArea;
+
+ public float getLastDownX() {
+ return mLastDownX;
+ }
+
+ public float getLastDownY() {
+ return mLastDownY;
+ }
+
+ public float getLastUpX() {
+ return mLastUpX;
+ }
+
+ public float getLastUpY() {
+ return mLastUpY;
+ }
+
+ public boolean isDoubleTap() {
+ return mMultiTapStatus == MultiTapStatus.DOUBLE_TAP;
+ }
+
+ public boolean isTripleClick() {
+ return mMultiTapStatus == MultiTapStatus.TRIPLE_CLICK;
+ }
+
+ public boolean isMultiTap() {
+ return mMultiTapStatus == MultiTapStatus.DOUBLE_TAP
+ || mMultiTapStatus == MultiTapStatus.TRIPLE_CLICK;
+ }
+
+ public boolean isMultiTapInSameArea() {
+ return isMultiTap() && mMultiTapInSameArea;
+ }
+
+ /**
+ * Updates the state based on the new event.
+ */
+ public void update(MotionEvent event, ViewConfiguration viewConfiguration) {
+ final int action = event.getActionMasked();
+ if (action == MotionEvent.ACTION_DOWN) {
+ final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);
+ final long millisSinceLastUp = event.getEventTime() - mLastUpMillis;
+ // Detect double tap and triple click.
+ if (millisSinceLastUp <= ViewConfiguration.getDoubleTapTimeout()
+ && (mMultiTapStatus == MultiTapStatus.FIRST_TAP
+ || (mMultiTapStatus == MultiTapStatus.DOUBLE_TAP && isMouse))) {
+ if (mMultiTapStatus == MultiTapStatus.FIRST_TAP) {
+ mMultiTapStatus = MultiTapStatus.DOUBLE_TAP;
+ } else {
+ mMultiTapStatus = MultiTapStatus.TRIPLE_CLICK;
+ }
+ final float deltaX = event.getX() - mLastDownX;
+ final float deltaY = event.getY() - mLastDownY;
+ final int distanceSquared = (int) ((deltaX * deltaX) + (deltaY * deltaY));
+ int doubleTapSlop = viewConfiguration.getScaledDoubleTapSlop();
+ mMultiTapInSameArea = distanceSquared < doubleTapSlop * doubleTapSlop;
+ if (TextView.DEBUG_CURSOR) {
+ String status = isDoubleTap() ? "double" : "triple";
+ String inSameArea = mMultiTapInSameArea ? "in same area" : "not in same area";
+ logCursor("EditorTouchState", "ACTION_DOWN: %s tap detected, %s",
+ status, inSameArea);
+ }
+ } else {
+ mMultiTapStatus = MultiTapStatus.FIRST_TAP;
+ mMultiTapInSameArea = false;
+ if (TextView.DEBUG_CURSOR) {
+ logCursor("EditorTouchState", "ACTION_DOWN: first tap detected");
+ }
+ }
+ mLastDownX = event.getX();
+ mLastDownY = event.getY();
+ } else if (action == MotionEvent.ACTION_UP) {
+ if (TextView.DEBUG_CURSOR) {
+ logCursor("EditorTouchState", "ACTION_UP");
+ }
+ mLastUpX = event.getX();
+ mLastUpY = event.getY();
+ mLastUpMillis = event.getEventTime();
+ }
+ }
+}
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 90e8ef2..ee169f2 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -10860,7 +10860,10 @@
@Override
public boolean onTouchEvent(MotionEvent event) {
if (DEBUG_CURSOR) {
- logCursor("onTouchEvent", MotionEvent.actionToString(event.getActionMasked()));
+ logCursor("onTouchEvent", "%d: %s (%f,%f)",
+ event.getSequenceNumber(),
+ MotionEvent.actionToString(event.getActionMasked()),
+ event.getX(), event.getY());
}
final int action = event.getActionMasked();
diff --git a/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/ResourceLoaderChangesTest.kt b/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/ResourceLoaderChangesTest.kt
index e01e254..0c3d34e 100644
--- a/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/ResourceLoaderChangesTest.kt
+++ b/core/tests/ResourceLoaderTests/src/android/content/res/loader/test/ResourceLoaderChangesTest.kt
@@ -31,7 +31,6 @@
import androidx.test.runner.lifecycle.Stage
import com.google.common.truth.Truth.assertThat
import org.junit.After
-import org.junit.Assume
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -43,7 +42,6 @@
import java.util.concurrent.FutureTask
import java.util.concurrent.TimeUnit
-// @Ignore("UiAutomation is crashing with not connected, not sure why")
@RunWith(Parameterized::class)
class ResourceLoaderChangesTest : ResourceLoaderTestBase() {
@@ -75,7 +73,7 @@
@Before
@After
fun disableOverlay() {
-// enableOverlay(OVERLAY_PACKAGE, false)
+ enableOverlay(OVERLAY_PACKAGE, false)
}
@Test
@@ -156,25 +154,32 @@
// All these tests assert for the exact same loaders/values, so extract that logic out
private fun verifySameBeforeAndAfter(block: () -> Resources) {
- // TODO(chiuwinson): atest doesn't work with @Ignore, UiAutomation not connected error
- Assume.assumeFalse(true)
+ fun Resources.resource() = this.getString(android.R.string.cancel)
+ fun Resources.asset() = this.assets.open("Asset.txt").reader().readText()
- val originalValue = resources.getString(android.R.string.cancel)
+ val originalResource = resources.resource()
+ val originalAsset = resources.asset()
- val loader = "stringOne".openLoader()
- addLoader(loader)
+ val loaderResource = "stringOne".openLoader()
+ val loaderAsset = "assetOne".openLoader(dataType = DataType.ASSET)
+ addLoader(loaderResource)
+ addLoader(loaderAsset)
val oldLoaders = resources.loaders
- val oldValue = resources.getString(android.R.string.cancel)
+ val oldResource = resources.resource()
+ val oldAsset = resources.asset()
- assertThat(oldValue).isNotEqualTo(originalValue)
+ assertThat(oldResource).isNotEqualTo(originalResource)
+ assertThat(oldAsset).isNotEqualTo(originalAsset)
val newResources = block()
val newLoaders = newResources.loaders
- val newValue = newResources.getString(android.R.string.cancel)
+ val newResource = newResources.resource()
+ val newAsset = newResources.asset()
- assertThat(newValue).isEqualTo(oldValue)
+ assertThat(newResource).isEqualTo(oldResource)
+ assertThat(newAsset).isEqualTo(oldAsset)
assertThat(newLoaders).isEqualTo(oldLoaders)
}
diff --git a/core/tests/coretests/src/android/app/timedetector/ManualTimeSuggestionTest.java b/core/tests/coretests/src/android/app/timedetector/ManualTimeSuggestionTest.java
new file mode 100644
index 0000000..de6f8f7
--- /dev/null
+++ b/core/tests/coretests/src/android/app/timedetector/ManualTimeSuggestionTest.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2019 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.app.timedetector;
+
+import static android.app.timezonedetector.ParcelableTestSupport.assertRoundTripParcelable;
+import static android.app.timezonedetector.ParcelableTestSupport.roundTripParcelable;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import android.util.TimestampedValue;
+
+import org.junit.Test;
+
+public class ManualTimeSuggestionTest {
+
+ private static final TimestampedValue<Long> ARBITRARY_TIME =
+ new TimestampedValue<>(1111L, 2222L);
+
+ @Test
+ public void testEquals() {
+ ManualTimeSuggestion one = new ManualTimeSuggestion(ARBITRARY_TIME);
+ assertEquals(one, one);
+
+ ManualTimeSuggestion two = new ManualTimeSuggestion(ARBITRARY_TIME);
+ assertEquals(one, two);
+ assertEquals(two, one);
+
+ TimestampedValue<Long> differentTime = new TimestampedValue<>(
+ ARBITRARY_TIME.getReferenceTimeMillis() + 1,
+ ARBITRARY_TIME.getValue());
+ ManualTimeSuggestion three = new ManualTimeSuggestion(differentTime);
+ assertNotEquals(one, three);
+ assertNotEquals(three, one);
+
+ // DebugInfo must not be considered in equals().
+ one.addDebugInfo("Debug info 1");
+ two.addDebugInfo("Debug info 2");
+ assertEquals(one, two);
+ }
+
+ @Test
+ public void testParcelable() {
+ ManualTimeSuggestion suggestion = new ManualTimeSuggestion(ARBITRARY_TIME);
+ assertRoundTripParcelable(suggestion);
+
+ // DebugInfo should also be stored (but is not checked by equals()
+ suggestion.addDebugInfo("This is debug info");
+ ManualTimeSuggestion rtSuggestion = roundTripParcelable(suggestion);
+ assertEquals(suggestion.getDebugInfo(), rtSuggestion.getDebugInfo());
+ }
+}
diff --git a/core/tests/coretests/src/android/app/timedetector/PhoneTimeSuggestionTest.java b/core/tests/coretests/src/android/app/timedetector/PhoneTimeSuggestionTest.java
index c9a86dc..bee270e 100644
--- a/core/tests/coretests/src/android/app/timedetector/PhoneTimeSuggestionTest.java
+++ b/core/tests/coretests/src/android/app/timedetector/PhoneTimeSuggestionTest.java
@@ -16,11 +16,12 @@
package android.app.timedetector;
+import static android.app.timezonedetector.ParcelableTestSupport.assertRoundTripParcelable;
+import static android.app.timezonedetector.ParcelableTestSupport.roundTripParcelable;
+
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
-import android.os.Parcel;
-import android.os.Parcelable;
import android.util.TimestampedValue;
import org.junit.Test;
@@ -30,53 +31,67 @@
@Test
public void testEquals() {
- PhoneTimeSuggestion one = new PhoneTimeSuggestion(PHONE_ID);
- assertEquals(one, one);
+ PhoneTimeSuggestion.Builder builder1 = new PhoneTimeSuggestion.Builder(PHONE_ID);
+ {
+ PhoneTimeSuggestion one = builder1.build();
+ assertEquals(one, one);
+ }
- PhoneTimeSuggestion two = new PhoneTimeSuggestion(PHONE_ID);
- assertEquals(one, two);
- assertEquals(two, one);
+ PhoneTimeSuggestion.Builder builder2 = new PhoneTimeSuggestion.Builder(PHONE_ID);
+ {
+ PhoneTimeSuggestion one = builder1.build();
+ PhoneTimeSuggestion two = builder2.build();
+ assertEquals(one, two);
+ assertEquals(two, one);
+ }
- one.setUtcTime(new TimestampedValue<>(1111L, 2222L));
- assertEquals(one, one);
+ builder1.setUtcTime(new TimestampedValue<>(1111L, 2222L));
+ {
+ PhoneTimeSuggestion one = builder1.build();
+ assertEquals(one, one);
+ }
- two.setUtcTime(new TimestampedValue<>(1111L, 2222L));
- assertEquals(one, two);
- assertEquals(two, one);
+ builder2.setUtcTime(new TimestampedValue<>(1111L, 2222L));
+ {
+ PhoneTimeSuggestion one = builder1.build();
+ PhoneTimeSuggestion two = builder2.build();
+ assertEquals(one, two);
+ assertEquals(two, one);
+ }
- PhoneTimeSuggestion three = new PhoneTimeSuggestion(PHONE_ID + 1);
- three.setUtcTime(new TimestampedValue<>(1111L, 2222L));
- assertNotEquals(one, three);
- assertNotEquals(three, one);
+ PhoneTimeSuggestion.Builder builder3 = new PhoneTimeSuggestion.Builder(PHONE_ID + 1);
+ builder3.setUtcTime(new TimestampedValue<>(1111L, 2222L));
+ {
+ PhoneTimeSuggestion one = builder1.build();
+ PhoneTimeSuggestion three = builder3.build();
+ assertNotEquals(one, three);
+ assertNotEquals(three, one);
+ }
// DebugInfo must not be considered in equals().
- one.addDebugInfo("Debug info 1");
- two.addDebugInfo("Debug info 2");
- assertEquals(one, two);
+ builder1.addDebugInfo("Debug info 1");
+ builder2.addDebugInfo("Debug info 2");
+ {
+ PhoneTimeSuggestion one = builder1.build();
+ PhoneTimeSuggestion two = builder2.build();
+ assertEquals(one, two);
+ }
}
@Test
public void testParcelable() {
- PhoneTimeSuggestion one = new PhoneTimeSuggestion(PHONE_ID);
- assertEquals(one, roundTripParcelable(one));
+ PhoneTimeSuggestion.Builder builder = new PhoneTimeSuggestion.Builder(PHONE_ID);
+ assertRoundTripParcelable(builder.build());
- one.setUtcTime(new TimestampedValue<>(1111L, 2222L));
- assertEquals(one, roundTripParcelable(one));
+ builder.setUtcTime(new TimestampedValue<>(1111L, 2222L));
+ assertRoundTripParcelable(builder.build());
// DebugInfo should also be stored (but is not checked by equals()
- one.addDebugInfo("This is debug info");
- PhoneTimeSuggestion two = roundTripParcelable(one);
- assertEquals(one.getDebugInfo(), two.getDebugInfo());
- }
-
- @SuppressWarnings("unchecked")
- private static <T extends Parcelable> T roundTripParcelable(T one) {
- Parcel parcel = Parcel.obtain();
- parcel.writeTypedObject(one, 0);
- parcel.setDataPosition(0);
-
- T toReturn = (T) parcel.readTypedObject(PhoneTimeSuggestion.CREATOR);
- parcel.recycle();
- return toReturn;
+ {
+ PhoneTimeSuggestion suggestion1 = builder.build();
+ builder.addDebugInfo("This is debug info");
+ PhoneTimeSuggestion rtSuggestion1 = roundTripParcelable(suggestion1);
+ assertEquals(suggestion1.getDebugInfo(), rtSuggestion1.getDebugInfo());
+ }
}
}
diff --git a/core/tests/coretests/src/android/widget/EditorTouchStateTest.java b/core/tests/coretests/src/android/widget/EditorTouchStateTest.java
new file mode 100644
index 0000000..6d50e3a
--- /dev/null
+++ b/core/tests/coretests/src/android/widget/EditorTouchStateTest.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright (C) 2019 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.widget;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+import android.view.InputDevice;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+import android.widget.EditorTouchState.MultiTapStatus;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+@SmallTest
+public class EditorTouchStateTest {
+
+ private EditorTouchState mTouchState;
+ private ViewConfiguration mConfig;
+
+ @Before
+ public void before() throws Exception {
+ mTouchState = new EditorTouchState();
+ mConfig = new ViewConfiguration();
+ }
+
+ @Test
+ public void testUpdate_singleTap() throws Exception {
+ // Simulate an ACTION_DOWN event.
+ long event1Time = 1000;
+ MotionEvent event1 = downEvent(event1Time, event1Time, 20f, 30f);
+ mTouchState.update(event1, mConfig);
+ assertSingleTap(mTouchState, 20f, 30f, 0, 0);
+
+ // Simulate an ACTION_UP event.
+ long event2Time = 1001;
+ MotionEvent event2 = upEvent(event1Time, event2Time, 20f, 30f);
+ mTouchState.update(event2, mConfig);
+ assertSingleTap(mTouchState, 20f, 30f, 20f, 30f);
+
+ // Generate an ACTION_DOWN event whose time is after the double-tap timeout.
+ long event3Time = event2Time + ViewConfiguration.getDoubleTapTimeout() + 1;
+ MotionEvent event3 = downEvent(event3Time, event3Time, 22f, 33f);
+ mTouchState.update(event3, mConfig);
+ assertSingleTap(mTouchState, 22f, 33f, 20f, 30f);
+ }
+
+ @Test
+ public void testUpdate_doubleTap_sameArea() throws Exception {
+ // Simulate an ACTION_DOWN event.
+ long event1Time = 1000;
+ MotionEvent event1 = downEvent(event1Time, event1Time, 20f, 30f);
+ mTouchState.update(event1, mConfig);
+ assertSingleTap(mTouchState, 20f, 30f, 0, 0);
+
+ // Simulate an ACTION_UP event.
+ long event2Time = 1001;
+ MotionEvent event2 = upEvent(event1Time, event2Time, 20f, 30f);
+ mTouchState.update(event2, mConfig);
+ assertSingleTap(mTouchState, 20f, 30f, 20f, 30f);
+
+ // Generate an ACTION_DOWN event whose time is within the double-tap timeout.
+ long event3Time = 1002;
+ MotionEvent event3 = downEvent(event3Time, event3Time, 22f, 33f);
+ mTouchState.update(event3, mConfig);
+ assertTap(mTouchState, 22f, 33f, 20f, 30f,
+ MultiTapStatus.DOUBLE_TAP, true);
+ }
+
+ @Test
+ public void testUpdate_doubleTap_notSameArea() throws Exception {
+ // Simulate an ACTION_DOWN event.
+ long event1Time = 1000;
+ MotionEvent event1 = downEvent(event1Time, event1Time, 20f, 30f);
+ mTouchState.update(event1, mConfig);
+ assertSingleTap(mTouchState, 20f, 30f, 0, 0);
+
+ // Simulate an ACTION_UP event.
+ long event2Time = 1001;
+ MotionEvent event2 = upEvent(event1Time, event2Time, 20f, 30f);
+ mTouchState.update(event2, mConfig);
+ assertSingleTap(mTouchState, 20f, 30f, 20f, 30f);
+
+ // Generate an ACTION_DOWN event whose time is within the double-tap timeout.
+ long event3Time = 1002;
+ MotionEvent event3 = downEvent(event3Time, event3Time, 200f, 300f);
+ mTouchState.update(event3, mConfig);
+ assertTap(mTouchState, 200f, 300f, 20f, 30f,
+ MultiTapStatus.DOUBLE_TAP, false);
+
+ // Simulate an ACTION_UP event.
+ long event4Time = 1003;
+ MotionEvent event4 = upEvent(event3Time, event4Time, 200f, 300f);
+ mTouchState.update(event4, mConfig);
+ assertTap(mTouchState, 200f, 300f, 200f, 300f,
+ MultiTapStatus.DOUBLE_TAP, false);
+ }
+
+ @Test
+ public void testUpdate_tripleClick_mouse() throws Exception {
+ // Simulate an ACTION_DOWN event.
+ long event1Time = 1000;
+ MotionEvent event1 = downEvent(event1Time, event1Time, 20f, 30f);
+ event1.setSource(InputDevice.SOURCE_MOUSE);
+ mTouchState.update(event1, mConfig);
+ assertSingleTap(mTouchState, 20f, 30f, 0, 0);
+
+ // Simulate an ACTION_UP event.
+ long event2Time = 1001;
+ MotionEvent event2 = upEvent(event1Time, event2Time, 20f, 30f);
+ event2.setSource(InputDevice.SOURCE_MOUSE);
+ mTouchState.update(event2, mConfig);
+ assertSingleTap(mTouchState, 20f, 30f, 20f, 30f);
+
+ // Generate a second ACTION_DOWN event whose time is within the double-tap timeout.
+ long event3Time = 1002;
+ MotionEvent event3 = downEvent(event3Time, event3Time, 21f, 31f);
+ event3.setSource(InputDevice.SOURCE_MOUSE);
+ mTouchState.update(event3, mConfig);
+ assertTap(mTouchState, 21f, 31f, 20f, 30f,
+ MultiTapStatus.DOUBLE_TAP, true);
+
+ // Simulate an ACTION_UP event.
+ long event4Time = 1003;
+ MotionEvent event4 = upEvent(event3Time, event4Time, 21f, 31f);
+ event4.setSource(InputDevice.SOURCE_MOUSE);
+ mTouchState.update(event4, mConfig);
+ assertTap(mTouchState, 21f, 31f, 21f, 31f,
+ MultiTapStatus.DOUBLE_TAP, true);
+
+ // Generate a third ACTION_DOWN event whose time is within the double-tap timeout.
+ long event5Time = 1004;
+ MotionEvent event5 = downEvent(event5Time, event5Time, 22f, 32f);
+ event5.setSource(InputDevice.SOURCE_MOUSE);
+ mTouchState.update(event5, mConfig);
+ assertTap(mTouchState, 22f, 32f, 21f, 31f,
+ MultiTapStatus.TRIPLE_CLICK, true);
+ }
+
+ @Test
+ public void testUpdate_tripleClick_touch() throws Exception {
+ // Simulate an ACTION_DOWN event.
+ long event1Time = 1000;
+ MotionEvent event1 = downEvent(event1Time, event1Time, 20f, 30f);
+ mTouchState.update(event1, mConfig);
+ assertSingleTap(mTouchState, 20f, 30f, 0, 0);
+
+ // Simulate an ACTION_UP event.
+ long event2Time = 1001;
+ MotionEvent event2 = upEvent(event1Time, event2Time, 20f, 30f);
+ mTouchState.update(event2, mConfig);
+ assertSingleTap(mTouchState, 20f, 30f, 20f, 30f);
+
+ // Generate a second ACTION_DOWN event whose time is within the double-tap timeout.
+ long event3Time = 1002;
+ MotionEvent event3 = downEvent(event3Time, event3Time, 21f, 31f);
+ mTouchState.update(event3, mConfig);
+ assertTap(mTouchState, 21f, 31f, 20f, 30f,
+ MultiTapStatus.DOUBLE_TAP, true);
+
+ // Simulate an ACTION_UP event.
+ long event4Time = 1003;
+ MotionEvent event4 = upEvent(event3Time, event4Time, 21f, 31f);
+ mTouchState.update(event4, mConfig);
+ assertTap(mTouchState, 21f, 31f, 21f, 31f,
+ MultiTapStatus.DOUBLE_TAP, true);
+
+ // Generate a third ACTION_DOWN event whose time is within the double-tap timeout.
+ long event5Time = 1004;
+ MotionEvent event5 = downEvent(event5Time, event5Time, 22f, 32f);
+ mTouchState.update(event5, mConfig);
+ assertTap(mTouchState, 22f, 32f, 21f, 31f,
+ MultiTapStatus.FIRST_TAP, false);
+ }
+
+ private static MotionEvent downEvent(long downTime, long eventTime, float x, float y) {
+ return MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_DOWN, x, y, 0);
+ }
+
+ private static MotionEvent upEvent(long downTime, long eventTime, float x, float y) {
+ return MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_UP, x, y, 0);
+ }
+
+ private static void assertSingleTap(EditorTouchState touchState, float lastDownX,
+ float lastDownY, float lastUpX, float lastUpY) {
+ assertThat(touchState.getLastDownX(), is(lastDownX));
+ assertThat(touchState.getLastDownY(), is(lastDownY));
+ assertThat(touchState.getLastUpX(), is(lastUpX));
+ assertThat(touchState.getLastUpY(), is(lastUpY));
+ assertThat(touchState.isDoubleTap(), is(false));
+ assertThat(touchState.isTripleClick(), is(false));
+ assertThat(touchState.isMultiTap(), is(false));
+ assertThat(touchState.isMultiTapInSameArea(), is(false));
+ }
+
+ private static void assertTap(EditorTouchState touchState,
+ float lastDownX, float lastDownY, float lastUpX, float lastUpY,
+ @MultiTapStatus int multiTapStatus, boolean isMultiTapInSameArea) {
+ assertThat(touchState.getLastDownX(), is(lastDownX));
+ assertThat(touchState.getLastDownY(), is(lastDownY));
+ assertThat(touchState.getLastUpX(), is(lastUpX));
+ assertThat(touchState.getLastUpY(), is(lastUpY));
+ assertThat(touchState.isDoubleTap(), is(multiTapStatus == MultiTapStatus.DOUBLE_TAP));
+ assertThat(touchState.isTripleClick(), is(multiTapStatus == MultiTapStatus.TRIPLE_CLICK));
+ assertThat(touchState.isMultiTap(), is(multiTapStatus == MultiTapStatus.DOUBLE_TAP
+ || multiTapStatus == MultiTapStatus.TRIPLE_CLICK));
+ assertThat(touchState.isMultiTapInSameArea(), is(isMultiTapInSameArea));
+ }
+}
diff --git a/packages/SystemUI/res/layout/bubble_view.xml b/packages/SystemUI/res/layout/bubble_view.xml
index e2dea45..78f7cff 100644
--- a/packages/SystemUI/res/layout/bubble_view.xml
+++ b/packages/SystemUI/res/layout/bubble_view.xml
@@ -14,16 +14,8 @@
~ See the License for the specific language governing permissions and
~ limitations under the License
-->
-<com.android.systemui.bubbles.BubbleView
+<com.android.systemui.bubbles.BadgedImageView
xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_height="wrap_content"
- android:layout_width="wrap_content"
- android:id="@+id/bubble_view">
-
- <com.android.systemui.bubbles.BadgedImageView
- android:id="@+id/bubble_image"
- android:layout_width="@dimen/individual_bubble_size"
- android:layout_height="@dimen/individual_bubble_size"
- android:clipToPadding="false"/>
-
-</com.android.systemui.bubbles.BubbleView>
+ android:id="@+id/bubble_view"
+ android:layout_width="@dimen/individual_bubble_size"
+ android:layout_height="@dimen/individual_bubble_size"/>
diff --git a/packages/SystemUI/res/values/arrays_tv.xml b/packages/SystemUI/res/values/arrays_tv.xml
index 1fe6141..95716c8 100644
--- a/packages/SystemUI/res/values/arrays_tv.xml
+++ b/packages/SystemUI/res/values/arrays_tv.xml
@@ -36,5 +36,6 @@
<string-array name="audio_recording_disclosure_exempt_apps" translatable="false">
<item>com.google.android.katniss</item>
+ <item>com.google.android.apps.mediashell</item>
</string-array>
</resources>
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BadgedImageView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BadgedImageView.java
index c0053d1..a6a3ce0 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BadgedImageView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BadgedImageView.java
@@ -15,35 +15,61 @@
*/
package com.android.systemui.bubbles;
+import android.annotation.Nullable;
+import android.app.Notification;
import android.content.Context;
-import android.content.res.TypedArray;
+import android.content.pm.LauncherApps;
import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Matrix;
import android.graphics.Path;
import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
import android.util.AttributeSet;
+import android.util.PathParser;
import android.widget.ImageView;
import com.android.internal.graphics.ColorUtils;
+import com.android.launcher3.icons.BitmapInfo;
import com.android.launcher3.icons.DotRenderer;
+import com.android.systemui.Interpolators;
import com.android.systemui.R;
/**
- * View that circle crops its contents and supports displaying a coloured dot on a top corner.
+ * View that displays an adaptive icon with an app-badge and a dot.
+ *
+ * Dot = a small colored circle that indicates whether this bubble has an unread update.
+ * Badge = the icon associated with the app that created this bubble, this will show work profile
+ * badge if appropriate.
*/
public class BadgedImageView extends ImageView {
- private Rect mTempBounds = new Rect();
+ /** Same value as Launcher3 dot code */
+ private static final float WHITE_SCRIM_ALPHA = 0.54f;
+ /** Same as value in Launcher3 IconShape */
+ private static final int DEFAULT_PATH_SIZE = 100;
+ static final int DOT_STATE_DEFAULT = 0;
+ static final int DOT_STATE_SUPPRESSED_FOR_FLYOUT = 1;
+ static final int DOT_STATE_ANIMATING = 2;
+
+ // Flyout gets shown before the dot
+ private int mCurrentDotState = DOT_STATE_SUPPRESSED_FOR_FLYOUT;
+
+ private Bubble mBubble;
+ private BubbleIconFactory mBubbleIconFactory;
+
+ private int mIconBitmapSize;
private DotRenderer mDotRenderer;
private DotRenderer.DrawParams mDrawParams;
- private int mIconBitmapSize;
- private int mDotColor;
- private float mDotScale = 0f;
- private boolean mShowDot;
private boolean mOnLeft;
- /** Same as value in Launcher3 IconShape */
- static final int DEFAULT_PATH_SIZE = 100;
+ private int mDotColor;
+ private float mDotScale = 0f;
+ private boolean mDotDrawn;
+
+ private Rect mTempBounds = new Rect();
public BadgedImageView(Context context) {
this(context, null);
@@ -63,17 +89,19 @@
mIconBitmapSize = getResources().getDimensionPixelSize(R.dimen.bubble_icon_bitmap_size);
mDrawParams = new DotRenderer.DrawParams();
- TypedArray ta = context.obtainStyledAttributes(
- new int[]{android.R.attr.colorBackgroundFloating});
- ta.recycle();
+ Path iconPath = PathParser.createPathFromPathData(
+ getResources().getString(com.android.internal.R.string.config_icon_mask));
+ mDotRenderer = new DotRenderer(mIconBitmapSize, iconPath, DEFAULT_PATH_SIZE);
}
@Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
- if (!mShowDot) {
+ if (isDotHidden()) {
+ mDotDrawn = false;
return;
}
+ mDotDrawn = mDotScale > 0.1f;
getDrawingRect(mTempBounds);
mDrawParams.color = mDotColor;
@@ -81,16 +109,29 @@
mDrawParams.leftAlign = mOnLeft;
mDrawParams.scale = mDotScale;
- if (mDotRenderer == null) {
- Path circlePath = new Path();
- float radius = DEFAULT_PATH_SIZE * 0.5f;
- circlePath.addCircle(radius /* x */, radius /* y */, radius, Path.Direction.CW);
- mDotRenderer = new DotRenderer(mIconBitmapSize, circlePath, DEFAULT_PATH_SIZE);
- }
mDotRenderer.draw(canvas, mDrawParams);
}
/**
+ * Sets the dot state, does not animate changes.
+ */
+ void setDotState(int state) {
+ mCurrentDotState = state;
+ if (state == DOT_STATE_SUPPRESSED_FOR_FLYOUT || state == DOT_STATE_DEFAULT) {
+ mDotScale = mBubble.showDot() ? 1f : 0f;
+ invalidate();
+ }
+ }
+
+ /**
+ * Whether the dot should be hidden based on current dot state.
+ */
+ private boolean isDotHidden() {
+ return (mCurrentDotState == DOT_STATE_DEFAULT && !mBubble.showDot())
+ || mCurrentDotState == DOT_STATE_SUPPRESSED_FOR_FLYOUT;
+ }
+
+ /**
* Set whether the dot should appear on left or right side of the view.
*/
void setDotOnLeft(boolean onLeft) {
@@ -98,29 +139,10 @@
invalidate();
}
- boolean getDotOnLeft() {
- return mOnLeft;
- }
-
- /**
- * Set whether the dot should show or not.
- */
- void setShowDot(boolean showDot) {
- mShowDot = showDot;
- invalidate();
- }
-
- /**
- * @return whether the dot is being displayed.
- */
- boolean isShowingDot() {
- return mShowDot;
- }
-
/**
* The colour to use for the dot.
*/
- public void setDotColor(int color) {
+ void setDotColor(int color) {
mDotColor = ColorUtils.setAlphaComponent(color, 255 /* alpha */);
invalidate();
}
@@ -128,7 +150,7 @@
/**
* @param iconPath The new icon path to use when calculating dot position.
*/
- public void drawDot(Path iconPath) {
+ void drawDot(Path iconPath) {
mDotRenderer = new DotRenderer(mIconBitmapSize, iconPath, DEFAULT_PATH_SIZE);
invalidate();
}
@@ -142,6 +164,13 @@
}
/**
+ * Whether decorations (badges or dots) are on the left.
+ */
+ boolean getDotOnLeft() {
+ return mOnLeft;
+ }
+
+ /**
* Return dot position relative to bubble view container bounds.
*/
float[] getDotCenter() {
@@ -149,11 +178,146 @@
if (mOnLeft) {
dotPosition = mDotRenderer.getLeftDotPosition();
} else {
- dotPosition = mDotRenderer.getRightDotPosition();
+ dotPosition = mDotRenderer.getRightDotPosition();
}
getDrawingRect(mTempBounds);
float dotCenterX = mTempBounds.width() * dotPosition[0];
float dotCenterY = mTempBounds.height() * dotPosition[1];
return new float[]{dotCenterX, dotCenterY};
}
+
+ /**
+ * Populates this view with a bubble.
+ * <p>
+ * This should only be called when a new bubble is being set on the view, updates to the
+ * current bubble should use {@link #update(Bubble)}.
+ *
+ * @param bubble the bubble to display in this view.
+ */
+ public void setBubble(Bubble bubble) {
+ mBubble = bubble;
+ }
+
+ /**
+ * @param factory Factory for creating normalized bubble icons.
+ */
+ public void setBubbleIconFactory(BubbleIconFactory factory) {
+ mBubbleIconFactory = factory;
+ }
+
+ /**
+ * The key for the {@link Bubble} associated with this view, if one exists.
+ */
+ @Nullable
+ public String getKey() {
+ return (mBubble != null) ? mBubble.getKey() : null;
+ }
+
+ /**
+ * Updates the UI based on the bubble, updates badge and animates messages as needed.
+ */
+ public void update(Bubble bubble) {
+ mBubble = bubble;
+ setDotState(DOT_STATE_SUPPRESSED_FOR_FLYOUT);
+ updateViews();
+ }
+
+ int getDotColor() {
+ return mDotColor;
+ }
+
+ /** Sets the position of the 'new' dot, animating it out and back in if requested. */
+ void setDotPosition(boolean onLeft, boolean animate) {
+ if (animate && onLeft != getDotOnLeft() && !isDotHidden()) {
+ animateDot(false /* showDot */, () -> {
+ setDotOnLeft(onLeft);
+ animateDot(true /* showDot */, null);
+ });
+ } else {
+ setDotOnLeft(onLeft);
+ }
+ }
+
+ boolean getDotPositionOnLeft() {
+ return getDotOnLeft();
+ }
+
+ /** Changes the dot's visibility to match the bubble view's state. */
+ void animateDot() {
+ if (mCurrentDotState == DOT_STATE_DEFAULT) {
+ animateDot(mBubble.showDot(), null);
+ }
+ }
+
+ /**
+ * Animates the dot to show or hide.
+ */
+ private void animateDot(boolean showDot, Runnable after) {
+ if (mDotDrawn == showDot) {
+ // State is consistent, do nothing.
+ return;
+ }
+
+ setDotState(DOT_STATE_ANIMATING);
+
+ // Do NOT wait until after animation ends to setShowDot
+ // to avoid overriding more recent showDot states.
+ clearAnimation();
+ animate().setDuration(200)
+ .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
+ .setUpdateListener((valueAnimator) -> {
+ float fraction = valueAnimator.getAnimatedFraction();
+ fraction = showDot ? fraction : 1f - fraction;
+ setDotScale(fraction);
+ }).withEndAction(() -> {
+ setDotScale(showDot ? 1f : 0f);
+ setDotState(DOT_STATE_DEFAULT);
+ if (after != null) {
+ after.run();
+ }
+ }).start();
+ }
+
+ void updateViews() {
+ if (mBubble == null || mBubbleIconFactory == null) {
+ return;
+ }
+
+ Drawable bubbleDrawable = getBubbleDrawable(mContext);
+ BitmapInfo badgeBitmapInfo = mBubbleIconFactory.getBadgedBitmap(mBubble);
+ BitmapInfo bubbleBitmapInfo = mBubbleIconFactory.getBubbleBitmap(bubbleDrawable,
+ badgeBitmapInfo);
+ setImageBitmap(bubbleBitmapInfo.icon);
+
+ // Update badge.
+ mDotColor = ColorUtils.blendARGB(badgeBitmapInfo.color, Color.WHITE, WHITE_SCRIM_ALPHA);
+ setDotColor(mDotColor);
+
+ // Update dot.
+ Path iconPath = PathParser.createPathFromPathData(
+ getResources().getString(com.android.internal.R.string.config_icon_mask));
+ Matrix matrix = new Matrix();
+ float scale = mBubbleIconFactory.getNormalizer().getScale(bubbleDrawable,
+ null /* outBounds */, null /* path */, null /* outMaskShape */);
+ float radius = BadgedImageView.DEFAULT_PATH_SIZE / 2f;
+ matrix.setScale(scale /* x scale */, scale /* y scale */, radius /* pivot x */,
+ radius /* pivot y */);
+ iconPath.transform(matrix);
+ drawDot(iconPath);
+
+ animateDot();
+ }
+
+ Drawable getBubbleDrawable(Context context) {
+ if (mBubble.getShortcutInfo() != null && mBubble.usingShortcutInfo()) {
+ LauncherApps launcherApps =
+ (LauncherApps) getContext().getSystemService(Context.LAUNCHER_APPS_SERVICE);
+ int density = getContext().getResources().getConfiguration().densityDpi;
+ return launcherApps.getShortcutIconDrawable(mBubble.getShortcutInfo(), density);
+ } else {
+ Notification.BubbleMetadata metadata = mBubble.getEntry().getBubbleMetadata();
+ Icon ic = metadata.getIcon();
+ return ic.loadDrawable(context);
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
index 46d060e..f3a7ca9 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
@@ -64,8 +64,9 @@
private ShortcutInfo mShortcutInfo;
private boolean mInflated;
- private BubbleView mIconView;
+ private BadgedImageView mIconView;
private BubbleExpandedView mExpandedView;
+ private BubbleIconFactory mBubbleIconFactory;
private long mLastUpdated;
private long mLastAccessed;
@@ -146,7 +147,7 @@
return mAppName;
}
- public Drawable getUserBadgedAppIcon() {
+ Drawable getUserBadgedAppIcon() {
return mUserBadgedAppIcon;
}
@@ -165,17 +166,15 @@
return BubbleExperimentConfig.isShortcutIntent(getBubbleIntent());
}
+ void setBubbleIconFactory(BubbleIconFactory factory) {
+ mBubbleIconFactory = factory;
+ }
+
boolean isInflated() {
return mInflated;
}
- void updateDotVisibility() {
- if (mIconView != null) {
- mIconView.updateDotVisibility(true /* animate */);
- }
- }
-
- BubbleView getIconView() {
+ BadgedImageView getIconView() {
return mIconView;
}
@@ -193,8 +192,9 @@
if (mInflated) {
return;
}
- mIconView = (BubbleView) inflater.inflate(
+ mIconView = (BadgedImageView) inflater.inflate(
R.layout.bubble_view, stackView, false /* attachToRoot */);
+ mIconView.setBubbleIconFactory(mBubbleIconFactory);
mIconView.setBubble(this);
mExpandedView = (BubbleExpandedView) inflater.inflate(
@@ -260,15 +260,15 @@
*/
void markAsAccessedAt(long lastAccessedMillis) {
mLastAccessed = lastAccessedMillis;
- setShowInShadeWhenBubble(false);
- setShowBubbleDot(false);
+ setShowInShade(false);
+ setShowDot(false /* show */, true /* animate */);
}
/**
* Whether this notification should be shown in the shade when it is also displayed as a
* bubble.
*/
- boolean showInShadeWhenBubble() {
+ boolean showInShade() {
return !mEntry.isRowDismissed() && !shouldSuppressNotification()
&& (!mEntry.isClearable() || mShowInShadeWhenBubble);
}
@@ -277,28 +277,33 @@
* Sets whether this notification should be shown in the shade when it is also displayed as a
* bubble.
*/
- void setShowInShadeWhenBubble(boolean showInShade) {
+ void setShowInShade(boolean showInShade) {
mShowInShadeWhenBubble = showInShade;
}
/**
* Sets whether the bubble for this notification should show a dot indicating updated content.
*/
- void setShowBubbleDot(boolean showDot) {
+ void setShowDot(boolean showDot, boolean animate) {
mShowBubbleUpdateDot = showDot;
+ if (animate && mIconView != null) {
+ mIconView.animateDot();
+ } else if (mIconView != null) {
+ mIconView.invalidate();
+ }
}
/**
* Whether the bubble for this notification should show a dot indicating updated content.
*/
- boolean showBubbleDot() {
+ boolean showDot() {
return mShowBubbleUpdateDot && !mEntry.shouldSuppressNotificationDot();
}
/**
* Whether the flyout for the bubble should be shown.
*/
- boolean showFlyoutForBubble() {
+ boolean showFlyout() {
return !mSuppressFlyout && !mEntry.shouldSuppressPeek()
&& !mEntry.shouldSuppressNotificationList();
}
@@ -470,9 +475,9 @@
public void dump(
@NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) {
pw.print("key: "); pw.println(mKey);
- pw.print(" showInShade: "); pw.println(showInShadeWhenBubble());
- pw.print(" showDot: "); pw.println(showBubbleDot());
- pw.print(" showFlyout: "); pw.println(showFlyoutForBubble());
+ pw.print(" showInShade: "); pw.println(showInShade());
+ pw.print(" showDot: "); pw.println(showDot());
+ pw.print(" showFlyout: "); pw.println(showFlyout());
pw.print(" desiredHeight: "); pw.println(getDesiredHeightString());
pw.print(" suppressNotif: "); pw.println(shouldSuppressNotification());
pw.print(" autoExpand: "); pw.println(shouldAutoExpand());
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
index 15bb330..dbb1936 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
@@ -251,15 +251,15 @@
mZenModeController.addCallback(new ZenModeController.Callback() {
@Override
public void onZenChanged(int zen) {
- if (mStackView != null) {
- mStackView.updateDots();
+ for (Bubble b : mBubbleData.getBubbles()) {
+ b.setShowDot(b.showInShade(), true /* animate */);
}
}
@Override
public void onConfigChanged(ZenModeConfig config) {
- if (mStackView != null) {
- mStackView.updateDots();
+ for (Bubble b : mBubbleData.getBubbles()) {
+ b.setShowDot(b.showInShade(), true /* animate */);
}
}
});
@@ -465,7 +465,7 @@
*/
public boolean isBubbleNotificationSuppressedFromShade(String key) {
boolean isBubbleAndSuppressed = mBubbleData.hasBubbleWithKey(key)
- && !mBubbleData.getBubbleWithKey(key).showInShadeWhenBubble();
+ && !mBubbleData.getBubbleWithKey(key).showInShade();
NotificationEntry entry = mNotificationEntryManager.getActiveNotificationUnfiltered(key);
String groupKey = entry != null ? entry.getSbn().getGroupKey() : null;
boolean isSuppressedSummary = mBubbleData.isSummarySuppressed(groupKey);
@@ -630,11 +630,8 @@
Bubble bubble = mBubbleData.getBubbleWithKey(key);
boolean bubbleExtended = entry != null && entry.isBubble() && userRemovedNotif;
if (bubbleExtended) {
- bubble.setShowInShadeWhenBubble(false);
- bubble.setShowBubbleDot(false);
- if (mStackView != null) {
- mStackView.updateDotVisibility(entry.getKey());
- }
+ bubble.setShowInShade(false);
+ bubble.setShowDot(false /* show */, true /* animate */);
mNotificationEntryManager.updateNotifications(
"BubbleController.onNotificationRemoveRequested");
return true;
@@ -660,11 +657,8 @@
// As far as group manager is concerned, once a child is no longer shown
// in the shade, it is essentially removed.
mNotificationGroupManager.onEntryRemoved(bubbleChild.getEntry());
- bubbleChild.setShowInShadeWhenBubble(false);
- bubbleChild.setShowBubbleDot(false);
- if (mStackView != null) {
- mStackView.updateDotVisibility(bubbleChild.getKey());
- }
+ bubbleChild.setShowInShade(false);
+ bubbleChild.setShowDot(false /* show */, true /* animate */);
}
// And since all children are removed, remove the summary.
mNotificationGroupManager.onEntryRemoved(summary);
@@ -705,6 +699,10 @@
if (mNotificationInterruptionStateProvider.shouldBubbleUp(entry)
&& (canLaunchInActivityView(mContext, entry) || wasAdjusted)) {
+ if (wasAdjusted && !previouslyUserCreated) {
+ // Gotta treat the auto-bubbled / whitelisted packaged bubbles as usercreated
+ mUserCreatedBubbles.add(entry.getKey());
+ }
updateBubble(entry);
}
}
@@ -721,6 +719,10 @@
// It was previously a bubble but no longer a bubble -- lets remove it
removeBubble(entry.getKey(), DISMISS_NO_LONGER_BUBBLE);
} else if (shouldBubble) {
+ if (wasAdjusted && !previouslyUserCreated) {
+ // Gotta treat the auto-bubbled / whitelisted packaged bubbles as usercreated
+ mUserCreatedBubbles.add(entry.getKey());
+ }
updateBubble(entry);
}
}
@@ -767,7 +769,7 @@
// If the bubble is removed for user switching, leave the notification in place.
if (reason != DISMISS_USER_CHANGED) {
if (!mBubbleData.hasBubbleWithKey(bubble.getKey())
- && !bubble.showInShadeWhenBubble()) {
+ && !bubble.showInShade()) {
// The bubble is gone & the notification is gone, time to actually remove it
mNotificationEntryManager.performRemoveNotification(
bubble.getEntry().getSbn(), UNDEFINED_DISMISS_REASON);
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
index 2ca993b..034bff3 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
@@ -210,8 +210,8 @@
setSelectedBubbleInternal(bubble);
}
boolean isBubbleExpandedAndSelected = mExpanded && mSelectedBubble == bubble;
- bubble.setShowInShadeWhenBubble(!isBubbleExpandedAndSelected && showInShade);
- bubble.setShowBubbleDot(!isBubbleExpandedAndSelected);
+ bubble.setShowInShade(!isBubbleExpandedAndSelected && showInShade);
+ bubble.setShowDot(!isBubbleExpandedAndSelected /* show */, true /* animate */);
dispatchPendingChanges();
}
@@ -303,9 +303,8 @@
if (notif.getRanking().visuallyInterruptive()) {
return true;
}
- final boolean suppressedFromShade = hasBubbleWithKey(notif.getKey())
- && !getBubbleWithKey(notif.getKey()).showInShadeWhenBubble();
- return suppressedFromShade;
+ return hasBubbleWithKey(notif.getKey())
+ && !getBubbleWithKey(notif.getKey()).showInShade();
}
private void doAdd(Bubble bubble) {
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java
index 856b15e..efc955d 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedView.java
@@ -603,7 +603,7 @@
action,
mStackView.getNormalizedXPosition(),
mStackView.getNormalizedYPosition(),
- bubble.showInShadeWhenBubble(),
+ bubble.showInShade(),
bubble.isOngoing(),
false /* isAppForeground (unused) */);
}
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExperimentConfig.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExperimentConfig.java
index fd7fff4..e138d93 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExperimentConfig.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExperimentConfig.java
@@ -66,6 +66,8 @@
private static final String ALLOW_SHORTCUTS_TO_BUBBLE = "allow_shortcuts_to_bubble";
private static final boolean ALLOW_SHORTCUT_TO_BUBBLE_DEFAULT = false;
+ private static final String WHITELISTED_AUTO_BUBBLE_APPS = "whitelisted_auto_bubble_apps";
+
/**
* When true, if a notification has the information necessary to bubble (i.e. valid
* contentIntent and an icon or image), then a {@link android.app.Notification.BubbleMetadata}
@@ -103,6 +105,24 @@
}
/**
+ * Returns whether the provided package is whitelisted to bubble.
+ */
+ static boolean isPackageWhitelistedToAutoBubble(Context context, String packageName) {
+ String unsplitList = Settings.Secure.getString(context.getContentResolver(),
+ WHITELISTED_AUTO_BUBBLE_APPS);
+ if (unsplitList != null) {
+ // We expect the list to be separated by commas and no white space (but we trim in case)
+ String[] packageList = unsplitList.split(",");
+ for (int i = 0; i < packageList.length; i++) {
+ if (packageList[i].trim().equals(packageName)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
* If {@link #allowAnyNotifToBubble(Context)} is true, this method creates and adds
* {@link android.app.Notification.BubbleMetadata} to the notification entry as long as
* the notification has necessary info for BubbleMetadata.
@@ -113,6 +133,8 @@
boolean previouslyUserCreated) {
Notification.BubbleMetadata metadata = null;
boolean addedMetadata = false;
+ boolean whiteListedToAutoBubble =
+ isPackageWhitelistedToAutoBubble(context, entry.getSbn().getPackageName());
Notification notification = entry.getSbn().getNotification();
boolean isMessage = Notification.MessagingStyle.class.equals(
@@ -170,9 +192,9 @@
}
}
- if (previouslyUserCreated && addedMetadata) {
- // Update to a previous bubble, set its flag now so the update goes
- // to the bubble.
+ boolean bubbleForWhitelist = whiteListedToAutoBubble && (addedMetadata || hasMetadata);
+ if ((previouslyUserCreated && addedMetadata) || bubbleForWhitelist) {
+ // Update to a previous bubble (or new autobubble), set its flag now.
if (DEBUG_EXPERIMENTS) {
Log.d(TAG, "Setting FLAG_BUBBLE for: " + entry.getKey());
}
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleFlyoutView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleFlyoutView.java
index 58f3f22..78e98eb 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleFlyoutView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleFlyoutView.java
@@ -206,7 +206,7 @@
void setupFlyoutStartingAsDot(
CharSequence updateMessage, PointF stackPos, float parentWidth,
boolean arrowPointingLeft, int dotColor, @Nullable Runnable onLayoutComplete,
- @Nullable Runnable onHide, float[] dotCenter) {
+ @Nullable Runnable onHide, float[] dotCenter, boolean hideDot) {
mArrowPointingLeft = arrowPointingLeft;
mDotColor = dotColor;
mOnHide = onHide;
@@ -242,12 +242,14 @@
// Calculate the difference in size between the flyout and the 'dot' so that we can
// transform into the dot later.
- mFlyoutToDotWidthDelta = getWidth() - mNewDotSize;
- mFlyoutToDotHeightDelta = getHeight() - mNewDotSize;
+ final float newDotSize = hideDot ? 0f : mNewDotSize;
+ mFlyoutToDotWidthDelta = getWidth() - newDotSize;
+ mFlyoutToDotHeightDelta = getHeight() - newDotSize;
// Calculate the translation values needed to be in the correct 'new dot' position.
- final float dotPositionX = stackPos.x + mDotCenter[0] - (mOriginalDotSize / 2f);
- final float dotPositionY = stackPos.y + mDotCenter[1] - (mOriginalDotSize / 2f);
+ final float adjustmentForScaleAway = hideDot ? 0f : (mOriginalDotSize / 2f);
+ final float dotPositionX = stackPos.x + mDotCenter[0] - adjustmentForScaleAway;
+ final float dotPositionY = stackPos.y + mDotCenter[1] - adjustmentForScaleAway;
final float distanceFromFlyoutLeftToDotCenterX = mRestingTranslationX - dotPositionX;
final float distanceFromLayoutTopToDotCenterY = restingTranslationY - dotPositionY;
@@ -319,8 +321,7 @@
// percentage.
final float width = getWidth() - (mFlyoutToDotWidthDelta * mPercentTransitionedToDot);
final float height = getHeight() - (mFlyoutToDotHeightDelta * mPercentTransitionedToDot);
- final float interpolatedRadius = mNewDotRadius * mPercentTransitionedToDot
- + mCornerRadius * (1 - mPercentTransitionedToDot);
+ final float interpolatedRadius = getInterpolatedRadius();
// Translate the flyout background towards the collapsed 'dot' state.
mBgTranslationX = mTranslationXWhenDot * mPercentTransitionedToDot;
@@ -387,8 +388,7 @@
if (!mTriangleOutline.isEmpty()) {
// Draw the rect into the outline as a path so we can merge the triangle path into it.
final Path rectPath = new Path();
- final float interpolatedRadius = mNewDotRadius * mPercentTransitionedToDot
- + mCornerRadius * (1 - mPercentTransitionedToDot);
+ final float interpolatedRadius = getInterpolatedRadius();
rectPath.addRoundRect(mBgRect, interpolatedRadius,
interpolatedRadius, Path.Direction.CW);
outline.setConvexPath(rectPath);
@@ -420,4 +420,9 @@
outline.mPath.transform(outlineMatrix);
}
}
+
+ private float getInterpolatedRadius() {
+ return mNewDotRadius * mPercentTransitionedToDot
+ + mCornerRadius * (1 - mPercentTransitionedToDot);
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleIconFactory.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleIconFactory.java
index a1c77c0..9ff033c 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleIconFactory.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleIconFactory.java
@@ -16,8 +16,14 @@
package com.android.systemui.bubbles;
import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
import com.android.launcher3.icons.BaseIconFactory;
+import com.android.launcher3.icons.BitmapInfo;
+import com.android.launcher3.icons.ShadowGenerator;
import com.android.systemui.R;
/**
@@ -26,13 +32,37 @@
* so there is no need to manage a pool across multiple threads.
*/
public class BubbleIconFactory extends BaseIconFactory {
+
protected BubbleIconFactory(Context context) {
super(context, context.getResources().getConfiguration().densityDpi,
context.getResources().getDimensionPixelSize(R.dimen.individual_bubble_size));
}
- public int getBadgeSize() {
+ int getBadgeSize() {
return mContext.getResources().getDimensionPixelSize(
com.android.launcher3.icons.R.dimen.profile_badge_size);
}
+
+ BitmapInfo getBadgedBitmap(Bubble b) {
+ Bitmap userBadgedBitmap = createIconBitmap(
+ b.getUserBadgedAppIcon(), 1f, getBadgeSize());
+
+ Canvas c = new Canvas();
+ ShadowGenerator shadowGenerator = new ShadowGenerator(getBadgeSize());
+ c.setBitmap(userBadgedBitmap);
+ shadowGenerator.recreateIcon(Bitmap.createBitmap(userBadgedBitmap), c);
+ BitmapInfo bitmapInfo = createIconBitmap(userBadgedBitmap);
+ return bitmapInfo;
+ }
+
+ BitmapInfo getBubbleBitmap(Drawable bubble, BitmapInfo badge) {
+ BitmapInfo bubbleIconInfo = createBadgedIconBitmap(bubble,
+ null /* user */,
+ true /* shrinkNonAdaptiveIcons */);
+
+ badgeWithDrawable(bubbleIconInfo.icon,
+ new BitmapDrawable(mContext.getResources(), badge.icon));
+ return bubbleIconInfo;
+ }
+
}
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
index 4a1bbe4..29de2f0 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
@@ -19,6 +19,8 @@
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
+import static com.android.systemui.bubbles.BadgedImageView.DOT_STATE_DEFAULT;
+import static com.android.systemui.bubbles.BadgedImageView.DOT_STATE_SUPPRESSED_FOR_FLYOUT;
import static com.android.systemui.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_STACK_VIEW;
import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_BUBBLES;
import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME;
@@ -169,7 +171,7 @@
* Callback to run after the flyout hides. Also called if a new flyout is shown before the
* previous one animates out.
*/
- private Runnable mFlyoutOnHide;
+ private Runnable mAfterFlyoutHidden;
/** Layout change listener that moves the stack to the nearest valid position on rotation. */
private OnLayoutChangeListener mOrientationChangedListener;
@@ -674,18 +676,6 @@
}
/**
- * Updates the visibility of the 'dot' indicating an update on the bubble.
- *
- * @param key the {@link NotificationEntry#key} associated with the bubble.
- */
- public void updateDotVisibility(String key) {
- Bubble b = mBubbleData.getBubbleWithKey(key);
- if (b != null) {
- b.updateDotVisibility();
- }
- }
-
- /**
* Sets the listener to notify when the bubble stack is expanded.
*/
public void setExpandListener(BubbleController.BubbleExpandListener listener) {
@@ -707,9 +697,9 @@
}
/**
- * The {@link BubbleView} that is expanded, null if one does not exist.
+ * The {@link BadgedImageView} that is expanded, null if one does not exist.
*/
- BubbleView getExpandedBubbleView() {
+ BadgedImageView getExpandedBubbleView() {
return mExpandedBubble != null ? mExpandedBubble.getIconView() : null;
}
@@ -731,7 +721,7 @@
Bubble bubbleToExpand = mBubbleData.getBubbleWithKey(key);
if (bubbleToExpand != null) {
setSelectedBubble(bubbleToExpand);
- bubbleToExpand.setShowInShadeWhenBubble(false);
+ bubbleToExpand.setShowInShade(false);
setExpanded(true);
}
}
@@ -746,8 +736,8 @@
mStackOnLeftOrWillBe = mStackAnimationController.isStackOnLeftSide();
}
+ bubble.setBubbleIconFactory(mBubbleIconFactory);
bubble.inflate(mInflater, this);
- bubble.getIconView().setBubbleIconFactory(mBubbleIconFactory);
bubble.getIconView().updateViews();
// Set the dot position to the opposite of the side the stack is resting on, since the stack
@@ -884,7 +874,7 @@
if (isIntersecting(mBubbleContainer, x, y)) {
// Could be tapping or dragging a bubble while expanded
for (int i = 0; i < mBubbleContainer.getChildCount(); i++) {
- BubbleView view = (BubbleView) mBubbleContainer.getChildAt(i);
+ BadgedImageView view = (BadgedImageView) mBubbleContainer.getChildAt(i);
if (isIntersecting(view, x, y)) {
return view;
}
@@ -1028,9 +1018,9 @@
}
/** Return the BubbleView at the given index from the bubble container. */
- public BubbleView getBubbleAt(int i) {
+ public BadgedImageView getBubbleAt(int i) {
return mBubbleContainer.getChildCount() > i
- ? (BubbleView) mBubbleContainer.getChildAt(i)
+ ? (BadgedImageView) mBubbleContainer.getChildAt(i)
: null;
}
@@ -1382,16 +1372,6 @@
: 0f);
}
- /** Updates the dot visibility, this is used in response to a zen mode config change. */
- void updateDots() {
- int bubbsCount = mBubbleContainer.getChildCount();
- for (int i = 0; i < bubbsCount; i++) {
- BubbleView bv = (BubbleView) mBubbleContainer.getChildAt(i);
- // If nothing changed the animation won't happen
- bv.updateDotVisibility(true /* animate */);
- }
- }
-
/**
* Calculates the y position of the expanded view when it is expanded.
*/
@@ -1405,37 +1385,40 @@
@VisibleForTesting
void animateInFlyoutForBubble(Bubble bubble) {
final CharSequence updateMessage = bubble.getUpdateMessage(getContext());
- if (!bubble.showFlyoutForBubble()) {
- // In case flyout was suppressed for this update, reset now.
- bubble.setSuppressFlyout(false);
- return;
- }
+ final BadgedImageView bubbleView = bubble.getIconView();
if (updateMessage == null
+ || !bubble.showFlyout()
|| isExpanded()
|| mIsExpansionAnimating
|| mIsGestureInProgress
|| mBubbleToExpandAfterFlyoutCollapse != null
- || bubble.getIconView() == null) {
+ || bubbleView == null) {
+ if (bubbleView != null) {
+ bubbleView.setDotState(DOT_STATE_DEFAULT);
+ }
// Skip the message if none exists, we're expanded or animating expansion, or we're
// about to expand a bubble from the previous tapped flyout, or if bubble view is null.
return;
}
+
mFlyoutDragDeltaX = 0f;
clearFlyoutOnHide();
- mFlyoutOnHide = () -> {
- resetDot(bubble);
- if (mBubbleToExpandAfterFlyoutCollapse == null) {
- return;
+ mAfterFlyoutHidden = () -> {
+ // Null it out to ensure it runs once.
+ mAfterFlyoutHidden = null;
+
+ if (mBubbleToExpandAfterFlyoutCollapse != null) {
+ // User tapped on the flyout and we should expand
+ mBubbleData.setSelectedBubble(mBubbleToExpandAfterFlyoutCollapse);
+ mBubbleData.setExpanded(true);
+ mBubbleToExpandAfterFlyoutCollapse = null;
}
- mBubbleData.setSelectedBubble(mBubbleToExpandAfterFlyoutCollapse);
- mBubbleData.setExpanded(true);
- mBubbleToExpandAfterFlyoutCollapse = null;
+ bubbleView.setDotState(DOT_STATE_DEFAULT);
};
mFlyout.setVisibility(INVISIBLE);
- // Temporarily suppress the dot while the flyout is visible.
- bubble.getIconView().setSuppressDot(
- true /* suppressDot */, false /* animate */);
+ // Don't show the dot when we're animating the flyout
+ bubbleView.setDotState(DOT_STATE_SUPPRESSED_FOR_FLYOUT);
// Start flyout expansion. Post in case layout isn't complete and getWidth returns 0.
post(() -> {
@@ -1461,8 +1444,9 @@
mStackAnimationController.isStackOnLeftSide(),
bubble.getIconView().getDotColor() /* dotColor */,
expandFlyoutAfterDelay /* onLayoutComplete */,
- mFlyoutOnHide,
- bubble.getIconView().getDotCenter());
+ mAfterFlyoutHidden,
+ bubble.getIconView().getDotCenter(),
+ !bubble.showDot());
mFlyout.bringToFront();
});
mFlyout.removeCallbacks(mHideFlyout);
@@ -1470,24 +1454,6 @@
logBubbleEvent(bubble, StatsLog.BUBBLE_UICHANGED__ACTION__FLYOUT);
}
- private void resetDot(Bubble bubble) {
- final boolean suppressDot = !bubble.showBubbleDot();
- // If we're going to suppress the dot, make it visible first so it'll
- // visibly animate away.
-
- if (suppressDot) {
- bubble.getIconView().setSuppressDot(
- false /* suppressDot */, false /* animate */);
- }
- // Reset dot suppression. If we're not suppressing due to DND, then
- // stop suppressing it with no animation (since the flyout has
- // transformed into the dot). If we are suppressing due to DND, animate
- // it away.
- bubble.getIconView().setSuppressDot(
- suppressDot /* suppressDot */,
- suppressDot /* animate */);
- }
-
/** Hide the flyout immediately and cancel any pending hide runnables. */
private void hideFlyoutImmediate() {
clearFlyoutOnHide();
@@ -1498,11 +1464,11 @@
private void clearFlyoutOnHide() {
mFlyout.removeCallbacks(mAnimateInFlyout);
- if (mFlyoutOnHide == null) {
+ if (mAfterFlyoutHidden == null) {
return;
}
- mFlyoutOnHide.run();
- mFlyoutOnHide = null;
+ mAfterFlyoutHidden.run();
+ mAfterFlyoutHidden = null;
}
@Override
@@ -1599,8 +1565,7 @@
private void updateBubbleZOrdersAndDotPosition(boolean animate) {
int bubbleCount = mBubbleContainer.getChildCount();
for (int i = 0; i < bubbleCount; i++) {
- BubbleView bv = (BubbleView) mBubbleContainer.getChildAt(i);
- bv.updateDotVisibility(true /* animate */);
+ BadgedImageView bv = (BadgedImageView) mBubbleContainer.getChildAt(i);
bv.setZ((mMaxBubbles * mBubbleElevation) - i);
// If the dot is on the left, and so is the stack, we need to change the dot position.
if (bv.getDotPositionOnLeft() == mStackOnLeftOrWillBe) {
@@ -1705,7 +1670,7 @@
action,
getNormalizedXPosition(),
getNormalizedYPosition(),
- bubble.showInShadeWhenBubble(),
+ bubble.showInShade(),
bubble.isOngoing(),
false /* isAppForeground (unused) */);
}
@@ -1727,8 +1692,8 @@
List<Bubble> bubbles = new ArrayList<>();
for (int i = 0; i < mBubbleContainer.getChildCount(); i++) {
View child = mBubbleContainer.getChildAt(i);
- if (child instanceof BubbleView) {
- String key = ((BubbleView) child).getKey();
+ if (child instanceof BadgedImageView) {
+ String key = ((BadgedImageView) child).getKey();
Bubble bubble = mBubbleData.getBubbleWithKey(key);
bubbles.add(bubble);
}
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java
index 4240e06..44e013a 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java
@@ -95,7 +95,7 @@
return false;
}
- if (!(mTouchedView instanceof BubbleView)
+ if (!(mTouchedView instanceof BadgedImageView)
&& !(mTouchedView instanceof BubbleStackView)
&& !(mTouchedView instanceof BubbleFlyoutView)) {
// Not touching anything touchable, but we shouldn't collapse (e.g. touching edge
@@ -187,7 +187,7 @@
mStack.onFlyoutDragFinished(rawX - mTouchDown.x /* deltaX */, velX);
} else if (shouldDismiss) {
final String individualBubbleKey =
- isStack ? null : ((BubbleView) mTouchedView).getKey();
+ isStack ? null : ((BadgedImageView) mTouchedView).getKey();
mStack.magnetToStackIfNeededThenAnimateDismissal(mTouchedView, velX, velY,
() -> {
if (isStack) {
@@ -214,7 +214,7 @@
// Toggle expansion
mBubbleData.setExpanded(!mBubbleData.isExpanded());
} else {
- final String key = ((BubbleView) mTouchedView).getKey();
+ final String key = ((BadgedImageView) mTouchedView).getKey();
mBubbleData.setSelectedBubble(mBubbleData.getBubbleWithKey(key));
}
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleView.java
deleted file mode 100644
index 79807b3..0000000
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleView.java
+++ /dev/null
@@ -1,280 +0,0 @@
-/*
- * Copyright (C) 2018 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.systemui.bubbles;
-
-import android.annotation.Nullable;
-import android.app.Notification;
-import android.content.Context;
-import android.content.pm.LauncherApps;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Matrix;
-import android.graphics.Path;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.Icon;
-import android.util.AttributeSet;
-import android.util.PathParser;
-import android.widget.FrameLayout;
-
-import com.android.internal.graphics.ColorUtils;
-import com.android.launcher3.icons.BitmapInfo;
-import com.android.launcher3.icons.ColorExtractor;
-import com.android.launcher3.icons.ShadowGenerator;
-import com.android.systemui.Interpolators;
-import com.android.systemui.R;
-import com.android.systemui.statusbar.notification.collection.NotificationEntry;
-
-/**
- * A floating object on the screen that can post message updates.
- */
-public class BubbleView extends FrameLayout {
-
- // Same value as Launcher3 badge code
- private static final float WHITE_SCRIM_ALPHA = 0.54f;
- private Context mContext;
-
- private BadgedImageView mBadgedImageView;
- private int mDotColor;
- private ColorExtractor mColorExtractor;
-
- // mBubbleIconFactory cannot be static because it depends on Context.
- private BubbleIconFactory mBubbleIconFactory;
-
- private boolean mSuppressDot;
-
- private Bubble mBubble;
-
- public BubbleView(Context context) {
- this(context, null);
- }
-
- public BubbleView(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public BubbleView(Context context, AttributeSet attrs, int defStyleAttr) {
- this(context, attrs, defStyleAttr, 0);
- }
-
- public BubbleView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
- super(context, attrs, defStyleAttr, defStyleRes);
- mContext = context;
- }
-
- @Override
- protected void onFinishInflate() {
- super.onFinishInflate();
- mBadgedImageView = findViewById(R.id.bubble_image);
- mColorExtractor = new ColorExtractor();
- }
-
- @Override
- protected void onAttachedToWindow() {
- super.onAttachedToWindow();
- }
-
- /**
- * Populates this view with a bubble.
- * <p>
- * This should only be called when a new bubble is being set on the view, updates to the
- * current bubble should use {@link #update(Bubble)}.
- *
- * @param bubble the bubble to display in this view.
- */
- public void setBubble(Bubble bubble) {
- mBubble = bubble;
- }
-
- /**
- * @param factory Factory for creating normalized bubble icons.
- */
- public void setBubbleIconFactory(BubbleIconFactory factory) {
- mBubbleIconFactory = factory;
- }
-
- /**
- * The {@link NotificationEntry} associated with this view, if one exists.
- */
- @Nullable
- public NotificationEntry getEntry() {
- return mBubble != null ? mBubble.getEntry() : null;
- }
-
- /**
- * The key for the {@link NotificationEntry} associated with this view, if one exists.
- */
- @Nullable
- public String getKey() {
- return (mBubble != null) ? mBubble.getKey() : null;
- }
-
- /**
- * Updates the UI based on the bubble, updates badge and animates messages as needed.
- */
- public void update(Bubble bubble) {
- mBubble = bubble;
- updateViews();
- }
-
- /** Changes the dot's visibility to match the bubble view's state. */
- void updateDotVisibility(boolean animate) {
- updateDotVisibility(animate, null /* after */);
- }
-
- /**
- * Sets whether or not to hide the dot even if we'd otherwise show it. This is used while the
- * flyout is visible or animating, to hide the dot until the flyout visually transforms into it.
- */
- void setSuppressDot(boolean suppressDot, boolean animate) {
- mSuppressDot = suppressDot;
- updateDotVisibility(animate);
- }
-
- boolean isDotShowing() {
- return mBubble.showBubbleDot() && !mSuppressDot;
- }
-
- int getDotColor() {
- return mDotColor;
- }
-
- /** Sets the position of the 'new' dot, animating it out and back in if requested. */
- void setDotPosition(boolean onLeft, boolean animate) {
- if (animate && onLeft != mBadgedImageView.getDotOnLeft() && isDotShowing()) {
- animateDot(false /* showDot */, () -> {
- mBadgedImageView.setDotOnLeft(onLeft);
- animateDot(true /* showDot */, null);
- });
- } else {
- mBadgedImageView.setDotOnLeft(onLeft);
- }
- }
-
- float[] getDotCenter() {
- float[] unscaled = mBadgedImageView.getDotCenter();
- return new float[]{unscaled[0], unscaled[1]};
- }
-
- boolean getDotPositionOnLeft() {
- return mBadgedImageView.getDotOnLeft();
- }
-
- /**
- * Changes the dot's visibility to match the bubble view's state, running the provided callback
- * after animation if requested.
- */
- private void updateDotVisibility(boolean animate, Runnable after) {
- final boolean showDot = isDotShowing();
- if (animate) {
- animateDot(showDot, after);
- } else {
- mBadgedImageView.setShowDot(showDot);
- mBadgedImageView.setDotScale(showDot ? 1f : 0f);
- }
- }
-
- /**
- * Animates the badge to show or hide.
- */
- private void animateDot(boolean showDot, Runnable after) {
- if (mBadgedImageView.isShowingDot() == showDot) {
- return;
- }
- // Do NOT wait until after animation ends to setShowDot
- // to avoid overriding more recent showDot states.
- mBadgedImageView.setShowDot(showDot);
- mBadgedImageView.clearAnimation();
- mBadgedImageView.animate().setDuration(200)
- .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
- .setUpdateListener((valueAnimator) -> {
- float fraction = valueAnimator.getAnimatedFraction();
- fraction = showDot ? fraction : 1f - fraction;
- mBadgedImageView.setDotScale(fraction);
- }).withEndAction(() -> {
- mBadgedImageView.setDotScale(showDot ? 1f : 0f);
- if (after != null) {
- after.run();
- }
- }).start();
- }
-
- void updateViews() {
- if (mBubble == null || mBubbleIconFactory == null) {
- return;
- }
-
- Drawable bubbleDrawable = getBubbleDrawable(mContext);
- BitmapInfo badgeBitmapInfo = getBadgedBitmap();
- BitmapInfo bubbleBitmapInfo = getBubbleBitmap(bubbleDrawable, badgeBitmapInfo);
- mBadgedImageView.setImageBitmap(bubbleBitmapInfo.icon);
-
- // Update badge.
- mDotColor = ColorUtils.blendARGB(badgeBitmapInfo.color, Color.WHITE, WHITE_SCRIM_ALPHA);
- mBadgedImageView.setDotColor(mDotColor);
-
- // Update dot.
- Path iconPath = PathParser.createPathFromPathData(
- getResources().getString(com.android.internal.R.string.config_icon_mask));
- Matrix matrix = new Matrix();
- float scale = mBubbleIconFactory.getNormalizer().getScale(bubbleDrawable,
- null /* outBounds */, null /* path */, null /* outMaskShape */);
- float radius = BadgedImageView.DEFAULT_PATH_SIZE / 2f;
- matrix.setScale(scale /* x scale */, scale /* y scale */, radius /* pivot x */,
- radius /* pivot y */);
- iconPath.transform(matrix);
- mBadgedImageView.drawDot(iconPath);
-
- animateDot(isDotShowing(), null /* after */);
- }
-
- Drawable getBubbleDrawable(Context context) {
- if (mBubble.getShortcutInfo() != null && mBubble.usingShortcutInfo()) {
- LauncherApps launcherApps =
- (LauncherApps) getContext().getSystemService(Context.LAUNCHER_APPS_SERVICE);
- int density = getContext().getResources().getConfiguration().densityDpi;
- return launcherApps.getShortcutIconDrawable(mBubble.getShortcutInfo(), density);
- } else {
- Notification.BubbleMetadata metadata = getEntry().getBubbleMetadata();
- Icon ic = metadata.getIcon();
- return ic.loadDrawable(context);
- }
- }
-
- BitmapInfo getBadgedBitmap() {
- Bitmap userBadgedBitmap = mBubbleIconFactory.createIconBitmap(
- mBubble.getUserBadgedAppIcon(), 1f, mBubbleIconFactory.getBadgeSize());
-
- Canvas c = new Canvas();
- ShadowGenerator shadowGenerator = new ShadowGenerator(mBubbleIconFactory.getBadgeSize());
- c.setBitmap(userBadgedBitmap);
- shadowGenerator.recreateIcon(Bitmap.createBitmap(userBadgedBitmap), c);
- BitmapInfo bitmapInfo = mBubbleIconFactory.createIconBitmap(userBadgedBitmap);
- return bitmapInfo;
- }
-
- BitmapInfo getBubbleBitmap(Drawable bubble, BitmapInfo badge) {
- BitmapInfo bubbleIconInfo = mBubbleIconFactory.createBadgedIconBitmap(bubble,
- null /* user */,
- true /* shrinkNonAdaptiveIcons */);
-
- mBubbleIconFactory.badgeWithDrawable(bubbleIconInfo.icon,
- new BitmapDrawable(mContext.getResources(), badge.icon));
- return bubbleIconInfo;
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java b/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java
index fedd855..5041354 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java
@@ -804,8 +804,8 @@
List<Notification.Action> actions = smartActionsFuture.get(timeoutMs,
TimeUnit.MILLISECONDS);
long waitTimeMs = SystemClock.uptimeMillis() - startTimeMs;
- Slog.d(TAG, String.format("Wait time for smart actions: %d ms",
- waitTimeMs));
+ Slog.d(TAG, String.format("Got %d smart actions. Wait time: %d ms",
+ actions.size(), waitTimeMs));
notifyScreenshotOp(screenshotId, smartActionsProvider,
ScreenshotNotificationSmartActionsProvider.ScreenshotOp.WAIT_FOR_SMART_ACTIONS,
ScreenshotNotificationSmartActionsProvider.ScreenshotOpStatus.SUCCESS,
@@ -813,7 +813,8 @@
return actions;
} catch (Throwable e) {
long waitTimeMs = SystemClock.uptimeMillis() - startTimeMs;
- Slog.d(TAG, "Failed to obtain screenshot notification smart actions.", e);
+ Slog.e(TAG, String.format("Error getting smart actions. Wait time: %d ms", waitTimeMs),
+ e);
ScreenshotNotificationSmartActionsProvider.ScreenshotOpStatus status =
(e instanceof TimeoutException)
? ScreenshotNotificationSmartActionsProvider.ScreenshotOpStatus.TIMEOUT
@@ -942,14 +943,16 @@
public static class SmartActionsReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
- PendingIntent actionIntent = intent.getParcelableExtra(EXTRA_ACTION_INTENT);
+ PendingIntent pendingIntent = intent.getParcelableExtra(EXTRA_ACTION_INTENT);
+ Intent actionIntent = pendingIntent.getIntent();
+ String actionType = intent.getStringExtra(EXTRA_ACTION_TYPE);
+ Slog.d(TAG, "Executing smart action [" + actionType + "]:" + actionIntent);
ActivityOptions opts = ActivityOptions.makeBasic();
- context.startActivityAsUser(actionIntent.getIntent(), opts.toBundle(),
+ context.startActivityAsUser(actionIntent, opts.toBundle(),
UserHandle.CURRENT);
- Slog.d(TAG, "Screenshot notification smart action is invoked.");
notifyScreenshotAction(context, intent.getStringExtra(EXTRA_ID),
- intent.getStringExtra(EXTRA_ACTION_TYPE),
+ actionType,
true);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
index d2268e1..76925b4 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
@@ -456,9 +456,9 @@
SystemUiDeviceConfigFlags
.SCREENSHOT_NOTIFICATION_SMART_ACTIONS_TIMEOUT_MS,
1000);
- List<Notification.Action> smartActions = buildSmartActions(
- GlobalScreenshot.getSmartActions(mScreenshotId, smartActionsFuture,
- timeoutMs, mSmartActionsProvider), context);
+ List<Notification.Action> smartActions = GlobalScreenshot.getSmartActions(mScreenshotId,
+ smartActionsFuture, timeoutMs, mSmartActionsProvider);
+ smartActions = buildSmartActions(smartActions, context);
for (Notification.Action action : smartActions) {
notificationBuilder.addAction(action);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
index 5c71a57..5575d10 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
@@ -343,6 +343,7 @@
private BiometricUnlockController mBiometricUnlockController;
private final LightBarController mLightBarController;
private final Lazy<LockscreenWallpaper> mLockscreenWallpaperLazy;
+ @Nullable
protected LockscreenWallpaper mLockscreenWallpaper;
private final AutoHideController mAutoHideController;
@Nullable
@@ -1059,7 +1060,7 @@
createNavigationBar(result);
- if (ENABLE_LOCKSCREEN_WALLPAPER) {
+ if (ENABLE_LOCKSCREEN_WALLPAPER && mWallpaperSupported) {
mLockscreenWallpaper = mLockscreenWallpaperLazy.get();
mLockscreenWallpaper.setHandler(mHandler);
}
@@ -2807,7 +2808,9 @@
@Override
public void setLockscreenUser(int newUserId) {
- mLockscreenWallpaper.setCurrentUser(newUserId);
+ if (mLockscreenWallpaper != null) {
+ mLockscreenWallpaper.setCurrentUser(newUserId);
+ }
mScrimController.setCurrentUser(newUserId);
if (mWallpaperSupported) {
mWallpaperChangedReceiver.onReceive(mContext, null);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java
index 8c9f759..4c707f4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java
@@ -157,9 +157,14 @@
private SuperStatusBarViewFactory mSuperStatusBarViewFactory;
private BubbleData mBubbleData;
+ private TestableLooper mTestableLooper;
+
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
+
+ mTestableLooper = TestableLooper.get(this);
+
mContext.addMockSystemService(FaceManager.class, mFaceManager);
when(mColorExtractor.getNeutralColors()).thenReturn(mGradientColors);
@@ -262,7 +267,7 @@
mRow.getEntry().getKey()));
// Make it look like dismissed notif
- mBubbleData.getBubbleWithKey(mRow.getEntry().getKey()).setShowInShadeWhenBubble(false);
+ mBubbleData.getBubbleWithKey(mRow.getEntry().getKey()).setShowInShade(false);
// Now remove the bubble
mBubbleController.removeBubble(
@@ -346,14 +351,14 @@
verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow2.getEntry().getKey());
// Last added is the one that is expanded
- assertEquals(mRow2.getEntry(), stackView.getExpandedBubbleView().getEntry());
+ assertEquals(mRow2.getEntry(), mBubbleData.getSelectedBubble().getEntry());
assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade(
mRow2.getEntry().getKey()));
// Switch which bubble is expanded
mBubbleController.selectBubble(mRow.getEntry().getKey());
stackView.setExpandedBubble(mRow.getEntry().getKey());
- assertEquals(mRow.getEntry(), stackView.getExpandedBubbleView().getEntry());
+ assertEquals(mRow.getEntry(), stackView.getExpandedBubble().getEntry());
assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade(
mRow.getEntry().getKey()));
@@ -377,7 +382,9 @@
assertTrue(mBubbleController.hasBubbles());
assertFalse(mBubbleController.isBubbleNotificationSuppressedFromShade(
mRow.getEntry().getKey()));
- assertTrue(mBubbleData.getBubbleWithKey(mRow.getEntry().getKey()).showBubbleDot());
+
+ mTestableLooper.processAllMessages();
+ assertTrue(mBubbleData.getBubbleWithKey(mRow.getEntry().getKey()).showDot());
// Expand
mBubbleController.expandStack();
@@ -388,7 +395,7 @@
assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade(
mRow.getEntry().getKey()));
// Notif shouldn't show dot after expansion
- assertFalse(mBubbleData.getBubbleWithKey(mRow.getEntry().getKey()).showBubbleDot());
+ assertFalse(mBubbleData.getBubbleWithKey(mRow.getEntry().getKey()).showDot());
}
@Test
@@ -401,10 +408,11 @@
assertTrue(mBubbleController.hasBubbles());
assertFalse(mBubbleController.isBubbleNotificationSuppressedFromShade(
mRow.getEntry().getKey()));
- assertTrue(mBubbleData.getBubbleWithKey(mRow.getEntry().getKey()).showBubbleDot());
+
+ mTestableLooper.processAllMessages();
+ assertTrue(mBubbleData.getBubbleWithKey(mRow.getEntry().getKey()).showDot());
// Expand
- BubbleStackView stackView = mBubbleController.getStackView();
mBubbleController.expandStack();
assertTrue(mBubbleController.isStackExpanded());
verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getEntry().getKey());
@@ -413,7 +421,7 @@
assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade(
mRow.getEntry().getKey()));
// Notif shouldn't show dot after expansion
- assertFalse(mBubbleData.getBubbleWithKey(mRow.getEntry().getKey()).showBubbleDot());
+ assertFalse(mBubbleData.getBubbleWithKey(mRow.getEntry().getKey()).showDot());
// Send update
mEntryListener.onPreEntryUpdated(mRow.getEntry());
@@ -423,7 +431,7 @@
assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade(
mRow.getEntry().getKey()));
// Notif shouldn't show dot after expansion
- assertFalse(mBubbleData.getBubbleWithKey(mRow.getEntry().getKey()).showBubbleDot());
+ assertFalse(mBubbleData.getBubbleWithKey(mRow.getEntry().getKey()).showDot());
}
@Test
@@ -443,7 +451,7 @@
verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow2.getEntry().getKey());
// Last added is the one that is expanded
- assertEquals(mRow2.getEntry(), stackView.getExpandedBubbleView().getEntry());
+ assertEquals(mRow2.getEntry(), stackView.getExpandedBubble().getEntry());
assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade(
mRow2.getEntry().getKey()));
@@ -453,7 +461,7 @@
verify(mBubbleExpandListener).onBubbleExpandChanged(false, mRow2.getEntry().getKey());
// Make sure first bubble is selected
- assertEquals(mRow.getEntry(), stackView.getExpandedBubbleView().getEntry());
+ assertEquals(mRow.getEntry(), stackView.getExpandedBubble().getEntry());
verify(mBubbleExpandListener).onBubbleExpandChanged(true, mRow.getEntry().getKey());
// Dismiss that one
@@ -555,7 +563,9 @@
mEntryListener.onPendingEntryAdded(mRow.getEntry());
assertFalse(mBubbleController.isBubbleNotificationSuppressedFromShade(
mRow.getEntry().getKey()));
- assertTrue(mBubbleData.getBubbleWithKey(mRow.getEntry().getKey()).showBubbleDot());
+
+ mTestableLooper.processAllMessages();
+ assertTrue(mBubbleData.getBubbleWithKey(mRow.getEntry().getKey()).showDot());
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java
index a9be30b..95c7af3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleDataTest.java
@@ -183,7 +183,7 @@
// Verify
verifyUpdateReceived();
BubbleData.Update update = mUpdateCaptor.getValue();
- assertThat(update.addedBubble.showFlyoutForBubble()).isFalse();
+ assertThat(update.addedBubble.showFlyout()).isFalse();
}
@Test
@@ -199,7 +199,7 @@
// Verify
verifyUpdateReceived();
BubbleData.Update update = mUpdateCaptor.getValue();
- assertThat(update.addedBubble.showFlyoutForBubble()).isTrue();
+ assertThat(update.addedBubble.showFlyout()).isTrue();
}
@Test
@@ -218,7 +218,7 @@
// Verify
BubbleData.Update update = mUpdateCaptor.getValue();
- assertThat(update.updatedBubble.showFlyoutForBubble()).isFalse();
+ assertThat(update.updatedBubble.showFlyout()).isFalse();
}
@Test
@@ -239,7 +239,7 @@
// Verify
BubbleData.Update update = mUpdateCaptor.getValue();
- assertThat(update.updatedBubble.showFlyoutForBubble()).isTrue();
+ assertThat(update.updatedBubble.showFlyout()).isTrue();
}
// COLLAPSED / ADD
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleFlyoutViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleFlyoutViewTest.java
index a8961a8..376ecf7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleFlyoutViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleFlyoutViewTest.java
@@ -60,7 +60,8 @@
@Test
public void testShowFlyout_isVisible() {
mFlyout.setupFlyoutStartingAsDot(
- "Hello", new PointF(100, 100), 500, true, Color.WHITE, null, null, mDotCenter);
+ "Hello", new PointF(100, 100), 500, true, Color.WHITE, null, null, mDotCenter,
+ false);
mFlyout.setVisibility(View.VISIBLE);
assertEquals("Hello", mFlyoutText.getText());
@@ -71,7 +72,8 @@
public void testFlyoutHide_runsCallback() {
Runnable after = Mockito.mock(Runnable.class);
mFlyout.setupFlyoutStartingAsDot(
- "Hello", new PointF(100, 100), 500, true, Color.WHITE, null, after, mDotCenter);
+ "Hello", new PointF(100, 100), 500, true, Color.WHITE, null, after, mDotCenter,
+ false);
mFlyout.hideFlyout();
verify(after).run();
@@ -80,7 +82,8 @@
@Test
public void testSetCollapsePercent() {
mFlyout.setupFlyoutStartingAsDot(
- "Hello", new PointF(100, 100), 500, true, Color.WHITE, null, null, mDotCenter);
+ "Hello", new PointF(100, 100), 500, true, Color.WHITE, null, null, mDotCenter,
+ false);
mFlyout.setVisibility(View.VISIBLE);
mFlyout.setCollapsePercent(1f);
diff --git a/services/core/java/com/android/server/PinnerService.java b/services/core/java/com/android/server/PinnerService.java
index d5f7956..135f6f3 100644
--- a/services/core/java/com/android/server/PinnerService.java
+++ b/services/core/java/com/android/server/PinnerService.java
@@ -45,6 +45,7 @@
import android.os.SystemProperties;
import android.os.UserHandle;
import android.os.UserManager;
+import android.provider.DeviceConfig;
import android.provider.MediaStore;
import android.provider.Settings;
import android.system.ErrnoException;
@@ -97,9 +98,12 @@
private static final int KEY_HOME = 1;
private static final int KEY_ASSISTANT = 2;
- // Pin the camera application.
- private static boolean PROP_PIN_CAMERA = SystemProperties.getBoolean(
- "pinner.pin_camera", true);
+ // Pin the camera application. Default to the system property only if the experiment phenotype
+ // property is not set.
+ private static boolean PROP_PIN_CAMERA =
+ DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_RUNTIME_NATIVE_BOOT,
+ "pin_camera",
+ SystemProperties.getBoolean("pinner.pin_camera", true));
// Pin using pinlist.meta when pinning apps.
private static boolean PROP_PIN_PINLIST = SystemProperties.getBoolean(
"pinner.use_pinlist", true);
diff --git a/services/core/java/com/android/server/integrity/model/BitInputStream.java b/services/core/java/com/android/server/integrity/model/BitInputStream.java
new file mode 100644
index 0000000..09bc7e8
--- /dev/null
+++ b/services/core/java/com/android/server/integrity/model/BitInputStream.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2019 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.integrity.model;
+
+/** A wrapper class for reading a stream of bits. */
+public class BitInputStream {
+
+ private byte[] mRuleBytes;
+ private long mBitPointer;
+
+ public BitInputStream(byte[] ruleBytes) {
+ this.mRuleBytes = ruleBytes;
+ this.mBitPointer = 0;
+ }
+
+ /**
+ * Read the next number of bits from the stream.
+ *
+ * @param numOfBits The number of bits to read.
+ * @return The value read from the stream.
+ */
+ public int getNext(int numOfBits) {
+ int component = 0;
+ int count = 0;
+
+ int idx = (int) (mBitPointer / 8);
+ int offset = 7 - (int) (mBitPointer % 8);
+
+ while (count++ < numOfBits) {
+ if (idx >= mRuleBytes.length) {
+ throw new IllegalArgumentException(String.format("Invalid byte index: %d", idx));
+ }
+
+ component <<= 1;
+ component |= (mRuleBytes[idx] >>> offset) & 1;
+
+ offset--;
+ if (offset == -1) {
+ idx++;
+ offset = 7;
+ }
+ }
+
+ mBitPointer += numOfBits;
+ return component;
+ }
+
+ /** Check if there are bits left in the stream. */
+ public boolean hasNext() {
+ return mBitPointer / 8 < mRuleBytes.length;
+ }
+}
diff --git a/services/core/java/com/android/server/integrity/model/BitOutputStream.java b/services/core/java/com/android/server/integrity/model/BitOutputStream.java
new file mode 100644
index 0000000..ecb9189
--- /dev/null
+++ b/services/core/java/com/android/server/integrity/model/BitOutputStream.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2019 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.integrity.model;
+
+import java.util.BitSet;
+
+/** A wrapper class for writing a stream of bits. */
+public class BitOutputStream {
+
+ private BitSet mBitSet;
+ private int mIndex;
+
+ public BitOutputStream() {
+ mBitSet = new BitSet();
+ mIndex = 0;
+ }
+
+ /**
+ * Set the next number of bits in the stream to value.
+ *
+ * @param numOfBits The number of bits used to represent the value.
+ * @param value The value to convert to bits.
+ */
+ public void setNext(int numOfBits, int value) {
+ if (numOfBits <= 0) {
+ return;
+ }
+ int offset = 1 << (numOfBits - 1);
+ while (numOfBits-- > 0) {
+ mBitSet.set(mIndex, (value & offset) != 0);
+ offset >>= 1;
+ mIndex++;
+ }
+ }
+
+ /**
+ * Set the next bit in the stream to value.
+ *
+ * @param value The value to set the bit to.
+ */
+ public void setNext(boolean value) {
+ mBitSet.set(mIndex, value);
+ mIndex++;
+ }
+
+ /** Set the next bit in the stream to true. */
+ public void setNext() {
+ setNext(/* value= */ true);
+ }
+
+ /** Convert BitSet in big-endian to ByteArray in big-endian. */
+ public byte[] toByteArray() {
+ int bitSetSize = mBitSet.length();
+ int numOfBytes = bitSetSize / 8;
+ if (bitSetSize % 8 != 0) {
+ numOfBytes++;
+ }
+ byte[] bytes = new byte[numOfBytes];
+ for (int i = 0; i < mBitSet.length(); i++) {
+ if (mBitSet.get(i)) {
+ bytes[i / 8] |= 1 << (7 - (i % 8));
+ }
+ }
+ return bytes;
+ }
+
+ /** Clear the stream. */
+ public void clear() {
+ mBitSet.clear();
+ mIndex = 0;
+ }
+}
diff --git a/services/core/java/com/android/server/integrity/model/ComponentBitSize.java b/services/core/java/com/android/server/integrity/model/ComponentBitSize.java
new file mode 100644
index 0000000..d47ce2d
--- /dev/null
+++ b/services/core/java/com/android/server/integrity/model/ComponentBitSize.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2019 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.integrity.model;
+
+import android.content.integrity.Rule;
+
+/**
+ * A helper class containing information about the binary representation of different {@link Rule}
+ * components.
+ */
+public final class ComponentBitSize {
+ public static final int FORMAT_VERSION_BITS = 5;
+ public static final int EFFECT_BITS = 3;
+ public static final int KEY_BITS = 4;
+ public static final int OPERATOR_BITS = 3;
+ public static final int CONNECTOR_BITS = 2;
+ public static final int SEPARATOR_BITS = 2;
+ public static final int VALUE_SIZE_BITS = 5;
+ public static final int IS_HASHED_BITS = 1;
+
+ public static final int ATOMIC_FORMULA_START = 0;
+ public static final int COMPOUND_FORMULA_START = 1;
+ public static final int COMPOUND_FORMULA_END = 2;
+
+ public static final int DEFAULT_FORMAT_VERSION = 1;
+ public static final int SIGNAL_BIT = 1;
+}
diff --git a/services/core/java/com/android/server/integrity/parser/RuleBinaryParser.java b/services/core/java/com/android/server/integrity/parser/RuleBinaryParser.java
index aad177e..8aa0751 100644
--- a/services/core/java/com/android/server/integrity/parser/RuleBinaryParser.java
+++ b/services/core/java/com/android/server/integrity/parser/RuleBinaryParser.java
@@ -16,23 +16,132 @@
package com.android.server.integrity.parser;
+import static com.android.server.integrity.model.ComponentBitSize.ATOMIC_FORMULA_START;
+import static com.android.server.integrity.model.ComponentBitSize.COMPOUND_FORMULA_END;
+import static com.android.server.integrity.model.ComponentBitSize.COMPOUND_FORMULA_START;
+import static com.android.server.integrity.model.ComponentBitSize.CONNECTOR_BITS;
+import static com.android.server.integrity.model.ComponentBitSize.EFFECT_BITS;
+import static com.android.server.integrity.model.ComponentBitSize.FORMAT_VERSION_BITS;
+import static com.android.server.integrity.model.ComponentBitSize.IS_HASHED_BITS;
+import static com.android.server.integrity.model.ComponentBitSize.KEY_BITS;
+import static com.android.server.integrity.model.ComponentBitSize.OPERATOR_BITS;
+import static com.android.server.integrity.model.ComponentBitSize.SEPARATOR_BITS;
+import static com.android.server.integrity.model.ComponentBitSize.SIGNAL_BIT;
+import static com.android.server.integrity.model.ComponentBitSize.VALUE_SIZE_BITS;
+
+import android.content.integrity.AtomicFormula;
+import android.content.integrity.CompoundFormula;
+import android.content.integrity.Formula;
import android.content.integrity.Rule;
+import com.android.server.integrity.model.BitInputStream;
+
import java.io.InputStream;
+import java.util.ArrayList;
import java.util.List;
/** A helper class to parse rules into the {@link Rule} model from Binary representation. */
public class RuleBinaryParser implements RuleParser {
@Override
- public List<Rule> parse(byte[] ruleBytes) {
- // TODO: Implement binary text parser.
- return null;
+ public List<Rule> parse(byte[] ruleBytes) throws RuleParseException {
+ try {
+ BitInputStream bitInputStream = new BitInputStream(ruleBytes);
+ return parseRules(bitInputStream);
+ } catch (Exception e) {
+ throw new RuleParseException(e.getMessage(), e);
+ }
}
@Override
- public List<Rule> parse(InputStream inputStream) {
- // TODO: Implement stream parser.
- return null;
+ public List<Rule> parse(InputStream inputStream) throws RuleParseException {
+ try {
+ byte[] ruleBytes = new byte[inputStream.available()];
+ inputStream.read(ruleBytes);
+ return parse(ruleBytes);
+ } catch (Exception e) {
+ throw new RuleParseException(e.getMessage(), e);
+ }
+ }
+
+ private List<Rule> parseRules(BitInputStream bitInputStream) {
+ List<Rule> parsedRules = new ArrayList<>();
+
+ // Read the rule binary file format version.
+ bitInputStream.getNext(FORMAT_VERSION_BITS);
+
+ while (bitInputStream.hasNext()) {
+ if (bitInputStream.getNext(SIGNAL_BIT) == 1) {
+ parsedRules.add(parseRule(bitInputStream));
+ }
+ }
+
+ return parsedRules;
+ }
+
+ private Rule parseRule(BitInputStream bitInputStream) {
+ Formula formula = parseFormula(bitInputStream);
+ int effect = bitInputStream.getNext(EFFECT_BITS);
+
+ if (bitInputStream.getNext(SIGNAL_BIT) != 1) {
+ throw new IllegalArgumentException("A rule must end with a '1' bit.");
+ }
+
+ return new Rule(formula, effect);
+ }
+
+ private Formula parseFormula(BitInputStream bitInputStream) {
+ int separator = bitInputStream.getNext(SEPARATOR_BITS);
+ switch (separator) {
+ case ATOMIC_FORMULA_START:
+ return parseAtomicFormula(bitInputStream);
+ case COMPOUND_FORMULA_START:
+ return parseCompoundFormula(bitInputStream);
+ case COMPOUND_FORMULA_END:
+ return null;
+ default:
+ throw new IllegalArgumentException(
+ String.format("Unknown formula separator: %s", separator));
+ }
+ }
+
+ private CompoundFormula parseCompoundFormula(BitInputStream bitInputStream) {
+ int connector = bitInputStream.getNext(CONNECTOR_BITS);
+ List<Formula> formulas = new ArrayList<>();
+
+ Formula parsedFormula = parseFormula(bitInputStream);
+ while (parsedFormula != null) {
+ formulas.add(parsedFormula);
+ parsedFormula = parseFormula(bitInputStream);
+ }
+
+ return new CompoundFormula(connector, formulas);
+ }
+
+ private AtomicFormula parseAtomicFormula(BitInputStream bitInputStream) {
+ int key = bitInputStream.getNext(KEY_BITS);
+ int operator = bitInputStream.getNext(OPERATOR_BITS);
+
+ boolean isHashedValue = bitInputStream.getNext(IS_HASHED_BITS) == 1;
+ int valueSize = bitInputStream.getNext(VALUE_SIZE_BITS);
+ StringBuilder value = new StringBuilder();
+ while (valueSize-- > 0) {
+ value.append((char) bitInputStream.getNext(/* numOfBits= */ 8));
+ }
+
+ switch (key) {
+ case AtomicFormula.PACKAGE_NAME:
+ case AtomicFormula.APP_CERTIFICATE:
+ case AtomicFormula.INSTALLER_NAME:
+ case AtomicFormula.INSTALLER_CERTIFICATE:
+ return new AtomicFormula.StringAtomicFormula(key, value.toString(), isHashedValue);
+ case AtomicFormula.VERSION_CODE:
+ return new AtomicFormula.IntAtomicFormula(
+ key, operator, Integer.parseInt(value.toString()));
+ case AtomicFormula.PRE_INSTALLED:
+ return new AtomicFormula.BooleanAtomicFormula(key, value.toString().equals("1"));
+ default:
+ throw new IllegalArgumentException(String.format("Unknown key: %d", key));
+ }
}
}
diff --git a/services/core/java/com/android/server/integrity/parser/RuleXmlParser.java b/services/core/java/com/android/server/integrity/parser/RuleXmlParser.java
index 2e99d0f..d405583 100644
--- a/services/core/java/com/android/server/integrity/parser/RuleXmlParser.java
+++ b/services/core/java/com/android/server/integrity/parser/RuleXmlParser.java
@@ -41,7 +41,7 @@
private static final String NAMESPACE = "";
private static final String RULE_LIST_TAG = "RL";
private static final String RULE_TAG = "R";
- private static final String OPEN_FORMULA_TAG = "OF";
+ private static final String COMPOUND_FORMULA_TAG = "OF";
private static final String ATOMIC_FORMULA_TAG = "AF";
private static final String EFFECT_ATTRIBUTE = "E";
private static final String KEY_ATTRIBUTE = "K";
@@ -118,8 +118,8 @@
if (eventType == XmlPullParser.START_TAG) {
switch (nodeName) {
- case OPEN_FORMULA_TAG:
- formula = parseOpenFormula(parser);
+ case COMPOUND_FORMULA_TAG:
+ formula = parseCompoundFormula(parser);
break;
case ATOMIC_FORMULA_TAG:
formula = parseAtomicFormula(parser);
@@ -137,7 +137,7 @@
return new Rule(formula, effect);
}
- private static Formula parseOpenFormula(XmlPullParser parser)
+ private static Formula parseCompoundFormula(XmlPullParser parser)
throws IOException, XmlPullParserException {
int connector =
Integer.parseInt(extractAttributeValue(parser, CONNECTOR_ATTRIBUTE).orElse("-1"));
@@ -147,7 +147,8 @@
while ((eventType = parser.next()) != XmlPullParser.END_DOCUMENT) {
String nodeName = parser.getName();
- if (eventType == XmlPullParser.END_TAG && parser.getName().equals(OPEN_FORMULA_TAG)) {
+ if (eventType == XmlPullParser.END_TAG
+ && parser.getName().equals(COMPOUND_FORMULA_TAG)) {
break;
}
@@ -156,8 +157,8 @@
case ATOMIC_FORMULA_TAG:
formulas.add(parseAtomicFormula(parser));
break;
- case OPEN_FORMULA_TAG:
- formulas.add(parseOpenFormula(parser));
+ case COMPOUND_FORMULA_TAG:
+ formulas.add(parseCompoundFormula(parser));
break;
default:
throw new RuntimeException(
diff --git a/services/core/java/com/android/server/integrity/serializer/RuleBinarySerializer.java b/services/core/java/com/android/server/integrity/serializer/RuleBinarySerializer.java
index d4f41eb..b988fd4 100644
--- a/services/core/java/com/android/server/integrity/serializer/RuleBinarySerializer.java
+++ b/services/core/java/com/android/server/integrity/serializer/RuleBinarySerializer.java
@@ -16,24 +16,155 @@
package com.android.server.integrity.serializer;
+import static com.android.server.integrity.model.ComponentBitSize.ATOMIC_FORMULA_START;
+import static com.android.server.integrity.model.ComponentBitSize.COMPOUND_FORMULA_END;
+import static com.android.server.integrity.model.ComponentBitSize.COMPOUND_FORMULA_START;
+import static com.android.server.integrity.model.ComponentBitSize.CONNECTOR_BITS;
+import static com.android.server.integrity.model.ComponentBitSize.DEFAULT_FORMAT_VERSION;
+import static com.android.server.integrity.model.ComponentBitSize.EFFECT_BITS;
+import static com.android.server.integrity.model.ComponentBitSize.FORMAT_VERSION_BITS;
+import static com.android.server.integrity.model.ComponentBitSize.KEY_BITS;
+import static com.android.server.integrity.model.ComponentBitSize.OPERATOR_BITS;
+import static com.android.server.integrity.model.ComponentBitSize.SEPARATOR_BITS;
+import static com.android.server.integrity.model.ComponentBitSize.VALUE_SIZE_BITS;
+
+import android.content.integrity.AtomicFormula;
+import android.content.integrity.CompoundFormula;
+import android.content.integrity.Formula;
import android.content.integrity.Rule;
+import com.android.server.integrity.model.BitOutputStream;
+
+import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Optional;
-/** A helper class to serialize rules from the {@link Rule} model to Xml representation. */
+/** A helper class to serialize rules from the {@link Rule} model to Binary representation. */
public class RuleBinarySerializer implements RuleSerializer {
+ // Get the byte representation for a list of rules, and write them to an output stream.
@Override
public void serialize(
- List<Rule> rules, Optional<Integer> formatVersion, OutputStream outputStream) {
- // TODO: Implement stream serializer.
+ List<Rule> rules, Optional<Integer> formatVersion, OutputStream outputStream)
+ throws RuleSerializeException {
+ try {
+ BitOutputStream bitOutputStream = new BitOutputStream();
+
+ int formatVersionValue = formatVersion.orElse(DEFAULT_FORMAT_VERSION);
+ bitOutputStream.setNext(FORMAT_VERSION_BITS, formatVersionValue);
+ outputStream.write(bitOutputStream.toByteArray());
+
+ for (Rule rule : rules) {
+ bitOutputStream.clear();
+ serializeRule(rule, bitOutputStream);
+ outputStream.write(bitOutputStream.toByteArray());
+ }
+ } catch (Exception e) {
+ throw new RuleSerializeException(e.getMessage(), e);
+ }
}
+ // Get the byte representation for a list of rules.
@Override
- public byte[] serialize(List<Rule> rules, Optional<Integer> formatVersion) {
- // TODO: Implement text serializer.
- return null;
+ public byte[] serialize(List<Rule> rules, Optional<Integer> formatVersion)
+ throws RuleSerializeException {
+ try {
+ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+ serialize(rules, formatVersion, byteArrayOutputStream);
+ return byteArrayOutputStream.toByteArray();
+ } catch (Exception e) {
+ throw new RuleSerializeException(e.getMessage(), e);
+ }
+ }
+
+ private void serializeRule(Rule rule, BitOutputStream bitOutputStream) {
+ if (rule == null) {
+ throw new IllegalArgumentException("Null rule can not be serialized");
+ }
+
+ // Start with a '1' bit to mark the start of a rule.
+ bitOutputStream.setNext();
+
+ serializeFormula(rule.getFormula(), bitOutputStream);
+ bitOutputStream.setNext(EFFECT_BITS, rule.getEffect());
+
+ // End with a '1' bit to mark the end of a rule.
+ bitOutputStream.setNext();
+ }
+
+ private void serializeFormula(Formula formula, BitOutputStream bitOutputStream) {
+ if (formula instanceof AtomicFormula) {
+ serializeAtomicFormula((AtomicFormula) formula, bitOutputStream);
+ } else if (formula instanceof CompoundFormula) {
+ serializeCompoundFormula((CompoundFormula) formula, bitOutputStream);
+ } else {
+ throw new IllegalArgumentException(
+ String.format("Invalid formula type: %s", formula.getClass()));
+ }
+ }
+
+ private void serializeCompoundFormula(
+ CompoundFormula compoundFormula, BitOutputStream bitOutputStream) {
+ if (compoundFormula == null) {
+ throw new IllegalArgumentException("Null compound formula can not be serialized");
+ }
+
+ bitOutputStream.setNext(SEPARATOR_BITS, COMPOUND_FORMULA_START);
+ bitOutputStream.setNext(CONNECTOR_BITS, compoundFormula.getConnector());
+ for (Formula formula : compoundFormula.getFormulas()) {
+ serializeFormula(formula, bitOutputStream);
+ }
+ bitOutputStream.setNext(SEPARATOR_BITS, COMPOUND_FORMULA_END);
+ }
+
+ private void serializeAtomicFormula(
+ AtomicFormula atomicFormula, BitOutputStream bitOutputStream) {
+ if (atomicFormula == null) {
+ throw new IllegalArgumentException("Null atomic formula can not be serialized");
+ }
+
+ bitOutputStream.setNext(SEPARATOR_BITS, ATOMIC_FORMULA_START);
+ bitOutputStream.setNext(KEY_BITS, atomicFormula.getKey());
+ if (atomicFormula instanceof AtomicFormula.StringAtomicFormula) {
+ AtomicFormula.StringAtomicFormula stringAtomicFormula =
+ (AtomicFormula.StringAtomicFormula) atomicFormula;
+ bitOutputStream.setNext(OPERATOR_BITS, AtomicFormula.EQ);
+ serializeValue(
+ stringAtomicFormula.getValue(),
+ stringAtomicFormula.getIsHashedValue(),
+ bitOutputStream);
+ } else if (atomicFormula instanceof AtomicFormula.IntAtomicFormula) {
+ AtomicFormula.IntAtomicFormula intAtomicFormula =
+ (AtomicFormula.IntAtomicFormula) atomicFormula;
+ bitOutputStream.setNext(OPERATOR_BITS, intAtomicFormula.getOperator());
+ serializeValue(
+ String.valueOf(intAtomicFormula.getValue()),
+ /* isHashedValue= */ false,
+ bitOutputStream);
+ } else if (atomicFormula instanceof AtomicFormula.BooleanAtomicFormula) {
+ AtomicFormula.BooleanAtomicFormula booleanAtomicFormula =
+ (AtomicFormula.BooleanAtomicFormula) atomicFormula;
+ bitOutputStream.setNext(OPERATOR_BITS, AtomicFormula.EQ);
+ serializeValue(
+ booleanAtomicFormula.getValue() ? "1" : "0",
+ /* isHashedValue= */ false,
+ bitOutputStream);
+ } else {
+ throw new IllegalArgumentException(
+ String.format("Invalid atomic formula type: %s", atomicFormula.getClass()));
+ }
+ }
+
+ private void serializeValue(
+ String value, boolean isHashedValue, BitOutputStream bitOutputStream) {
+ byte[] valueBytes = value.getBytes(StandardCharsets.UTF_8);
+
+ bitOutputStream.setNext(isHashedValue);
+ bitOutputStream.setNext(VALUE_SIZE_BITS, valueBytes.length);
+ for (byte valueByte : valueBytes) {
+ bitOutputStream.setNext(/* numOfBits= */ 8, valueByte);
+ }
}
}
diff --git a/services/core/java/com/android/server/integrity/serializer/RuleXmlSerializer.java b/services/core/java/com/android/server/integrity/serializer/RuleXmlSerializer.java
index 3ec9cf2..72068ce 100644
--- a/services/core/java/com/android/server/integrity/serializer/RuleXmlSerializer.java
+++ b/services/core/java/com/android/server/integrity/serializer/RuleXmlSerializer.java
@@ -39,7 +39,7 @@
private static final String RULE_LIST_TAG = "RL";
private static final String RULE_TAG = "R";
- private static final String OPEN_FORMULA_TAG = "OF";
+ private static final String COMPOUND_FORMULA_TAG = "OF";
private static final String ATOMIC_FORMULA_TAG = "AF";
private static final String EFFECT_ATTRIBUTE = "E";
private static final String KEY_ATTRIBUTE = "K";
@@ -78,13 +78,13 @@
private void serializeRules(List<Rule> rules, XmlSerializer xmlSerializer) throws IOException {
xmlSerializer.startTag(NAMESPACE, RULE_LIST_TAG);
for (Rule rule : rules) {
- serialize(rule, xmlSerializer);
+ serializeRule(rule, xmlSerializer);
}
xmlSerializer.endTag(NAMESPACE, RULE_LIST_TAG);
xmlSerializer.endDocument();
}
- private void serialize(Rule rule, XmlSerializer xmlSerializer) throws IOException {
+ private void serializeRule(Rule rule, XmlSerializer xmlSerializer) throws IOException {
if (rule == null) {
return;
}
@@ -98,25 +98,25 @@
if (formula instanceof AtomicFormula) {
serializeAtomicFormula((AtomicFormula) formula, xmlSerializer);
} else if (formula instanceof CompoundFormula) {
- serializeOpenFormula((CompoundFormula) formula, xmlSerializer);
+ serializeCompoundFormula((CompoundFormula) formula, xmlSerializer);
} else {
throw new IllegalArgumentException(
String.format("Invalid formula type: %s", formula.getClass()));
}
}
- private void serializeOpenFormula(CompoundFormula compoundFormula, XmlSerializer xmlSerializer)
- throws IOException {
+ private void serializeCompoundFormula(
+ CompoundFormula compoundFormula, XmlSerializer xmlSerializer) throws IOException {
if (compoundFormula == null) {
return;
}
- xmlSerializer.startTag(NAMESPACE, OPEN_FORMULA_TAG);
+ xmlSerializer.startTag(NAMESPACE, COMPOUND_FORMULA_TAG);
serializeAttributeValue(
CONNECTOR_ATTRIBUTE, String.valueOf(compoundFormula.getConnector()), xmlSerializer);
for (Formula formula : compoundFormula.getFormulas()) {
serializeFormula(formula, xmlSerializer);
}
- xmlSerializer.endTag(NAMESPACE, OPEN_FORMULA_TAG);
+ xmlSerializer.endTag(NAMESPACE, COMPOUND_FORMULA_TAG);
}
private void serializeAtomicFormula(AtomicFormula atomicFormula, XmlSerializer xmlSerializer)
diff --git a/services/core/java/com/android/server/timedetector/SimpleTimeDetectorStrategy.java b/services/core/java/com/android/server/timedetector/SimpleTimeDetectorStrategy.java
index 0b970bf..340fe3d 100644
--- a/services/core/java/com/android/server/timedetector/SimpleTimeDetectorStrategy.java
+++ b/services/core/java/com/android/server/timedetector/SimpleTimeDetectorStrategy.java
@@ -150,7 +150,8 @@
if (!mCallback.isAutoTimeDetectionEnabled()) {
if (DBG) {
Slog.d(LOG_TAG, "Auto time detection is not enabled."
- + " time=" + time
+ + " origin=" + origin
+ + ", time=" + time
+ ", cause=" + cause);
}
return;
@@ -159,7 +160,8 @@
if (mCallback.isAutoTimeDetectionEnabled()) {
if (DBG) {
Slog.d(LOG_TAG, "Auto time detection is enabled."
- + " time=" + time
+ + " origin=" + origin
+ + ", time=" + time
+ ", cause=" + cause);
}
return;
@@ -232,16 +234,16 @@
@Override
public synchronized void dump(@NonNull PrintWriter pw, @Nullable String[] args) {
- pw.println("mLastPhoneSuggestion=" + mLastPhoneSuggestion);
- pw.println("mLastAutoSystemClockTimeSet=" + mLastAutoSystemClockTimeSet);
- pw.println("mLastAutoSystemClockTime=" + mLastAutoSystemClockTime);
- pw.println("mLastAutoSystemClockTimeSendNetworkBroadcast="
+ IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " ");
+ ipw.println("TimeDetectorStrategy:");
+ ipw.increaseIndent(); // level 1
+
+ ipw.println("mLastPhoneSuggestion=" + mLastPhoneSuggestion);
+ ipw.println("mLastAutoSystemClockTimeSet=" + mLastAutoSystemClockTimeSet);
+ ipw.println("mLastAutoSystemClockTime=" + mLastAutoSystemClockTime);
+ ipw.println("mLastAutoSystemClockTimeSendNetworkBroadcast="
+ mLastAutoSystemClockTimeSendNetworkBroadcast);
- IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " ");
-
- ipw.println("TimeDetectorStrategyImpl logs:");
- ipw.increaseIndent(); // level 1
ipw.println("Time change log:");
ipw.increaseIndent(); // level 2
@@ -249,6 +251,7 @@
ipw.decreaseIndent(); // level 2
ipw.decreaseIndent(); // level 1
+ ipw.flush();
}
@GuardedBy("this")
diff --git a/services/core/java/com/android/server/timedetector/TimeDetectorStrategy.java b/services/core/java/com/android/server/timedetector/TimeDetectorStrategy.java
index 32cee2d..0a6c2e7 100644
--- a/services/core/java/com/android/server/timedetector/TimeDetectorStrategy.java
+++ b/services/core/java/com/android/server/timedetector/TimeDetectorStrategy.java
@@ -39,6 +39,12 @@
/**
* The interface used by the strategy to interact with the surrounding service.
+ *
+ * <p>Note: Because the system properties-derived value {@link #isAutoTimeDetectionEnabled()}
+ * can be modified independently and from different threads (and processes!). its use is prone
+ * to race conditions. That will be true until the responsibility for setting their values is
+ * moved to {@link TimeDetectorStrategy}. There are similar issues with
+ * {@link #systemClockMillis()} while any process can modify the system clock.
*/
interface Callback {
diff --git a/services/tests/servicestests/AndroidManifest.xml b/services/tests/servicestests/AndroidManifest.xml
index fb42507..3a07a69 100644
--- a/services/tests/servicestests/AndroidManifest.xml
+++ b/services/tests/servicestests/AndroidManifest.xml
@@ -40,6 +40,7 @@
<uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
<uses-permission android:name="android.permission.MANAGE_DEVICE_ADMINS" />
<uses-permission android:name="android.permission.MODIFY_PHONE_STATE" />
+ <uses-permission android:name="android.permission.READ_PRIVILEGED_PHONE_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.PACKET_KEEPALIVE_OFFLOAD" />
diff --git a/services/tests/servicestests/src/com/android/server/integrity/parser/RuleBinaryParserTest.java b/services/tests/servicestests/src/com/android/server/integrity/parser/RuleBinaryParserTest.java
new file mode 100644
index 0000000..88b6d70
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/integrity/parser/RuleBinaryParserTest.java
@@ -0,0 +1,628 @@
+/*
+ * Copyright (C) 2019 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.integrity.parser;
+
+import static com.android.server.integrity.model.ComponentBitSize.ATOMIC_FORMULA_START;
+import static com.android.server.integrity.model.ComponentBitSize.COMPOUND_FORMULA_END;
+import static com.android.server.integrity.model.ComponentBitSize.COMPOUND_FORMULA_START;
+import static com.android.server.integrity.model.ComponentBitSize.CONNECTOR_BITS;
+import static com.android.server.integrity.model.ComponentBitSize.DEFAULT_FORMAT_VERSION;
+import static com.android.server.integrity.model.ComponentBitSize.EFFECT_BITS;
+import static com.android.server.integrity.model.ComponentBitSize.FORMAT_VERSION_BITS;
+import static com.android.server.integrity.model.ComponentBitSize.KEY_BITS;
+import static com.android.server.integrity.model.ComponentBitSize.OPERATOR_BITS;
+import static com.android.server.integrity.model.ComponentBitSize.SEPARATOR_BITS;
+import static com.android.server.integrity.model.ComponentBitSize.VALUE_SIZE_BITS;
+import static com.android.server.integrity.utils.TestUtils.getBits;
+import static com.android.server.integrity.utils.TestUtils.getBytes;
+import static com.android.server.integrity.utils.TestUtils.getValueBits;
+import static com.android.server.testutils.TestUtils.assertExpectException;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.integrity.AtomicFormula;
+import android.content.integrity.CompoundFormula;
+import android.content.integrity.Rule;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+@RunWith(JUnit4.class)
+public class RuleBinaryParserTest {
+
+ private static final String COMPOUND_FORMULA_START_BITS =
+ getBits(COMPOUND_FORMULA_START, SEPARATOR_BITS);
+ private static final String COMPOUND_FORMULA_END_BITS =
+ getBits(COMPOUND_FORMULA_END, SEPARATOR_BITS);
+ private static final String ATOMIC_FORMULA_START_BITS =
+ getBits(ATOMIC_FORMULA_START, SEPARATOR_BITS);
+ private static final int INVALID_FORMULA_SEPARATOR_VALUE = 3;
+ private static final String INVALID_FORMULA_SEPARATOR_BITS =
+ getBits(INVALID_FORMULA_SEPARATOR_VALUE, SEPARATOR_BITS);
+
+ private static final String NOT = getBits(CompoundFormula.NOT, CONNECTOR_BITS);
+ private static final String AND = getBits(CompoundFormula.AND, CONNECTOR_BITS);
+ private static final String OR = getBits(CompoundFormula.OR, CONNECTOR_BITS);
+ private static final int INVALID_CONNECTOR_VALUE = 3;
+ private static final String INVALID_CONNECTOR =
+ getBits(INVALID_CONNECTOR_VALUE, CONNECTOR_BITS);
+
+ private static final String PACKAGE_NAME = getBits(AtomicFormula.PACKAGE_NAME, KEY_BITS);
+ private static final String APP_CERTIFICATE = getBits(AtomicFormula.APP_CERTIFICATE, KEY_BITS);
+ private static final String VERSION_CODE = getBits(AtomicFormula.VERSION_CODE, KEY_BITS);
+ private static final String PRE_INSTALLED = getBits(AtomicFormula.PRE_INSTALLED, KEY_BITS);
+ private static final int INVALID_KEY_VALUE = 6;
+ private static final String INVALID_KEY = getBits(INVALID_KEY_VALUE, KEY_BITS);
+
+ private static final String EQ = getBits(AtomicFormula.EQ, OPERATOR_BITS);
+ private static final int INVALID_OPERATOR_VALUE = 5;
+ private static final String INVALID_OPERATOR = getBits(INVALID_OPERATOR_VALUE, OPERATOR_BITS);
+
+ private static final String IS_NOT_HASHED = "0";
+
+ private static final String DENY = getBits(Rule.DENY, EFFECT_BITS);
+ private static final int INVALID_EFFECT_VALUE = 5;
+ private static final String INVALID_EFFECT = getBits(INVALID_EFFECT_VALUE, EFFECT_BITS);
+
+ private static final String START_BIT = "1";
+ private static final String END_BIT = "1";
+ private static final String INVALID_MARKER_BIT = "0";
+
+ private static final byte[] DEFAULT_FORMAT_VERSION_BYTES =
+ getBytes(getBits(DEFAULT_FORMAT_VERSION, FORMAT_VERSION_BITS));
+
+ @Test
+ public void testBinaryStream_validCompoundFormula() throws Exception {
+ String packageName = "com.test.app";
+ String ruleBits =
+ START_BIT
+ + COMPOUND_FORMULA_START_BITS
+ + NOT
+ + ATOMIC_FORMULA_START_BITS
+ + PACKAGE_NAME
+ + EQ
+ + IS_NOT_HASHED
+ + getBits(packageName.length(), VALUE_SIZE_BITS)
+ + getValueBits(packageName)
+ + COMPOUND_FORMULA_END_BITS
+ + DENY
+ + END_BIT;
+ byte[] ruleBytes = getBytes(ruleBits);
+ ByteBuffer rule =
+ ByteBuffer.allocate(DEFAULT_FORMAT_VERSION_BYTES.length + ruleBytes.length);
+ rule.put(DEFAULT_FORMAT_VERSION_BYTES);
+ rule.put(ruleBytes);
+ RuleParser binaryParser = new RuleBinaryParser();
+ InputStream inputStream = new ByteArrayInputStream(rule.array());
+ Rule expectedRule =
+ new Rule(
+ new CompoundFormula(
+ CompoundFormula.NOT,
+ Collections.singletonList(
+ new AtomicFormula.StringAtomicFormula(
+ AtomicFormula.PACKAGE_NAME,
+ packageName,
+ /* isHashedValue= */ false))),
+ Rule.DENY);
+
+ List<Rule> rules = binaryParser.parse(inputStream);
+
+ assertThat(rules).isEqualTo(Collections.singletonList(expectedRule));
+ }
+
+ @Test
+ public void testBinaryString_validCompoundFormula_notConnector() throws Exception {
+ String packageName = "com.test.app";
+ String ruleBits =
+ START_BIT
+ + COMPOUND_FORMULA_START_BITS
+ + NOT
+ + ATOMIC_FORMULA_START_BITS
+ + PACKAGE_NAME
+ + EQ
+ + IS_NOT_HASHED
+ + getBits(packageName.length(), VALUE_SIZE_BITS)
+ + getValueBits(packageName)
+ + COMPOUND_FORMULA_END_BITS
+ + DENY
+ + END_BIT;
+ byte[] ruleBytes = getBytes(ruleBits);
+ ByteBuffer rule =
+ ByteBuffer.allocate(DEFAULT_FORMAT_VERSION_BYTES.length + ruleBytes.length);
+ rule.put(DEFAULT_FORMAT_VERSION_BYTES);
+ rule.put(ruleBytes);
+ RuleParser binaryParser = new RuleBinaryParser();
+ Rule expectedRule =
+ new Rule(
+ new CompoundFormula(
+ CompoundFormula.NOT,
+ Collections.singletonList(
+ new AtomicFormula.StringAtomicFormula(
+ AtomicFormula.PACKAGE_NAME,
+ packageName,
+ /* isHashedValue= */ false))),
+ Rule.DENY);
+
+ List<Rule> rules = binaryParser.parse(rule.array());
+
+ assertThat(rules).isEqualTo(Collections.singletonList(expectedRule));
+ }
+
+ @Test
+ public void testBinaryString_validCompoundFormula_andConnector() throws Exception {
+ String packageName = "com.test.app";
+ String appCertificate = "test_cert";
+ String ruleBits =
+ START_BIT
+ + COMPOUND_FORMULA_START_BITS
+ + AND
+ + ATOMIC_FORMULA_START_BITS
+ + PACKAGE_NAME
+ + EQ
+ + IS_NOT_HASHED
+ + getBits(packageName.length(), VALUE_SIZE_BITS)
+ + getValueBits(packageName)
+ + ATOMIC_FORMULA_START_BITS
+ + APP_CERTIFICATE
+ + EQ
+ + IS_NOT_HASHED
+ + getBits(appCertificate.length(), VALUE_SIZE_BITS)
+ + getValueBits(appCertificate)
+ + COMPOUND_FORMULA_END_BITS
+ + DENY
+ + END_BIT;
+ byte[] ruleBytes = getBytes(ruleBits);
+ ByteBuffer rule =
+ ByteBuffer.allocate(DEFAULT_FORMAT_VERSION_BYTES.length + ruleBytes.length);
+ rule.put(DEFAULT_FORMAT_VERSION_BYTES);
+ rule.put(ruleBytes);
+ RuleParser binaryParser = new RuleBinaryParser();
+ Rule expectedRule =
+ new Rule(
+ new CompoundFormula(
+ CompoundFormula.AND,
+ Arrays.asList(
+ new AtomicFormula.StringAtomicFormula(
+ AtomicFormula.PACKAGE_NAME,
+ packageName,
+ /* isHashedValue= */ false),
+ new AtomicFormula.StringAtomicFormula(
+ AtomicFormula.APP_CERTIFICATE,
+ appCertificate,
+ /* isHashedValue= */ false))),
+ Rule.DENY);
+ List<Rule> rules = binaryParser.parse(rule.array());
+
+ assertThat(rules).isEqualTo(Collections.singletonList(expectedRule));
+ }
+
+ @Test
+ public void testBinaryString_validCompoundFormula_orConnector() throws Exception {
+ String packageName = "com.test.app";
+ String appCertificate = "test_cert";
+ String ruleBits =
+ START_BIT
+ + COMPOUND_FORMULA_START_BITS
+ + OR
+ + ATOMIC_FORMULA_START_BITS
+ + PACKAGE_NAME
+ + EQ
+ + IS_NOT_HASHED
+ + getBits(packageName.length(), VALUE_SIZE_BITS)
+ + getValueBits(packageName)
+ + ATOMIC_FORMULA_START_BITS
+ + APP_CERTIFICATE
+ + EQ
+ + IS_NOT_HASHED
+ + getBits(appCertificate.length(), VALUE_SIZE_BITS)
+ + getValueBits(appCertificate)
+ + COMPOUND_FORMULA_END_BITS
+ + DENY
+ + END_BIT;
+ byte[] ruleBytes = getBytes(ruleBits);
+ ByteBuffer rule =
+ ByteBuffer.allocate(DEFAULT_FORMAT_VERSION_BYTES.length + ruleBytes.length);
+ rule.put(DEFAULT_FORMAT_VERSION_BYTES);
+ rule.put(ruleBytes);
+ RuleParser binaryParser = new RuleBinaryParser();
+ Rule expectedRule =
+ new Rule(
+ new CompoundFormula(
+ CompoundFormula.OR,
+ Arrays.asList(
+ new AtomicFormula.StringAtomicFormula(
+ AtomicFormula.PACKAGE_NAME,
+ packageName,
+ /* isHashedValue= */ false),
+ new AtomicFormula.StringAtomicFormula(
+ AtomicFormula.APP_CERTIFICATE,
+ appCertificate,
+ /* isHashedValue= */ false))),
+ Rule.DENY);
+
+ List<Rule> rules = binaryParser.parse(rule.array());
+
+ assertThat(rules).isEqualTo(Collections.singletonList(expectedRule));
+ }
+
+ @Test
+ public void testBinaryString_validAtomicFormula_stringValue() throws Exception {
+ String packageName = "com.test.app";
+ String ruleBits =
+ START_BIT
+ + ATOMIC_FORMULA_START_BITS
+ + PACKAGE_NAME
+ + EQ
+ + IS_NOT_HASHED
+ + getBits(packageName.length(), VALUE_SIZE_BITS)
+ + getValueBits(packageName)
+ + DENY
+ + END_BIT;
+ byte[] ruleBytes = getBytes(ruleBits);
+ ByteBuffer rule =
+ ByteBuffer.allocate(DEFAULT_FORMAT_VERSION_BYTES.length + ruleBytes.length);
+ rule.put(DEFAULT_FORMAT_VERSION_BYTES);
+ rule.put(ruleBytes);
+ RuleParser binaryParser = new RuleBinaryParser();
+ Rule expectedRule =
+ new Rule(
+ new AtomicFormula.StringAtomicFormula(
+ AtomicFormula.PACKAGE_NAME,
+ packageName,
+ /* isHashedValue= */ false),
+ Rule.DENY);
+
+ List<Rule> rules = binaryParser.parse(rule.array());
+
+ assertThat(rules).isEqualTo(Collections.singletonList(expectedRule));
+ }
+
+ @Test
+ public void testBinaryString_validAtomicFormula_integerValue() throws Exception {
+ String versionCode = "1";
+ String ruleBits =
+ START_BIT
+ + ATOMIC_FORMULA_START_BITS
+ + VERSION_CODE
+ + EQ
+ + IS_NOT_HASHED
+ + getBits(versionCode.length(), VALUE_SIZE_BITS)
+ + getValueBits(versionCode)
+ + DENY
+ + END_BIT;
+ byte[] ruleBytes = getBytes(ruleBits);
+ ByteBuffer rule =
+ ByteBuffer.allocate(DEFAULT_FORMAT_VERSION_BYTES.length + ruleBytes.length);
+ rule.put(DEFAULT_FORMAT_VERSION_BYTES);
+ rule.put(ruleBytes);
+ RuleParser binaryParser = new RuleBinaryParser();
+ Rule expectedRule =
+ new Rule(
+ new AtomicFormula.IntAtomicFormula(
+ AtomicFormula.VERSION_CODE, AtomicFormula.EQ, 1),
+ Rule.DENY);
+
+ List<Rule> rules = binaryParser.parse(rule.array());
+
+ assertThat(rules).isEqualTo(Collections.singletonList(expectedRule));
+ }
+
+ @Test
+ public void testBinaryString_validAtomicFormula_booleanValue() throws Exception {
+ String isPreInstalled = "1";
+ String ruleBits =
+ START_BIT
+ + ATOMIC_FORMULA_START_BITS
+ + PRE_INSTALLED
+ + EQ
+ + IS_NOT_HASHED
+ + getBits(isPreInstalled.length(), VALUE_SIZE_BITS)
+ + getValueBits(isPreInstalled)
+ + DENY
+ + END_BIT;
+ byte[] ruleBytes = getBytes(ruleBits);
+ ByteBuffer rule =
+ ByteBuffer.allocate(DEFAULT_FORMAT_VERSION_BYTES.length + ruleBytes.length);
+ rule.put(DEFAULT_FORMAT_VERSION_BYTES);
+ rule.put(ruleBytes);
+ RuleParser binaryParser = new RuleBinaryParser();
+ Rule expectedRule =
+ new Rule(
+ new AtomicFormula.BooleanAtomicFormula(AtomicFormula.PRE_INSTALLED, true),
+ Rule.DENY);
+
+ List<Rule> rules = binaryParser.parse(rule.array());
+
+ assertThat(rules).isEqualTo(Collections.singletonList(expectedRule));
+ }
+
+ @Test
+ public void testBinaryString_invalidAtomicFormula() throws Exception {
+ String versionCode = "test";
+ String ruleBits =
+ START_BIT
+ + ATOMIC_FORMULA_START_BITS
+ + VERSION_CODE
+ + EQ
+ + IS_NOT_HASHED
+ + getBits(versionCode.length(), VALUE_SIZE_BITS)
+ + getValueBits(versionCode)
+ + DENY
+ + END_BIT;
+ byte[] ruleBytes = getBytes(ruleBits);
+ ByteBuffer rule =
+ ByteBuffer.allocate(DEFAULT_FORMAT_VERSION_BYTES.length + ruleBytes.length);
+ rule.put(DEFAULT_FORMAT_VERSION_BYTES);
+ rule.put(ruleBytes);
+ RuleParser binaryParser = new RuleBinaryParser();
+
+ assertExpectException(
+ RuleParseException.class,
+ /* expectedExceptionMessageRegex */ "For input string:",
+ () -> binaryParser.parse(rule.array()));
+ }
+
+ @Test
+ public void testBinaryString_withNoRuleList() throws RuleParseException {
+ ByteBuffer rule = ByteBuffer.allocate(DEFAULT_FORMAT_VERSION_BYTES.length);
+ rule.put(DEFAULT_FORMAT_VERSION_BYTES);
+ RuleParser binaryParser = new RuleBinaryParser();
+
+ List<Rule> rules = binaryParser.parse(rule.array());
+
+ assertThat(rules).isEmpty();
+ }
+
+ @Test
+ public void testBinaryString_withEmptyRule() throws RuleParseException {
+ String ruleBits = START_BIT;
+ byte[] ruleBytes = getBytes(ruleBits);
+ ByteBuffer rule =
+ ByteBuffer.allocate(DEFAULT_FORMAT_VERSION_BYTES.length + ruleBytes.length);
+ rule.put(DEFAULT_FORMAT_VERSION_BYTES);
+ rule.put(ruleBytes);
+ RuleParser binaryParser = new RuleBinaryParser();
+
+ assertExpectException(
+ RuleParseException.class,
+ /* expectedExceptionMessageRegex */ "Invalid byte index",
+ () -> binaryParser.parse(rule.array()));
+ }
+
+ @Test
+ public void testBinaryString_invalidCompoundFormula_invalidNumberOfFormulas() throws Exception {
+ String packageName = "com.test.app";
+ String appCertificate = "test_cert";
+ String ruleBits =
+ START_BIT
+ + COMPOUND_FORMULA_START_BITS
+ + NOT
+ + ATOMIC_FORMULA_START_BITS
+ + PACKAGE_NAME
+ + EQ
+ + IS_NOT_HASHED
+ + getBits(packageName.length(), VALUE_SIZE_BITS)
+ + getValueBits(packageName)
+ + ATOMIC_FORMULA_START_BITS
+ + APP_CERTIFICATE
+ + EQ
+ + IS_NOT_HASHED
+ + getBits(appCertificate.length(), VALUE_SIZE_BITS)
+ + getValueBits(appCertificate)
+ + COMPOUND_FORMULA_END_BITS
+ + DENY
+ + END_BIT;
+ byte[] ruleBytes = getBytes(ruleBits);
+ ByteBuffer rule =
+ ByteBuffer.allocate(DEFAULT_FORMAT_VERSION_BYTES.length + ruleBytes.length);
+ rule.put(DEFAULT_FORMAT_VERSION_BYTES);
+ rule.put(ruleBytes);
+ RuleParser binaryParser = new RuleBinaryParser();
+
+ assertExpectException(
+ RuleParseException.class,
+ /* expectedExceptionMessageRegex */ "Connector NOT must have 1 formula only",
+ () -> binaryParser.parse(rule.array()));
+ }
+
+ @Test
+ public void testBinaryString_invalidRule_invalidOperator() throws Exception {
+ String versionCode = "1";
+ String ruleBits =
+ START_BIT
+ + COMPOUND_FORMULA_START_BITS
+ + NOT
+ + ATOMIC_FORMULA_START_BITS
+ + VERSION_CODE
+ + INVALID_OPERATOR
+ + IS_NOT_HASHED
+ + getBits(versionCode.length(), VALUE_SIZE_BITS)
+ + getValueBits(versionCode)
+ + COMPOUND_FORMULA_END_BITS
+ + DENY
+ + END_BIT;
+ byte[] ruleBytes = getBytes(ruleBits);
+ ByteBuffer rule =
+ ByteBuffer.allocate(DEFAULT_FORMAT_VERSION_BYTES.length + ruleBytes.length);
+ rule.put(DEFAULT_FORMAT_VERSION_BYTES);
+ rule.put(ruleBytes);
+ RuleParser binaryParser = new RuleBinaryParser();
+
+ assertExpectException(
+ RuleParseException.class,
+ /* expectedExceptionMessageRegex */ String.format(
+ "Unknown operator: %d", INVALID_OPERATOR_VALUE),
+ () -> binaryParser.parse(rule.array()));
+ }
+
+ @Test
+ public void testBinaryString_invalidRule_invalidEffect() throws Exception {
+ String packageName = "com.test.app";
+ String ruleBits =
+ START_BIT
+ + COMPOUND_FORMULA_START_BITS
+ + NOT
+ + ATOMIC_FORMULA_START_BITS
+ + PACKAGE_NAME
+ + EQ
+ + IS_NOT_HASHED
+ + getBits(packageName.length(), VALUE_SIZE_BITS)
+ + getValueBits(packageName)
+ + COMPOUND_FORMULA_END_BITS
+ + INVALID_EFFECT
+ + END_BIT;
+ byte[] ruleBytes = getBytes(ruleBits);
+ ByteBuffer rule =
+ ByteBuffer.allocate(DEFAULT_FORMAT_VERSION_BYTES.length + ruleBytes.length);
+ rule.put(DEFAULT_FORMAT_VERSION_BYTES);
+ rule.put(ruleBytes);
+ RuleParser binaryParser = new RuleBinaryParser();
+
+ assertExpectException(
+ RuleParseException.class,
+ /* expectedExceptionMessageRegex */ String.format(
+ "Unknown effect: %d", INVALID_EFFECT_VALUE),
+ () -> binaryParser.parse(rule.array()));
+ }
+
+ @Test
+ public void testBinaryString_invalidRule_invalidConnector() throws Exception {
+ String packageName = "com.test.app";
+ String ruleBits =
+ START_BIT
+ + COMPOUND_FORMULA_START_BITS
+ + INVALID_CONNECTOR
+ + ATOMIC_FORMULA_START_BITS
+ + PACKAGE_NAME
+ + EQ
+ + IS_NOT_HASHED
+ + getBits(packageName.length(), VALUE_SIZE_BITS)
+ + getValueBits(packageName)
+ + COMPOUND_FORMULA_END_BITS
+ + DENY
+ + END_BIT;
+ byte[] ruleBytes = getBytes(ruleBits);
+ ByteBuffer rule =
+ ByteBuffer.allocate(DEFAULT_FORMAT_VERSION_BYTES.length + ruleBytes.length);
+ rule.put(DEFAULT_FORMAT_VERSION_BYTES);
+ rule.put(ruleBytes);
+ RuleParser binaryParser = new RuleBinaryParser();
+
+ assertExpectException(
+ RuleParseException.class,
+ /* expectedExceptionMessageRegex */ String.format(
+ "Unknown connector: %d", INVALID_CONNECTOR_VALUE),
+ () -> binaryParser.parse(rule.array()));
+ }
+
+ @Test
+ public void testBinaryString_invalidRule_invalidKey() throws Exception {
+ String packageName = "com.test.app";
+ String ruleBits =
+ START_BIT
+ + COMPOUND_FORMULA_START_BITS
+ + NOT
+ + ATOMIC_FORMULA_START_BITS
+ + INVALID_KEY
+ + EQ
+ + IS_NOT_HASHED
+ + getBits(packageName.length(), VALUE_SIZE_BITS)
+ + getValueBits(packageName)
+ + COMPOUND_FORMULA_END_BITS
+ + DENY
+ + END_BIT;
+ byte[] ruleBytes = getBytes(ruleBits);
+ ByteBuffer rule =
+ ByteBuffer.allocate(DEFAULT_FORMAT_VERSION_BYTES.length + ruleBytes.length);
+ rule.put(DEFAULT_FORMAT_VERSION_BYTES);
+ rule.put(ruleBytes);
+ RuleParser binaryParser = new RuleBinaryParser();
+
+ assertExpectException(
+ RuleParseException.class,
+ /* expectedExceptionMessageRegex */ String.format(
+ "Unknown key: %d", INVALID_KEY_VALUE),
+ () -> binaryParser.parse(rule.array()));
+ }
+
+ @Test
+ public void testBinaryString_invalidRule_invalidSeparator() throws Exception {
+ String packageName = "com.test.app";
+ String ruleBits =
+ START_BIT
+ + INVALID_FORMULA_SEPARATOR_BITS
+ + NOT
+ + ATOMIC_FORMULA_START_BITS
+ + PACKAGE_NAME
+ + EQ
+ + IS_NOT_HASHED
+ + getBits(packageName.length(), VALUE_SIZE_BITS)
+ + getValueBits(packageName)
+ + COMPOUND_FORMULA_END_BITS
+ + DENY
+ + END_BIT;
+ byte[] ruleBytes = getBytes(ruleBits);
+ ByteBuffer rule =
+ ByteBuffer.allocate(DEFAULT_FORMAT_VERSION_BYTES.length + ruleBytes.length);
+ rule.put(DEFAULT_FORMAT_VERSION_BYTES);
+ rule.put(ruleBytes);
+ RuleParser binaryParser = new RuleBinaryParser();
+
+ assertExpectException(
+ RuleParseException.class,
+ /* expectedExceptionMessageRegex */ String.format(
+ "Unknown formula separator: %d", INVALID_FORMULA_SEPARATOR_VALUE),
+ () -> binaryParser.parse(rule.array()));
+ }
+
+ @Test
+ public void testBinaryString_invalidRule_invalidEndMarker() throws Exception {
+ String packageName = "com.test.app";
+ String ruleBits =
+ START_BIT
+ + COMPOUND_FORMULA_START_BITS
+ + NOT
+ + ATOMIC_FORMULA_START_BITS
+ + PACKAGE_NAME
+ + EQ
+ + IS_NOT_HASHED
+ + getBits(packageName.length(), VALUE_SIZE_BITS)
+ + getValueBits(packageName)
+ + COMPOUND_FORMULA_END_BITS
+ + DENY
+ + INVALID_MARKER_BIT;
+ byte[] ruleBytes = getBytes(ruleBits);
+ ByteBuffer rule =
+ ByteBuffer.allocate(DEFAULT_FORMAT_VERSION_BYTES.length + ruleBytes.length);
+ rule.put(DEFAULT_FORMAT_VERSION_BYTES);
+ rule.put(ruleBytes);
+ RuleParser binaryParser = new RuleBinaryParser();
+
+ assertExpectException(
+ RuleParseException.class,
+ /* expectedExceptionMessageRegex */ "A rule must end with a '1' bit",
+ () -> binaryParser.parse(rule.array()));
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/integrity/serializer/RuleBinarySerializerTest.java b/services/tests/servicestests/src/com/android/server/integrity/serializer/RuleBinarySerializerTest.java
new file mode 100644
index 0000000..901277d
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/integrity/serializer/RuleBinarySerializerTest.java
@@ -0,0 +1,444 @@
+/*
+ * Copyright (C) 2019 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.integrity.serializer;
+
+import static com.android.server.integrity.model.ComponentBitSize.ATOMIC_FORMULA_START;
+import static com.android.server.integrity.model.ComponentBitSize.COMPOUND_FORMULA_END;
+import static com.android.server.integrity.model.ComponentBitSize.COMPOUND_FORMULA_START;
+import static com.android.server.integrity.model.ComponentBitSize.CONNECTOR_BITS;
+import static com.android.server.integrity.model.ComponentBitSize.DEFAULT_FORMAT_VERSION;
+import static com.android.server.integrity.model.ComponentBitSize.EFFECT_BITS;
+import static com.android.server.integrity.model.ComponentBitSize.FORMAT_VERSION_BITS;
+import static com.android.server.integrity.model.ComponentBitSize.KEY_BITS;
+import static com.android.server.integrity.model.ComponentBitSize.OPERATOR_BITS;
+import static com.android.server.integrity.model.ComponentBitSize.SEPARATOR_BITS;
+import static com.android.server.integrity.model.ComponentBitSize.VALUE_SIZE_BITS;
+import static com.android.server.integrity.utils.TestUtils.getBits;
+import static com.android.server.integrity.utils.TestUtils.getBytes;
+import static com.android.server.integrity.utils.TestUtils.getValueBits;
+import static com.android.server.testutils.TestUtils.assertExpectException;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.integrity.AppInstallMetadata;
+import android.content.integrity.AtomicFormula;
+import android.content.integrity.CompoundFormula;
+import android.content.integrity.Formula;
+import android.content.integrity.Rule;
+
+import androidx.annotation.NonNull;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.ByteArrayOutputStream;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Optional;
+
+@RunWith(JUnit4.class)
+public class RuleBinarySerializerTest {
+
+ private static final String COMPOUND_FORMULA_START_BITS =
+ getBits(COMPOUND_FORMULA_START, SEPARATOR_BITS);
+ private static final String COMPOUND_FORMULA_END_BITS =
+ getBits(COMPOUND_FORMULA_END, SEPARATOR_BITS);
+ private static final String ATOMIC_FORMULA_START_BITS =
+ getBits(ATOMIC_FORMULA_START, SEPARATOR_BITS);
+
+ private static final String NOT = getBits(CompoundFormula.NOT, CONNECTOR_BITS);
+ private static final String AND = getBits(CompoundFormula.AND, CONNECTOR_BITS);
+ private static final String OR = getBits(CompoundFormula.OR, CONNECTOR_BITS);
+
+ private static final String PACKAGE_NAME = getBits(AtomicFormula.PACKAGE_NAME, KEY_BITS);
+ private static final String APP_CERTIFICATE = getBits(AtomicFormula.APP_CERTIFICATE, KEY_BITS);
+ private static final String VERSION_CODE = getBits(AtomicFormula.VERSION_CODE, KEY_BITS);
+ private static final String PRE_INSTALLED = getBits(AtomicFormula.PRE_INSTALLED, KEY_BITS);
+
+ private static final String EQ = getBits(AtomicFormula.EQ, OPERATOR_BITS);
+
+ private static final String IS_NOT_HASHED = "0";
+
+ private static final String DENY = getBits(Rule.DENY, EFFECT_BITS);
+
+ private static final String START_BIT = "1";
+ private static final String END_BIT = "1";
+
+ private static final byte[] DEFAULT_FORMAT_VERSION_BYTES =
+ getBytes(getBits(DEFAULT_FORMAT_VERSION, FORMAT_VERSION_BITS));
+
+ @Test
+ public void testBinaryString_serializeEmptyRule() throws Exception {
+ Rule rule = null;
+ RuleSerializer binarySerializer = new RuleBinarySerializer();
+
+ assertExpectException(
+ RuleSerializeException.class,
+ /* expectedExceptionMessageRegex= */ "Null rule can not be serialized",
+ () ->
+ binarySerializer.serialize(
+ Collections.singletonList(rule),
+ /* formatVersion= */ Optional.empty()));
+ }
+
+ @Test
+ public void testBinaryStream_serializeValidCompoundFormula() throws Exception {
+ String packageName = "com.test.app";
+ Rule rule =
+ new Rule(
+ new CompoundFormula(
+ CompoundFormula.NOT,
+ Collections.singletonList(
+ new AtomicFormula.StringAtomicFormula(
+ AtomicFormula.PACKAGE_NAME,
+ packageName,
+ /* isHashedValue= */ false))),
+ Rule.DENY);
+ RuleSerializer binarySerializer = new RuleBinarySerializer();
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ String expectedBits =
+ START_BIT
+ + COMPOUND_FORMULA_START_BITS
+ + NOT
+ + ATOMIC_FORMULA_START_BITS
+ + PACKAGE_NAME
+ + EQ
+ + IS_NOT_HASHED
+ + getBits(packageName.length(), VALUE_SIZE_BITS)
+ + getValueBits(packageName)
+ + COMPOUND_FORMULA_END_BITS
+ + DENY
+ + END_BIT;
+ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+ byteArrayOutputStream.write(DEFAULT_FORMAT_VERSION_BYTES);
+ byteArrayOutputStream.write(getBytes(expectedBits));
+ byte[] expectedRules = byteArrayOutputStream.toByteArray();
+
+ binarySerializer.serialize(
+ Collections.singletonList(rule),
+ /* formatVersion= */ Optional.empty(),
+ outputStream);
+
+ byte[] actualRules = outputStream.toByteArray();
+ assertThat(actualRules).isEqualTo(expectedRules);
+ }
+
+ @Test
+ public void testBinaryString_serializeValidCompoundFormula_notConnector() throws Exception {
+ String packageName = "com.test.app";
+ Rule rule =
+ new Rule(
+ new CompoundFormula(
+ CompoundFormula.NOT,
+ Collections.singletonList(
+ new AtomicFormula.StringAtomicFormula(
+ AtomicFormula.PACKAGE_NAME,
+ packageName,
+ /* isHashedValue= */ false))),
+ Rule.DENY);
+ RuleSerializer binarySerializer = new RuleBinarySerializer();
+ String expectedBits =
+ START_BIT
+ + COMPOUND_FORMULA_START_BITS
+ + NOT
+ + ATOMIC_FORMULA_START_BITS
+ + PACKAGE_NAME
+ + EQ
+ + IS_NOT_HASHED
+ + getBits(packageName.length(), VALUE_SIZE_BITS)
+ + getValueBits(packageName)
+ + COMPOUND_FORMULA_END_BITS
+ + DENY
+ + END_BIT;
+ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+ byteArrayOutputStream.write(DEFAULT_FORMAT_VERSION_BYTES);
+ byteArrayOutputStream.write(getBytes(expectedBits));
+ byte[] expectedRules = byteArrayOutputStream.toByteArray();
+
+ byte[] actualRules =
+ binarySerializer.serialize(
+ Collections.singletonList(rule), /* formatVersion= */ Optional.empty());
+
+ assertThat(actualRules).isEqualTo(expectedRules);
+ }
+
+ @Test
+ public void testBinaryString_serializeValidCompoundFormula_andConnector() throws Exception {
+ String packageName = "com.test.app";
+ String appCertificate = "test_cert";
+ Rule rule =
+ new Rule(
+ new CompoundFormula(
+ CompoundFormula.AND,
+ Arrays.asList(
+ new AtomicFormula.StringAtomicFormula(
+ AtomicFormula.PACKAGE_NAME,
+ packageName,
+ /* isHashedValue= */ false),
+ new AtomicFormula.StringAtomicFormula(
+ AtomicFormula.APP_CERTIFICATE,
+ appCertificate,
+ /* isHashedValue= */ false))),
+ Rule.DENY);
+ RuleSerializer binarySerializer = new RuleBinarySerializer();
+ String expectedBits =
+ START_BIT
+ + COMPOUND_FORMULA_START_BITS
+ + AND
+ + ATOMIC_FORMULA_START_BITS
+ + PACKAGE_NAME
+ + EQ
+ + IS_NOT_HASHED
+ + getBits(packageName.length(), VALUE_SIZE_BITS)
+ + getValueBits(packageName)
+ + ATOMIC_FORMULA_START_BITS
+ + APP_CERTIFICATE
+ + EQ
+ + IS_NOT_HASHED
+ + getBits(appCertificate.length(), VALUE_SIZE_BITS)
+ + getValueBits(appCertificate)
+ + COMPOUND_FORMULA_END_BITS
+ + DENY
+ + END_BIT;
+ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+ byteArrayOutputStream.write(DEFAULT_FORMAT_VERSION_BYTES);
+ byteArrayOutputStream.write(getBytes(expectedBits));
+ byte[] expectedRules = byteArrayOutputStream.toByteArray();
+
+ byte[] actualRules =
+ binarySerializer.serialize(
+ Collections.singletonList(rule), /* formatVersion= */ Optional.empty());
+
+ assertThat(actualRules).isEqualTo(expectedRules);
+ }
+
+ @Test
+ public void testBinaryString_serializeValidCompoundFormula_orConnector() throws Exception {
+ String packageName = "com.test.app";
+ String appCertificate = "test_cert";
+ Rule rule =
+ new Rule(
+ new CompoundFormula(
+ CompoundFormula.OR,
+ Arrays.asList(
+ new AtomicFormula.StringAtomicFormula(
+ AtomicFormula.PACKAGE_NAME,
+ packageName,
+ /* isHashedValue= */ false),
+ new AtomicFormula.StringAtomicFormula(
+ AtomicFormula.APP_CERTIFICATE,
+ appCertificate,
+ /* isHashedValue= */ false))),
+ Rule.DENY);
+ RuleSerializer binarySerializer = new RuleBinarySerializer();
+ String expectedBits =
+ START_BIT
+ + COMPOUND_FORMULA_START_BITS
+ + OR
+ + ATOMIC_FORMULA_START_BITS
+ + PACKAGE_NAME
+ + EQ
+ + IS_NOT_HASHED
+ + getBits(packageName.length(), VALUE_SIZE_BITS)
+ + getValueBits(packageName)
+ + ATOMIC_FORMULA_START_BITS
+ + APP_CERTIFICATE
+ + EQ
+ + IS_NOT_HASHED
+ + getBits(appCertificate.length(), VALUE_SIZE_BITS)
+ + getValueBits(appCertificate)
+ + COMPOUND_FORMULA_END_BITS
+ + DENY
+ + END_BIT;
+ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+ byteArrayOutputStream.write(DEFAULT_FORMAT_VERSION_BYTES);
+ byteArrayOutputStream.write(getBytes(expectedBits));
+ byte[] expectedRules = byteArrayOutputStream.toByteArray();
+
+ byte[] actualRules =
+ binarySerializer.serialize(
+ Collections.singletonList(rule), /* formatVersion= */ Optional.empty());
+
+ assertThat(actualRules).isEqualTo(expectedRules);
+ }
+
+ @Test
+ public void testBinaryString_serializeValidAtomicFormula_stringValue() throws Exception {
+ String packageName = "com.test.app";
+ Rule rule =
+ new Rule(
+ new AtomicFormula.StringAtomicFormula(
+ AtomicFormula.PACKAGE_NAME,
+ packageName,
+ /* isHashedValue= */ false),
+ Rule.DENY);
+ RuleSerializer binarySerializer = new RuleBinarySerializer();
+ String expectedBits =
+ START_BIT
+ + ATOMIC_FORMULA_START_BITS
+ + PACKAGE_NAME
+ + EQ
+ + IS_NOT_HASHED
+ + getBits(packageName.length(), VALUE_SIZE_BITS)
+ + getValueBits(packageName)
+ + DENY
+ + END_BIT;
+ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+ byteArrayOutputStream.write(DEFAULT_FORMAT_VERSION_BYTES);
+ byteArrayOutputStream.write(getBytes(expectedBits));
+ byte[] expectedRules = byteArrayOutputStream.toByteArray();
+
+ byte[] actualRules =
+ binarySerializer.serialize(
+ Collections.singletonList(rule), /* formatVersion= */ Optional.empty());
+
+ assertThat(actualRules).isEqualTo(expectedRules);
+ }
+
+ @Test
+ public void testBinaryString_serializeValidAtomicFormula_integerValue() throws Exception {
+ String versionCode = "1";
+ Rule rule =
+ new Rule(
+ new AtomicFormula.IntAtomicFormula(
+ AtomicFormula.VERSION_CODE,
+ AtomicFormula.EQ,
+ Integer.parseInt(versionCode)),
+ Rule.DENY);
+ RuleSerializer binarySerializer = new RuleBinarySerializer();
+ String expectedBits =
+ START_BIT
+ + ATOMIC_FORMULA_START_BITS
+ + VERSION_CODE
+ + EQ
+ + IS_NOT_HASHED
+ + getBits(versionCode.length(), VALUE_SIZE_BITS)
+ + getValueBits(versionCode)
+ + DENY
+ + END_BIT;
+ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+ byteArrayOutputStream.write(DEFAULT_FORMAT_VERSION_BYTES);
+ byteArrayOutputStream.write(getBytes(expectedBits));
+ byte[] expectedRules = byteArrayOutputStream.toByteArray();
+
+ byte[] actualRules =
+ binarySerializer.serialize(
+ Collections.singletonList(rule), /* formatVersion= */ Optional.empty());
+
+ assertThat(actualRules).isEqualTo(expectedRules);
+ }
+
+ @Test
+ public void testBinaryString_serializeValidAtomicFormula_booleanValue() throws Exception {
+ String preInstalled = "1";
+ Rule rule =
+ new Rule(
+ new AtomicFormula.BooleanAtomicFormula(AtomicFormula.PRE_INSTALLED, true),
+ Rule.DENY);
+ RuleSerializer binarySerializer = new RuleBinarySerializer();
+ String expectedBits =
+ START_BIT
+ + ATOMIC_FORMULA_START_BITS
+ + PRE_INSTALLED
+ + EQ
+ + IS_NOT_HASHED
+ + getBits(preInstalled.length(), VALUE_SIZE_BITS)
+ + getValueBits(preInstalled)
+ + DENY
+ + END_BIT;
+ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+ byteArrayOutputStream.write(DEFAULT_FORMAT_VERSION_BYTES);
+ byteArrayOutputStream.write(getBytes(expectedBits));
+ byte[] expectedRules = byteArrayOutputStream.toByteArray();
+
+ byte[] actualRules =
+ binarySerializer.serialize(
+ Collections.singletonList(rule), /* formatVersion= */ Optional.empty());
+
+ assertThat(actualRules).isEqualTo(expectedRules);
+ }
+
+ @Test
+ public void testBinaryString_serializeInvalidFormulaType() throws Exception {
+ Formula invalidFormula = getInvalidFormula();
+ Rule rule = new Rule(invalidFormula, Rule.DENY);
+ RuleSerializer binarySerializer = new RuleBinarySerializer();
+
+ assertExpectException(
+ RuleSerializeException.class,
+ /* expectedExceptionMessageRegex= */ "Invalid formula type",
+ () ->
+ binarySerializer.serialize(
+ Collections.singletonList(rule),
+ /* formatVersion= */ Optional.empty()));
+ }
+
+ @Test
+ public void testBinaryString_serializeFormatVersion() throws Exception {
+ int formatVersion = 1;
+ RuleSerializer binarySerializer = new RuleBinarySerializer();
+ String expectedBits = getBits(formatVersion, FORMAT_VERSION_BITS);
+ byte[] expectedRules = getBytes(expectedBits);
+
+ byte[] actualRules =
+ binarySerializer.serialize(
+ Collections.emptyList(), /* formatVersion= */ Optional.of(formatVersion));
+
+ assertThat(actualRules).isEqualTo(expectedRules);
+ }
+
+ private static Formula getInvalidFormula() {
+ return new Formula() {
+ @Override
+ public boolean isSatisfied(AppInstallMetadata appInstallMetadata) {
+ return false;
+ }
+
+ @Override
+ public int getTag() {
+ return 0;
+ }
+
+ @Override
+ public int hashCode() {
+ return super.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return super.equals(obj);
+ }
+
+ @NonNull
+ @Override
+ protected Object clone() throws CloneNotSupportedException {
+ return super.clone();
+ }
+
+ @Override
+ public String toString() {
+ return super.toString();
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ super.finalize();
+ }
+ };
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/integrity/serializer/RuleXmlSerializerTest.java b/services/tests/servicestests/src/com/android/server/integrity/serializer/RuleXmlSerializerTest.java
index 5903b5a..ad74901 100644
--- a/services/tests/servicestests/src/com/android/server/integrity/serializer/RuleXmlSerializerTest.java
+++ b/services/tests/servicestests/src/com/android/server/integrity/serializer/RuleXmlSerializerTest.java
@@ -91,7 +91,7 @@
}
@Test
- public void testXmlStream_serializeValidOpenFormula() throws Exception {
+ public void testXmlStream_serializeValidCompoundFormula() throws Exception {
Rule rule =
new Rule(
new CompoundFormula(
@@ -134,7 +134,7 @@
}
@Test
- public void testXmlString_serializeValidOpenFormula_notConnector() throws Exception {
+ public void testXmlString_serializeValidCompoundFormula_notConnector() throws Exception {
Rule rule =
new Rule(
new CompoundFormula(
@@ -174,7 +174,7 @@
}
@Test
- public void testXmlString_serializeValidOpenFormula_andConnector() throws Exception {
+ public void testXmlString_serializeValidCompoundFormula_andConnector() throws Exception {
Rule rule =
new Rule(
new CompoundFormula(
@@ -224,7 +224,7 @@
}
@Test
- public void testXmlString_serializeValidOpenFormula_orConnector() throws Exception {
+ public void testXmlString_serializeValidCompoundFormula_orConnector() throws Exception {
Rule rule =
new Rule(
new CompoundFormula(
diff --git a/services/tests/servicestests/src/com/android/server/integrity/utils/TestUtils.java b/services/tests/servicestests/src/com/android/server/integrity/utils/TestUtils.java
new file mode 100644
index 0000000..e54410b
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/integrity/utils/TestUtils.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2019 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.integrity.utils;
+
+public class TestUtils {
+
+ public static String getBits(int component, int numOfBits) {
+ return String.format("%" + numOfBits + "s", Integer.toBinaryString(component))
+ .replace(' ', '0');
+ }
+
+ public static String getValueBits(String value) {
+ StringBuilder stringBuilder = new StringBuilder();
+ for (byte valueByte : value.getBytes()) {
+ stringBuilder.append(getBits(valueByte, /* numOfBits= */ 8));
+ }
+ return stringBuilder.toString();
+ }
+
+ public static byte[] getBytes(String bits) {
+ int bitStringSize = bits.length();
+ int numOfBytes = bitStringSize / 8;
+ if (bitStringSize % 8 != 0) {
+ numOfBytes++;
+ }
+ byte[] bytes = new byte[numOfBytes];
+ for (int i = 0; i < bits.length(); i++) {
+ if (bits.charAt(i) == '1') {
+ bytes[i / 8] |= 1 << (7 - (i % 8));
+ }
+ }
+ return bytes;
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/timedetector/SimpleTimeDetectorStrategyTest.java b/services/tests/servicestests/src/com/android/server/timedetector/SimpleTimeDetectorStrategyTest.java
index 8a7edf7..7a0a28d 100644
--- a/services/tests/servicestests/src/com/android/server/timedetector/SimpleTimeDetectorStrategyTest.java
+++ b/services/tests/servicestests/src/com/android/server/timedetector/SimpleTimeDetectorStrategyTest.java
@@ -640,9 +640,9 @@
private static PhoneTimeSuggestion createPhoneTimeSuggestion(int phoneId,
TimestampedValue<Long> utcTime) {
- PhoneTimeSuggestion timeSuggestion = new PhoneTimeSuggestion(phoneId);
- timeSuggestion.setUtcTime(utcTime);
- return timeSuggestion;
+ return new PhoneTimeSuggestion.Builder(phoneId)
+ .setUtcTime(utcTime)
+ .build();
}
private ManualTimeSuggestion createManualTimeSuggestion(long timeMillis) {
diff --git a/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorServiceTest.java b/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorServiceTest.java
index 9951e85..84b495f 100644
--- a/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorServiceTest.java
@@ -140,10 +140,10 @@
private static PhoneTimeSuggestion createPhoneTimeSuggestion() {
int phoneId = 1234;
- PhoneTimeSuggestion suggestion = new PhoneTimeSuggestion(phoneId);
TimestampedValue<Long> timeValue = new TimestampedValue<>(100L, 1_000_000L);
- suggestion.setUtcTime(timeValue);
- return suggestion;
+ return new PhoneTimeSuggestion.Builder(phoneId)
+ .setUtcTime(timeValue)
+ .build();
}
private static ManualTimeSuggestion createManualTimeSuggestion() {
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index 3bc3e49..e3981f6 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -8161,17 +8161,25 @@
return Collections.EMPTY_LIST;
}
- /** @hide */
- public List<String> getPackagesWithCarrierPrivilegesForAllPhones() {
+ /**
+ * Get the names of packages with carrier privileges for all the active subscriptions.
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
+ @NonNull
+ public List<String> getCarrierPrivilegedPackagesForAllActiveSubscriptions() {
try {
ITelephony telephony = getITelephony();
if (telephony != null) {
return telephony.getPackagesWithCarrierPrivilegesForAllPhones();
}
} catch (RemoteException ex) {
- Rlog.e(TAG, "getPackagesWithCarrierPrivilegesForAllPhones RemoteException", ex);
+ Rlog.e(TAG, "getCarrierPrivilegedPackagesForAllActiveSubscriptions RemoteException",
+ ex);
} catch (NullPointerException ex) {
- Rlog.e(TAG, "getPackagesWithCarrierPrivilegesForAllPhones NPE", ex);
+ Rlog.e(TAG, "getCarrierPrivilegedPackagesForAllActiveSubscriptions NPE", ex);
}
return Collections.EMPTY_LIST;
}