diff options
67 files changed, 3051 insertions, 784 deletions
diff --git a/apex/jobscheduler/framework/java/android/os/DeviceIdleManager.java b/apex/jobscheduler/framework/java/android/os/DeviceIdleManager.java index 8019d4fdb870..4c443349ea58 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 @@ public class DeviceIdleManager { } /** - * 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 7a3ed92c1556..4ffcf8ab6076 100644 --- a/apex/jobscheduler/framework/java/android/os/PowerWhitelistManager.java +++ b/apex/jobscheduler/framework/java/android/os/PowerWhitelistManager.java @@ -26,6 +26,8 @@ import android.content.Context; 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 @@ public class PowerWhitelistManager { } /** + * 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 bcd8be7b63e0..2f8b5130edb0 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 @@ public class AppStandbyController implements AppStandbyInternal { 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 b8dcb90057d2..40f3c45518bb 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 @@ android_app_certificate { 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 000000000000..c4e3c296f210 --- /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 000000000000..0aacebefaaca --- /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 000000000000..1b667949eeaa --- /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 000000000000..d15b93552ff4 --- /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 000000000000..5af478ba7fe6 --- /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 000000000000..b4470685f4fc --- /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 8533ab197148..eb506e8d6b85 100644 --- a/api/current.txt +++ b/api/current.txt @@ -29323,12 +29323,14 @@ package android.net { 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 @@ package android.system { 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 fe9c0d1a7ff2..c7bde93c61fd 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -6379,6 +6379,8 @@ package android.os { } 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 @@ package android.telephony { 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 3ddbbf82d9d7..3ce8c5ead4cb 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -1796,7 +1796,6 @@ package android.os { } 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 @@ package android.os { } 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 e8c0e1540e9c..d10a661c6d83 100644 --- a/cmds/statsd/src/StatsService.cpp +++ b/cmds/statsd/src/StatsService.cpp @@ -1295,7 +1295,18 @@ Status StatsService::registerPullAtomCallback(int32_t uid, int32_t atomTag, int6 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 6d40007826e7..8c98e7b96936 100644 --- a/cmds/statsd/src/StatsService.h +++ b/cmds/statsd/src/StatsService.h @@ -187,6 +187,13 @@ public: 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 52a1269798ca..69aae3d1e31c 100644 --- a/cmds/statsd/src/condition/CombinationConditionTracker.cpp +++ b/cmds/statsd/src/condition/CombinationConditionTracker.cpp @@ -86,7 +86,7 @@ bool CombinationConditionTracker::init(const vector<Predicate>& allConditionConf 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 e7d619a27607..471606da4d75 100644 --- a/core/java/android/app/timedetector/ManualTimeSuggestion.java +++ b/core/java/android/app/timedetector/ManualTimeSuggestion.java @@ -85,7 +85,8 @@ public final class ManualTimeSuggestion implements Parcelable { @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 233dbbc42f50..dd02af7a3ac7 100644 --- a/core/java/android/app/timedetector/PhoneTimeSuggestion.java +++ b/core/java/android/app/timedetector/PhoneTimeSuggestion.java @@ -23,7 +23,6 @@ import android.os.Parcelable; 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 @@ public final class PhoneTimeSuggestion implements Parcelable { }; 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 @@ public final class PhoneTimeSuggestion implements Parcelable { return mPhoneId; } - public void setUtcTime(@Nullable TimestampedValue<Long> utcTime) { - mUtcTime = utcTime; - } - @Nullable public TimestampedValue<Long> getUtcTime() { return mUtcTime; @@ -96,7 +96,8 @@ public final class PhoneTimeSuggestion implements Parcelable { @NonNull public List<String> getDebugInfo() { - return Collections.unmodifiableList(mDebugInfo); + return mDebugInfo == null + ? Collections.emptyList() : Collections.unmodifiableList(mDebugInfo); } /** @@ -104,11 +105,23 @@ public final class PhoneTimeSuggestion implements Parcelable { * 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 @@ public final class PhoneTimeSuggestion implements Parcelable { + ", 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 ddbfa81390e4..592ec09aa730 100644 --- a/core/java/android/content/res/loader/ResourceLoaderManager.java +++ b/core/java/android/content/res/loader/ResourceLoaderManager.java @@ -151,6 +151,7 @@ public class ResourceLoaderManager { 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 1c6a48434adc..bf4884aa96a7 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 @@ public class TrafficStats { 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 @@ public class TrafficStats { } } - /** {@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 29871b6cf017..5ebb9f2e4e90 100644 --- a/core/java/android/os/IStatsManager.aidl +++ b/core/java/android/os/IStatsManager.aidl @@ -202,6 +202,13 @@ interface IStatsManager { 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 6a29d485b997..5d003706ac83 100644 --- a/core/java/android/service/autofill/augmented/FillWindow.java +++ b/core/java/android/service/autofill/augmented/FillWindow.java @@ -242,6 +242,7 @@ public final class FillWindow implements AutoCloseable { 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 54446e1cf057..9c04b392b9d3 100644 --- a/core/java/android/view/autofill/AutofillManager.java +++ b/core/java/android/view/autofill/AutofillManager.java @@ -47,6 +47,7 @@ import android.os.SystemClock; 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.AccessibilityManager; 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 @@ public final class AutofillManager { } else if (sVerbose) { Log.v(TAG, "Ignoring visibility change on " + id + ": no tracked views"); } + } else if (!virtual && isVisible) { + startAutofillIfNeededLocked(view); } } } @@ -1238,9 +1242,11 @@ public final class AutofillManager { 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 @@ public final class AutofillManager { } } + @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 187ab46b2e08..eb0d9bf088af 100644 --- a/core/java/android/widget/Editor.java +++ b/core/java/android/widget/Editor.java @@ -152,6 +152,9 @@ public class Editor { // 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 @@ public class Editor { // 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 @@ public class Editor { @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 @@ public class Editor { 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 @@ public class Editor { } float getLastUpPositionX() { - return mLastUpPositionX; + return mTouchState.getLastUpX(); } float getLastUpPositionY() { - return mLastUpPositionY; + return mTouchState.getLastUpY(); } private long getLastTouchOffsets() { @@ -1279,6 +1273,9 @@ public class Editor { // 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 @@ public class Editor { } } - 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 @@ public class Editor { } return; } - updateTapState(event); + ViewConfiguration viewConfiguration = ViewConfiguration.get(mTextView.getContext()); + mTouchState.update(event, viewConfiguration); updateFloatingToolbarVisibility(event); if (hasSelectionController()) { @@ -1515,15 +1480,7 @@ public class Editor { 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 class Editor { 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 @@ public class Editor { } 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 @@ public class Editor { 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 @@ public class Editor { 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 class Editor { 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 @@ public class Editor { // 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 class Editor { } 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 @@ public class Editor { 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 @@ public class Editor { 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 @@ public class Editor { } } - 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 000000000000..f880939bee35 --- /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 90e8ef2c6423..ee169f25b778 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -10860,7 +10860,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener @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 e01e254b1f16..0c3d34e686dd 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.ActivityLifecycleMonitorRegistry 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.Executor 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 @@ class ResourceLoaderChangesTest : ResourceLoaderTestBase() { @Before @After fun disableOverlay() { -// enableOverlay(OVERLAY_PACKAGE, false) + enableOverlay(OVERLAY_PACKAGE, false) } @Test @@ -156,25 +154,32 @@ class ResourceLoaderChangesTest : ResourceLoaderTestBase() { // 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 000000000000..de6f8f7231fa --- /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 c9a86dcea84c..bee270e5f5c9 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 @@ public class PhoneTimeSuggestionTest { @Test public void testEquals() { - PhoneTimeSuggestion one = new PhoneTimeSuggestion(PHONE_ID); - assertEquals(one, one); - - PhoneTimeSuggestion two = new PhoneTimeSuggestion(PHONE_ID); - assertEquals(one, two); - assertEquals(two, one); - - one.setUtcTime(new TimestampedValue<>(1111L, 2222L)); - assertEquals(one, one); - - two.setUtcTime(new TimestampedValue<>(1111L, 2222L)); - 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 builder1 = new PhoneTimeSuggestion.Builder(PHONE_ID); + { + PhoneTimeSuggestion one = builder1.build(); + assertEquals(one, one); + } + + PhoneTimeSuggestion.Builder builder2 = new PhoneTimeSuggestion.Builder(PHONE_ID); + { + PhoneTimeSuggestion one = builder1.build(); + PhoneTimeSuggestion two = builder2.build(); + assertEquals(one, two); + assertEquals(two, one); + } + + builder1.setUtcTime(new TimestampedValue<>(1111L, 2222L)); + { + PhoneTimeSuggestion one = builder1.build(); + assertEquals(one, one); + } + + builder2.setUtcTime(new TimestampedValue<>(1111L, 2222L)); + { + PhoneTimeSuggestion one = builder1.build(); + PhoneTimeSuggestion two = builder2.build(); + assertEquals(one, two); + assertEquals(two, 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 000000000000..6d50e3aa3b8f --- /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 e2dea45e3406..78f7cffab650 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 1fe6141a53b7..95716c834483 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 c0053d194ff6..a6a3ce06324f 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 boolean mOnLeft; + private int mDotColor; private float mDotScale = 0f; - private boolean mShowDot; - private boolean mOnLeft; + private boolean mDotDrawn; - /** Same as value in Launcher3 IconShape */ - static final int DEFAULT_PATH_SIZE = 100; + private Rect mTempBounds = new Rect(); public BadgedImageView(Context context) { this(context, null); @@ -63,17 +89,19 @@ public class BadgedImageView extends ImageView { 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,46 +109,40 @@ public class BadgedImageView extends ImageView { 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); } /** - * Set whether the dot should appear on left or right side of the view. + * Sets the dot state, does not animate changes. */ - void setDotOnLeft(boolean onLeft) { - mOnLeft = onLeft; - invalidate(); - } - - boolean getDotOnLeft() { - return mOnLeft; + void setDotState(int state) { + mCurrentDotState = state; + if (state == DOT_STATE_SUPPRESSED_FOR_FLYOUT || state == DOT_STATE_DEFAULT) { + mDotScale = mBubble.showDot() ? 1f : 0f; + invalidate(); + } } /** - * Set whether the dot should show or not. + * Whether the dot should be hidden based on current dot state. */ - void setShowDot(boolean showDot) { - mShowDot = showDot; - invalidate(); + private boolean isDotHidden() { + return (mCurrentDotState == DOT_STATE_DEFAULT && !mBubble.showDot()) + || mCurrentDotState == DOT_STATE_SUPPRESSED_FOR_FLYOUT; } /** - * @return whether the dot is being displayed. + * Set whether the dot should appear on left or right side of the view. */ - boolean isShowingDot() { - return mShowDot; + void setDotOnLeft(boolean onLeft) { + mOnLeft = onLeft; + invalidate(); } /** * 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 @@ public class BadgedImageView extends ImageView { /** * @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 @@ public class BadgedImageView extends ImageView { } /** + * 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 @@ public class BadgedImageView extends ImageView { 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 46d060e5ebda..f3a7ca9c0a75 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 @@ class Bubble { 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 @@ class Bubble { return mAppName; } - public Drawable getUserBadgedAppIcon() { + Drawable getUserBadgedAppIcon() { return mUserBadgedAppIcon; } @@ -165,17 +166,15 @@ class Bubble { return BubbleExperimentConfig.isShortcutIntent(getBubbleIntent()); } - boolean isInflated() { - return mInflated; + void setBubbleIconFactory(BubbleIconFactory factory) { + mBubbleIconFactory = factory; } - void updateDotVisibility() { - if (mIconView != null) { - mIconView.updateDotVisibility(true /* animate */); - } + boolean isInflated() { + return mInflated; } - BubbleView getIconView() { + BadgedImageView getIconView() { return mIconView; } @@ -193,8 +192,9 @@ class Bubble { 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 @@ class Bubble { */ 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 @@ class Bubble { * 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 @@ class Bubble { 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 15bb3305ea52..dbb193669083 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 @@ public class BubbleController implements ConfigurationController.ConfigurationLi 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 class BubbleController implements ConfigurationController.ConfigurationLi */ 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 @@ public class BubbleController implements ConfigurationController.ConfigurationLi 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 @@ public class BubbleController implements ConfigurationController.ConfigurationLi // 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 @@ public class BubbleController implements ConfigurationController.ConfigurationLi 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 @@ public class BubbleController implements ConfigurationController.ConfigurationLi // 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 @@ public class BubbleController implements ConfigurationController.ConfigurationLi // 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 2ca993bd200a..034bff345d71 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 @@ public class BubbleData { 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 @@ public class BubbleData { 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 856b15e7135d..efc955d9bca4 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 @@ public class BubbleExpandedView extends LinearLayout implements View.OnClickList 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 fd7fff4112c8..e138d9387ca6 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 @@ public class BubbleExperimentConfig { 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 @@ public class BubbleExperimentConfig { } /** + * 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 @@ public class BubbleExperimentConfig { 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 @@ public class BubbleExperimentConfig { } } - 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 58f3f2211d81..78e98eb72fa3 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 @@ public class BubbleFlyoutView extends FrameLayout { 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 @@ public class BubbleFlyoutView extends FrameLayout { // 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 @@ public class BubbleFlyoutView extends FrameLayout { // 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 @@ public class BubbleFlyoutView extends FrameLayout { 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 @@ public class BubbleFlyoutView extends FrameLayout { 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 a1c77c0af6bc..9ff033cb3411 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 @@ import com.android.systemui.R; * 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 4a1bbe48efb0..29de2f049690 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 @@ package com.android.systemui.bubbles; 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 @@ public class BubbleStackView extends FrameLayout { * 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 @@ public class BubbleStackView extends FrameLayout { } /** - * 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 @@ public class BubbleStackView extends FrameLayout { } /** - * 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 @@ public class BubbleStackView extends FrameLayout { Bubble bubbleToExpand = mBubbleData.getBubbleWithKey(key); if (bubbleToExpand != null) { setSelectedBubble(bubbleToExpand); - bubbleToExpand.setShowInShadeWhenBubble(false); + bubbleToExpand.setShowInShade(false); setExpanded(true); } } @@ -746,8 +736,8 @@ public class BubbleStackView extends FrameLayout { 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 @@ public class BubbleStackView extends FrameLayout { 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 @@ public class BubbleStackView extends FrameLayout { } /** 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 @@ public class BubbleStackView extends FrameLayout { : 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 @@ public class BubbleStackView extends FrameLayout { @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 @@ public class BubbleStackView extends FrameLayout { 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 @@ public class BubbleStackView extends FrameLayout { 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 @@ public class BubbleStackView extends FrameLayout { 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 @@ public class BubbleStackView extends FrameLayout { 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 @@ public class BubbleStackView extends FrameLayout { action, getNormalizedXPosition(), getNormalizedYPosition(), - bubble.showInShadeWhenBubble(), + bubble.showInShade(), bubble.isOngoing(), false /* isAppForeground (unused) */); } @@ -1727,8 +1692,8 @@ public class BubbleStackView extends FrameLayout { 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 4240e06a8800..44e013a34f54 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 @@ class BubbleTouchHandler implements View.OnTouchListener { 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 @@ class BubbleTouchHandler implements View.OnTouchListener { 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 @@ class BubbleTouchHandler implements View.OnTouchListener { // 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 79807b388c8c..000000000000 --- 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 fedd855a858e..50413548dbba 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 @@ public class GlobalScreenshot { 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 @@ public class GlobalScreenshot { 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 class GlobalScreenshot { 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 d2268e12c662..76925b43cfb8 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 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { 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 5c71a57cba77..5575d10c7fe1 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 @@ public class StatusBar extends SystemUI implements DemoMode, 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 @@ public class StatusBar extends SystemUI implements DemoMode, createNavigationBar(result); - if (ENABLE_LOCKSCREEN_WALLPAPER) { + if (ENABLE_LOCKSCREEN_WALLPAPER && mWallpaperSupported) { mLockscreenWallpaper = mLockscreenWallpaperLazy.get(); mLockscreenWallpaper.setHandler(mHandler); } @@ -2807,7 +2808,9 @@ public class StatusBar extends SystemUI implements DemoMode, @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 8c9f75950eb4..4c707f45efc1 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 @@ public class BubbleControllerTest extends SysuiTestCase { 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 @@ public class BubbleControllerTest extends SysuiTestCase { 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 @@ public class BubbleControllerTest extends SysuiTestCase { 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 @@ public class BubbleControllerTest extends SysuiTestCase { 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 @@ public class BubbleControllerTest extends SysuiTestCase { 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 @@ public class BubbleControllerTest extends SysuiTestCase { 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 @@ public class BubbleControllerTest extends SysuiTestCase { 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 @@ public class BubbleControllerTest extends SysuiTestCase { 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 @@ public class BubbleControllerTest extends SysuiTestCase { 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 @@ public class BubbleControllerTest extends SysuiTestCase { 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 @@ public class BubbleControllerTest extends SysuiTestCase { 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 a9be30ba82a6..95c7af31865b 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 @@ public class BubbleDataTest extends SysuiTestCase { // Verify verifyUpdateReceived(); BubbleData.Update update = mUpdateCaptor.getValue(); - assertThat(update.addedBubble.showFlyoutForBubble()).isFalse(); + assertThat(update.addedBubble.showFlyout()).isFalse(); } @Test @@ -199,7 +199,7 @@ public class BubbleDataTest extends SysuiTestCase { // Verify verifyUpdateReceived(); BubbleData.Update update = mUpdateCaptor.getValue(); - assertThat(update.addedBubble.showFlyoutForBubble()).isTrue(); + assertThat(update.addedBubble.showFlyout()).isTrue(); } @Test @@ -218,7 +218,7 @@ public class BubbleDataTest extends SysuiTestCase { // Verify BubbleData.Update update = mUpdateCaptor.getValue(); - assertThat(update.updatedBubble.showFlyoutForBubble()).isFalse(); + assertThat(update.updatedBubble.showFlyout()).isFalse(); } @Test @@ -239,7 +239,7 @@ public class BubbleDataTest extends SysuiTestCase { // 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 a8961a85c4c7..376ecf7815ac 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 @@ public class BubbleFlyoutViewTest extends SysuiTestCase { @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 class BubbleFlyoutViewTest extends SysuiTestCase { 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 @@ public class BubbleFlyoutViewTest extends SysuiTestCase { @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 d5f795683675..135f6f3ea125 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.RemoteException; 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 @@ public final class PinnerService extends SystemService { 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 000000000000..09bc7e8b9861 --- /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 000000000000..ecb9189b2a0d --- /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 000000000000..d47ce2df45e1 --- /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 aad177e81c77..8aa0751af11c 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 2e99d0f92109..d405583442bd 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 @@ public final class RuleXmlParser implements RuleParser { 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 @@ public final class RuleXmlParser implements RuleParser { 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 @@ public final class RuleXmlParser implements RuleParser { 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 @@ public final class RuleXmlParser implements RuleParser { 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 @@ public final class RuleXmlParser implements RuleParser { 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 d4f41eb5c9e0..b988fd4c40f1 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 3ec9cf294136..72068ceeb4f0 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 @@ public class RuleXmlSerializer implements RuleSerializer { 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 @@ public class RuleXmlSerializer implements RuleSerializer { 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 @@ public class RuleXmlSerializer implements RuleSerializer { 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 0b970bfc0076..340fe3dbaa97 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 @@ public final class SimpleTimeDetectorStrategy implements TimeDetectorStrategy { 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 @@ public final class SimpleTimeDetectorStrategy implements TimeDetectorStrategy { if (mCallback.isAutoTimeDetectionEnabled()) { if (DBG) { Slog.d(LOG_TAG, "Auto time detection is enabled." - + " time=" + time + + " origin=" + origin + + ", time=" + time + ", cause=" + cause); } return; @@ -232,23 +234,24 @@ public final class SimpleTimeDetectorStrategy implements TimeDetectorStrategy { @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=" - + mLastAutoSystemClockTimeSendNetworkBroadcast); - IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " "); - - ipw.println("TimeDetectorStrategyImpl logs:"); + ipw.println("TimeDetectorStrategy:"); ipw.increaseIndent(); // level 1 + ipw.println("mLastPhoneSuggestion=" + mLastPhoneSuggestion); + ipw.println("mLastAutoSystemClockTimeSet=" + mLastAutoSystemClockTimeSet); + ipw.println("mLastAutoSystemClockTime=" + mLastAutoSystemClockTime); + ipw.println("mLastAutoSystemClockTimeSendNetworkBroadcast=" + + mLastAutoSystemClockTimeSendNetworkBroadcast); + + ipw.println("Time change log:"); ipw.increaseIndent(); // level 2 mTimeChangesLog.dump(ipw); 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 32cee2de3d75..0a6c2e776072 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 @@ public interface TimeDetectorStrategy { /** * 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 fb42507f87c2..3a07a692ecdd 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 000000000000..88b6d70688ad --- /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 000000000000..901277ded5dd --- /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 5903b5a11d22..ad74901ece2c 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 @@ public class RuleXmlSerializerTest { } @Test - public void testXmlStream_serializeValidOpenFormula() throws Exception { + public void testXmlStream_serializeValidCompoundFormula() throws Exception { Rule rule = new Rule( new CompoundFormula( @@ -134,7 +134,7 @@ public class RuleXmlSerializerTest { } @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 @@ public class RuleXmlSerializerTest { } @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 @@ public class RuleXmlSerializerTest { } @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 000000000000..e54410b04b79 --- /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 8a7edf746340..7a0a28dfbd16 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 @@ public class SimpleTimeDetectorStrategyTest { 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 9951e8597a8b..84b495f14c1f 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 @@ public class TimeDetectorServiceTest { 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 3bc3e49e6433..e3981f6abf34 100644 --- a/telephony/java/android/telephony/TelephonyManager.java +++ b/telephony/java/android/telephony/TelephonyManager.java @@ -8161,17 +8161,25 @@ public class TelephonyManager { 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; } |