Merge "Revert "Add platform key generation ID to WrappedKey instances""
diff --git a/api/current.txt b/api/current.txt
index 1bc38d4..9bdcdad 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -16643,6 +16643,17 @@
     field public static final int KNOTTED_HEH = 21; // 0x15
     field public static final int LAM = 22; // 0x16
     field public static final int LAMADH = 23; // 0x17
+    field public static final int MALAYALAM_BHA = 89; // 0x59
+    field public static final int MALAYALAM_JA = 90; // 0x5a
+    field public static final int MALAYALAM_LLA = 91; // 0x5b
+    field public static final int MALAYALAM_LLLA = 92; // 0x5c
+    field public static final int MALAYALAM_NGA = 93; // 0x5d
+    field public static final int MALAYALAM_NNA = 94; // 0x5e
+    field public static final int MALAYALAM_NNNA = 95; // 0x5f
+    field public static final int MALAYALAM_NYA = 96; // 0x60
+    field public static final int MALAYALAM_RA = 97; // 0x61
+    field public static final int MALAYALAM_SSA = 98; // 0x62
+    field public static final int MALAYALAM_TTA = 99; // 0x63
     field public static final int MANICHAEAN_ALEPH = 58; // 0x3a
     field public static final int MANICHAEAN_AYIN = 59; // 0x3b
     field public static final int MANICHAEAN_BETH = 60; // 0x3c
@@ -16898,6 +16909,8 @@
     field public static final int CJK_UNIFIED_IDEOGRAPHS_EXTENSION_D_ID = 209; // 0xd1
     field public static final android.icu.lang.UCharacter.UnicodeBlock CJK_UNIFIED_IDEOGRAPHS_EXTENSION_E;
     field public static final int CJK_UNIFIED_IDEOGRAPHS_EXTENSION_E_ID = 256; // 0x100
+    field public static final android.icu.lang.UCharacter.UnicodeBlock CJK_UNIFIED_IDEOGRAPHS_EXTENSION_F;
+    field public static final int CJK_UNIFIED_IDEOGRAPHS_EXTENSION_F_ID = 274; // 0x112
     field public static final int CJK_UNIFIED_IDEOGRAPHS_ID = 71; // 0x47
     field public static final android.icu.lang.UCharacter.UnicodeBlock COMBINING_DIACRITICAL_MARKS;
     field public static final android.icu.lang.UCharacter.UnicodeBlock COMBINING_DIACRITICAL_MARKS_EXTENDED;
@@ -17043,6 +17056,8 @@
     field public static final int JAVANESE_ID = 181; // 0xb5
     field public static final android.icu.lang.UCharacter.UnicodeBlock KAITHI;
     field public static final int KAITHI_ID = 193; // 0xc1
+    field public static final android.icu.lang.UCharacter.UnicodeBlock KANA_EXTENDED_A;
+    field public static final int KANA_EXTENDED_A_ID = 275; // 0x113
     field public static final android.icu.lang.UCharacter.UnicodeBlock KANA_SUPPLEMENT;
     field public static final int KANA_SUPPLEMENT_ID = 203; // 0xcb
     field public static final android.icu.lang.UCharacter.UnicodeBlock KANBUN;
@@ -17115,6 +17130,8 @@
     field public static final int MANICHAEAN_ID = 234; // 0xea
     field public static final android.icu.lang.UCharacter.UnicodeBlock MARCHEN;
     field public static final int MARCHEN_ID = 268; // 0x10c
+    field public static final android.icu.lang.UCharacter.UnicodeBlock MASARAM_GONDI;
+    field public static final int MASARAM_GONDI_ID = 276; // 0x114
     field public static final android.icu.lang.UCharacter.UnicodeBlock MATHEMATICAL_ALPHANUMERIC_SYMBOLS;
     field public static final int MATHEMATICAL_ALPHANUMERIC_SYMBOLS_ID = 93; // 0x5d
     field public static final android.icu.lang.UCharacter.UnicodeBlock MATHEMATICAL_OPERATORS;
@@ -17174,6 +17191,8 @@
     field public static final android.icu.lang.UCharacter.UnicodeBlock NO_BLOCK;
     field public static final android.icu.lang.UCharacter.UnicodeBlock NUMBER_FORMS;
     field public static final int NUMBER_FORMS_ID = 45; // 0x2d
+    field public static final android.icu.lang.UCharacter.UnicodeBlock NUSHU;
+    field public static final int NUSHU_ID = 277; // 0x115
     field public static final android.icu.lang.UCharacter.UnicodeBlock OGHAM;
     field public static final int OGHAM_ID = 34; // 0x22
     field public static final android.icu.lang.UCharacter.UnicodeBlock OLD_HUNGARIAN;
@@ -17252,6 +17271,8 @@
     field public static final int SMALL_FORM_VARIANTS_ID = 84; // 0x54
     field public static final android.icu.lang.UCharacter.UnicodeBlock SORA_SOMPENG;
     field public static final int SORA_SOMPENG_ID = 218; // 0xda
+    field public static final android.icu.lang.UCharacter.UnicodeBlock SOYOMBO;
+    field public static final int SOYOMBO_ID = 278; // 0x116
     field public static final android.icu.lang.UCharacter.UnicodeBlock SPACING_MODIFIER_LETTERS;
     field public static final int SPACING_MODIFIER_LETTERS_ID = 6; // 0x6
     field public static final android.icu.lang.UCharacter.UnicodeBlock SPECIALS;
@@ -17284,6 +17305,8 @@
     field public static final int SYLOTI_NAGRI_ID = 143; // 0x8f
     field public static final android.icu.lang.UCharacter.UnicodeBlock SYRIAC;
     field public static final int SYRIAC_ID = 13; // 0xd
+    field public static final android.icu.lang.UCharacter.UnicodeBlock SYRIAC_SUPPLEMENT;
+    field public static final int SYRIAC_SUPPLEMENT_ID = 279; // 0x117
     field public static final android.icu.lang.UCharacter.UnicodeBlock TAGALOG;
     field public static final int TAGALOG_ID = 98; // 0x62
     field public static final android.icu.lang.UCharacter.UnicodeBlock TAGBANWA;
@@ -17344,6 +17367,8 @@
     field public static final int YI_RADICALS_ID = 73; // 0x49
     field public static final android.icu.lang.UCharacter.UnicodeBlock YI_SYLLABLES;
     field public static final int YI_SYLLABLES_ID = 72; // 0x48
+    field public static final android.icu.lang.UCharacter.UnicodeBlock ZANABAZAR_SQUARE;
+    field public static final int ZANABAZAR_SQUARE_ID = 280; // 0x118
   }
 
   public static abstract interface UCharacter.WordBreak {
@@ -17494,6 +17519,11 @@
     field public static final int DIACRITIC = 7; // 0x7
     field public static final int DOUBLE_START = 12288; // 0x3000
     field public static final int EAST_ASIAN_WIDTH = 4100; // 0x1004
+    field public static final int EMOJI = 57; // 0x39
+    field public static final int EMOJI_COMPONENT = 61; // 0x3d
+    field public static final int EMOJI_MODIFIER = 59; // 0x3b
+    field public static final int EMOJI_MODIFIER_BASE = 60; // 0x3c
+    field public static final int EMOJI_PRESENTATION = 58; // 0x3a
     field public static final int EXTENDER = 8; // 0x8
     field public static final int FULL_COMPOSITION_EXCLUSION = 9; // 0x9
     field public static final int GENERAL_CATEGORY = 4101; // 0x1005
@@ -17541,8 +17571,10 @@
     field public static final int POSIX_GRAPH = 46; // 0x2e
     field public static final int POSIX_PRINT = 47; // 0x2f
     field public static final int POSIX_XDIGIT = 48; // 0x30
+    field public static final int PREPENDED_CONCATENATION_MARK = 63; // 0x3f
     field public static final int QUOTATION_MARK = 25; // 0x19
     field public static final int RADICAL = 26; // 0x1a
+    field public static final int REGIONAL_INDICATOR = 62; // 0x3e
     field public static final int SCRIPT = 4106; // 0x100a
     field public static final int SCRIPT_EXTENSIONS = 28672; // 0x7000
     field public static final int SEGMENT_STARTER = 41; // 0x29
@@ -17684,6 +17716,7 @@
     field public static final int MANDAIC = 84; // 0x54
     field public static final int MANICHAEAN = 121; // 0x79
     field public static final int MARCHEN = 169; // 0xa9
+    field public static final int MASARAM_GONDI = 175; // 0xaf
     field public static final int MATHEMATICAL_NOTATION = 128; // 0x80
     field public static final int MAYAN_HIEROGLYPHS = 85; // 0x55
     field public static final int MEITEI_MAYEK = 115; // 0x73
@@ -17738,6 +17771,7 @@
     field public static final int SINDHI = 145; // 0x91
     field public static final int SINHALA = 33; // 0x21
     field public static final int SORA_SOMPENG = 152; // 0x98
+    field public static final int SOYOMBO = 176; // 0xb0
     field public static final int SUNDANESE = 113; // 0x71
     field public static final int SYLOTI_NAGRI = 58; // 0x3a
     field public static final int SYMBOLS = 129; // 0x81
@@ -17768,6 +17802,7 @@
     field public static final int WESTERN_SYRIAC = 96; // 0x60
     field public static final int WOLEAI = 155; // 0x9b
     field public static final int YI = 41; // 0x29
+    field public static final int ZANABAZAR_SQUARE = 177; // 0xb1
   }
 
   public static final class UScript.ScriptUsage extends java.lang.Enum {
@@ -18562,11 +18597,14 @@
     method public android.icu.util.Currency getCurrency();
     method public java.lang.String getCurrencySymbol();
     method public char getDecimalSeparator();
+    method public java.lang.String getDecimalSeparatorString();
     method public char getDigit();
+    method public java.lang.String[] getDigitStrings();
     method public char[] getDigits();
     method public java.lang.String getExponentMultiplicationSign();
     method public java.lang.String getExponentSeparator();
     method public char getGroupingSeparator();
+    method public java.lang.String getGroupingSeparatorString();
     method public java.lang.String getInfinity();
     method public static android.icu.text.DecimalFormatSymbols getInstance();
     method public static android.icu.text.DecimalFormatSymbols getInstance(java.util.Locale);
@@ -18574,37 +18612,52 @@
     method public java.lang.String getInternationalCurrencySymbol();
     method public java.util.Locale getLocale();
     method public char getMinusSign();
+    method public java.lang.String getMinusSignString();
     method public char getMonetaryDecimalSeparator();
+    method public java.lang.String getMonetaryDecimalSeparatorString();
     method public char getMonetaryGroupingSeparator();
+    method public java.lang.String getMonetaryGroupingSeparatorString();
     method public java.lang.String getNaN();
     method public char getPadEscape();
     method public java.lang.String getPatternForCurrencySpacing(int, boolean);
     method public char getPatternSeparator();
     method public char getPerMill();
+    method public java.lang.String getPerMillString();
     method public char getPercent();
+    method public java.lang.String getPercentString();
     method public char getPlusSign();
+    method public java.lang.String getPlusSignString();
     method public char getSignificantDigit();
     method public android.icu.util.ULocale getULocale();
     method public char getZeroDigit();
     method public void setCurrency(android.icu.util.Currency);
     method public void setCurrencySymbol(java.lang.String);
     method public void setDecimalSeparator(char);
+    method public void setDecimalSeparatorString(java.lang.String);
     method public void setDigit(char);
+    method public void setDigitStrings(java.lang.String[]);
     method public void setExponentMultiplicationSign(java.lang.String);
     method public void setExponentSeparator(java.lang.String);
     method public void setGroupingSeparator(char);
+    method public void setGroupingSeparatorString(java.lang.String);
     method public void setInfinity(java.lang.String);
     method public void setInternationalCurrencySymbol(java.lang.String);
     method public void setMinusSign(char);
+    method public void setMinusSignString(java.lang.String);
     method public void setMonetaryDecimalSeparator(char);
+    method public void setMonetaryDecimalSeparatorString(java.lang.String);
     method public void setMonetaryGroupingSeparator(char);
+    method public void setMonetaryGroupingSeparatorString(java.lang.String);
     method public void setNaN(java.lang.String);
     method public void setPadEscape(char);
     method public void setPatternForCurrencySpacing(int, boolean, java.lang.String);
     method public void setPatternSeparator(char);
     method public void setPerMill(char);
+    method public void setPerMillString(java.lang.String);
     method public void setPercent(char);
+    method public void setPercentString(java.lang.String);
     method public void setPlusSign(char);
+    method public void setPlusSignString(java.lang.String);
     method public void setSignificantDigit(char);
     method public void setZeroDigit(char);
     field public static final int CURRENCY_SPC_CURRENCY_MATCH = 0; // 0x0
@@ -18625,7 +18678,9 @@
     enum_constant public static final android.icu.text.DisplayContext DIALECT_NAMES;
     enum_constant public static final android.icu.text.DisplayContext LENGTH_FULL;
     enum_constant public static final android.icu.text.DisplayContext LENGTH_SHORT;
+    enum_constant public static final android.icu.text.DisplayContext NO_SUBSTITUTE;
     enum_constant public static final android.icu.text.DisplayContext STANDARD_NAMES;
+    enum_constant public static final android.icu.text.DisplayContext SUBSTITUTE;
   }
 
   public static final class DisplayContext.Type extends java.lang.Enum {
@@ -18634,6 +18689,7 @@
     enum_constant public static final android.icu.text.DisplayContext.Type CAPITALIZATION;
     enum_constant public static final android.icu.text.DisplayContext.Type DIALECT_HANDLING;
     enum_constant public static final android.icu.text.DisplayContext.Type DISPLAY_LENGTH;
+    enum_constant public static final android.icu.text.DisplayContext.Type SUBSTITUTE_HANDLING;
   }
 
   public abstract class IDNA {
@@ -18741,6 +18797,7 @@
     method public static android.icu.text.MeasureFormat getInstance(java.util.Locale, android.icu.text.MeasureFormat.FormatWidth, android.icu.text.NumberFormat);
     method public final android.icu.util.ULocale getLocale();
     method public android.icu.text.NumberFormat getNumberFormat();
+    method public java.lang.String getUnitDisplayName(android.icu.util.MeasureUnit);
     method public android.icu.text.MeasureFormat.FormatWidth getWidth();
     method public final int hashCode();
     method public android.icu.util.Measure parseObject(java.lang.String, java.text.ParsePosition);
@@ -20279,6 +20336,7 @@
     field public static final android.icu.util.MeasureUnit MILLILITER;
     field public static final android.icu.util.MeasureUnit MILLIMETER;
     field public static final android.icu.util.MeasureUnit MILLIMETER_OF_MERCURY;
+    field public static final android.icu.util.MeasureUnit MILLIMOLE_PER_LITER;
     field public static final android.icu.util.MeasureUnit MILLISECOND;
     field public static final android.icu.util.MeasureUnit MILLIWATT;
     field public static final android.icu.util.TimeUnit MINUTE;
@@ -20290,6 +20348,7 @@
     field public static final android.icu.util.MeasureUnit OUNCE;
     field public static final android.icu.util.MeasureUnit OUNCE_TROY;
     field public static final android.icu.util.MeasureUnit PARSEC;
+    field public static final android.icu.util.MeasureUnit PART_PER_MILLION;
     field public static final android.icu.util.MeasureUnit PICOMETER;
     field public static final android.icu.util.MeasureUnit PINT;
     field public static final android.icu.util.MeasureUnit PINT_METRIC;
@@ -20413,6 +20472,9 @@
   public static final class TimeZone.SystemTimeZoneType extends java.lang.Enum {
     method public static android.icu.util.TimeZone.SystemTimeZoneType valueOf(java.lang.String);
     method public static final android.icu.util.TimeZone.SystemTimeZoneType[] values();
+    enum_constant public static final android.icu.util.TimeZone.SystemTimeZoneType ANY;
+    enum_constant public static final android.icu.util.TimeZone.SystemTimeZoneType CANONICAL;
+    enum_constant public static final android.icu.util.TimeZone.SystemTimeZoneType CANONICAL_LOCATION;
   }
 
   public final class ULocale implements java.lang.Comparable java.io.Serializable {
@@ -20614,6 +20676,7 @@
     field public static final android.icu.util.VersionInfo ICU_VERSION;
     field public static final android.icu.util.VersionInfo UCOL_BUILDER_VERSION;
     field public static final android.icu.util.VersionInfo UCOL_RUNTIME_VERSION;
+    field public static final android.icu.util.VersionInfo UNICODE_10_0;
     field public static final android.icu.util.VersionInfo UNICODE_1_0;
     field public static final android.icu.util.VersionInfo UNICODE_1_0_1;
     field public static final android.icu.util.VersionInfo UNICODE_1_1_0;
diff --git a/cmds/statsd/Android.mk b/cmds/statsd/Android.mk
index addba8c..3e517bb 100644
--- a/cmds/statsd/Android.mk
+++ b/cmds/statsd/Android.mk
@@ -173,6 +173,7 @@
     tests/metrics/DurationMetricProducer_test.cpp \
     tests/metrics/EventMetricProducer_test.cpp \
     tests/metrics/ValueMetricProducer_test.cpp \
+    tests/metrics/GaugeMetricProducer_test.cpp \
     tests/guardrail/StatsdStats_test.cpp
 
 LOCAL_STATIC_LIBRARIES := \
diff --git a/cmds/statsd/src/Log.h b/cmds/statsd/src/Log.h
index 7852709..87f4cba 100644
--- a/cmds/statsd/src/Log.h
+++ b/cmds/statsd/src/Log.h
@@ -26,5 +26,8 @@
 
 #include <log/log.h>
 
+// Use the local value to turn on/off debug logs instead of using log.tag. properties.
+// The advantage is that in production compiler can remove the logging code if the local
+// DEBUG/VERBOSE is false.
 #define VLOG(...) \
     if (DEBUG) ALOGD(__VA_ARGS__);
diff --git a/cmds/statsd/src/StatsLogProcessor.cpp b/cmds/statsd/src/StatsLogProcessor.cpp
index a3e39b6..f6caca8 100644
--- a/cmds/statsd/src/StatsLogProcessor.cpp
+++ b/cmds/statsd/src/StatsLogProcessor.cpp
@@ -24,6 +24,7 @@
 #include "android-base/stringprintf.h"
 #include "guardrail/StatsdStats.h"
 #include "metrics/CountMetricProducer.h"
+#include "external/StatsPullerManager.h"
 #include "stats_util.h"
 #include "storage/StorageManager.h"
 
@@ -63,10 +64,15 @@
 StatsLogProcessor::StatsLogProcessor(const sp<UidMap>& uidMap,
                                      const sp<AnomalyMonitor>& anomalyMonitor,
                                      const std::function<void(const ConfigKey&)>& sendBroadcast)
-    : mUidMap(uidMap), mAnomalyMonitor(anomalyMonitor), mSendBroadcast(sendBroadcast) {
+    : mUidMap(uidMap),
+      mAnomalyMonitor(anomalyMonitor),
+      mSendBroadcast(sendBroadcast),
+      mTimeBaseSec(time(nullptr)) {
     // On each initialization of StatsLogProcessor, check stats-data directory to see if there is
     // any left over data to be read.
     StorageManager::sendBroadcast(STATS_DATA_DIR, mSendBroadcast);
+    StatsPullerManager statsPullerManager;
+    statsPullerManager.SetTimeBaseSec(mTimeBaseSec);
 }
 
 StatsLogProcessor::~StatsLogProcessor() {
@@ -108,7 +114,7 @@
 
 void StatsLogProcessor::OnConfigUpdated(const ConfigKey& key, const StatsdConfig& config) {
     ALOGD("Updated configuration for key %s", key.ToString().c_str());
-    unique_ptr<MetricsManager> newMetricsManager = std::make_unique<MetricsManager>(key, config);
+    unique_ptr<MetricsManager> newMetricsManager = std::make_unique<MetricsManager>(key, config, mTimeBaseSec);
 
     auto it = mMetricsManagers.find(key);
     if (it == mMetricsManagers.end() && mMetricsManagers.size() > StatsdStats::kMaxConfigCount) {
diff --git a/cmds/statsd/src/StatsLogProcessor.h b/cmds/statsd/src/StatsLogProcessor.h
index a4df23a..7ec4e4b 100644
--- a/cmds/statsd/src/StatsLogProcessor.h
+++ b/cmds/statsd/src/StatsLogProcessor.h
@@ -76,6 +76,8 @@
     // to retrieve the stored data.
     std::function<void(const ConfigKey& key)> mSendBroadcast;
 
+    const long mTimeBaseSec;
+
     FRIEND_TEST(StatsLogProcessorTest, TestRateLimitByteSize);
     FRIEND_TEST(StatsLogProcessorTest, TestRateLimitBroadcast);
     FRIEND_TEST(StatsLogProcessorTest, TestDropWhenByteSizeTooLarge);
diff --git a/cmds/statsd/src/atoms.proto b/cmds/statsd/src/atoms.proto
index a39acb2..c37f05e 100644
--- a/cmds/statsd/src/atoms.proto
+++ b/cmds/statsd/src/atoms.proto
@@ -85,19 +85,19 @@
 
     // Pulled events will start at field 1000.
     oneof pulled {
-        WifiBytesTransferred wifi_bytes_transferred = 1000;
-        WifiBytesTransferredByFgBg wifi_bytes_transferred_by_fg_bg = 1001;
-        MobileBytesTransferred mobile_bytes_transferred = 1002;
-        MobileBytesTransferredByFgBg mobile_bytes_transferred_by_fg_bg = 1003;
-        KernelWakelockPulled kernel_wakelock_pulled = 1004;
-        PowerStatePlatformSleepStatePulled power_state_platform_sleep_state_pulled = 1005;
-        PowerStateVoterPulled power_state_voter_pulled = 1006;
-        PowerStateSubsystemSleepStatePulled power_state_subsystem_sleep_state_pulled = 1007;
-        CpuTimePerFreqPulled cpu_time_per_freq_pulled = 1008;
-        CpuTimePerUidPulled cpu_time_per_uid_pulled = 1009;
-        CpuTimePerUidFreqPulled cpu_time_per_uid_freq_pulled = 1010;
-        WifiActivityEnergyInfoPulled wifi_activity_energy_info_pulled = 1011;
-        ModemActivityInfoPulled modem_activity_info_pulled = 1012;
+        WifiBytesTransfer wifi_bytes_transfer = 1000;
+        WifiBytesTransferByFgBg wifi_bytes_transfer_by_fg_bg = 1001;
+        MobileBytesTransfer mobile_bytes_transfer = 1002;
+        MobileBytesTransferByFgBg mobile_bytes_transfer_by_fg_bg = 1003;
+        KernelWakelock kernel_wakelock = 1004;
+        PlatformSleepState platform_sleep_state = 1005;
+        SleepStateVoter sleep_state_voter = 1006;
+        SubsystemSleepState subsystem_sleep_state = 1007;
+        CpuTimePerFreq cpu_time_per_freq = 1008;
+        CpuTimePerUid cpu_time_per_uid = 1009;
+        CpuTimePerUidFreq cpu_time_per_uid_freq = 1010;
+        WifiActivityEnergyInfo wifi_activity_energy_info = 1011;
+        ModemActivityInfo modem_activity_info = 1012;
         AttributionChainDummyAtom attribution_chain_dummy_atom = 10000;
     }
 }
@@ -846,7 +846,7 @@
  * Pulled from:
  *   StatsCompanionService (using BatteryStats to get which interfaces are wifi)
  */
-message WifiBytesTransferred {
+message WifiBytesTransfer {
     optional int32 uid = 1;
 
     optional int64 rx_bytes = 2;
@@ -864,7 +864,7 @@
  * Pulled from:
  *   StatsCompanionService (using BatteryStats to get which interfaces are wifi)
  */
-message WifiBytesTransferredByFgBg {
+message WifiBytesTransferByFgBg {
     optional int32 uid = 1;
 
     // 1 denotes foreground and 0 denotes background. This is called Set in NetworkStats.
@@ -885,7 +885,7 @@
  * Pulled from:
  *   StatsCompanionService (using BatteryStats to get which interfaces are mobile data)
  */
-message MobileBytesTransferred {
+message MobileBytesTransfer {
     optional int32 uid = 1;
 
     optional int64 rx_bytes = 2;
@@ -903,7 +903,7 @@
  * Pulled from:
  *   StatsCompanionService (using BatteryStats to get which interfaces are mobile data)
  */
-message MobileBytesTransferredByFgBg {
+message MobileBytesTransferByFgBg {
     optional int32 uid = 1;
 
     // 1 denotes foreground and 0 denotes background. This is called Set in NetworkStats.
@@ -925,7 +925,7 @@
  * Pulled from:
  *   StatsCompanionService using KernelWakelockReader.
  */
-message KernelWakelockPulled {
+message KernelWakelock {
     optional string name = 1;
 
     optional int32 count = 2;
@@ -941,7 +941,7 @@
  * Definition here:
  *   hardware/interfaces/power/1.0/types.hal
  */
-message PowerStatePlatformSleepStatePulled {
+message PlatformSleepState {
     optional string name = 1;
     optional uint64 residency_in_msec_since_boot = 2;
     optional uint64 total_transitions = 3;
@@ -954,9 +954,9 @@
  * Definition here:
  *   hardware/interfaces/power/1.0/types.hal
  */
-message PowerStateVoterPulled {
-    optional string power_state_platform_sleep_state_name = 1;
-    optional string power_state_voter_name = 2;
+message SleepStateVoter {
+    optional string platform_sleep_state_name = 1;
+    optional string voter_name = 2;
     optional uint64 total_time_in_msec_voted_for_since_boot = 3;
     optional uint64 total_number_of_times_voted_since_boot = 4;
 }
@@ -967,9 +967,9 @@
  * Definition here:
  *   hardware/interfaces/power/1.1/types.hal
  */
-message PowerStateSubsystemSleepStatePulled {
-    optional string power_state_subsystem_name = 1;
-    optional string power_state_subsystem_sleep_state_name = 2;
+message SubsystemSleepState {
+    optional string subsystem_name = 1;
+    optional string subsystem_sleep_state_name = 2;
     optional uint64 residency_in_msec_since_boot = 3;
     optional uint64 total_transitions = 4;
     optional uint64 last_entry_timestamp_ms = 5;
@@ -1006,17 +1006,17 @@
  * if current time is smaller than last value, there must be a cpu
  * hotplug event, and the current time is taken as delta.
  */
-message CpuTimePerFreqPulled {
+message CpuTimePerFreq {
     optional uint32 cluster = 1;
     optional uint32 freq_index = 2;
-    optional uint64 time = 3;
+    optional uint64 time_ms = 3;
 }
 
 /**
  * Pulls Cpu Time Per Uid.
  * Note that isolated process uid time should be attributed to host uids.
  */
-message CpuTimePerUidPulled {
+message CpuTimePerUid {
     optional uint64 uid = 1;
     optional uint64 user_time_ms = 2;
     optional uint64 sys_time_ms = 3;
@@ -1027,7 +1027,7 @@
  * Note that isolated process uid time should be attributed to host uids.
  * For each uid, we order the time by descending frequencies.
  */
-message CpuTimePerUidFreqPulled {
+message CpuTimePerUidFreq {
     optional uint64 uid = 1;
     optional uint64 freq_idx = 2;
     optional uint64 time_ms = 3;
@@ -1065,7 +1065,7 @@
 /**
  * Pulls Wifi Controller Activity Energy Info
  */
-message WifiActivityEnergyInfoPulled {
+message WifiActivityEnergyInfo {
     // timestamp(wall clock) of record creation
     optional uint64 timestamp_ms = 1;
     // stack reported state
@@ -1084,7 +1084,7 @@
 /**
  * Pulls Modem Activity Energy Info
  */
-message ModemActivityInfoPulled {
+message ModemActivityInfo {
     // timestamp(wall clock) of record creation
     optional uint64 timestamp_ms = 1;
     // sleep time in ms.
diff --git a/cmds/statsd/src/condition/CombinationConditionTracker.cpp b/cmds/statsd/src/condition/CombinationConditionTracker.cpp
index 02aca1a..bb4b817 100644
--- a/cmds/statsd/src/condition/CombinationConditionTracker.cpp
+++ b/cmds/statsd/src/condition/CombinationConditionTracker.cpp
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-#define DEBUG true  // STOPSHIP if true
+#define DEBUG false  // STOPSHIP if true
 #include "Log.h"
 #include "CombinationConditionTracker.h"
 
diff --git a/cmds/statsd/src/config/ConfigManager.cpp b/cmds/statsd/src/config/ConfigManager.cpp
index 164f88f..a6d2719 100644
--- a/cmds/statsd/src/config/ConfigManager.cpp
+++ b/cmds/statsd/src/config/ConfigManager.cpp
@@ -55,8 +55,9 @@
     // for (const auto& pair : configsFromDisk) {
     //    UpdateConfig(pair.first, pair.second);
     //}
-    // this should be called from StatsService when it receives a statsd_config
-    UpdateConfig(ConfigKey(1000, "fake"), build_fake_config());
+
+    // Uncomment the following line and use the hard coded config for development.
+    // UpdateConfig(ConfigKey(1000, "fake"), build_fake_config());
 }
 
 void ConfigManager::AddListener(const sp<ConfigListener>& listener) {
@@ -369,7 +370,7 @@
     GaugeMetric* gaugeMetric = config.add_gauge_metric();
     gaugeMetric->set_name("METRIC_10");
     gaugeMetric->set_what("DEVICE_TEMPERATURE");
-    gaugeMetric->set_gauge_field(DEVICE_TEMPERATURE_KEY);
+    gaugeMetric->mutable_gauge_fields()->add_field_num(DEVICE_TEMPERATURE_KEY);
     gaugeMetric->mutable_bucket()->set_bucket_size_millis(60 * 1000L);
 
     // Event matchers............
diff --git a/cmds/statsd/src/external/CpuTimePerUidFreqPuller.cpp b/cmds/statsd/src/external/CpuTimePerUidFreqPuller.cpp
index e2745d2..9738760 100644
--- a/cmds/statsd/src/external/CpuTimePerUidFreqPuller.cpp
+++ b/cmds/statsd/src/external/CpuTimePerUidFreqPuller.cpp
@@ -70,7 +70,7 @@
     int idx = 0;
     do {
       timeMs = std::stoull(pch);
-      auto ptr = make_shared<LogEvent>(android::util::CPU_TIME_PER_UID_FREQ_PULLED, timestamp);
+      auto ptr = make_shared<LogEvent>(android::util::CPU_TIME_PER_UID_FREQ, timestamp);
       ptr->write(uid);
       ptr->write(idx);
       ptr->write(timeMs);
diff --git a/cmds/statsd/src/external/CpuTimePerUidPuller.cpp b/cmds/statsd/src/external/CpuTimePerUidPuller.cpp
index e0572dc..f69b9b5 100644
--- a/cmds/statsd/src/external/CpuTimePerUidPuller.cpp
+++ b/cmds/statsd/src/external/CpuTimePerUidPuller.cpp
@@ -65,7 +65,7 @@
     pch = strtok(buf, " ");
     uint64_t sysTimeMs = std::stoull(pch);
 
-    auto ptr = make_shared<LogEvent>(android::util::CPU_TIME_PER_UID_PULLED, timestamp);
+    auto ptr = make_shared<LogEvent>(android::util::CPU_TIME_PER_UID, timestamp);
     ptr->write(uid);
     ptr->write(userTimeMs);
     ptr->write(sysTimeMs);
diff --git a/cmds/statsd/src/external/ResourcePowerManagerPuller.cpp b/cmds/statsd/src/external/ResourcePowerManagerPuller.cpp
index 3ee636d..cb9f1cc 100644
--- a/cmds/statsd/src/external/ResourcePowerManagerPuller.cpp
+++ b/cmds/statsd/src/external/ResourcePowerManagerPuller.cpp
@@ -92,7 +92,7 @@
                     const PowerStatePlatformSleepState& state = states[i];
 
                     auto statePtr = make_shared<LogEvent>(
-                            android::util::POWER_STATE_PLATFORM_SLEEP_STATE_PULLED, timestamp);
+                            android::util::PLATFORM_SLEEP_STATE, timestamp);
                     statePtr->write(state.name);
                     statePtr->write(state.residencyInMsecSinceBoot);
                     statePtr->write(state.totalTransitions);
@@ -104,7 +104,7 @@
                          (long long)state.totalTransitions, state.supportedOnlyInSuspend ? 1 : 0);
                     for (auto voter : state.voters) {
                         auto voterPtr =
-                                make_shared<LogEvent>(android::util::POWER_STATE_VOTER_PULLED, timestamp);
+                                make_shared<LogEvent>(android::util::SLEEP_STATE_VOTER, timestamp);
                         voterPtr->write(state.name);
                         voterPtr->write(voter.name);
                         voterPtr->write(voter.totalTimeInMsecVotedForSinceBoot);
@@ -138,7 +138,7 @@
                             for (size_t j = 0; j < subsystem.states.size(); j++) {
                                 const PowerStateSubsystemSleepState& state = subsystem.states[j];
                                 auto subsystemStatePtr = make_shared<LogEvent>(
-                                        android::util::POWER_STATE_SUBSYSTEM_SLEEP_STATE_PULLED, timestamp);
+                                        android::util::SUBSYSTEM_SLEEP_STATE, timestamp);
                                 subsystemStatePtr->write(subsystem.name);
                                 subsystemStatePtr->write(state.name);
                                 subsystemStatePtr->write(state.residencyInMsecSinceBoot);
diff --git a/cmds/statsd/src/external/StatsPullerManager.h b/cmds/statsd/src/external/StatsPullerManager.h
index 2e803c9..00a1475 100644
--- a/cmds/statsd/src/external/StatsPullerManager.h
+++ b/cmds/statsd/src/external/StatsPullerManager.h
@@ -22,33 +22,41 @@
 namespace os {
 namespace statsd {
 
-class StatsPullerManager{
+class StatsPullerManager {
  public:
-  virtual ~StatsPullerManager() {}
+    virtual ~StatsPullerManager() {}
 
-  virtual void RegisterReceiver(int tagId, wp<PullDataReceiver> receiver, long intervalMs) {
-    mPullerManager.RegisterReceiver(tagId, receiver, intervalMs);
-  };
+    virtual void RegisterReceiver(int tagId,
+                                  wp <PullDataReceiver> receiver,
+                                  long intervalMs) {
+        mPullerManager.RegisterReceiver(tagId, receiver, intervalMs);
+    };
 
-  virtual void UnRegisterReceiver(int tagId, wp<PullDataReceiver> receiver) {
-    mPullerManager.UnRegisterReceiver(tagId, receiver);
-  };
+    virtual void UnRegisterReceiver(int tagId, wp <PullDataReceiver> receiver) {
+        mPullerManager.UnRegisterReceiver(tagId, receiver);
+    };
 
-  // Verify if we know how to pull for this matcher
-  bool PullerForMatcherExists(int tagId) {
-    return mPullerManager.PullerForMatcherExists(tagId);
-  }
+    // Verify if we know how to pull for this matcher
+    bool PullerForMatcherExists(int tagId) {
+        return mPullerManager.PullerForMatcherExists(tagId);
+    }
 
-  void OnAlarmFired() {
-    mPullerManager.OnAlarmFired();
-  }
+    void OnAlarmFired() {
+        mPullerManager.OnAlarmFired();
+    }
 
-  virtual bool Pull(const int tagId, vector<std::shared_ptr<LogEvent>>* data) {
-    return mPullerManager.Pull(tagId, data);
-  }
+    virtual bool
+    Pull(const int tagId, vector<std::shared_ptr<LogEvent>>* data) {
+        return mPullerManager.Pull(tagId, data);
+    }
+
+    virtual void SetTimeBaseSec(const long timeBaseSec) {
+        mPullerManager.SetTimeBaseSec(timeBaseSec);
+    }
 
  private:
-  StatsPullerManagerImpl& mPullerManager = StatsPullerManagerImpl::GetInstance();
+    StatsPullerManagerImpl
+        & mPullerManager = StatsPullerManagerImpl::GetInstance();
 };
 
 }  // namespace statsd
diff --git a/cmds/statsd/src/external/StatsPullerManagerImpl.cpp b/cmds/statsd/src/external/StatsPullerManagerImpl.cpp
index c4688a2..d707f85 100644
--- a/cmds/statsd/src/external/StatsPullerManagerImpl.cpp
+++ b/cmds/statsd/src/external/StatsPullerManagerImpl.cpp
@@ -44,30 +44,30 @@
 namespace statsd {
 
 StatsPullerManagerImpl::StatsPullerManagerImpl()
-    : mCurrentPullingInterval(LONG_MAX), mPullStartTimeMs(get_pull_start_time_ms()) {
+    : mCurrentPullingInterval(LONG_MAX) {
     shared_ptr<StatsPuller> statsCompanionServicePuller = make_shared<StatsCompanionServicePuller>();
     shared_ptr<StatsPuller> resourcePowerManagerPuller = make_shared<ResourcePowerManagerPuller>();
     shared_ptr<StatsPuller> cpuTimePerUidPuller = make_shared<CpuTimePerUidPuller>();
     shared_ptr<StatsPuller> cpuTimePerUidFreqPuller = make_shared<CpuTimePerUidFreqPuller>();
 
-    mPullers.insert({android::util::KERNEL_WAKELOCK_PULLED,
+    mPullers.insert({android::util::KERNEL_WAKELOCK,
                      statsCompanionServicePuller});
-    mPullers.insert({android::util::WIFI_BYTES_TRANSFERRED,
+    mPullers.insert({android::util::WIFI_BYTES_TRANSFER,
                      statsCompanionServicePuller});
-    mPullers.insert({android::util::MOBILE_BYTES_TRANSFERRED,
+    mPullers.insert({android::util::MOBILE_BYTES_TRANSFER,
                      statsCompanionServicePuller});
-    mPullers.insert({android::util::WIFI_BYTES_TRANSFERRED_BY_FG_BG,
+    mPullers.insert({android::util::WIFI_BYTES_TRANSFER_BY_FG_BG,
                      statsCompanionServicePuller});
-    mPullers.insert({android::util::MOBILE_BYTES_TRANSFERRED_BY_FG_BG,
+    mPullers.insert({android::util::MOBILE_BYTES_TRANSFER_BY_FG_BG,
                      statsCompanionServicePuller});
-    mPullers.insert({android::util::POWER_STATE_PLATFORM_SLEEP_STATE_PULLED,
+    mPullers.insert({android::util::PLATFORM_SLEEP_STATE,
                      resourcePowerManagerPuller});
-    mPullers.insert({android::util::POWER_STATE_VOTER_PULLED,
+    mPullers.insert({android::util::SLEEP_STATE_VOTER,
                      resourcePowerManagerPuller});
-    mPullers.insert({android::util::POWER_STATE_SUBSYSTEM_SLEEP_STATE_PULLED,
+    mPullers.insert({android::util::SUBSYSTEM_SLEEP_STATE,
                      resourcePowerManagerPuller});
-    mPullers.insert({android::util::CPU_TIME_PER_UID_PULLED, cpuTimePerUidPuller});
-    mPullers.insert({android::util::CPU_TIME_PER_UID_FREQ_PULLED, cpuTimePerUidFreqPuller});
+    mPullers.insert({android::util::CPU_TIME_PER_UID, cpuTimePerUidPuller});
+    mPullers.insert({android::util::CPU_TIME_PER_UID_FREQ, cpuTimePerUidFreqPuller});
 
     mStatsCompanionService = StatsService::getStatsCompanionService();
 }
@@ -94,11 +94,6 @@
     return mPullers.find(tagId) != mPullers.end();
 }
 
-long StatsPullerManagerImpl::get_pull_start_time_ms() const {
-    // TODO: limit and align pull intervals to 10min boundaries if this turns out to be a problem
-    return time(nullptr) * 1000;
-}
-
 void StatsPullerManagerImpl::RegisterReceiver(int tagId, wp<PullDataReceiver> receiver,
                                               long intervalMs) {
     AutoMutex _l(mReceiversLock);
@@ -114,12 +109,17 @@
     receiverInfo.timeInfo.first = intervalMs;
     receivers.push_back(receiverInfo);
 
+    // Round it to the nearest minutes. This is the limit of alarm manager.
+    // In practice, we should limit it higher.
+    long roundedIntervalMs = intervalMs/1000/60 * 1000 * 60;
     // There is only one alarm for all pulled events. So only set it to the smallest denom.
-    if (intervalMs < mCurrentPullingInterval) {
+    if (roundedIntervalMs < mCurrentPullingInterval) {
         VLOG("Updating pulling interval %ld", intervalMs);
-        mCurrentPullingInterval = intervalMs;
+        mCurrentPullingInterval = roundedIntervalMs;
+        long currentTimeMs = time(nullptr) * 1000;
+        long nextAlarmTimeMs = currentTimeMs + mCurrentPullingInterval - (currentTimeMs - mTimeBaseSec * 1000) % mCurrentPullingInterval;
         if (mStatsCompanionService != nullptr) {
-            mStatsCompanionService->setPullingAlarms(mPullStartTimeMs, mCurrentPullingInterval);
+            mStatsCompanionService->setPullingAlarms(nextAlarmTimeMs, mCurrentPullingInterval);
         } else {
             VLOG("Failed to update pulling interval");
         }
@@ -146,7 +146,7 @@
 void StatsPullerManagerImpl::OnAlarmFired() {
     AutoMutex _l(mReceiversLock);
 
-    uint64_t currentTimeMs = time(nullptr) * 1000;
+    uint64_t currentTimeMs = time(nullptr) /60 * 60 * 1000;
 
     vector<pair<int, vector<ReceiverInfo*>>> needToPull =
             vector<pair<int, vector<ReceiverInfo*>>>();
diff --git a/cmds/statsd/src/external/StatsPullerManagerImpl.h b/cmds/statsd/src/external/StatsPullerManagerImpl.h
index 306cc32..7c59f66 100644
--- a/cmds/statsd/src/external/StatsPullerManagerImpl.h
+++ b/cmds/statsd/src/external/StatsPullerManagerImpl.h
@@ -47,6 +47,8 @@
 
     bool Pull(const int tagId, vector<std::shared_ptr<LogEvent>>* data);
 
+    void SetTimeBaseSec(long timeBaseSec) {mTimeBaseSec = timeBaseSec;};
+
 private:
     StatsPullerManagerImpl();
 
@@ -73,11 +75,8 @@
 
     // for pulled metrics, it is important for the buckets to be aligned to multiple of smallest
     // bucket size. All pulled metrics start pulling based on this time, so that they can be
-    // correctly attributed to the correct buckets. Pulled data attach a timestamp which is the
-    // request time.
-    const long mPullStartTimeMs;
-
-    long get_pull_start_time_ms() const;
+    // correctly attributed to the correct buckets.
+    long mTimeBaseSec;
 
     LogEvent parse_pulled_data(String16 data);
 };
diff --git a/cmds/statsd/src/logd/LogEvent.cpp b/cmds/statsd/src/logd/LogEvent.cpp
index 1032138..01487f0 100644
--- a/cmds/statsd/src/logd/LogEvent.cpp
+++ b/cmds/statsd/src/logd/LogEvent.cpp
@@ -69,6 +69,13 @@
     return false;
 }
 
+bool LogEvent::write(int64_t value) {
+    if (mContext) {
+        return android_log_write_int64(mContext, value) >= 0;
+    }
+    return false;
+}
+
 bool LogEvent::write(uint64_t value) {
     if (mContext) {
         return android_log_write_int64(mContext, value) >= 0;
@@ -224,7 +231,7 @@
     if (elem.type == EVENT_TYPE_INT) {
         pair.set_value_int(elem.data.int32);
     } else if (elem.type == EVENT_TYPE_LONG) {
-        pair.set_value_int(elem.data.int64);
+        pair.set_value_long(elem.data.int64);
     } else if (elem.type == EVENT_TYPE_STRING) {
         pair.set_value_str(elem.data.string);
     } else if (elem.type == EVENT_TYPE_FLOAT) {
diff --git a/cmds/statsd/src/logd/LogEvent.h b/cmds/statsd/src/logd/LogEvent.h
index 176e16e..6ff6b87 100644
--- a/cmds/statsd/src/logd/LogEvent.h
+++ b/cmds/statsd/src/logd/LogEvent.h
@@ -110,6 +110,10 @@
      */
     void setTimestampNs(uint64_t timestampNs) {mTimestampNs = timestampNs;}
 
+    int size() const {
+        return mElements.size();
+    }
+
 private:
     /**
      * Don't copy, it's slower. If we really need this we can add it but let's try to
diff --git a/cmds/statsd/src/metrics/CountMetricProducer.cpp b/cmds/statsd/src/metrics/CountMetricProducer.cpp
index 7b865c2..bc12a78 100644
--- a/cmds/statsd/src/metrics/CountMetricProducer.cpp
+++ b/cmds/statsd/src/metrics/CountMetricProducer.cpp
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-#define DEBUG true  // STOPSHIP if true
+#define DEBUG false  // STOPSHIP if true
 #include "Log.h"
 
 #include "CountMetricProducer.h"
diff --git a/cmds/statsd/src/metrics/DurationMetricProducer.cpp b/cmds/statsd/src/metrics/DurationMetricProducer.cpp
index 6afbe45..220861d 100644
--- a/cmds/statsd/src/metrics/DurationMetricProducer.cpp
+++ b/cmds/statsd/src/metrics/DurationMetricProducer.cpp
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-#define DEBUG true
+#define DEBUG false
 
 #include "Log.h"
 #include "DurationMetricProducer.h"
diff --git a/cmds/statsd/src/metrics/EventMetricProducer.cpp b/cmds/statsd/src/metrics/EventMetricProducer.cpp
index 4752997..6a072b0 100644
--- a/cmds/statsd/src/metrics/EventMetricProducer.cpp
+++ b/cmds/statsd/src/metrics/EventMetricProducer.cpp
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-#define DEBUG true  // STOPSHIP if true
+#define DEBUG false  // STOPSHIP if true
 #include "Log.h"
 
 #include "EventMetricProducer.h"
diff --git a/cmds/statsd/src/metrics/GaugeMetricProducer.cpp b/cmds/statsd/src/metrics/GaugeMetricProducer.cpp
index ae9b86f..64026330 100644
--- a/cmds/statsd/src/metrics/GaugeMetricProducer.cpp
+++ b/cmds/statsd/src/metrics/GaugeMetricProducer.cpp
@@ -14,16 +14,13 @@
 * limitations under the License.
 */
 
-#define DEBUG true  // STOPSHIP if true
+#define DEBUG false  // STOPSHIP if true
 #include "Log.h"
 
 #include "GaugeMetricProducer.h"
 #include "guardrail/StatsdStats.h"
-#include "stats_util.h"
 
 #include <cutils/log.h>
-#include <limits.h>
-#include <stdlib.h>
 
 using android::util::FIELD_COUNT_REPEATED;
 using android::util::FIELD_TYPE_BOOL;
@@ -37,6 +34,8 @@
 using std::string;
 using std::unordered_map;
 using std::vector;
+using std::make_shared;
+using std::shared_ptr;
 
 namespace android {
 namespace os {
@@ -61,21 +60,27 @@
 // for GaugeBucketInfo
 const int FIELD_ID_START_BUCKET_NANOS = 1;
 const int FIELD_ID_END_BUCKET_NANOS = 2;
-const int FIELD_ID_GAUGE = 3;
+const int FIELD_ID_ATOM = 3;
 
 GaugeMetricProducer::GaugeMetricProducer(const ConfigKey& key, const GaugeMetric& metric,
                                          const int conditionIndex,
-                                         const sp<ConditionWizard>& wizard, const int pullTagId,
-                                         const int64_t startTimeNs)
+                                         const sp<ConditionWizard>& wizard, const int atomTagId,
+                                         const int pullTagId, const uint64_t startTimeNs,
+                                         shared_ptr<StatsPullerManager> statsPullerManager)
     : MetricProducer(metric.name(), key, startTimeNs, conditionIndex, wizard),
-      mGaugeField(metric.gauge_field()),
-      mPullTagId(pullTagId) {
+      mStatsPullerManager(statsPullerManager),
+      mPullTagId(pullTagId),
+      mAtomTagId(atomTagId) {
     if (metric.has_bucket() && metric.bucket().has_bucket_size_millis()) {
         mBucketSizeNs = metric.bucket().bucket_size_millis() * 1000 * 1000;
     } else {
         mBucketSizeNs = kDefaultGaugemBucketSizeNs;
     }
 
+    for (int i = 0; i < metric.gauge_fields().field_num_size(); i++) {
+        mGaugeFields.push_back(metric.gauge_fields().field_num(i));
+    }
+
     // TODO: use UidMap if uid->pkg_name is required
     mDimension.insert(mDimension.begin(), metric.dimension().begin(), metric.dimension().end());
 
@@ -87,7 +92,7 @@
 
     // Kicks off the puller immediately.
     if (mPullTagId != -1) {
-        mStatsPullerManager.RegisterReceiver(mPullTagId, this,
+        mStatsPullerManager->RegisterReceiver(mPullTagId, this,
                                              metric.bucket().bucket_size_millis());
     }
 
@@ -95,10 +100,19 @@
          (long long)mBucketSizeNs, (long long)mStartTimeNs);
 }
 
+// for testing
+GaugeMetricProducer::GaugeMetricProducer(const ConfigKey& key, const GaugeMetric& metric,
+                                         const int conditionIndex,
+                                         const sp<ConditionWizard>& wizard, const int pullTagId,
+                                         const int atomTagId, const int64_t startTimeNs)
+    : GaugeMetricProducer(key, metric, conditionIndex, wizard, pullTagId, atomTagId, startTimeNs,
+                          make_shared<StatsPullerManager>()) {
+}
+
 GaugeMetricProducer::~GaugeMetricProducer() {
     VLOG("~GaugeMetricProducer() called");
     if (mPullTagId != -1) {
-        mStatsPullerManager.UnRegisterReceiver(mPullTagId, this);
+        mStatsPullerManager->UnRegisterReceiver(mPullTagId, this);
     }
 }
 
@@ -149,10 +163,26 @@
                                (long long)bucket.mBucketStartNs);
             protoOutput->write(FIELD_TYPE_INT64 | FIELD_ID_END_BUCKET_NANOS,
                                (long long)bucket.mBucketEndNs);
-            protoOutput->write(FIELD_TYPE_INT64 | FIELD_ID_GAUGE, (long long)bucket.mGauge);
+            long long atomToken = protoOutput->start(FIELD_TYPE_MESSAGE | FIELD_ID_ATOM);
+            long long eventToken = protoOutput->start(FIELD_TYPE_MESSAGE | mAtomTagId);
+            for (const auto& pair : bucket.mEvent->kv) {
+                if (pair.has_value_int()) {
+                    protoOutput->write(FIELD_TYPE_INT32 | pair.key(), pair.value_int());
+                } else if (pair.has_value_long()) {
+                    protoOutput->write(FIELD_TYPE_INT64 | pair.key(), pair.value_long());
+                } else if (pair.has_value_str()) {
+                    protoOutput->write(FIELD_TYPE_STRING | pair.key(), pair.value_str());
+                } else if (pair.has_value_long()) {
+                    protoOutput->write(FIELD_TYPE_FLOAT | pair.key(), pair.value_float());
+                } else if (pair.has_value_bool()) {
+                    protoOutput->write(FIELD_TYPE_BOOL | pair.key(), pair.value_bool());
+                }
+            }
+            protoOutput->end(eventToken);
+            protoOutput->end(atomToken);
             protoOutput->end(bucketInfoToken);
-            VLOG("\t bucket [%lld - %lld] count: %lld", (long long)bucket.mBucketStartNs,
-                 (long long)bucket.mBucketEndNs, (long long)bucket.mGauge);
+            VLOG("\t bucket [%lld - %lld] content: %s", (long long)bucket.mBucketStartNs,
+                 (long long)bucket.mBucketEndNs, bucket.mEvent->ToString().c_str());
         }
         protoOutput->end(wrapperToken);
     }
@@ -174,6 +204,7 @@
     if (mPullTagId == -1) {
         return;
     }
+    // No need to pull again. Either scheduled pull or condition on true happened
     if (!mCondition) {
         return;
     }
@@ -182,7 +213,7 @@
         return;
     }
     vector<std::shared_ptr<LogEvent>> allData;
-    if (!mStatsPullerManager.Pull(mPullTagId, &allData)) {
+    if (!mStatsPullerManager->Pull(mPullTagId, &allData)) {
         ALOGE("Stats puller failed for tag: %d", mPullTagId);
         return;
     }
@@ -196,20 +227,25 @@
     VLOG("Metric %s onSlicedConditionMayChange", mName.c_str());
 }
 
-int64_t GaugeMetricProducer::getGauge(const LogEvent& event) {
-    status_t err = NO_ERROR;
-    int64_t val = event.GetLong(mGaugeField, &err);
-    if (err == NO_ERROR) {
-        return val;
+shared_ptr<EventKV> GaugeMetricProducer::getGauge(const LogEvent& event) {
+    shared_ptr<EventKV> ret = make_shared<EventKV>();
+    if (mGaugeFields.size() == 0) {
+        for (int i = 1; i <= event.size(); i++) {
+            ret->kv.push_back(event.GetKeyValueProto(i));
+        }
     } else {
-        VLOG("Can't find value in message.");
-        return -1;
+        for (int i = 0; i < (int)mGaugeFields.size(); i++) {
+            ret->kv.push_back(event.GetKeyValueProto(mGaugeFields[i]));
+        }
     }
+    return ret;
 }
 
 void GaugeMetricProducer::onDataPulled(const std::vector<std::shared_ptr<LogEvent>>& allData) {
     std::lock_guard<std::mutex> lock(mMutex);
-
+    if (allData.size() == 0) {
+        return;
+    }
     for (const auto& data : allData) {
         onMatchedLogEventLocked(0, *data);
     }
@@ -247,25 +283,48 @@
              (long long)mCurrentBucketStartTimeNs);
         return;
     }
-
-    // When the event happens in a new bucket, flush the old buckets.
-    if (eventTimeNs >= mCurrentBucketStartTimeNs + mBucketSizeNs) {
-        flushIfNeededLocked(eventTimeNs);
-    }
+    flushIfNeededLocked(eventTimeNs);
 
     // For gauge metric, we just simply use the first gauge in the given bucket.
-    if (!mCurrentSlicedBucket->empty()) {
+    if (mCurrentSlicedBucket->find(eventKey) != mCurrentSlicedBucket->end()) {
         return;
     }
-    const long gauge = getGauge(event);
-    if (gauge >= 0) {
-        if (hitGuardRailLocked(eventKey)) {
-            return;
-        }
-        (*mCurrentSlicedBucket)[eventKey] = gauge;
+    shared_ptr<EventKV> gauge = getGauge(event);
+    if (hitGuardRailLocked(eventKey)) {
+        return;
     }
-    for (auto& tracker : mAnomalyTrackers) {
-        tracker->detectAndDeclareAnomaly(eventTimeNs, mCurrentBucketNum, eventKey, gauge);
+    (*mCurrentSlicedBucket)[eventKey] = gauge;
+    // Anomaly detection on gauge metric only works when there is one numeric
+    // field specified.
+    if (mAnomalyTrackers.size() > 0) {
+        if (gauge->kv.size() == 1) {
+            KeyValuePair pair = gauge->kv[0];
+            long gaugeVal = 0;
+            if (pair.has_value_int()) {
+                gaugeVal = (long)pair.value_int();
+            } else if (pair.has_value_long()) {
+                gaugeVal = pair.value_long();
+            }
+            for (auto& tracker : mAnomalyTrackers) {
+                tracker->detectAndDeclareAnomaly(eventTimeNs, mCurrentBucketNum, eventKey,
+                                                 gaugeVal);
+            }
+        }
+    }
+}
+
+void GaugeMetricProducer::updateCurrentSlicedBucketForAnomaly() {
+    mCurrentSlicedBucketForAnomaly->clear();
+    status_t err = NO_ERROR;
+    for (const auto& slice : *mCurrentSlicedBucket) {
+        KeyValuePair pair = slice.second->kv[0];
+        long gaugeVal = 0;
+        if (pair.has_value_int()) {
+            gaugeVal = (long)pair.value_int();
+        } else if (pair.has_value_long()) {
+            gaugeVal = pair.value_long();
+        }
+        (*mCurrentSlicedBucketForAnomaly)[slice.first] = gaugeVal;
     }
 }
 
@@ -276,6 +335,8 @@
 // the GaugeMetricProducer while holding the lock.
 void GaugeMetricProducer::flushIfNeededLocked(const uint64_t& eventTimeNs) {
     if (eventTimeNs < mCurrentBucketStartTimeNs + mBucketSizeNs) {
+        VLOG("eventTime is %lld, less than next bucket start time %lld", (long long)eventTimeNs,
+             (long long)(mCurrentBucketStartTimeNs + mBucketSizeNs));
         return;
     }
 
@@ -285,19 +346,22 @@
     info.mBucketNum = mCurrentBucketNum;
 
     for (const auto& slice : *mCurrentSlicedBucket) {
-        info.mGauge = slice.second;
+        info.mEvent = slice.second;
         auto& bucketList = mPastBuckets[slice.first];
         bucketList.push_back(info);
-        VLOG("gauge metric %s, dump key value: %s -> %lld", mName.c_str(), slice.first.c_str(),
-             (long long)slice.second);
+        VLOG("gauge metric %s, dump key value: %s -> %s", mName.c_str(),
+             slice.first.c_str(), slice.second->ToString().c_str());
     }
 
     // Reset counters
-    for (auto& tracker : mAnomalyTrackers) {
-        tracker->addPastBucket(mCurrentSlicedBucket, mCurrentBucketNum);
+    if (mAnomalyTrackers.size() > 0) {
+        updateCurrentSlicedBucketForAnomaly();
+        for (auto& tracker : mAnomalyTrackers) {
+            tracker->addPastBucket(mCurrentSlicedBucketForAnomaly, mCurrentBucketNum);
+        }
     }
 
-    mCurrentSlicedBucket = std::make_shared<DimToValMap>();
+    mCurrentSlicedBucket = std::make_shared<DimToEventKVMap>();
 
     // Adjusts the bucket start time
     int64_t numBucketsForward = (eventTimeNs - mCurrentBucketStartTimeNs) / mBucketSizeNs;
diff --git a/cmds/statsd/src/metrics/GaugeMetricProducer.h b/cmds/statsd/src/metrics/GaugeMetricProducer.h
index 6e6f2bb..4a037ff 100644
--- a/cmds/statsd/src/metrics/GaugeMetricProducer.h
+++ b/cmds/statsd/src/metrics/GaugeMetricProducer.h
@@ -26,7 +26,7 @@
 #include "../matchers/matcher_util.h"
 #include "MetricProducer.h"
 #include "frameworks/base/cmds/statsd/src/statsd_config.pb.h"
-#include "stats_util.h"
+#include "../stats_util.h"
 
 namespace android {
 namespace os {
@@ -35,7 +35,7 @@
 struct GaugeBucket {
     int64_t mBucketStartNs;
     int64_t mBucketEndNs;
-    int64_t mGauge;
+    std::shared_ptr<EventKV> mEvent;
     uint64_t mBucketNum;
 };
 
@@ -49,7 +49,7 @@
     // for all metrics.
     GaugeMetricProducer(const ConfigKey& key, const GaugeMetric& countMetric,
                         const int conditionIndex, const sp<ConditionWizard>& wizard,
-                        const int pullTagId, const int64_t startTimeNs);
+                        const int pullTagId, const int atomTagId, const int64_t startTimeNs);
 
     virtual ~GaugeMetricProducer();
 
@@ -72,6 +72,12 @@
     void onDumpReportLocked(const uint64_t dumpTimeNs,
                             android::util::ProtoOutputStream* protoOutput) override;
 
+    // for testing
+    GaugeMetricProducer(const ConfigKey& key, const GaugeMetric& gaugeMetric,
+                        const int conditionIndex, const sp<ConditionWizard>& wizard,
+                        const int pullTagId, const int atomTagId, const uint64_t startTimeNs,
+                        std::shared_ptr<StatsPullerManager> statsPullerManager);
+
     // Internal interface to handle condition change.
     void onConditionChangedLocked(const bool conditionMet, const uint64_t eventTime) override;
 
@@ -84,12 +90,10 @@
     // Util function to flush the old packet.
     void flushIfNeededLocked(const uint64_t& eventTime);
 
-    // The default bucket size for gauge metric is 1 second.
-    static const uint64_t kDefaultGaugemBucketSizeNs = 1000 * 1000 * 1000;
+    // The default bucket size for gauge metric is 1 hr.
+    static const uint64_t kDefaultGaugemBucketSizeNs = 60ULL * 60 * 1000 * 1000 * 1000;
 
-    const int32_t mGaugeField;
-
-    StatsPullerManager mStatsPullerManager;
+    std::shared_ptr<StatsPullerManager> mStatsPullerManager;
     // tagId for pulled data. -1 if this is not pulled
     const int mPullTagId;
 
@@ -98,9 +102,21 @@
     std::unordered_map<HashableDimensionKey, std::vector<GaugeBucket>> mPastBuckets;
 
     // The current bucket.
-    std::shared_ptr<DimToValMap> mCurrentSlicedBucket = std::make_shared<DimToValMap>();
+    std::shared_ptr<DimToEventKVMap> mCurrentSlicedBucket = std::make_shared<DimToEventKVMap>();
 
-    int64_t getGauge(const LogEvent& event);
+    // The current bucket for anomaly detection.
+    std::shared_ptr<DimToValMap> mCurrentSlicedBucketForAnomaly = std::make_shared<DimToValMap>();
+
+    // Translate Atom based bucket to single numeric value bucket for anomaly
+    void updateCurrentSlicedBucketForAnomaly();
+
+    int mAtomTagId;
+
+    // Whitelist of fields to report. Empty means all are reported.
+    std::vector<int> mGaugeFields;
+
+    // apply a whitelist on the original input
+    std::shared_ptr<EventKV> getGauge(const LogEvent& event);
 
     // Util function to check whether the specified dimension hits the guardrail.
     bool hitGuardRailLocked(const HashableDimensionKey& newKey);
diff --git a/cmds/statsd/src/metrics/MetricsManager.cpp b/cmds/statsd/src/metrics/MetricsManager.cpp
index 3d0e20c..a5900f4 100644
--- a/cmds/statsd/src/metrics/MetricsManager.cpp
+++ b/cmds/statsd/src/metrics/MetricsManager.cpp
@@ -44,9 +44,9 @@
 
 const int FIELD_ID_METRICS = 1;
 
-MetricsManager::MetricsManager(const ConfigKey& key, const StatsdConfig& config) : mConfigKey(key) {
+MetricsManager::MetricsManager(const ConfigKey& key, const StatsdConfig& config, const long timeBaseSec) : mConfigKey(key) {
     mConfigValid =
-            initStatsdConfig(key, config, mTagIds, mAllAtomMatchers, mAllConditionTrackers,
+            initStatsdConfig(key, config, timeBaseSec, mTagIds, mAllAtomMatchers, mAllConditionTrackers,
                              mAllMetricProducers, mAllAnomalyTrackers, mConditionToMetricMap,
                              mTrackerToMetricMap, mTrackerToConditionMap);
 
diff --git a/cmds/statsd/src/metrics/MetricsManager.h b/cmds/statsd/src/metrics/MetricsManager.h
index 34ea667..738adc9 100644
--- a/cmds/statsd/src/metrics/MetricsManager.h
+++ b/cmds/statsd/src/metrics/MetricsManager.h
@@ -34,7 +34,7 @@
 // A MetricsManager is responsible for managing metrics from one single config source.
 class MetricsManager {
 public:
-    MetricsManager(const ConfigKey& configKey, const StatsdConfig& config);
+    MetricsManager(const ConfigKey& configKey, const StatsdConfig& config, const long timeBaseSec);
 
     virtual ~MetricsManager();
 
diff --git a/cmds/statsd/src/metrics/ValueMetricProducer.cpp b/cmds/statsd/src/metrics/ValueMetricProducer.cpp
index 1eabf11..7efa6cd 100644
--- a/cmds/statsd/src/metrics/ValueMetricProducer.cpp
+++ b/cmds/statsd/src/metrics/ValueMetricProducer.cpp
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-#define DEBUG true  // STOPSHIP if true
+#define DEBUG false  // STOPSHIP if true
 #include "Log.h"
 
 #include "ValueMetricProducer.h"
diff --git a/cmds/statsd/src/metrics/duration_helper/MaxDurationTracker.cpp b/cmds/statsd/src/metrics/duration_helper/MaxDurationTracker.cpp
index 08c9135..95c8a59 100644
--- a/cmds/statsd/src/metrics/duration_helper/MaxDurationTracker.cpp
+++ b/cmds/statsd/src/metrics/duration_helper/MaxDurationTracker.cpp
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-#define DEBUG true
+#define DEBUG false
 
 #include "Log.h"
 #include "MaxDurationTracker.h"
diff --git a/cmds/statsd/src/metrics/duration_helper/OringDurationTracker.cpp b/cmds/statsd/src/metrics/duration_helper/OringDurationTracker.cpp
index 8122744..36e25edf 100644
--- a/cmds/statsd/src/metrics/duration_helper/OringDurationTracker.cpp
+++ b/cmds/statsd/src/metrics/duration_helper/OringDurationTracker.cpp
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-#define DEBUG true
+#define DEBUG false
 #include "Log.h"
 #include "OringDurationTracker.h"
 #include "guardrail/StatsdStats.h"
diff --git a/cmds/statsd/src/metrics/metrics_manager_util.cpp b/cmds/statsd/src/metrics/metrics_manager_util.cpp
index 943becb..200ef0b 100644
--- a/cmds/statsd/src/metrics/metrics_manager_util.cpp
+++ b/cmds/statsd/src/metrics/metrics_manager_util.cpp
@@ -188,7 +188,7 @@
     return true;
 }
 
-bool initMetrics(const ConfigKey& key, const StatsdConfig& config,
+bool initMetrics(const ConfigKey& key, const StatsdConfig& config, const long timeBaseSec,
                  const unordered_map<string, int>& logTrackerMap,
                  const unordered_map<string, int>& conditionTrackerMap,
                  const vector<sp<LogMatchingTracker>>& allAtomMatchers,
@@ -202,7 +202,13 @@
                                 config.event_metric_size() + config.value_metric_size();
     allMetricProducers.reserve(allMetricsCount);
     StatsPullerManager statsPullerManager;
-    uint64_t startTimeNs = time(nullptr) * NS_PER_SEC;
+    // Align all buckets to same instant in MIN_BUCKET_SIZE_SEC, so that avoid alarm
+    // clock will not grow very aggressive. New metrics will be delayed up to
+    // MIN_BUCKET_SIZE_SEC before starting.
+    long currentTimeSec = time(nullptr);
+    uint64_t startTimeNs = (currentTimeSec + kMinBucketSizeSec -
+                            (currentTimeSec - timeBaseSec) % kMinBucketSizeSec) *
+                           NS_PER_SEC;
 
     // Build MetricProducers for each metric defined in config.
     // build CountMetricProducer
@@ -370,15 +376,11 @@
 
         sp<LogMatchingTracker> atomMatcher = allAtomMatchers.at(trackerIndex);
         // If it is pulled atom, it should be simple matcher with one tagId.
-        int pullTagId = -1;
-        for (int tagId : atomMatcher->getTagIds()) {
-            if (statsPullerManager.PullerForMatcherExists(tagId)) {
-                if (atomMatcher->getTagIds().size() != 1) {
-                    return false;
-                }
-                pullTagId = tagId;
-            }
+        if (atomMatcher->getTagIds().size() != 1) {
+            return false;
         }
+        int atomTagId = *(atomMatcher->getTagIds().begin());
+        int pullTagId = statsPullerManager.PullerForMatcherExists(atomTagId) ? atomTagId : -1;
 
         int conditionIndex = -1;
         if (metric.has_condition()) {
@@ -404,7 +406,17 @@
     for (int i = 0; i < config.gauge_metric_size(); i++) {
         const GaugeMetric& metric = config.gauge_metric(i);
         if (!metric.has_what()) {
-            ALOGW("cannot find \"what\" in ValueMetric \"%s\"", metric.name().c_str());
+            ALOGW("cannot find \"what\" in GaugeMetric \"%s\"", metric.name().c_str());
+            return false;
+        }
+
+        if (((!metric.gauge_fields().has_include_all() ||
+              (metric.gauge_fields().has_include_all() &&
+               metric.gauge_fields().include_all() == false)) &&
+             metric.gauge_fields().field_num_size() == 0) ||
+            (metric.gauge_fields().has_include_all() && metric.gauge_fields().include_all() == true &&
+             metric.gauge_fields().field_num_size() > 0)) {
+            ALOGW("Incorrect field filter setting in GaugeMetric %s", metric.name().c_str());
             return false;
         }
 
@@ -419,15 +431,11 @@
 
         sp<LogMatchingTracker> atomMatcher = allAtomMatchers.at(trackerIndex);
         // If it is pulled atom, it should be simple matcher with one tagId.
-        int pullTagId = -1;
-        for (int tagId : atomMatcher->getTagIds()) {
-            if (statsPullerManager.PullerForMatcherExists(tagId)) {
-                if (atomMatcher->getTagIds().size() != 1) {
-                    return false;
-                }
-                pullTagId = tagId;
-            }
+        if (atomMatcher->getTagIds().size() != 1) {
+            return false;
         }
+        int atomTagId = *(atomMatcher->getTagIds().begin());
+        int pullTagId = statsPullerManager.PullerForMatcherExists(atomTagId) ? atomTagId : -1;
 
         int conditionIndex = -1;
         if (metric.has_condition()) {
@@ -444,8 +452,8 @@
             }
         }
 
-        sp<MetricProducer> gaugeProducer = new GaugeMetricProducer(key, metric, conditionIndex,
-                                                                   wizard, pullTagId, startTimeNs);
+        sp<MetricProducer> gaugeProducer = new GaugeMetricProducer(
+                key, metric, conditionIndex, wizard, pullTagId, atomTagId, startTimeNs);
         allMetricProducers.push_back(gaugeProducer);
     }
     return true;
@@ -478,7 +486,7 @@
     return true;
 }
 
-bool initStatsdConfig(const ConfigKey& key, const StatsdConfig& config, set<int>& allTagIds,
+bool initStatsdConfig(const ConfigKey& key, const StatsdConfig& config, const long timeBaseSec, set<int>& allTagIds,
                       vector<sp<LogMatchingTracker>>& allAtomMatchers,
                       vector<sp<ConditionTracker>>& allConditionTrackers,
                       vector<sp<MetricProducer>>& allMetricProducers,
@@ -502,7 +510,7 @@
         return false;
     }
 
-    if (!initMetrics(key, config, logTrackerMap, conditionTrackerMap, allAtomMatchers,
+    if (!initMetrics(key, config, timeBaseSec, logTrackerMap, conditionTrackerMap, allAtomMatchers,
                      allConditionTrackers, allMetricProducers, conditionToMetricMap,
                      trackerToMetricMap, metricProducerMap)) {
         ALOGE("initMetricProducers failed");
diff --git a/cmds/statsd/src/metrics/metrics_manager_util.h b/cmds/statsd/src/metrics/metrics_manager_util.h
index 8e9c8e3..3337332 100644
--- a/cmds/statsd/src/metrics/metrics_manager_util.h
+++ b/cmds/statsd/src/metrics/metrics_manager_util.h
@@ -68,6 +68,7 @@
 // input:
 // [key]: the config key that this config belongs to
 // [config]: the input config
+// [timeBaseSec]: start time base for all metrics
 // [logTrackerMap]: LogMatchingTracker name to index mapping from previous step.
 // [conditionTrackerMap]: condition name to index mapping
 // output:
@@ -76,7 +77,7 @@
 //                          the list of MetricProducer index
 // [trackerToMetricMap]: contains the mapping from log tracker to MetricProducer index.
 bool initMetrics(
-        const ConfigKey& key, const StatsdConfig& config,
+        const ConfigKey& key, const StatsdConfig& config, const long timeBaseSec,
         const std::unordered_map<std::string, int>& logTrackerMap,
         const std::unordered_map<std::string, int>& conditionTrackerMap,
         const std::unordered_map<int, std::vector<MetricConditionLink>>& eventConditionLinks,
@@ -88,7 +89,7 @@
 
 // Initialize MetricsManager from StatsdConfig.
 // Parameters are the members of MetricsManager. See MetricsManager for declaration.
-bool initStatsdConfig(const ConfigKey& key, const StatsdConfig& config, std::set<int>& allTagIds,
+bool initStatsdConfig(const ConfigKey& key, const StatsdConfig& config, const long timeBaseSec, std::set<int>& allTagIds,
                       std::vector<sp<LogMatchingTracker>>& allAtomMatchers,
                       std::vector<sp<ConditionTracker>>& allConditionTrackers,
                       std::vector<sp<MetricProducer>>& allMetricProducers,
diff --git a/cmds/statsd/src/stats_log.proto b/cmds/statsd/src/stats_log.proto
index 20d9d5c..3c85c57 100644
--- a/cmds/statsd/src/stats_log.proto
+++ b/cmds/statsd/src/stats_log.proto
@@ -29,9 +29,10 @@
 
   oneof value {
     string value_str = 2;
-    int64 value_int = 3;
-    bool value_bool = 4;
-    float value_float = 5;
+    int32 value_int = 3;
+    int64 value_long = 4;
+    bool value_bool = 5;
+    float value_float = 6;
   }
 }
 
@@ -88,7 +89,7 @@
 
   optional int64 end_bucket_nanos = 2;
 
-  optional int64 gauge = 3;
+  optional Atom atom = 3;
 }
 
 message GaugeMetricData {
diff --git a/cmds/statsd/src/stats_util.cpp b/cmds/statsd/src/stats_util.cpp
index bfa3254..7527a64 100644
--- a/cmds/statsd/src/stats_util.cpp
+++ b/cmds/statsd/src/stats_util.cpp
@@ -35,6 +35,9 @@
             case KeyValuePair::ValueCase::kValueInt:
                 flattened += std::to_string(pair.value_int());
                 break;
+            case KeyValuePair::ValueCase::kValueLong:
+                flattened += std::to_string(pair.value_long());
+                break;
             case KeyValuePair::ValueCase::kValueBool:
                 flattened += std::to_string(pair.value_bool());
                 break;
diff --git a/cmds/statsd/src/stats_util.h b/cmds/statsd/src/stats_util.h
index 594561d..8fd1ea8c 100644
--- a/cmds/statsd/src/stats_util.h
+++ b/cmds/statsd/src/stats_util.h
@@ -17,6 +17,8 @@
 #pragma once
 
 #include "frameworks/base/cmds/statsd/src/stats_log.pb.h"
+#include <sstream>
+#include "logd/LogReader.h"
 
 #include <unordered_map>
 
@@ -26,12 +28,50 @@
 
 #define DEFAULT_DIMENSION_KEY ""
 
+// Minimum bucket size in seconds
+const long kMinBucketSizeSec = 5 * 60;
+
 typedef std::string HashableDimensionKey;
 
 typedef std::map<std::string, HashableDimensionKey> ConditionKey;
 
 typedef std::unordered_map<HashableDimensionKey, int64_t> DimToValMap;
 
+/*
+ * In memory rep for LogEvent. Uses much less memory than LogEvent
+ */
+typedef struct EventKV {
+    std::vector<KeyValuePair> kv;
+    string ToString() const {
+        std::ostringstream result;
+        result << "{ ";
+        const size_t N = kv.size();
+        for (size_t i = 0; i < N; i++) {
+            result << " ";
+            result << (i + 1);
+            result << "->";
+            const auto& pair = kv[i];
+            if (pair.has_value_int()) {
+                result << pair.value_int();
+            } else if (pair.has_value_long()) {
+                result << pair.value_long();
+            } else if (pair.has_value_float()) {
+                result << pair.value_float();
+            } else if (pair.has_value_str()) {
+                result << pair.value_str().c_str();
+            }
+        }
+        result << " }";
+        return result.str();
+    }
+} EventKV;
+
+typedef std::unordered_map<HashableDimensionKey, std::shared_ptr<EventKV>> DimToEventKVMap;
+
+EventMetricData parse(log_msg msg);
+
+int getTagId(log_msg msg);
+
 std::string getHashableKey(std::vector<KeyValuePair> key);
 
 }  // namespace statsd
diff --git a/cmds/statsd/src/statsd_config.proto b/cmds/statsd/src/statsd_config.proto
index c9654af..a30b5f8 100644
--- a/cmds/statsd/src/statsd_config.proto
+++ b/cmds/statsd/src/statsd_config.proto
@@ -120,6 +120,11 @@
     repeated KeyMatcher key_in_condition = 3;
 }
 
+message FieldFilter {
+    optional bool include_all = 1;
+    repeated int32 field_num = 2;
+}
+
 message EventMetric {
     optional string name = 1;
 
@@ -170,7 +175,7 @@
 
     optional string what = 2;
 
-    optional int32 gauge_field = 3;
+    optional FieldFilter gauge_fields = 3;
 
     optional string condition = 4;
 
diff --git a/cmds/statsd/tests/ConfigManager_test.cpp b/cmds/statsd/tests/ConfigManager_test.cpp
index 696fddf..b1924ea 100644
--- a/cmds/statsd/tests/ConfigManager_test.cpp
+++ b/cmds/statsd/tests/ConfigManager_test.cpp
@@ -63,7 +63,7 @@
 
 TEST(ConfigManagerTest, TestFakeConfig) {
     auto metricsManager =
-            std::make_unique<MetricsManager>(ConfigKey(0, "test"), build_fake_config());
+            std::make_unique<MetricsManager>(ConfigKey(0, "test"), build_fake_config(), 1000);
     EXPECT_TRUE(metricsManager->isConfigValid());
 }
 
@@ -88,12 +88,6 @@
     {
         InSequence s;
 
-        // The built-in fake one.
-        // TODO: Remove this when we get rid of the fake one, and make this
-        // test loading one from disk somewhere.
-        EXPECT_CALL(*(listener.get()),
-                    OnConfigUpdated(ConfigKeyEq(1000, "fake"), StatsdConfigEq("CONFIG_12345")))
-                .RetiresOnSaturation();
         manager->Startup();
 
         // Add another one
@@ -147,7 +141,7 @@
 
     StatsdConfig config;
 
-    EXPECT_CALL(*(listener.get()), OnConfigUpdated(_, _)).Times(6);
+    EXPECT_CALL(*(listener.get()), OnConfigUpdated(_, _)).Times(5);
     EXPECT_CALL(*(listener.get()), OnConfigRemoved(ConfigKeyEq(2, "xxx")));
     EXPECT_CALL(*(listener.get()), OnConfigRemoved(ConfigKeyEq(2, "yyy")));
     EXPECT_CALL(*(listener.get()), OnConfigRemoved(ConfigKeyEq(2, "zzz")));
diff --git a/cmds/statsd/tests/MetricsManager_test.cpp b/cmds/statsd/tests/MetricsManager_test.cpp
index c6a5310..3c8ccab 100644
--- a/cmds/statsd/tests/MetricsManager_test.cpp
+++ b/cmds/statsd/tests/MetricsManager_test.cpp
@@ -42,6 +42,8 @@
 
 const ConfigKey kConfigKey(0, "test");
 
+const long timeBaseSec = 1000;
+
 StatsdConfig buildGoodConfig() {
     StatsdConfig config;
     config.set_name("12345");
@@ -275,7 +277,7 @@
     unordered_map<int, std::vector<int>> trackerToMetricMap;
     unordered_map<int, std::vector<int>> trackerToConditionMap;
 
-    EXPECT_TRUE(initStatsdConfig(kConfigKey, config, allTagIds, allAtomMatchers,
+    EXPECT_TRUE(initStatsdConfig(kConfigKey, config, timeBaseSec,  allTagIds, allAtomMatchers,
                                  allConditionTrackers, allMetricProducers, allAnomalyTrackers,
                                  conditionToMetricMap, trackerToMetricMap, trackerToConditionMap));
     EXPECT_EQ(1u, allMetricProducers.size());
@@ -293,7 +295,7 @@
     unordered_map<int, std::vector<int>> trackerToMetricMap;
     unordered_map<int, std::vector<int>> trackerToConditionMap;
 
-    EXPECT_FALSE(initStatsdConfig(kConfigKey, config, allTagIds, allAtomMatchers,
+    EXPECT_FALSE(initStatsdConfig(kConfigKey, config, timeBaseSec, allTagIds, allAtomMatchers,
                                   allConditionTrackers, allMetricProducers, allAnomalyTrackers,
                                   conditionToMetricMap, trackerToMetricMap, trackerToConditionMap));
 }
@@ -309,7 +311,7 @@
     unordered_map<int, std::vector<int>> trackerToMetricMap;
     unordered_map<int, std::vector<int>> trackerToConditionMap;
 
-    EXPECT_FALSE(initStatsdConfig(kConfigKey, config, allTagIds, allAtomMatchers,
+    EXPECT_FALSE(initStatsdConfig(kConfigKey, config, timeBaseSec, allTagIds, allAtomMatchers,
                                   allConditionTrackers, allMetricProducers, allAnomalyTrackers,
                                   conditionToMetricMap, trackerToMetricMap, trackerToConditionMap));
 }
@@ -324,7 +326,7 @@
     unordered_map<int, std::vector<int>> conditionToMetricMap;
     unordered_map<int, std::vector<int>> trackerToMetricMap;
     unordered_map<int, std::vector<int>> trackerToConditionMap;
-    EXPECT_FALSE(initStatsdConfig(kConfigKey, config, allTagIds, allAtomMatchers,
+    EXPECT_FALSE(initStatsdConfig(kConfigKey, config, timeBaseSec, allTagIds, allAtomMatchers,
                                   allConditionTrackers, allMetricProducers, allAnomalyTrackers,
                                   conditionToMetricMap, trackerToMetricMap, trackerToConditionMap));
 }
@@ -339,7 +341,7 @@
     unordered_map<int, std::vector<int>> conditionToMetricMap;
     unordered_map<int, std::vector<int>> trackerToMetricMap;
     unordered_map<int, std::vector<int>> trackerToConditionMap;
-    EXPECT_FALSE(initStatsdConfig(kConfigKey, config, allTagIds, allAtomMatchers,
+    EXPECT_FALSE(initStatsdConfig(kConfigKey, config, timeBaseSec, allTagIds, allAtomMatchers,
                                   allConditionTrackers, allMetricProducers, allAnomalyTrackers,
                                   conditionToMetricMap, trackerToMetricMap, trackerToConditionMap));
 }
@@ -355,7 +357,7 @@
     unordered_map<int, std::vector<int>> trackerToMetricMap;
     unordered_map<int, std::vector<int>> trackerToConditionMap;
 
-    EXPECT_FALSE(initStatsdConfig(kConfigKey, config, allTagIds, allAtomMatchers,
+    EXPECT_FALSE(initStatsdConfig(kConfigKey, config, timeBaseSec, allTagIds, allAtomMatchers,
                                   allConditionTrackers, allMetricProducers, allAnomalyTrackers,
                                   conditionToMetricMap, trackerToMetricMap, trackerToConditionMap));
 }
@@ -371,7 +373,7 @@
     unordered_map<int, std::vector<int>> trackerToMetricMap;
     unordered_map<int, std::vector<int>> trackerToConditionMap;
 
-    EXPECT_FALSE(initStatsdConfig(kConfigKey, config, allTagIds, allAtomMatchers,
+    EXPECT_FALSE(initStatsdConfig(kConfigKey, config, timeBaseSec, allTagIds, allAtomMatchers,
                                   allConditionTrackers, allMetricProducers, allAnomalyTrackers,
                                   conditionToMetricMap, trackerToMetricMap, trackerToConditionMap));
 }
diff --git a/cmds/statsd/tests/StatsLogProcessor_test.cpp b/cmds/statsd/tests/StatsLogProcessor_test.cpp
index aff06ba..a5c8875 100644
--- a/cmds/statsd/tests/StatsLogProcessor_test.cpp
+++ b/cmds/statsd/tests/StatsLogProcessor_test.cpp
@@ -41,7 +41,7 @@
  */
 class MockMetricsManager : public MetricsManager {
 public:
-    MockMetricsManager() : MetricsManager(ConfigKey(1, "key"), StatsdConfig()) {
+    MockMetricsManager() : MetricsManager(ConfigKey(1, "key"), StatsdConfig(), 1000) {
     }
 
     MOCK_METHOD0(byteSize, size_t());
diff --git a/cmds/statsd/tests/guardrail/StatsdStats_test.cpp b/cmds/statsd/tests/guardrail/StatsdStats_test.cpp
index 312de1b..7658044 100644
--- a/cmds/statsd/tests/guardrail/StatsdStats_test.cpp
+++ b/cmds/statsd/tests/guardrail/StatsdStats_test.cpp
@@ -214,7 +214,7 @@
     stats.noteAtomLogged(android::util::SENSOR_STATE_CHANGED, now + 2);
     stats.noteAtomLogged(android::util::DROPBOX_ERROR_CHANGED, now + 3);
     // pulled event, should ignore
-    stats.noteAtomLogged(android::util::WIFI_BYTES_TRANSFERRED, now + 4);
+    stats.noteAtomLogged(android::util::WIFI_BYTES_TRANSFER, now + 4);
 
     vector<uint8_t> output;
     stats.dumpStats(&output, false);
diff --git a/cmds/statsd/tests/metrics/GaugeMetricProducer_test.cpp b/cmds/statsd/tests/metrics/GaugeMetricProducer_test.cpp
index 59475d2..68b7dcb 100644
--- a/cmds/statsd/tests/metrics/GaugeMetricProducer_test.cpp
+++ b/cmds/statsd/tests/metrics/GaugeMetricProducer_test.cpp
@@ -26,6 +26,7 @@
 using std::set;
 using std::unordered_map;
 using std::vector;
+using std::make_shared;
 
 #ifdef __ANDROID__
 
@@ -34,120 +35,142 @@
 namespace statsd {
 
 const ConfigKey kConfigKey(0, "test");
+const int tagId = 1;
+const string metricName = "test_metric";
+const int64_t bucketStartTimeNs = 10000000000;
+const int64_t bucketSizeNs = 60 * 1000 * 1000 * 1000LL;
+const int64_t bucket2StartTimeNs = bucketStartTimeNs + bucketSizeNs;
+const int64_t bucket3StartTimeNs = bucketStartTimeNs + 2 * bucketSizeNs;
+const int64_t bucket4StartTimeNs = bucketStartTimeNs + 3 * bucketSizeNs;
 
-TEST(GaugeMetricProducerTest, TestWithCondition) {
-    int64_t bucketStartTimeNs = 10000000000;
-    int64_t bucketSizeNs = 60 * 1000 * 1000 * 1000LL;
-
+TEST(GaugeMetricProducerTest, TestNoCondition) {
     GaugeMetric metric;
-    metric.set_name("1");
+    metric.set_name(metricName);
     metric.mutable_bucket()->set_bucket_size_millis(bucketSizeNs / 1000000);
-    metric.set_gauge_field(2);
+    metric.mutable_gauge_fields()->add_field_num(2);
 
     sp<MockConditionWizard> wizard = new NaggyMock<MockConditionWizard>();
 
-    GaugeMetricProducer gaugeProducer(metric, 1 /*has condition*/, wizard, -1, bucketStartTimeNs);
+    // TODO: pending refactor of StatsPullerManager
+    // For now we still need this so that it doesn't do real pulling.
+    shared_ptr<MockStatsPullerManager> pullerManager =
+            make_shared<StrictMock<MockStatsPullerManager>>();
+    EXPECT_CALL(*pullerManager, RegisterReceiver(tagId, _, _)).WillOnce(Return());
+    EXPECT_CALL(*pullerManager, UnRegisterReceiver(tagId, _)).WillOnce(Return());
 
-    vector<std::shared_ptr<LogEvent>> allData;
-    std::shared_ptr<LogEvent> event1 = std::make_shared<LogEvent>(1, bucketStartTimeNs + 1);
-    event1->write(1);
-    event1->write(13);
-    event1->init();
-    allData.push_back(event1);
+    GaugeMetricProducer gaugeProducer(kConfigKey, metric, -1 /*-1 meaning no condition*/, wizard,
+                                      tagId, tagId, bucketStartTimeNs, pullerManager);
 
-    std::shared_ptr<LogEvent> event2 = std::make_shared<LogEvent>(1, bucketStartTimeNs + 10);
-    event2->write(1);
-    event2->write(15);
-    event2->init();
-    allData.push_back(event2);
+    vector<shared_ptr<LogEvent>> allData;
+    allData.clear();
+    shared_ptr<LogEvent> event = make_shared<LogEvent>(tagId, bucket2StartTimeNs + 1);
+    event->write(tagId);
+    event->write(11);
+    event->init();
+    allData.push_back(event);
 
     gaugeProducer.onDataPulled(allData);
-    gaugeProducer.flushIfNeededLocked(event2->GetTimestampNs() + 1);
-    EXPECT_EQ(0UL, gaugeProducer.mCurrentSlicedBucket->size());
+    EXPECT_EQ(1UL, gaugeProducer.mCurrentSlicedBucket->size());
+    EXPECT_EQ(11, gaugeProducer.mCurrentSlicedBucket->begin()->second->kv[0].value_int());
     EXPECT_EQ(0UL, gaugeProducer.mPastBuckets.size());
 
-    gaugeProducer.onConditionChanged(true, bucketStartTimeNs + 11);
-    gaugeProducer.onConditionChanged(false, bucketStartTimeNs + 21);
-    gaugeProducer.onConditionChanged(true, bucketStartTimeNs + bucketSizeNs + 11);
-    std::shared_ptr<LogEvent> event3 =
-            std::make_shared<LogEvent>(1, bucketStartTimeNs + 2 * bucketSizeNs + 10);
-    event3->write(1);
-    event3->write(25);
-    event3->init();
-    allData.push_back(event3);
+    allData.clear();
+    std::shared_ptr<LogEvent> event2 =
+            std::make_shared<LogEvent>(tagId, bucket3StartTimeNs + 10);
+    event2->write(tagId);
+    event2->write(25);
+    event2->init();
+    allData.push_back(event2);
     gaugeProducer.onDataPulled(allData);
-    gaugeProducer.flushIfNeededLocked(bucketStartTimeNs + 2 * bucketSizeNs + 10);
     EXPECT_EQ(1UL, gaugeProducer.mCurrentSlicedBucket->size());
-    EXPECT_EQ(25L, gaugeProducer.mCurrentSlicedBucket->begin()->second);
+    EXPECT_EQ(25, gaugeProducer.mCurrentSlicedBucket->begin()->second->kv[0].value_int());
     // One dimension.
     EXPECT_EQ(1UL, gaugeProducer.mPastBuckets.size());
     EXPECT_EQ(1UL, gaugeProducer.mPastBuckets.begin()->second.size());
-    EXPECT_EQ(25L, gaugeProducer.mPastBuckets.begin()->second.front().mGauge);
-    EXPECT_EQ(2UL, gaugeProducer.mPastBuckets.begin()->second.front().mBucketNum);
-    EXPECT_EQ(bucketStartTimeNs + 2 * bucketSizeNs,
-              gaugeProducer.mPastBuckets.begin()->second.front().mBucketStartNs);
+    EXPECT_EQ(11L, gaugeProducer.mPastBuckets.begin()->second.back().mEvent->kv[0].value_int());
+    EXPECT_EQ(1UL, gaugeProducer.mPastBuckets.begin()->second.back().mBucketNum);
+
+    gaugeProducer.flushIfNeededLocked(bucket4StartTimeNs);
+    EXPECT_EQ(0UL, gaugeProducer.mCurrentSlicedBucket->size());
+    // One dimension.
+    EXPECT_EQ(1UL, gaugeProducer.mPastBuckets.size());
+    EXPECT_EQ(2UL, gaugeProducer.mPastBuckets.begin()->second.size());
+    EXPECT_EQ(25L, gaugeProducer.mPastBuckets.begin()->second.back().mEvent->kv[0].value_int());
+    EXPECT_EQ(2UL, gaugeProducer.mPastBuckets.begin()->second.back().mBucketNum);
 }
 
-TEST(GaugeMetricProducerTest, TestNoCondition) {
-    int64_t bucketStartTimeNs = 10000000000;
-    int64_t bucketSizeNs = 60 * 1000 * 1000 * 1000LL;
-
+TEST(GaugeMetricProducerTest, TestWithCondition) {
     GaugeMetric metric;
-    metric.set_name("1");
+    metric.set_name(metricName);
     metric.mutable_bucket()->set_bucket_size_millis(bucketSizeNs / 1000000);
-    metric.set_gauge_field(2);
+    metric.mutable_gauge_fields()->add_field_num(2);
+    metric.set_condition("SCREEN_ON");
 
     sp<MockConditionWizard> wizard = new NaggyMock<MockConditionWizard>();
 
-    GaugeMetricProducer gaugeProducer(metric, -1 /*no condition*/, wizard, -1, bucketStartTimeNs);
+    shared_ptr<MockStatsPullerManager> pullerManager =
+            make_shared<StrictMock<MockStatsPullerManager>>();
+    EXPECT_CALL(*pullerManager, RegisterReceiver(tagId, _, _)).WillOnce(Return());
+    EXPECT_CALL(*pullerManager, UnRegisterReceiver(tagId, _)).WillOnce(Return());
+    EXPECT_CALL(*pullerManager, Pull(tagId, _))
+            .WillOnce(Invoke([](int tagId, vector<std::shared_ptr<LogEvent>>* data) {
+                data->clear();
+                shared_ptr<LogEvent> event = make_shared<LogEvent>(tagId, bucketStartTimeNs + 10);
+                event->write(tagId);
+                event->write(100);
+                event->init();
+                data->push_back(event);
+                return true;
+            }));
 
-    vector<std::shared_ptr<LogEvent>> allData;
-    std::shared_ptr<LogEvent> event1 = std::make_shared<LogEvent>(1, bucketStartTimeNs + 1);
-    event1->write(1);
-    event1->write(13);
-    event1->init();
-    allData.push_back(event1);
+    GaugeMetricProducer gaugeProducer(kConfigKey, metric, 1, wizard, tagId, tagId,
+                                      bucketStartTimeNs, pullerManager);
 
-    std::shared_ptr<LogEvent> event2 = std::make_shared<LogEvent>(1, bucketStartTimeNs + 10);
-    event2->write(1);
-    event2->write(15);
-    event2->init();
-    allData.push_back(event2);
-
-    std::shared_ptr<LogEvent> event3 =
-            std::make_shared<LogEvent>(1, bucketStartTimeNs + 2 * bucketSizeNs + 10);
-    event3->write(1);
-    event3->write(25);
-    event3->init();
-    allData.push_back(event3);
-
-    gaugeProducer.onDataPulled(allData);
-    // Has one slice
+    gaugeProducer.onConditionChanged(true, bucketStartTimeNs + 8);
     EXPECT_EQ(1UL, gaugeProducer.mCurrentSlicedBucket->size());
-    EXPECT_EQ(25L, gaugeProducer.mCurrentSlicedBucket->begin()->second);
+    EXPECT_EQ(100, gaugeProducer.mCurrentSlicedBucket->begin()->second->kv[0].value_int());
+    EXPECT_EQ(0UL, gaugeProducer.mPastBuckets.size());
+
+    vector<shared_ptr<LogEvent>> allData;
+    allData.clear();
+    shared_ptr<LogEvent> event = make_shared<LogEvent>(tagId, bucket2StartTimeNs + 1);
+    event->write(1);
+    event->write(110);
+    event->init();
+    allData.push_back(event);
+    gaugeProducer.onDataPulled(allData);
+
+    EXPECT_EQ(1UL, gaugeProducer.mCurrentSlicedBucket->size());
+    EXPECT_EQ(110, gaugeProducer.mCurrentSlicedBucket->begin()->second->kv[0].value_int());
+    EXPECT_EQ(1UL, gaugeProducer.mPastBuckets.size());
+    EXPECT_EQ(100, gaugeProducer.mPastBuckets.begin()->second.back().mEvent->kv[0].value_int());
+
+    gaugeProducer.onConditionChanged(false, bucket2StartTimeNs + 10);
+    gaugeProducer.flushIfNeededLocked(bucket3StartTimeNs + 10);
+    EXPECT_EQ(1UL, gaugeProducer.mPastBuckets.size());
     EXPECT_EQ(2UL, gaugeProducer.mPastBuckets.begin()->second.size());
-    EXPECT_EQ(13L, gaugeProducer.mPastBuckets.begin()->second.front().mGauge);
-    EXPECT_EQ(0UL, gaugeProducer.mPastBuckets.begin()->second.front().mBucketNum);
-    EXPECT_EQ(25L, gaugeProducer.mPastBuckets.begin()->second.back().mGauge);
-    EXPECT_EQ(2UL, gaugeProducer.mPastBuckets.begin()->second.back().mBucketNum);
-    EXPECT_EQ(bucketStartTimeNs + 2 * bucketSizeNs,
-              gaugeProducer.mPastBuckets.begin()->second.back().mBucketStartNs);
+    EXPECT_EQ(110L, gaugeProducer.mPastBuckets.begin()->second.back().mEvent->kv[0].value_int());
+    EXPECT_EQ(1UL, gaugeProducer.mPastBuckets.begin()->second.back().mBucketNum);
 }
 
 TEST(GaugeMetricProducerTest, TestAnomalyDetection) {
-    int64_t bucketStartTimeNs = 10000000000;
-    int64_t bucketSizeNs = 60 * 1000 * 1000 * 1000LL;
     sp<MockConditionWizard> wizard = new NaggyMock<MockConditionWizard>();
 
+    shared_ptr<MockStatsPullerManager> pullerManager =
+            make_shared<StrictMock<MockStatsPullerManager>>();
+    EXPECT_CALL(*pullerManager, RegisterReceiver(tagId, _, _)).WillOnce(Return());
+    EXPECT_CALL(*pullerManager, UnRegisterReceiver(tagId, _)).WillOnce(Return());
+
     GaugeMetric metric;
-    metric.set_name("1");
+    metric.set_name(metricName);
     metric.mutable_bucket()->set_bucket_size_millis(bucketSizeNs / 1000000);
-    metric.set_gauge_field(2);
-    GaugeMetricProducer gaugeProducer(metric, -1 /*no condition*/, wizard, -1, bucketStartTimeNs);
+    metric.mutable_gauge_fields()->add_field_num(2);
+    GaugeMetricProducer gaugeProducer(kConfigKey, metric, -1 /*-1 meaning no condition*/, wizard,
+                                      tagId, tagId, bucketStartTimeNs, pullerManager);
 
     Alert alert;
     alert.set_name("alert");
-    alert.set_metric_name("1");
+    alert.set_metric_name(metricName);
     alert.set_trigger_if_sum_gt(25);
     alert.set_number_of_buckets(2);
     sp<AnomalyTracker> anomalyTracker = new AnomalyTracker(alert, kConfigKey);
@@ -160,7 +183,7 @@
 
     gaugeProducer.onDataPulled({event1});
     EXPECT_EQ(1UL, gaugeProducer.mCurrentSlicedBucket->size());
-    EXPECT_EQ(13L, gaugeProducer.mCurrentSlicedBucket->begin()->second);
+    EXPECT_EQ(13L, gaugeProducer.mCurrentSlicedBucket->begin()->second->kv[0].value_int());
     EXPECT_EQ(anomalyTracker->getLastAlarmTimestampNs(), -1LL);
 
     std::shared_ptr<LogEvent> event2 =
@@ -171,7 +194,7 @@
 
     gaugeProducer.onDataPulled({event2});
     EXPECT_EQ(1UL, gaugeProducer.mCurrentSlicedBucket->size());
-    EXPECT_EQ(15L, gaugeProducer.mCurrentSlicedBucket->begin()->second);
+    EXPECT_EQ(15L, gaugeProducer.mCurrentSlicedBucket->begin()->second->kv[0].value_int());
     EXPECT_EQ(anomalyTracker->getLastAlarmTimestampNs(), (long long)event2->GetTimestampNs());
 
     std::shared_ptr<LogEvent> event3 =
@@ -182,7 +205,7 @@
 
     gaugeProducer.onDataPulled({event3});
     EXPECT_EQ(1UL, gaugeProducer.mCurrentSlicedBucket->size());
-    EXPECT_EQ(24L, gaugeProducer.mCurrentSlicedBucket->begin()->second);
+    EXPECT_EQ(24L, gaugeProducer.mCurrentSlicedBucket->begin()->second->kv[0].value_int());
     EXPECT_EQ(anomalyTracker->getLastAlarmTimestampNs(), (long long)event3->GetTimestampNs());
 
     // The event4 does not have the gauge field. Thus the current bucket value is 0.
@@ -191,7 +214,8 @@
     event4->write(1);
     event4->init();
     gaugeProducer.onDataPulled({event4});
-    EXPECT_EQ(0UL, gaugeProducer.mCurrentSlicedBucket->size());
+    EXPECT_EQ(1UL, gaugeProducer.mCurrentSlicedBucket->size());
+    EXPECT_EQ(0, gaugeProducer.mCurrentSlicedBucket->begin()->second->kv[0].value_int());
     EXPECT_EQ(anomalyTracker->getLastAlarmTimestampNs(), (long long)event3->GetTimestampNs());
 }
 
diff --git a/cmds/statsd/tools/loadtest/src/com/android/statsd/loadtest/LoadtestActivity.java b/cmds/statsd/tools/loadtest/src/com/android/statsd/loadtest/LoadtestActivity.java
index a72f72e..0a30ff8 100644
--- a/cmds/statsd/tools/loadtest/src/com/android/statsd/loadtest/LoadtestActivity.java
+++ b/cmds/statsd/tools/loadtest/src/com/android/statsd/loadtest/LoadtestActivity.java
@@ -419,11 +419,6 @@
     private void clearConfigs() {
         // TODO: Clear all configs instead of specific ones.
         if (mStatsManager != null) {
-            if (!mStatsManager.removeConfiguration("fake")) {
-                Log.d(TAG, "Removed \"fake\" statsd configs.");
-            } else {
-                Log.d(TAG, "Failed to remove \"fake\" config. Loadtest results cannot be trusted.");
-            }
             if (mStarted) {
                 if (!mStatsManager.removeConfiguration(ConfigFactory.CONFIG_NAME)) {
                     Log.d(TAG, "Removed loadtest statsd configs.");
diff --git a/core/java/android/content/pm/ShortcutManager.java b/core/java/android/content/pm/ShortcutManager.java
index 61b0eb0..8623524 100644
--- a/core/java/android/content/pm/ShortcutManager.java
+++ b/core/java/android/content/pm/ShortcutManager.java
@@ -36,15 +36,26 @@
 import java.util.List;
 
 /**
- * The ShortcutManager manages an app's <em>shortcuts</em>. Shortcuts provide users with quick
- * access to activities other than an app's main activity in the currently-active launcher, provided
- * that the launcher supports app shortcuts.  For example, an email app may publish the "compose new
- * email" action, which will directly open the compose activity.  The {@link ShortcutInfo} class
- * contains information about each of the shortcuts themselves.
+ * The ShortcutManager performs operations on an app's set of <em>shortcuts</em>. The
+ * {@link ShortcutInfo} class contains information about each of the shortcuts themselves.
+ *
+ * <p>An app's shortcuts represent specific tasks and actions that users can take within your app.
+ * When a user selects a shortcut in the currently-active launcher, your app opens an activity other
+ * than the app's starting activity, provided that the currently-active launcher supports app
+ * shortcuts.</p>
+ *
+ * <p>The types of shortcuts that you create for your app depend on the app's key use cases. For
+ * example, an email app may publish the "compose new email" shortcut, which allows the app to
+ * directly open the compose activity.</p>
+ *
+ * <p class="note"><b>Note:</b> Only main activities&mdash;activities that handle the
+ * {@link Intent#ACTION_MAIN} action and the {@link Intent#CATEGORY_LAUNCHER} category&mdash;can
+ * have shortcuts. If an app has multiple main activities, you need to define the set of shortcuts
+ * for <em>each</em> activity.
  *
  * <p>This page discusses the implementation details of the <code>ShortcutManager</code> class. For
- * guidance on performing operations on app shortcuts within your app, see the
- * <a href="/guide/topics/ui/shortcuts.html">App Shortcuts</a> feature guide.
+ * definitions of key terms and guidance on performing operations on shortcuts within your app, see
+ * the <a href="/guide/topics/ui/shortcuts.html">App Shortcuts</a> feature guide.
  *
  * <h3>Shortcut characteristics</h3>
  *
@@ -69,8 +80,8 @@
  * <ul>
  *     <li>The user removes it.
  *     <li>The publisher app associated with the shortcut is uninstalled.
- *     <li>The user performs the clear data action on the publisher app from the device's
- *     <b>Settings</b> app.
+ *     <li>The user selects <b>Clear data</b> from the publisher app's <i>Storage</i> screen, within
+ *     the system's <b>Settings</b> app.
  * </ul>
  *
  * <p>Because the system performs
@@ -84,12 +95,15 @@
  * <p>When the launcher displays an app's shortcuts, they should appear in the following order:
  *
  * <ul>
- *   <li>Static shortcuts (if {@link ShortcutInfo#isDeclaredInManifest()} is {@code true}),
- *   and then show dynamic shortcuts (if {@link ShortcutInfo#isDynamic()} is {@code true}).
- *   <li>Within each shortcut type (static and dynamic), sort the shortcuts in order of increasing
+ *   <li>Static shortcuts&mdash;shortcuts whose {@link ShortcutInfo#isDeclaredInManifest()} method
+ *   returns {@code true}&mdash;followed by dynamic shortcuts&mdash;shortcuts whose
+ *   {@link ShortcutInfo#isDynamic()} method returns {@code true}.
+ *   <li>Within each shortcut type (static and dynamic), shortcuts are sorted in order of increasing
  *   rank according to {@link ShortcutInfo#getRank()}.
  * </ul>
  *
+ * <h4>Shortcut ranks</h4>
+ *
  * <p>Shortcut ranks are non-negative, sequential integers that determine the order in which
  * shortcuts appear, assuming that the shortcuts are all in the same category. You can update ranks
  * of existing shortcuts when you call {@link #updateShortcuts(List)},
@@ -103,64 +117,99 @@
  *
  * <h3>Options for static shortcuts</h3>
  *
- * The following list includes descriptions for the different attributes within a static shortcut:
+ * The following list includes descriptions for the different attributes within a static shortcut.
+ * You must provide a value for {@code android:shortcutId}, {@code android:shortcutShortLabel}; all
+ * other values are optional.
+ *
  * <dl>
  *   <dt>{@code android:shortcutId}</dt>
- *   <dd>Mandatory shortcut ID.
- *   <p>
- *   This must be a string literal.
- *   A resource string, such as <code>@string/foo</code>, cannot be used.
+ *   <dd><p>A string literal, which represents the shortcut when a {@code ShortcutManager} object
+ *   performs operations on it.</p>
+ *   <p class="note"><b>Note: </b>You cannot set this attribute's value to a resource string, such
+ *   as <code>@string/foo</code>.</p>
  *   </dd>
  *
  *   <dt>{@code android:enabled}</dt>
- *   <dd>Default is {@code true}.  Can be set to {@code false} in order
- *   to disable a static shortcut that was published in a previous version and set a custom
- *   disabled message.  If a custom disabled message is not needed, then a static shortcut can
- *   be simply removed from the XML file rather than keeping it with {@code enabled="false"}.</dd>
+ *   <dd><p>Whether the user can interact with the shortcut from a supported launcher.</p>
+ *   <p>The default value is {@code true}. If you set it to {@code false}, you should also set
+ *   {@code android:shortcutDisabledMessage} to a message that explains why you've disabled the
+ *   shortcut. If you don't think you need to provide such a message, it's easiest to just remove
+ *   the shortcut from the XML file entirely, rather than changing the values of its
+ *   {@code android:enabled} and {@code android:shortcutDisabledMessage} attributes.
+ *   </dd>
  *
  *   <dt>{@code android:icon}</dt>
- *   <dd>Shortcut icon.</dd>
+ *   <dd><p>The <a href="/topic/performance/graphics/index.html">bitmap</a> or
+ *   <a href="/guide/practices/ui_guidelines/icon_design_adaptive.html">adaptive icon</a> that the
+ *   launcher uses when displaying the shortcut to the user. This value can be either the path to an
+ *   image or the resource file that contains the image. Use adaptive icons whenever possible to
+ *   improve performance and consistency.</p>
+ *   <p class="note"><b>Note: </b>Shortcut icons cannot include
+ *   <a href="/training/material/drawables.html#DrawableTint">tints</a>.
+ *   </dd>
  *
  *   <dt>{@code android:shortcutShortLabel}</dt>
- *   <dd>Mandatory shortcut short label.
- *   See {@link ShortcutInfo.Builder#setShortLabel(CharSequence)}.
- *   <p>
- *   This must be a resource string, such as <code>@string/shortcut_label</code>.
+ *   <dd><p>A concise phrase that describes the shortcut's purpose. For more information, see
+ *   {@link ShortcutInfo.Builder#setShortLabel(CharSequence)}.</p>
+ *   <p class="note"><b>Note: </b>This attribute's value must be a resource string, such as
+ *   <code>@string/shortcut_label</code>.</p>
  *   </dd>
  *
  *   <dt>{@code android:shortcutLongLabel}</dt>
- *   <dd>Shortcut long label.
- *   See {@link ShortcutInfo.Builder#setLongLabel(CharSequence)}.
- *   <p>
- *   This must be a resource string, such as <code>@string/shortcut_long_label</code>.
+ *   <dd><p>An extended phrase that describes the shortcut's purpose. If there's enough space, the
+ *   launcher displays this value instead of {@code android:shortcutShortLabel}. For more
+ *   information, see {@link ShortcutInfo.Builder#setLongLabel(CharSequence)}.</p>
+ *   <p class="note"><b>Note: </b>This attribute's value must be a resource string, such as
+ *   <code>@string/shortcut_long_label</code>.</p>
  *   </dd>
  *
  *   <dt>{@code android:shortcutDisabledMessage}</dt>
- *   <dd>When {@code android:enabled} is set to
- *   {@code false}, this attribute is used to display a custom disabled message.
- *   <p>
- *   This must be a resource string, such as <code>@string/shortcut_disabled_message</code>.
+ *   <dd><p>The message that appears in a supported launcher when the user attempts to launch a
+ *   disabled shortcut. This attribute's value has no effect if {@code android:enabled} is
+ *   {@code true}. The message should explain to the user why the shortcut is now disabled.</p>
+ *   <p class="note"><b>Note: </b>This attribute's value must be a resource string, such as
+ *   <code>@string/shortcut_disabled_message</code>.</p>
  *   </dd>
+ * </dl>
  *
+ * <h3>Inner elements that define static shortcuts</h3>
+ *
+ * <p>The XML file that lists an app's static shortcuts supports the following elements inside each
+ * {@code &lt;shortcut&gt;} element. You must include an {@code intent} inner element for each
+ * static shortcut that you define.</p>
+ *
+ * <dl>
  *   <dt>{@code intent}</dt>
- *   <dd>Intent to launch when the user selects the shortcut.
- *   {@code android:action} is mandatory.
- *   See <a href="{@docRoot}guide/topics/ui/settings.html#Intents">Using intents</a> for the
- *   other supported tags.
+ *   <dd><p>The action that the system launches when the user selects the shortcut. This intent must
+ *   provide a value for the {@code android:action} attribute.</p>
  *   <p>You can provide multiple intents for a single shortcut so that the last defined activity is
  *   launched with the other activities in the
  *   <a href="/guide/components/tasks-and-back-stack.html">back stack</a>. See
- *   {@link android.app.TaskStackBuilder} for details.
- *   <p><b>Note:</b> String resources may not be used within an {@code <intent>} element.
+ *   <a href="/guide/topics/ui/shortcuts.html#static">Using Static Shortcuts</a> and the
+ *   {@link android.app.TaskStackBuilder} class reference for details.</p>
+ *   <p class="note"><b>Note:</b> This {@code intent} element cannot include string resources.</p>
+ *   <p>For more information, see
+ *   <a href="{@docRoot}guide/topics/ui/settings.html#Intents">Using intents</a>.</p>
  *   </dd>
+ *
  *   <dt>{@code categories}</dt>
- *   <dd>Specify shortcut categories.  Currently only
- *   {@link ShortcutInfo#SHORTCUT_CATEGORY_CONVERSATION} is defined in the framework.
+ *   <dd><p>Provides a grouping for the types of actions that your app's shortcuts perform, such as
+ *   creating new chat messages.</p>
+ *   <p>For a list of supported shortcut categories, see the {@link ShortcutInfo} class reference
+ *   for a list of supported shortcut categories.
  *   </dd>
  * </dl>
  *
  * <h3>Updating shortcuts</h3>
  *
+ * <p>Each app's launcher icon can contain at most {@link #getMaxShortcutCountPerActivity()} number
+ * of static and dynamic shortcuts combined. There is no limit to the number of pinned shortcuts
+ * that an app can create, though.
+ *
+ * <p>When a dynamic shortcut is pinned, even when the publisher removes it as a dynamic shortcut,
+ * the pinned shortcut is still visible and launchable.  This allows an app to have more than
+ * {@link #getMaxShortcutCountPerActivity()} number of shortcuts.
+ *
  * <p>As an example, suppose {@link #getMaxShortcutCountPerActivity()} is 5:
  * <ol>
  *     <li>A chat app publishes 5 dynamic shortcuts for the 5 most recent
@@ -168,18 +217,13 @@
  *
  *     <li>The user pins all 5 of the shortcuts.
  *
- *     <li>Later, the user has started 3 additional conversations (c6, c7, and c8),
- *     so the publisher app
- *     re-publishes its dynamic shortcuts.  The new dynamic shortcut list is:
- *     c4, c5, ..., c8.
- *     The publisher app has to remove c1, c2, and c3 because it can't have more than
- *     5 dynamic shortcuts.
- *
- *     <li>However, even though c1, c2, and c3 are no longer dynamic shortcuts, the pinned
- *     shortcuts for these conversations are still available and launchable.
- *
- *     <li>At this point, the user can access a total of 8 shortcuts that link to activities in
- *     the publisher app, including the 3 pinned shortcuts, even though an app can have at most 5
+ *     <li>Later, the user has started 3 additional conversations (c6, c7, and c8), so the publisher
+ *     app re-publishes its dynamic shortcuts. The new dynamic shortcut list is: c4, c5, ..., c8.
+ *     <p>The publisher app has to remove c1, c2, and c3 because it can't have more than 5 dynamic
+ *     shortcuts. However, c1, c2, and c3 are still pinned shortcuts that the user can access and
+ *     launch.
+ *     <p>At this point, the user can access a total of 8 shortcuts that link to activities in the
+ *     publisher app, including the 3 pinned shortcuts, even though an app can have at most 5
  *     dynamic shortcuts.
  *
  *     <li>The app can use {@link #updateShortcuts(List)} to update <em>any</em> of the existing
@@ -196,44 +240,23 @@
  * Dynamic shortcuts can be published with any set of {@link Intent#addFlags Intent} flags.
  * Typically, {@link Intent#FLAG_ACTIVITY_CLEAR_TASK} is specified, possibly along with other
  * flags; otherwise, if the app is already running, the app is simply brought to
- * the foreground, and the target activity may not appear.
+ * the foreground, and the target activity might not appear.
  *
  * <p>Static shortcuts <b>cannot</b> have custom intent flags.
  * The first intent of a static shortcut will always have {@link Intent#FLAG_ACTIVITY_NEW_TASK}
  * and {@link Intent#FLAG_ACTIVITY_CLEAR_TASK} set. This means, when the app is already running, all
- * the existing activities in your app will be destroyed when a static shortcut is launched.
+ * the existing activities in your app are destroyed when a static shortcut is launched.
  * If this behavior is not desirable, you can use a <em>trampoline activity</em>, or an invisible
  * activity that starts another activity in {@link Activity#onCreate}, then calls
  * {@link Activity#finish()}:
  * <ol>
  *     <li>In the <code>AndroidManifest.xml</code> file, the trampoline activity should include the
  *     attribute assignment {@code android:taskAffinity=""}.
- *     <li>In the shortcuts resource file, the intent within the static shortcut should point at
+ *     <li>In the shortcuts resource file, the intent within the static shortcut should reference
  *     the trampoline activity.
  * </ol>
  *
- * <h3>Handling system locale changes</h3>
- *
- * <p>Apps should update dynamic and pinned shortcuts when the system locale changes using the
- * {@link Intent#ACTION_LOCALE_CHANGED} broadcast. When the system locale changes,
- * <a href="/guide/topics/ui/shortcuts.html#rate-limit">rate limiting</a> is reset, so even
- * background apps can add and update dynamic shortcuts until the rate limit is reached again.
- *
- * <h3>Shortcut limits</h3>
- *
- * <p>Only main activities&mdash;activities that handle the {@code MAIN} action and the
- * {@code LAUNCHER} category&mdash;can have shortcuts. If an app has multiple main activities, you
- * need to define the set of shortcuts for <em>each</em> activity.
- *
- * <p>Each launcher icon can have at most {@link #getMaxShortcutCountPerActivity()} number of
- * static and dynamic shortcuts combined. There is no limit to the number of pinned shortcuts that
- * an app can create.
- *
- * <p>When a dynamic shortcut is pinned, even when the publisher removes it as a dynamic shortcut,
- * the pinned shortcut is still visible and launchable.  This allows an app to have more than
- * {@link #getMaxShortcutCountPerActivity()} number of shortcuts.
- *
- * <h4>Rate limiting</h4>
+ * <h3>Rate limiting</h3>
  *
  * <p>When <a href="/guide/topics/ui/shortcuts.html#rate-limit">rate limiting</a> is active,
  * {@link #isRateLimitingActive()} returns {@code true}.
@@ -243,8 +266,20 @@
  * <ul>
  *   <li>An app comes to the foreground.
  *   <li>The system locale changes.
- *   <li>The user performs the <strong>inline reply</strong> action on a notification.
+ *   <li>The user performs the <a href="/guide/topics/ui/notifiers/notifications.html#direct">inline
+ *   reply</a> action on a notification.
  * </ul>
+ *
+ * <h3>Handling system locale changes</h3>
+ *
+ * <p>Apps should update dynamic and pinned shortcuts when they receive the
+ * {@link Intent#ACTION_LOCALE_CHANGED} broadcast, indicating that the system locale has changed.
+ * <p>When the system locale changes, <a href="/guide/topics/ui/shortcuts.html#rate-limit">rate
+ * limiting</a> is reset, so even background apps can add and update dynamic shortcuts until the
+ * rate limit is reached again.
+ *
+ * <h3>Retrieving class instances</h3>
+ * <!-- Provides a heading for the content filled in by the @SystemService annotation below -->
  */
 @SystemService(Context.SHORTCUT_SERVICE)
 public class ShortcutManager {
diff --git a/core/java/android/os/storage/StorageVolume.java b/core/java/android/os/storage/StorageVolume.java
index 1fc0b82..070b8c1 100644
--- a/core/java/android/os/storage/StorageVolume.java
+++ b/core/java/android/os/storage/StorageVolume.java
@@ -19,7 +19,6 @@
 import android.annotation.Nullable;
 import android.content.Context;
 import android.content.Intent;
-import android.net.TrafficStats;
 import android.net.Uri;
 import android.os.Environment;
 import android.os.Parcel;
@@ -78,13 +77,11 @@
 public final class StorageVolume implements Parcelable {
 
     private final String mId;
-    private final int mStorageId;
     private final File mPath;
     private final String mDescription;
     private final boolean mPrimary;
     private final boolean mRemovable;
     private final boolean mEmulated;
-    private final long mMtpReserveSize;
     private final boolean mAllowMassStorage;
     private final long mMaxFileSize;
     private final UserHandle mOwner;
@@ -121,17 +118,15 @@
     public static final int STORAGE_ID_PRIMARY = 0x00010001;
 
     /** {@hide} */
-    public StorageVolume(String id, int storageId, File path, String description, boolean primary,
-            boolean removable, boolean emulated, long mtpReserveSize, boolean allowMassStorage,
+    public StorageVolume(String id, File path, String description, boolean primary,
+            boolean removable, boolean emulated, boolean allowMassStorage,
             long maxFileSize, UserHandle owner, String fsUuid, String state) {
         mId = Preconditions.checkNotNull(id);
-        mStorageId = storageId;
         mPath = Preconditions.checkNotNull(path);
         mDescription = Preconditions.checkNotNull(description);
         mPrimary = primary;
         mRemovable = removable;
         mEmulated = emulated;
-        mMtpReserveSize = mtpReserveSize;
         mAllowMassStorage = allowMassStorage;
         mMaxFileSize = maxFileSize;
         mOwner = Preconditions.checkNotNull(owner);
@@ -141,13 +136,11 @@
 
     private StorageVolume(Parcel in) {
         mId = in.readString();
-        mStorageId = in.readInt();
         mPath = new File(in.readString());
         mDescription = in.readString();
         mPrimary = in.readInt() != 0;
         mRemovable = in.readInt() != 0;
         mEmulated = in.readInt() != 0;
-        mMtpReserveSize = in.readLong();
         mAllowMassStorage = in.readInt() != 0;
         mMaxFileSize = in.readLong();
         mOwner = in.readParcelable(null);
@@ -211,34 +204,6 @@
     }
 
     /**
-     * Returns the MTP storage ID for the volume.
-     * this is also used for the storage_id column in the media provider.
-     *
-     * @return MTP storage ID
-     * @hide
-     */
-    public int getStorageId() {
-        return mStorageId;
-    }
-
-    /**
-     * Number of megabytes of space to leave unallocated by MTP.
-     * MTP will subtract this value from the free space it reports back
-     * to the host via GetStorageInfo, and will not allow new files to
-     * be added via MTP if there is less than this amount left free in the storage.
-     * If MTP has dedicated storage this value should be zero, but if MTP is
-     * sharing storage with the rest of the system, set this to a positive value
-     * to ensure that MTP activity does not result in the storage being
-     * too close to full.
-     *
-     * @return MTP reserve space
-     * @hide
-     */
-    public int getMtpReserveSpace() {
-        return (int) (mMtpReserveSize / TrafficStats.MB_IN_BYTES);
-    }
-
-    /**
      * Returns true if this volume can be shared via USB mass storage.
      *
      * @return whether mass storage is allowed
@@ -385,13 +350,11 @@
         pw.println("StorageVolume:");
         pw.increaseIndent();
         pw.printPair("mId", mId);
-        pw.printPair("mStorageId", mStorageId);
         pw.printPair("mPath", mPath);
         pw.printPair("mDescription", mDescription);
         pw.printPair("mPrimary", mPrimary);
         pw.printPair("mRemovable", mRemovable);
         pw.printPair("mEmulated", mEmulated);
-        pw.printPair("mMtpReserveSize", mMtpReserveSize);
         pw.printPair("mAllowMassStorage", mAllowMassStorage);
         pw.printPair("mMaxFileSize", mMaxFileSize);
         pw.printPair("mOwner", mOwner);
@@ -420,13 +383,11 @@
     @Override
     public void writeToParcel(Parcel parcel, int flags) {
         parcel.writeString(mId);
-        parcel.writeInt(mStorageId);
         parcel.writeString(mPath.toString());
         parcel.writeString(mDescription);
         parcel.writeInt(mPrimary ? 1 : 0);
         parcel.writeInt(mRemovable ? 1 : 0);
         parcel.writeInt(mEmulated ? 1 : 0);
-        parcel.writeLong(mMtpReserveSize);
         parcel.writeInt(mAllowMassStorage ? 1 : 0);
         parcel.writeLong(mMaxFileSize);
         parcel.writeParcelable(mOwner, flags);
diff --git a/core/java/android/os/storage/VolumeInfo.java b/core/java/android/os/storage/VolumeInfo.java
index 76f79f1..d3877ca 100644
--- a/core/java/android/os/storage/VolumeInfo.java
+++ b/core/java/android/os/storage/VolumeInfo.java
@@ -343,9 +343,7 @@
 
         String description = null;
         String derivedFsUuid = fsUuid;
-        long mtpReserveSize = 0;
         long maxFileSize = 0;
-        int mtpStorageId = StorageVolume.STORAGE_ID_INVALID;
 
         if (type == TYPE_EMULATED) {
             emulated = true;
@@ -356,12 +354,6 @@
                 derivedFsUuid = privateVol.fsUuid;
             }
 
-            if (isPrimary()) {
-                mtpStorageId = StorageVolume.STORAGE_ID_PRIMARY;
-            }
-
-            mtpReserveSize = storage.getStorageLowBytes(userPath);
-
             if (ID_EMULATED_INTERNAL.equals(id)) {
                 removable = false;
             } else {
@@ -374,14 +366,6 @@
 
             description = storage.getBestVolumeDescription(this);
 
-            if (isPrimary()) {
-                mtpStorageId = StorageVolume.STORAGE_ID_PRIMARY;
-            } else {
-                // Since MediaProvider currently persists this value, we need a
-                // value that is stable over time.
-                mtpStorageId = buildStableMtpStorageId(fsUuid);
-            }
-
             if ("vfat".equals(fsType)) {
                 maxFileSize = 4294967295L;
             }
@@ -394,8 +378,8 @@
             description = context.getString(android.R.string.unknownName);
         }
 
-        return new StorageVolume(id, mtpStorageId, userPath, description, isPrimary(), removable,
-                emulated, mtpReserveSize, allowMassStorage, maxFileSize, new UserHandle(userId),
+        return new StorageVolume(id, userPath, description, isPrimary(), removable,
+                emulated, allowMassStorage, maxFileSize, new UserHandle(userId),
                 derivedFsUuid, envState);
     }
 
diff --git a/core/java/android/provider/MediaStore.java b/core/java/android/provider/MediaStore.java
index 32d68cd..d9808a3 100644
--- a/core/java/android/provider/MediaStore.java
+++ b/core/java/android/provider/MediaStore.java
@@ -63,15 +63,6 @@
 
     private static final String CONTENT_AUTHORITY_SLASH = "content://" + AUTHORITY + "/";
 
-   /**
-     * Broadcast Action:  A broadcast to indicate the end of an MTP session with the host.
-     * This broadcast is only sent if MTP activity has modified the media database during the
-     * most recent MTP session.
-     *
-     * @hide
-     */
-    public static final String ACTION_MTP_SESSION_END = "android.provider.action.MTP_SESSION_END";
-
     /**
      * The method name used by the media scanner and mtp to tell the media provider to
      * rescan and reclassify that have become unhidden because of renaming folders or
diff --git a/core/java/android/view/textclassifier/TextLinks.java b/core/java/android/view/textclassifier/TextLinks.java
index 0e039e3..4fe5662 100644
--- a/core/java/android/view/textclassifier/TextLinks.java
+++ b/core/java/android/view/textclassifier/TextLinks.java
@@ -22,6 +22,8 @@
 import android.os.LocaleList;
 import android.text.SpannableString;
 import android.text.style.ClickableSpan;
+import android.view.View;
+import android.widget.TextView;
 
 import com.android.internal.util.Preconditions;
 
@@ -189,9 +191,14 @@
      * @hide
      */
     public static final Function<TextLink, ClickableSpan> DEFAULT_SPAN_FACTORY =
-            textLink -> {
-                // TODO: Implement.
-                throw new UnsupportedOperationException("Not yet implemented");
+            textLink -> new ClickableSpan() {
+                @Override
+                public void onClick(View widget) {
+                    if (widget instanceof TextView) {
+                        final TextView textView = (TextView) widget;
+                        textView.requestActionMode(textLink);
+                    }
+                }
             };
 
     /**
diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java
index a440398..05d18d1 100644
--- a/core/java/android/widget/Editor.java
+++ b/core/java/android/widget/Editor.java
@@ -107,6 +107,7 @@
 import android.view.inputmethod.InputConnection;
 import android.view.inputmethod.InputMethodManager;
 import android.view.textclassifier.TextClassification;
+import android.view.textclassifier.TextLinks;
 import android.widget.AdapterView.OnItemClickListener;
 import android.widget.TextView.Drawables;
 import android.widget.TextView.OnEditorActionListener;
@@ -174,6 +175,13 @@
         int SELECTION_END = 2;
     }
 
+    @IntDef({TextActionMode.SELECTION, TextActionMode.INSERTION, TextActionMode.TEXT_LINK})
+    @interface TextActionMode {
+        int SELECTION = 0;
+        int INSERTION = 1;
+        int TEXT_LINK = 2;
+    }
+
     // Each Editor manages its own undo stack.
     private final UndoManager mUndoManager = new UndoManager();
     private UndoOwner mUndoOwner = mUndoManager.getOwner(UNDO_OWNER_TAG, this);
@@ -2053,7 +2061,7 @@
         stopTextActionMode();
 
         ActionMode.Callback actionModeCallback =
-                new TextActionModeCallback(false /* hasSelection */);
+                new TextActionModeCallback(TextActionMode.INSERTION);
         mTextActionMode = mTextView.startActionMode(
                 actionModeCallback, ActionMode.TYPE_FLOATING);
         if (mTextActionMode != null && getInsertionController() != null) {
@@ -2079,7 +2087,23 @@
      * Asynchronously starts a selection action mode using the TextClassifier.
      */
     void startSelectionActionModeAsync(boolean adjustSelection) {
-        getSelectionActionModeHelper().startActionModeAsync(adjustSelection);
+        getSelectionActionModeHelper().startSelectionActionModeAsync(adjustSelection);
+    }
+
+    void startLinkActionModeAsync(TextLinks.TextLink link) {
+        Preconditions.checkNotNull(link);
+        if (!(mTextView.getText() instanceof Spannable)) {
+            return;
+        }
+        Spannable text = (Spannable) mTextView.getText();
+        stopTextActionMode();
+        if (mTextView.isTextSelectable()) {
+            Selection.setSelection((Spannable) text, link.getStart(), link.getEnd());
+        } else {
+            //TODO: Nonselectable text
+        }
+
+        getSelectionActionModeHelper().startLinkActionModeAsync(link);
     }
 
     /**
@@ -2145,7 +2169,7 @@
         return true;
     }
 
-    boolean startSelectionActionModeInternal() {
+    boolean startActionModeInternal(@TextActionMode int actionMode) {
         if (extractedTextModeWillBeStarted()) {
             return false;
         }
@@ -2159,8 +2183,7 @@
             return false;
         }
 
-        ActionMode.Callback actionModeCallback =
-                new TextActionModeCallback(true /* hasSelection */);
+        ActionMode.Callback actionModeCallback = new TextActionModeCallback(actionMode);
         mTextActionMode = mTextView.startActionMode(actionModeCallback, ActionMode.TYPE_FLOATING);
 
         final boolean selectionStarted = mTextActionMode != null;
@@ -3828,8 +3851,9 @@
         private final int mHandleHeight;
         private final Map<MenuItem, OnClickListener> mAssistClickHandlers = new HashMap<>();
 
-        public TextActionModeCallback(boolean hasSelection) {
-            mHasSelection = hasSelection;
+        TextActionModeCallback(@TextActionMode int mode) {
+            mHasSelection = mode == TextActionMode.SELECTION
+                    || (mTextIsSelectable && mode == TextActionMode.TEXT_LINK);
             if (mHasSelection) {
                 SelectionModifierCursorController selectionController = getSelectionController();
                 if (selectionController.mStartHandle == null) {
diff --git a/core/java/android/widget/SelectionActionModeHelper.java b/core/java/android/widget/SelectionActionModeHelper.java
index d0ad27a..2c6466c 100644
--- a/core/java/android/widget/SelectionActionModeHelper.java
+++ b/core/java/android/widget/SelectionActionModeHelper.java
@@ -35,6 +35,7 @@
 import android.view.ActionMode;
 import android.view.textclassifier.TextClassification;
 import android.view.textclassifier.TextClassifier;
+import android.view.textclassifier.TextLinks;
 import android.view.textclassifier.TextSelection;
 import android.view.textclassifier.logging.SmartSelectionEventTracker;
 import android.view.textclassifier.logging.SmartSelectionEventTracker.SelectionEvent;
@@ -97,7 +98,10 @@
         }
     }
 
-    public void startActionModeAsync(boolean adjustSelection) {
+    /**
+     * Starts Selection ActionMode.
+     */
+    public void startSelectionActionModeAsync(boolean adjustSelection) {
         // Check if the smart selection should run for editable text.
         adjustSelection &= !mTextView.isTextEditable()
                 || mTextView.getTextClassifier().getSettings()
@@ -109,7 +113,7 @@
                 mTextView.getSelectionEnd());
         cancelAsyncTask();
         if (skipTextClassification()) {
-            startActionMode(null);
+            startSelectionActionMode(null);
         } else {
             resetTextClassificationHelper();
             mTextClassificationAsyncTask = new TextClassificationAsyncTask(
@@ -119,8 +123,27 @@
                             ? mTextClassificationHelper::suggestSelection
                             : mTextClassificationHelper::classifyText,
                     mSmartSelectSprite != null
-                            ? this::startActionModeWithSmartSelectAnimation
-                            : this::startActionMode)
+                            ? this::startSelectionActionModeWithSmartSelectAnimation
+                            : this::startSelectionActionMode)
+                    .execute();
+        }
+    }
+
+    /**
+     * Starts Link ActionMode.
+     */
+    public void startLinkActionModeAsync(TextLinks.TextLink textLink) {
+        //TODO: tracking/logging
+        cancelAsyncTask();
+        if (skipTextClassification()) {
+            startLinkActionMode(null);
+        } else {
+            resetTextClassificationHelper(textLink.getStart(), textLink.getEnd());
+            mTextClassificationAsyncTask = new TextClassificationAsyncTask(
+                    mTextView,
+                    mTextClassificationHelper.getTimeoutDuration(),
+                    mTextClassificationHelper::classifyText,
+                    this::startLinkActionMode)
                     .execute();
         }
     }
@@ -200,9 +223,19 @@
         return noOpTextClassifier || noSelection || password;
     }
 
-    private void startActionMode(@Nullable SelectionResult result) {
+    private void startLinkActionMode(@Nullable SelectionResult result) {
+        startActionMode(Editor.TextActionMode.TEXT_LINK, result);
+    }
+
+    private void startSelectionActionMode(@Nullable SelectionResult result) {
+        startActionMode(Editor.TextActionMode.SELECTION, result);
+    }
+
+    private void startActionMode(
+            @Editor.TextActionMode int actionMode, @Nullable SelectionResult result) {
         final CharSequence text = getText(mTextView);
-        if (result != null && text instanceof Spannable) {
+        if (result != null && text instanceof Spannable
+                && (mTextView.isTextSelectable() || mTextView.isTextEditable())) {
             // Do not change the selection if TextClassifier should be dark launched.
             if (!mTextView.getTextClassifier().getSettings().isDarkLaunch()) {
                 Selection.setSelection((Spannable) text, result.mStart, result.mEnd);
@@ -211,12 +244,13 @@
         } else {
             mTextClassification = null;
         }
-        if (mEditor.startSelectionActionModeInternal()) {
+        if (mEditor.startActionModeInternal(actionMode)) {
             final SelectionModifierCursorController controller = mEditor.getSelectionController();
-            if (controller != null) {
+            if (controller != null
+                    && (mTextView.isTextSelectable() || mTextView.isTextEditable())) {
                 controller.show();
             }
-            if (result != null) {
+            if (result != null && actionMode == Editor.TextActionMode.SELECTION) {
                 mSelectionTracker.onSmartSelection(result);
             }
         }
@@ -224,10 +258,11 @@
         mTextClassificationAsyncTask = null;
     }
 
-    private void startActionModeWithSmartSelectAnimation(@Nullable SelectionResult result) {
+    private void startSelectionActionModeWithSmartSelectAnimation(
+            @Nullable SelectionResult result) {
         final Layout layout = mTextView.getLayout();
 
-        final Runnable onAnimationEndCallback = () -> startActionMode(result);
+        final Runnable onAnimationEndCallback = () -> startSelectionActionMode(result);
         // TODO do not trigger the animation if the change included only non-printable characters
         final boolean didSelectionChange =
                 result != null && (mTextView.getSelectionStart() != result.mStart
@@ -386,15 +421,24 @@
         mTextClassificationAsyncTask = null;
     }
 
-    private void resetTextClassificationHelper() {
+    private void resetTextClassificationHelper(int selectionStart, int selectionEnd) {
+        if (selectionStart < 0 || selectionEnd < 0) {
+            // Use selection indices
+            selectionStart = mTextView.getSelectionStart();
+            selectionEnd = mTextView.getSelectionEnd();
+        }
         mTextClassificationHelper.init(
                 mTextView.getContext(),
                 mTextView.getTextClassifier(),
                 getText(mTextView),
-                mTextView.getSelectionStart(), mTextView.getSelectionEnd(),
+                selectionStart, selectionEnd,
                 mTextView.getTextLocales());
     }
 
+    private void resetTextClassificationHelper() {
+        resetTextClassificationHelper(-1, -1);
+    }
+
     private void cancelSmartSelectAnimation() {
         if (mSmartSelectSprite != null) {
             mSmartSelectSprite.cancelAnimation();
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 903d3ca..9ac443b 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -160,6 +160,7 @@
 import android.view.inputmethod.InputMethodManager;
 import android.view.textclassifier.TextClassificationManager;
 import android.view.textclassifier.TextClassifier;
+import android.view.textclassifier.TextLinks;
 import android.view.textservice.SpellCheckerSubtype;
 import android.view.textservice.TextServicesManager;
 import android.widget.RemoteViews.RemoteView;
@@ -168,6 +169,7 @@
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.internal.util.FastMath;
+import com.android.internal.util.Preconditions;
 import com.android.internal.widget.EditableInputConnection;
 
 import libcore.util.EmptyArray;
@@ -11151,6 +11153,20 @@
     }
 
     /**
+     * Starts an ActionMode for the specified TextLink.
+     *
+     * @return Whether or not we're attempting to start the action mode.
+     * @hide
+     */
+    public boolean requestActionMode(@NonNull TextLinks.TextLink link) {
+        Preconditions.checkNotNull(link);
+        if (mEditor != null) {
+            mEditor.startLinkActionModeAsync(link);
+            return true;
+        }
+        return false;
+    }
+    /**
      * @hide
      */
     protected void stopTextActionMode() {
diff --git a/core/res/res/interpolator/aggressive_ease.xml b/core/res/res/interpolator/aggressive_ease.xml
new file mode 100644
index 0000000..620424f
--- /dev/null
+++ b/core/res/res/interpolator/aggressive_ease.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+
+<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
+    android:controlX1="0.2"
+    android:controlY1="0"
+    android:controlX2="0"
+    android:controlY2="1"/>
\ No newline at end of file
diff --git a/core/res/res/interpolator/emphasized_deceleration.xml b/core/res/res/interpolator/emphasized_deceleration.xml
new file mode 100644
index 0000000..60c315c
--- /dev/null
+++ b/core/res/res/interpolator/emphasized_deceleration.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+
+<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
+    android:controlX1="0.1"
+    android:controlY1="0.8"
+    android:controlX2="0.2"
+    android:controlY2="1"/>
\ No newline at end of file
diff --git a/core/res/res/interpolator/exaggerated_ease.xml b/core/res/res/interpolator/exaggerated_ease.xml
new file mode 100644
index 0000000..4961c1c
--- /dev/null
+++ b/core/res/res/interpolator/exaggerated_ease.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+
+<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
+    android:pathData="M 0,0 C 0.05, 0, 0.133333, 0.08, 0.166666, 0.4 C 0.225, 0.94, 0.25, 1, 1, 1"/>
diff --git a/core/tests/coretests/src/android/widget/TextViewActivityTest.java b/core/tests/coretests/src/android/widget/TextViewActivityTest.java
index 0e460b9..1a654f4 100644
--- a/core/tests/coretests/src/android/widget/TextViewActivityTest.java
+++ b/core/tests/coretests/src/android/widget/TextViewActivityTest.java
@@ -27,8 +27,10 @@
 import static android.support.test.espresso.matcher.ViewMatchers.withText;
 import static android.widget.espresso.CustomViewActions.longPressAtRelativeCoordinates;
 import static android.widget.espresso.DragHandleUtils.onHandleView;
-import static android.widget.espresso.FloatingToolbarEspressoUtils.assertFloatingToolbarContainsItem;
-import static android.widget.espresso.FloatingToolbarEspressoUtils.assertFloatingToolbarDoesNotContainItem;
+import static android.widget.espresso.FloatingToolbarEspressoUtils
+        .assertFloatingToolbarContainsItem;
+import static android.widget.espresso.FloatingToolbarEspressoUtils
+        .assertFloatingToolbarDoesNotContainItem;
 import static android.widget.espresso.FloatingToolbarEspressoUtils.assertFloatingToolbarIsDisplayed;
 import static android.widget.espresso.FloatingToolbarEspressoUtils.assertFloatingToolbarItemIndex;
 import static android.widget.espresso.FloatingToolbarEspressoUtils.clickFloatingToolbarItem;
@@ -68,12 +70,15 @@
 import android.text.InputType;
 import android.text.Selection;
 import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.method.LinkMovementMethod;
 import android.view.ActionMode;
 import android.view.KeyEvent;
 import android.view.Menu;
 import android.view.MenuItem;
 import android.view.textclassifier.TextClassificationManager;
 import android.view.textclassifier.TextClassifier;
+import android.view.textclassifier.TextLinks;
 import android.widget.espresso.CustomViewActions.RelativeCoordinatesProvider;
 
 import com.android.frameworks.coretests.R;
@@ -305,6 +310,33 @@
     }
 
     @Test
+    public void testToolbarAppearsAfterLinkClicked() throws Throwable {
+        useSystemDefaultTextClassifier();
+        TextClassificationManager textClassificationManager =
+                mActivity.getSystemService(TextClassificationManager.class);
+        TextClassifier textClassifier = textClassificationManager.getTextClassifier();
+        final TextView textView = mActivity.findViewById(R.id.textview);
+        SpannableString content = new SpannableString("Call me at +19148277737");
+        TextLinks links = textClassifier.generateLinks(content);
+        links.apply(content, null);
+
+        mActivityRule.runOnUiThread(() -> {
+            textView.setText(content);
+            textView.setMovementMethod(LinkMovementMethod.getInstance());
+        });
+        mInstrumentation.waitForIdleSync();
+
+        // Wait for the UI thread to refresh
+        Thread.sleep(1000);
+
+        TextLinks.TextLink textLink = links.getLinks().iterator().next();
+        int position = (textLink.getStart() + textLink.getEnd()) / 2;
+        onView(withId(R.id.textview)).perform(clickOnTextAtIndex(position));
+        sleepForFloatingToolbarPopup();
+        assertFloatingToolbarIsDisplayed();
+    }
+
+    @Test
     public void testToolbarAndInsertionHandle() {
         final String text = "text";
         onView(withId(R.id.textview)).perform(replaceText(text));
diff --git a/media/java/android/mtp/MtpDatabase.java b/media/java/android/mtp/MtpDatabase.java
index ba29d2d..a647dcc 100755
--- a/media/java/android/mtp/MtpDatabase.java
+++ b/media/java/android/mtp/MtpDatabase.java
@@ -30,6 +30,7 @@
 import android.os.BatteryManager;
 import android.os.RemoteException;
 import android.os.SystemProperties;
+import android.os.storage.StorageVolume;
 import android.provider.MediaStore;
 import android.provider.MediaStore.Audio;
 import android.provider.MediaStore.Files;
@@ -40,21 +41,31 @@
 
 import dalvik.system.CloseGuard;
 
+import com.google.android.collect.Sets;
+
 import java.io.File;
-import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashMap;
+import java.util.Iterator;
 import java.util.Locale;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
 
 /**
+ * MtpDatabase provides an interface for MTP operations that MtpServer can use. To do this, it uses
+ * MtpStorageManager for filesystem operations and MediaProvider to get media metadata. File
+ * operations are also reflected in MediaProvider if possible.
+ * operations
  * {@hide}
  */
 public class MtpDatabase implements AutoCloseable {
-    private static final String TAG = "MtpDatabase";
+    private static final String TAG = MtpDatabase.class.getSimpleName();
 
-    private final Context mUserContext;
     private final Context mContext;
-    private final String mPackageName;
     private final ContentProviderClient mMediaProvider;
     private final String mVolumeName;
     private final Uri mObjectsUri;
@@ -63,527 +74,36 @@
     private final AtomicBoolean mClosed = new AtomicBoolean();
     private final CloseGuard mCloseGuard = CloseGuard.get();
 
-    // path to primary storage
-    private final String mMediaStoragePath;
-    // if not null, restrict all queries to these subdirectories
-    private final String[] mSubDirectories;
-    // where clause for restricting queries to files in mSubDirectories
-    private String mSubDirectoriesWhere;
-    // where arguments for restricting queries to files in mSubDirectories
-    private String[] mSubDirectoriesWhereArgs;
-
-    private final HashMap<String, MtpStorage> mStorageMap = new HashMap<String, MtpStorage>();
+    private final HashMap<String, MtpStorage> mStorageMap = new HashMap<>();
 
     // cached property groups for single properties
-    private final HashMap<Integer, MtpPropertyGroup> mPropertyGroupsByProperty
-            = new HashMap<Integer, MtpPropertyGroup>();
+    private final HashMap<Integer, MtpPropertyGroup> mPropertyGroupsByProperty = new HashMap<>();
 
     // cached property groups for all properties for a given format
-    private final HashMap<Integer, MtpPropertyGroup> mPropertyGroupsByFormat
-            = new HashMap<Integer, MtpPropertyGroup>();
-
-    // true if the database has been modified in the current MTP session
-    private boolean mDatabaseModified;
+    private final HashMap<Integer, MtpPropertyGroup> mPropertyGroupsByFormat = new HashMap<>();
 
     // SharedPreferences for writable MTP device properties
     private SharedPreferences mDeviceProperties;
-    private static final int DEVICE_PROPERTIES_DATABASE_VERSION = 1;
 
-    private static final String[] ID_PROJECTION = new String[] {
-            Files.FileColumns._ID, // 0
-    };
-    private static final String[] PATH_PROJECTION = new String[] {
-            Files.FileColumns._ID, // 0
-            Files.FileColumns.DATA, // 1
-    };
-    private static final String[] FORMAT_PROJECTION = new String[] {
-            Files.FileColumns._ID, // 0
-            Files.FileColumns.FORMAT, // 1
-    };
-    private static final String[] PATH_FORMAT_PROJECTION = new String[] {
-            Files.FileColumns._ID, // 0
-            Files.FileColumns.DATA, // 1
-            Files.FileColumns.FORMAT, // 2
-    };
-    private static final String[] OBJECT_INFO_PROJECTION = new String[] {
-            Files.FileColumns._ID, // 0
-            Files.FileColumns.STORAGE_ID, // 1
-            Files.FileColumns.FORMAT, // 2
-            Files.FileColumns.PARENT, // 3
-            Files.FileColumns.DATA, // 4
-            Files.FileColumns.DATE_ADDED, // 5
-            Files.FileColumns.DATE_MODIFIED, // 6
-    };
-    private static final String ID_WHERE = Files.FileColumns._ID + "=?";
-    private static final String PATH_WHERE = Files.FileColumns.DATA + "=?";
-
-    private static final String STORAGE_WHERE = Files.FileColumns.STORAGE_ID + "=?";
-    private static final String FORMAT_WHERE = Files.FileColumns.FORMAT + "=?";
-    private static final String PARENT_WHERE = Files.FileColumns.PARENT + "=?";
-    private static final String STORAGE_FORMAT_WHERE = STORAGE_WHERE + " AND "
-                                            + Files.FileColumns.FORMAT + "=?";
-    private static final String STORAGE_PARENT_WHERE = STORAGE_WHERE + " AND "
-                                            + Files.FileColumns.PARENT + "=?";
-    private static final String FORMAT_PARENT_WHERE = FORMAT_WHERE + " AND "
-                                            + Files.FileColumns.PARENT + "=?";
-    private static final String STORAGE_FORMAT_PARENT_WHERE = STORAGE_FORMAT_WHERE + " AND "
-                                            + Files.FileColumns.PARENT + "=?";
-
-    private MtpServer mServer;
-
-    // read from native code
+    // Cached device properties
     private int mBatteryLevel;
     private int mBatteryScale;
-
     private int mDeviceType;
 
+    private MtpServer mServer;
+    private MtpStorageManager mManager;
+
+    private static final String PATH_WHERE = Files.FileColumns.DATA + "=?";
+    private static final String[] ID_PROJECTION = new String[] {Files.FileColumns._ID};
+    private static final String[] PATH_PROJECTION = new String[] {Files.FileColumns.DATA};
+    private static final String NO_MEDIA = ".nomedia";
+
     static {
         System.loadLibrary("media_jni");
     }
 
-    private BroadcastReceiver mBatteryReceiver = new BroadcastReceiver() {
-          @Override
-        public void onReceive(Context context, Intent intent) {
-            String action = intent.getAction();
-            if (action.equals(Intent.ACTION_BATTERY_CHANGED)) {
-                mBatteryScale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, 0);
-                int newLevel = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0);
-                if (newLevel != mBatteryLevel) {
-                    mBatteryLevel = newLevel;
-                    if (mServer != null) {
-                        // send device property changed event
-                        mServer.sendDevicePropertyChanged(
-                                MtpConstants.DEVICE_PROPERTY_BATTERY_LEVEL);
-                    }
-                }
-            }
-        }
-    };
-
-    public MtpDatabase(Context context, Context userContext, String volumeName, String storagePath,
-            String[] subDirectories) {
-        native_setup();
-
-        mContext = context;
-        mUserContext = userContext;
-        mPackageName = context.getPackageName();
-        mMediaProvider = userContext.getContentResolver()
-                .acquireContentProviderClient(MediaStore.AUTHORITY);
-        mVolumeName = volumeName;
-        mMediaStoragePath = storagePath;
-        mObjectsUri = Files.getMtpObjectsUri(volumeName);
-        mMediaScanner = new MediaScanner(context, mVolumeName);
-
-        mSubDirectories = subDirectories;
-        if (subDirectories != null) {
-            // Compute "where" string for restricting queries to subdirectories
-            StringBuilder builder = new StringBuilder();
-            builder.append("(");
-            int count = subDirectories.length;
-            for (int i = 0; i < count; i++) {
-                builder.append(Files.FileColumns.DATA + "=? OR "
-                        + Files.FileColumns.DATA + " LIKE ?");
-                if (i != count - 1) {
-                    builder.append(" OR ");
-                }
-            }
-            builder.append(")");
-            mSubDirectoriesWhere = builder.toString();
-
-            // Compute "where" arguments for restricting queries to subdirectories
-            mSubDirectoriesWhereArgs = new String[count * 2];
-            for (int i = 0, j = 0; i < count; i++) {
-                String path = subDirectories[i];
-                mSubDirectoriesWhereArgs[j++] = path;
-                mSubDirectoriesWhereArgs[j++] = path + "/%";
-            }
-        }
-
-        initDeviceProperties(context);
-        mDeviceType = SystemProperties.getInt("sys.usb.mtp.device_type", 0);
-
-        mCloseGuard.open("close");
-    }
-
-    public void setServer(MtpServer server) {
-        mServer = server;
-
-        // always unregister before registering
-        try {
-            mContext.unregisterReceiver(mBatteryReceiver);
-        } catch (IllegalArgumentException e) {
-            // wasn't previously registered, ignore
-        }
-
-        // register for battery notifications when we are connected
-        if (server != null) {
-            mContext.registerReceiver(mBatteryReceiver,
-                    new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
-        }
-    }
-
-    @Override
-    public void close() {
-        mCloseGuard.close();
-        if (mClosed.compareAndSet(false, true)) {
-            mMediaScanner.close();
-            mMediaProvider.close();
-            native_finalize();
-        }
-    }
-
-    @Override
-    protected void finalize() throws Throwable {
-        try {
-            if (mCloseGuard != null) {
-                mCloseGuard.warnIfOpen();
-            }
-
-            close();
-        } finally {
-            super.finalize();
-        }
-    }
-
-    public void addStorage(MtpStorage storage) {
-        mStorageMap.put(storage.getPath(), storage);
-    }
-
-    public void removeStorage(MtpStorage storage) {
-        mStorageMap.remove(storage.getPath());
-    }
-
-    private void initDeviceProperties(Context context) {
-        final String devicePropertiesName = "device-properties";
-        mDeviceProperties = context.getSharedPreferences(devicePropertiesName, Context.MODE_PRIVATE);
-        File databaseFile = context.getDatabasePath(devicePropertiesName);
-
-        if (databaseFile.exists()) {
-            // for backward compatibility - read device properties from sqlite database
-            // and migrate them to shared prefs
-            SQLiteDatabase db = null;
-            Cursor c = null;
-            try {
-                db = context.openOrCreateDatabase("device-properties", Context.MODE_PRIVATE, null);
-                if (db != null) {
-                    c = db.query("properties", new String[] { "_id", "code", "value" },
-                            null, null, null, null, null);
-                    if (c != null) {
-                        SharedPreferences.Editor e = mDeviceProperties.edit();
-                        while (c.moveToNext()) {
-                            String name = c.getString(1);
-                            String value = c.getString(2);
-                            e.putString(name, value);
-                        }
-                        e.commit();
-                    }
-                }
-            } catch (Exception e) {
-                Log.e(TAG, "failed to migrate device properties", e);
-            } finally {
-                if (c != null) c.close();
-                if (db != null) db.close();
-            }
-            context.deleteDatabase(devicePropertiesName);
-        }
-    }
-
-    // check to see if the path is contained in one of our storage subdirectories
-    // returns true if we have no special subdirectories
-    private boolean inStorageSubDirectory(String path) {
-        if (mSubDirectories == null) return true;
-        if (path == null) return false;
-
-        boolean allowed = false;
-        int pathLength = path.length();
-        for (int i = 0; i < mSubDirectories.length && !allowed; i++) {
-            String subdir = mSubDirectories[i];
-            int subdirLength = subdir.length();
-            if (subdirLength < pathLength &&
-                    path.charAt(subdirLength) == '/' &&
-                    path.startsWith(subdir)) {
-                allowed = true;
-            }
-        }
-        return allowed;
-    }
-
-    // check to see if the path matches one of our storage subdirectories
-    // returns true if we have no special subdirectories
-    private boolean isStorageSubDirectory(String path) {
-    if (mSubDirectories == null) return false;
-        for (int i = 0; i < mSubDirectories.length; i++) {
-            if (path.equals(mSubDirectories[i])) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    // returns true if the path is in the storage root
-    private boolean inStorageRoot(String path) {
-        try {
-            File f = new File(path);
-            String canonical = f.getCanonicalPath();
-            for (String root: mStorageMap.keySet()) {
-                if (canonical.startsWith(root)) {
-                    return true;
-                }
-            }
-        } catch (IOException e) {
-            // ignore
-        }
-        return false;
-    }
-
-    private int beginSendObject(String path, int format, int parent,
-                         int storageId, long size, long modified) {
-        // if the path is outside of the storage root, do not allow access
-        if (!inStorageRoot(path)) {
-            Log.e(TAG, "attempt to put file outside of storage area: " + path);
-            return -1;
-        }
-        // if mSubDirectories is not null, do not allow copying files to any other locations
-        if (!inStorageSubDirectory(path)) return -1;
-
-        // make sure the object does not exist
-        if (path != null) {
-            Cursor c = null;
-            try {
-                c = mMediaProvider.query(mObjectsUri, ID_PROJECTION, PATH_WHERE,
-                        new String[] { path }, null, null);
-                if (c != null && c.getCount() > 0) {
-                    Log.w(TAG, "file already exists in beginSendObject: " + path);
-                    return -1;
-                }
-            } catch (RemoteException e) {
-                Log.e(TAG, "RemoteException in beginSendObject", e);
-            } finally {
-                if (c != null) {
-                    c.close();
-                }
-            }
-        }
-
-        mDatabaseModified = true;
-        ContentValues values = new ContentValues();
-        values.put(Files.FileColumns.DATA, path);
-        values.put(Files.FileColumns.FORMAT, format);
-        values.put(Files.FileColumns.PARENT, parent);
-        values.put(Files.FileColumns.STORAGE_ID, storageId);
-        values.put(Files.FileColumns.SIZE, size);
-        values.put(Files.FileColumns.DATE_MODIFIED, modified);
-
-        try {
-            Uri uri = mMediaProvider.insert(mObjectsUri, values);
-            if (uri != null) {
-                return Integer.parseInt(uri.getPathSegments().get(2));
-            } else {
-                return -1;
-            }
-        } catch (RemoteException e) {
-            Log.e(TAG, "RemoteException in beginSendObject", e);
-            return -1;
-        }
-    }
-
-    private void endSendObject(String path, int handle, int format, boolean succeeded) {
-        if (succeeded) {
-            // handle abstract playlists separately
-            // they do not exist in the file system so don't use the media scanner here
-            if (format == MtpConstants.FORMAT_ABSTRACT_AV_PLAYLIST) {
-                // extract name from path
-                String name = path;
-                int lastSlash = name.lastIndexOf('/');
-                if (lastSlash >= 0) {
-                    name = name.substring(lastSlash + 1);
-                }
-                // strip trailing ".pla" from the name
-                if (name.endsWith(".pla")) {
-                    name = name.substring(0, name.length() - 4);
-                }
-
-                ContentValues values = new ContentValues(1);
-                values.put(Audio.Playlists.DATA, path);
-                values.put(Audio.Playlists.NAME, name);
-                values.put(Files.FileColumns.FORMAT, format);
-                values.put(Files.FileColumns.DATE_MODIFIED, System.currentTimeMillis() / 1000);
-                values.put(MediaColumns.MEDIA_SCANNER_NEW_OBJECT_ID, handle);
-                try {
-                    Uri uri = mMediaProvider.insert(
-                            Audio.Playlists.EXTERNAL_CONTENT_URI, values);
-                } catch (RemoteException e) {
-                    Log.e(TAG, "RemoteException in endSendObject", e);
-                }
-            } else {
-                mMediaScanner.scanMtpFile(path, handle, format);
-            }
-        } else {
-            deleteFile(handle);
-        }
-    }
-
-    private void doScanDirectory(String path) {
-        String[] scanPath;
-        scanPath = new String[] { path };
-        mMediaScanner.scanDirectories(scanPath);
-    }
-
-    private Cursor createObjectQuery(int storageID, int format, int parent) throws RemoteException {
-        String where;
-        String[] whereArgs;
-
-        if (storageID == 0xFFFFFFFF) {
-            // query all stores
-            if (format == 0) {
-                // query all formats
-                if (parent == 0) {
-                    // query all objects
-                    where = null;
-                    whereArgs = null;
-                } else {
-                    if (parent == 0xFFFFFFFF) {
-                        // all objects in root of store
-                        parent = 0;
-                    }
-                    where = PARENT_WHERE;
-                    whereArgs = new String[] { Integer.toString(parent) };
-                }
-            } else {
-                // query specific format
-                if (parent == 0) {
-                    // query all objects
-                    where = FORMAT_WHERE;
-                    whereArgs = new String[] { Integer.toString(format) };
-                } else {
-                    if (parent == 0xFFFFFFFF) {
-                        // all objects in root of store
-                        parent = 0;
-                    }
-                    where = FORMAT_PARENT_WHERE;
-                    whereArgs = new String[] { Integer.toString(format),
-                                               Integer.toString(parent) };
-                }
-            }
-        } else {
-            // query specific store
-            if (format == 0) {
-                // query all formats
-                if (parent == 0) {
-                    // query all objects
-                    where = STORAGE_WHERE;
-                    whereArgs = new String[] { Integer.toString(storageID) };
-                } else {
-                    if (parent == 0xFFFFFFFF) {
-                        // all objects in root of store
-                        parent = 0;
-                        where = STORAGE_PARENT_WHERE;
-                        whereArgs = new String[]{Integer.toString(storageID),
-                                Integer.toString(parent)};
-                    }  else {
-                        // If a parent is specified, the storage is redundant
-                        where = PARENT_WHERE;
-                        whereArgs = new String[]{Integer.toString(parent)};
-                    }
-                }
-            } else {
-                // query specific format
-                if (parent == 0) {
-                    // query all objects
-                    where = STORAGE_FORMAT_WHERE;
-                    whereArgs = new String[] {  Integer.toString(storageID),
-                                                Integer.toString(format) };
-                } else {
-                    if (parent == 0xFFFFFFFF) {
-                        // all objects in root of store
-                        parent = 0;
-                        where = STORAGE_FORMAT_PARENT_WHERE;
-                        whereArgs = new String[]{Integer.toString(storageID),
-                                Integer.toString(format),
-                                Integer.toString(parent)};
-                    } else {
-                        // If a parent is specified, the storage is redundant
-                        where = FORMAT_PARENT_WHERE;
-                        whereArgs = new String[]{Integer.toString(format),
-                                Integer.toString(parent)};
-                    }
-                }
-            }
-        }
-
-        // if we are restricting queries to mSubDirectories, we need to add the restriction
-        // onto our "where" arguments
-        if (mSubDirectoriesWhere != null) {
-            if (where == null) {
-                where = mSubDirectoriesWhere;
-                whereArgs = mSubDirectoriesWhereArgs;
-            } else {
-                where = where + " AND " + mSubDirectoriesWhere;
-
-                // create new array to hold whereArgs and mSubDirectoriesWhereArgs
-                String[] newWhereArgs =
-                        new String[whereArgs.length + mSubDirectoriesWhereArgs.length];
-                int i, j;
-                for (i = 0; i < whereArgs.length; i++) {
-                    newWhereArgs[i] = whereArgs[i];
-                }
-                for (j = 0; j < mSubDirectoriesWhereArgs.length; i++, j++) {
-                    newWhereArgs[i] = mSubDirectoriesWhereArgs[j];
-                }
-                whereArgs = newWhereArgs;
-            }
-        }
-
-        return mMediaProvider.query(mObjectsUri, ID_PROJECTION, where,
-                whereArgs, null, null);
-    }
-
-    private int[] getObjectList(int storageID, int format, int parent) {
-        Cursor c = null;
-        try {
-            c = createObjectQuery(storageID, format, parent);
-            if (c == null) {
-                return null;
-            }
-            int count = c.getCount();
-            if (count > 0) {
-                int[] result = new int[count];
-                for (int i = 0; i < count; i++) {
-                    c.moveToNext();
-                    result[i] = c.getInt(0);
-                }
-                return result;
-            }
-        } catch (RemoteException e) {
-            Log.e(TAG, "RemoteException in getObjectList", e);
-        } finally {
-            if (c != null) {
-                c.close();
-            }
-        }
-        return null;
-    }
-
-    private int getNumObjects(int storageID, int format, int parent) {
-        Cursor c = null;
-        try {
-            c = createObjectQuery(storageID, format, parent);
-            if (c != null) {
-                return c.getCount();
-            }
-        } catch (RemoteException e) {
-            Log.e(TAG, "RemoteException in getNumObjects", e);
-        } finally {
-            if (c != null) {
-                c.close();
-            }
-        }
-        return -1;
-    }
-
-    private int[] getSupportedPlaybackFormats() {
-        return new int[] {
-            // allow transfering arbitrary files
+    private static final int[] PLAYBACK_FORMATS = {
+            // allow transferring arbitrary files
             MtpConstants.FORMAT_UNDEFINED,
 
             MtpConstants.FORMAT_ASSOCIATION,
@@ -613,45 +133,23 @@
             MtpConstants.FORMAT_FLAC,
             MtpConstants.FORMAT_DNG,
             MtpConstants.FORMAT_HEIF,
-        };
-    }
+    };
 
-    private int[] getSupportedCaptureFormats() {
-        // no capture formats yet
-        return null;
-    }
-
-    static final int[] FILE_PROPERTIES = {
-            // NOTE must match beginning of AUDIO_PROPERTIES, VIDEO_PROPERTIES
-            // and IMAGE_PROPERTIES below
+    private static final int[] FILE_PROPERTIES = {
             MtpConstants.PROPERTY_STORAGE_ID,
             MtpConstants.PROPERTY_OBJECT_FORMAT,
             MtpConstants.PROPERTY_PROTECTION_STATUS,
             MtpConstants.PROPERTY_OBJECT_SIZE,
             MtpConstants.PROPERTY_OBJECT_FILE_NAME,
             MtpConstants.PROPERTY_DATE_MODIFIED,
-            MtpConstants.PROPERTY_PARENT_OBJECT,
             MtpConstants.PROPERTY_PERSISTENT_UID,
+            MtpConstants.PROPERTY_PARENT_OBJECT,
             MtpConstants.PROPERTY_NAME,
             MtpConstants.PROPERTY_DISPLAY_NAME,
             MtpConstants.PROPERTY_DATE_ADDED,
     };
 
-    static final int[] AUDIO_PROPERTIES = {
-            // NOTE must match FILE_PROPERTIES above
-            MtpConstants.PROPERTY_STORAGE_ID,
-            MtpConstants.PROPERTY_OBJECT_FORMAT,
-            MtpConstants.PROPERTY_PROTECTION_STATUS,
-            MtpConstants.PROPERTY_OBJECT_SIZE,
-            MtpConstants.PROPERTY_OBJECT_FILE_NAME,
-            MtpConstants.PROPERTY_DATE_MODIFIED,
-            MtpConstants.PROPERTY_PARENT_OBJECT,
-            MtpConstants.PROPERTY_PERSISTENT_UID,
-            MtpConstants.PROPERTY_NAME,
-            MtpConstants.PROPERTY_DISPLAY_NAME,
-            MtpConstants.PROPERTY_DATE_ADDED,
-
-            // audio specific properties
+    private static final int[] AUDIO_PROPERTIES = {
             MtpConstants.PROPERTY_ARTIST,
             MtpConstants.PROPERTY_ALBUM_NAME,
             MtpConstants.PROPERTY_ALBUM_ARTIST,
@@ -667,45 +165,25 @@
             MtpConstants.PROPERTY_SAMPLE_RATE,
     };
 
-    static final int[] VIDEO_PROPERTIES = {
-            // NOTE must match FILE_PROPERTIES above
-            MtpConstants.PROPERTY_STORAGE_ID,
-            MtpConstants.PROPERTY_OBJECT_FORMAT,
-            MtpConstants.PROPERTY_PROTECTION_STATUS,
-            MtpConstants.PROPERTY_OBJECT_SIZE,
-            MtpConstants.PROPERTY_OBJECT_FILE_NAME,
-            MtpConstants.PROPERTY_DATE_MODIFIED,
-            MtpConstants.PROPERTY_PARENT_OBJECT,
-            MtpConstants.PROPERTY_PERSISTENT_UID,
-            MtpConstants.PROPERTY_NAME,
-            MtpConstants.PROPERTY_DISPLAY_NAME,
-            MtpConstants.PROPERTY_DATE_ADDED,
-
-            // video specific properties
+    private static final int[] VIDEO_PROPERTIES = {
             MtpConstants.PROPERTY_ARTIST,
             MtpConstants.PROPERTY_ALBUM_NAME,
             MtpConstants.PROPERTY_DURATION,
             MtpConstants.PROPERTY_DESCRIPTION,
     };
 
-    static final int[] IMAGE_PROPERTIES = {
-            // NOTE must match FILE_PROPERTIES above
-            MtpConstants.PROPERTY_STORAGE_ID,
-            MtpConstants.PROPERTY_OBJECT_FORMAT,
-            MtpConstants.PROPERTY_PROTECTION_STATUS,
-            MtpConstants.PROPERTY_OBJECT_SIZE,
-            MtpConstants.PROPERTY_OBJECT_FILE_NAME,
-            MtpConstants.PROPERTY_DATE_MODIFIED,
-            MtpConstants.PROPERTY_PARENT_OBJECT,
-            MtpConstants.PROPERTY_PERSISTENT_UID,
-            MtpConstants.PROPERTY_NAME,
-            MtpConstants.PROPERTY_DISPLAY_NAME,
-            MtpConstants.PROPERTY_DATE_ADDED,
-
-            // image specific properties
+    private static final int[] IMAGE_PROPERTIES = {
             MtpConstants.PROPERTY_DESCRIPTION,
     };
 
+    private static final int[] DEVICE_PROPERTIES = {
+            MtpConstants.DEVICE_PROPERTY_SYNCHRONIZATION_PARTNER,
+            MtpConstants.DEVICE_PROPERTY_DEVICE_FRIENDLY_NAME,
+            MtpConstants.DEVICE_PROPERTY_IMAGE_SIZE,
+            MtpConstants.DEVICE_PROPERTY_BATTERY_LEVEL,
+            MtpConstants.DEVICE_PROPERTY_PERCEIVED_DEVICE_TYPE,
+    };
+
     private int[] getSupportedObjectProperties(int format) {
         switch (format) {
             case MtpConstants.FORMAT_MP3:
@@ -713,183 +191,541 @@
             case MtpConstants.FORMAT_WMA:
             case MtpConstants.FORMAT_OGG:
             case MtpConstants.FORMAT_AAC:
-                return AUDIO_PROPERTIES;
+                return IntStream.concat(Arrays.stream(FILE_PROPERTIES),
+                        Arrays.stream(AUDIO_PROPERTIES)).toArray();
             case MtpConstants.FORMAT_MPEG:
             case MtpConstants.FORMAT_3GP_CONTAINER:
             case MtpConstants.FORMAT_WMV:
-                return VIDEO_PROPERTIES;
+                return IntStream.concat(Arrays.stream(FILE_PROPERTIES),
+                        Arrays.stream(VIDEO_PROPERTIES)).toArray();
             case MtpConstants.FORMAT_EXIF_JPEG:
             case MtpConstants.FORMAT_GIF:
             case MtpConstants.FORMAT_PNG:
             case MtpConstants.FORMAT_BMP:
             case MtpConstants.FORMAT_DNG:
             case MtpConstants.FORMAT_HEIF:
-                return IMAGE_PROPERTIES;
+                return IntStream.concat(Arrays.stream(FILE_PROPERTIES),
+                        Arrays.stream(IMAGE_PROPERTIES)).toArray();
             default:
                 return FILE_PROPERTIES;
         }
     }
 
     private int[] getSupportedDeviceProperties() {
-        return new int[] {
-            MtpConstants.DEVICE_PROPERTY_SYNCHRONIZATION_PARTNER,
-            MtpConstants.DEVICE_PROPERTY_DEVICE_FRIENDLY_NAME,
-            MtpConstants.DEVICE_PROPERTY_IMAGE_SIZE,
-            MtpConstants.DEVICE_PROPERTY_BATTERY_LEVEL,
-            MtpConstants.DEVICE_PROPERTY_PERCEIVED_DEVICE_TYPE,
-        };
+        return DEVICE_PROPERTIES;
+    }
+
+    private int[] getSupportedPlaybackFormats() {
+        return PLAYBACK_FORMATS;
+    }
+
+    private int[] getSupportedCaptureFormats() {
+        // no capture formats yet
+        return null;
+    }
+
+    private BroadcastReceiver mBatteryReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            if (action.equals(Intent.ACTION_BATTERY_CHANGED)) {
+                mBatteryScale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, 0);
+                int newLevel = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0);
+                if (newLevel != mBatteryLevel) {
+                    mBatteryLevel = newLevel;
+                    if (mServer != null) {
+                        // send device property changed event
+                        mServer.sendDevicePropertyChanged(
+                                MtpConstants.DEVICE_PROPERTY_BATTERY_LEVEL);
+                    }
+                }
+            }
+        }
+    };
+
+    public MtpDatabase(Context context, Context userContext, String volumeName,
+            String[] subDirectories) {
+        native_setup();
+        mContext = context;
+        mMediaProvider = userContext.getContentResolver()
+                .acquireContentProviderClient(MediaStore.AUTHORITY);
+        mVolumeName = volumeName;
+        mObjectsUri = Files.getMtpObjectsUri(volumeName);
+        mMediaScanner = new MediaScanner(context, mVolumeName);
+        mManager = new MtpStorageManager(new MtpStorageManager.MtpNotifier() {
+            @Override
+            public void sendObjectAdded(int id) {
+                if (MtpDatabase.this.mServer != null)
+                    MtpDatabase.this.mServer.sendObjectAdded(id);
+            }
+
+            @Override
+            public void sendObjectRemoved(int id) {
+                if (MtpDatabase.this.mServer != null)
+                    MtpDatabase.this.mServer.sendObjectRemoved(id);
+            }
+        }, subDirectories == null ? null : Sets.newHashSet(subDirectories));
+
+        initDeviceProperties(context);
+        mDeviceType = SystemProperties.getInt("sys.usb.mtp.device_type", 0);
+        mCloseGuard.open("close");
+    }
+
+    public void setServer(MtpServer server) {
+        mServer = server;
+        // always unregister before registering
+        try {
+            mContext.unregisterReceiver(mBatteryReceiver);
+        } catch (IllegalArgumentException e) {
+            // wasn't previously registered, ignore
+        }
+        // register for battery notifications when we are connected
+        if (server != null) {
+            mContext.registerReceiver(mBatteryReceiver,
+                    new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
+        }
+    }
+
+    @Override
+    public void close() {
+        mManager.close();
+        mCloseGuard.close();
+        if (mClosed.compareAndSet(false, true)) {
+            mMediaScanner.close();
+            mMediaProvider.close();
+            native_finalize();
+        }
+    }
+
+    @Override
+    protected void finalize() throws Throwable {
+        try {
+            if (mCloseGuard != null) {
+                mCloseGuard.warnIfOpen();
+            }
+            close();
+        } finally {
+            super.finalize();
+        }
+    }
+
+    public void addStorage(StorageVolume storage) {
+        MtpStorage mtpStorage = mManager.addMtpStorage(storage);
+        mStorageMap.put(storage.getPath(), mtpStorage);
+        mServer.addStorage(mtpStorage);
+    }
+
+    public void removeStorage(StorageVolume storage) {
+        MtpStorage mtpStorage = mStorageMap.get(storage.getPath());
+        if (mtpStorage == null) {
+            return;
+        }
+        mServer.removeStorage(mtpStorage);
+        mManager.removeMtpStorage(mtpStorage);
+        mStorageMap.remove(storage.getPath());
+    }
+
+    private void initDeviceProperties(Context context) {
+        final String devicePropertiesName = "device-properties";
+        mDeviceProperties = context.getSharedPreferences(devicePropertiesName,
+                Context.MODE_PRIVATE);
+        File databaseFile = context.getDatabasePath(devicePropertiesName);
+
+        if (databaseFile.exists()) {
+            // for backward compatibility - read device properties from sqlite database
+            // and migrate them to shared prefs
+            SQLiteDatabase db = null;
+            Cursor c = null;
+            try {
+                db = context.openOrCreateDatabase("device-properties", Context.MODE_PRIVATE, null);
+                if (db != null) {
+                    c = db.query("properties", new String[]{"_id", "code", "value"},
+                            null, null, null, null, null);
+                    if (c != null) {
+                        SharedPreferences.Editor e = mDeviceProperties.edit();
+                        while (c.moveToNext()) {
+                            String name = c.getString(1);
+                            String value = c.getString(2);
+                            e.putString(name, value);
+                        }
+                        e.commit();
+                    }
+                }
+            } catch (Exception e) {
+                Log.e(TAG, "failed to migrate device properties", e);
+            } finally {
+                if (c != null) c.close();
+                if (db != null) db.close();
+            }
+            context.deleteDatabase(devicePropertiesName);
+        }
+    }
+
+    private int beginSendObject(String path, int format, int parent, int storageId) {
+        MtpStorageManager.MtpObject parentObj =
+                parent == 0 ? mManager.getStorageRoot(storageId) : mManager.getObject(parent);
+        if (parentObj == null) {
+            return -1;
+        }
+
+        Path objPath = Paths.get(path);
+        return mManager.beginSendObject(parentObj, objPath.getFileName().toString(), format);
+    }
+
+    private void endSendObject(int handle, boolean succeeded) {
+        MtpStorageManager.MtpObject obj = mManager.getObject(handle);
+        if (obj == null || !mManager.endSendObject(obj, succeeded)) {
+            Log.e(TAG, "Failed to successfully end send object");
+            return;
+        }
+        // Add the new file to MediaProvider
+        if (succeeded) {
+            String path = obj.getPath().toString();
+            int format = obj.getFormat();
+            // Get parent info from MediaProvider, since the id is different from MTP's
+            ContentValues values = new ContentValues();
+            values.put(Files.FileColumns.DATA, path);
+            values.put(Files.FileColumns.FORMAT, format);
+            values.put(Files.FileColumns.SIZE, obj.getSize());
+            values.put(Files.FileColumns.DATE_MODIFIED, obj.getModifiedTime());
+            try {
+                if (obj.getParent().isRoot()) {
+                    values.put(Files.FileColumns.PARENT, 0);
+                } else {
+                    int parentId = findInMedia(obj.getParent().getPath());
+                    if (parentId != -1) {
+                        values.put(Files.FileColumns.PARENT, parentId);
+                    } else {
+                        // The parent isn't in MediaProvider. Don't add the new file.
+                        return;
+                    }
+                }
+
+                Uri uri = mMediaProvider.insert(mObjectsUri, values);
+                if (uri != null) {
+                    rescanFile(path, Integer.parseInt(uri.getPathSegments().get(2)), format);
+                }
+            } catch (RemoteException e) {
+                Log.e(TAG, "RemoteException in beginSendObject", e);
+            }
+        }
+    }
+
+    private void rescanFile(String path, int handle, int format) {
+        // handle abstract playlists separately
+        // they do not exist in the file system so don't use the media scanner here
+        if (format == MtpConstants.FORMAT_ABSTRACT_AV_PLAYLIST) {
+            // extract name from path
+            String name = path;
+            int lastSlash = name.lastIndexOf('/');
+            if (lastSlash >= 0) {
+                name = name.substring(lastSlash + 1);
+            }
+            // strip trailing ".pla" from the name
+            if (name.endsWith(".pla")) {
+                name = name.substring(0, name.length() - 4);
+            }
+
+            ContentValues values = new ContentValues(1);
+            values.put(Audio.Playlists.DATA, path);
+            values.put(Audio.Playlists.NAME, name);
+            values.put(Files.FileColumns.FORMAT, format);
+            values.put(Files.FileColumns.DATE_MODIFIED, System.currentTimeMillis() / 1000);
+            values.put(MediaColumns.MEDIA_SCANNER_NEW_OBJECT_ID, handle);
+            try {
+                mMediaProvider.insert(
+                        Audio.Playlists.EXTERNAL_CONTENT_URI, values);
+            } catch (RemoteException e) {
+                Log.e(TAG, "RemoteException in endSendObject", e);
+            }
+        } else {
+            mMediaScanner.scanMtpFile(path, handle, format);
+        }
+    }
+
+    private int[] getObjectList(int storageID, int format, int parent) {
+        Stream<MtpStorageManager.MtpObject> objectStream = mManager.getObjects(parent,
+                format, storageID);
+        if (objectStream == null) {
+            return null;
+        }
+        return objectStream.mapToInt(MtpStorageManager.MtpObject::getId).toArray();
+    }
+
+    private int getNumObjects(int storageID, int format, int parent) {
+        Stream<MtpStorageManager.MtpObject> objectStream = mManager.getObjects(parent,
+                format, storageID);
+        if (objectStream == null) {
+            return -1;
+        }
+        return (int) objectStream.count();
     }
 
     private MtpPropertyList getObjectPropertyList(int handle, int format, int property,
-                        int groupCode, int depth) {
+            int groupCode, int depth) {
         // FIXME - implement group support
-        if (groupCode != 0) {
-            return new MtpPropertyList(0, MtpConstants.RESPONSE_SPECIFICATION_BY_GROUP_UNSUPPORTED);
+        if (property == 0) {
+            if (groupCode == 0) {
+                return new MtpPropertyList(MtpConstants.RESPONSE_PARAMETER_NOT_SUPPORTED);
+            }
+            return new MtpPropertyList(MtpConstants.RESPONSE_SPECIFICATION_BY_GROUP_UNSUPPORTED);
+        }
+        if (depth == 0xFFFFFFFF && (handle == 0 || handle == 0xFFFFFFFF)) {
+            // request all objects starting at root
+            handle = 0xFFFFFFFF;
+            depth = 0;
+        }
+        if (!(depth == 0 || depth == 1)) {
+            // we only support depth 0 and 1
+            // depth 0: single object, depth 1: immediate children
+            return new MtpPropertyList(MtpConstants.RESPONSE_SPECIFICATION_BY_DEPTH_UNSUPPORTED);
+        }
+        Stream<MtpStorageManager.MtpObject> objectStream = Stream.of();
+        if (handle == 0xFFFFFFFF) {
+            // All objects are requested
+            objectStream = mManager.getObjects(0, format, 0xFFFFFFFF);
+            if (objectStream == null) {
+                return new MtpPropertyList(MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE);
+            }
+        } else if (handle != 0) {
+            // Add the requested object if format matches
+            MtpStorageManager.MtpObject obj = mManager.getObject(handle);
+            if (obj == null) {
+                return new MtpPropertyList(MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE);
+            }
+            if (obj.getFormat() == format || format == 0) {
+                objectStream = Stream.of(obj);
+            }
+        }
+        if (handle == 0 || depth == 1) {
+            if (handle == 0) {
+                handle = 0xFFFFFFFF;
+            }
+            // Get the direct children of root or this object.
+            Stream<MtpStorageManager.MtpObject> childStream = mManager.getObjects(handle, format,
+                    0xFFFFFFFF);
+            if (childStream == null) {
+                return new MtpPropertyList(MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE);
+            }
+            objectStream = Stream.concat(objectStream, childStream);
         }
 
+        MtpPropertyList ret = new MtpPropertyList(MtpConstants.RESPONSE_OK);
         MtpPropertyGroup propertyGroup;
-        if (property == 0xffffffff) {
-            if (format == 0 && handle != 0 && handle != 0xffffffff) {
-                // return properties based on the object's format
-                format = getObjectFormat(handle);
+        Iterator<MtpStorageManager.MtpObject> iter = objectStream.iterator();
+        while (iter.hasNext()) {
+            MtpStorageManager.MtpObject obj = iter.next();
+            if (property == 0xffffffff) {
+                // Get all properties supported by this object
+                propertyGroup = mPropertyGroupsByFormat.get(obj.getFormat());
+                if (propertyGroup == null) {
+                    int[] propertyList = getSupportedObjectProperties(format);
+                    propertyGroup = new MtpPropertyGroup(mMediaProvider, mVolumeName,
+                            propertyList);
+                    mPropertyGroupsByFormat.put(format, propertyGroup);
+                }
+            } else {
+                // Get this property value
+                final int[] propertyList = new int[]{property};
+                propertyGroup = mPropertyGroupsByProperty.get(property);
+                if (propertyGroup == null) {
+                    propertyGroup = new MtpPropertyGroup(mMediaProvider, mVolumeName,
+                            propertyList);
+                    mPropertyGroupsByProperty.put(property, propertyGroup);
+                }
             }
-            propertyGroup = mPropertyGroupsByFormat.get(format);
-            if (propertyGroup == null) {
-                int[] propertyList = getSupportedObjectProperties(format);
-                propertyGroup = new MtpPropertyGroup(this, mMediaProvider,
-                        mVolumeName, propertyList);
-                mPropertyGroupsByFormat.put(format, propertyGroup);
-            }
-        } else {
-            propertyGroup = mPropertyGroupsByProperty.get(property);
-            if (propertyGroup == null) {
-                final int[] propertyList = new int[] { property };
-                propertyGroup = new MtpPropertyGroup(
-                        this, mMediaProvider, mVolumeName, propertyList);
-                mPropertyGroupsByProperty.put(property, propertyGroup);
+            int err = propertyGroup.getPropertyList(obj, ret);
+            if (err != MtpConstants.RESPONSE_OK) {
+                return new MtpPropertyList(err);
             }
         }
-
-        return propertyGroup.getPropertyList(handle, format, depth);
+        return ret;
     }
 
     private int renameFile(int handle, String newName) {
-        Cursor c = null;
-
-        // first compute current path
-        String path = null;
-        String[] whereArgs = new String[] {  Integer.toString(handle) };
-        try {
-            c = mMediaProvider.query(mObjectsUri, PATH_PROJECTION, ID_WHERE,
-                    whereArgs, null, null);
-            if (c != null && c.moveToNext()) {
-                path = c.getString(1);
-            }
-        } catch (RemoteException e) {
-            Log.e(TAG, "RemoteException in getObjectFilePath", e);
-            return MtpConstants.RESPONSE_GENERAL_ERROR;
-        } finally {
-            if (c != null) {
-                c.close();
-            }
-        }
-        if (path == null) {
+        MtpStorageManager.MtpObject obj = mManager.getObject(handle);
+        if (obj == null) {
             return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
         }
-
-        // do not allow renaming any of the special subdirectories
-        if (isStorageSubDirectory(path)) {
-            return MtpConstants.RESPONSE_OBJECT_WRITE_PROTECTED;
-        }
+        Path oldPath = obj.getPath();
 
         // now rename the file.  make sure this succeeds before updating database
-        File oldFile = new File(path);
-        int lastSlash = path.lastIndexOf('/');
-        if (lastSlash <= 1) {
+        if (!mManager.beginRenameObject(obj, newName))
             return MtpConstants.RESPONSE_GENERAL_ERROR;
+        Path newPath = obj.getPath();
+        boolean success = oldPath.toFile().renameTo(newPath.toFile());
+        if (!mManager.endRenameObject(obj, oldPath.getFileName().toString(), success)) {
+            Log.e(TAG, "Failed to end rename object");
         }
-        String newPath = path.substring(0, lastSlash + 1) + newName;
-        File newFile = new File(newPath);
-        boolean success = oldFile.renameTo(newFile);
         if (!success) {
-            Log.w(TAG, "renaming "+ path + " to " + newPath + " failed");
             return MtpConstants.RESPONSE_GENERAL_ERROR;
         }
 
-        // finally update database
+        // finally update MediaProvider
         ContentValues values = new ContentValues();
-        values.put(Files.FileColumns.DATA, newPath);
-        int updated = 0;
+        values.put(Files.FileColumns.DATA, newPath.toString());
+        String[] whereArgs = new String[]{oldPath.toString()};
         try {
             // note - we are relying on a special case in MediaProvider.update() to update
             // the paths for all children in the case where this is a directory.
-            updated = mMediaProvider.update(mObjectsUri, values, ID_WHERE, whereArgs);
+            mMediaProvider.update(mObjectsUri, values, PATH_WHERE, whereArgs);
         } catch (RemoteException e) {
             Log.e(TAG, "RemoteException in mMediaProvider.update", e);
         }
-        if (updated == 0) {
-            Log.e(TAG, "Unable to update path for " + path + " to " + newPath);
-            // this shouldn't happen, but if it does we need to rename the file to its original name
-            newFile.renameTo(oldFile);
-            return MtpConstants.RESPONSE_GENERAL_ERROR;
-        }
 
         // check if nomedia status changed
-        if (newFile.isDirectory()) {
+        if (obj.isDir()) {
             // for directories, check if renamed from something hidden to something non-hidden
-            if (oldFile.getName().startsWith(".") && !newPath.startsWith(".")) {
+            if (oldPath.getFileName().startsWith(".") && !newPath.startsWith(".")) {
                 // directory was unhidden
                 try {
-                    mMediaProvider.call(MediaStore.UNHIDE_CALL, newPath, null);
+                    mMediaProvider.call(MediaStore.UNHIDE_CALL, newPath.toString(), null);
                 } catch (RemoteException e) {
                     Log.e(TAG, "failed to unhide/rescan for " + newPath);
                 }
             }
         } else {
             // for files, check if renamed from .nomedia to something else
-            if (oldFile.getName().toLowerCase(Locale.US).equals(".nomedia")
-                    && !newPath.toLowerCase(Locale.US).equals(".nomedia")) {
+            if (oldPath.getFileName().toString().toLowerCase(Locale.US).equals(NO_MEDIA)
+                    && !newPath.getFileName().toString().toLowerCase(Locale.US).equals(NO_MEDIA)) {
                 try {
-                    mMediaProvider.call(MediaStore.UNHIDE_CALL, oldFile.getParent(), null);
+                    mMediaProvider.call(MediaStore.UNHIDE_CALL,
+                            oldPath.getParent().toString(), null);
                 } catch (RemoteException e) {
                     Log.e(TAG, "failed to unhide/rescan for " + newPath);
                 }
             }
         }
-
         return MtpConstants.RESPONSE_OK;
     }
 
-    private int moveObject(int handle, int newParent, int newStorage, String newPath) {
-        String[] whereArgs = new String[] {  Integer.toString(handle) };
+    private int beginMoveObject(int handle, int newParent, int newStorage) {
+        MtpStorageManager.MtpObject obj = mManager.getObject(handle);
+        MtpStorageManager.MtpObject parent = newParent == 0 ?
+                mManager.getStorageRoot(newStorage) : mManager.getObject(newParent);
+        if (obj == null || parent == null)
+            return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
 
-        // do not allow renaming any of the special subdirectories
-        if (isStorageSubDirectory(newPath)) {
-            return MtpConstants.RESPONSE_OBJECT_WRITE_PROTECTED;
+        boolean allowed = mManager.beginMoveObject(obj, parent);
+        return allowed ? MtpConstants.RESPONSE_OK : MtpConstants.RESPONSE_GENERAL_ERROR;
+    }
+
+    private void endMoveObject(int oldParent, int newParent, int oldStorage, int newStorage,
+            int objId, boolean success) {
+        MtpStorageManager.MtpObject oldParentObj = oldParent == 0 ?
+                mManager.getStorageRoot(oldStorage) : mManager.getObject(oldParent);
+        MtpStorageManager.MtpObject newParentObj = newParent == 0 ?
+                mManager.getStorageRoot(newStorage) : mManager.getObject(newParent);
+        MtpStorageManager.MtpObject obj = mManager.getObject(objId);
+        String name = obj.getName();
+        if (newParentObj == null || oldParentObj == null
+                ||!mManager.endMoveObject(oldParentObj, newParentObj, name, success)) {
+            Log.e(TAG, "Failed to end move object");
+            return;
         }
 
-        // update database
+        obj = mManager.getObject(objId);
+        if (!success || obj == null)
+            return;
+        // Get parent info from MediaProvider, since the id is different from MTP's
         ContentValues values = new ContentValues();
-        values.put(Files.FileColumns.DATA, newPath);
-        values.put(Files.FileColumns.PARENT, newParent);
-        values.put(Files.FileColumns.STORAGE_ID, newStorage);
-        int updated = 0;
+        Path path = newParentObj.getPath().resolve(name);
+        Path oldPath = oldParentObj.getPath().resolve(name);
+        values.put(Files.FileColumns.DATA, path.toString());
+        if (obj.getParent().isRoot()) {
+            values.put(Files.FileColumns.PARENT, 0);
+        } else {
+            int parentId = findInMedia(path.getParent());
+            if (parentId != -1) {
+                values.put(Files.FileColumns.PARENT, parentId);
+            } else {
+                // The new parent isn't in MediaProvider, so delete the object instead
+                deleteFromMedia(oldPath, obj.isDir());
+                return;
+            }
+        }
+        // update MediaProvider
+        Cursor c = null;
+        String[] whereArgs = new String[]{oldPath.toString()};
         try {
-            // note - we are relying on a special case in MediaProvider.update() to update
-            // the paths for all children in the case where this is a directory.
-            updated = mMediaProvider.update(mObjectsUri, values, ID_WHERE, whereArgs);
+            int parentId = -1;
+            if (!oldParentObj.isRoot()) {
+                parentId = findInMedia(oldPath.getParent());
+            }
+            if (oldParentObj.isRoot() || parentId != -1) {
+                // Old parent exists in MediaProvider - perform a move
+                // note - we are relying on a special case in MediaProvider.update() to update
+                // the paths for all children in the case where this is a directory.
+                mMediaProvider.update(mObjectsUri, values, PATH_WHERE, whereArgs);
+            } else {
+                // Old parent doesn't exist - add the object
+                values.put(Files.FileColumns.FORMAT, obj.getFormat());
+                values.put(Files.FileColumns.SIZE, obj.getSize());
+                values.put(Files.FileColumns.DATE_MODIFIED, obj.getModifiedTime());
+                Uri uri = mMediaProvider.insert(mObjectsUri, values);
+                if (uri != null) {
+                    rescanFile(path.toString(),
+                            Integer.parseInt(uri.getPathSegments().get(2)), obj.getFormat());
+                }
+            }
         } catch (RemoteException e) {
             Log.e(TAG, "RemoteException in mMediaProvider.update", e);
         }
-        if (updated == 0) {
-            Log.e(TAG, "Unable to update path for " + handle + " to " + newPath);
-            return MtpConstants.RESPONSE_GENERAL_ERROR;
+    }
+
+    private int beginCopyObject(int handle, int newParent, int newStorage) {
+        MtpStorageManager.MtpObject obj = mManager.getObject(handle);
+        MtpStorageManager.MtpObject parent = newParent == 0 ?
+                mManager.getStorageRoot(newStorage) : mManager.getObject(newParent);
+        if (obj == null || parent == null)
+            return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
+        return mManager.beginCopyObject(obj, parent);
+    }
+
+    private void endCopyObject(int handle, boolean success) {
+        MtpStorageManager.MtpObject obj = mManager.getObject(handle);
+        if (obj == null || !mManager.endCopyObject(obj, success)) {
+            Log.e(TAG, "Failed to end copy object");
+            return;
         }
-        return MtpConstants.RESPONSE_OK;
+        if (!success) {
+            return;
+        }
+        String path = obj.getPath().toString();
+        int format = obj.getFormat();
+        // Get parent info from MediaProvider, since the id is different from MTP's
+        ContentValues values = new ContentValues();
+        values.put(Files.FileColumns.DATA, path);
+        values.put(Files.FileColumns.FORMAT, format);
+        values.put(Files.FileColumns.SIZE, obj.getSize());
+        values.put(Files.FileColumns.DATE_MODIFIED, obj.getModifiedTime());
+        try {
+            if (obj.getParent().isRoot()) {
+                values.put(Files.FileColumns.PARENT, 0);
+            } else {
+                int parentId = findInMedia(obj.getParent().getPath());
+                if (parentId != -1) {
+                    values.put(Files.FileColumns.PARENT, parentId);
+                } else {
+                    // The parent isn't in MediaProvider. Don't add the new file.
+                    return;
+                }
+            }
+            if (obj.isDir()) {
+                mMediaScanner.scanDirectories(new String[]{path});
+            } else {
+                Uri uri = mMediaProvider.insert(mObjectsUri, values);
+                if (uri != null) {
+                    rescanFile(path, Integer.parseInt(uri.getPathSegments().get(2)), format);
+                }
+            }
+        } catch (RemoteException e) {
+            Log.e(TAG, "RemoteException in beginSendObject", e);
+        }
     }
 
     private int setObjectProperty(int handle, int property,
-                            long intValue, String stringValue) {
+            long intValue, String stringValue) {
         switch (property) {
             case MtpConstants.PROPERTY_OBJECT_FILE_NAME:
                 return renameFile(handle, stringValue);
@@ -912,24 +748,23 @@
                 value.getChars(0, length, outStringValue, 0);
                 outStringValue[length] = 0;
                 return MtpConstants.RESPONSE_OK;
-
             case MtpConstants.DEVICE_PROPERTY_IMAGE_SIZE:
                 // use screen size as max image size
-                Display display = ((WindowManager)mContext.getSystemService(
+                Display display = ((WindowManager) mContext.getSystemService(
                         Context.WINDOW_SERVICE)).getDefaultDisplay();
                 int width = display.getMaximumSizeDimension();
                 int height = display.getMaximumSizeDimension();
-                String imageSize = Integer.toString(width) + "x" +  Integer.toString(height);
+                String imageSize = Integer.toString(width) + "x" + Integer.toString(height);
                 imageSize.getChars(0, imageSize.length(), outStringValue, 0);
                 outStringValue[imageSize.length()] = 0;
                 return MtpConstants.RESPONSE_OK;
-
             case MtpConstants.DEVICE_PROPERTY_PERCEIVED_DEVICE_TYPE:
                 outIntValue[0] = mDeviceType;
                 return MtpConstants.RESPONSE_OK;
-
-            // DEVICE_PROPERTY_BATTERY_LEVEL is implemented in the JNI code
-
+            case MtpConstants.DEVICE_PROPERTY_BATTERY_LEVEL:
+                outIntValue[0] = mBatteryLevel;
+                outIntValue[1] = mBatteryScale;
+                return MtpConstants.RESPONSE_OK;
             default:
                 return MtpConstants.RESPONSE_DEVICE_PROP_NOT_SUPPORTED;
         }
@@ -950,179 +785,144 @@
     }
 
     private boolean getObjectInfo(int handle, int[] outStorageFormatParent,
-                        char[] outName, long[] outCreatedModified) {
-        Cursor c = null;
-        try {
-            c = mMediaProvider.query(mObjectsUri, OBJECT_INFO_PROJECTION,
-                            ID_WHERE, new String[] {  Integer.toString(handle) }, null, null);
-            if (c != null && c.moveToNext()) {
-                outStorageFormatParent[0] = c.getInt(1);
-                outStorageFormatParent[1] = c.getInt(2);
-                outStorageFormatParent[2] = c.getInt(3);
-
-                // extract name from path
-                String path = c.getString(4);
-                int lastSlash = path.lastIndexOf('/');
-                int start = (lastSlash >= 0 ? lastSlash + 1 : 0);
-                int end = path.length();
-                if (end - start > 255) {
-                    end = start + 255;
-                }
-                path.getChars(start, end, outName, 0);
-                outName[end - start] = 0;
-
-                outCreatedModified[0] = c.getLong(5);
-                outCreatedModified[1] = c.getLong(6);
-                // use modification date as creation date if date added is not set
-                if (outCreatedModified[0] == 0) {
-                    outCreatedModified[0] = outCreatedModified[1];
-                }
-                return true;
-            }
-        } catch (RemoteException e) {
-            Log.e(TAG, "RemoteException in getObjectInfo", e);
-        } finally {
-            if (c != null) {
-                c.close();
-            }
+            char[] outName, long[] outCreatedModified) {
+        MtpStorageManager.MtpObject obj = mManager.getObject(handle);
+        if (obj == null) {
+            return false;
         }
-        return false;
+        outStorageFormatParent[0] = obj.getStorageId();
+        outStorageFormatParent[1] = obj.getFormat();
+        outStorageFormatParent[2] = obj.getParent().isRoot() ? 0 : obj.getParent().getId();
+
+        int nameLen = Integer.min(obj.getName().length(), 255);
+        obj.getName().getChars(0, nameLen, outName, 0);
+        outName[nameLen] = 0;
+
+        outCreatedModified[0] = obj.getModifiedTime();
+        outCreatedModified[1] = obj.getModifiedTime();
+        return true;
     }
 
     private int getObjectFilePath(int handle, char[] outFilePath, long[] outFileLengthFormat) {
-        if (handle == 0) {
-            // special case root directory
-            mMediaStoragePath.getChars(0, mMediaStoragePath.length(), outFilePath, 0);
-            outFilePath[mMediaStoragePath.length()] = 0;
-            outFileLengthFormat[0] = 0;
-            outFileLengthFormat[1] = MtpConstants.FORMAT_ASSOCIATION;
-            return MtpConstants.RESPONSE_OK;
+        MtpStorageManager.MtpObject obj = mManager.getObject(handle);
+        if (obj == null) {
+            return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
         }
-        Cursor c = null;
-        try {
-            c = mMediaProvider.query(mObjectsUri, PATH_FORMAT_PROJECTION,
-                            ID_WHERE, new String[] {  Integer.toString(handle) }, null, null);
-            if (c != null && c.moveToNext()) {
-                String path = c.getString(1);
-                path.getChars(0, path.length(), outFilePath, 0);
-                outFilePath[path.length()] = 0;
-                // File transfers from device to host will likely fail if the size is incorrect.
-                // So to be safe, use the actual file size here.
-                outFileLengthFormat[0] = new File(path).length();
-                outFileLengthFormat[1] = c.getLong(2);
-                return MtpConstants.RESPONSE_OK;
-            } else {
-                return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
-            }
-        } catch (RemoteException e) {
-            Log.e(TAG, "RemoteException in getObjectFilePath", e);
-            return MtpConstants.RESPONSE_GENERAL_ERROR;
-        } finally {
-            if (c != null) {
-                c.close();
-            }
-        }
+
+        String path = obj.getPath().toString();
+        int pathLen = Integer.min(path.length(), 4096);
+        path.getChars(0, pathLen, outFilePath, 0);
+        outFilePath[pathLen] = 0;
+
+        outFileLengthFormat[0] = obj.getSize();
+        outFileLengthFormat[1] = obj.getFormat();
+        return MtpConstants.RESPONSE_OK;
     }
 
     private int getObjectFormat(int handle) {
-        Cursor c = null;
-        try {
-            c = mMediaProvider.query(mObjectsUri, FORMAT_PROJECTION,
-                            ID_WHERE, new String[] { Integer.toString(handle) }, null, null);
-            if (c != null && c.moveToNext()) {
-                return c.getInt(1);
-            } else {
-                return -1;
-            }
-        } catch (RemoteException e) {
-            Log.e(TAG, "RemoteException in getObjectFilePath", e);
+        MtpStorageManager.MtpObject obj = mManager.getObject(handle);
+        if (obj == null) {
             return -1;
-        } finally {
-            if (c != null) {
-                c.close();
-            }
         }
+        return obj.getFormat();
     }
 
-    private int deleteFile(int handle) {
-        mDatabaseModified = true;
-        String path = null;
-        int format = 0;
+    private int beginDeleteObject(int handle) {
+        MtpStorageManager.MtpObject obj = mManager.getObject(handle);
+        if (obj == null) {
+            return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
+        }
+        if (!mManager.beginRemoveObject(obj)) {
+            return MtpConstants.RESPONSE_GENERAL_ERROR;
+        }
+        return MtpConstants.RESPONSE_OK;
+    }
 
+    private void endDeleteObject(int handle, boolean success) {
+        MtpStorageManager.MtpObject obj = mManager.getObject(handle);
+        if (obj == null) {
+            return;
+        }
+        if (!mManager.endRemoveObject(obj, success))
+            Log.e(TAG, "Failed to end remove object");
+        if (success)
+            deleteFromMedia(obj.getPath(), obj.isDir());
+    }
+
+    private int findInMedia(Path path) {
+        int ret = -1;
         Cursor c = null;
         try {
-            c = mMediaProvider.query(mObjectsUri, PATH_FORMAT_PROJECTION,
-                            ID_WHERE, new String[] {  Integer.toString(handle) }, null, null);
+            c = mMediaProvider.query(mObjectsUri, ID_PROJECTION, PATH_WHERE,
+                    new String[]{path.toString()}, null, null);
             if (c != null && c.moveToNext()) {
-                // don't convert to media path here, since we will be matching
-                // against paths in the database matching /data/media
-                path = c.getString(1);
-                format = c.getInt(2);
-            } else {
-                return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
+                ret = c.getInt(0);
             }
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error finding " + path + " in MediaProvider");
+        } finally {
+            if (c != null)
+                c.close();
+        }
+        return ret;
+    }
 
-            if (path == null || format == 0) {
-                return MtpConstants.RESPONSE_GENERAL_ERROR;
-            }
-
-            // do not allow deleting any of the special subdirectories
-            if (isStorageSubDirectory(path)) {
-                return MtpConstants.RESPONSE_OBJECT_WRITE_PROTECTED;
-            }
-
-            if (format == MtpConstants.FORMAT_ASSOCIATION) {
+    private void deleteFromMedia(Path path, boolean isDir) {
+        try {
+            // Delete the object(s) from MediaProvider, but ignore errors.
+            if (isDir) {
                 // recursive case - delete all children first
-                Uri uri = Files.getMtpObjectsUri(mVolumeName);
-                int count = mMediaProvider.delete(uri,
-                    // the 'like' makes it use the index, the 'lower()' makes it correct
-                    // when the path contains sqlite wildcard characters
-                    "_data LIKE ?1 AND lower(substr(_data,1,?2))=lower(?3)",
-                    new String[] { path + "/%",Integer.toString(path.length() + 1), path + "/"});
+                mMediaProvider.delete(mObjectsUri,
+                        // the 'like' makes it use the index, the 'lower()' makes it correct
+                        // when the path contains sqlite wildcard characters
+                        "_data LIKE ?1 AND lower(substr(_data,1,?2))=lower(?3)",
+                        new String[]{path + "/%", Integer.toString(path.toString().length() + 1),
+                                path.toString() + "/"});
             }
 
-            Uri uri = Files.getMtpObjectsUri(mVolumeName, handle);
-            if (mMediaProvider.delete(uri, null, null) > 0) {
-                if (format != MtpConstants.FORMAT_ASSOCIATION
-                        && path.toLowerCase(Locale.US).endsWith("/.nomedia")) {
+            String[] whereArgs = new String[]{path.toString()};
+            if (mMediaProvider.delete(mObjectsUri, PATH_WHERE, whereArgs) > 0) {
+                if (!isDir && path.toString().toLowerCase(Locale.US).endsWith(NO_MEDIA)) {
                     try {
-                        String parentPath = path.substring(0, path.lastIndexOf("/"));
+                        String parentPath = path.getParent().toString();
                         mMediaProvider.call(MediaStore.UNHIDE_CALL, parentPath, null);
                     } catch (RemoteException e) {
                         Log.e(TAG, "failed to unhide/rescan for " + path);
                     }
                 }
-                return MtpConstants.RESPONSE_OK;
             } else {
-                return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
+                Log.i(TAG, "Mediaprovider didn't delete " + path);
             }
-        } catch (RemoteException e) {
-            Log.e(TAG, "RemoteException in deleteFile", e);
-            return MtpConstants.RESPONSE_GENERAL_ERROR;
-        } finally {
-            if (c != null) {
-                c.close();
-            }
+        } catch (Exception e) {
+            Log.d(TAG, "Failed to delete " + path + " from MediaProvider");
         }
     }
 
     private int[] getObjectReferences(int handle) {
+        MtpStorageManager.MtpObject obj = mManager.getObject(handle);
+        if (obj == null)
+            return null;
+        // Translate this handle to the MediaProvider Handle
+        handle = findInMedia(obj.getPath());
+        if (handle == -1)
+            return null;
         Uri uri = Files.getMtpReferencesUri(mVolumeName, handle);
         Cursor c = null;
         try {
-            c = mMediaProvider.query(uri, ID_PROJECTION, null, null, null, null);
+            c = mMediaProvider.query(uri, PATH_PROJECTION, null, null, null, null);
             if (c == null) {
                 return null;
             }
-            int count = c.getCount();
-            if (count > 0) {
-                int[] result = new int[count];
-                for (int i = 0; i < count; i++) {
-                    c.moveToNext();
-                    result[i] = c.getInt(0);
+                ArrayList<Integer> result = new ArrayList<>();
+                while (c.moveToNext()) {
+                    // Translate result handles back into handles for this session.
+                    String refPath = c.getString(0);
+                    MtpStorageManager.MtpObject refObj = mManager.getByPath(refPath);
+                    if (refObj != null) {
+                        result.add(refObj.getId());
+                    }
                 }
-                return result;
-            }
+                return result.stream().mapToInt(Integer::intValue).toArray();
         } catch (RemoteException e) {
             Log.e(TAG, "RemoteException in getObjectList", e);
         } finally {
@@ -1134,17 +934,29 @@
     }
 
     private int setObjectReferences(int handle, int[] references) {
-        mDatabaseModified = true;
+        MtpStorageManager.MtpObject obj = mManager.getObject(handle);
+        if (obj == null)
+            return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
+        // Translate this handle to the MediaProvider Handle
+        handle = findInMedia(obj.getPath());
+        if (handle == -1)
+            return MtpConstants.RESPONSE_GENERAL_ERROR;
         Uri uri = Files.getMtpReferencesUri(mVolumeName, handle);
-        int count = references.length;
-        ContentValues[] valuesList = new ContentValues[count];
-        for (int i = 0; i < count; i++) {
+        ArrayList<ContentValues> valuesList = new ArrayList<>();
+        for (int id : references) {
+            // Translate each reference id to the MediaProvider Id
+            MtpStorageManager.MtpObject refObj = mManager.getObject(id);
+            if (refObj == null)
+                continue;
+            int refHandle = findInMedia(refObj.getPath());
+            if (refHandle == -1)
+                continue;
             ContentValues values = new ContentValues();
-            values.put(Files.FileColumns._ID, references[i]);
-            valuesList[i] = values;
+            values.put(Files.FileColumns._ID, refHandle);
+            valuesList.add(values);
         }
         try {
-            if (mMediaProvider.bulkInsert(uri, valuesList) > 0) {
+            if (mMediaProvider.bulkInsert(uri, valuesList.toArray(new ContentValues[0])) > 0) {
                 return MtpConstants.RESPONSE_OK;
             }
         } catch (RemoteException e) {
@@ -1153,17 +965,6 @@
         return MtpConstants.RESPONSE_GENERAL_ERROR;
     }
 
-    private void sessionStarted() {
-        mDatabaseModified = false;
-    }
-
-    private void sessionEnded() {
-        if (mDatabaseModified) {
-            mUserContext.sendBroadcast(new Intent(MediaStore.ACTION_MTP_SESSION_END));
-            mDatabaseModified = false;
-        }
-    }
-
     // used by the JNI code
     private long mNativeContext;
 
diff --git a/media/java/android/mtp/MtpPropertyGroup.java b/media/java/android/mtp/MtpPropertyGroup.java
index dea3008..77d0f34f 100644
--- a/media/java/android/mtp/MtpPropertyGroup.java
+++ b/media/java/android/mtp/MtpPropertyGroup.java
@@ -23,22 +23,21 @@
 import android.provider.MediaStore.Audio;
 import android.provider.MediaStore.Files;
 import android.provider.MediaStore.Images;
-import android.provider.MediaStore.MediaColumns;
 import android.util.Log;
 
 import java.util.ArrayList;
 
+/**
+ * MtpPropertyGroup represents a list of MTP properties.
+ * {@hide}
+ */
 class MtpPropertyGroup {
-
-    private static final String TAG = "MtpPropertyGroup";
+    private static final String TAG = MtpPropertyGroup.class.getSimpleName();
 
     private class Property {
-        // MTP property code
-        int     code;
-        // MTP data type
-        int     type;
-        // column index for our query
-        int     column;
+        int code;
+        int type;
+        int column;
 
         Property(int code, int type, int column) {
             this.code = code;
@@ -47,32 +46,26 @@
         }
     }
 
-    private final MtpDatabase mDatabase;
     private final ContentProviderClient mProvider;
     private final String mVolumeName;
     private final Uri mUri;
 
     // list of all properties in this group
-    private final Property[]    mProperties;
+    private final Property[] mProperties;
 
     // list of columns for database query
-    private String[]             mColumns;
+    private String[] mColumns;
 
-    private static final String ID_WHERE = Files.FileColumns._ID + "=?";
-    private static final String FORMAT_WHERE = Files.FileColumns.FORMAT + "=?";
-    private static final String ID_FORMAT_WHERE = ID_WHERE + " AND " + FORMAT_WHERE;
-    private static final String PARENT_WHERE = Files.FileColumns.PARENT + "=?";
-    private static final String PARENT_FORMAT_WHERE = PARENT_WHERE + " AND " + FORMAT_WHERE;
+    private static final String PATH_WHERE = Files.FileColumns.DATA + "=?";
+
     // constructs a property group for a list of properties
-    public MtpPropertyGroup(MtpDatabase database, ContentProviderClient provider, String volumeName,
-            int[] properties) {
-        mDatabase = database;
+    public MtpPropertyGroup(ContentProviderClient provider, String volumeName, int[] properties) {
         mProvider = provider;
         mVolumeName = volumeName;
         mUri = Files.getMtpObjectsUri(volumeName);
 
         int count = properties.length;
-        ArrayList<String> columns = new ArrayList<String>(count);
+        ArrayList<String> columns = new ArrayList<>(count);
         columns.add(Files.FileColumns._ID);
 
         mProperties = new Property[count];
@@ -90,37 +83,29 @@
         String column = null;
         int type;
 
-         switch (code) {
+        switch (code) {
             case MtpConstants.PROPERTY_STORAGE_ID:
-                column = Files.FileColumns.STORAGE_ID;
                 type = MtpConstants.TYPE_UINT32;
                 break;
-             case MtpConstants.PROPERTY_OBJECT_FORMAT:
-                column = Files.FileColumns.FORMAT;
+            case MtpConstants.PROPERTY_OBJECT_FORMAT:
                 type = MtpConstants.TYPE_UINT16;
                 break;
             case MtpConstants.PROPERTY_PROTECTION_STATUS:
-                // protection status is always 0
                 type = MtpConstants.TYPE_UINT16;
                 break;
             case MtpConstants.PROPERTY_OBJECT_SIZE:
-                column = Files.FileColumns.SIZE;
                 type = MtpConstants.TYPE_UINT64;
                 break;
             case MtpConstants.PROPERTY_OBJECT_FILE_NAME:
-                column = Files.FileColumns.DATA;
                 type = MtpConstants.TYPE_STR;
                 break;
             case MtpConstants.PROPERTY_NAME:
-                column = MediaColumns.TITLE;
                 type = MtpConstants.TYPE_STR;
                 break;
             case MtpConstants.PROPERTY_DATE_MODIFIED:
-                column = Files.FileColumns.DATE_MODIFIED;
                 type = MtpConstants.TYPE_STR;
                 break;
             case MtpConstants.PROPERTY_DATE_ADDED:
-                column = Files.FileColumns.DATE_ADDED;
                 type = MtpConstants.TYPE_STR;
                 break;
             case MtpConstants.PROPERTY_ORIGINAL_RELEASE_DATE:
@@ -128,12 +113,9 @@
                 type = MtpConstants.TYPE_STR;
                 break;
             case MtpConstants.PROPERTY_PARENT_OBJECT:
-                column = Files.FileColumns.PARENT;
                 type = MtpConstants.TYPE_UINT32;
                 break;
             case MtpConstants.PROPERTY_PERSISTENT_UID:
-                // PUID is concatenation of storageID and object handle
-                column = Files.FileColumns.STORAGE_ID;
                 type = MtpConstants.TYPE_UINT128;
                 break;
             case MtpConstants.PROPERTY_DURATION:
@@ -145,7 +127,6 @@
                 type = MtpConstants.TYPE_UINT16;
                 break;
             case MtpConstants.PROPERTY_DISPLAY_NAME:
-                column = MediaColumns.DISPLAY_NAME;
                 type = MtpConstants.TYPE_STR;
                 break;
             case MtpConstants.PROPERTY_ARTIST:
@@ -195,40 +176,19 @@
         }
     }
 
-   private String queryString(int id, String column) {
-        Cursor c = null;
-        try {
-            // for now we are only reading properties from the "objects" table
-            c = mProvider.query(mUri,
-                            new String [] { Files.FileColumns._ID, column },
-                            ID_WHERE, new String[] { Integer.toString(id) }, null, null);
-            if (c != null && c.moveToNext()) {
-                return c.getString(1);
-            } else {
-                return "";
-            }
-        } catch (Exception e) {
-            return null;
-        } finally {
-            if (c != null) {
-                c.close();
-            }
-        }
-    }
-
-    private String queryAudio(int id, String column) {
+    private String queryAudio(String path, String column) {
         Cursor c = null;
         try {
             c = mProvider.query(Audio.Media.getContentUri(mVolumeName),
-                            new String [] { Files.FileColumns._ID, column },
-                            ID_WHERE, new String[] { Integer.toString(id) }, null, null);
+                            new String [] { column },
+                            PATH_WHERE, new String[] {path}, null, null);
             if (c != null && c.moveToNext()) {
-                return c.getString(1);
+                return c.getString(0);
             } else {
                 return "";
             }
         } catch (Exception e) {
-            return null;
+            return "";
         } finally {
             if (c != null) {
                 c.close();
@@ -236,21 +196,19 @@
         }
     }
 
-    private String queryGenre(int id) {
+    private String queryGenre(String path) {
         Cursor c = null;
         try {
-            Uri uri = Audio.Genres.getContentUriForAudioId(mVolumeName, id);
-            c = mProvider.query(uri,
-                            new String [] { Files.FileColumns._ID, Audio.GenresColumns.NAME },
-                            null, null, null, null);
+            c = mProvider.query(Audio.Genres.getContentUri(mVolumeName),
+                            new String [] { Audio.GenresColumns.NAME },
+                            PATH_WHERE, new String[] {path}, null, null);
             if (c != null && c.moveToNext()) {
-                return c.getString(1);
+                return c.getString(0);
             } else {
                 return "";
             }
         } catch (Exception e) {
-            Log.e(TAG, "queryGenre exception", e);
-            return null;
+            return "";
         } finally {
             if (c != null) {
                 c.close();
@@ -258,211 +216,127 @@
         }
     }
 
-    private Long queryLong(int id, String column) {
+    /**
+     * Gets the values of the properties represented by this property group for the given
+     * object and adds them to the given property list.
+     * @return Response_OK if the operation succeeded.
+     */
+    public int getPropertyList(MtpStorageManager.MtpObject object, MtpPropertyList list) {
         Cursor c = null;
-        try {
-            // for now we are only reading properties from the "objects" table
-            c = mProvider.query(mUri,
-                            new String [] { Files.FileColumns._ID, column },
-                            ID_WHERE, new String[] { Integer.toString(id) }, null, null);
-            if (c != null && c.moveToNext()) {
-                return new Long(c.getLong(1));
-            }
-        } catch (Exception e) {
-        } finally {
-            if (c != null) {
-                c.close();
-            }
-        }
-        return null;
-    }
-
-    private static String nameFromPath(String path) {
-        // extract name from full path
-        int start = 0;
-        int lastSlash = path.lastIndexOf('/');
-        if (lastSlash >= 0) {
-            start = lastSlash + 1;
-        }
-        int end = path.length();
-        if (end - start > 255) {
-            end = start + 255;
-        }
-        return path.substring(start, end);
-    }
-
-    MtpPropertyList getPropertyList(int handle, int format, int depth) {
-        //Log.d(TAG, "getPropertyList handle: " + handle + " format: " + format + " depth: " + depth);
-        if (depth > 1) {
-            // we only support depth 0 and 1
-            // depth 0: single object, depth 1: immediate children
-            return new MtpPropertyList(0, MtpConstants.RESPONSE_SPECIFICATION_BY_DEPTH_UNSUPPORTED);
-        }
-
-        String where;
-        String[] whereArgs;
-        if (format == 0) {
-            if (handle == 0xFFFFFFFF) {
-                // select all objects
-                where = null;
-                whereArgs = null;
-            } else {
-                whereArgs = new String[] { Integer.toString(handle) };
-                if (depth == 1) {
-                    where = PARENT_WHERE;
-                } else {
-                    where = ID_WHERE;
+        int id = object.getId();
+        String path = object.getPath().toString();
+        for (Property property : mProperties) {
+            if (property.column != -1 && c == null) {
+                try {
+                    // Look up the entry in MediaProvider only if one of those properties is needed.
+                    c = mProvider.query(mUri, mColumns,
+                            PATH_WHERE, new String[] {path}, null, null);
+                    if (c != null && !c.moveToNext()) {
+                        c.close();
+                        c = null;
+                    }
+                } catch (RemoteException e) {
+                    Log.e(TAG, "Mediaprovider lookup failed");
                 }
             }
-        } else {
-            if (handle == 0xFFFFFFFF) {
-                // select all objects with given format
-                where = FORMAT_WHERE;
-                whereArgs = new String[] { Integer.toString(format) };
-            } else {
-                whereArgs = new String[] { Integer.toString(handle), Integer.toString(format) };
-                if (depth == 1) {
-                    where = PARENT_FORMAT_WHERE;
-                } else {
-                    where = ID_FORMAT_WHERE;
-                }
-            }
-        }
-
-        Cursor c = null;
-        try {
-            // don't query if not necessary
-            if (depth > 0 || handle == 0xFFFFFFFF || mColumns.length > 1) {
-                c = mProvider.query(mUri, mColumns, where, whereArgs, null, null);
-                if (c == null) {
-                    return new MtpPropertyList(0, MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE);
-                }
-            }
-
-            int count = (c == null ? 1 : c.getCount());
-            MtpPropertyList result = new MtpPropertyList(count * mProperties.length,
-                    MtpConstants.RESPONSE_OK);
-
-            // iterate over all objects in the query
-            for (int objectIndex = 0; objectIndex < count; objectIndex++) {
-                if (c != null) {
-                    c.moveToNext();
-                    handle = (int)c.getLong(0);
-                }
-
-                // iterate over all properties in the query for the given object
-                for (int propertyIndex = 0; propertyIndex < mProperties.length; propertyIndex++) {
-                    Property property = mProperties[propertyIndex];
-                    int propertyCode = property.code;
-                    int column = property.column;
-
-                    // handle some special cases
-                    switch (propertyCode) {
-                        case MtpConstants.PROPERTY_PROTECTION_STATUS:
-                            // protection status is always 0
-                            result.append(handle, propertyCode, MtpConstants.TYPE_UINT16, 0);
+            switch (property.code) {
+                case MtpConstants.PROPERTY_PROTECTION_STATUS:
+                    // protection status is always 0
+                    list.append(id, property.code, property.type, 0);
+                    break;
+                case MtpConstants.PROPERTY_NAME:
+                case MtpConstants.PROPERTY_OBJECT_FILE_NAME:
+                case MtpConstants.PROPERTY_DISPLAY_NAME:
+                    list.append(id, property.code, object.getName());
+                    break;
+                case MtpConstants.PROPERTY_DATE_MODIFIED:
+                case MtpConstants.PROPERTY_DATE_ADDED:
+                    // convert from seconds to DateTime
+                    list.append(id, property.code,
+                            format_date_time(object.getModifiedTime()));
+                    break;
+                case MtpConstants.PROPERTY_STORAGE_ID:
+                    list.append(id, property.code, property.type, object.getStorageId());
+                    break;
+                case MtpConstants.PROPERTY_OBJECT_FORMAT:
+                    list.append(id, property.code, property.type, object.getFormat());
+                    break;
+                case MtpConstants.PROPERTY_OBJECT_SIZE:
+                    list.append(id, property.code, property.type, object.getSize());
+                    break;
+                case MtpConstants.PROPERTY_PARENT_OBJECT:
+                    list.append(id, property.code, property.type,
+                            object.getParent().isRoot() ? 0 : object.getParent().getId());
+                    break;
+                case MtpConstants.PROPERTY_PERSISTENT_UID:
+                    // The persistent uid must be unique and never reused among all objects,
+                    // and remain the same between sessions.
+                    long puid = (object.getPath().toString().hashCode() << 32)
+                            + object.getModifiedTime();
+                    list.append(id, property.code, property.type, puid);
+                    break;
+                case MtpConstants.PROPERTY_ORIGINAL_RELEASE_DATE:
+                    // release date is stored internally as just the year
+                    int year = 0;
+                    if (c != null)
+                        year = c.getInt(property.column);
+                    String dateTime = Integer.toString(year) + "0101T000000";
+                    list.append(id, property.code, dateTime);
+                    break;
+                case MtpConstants.PROPERTY_TRACK:
+                    int track = 0;
+                    if (c != null)
+                        track = c.getInt(property.column);
+                    list.append(id, property.code, MtpConstants.TYPE_UINT16,
+                            track % 1000);
+                    break;
+                case MtpConstants.PROPERTY_ARTIST:
+                    list.append(id, property.code,
+                            queryAudio(path, Audio.AudioColumns.ARTIST));
+                    break;
+                case MtpConstants.PROPERTY_ALBUM_NAME:
+                    list.append(id, property.code,
+                            queryAudio(path, Audio.AudioColumns.ALBUM));
+                    break;
+                case MtpConstants.PROPERTY_GENRE:
+                    String genre = queryGenre(path);
+                    if (genre != null) {
+                        list.append(id, property.code, genre);
+                    }
+                    break;
+                case MtpConstants.PROPERTY_AUDIO_WAVE_CODEC:
+                case MtpConstants.PROPERTY_AUDIO_BITRATE:
+                case MtpConstants.PROPERTY_SAMPLE_RATE:
+                    // we don't have these in our database, so return 0
+                    list.append(id, property.code, MtpConstants.TYPE_UINT32, 0);
+                    break;
+                case MtpConstants.PROPERTY_BITRATE_TYPE:
+                case MtpConstants.PROPERTY_NUMBER_OF_CHANNELS:
+                    // we don't have these in our database, so return 0
+                    list.append(id, property.code, MtpConstants.TYPE_UINT16, 0);
+                    break;
+                default:
+                    switch(property.type) {
+                        case MtpConstants.TYPE_UNDEFINED:
+                            list.append(id, property.code, property.type, 0);
                             break;
-                        case MtpConstants.PROPERTY_OBJECT_FILE_NAME:
-                            // special case - need to extract file name from full path
-                            String value = c.getString(column);
-                            if (value != null) {
-                                result.append(handle, propertyCode, nameFromPath(value));
-                            } else {
-                                result.setResult(MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE);
-                            }
-                            break;
-                        case MtpConstants.PROPERTY_NAME:
-                            // first try title
-                            String name = c.getString(column);
-                            // then try name
-                            if (name == null) {
-                                name = queryString(handle, Audio.PlaylistsColumns.NAME);
-                            }
-                            // if title and name fail, extract name from full path
-                            if (name == null) {
-                                name = queryString(handle, Files.FileColumns.DATA);
-                                if (name != null) {
-                                    name = nameFromPath(name);
-                                }
-                            }
-                            if (name != null) {
-                                result.append(handle, propertyCode, name);
-                            } else {
-                                result.setResult(MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE);
-                            }
-                            break;
-                        case MtpConstants.PROPERTY_DATE_MODIFIED:
-                        case MtpConstants.PROPERTY_DATE_ADDED:
-                            // convert from seconds to DateTime
-                            result.append(handle, propertyCode, format_date_time(c.getInt(column)));
-                            break;
-                        case MtpConstants.PROPERTY_ORIGINAL_RELEASE_DATE:
-                            // release date is stored internally as just the year
-                            int year = c.getInt(column);
-                            String dateTime = Integer.toString(year) + "0101T000000";
-                            result.append(handle, propertyCode, dateTime);
-                            break;
-                        case MtpConstants.PROPERTY_PERSISTENT_UID:
-                            // PUID is concatenation of storageID and object handle
-                            long puid = c.getLong(column);
-                            puid <<= 32;
-                            puid += handle;
-                            result.append(handle, propertyCode, MtpConstants.TYPE_UINT128, puid);
-                            break;
-                        case MtpConstants.PROPERTY_TRACK:
-                            result.append(handle, propertyCode, MtpConstants.TYPE_UINT16,
-                                        c.getInt(column) % 1000);
-                            break;
-                        case MtpConstants.PROPERTY_ARTIST:
-                            result.append(handle, propertyCode,
-                                    queryAudio(handle, Audio.AudioColumns.ARTIST));
-                            break;
-                        case MtpConstants.PROPERTY_ALBUM_NAME:
-                            result.append(handle, propertyCode,
-                                    queryAudio(handle, Audio.AudioColumns.ALBUM));
-                            break;
-                        case MtpConstants.PROPERTY_GENRE:
-                            String genre = queryGenre(handle);
-                            if (genre != null) {
-                                result.append(handle, propertyCode, genre);
-                            } else {
-                                result.setResult(MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE);
-                            }
-                            break;
-                        case MtpConstants.PROPERTY_AUDIO_WAVE_CODEC:
-                        case MtpConstants.PROPERTY_AUDIO_BITRATE:
-                        case MtpConstants.PROPERTY_SAMPLE_RATE:
-                            // we don't have these in our database, so return 0
-                            result.append(handle, propertyCode, MtpConstants.TYPE_UINT32, 0);
-                            break;
-                        case MtpConstants.PROPERTY_BITRATE_TYPE:
-                        case MtpConstants.PROPERTY_NUMBER_OF_CHANNELS:
-                            // we don't have these in our database, so return 0
-                            result.append(handle, propertyCode, MtpConstants.TYPE_UINT16, 0);
+                        case MtpConstants.TYPE_STR:
+                            String value = "";
+                            if (c != null)
+                                value = c.getString(property.column);
+                            list.append(id, property.code, value);
                             break;
                         default:
-                            if (property.type == MtpConstants.TYPE_STR) {
-                                result.append(handle, propertyCode, c.getString(column));
-                            } else if (property.type == MtpConstants.TYPE_UNDEFINED) {
-                                result.append(handle, propertyCode, property.type, 0);
-                            } else {
-                                result.append(handle, propertyCode, property.type,
-                                        c.getLong(column));
-                            }
-                            break;
+                            long longValue = 0L;
+                            if (c != null)
+                                longValue = c.getLong(property.column);
+                            list.append(id, property.code, property.type, longValue);
                     }
-                }
-            }
-
-            return result;
-        } catch (RemoteException e) {
-            return new MtpPropertyList(0, MtpConstants.RESPONSE_GENERAL_ERROR);
-        } finally {
-            if (c != null) {
-                c.close();
             }
         }
-        // impossible to get here, so no return statement
+        if (c != null)
+            c.close();
+        return MtpConstants.RESPONSE_OK;
     }
 
     private native String format_date_time(long seconds);
diff --git a/media/java/android/mtp/MtpPropertyList.java b/media/java/android/mtp/MtpPropertyList.java
index f9bc603..ede90da 100644
--- a/media/java/android/mtp/MtpPropertyList.java
+++ b/media/java/android/mtp/MtpPropertyList.java
@@ -16,6 +16,9 @@
 
 package android.mtp;
 
+import java.util.ArrayList;
+import java.util.List;
+
 /**
  * Encapsulates the ObjectPropList dataset used by the GetObjectPropList command.
  * The fields of this class are read by JNI code in android_media_MtpDatabase.cpp
@@ -23,56 +26,70 @@
 
 class MtpPropertyList {
 
-    // number of results returned
-    private int             mCount;
-    // maximum number of results
-    private final int       mMaxCount;
-    // result code for GetObjectPropList
-    public int              mResult;
     // list of object handles (first field in quadruplet)
-    public final int[]      mObjectHandles;
-    // list of object propery codes (second field in quadruplet)
-    public final int[]      mPropertyCodes;
+    private List<Integer> mObjectHandles;
+    // list of object property codes (second field in quadruplet)
+    private List<Integer> mPropertyCodes;
     // list of data type codes (third field in quadruplet)
-    public final int[]     mDataTypes;
+    private List<Integer> mDataTypes;
     // list of long int property values (fourth field in quadruplet, when value is integer type)
-    public long[]     mLongValues;
+    private List<Long> mLongValues;
     // list of long int property values (fourth field in quadruplet, when value is string type)
-    public String[]   mStringValues;
+    private List<String> mStringValues;
 
-    // constructor only called from MtpDatabase
-    public MtpPropertyList(int maxCount, int result) {
-        mMaxCount = maxCount;
-        mResult = result;
-        mObjectHandles = new int[maxCount];
-        mPropertyCodes = new int[maxCount];
-        mDataTypes = new int[maxCount];
-        // mLongValues and mStringValues are created lazily since both might not be necessary
+    // Return value of this operation
+    private int mCode;
+
+    public MtpPropertyList(int code) {
+        mCode = code;
+        mObjectHandles = new ArrayList<>();
+        mPropertyCodes = new ArrayList<>();
+        mDataTypes = new ArrayList<>();
+        mLongValues = new ArrayList<>();
+        mStringValues = new ArrayList<>();
     }
 
     public void append(int handle, int property, int type, long value) {
-        int index = mCount++;
-        if (mLongValues == null) {
-            mLongValues = new long[mMaxCount];
-        }
-        mObjectHandles[index] = handle;
-        mPropertyCodes[index] = property;
-        mDataTypes[index] = type;
-        mLongValues[index] = value;
+        mObjectHandles.add(handle);
+        mPropertyCodes.add(property);
+        mDataTypes.add(type);
+        mLongValues.add(value);
+        mStringValues.add(null);
     }
 
     public void append(int handle, int property, String value) {
-        int index = mCount++;
-        if (mStringValues == null) {
-            mStringValues = new String[mMaxCount];
-        }
-        mObjectHandles[index] = handle;
-        mPropertyCodes[index] = property;
-        mDataTypes[index] = MtpConstants.TYPE_STR;
-        mStringValues[index] = value;
+        mObjectHandles.add(handle);
+        mPropertyCodes.add(property);
+        mDataTypes.add(MtpConstants.TYPE_STR);
+        mStringValues.add(value);
+        mLongValues.add(0L);
     }
 
-    public void setResult(int result) {
-        mResult = result;
+    public int getCode() {
+        return mCode;
+    }
+
+    public int getCount() {
+        return mObjectHandles.size();
+    }
+
+    public int[] getObjectHandles() {
+        return mObjectHandles.stream().mapToInt(Integer::intValue).toArray();
+    }
+
+    public int[] getPropertyCodes() {
+        return mPropertyCodes.stream().mapToInt(Integer::intValue).toArray();
+    }
+
+    public int[] getDataTypes() {
+        return mDataTypes.stream().mapToInt(Integer::intValue).toArray();
+    }
+
+    public long[] getLongValues() {
+        return mLongValues.stream().mapToLong(Long::longValue).toArray();
+    }
+
+    public String[] getStringValues() {
+        return mStringValues.toArray(new String[0]);
     }
 }
diff --git a/media/java/android/mtp/MtpStorage.java b/media/java/android/mtp/MtpStorage.java
index 6ca442c..c72b827 100644
--- a/media/java/android/mtp/MtpStorage.java
+++ b/media/java/android/mtp/MtpStorage.java
@@ -31,15 +31,13 @@
     private final int mStorageId;
     private final String mPath;
     private final String mDescription;
-    private final long mReserveSpace;
     private final boolean mRemovable;
     private final long mMaxFileSize;
 
-    public MtpStorage(StorageVolume volume, Context context) {
-        mStorageId = volume.getStorageId();
+    public MtpStorage(StorageVolume volume, int storageId) {
+        mStorageId = storageId;
         mPath = volume.getPath();
-        mDescription = volume.getDescription(context);
-        mReserveSpace = volume.getMtpReserveSpace() * 1024L * 1024L;
+        mDescription = volume.getDescription(null);
         mRemovable = volume.isRemovable();
         mMaxFileSize = volume.getMaxFileSize();
     }
@@ -72,16 +70,6 @@
     }
 
    /**
-     * Returns the amount of space to reserve on the storage file system.
-     * This can be set to a non-zero value to prevent MTP from filling up the entire storage.
-     *
-     * @return reserved space in bytes.
-     */
-    public final long getReserveSpace() {
-        return mReserveSpace;
-    }
-
-   /**
      * Returns true if the storage is removable.
      *
      * @return is removable
diff --git a/media/java/android/mtp/MtpStorageManager.java b/media/java/android/mtp/MtpStorageManager.java
new file mode 100644
index 0000000..bdc8741
--- /dev/null
+++ b/media/java/android/mtp/MtpStorageManager.java
@@ -0,0 +1,1210 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.mtp;
+
+import android.media.MediaFile;
+import android.os.FileObserver;
+import android.os.storage.StorageVolume;
+import android.util.Log;
+
+import java.io.IOException;
+import java.nio.file.DirectoryIteratorException;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Stream;
+
+/**
+ * MtpStorageManager provides functionality for listing, tracking, and notifying MtpServer of
+ * filesystem changes. As directories are listed, this class will cache the results,
+ * and send events when objects are added/removed from cached directories.
+ * {@hide}
+ */
+public class MtpStorageManager {
+    private static final String TAG = MtpStorageManager.class.getSimpleName();
+    public static boolean sDebug = false;
+
+    // Inotify flags not provided by FileObserver
+    private static final int IN_ONLYDIR = 0x01000000;
+    private static final int IN_Q_OVERFLOW = 0x00004000;
+    private static final int IN_IGNORED    = 0x00008000;
+    private static final int IN_ISDIR = 0x40000000;
+
+    private class MtpObjectObserver extends FileObserver {
+        MtpObject mObject;
+
+        MtpObjectObserver(MtpObject object) {
+            super(object.getPath().toString(),
+                    MOVED_FROM | MOVED_TO | DELETE | CREATE | IN_ONLYDIR);
+            mObject = object;
+        }
+
+        @Override
+        public void onEvent(int event, String path) {
+            synchronized (MtpStorageManager.this) {
+                if ((event & IN_Q_OVERFLOW) != 0) {
+                    // We are out of space in the inotify queue.
+                    Log.e(TAG, "Received Inotify overflow event!");
+                }
+                MtpObject obj = mObject.getChild(path);
+                if ((event & MOVED_TO) != 0 || (event & CREATE) != 0) {
+                    if (sDebug)
+                        Log.i(TAG, "Got inotify added event for " + path + " " + event);
+                    handleAddedObject(mObject, path, (event & IN_ISDIR) != 0);
+                } else if ((event & MOVED_FROM) != 0 || (event & DELETE) != 0) {
+                    if (obj == null) {
+                        Log.w(TAG, "Object was null in event " + path);
+                        return;
+                    }
+                    if (sDebug)
+                        Log.i(TAG, "Got inotify removed event for " + path + " " + event);
+                    handleRemovedObject(obj);
+                } else if ((event & IN_IGNORED) != 0) {
+                    if (sDebug)
+                        Log.i(TAG, "inotify for " + mObject.getPath() + " deleted");
+                    if (mObject.mObserver != null)
+                        mObject.mObserver.stopWatching();
+                    mObject.mObserver = null;
+                } else {
+                    Log.w(TAG, "Got unrecognized event " + path + " " + event);
+                }
+            }
+        }
+
+        @Override
+        public void finalize() {
+            // If the server shuts down and starts up again, the new server's observers can be
+            // invalidated by the finalize() calls of the previous server's observers.
+            // Hence, disable the automatic stopWatching() call in FileObserver#finalize, and
+            // always call stopWatching() manually whenever an observer should be shut down.
+        }
+    }
+
+    /**
+     * Describes how the object is being acted on, to determine how events are handled.
+     */
+    private enum MtpObjectState {
+        NORMAL,
+        FROZEN,             // Object is going to be modified in this session.
+        FROZEN_ADDED,       // Object was frozen, and has been added.
+        FROZEN_REMOVED,     // Object was frozen, and has been removed.
+        FROZEN_ONESHOT_ADD, // Object is waiting for single add event before being unfrozen.
+        FROZEN_ONESHOT_DEL, // Object is waiting for single remove event and will then be removed.
+    }
+
+    /**
+     * Describes the current operation being done on an object. Determines whether observers are
+     * created on new folders.
+     */
+    private enum MtpOperation {
+        NONE,     // Any new folders not added as part of the session are immediately observed.
+        ADD,      // New folders added as part of the session are immediately observed.
+        RENAME,   // Renamed or moved folders are not immediately observed.
+        COPY,     // Copied folders are immediately observed iff the original was.
+        DELETE,   // Exists for debugging purposes only.
+    }
+
+    /** MtpObject represents either a file or directory in an associated storage. **/
+    public static class MtpObject {
+        // null for root objects
+        private MtpObject mParent;
+
+        private String mName;
+        private int mId;
+        private MtpObjectState mState;
+        private MtpOperation mOp;
+
+        private boolean mVisited;
+        private boolean mIsDir;
+
+        // null if not a directory
+        private HashMap<String, MtpObject> mChildren;
+        // null if not both a directory and visited
+        private FileObserver mObserver;
+
+        MtpObject(String name, int id, MtpObject parent, boolean isDir) {
+            mId = id;
+            mName = name;
+            mParent = parent;
+            mObserver = null;
+            mVisited = false;
+            mState = MtpObjectState.NORMAL;
+            mIsDir = isDir;
+            mOp = MtpOperation.NONE;
+
+            mChildren = mIsDir ? new HashMap<>() : null;
+        }
+
+        /** Public methods for getting object info **/
+
+        public String getName() {
+            return mName;
+        }
+
+        public int getId() {
+            return mId;
+        }
+
+        public boolean isDir() {
+            return mIsDir;
+        }
+
+        public int getFormat() {
+            return mIsDir ? MtpConstants.FORMAT_ASSOCIATION : MediaFile.getFormatCode(mName, null);
+        }
+
+        public int getStorageId() {
+            return getRoot().getId();
+        }
+
+        public long getModifiedTime() {
+            return getPath().toFile().lastModified() / 1000;
+        }
+
+        public MtpObject getParent() {
+            return mParent;
+        }
+
+        public MtpObject getRoot() {
+            return isRoot() ? this : mParent.getRoot();
+        }
+
+        public long getSize() {
+            return mIsDir ? 0 : getPath().toFile().length();
+        }
+
+        public Path getPath() {
+            return isRoot() ? Paths.get(mName) : mParent.getPath().resolve(mName);
+        }
+
+        public boolean isRoot() {
+            return mParent == null;
+        }
+
+        /** For MtpStorageManager only **/
+
+        private void setName(String name) {
+            mName = name;
+        }
+
+        private void setId(int id) {
+            mId = id;
+        }
+
+        private boolean isVisited() {
+            return mVisited;
+        }
+
+        private void setParent(MtpObject parent) {
+            mParent = parent;
+        }
+
+        private void setDir(boolean dir) {
+            if (dir != mIsDir) {
+                mIsDir = dir;
+                mChildren = mIsDir ? new HashMap<>() : null;
+            }
+        }
+
+        private void setVisited(boolean visited) {
+            mVisited = visited;
+        }
+
+        private MtpObjectState getState() {
+            return mState;
+        }
+
+        private void setState(MtpObjectState state) {
+            mState = state;
+            if (mState == MtpObjectState.NORMAL)
+                mOp = MtpOperation.NONE;
+        }
+
+        private MtpOperation getOperation() {
+            return mOp;
+        }
+
+        private void setOperation(MtpOperation op) {
+            mOp = op;
+        }
+
+        private FileObserver getObserver() {
+            return mObserver;
+        }
+
+        private void setObserver(FileObserver observer) {
+            mObserver = observer;
+        }
+
+        private void addChild(MtpObject child) {
+            mChildren.put(child.getName(), child);
+        }
+
+        private MtpObject getChild(String name) {
+            return mChildren.get(name);
+        }
+
+        private Collection<MtpObject> getChildren() {
+            return mChildren.values();
+        }
+
+        private boolean exists() {
+            return getPath().toFile().exists();
+        }
+
+        private MtpObject copy(boolean recursive) {
+            MtpObject copy = new MtpObject(mName, mId, mParent, mIsDir);
+            copy.mIsDir = mIsDir;
+            copy.mVisited = mVisited;
+            copy.mState = mState;
+            copy.mChildren = mIsDir ? new HashMap<>() : null;
+            if (recursive && mIsDir) {
+                for (MtpObject child : mChildren.values()) {
+                    MtpObject childCopy = child.copy(true);
+                    childCopy.setParent(copy);
+                    copy.addChild(childCopy);
+                }
+            }
+            return copy;
+        }
+    }
+
+    /**
+     * A class that processes generated filesystem events.
+     */
+    public static abstract class MtpNotifier {
+        /**
+         * Called when an object is added.
+         */
+        public abstract void sendObjectAdded(int id);
+
+        /**
+         * Called when an object is deleted.
+         */
+        public abstract void sendObjectRemoved(int id);
+    }
+
+    private MtpNotifier mMtpNotifier;
+
+    // A cache of MtpObjects. The objects in the cache are keyed by object id.
+    // The root object of each storage isn't in this map since they all have ObjectId 0.
+    // Instead, they can be found in mRoots keyed by storageId.
+    private HashMap<Integer, MtpObject> mObjects;
+
+    // A cache of the root MtpObject for each storage, keyed by storage id.
+    private HashMap<Integer, MtpObject> mRoots;
+
+    // Object and Storage ids are allocated incrementally and not to be reused.
+    private int mNextObjectId;
+    private int mNextStorageId;
+
+    // Special subdirectories. When set, only return objects rooted in these directories, and do
+    // not allow them to be modified.
+    private Set<String> mSubdirectories;
+
+    private volatile boolean mCheckConsistency;
+    private Thread mConsistencyThread;
+
+    public MtpStorageManager(MtpNotifier notifier, Set<String> subdirectories) {
+        mMtpNotifier = notifier;
+        mSubdirectories = subdirectories;
+        mObjects = new HashMap<>();
+        mRoots = new HashMap<>();
+        mNextObjectId = 1;
+        mNextStorageId = 1;
+
+        mCheckConsistency = false; // Set to true to turn on automatic consistency checking
+        mConsistencyThread = new Thread(() -> {
+            while (mCheckConsistency) {
+                try {
+                    Thread.sleep(15 * 1000);
+                } catch (InterruptedException e) {
+                    return;
+                }
+                if (MtpStorageManager.this.checkConsistency()) {
+                    Log.v(TAG, "Cache is consistent");
+                } else {
+                    Log.w(TAG, "Cache is not consistent");
+                }
+            }
+        });
+        if (mCheckConsistency)
+            mConsistencyThread.start();
+    }
+
+    /**
+     * Clean up resources used by the storage manager.
+     */
+    public synchronized void close() {
+        Stream<MtpObject> objs = Stream.concat(mRoots.values().stream(),
+                mObjects.values().stream());
+
+        Iterator<MtpObject> iter = objs.iterator();
+        while (iter.hasNext()) {
+            // Close all FileObservers.
+            MtpObject obj = iter.next();
+            if (obj.getObserver() != null) {
+                obj.getObserver().stopWatching();
+                obj.setObserver(null);
+            }
+        }
+
+        // Shut down the consistency checking thread
+        if (mCheckConsistency) {
+            mCheckConsistency = false;
+            mConsistencyThread.interrupt();
+            try {
+                mConsistencyThread.join();
+            } catch (InterruptedException e) {
+                // ignore
+            }
+        }
+    }
+
+    /**
+     * Sets the special subdirectories, which are the subdirectories of root storage that queries
+     * are restricted to. Must be done before any root storages are accessed.
+     * @param subDirs Subdirectories to set, or null to reset.
+     */
+    public synchronized void setSubdirectories(Set<String> subDirs) {
+        mSubdirectories = subDirs;
+    }
+
+    /**
+     * Allocates an MTP storage id for the given volume and add it to current roots.
+     * @param volume Storage to add.
+     * @return the associated MtpStorage
+     */
+    public synchronized MtpStorage addMtpStorage(StorageVolume volume) {
+        int storageId = ((getNextStorageId() & 0x0000FFFF) << 16) + 1;
+        MtpObject root = new MtpObject(volume.getPath(), storageId, null, true);
+        MtpStorage storage = new MtpStorage(volume, storageId);
+        mRoots.put(storageId, root);
+        return storage;
+    }
+
+    /**
+     * Removes the given storage and all associated items from the cache.
+     * @param storage Storage to remove.
+     */
+    public synchronized void removeMtpStorage(MtpStorage storage) {
+        removeObjectFromCache(getStorageRoot(storage.getStorageId()), true, true);
+    }
+
+    /**
+     * Checks if the given object can be renamed, moved, or deleted.
+     * If there are special subdirectories, they cannot be modified.
+     * @param obj Object to check.
+     * @return Whether object can be modified.
+     */
+    private synchronized boolean isSpecialSubDir(MtpObject obj) {
+        return obj.getParent().isRoot() && mSubdirectories != null
+                && !mSubdirectories.contains(obj.getName());
+    }
+
+    /**
+     * Get the object with the specified path. Visit any necessary directories on the way.
+     * @param path Full path of the object to find.
+     * @return The desired object, or null if it cannot be found.
+     */
+    public synchronized MtpObject getByPath(String path) {
+        MtpObject obj = null;
+        for (MtpObject root : mRoots.values()) {
+            if (path.startsWith(root.getName())) {
+                obj = root;
+                path = path.substring(root.getName().length());
+            }
+        }
+        for (String name : path.split("/")) {
+            if (obj == null || !obj.isDir())
+                return null;
+            if ("".equals(name))
+                continue;
+            if (!obj.isVisited())
+                getChildren(obj);
+            obj = obj.getChild(name);
+        }
+        return obj;
+    }
+
+    /**
+     * Get the object with specified id.
+     * @param id Id of object. must not be 0 or 0xFFFFFFFF
+     * @return Object, or null if error.
+     */
+    public synchronized MtpObject getObject(int id) {
+        if (id == 0 || id == 0xFFFFFFFF) {
+            Log.w(TAG, "Can't get root storages with getObject()");
+            return null;
+        }
+        if (!mObjects.containsKey(id)) {
+            Log.w(TAG, "Id " + id + " doesn't exist");
+            return null;
+        }
+        return mObjects.get(id);
+    }
+
+    /**
+     * Get the storage with specified id.
+     * @param id Storage id.
+     * @return Object that is the root of the storage, or null if error.
+     */
+    public MtpObject getStorageRoot(int id) {
+        if (!mRoots.containsKey(id)) {
+            Log.w(TAG, "StorageId " + id + " doesn't exist");
+            return null;
+        }
+        return mRoots.get(id);
+    }
+
+    private int getNextObjectId() {
+        int ret = mNextObjectId;
+        // Treat the id as unsigned int
+        mNextObjectId = (int) ((long) mNextObjectId + 1);
+        return ret;
+    }
+
+    private int getNextStorageId() {
+        return mNextStorageId++;
+    }
+
+    /**
+     * Get all objects matching the given parent, format, and storage
+     * @param parent object id of the parent. 0 for all objects, 0xFFFFFFFF for all object in root
+     * @param format format of returned objects. 0 for any format
+     * @param storageId storage id to look in. 0xFFFFFFFF for all storages
+     * @return A stream of matched objects, or null if error
+     */
+    public synchronized Stream<MtpObject> getObjects(int parent, int format, int storageId) {
+        boolean recursive = parent == 0;
+        if (parent == 0xFFFFFFFF)
+            parent = 0;
+        if (storageId == 0xFFFFFFFF) {
+            // query all stores
+            if (parent == 0) {
+                // Get the objects of this format and parent in each store.
+                ArrayList<Stream<MtpObject>> streamList = new ArrayList<>();
+                for (MtpObject root : mRoots.values()) {
+                    streamList.add(getObjects(root, format, recursive));
+                }
+                return Stream.of(streamList).flatMap(Collection::stream).reduce(Stream::concat)
+                        .orElseGet(Stream::empty);
+            }
+        }
+        MtpObject obj = parent == 0 ? getStorageRoot(storageId) : getObject(parent);
+        if (obj == null)
+            return null;
+        return getObjects(obj, format, recursive);
+    }
+
+    private synchronized Stream<MtpObject> getObjects(MtpObject parent, int format, boolean rec) {
+        Collection<MtpObject> children = getChildren(parent);
+        if (children == null)
+            return null;
+        Stream<MtpObject> ret = Stream.of(children).flatMap(Collection::stream);
+
+        if (format != 0) {
+            ret = ret.filter(o -> o.getFormat() == format);
+        }
+        if (rec) {
+            // Get all objects recursively.
+            ArrayList<Stream<MtpObject>> streamList = new ArrayList<>();
+            streamList.add(ret);
+            for (MtpObject o : children) {
+                if (o.isDir())
+                    streamList.add(getObjects(o, format, true));
+            }
+            ret = Stream.of(streamList).filter(Objects::nonNull).flatMap(Collection::stream)
+                    .reduce(Stream::concat).orElseGet(Stream::empty);
+        }
+        return ret;
+    }
+
+    /**
+     * Return the children of the given object. If the object hasn't been visited yet, add
+     * its children to the cache and start observing it.
+     * @param object the parent object
+     * @return The collection of child objects or null if error
+     */
+    private synchronized Collection<MtpObject> getChildren(MtpObject object) {
+        if (object == null || !object.isDir()) {
+            Log.w(TAG, "Can't find children of " + (object == null ? "null" : object.getId()));
+            return null;
+        }
+        if (!object.isVisited()) {
+            Path dir = object.getPath();
+            /*
+             * If a file is added after the observer starts watching the directory, but before
+             * the contents are listed, it will generate an event that will get processed
+             * after this synchronized function returns. We handle this by ignoring object
+             * added events if an object at that path already exists.
+             */
+            if (object.getObserver() != null)
+                Log.e(TAG, "Observer is not null!");
+            object.setObserver(new MtpObjectObserver(object));
+            object.getObserver().startWatching();
+            try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {
+                for (Path file : stream) {
+                    addObjectToCache(object, file.getFileName().toString(),
+                            file.toFile().isDirectory());
+                }
+            } catch (IOException | DirectoryIteratorException e) {
+                Log.e(TAG, e.toString());
+                object.getObserver().stopWatching();
+                object.setObserver(null);
+                return null;
+            }
+            object.setVisited(true);
+        }
+        return object.getChildren();
+    }
+
+    /**
+     * Create a new object from the given path and add it to the cache.
+     * @param parent The parent object
+     * @param newName Path of the new object
+     * @return the new object if success, else null
+     */
+    private synchronized MtpObject addObjectToCache(MtpObject parent, String newName,
+            boolean isDir) {
+        if (!parent.isRoot() && getObject(parent.getId()) != parent)
+            // parent object has been removed
+            return null;
+        if (parent.getChild(newName) != null) {
+            // Object already exists
+            return null;
+        }
+        if (mSubdirectories != null && parent.isRoot() && !mSubdirectories.contains(newName)) {
+            // Not one of the restricted subdirectories.
+            return null;
+        }
+
+        MtpObject obj = new MtpObject(newName, getNextObjectId(), parent, isDir);
+        mObjects.put(obj.getId(), obj);
+        parent.addChild(obj);
+        return obj;
+    }
+
+    /**
+     * Remove the given path from the cache.
+     * @param removed The removed object
+     * @param removeGlobal Whether to remove the object from the global id map
+     * @param recursive Whether to also remove its children recursively.
+     * @return true if successfully removed
+     */
+    private synchronized boolean removeObjectFromCache(MtpObject removed, boolean removeGlobal,
+            boolean recursive) {
+        boolean ret = removed.isRoot()
+                || removed.getParent().mChildren.remove(removed.getName(), removed);
+        if (!ret && sDebug)
+            Log.w(TAG, "Failed to remove from parent " + removed.getPath());
+        if (removed.isRoot()) {
+            ret = mRoots.remove(removed.getId(), removed) && ret;
+        } else if (removeGlobal) {
+            ret = mObjects.remove(removed.getId(), removed) && ret;
+        }
+        if (!ret && sDebug)
+            Log.w(TAG, "Failed to remove from global cache " + removed.getPath());
+        if (removed.getObserver() != null) {
+            removed.getObserver().stopWatching();
+            removed.setObserver(null);
+        }
+        if (removed.isDir() && recursive) {
+            // Remove all descendants from cache recursively
+            Collection<MtpObject> children = new ArrayList<>(removed.getChildren());
+            for (MtpObject child : children) {
+                ret = removeObjectFromCache(child, removeGlobal, true) && ret;
+            }
+        }
+        return ret;
+    }
+
+    private synchronized void handleAddedObject(MtpObject parent, String path, boolean isDir) {
+        MtpOperation op = MtpOperation.NONE;
+        MtpObject obj = parent.getChild(path);
+        if (obj != null) {
+            MtpObjectState state = obj.getState();
+            op = obj.getOperation();
+            if (obj.isDir() != isDir && state != MtpObjectState.FROZEN_REMOVED)
+                Log.d(TAG, "Inconsistent directory info! " + obj.getPath());
+            obj.setDir(isDir);
+            switch (state) {
+                case FROZEN:
+                case FROZEN_REMOVED:
+                    obj.setState(MtpObjectState.FROZEN_ADDED);
+                    break;
+                case FROZEN_ONESHOT_ADD:
+                    obj.setState(MtpObjectState.NORMAL);
+                    break;
+                case NORMAL:
+                case FROZEN_ADDED:
+                    // This can happen when handling listed object in a new directory.
+                    return;
+                default:
+                    Log.w(TAG, "Unexpected state in add " + path + " " + state);
+            }
+            if (sDebug)
+                Log.i(TAG, state + " transitioned to " + obj.getState() + " in op " + op);
+        } else {
+            obj = MtpStorageManager.this.addObjectToCache(parent, path, isDir);
+            if (obj != null) {
+                MtpStorageManager.this.mMtpNotifier.sendObjectAdded(obj.getId());
+            } else {
+                if (sDebug)
+                    Log.w(TAG, "object " + path + " already exists");
+                return;
+            }
+        }
+        if (isDir) {
+            // If this was added as part of a rename do not visit or send events.
+            if (op == MtpOperation.RENAME)
+                return;
+
+            // If it was part of a copy operation, then only add observer if it was visited before.
+            if (op == MtpOperation.COPY && !obj.isVisited())
+                return;
+
+            if (obj.getObserver() != null) {
+                Log.e(TAG, "Observer is not null!");
+                return;
+            }
+            obj.setObserver(new MtpObjectObserver(obj));
+            obj.getObserver().startWatching();
+            obj.setVisited(true);
+
+            // It's possible that objects were added to a watched directory before the watch can be
+            // created, so manually handle those.
+            try (DirectoryStream<Path> stream = Files.newDirectoryStream(obj.getPath())) {
+                for (Path file : stream) {
+                    if (sDebug)
+                        Log.i(TAG, "Manually handling event for " + file.getFileName().toString());
+                    handleAddedObject(obj, file.getFileName().toString(),
+                            file.toFile().isDirectory());
+                }
+            } catch (IOException | DirectoryIteratorException e) {
+                Log.e(TAG, e.toString());
+                obj.getObserver().stopWatching();
+                obj.setObserver(null);
+            }
+        }
+    }
+
+    private synchronized void handleRemovedObject(MtpObject obj) {
+        MtpObjectState state = obj.getState();
+        MtpOperation op = obj.getOperation();
+        switch (state) {
+            case FROZEN_ADDED:
+                obj.setState(MtpObjectState.FROZEN_REMOVED);
+                break;
+            case FROZEN_ONESHOT_DEL:
+                removeObjectFromCache(obj, op != MtpOperation.RENAME, false);
+                break;
+            case FROZEN:
+                obj.setState(MtpObjectState.FROZEN_REMOVED);
+                break;
+            case NORMAL:
+                if (MtpStorageManager.this.removeObjectFromCache(obj, true, true))
+                    MtpStorageManager.this.mMtpNotifier.sendObjectRemoved(obj.getId());
+                break;
+            default:
+                // This shouldn't happen; states correspond to objects that don't exist
+                Log.e(TAG, "Got unexpected object remove for " + obj.getName());
+        }
+        if (sDebug)
+            Log.i(TAG, state + " transitioned to " + obj.getState() + " in op " + op);
+    }
+
+    /**
+     * Block the caller until all events currently in the event queue have been
+     * read and processed. Used for testing purposes.
+     */
+    public void flushEvents() {
+        try {
+            // TODO make this smarter
+            Thread.sleep(500);
+        } catch (InterruptedException e) {
+
+        }
+    }
+
+    /**
+     * Dumps a representation of the cache to log.
+     */
+    public synchronized void dump() {
+        for (int key : mObjects.keySet()) {
+            MtpObject obj = mObjects.get(key);
+            Log.i(TAG, key + " | " + (obj.getParent() == null ? obj.getParent().getId() : "null")
+                    + " | " + obj.getName() + " | " + (obj.isDir() ? "dir" : "obj")
+                    + " | " + (obj.isVisited() ? "v" : "nv") + " | " + obj.getState());
+        }
+    }
+
+    /**
+     * Checks consistency of the cache. This checks whether all objects have correct links
+     * to their parent, and whether directories are missing or have extraneous objects.
+     * @return true iff cache is consistent
+     */
+    public synchronized boolean checkConsistency() {
+        Stream<MtpObject> objs = Stream.concat(mRoots.values().stream(),
+                mObjects.values().stream());
+        Iterator<MtpObject> iter = objs.iterator();
+        boolean ret = true;
+        while (iter.hasNext()) {
+            MtpObject obj = iter.next();
+            if (!obj.exists()) {
+                Log.w(TAG, "Object doesn't exist " + obj.getPath() + " " + obj.getId());
+                ret = false;
+            }
+            if (obj.getState() != MtpObjectState.NORMAL) {
+                Log.w(TAG, "Object " + obj.getPath() + " in state " + obj.getState());
+                ret = false;
+            }
+            if (obj.getOperation() != MtpOperation.NONE) {
+                Log.w(TAG, "Object " + obj.getPath() + " in operation " + obj.getOperation());
+                ret = false;
+            }
+            if (!obj.isRoot() && mObjects.get(obj.getId()) != obj) {
+                Log.w(TAG, "Object " + obj.getPath() + " is not in map correctly");
+                ret = false;
+            }
+            if (obj.getParent() != null) {
+                if (obj.getParent().isRoot() && obj.getParent()
+                        != mRoots.get(obj.getParent().getId())) {
+                    Log.w(TAG, "Root parent is not in root mapping " + obj.getPath());
+                    ret = false;
+                }
+                if (!obj.getParent().isRoot() && obj.getParent()
+                        != mObjects.get(obj.getParent().getId())) {
+                    Log.w(TAG, "Parent is not in object mapping " + obj.getPath());
+                    ret = false;
+                }
+                if (obj.getParent().getChild(obj.getName()) != obj) {
+                    Log.w(TAG, "Child does not exist in parent " + obj.getPath());
+                    ret = false;
+                }
+            }
+            if (obj.isDir()) {
+                if (obj.isVisited() == (obj.getObserver() == null)) {
+                    Log.w(TAG, obj.getPath() + " is " + (obj.isVisited() ? "" : "not ")
+                            + " visited but observer is " + obj.getObserver());
+                    ret = false;
+                }
+                if (!obj.isVisited() && obj.getChildren().size() > 0) {
+                    Log.w(TAG, obj.getPath() + " is not visited but has children");
+                    ret = false;
+                }
+                try (DirectoryStream<Path> stream = Files.newDirectoryStream(obj.getPath())) {
+                    Set<String> files = new HashSet<>();
+                    for (Path file : stream) {
+                        if (obj.isVisited() &&
+                                obj.getChild(file.getFileName().toString()) == null &&
+                                (mSubdirectories == null || !obj.isRoot() ||
+                                        mSubdirectories.contains(file.getFileName().toString()))) {
+                            Log.w(TAG, "File exists in fs but not in children " + file);
+                            ret = false;
+                        }
+                        files.add(file.toString());
+                    }
+                    for (MtpObject child : obj.getChildren()) {
+                        if (!files.contains(child.getPath().toString())) {
+                            Log.w(TAG, "File in children doesn't exist in fs " + child.getPath());
+                            ret = false;
+                        }
+                        if (child != mObjects.get(child.getId())) {
+                            Log.w(TAG, "Child is not in object map " + child.getPath());
+                            ret = false;
+                        }
+                    }
+                } catch (IOException | DirectoryIteratorException e) {
+                    Log.w(TAG, e.toString());
+                    ret = false;
+                }
+            }
+        }
+        return ret;
+    }
+
+    /**
+     * Informs MtpStorageManager that an object with the given path is about to be added.
+     * @param parent The parent object of the object to be added.
+     * @param name Filename of object to add.
+     * @return Object id of the added object, or -1 if it cannot be added.
+     */
+    public synchronized int beginSendObject(MtpObject parent, String name, int format) {
+        if (sDebug)
+            Log.v(TAG, "beginSendObject " + name);
+        if (!parent.isDir())
+            return -1;
+        if (parent.isRoot() && mSubdirectories != null && !mSubdirectories.contains(name))
+            return -1;
+        getChildren(parent); // Ensure parent is visited
+        MtpObject obj  = addObjectToCache(parent, name, format == MtpConstants.FORMAT_ASSOCIATION);
+        if (obj == null)
+            return -1;
+        obj.setState(MtpObjectState.FROZEN);
+        obj.setOperation(MtpOperation.ADD);
+        return obj.getId();
+    }
+
+    /**
+     * Clean up the object state after a sendObject operation.
+     * @param obj The object, returned from beginAddObject().
+     * @param succeeded Whether the file was successfully created.
+     * @return Whether cache state was successfully cleaned up.
+     */
+    public synchronized boolean endSendObject(MtpObject obj, boolean succeeded) {
+        if (sDebug)
+            Log.v(TAG, "endSendObject " + succeeded);
+        return generalEndAddObject(obj, succeeded, true);
+    }
+
+    /**
+     * Informs MtpStorageManager that the given object is about to be renamed.
+     * If this returns true, it must be followed with an endRenameObject()
+     * @param obj Object to be renamed.
+     * @param newName New name of the object.
+     * @return Whether renaming is allowed.
+     */
+    public synchronized boolean beginRenameObject(MtpObject obj, String newName) {
+        if (sDebug)
+            Log.v(TAG, "beginRenameObject " + obj.getName() + " " + newName);
+        if (obj.isRoot())
+            return false;
+        if (isSpecialSubDir(obj))
+            return false;
+        if (obj.getParent().getChild(newName) != null)
+            // Object already exists in parent with that name.
+            return false;
+
+        MtpObject oldObj = obj.copy(false);
+        obj.setName(newName);
+        obj.getParent().addChild(obj);
+        oldObj.getParent().addChild(oldObj);
+        return generalBeginRenameObject(oldObj, obj);
+    }
+
+    /**
+     * Cleans up cache state after a rename operation and sends any events that were missed.
+     * @param obj The object being renamed, the same one that was passed in beginRenameObject().
+     * @param oldName The previous name of the object.
+     * @param success Whether the rename operation succeeded.
+     * @return Whether state was successfully cleaned up.
+     */
+    public synchronized boolean endRenameObject(MtpObject obj, String oldName, boolean success) {
+        if (sDebug)
+            Log.v(TAG, "endRenameObject " + success);
+        MtpObject parent = obj.getParent();
+        MtpObject oldObj = parent.getChild(oldName);
+        if (!success) {
+            // If the rename failed, we want oldObj to be the original and obj to be the dummy.
+            // Switch the objects, except for their name and state.
+            MtpObject temp = oldObj;
+            MtpObjectState oldState = oldObj.getState();
+            temp.setName(obj.getName());
+            temp.setState(obj.getState());
+            oldObj = obj;
+            oldObj.setName(oldName);
+            oldObj.setState(oldState);
+            obj = temp;
+            parent.addChild(obj);
+            parent.addChild(oldObj);
+        }
+        return generalEndRenameObject(oldObj, obj, success);
+    }
+
+    /**
+     * Informs MtpStorageManager that the given object is about to be deleted by the initiator,
+     * so don't send an event.
+     * @param obj Object to be deleted.
+     * @return Whether cache deletion is allowed.
+     */
+    public synchronized boolean beginRemoveObject(MtpObject obj) {
+        if (sDebug)
+            Log.v(TAG, "beginRemoveObject " + obj.getName());
+        return !obj.isRoot() && !isSpecialSubDir(obj)
+                && generalBeginRemoveObject(obj, MtpOperation.DELETE);
+    }
+
+    /**
+     * Clean up cache state after a delete operation and send any events that were missed.
+     * @param obj Object to be deleted, same one passed in beginRemoveObject().
+     * @param success Whether operation was completed successfully.
+     * @return Whether cache state is correct.
+     */
+    public synchronized boolean endRemoveObject(MtpObject obj, boolean success) {
+        if (sDebug)
+            Log.v(TAG, "endRemoveObject " + success);
+        boolean ret = true;
+        if (obj.isDir()) {
+            for (MtpObject child : new ArrayList<>(obj.getChildren()))
+                if (child.getOperation() == MtpOperation.DELETE)
+                    ret = endRemoveObject(child, success) && ret;
+        }
+        return generalEndRemoveObject(obj, success, true) && ret;
+    }
+
+    /**
+     * Informs MtpStorageManager that the given object is about to be moved to a new parent.
+     * @param obj Object to be moved.
+     * @param newParent The new parent object.
+     * @return Whether the move is allowed.
+     */
+    public synchronized boolean beginMoveObject(MtpObject obj, MtpObject newParent) {
+        if (sDebug)
+            Log.v(TAG, "beginMoveObject " + newParent.getPath());
+        if (obj.isRoot())
+            return false;
+        if (isSpecialSubDir(obj))
+            return false;
+        getChildren(newParent); // Ensure parent is visited
+        if (newParent.getChild(obj.getName()) != null)
+            // Object already exists in parent with that name.
+            return false;
+        if (obj.getStorageId() != newParent.getStorageId()) {
+            /*
+             * The move is occurring across storages. The observers will not remain functional
+             * after the move, and the move will not be atomic. We have to copy the file tree
+             * to the destination and recreate the observers once copy is complete.
+             */
+            MtpObject newObj = obj.copy(true);
+            newObj.setParent(newParent);
+            newParent.addChild(newObj);
+            return generalBeginRemoveObject(obj, MtpOperation.RENAME)
+                    && generalBeginCopyObject(newObj, false);
+        }
+        // Move obj to new parent, create a dummy object in the old parent.
+        MtpObject oldObj = obj.copy(false);
+        obj.setParent(newParent);
+        oldObj.getParent().addChild(oldObj);
+        obj.getParent().addChild(obj);
+        return generalBeginRenameObject(oldObj, obj);
+    }
+
+    /**
+     * Clean up cache state after a move operation and send any events that were missed.
+     * @param oldParent The old parent object.
+     * @param newParent The new parent object.
+     * @param name The name of the object being moved.
+     * @param success Whether operation was completed successfully.
+     * @return Whether cache state is correct.
+     */
+    public synchronized boolean endMoveObject(MtpObject oldParent, MtpObject newParent, String name,
+            boolean success) {
+        if (sDebug)
+            Log.v(TAG, "endMoveObject " + success);
+        MtpObject oldObj = oldParent.getChild(name);
+        MtpObject newObj = newParent.getChild(name);
+        if (oldObj == null || newObj == null)
+            return false;
+        if (oldParent.getStorageId() != newObj.getStorageId()) {
+            boolean ret = endRemoveObject(oldObj, success);
+            return generalEndCopyObject(newObj, success, true) && ret;
+        }
+        if (!success) {
+            // If the rename failed, we want oldObj to be the original and obj to be the dummy.
+            // Switch the objects, except for their parent and state.
+            MtpObject temp = oldObj;
+            MtpObjectState oldState = oldObj.getState();
+            temp.setParent(newObj.getParent());
+            temp.setState(newObj.getState());
+            oldObj = newObj;
+            oldObj.setParent(oldParent);
+            oldObj.setState(oldState);
+            newObj = temp;
+            newObj.getParent().addChild(newObj);
+            oldParent.addChild(oldObj);
+        }
+        return generalEndRenameObject(oldObj, newObj, success);
+    }
+
+    /**
+     * Informs MtpStorageManager that the given object is about to be copied recursively.
+     * @param object Object to be copied
+     * @param newParent New parent for the object.
+     * @return The object id for the new copy, or -1 if error.
+     */
+    public synchronized int beginCopyObject(MtpObject object, MtpObject newParent) {
+        if (sDebug)
+            Log.v(TAG, "beginCopyObject " + object.getName() + " to " + newParent.getPath());
+        String name = object.getName();
+        if (!newParent.isDir())
+            return -1;
+        if (newParent.isRoot() && mSubdirectories != null && !mSubdirectories.contains(name))
+            return -1;
+        getChildren(newParent); // Ensure parent is visited
+        if (newParent.getChild(name) != null)
+            return -1;
+        MtpObject newObj  = object.copy(object.isDir());
+        newParent.addChild(newObj);
+        newObj.setParent(newParent);
+        if (!generalBeginCopyObject(newObj, true))
+            return -1;
+        return newObj.getId();
+    }
+
+    /**
+     * Cleans up cache state after a copy operation.
+     * @param object Object that was copied.
+     * @param success Whether the operation was successful.
+     * @return Whether cache state is consistent.
+     */
+    public synchronized boolean endCopyObject(MtpObject object, boolean success) {
+        if (sDebug)
+            Log.v(TAG, "endCopyObject " + object.getName() + " " + success);
+        return generalEndCopyObject(object, success, false);
+    }
+
+    private synchronized boolean generalEndAddObject(MtpObject obj, boolean succeeded,
+            boolean removeGlobal) {
+        switch (obj.getState()) {
+            case FROZEN:
+                // Object was never created.
+                if (succeeded) {
+                    // The operation was successful so the event must still be in the queue.
+                    obj.setState(MtpObjectState.FROZEN_ONESHOT_ADD);
+                } else {
+                    // The operation failed and never created the file.
+                    if (!removeObjectFromCache(obj, removeGlobal, false)) {
+                        return false;
+                    }
+                }
+                break;
+            case FROZEN_ADDED:
+                obj.setState(MtpObjectState.NORMAL);
+                if (!succeeded) {
+                    MtpObject parent = obj.getParent();
+                    // The operation failed but some other process created the file. Send an event.
+                    if (!removeObjectFromCache(obj, removeGlobal, false))
+                        return false;
+                    handleAddedObject(parent, obj.getName(), obj.isDir());
+                }
+                // else: The operation successfully created the object.
+                break;
+            case FROZEN_REMOVED:
+                if (!removeObjectFromCache(obj, removeGlobal, false))
+                    return false;
+                if (succeeded) {
+                    // Some other process deleted the object. Send an event.
+                    mMtpNotifier.sendObjectRemoved(obj.getId());
+                }
+                // else: Mtp deleted the object as part of cleanup. Don't send an event.
+                break;
+            default:
+                return false;
+        }
+        return true;
+    }
+
+    private synchronized boolean generalEndRemoveObject(MtpObject obj, boolean success,
+            boolean removeGlobal) {
+        switch (obj.getState()) {
+            case FROZEN:
+                if (success) {
+                    // Object was deleted successfully, and event is still in the queue.
+                    obj.setState(MtpObjectState.FROZEN_ONESHOT_DEL);
+                } else {
+                    // Object was not deleted.
+                    obj.setState(MtpObjectState.NORMAL);
+                }
+                break;
+            case FROZEN_ADDED:
+                // Object was deleted, and then readded.
+                obj.setState(MtpObjectState.NORMAL);
+                if (success) {
+                    // Some other process readded the object.
+                    MtpObject parent = obj.getParent();
+                    if (!removeObjectFromCache(obj, removeGlobal, false))
+                        return false;
+                    handleAddedObject(parent, obj.getName(), obj.isDir());
+                }
+                // else : Object still exists after failure.
+                break;
+            case FROZEN_REMOVED:
+                if (!removeObjectFromCache(obj, removeGlobal, false))
+                    return false;
+                if (!success) {
+                    // Some other process deleted the object.
+                    mMtpNotifier.sendObjectRemoved(obj.getId());
+                }
+                // else : This process deleted the object as part of the operation.
+                break;
+            default:
+                return false;
+        }
+        return true;
+    }
+
+    private synchronized boolean generalBeginRenameObject(MtpObject fromObj, MtpObject toObj) {
+        fromObj.setState(MtpObjectState.FROZEN);
+        toObj.setState(MtpObjectState.FROZEN);
+        fromObj.setOperation(MtpOperation.RENAME);
+        toObj.setOperation(MtpOperation.RENAME);
+        return true;
+    }
+
+    private synchronized boolean generalEndRenameObject(MtpObject fromObj, MtpObject toObj,
+            boolean success) {
+        boolean ret = generalEndRemoveObject(fromObj, success, !success);
+        return generalEndAddObject(toObj, success, success) && ret;
+    }
+
+    private synchronized boolean generalBeginRemoveObject(MtpObject obj, MtpOperation op) {
+        obj.setState(MtpObjectState.FROZEN);
+        obj.setOperation(op);
+        if (obj.isDir()) {
+            for (MtpObject child : obj.getChildren())
+                generalBeginRemoveObject(child, op);
+        }
+        return true;
+    }
+
+    private synchronized boolean generalBeginCopyObject(MtpObject obj, boolean newId) {
+        obj.setState(MtpObjectState.FROZEN);
+        obj.setOperation(MtpOperation.COPY);
+        if (newId) {
+            obj.setId(getNextObjectId());
+            mObjects.put(obj.getId(), obj);
+        }
+        if (obj.isDir())
+            for (MtpObject child : obj.getChildren())
+                if (!generalBeginCopyObject(child, newId))
+                    return false;
+        return true;
+    }
+
+    private synchronized boolean generalEndCopyObject(MtpObject obj, boolean success, boolean addGlobal) {
+        if (success && addGlobal)
+            mObjects.put(obj.getId(), obj);
+        boolean ret = true;
+        if (obj.isDir()) {
+            for (MtpObject child : new ArrayList<>(obj.getChildren())) {
+                if (child.getOperation() == MtpOperation.COPY)
+                    ret = generalEndCopyObject(child, success, addGlobal) && ret;
+            }
+        }
+        ret = generalEndAddObject(obj, success, success || !addGlobal) && ret;
+        return ret;
+    }
+}
diff --git a/media/jni/android_mtp_MtpDatabase.cpp b/media/jni/android_mtp_MtpDatabase.cpp
index 4e8c72b..23ef84f6 100644
--- a/media/jni/android_mtp_MtpDatabase.cpp
+++ b/media/jni/android_mtp_MtpDatabase.cpp
@@ -19,7 +19,7 @@
 
 #include "android_media_Utils.h"
 #include "mtp.h"
-#include "MtpDatabase.h"
+#include "IMtpDatabase.h"
 #include "MtpDataPacket.h"
 #include "MtpObjectInfo.h"
 #include "MtpProperty.h"
@@ -55,7 +55,7 @@
 
 static jmethodID method_beginSendObject;
 static jmethodID method_endSendObject;
-static jmethodID method_doScanDirectory;
+static jmethodID method_rescanFile;
 static jmethodID method_getObjectList;
 static jmethodID method_getNumObjects;
 static jmethodID method_getSupportedPlaybackFormats;
@@ -68,35 +68,34 @@
 static jmethodID method_getObjectPropertyList;
 static jmethodID method_getObjectInfo;
 static jmethodID method_getObjectFilePath;
-static jmethodID method_deleteFile;
-static jmethodID method_moveObject;
+static jmethodID method_beginDeleteObject;
+static jmethodID method_endDeleteObject;
+static jmethodID method_beginMoveObject;
+static jmethodID method_endMoveObject;
+static jmethodID method_beginCopyObject;
+static jmethodID method_endCopyObject;
 static jmethodID method_getObjectReferences;
 static jmethodID method_setObjectReferences;
-static jmethodID method_sessionStarted;
-static jmethodID method_sessionEnded;
 
 static jfieldID field_context;
-static jfieldID field_batteryLevel;
-static jfieldID field_batteryScale;
-static jfieldID field_deviceType;
 
-// MtpPropertyList fields
-static jfieldID field_mCount;
-static jfieldID field_mResult;
-static jfieldID field_mObjectHandles;
-static jfieldID field_mPropertyCodes;
-static jfieldID field_mDataTypes;
-static jfieldID field_mLongValues;
-static jfieldID field_mStringValues;
+// MtpPropertyList methods
+static jmethodID method_getCode;
+static jmethodID method_getCount;
+static jmethodID method_getObjectHandles;
+static jmethodID method_getPropertyCodes;
+static jmethodID method_getDataTypes;
+static jmethodID method_getLongValues;
+static jmethodID method_getStringValues;
 
 
-MtpDatabase* getMtpDatabase(JNIEnv *env, jobject database) {
-    return (MtpDatabase *)env->GetLongField(database, field_context);
+IMtpDatabase* getMtpDatabase(JNIEnv *env, jobject database) {
+    return (IMtpDatabase *)env->GetLongField(database, field_context);
 }
 
 // ----------------------------------------------------------------------------
 
-class MyMtpDatabase : public MtpDatabase {
+class MtpDatabase : public IMtpDatabase {
 private:
     jobject         mDatabase;
     jintArray       mIntBuffer;
@@ -104,23 +103,20 @@
     jcharArray      mStringBuffer;
 
 public:
-                                    MyMtpDatabase(JNIEnv *env, jobject client);
-    virtual                         ~MyMtpDatabase();
+                                    MtpDatabase(JNIEnv *env, jobject client);
+    virtual                         ~MtpDatabase();
     void                            cleanup(JNIEnv *env);
 
     virtual MtpObjectHandle         beginSendObject(const char* path,
                                             MtpObjectFormat format,
                                             MtpObjectHandle parent,
-                                            MtpStorageID storage,
-                                            uint64_t size,
-                                            time_t modified);
+                                            MtpStorageID storage);
 
-    virtual void                    endSendObject(const char* path,
+    virtual void                    endSendObject(MtpObjectHandle handle, bool succeeded);
+
+    virtual void                    rescanFile(const char* path,
                                             MtpObjectHandle handle,
-                                            MtpObjectFormat format,
-                                            bool succeeded);
-
-    virtual void                    doScanDirectory(const char* path);
+                                            MtpObjectFormat format);
 
     virtual MtpObjectHandleList*    getObjectList(MtpStorageID storageID,
                                     MtpObjectFormat format,
@@ -167,7 +163,8 @@
                                             MtpString& outFilePath,
                                             int64_t& outFileLength,
                                             MtpObjectFormat& outFormat);
-    virtual MtpResponseCode         deleteFile(MtpObjectHandle handle);
+    virtual MtpResponseCode         beginDeleteObject(MtpObjectHandle handle);
+    virtual void                    endDeleteObject(MtpObjectHandle handle, bool succeeded);
 
     bool                            getObjectPropertyInfo(MtpObjectProperty property, int& type);
     bool                            getDevicePropertyInfo(MtpDeviceProperty property, int& type);
@@ -182,12 +179,17 @@
 
     virtual MtpProperty*            getDevicePropertyDesc(MtpDeviceProperty property);
 
-    virtual MtpResponseCode         moveObject(MtpObjectHandle handle, MtpObjectHandle newParent,
-                                            MtpStorageID newStorage, MtpString& newPath);
+    virtual MtpResponseCode         beginMoveObject(MtpObjectHandle handle, MtpObjectHandle newParent,
+                                            MtpStorageID newStorage);
 
-    virtual void                    sessionStarted();
+    virtual void                    endMoveObject(MtpObjectHandle oldParent, MtpObjectHandle newParent,
+                                            MtpStorageID oldStorage, MtpStorageID newStorage,
+                                             MtpObjectHandle handle, bool succeeded);
 
-    virtual void                    sessionEnded();
+    virtual MtpResponseCode         beginCopyObject(MtpObjectHandle handle, MtpObjectHandle newParent,
+                                            MtpStorageID newStorage);
+    virtual void                    endCopyObject(MtpObjectHandle handle, bool succeeded);
+
 };
 
 // ----------------------------------------------------------------------------
@@ -202,7 +204,7 @@
 
 // ----------------------------------------------------------------------------
 
-MyMtpDatabase::MyMtpDatabase(JNIEnv *env, jobject client)
+MtpDatabase::MtpDatabase(JNIEnv *env, jobject client)
     :   mDatabase(env->NewGlobalRef(client)),
         mIntBuffer(NULL),
         mLongBuffer(NULL),
@@ -228,27 +230,24 @@
     mStringBuffer = (jcharArray)env->NewGlobalRef(charArray);
 }
 
-void MyMtpDatabase::cleanup(JNIEnv *env) {
+void MtpDatabase::cleanup(JNIEnv *env) {
     env->DeleteGlobalRef(mDatabase);
     env->DeleteGlobalRef(mIntBuffer);
     env->DeleteGlobalRef(mLongBuffer);
     env->DeleteGlobalRef(mStringBuffer);
 }
 
-MyMtpDatabase::~MyMtpDatabase() {
+MtpDatabase::~MtpDatabase() {
 }
 
-MtpObjectHandle MyMtpDatabase::beginSendObject(const char* path,
+MtpObjectHandle MtpDatabase::beginSendObject(const char* path,
                                                MtpObjectFormat format,
                                                MtpObjectHandle parent,
-                                               MtpStorageID storage,
-                                               uint64_t size,
-                                               time_t modified) {
+                                               MtpStorageID storage) {
     JNIEnv* env = AndroidRuntime::getJNIEnv();
     jstring pathStr = env->NewStringUTF(path);
     MtpObjectHandle result = env->CallIntMethod(mDatabase, method_beginSendObject,
-            pathStr, (jint)format, (jint)parent, (jint)storage,
-            (jlong)size, (jlong)modified);
+            pathStr, (jint)format, (jint)parent, (jint)storage);
 
     if (pathStr)
         env->DeleteLocalRef(pathStr);
@@ -256,29 +255,26 @@
     return result;
 }
 
-void MyMtpDatabase::endSendObject(const char* path, MtpObjectHandle handle,
-                                  MtpObjectFormat format, bool succeeded) {
+void MtpDatabase::endSendObject(MtpObjectHandle handle, bool succeeded) {
+    JNIEnv* env = AndroidRuntime::getJNIEnv();
+    env->CallVoidMethod(mDatabase, method_endSendObject, (jint)handle, (jboolean)succeeded);
+
+    checkAndClearExceptionFromCallback(env, __FUNCTION__);
+}
+
+void MtpDatabase::rescanFile(const char* path, MtpObjectHandle handle,
+                                  MtpObjectFormat format) {
     JNIEnv* env = AndroidRuntime::getJNIEnv();
     jstring pathStr = env->NewStringUTF(path);
-    env->CallVoidMethod(mDatabase, method_endSendObject, pathStr,
-                        (jint)handle, (jint)format, (jboolean)succeeded);
+    env->CallVoidMethod(mDatabase, method_rescanFile, pathStr,
+                        (jint)handle, (jint)format);
 
     if (pathStr)
         env->DeleteLocalRef(pathStr);
     checkAndClearExceptionFromCallback(env, __FUNCTION__);
 }
 
-void MyMtpDatabase::doScanDirectory(const char* path) {
-    JNIEnv* env = AndroidRuntime::getJNIEnv();
-    jstring pathStr = env->NewStringUTF(path);
-    env->CallVoidMethod(mDatabase, method_doScanDirectory, pathStr);
-
-    if (pathStr)
-        env->DeleteLocalRef(pathStr);
-    checkAndClearExceptionFromCallback(env, __FUNCTION__);
-}
-
-MtpObjectHandleList* MyMtpDatabase::getObjectList(MtpStorageID storageID,
+MtpObjectHandleList* MtpDatabase::getObjectList(MtpStorageID storageID,
                                                   MtpObjectFormat format,
                                                   MtpObjectHandle parent) {
     JNIEnv* env = AndroidRuntime::getJNIEnv();
@@ -298,7 +294,7 @@
     return list;
 }
 
-int MyMtpDatabase::getNumObjects(MtpStorageID storageID,
+int MtpDatabase::getNumObjects(MtpStorageID storageID,
                                  MtpObjectFormat format,
                                  MtpObjectHandle parent) {
     JNIEnv* env = AndroidRuntime::getJNIEnv();
@@ -309,7 +305,7 @@
     return result;
 }
 
-MtpObjectFormatList* MyMtpDatabase::getSupportedPlaybackFormats() {
+MtpObjectFormatList* MtpDatabase::getSupportedPlaybackFormats() {
     JNIEnv* env = AndroidRuntime::getJNIEnv();
     jintArray array = (jintArray)env->CallObjectMethod(mDatabase,
             method_getSupportedPlaybackFormats);
@@ -327,7 +323,7 @@
     return list;
 }
 
-MtpObjectFormatList* MyMtpDatabase::getSupportedCaptureFormats() {
+MtpObjectFormatList* MtpDatabase::getSupportedCaptureFormats() {
     JNIEnv* env = AndroidRuntime::getJNIEnv();
     jintArray array = (jintArray)env->CallObjectMethod(mDatabase,
             method_getSupportedCaptureFormats);
@@ -345,7 +341,7 @@
     return list;
 }
 
-MtpObjectPropertyList* MyMtpDatabase::getSupportedObjectProperties(MtpObjectFormat format) {
+MtpObjectPropertyList* MtpDatabase::getSupportedObjectProperties(MtpObjectFormat format) {
     JNIEnv* env = AndroidRuntime::getJNIEnv();
     jintArray array = (jintArray)env->CallObjectMethod(mDatabase,
             method_getSupportedObjectProperties, (jint)format);
@@ -363,7 +359,7 @@
     return list;
 }
 
-MtpDevicePropertyList* MyMtpDatabase::getSupportedDeviceProperties() {
+MtpDevicePropertyList* MtpDatabase::getSupportedDeviceProperties() {
     JNIEnv* env = AndroidRuntime::getJNIEnv();
     jintArray array = (jintArray)env->CallObjectMethod(mDatabase,
             method_getSupportedDeviceProperties);
@@ -381,7 +377,7 @@
     return list;
 }
 
-MtpResponseCode MyMtpDatabase::getObjectPropertyValue(MtpObjectHandle handle,
+MtpResponseCode MtpDatabase::getObjectPropertyValue(MtpObjectHandle handle,
                                                       MtpObjectProperty property,
                                                       MtpDataPacket& packet) {
     static_assert(sizeof(jint) >= sizeof(MtpObjectHandle),
@@ -397,42 +393,26 @@
             static_cast<jint>(property),
             0,
             0);
-    MtpResponseCode result = env->GetIntField(list, field_mResult);
-    int count = env->GetIntField(list, field_mCount);
-    if (result == MTP_RESPONSE_OK && count != 1)
+    MtpResponseCode result = env->CallIntMethod(list, method_getCode);
+    jint count = env->CallIntMethod(list, method_getCount);
+    if (count != 1)
         result = MTP_RESPONSE_GENERAL_ERROR;
 
     if (result == MTP_RESPONSE_OK) {
-        jintArray objectHandlesArray = (jintArray)env->GetObjectField(list, field_mObjectHandles);
-        jintArray propertyCodesArray = (jintArray)env->GetObjectField(list, field_mPropertyCodes);
-        jintArray dataTypesArray = (jintArray)env->GetObjectField(list, field_mDataTypes);
-        jlongArray longValuesArray = (jlongArray)env->GetObjectField(list, field_mLongValues);
-        jobjectArray stringValuesArray = (jobjectArray)env->GetObjectField(list, field_mStringValues);
+        jintArray objectHandlesArray = (jintArray)env->CallObjectMethod(list, method_getObjectHandles);
+        jintArray propertyCodesArray = (jintArray)env->CallObjectMethod(list, method_getPropertyCodes);
+        jintArray dataTypesArray = (jintArray)env->CallObjectMethod(list, method_getDataTypes);
+        jlongArray longValuesArray = (jlongArray)env->CallObjectMethod(list, method_getLongValues);
+        jobjectArray stringValuesArray = (jobjectArray)env->CallObjectMethod(list, method_getStringValues);
 
         jint* objectHandles = env->GetIntArrayElements(objectHandlesArray, 0);
         jint* propertyCodes = env->GetIntArrayElements(propertyCodesArray, 0);
         jint* dataTypes = env->GetIntArrayElements(dataTypesArray, 0);
-        jlong* longValues = (longValuesArray ? env->GetLongArrayElements(longValuesArray, 0) : NULL);
+        jlong* longValues = env->GetLongArrayElements(longValuesArray, 0);
 
         int type = dataTypes[0];
         jlong longValue = (longValues ? longValues[0] : 0);
 
-        // special case date properties, which are strings to MTP
-        // but stored internally as a uint64
-        if (property == MTP_PROPERTY_DATE_MODIFIED || property == MTP_PROPERTY_DATE_ADDED) {
-            char    date[20];
-            formatDateTime(longValue, date, sizeof(date));
-            packet.putString(date);
-            goto out;
-        }
-        // release date is stored internally as just the year
-        if (property == MTP_PROPERTY_ORIGINAL_RELEASE_DATE) {
-            char    date[20];
-            snprintf(date, sizeof(date), "%04" PRId64 "0101T000000", longValue);
-            packet.putString(date);
-            goto out;
-        }
-
         switch (type) {
             case MTP_TYPE_INT8:
                 packet.putInt8(longValue);
@@ -481,20 +461,16 @@
                 ALOGE("unsupported type in getObjectPropertyValue\n");
                 result = MTP_RESPONSE_INVALID_OBJECT_PROP_FORMAT;
         }
-out:
         env->ReleaseIntArrayElements(objectHandlesArray, objectHandles, 0);
         env->ReleaseIntArrayElements(propertyCodesArray, propertyCodes, 0);
         env->ReleaseIntArrayElements(dataTypesArray, dataTypes, 0);
-        if (longValues)
-            env->ReleaseLongArrayElements(longValuesArray, longValues, 0);
+        env->ReleaseLongArrayElements(longValuesArray, longValues, 0);
 
         env->DeleteLocalRef(objectHandlesArray);
         env->DeleteLocalRef(propertyCodesArray);
         env->DeleteLocalRef(dataTypesArray);
-        if (longValuesArray)
-            env->DeleteLocalRef(longValuesArray);
-        if (stringValuesArray)
-            env->DeleteLocalRef(stringValuesArray);
+        env->DeleteLocalRef(longValuesArray);
+        env->DeleteLocalRef(stringValuesArray);
     }
 
     env->DeleteLocalRef(list);
@@ -559,7 +535,7 @@
     return true;
 }
 
-MtpResponseCode MyMtpDatabase::setObjectPropertyValue(MtpObjectHandle handle,
+MtpResponseCode MtpDatabase::setObjectPropertyValue(MtpObjectHandle handle,
                                                       MtpObjectProperty property,
                                                       MtpDataPacket& packet) {
     int         type;
@@ -590,80 +566,73 @@
     return result;
 }
 
-MtpResponseCode MyMtpDatabase::getDevicePropertyValue(MtpDeviceProperty property,
+MtpResponseCode MtpDatabase::getDevicePropertyValue(MtpDeviceProperty property,
                                                       MtpDataPacket& packet) {
     JNIEnv* env = AndroidRuntime::getJNIEnv();
+    int type;
 
-    if (property == MTP_DEVICE_PROPERTY_BATTERY_LEVEL) {
-        // special case - implemented here instead of Java
-        packet.putUInt8((uint8_t)env->GetIntField(mDatabase, field_batteryLevel));
-        return MTP_RESPONSE_OK;
-    } else {
-        int type;
+    if (!getDevicePropertyInfo(property, type))
+        return MTP_RESPONSE_DEVICE_PROP_NOT_SUPPORTED;
 
-        if (!getDevicePropertyInfo(property, type))
-            return MTP_RESPONSE_DEVICE_PROP_NOT_SUPPORTED;
-
-        jint result = env->CallIntMethod(mDatabase, method_getDeviceProperty,
-                    (jint)property, mLongBuffer, mStringBuffer);
-        if (result != MTP_RESPONSE_OK) {
-            checkAndClearExceptionFromCallback(env, __FUNCTION__);
-            return result;
-        }
-
-        jlong* longValues = env->GetLongArrayElements(mLongBuffer, 0);
-        jlong longValue = longValues[0];
-        env->ReleaseLongArrayElements(mLongBuffer, longValues, 0);
-
-        switch (type) {
-            case MTP_TYPE_INT8:
-                packet.putInt8(longValue);
-                break;
-            case MTP_TYPE_UINT8:
-                packet.putUInt8(longValue);
-                break;
-            case MTP_TYPE_INT16:
-                packet.putInt16(longValue);
-                break;
-            case MTP_TYPE_UINT16:
-                packet.putUInt16(longValue);
-                break;
-            case MTP_TYPE_INT32:
-                packet.putInt32(longValue);
-                break;
-            case MTP_TYPE_UINT32:
-                packet.putUInt32(longValue);
-                break;
-            case MTP_TYPE_INT64:
-                packet.putInt64(longValue);
-                break;
-            case MTP_TYPE_UINT64:
-                packet.putUInt64(longValue);
-                break;
-            case MTP_TYPE_INT128:
-                packet.putInt128(longValue);
-                break;
-            case MTP_TYPE_UINT128:
-                packet.putInt128(longValue);
-                break;
-            case MTP_TYPE_STR:
-            {
-                jchar* str = env->GetCharArrayElements(mStringBuffer, 0);
-                packet.putString(str);
-                env->ReleaseCharArrayElements(mStringBuffer, str, 0);
-                break;
-             }
-            default:
-                ALOGE("unsupported type in getDevicePropertyValue\n");
-                return MTP_RESPONSE_INVALID_DEVICE_PROP_FORMAT;
-        }
-
+    jint result = env->CallIntMethod(mDatabase, method_getDeviceProperty,
+                (jint)property, mLongBuffer, mStringBuffer);
+    if (result != MTP_RESPONSE_OK) {
         checkAndClearExceptionFromCallback(env, __FUNCTION__);
-        return MTP_RESPONSE_OK;
+        return result;
     }
+
+    jlong* longValues = env->GetLongArrayElements(mLongBuffer, 0);
+    jlong longValue = longValues[0];
+    env->ReleaseLongArrayElements(mLongBuffer, longValues, 0);
+
+    switch (type) {
+        case MTP_TYPE_INT8:
+            packet.putInt8(longValue);
+            break;
+        case MTP_TYPE_UINT8:
+            packet.putUInt8(longValue);
+            break;
+        case MTP_TYPE_INT16:
+            packet.putInt16(longValue);
+            break;
+        case MTP_TYPE_UINT16:
+            packet.putUInt16(longValue);
+            break;
+        case MTP_TYPE_INT32:
+            packet.putInt32(longValue);
+            break;
+        case MTP_TYPE_UINT32:
+            packet.putUInt32(longValue);
+            break;
+        case MTP_TYPE_INT64:
+            packet.putInt64(longValue);
+            break;
+        case MTP_TYPE_UINT64:
+            packet.putUInt64(longValue);
+            break;
+        case MTP_TYPE_INT128:
+            packet.putInt128(longValue);
+            break;
+        case MTP_TYPE_UINT128:
+            packet.putInt128(longValue);
+            break;
+        case MTP_TYPE_STR:
+        {
+            jchar* str = env->GetCharArrayElements(mStringBuffer, 0);
+            packet.putString(str);
+            env->ReleaseCharArrayElements(mStringBuffer, str, 0);
+            break;
+        }
+        default:
+            ALOGE("unsupported type in getDevicePropertyValue\n");
+            return MTP_RESPONSE_INVALID_DEVICE_PROP_FORMAT;
+    }
+
+    checkAndClearExceptionFromCallback(env, __FUNCTION__);
+    return MTP_RESPONSE_OK;
 }
 
-MtpResponseCode MyMtpDatabase::setDevicePropertyValue(MtpDeviceProperty property,
+MtpResponseCode MtpDatabase::setDevicePropertyValue(MtpDeviceProperty property,
                                                       MtpDataPacket& packet) {
     int         type;
 
@@ -693,11 +662,11 @@
     return result;
 }
 
-MtpResponseCode MyMtpDatabase::resetDeviceProperty(MtpDeviceProperty /*property*/) {
+MtpResponseCode MtpDatabase::resetDeviceProperty(MtpDeviceProperty /*property*/) {
     return -1;
 }
 
-MtpResponseCode MyMtpDatabase::getObjectPropertyList(MtpObjectHandle handle,
+MtpResponseCode MtpDatabase::getObjectPropertyList(MtpObjectHandle handle,
                                                      uint32_t format, uint32_t property,
                                                      int groupCode, int depth,
                                                      MtpDataPacket& packet) {
@@ -715,16 +684,16 @@
     checkAndClearExceptionFromCallback(env, __FUNCTION__);
     if (!list)
         return MTP_RESPONSE_GENERAL_ERROR;
-    int count = env->GetIntField(list, field_mCount);
-    MtpResponseCode result = env->GetIntField(list, field_mResult);
+    int count = env->CallIntMethod(list, method_getCount);
+    MtpResponseCode result = env->CallIntMethod(list, method_getCode);
 
     packet.putUInt32(count);
     if (count > 0) {
-        jintArray objectHandlesArray = (jintArray)env->GetObjectField(list, field_mObjectHandles);
-        jintArray propertyCodesArray = (jintArray)env->GetObjectField(list, field_mPropertyCodes);
-        jintArray dataTypesArray = (jintArray)env->GetObjectField(list, field_mDataTypes);
-        jlongArray longValuesArray = (jlongArray)env->GetObjectField(list, field_mLongValues);
-        jobjectArray stringValuesArray = (jobjectArray)env->GetObjectField(list, field_mStringValues);
+        jintArray objectHandlesArray = (jintArray)env->CallObjectMethod(list, method_getObjectHandles);
+        jintArray propertyCodesArray = (jintArray)env->CallObjectMethod(list, method_getPropertyCodes);
+        jintArray dataTypesArray = (jintArray)env->CallObjectMethod(list, method_getDataTypes);
+        jlongArray longValuesArray = (jlongArray)env->CallObjectMethod(list, method_getLongValues);
+        jobjectArray stringValuesArray = (jobjectArray)env->CallObjectMethod(list, method_getStringValues);
 
         jint* objectHandles = env->GetIntArrayElements(objectHandlesArray, 0);
         jint* propertyCodes = env->GetIntArrayElements(propertyCodesArray, 0);
@@ -781,7 +750,7 @@
                     break;
                 }
                 default:
-                    ALOGE("bad or unsupported data type in MyMtpDatabase::getObjectPropertyList");
+                    ALOGE("bad or unsupported data type in MtpDatabase::getObjectPropertyList");
                     break;
             }
         }
@@ -789,16 +758,13 @@
         env->ReleaseIntArrayElements(objectHandlesArray, objectHandles, 0);
         env->ReleaseIntArrayElements(propertyCodesArray, propertyCodes, 0);
         env->ReleaseIntArrayElements(dataTypesArray, dataTypes, 0);
-        if (longValues)
-            env->ReleaseLongArrayElements(longValuesArray, longValues, 0);
+        env->ReleaseLongArrayElements(longValuesArray, longValues, 0);
 
         env->DeleteLocalRef(objectHandlesArray);
         env->DeleteLocalRef(propertyCodesArray);
         env->DeleteLocalRef(dataTypesArray);
-        if (longValuesArray)
-            env->DeleteLocalRef(longValuesArray);
-        if (stringValuesArray)
-            env->DeleteLocalRef(stringValuesArray);
+        env->DeleteLocalRef(longValuesArray);
+        env->DeleteLocalRef(stringValuesArray);
     }
 
     env->DeleteLocalRef(list);
@@ -822,7 +788,7 @@
     return exif_get_long(e->data, o);
 }
 
-MtpResponseCode MyMtpDatabase::getObjectInfo(MtpObjectHandle handle,
+MtpResponseCode MtpDatabase::getObjectInfo(MtpObjectHandle handle,
                                              MtpObjectInfo& info) {
     MtpString       path;
     int64_t         length;
@@ -914,7 +880,7 @@
     return MTP_RESPONSE_OK;
 }
 
-void* MyMtpDatabase::getThumbnail(MtpObjectHandle handle, size_t& outThumbSize) {
+void* MtpDatabase::getThumbnail(MtpObjectHandle handle, size_t& outThumbSize) {
     MtpString path;
     int64_t length;
     MtpObjectFormat format;
@@ -979,7 +945,7 @@
     return result;
 }
 
-MtpResponseCode MyMtpDatabase::getObjectFilePath(MtpObjectHandle handle,
+MtpResponseCode MtpDatabase::getObjectFilePath(MtpObjectHandle handle,
                                                  MtpString& outFilePath,
                                                  int64_t& outFileLength,
                                                  MtpObjectFormat& outFormat) {
@@ -1005,26 +971,60 @@
     return result;
 }
 
-MtpResponseCode MyMtpDatabase::deleteFile(MtpObjectHandle handle) {
+MtpResponseCode MtpDatabase::beginDeleteObject(MtpObjectHandle handle) {
     JNIEnv* env = AndroidRuntime::getJNIEnv();
-    MtpResponseCode result = env->CallIntMethod(mDatabase, method_deleteFile, (jint)handle);
+    MtpResponseCode result = env->CallIntMethod(mDatabase, method_beginDeleteObject, (jint)handle);
 
     checkAndClearExceptionFromCallback(env, __FUNCTION__);
     return result;
 }
 
-MtpResponseCode MyMtpDatabase::moveObject(MtpObjectHandle handle, MtpObjectHandle newParent,
-        MtpStorageID newStorage, MtpString &newPath) {
+void MtpDatabase::endDeleteObject(MtpObjectHandle handle, bool succeeded) {
     JNIEnv* env = AndroidRuntime::getJNIEnv();
-    jstring stringValue = env->NewStringUTF((const char *) newPath);
-    MtpResponseCode result = env->CallIntMethod(mDatabase, method_moveObject,
-                (jint)handle, (jint)newParent, (jint) newStorage, stringValue);
+    env->CallVoidMethod(mDatabase, method_endDeleteObject, (jint)handle, (jboolean) succeeded);
 
     checkAndClearExceptionFromCallback(env, __FUNCTION__);
-    env->DeleteLocalRef(stringValue);
+}
+
+MtpResponseCode MtpDatabase::beginMoveObject(MtpObjectHandle handle, MtpObjectHandle newParent,
+        MtpStorageID newStorage) {
+    JNIEnv* env = AndroidRuntime::getJNIEnv();
+    MtpResponseCode result = env->CallIntMethod(mDatabase, method_beginMoveObject,
+                (jint)handle, (jint)newParent, (jint) newStorage);
+
+    checkAndClearExceptionFromCallback(env, __FUNCTION__);
     return result;
 }
 
+void MtpDatabase::endMoveObject(MtpObjectHandle oldParent, MtpObjectHandle newParent,
+                                            MtpStorageID oldStorage, MtpStorageID newStorage,
+                                             MtpObjectHandle handle, bool succeeded) {
+    JNIEnv* env = AndroidRuntime::getJNIEnv();
+    env->CallVoidMethod(mDatabase, method_endMoveObject,
+                (jint)oldParent, (jint) newParent, (jint) oldStorage, (jint) newStorage,
+                (jint) handle, (jboolean) succeeded);
+
+    checkAndClearExceptionFromCallback(env, __FUNCTION__);
+}
+
+MtpResponseCode MtpDatabase::beginCopyObject(MtpObjectHandle handle, MtpObjectHandle newParent,
+        MtpStorageID newStorage) {
+    JNIEnv* env = AndroidRuntime::getJNIEnv();
+    MtpResponseCode result = env->CallIntMethod(mDatabase, method_beginCopyObject,
+                (jint)handle, (jint)newParent, (jint) newStorage);
+
+    checkAndClearExceptionFromCallback(env, __FUNCTION__);
+    return result;
+}
+
+void MtpDatabase::endCopyObject(MtpObjectHandle handle, bool succeeded) {
+    JNIEnv* env = AndroidRuntime::getJNIEnv();
+    env->CallVoidMethod(mDatabase, method_endCopyObject, (jint)handle, (jboolean)succeeded);
+
+    checkAndClearExceptionFromCallback(env, __FUNCTION__);
+}
+
+
 struct PropertyTableEntry {
     MtpObjectProperty   property;
     int                 type;
@@ -1066,7 +1066,7 @@
     {   MTP_DEVICE_PROPERTY_PERCEIVED_DEVICE_TYPE,      MTP_TYPE_UINT32 },
 };
 
-bool MyMtpDatabase::getObjectPropertyInfo(MtpObjectProperty property, int& type) {
+bool MtpDatabase::getObjectPropertyInfo(MtpObjectProperty property, int& type) {
     int count = sizeof(kObjectPropertyTable) / sizeof(kObjectPropertyTable[0]);
     const PropertyTableEntry* entry = kObjectPropertyTable;
     for (int i = 0; i < count; i++, entry++) {
@@ -1078,7 +1078,7 @@
     return false;
 }
 
-bool MyMtpDatabase::getDevicePropertyInfo(MtpDeviceProperty property, int& type) {
+bool MtpDatabase::getDevicePropertyInfo(MtpDeviceProperty property, int& type) {
     int count = sizeof(kDevicePropertyTable) / sizeof(kDevicePropertyTable[0]);
     const PropertyTableEntry* entry = kDevicePropertyTable;
     for (int i = 0; i < count; i++, entry++) {
@@ -1090,7 +1090,7 @@
     return false;
 }
 
-MtpObjectHandleList* MyMtpDatabase::getObjectReferences(MtpObjectHandle handle) {
+MtpObjectHandleList* MtpDatabase::getObjectReferences(MtpObjectHandle handle) {
     JNIEnv* env = AndroidRuntime::getJNIEnv();
     jintArray array = (jintArray)env->CallObjectMethod(mDatabase, method_getObjectReferences,
                 (jint)handle);
@@ -1108,7 +1108,7 @@
     return list;
 }
 
-MtpResponseCode MyMtpDatabase::setObjectReferences(MtpObjectHandle handle,
+MtpResponseCode MtpDatabase::setObjectReferences(MtpObjectHandle handle,
                                                    MtpObjectHandleList* references) {
     JNIEnv* env = AndroidRuntime::getJNIEnv();
     int count = references->size();
@@ -1129,7 +1129,7 @@
     return result;
 }
 
-MtpProperty* MyMtpDatabase::getObjectPropertyDesc(MtpObjectProperty property,
+MtpProperty* MtpDatabase::getObjectPropertyDesc(MtpObjectProperty property,
                                                   MtpObjectFormat format) {
     static const int channelEnum[] = {
                                         1,  // mono
@@ -1210,67 +1210,65 @@
     return result;
 }
 
-MtpProperty* MyMtpDatabase::getDevicePropertyDesc(MtpDeviceProperty property) {
+MtpProperty* MtpDatabase::getDevicePropertyDesc(MtpDeviceProperty property) {
     JNIEnv* env = AndroidRuntime::getJNIEnv();
     MtpProperty* result = NULL;
     bool writable = false;
 
-    switch (property) {
-        case MTP_DEVICE_PROPERTY_SYNCHRONIZATION_PARTNER:
-        case MTP_DEVICE_PROPERTY_DEVICE_FRIENDLY_NAME:
-            writable = true;
-            // fall through
-        case MTP_DEVICE_PROPERTY_IMAGE_SIZE: {
-            result = new MtpProperty(property, MTP_TYPE_STR, writable);
-
-            // get current value
-            jint ret = env->CallIntMethod(mDatabase, method_getDeviceProperty,
-                        (jint)property, mLongBuffer, mStringBuffer);
-            if (ret == MTP_RESPONSE_OK) {
+    // get current value
+    jint ret = env->CallIntMethod(mDatabase, method_getDeviceProperty,
+        (jint)property, mLongBuffer, mStringBuffer);
+    if (ret == MTP_RESPONSE_OK) {
+        switch (property) {
+            case MTP_DEVICE_PROPERTY_SYNCHRONIZATION_PARTNER:
+            case MTP_DEVICE_PROPERTY_DEVICE_FRIENDLY_NAME:
+                writable = true;
+                // fall through
+            case MTP_DEVICE_PROPERTY_IMAGE_SIZE:
+            {
+                result = new MtpProperty(property, MTP_TYPE_STR, writable);
                 jchar* str = env->GetCharArrayElements(mStringBuffer, 0);
                 result->setCurrentValue(str);
                 // for read-only properties it is safe to assume current value is default value
                 if (!writable)
                     result->setDefaultValue(str);
                 env->ReleaseCharArrayElements(mStringBuffer, str, 0);
-            } else {
-                ALOGE("unable to read device property, response: %04X", ret);
+                break;
             }
-            break;
+            case MTP_DEVICE_PROPERTY_BATTERY_LEVEL:
+            {
+                result = new MtpProperty(property, MTP_TYPE_UINT8);
+                jlong* arr = env->GetLongArrayElements(mLongBuffer, 0);
+                result->setFormRange(0, arr[1], 1);
+                result->mCurrentValue.u.u8 = (uint8_t) arr[0];
+                env->ReleaseLongArrayElements(mLongBuffer, arr, 0);
+                break;
+            }
+            case MTP_DEVICE_PROPERTY_PERCEIVED_DEVICE_TYPE:
+            {
+                jlong* arr = env->GetLongArrayElements(mLongBuffer, 0);
+                result = new MtpProperty(property, MTP_TYPE_UINT32);
+                result->mCurrentValue.u.u32 = (uint32_t) arr[0];
+                env->ReleaseLongArrayElements(mLongBuffer, arr, 0);
+                break;
+            }
+            default:
+                ALOGE("Unrecognized property %x", property);
         }
-        case MTP_DEVICE_PROPERTY_BATTERY_LEVEL:
-            result = new MtpProperty(property, MTP_TYPE_UINT8);
-            result->setFormRange(0, env->GetIntField(mDatabase, field_batteryScale), 1);
-            result->mCurrentValue.u.u8 = (uint8_t)env->GetIntField(mDatabase, field_batteryLevel);
-            break;
-        case MTP_DEVICE_PROPERTY_PERCEIVED_DEVICE_TYPE:
-            result = new MtpProperty(property, MTP_TYPE_UINT32);
-            result->mCurrentValue.u.u32 = (uint32_t)env->GetIntField(mDatabase, field_deviceType);
-            break;
+    } else {
+        ALOGE("unable to read device property, response: %04X", ret);
     }
 
     checkAndClearExceptionFromCallback(env, __FUNCTION__);
     return result;
 }
 
-void MyMtpDatabase::sessionStarted() {
-    JNIEnv* env = AndroidRuntime::getJNIEnv();
-    env->CallVoidMethod(mDatabase, method_sessionStarted);
-    checkAndClearExceptionFromCallback(env, __FUNCTION__);
-}
-
-void MyMtpDatabase::sessionEnded() {
-    JNIEnv* env = AndroidRuntime::getJNIEnv();
-    env->CallVoidMethod(mDatabase, method_sessionEnded);
-    checkAndClearExceptionFromCallback(env, __FUNCTION__);
-}
-
 // ----------------------------------------------------------------------------
 
 static void
 android_mtp_MtpDatabase_setup(JNIEnv *env, jobject thiz)
 {
-    MyMtpDatabase* database = new MyMtpDatabase(env, thiz);
+    MtpDatabase* database = new MtpDatabase(env, thiz);
     env->SetLongField(thiz, field_context, (jlong)database);
     checkAndClearExceptionFromCallback(env, __FUNCTION__);
 }
@@ -1278,7 +1276,7 @@
 static void
 android_mtp_MtpDatabase_finalize(JNIEnv *env, jobject thiz)
 {
-    MyMtpDatabase* database = (MyMtpDatabase *)env->GetLongField(thiz, field_context);
+    MtpDatabase* database = (MtpDatabase *)env->GetLongField(thiz, field_context);
     database->cleanup(env);
     delete database;
     env->SetLongField(thiz, field_context, 0);
@@ -1305,6 +1303,13 @@
                                         (void *)android_mtp_MtpPropertyGroup_format_date_time},
 };
 
+#define GET_METHOD_ID(name, jclass, signature)                              \
+    method_##name = env->GetMethodID(jclass, #name, signature);             \
+    if (method_##name == NULL) {                                            \
+        ALOGE("Can't find " #name);                                         \
+        return -1;                                                          \
+    }                                                                       \
+
 int register_android_mtp_MtpDatabase(JNIEnv *env)
 {
     jclass clazz;
@@ -1314,175 +1319,48 @@
         ALOGE("Can't find android/mtp/MtpDatabase");
         return -1;
     }
-    method_beginSendObject = env->GetMethodID(clazz, "beginSendObject", "(Ljava/lang/String;IIIJJ)I");
-    if (method_beginSendObject == NULL) {
-        ALOGE("Can't find beginSendObject");
-        return -1;
-    }
-    method_endSendObject = env->GetMethodID(clazz, "endSendObject", "(Ljava/lang/String;IIZ)V");
-    if (method_endSendObject == NULL) {
-        ALOGE("Can't find endSendObject");
-        return -1;
-    }
-    method_doScanDirectory = env->GetMethodID(clazz, "doScanDirectory", "(Ljava/lang/String;)V");
-    if (method_doScanDirectory == NULL) {
-        ALOGE("Can't find doScanDirectory");
-        return -1;
-    }
-    method_getObjectList = env->GetMethodID(clazz, "getObjectList", "(III)[I");
-    if (method_getObjectList == NULL) {
-        ALOGE("Can't find getObjectList");
-        return -1;
-    }
-    method_getNumObjects = env->GetMethodID(clazz, "getNumObjects", "(III)I");
-    if (method_getNumObjects == NULL) {
-        ALOGE("Can't find getNumObjects");
-        return -1;
-    }
-    method_getSupportedPlaybackFormats = env->GetMethodID(clazz, "getSupportedPlaybackFormats", "()[I");
-    if (method_getSupportedPlaybackFormats == NULL) {
-        ALOGE("Can't find getSupportedPlaybackFormats");
-        return -1;
-    }
-    method_getSupportedCaptureFormats = env->GetMethodID(clazz, "getSupportedCaptureFormats", "()[I");
-    if (method_getSupportedCaptureFormats == NULL) {
-        ALOGE("Can't find getSupportedCaptureFormats");
-        return -1;
-    }
-    method_getSupportedObjectProperties = env->GetMethodID(clazz, "getSupportedObjectProperties", "(I)[I");
-    if (method_getSupportedObjectProperties == NULL) {
-        ALOGE("Can't find getSupportedObjectProperties");
-        return -1;
-    }
-    method_getSupportedDeviceProperties = env->GetMethodID(clazz, "getSupportedDeviceProperties", "()[I");
-    if (method_getSupportedDeviceProperties == NULL) {
-        ALOGE("Can't find getSupportedDeviceProperties");
-        return -1;
-    }
-    method_setObjectProperty = env->GetMethodID(clazz, "setObjectProperty", "(IIJLjava/lang/String;)I");
-    if (method_setObjectProperty == NULL) {
-        ALOGE("Can't find setObjectProperty");
-        return -1;
-    }
-    method_getDeviceProperty = env->GetMethodID(clazz, "getDeviceProperty", "(I[J[C)I");
-    if (method_getDeviceProperty == NULL) {
-        ALOGE("Can't find getDeviceProperty");
-        return -1;
-    }
-    method_setDeviceProperty = env->GetMethodID(clazz, "setDeviceProperty", "(IJLjava/lang/String;)I");
-    if (method_setDeviceProperty == NULL) {
-        ALOGE("Can't find setDeviceProperty");
-        return -1;
-    }
-    method_getObjectPropertyList = env->GetMethodID(clazz, "getObjectPropertyList",
-            "(IIIII)Landroid/mtp/MtpPropertyList;");
-    if (method_getObjectPropertyList == NULL) {
-        ALOGE("Can't find getObjectPropertyList");
-        return -1;
-    }
-    method_getObjectInfo = env->GetMethodID(clazz, "getObjectInfo", "(I[I[C[J)Z");
-    if (method_getObjectInfo == NULL) {
-        ALOGE("Can't find getObjectInfo");
-        return -1;
-    }
-    method_getObjectFilePath = env->GetMethodID(clazz, "getObjectFilePath", "(I[C[J)I");
-    if (method_getObjectFilePath == NULL) {
-        ALOGE("Can't find getObjectFilePath");
-        return -1;
-    }
-    method_deleteFile = env->GetMethodID(clazz, "deleteFile", "(I)I");
-    if (method_deleteFile == NULL) {
-        ALOGE("Can't find deleteFile");
-        return -1;
-    }
-    method_moveObject = env->GetMethodID(clazz, "moveObject", "(IIILjava/lang/String;)I");
-    if (method_moveObject == NULL) {
-        ALOGE("Can't find moveObject");
-        return -1;
-    }
-    method_getObjectReferences = env->GetMethodID(clazz, "getObjectReferences", "(I)[I");
-    if (method_getObjectReferences == NULL) {
-        ALOGE("Can't find getObjectReferences");
-        return -1;
-    }
-    method_setObjectReferences = env->GetMethodID(clazz, "setObjectReferences", "(I[I)I");
-    if (method_setObjectReferences == NULL) {
-        ALOGE("Can't find setObjectReferences");
-        return -1;
-    }
-    method_sessionStarted = env->GetMethodID(clazz, "sessionStarted", "()V");
-    if (method_sessionStarted == NULL) {
-        ALOGE("Can't find sessionStarted");
-        return -1;
-    }
-    method_sessionEnded = env->GetMethodID(clazz, "sessionEnded", "()V");
-    if (method_sessionEnded == NULL) {
-        ALOGE("Can't find sessionEnded");
-        return -1;
-    }
+    GET_METHOD_ID(beginSendObject, clazz, "(Ljava/lang/String;III)I");
+    GET_METHOD_ID(endSendObject, clazz, "(IZ)V");
+    GET_METHOD_ID(rescanFile, clazz, "(Ljava/lang/String;II)V");
+    GET_METHOD_ID(getObjectList, clazz, "(III)[I");
+    GET_METHOD_ID(getNumObjects, clazz, "(III)I");
+    GET_METHOD_ID(getSupportedPlaybackFormats, clazz, "()[I");
+    GET_METHOD_ID(getSupportedCaptureFormats, clazz, "()[I");
+    GET_METHOD_ID(getSupportedObjectProperties, clazz, "(I)[I");
+    GET_METHOD_ID(getSupportedDeviceProperties, clazz, "()[I");
+    GET_METHOD_ID(setObjectProperty, clazz, "(IIJLjava/lang/String;)I");
+    GET_METHOD_ID(getDeviceProperty, clazz, "(I[J[C)I");
+    GET_METHOD_ID(setDeviceProperty, clazz, "(IJLjava/lang/String;)I");
+    GET_METHOD_ID(getObjectPropertyList, clazz, "(IIIII)Landroid/mtp/MtpPropertyList;");
+    GET_METHOD_ID(getObjectInfo, clazz, "(I[I[C[J)Z");
+    GET_METHOD_ID(getObjectFilePath, clazz, "(I[C[J)I");
+    GET_METHOD_ID(beginDeleteObject, clazz, "(I)I");
+    GET_METHOD_ID(endDeleteObject, clazz, "(IZ)V");
+    GET_METHOD_ID(beginMoveObject, clazz, "(III)I");
+    GET_METHOD_ID(endMoveObject, clazz, "(IIIIIZ)V");
+    GET_METHOD_ID(beginCopyObject, clazz, "(III)I");
+    GET_METHOD_ID(endCopyObject, clazz, "(IZ)V");
+    GET_METHOD_ID(getObjectReferences, clazz, "(I)[I");
+    GET_METHOD_ID(setObjectReferences, clazz, "(I[I)I");
 
     field_context = env->GetFieldID(clazz, "mNativeContext", "J");
     if (field_context == NULL) {
         ALOGE("Can't find MtpDatabase.mNativeContext");
         return -1;
     }
-    field_batteryLevel = env->GetFieldID(clazz, "mBatteryLevel", "I");
-    if (field_batteryLevel == NULL) {
-        ALOGE("Can't find MtpDatabase.mBatteryLevel");
-        return -1;
-    }
-    field_batteryScale = env->GetFieldID(clazz, "mBatteryScale", "I");
-    if (field_batteryScale == NULL) {
-        ALOGE("Can't find MtpDatabase.mBatteryScale");
-        return -1;
-    }
-    field_deviceType = env->GetFieldID(clazz, "mDeviceType", "I");
-    if (field_deviceType == NULL) {
-        ALOGE("Can't find MtpDatabase.mDeviceType");
-        return -1;
-    }
 
-    // now set up fields for MtpPropertyList class
     clazz = env->FindClass("android/mtp/MtpPropertyList");
     if (clazz == NULL) {
         ALOGE("Can't find android/mtp/MtpPropertyList");
         return -1;
     }
-    field_mCount = env->GetFieldID(clazz, "mCount", "I");
-    if (field_mCount == NULL) {
-        ALOGE("Can't find MtpPropertyList.mCount");
-        return -1;
-    }
-    field_mResult = env->GetFieldID(clazz, "mResult", "I");
-    if (field_mResult == NULL) {
-        ALOGE("Can't find MtpPropertyList.mResult");
-        return -1;
-    }
-    field_mObjectHandles = env->GetFieldID(clazz, "mObjectHandles", "[I");
-    if (field_mObjectHandles == NULL) {
-        ALOGE("Can't find MtpPropertyList.mObjectHandles");
-        return -1;
-    }
-    field_mPropertyCodes = env->GetFieldID(clazz, "mPropertyCodes", "[I");
-    if (field_mPropertyCodes == NULL) {
-        ALOGE("Can't find MtpPropertyList.mPropertyCodes");
-        return -1;
-    }
-    field_mDataTypes = env->GetFieldID(clazz, "mDataTypes", "[I");
-    if (field_mDataTypes == NULL) {
-        ALOGE("Can't find MtpPropertyList.mDataTypes");
-        return -1;
-    }
-    field_mLongValues = env->GetFieldID(clazz, "mLongValues", "[J");
-    if (field_mLongValues == NULL) {
-        ALOGE("Can't find MtpPropertyList.mLongValues");
-        return -1;
-    }
-    field_mStringValues = env->GetFieldID(clazz, "mStringValues", "[Ljava/lang/String;");
-    if (field_mStringValues == NULL) {
-        ALOGE("Can't find MtpPropertyList.mStringValues");
-        return -1;
-    }
+    GET_METHOD_ID(getCode, clazz, "()I");
+    GET_METHOD_ID(getCount, clazz, "()I");
+    GET_METHOD_ID(getObjectHandles, clazz, "()[I");
+    GET_METHOD_ID(getPropertyCodes, clazz, "()[I");
+    GET_METHOD_ID(getDataTypes, clazz, "()[I");
+    GET_METHOD_ID(getLongValues, clazz, "()[J");
+    GET_METHOD_ID(getStringValues, clazz, "()[Ljava/lang/String;");
 
     if (AndroidRuntime::registerNativeMethods(env,
                 "android/mtp/MtpDatabase", gMtpDatabaseMethods, NELEM(gMtpDatabaseMethods)))
diff --git a/media/jni/android_mtp_MtpServer.cpp b/media/jni/android_mtp_MtpServer.cpp
index 6ce104d..c76cebe 100644
--- a/media/jni/android_mtp_MtpServer.cpp
+++ b/media/jni/android_mtp_MtpServer.cpp
@@ -41,7 +41,6 @@
 static jfieldID field_MtpStorage_storageId;
 static jfieldID field_MtpStorage_path;
 static jfieldID field_MtpStorage_description;
-static jfieldID field_MtpStorage_reserveSpace;
 static jfieldID field_MtpStorage_removable;
 static jfieldID field_MtpStorage_maxFileSize;
 
@@ -50,7 +49,7 @@
 // ----------------------------------------------------------------------------
 
 // in android_mtp_MtpDatabase.cpp
-extern MtpDatabase* getMtpDatabase(JNIEnv *env, jobject database);
+extern IMtpDatabase* getMtpDatabase(JNIEnv *env, jobject database);
 
 static inline MtpServer* getMtpServer(JNIEnv *env, jobject thiz) {
     return (MtpServer*)env->GetLongField(thiz, field_MtpServer_nativeContext);
@@ -162,7 +161,6 @@
         jint storageID = env->GetIntField(jstorage, field_MtpStorage_storageId);
         jstring path = (jstring)env->GetObjectField(jstorage, field_MtpStorage_path);
         jstring description = (jstring)env->GetObjectField(jstorage, field_MtpStorage_description);
-        jlong reserveSpace = env->GetLongField(jstorage, field_MtpStorage_reserveSpace);
         jboolean removable = env->GetBooleanField(jstorage, field_MtpStorage_removable);
         jlong maxFileSize = env->GetLongField(jstorage, field_MtpStorage_maxFileSize);
 
@@ -171,7 +169,7 @@
             const char *descriptionStr = env->GetStringUTFChars(description, NULL);
             if (descriptionStr != NULL) {
                 MtpStorage* storage = new MtpStorage(storageID, pathStr, descriptionStr,
-                        reserveSpace, removable, maxFileSize);
+                        removable, maxFileSize);
                 server->addStorage(storage);
                 env->ReleaseStringUTFChars(path, pathStr);
                 env->ReleaseStringUTFChars(description, descriptionStr);
@@ -241,11 +239,6 @@
         ALOGE("Can't find MtpStorage.mDescription");
         return -1;
     }
-    field_MtpStorage_reserveSpace = env->GetFieldID(clazz, "mReserveSpace", "J");
-    if (field_MtpStorage_reserveSpace == NULL) {
-        ALOGE("Can't find MtpStorage.mReserveSpace");
-        return -1;
-    }
     field_MtpStorage_removable = env->GetFieldID(clazz, "mRemovable", "Z");
     if (field_MtpStorage_removable == NULL) {
         ALOGE("Can't find MtpStorage.mRemovable");
diff --git a/media/tests/MtpTests/Android.mk b/media/tests/MtpTests/Android.mk
new file mode 100644
index 0000000..616e600
--- /dev/null
+++ b/media/tests/MtpTests/Android.mk
@@ -0,0 +1,12 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := tests
+
+LOCAL_SRC_FILES := $(call all-subdir-java-files)
+
+LOCAL_STATIC_JAVA_LIBRARIES := android-support-test
+
+LOCAL_PACKAGE_NAME := MtpTests
+
+include $(BUILD_PACKAGE)
diff --git a/media/tests/MtpTests/AndroidManifest.xml b/media/tests/MtpTests/AndroidManifest.xml
new file mode 100644
index 0000000..21e2b01
--- /dev/null
+++ b/media/tests/MtpTests/AndroidManifest.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 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="android.mtp" >
+
+    <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="21" />
+
+    <uses-permission  android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation android:name="android.support.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="android.mtp"
+                     android:label="MtpTests"/>
+</manifest>
diff --git a/media/tests/MtpTests/AndroidTest.xml b/media/tests/MtpTests/AndroidTest.xml
new file mode 100644
index 0000000..a61a3b4
--- /dev/null
+++ b/media/tests/MtpTests/AndroidTest.xml
@@ -0,0 +1,15 @@
+<configuration description="Runs sample instrumentation test.">
+    <target_preparer class="com.android.tradefed.targetprep.TestFilePushSetup"/>
+    <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+        <option name="test-file-name" value="MtpTests.apk"/>
+    </target_preparer>
+    <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer"/>
+    <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer"/>
+    <option name="test-suite-tag" value="apct"/>
+    <option name="test-tag" value="MtpTests"/>
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest">
+        <option name="package" value="android.mtp"/>
+        <option name="runner" value="android.support.test.runner.AndroidJUnitRunner"/>
+    </test>
+</configuration>
\ No newline at end of file
diff --git a/media/tests/MtpTests/src/android/mtp/MtpStorageManagerTest.java b/media/tests/MtpTests/src/android/mtp/MtpStorageManagerTest.java
new file mode 100644
index 0000000..0d7f3fe
--- /dev/null
+++ b/media/tests/MtpTests/src/android/mtp/MtpStorageManagerTest.java
@@ -0,0 +1,1657 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.mtp;
+
+import android.os.FileUtils;
+import android.os.UserHandle;
+import android.os.storage.StorageVolume;
+import android.support.test.filters.SmallTest;
+import android.support.test.InstrumentationRegistry;
+import android.util.Log;
+
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.runners.MethodSorters;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.UUID;
+import java.util.function.Predicate;
+import java.util.stream.Stream;
+
+/**
+ * Tests for MtpStorageManager functionality.
+ */
+@RunWith(JUnit4.class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class MtpStorageManagerTest {
+    private static final String TAG = MtpStorageManagerTest.class.getSimpleName();
+
+    private static final String TEMP_DIR = InstrumentationRegistry.getContext().getFilesDir()
+            + "/" + TAG + "/";
+    private static final File TEMP_DIR_FILE = new File(TEMP_DIR);
+
+    private MtpStorageManager manager;
+
+    private ArrayList<Integer> objectsAdded;
+    private ArrayList<Integer> objectsRemoved;
+
+    private File mainStorageDir;
+    private File secondaryStorageDir;
+
+    private MtpStorage mainMtpStorage;
+    private MtpStorage secondaryMtpStorage;
+
+    static {
+        MtpStorageManager.sDebug = true;
+    }
+
+    private static void logMethodName() {
+        Log.d(TAG, Thread.currentThread().getStackTrace()[3].getMethodName());
+    }
+
+    private static File createNewFile(File parent) {
+        return createNewFile(parent, UUID.randomUUID().toString());
+    }
+
+    private static File createNewFile(File parent, String name) {
+        try {
+            File ret = new File(parent, name);
+            if (!ret.createNewFile())
+                throw new AssertionError("Failed to create file");
+            return ret;
+        } catch (IOException e) {
+            throw new AssertionError(e.getMessage());
+        }
+    }
+
+    private static File createNewDir(File parent, String name) {
+        File ret = new File(parent, name);
+        if (!ret.mkdir())
+            throw new AssertionError("Failed to create file");
+        return ret;
+    }
+
+    private static File createNewDir(File parent) {
+        return createNewDir(parent, UUID.randomUUID().toString());
+    }
+
+    @Before
+    public void before() {
+        Assert.assertTrue(TEMP_DIR_FILE.mkdir());
+        mainStorageDir = createNewDir(TEMP_DIR_FILE);
+        secondaryStorageDir = createNewDir(TEMP_DIR_FILE);
+
+        StorageVolume mainStorage = new StorageVolume("1", mainStorageDir, "", true, false, true,
+                false, -1, UserHandle.CURRENT, "", "");
+        StorageVolume secondaryStorage = new StorageVolume("2", secondaryStorageDir, "", false,
+                false, true, false, -1, UserHandle.CURRENT, "", "");
+
+        objectsAdded = new ArrayList<>();
+        objectsRemoved = new ArrayList<>();
+
+        manager = new MtpStorageManager(new MtpStorageManager.MtpNotifier() {
+            @Override
+            public void sendObjectAdded(int id) {
+                objectsAdded.add(id);
+            }
+
+            @Override
+            public void sendObjectRemoved(int id) {
+                objectsRemoved.add(id);
+            }
+        }, null);
+
+        mainMtpStorage = manager.addMtpStorage(mainStorage);
+        secondaryMtpStorage = manager.addMtpStorage(secondaryStorage);
+    }
+
+    @After
+    public void after() {
+        manager.close();
+        FileUtils.deleteContentsAndDir(TEMP_DIR_FILE);
+    }
+
+    /** MtpObject getter tests. **/
+
+    @Test
+    @SmallTest
+    public void testMtpObjectGetNameRoot() {
+        logMethodName();
+        MtpStorageManager.MtpObject obj = manager.getStorageRoot(mainMtpStorage.getStorageId());
+        Assert.assertEquals(obj.getName(), mainStorageDir.getPath());
+    }
+
+    @Test
+    @SmallTest
+    public void testMtpObjectGetNameNonRoot() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId());
+        Assert.assertEquals(stream.findFirst().get().getName(), newFile.getName());
+    }
+
+    @Test
+    @SmallTest
+    public void testMtpObjectGetIdRoot() {
+        logMethodName();
+        MtpStorageManager.MtpObject obj = manager.getStorageRoot(mainMtpStorage.getStorageId());
+        Assert.assertEquals(obj.getId(), mainMtpStorage.getStorageId());
+    }
+
+    @Test
+    @SmallTest
+    public void testMtpObjectGetIdNonRoot() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId());
+        Assert.assertEquals(stream.findFirst().get().getId(), 1);
+    }
+
+    @Test
+    @SmallTest
+    public void testMtpObjectIsDirTrue() {
+        logMethodName();
+        MtpStorageManager.MtpObject obj = manager.getStorageRoot(mainMtpStorage.getStorageId());
+        Assert.assertTrue(obj.isDir());
+    }
+
+    @Test
+    @SmallTest
+    public void testMtpObjectIsDirFalse() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir, "TEST123.mp3");
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId());
+        Assert.assertFalse(stream.findFirst().get().isDir());
+    }
+
+    @Test
+    @SmallTest
+    public void testMtpObjectGetFormatDir() {
+        logMethodName();
+        File newFile = createNewDir(mainStorageDir);
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId());
+        Assert.assertEquals(stream.findFirst().get().getFormat(), MtpConstants.FORMAT_ASSOCIATION);
+    }
+
+    @Test
+    @SmallTest
+    public void testMtpObjectGetFormatNonDir() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir, "TEST123.mp3");
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId());
+        Assert.assertEquals(stream.findFirst().get().getFormat(), MtpConstants.FORMAT_MP3);
+    }
+
+    @Test
+    @SmallTest
+    public void testMtpObjectGetStorageId() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId());
+        Assert.assertEquals(stream.findFirst().get().getStorageId(), mainMtpStorage.getStorageId());
+    }
+
+    @Test
+    @SmallTest
+    public void testMtpObjectGetLastModified() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId());
+        Assert.assertEquals(stream.findFirst().get().getModifiedTime(),
+                newFile.lastModified() / 1000);
+    }
+
+    @Test
+    @SmallTest
+    public void testMtpObjectGetParent() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId());
+        Assert.assertEquals(stream.findFirst().get().getParent(),
+                manager.getStorageRoot(mainMtpStorage.getStorageId()));
+    }
+
+    @Test
+    @SmallTest
+    public void testMtpObjectGetRoot() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId());
+        Assert.assertEquals(stream.findFirst().get().getRoot(),
+                manager.getStorageRoot(mainMtpStorage.getStorageId()));
+    }
+
+    @Test
+    @SmallTest
+    public void testMtpObjectGetPath() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId());
+        Assert.assertEquals(stream.findFirst().get().getPath().toString(), newFile.getPath());
+    }
+
+    @Test
+    @SmallTest
+    public void testMtpObjectGetSize() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        try {
+            new FileOutputStream(newFile).write(new byte[] {0, 0, 0, 0, 0, 0, 0, 0});
+        } catch (IOException e) {
+            Assert.fail();
+        }
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId());
+        Assert.assertEquals(stream.findFirst().get().getSize(), 8);
+    }
+
+    @Test
+    @SmallTest
+    public void testMtpObjectGetSizeDir() {
+        logMethodName();
+        File newDir = createNewDir(mainStorageDir);
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId());
+        Assert.assertEquals(stream.findFirst().get().getSize(), 0);
+    }
+
+    /** MtpStorageManager cache access tests. **/
+
+    @Test
+    @SmallTest
+    public void testAddMtpStorage() {
+        logMethodName();
+        Assert.assertEquals(mainMtpStorage.getPath(), mainStorageDir.getPath());
+        Assert.assertNotNull(manager.getStorageRoot(mainMtpStorage.getStorageId()));
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testRemoveMtpStorage() {
+        logMethodName();
+        File newFile = createNewFile(secondaryStorageDir);
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(0xFFFFFFFF, 0,
+                secondaryMtpStorage.getStorageId());
+        Assert.assertEquals(stream.count(), 1);
+
+        manager.removeMtpStorage(secondaryMtpStorage);
+        Assert.assertNull(manager.getStorageRoot(secondaryMtpStorage.getStorageId()));
+        Assert.assertNull(manager.getObject(1));
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testGetByPath() {
+        logMethodName();
+        File newFile = createNewFile(createNewDir(createNewDir(mainStorageDir)));
+
+        MtpStorageManager.MtpObject obj = manager.getByPath(newFile.getPath());
+        Assert.assertNotNull(obj);
+        Assert.assertEquals(obj.getPath().toString(), newFile.getPath());
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testGetByPathError() {
+        logMethodName();
+        File newFile = createNewFile(createNewDir(createNewDir(mainStorageDir)));
+
+        MtpStorageManager.MtpObject obj = manager.getByPath(newFile.getPath() + "q");
+        Assert.assertNull(obj);
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testGetObject() {
+        logMethodName();
+        File newFile = createNewFile(createNewDir(createNewDir(mainStorageDir)));
+        MtpStorageManager.MtpObject obj = manager.getByPath(newFile.getPath());
+        Assert.assertNotNull(obj);
+
+        Assert.assertEquals(manager.getObject(obj.getId()), obj);
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testGetObjectError() {
+        logMethodName();
+        File newFile = createNewFile(createNewDir(createNewDir(mainStorageDir)));
+
+        Assert.assertNull(manager.getObject(42));
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testGetStorageRoot() {
+        logMethodName();
+        MtpStorageManager.MtpObject obj = manager.getStorageRoot(mainMtpStorage.getStorageId());
+        Assert.assertEquals(obj.getPath().toString(), mainStorageDir.getPath());
+    }
+
+    @Test
+    @SmallTest
+    public void testGetObjectsParent() {
+        logMethodName();
+        File newDir = createNewDir(createNewDir(mainStorageDir));
+        File newFile = createNewFile(newDir);
+        File newMP3File = createNewFile(newDir, "lalala.mp3");
+        MtpStorageManager.MtpObject parent = manager.getByPath(newDir.getPath());
+        Assert.assertNotNull(parent);
+
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(parent.getId(), 0,
+                mainMtpStorage.getStorageId());
+        Assert.assertEquals(stream.count(), 2);
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testGetObjectsFormat() {
+        logMethodName();
+        File newDir = createNewDir(createNewDir(mainStorageDir));
+        File newFile = createNewFile(newDir);
+        File newMP3File = createNewFile(newDir, "lalala.mp3");
+        MtpStorageManager.MtpObject parent = manager.getByPath(newDir.getPath());
+        Assert.assertNotNull(parent);
+
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(parent.getId(),
+                MtpConstants.FORMAT_MP3, mainMtpStorage.getStorageId());
+        Assert.assertEquals(stream.findFirst().get().getPath().toString(), newMP3File.toString());
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testGetObjectsRoot() {
+        logMethodName();
+        File newDir = createNewDir(mainStorageDir);
+        File newFile = createNewFile(mainStorageDir);
+        File newMP3File = createNewFile(newDir, "lalala.mp3");
+
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId());
+        Assert.assertEquals(stream.count(), 2);
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testGetObjectsAll() {
+        logMethodName();
+        File newDir = createNewDir(mainStorageDir);
+        File newFile = createNewFile(mainStorageDir);
+        File newMP3File = createNewFile(newDir, "lalala.mp3");
+
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(0, 0,
+                mainMtpStorage.getStorageId());
+        Assert.assertEquals(stream.count(), 3);
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testGetObjectsAllStorages() {
+        logMethodName();
+        File newDir = createNewDir(mainStorageDir);
+        createNewFile(mainStorageDir);
+        createNewFile(newDir, "lalala.mp3");
+        File newDir2 = createNewDir(secondaryStorageDir);
+        createNewFile(secondaryStorageDir);
+        createNewFile(newDir2, "lalala.mp3");
+
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(0, 0, 0xFFFFFFFF);
+        Assert.assertEquals(stream.count(), 6);
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testGetObjectsAllStoragesRoot() {
+        logMethodName();
+        File newDir = createNewDir(mainStorageDir);
+        createNewFile(mainStorageDir);
+        createNewFile(newDir, "lalala.mp3");
+        File newDir2 = createNewDir(secondaryStorageDir);
+        createNewFile(secondaryStorageDir);
+        createNewFile(newDir2, "lalala.mp3");
+
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(0xFFFFFFFF, 0, 0xFFFFFFFF);
+        Assert.assertEquals(stream.count(), 4);
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    /** MtpStorageManager event handling tests. **/
+
+    @Test
+    @SmallTest
+    public void testObjectAdded() {
+        logMethodName();
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId());
+        Assert.assertEquals(stream.count(), 0);
+
+        File newFile = createNewFile(mainStorageDir);
+        manager.flushEvents();
+        Assert.assertEquals(objectsAdded.size(), 1);
+        Assert.assertEquals(manager.getObject(objectsAdded.get(0)).getPath().toString(),
+                newFile.getPath());
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testObjectAddedDir() {
+        logMethodName();
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId());
+        Assert.assertEquals(stream.count(), 0);
+
+        File newDir = createNewDir(mainStorageDir);
+        manager.flushEvents();
+        Assert.assertEquals(objectsAdded.size(), 1);
+        Assert.assertEquals(manager.getObject(objectsAdded.get(0)).getPath().toString(),
+                newDir.getPath());
+        Assert.assertTrue(manager.getObject(objectsAdded.get(0)).isDir());
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testObjectAddedRecursiveDir() {
+        logMethodName();
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId());
+        Assert.assertEquals(stream.count(), 0);
+
+        File newDir = createNewDir(createNewDir(createNewDir(mainStorageDir)));
+        manager.flushEvents();
+        Assert.assertEquals(objectsAdded.size(), 3);
+        Assert.assertEquals(manager.getObject(objectsAdded.get(2)).getPath().toString(),
+                newDir.getPath());
+        Assert.assertTrue(manager.getObject(objectsAdded.get(2)).isDir());
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testObjectRemoved() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId());
+        Assert.assertEquals(stream.count(), 1);
+
+        Assert.assertTrue(newFile.delete());
+        manager.flushEvents();
+        Assert.assertEquals(objectsRemoved.size(), 1);
+        Assert.assertNull(manager.getObject(objectsRemoved.get(0)));
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testObjectMoved() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId());
+        Assert.assertEquals(stream.count(), 1);
+        File toFile = new File(mainStorageDir, "to" + newFile.getName());
+
+        Assert.assertTrue(newFile.renameTo(toFile));
+        manager.flushEvents();
+        Assert.assertEquals(objectsAdded.size(), 1);
+        Assert.assertEquals(objectsRemoved.size(), 1);
+        Assert.assertEquals(manager.getObject(objectsAdded.get(0)).getPath().toString(),
+                toFile.getPath());
+        Assert.assertNull(manager.getObject(objectsRemoved.get(0)));
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    /** MtpStorageManager operation tests. Ensure that events are not sent for the main operation,
+        and also test all possible cases of other processes accessing the file at the same time, as
+        well as cases of both failure and success. **/
+
+    @Test
+    @SmallTest
+    public void testSendObjectSuccess() {
+        logMethodName();
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId());
+        int id = manager.beginSendObject(manager.getStorageRoot(mainMtpStorage.getStorageId()),
+                "newFile", MtpConstants.FORMAT_UNDEFINED);
+        Assert.assertEquals(id, 1);
+
+        File newFile = createNewFile(mainStorageDir, "newFile");
+        manager.flushEvents();
+        MtpStorageManager.MtpObject obj = manager.getObject(id);
+        Assert.assertTrue(manager.endSendObject(obj, true));
+        Assert.assertEquals(obj.getPath().toString(), newFile.getPath());
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testSendObjectSuccessDir() {
+        logMethodName();
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId());
+        int id = manager.beginSendObject(manager.getStorageRoot(mainMtpStorage.getStorageId()),
+                "newDir", MtpConstants.FORMAT_ASSOCIATION);
+        Assert.assertEquals(id, 1);
+
+        File newFile = createNewDir(mainStorageDir, "newDir");
+        manager.flushEvents();
+        MtpStorageManager.MtpObject obj = manager.getObject(id);
+        Assert.assertTrue(manager.endSendObject(obj, true));
+        Assert.assertEquals(obj.getPath().toString(), newFile.getPath());
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertEquals(obj.getFormat(), MtpConstants.FORMAT_ASSOCIATION);
+        Assert.assertTrue(manager.checkConsistency());
+
+        // Check that new dir receives events
+        File newerFile = createNewFile(newFile);
+        manager.flushEvents();
+        Assert.assertEquals(objectsAdded.size(), 1);
+        Assert.assertEquals(manager.getObject(objectsAdded.get(0)).getPath().toString(),
+                newerFile.getPath());
+    }
+
+    @Test
+    @SmallTest
+    public void testSendObjectSuccessDelayed() {
+        logMethodName();
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId());
+        int id = manager.beginSendObject(manager.getStorageRoot(mainMtpStorage.getStorageId()),
+                "newFile", MtpConstants.FORMAT_UNDEFINED);
+        Assert.assertEquals(id, 1);
+        MtpStorageManager.MtpObject obj = manager.getObject(id);
+        Assert.assertTrue(manager.endSendObject(obj, true));
+
+        File newFile = createNewFile(mainStorageDir, "newFile");
+        manager.flushEvents();
+        Assert.assertEquals(obj.getPath().toString(), newFile.getPath());
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testSendObjectSuccessDirDelayed() {
+        logMethodName();
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId());
+        int id = manager.beginSendObject(manager.getStorageRoot(mainMtpStorage.getStorageId()),
+                "newDir", MtpConstants.FORMAT_ASSOCIATION);
+        Assert.assertEquals(id, 1);
+
+        MtpStorageManager.MtpObject obj = manager.getObject(id);
+        Assert.assertTrue(manager.endSendObject(obj, true));
+        File newFile = createNewDir(mainStorageDir, "newDir");
+        manager.flushEvents();
+        Assert.assertEquals(obj.getPath().toString(), newFile.getPath());
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertEquals(obj.getFormat(), MtpConstants.FORMAT_ASSOCIATION);
+        Assert.assertTrue(manager.checkConsistency());
+
+        // Check that new dir receives events
+        File newerFile = createNewFile(newFile);
+        manager.flushEvents();
+        Assert.assertEquals(objectsAdded.size(), 1);
+        Assert.assertEquals(manager.getObject(objectsAdded.get(0)).getPath().toString(),
+                newerFile.getPath());
+    }
+
+    @Test
+    @SmallTest
+    public void testSendObjectSuccessDeleted() {
+        logMethodName();
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId());
+        int id = manager.beginSendObject(manager.getStorageRoot(mainMtpStorage.getStorageId()),
+                "newFile", MtpConstants.FORMAT_UNDEFINED);
+        Assert.assertEquals(id, 1);
+
+        File newFile = createNewFile(mainStorageDir, "newFile");
+        Assert.assertTrue(newFile.delete());
+        manager.flushEvents();
+        MtpStorageManager.MtpObject obj = manager.getObject(id);
+        Assert.assertTrue(manager.endSendObject(obj, true));
+        Assert.assertNull(manager.getObject(obj.getId()));
+        Assert.assertEquals(objectsRemoved.get(0).intValue(), obj.getId());
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testSendObjectFailed() {
+        logMethodName();
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId());
+        int id = manager.beginSendObject(manager.getStorageRoot(mainMtpStorage.getStorageId()),
+                "newFile", MtpConstants.FORMAT_UNDEFINED);
+        Assert.assertEquals(id, 1);
+
+        MtpStorageManager.MtpObject obj = manager.getObject(id);
+        Assert.assertTrue(manager.endSendObject(obj, false));
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testSendObjectFailedDeleted() {
+        logMethodName();
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId());
+        int id = manager.beginSendObject(manager.getStorageRoot(mainMtpStorage.getStorageId()),
+                "newFile", MtpConstants.FORMAT_UNDEFINED);
+        Assert.assertEquals(id, 1);
+        MtpStorageManager.MtpObject obj = manager.getObject(id);
+
+        File newFile = createNewFile(mainStorageDir, "newFile");
+        Assert.assertTrue(newFile.delete());
+        manager.flushEvents();
+        Assert.assertTrue(manager.endSendObject(obj, false));
+        Assert.assertEquals(objectsRemoved.size(), 0);
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testSendObjectFailedAdded() {
+        logMethodName();
+        Stream<MtpStorageManager.MtpObject> stream = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId());
+        int id = manager.beginSendObject(manager.getStorageRoot(mainMtpStorage.getStorageId()),
+                "newFile", MtpConstants.FORMAT_UNDEFINED);
+        Assert.assertEquals(id, 1);
+        MtpStorageManager.MtpObject obj = manager.getObject(id);
+
+        File newDir = createNewDir(mainStorageDir, "newFile");
+        manager.flushEvents();
+        Assert.assertTrue(manager.endSendObject(obj, false));
+        Assert.assertNotEquals(objectsAdded.get(0).intValue(), id);
+        Assert.assertNull(manager.getObject(id));
+        Assert.assertEquals(manager.getObject(objectsAdded.get(0)).getPath().toString(),
+                newDir.getPath());
+        Assert.assertTrue(manager.checkConsistency());
+
+        // Expect events in new dir
+        createNewFile(newDir);
+        manager.flushEvents();
+        Assert.assertEquals(objectsAdded.size(), 2);
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testRemoveObjectSuccess() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        MtpStorageManager.MtpObject obj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId()).findFirst().get();
+        Assert.assertTrue(manager.beginRemoveObject(obj));
+
+        Assert.assertTrue(newFile.delete());
+        manager.flushEvents();
+        Assert.assertTrue(manager.endRemoveObject(obj, true));
+        Assert.assertEquals(objectsRemoved.size(), 0);
+        Assert.assertNull(manager.getObject(obj.getId()));
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testRemoveObjectDelayed() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        MtpStorageManager.MtpObject obj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId()).findFirst().get();
+        Assert.assertTrue(manager.beginRemoveObject(obj));
+
+        Assert.assertTrue(manager.endRemoveObject(obj, true));
+        Assert.assertTrue(newFile.delete());
+        manager.flushEvents();
+        Assert.assertEquals(objectsRemoved.size(), 0);
+        Assert.assertNull(manager.getObject(obj.getId()));
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testRemoveObjectDir() {
+        logMethodName();
+        File newDir = createNewDir(mainStorageDir);
+        createNewFile(createNewDir(newDir));
+        MtpStorageManager.MtpObject obj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId()).findFirst().get();
+        manager.getObjects(obj.getId(), 0, mainMtpStorage.getStorageId());
+        Assert.assertTrue(manager.beginRemoveObject(obj));
+
+        createNewFile(newDir);
+        Assert.assertTrue(FileUtils.deleteContentsAndDir(newDir));
+        manager.flushEvents();
+        Assert.assertTrue(manager.endRemoveObject(obj, true));
+        Assert.assertEquals(objectsAdded.size(), 1);
+        Assert.assertEquals(objectsRemoved.size(), 1);
+        Assert.assertEquals(manager.getObjects(0, 0, mainMtpStorage.getStorageId()).count(), 0);
+        Assert.assertNull(manager.getObject(obj.getId()));
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testRemoveObjectDirDelayed() {
+        logMethodName();
+        File newDir = createNewDir(mainStorageDir);
+        createNewFile(createNewDir(newDir));
+        MtpStorageManager.MtpObject obj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId()).findFirst().get();
+        Assert.assertTrue(manager.beginRemoveObject(obj));
+
+        Assert.assertTrue(manager.endRemoveObject(obj, true));
+        Assert.assertTrue(FileUtils.deleteContentsAndDir(newDir));
+        manager.flushEvents();
+        Assert.assertEquals(objectsRemoved.size(), 0);
+        Assert.assertEquals(manager.getObjects(0, 0, mainMtpStorage.getStorageId()).count(), 0);
+        Assert.assertNull(manager.getObject(obj.getId()));
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testRemoveObjectSuccessAdded() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        MtpStorageManager.MtpObject obj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId()).findFirst().get();
+        int id = obj.getId();
+        Assert.assertTrue(manager.beginRemoveObject(obj));
+
+        Assert.assertTrue(newFile.delete());
+        createNewFile(mainStorageDir, newFile.getName());
+        manager.flushEvents();
+        Assert.assertTrue(manager.endRemoveObject(obj, true));
+        Assert.assertEquals(objectsRemoved.size(), 0);
+        Assert.assertEquals(objectsAdded.size(), 1);
+        Assert.assertNull(manager.getObject(id));
+        Assert.assertNotEquals(objectsAdded.get(0).intValue(), id);
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testRemoveObjectFailed() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        MtpStorageManager.MtpObject obj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId()).findFirst().get();
+        Assert.assertTrue(manager.beginRemoveObject(obj));
+
+        Assert.assertTrue(manager.endRemoveObject(obj, false));
+        Assert.assertEquals(manager.getObject(obj.getId()), obj);
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testRemoveObjectFailedDir() {
+        logMethodName();
+        File newDir = createNewDir(mainStorageDir);
+        MtpStorageManager.MtpObject obj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId()).findFirst().get();
+        manager.getObjects(obj.getId(), 0, mainMtpStorage.getStorageId());
+        Assert.assertTrue(manager.beginRemoveObject(obj));
+
+        createNewFile(newDir);
+        manager.flushEvents();
+        Assert.assertTrue(manager.endRemoveObject(obj, false));
+        Assert.assertEquals(manager.getObject(obj.getId()), obj);
+        Assert.assertEquals(objectsAdded.size(), 1);
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testRemoveObjectFailedRemoved() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        MtpStorageManager.MtpObject obj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId()).findFirst().get();
+        Assert.assertTrue(manager.beginRemoveObject(obj));
+
+        Assert.assertTrue(newFile.delete());
+        manager.flushEvents();
+        Assert.assertTrue(manager.endRemoveObject(obj, false));
+        Assert.assertEquals(objectsRemoved.size(), 1);
+        Assert.assertEquals(objectsRemoved.get(0).intValue(), obj.getId());
+        Assert.assertNull(manager.getObject(obj.getId()));
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testCopyObjectSuccess() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        File newDir = createNewDir(mainStorageDir);
+        MtpStorageManager.MtpObject dirObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId())
+                .filter(MtpStorageManager.MtpObject::isDir).findFirst().get();
+        MtpStorageManager.MtpObject fileObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId())
+                .filter(o -> !o.isDir()).findFirst().get();
+
+        int id = manager.beginCopyObject(fileObj, dirObj);
+        Assert.assertNotEquals(id, -1);
+        createNewFile(newDir, newFile.getName());
+        manager.flushEvents();
+        MtpStorageManager.MtpObject obj = manager.getObject(id);
+        Assert.assertTrue(manager.endCopyObject(obj, true));
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testCopyObjectSuccessRecursive() {
+        logMethodName();
+        File newDirFrom = createNewDir(mainStorageDir);
+        File newDirFrom1 = createNewDir(newDirFrom);
+        File newDirFrom2 = createNewFile(newDirFrom1);
+        File delayedFile = createNewFile(newDirFrom);
+        File deletedFile = createNewFile(newDirFrom);
+        File newDirTo = createNewDir(mainStorageDir);
+        MtpStorageManager.MtpObject toObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId())
+                .filter(o -> o.getName().equals(newDirTo.getName())).findFirst().get();
+        MtpStorageManager.MtpObject fromObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId())
+                .filter(o -> o.getName().equals(newDirFrom.getName())).findFirst().get();
+
+        manager.getObjects(fromObj.getId(), 0, mainMtpStorage.getStorageId());
+        int id = manager.beginCopyObject(fromObj, toObj);
+        Assert.assertNotEquals(id, -1);
+        File copiedDir = createNewDir(newDirTo, newDirFrom.getName());
+        File copiedDir1 = createNewDir(copiedDir, newDirFrom1.getName());
+        createNewFile(copiedDir1, newDirFrom2.getName());
+        createNewFile(copiedDir, "extraFile");
+        File toDelete = createNewFile(copiedDir, deletedFile.getName());
+        manager.flushEvents();
+        Assert.assertTrue(toDelete.delete());
+        manager.flushEvents();
+        MtpStorageManager.MtpObject obj = manager.getObject(id);
+        Assert.assertTrue(manager.endCopyObject(obj, true));
+        Assert.assertEquals(objectsAdded.size(), 1);
+        Assert.assertEquals(objectsRemoved.size(), 1);
+
+        createNewFile(copiedDir, delayedFile.getName());
+        manager.flushEvents();
+        Assert.assertTrue(manager.checkConsistency());
+
+        // Expect events in the visited dir, but not the unvisited dir.
+        createNewFile(copiedDir);
+        createNewFile(copiedDir1);
+        manager.flushEvents();
+        Assert.assertEquals(objectsAdded.size(), 2);
+        Assert.assertEquals(objectsAdded.size(), 2);
+
+        // Number of files/dirs created, minus the one that was deleted.
+        Assert.assertEquals(manager.getObjects(0, 0, mainMtpStorage.getStorageId()).count(), 13);
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testCopyObjectFailed() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        File newDir = createNewDir(mainStorageDir);
+        MtpStorageManager.MtpObject dirObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId())
+                .filter(MtpStorageManager.MtpObject::isDir).findFirst().get();
+        MtpStorageManager.MtpObject fileObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId())
+                .filter(o -> !o.isDir()).findFirst().get();
+
+        int id = manager.beginCopyObject(fileObj, dirObj);
+        Assert.assertNotEquals(id, -1);
+        manager.flushEvents();
+        MtpStorageManager.MtpObject obj = manager.getObject(id);
+        Assert.assertTrue(manager.endCopyObject(obj, false));
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testCopyObjectFailedAdded() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        File newDir = createNewDir(mainStorageDir);
+        MtpStorageManager.MtpObject dirObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId())
+                .filter(MtpStorageManager.MtpObject::isDir).findFirst().get();
+        MtpStorageManager.MtpObject fileObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId())
+                .filter(o -> !o.isDir()).findFirst().get();
+
+        int id = manager.beginCopyObject(fileObj, dirObj);
+        Assert.assertNotEquals(id, -1);
+        File addedDir = createNewDir(newDir, newFile.getName());
+        manager.flushEvents();
+        MtpStorageManager.MtpObject obj = manager.getObject(id);
+        Assert.assertTrue(manager.endCopyObject(obj, false));
+        Assert.assertEquals(objectsAdded.size(), 1);
+        Assert.assertNotEquals(objectsAdded.get(0).intValue(), id);
+        Assert.assertTrue(manager.checkConsistency());
+
+        // Expect events in new dir
+        createNewFile(addedDir);
+        manager.flushEvents();
+        Assert.assertEquals(objectsAdded.size(), 2);
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testCopyObjectFailedDeleted() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        File newDir = createNewDir(mainStorageDir);
+        MtpStorageManager.MtpObject dirObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId())
+                .filter(MtpStorageManager.MtpObject::isDir).findFirst().get();
+        MtpStorageManager.MtpObject fileObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId())
+                .filter(o -> !o.isDir()).findFirst().get();
+
+        int id = manager.beginCopyObject(fileObj, dirObj);
+        Assert.assertNotEquals(id, -1);
+        Assert.assertTrue(createNewFile(newDir, newFile.getName()).delete());
+        manager.flushEvents();
+        MtpStorageManager.MtpObject obj = manager.getObject(id);
+        Assert.assertTrue(manager.endCopyObject(obj, false));
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testRenameObjectSuccess() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        MtpStorageManager.MtpObject obj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId()).findFirst().get();
+        Assert.assertTrue(manager.beginRenameObject(obj, "renamed"));
+
+        File renamed = new File(mainStorageDir, "renamed");
+        Assert.assertTrue(newFile.renameTo(renamed));
+        manager.flushEvents();
+        Assert.assertTrue(manager.endRenameObject(obj, newFile.getName(), true));
+
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertEquals(objectsRemoved.size(), 0);
+        Assert.assertEquals(obj.getPath().toString(), renamed.getPath());
+
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testRenameObjectDirSuccess() {
+        logMethodName();
+        File newDir = createNewDir(mainStorageDir);
+        MtpStorageManager.MtpObject obj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId()).findFirst().get();
+        Assert.assertTrue(manager.beginRenameObject(obj, "renamed"));
+
+        File renamed = new File(mainStorageDir, "renamed");
+        Assert.assertTrue(newDir.renameTo(renamed));
+        manager.flushEvents();
+        Assert.assertTrue(manager.endRenameObject(obj, newDir.getName(), true));
+
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertEquals(objectsRemoved.size(), 0);
+        Assert.assertEquals(obj.getPath().toString(), renamed.getPath());
+
+        Assert.assertTrue(manager.checkConsistency());
+
+        // Don't expect events
+        createNewFile(renamed);
+        manager.flushEvents();
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testRenameObjectDirVisitedSuccess() {
+        logMethodName();
+        File newDir = createNewDir(mainStorageDir);
+        MtpStorageManager.MtpObject obj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId()).findFirst().get();
+        manager.getObjects(obj.getId(), 0, mainMtpStorage.getStorageId());
+        Assert.assertTrue(manager.beginRenameObject(obj, "renamed"));
+
+        File renamed = new File(mainStorageDir, "renamed");
+        Assert.assertTrue(newDir.renameTo(renamed));
+        manager.flushEvents();
+        Assert.assertTrue(manager.endRenameObject(obj, newDir.getName(), true));
+
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertEquals(objectsRemoved.size(), 0);
+        Assert.assertEquals(obj.getPath().toString(), renamed.getPath());
+
+        Assert.assertTrue(manager.checkConsistency());
+
+        // Expect events since the dir was visited
+        createNewFile(renamed);
+        manager.flushEvents();
+        Assert.assertEquals(objectsAdded.size(), 1);
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testRenameObjectDelayed() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        MtpStorageManager.MtpObject obj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId()).findFirst().get();
+        Assert.assertTrue(manager.beginRenameObject(obj, "renamed"));
+
+        Assert.assertTrue(manager.endRenameObject(obj, newFile.getName(), true));
+        File renamed = new File(mainStorageDir, "renamed");
+        Assert.assertTrue(newFile.renameTo(renamed));
+        manager.flushEvents();
+
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertEquals(objectsRemoved.size(), 0);
+        Assert.assertEquals(obj.getPath().toString(), renamed.getPath());
+
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testRenameObjectDirVisitedDelayed() {
+        logMethodName();
+        File newDir = createNewDir(mainStorageDir);
+        MtpStorageManager.MtpObject obj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId()).findFirst().get();
+        manager.getObjects(obj.getId(), 0, mainMtpStorage.getStorageId());
+        Assert.assertTrue(manager.beginRenameObject(obj, "renamed"));
+
+        Assert.assertTrue(manager.endRenameObject(obj, newDir.getName(), true));
+        File renamed = new File(mainStorageDir, "renamed");
+        Assert.assertTrue(newDir.renameTo(renamed));
+        manager.flushEvents();
+
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertEquals(objectsRemoved.size(), 0);
+        Assert.assertEquals(obj.getPath().toString(), renamed.getPath());
+
+        Assert.assertTrue(manager.checkConsistency());
+
+        // Expect events since the dir was visited
+        createNewFile(renamed);
+        manager.flushEvents();
+        Assert.assertEquals(objectsAdded.size(), 1);
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testRenameObjectFailed() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        MtpStorageManager.MtpObject obj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId()).findFirst().get();
+        Assert.assertTrue(manager.beginRenameObject(obj, "renamed"));
+
+        Assert.assertTrue(manager.endRenameObject(obj, newFile.getName(), false));
+
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertEquals(objectsRemoved.size(), 0);
+
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testRenameObjectFailedOldRemoved() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        MtpStorageManager.MtpObject obj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId()).findFirst().get();
+        Assert.assertTrue(manager.beginRenameObject(obj, "renamed"));
+
+        Assert.assertTrue(newFile.delete());
+        manager.flushEvents();
+        Assert.assertTrue(manager.endRenameObject(obj, newFile.getName(), false));
+
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertEquals(objectsRemoved.size(), 1);
+
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testRenameObjectFailedNewAdded() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        MtpStorageManager.MtpObject obj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId()).findFirst().get();
+        Assert.assertTrue(manager.beginRenameObject(obj, "renamed"));
+
+        createNewFile(mainStorageDir, "renamed");
+        manager.flushEvents();
+        Assert.assertTrue(manager.endRenameObject(obj, newFile.getName(), false));
+
+        Assert.assertEquals(objectsAdded.size(), 1);
+        Assert.assertEquals(objectsRemoved.size(), 0);
+
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testMoveObjectSuccess() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        File dir = createNewDir(mainStorageDir);
+        MtpStorageManager.MtpObject dirObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId())
+                .filter(MtpStorageManager.MtpObject::isDir).findFirst().get();
+        MtpStorageManager.MtpObject fileObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId())
+                .filter(o -> !o.isDir()).findFirst().get();
+        Assert.assertTrue(manager.beginMoveObject(fileObj, dirObj));
+
+        File moved = new File(dir, newFile.getName());
+        Assert.assertTrue(newFile.renameTo(moved));
+        manager.flushEvents();
+        Assert.assertTrue(manager.endMoveObject(
+                manager.getStorageRoot(mainMtpStorage.getStorageId()),
+                dirObj, newFile.getName(), true));
+
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertEquals(objectsRemoved.size(), 0);
+        Assert.assertEquals(fileObj.getPath().toString(), moved.getPath());
+
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testMoveObjectDirSuccess() {
+        logMethodName();
+        File newDir = createNewDir(mainStorageDir);
+        File movedDir = createNewDir(mainStorageDir);
+        MtpStorageManager.MtpObject dirObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId())
+                .filter(o -> o.getName().equals(newDir.getName())).findFirst().get();
+        MtpStorageManager.MtpObject movedObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId())
+                .filter(o -> o.getName().equals(movedDir.getName())).findFirst().get();
+        Assert.assertTrue(manager.beginMoveObject(movedObj, dirObj));
+
+        File renamed = new File(newDir, movedDir.getName());
+        Assert.assertTrue(movedDir.renameTo(renamed));
+        manager.flushEvents();
+        Assert.assertTrue(manager.endMoveObject(
+                manager.getStorageRoot(mainMtpStorage.getStorageId()),
+                dirObj, movedDir.getName(), true));
+
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertEquals(objectsRemoved.size(), 0);
+        Assert.assertEquals(movedObj.getPath().toString(), renamed.getPath());
+
+        Assert.assertTrue(manager.checkConsistency());
+
+        // Don't expect events
+        createNewFile(renamed);
+        manager.flushEvents();
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testMoveObjectDirVisitedSuccess() {
+        logMethodName();
+        File newDir = createNewDir(mainStorageDir);
+        File movedDir = createNewDir(mainStorageDir);
+        MtpStorageManager.MtpObject dirObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId())
+                .filter(o -> o.getName().equals(newDir.getName())).findFirst().get();
+        MtpStorageManager.MtpObject movedObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId())
+                .filter(o -> o.getName().equals(movedDir.getName())).findFirst().get();
+        manager.getObjects(movedObj.getId(), 0, mainMtpStorage.getStorageId());
+        Assert.assertTrue(manager.beginMoveObject(movedObj, dirObj));
+
+        File renamed = new File(newDir, movedDir.getName());
+        Assert.assertTrue(movedDir.renameTo(renamed));
+        manager.flushEvents();
+        Assert.assertTrue(manager.endMoveObject(
+                manager.getStorageRoot(mainMtpStorage.getStorageId()),
+                dirObj, movedDir.getName(), true));
+
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertEquals(objectsRemoved.size(), 0);
+        Assert.assertEquals(movedObj.getPath().toString(), renamed.getPath());
+
+        Assert.assertTrue(manager.checkConsistency());
+
+        // Expect events since the dir was visited
+        createNewFile(renamed);
+        manager.flushEvents();
+        Assert.assertEquals(objectsAdded.size(), 1);
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testMoveObjectDelayed() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        File dir = createNewDir(mainStorageDir);
+        MtpStorageManager.MtpObject dirObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId())
+                .filter(MtpStorageManager.MtpObject::isDir).findFirst().get();
+        MtpStorageManager.MtpObject fileObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId())
+                .filter(o -> !o.isDir()).findFirst().get();
+        Assert.assertTrue(manager.beginMoveObject(fileObj, dirObj));
+
+        Assert.assertTrue(manager.endMoveObject(
+                manager.getStorageRoot(mainMtpStorage.getStorageId()),
+                dirObj, newFile.getName(), true));
+
+        File moved = new File(dir, newFile.getName());
+        Assert.assertTrue(newFile.renameTo(moved));
+        manager.flushEvents();
+
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertEquals(objectsRemoved.size(), 0);
+        Assert.assertEquals(fileObj.getPath().toString(), moved.getPath());
+
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testMoveObjectDirVisitedDelayed() {
+        logMethodName();
+        File newDir = createNewDir(mainStorageDir);
+        File movedDir = createNewDir(mainStorageDir);
+        MtpStorageManager.MtpObject dirObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId())
+                .filter(o -> o.getName().equals(newDir.getName())).findFirst().get();
+        MtpStorageManager.MtpObject movedObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId())
+                .filter(o -> o.getName().equals(movedDir.getName())).findFirst().get();
+        manager.getObjects(movedObj.getId(), 0, mainMtpStorage.getStorageId());
+        Assert.assertTrue(manager.beginMoveObject(movedObj, dirObj));
+
+        Assert.assertTrue(manager.endMoveObject(
+                manager.getStorageRoot(mainMtpStorage.getStorageId()),
+                dirObj, movedDir.getName(), true));
+
+        File renamed = new File(newDir, movedDir.getName());
+        Assert.assertTrue(movedDir.renameTo(renamed));
+        manager.flushEvents();
+
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertEquals(objectsRemoved.size(), 0);
+        Assert.assertEquals(movedObj.getPath().toString(), renamed.getPath());
+
+        Assert.assertTrue(manager.checkConsistency());
+
+        // Expect events since the dir was visited
+        createNewFile(renamed);
+        manager.flushEvents();
+        Assert.assertEquals(objectsAdded.size(), 1);
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testMoveObjectFailed() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        File dir = createNewDir(mainStorageDir);
+        MtpStorageManager.MtpObject dirObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId())
+                .filter(MtpStorageManager.MtpObject::isDir).findFirst().get();
+        MtpStorageManager.MtpObject fileObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId())
+                .filter(o -> !o.isDir()).findFirst().get();
+        Assert.assertTrue(manager.beginMoveObject(fileObj, dirObj));
+
+        Assert.assertTrue(manager.endMoveObject(
+                manager.getStorageRoot(mainMtpStorage.getStorageId()),
+                dirObj, newFile.getName(), false));
+
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertEquals(objectsRemoved.size(), 0);
+
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testMoveObjectFailedOldRemoved() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        File dir = createNewDir(mainStorageDir);
+        MtpStorageManager.MtpObject dirObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId())
+                .filter(MtpStorageManager.MtpObject::isDir).findFirst().get();
+        MtpStorageManager.MtpObject fileObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId())
+                .filter(o -> !o.isDir()).findFirst().get();
+        Assert.assertTrue(manager.beginMoveObject(fileObj, dirObj));
+
+        Assert.assertTrue(newFile.delete());
+        manager.flushEvents();
+        Assert.assertTrue(manager.endMoveObject(
+                manager.getStorageRoot(mainMtpStorage.getStorageId()),
+                dirObj, newFile.getName(), false));
+
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertEquals(objectsRemoved.size(), 1);
+
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testMoveObjectFailedNewAdded() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        File dir = createNewDir(mainStorageDir);
+        MtpStorageManager.MtpObject dirObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId())
+                .filter(MtpStorageManager.MtpObject::isDir).findFirst().get();
+        MtpStorageManager.MtpObject fileObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId())
+                .filter(o -> !o.isDir()).findFirst().get();
+        Assert.assertTrue(manager.beginMoveObject(fileObj, dirObj));
+
+        createNewFile(dir, newFile.getName());
+        manager.flushEvents();
+        Assert.assertTrue(manager.endMoveObject(
+                manager.getStorageRoot(mainMtpStorage.getStorageId()),
+                dirObj, newFile.getName(), false));
+
+        Assert.assertEquals(objectsAdded.size(), 1);
+        Assert.assertEquals(objectsRemoved.size(), 0);
+
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testMoveObjectXStorageSuccess() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        MtpStorageManager.MtpObject fileObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId()).findFirst().get();
+        Assert.assertTrue(manager.beginMoveObject(fileObj,
+                manager.getStorageRoot(secondaryMtpStorage.getStorageId())));
+
+        Assert.assertTrue(newFile.delete());
+        File moved = createNewFile(secondaryStorageDir, newFile.getName());
+        manager.flushEvents();
+        Assert.assertTrue(manager.endMoveObject(
+                manager.getStorageRoot(mainMtpStorage.getStorageId()),
+                manager.getStorageRoot(secondaryMtpStorage.getStorageId()),
+                newFile.getName(), true));
+
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertEquals(objectsRemoved.size(), 0);
+        Assert.assertEquals(manager.getObject(fileObj.getId()).getPath().toString(),
+                moved.getPath());
+
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testMoveObjectXStorageDirSuccess() {
+        logMethodName();
+        File movedDir = createNewDir(mainStorageDir);
+        MtpStorageManager.MtpObject movedObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId()).findFirst().get();
+        Assert.assertTrue(manager.beginMoveObject(movedObj,
+                manager.getStorageRoot(secondaryMtpStorage.getStorageId())));
+
+        Assert.assertTrue(movedDir.delete());
+        File moved = createNewDir(secondaryStorageDir, movedDir.getName());
+        manager.flushEvents();
+        Assert.assertTrue(manager.endMoveObject(
+                manager.getStorageRoot(mainMtpStorage.getStorageId()),
+                manager.getStorageRoot(secondaryMtpStorage.getStorageId()),
+                movedDir.getName(), true));
+
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertEquals(objectsRemoved.size(), 0);
+        Assert.assertEquals(manager.getObject(movedObj.getId()).getPath().toString(),
+                moved.getPath());
+
+        Assert.assertTrue(manager.checkConsistency());
+
+        // Don't expect events
+        createNewFile(moved);
+        manager.flushEvents();
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testMoveObjectXStorageDirVisitedSuccess() {
+        logMethodName();
+        File movedDir = createNewDir(mainStorageDir);
+        MtpStorageManager.MtpObject movedObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId()).findFirst().get();
+        manager.getObjects(movedObj.getId(), 0, mainMtpStorage.getStorageId());
+        Assert.assertTrue(manager.beginMoveObject(movedObj,
+                manager.getStorageRoot(secondaryMtpStorage.getStorageId())));
+
+        Assert.assertTrue(movedDir.delete());
+        File moved = createNewDir(secondaryStorageDir, movedDir.getName());
+        manager.flushEvents();
+        Assert.assertTrue(manager.endMoveObject(
+                manager.getStorageRoot(mainMtpStorage.getStorageId()),
+                manager.getStorageRoot(secondaryMtpStorage.getStorageId()),
+                movedDir.getName(), true));
+
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertEquals(objectsRemoved.size(), 0);
+        Assert.assertEquals(manager.getObject(movedObj.getId()).getPath().toString(),
+                moved.getPath());
+
+        Assert.assertTrue(manager.checkConsistency());
+
+        // Expect events since the dir was visited
+        createNewFile(moved);
+        manager.flushEvents();
+        Assert.assertEquals(objectsAdded.size(), 1);
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testMoveObjectXStorageDelayed() {
+        logMethodName();
+        File movedFile = createNewFile(mainStorageDir);
+        MtpStorageManager.MtpObject movedObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId()).findFirst().get();
+        Assert.assertTrue(manager.beginMoveObject(movedObj,
+                manager.getStorageRoot(secondaryMtpStorage.getStorageId())));
+
+        Assert.assertTrue(manager.endMoveObject(
+                manager.getStorageRoot(mainMtpStorage.getStorageId()),
+                manager.getStorageRoot(secondaryMtpStorage.getStorageId()),
+                movedFile.getName(), true));
+
+        Assert.assertTrue(movedFile.delete());
+        File moved = createNewFile(secondaryStorageDir, movedFile.getName());
+        manager.flushEvents();
+
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertEquals(objectsRemoved.size(), 0);
+        Assert.assertEquals(manager.getObject(movedObj.getId()).getPath().toString(),
+                moved.getPath());
+
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testMoveObjectXStorageDirVisitedDelayed() {
+        logMethodName();
+        File movedDir = createNewDir(mainStorageDir);
+        MtpStorageManager.MtpObject movedObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId()).findFirst().get();
+        manager.getObjects(movedObj.getId(), 0, mainMtpStorage.getStorageId());
+        Assert.assertTrue(manager.beginMoveObject(movedObj,
+                manager.getStorageRoot(secondaryMtpStorage.getStorageId())));
+
+        Assert.assertTrue(manager.endMoveObject(
+                manager.getStorageRoot(mainMtpStorage.getStorageId()),
+                manager.getStorageRoot(secondaryMtpStorage.getStorageId()),
+                movedDir.getName(), true));
+
+        Assert.assertTrue(movedDir.delete());
+        File moved = createNewDir(secondaryStorageDir, movedDir.getName());
+        manager.flushEvents();
+
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertEquals(objectsRemoved.size(), 0);
+        Assert.assertEquals(manager.getObject(movedObj.getId()).getPath().toString(),
+                moved.getPath());
+
+        Assert.assertTrue(manager.checkConsistency());
+
+        // Expect events since the dir was visited
+        createNewFile(moved);
+        manager.flushEvents();
+        Assert.assertEquals(objectsAdded.size(), 1);
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testMoveObjectXStorageFailed() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        MtpStorageManager.MtpObject fileObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId()).findFirst().get();
+        Assert.assertTrue(manager.beginMoveObject(fileObj,
+                manager.getStorageRoot(secondaryMtpStorage.getStorageId())));
+
+        Assert.assertTrue(manager.endMoveObject(
+                manager.getStorageRoot(mainMtpStorage.getStorageId()),
+                manager.getStorageRoot(secondaryMtpStorage.getStorageId()),
+                newFile.getName(), false));
+
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertEquals(objectsRemoved.size(), 0);
+
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testMoveObjectXStorageFailedOldRemoved() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        MtpStorageManager.MtpObject fileObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId()).findFirst().get();
+        Assert.assertTrue(manager.beginMoveObject(fileObj,
+                manager.getStorageRoot(secondaryMtpStorage.getStorageId())));
+
+        Assert.assertTrue(newFile.delete());
+        manager.flushEvents();
+        Assert.assertTrue(manager.endMoveObject(
+                manager.getStorageRoot(mainMtpStorage.getStorageId()),
+                manager.getStorageRoot(secondaryMtpStorage.getStorageId()),
+                newFile.getName(), false));
+
+        Assert.assertEquals(objectsAdded.size(), 0);
+        Assert.assertEquals(objectsRemoved.size(), 1);
+
+        Assert.assertTrue(manager.checkConsistency());
+    }
+
+    @Test
+    @SmallTest
+    public void testMoveObjectXStorageFailedNewAdded() {
+        logMethodName();
+        File newFile = createNewFile(mainStorageDir);
+        MtpStorageManager.MtpObject fileObj = manager.getObjects(0xFFFFFFFF, 0,
+                mainMtpStorage.getStorageId()).findFirst().get();
+        Assert.assertTrue(manager.beginMoveObject(fileObj,
+                manager.getStorageRoot(secondaryMtpStorage.getStorageId())));
+
+        createNewFile(secondaryStorageDir, newFile.getName());
+        manager.flushEvents();
+        Assert.assertTrue(manager.endMoveObject(
+                manager.getStorageRoot(mainMtpStorage.getStorageId()),
+                manager.getStorageRoot(secondaryMtpStorage.getStorageId()),
+                newFile.getName(), false));
+
+        Assert.assertEquals(objectsAdded.size(), 1);
+        Assert.assertEquals(objectsRemoved.size(), 0);
+
+        Assert.assertTrue(manager.checkConsistency());
+    }
+}
\ No newline at end of file
diff --git a/packages/SettingsLib/tests/robotests/Android.mk b/packages/SettingsLib/tests/robotests/Android.mk
index 2738027..02a4973 100644
--- a/packages/SettingsLib/tests/robotests/Android.mk
+++ b/packages/SettingsLib/tests/robotests/Android.mk
@@ -49,7 +49,7 @@
 
 LOCAL_JAVA_LIBRARIES := \
     junit \
-    platform-robolectric-3.4.2-prebuilt
+    platform-robolectric-3.5.1-prebuilt
 
 LOCAL_INSTRUMENTATION_FOR := SettingsLibShell
 LOCAL_MODULE := SettingsLibRoboTests
@@ -74,4 +74,4 @@
 
 LOCAL_ROBOTEST_TIMEOUT := 36000
 
-include prebuilts/misc/common/robolectric/3.4.2/run_robotests.mk
+include prebuilts/misc/common/robolectric/3.5.1/run_robotests.mk
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/SettingsLibRobolectricTestRunner.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/SettingsLibRobolectricTestRunner.java
index 698e442..df850be 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/SettingsLibRobolectricTestRunner.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/SettingsLibRobolectricTestRunner.java
@@ -38,32 +38,25 @@
         final String resDir = appRoot + "/tests/robotests/res";
         final String assetsDir = appRoot + config.assetDir();
 
-        final AndroidManifest manifest = new AndroidManifest(Fs.fileFromPath(manifestPath),
-                Fs.fileFromPath(resDir), Fs.fileFromPath(assetsDir)) {
+        return new AndroidManifest(Fs.fileFromPath(manifestPath), Fs.fileFromPath(resDir),
+            Fs.fileFromPath(assetsDir), "com.android.settingslib") {
             @Override
             public List<ResourcePath> getIncludedResourcePaths() {
                 List<ResourcePath> paths = super.getIncludedResourcePaths();
-                SettingsLibRobolectricTestRunner.getIncludedResourcePaths(getPackageName(), paths);
+                paths.add(new ResourcePath(
+                    null,
+                    Fs.fileFromPath("./frameworks/base/packages/SettingsLib/res"),
+                    null));
+                paths.add(new ResourcePath(
+                    null,
+                    Fs.fileFromPath("./frameworks/base/core/res/res"),
+                    null));
+                paths.add(new ResourcePath(
+                    null,
+                    Fs.fileFromPath("./frameworks/support/v7/appcompat/res"),
+                    null));
                 return paths;
             }
         };
-        manifest.setPackageName("com.android.settingslib");
-        return manifest;
     }
-
-    static void getIncludedResourcePaths(String packageName, List<ResourcePath> paths) {
-        paths.add(new ResourcePath(
-                null,
-                Fs.fileFromPath("./frameworks/base/packages/SettingsLib/res"),
-                null));
-        paths.add(new ResourcePath(
-                null,
-                Fs.fileFromPath("./frameworks/base/core/res/res"),
-                null));
-        paths.add(new ResourcePath(
-                null,
-                Fs.fileFromPath("./frameworks/support/v7/appcompat/res"),
-                null));
-    }
-
 }
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java b/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java
index ebeb351..e80d6d3 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIFactory.java
@@ -30,6 +30,8 @@
 import com.android.systemui.statusbar.KeyguardIndicationController;
 import com.android.systemui.statusbar.NotificationGutsManager;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
+import com.android.systemui.statusbar.NotificationListener;
+import com.android.systemui.statusbar.NotificationLogger;
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
 import com.android.systemui.statusbar.ScrimView;
 import com.android.systemui.statusbar.phone.DozeParameters;
@@ -37,6 +39,7 @@
 import com.android.systemui.statusbar.phone.LightBarController;
 import com.android.systemui.statusbar.phone.LockIcon;
 import com.android.systemui.statusbar.phone.LockscreenWallpaper;
+import com.android.systemui.statusbar.phone.NotificationGroupManager;
 import com.android.systemui.statusbar.phone.NotificationIconAreaController;
 import com.android.systemui.statusbar.phone.ScrimController;
 import com.android.systemui.statusbar.phone.StatusBar;
@@ -114,10 +117,16 @@
             Context context) {
         providers.put(NotificationLockscreenUserManager.class,
                 () -> new NotificationLockscreenUserManager(context));
+        providers.put(NotificationGroupManager.class, NotificationGroupManager::new);
         providers.put(NotificationGutsManager.class, () -> new NotificationGutsManager(
                 Dependency.get(NotificationLockscreenUserManager.class), context));
         providers.put(NotificationRemoteInputManager.class,
                 () -> new NotificationRemoteInputManager(
                         Dependency.get(NotificationLockscreenUserManager.class), context));
+        providers.put(NotificationListener.class, () -> new NotificationListener(
+                Dependency.get(NotificationRemoteInputManager.class), context));
+        providers.put(NotificationLogger.class, () -> new NotificationLogger(
+                Dependency.get(NotificationListener.class),
+                Dependency.get(UiOffloadThread.class)));
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java
index 4952da4..a72e8ac 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java
@@ -36,13 +36,13 @@
 public class NotificationListener extends NotificationListenerWithPlugins {
     private static final String TAG = "NotificationListener";
 
-    private final NotificationPresenter mPresenter;
     private final NotificationRemoteInputManager mRemoteInputManager;
     private final Context mContext;
 
-    public NotificationListener(NotificationPresenter presenter,
-            NotificationRemoteInputManager remoteInputManager, Context context) {
-        mPresenter = presenter;
+    private NotificationPresenter mPresenter;
+
+    public NotificationListener(NotificationRemoteInputManager remoteInputManager,
+            Context context) {
         mRemoteInputManager = remoteInputManager;
         mContext = context;
     }
@@ -120,7 +120,9 @@
         }
     }
 
-    public void register() {
+    public void setUpWithPresenter(NotificationPresenter presenter) {
+        mPresenter = presenter;
+
         try {
             registerAsSystemService(mContext,
                     new ComponentName(mContext.getPackageName(), getClass().getCanonicalName()),
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLogger.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLogger.java
new file mode 100644
index 0000000..e58d801
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLogger.java
@@ -0,0 +1,223 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.systemui.statusbar;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.SystemClock;
+import android.service.notification.NotificationListenerService;
+import android.util.ArraySet;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.statusbar.IStatusBarService;
+import com.android.internal.statusbar.NotificationVisibility;
+import com.android.systemui.UiOffloadThread;
+import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+
+/**
+ * Handles notification logging, in particular, logging which notifications are visible and which
+ * are not.
+ */
+public class NotificationLogger {
+    private static final String TAG = "NotificationLogger";
+
+    /** The minimum delay in ms between reports of notification visibility. */
+    private static final int VISIBILITY_REPORT_MIN_DELAY_MS = 500;
+
+    /** Keys of notifications currently visible to the user. */
+    private final ArraySet<NotificationVisibility> mCurrentlyVisibleNotifications =
+            new ArraySet<>();
+    private final NotificationListenerService mNotificationListener;
+    private final UiOffloadThread mUiOffloadThread;
+
+    protected NotificationPresenter mPresenter;
+    protected Handler mHandler = new Handler();
+    protected IStatusBarService mBarService;
+    private long mLastVisibilityReportUptimeMs;
+    private NotificationStackScrollLayout mStackScroller;
+
+    protected final NotificationStackScrollLayout.OnChildLocationsChangedListener
+            mNotificationLocationsChangedListener =
+            new NotificationStackScrollLayout.OnChildLocationsChangedListener() {
+                @Override
+                public void onChildLocationsChanged(
+                        NotificationStackScrollLayout stackScrollLayout) {
+                    if (mHandler.hasCallbacks(mVisibilityReporter)) {
+                        // Visibilities will be reported when the existing
+                        // callback is executed.
+                        return;
+                    }
+                    // Calculate when we're allowed to run the visibility
+                    // reporter. Note that this timestamp might already have
+                    // passed. That's OK, the callback will just be executed
+                    // ASAP.
+                    long nextReportUptimeMs =
+                            mLastVisibilityReportUptimeMs + VISIBILITY_REPORT_MIN_DELAY_MS;
+                    mHandler.postAtTime(mVisibilityReporter, nextReportUptimeMs);
+                }
+            };
+
+    // Tracks notifications currently visible in mNotificationStackScroller and
+    // emits visibility events via NoMan on changes.
+    protected final Runnable mVisibilityReporter = new Runnable() {
+        private final ArraySet<NotificationVisibility> mTmpNewlyVisibleNotifications =
+                new ArraySet<>();
+        private final ArraySet<NotificationVisibility> mTmpCurrentlyVisibleNotifications =
+                new ArraySet<>();
+        private final ArraySet<NotificationVisibility> mTmpNoLongerVisibleNotifications =
+                new ArraySet<>();
+
+        @Override
+        public void run() {
+            mLastVisibilityReportUptimeMs = SystemClock.uptimeMillis();
+
+            // 1. Loop over mNotificationData entries:
+            //   A. Keep list of visible notifications.
+            //   B. Keep list of previously hidden, now visible notifications.
+            // 2. Compute no-longer visible notifications by removing currently
+            //    visible notifications from the set of previously visible
+            //    notifications.
+            // 3. Report newly visible and no-longer visible notifications.
+            // 4. Keep currently visible notifications for next report.
+            ArrayList<NotificationData.Entry> activeNotifications = mPresenter.
+                    getNotificationData().getActiveNotifications();
+            int N = activeNotifications.size();
+            for (int i = 0; i < N; i++) {
+                NotificationData.Entry entry = activeNotifications.get(i);
+                String key = entry.notification.getKey();
+                boolean isVisible = mStackScroller.isInVisibleLocation(entry.row);
+                NotificationVisibility visObj = NotificationVisibility.obtain(key, i, isVisible);
+                boolean previouslyVisible = mCurrentlyVisibleNotifications.contains(visObj);
+                if (isVisible) {
+                    // Build new set of visible notifications.
+                    mTmpCurrentlyVisibleNotifications.add(visObj);
+                    if (!previouslyVisible) {
+                        mTmpNewlyVisibleNotifications.add(visObj);
+                    }
+                } else {
+                    // release object
+                    visObj.recycle();
+                }
+            }
+            mTmpNoLongerVisibleNotifications.addAll(mCurrentlyVisibleNotifications);
+            mTmpNoLongerVisibleNotifications.removeAll(mTmpCurrentlyVisibleNotifications);
+
+            logNotificationVisibilityChanges(
+                    mTmpNewlyVisibleNotifications, mTmpNoLongerVisibleNotifications);
+
+            recycleAllVisibilityObjects(mCurrentlyVisibleNotifications);
+            mCurrentlyVisibleNotifications.addAll(mTmpCurrentlyVisibleNotifications);
+
+            recycleAllVisibilityObjects(mTmpNoLongerVisibleNotifications);
+            mTmpCurrentlyVisibleNotifications.clear();
+            mTmpNewlyVisibleNotifications.clear();
+            mTmpNoLongerVisibleNotifications.clear();
+        }
+    };
+
+    public NotificationLogger(NotificationListenerService notificationListener,
+            UiOffloadThread uiOffloadThread) {
+        mNotificationListener = notificationListener;
+        mUiOffloadThread = uiOffloadThread;
+        mBarService = IStatusBarService.Stub.asInterface(
+                ServiceManager.getService(Context.STATUS_BAR_SERVICE));
+    }
+
+    // TODO: Remove dependency on NotificationStackScrollLayout.
+    public void setUpWithPresenter(NotificationPresenter presenter,
+            NotificationStackScrollLayout stackScroller) {
+        mPresenter = presenter;
+        mStackScroller = stackScroller;
+    }
+
+    public void stopNotificationLogging() {
+        // Report all notifications as invisible and turn down the
+        // reporter.
+        if (!mCurrentlyVisibleNotifications.isEmpty()) {
+            logNotificationVisibilityChanges(
+                    Collections.emptyList(), mCurrentlyVisibleNotifications);
+            recycleAllVisibilityObjects(mCurrentlyVisibleNotifications);
+        }
+        mHandler.removeCallbacks(mVisibilityReporter);
+        mStackScroller.setChildLocationsChangedListener(null);
+    }
+
+    public void startNotificationLogging() {
+        mStackScroller.setChildLocationsChangedListener(mNotificationLocationsChangedListener);
+        // Some transitions like mVisibleToUser=false -> mVisibleToUser=true don't
+        // cause the scroller to emit child location events. Hence generate
+        // one ourselves to guarantee that we're reporting visible
+        // notifications.
+        // (Note that in cases where the scroller does emit events, this
+        // additional event doesn't break anything.)
+        mNotificationLocationsChangedListener.onChildLocationsChanged(mStackScroller);
+    }
+
+    private void logNotificationVisibilityChanges(
+            Collection<NotificationVisibility> newlyVisible,
+            Collection<NotificationVisibility> noLongerVisible) {
+        if (newlyVisible.isEmpty() && noLongerVisible.isEmpty()) {
+            return;
+        }
+        NotificationVisibility[] newlyVisibleAr =
+                newlyVisible.toArray(new NotificationVisibility[newlyVisible.size()]);
+        NotificationVisibility[] noLongerVisibleAr =
+                noLongerVisible.toArray(new NotificationVisibility[noLongerVisible.size()]);
+        mUiOffloadThread.submit(() -> {
+            try {
+                mBarService.onNotificationVisibilityChanged(newlyVisibleAr, noLongerVisibleAr);
+            } catch (RemoteException e) {
+                // Ignore.
+            }
+
+            final int N = newlyVisible.size();
+            if (N > 0) {
+                String[] newlyVisibleKeyAr = new String[N];
+                for (int i = 0; i < N; i++) {
+                    newlyVisibleKeyAr[i] = newlyVisibleAr[i].key;
+                }
+
+                // TODO: Call NotificationEntryManager to do this, once it exists.
+                // TODO: Consider not catching all runtime exceptions here.
+                try {
+                    mNotificationListener.setNotificationsShown(newlyVisibleKeyAr);
+                } catch (RuntimeException e) {
+                    Log.d(TAG, "failed setNotificationsShown: ", e);
+                }
+            }
+        });
+    }
+
+    private void recycleAllVisibilityObjects(ArraySet<NotificationVisibility> array) {
+        final int N = array.size();
+        for (int i = 0 ; i < N; i++) {
+            array.valueAt(i).recycle();
+        }
+        array.clear();
+    }
+
+    @VisibleForTesting
+    public Runnable getVisibilityReporter() {
+        return mVisibilityReporter;
+    }
+}
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 d162448..fecd6bd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
@@ -136,7 +136,6 @@
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
 import com.android.internal.statusbar.IStatusBarService;
-import com.android.internal.statusbar.NotificationVisibility;
 import com.android.internal.statusbar.StatusBarIcon;
 import com.android.internal.util.NotificationMessagingUtil;
 import com.android.internal.widget.LockPatternUtils;
@@ -203,6 +202,7 @@
 import com.android.systemui.statusbar.NotificationInfo;
 import com.android.systemui.statusbar.NotificationListener;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
+import com.android.systemui.statusbar.NotificationLogger;
 import com.android.systemui.statusbar.NotificationMediaManager;
 import com.android.systemui.statusbar.NotificationPresenter;
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
@@ -236,8 +236,6 @@
 import com.android.systemui.statusbar.policy.UserInfoControllerImpl;
 import com.android.systemui.statusbar.policy.UserSwitcherController;
 import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
-import com.android.systemui.statusbar.stack.NotificationStackScrollLayout
-        .OnChildLocationsChangedListener;
 import com.android.systemui.util.NotificationChannels;
 import com.android.systemui.util.leak.LeakDetector;
 import com.android.systemui.volume.VolumeComponent;
@@ -247,7 +245,6 @@
 import java.io.StringWriter;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -317,9 +314,6 @@
             View.STATUS_BAR_TRANSIENT | View.NAVIGATION_BAR_TRANSIENT;
     private static final long AUTOHIDE_TIMEOUT_MS = 2250;
 
-    /** The minimum delay in ms between reports of notification visibility. */
-    private static final int VISIBILITY_REPORT_MIN_DELAY_MS = 500;
-
     /**
      * The delay to reset the hint text when the hint animation is finished running.
      */
@@ -431,6 +425,7 @@
     private final ArrayList<Runnable> mPostCollapseRunnables = new ArrayList<>();
 
     private NotificationGutsManager mGutsManager;
+    protected NotificationLogger mNotificationLogger;
 
     // for disabling the status bar
     private int mDisabled1 = 0;
@@ -531,10 +526,7 @@
     protected NotificationLockscreenUserManager mLockscreenUserManager;
     protected NotificationRemoteInputManager mRemoteInputManager;
 
-    /** Keys of notifications currently visible to the user. */
-    private final ArraySet<NotificationVisibility> mCurrentlyVisibleNotifications =
-            new ArraySet<>();
-    private long mLastVisibilityReportUptimeMs;
+
 
     private Runnable mLaunchTransitionEndRunnable;
     protected boolean mLaunchTransitionFadingAway;
@@ -557,83 +549,6 @@
     private boolean mWereIconsJustHidden;
     private boolean mBouncerWasShowingWhenHidden;
 
-    private final OnChildLocationsChangedListener mNotificationLocationsChangedListener =
-            new OnChildLocationsChangedListener() {
-                @Override
-                public void onChildLocationsChanged(
-                        NotificationStackScrollLayout stackScrollLayout) {
-                    if (mHandler.hasCallbacks(mVisibilityReporter)) {
-                        // Visibilities will be reported when the existing
-                        // callback is executed.
-                        return;
-                    }
-                    // Calculate when we're allowed to run the visibility
-                    // reporter. Note that this timestamp might already have
-                    // passed. That's OK, the callback will just be executed
-                    // ASAP.
-                    long nextReportUptimeMs =
-                            mLastVisibilityReportUptimeMs + VISIBILITY_REPORT_MIN_DELAY_MS;
-                    mHandler.postAtTime(mVisibilityReporter, nextReportUptimeMs);
-                }
-            };
-
-    // Tracks notifications currently visible in mNotificationStackScroller and
-    // emits visibility events via NoMan on changes.
-    protected final Runnable mVisibilityReporter = new Runnable() {
-        private final ArraySet<NotificationVisibility> mTmpNewlyVisibleNotifications =
-                new ArraySet<>();
-        private final ArraySet<NotificationVisibility> mTmpCurrentlyVisibleNotifications =
-                new ArraySet<>();
-        private final ArraySet<NotificationVisibility> mTmpNoLongerVisibleNotifications =
-                new ArraySet<>();
-
-        @Override
-        public void run() {
-            mLastVisibilityReportUptimeMs = SystemClock.uptimeMillis();
-
-            // 1. Loop over mNotificationData entries:
-            //   A. Keep list of visible notifications.
-            //   B. Keep list of previously hidden, now visible notifications.
-            // 2. Compute no-longer visible notifications by removing currently
-            //    visible notifications from the set of previously visible
-            //    notifications.
-            // 3. Report newly visible and no-longer visible notifications.
-            // 4. Keep currently visible notifications for next report.
-            ArrayList<Entry> activeNotifications = mNotificationData.getActiveNotifications();
-            int N = activeNotifications.size();
-            for (int i = 0; i < N; i++) {
-                Entry entry = activeNotifications.get(i);
-                String key = entry.notification.getKey();
-                boolean isVisible = mStackScroller.isInVisibleLocation(entry.row);
-                NotificationVisibility visObj = NotificationVisibility.obtain(key, i, isVisible);
-                boolean previouslyVisible = mCurrentlyVisibleNotifications.contains(visObj);
-                if (isVisible) {
-                    // Build new set of visible notifications.
-                    mTmpCurrentlyVisibleNotifications.add(visObj);
-                    if (!previouslyVisible) {
-                        mTmpNewlyVisibleNotifications.add(visObj);
-                    }
-                } else {
-                    // release object
-                    visObj.recycle();
-                }
-            }
-            mTmpNoLongerVisibleNotifications.addAll(mCurrentlyVisibleNotifications);
-            mTmpNoLongerVisibleNotifications.removeAll(mTmpCurrentlyVisibleNotifications);
-
-            logNotificationVisibilityChanges(
-                    mTmpNewlyVisibleNotifications, mTmpNoLongerVisibleNotifications);
-
-            recycleAllVisibilityObjects(mCurrentlyVisibleNotifications);
-            mCurrentlyVisibleNotifications.addAll(mTmpCurrentlyVisibleNotifications);
-
-            recycleAllVisibilityObjects(mTmpNoLongerVisibleNotifications);
-            mTmpCurrentlyVisibleNotifications.clear();
-            mTmpNewlyVisibleNotifications.clear();
-            mTmpNoLongerVisibleNotifications.clear();
-        }
-    };
-
     // Notifies StatusBarKeyguardViewManager every time the keyguard transition is over,
     // this animation is tied to the scrim for historic reasons.
     // TODO: notify when keyguard has faded away instead of the scrim.
@@ -680,14 +595,6 @@
     private ScreenLifecycle mScreenLifecycle;
     @VisibleForTesting WakefulnessLifecycle mWakefulnessLifecycle;
 
-    private void recycleAllVisibilityObjects(ArraySet<NotificationVisibility> array) {
-        final int N = array.size();
-        for (int i = 0 ; i < N; i++) {
-            array.valueAt(i).recycle();
-        }
-        array.clear();
-    }
-
     private final View.OnClickListener mGoToLockedShadeListener = v -> {
         if (mState == StatusBarState.KEYGUARD) {
             wakeUpIfDozing(SystemClock.uptimeMillis(), v);
@@ -715,6 +622,8 @@
 
     @Override
     public void start() {
+        mGroupManager = Dependency.get(NotificationGroupManager.class);
+        mNotificationLogger = Dependency.get(NotificationLogger.class);
         mRemoteInputManager = Dependency.get(NotificationRemoteInputManager.class);
         mNetworkController = Dependency.get(NetworkController.class);
         mUserSwitcherController = Dependency.get(UserSwitcherController.class);
@@ -809,8 +718,8 @@
         }
 
         // Set up the initial notification state.
-        mNotificationListener = new NotificationListener(this, mRemoteInputManager, mContext);
-        mNotificationListener.register();
+        mNotificationListener = Dependency.get(NotificationListener.class);
+        mNotificationListener.setUpWithPresenter(this);
 
         if (DEBUG) {
             Log.d(TAG, String.format(
@@ -895,6 +804,7 @@
                         // if we're here we're dead
                     }
                 });
+        mNotificationLogger.setUpWithPresenter(this, mStackScroller);
         mNotificationPanel.setStatusBar(this);
         mNotificationPanel.setGroupManager(mGroupManager);
         mAboveShelfObserver = new AboveShelfObserver(mStackScroller);
@@ -3617,9 +3527,9 @@
     protected void handleVisibleToUserChanged(boolean visibleToUser) {
         if (visibleToUser) {
             handleVisibleToUserChangedImpl(visibleToUser);
-            startNotificationLogging();
+            mNotificationLogger.startNotificationLogging();
         } else {
-            stopNotificationLogging();
+            mNotificationLogger.stopNotificationLogging();
             handleVisibleToUserChangedImpl(visibleToUser);
         }
     }
@@ -3671,60 +3581,6 @@
 
     }
 
-    private void stopNotificationLogging() {
-        // Report all notifications as invisible and turn down the
-        // reporter.
-        if (!mCurrentlyVisibleNotifications.isEmpty()) {
-            logNotificationVisibilityChanges(
-                    Collections.emptyList(), mCurrentlyVisibleNotifications);
-            recycleAllVisibilityObjects(mCurrentlyVisibleNotifications);
-        }
-        mHandler.removeCallbacks(mVisibilityReporter);
-        mStackScroller.setChildLocationsChangedListener(null);
-    }
-
-    private void startNotificationLogging() {
-        mStackScroller.setChildLocationsChangedListener(mNotificationLocationsChangedListener);
-        // Some transitions like mVisibleToUser=false -> mVisibleToUser=true don't
-        // cause the scroller to emit child location events. Hence generate
-        // one ourselves to guarantee that we're reporting visible
-        // notifications.
-        // (Note that in cases where the scroller does emit events, this
-        // additional event doesn't break anything.)
-        mNotificationLocationsChangedListener.onChildLocationsChanged(mStackScroller);
-    }
-
-    private void logNotificationVisibilityChanges(
-            Collection<NotificationVisibility> newlyVisible,
-            Collection<NotificationVisibility> noLongerVisible) {
-        if (newlyVisible.isEmpty() && noLongerVisible.isEmpty()) {
-            return;
-        }
-        NotificationVisibility[] newlyVisibleAr =
-                newlyVisible.toArray(new NotificationVisibility[newlyVisible.size()]);
-        NotificationVisibility[] noLongerVisibleAr =
-                noLongerVisible.toArray(new NotificationVisibility[noLongerVisible.size()]);
-        mUiOffloadThread.submit(() -> {
-            try {
-                mBarService.onNotificationVisibilityChanged(newlyVisibleAr, noLongerVisibleAr);
-            } catch (RemoteException e) {
-                // Ignore.
-            }
-
-            final int N = newlyVisible.size();
-            if (N > 0) {
-                String[] newlyVisibleKeyAr = new String[N];
-                for (int i = 0; i < N; i++) {
-                    newlyVisibleKeyAr[i] = newlyVisibleAr[i].key;
-                }
-
-                setNotificationsShown(newlyVisibleKeyAr);
-            }
-        });
-    }
-
-    // State logging
-
     private void logStateToEventlog() {
         boolean isShowing = mStatusBarKeyguardViewManager.isShowing();
         boolean isOccluded = mStatusBarKeyguardViewManager.isOccluded();
@@ -5394,7 +5250,7 @@
     protected NotificationData mNotificationData;
     protected NotificationStackScrollLayout mStackScroller;
 
-    protected final NotificationGroupManager mGroupManager = new NotificationGroupManager();
+    protected NotificationGroupManager mGroupManager;
 
 
     // for heads up notifications
@@ -5562,12 +5418,8 @@
     }
 
     protected void setNotificationShown(StatusBarNotification n) {
-        setNotificationsShown(new String[]{n.getKey()});
-    }
-
-    protected void setNotificationsShown(String[] keys) {
         try {
-            mNotificationListener.setNotificationsShown(keys);
+            mNotificationListener.setNotificationsShown(new String[]{n.getKey()});
         } catch (RuntimeException e) {
             Log.d(TAG, "failed setNotificationsShown: ", e);
         }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationListenerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationListenerTest.java
index f562340..ccc3006 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationListenerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationListenerTest.java
@@ -71,9 +71,11 @@
         when(mPresenter.getNotificationData()).thenReturn(mNotificationData);
         when(mRemoteInputManager.getKeysKeptForRemoteInput()).thenReturn(mKeysKeptForRemoteInput);
 
-        mListener = new NotificationListener(mPresenter, mRemoteInputManager, mContext);
+        mListener = new NotificationListener(mRemoteInputManager, mContext);
         mSbn = new StatusBarNotification(TEST_PACKAGE_NAME, TEST_PACKAGE_NAME, 0, null, TEST_UID, 0,
                 new Notification(), UserHandle.CURRENT, null, 0);
+
+        mListener.setUpWithPresenter(mPresenter);
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLoggerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLoggerTest.java
new file mode 100644
index 0000000..142ce63
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLoggerTest.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar;
+
+import static org.mockito.ArgumentMatchers.any;
+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.Notification;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.UserHandle;
+import android.service.notification.NotificationListenerService;
+import android.service.notification.StatusBarNotification;
+import android.support.test.filters.SmallTest;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+
+import com.android.internal.statusbar.IStatusBarService;
+import com.android.internal.statusbar.NotificationVisibility;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.UiOffloadThread;
+import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
+
+import com.google.android.collect.Lists;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class NotificationLoggerTest extends SysuiTestCase {
+    private static final String TEST_PACKAGE_NAME = "test";
+    private static final int TEST_UID = 0;
+
+    @Mock private NotificationPresenter mPresenter;
+    @Mock private NotificationListener mListener;
+    @Mock private NotificationStackScrollLayout mStackScroller;
+    @Mock private IStatusBarService mBarService;
+    @Mock private NotificationData mNotificationData;
+    @Mock private ExpandableNotificationRow mRow;
+
+    private NotificationData.Entry mEntry;
+    private StatusBarNotification mSbn;
+    private TestableNotificationLogger mLogger;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        when(mPresenter.getNotificationData()).thenReturn(mNotificationData);
+
+        mSbn = new StatusBarNotification(TEST_PACKAGE_NAME, TEST_PACKAGE_NAME, 0, null, TEST_UID,
+                0, new Notification(), UserHandle.CURRENT, null, 0);
+        mEntry = new NotificationData.Entry(mSbn);
+        mEntry.row = mRow;
+
+        mLogger = new TestableNotificationLogger(mListener, mDependency.get(UiOffloadThread.class),
+                mBarService);
+        mLogger.setUpWithPresenter(mPresenter, mStackScroller);
+    }
+
+    @Test
+    public void testOnChildLocationsChangedReportsVisibilityChanged() throws Exception {
+        when(mStackScroller.isInVisibleLocation(any())).thenReturn(true);
+        when(mNotificationData.getActiveNotifications()).thenReturn(Lists.newArrayList(mEntry));
+        mLogger.getChildLocationsChangedListenerForTest().onChildLocationsChanged(mStackScroller);
+        waitForIdleSync(mLogger.getHandlerForTest());
+        waitForUiOffloadThread();
+
+        NotificationVisibility[] newlyVisibleKeys = {
+                NotificationVisibility.obtain(mEntry.key, 0, true)
+        };
+        NotificationVisibility[] noLongerVisibleKeys = {};
+        verify(mBarService).onNotificationVisibilityChanged(newlyVisibleKeys, noLongerVisibleKeys);
+
+        // |mEntry| won't change visibility, so it shouldn't be reported again:
+        Mockito.reset(mBarService);
+        mLogger.getChildLocationsChangedListenerForTest().onChildLocationsChanged(mStackScroller);
+        waitForIdleSync(mLogger.getHandlerForTest());
+        waitForUiOffloadThread();
+
+        verify(mBarService, never()).onNotificationVisibilityChanged(any(), any());
+    }
+
+    @Test
+    public void testStoppingNotificationLoggingReportsCurrentNotifications()
+            throws Exception {
+        when(mStackScroller.isInVisibleLocation(any())).thenReturn(true);
+        when(mNotificationData.getActiveNotifications()).thenReturn(Lists.newArrayList(mEntry));
+        mLogger.getChildLocationsChangedListenerForTest().onChildLocationsChanged(mStackScroller);
+        waitForIdleSync(mLogger.getHandlerForTest());
+        waitForUiOffloadThread();
+        Mockito.reset(mBarService);
+
+        mLogger.stopNotificationLogging();
+        waitForUiOffloadThread();
+        // The visibility objects are recycled by NotificationLogger, so we can't use specific
+        // matchers here.
+        verify(mBarService, times(1)).onNotificationVisibilityChanged(any(), any());
+    }
+
+    private class TestableNotificationLogger extends NotificationLogger {
+
+        public TestableNotificationLogger(
+                NotificationListenerService notificationListener,
+                UiOffloadThread uiOffloadThread,
+                IStatusBarService barService) {
+            super(notificationListener, uiOffloadThread);
+            mBarService = barService;
+            // Make this on the main thread so we can wait for it during tests.
+            mHandler = new Handler(Looper.getMainLooper());
+        }
+
+        public NotificationStackScrollLayout.OnChildLocationsChangedListener
+                getChildLocationsChangedListenerForTest() {
+            return mNotificationLocationsChangedListener;
+        }
+
+        public Handler getHandlerForTest() {
+            return mHandler;
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java
index e4c33f1..0732866 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java
@@ -43,7 +43,6 @@
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.IPowerManager;
-import android.os.Message;
 import android.os.PowerManager;
 import android.os.RemoteException;
 import android.os.UserHandle;
@@ -52,7 +51,6 @@
 import android.support.test.metricshelper.MetricsAsserts;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
-import android.testing.TestableLooper.MessageHandler;
 import android.testing.TestableLooper.RunWithLooper;
 import android.util.DisplayMetrics;
 import android.util.SparseArray;
@@ -65,6 +63,7 @@
 import com.android.keyguard.KeyguardHostView.OnDismissAction;
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.UiOffloadThread;
 import com.android.systemui.assist.AssistManager;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.recents.misc.SystemServicesProxy;
@@ -73,7 +72,9 @@
 import com.android.systemui.statusbar.KeyguardIndicationController;
 import com.android.systemui.statusbar.NotificationData;
 import com.android.systemui.statusbar.NotificationData.Entry;
+import com.android.systemui.statusbar.NotificationListener;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
+import com.android.systemui.statusbar.NotificationLogger;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.statusbar.policy.HeadsUpManager;
@@ -107,6 +108,8 @@
     NotificationPanelView mNotificationPanelView;
     ScrimController mScrimController;
     IStatusBarService mBarService;
+    NotificationListener mNotificationListener;
+    NotificationLogger mNotificationLogger;
     ArrayList<Entry> mNotificationList;
     FingerprintUnlockController mFingerprintUnlockController;
     private DisplayMetrics mDisplayMetrics = new DisplayMetrics();
@@ -143,12 +146,16 @@
                 new Handler(handlerThread.getLooper()));
         when(powerManagerService.isInteractive()).thenReturn(true);
         mBarService = mock(IStatusBarService.class);
+        mNotificationListener = mock(NotificationListener.class);
+        mNotificationLogger = new NotificationLogger(mNotificationListener, mDependency.get(
+                UiOffloadThread.class));
 
         mDependency.injectTestDependency(MetricsLogger.class, mMetricsLogger);
         mStatusBar = new TestableStatusBar(mStatusBarKeyguardViewManager, mUnlockMethodCache,
                 mKeyguardIndicationController, mStackScroller, mHeadsUpManager,
                 mNotificationData, mPowerManager, mSystemServicesProxy, mNotificationPanelView,
-                mBarService, mScrimController, mFingerprintUnlockController);
+                mBarService, mNotificationListener, mNotificationLogger, mScrimController,
+                mFingerprintUnlockController);
         mStatusBar.mContext = mContext;
         mStatusBar.mComponents = mContext.getComponents();
         doAnswer(invocation -> {
@@ -163,15 +170,14 @@
             return null;
         }).when(mStatusBarKeyguardViewManager).addAfterKeyguardGoneRunnable(any());
 
+        mNotificationLogger.setUpWithPresenter(mStatusBar, mStackScroller);
+
         when(mStackScroller.getActivatedChild()).thenReturn(null);
-        TestableLooper.get(this).setMessageHandler(new MessageHandler() {
-            @Override
-            public boolean onMessageHandled(Message m) {
-                if (m.getCallback() == mStatusBar.mVisibilityReporter) {
-                    return false;
-                }
-                return true;
+        TestableLooper.get(this).setMessageHandler(m -> {
+            if (m.getCallback() == mStatusBar.mNotificationLogger.getVisibilityReporter()) {
+                return false;
             }
+            return true;
         });
     }
 
@@ -560,7 +566,8 @@
                 UnlockMethodCache unlock, KeyguardIndicationController key,
                 NotificationStackScrollLayout stack, HeadsUpManager hum, NotificationData nd,
                 PowerManager pm, SystemServicesProxy ssp, NotificationPanelView panelView,
-                IStatusBarService barService, ScrimController scrimController,
+                IStatusBarService barService, NotificationListener notificationListener,
+                NotificationLogger notificationLogger, ScrimController scrimController,
                 FingerprintUnlockController fingerprintUnlockController) {
             mStatusBarKeyguardViewManager = man;
             mUnlockMethodCache = unlock;
@@ -573,6 +580,8 @@
             mSystemServicesProxy = ssp;
             mNotificationPanel = panelView;
             mBarService = barService;
+            mNotificationListener = notificationListener;
+            mNotificationLogger = notificationLogger;
             mWakefulnessLifecycle = createAwakeWakefulnessLifecycle();
             mScrimController = scrimController;
             mFingerprintUnlockController = fingerprintUnlockController;
diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java
index 7ecb9ce..6a0d3ff 100644
--- a/services/core/java/com/android/server/StorageManagerService.java
+++ b/services/core/java/com/android/server/StorageManagerService.java
@@ -2718,15 +2718,14 @@
             final boolean primary = true;
             final boolean removable = primaryPhysical;
             final boolean emulated = !primaryPhysical;
-            final long mtpReserveSize = 0L;
             final boolean allowMassStorage = false;
             final long maxFileSize = 0L;
             final UserHandle owner = new UserHandle(userId);
             final String uuid = null;
             final String state = Environment.MEDIA_REMOVED;
 
-            res.add(0, new StorageVolume(id, StorageVolume.STORAGE_ID_INVALID, path,
-                    description, primary, removable, emulated, mtpReserveSize,
+            res.add(0, new StorageVolume(id, path,
+                    description, primary, removable, emulated,
                     allowMassStorage, maxFileSize, owner, uuid, state));
         }
 
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/KeySyncUtils.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/KeySyncUtils.java
index e4d2b953..37aeb3a 100644
--- a/services/core/java/com/android/server/locksettings/recoverablekeystore/KeySyncUtils.java
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/KeySyncUtils.java
@@ -16,6 +16,8 @@
 
 package com.android.server.locksettings.recoverablekeystore;
 
+import com.android.internal.annotations.VisibleForTesting;
+
 import java.nio.charset.StandardCharsets;
 import java.security.InvalidKeyException;
 import java.security.MessageDigest;
@@ -25,6 +27,7 @@
 import java.util.HashMap;
 import java.util.Map;
 
+import javax.crypto.AEADBadTagException;
 import javax.crypto.KeyGenerator;
 import javax.crypto.SecretKey;
 
@@ -45,9 +48,13 @@
             "V1 locally_encrypted_recovery_key".getBytes(StandardCharsets.UTF_8);
     private static final byte[] ENCRYPTED_APPLICATION_KEY_HEADER =
             "V1 encrypted_application_key".getBytes(StandardCharsets.UTF_8);
+    private static final byte[] RECOVERY_CLAIM_HEADER =
+            "V1 KF_claim".getBytes(StandardCharsets.UTF_8);
 
     private static final byte[] THM_KF_HASH_PREFIX = "THM_KF_hash".getBytes(StandardCharsets.UTF_8);
 
+    private static final int KEY_CLAIMANT_LENGTH_BYTES = 16;
+
     /**
      * Encrypts the recovery key using both the lock screen hash and the remote storage's public
      * key.
@@ -121,7 +128,7 @@
      */
     public static SecretKey generateRecoveryKey() throws NoSuchAlgorithmException {
         KeyGenerator keyGenerator = KeyGenerator.getInstance(RECOVERY_KEY_ALGORITHM);
-        keyGenerator.init(RECOVERY_KEY_SIZE_BITS, SecureRandom.getInstanceStrong());
+        keyGenerator.init(RECOVERY_KEY_SIZE_BITS, new SecureRandom());
         return keyGenerator.generateKey();
     }
 
@@ -153,13 +160,100 @@
     }
 
     /**
-     * Returns a new array, the contents of which are the concatenation of {@code a} and {@code b}.
+     * Returns a random 16-byte key claimant.
+     *
+     * @hide
      */
-    private static byte[] concat(byte[] a, byte[] b) {
-        byte[] result = new byte[a.length + b.length];
-        System.arraycopy(a, 0, result, 0, a.length);
-        System.arraycopy(b, 0, result, a.length, b.length);
-        return result;
+    public static byte[] generateKeyClaimant() {
+        SecureRandom secureRandom = new SecureRandom();
+        byte[] key = new byte[KEY_CLAIMANT_LENGTH_BYTES];
+        secureRandom.nextBytes(key);
+        return key;
+    }
+
+    /**
+     * Encrypts a claim to recover a remote recovery key.
+     *
+     * @param publicKey The public key of the remote server.
+     * @param vaultParams Associated vault parameters.
+     * @param challenge The challenge issued by the server.
+     * @param thmKfHash The THM hash of the lock screen.
+     * @param keyClaimant The random key claimant.
+     * @return The encrypted recovery claim, to be sent to the remote server.
+     * @throws NoSuchAlgorithmException if any SecureBox algorithm is not present.
+     * @throws InvalidKeyException if the {@code publicKey} could not be used to encrypt.
+     *
+     * @hide
+     */
+    public static byte[] encryptRecoveryClaim(
+            PublicKey publicKey,
+            byte[] vaultParams,
+            byte[] challenge,
+            byte[] thmKfHash,
+            byte[] keyClaimant) throws NoSuchAlgorithmException, InvalidKeyException {
+        return SecureBox.encrypt(
+                publicKey,
+                /*sharedSecret=*/ null,
+                /*header=*/ concat(RECOVERY_CLAIM_HEADER, vaultParams, challenge),
+                /*payload=*/ concat(thmKfHash, keyClaimant));
+    }
+
+    /**
+     * Decrypts a recovery key, after having retrieved it from a remote server.
+     *
+     * @param lskfHash The lock screen hash associated with the key.
+     * @param encryptedRecoveryKey The encrypted key.
+     * @return The raw key material.
+     * @throws NoSuchAlgorithmException if any SecureBox algorithm is unavailable.
+     * @throws AEADBadTagException if the message has been tampered with or was encrypted with a
+     *     different key.
+     */
+    public static byte[] decryptRecoveryKey(byte[] lskfHash, byte[] encryptedRecoveryKey)
+            throws NoSuchAlgorithmException, InvalidKeyException, AEADBadTagException {
+        return SecureBox.decrypt(
+                /*ourPrivateKey=*/ null,
+                /*sharedSecret=*/ lskfHash,
+                /*header=*/ LOCALLY_ENCRYPTED_RECOVERY_KEY_HEADER,
+                /*encryptedPayload=*/ encryptedRecoveryKey);
+    }
+
+    /**
+     * Decrypts an application key, using the recovery key.
+     *
+     * @param recoveryKey The recovery key - used to wrap all application keys.
+     * @param encryptedApplicationKey The application key to unwrap.
+     * @return The raw key material of the application key.
+     * @throws NoSuchAlgorithmException if any SecureBox algorithm is unavailable.
+     * @throws AEADBadTagException if the message has been tampered with or was encrypted with a
+     *     different key.
+     */
+    public static byte[] decryptApplicationKey(byte[] recoveryKey, byte[] encryptedApplicationKey)
+            throws NoSuchAlgorithmException, InvalidKeyException, AEADBadTagException {
+        return SecureBox.decrypt(
+                /*ourPrivateKey=*/ null,
+                /*sharedSecret=*/ recoveryKey,
+                /*header=*/ ENCRYPTED_APPLICATION_KEY_HEADER,
+                /*encryptedPayload=*/ encryptedApplicationKey);
+    }
+
+    /**
+     * Returns the concatenation of all the given {@code arrays}.
+     */
+    @VisibleForTesting
+    static byte[] concat(byte[]... arrays) {
+        int length = 0;
+        for (byte[] array : arrays) {
+            length += array.length;
+        }
+
+        byte[] concatenated = new byte[length];
+        int pos = 0;
+        for (byte[] array : arrays) {
+            System.arraycopy(array, /*srcPos=*/ 0, concatenated, pos, array.length);
+            pos += array.length;
+        }
+
+        return concatenated;
     }
 
     // Statics only
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/SecureBox.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/SecureBox.java
index 457fdc1..742cb45 100644
--- a/services/core/java/com/android/server/locksettings/recoverablekeystore/SecureBox.java
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/SecureBox.java
@@ -16,10 +16,15 @@
 
 package com.android.server.locksettings.recoverablekeystore;
 
+import android.annotation.Nullable;
+
 import java.security.InvalidKeyException;
 import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
 import java.security.PublicKey;
 
+import javax.crypto.AEADBadTagException;
+
 /**
  * TODO(b/69056040) Add implementation of SecureBox. This is a placeholder so KeySyncUtils compiles.
  *
@@ -32,8 +37,25 @@
      * @hide
      */
     public static byte[] encrypt(
-            PublicKey theirPublicKey, byte[] sharedSecret, byte[] header, byte[] payload)
+            @Nullable PublicKey theirPublicKey,
+            @Nullable byte[] sharedSecret,
+            @Nullable byte[] header,
+            @Nullable byte[] payload)
             throws NoSuchAlgorithmException, InvalidKeyException {
         throw new UnsupportedOperationException("Needs to be implemented.");
     }
+
+    /**
+     * TODO(b/69056040) Add implementation of decrypt.
+     *
+     * @hide
+     */
+    public static byte[] decrypt(
+            @Nullable PrivateKey ourPrivateKey,
+            @Nullable byte[] sharedSecret,
+            @Nullable byte[] header,
+            byte[] encryptedPayload)
+            throws NoSuchAlgorithmException, InvalidKeyException, AEADBadTagException {
+        throw new UnsupportedOperationException("Needs to be implemented.");
+    }
 }
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDb.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDb.java
new file mode 100644
index 0000000..79bf5aa
--- /dev/null
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDb.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.locksettings.recoverablekeystore.storage;
+
+import android.annotation.Nullable;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.util.Log;
+
+import com.android.server.locksettings.recoverablekeystore.WrappedKey;
+import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDbContract.KeysEntry;
+
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * Database of recoverable key information.
+ *
+ * @hide
+ */
+public class RecoverableKeyStoreDb {
+    private static final String TAG = "RecoverableKeyStoreDb";
+    private static final int IDLE_TIMEOUT_SECONDS = 30;
+
+    private final RecoverableKeyStoreDbHelper mKeyStoreDbHelper;
+
+    /**
+     * A new instance, storing the database in the user directory of {@code context}.
+     *
+     * @hide
+     */
+    public static RecoverableKeyStoreDb newInstance(Context context) {
+        RecoverableKeyStoreDbHelper helper = new RecoverableKeyStoreDbHelper(context);
+        helper.setWriteAheadLoggingEnabled(true);
+        helper.setIdleConnectionTimeout(IDLE_TIMEOUT_SECONDS);
+        return new RecoverableKeyStoreDb(helper);
+    }
+
+    private RecoverableKeyStoreDb(RecoverableKeyStoreDbHelper keyStoreDbHelper) {
+        this.mKeyStoreDbHelper = keyStoreDbHelper;
+    }
+
+    /**
+     * Inserts a key into the database.
+     *
+     * @param uid Uid of the application to whom the key belongs.
+     * @param alias The alias of the key in the AndroidKeyStore.
+     * @param wrappedKey The wrapped bytes of the key.
+     * @param generationId The generation ID of the platform key that wrapped the key.
+     * @return The primary key of the inserted row, or -1 if failed.
+     *
+     * @hide
+     */
+    public long insertKey(int uid, String alias, WrappedKey wrappedKey, int generationId) {
+        SQLiteDatabase db = mKeyStoreDbHelper.getWritableDatabase();
+        ContentValues values = new ContentValues();
+        values.put(KeysEntry.COLUMN_NAME_UID, uid);
+        values.put(KeysEntry.COLUMN_NAME_ALIAS, alias);
+        values.put(KeysEntry.COLUMN_NAME_NONCE, wrappedKey.getNonce());
+        values.put(KeysEntry.COLUMN_NAME_WRAPPED_KEY, wrappedKey.getKeyMaterial());
+        values.put(KeysEntry.COLUMN_NAME_LAST_SYNCED_AT, -1);
+        values.put(KeysEntry.COLUMN_NAME_GENERATION_ID, generationId);
+        return db.replace(KeysEntry.TABLE_NAME, /*nullColumnHack=*/ null, values);
+    }
+
+    /**
+     * Gets the key with {@code alias} for the app with {@code uid}.
+     *
+     * @hide
+     */
+    @Nullable public WrappedKey getKey(int uid, String alias) {
+        SQLiteDatabase db = mKeyStoreDbHelper.getReadableDatabase();
+        String[] projection = {
+                KeysEntry._ID,
+                KeysEntry.COLUMN_NAME_NONCE,
+                KeysEntry.COLUMN_NAME_WRAPPED_KEY,
+                KeysEntry.COLUMN_NAME_GENERATION_ID};
+        String selection =
+                KeysEntry.COLUMN_NAME_UID + " = ? AND "
+                + KeysEntry.COLUMN_NAME_ALIAS + " = ?";
+        String[] selectionArguments = { Integer.toString(uid), alias };
+
+        try (
+            Cursor cursor = db.query(
+                KeysEntry.TABLE_NAME,
+                projection,
+                selection,
+                selectionArguments,
+                /*groupBy=*/ null,
+                /*having=*/ null,
+                /*orderBy=*/ null)
+        ) {
+            int count = cursor.getCount();
+            if (count == 0) {
+                return null;
+            }
+            if (count > 1) {
+                Log.wtf(TAG,
+                        String.format(Locale.US,
+                                "%d WrappedKey entries found for uid=%d alias='%s'. "
+                                        + "Should only ever be 0 or 1.", count, uid, alias));
+                return null;
+            }
+            cursor.moveToFirst();
+            byte[] nonce = cursor.getBlob(
+                    cursor.getColumnIndexOrThrow(KeysEntry.COLUMN_NAME_NONCE));
+            byte[] keyMaterial = cursor.getBlob(
+                    cursor.getColumnIndexOrThrow(KeysEntry.COLUMN_NAME_WRAPPED_KEY));
+            return new WrappedKey(nonce, keyMaterial);
+        }
+    }
+
+    /**
+     * Returns all keys for the given {@code uid} and {@code platformKeyGenerationId}.
+     *
+     * @param uid User id of the profile to which all the keys are associated.
+     * @param platformKeyGenerationId The generation ID of the platform key that wrapped these keys.
+     *     (i.e., this should be the most recent generation ID, as older platform keys are not
+     *     usable.)
+     *
+     * @hide
+     */
+    public Map<String, WrappedKey> getAllKeys(int uid, int platformKeyGenerationId) {
+        SQLiteDatabase db = mKeyStoreDbHelper.getReadableDatabase();
+        String[] projection = {
+                KeysEntry._ID,
+                KeysEntry.COLUMN_NAME_NONCE,
+                KeysEntry.COLUMN_NAME_WRAPPED_KEY,
+                KeysEntry.COLUMN_NAME_ALIAS};
+        String selection =
+                KeysEntry.COLUMN_NAME_UID + " = ? AND "
+                + KeysEntry.COLUMN_NAME_GENERATION_ID + " = ?";
+        String[] selectionArguments = {
+                Integer.toString(uid), Integer.toString(platformKeyGenerationId) };
+
+        try (
+            Cursor cursor = db.query(
+                KeysEntry.TABLE_NAME,
+                projection,
+                selection,
+                selectionArguments,
+                /*groupBy=*/ null,
+                /*having=*/ null,
+                /*orderBy=*/ null)
+        ) {
+            HashMap<String, WrappedKey> keys = new HashMap<>();
+            while (cursor.moveToNext()) {
+                byte[] nonce = cursor.getBlob(
+                        cursor.getColumnIndexOrThrow(KeysEntry.COLUMN_NAME_NONCE));
+                byte[] keyMaterial = cursor.getBlob(
+                        cursor.getColumnIndexOrThrow(KeysEntry.COLUMN_NAME_WRAPPED_KEY));
+                String alias = cursor.getString(
+                        cursor.getColumnIndexOrThrow(KeysEntry.COLUMN_NAME_ALIAS));
+                keys.put(alias, new WrappedKey(nonce, keyMaterial));
+            }
+            return keys;
+        }
+    }
+
+    /**
+     * Closes all open connections to the database.
+     */
+    public void close() {
+        mKeyStoreDbHelper.close();
+    }
+
+    // TODO: Add method for updating the 'last synced' time.
+}
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbContract.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbContract.java
new file mode 100644
index 0000000..c54d0a6
--- /dev/null
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbContract.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.locksettings.recoverablekeystore.storage;
+
+import android.provider.BaseColumns;
+
+/**
+ * Contract for recoverable key database. Describes the tables present.
+ */
+class RecoverableKeyStoreDbContract {
+    /**
+     * Table holding wrapped keys, and information about when they were last synced.
+     */
+    static class KeysEntry implements BaseColumns {
+        static final String TABLE_NAME = "keys";
+
+        /**
+         * The uid of the application that generated the key.
+         */
+        static final String COLUMN_NAME_UID = "uid";
+
+        /**
+         * The alias of the key, as set in AndroidKeyStore.
+         */
+        static final String COLUMN_NAME_ALIAS = "alias";
+
+        /**
+         * Nonce with which the key was encrypted.
+         */
+        static final String COLUMN_NAME_NONCE = "nonce";
+
+        /**
+         * Encrypted bytes of the key.
+         */
+        static final String COLUMN_NAME_WRAPPED_KEY = "wrapped_key";
+
+        /**
+         * Generation ID of the platform key that was used to encrypt this key.
+         */
+        static final String COLUMN_NAME_GENERATION_ID = "platform_key_generation_id";
+
+        /**
+         * Timestamp of when this key was last synced with remote storage, or -1 if never synced.
+         */
+        static final String COLUMN_NAME_LAST_SYNCED_AT = "last_synced_at";
+    }
+}
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbHelper.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbHelper.java
new file mode 100644
index 0000000..e3783c4
--- /dev/null
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbHelper.java
@@ -0,0 +1,43 @@
+package com.android.server.locksettings.recoverablekeystore.storage;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+
+import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDbContract.KeysEntry;
+
+/**
+ * Helper for creating the recoverable key database.
+ */
+class RecoverableKeyStoreDbHelper extends SQLiteOpenHelper {
+    private static final int DATABASE_VERSION = 1;
+    private static final String DATABASE_NAME = "recoverablekeystore.db";
+
+    private static final String SQL_CREATE_ENTRIES =
+            "CREATE TABLE " + KeysEntry.TABLE_NAME + "( "
+                    + KeysEntry._ID + " INTEGER PRIMARY KEY,"
+                    + KeysEntry.COLUMN_NAME_UID + " INTEGER UNIQUE,"
+                    + KeysEntry.COLUMN_NAME_ALIAS + " TEXT UNIQUE,"
+                    + KeysEntry.COLUMN_NAME_NONCE + " BLOB,"
+                    + KeysEntry.COLUMN_NAME_WRAPPED_KEY + " BLOB,"
+                    + KeysEntry.COLUMN_NAME_GENERATION_ID + " INTEGER,"
+                    + KeysEntry.COLUMN_NAME_LAST_SYNCED_AT + " INTEGER)";
+
+    private static final String SQL_DELETE_ENTRIES =
+            "DROP TABLE IF EXISTS " + KeysEntry.TABLE_NAME;
+
+    RecoverableKeyStoreDbHelper(Context context) {
+        super(context, DATABASE_NAME, null, DATABASE_VERSION);
+    }
+
+    @Override
+    public void onCreate(SQLiteDatabase db) {
+        db.execSQL(SQL_CREATE_ENTRIES);
+    }
+
+    @Override
+    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+        db.execSQL(SQL_DELETE_ENTRIES);
+        onCreate(db);
+    }
+}
diff --git a/services/core/java/com/android/server/stats/StatsCompanionService.java b/services/core/java/com/android/server/stats/StatsCompanionService.java
index 1ce1400..b31f4b3 100644
--- a/services/core/java/com/android/server/stats/StatsCompanionService.java
+++ b/services/core/java/com/android/server/stats/StatsCompanionService.java
@@ -455,7 +455,7 @@
             Slog.d(TAG, "Pulling " + tagId);
 
         switch (tagId) {
-            case StatsLog.WIFI_BYTES_TRANSFERRED: {
+            case StatsLog.WIFI_BYTES_TRANSFER: {
                 long token = Binder.clearCallingIdentity();
                 try {
                     // TODO: Consider caching the following call to get BatteryStatsInternal.
@@ -476,7 +476,7 @@
                 }
                 break;
             }
-            case StatsLog.MOBILE_BYTES_TRANSFERRED: {
+            case StatsLog.MOBILE_BYTES_TRANSFER: {
                 long token = Binder.clearCallingIdentity();
                 try {
                     BatteryStatsInternal bs = LocalServices.getService(BatteryStatsInternal.class);
@@ -496,7 +496,7 @@
                 }
                 break;
             }
-            case StatsLog.WIFI_BYTES_TRANSFERRED_BY_FG_BG: {
+            case StatsLog.WIFI_BYTES_TRANSFER_BY_FG_BG: {
                 long token = Binder.clearCallingIdentity();
                 try {
                     BatteryStatsInternal bs = LocalServices.getService(BatteryStatsInternal.class);
@@ -516,7 +516,7 @@
                 }
                 break;
             }
-            case StatsLog.MOBILE_BYTES_TRANSFERRED_BY_FG_BG: {
+            case StatsLog.MOBILE_BYTES_TRANSFER_BY_FG_BG: {
                 long token = Binder.clearCallingIdentity();
                 try {
                     BatteryStatsInternal bs = LocalServices.getService(BatteryStatsInternal.class);
@@ -536,7 +536,7 @@
                 }
                 break;
             }
-            case StatsLog.KERNEL_WAKELOCK_PULLED: {
+            case StatsLog.KERNEL_WAKELOCK: {
                 final KernelWakelockStats wakelockStats =
                         mKernelWakelockReader.readKernelWakelockStats(mTmpWakelockStats);
                 List<StatsLogEventWrapper> ret = new ArrayList();
@@ -552,7 +552,7 @@
                 }
                 return ret.toArray(new StatsLogEventWrapper[ret.size()]);
             }
-            case StatsLog.CPU_TIME_PER_FREQ_PULLED: {
+            case StatsLog.CPU_TIME_PER_FREQ: {
                 List<StatsLogEventWrapper> ret = new ArrayList();
                 for (int cluster = 0; cluster < mKernelCpuSpeedReaders.length; cluster++) {
                     long[] clusterTimeMs = mKernelCpuSpeedReaders[cluster].readDelta();
@@ -568,7 +568,7 @@
                 }
                 return ret.toArray(new StatsLogEventWrapper[ret.size()]);
             }
-            case StatsLog.WIFI_ACTIVITY_ENERGY_INFO_PULLED: {
+            case StatsLog.WIFI_ACTIVITY_ENERGY_INFO: {
                 List<StatsLogEventWrapper> ret = new ArrayList();
                 long token = Binder.clearCallingIdentity();
                 if (mWifiManager == null) {
@@ -596,7 +596,7 @@
                 }
                 break;
             }
-            case StatsLog.MODEM_ACTIVITY_INFO_PULLED: {
+            case StatsLog.MODEM_ACTIVITY_INFO: {
                 List<StatsLogEventWrapper> ret = new ArrayList();
                 long token = Binder.clearCallingIdentity();
                 if (mTelephony == null) {
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/KeySyncUtilsTest.java b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/KeySyncUtilsTest.java
index c918e8c..ac3abed 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/KeySyncUtilsTest.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/KeySyncUtilsTest.java
@@ -37,6 +37,7 @@
 public class KeySyncUtilsTest {
     private static final int RECOVERY_KEY_LENGTH_BITS = 256;
     private static final int THM_KF_HASH_SIZE = 256;
+    private static final int KEY_CLAIMANT_LENGTH_BYTES = 16;
     private static final String SHA_256_ALGORITHM = "SHA-256";
 
     @Test
@@ -70,6 +71,32 @@
         assertFalse(Arrays.equals(a.getEncoded(), b.getEncoded()));
     }
 
+    @Test
+    public void generateKeyClaimant_returns16Bytes() throws Exception {
+        byte[] keyClaimant = KeySyncUtils.generateKeyClaimant();
+
+        assertEquals(KEY_CLAIMANT_LENGTH_BYTES, keyClaimant.length);
+    }
+
+    @Test
+    public void generateKeyClaimant_generatesANewClaimantEachTime() {
+        byte[] a = KeySyncUtils.generateKeyClaimant();
+        byte[] b = KeySyncUtils.generateKeyClaimant();
+
+        assertFalse(Arrays.equals(a, b));
+    }
+
+    @Test
+    public void concat_concatenatesArrays() {
+        assertArrayEquals(
+                utf8Bytes("hello, world!"),
+                KeySyncUtils.concat(
+                        utf8Bytes("hello"),
+                        utf8Bytes(", "),
+                        utf8Bytes("world"),
+                        utf8Bytes("!")));
+    }
+
     private static byte[] utf8Bytes(String s) {
         return s.getBytes(StandardCharsets.UTF_8);
     }
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbTest.java b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbTest.java
new file mode 100644
index 0000000..5cb88dd
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbTest.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.locksettings.recoverablekeystore.storage;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.android.server.locksettings.recoverablekeystore.WrappedKey;
+
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class RecoverableKeyStoreDbTest {
+    private static final String DATABASE_FILE_NAME = "recoverablekeystore.db";
+
+    private RecoverableKeyStoreDb mRecoverableKeyStoreDb;
+    private File mDatabaseFile;
+
+    @Before
+    public void setUp() {
+        Context context = InstrumentationRegistry.getTargetContext();
+        mDatabaseFile = context.getDatabasePath(DATABASE_FILE_NAME);
+        mRecoverableKeyStoreDb = RecoverableKeyStoreDb.newInstance(context);
+    }
+
+    @After
+    public void tearDown() {
+        mRecoverableKeyStoreDb.close();
+        mDatabaseFile.delete();
+    }
+
+    @Test
+    public void insertKey_replacesOldKey() {
+        int userId = 12;
+        String alias = "test";
+        WrappedKey oldWrappedKey = new WrappedKey(
+                getUtf8Bytes("nonce1"), getUtf8Bytes("keymaterial1"));
+        mRecoverableKeyStoreDb.insertKey(
+                userId, alias, oldWrappedKey, /*generationId=*/ 1);
+        byte[] nonce = getUtf8Bytes("nonce2");
+        byte[] keyMaterial = getUtf8Bytes("keymaterial2");
+        WrappedKey newWrappedKey = new WrappedKey(nonce, keyMaterial);
+
+        mRecoverableKeyStoreDb.insertKey(
+                userId, alias, newWrappedKey, /*generationId=*/ 2);
+
+        WrappedKey retrievedKey = mRecoverableKeyStoreDb.getKey(userId, alias);
+        assertArrayEquals(nonce, retrievedKey.getNonce());
+        assertArrayEquals(keyMaterial, retrievedKey.getKeyMaterial());
+    }
+
+    @Test
+    public void getKey_returnsNullIfNoKey() {
+        WrappedKey key = mRecoverableKeyStoreDb.getKey(
+                /*userId=*/ 1, /*alias=*/ "hello");
+
+        assertNull(key);
+    }
+
+    @Test
+    public void getKey_returnsInsertedKey() {
+        int userId = 12;
+        int generationId = 6;
+        String alias = "test";
+        byte[] nonce = getUtf8Bytes("nonce");
+        byte[] keyMaterial = getUtf8Bytes("keymaterial");
+        WrappedKey wrappedKey = new WrappedKey(nonce, keyMaterial);
+        mRecoverableKeyStoreDb.insertKey(userId, alias, wrappedKey, generationId);
+
+        WrappedKey retrievedKey = mRecoverableKeyStoreDb.getKey(userId, alias);
+
+        assertArrayEquals(nonce, retrievedKey.getNonce());
+        assertArrayEquals(keyMaterial, retrievedKey.getKeyMaterial());
+    }
+
+    @Test
+    public void getAllKeys_getsKeysWithUserIdAndGenerationId() {
+        int userId = 12;
+        int generationId = 6;
+        String alias = "test";
+        byte[] nonce = getUtf8Bytes("nonce");
+        byte[] keyMaterial = getUtf8Bytes("keymaterial");
+        WrappedKey wrappedKey = new WrappedKey(nonce, keyMaterial);
+        mRecoverableKeyStoreDb.insertKey(userId, alias, wrappedKey, generationId);
+
+        Map<String, WrappedKey> keys = mRecoverableKeyStoreDb.getAllKeys(userId, generationId);
+
+        assertEquals(1, keys.size());
+        assertTrue(keys.containsKey(alias));
+        WrappedKey retrievedKey = keys.get(alias);
+        assertArrayEquals(nonce, retrievedKey.getNonce());
+        assertArrayEquals(keyMaterial, retrievedKey.getKeyMaterial());
+    }
+
+    @Test
+    public void getAllKeys_doesNotReturnKeysWithBadGenerationId() {
+        int userId = 12;
+        WrappedKey wrappedKey = new WrappedKey(
+                getUtf8Bytes("nonce"), getUtf8Bytes("keymaterial"));
+        mRecoverableKeyStoreDb.insertKey(
+                userId, /*alias=*/ "test", wrappedKey, /*generationId=*/ 5);
+
+        Map<String, WrappedKey> keys = mRecoverableKeyStoreDb.getAllKeys(
+                userId, /*generationId=*/ 7);
+
+        assertTrue(keys.isEmpty());
+    }
+
+    @Test
+    public void getAllKeys_doesNotReturnKeysWithBadUserId() {
+        int generationId = 12;
+        WrappedKey wrappedKey = new WrappedKey(
+                getUtf8Bytes("nonce"), getUtf8Bytes("keymaterial"));
+        mRecoverableKeyStoreDb.insertKey(
+                /*userId=*/ 1, /*alias=*/ "test", wrappedKey, generationId);
+
+        Map<String, WrappedKey> keys = mRecoverableKeyStoreDb.getAllKeys(
+                /*userId=*/ 2, generationId);
+
+        assertTrue(keys.isEmpty());
+    }
+
+    private static byte[] getUtf8Bytes(String s) {
+        return s.getBytes(StandardCharsets.UTF_8);
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java
index 2257960f..56d4b7e 100644
--- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java
+++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java
@@ -120,9 +120,6 @@
  */
 @SmallTest
 public class ShortcutManagerTest1 extends BaseShortcutManagerTest {
-
-    private static final boolean SKIP_FOR_BUG_67325252 = true;
-
     /**
      * Test for the first launch path, no settings file available.
      */
@@ -724,10 +721,6 @@
     }
 
     public void testCleanupDanglingBitmaps() throws Exception {
-        if (SKIP_FOR_BUG_67325252) {
-            return;
-        }
-
         assertBitmapDirectories(USER_0, EMPTY_STRINGS);
         assertBitmapDirectories(USER_10, EMPTY_STRINGS);
 
@@ -786,6 +779,7 @@
 
         dumpsysOnLogcat();
 
+        mService.waitForBitmapSavesForTest();
         // Check files and directories.
         // Package 3 has no bitmaps, so we don't create a directory.
         assertBitmapDirectories(USER_0, CALLING_PACKAGE_1, CALLING_PACKAGE_2);
@@ -841,6 +835,7 @@
         makeFile(mService.getUserBitmapFilePath(USER_10), CALLING_PACKAGE_2, "3").createNewFile();
         makeFile(mService.getUserBitmapFilePath(USER_10), CALLING_PACKAGE_2, "4").createNewFile();
 
+        mService.waitForBitmapSavesForTest();
         assertBitmapDirectories(USER_0, CALLING_PACKAGE_1, CALLING_PACKAGE_2, CALLING_PACKAGE_3,
                 "a.b.c", "d.e.f");
 
@@ -855,6 +850,7 @@
         // The below check is the same as above, except this time USER_0 use the CALLING_PACKAGE_3
         // directory.
 
+        mService.waitForBitmapSavesForTest();
         assertBitmapDirectories(USER_0, CALLING_PACKAGE_1, CALLING_PACKAGE_2, CALLING_PACKAGE_3);
         assertBitmapDirectories(USER_10, CALLING_PACKAGE_1, CALLING_PACKAGE_2);
 
@@ -1133,13 +1129,13 @@
                             .setIcon(Icon.createWithResource(getTestContext(), R.drawable.black_32x32))
                             .build()
             )));
-
+            mService.waitForBitmapSavesForTest();
             assertWith(getCallerShortcuts())
                     .forShortcutWithId("s1", si -> {
                         assertTrue(si.hasIconResource());
                         assertEquals(R.drawable.black_32x32, si.getIconResourceId());
                     });
-
+            mService.waitForBitmapSavesForTest();
             // Set bitmap icon
             assertTrue(mManager.updateShortcuts(list(
                     new ShortcutInfo.Builder(mClientContext, "s1")
@@ -1147,7 +1143,7 @@
                                     getTestContext().getResources(), R.drawable.black_64x64)))
                             .build()
             )));
-
+            mService.waitForBitmapSavesForTest();
             assertWith(getCallerShortcuts())
                     .forShortcutWithId("s1", si -> {
                         assertTrue(si.hasIconFile());
@@ -1167,7 +1163,7 @@
                                     getTestContext().getResources(), R.drawable.black_64x64)))
                             .build()
             )));
-
+            mService.waitForBitmapSavesForTest();
             assertWith(getCallerShortcuts())
                     .forShortcutWithId("s1", si -> {
                         assertTrue(si.hasIconFile());
@@ -1179,7 +1175,7 @@
                             .setIcon(Icon.createWithResource(getTestContext(), R.drawable.black_32x32))
                             .build()
             )));
-
+            mService.waitForBitmapSavesForTest();
             assertWith(getCallerShortcuts())
                     .forShortcutWithId("s1", si -> {
                         assertTrue(si.hasIconResource());