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..6402633 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—activities that handle the
+ * {@link Intent#ACTION_MAIN} action and the {@link Intent#CATEGORY_LAUNCHER} category—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—shortcuts whose {@link ShortcutInfo#isDeclaredInManifest()} method
+ * returns {@code true}—followed by dynamic shortcuts—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 <shortcut>} 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—activities that handle the {@code MAIN} action and the
- * {@code LAUNCHER} category—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());