Merge "Move logic from QSCarrierGroup into Controller."
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
index 3aef5d1..3fdc571 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
@@ -50,12 +50,11 @@
import android.os.RemoteException;
import android.os.UserHandle;
import android.provider.Settings;
-import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.KeyValueListParser;
import android.util.Log;
import android.util.Slog;
-import android.util.SparseArray;
+import android.util.SparseArrayMap;
import android.util.SparseBooleanArray;
import android.util.SparseSetArray;
import android.util.proto.ProtoOutputStream;
@@ -114,92 +113,6 @@
private static final String ALARM_TAG_QUOTA_CHECK = "*job.quota_check*";
/**
- * A sparse array of ArrayMaps, which is suitable for holding (userId, packageName)->object
- * associations.
- */
- private static class UserPackageMap<T> {
- private final SparseArray<ArrayMap<String, T>> mData = new SparseArray<>();
-
- public void add(int userId, @NonNull String packageName, @Nullable T obj) {
- ArrayMap<String, T> data = mData.get(userId);
- if (data == null) {
- data = new ArrayMap<String, T>();
- mData.put(userId, data);
- }
- data.put(packageName, obj);
- }
-
- public void clear() {
- for (int i = 0; i < mData.size(); ++i) {
- mData.valueAt(i).clear();
- }
- }
-
- /** Removes all the data for the user, if there was any. */
- public void delete(int userId) {
- mData.delete(userId);
- }
-
- /** Removes the data for the user and package, if there was any. */
- public void delete(int userId, @NonNull String packageName) {
- ArrayMap<String, T> data = mData.get(userId);
- if (data != null) {
- data.remove(packageName);
- }
- }
-
- @Nullable
- public T get(int userId, @NonNull String packageName) {
- ArrayMap<String, T> data = mData.get(userId);
- if (data != null) {
- return data.get(packageName);
- }
- return null;
- }
-
- /** @see SparseArray#indexOfKey */
- public int indexOfKey(int userId) {
- return mData.indexOfKey(userId);
- }
-
- /** Returns the userId at the given index. */
- public int keyAt(int index) {
- return mData.keyAt(index);
- }
-
- /** Returns the package name at the given index. */
- @NonNull
- public String keyAt(int userIndex, int packageIndex) {
- return mData.valueAt(userIndex).keyAt(packageIndex);
- }
-
- /** Returns the size of the outer (userId) array. */
- public int numUsers() {
- return mData.size();
- }
-
- public int numPackagesForUser(int userId) {
- ArrayMap<String, T> data = mData.get(userId);
- return data == null ? 0 : data.size();
- }
-
- /** Returns the value T at the given user and index. */
- @Nullable
- public T valueAt(int userIndex, int packageIndex) {
- return mData.valueAt(userIndex).valueAt(packageIndex);
- }
-
- public void forEach(Consumer<T> consumer) {
- for (int i = numUsers() - 1; i >= 0; --i) {
- ArrayMap<String, T> data = mData.valueAt(i);
- for (int j = data.size() - 1; j >= 0; --j) {
- consumer.accept(data.valueAt(j));
- }
- }
- }
- }
-
- /**
* Standardize the output of userId-packageName combo.
*/
private static String string(int userId, String packageName) {
@@ -378,22 +291,22 @@
}
/** List of all tracked jobs keyed by source package-userId combo. */
- private final UserPackageMap<ArraySet<JobStatus>> mTrackedJobs = new UserPackageMap<>();
+ private final SparseArrayMap<ArraySet<JobStatus>> mTrackedJobs = new SparseArrayMap<>();
/** Timer for each package-userId combo. */
- private final UserPackageMap<Timer> mPkgTimers = new UserPackageMap<>();
+ private final SparseArrayMap<Timer> mPkgTimers = new SparseArrayMap<>();
/** List of all timing sessions for a package-userId combo, in chronological order. */
- private final UserPackageMap<List<TimingSession>> mTimingSessions = new UserPackageMap<>();
+ private final SparseArrayMap<List<TimingSession>> mTimingSessions = new SparseArrayMap<>();
/**
* List of alarm listeners for each package that listen for when each package comes back within
* quota.
*/
- private final UserPackageMap<QcAlarmListener> mInQuotaAlarmListeners = new UserPackageMap<>();
+ private final SparseArrayMap<QcAlarmListener> mInQuotaAlarmListeners = new SparseArrayMap<>();
/** Cached calculation results for each app, with the standby buckets as the array indices. */
- private final UserPackageMap<ExecutionStats[]> mExecutionStatsCache = new UserPackageMap<>();
+ private final SparseArrayMap<ExecutionStats[]> mExecutionStatsCache = new SparseArrayMap<>();
/** List of UIDs currently in the foreground. */
private final SparseBooleanArray mForegroundUids = new SparseBooleanArray();
@@ -1206,9 +1119,9 @@
private void maybeUpdateAllConstraintsLocked() {
boolean changed = false;
- for (int u = 0; u < mTrackedJobs.numUsers(); ++u) {
+ for (int u = 0; u < mTrackedJobs.numMaps(); ++u) {
final int userId = mTrackedJobs.keyAt(u);
- for (int p = 0; p < mTrackedJobs.numPackagesForUser(userId); ++p) {
+ for (int p = 0; p < mTrackedJobs.numElementsForKey(userId); ++p) {
final String packageName = mTrackedJobs.keyAt(u, p);
changed |= maybeUpdateConstraintForPkgLocked(userId, packageName);
}
@@ -1268,7 +1181,7 @@
}
private class UidConstraintUpdater implements Consumer<JobStatus> {
- private final UserPackageMap<Integer> mToScheduleStartAlarms = new UserPackageMap<>();
+ private final SparseArrayMap<Integer> mToScheduleStartAlarms = new SparseArrayMap<>();
public boolean wasJobChanged;
@Override
@@ -1290,9 +1203,9 @@
}
void postProcess() {
- for (int u = 0; u < mToScheduleStartAlarms.numUsers(); ++u) {
+ for (int u = 0; u < mToScheduleStartAlarms.numMaps(); ++u) {
final int userId = mToScheduleStartAlarms.keyAt(u);
- for (int p = 0; p < mToScheduleStartAlarms.numPackagesForUser(userId); ++p) {
+ for (int p = 0; p < mToScheduleStartAlarms.numElementsForKey(userId); ++p) {
final String packageName = mToScheduleStartAlarms.keyAt(u, p);
final int standbyBucket = mToScheduleStartAlarms.get(userId, packageName);
maybeScheduleStartAlarmLocked(userId, packageName, standbyBucket);
@@ -2547,9 +2460,9 @@
});
pw.println();
- for (int u = 0; u < mPkgTimers.numUsers(); ++u) {
+ for (int u = 0; u < mPkgTimers.numMaps(); ++u) {
final int userId = mPkgTimers.keyAt(u);
- for (int p = 0; p < mPkgTimers.numPackagesForUser(userId); ++p) {
+ for (int p = 0; p < mPkgTimers.numElementsForKey(userId); ++p) {
final String pkgName = mPkgTimers.keyAt(u, p);
mPkgTimers.valueAt(u, p).dump(pw, predicate);
pw.println();
@@ -2571,9 +2484,9 @@
pw.println("Cached execution stats:");
pw.increaseIndent();
- for (int u = 0; u < mExecutionStatsCache.numUsers(); ++u) {
+ for (int u = 0; u < mExecutionStatsCache.numMaps(); ++u) {
final int userId = mExecutionStatsCache.keyAt(u);
- for (int p = 0; p < mExecutionStatsCache.numPackagesForUser(userId); ++p) {
+ for (int p = 0; p < mExecutionStatsCache.numElementsForKey(userId); ++p) {
final String pkgName = mExecutionStatsCache.keyAt(u, p);
ExecutionStats[] stats = mExecutionStatsCache.valueAt(u, p);
@@ -2595,9 +2508,9 @@
pw.println();
pw.println("In quota alarms:");
pw.increaseIndent();
- for (int u = 0; u < mInQuotaAlarmListeners.numUsers(); ++u) {
+ for (int u = 0; u < mInQuotaAlarmListeners.numMaps(); ++u) {
final int userId = mInQuotaAlarmListeners.keyAt(u);
- for (int p = 0; p < mInQuotaAlarmListeners.numPackagesForUser(userId); ++p) {
+ for (int p = 0; p < mInQuotaAlarmListeners.numElementsForKey(userId); ++p) {
final String pkgName = mInQuotaAlarmListeners.keyAt(u, p);
QcAlarmListener alarmListener = mInQuotaAlarmListeners.valueAt(u, p);
@@ -2667,9 +2580,9 @@
}
});
- for (int u = 0; u < mPkgTimers.numUsers(); ++u) {
+ for (int u = 0; u < mPkgTimers.numMaps(); ++u) {
final int userId = mPkgTimers.keyAt(u);
- for (int p = 0; p < mPkgTimers.numPackagesForUser(userId); ++p) {
+ for (int p = 0; p < mPkgTimers.numElementsForKey(userId); ++p) {
final String pkgName = mPkgTimers.keyAt(u, p);
final long psToken = proto.start(
StateControllerProto.QuotaController.PACKAGE_STATS);
diff --git a/api/current.txt b/api/current.txt
index 8c27430..85cc06c 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -48769,6 +48769,7 @@
method public void append(int, E);
method public void clear();
method public android.util.SparseArray<E> clone();
+ method public boolean contains(int);
method public void delete(int);
method public E get(int);
method public E get(int, E);
diff --git a/api/test-current.txt b/api/test-current.txt
index c6b02b9..19e1212 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -4245,6 +4245,25 @@
field public static final String SETTINGS_WIFITRACKER2 = "settings_wifitracker2";
}
+ public class SparseArrayMap<T> {
+ ctor public SparseArrayMap();
+ method public void add(int, @NonNull String, @Nullable T);
+ method public void clear();
+ method public boolean contains(int, @NonNull String);
+ method public void delete(int);
+ method @Nullable public T delete(int, @NonNull String);
+ method public void forEach(@NonNull java.util.function.Consumer<T>);
+ method @Nullable public T get(int, @NonNull String);
+ method @Nullable public T getOrDefault(int, @NonNull String, T);
+ method public int indexOfKey(int);
+ method public int indexOfKey(int, @NonNull String);
+ method public int keyAt(int);
+ method @NonNull public String keyAt(int, int);
+ method public int numElementsForKey(int);
+ method public int numMaps();
+ method @Nullable public T valueAt(int, int);
+ }
+
public class TimeUtils {
method public static String formatDuration(long);
}
diff --git a/cmds/statsd/src/StatsLogProcessor.cpp b/cmds/statsd/src/StatsLogProcessor.cpp
index 91cadc9..98d41c2 100644
--- a/cmds/statsd/src/StatsLogProcessor.cpp
+++ b/cmds/statsd/src/StatsLogProcessor.cpp
@@ -328,11 +328,6 @@
mAnomalyAlarmMonitor, mPeriodicAlarmMonitor);
if (newMetricsManager->isConfigValid()) {
mUidMap->OnConfigUpdated(key);
- if (newMetricsManager->shouldAddUidMapListener()) {
- // We have to add listener after the MetricsManager is constructed because it's
- // not safe to create wp or sp from this pointer inside its constructor.
- mUidMap->addListener(newMetricsManager.get());
- }
newMetricsManager->refreshTtl(timestampNs);
mMetricsManagers[key] = newMetricsManager;
VLOG("StatsdConfig valid");
@@ -753,6 +748,32 @@
}
}
+void StatsLogProcessor::notifyAppUpgrade(const int64_t& eventTimeNs, const string& apk,
+ const int uid, const int64_t version) {
+ std::lock_guard<std::mutex> lock(mMetricsMutex);
+ ALOGW("Received app upgrade");
+ for (auto it : mMetricsManagers) {
+ it.second->notifyAppUpgrade(eventTimeNs, apk, uid, version);
+ }
+}
+
+void StatsLogProcessor::notifyAppRemoved(const int64_t& eventTimeNs, const string& apk,
+ const int uid) {
+ std::lock_guard<std::mutex> lock(mMetricsMutex);
+ ALOGW("Received app removed");
+ for (auto it : mMetricsManagers) {
+ it.second->notifyAppRemoved(eventTimeNs, apk, uid);
+ }
+}
+
+void StatsLogProcessor::onUidMapReceived(const int64_t& eventTimeNs) {
+ std::lock_guard<std::mutex> lock(mMetricsMutex);
+ ALOGW("Received uid map");
+ for (auto it : mMetricsManagers) {
+ it.second->onUidMapReceived(eventTimeNs);
+ }
+}
+
void StatsLogProcessor::noteOnDiskData(const ConfigKey& key) {
std::lock_guard<std::mutex> lock(mMetricsMutex);
mOnDiskDataConfigs.insert(key);
diff --git a/cmds/statsd/src/StatsLogProcessor.h b/cmds/statsd/src/StatsLogProcessor.h
index b41771d..68a51ef 100644
--- a/cmds/statsd/src/StatsLogProcessor.h
+++ b/cmds/statsd/src/StatsLogProcessor.h
@@ -32,7 +32,7 @@
namespace statsd {
-class StatsLogProcessor : public ConfigListener {
+class StatsLogProcessor : public ConfigListener, public virtual PackageInfoListener {
public:
StatsLogProcessor(const sp<UidMap>& uidMap, const sp<StatsPullerManager>& pullerManager,
const sp<AlarmMonitor>& anomalyAlarmMonitor,
@@ -91,6 +91,16 @@
/* Sets the active status/ttl for all configs and metrics to the status in ActiveConfigList. */
void SetConfigsActiveState(const ActiveConfigList& activeConfigList, int64_t currentTimeNs);
+ /* Notify all MetricsManagers of app upgrades */
+ void notifyAppUpgrade(const int64_t& eventTimeNs, const string& apk, const int uid,
+ const int64_t version) override;
+
+ /* Notify all MetricsManagers of app removals */
+ void notifyAppRemoved(const int64_t& eventTimeNs, const string& apk, const int uid) override;
+
+ /* Notify all MetricsManagers of uid map snapshots received */
+ void onUidMapReceived(const int64_t& eventTimeNs) override;
+
// Reset all configs.
void resetConfigs();
diff --git a/cmds/statsd/src/StatsService.cpp b/cmds/statsd/src/StatsService.cpp
index f072c9c..2c325ba6 100644
--- a/cmds/statsd/src/StatsService.cpp
+++ b/cmds/statsd/src/StatsService.cpp
@@ -200,6 +200,7 @@
}
});
+ mUidMap->setListener(mProcessor);
mConfigManager->AddListener(mProcessor);
init_system_properties();
diff --git a/cmds/statsd/src/metrics/MetricProducer.h b/cmds/statsd/src/metrics/MetricProducer.h
index d7cbcc8..a513db6 100644
--- a/cmds/statsd/src/metrics/MetricProducer.h
+++ b/cmds/statsd/src/metrics/MetricProducer.h
@@ -87,7 +87,7 @@
// writing the report to dropbox. MetricProducers should respond to package changes as required in
// PackageInfoListener, but if none of the metrics are slicing by package name, then the update can
// be a no-op.
-class MetricProducer : public virtual PackageInfoListener, public virtual StateListener {
+class MetricProducer : public virtual android::RefBase, public virtual StateListener {
public:
MetricProducer(const int64_t& metricId, const ConfigKey& key, const int64_t timeBaseNs,
const int conditionIndex, const sp<ConditionWizard>& wizard,
@@ -109,8 +109,8 @@
* the flush again when the end timestamp is forced to be now, and then after flushing, update
* the start timestamp to be now.
*/
- void notifyAppUpgrade(const int64_t& eventTimeNs, const string& apk, const int uid,
- const int64_t version) override {
+ virtual void notifyAppUpgrade(const int64_t& eventTimeNs, const string& apk, const int uid,
+ const int64_t version) {
std::lock_guard<std::mutex> lock(mMutex);
if (eventTimeNs > getCurrentBucketEndTimeNs()) {
@@ -123,16 +123,11 @@
// is a partial bucket and can merge it with the previous bucket.
};
- void notifyAppRemoved(const int64_t& eventTimeNs, const string& apk, const int uid) override{
+ void notifyAppRemoved(const int64_t& eventTimeNs, const string& apk, const int uid) {
// Force buckets to split on removal also.
notifyAppUpgrade(eventTimeNs, apk, uid, 0);
};
- void onUidMapReceived(const int64_t& eventTimeNs) override{
- // Purposefully don't flush partial buckets on a new snapshot.
- // This occurs if a new user is added/removed or statsd crashes.
- };
-
// Consume the parsed stats log entry that already matched the "what" of the metric.
void onMatchedLogEvent(const size_t matcherIndex, const LogEvent& event) {
std::lock_guard<std::mutex> lock(mMutex);
diff --git a/cmds/statsd/src/metrics/MetricsManager.cpp b/cmds/statsd/src/metrics/MetricsManager.cpp
index 7bae4b9..464cec3 100644
--- a/cmds/statsd/src/metrics/MetricsManager.cpp
+++ b/cmds/statsd/src/metrics/MetricsManager.cpp
@@ -182,6 +182,10 @@
void MetricsManager::notifyAppUpgrade(const int64_t& eventTimeNs, const string& apk, const int uid,
const int64_t version) {
+ // Inform all metric producers.
+ for (auto it : mAllMetricProducers) {
+ it->notifyAppUpgrade(eventTimeNs, apk, uid, version);
+ }
// check if we care this package
if (std::find(mAllowedPkg.begin(), mAllowedPkg.end(), apk) == mAllowedPkg.end()) {
return;
@@ -193,6 +197,10 @@
void MetricsManager::notifyAppRemoved(const int64_t& eventTimeNs, const string& apk,
const int uid) {
+ // Inform all metric producers.
+ for (auto it : mAllMetricProducers) {
+ it->notifyAppRemoved(eventTimeNs, apk, uid);
+ }
// check if we care this package
if (std::find(mAllowedPkg.begin(), mAllowedPkg.end(), apk) == mAllowedPkg.end()) {
return;
@@ -203,6 +211,9 @@
}
void MetricsManager::onUidMapReceived(const int64_t& eventTimeNs) {
+ // Purposefully don't inform metric producers on a new snapshot
+ // because we don't need to flush partial buckets.
+ // This occurs if a new user is added/removed or statsd crashes.
if (mAllowedPkg.size() == 0) {
return;
}
diff --git a/cmds/statsd/src/metrics/MetricsManager.h b/cmds/statsd/src/metrics/MetricsManager.h
index 286610a..1fda696 100644
--- a/cmds/statsd/src/metrics/MetricsManager.h
+++ b/cmds/statsd/src/metrics/MetricsManager.h
@@ -35,7 +35,7 @@
namespace statsd {
// A MetricsManager is responsible for managing metrics from one single config source.
-class MetricsManager : public PackageInfoListener {
+class MetricsManager : public virtual android::RefBase {
public:
MetricsManager(const ConfigKey& configKey, const StatsdConfig& config, const int64_t timeBaseNs,
const int64_t currentTimeNs, const sp<UidMap>& uidMap,
@@ -63,15 +63,11 @@
unordered_set<sp<const InternalAlarm>, SpHash<InternalAlarm>>& alarmSet);
void notifyAppUpgrade(const int64_t& eventTimeNs, const string& apk, const int uid,
- const int64_t version) override;
+ const int64_t version);
- void notifyAppRemoved(const int64_t& eventTimeNs, const string& apk, const int uid) override;
+ void notifyAppRemoved(const int64_t& eventTimeNs, const string& apk, const int uid);
- void onUidMapReceived(const int64_t& eventTimeNs) override;
-
- bool shouldAddUidMapListener() const {
- return !mAllowedPkg.empty();
- }
+ void onUidMapReceived(const int64_t& eventTimeNs);
bool shouldWriteToDisk() const {
return mNoReportMetricIds.size() != mAllMetricProducers.size();
diff --git a/cmds/statsd/src/metrics/metrics_manager_util.cpp b/cmds/statsd/src/metrics/metrics_manager_util.cpp
index 6e76717..9131802 100644
--- a/cmds/statsd/src/metrics/metrics_manager_util.cpp
+++ b/cmds/statsd/src/metrics/metrics_manager_util.cpp
@@ -396,7 +396,7 @@
}
bool initMetrics(const ConfigKey& key, const StatsdConfig& config, const int64_t timeBaseTimeNs,
- const int64_t currentTimeNs, UidMap& uidMap,
+ const int64_t currentTimeNs,
const sp<StatsPullerManager>& pullerManager,
const unordered_map<int64_t, int>& logTrackerMap,
const unordered_map<int64_t, int>& conditionTrackerMap,
@@ -788,8 +788,6 @@
noReportMetricIds.insert(no_report_metric);
}
for (const auto& it : allMetricProducers) {
- uidMap.addListener(it);
-
// Register metrics to StateTrackers
for (int atomId : it->getSlicedStateAtoms()) {
if (!StateManager::getInstance().registerListener(atomId, it)) {
@@ -939,7 +937,7 @@
ALOGE("initStates failed");
return false;
}
- if (!initMetrics(key, config, timeBaseNs, currentTimeNs, uidMap, pullerManager, logTrackerMap,
+ if (!initMetrics(key, config, timeBaseNs, currentTimeNs, pullerManager, logTrackerMap,
conditionTrackerMap, allAtomMatchers, stateAtomIdMap, allStateGroupMaps,
allConditionTrackers, allMetricProducers,
conditionToMetricMap, trackerToMetricMap, metricProducerMap,
diff --git a/cmds/statsd/src/packages/UidMap.cpp b/cmds/statsd/src/packages/UidMap.cpp
index d4b57dd..7e63bbf 100644
--- a/cmds/statsd/src/packages/UidMap.cpp
+++ b/cmds/statsd/src/packages/UidMap.cpp
@@ -119,7 +119,7 @@
void UidMap::updateMap(const int64_t& timestamp, const vector<int32_t>& uid,
const vector<int64_t>& versionCode, const vector<String16>& versionString,
const vector<String16>& packageName, const vector<String16>& installer) {
- vector<wp<PackageInfoListener>> broadcastList;
+ wp<PackageInfoListener> broadcast = NULL;
{
lock_guard<mutex> lock(mMutex); // Exclusively lock for updates.
@@ -150,25 +150,22 @@
ensureBytesUsedBelowLimit();
StatsdStats::getInstance().setCurrentUidMapMemory(mBytesUsed);
- getListenerListCopyLocked(&broadcastList);
+ broadcast = mSubscriber;
}
// To avoid invoking callback while holding the internal lock. we get a copy of the listener
- // list and invoke the callback. It's still possible that after we copy the list, a
- // listener removes itself before we call it. It's then the listener's job to handle it (expect
- // the callback to be called after listener is removed, and the listener should properly
- // ignore it).
- for (const auto& weakPtr : broadcastList) {
- auto strongPtr = weakPtr.promote();
- if (strongPtr != NULL) {
- strongPtr->onUidMapReceived(timestamp);
- }
+ // and invoke the callback. It's still possible that after we copy the listener, it removes
+ // itself before we call it. It's then the listener's job to handle it (expect the callback to
+ // be called after listener is removed, and the listener should properly ignore it).
+ auto strongPtr = broadcast.promote();
+ if (strongPtr != NULL) {
+ strongPtr->onUidMapReceived(timestamp);
}
}
void UidMap::updateApp(const int64_t& timestamp, const String16& app_16, const int32_t& uid,
const int64_t& versionCode, const String16& versionString,
const String16& installer) {
- vector<wp<PackageInfoListener>> broadcastList;
+ wp<PackageInfoListener> broadcast = NULL;
string appName = string(String8(app_16).string());
{
lock_guard<mutex> lock(mMutex);
@@ -195,7 +192,7 @@
// for the first time, then we don't notify the listeners.
// It's also OK to split again if we're forming a partial bucket after re-installing an
// app after deletion.
- getListenerListCopyLocked(&broadcastList);
+ broadcast = mSubscriber;
}
mChanges.emplace_back(false, timestamp, appName, uid, versionCode, newVersionString,
prevVersion, prevVersionString);
@@ -205,11 +202,9 @@
StatsdStats::getInstance().setUidMapChanges(mChanges.size());
}
- for (const auto& weakPtr : broadcastList) {
- auto strongPtr = weakPtr.promote();
- if (strongPtr != NULL) {
- strongPtr->notifyAppUpgrade(timestamp, appName, uid, versionCode);
- }
+ auto strongPtr = broadcast.promote();
+ if (strongPtr != NULL) {
+ strongPtr->notifyAppUpgrade(timestamp, appName, uid, versionCode);
}
}
@@ -230,21 +225,8 @@
}
}
-void UidMap::getListenerListCopyLocked(vector<wp<PackageInfoListener>>* output) {
- for (auto weakIt = mSubscribers.begin(); weakIt != mSubscribers.end();) {
- auto strongPtr = weakIt->promote();
- if (strongPtr != NULL) {
- output->push_back(*weakIt);
- weakIt++;
- } else {
- weakIt = mSubscribers.erase(weakIt);
- VLOG("The UidMap listener is gone, remove it now");
- }
- }
-}
-
void UidMap::removeApp(const int64_t& timestamp, const String16& app_16, const int32_t& uid) {
- vector<wp<PackageInfoListener>> broadcastList;
+ wp<PackageInfoListener> broadcast = NULL;
string app = string(String8(app_16).string());
{
lock_guard<mutex> lock(mMutex);
@@ -271,25 +253,18 @@
ensureBytesUsedBelowLimit();
StatsdStats::getInstance().setCurrentUidMapMemory(mBytesUsed);
StatsdStats::getInstance().setUidMapChanges(mChanges.size());
- getListenerListCopyLocked(&broadcastList);
+ broadcast = mSubscriber;
}
- for (const auto& weakPtr : broadcastList) {
- auto strongPtr = weakPtr.promote();
- if (strongPtr != NULL) {
- strongPtr->notifyAppRemoved(timestamp, app, uid);
- }
+ auto strongPtr = broadcast.promote();
+ if (strongPtr != NULL) {
+ strongPtr->notifyAppRemoved(timestamp, app, uid);
}
}
-void UidMap::addListener(wp<PackageInfoListener> producer) {
+void UidMap::setListener(wp<PackageInfoListener> listener) {
lock_guard<mutex> lock(mMutex); // Lock for updates
- mSubscribers.insert(producer);
-}
-
-void UidMap::removeListener(wp<PackageInfoListener> producer) {
- lock_guard<mutex> lock(mMutex); // Lock for updates
- mSubscribers.erase(producer);
+ mSubscriber = listener;
}
void UidMap::assignIsolatedUid(int isolatedUid, int parentUid) {
diff --git a/cmds/statsd/src/packages/UidMap.h b/cmds/statsd/src/packages/UidMap.h
index a7c5fb2..2d3f6ee 100644
--- a/cmds/statsd/src/packages/UidMap.h
+++ b/cmds/statsd/src/packages/UidMap.h
@@ -118,12 +118,10 @@
// adb shell cmd stats print-uid-map
void printUidMap(int outFd) const;
- // Commands for indicating to the map that a producer should be notified if an app is updated.
- // This allows the metric producer to distinguish when the same uid or app represents a
- // different version of an app.
- void addListener(wp<PackageInfoListener> producer);
- // Remove the listener from the set of metric producers that subscribe to updates.
- void removeListener(wp<PackageInfoListener> producer);
+ // Command for indicating to the map that StatsLogProcessor should be notified if an app is
+ // updated. This allows metric producers and managers to distinguish when the same uid or app
+ // represents a different version of an app.
+ void setListener(wp<PackageInfoListener> listener);
// Informs uid map that a config is added/updated. Used for keeping mConfigKeys up to date.
void OnConfigUpdated(const ConfigKey& key);
@@ -167,8 +165,6 @@
std::set<string> getAppNamesFromUidLocked(const int32_t& uid, bool returnNormalized) const;
string normalizeAppName(const string& appName) const;
- void getListenerListCopyLocked(std::vector<wp<PackageInfoListener>>* output);
-
void writeUidMapSnapshotLocked(int64_t timestamp, bool includeVersionStrings,
bool includeInstaller, const std::set<int32_t>& interestingUids,
std::set<string>* str_set, ProtoOutputStream* proto);
@@ -195,8 +191,8 @@
// Store which uid and apps represent deleted ones.
std::list<std::pair<int, string>> mDeletedApps;
- // Metric producers that should be notified if there's an upgrade in any app.
- set<wp<PackageInfoListener>> mSubscribers;
+ // Notify StatsLogProcessor if there's an upgrade/removal in any app.
+ wp<PackageInfoListener> mSubscriber;
// Mapping of config keys we're aware of to the epoch time they last received an update. This
// lets us know it's safe to delete events older than the oldest update. The value is nanosec.
diff --git a/cmds/statsd/tests/e2e/PartialBucket_e2e_test.cpp b/cmds/statsd/tests/e2e/PartialBucket_e2e_test.cpp
index 309d251..0bc3ebb 100644
--- a/cmds/statsd/tests/e2e/PartialBucket_e2e_test.cpp
+++ b/cmds/statsd/tests/e2e/PartialBucket_e2e_test.cpp
@@ -162,7 +162,10 @@
ConfigMetricsReport report = GetReports(service.mProcessor, start + 4);
backfillStartEndTimestamp(&report);
- EXPECT_EQ(1, report.metrics_size());
+
+ ASSERT_EQ(1, report.metrics_size());
+ ASSERT_EQ(1, report.metrics(0).count_metrics().data_size());
+ ASSERT_EQ(1, report.metrics(0).count_metrics().data(0).bucket_info_size());
EXPECT_TRUE(report.metrics(0).count_metrics().data(0).bucket_info(0).
has_start_bucket_elapsed_nanos());
EXPECT_TRUE(report.metrics(0).count_metrics().data(0).bucket_info(0).
@@ -186,7 +189,10 @@
ConfigMetricsReport report = GetReports(service.mProcessor, start + 4);
backfillStartEndTimestamp(&report);
- EXPECT_EQ(1, report.metrics_size());
+
+ ASSERT_EQ(1, report.metrics_size());
+ ASSERT_EQ(1, report.metrics(0).count_metrics().data_size());
+ ASSERT_EQ(1, report.metrics(0).count_metrics().data(0).bucket_info_size());
EXPECT_TRUE(report.metrics(0).count_metrics().data(0).bucket_info(0).
has_start_bucket_elapsed_nanos());
EXPECT_TRUE(report.metrics(0).count_metrics().data(0).bucket_info(0).
@@ -228,8 +234,9 @@
ConfigMetricsReport report =
GetReports(service.mProcessor, 5 * 60 * NS_PER_SEC + start + 100 * NS_PER_SEC, true);
backfillStartEndTimestamp(&report);
- EXPECT_EQ(1, report.metrics_size());
- EXPECT_EQ(1, report.metrics(0).value_metrics().skipped_size());
+
+ ASSERT_EQ(1, report.metrics_size());
+ ASSERT_EQ(1, report.metrics(0).value_metrics().skipped_size());
EXPECT_TRUE(report.metrics(0).value_metrics().skipped(0).has_start_bucket_elapsed_nanos());
// Can't test the start time since it will be based on the actual time when the pulling occurs.
EXPECT_EQ(MillisToNano(NanoToMillis(endSkipped)),
@@ -270,8 +277,8 @@
ConfigMetricsReport report =
GetReports(service.mProcessor, 5 * 60 * NS_PER_SEC + start + 100 * NS_PER_SEC, true);
backfillStartEndTimestamp(&report);
- EXPECT_EQ(1, report.metrics_size());
- EXPECT_EQ(1, report.metrics(0).gauge_metrics().skipped_size());
+ ASSERT_EQ(1, report.metrics_size());
+ ASSERT_EQ(1, report.metrics(0).gauge_metrics().skipped_size());
// Can't test the start time since it will be based on the actual time when the pulling occurs.
EXPECT_TRUE(report.metrics(0).gauge_metrics().skipped(0).has_start_bucket_elapsed_nanos());
EXPECT_EQ(MillisToNano(NanoToMillis(endSkipped)),
diff --git a/core/java/android/app/NotificationHistory.java b/core/java/android/app/NotificationHistory.java
index c35246b..8ba39a8 100644
--- a/core/java/android/app/NotificationHistory.java
+++ b/core/java/android/app/NotificationHistory.java
@@ -311,6 +311,14 @@
mHistoryCount++;
}
+ public void addNotificationsToWrite(@NonNull NotificationHistory notificationHistory) {
+ for (HistoricalNotification hn : notificationHistory.getNotificationsToWrite()) {
+ // TODO: consider merging by date
+ addNotificationToWrite(hn);
+ }
+ poolStringsFromNotifications();
+ }
+
/**
* Removes a package's historical notifications and regenerates the string pool
*/
diff --git a/core/java/android/service/contentsuggestions/ContentSuggestionsService.java b/core/java/android/service/contentsuggestions/ContentSuggestionsService.java
index 4bcd39f..306b483 100644
--- a/core/java/android/service/contentsuggestions/ContentSuggestionsService.java
+++ b/core/java/android/service/contentsuggestions/ContentSuggestionsService.java
@@ -64,6 +64,10 @@
@Override
public void provideContextImage(int taskId, GraphicBuffer contextImage,
int colorSpaceId, Bundle imageContextRequestExtras) {
+ if (imageContextRequestExtras.containsKey(ContentSuggestionsManager.EXTRA_BITMAP)
+ && contextImage != null) {
+ throw new IllegalArgumentException("Two bitmaps provided; expected one.");
+ }
Bitmap wrappedBuffer = null;
if (imageContextRequestExtras.containsKey(ContentSuggestionsManager.EXTRA_BITMAP)) {
diff --git a/core/java/android/util/SparseArray.java b/core/java/android/util/SparseArray.java
index 0a15db2..484894f 100644
--- a/core/java/android/util/SparseArray.java
+++ b/core/java/android/util/SparseArray.java
@@ -104,6 +104,14 @@
}
/**
+ * Returns true if the key exists in the array. This is equivalent to
+ * {@link #indexOfKey(int)} >= 0.
+ */
+ public boolean contains(int key) {
+ return indexOfKey(key) >= 0;
+ }
+
+ /**
* Gets the Object mapped from the specified key, or <code>null</code>
* if no such mapping has been made.
*/
diff --git a/core/java/android/util/SparseArrayMap.java b/core/java/android/util/SparseArrayMap.java
new file mode 100644
index 0000000..3ec6b81
--- /dev/null
+++ b/core/java/android/util/SparseArrayMap.java
@@ -0,0 +1,159 @@
+/*
+ * 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.util;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.TestApi;
+
+import java.util.function.Consumer;
+
+/**
+ * A sparse array of ArrayMaps, which is suitable for holding (userId, packageName)->object
+ * associations.
+ *
+ * @param <T> Any class
+ * @hide
+ */
+@TestApi
+public class SparseArrayMap<T> {
+ private final SparseArray<ArrayMap<String, T>> mData = new SparseArray<>();
+
+ /** Add an entry associating obj with the int-String pair. */
+ public void add(int key, @NonNull String mapKey, @Nullable T obj) {
+ ArrayMap<String, T> data = mData.get(key);
+ if (data == null) {
+ data = new ArrayMap<>();
+ mData.put(key, data);
+ }
+ data.put(mapKey, obj);
+ }
+
+ /** Remove all entries from the map. */
+ public void clear() {
+ for (int i = 0; i < mData.size(); ++i) {
+ mData.valueAt(i).clear();
+ }
+ }
+
+ /** Return true if the structure contains an explicit entry for the int-String pair. */
+ public boolean contains(int key, @NonNull String mapKey) {
+ return mData.contains(key) && mData.get(key).containsKey(mapKey);
+ }
+
+ /** Removes all the data for the key, if there was any. */
+ public void delete(int key) {
+ mData.delete(key);
+ }
+
+ /**
+ * Removes the data for the key and mapKey, if there was any.
+ *
+ * @return Returns the value that was stored under the keys, or null if there was none.
+ */
+ @Nullable
+ public T delete(int key, @NonNull String mapKey) {
+ ArrayMap<String, T> data = mData.get(key);
+ if (data != null) {
+ return data.remove(mapKey);
+ }
+ return null;
+ }
+
+ /**
+ * Get the value associated with the int-String pair.
+ */
+ @Nullable
+ public T get(int key, @NonNull String mapKey) {
+ ArrayMap<String, T> data = mData.get(key);
+ if (data != null) {
+ return data.get(mapKey);
+ }
+ return null;
+ }
+
+ /**
+ * Returns the value to which the specified key and mapKey are mapped, or defaultValue if this
+ * map contains no mapping for them.
+ */
+ @Nullable
+ public T getOrDefault(int key, @NonNull String mapKey, T defaultValue) {
+ if (mData.contains(key)) {
+ ArrayMap<String, T> data = mData.get(key);
+ if (data != null && data.containsKey(mapKey)) {
+ return data.get(mapKey);
+ }
+ }
+ return defaultValue;
+ }
+
+ /** @see SparseArray#indexOfKey */
+ public int indexOfKey(int key) {
+ return mData.indexOfKey(key);
+ }
+
+ /**
+ * Returns the index of the mapKey.
+ *
+ * @see SparseArray#indexOfKey
+ */
+ public int indexOfKey(int key, @NonNull String mapKey) {
+ ArrayMap<String, T> data = mData.get(key);
+ if (data != null) {
+ return data.indexOfKey(mapKey);
+ }
+ return -1;
+ }
+
+ /** Returns the key at the given index. */
+ public int keyAt(int index) {
+ return mData.keyAt(index);
+ }
+
+ /** Returns the map's key at the given mapIndex for the given keyIndex. */
+ @NonNull
+ public String keyAt(int keyIndex, int mapIndex) {
+ return mData.valueAt(keyIndex).keyAt(mapIndex);
+ }
+
+ /** Returns the size of the outer array. */
+ public int numMaps() {
+ return mData.size();
+ }
+
+ /** Returns the number of elements in the map of the given key. */
+ public int numElementsForKey(int key) {
+ ArrayMap<String, T> data = mData.get(key);
+ return data == null ? 0 : data.size();
+ }
+
+ /** Returns the value T at the given key and map index. */
+ @Nullable
+ public T valueAt(int keyIndex, int mapIndex) {
+ return mData.valueAt(keyIndex).valueAt(mapIndex);
+ }
+
+ /** Iterate through all int-String pairs and operate on all of the values. */
+ public void forEach(@NonNull Consumer<T> consumer) {
+ for (int i = numMaps() - 1; i >= 0; --i) {
+ ArrayMap<String, T> data = mData.valueAt(i);
+ for (int j = data.size() - 1; j >= 0; --j) {
+ consumer.accept(data.valueAt(j));
+ }
+ }
+ }
+}
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index ba29382..db76b71 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -10353,6 +10353,9 @@
* the user. It is a supplement to the boolean states (for example, checked/unchecked) and
* it is used for customized state description (for example, "wifi, connected, three bars").
* State description changes frequently while content description should change less often.
+ * State description should be localized. For android widgets which have default state
+ * descriptions, app developers can call this method to override the state descriptions.
+ * Setting state description to null restores the default behavior.
*
* @param stateDescription The state description.
* @see #getStateDescription()
diff --git a/core/proto/android/app/settings_enums.proto b/core/proto/android/app/settings_enums.proto
index 9e0c35a..1008a0d 100644
--- a/core/proto/android/app/settings_enums.proto
+++ b/core/proto/android/app/settings_enums.proto
@@ -2441,4 +2441,9 @@
// CATEGORY: SETTINGS
// OS: R
NOTIFICATION_ACCESS_DETAIL = 1804;
+
+ // OPEN: Settings > Developer Options > Platform Compat
+ // CATEGORY: SETTINGS
+ // OS: R
+ SETTINGS_PLATFORM_COMPAT_DASHBOARD = 1805;
}
diff --git a/core/tests/coretests/src/android/app/NotificationHistoryTest.java b/core/tests/coretests/src/android/app/NotificationHistoryTest.java
index 08595bb..f9a6a5c 100644
--- a/core/tests/coretests/src/android/app/NotificationHistoryTest.java
+++ b/core/tests/coretests/src/android/app/NotificationHistoryTest.java
@@ -114,6 +114,33 @@
}
@Test
+ public void testAddNotificationsToWrite() {
+ NotificationHistory history = new NotificationHistory();
+ HistoricalNotification n = getHistoricalNotification(0);
+ HistoricalNotification n2 = getHistoricalNotification(1);
+ history.addNotificationToWrite(n2);
+ history.addNotificationToWrite(n);
+
+ NotificationHistory secondHistory = new NotificationHistory();
+ HistoricalNotification n3 = getHistoricalNotification(2);
+ HistoricalNotification n4 = getHistoricalNotification(3);
+ secondHistory.addNotificationToWrite(n4);
+ secondHistory.addNotificationToWrite(n3);
+
+ history.addNotificationsToWrite(secondHistory);
+
+ assertThat(history.getNotificationsToWrite().size()).isEqualTo(4);
+ assertThat(history.getNotificationsToWrite().get(0)).isSameAs(n2);
+ assertThat(history.getNotificationsToWrite().get(1)).isSameAs(n);
+ assertThat(history.getNotificationsToWrite().get(2)).isSameAs(n4);
+ assertThat(history.getNotificationsToWrite().get(3)).isSameAs(n3);
+ assertThat(history.getHistoryCount()).isEqualTo(4);
+
+ assertThat(history.getPooledStringsToWrite()).asList().contains(n2.getChannelName());
+ assertThat(history.getPooledStringsToWrite()).asList().contains(n4.getPackage());
+ }
+
+ @Test
public void testPoolStringsFromNotifications() {
NotificationHistory history = new NotificationHistory();
diff --git a/packages/PackageInstaller/AndroidManifest.xml b/packages/PackageInstaller/AndroidManifest.xml
index 1c0e409..7d07fda 100644
--- a/packages/PackageInstaller/AndroidManifest.xml
+++ b/packages/PackageInstaller/AndroidManifest.xml
@@ -118,6 +118,7 @@
</receiver>
<activity android:name=".UninstallUninstalling"
+ android:theme="@style/Theme.AlertDialogActivity.NoActionBar"
android:excludeFromRecents="true"
android:exported="false" />
diff --git a/packages/PackageInstaller/res/values/themes.xml b/packages/PackageInstaller/res/values/themes.xml
index b11d28b..eecf9a1 100644
--- a/packages/PackageInstaller/res/values/themes.xml
+++ b/packages/PackageInstaller/res/values/themes.xml
@@ -17,7 +17,8 @@
<resources>
- <style name="Theme.AlertDialogActivity.NoAnimation">
+ <style name="Theme.AlertDialogActivity.NoAnimation"
+ parent="@style/Theme.AlertDialogActivity.NoActionBar">
<item name="android:windowAnimationStyle">@null</item>
</style>
diff --git a/packages/SystemUI/src/com/android/systemui/ForegroundServiceController.java b/packages/SystemUI/src/com/android/systemui/ForegroundServiceController.java
index 15a5c27..c1a23c8 100644
--- a/packages/SystemUI/src/com/android/systemui/ForegroundServiceController.java
+++ b/packages/SystemUI/src/com/android/systemui/ForegroundServiceController.java
@@ -27,6 +27,7 @@
import com.android.systemui.dagger.qualifiers.MainHandler;
import com.android.systemui.statusbar.notification.NotificationEntryManager;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+import com.android.systemui.util.Assert;
import javax.inject.Inject;
import javax.inject.Singleton;
@@ -121,6 +122,8 @@
* @param active whether the appOpCode is active or not
*/
void onAppOpChanged(int appOpCode, int uid, String packageName, boolean active) {
+ Assert.isMainThread();
+
int userId = UserHandle.getUserId(uid);
// Record active app ops
synchronized (mMutex) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/ForegroundServiceControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/ForegroundServiceControllerTest.java
index 02a3766..0990e22 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/ForegroundServiceControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/ForegroundServiceControllerTest.java
@@ -18,6 +18,7 @@
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNull;
+import static junit.framework.TestCase.fail;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@@ -34,12 +35,14 @@
import android.app.NotificationManager;
import android.os.Bundle;
import android.os.Handler;
+import android.os.Looper;
import android.os.UserHandle;
import android.service.notification.StatusBarNotification;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
import android.widget.RemoteViews;
import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
import com.android.internal.messages.nano.SystemMessageProto;
import com.android.systemui.appops.AppOpsController;
@@ -58,7 +61,8 @@
import org.mockito.MockitoAnnotations;
@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
public class ForegroundServiceControllerTest extends SysuiTestCase {
private ForegroundServiceController mFsc;
private ForegroundServiceNotificationListener mListener;
@@ -69,6 +73,9 @@
@Before
public void setUp() throws Exception {
+ // assume the TestLooper is the main looper for these tests
+ com.android.systemui.util.Assert.sMainLooper = TestableLooper.get(this).getLooper();
+
MockitoAnnotations.initMocks(this);
mFsc = new ForegroundServiceController(mEntryManager, mAppOpsController, mMainHandler);
mListener = new ForegroundServiceNotificationListener(
@@ -81,6 +88,26 @@
}
@Test
+ public void testAppOpsChangedCalledFromBgThread() {
+ try {
+ // WHEN onAppOpChanged is called from a different thread than the MainLooper
+ com.android.systemui.util.Assert.sMainLooper = Looper.getMainLooper();
+ NotificationEntry entry = createFgEntry();
+ mFsc.onAppOpChanged(
+ AppOpsManager.OP_CAMERA,
+ entry.getSbn().getUid(),
+ entry.getSbn().getPackageName(),
+ true);
+
+ // This test is run on the TestableLooper, which is not the MainLooper, so
+ // we expect an exception to be thrown
+ fail("onAppOpChanged shouldn't be allowed to be called from a bg thread.");
+ } catch (IllegalStateException e) {
+ // THEN expect an exception
+ }
+ }
+
+ @Test
public void testAppOps_appOpChangedBeforeNotificationExists() {
// GIVEN app op exists, but notification doesn't exist in NEM yet
NotificationEntry entry = createFgEntry();
diff --git a/packages/overlays/IconShapeHeartOverlay/Android.mk b/packages/overlays/IconShapeHeartOverlay/Android.mk
new file mode 100644
index 0000000..20fe71f
--- /dev/null
+++ b/packages/overlays/IconShapeHeartOverlay/Android.mk
@@ -0,0 +1,29 @@
+#
+# 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.
+#
+
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_RRO_THEME := IconShapeHeart
+
+LOCAL_PRODUCT_MODULE := true
+
+LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+
+LOCAL_PACKAGE_NAME := IconShapeHeartOverlay
+LOCAL_SDK_VERSION := current
+
+include $(BUILD_RRO_PACKAGE)
diff --git a/packages/overlays/IconShapeHeartOverlay/AndroidManifest.xml b/packages/overlays/IconShapeHeartOverlay/AndroidManifest.xml
new file mode 100644
index 0000000..8fb19df
--- /dev/null
+++ b/packages/overlays/IconShapeHeartOverlay/AndroidManifest.xml
@@ -0,0 +1,29 @@
+<!--
+/**
+ * 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.
+ */
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.theme.icon.heart"
+ android:versionCode="1"
+ android:versionName="1.0">
+<overlay
+ android:targetPackage="android"
+ android:targetName="IconShapeCustomization"
+ android:category="android.theme.customization.adaptive_icon_shape"
+ android:priority="1"/>
+
+ <application android:label="@string/icon_shape_heart_overlay" android:hasCode="false"/>
+</manifest>
diff --git a/packages/overlays/IconShapeHeartOverlay/res/values/config.xml b/packages/overlays/IconShapeHeartOverlay/res/values/config.xml
new file mode 100644
index 0000000..f9929f5
--- /dev/null
+++ b/packages/overlays/IconShapeHeartOverlay/res/values/config.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** 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.
+*/
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- Specifies the path that is used by AdaptiveIconDrawable class to crop launcher icons. -->
+ <string name="config_icon_mask" translatable="false">"M50,20 C45,0 30,0 25,0 20,0 0,5 0,34 0,72 40,97 50,100 60,97 100,72 100,34 100,5 80,0 75,0 70,0 55,0 50,20 Z"</string>
+ <!-- Flag indicating whether round icons should be parsed from the application manifest. -->
+ <bool name="config_useRoundIcon">false</bool>
+ <!-- Corner radius of system dialogs -->
+ <dimen name="config_dialogCornerRadius">8dp</dimen>
+ <!-- Corner radius for bottom sheet system dialogs -->
+ <dimen name="config_bottomDialogCornerRadius">16dp</dimen>
+
+</resources>
+
diff --git a/packages/overlays/IconShapeHeartOverlay/res/values/strings.xml b/packages/overlays/IconShapeHeartOverlay/res/values/strings.xml
new file mode 100644
index 0000000..92c33fa
--- /dev/null
+++ b/packages/overlays/IconShapeHeartOverlay/res/values/strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * 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.
+ */
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- Heart icon overlay -->
+ <string name="icon_shape_heart_overlay" translatable="false">Heart</string>
+
+</resources>
diff --git a/services/core/java/com/android/server/BinderCallsStatsService.java b/services/core/java/com/android/server/BinderCallsStatsService.java
index e510259..f2ce444 100644
--- a/services/core/java/com/android/server/BinderCallsStatsService.java
+++ b/services/core/java/com/android/server/BinderCallsStatsService.java
@@ -19,6 +19,7 @@
import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE;
import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
+import android.app.ActivityThread;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
@@ -40,6 +41,7 @@
import com.android.internal.os.BinderCallsStats;
import com.android.internal.os.BinderInternal;
import com.android.internal.os.CachedDeviceState;
+import com.android.internal.util.DumpUtils;
import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -49,6 +51,7 @@
public class BinderCallsStatsService extends Binder {
private static final String TAG = "BinderCallsStatsService";
+ private static final String SERVICE_NAME = "binder_calls_stats";
private static final String PERSIST_SYS_BINDER_CALLS_DETAILED_TRACKING
= "persist.sys.binder_calls_detailed_tracking";
@@ -246,7 +249,7 @@
mService = new BinderCallsStatsService(
mBinderCallsStats, mWorkSourceProvider);
publishLocalService(Internal.class, new Internal(mBinderCallsStats));
- publishBinderService("binder_calls_stats", mService);
+ publishBinderService(SERVICE_NAME, mService);
boolean detailedTrackingEnabled = SystemProperties.getBoolean(
PERSIST_SYS_BINDER_CALLS_DETAILED_TRACKING, false);
@@ -293,6 +296,11 @@
@Override
protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ if (!DumpUtils.checkDumpAndUsageStatsPermission(ActivityThread.currentApplication(),
+ SERVICE_NAME, pw)) {
+ return;
+ }
+
boolean verbose = false;
if (args != null) {
for (final String arg : args) {
diff --git a/services/core/java/com/android/server/notification/NotificationHistoryDatabase.java b/services/core/java/com/android/server/notification/NotificationHistoryDatabase.java
index f05b2bf..378ca4a 100644
--- a/services/core/java/com/android/server/notification/NotificationHistoryDatabase.java
+++ b/services/core/java/com/android/server/notification/NotificationHistoryDatabase.java
@@ -75,7 +75,7 @@
private final Context mContext;
private final AlarmManager mAlarmManager;
private final Object mLock = new Object();
- private Handler mFileWriteHandler;
+ private final Handler mFileWriteHandler;
@VisibleForTesting
// List of files holding history information, sorted newest to oldest
final LinkedList<AtomicFile> mHistoryFiles;
@@ -90,11 +90,12 @@
@VisibleForTesting
NotificationHistory mBuffer;
- public NotificationHistoryDatabase(Context context, File dir,
+ public NotificationHistoryDatabase(Context context, Handler fileWriteHandler, File dir,
FileAttrProvider fileAttrProvider) {
mContext = context;
mAlarmManager = context.getSystemService(AlarmManager.class);
mCurrentVersion = DEFAULT_CURRENT_VERSION;
+ mFileWriteHandler = fileWriteHandler;
mVersionFile = new File(dir, "version");
mHistoryDir = new File(dir, "history");
mHistoryFiles = new LinkedList<>();
@@ -107,10 +108,8 @@
mContext.registerReceiver(mFileCleaupReceiver, deletionFilter);
}
- public void init(Handler fileWriteHandler) {
+ public void init() {
synchronized (mLock) {
- mFileWriteHandler = fileWriteHandler;
-
try {
mHistoryDir.mkdir();
mVersionFile.createNewFile();
@@ -160,13 +159,13 @@
}
}
- void forceWriteToDisk() {
+ public void forceWriteToDisk() {
if (!mFileWriteHandler.hasCallbacks(mWriteBufferRunnable)) {
mFileWriteHandler.post(mWriteBufferRunnable);
}
}
- void onPackageRemoved(String packageName) {
+ public void onPackageRemoved(String packageName) {
RemovePackageRunnable rpr = new RemovePackageRunnable(packageName);
mFileWriteHandler.post(rpr);
}
@@ -227,7 +226,7 @@
/**
* Remove any files that are too old and schedule jobs to clean up the rest
*/
- public void prune(final int retentionDays, final long currentTimeMillis) {
+ void prune(final int retentionDays, final long currentTimeMillis) {
synchronized (mLock) {
GregorianCalendar retentionBoundary = new GregorianCalendar();
retentionBoundary.setTimeInMillis(currentTimeMillis);
@@ -252,7 +251,7 @@
}
}
- void scheduleDeletion(File file, long deletionTime) {
+ private void scheduleDeletion(File file, long deletionTime) {
if (DEBUG) {
Slog.d(TAG, "Scheduling deletion for " + file.getName() + " at " + deletionTime);
}
diff --git a/services/core/java/com/android/server/notification/NotificationHistoryDatabaseFactory.java b/services/core/java/com/android/server/notification/NotificationHistoryDatabaseFactory.java
new file mode 100644
index 0000000..b4940a5
--- /dev/null
+++ b/services/core/java/com/android/server/notification/NotificationHistoryDatabaseFactory.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.notification;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.os.Handler;
+
+import java.io.File;
+
+public class NotificationHistoryDatabaseFactory {
+
+ private static NotificationHistoryDatabase sTestingNotificationHistoryDb;
+
+ public static void setTestingNotificationHistoryDatabase(NotificationHistoryDatabase db) {
+ sTestingNotificationHistoryDb = db;
+ }
+
+ public static NotificationHistoryDatabase create(@NonNull Context context,
+ @NonNull Handler handler, @NonNull File rootDir,
+ @NonNull NotificationHistoryDatabase.FileAttrProvider fileAttrProvider) {
+ if(sTestingNotificationHistoryDb != null) {
+ return sTestingNotificationHistoryDb;
+ }
+ return new NotificationHistoryDatabase(context, handler, rootDir, fileAttrProvider);
+ }
+}
diff --git a/services/core/java/com/android/server/notification/NotificationHistoryManager.java b/services/core/java/com/android/server/notification/NotificationHistoryManager.java
new file mode 100644
index 0000000..a350a6b
--- /dev/null
+++ b/services/core/java/com/android/server/notification/NotificationHistoryManager.java
@@ -0,0 +1,245 @@
+/*
+ * 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.notification;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.app.NotificationHistory;
+import android.app.NotificationHistory.HistoricalNotification;
+import android.content.Context;
+import android.os.Environment;
+import android.os.UserManager;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.util.SparseBooleanArray;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.IoThread;
+import com.android.server.notification.NotificationHistoryDatabase.NotificationHistoryFileAttrProvider;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Keeps track of per-user notification histories.
+ */
+public class NotificationHistoryManager {
+ private static final String TAG = "NotificationHistory";
+ private static final boolean DEBUG = NotificationManagerService.DBG;
+
+ @VisibleForTesting
+ static final String DIRECTORY_PER_USER = "notification_history";
+
+ private final Context mContext;
+ private final UserManager mUserManager;
+ private final Object mLock = new Object();
+ @GuardedBy("mLock")
+ private final SparseArray<NotificationHistoryDatabase> mUserState = new SparseArray<>();
+ @GuardedBy("mLock")
+ private final SparseBooleanArray mUserUnlockedStates = new SparseBooleanArray();
+ // TODO: does this need to be persisted across reboots?
+ @GuardedBy("mLock")
+ private final SparseArray<List<String>> mUserPendingPackageRemovals = new SparseArray<>();
+
+ public NotificationHistoryManager(Context context) {
+ mContext = context;
+ mUserManager = context.getSystemService(UserManager.class);
+ }
+
+ public void onUserUnlocked(@UserIdInt int userId) {
+ synchronized (mLock) {
+ mUserUnlockedStates.put(userId, true);
+ final NotificationHistoryDatabase userHistory =
+ getUserHistoryAndInitializeIfNeededLocked(userId);
+ if (userHistory == null) {
+ Slog.i(TAG, "Attempted to unlock stopped or removed user " + userId);
+ return;
+ }
+
+ // remove any packages that were deleted while the user was locked
+ final List<String> pendingPackageRemovals = mUserPendingPackageRemovals.get(userId);
+ if (pendingPackageRemovals != null) {
+ for (int i = 0; i < pendingPackageRemovals.size(); i++) {
+ userHistory.onPackageRemoved(pendingPackageRemovals.get(i));
+ }
+ mUserPendingPackageRemovals.put(userId, null);
+ }
+ }
+ }
+
+ public void onUserStopped(@UserIdInt int userId) {
+ synchronized (mLock) {
+ mUserUnlockedStates.put(userId, false);
+ mUserState.put(userId, null); // release the service (mainly for GC)
+ }
+ }
+
+ void onUserRemoved(@UserIdInt int userId) {
+ synchronized (mLock) {
+ // Actual data deletion is handled by other parts of the system (the entire directory is
+ // removed) - we just need clean up our internal state for GC
+ mUserPendingPackageRemovals.put(userId, null);
+ onUserStopped(userId);
+ }
+ }
+
+ void onPackageRemoved(int userId, String packageName) {
+ synchronized (mLock) {
+ if (!mUserUnlockedStates.get(userId, false)) {
+ List<String> userPendingRemovals =
+ mUserPendingPackageRemovals.get(userId, new ArrayList<>());
+ userPendingRemovals.add(packageName);
+ mUserPendingPackageRemovals.put(userId, userPendingRemovals);
+ return;
+ }
+ final NotificationHistoryDatabase userHistory = mUserState.get(userId);
+ if (userHistory == null) {
+ return;
+ }
+
+ userHistory.onPackageRemoved(packageName);
+ }
+ }
+
+ void triggerWriteToDisk() {
+ synchronized (mLock) {
+ final int userCount = mUserState.size();
+ for (int i = 0; i < userCount; i++) {
+ final int userId = mUserState.keyAt(i);
+ if (!mUserUnlockedStates.get(userId)) {
+ continue;
+ }
+ NotificationHistoryDatabase userHistory = mUserState.get(userId);
+ if (userHistory != null) {
+ userHistory.forceWriteToDisk();
+ }
+ }
+ }
+ }
+
+ public void addNotification(@NonNull final HistoricalNotification notification) {
+ synchronized (mLock) {
+ final NotificationHistoryDatabase userHistory =
+ getUserHistoryAndInitializeIfNeededLocked(notification.getUserId());
+ if (userHistory == null) {
+ Slog.w(TAG, "Attempted to add notif for locked/gone user "
+ + notification.getUserId());
+ return;
+ }
+ userHistory.addNotification(notification);
+ }
+ }
+
+ public @NonNull NotificationHistory readNotificationHistory(@UserIdInt int[] userIds) {
+ synchronized (mLock) {
+ NotificationHistory mergedHistory = new NotificationHistory();
+ if (userIds == null) {
+ return mergedHistory;
+ }
+ for (int userId : userIds) {
+ final NotificationHistoryDatabase userHistory =
+ getUserHistoryAndInitializeIfNeededLocked(userId);
+ if (userHistory == null) {
+ Slog.i(TAG, "Attempted to read history for locked/gone user " +userId);
+ continue;
+ }
+ mergedHistory.addNotificationsToWrite(userHistory.readNotificationHistory());
+ }
+ return mergedHistory;
+ }
+ }
+
+ public @NonNull android.app.NotificationHistory readFilteredNotificationHistory(
+ @UserIdInt int userId, String packageName, String channelId, int maxNotifications) {
+ synchronized (mLock) {
+ final NotificationHistoryDatabase userHistory =
+ getUserHistoryAndInitializeIfNeededLocked(userId);
+ if (userHistory == null) {
+ Slog.i(TAG, "Attempted to read history for locked/gone user " +userId);
+ return new android.app.NotificationHistory();
+ }
+
+ return userHistory.readNotificationHistory(packageName, channelId, maxNotifications);
+ }
+ }
+
+ @GuardedBy("mLock")
+ private @Nullable NotificationHistoryDatabase getUserHistoryAndInitializeIfNeededLocked(
+ int userId) {
+ NotificationHistoryDatabase userHistory = mUserState.get(userId);
+ if (userHistory == null) {
+ final File historyDir = new File(Environment.getDataSystemCeDirectory(userId),
+ DIRECTORY_PER_USER);
+ userHistory = NotificationHistoryDatabaseFactory.create(mContext, IoThread.getHandler(),
+ historyDir, new NotificationHistoryFileAttrProvider());
+ if (mUserUnlockedStates.get(userId)) {
+ try {
+ userHistory.init();
+ } catch (Exception e) {
+ if (mUserManager.isUserUnlocked(userId)) {
+ throw e; // rethrow exception - user is unlocked
+ } else {
+ Slog.w(TAG, "Attempted to initialize service for "
+ + "stopped or removed user " + userId);
+ return null;
+ }
+ }
+ } else {
+ // locked! data unavailable
+ Slog.w(TAG, "Attempted to initialize service for "
+ + "stopped or removed user " + userId);
+ return null;
+ }
+ mUserState.put(userId, userHistory);
+ }
+ return userHistory;
+ }
+
+ @VisibleForTesting
+ boolean isUserUnlocked(@UserIdInt int userId) {
+ synchronized (mLock) {
+ return mUserUnlockedStates.get(userId);
+ }
+ }
+
+ @VisibleForTesting
+ boolean doesHistoryExistForUser(@UserIdInt int userId) {
+ synchronized (mLock) {
+ return mUserState.get(userId) != null;
+ }
+ }
+
+ @VisibleForTesting
+ void replaceNotificationHistoryDatabase(@UserIdInt int userId,
+ NotificationHistoryDatabase replacement) {
+ synchronized (mLock) {
+ if (mUserState.get(userId) != null) {
+ mUserState.put(userId, replacement);
+ }
+ }
+ }
+
+ @VisibleForTesting
+ List<String> getPendingPackageRemovalsForUser(@UserIdInt int userId) {
+ synchronized (mLock) {
+ return mUserPendingPackageRemovals.get(userId);
+ }
+ }
+}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryDatabaseTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryDatabaseTest.java
index 608625f..a00afec 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryDatabaseTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryDatabaseTest.java
@@ -45,11 +45,6 @@
import org.mockito.MockitoAnnotations;
import java.io.File;
-import java.nio.file.FileSystem;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.attribute.BasicFileAttributes;
-import java.nio.file.attribute.FileTime;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.GregorianCalendar;
@@ -109,8 +104,9 @@
mFileAttrProvider = new TestFileAttrProvider();
mRootDir = new File(mContext.getFilesDir(), "NotificationHistoryDatabaseTest");
- mDataBase = new NotificationHistoryDatabase(mContext, mRootDir, mFileAttrProvider);
- mDataBase.init(mFileWriteHandler);
+ mDataBase = new NotificationHistoryDatabase(
+ mContext, mFileWriteHandler, mRootDir, mFileAttrProvider);
+ mDataBase.init();
}
@Test
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryManagerTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryManagerTest.java
new file mode 100644
index 0000000..aa3c465
--- /dev/null
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationHistoryManagerTest.java
@@ -0,0 +1,315 @@
+/*
+ * 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.notification;
+
+import static android.os.UserHandle.USER_ALL;
+import static android.os.UserHandle.USER_SYSTEM;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.NotificationHistory;
+import android.app.NotificationHistory.HistoricalNotification;
+import android.content.Context;
+import android.graphics.drawable.Icon;
+import android.os.UserManager;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.server.UiServiceTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+
+@RunWith(AndroidJUnit4.class)
+public class NotificationHistoryManagerTest extends UiServiceTestCase {
+
+ @Mock
+ Context mContext;
+ @Mock
+ UserManager mUserManager;
+ @Mock
+ NotificationHistoryDatabase mDb;
+
+ NotificationHistoryManager mHistoryManager;
+
+ private HistoricalNotification getHistoricalNotification(int index) {
+ return getHistoricalNotification("package" + index, index);
+ }
+
+ private HistoricalNotification getHistoricalNotification(String packageName, int index) {
+ String expectedChannelName = "channelName" + index;
+ String expectedChannelId = "channelId" + index;
+ int expectedUid = 1123456 + index;
+ int expectedUserId = index;
+ long expectedPostTime = 987654321 + index;
+ String expectedTitle = "title" + index;
+ String expectedText = "text" + index;
+ Icon expectedIcon = Icon.createWithResource(InstrumentationRegistry.getContext(),
+ index);
+
+ return new HistoricalNotification.Builder()
+ .setPackage(packageName)
+ .setChannelName(expectedChannelName)
+ .setChannelId(expectedChannelId)
+ .setUid(expectedUid)
+ .setUserId(expectedUserId)
+ .setPostedTimeMs(expectedPostTime)
+ .setTitle(expectedTitle)
+ .setText(expectedText)
+ .setIcon(expectedIcon)
+ .build();
+ }
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ when(mContext.getSystemService(UserManager.class)).thenReturn(mUserManager);
+ when(mContext.getUser()).thenReturn(getContext().getUser());
+ when(mContext.getPackageName()).thenReturn(getContext().getPackageName());
+
+ NotificationHistoryDatabaseFactory.setTestingNotificationHistoryDatabase(mDb);
+
+ mHistoryManager = new NotificationHistoryManager(mContext);
+ }
+
+ @Test
+ public void testOnUserUnlocked() {
+ assertThat(mHistoryManager.doesHistoryExistForUser(USER_SYSTEM)).isFalse();
+ assertThat(mHistoryManager.isUserUnlocked(USER_SYSTEM)).isFalse();
+ mHistoryManager.onUserUnlocked(USER_SYSTEM);
+ assertThat(mHistoryManager.doesHistoryExistForUser(USER_SYSTEM)).isTrue();
+ assertThat(mHistoryManager.isUserUnlocked(USER_SYSTEM)).isTrue();
+ verify(mDb, times(1)).init();
+ }
+
+ @Test
+ public void testOnUserUnlocked_cleansUpRemovedPackages() {
+ String pkg = "pkg";
+ mHistoryManager.onPackageRemoved(USER_SYSTEM, pkg);
+ assertThat(mHistoryManager.doesHistoryExistForUser(USER_SYSTEM)).isFalse();
+
+ mHistoryManager.onUserUnlocked(USER_SYSTEM);
+ assertThat(mHistoryManager.doesHistoryExistForUser(USER_SYSTEM)).isTrue();
+ assertThat(mHistoryManager.isUserUnlocked(USER_SYSTEM)).isTrue();
+
+ verify(mDb, times(1)).onPackageRemoved(pkg);
+ }
+
+ @Test
+ public void testOnUserStopped_userExists() {
+ mHistoryManager.onUserUnlocked(USER_SYSTEM);
+ mHistoryManager.onUserStopped(USER_SYSTEM);
+
+ assertThat(mHistoryManager.doesHistoryExistForUser(USER_SYSTEM)).isFalse();
+ assertThat(mHistoryManager.isUserUnlocked(USER_SYSTEM)).isFalse();
+ }
+
+ @Test
+ public void testOnUserStopped_userDoesNotExist() {
+ mHistoryManager.onUserStopped(USER_SYSTEM);
+ // no crash
+ assertThat(mHistoryManager.doesHistoryExistForUser(USER_SYSTEM)).isFalse();
+ assertThat(mHistoryManager.isUserUnlocked(USER_SYSTEM)).isFalse();
+ }
+
+ @Test
+ public void testOnUserRemoved_userExists() {
+ mHistoryManager.onUserUnlocked(USER_SYSTEM);
+ mHistoryManager.onUserRemoved(USER_SYSTEM);
+
+ assertThat(mHistoryManager.doesHistoryExistForUser(USER_SYSTEM)).isFalse();
+ assertThat(mHistoryManager.isUserUnlocked(USER_SYSTEM)).isFalse();
+ }
+
+ @Test
+ public void testOnUserRemoved_userDoesNotExist() {
+ mHistoryManager.onUserRemoved(USER_SYSTEM);
+ // no crash
+ assertThat(mHistoryManager.doesHistoryExistForUser(USER_SYSTEM)).isFalse();
+ assertThat(mHistoryManager.isUserUnlocked(USER_SYSTEM)).isFalse();
+ }
+
+ @Test
+ public void testOnUserRemoved_cleanupPendingPackages() {
+ mHistoryManager.onUserUnlocked(USER_SYSTEM);
+ mHistoryManager.onUserStopped(USER_SYSTEM);
+ String pkg = "pkg";
+ mHistoryManager.onPackageRemoved(USER_SYSTEM, pkg);
+ mHistoryManager.onUserRemoved(USER_SYSTEM);
+
+ assertThat(mHistoryManager.getPendingPackageRemovalsForUser(USER_SYSTEM)).isNull();
+ }
+
+ @Test
+ public void testOnPackageRemoved_userUnlocked() {
+ String pkg = "pkg";
+ NotificationHistoryDatabase userHistory = mock(NotificationHistoryDatabase.class);
+
+ mHistoryManager.onUserUnlocked(USER_SYSTEM);
+ mHistoryManager.replaceNotificationHistoryDatabase(USER_SYSTEM, userHistory);
+
+ mHistoryManager.onPackageRemoved(USER_SYSTEM, pkg);
+
+ verify(userHistory, times(1)).onPackageRemoved(pkg);
+ }
+
+ @Test
+ public void testOnPackageRemoved_userLocked() {
+ String pkg = "pkg";
+ mHistoryManager.onPackageRemoved(USER_SYSTEM, pkg);
+
+ assertThat(mHistoryManager.getPendingPackageRemovalsForUser(USER_SYSTEM)).contains(pkg);
+ }
+
+ @Test
+ public void testOnPackageRemoved_multiUser() {
+ String pkg = "pkg";
+ NotificationHistoryDatabase userHistorySystem = mock(NotificationHistoryDatabase.class);
+ NotificationHistoryDatabase userHistoryAll = mock(NotificationHistoryDatabase.class);
+
+ mHistoryManager.onUserUnlocked(USER_SYSTEM);
+ mHistoryManager.replaceNotificationHistoryDatabase(USER_SYSTEM, userHistorySystem);
+
+ mHistoryManager.onUserUnlocked(USER_ALL);
+ mHistoryManager.replaceNotificationHistoryDatabase(USER_ALL, userHistoryAll);
+
+ mHistoryManager.onPackageRemoved(USER_SYSTEM, pkg);
+
+ verify(userHistorySystem, times(1)).onPackageRemoved(pkg);
+ verify(userHistoryAll, never()).onPackageRemoved(pkg);
+ }
+
+ @Test
+ public void testTriggerWriteToDisk() {
+ NotificationHistoryDatabase userHistorySystem = mock(NotificationHistoryDatabase.class);
+ NotificationHistoryDatabase userHistoryAll = mock(NotificationHistoryDatabase.class);
+
+ mHistoryManager.onUserUnlocked(USER_SYSTEM);
+ mHistoryManager.replaceNotificationHistoryDatabase(USER_SYSTEM, userHistorySystem);
+
+ mHistoryManager.onUserUnlocked(USER_ALL);
+ mHistoryManager.replaceNotificationHistoryDatabase(USER_ALL, userHistoryAll);
+
+ mHistoryManager.triggerWriteToDisk();
+
+ verify(userHistorySystem, times(1)).forceWriteToDisk();
+ verify(userHistoryAll, times(1)).forceWriteToDisk();
+ }
+
+ @Test
+ public void testTriggerWriteToDisk_onlyUnlockedUsers() {
+ NotificationHistoryDatabase userHistorySystem = mock(NotificationHistoryDatabase.class);
+ NotificationHistoryDatabase userHistoryAll = mock(NotificationHistoryDatabase.class);
+
+ mHistoryManager.onUserUnlocked(USER_SYSTEM);
+ mHistoryManager.replaceNotificationHistoryDatabase(USER_SYSTEM, userHistorySystem);
+
+ mHistoryManager.onUserUnlocked(USER_ALL);
+ mHistoryManager.replaceNotificationHistoryDatabase(USER_ALL, userHistoryAll);
+ mHistoryManager.onUserStopped(USER_ALL);
+
+ mHistoryManager.triggerWriteToDisk();
+
+ verify(userHistorySystem, times(1)).forceWriteToDisk();
+ verify(userHistoryAll, never()).forceWriteToDisk();
+ }
+
+ @Test
+ public void testAddNotification_userLocked_noCrash() {
+ HistoricalNotification hn = getHistoricalNotification("pkg", 1);
+
+ mHistoryManager.addNotification(hn);
+ }
+
+ @Test
+ public void testAddNotification() {
+ HistoricalNotification hnSystem = getHistoricalNotification("pkg", USER_SYSTEM);
+ HistoricalNotification hnAll = getHistoricalNotification("pkg", USER_ALL);
+
+ NotificationHistoryDatabase userHistorySystem = mock(NotificationHistoryDatabase.class);
+ NotificationHistoryDatabase userHistoryAll = mock(NotificationHistoryDatabase.class);
+
+ mHistoryManager.onUserUnlocked(USER_SYSTEM);
+ mHistoryManager.replaceNotificationHistoryDatabase(USER_SYSTEM, userHistorySystem);
+
+ mHistoryManager.onUserUnlocked(USER_ALL);
+ mHistoryManager.replaceNotificationHistoryDatabase(USER_ALL, userHistoryAll);
+
+ mHistoryManager.addNotification(hnSystem);
+ mHistoryManager.addNotification(hnAll);
+
+ verify(userHistorySystem, times(1)).addNotification(hnSystem);
+ verify(userHistoryAll, times(1)).addNotification(hnAll);
+ }
+
+ @Test
+ public void testReadNotificationHistory() {
+ HistoricalNotification hnSystem = getHistoricalNotification("pkg", USER_SYSTEM);
+ HistoricalNotification hnAll = getHistoricalNotification("pkg", USER_ALL);
+
+ NotificationHistoryDatabase userHistorySystem = mock(NotificationHistoryDatabase.class);
+ NotificationHistoryDatabase userHistoryAll = mock(NotificationHistoryDatabase.class);
+
+ mHistoryManager.onUserUnlocked(USER_SYSTEM);
+ mHistoryManager.replaceNotificationHistoryDatabase(USER_SYSTEM, userHistorySystem);
+ NotificationHistory nhSystem = mock(NotificationHistory.class);
+ ArrayList<HistoricalNotification> nhSystemList = new ArrayList<>();
+ nhSystemList.add(hnSystem);
+ when(nhSystem.getNotificationsToWrite()).thenReturn(nhSystemList);
+ when(userHistorySystem.readNotificationHistory()).thenReturn(nhSystem);
+
+ mHistoryManager.onUserUnlocked(USER_ALL);
+ mHistoryManager.replaceNotificationHistoryDatabase(USER_ALL, userHistoryAll);
+ NotificationHistory nhAll = mock(NotificationHistory.class);
+ ArrayList<HistoricalNotification> nhAllList = new ArrayList<>();
+ nhAllList.add(hnAll);
+ when(nhAll.getNotificationsToWrite()).thenReturn(nhAllList);
+ when(userHistoryAll.readNotificationHistory()).thenReturn(nhAll);
+
+ // ensure read history returns both historical notifs
+ NotificationHistory nh = mHistoryManager.readNotificationHistory(
+ new int[] {USER_SYSTEM, USER_ALL});
+ assertThat(nh.getNotificationsToWrite()).contains(hnSystem);
+ assertThat(nh.getNotificationsToWrite()).contains(hnAll);
+ }
+
+ @Test
+ public void readFilteredNotificationHistory_userUnlocked() {
+ NotificationHistory nh =
+ mHistoryManager.readFilteredNotificationHistory(USER_SYSTEM, "", "", 1000);
+ assertThat(nh.getNotificationsToWrite()).isEmpty();
+ }
+
+ @Test
+ public void readFilteredNotificationHistory() {
+ mHistoryManager.onUserUnlocked(USER_SYSTEM);
+
+ mHistoryManager.readFilteredNotificationHistory(USER_SYSTEM, "pkg", "chn", 1000);
+ verify(mDb, times(1)).readNotificationHistory("pkg", "chn", 1000);
+ }
+}