diff options
6 files changed, 648 insertions, 92 deletions
diff --git a/services/core/java/com/android/server/timedetector/TimeDetectorStrategyImpl.java b/services/core/java/com/android/server/timedetector/TimeDetectorStrategyImpl.java index d99e03b091a3..c50248d4b402 100644 --- a/services/core/java/com/android/server/timedetector/TimeDetectorStrategyImpl.java +++ b/services/core/java/com/android/server/timedetector/TimeDetectorStrategyImpl.java @@ -24,7 +24,6 @@ import android.app.timedetector.ManualTimeSuggestion; import android.app.timedetector.PhoneTimeSuggestion; import android.content.Intent; import android.telephony.TelephonyManager; -import android.util.ArrayMap; import android.util.LocalLog; import android.util.Slog; import android.util.TimestampedValue; @@ -32,12 +31,11 @@ import android.util.TimestampedValue; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.IndentingPrintWriter; +import com.android.server.timezonedetector.ArrayMapWithHistory; import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import java.util.LinkedList; -import java.util.Map; /** * An implementation of TimeDetectorStrategy that passes phone and manual suggestions to @@ -99,14 +97,12 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy { private TimestampedValue<Long> mLastAutoSystemClockTimeSet; /** - * A mapping from phoneId to a linked list of time suggestions (the "first" being the latest). - * We typically expect one or two entries in this Map: devices will have a small number - * of telephony devices and phoneIds are assumed to be stable. The LinkedList associated with - * the ID will not exceed {@link #KEEP_SUGGESTION_HISTORY_SIZE} in size. + * A mapping from phoneId to a time suggestion. We typically expect one or two mappings: devices + * will have a small number of telephony devices and phoneIds are assumed to be stable. */ @GuardedBy("this") - private ArrayMap<Integer, LinkedList<PhoneTimeSuggestion>> mSuggestionByPhoneId = - new ArrayMap<>(); + private ArrayMapWithHistory<Integer, PhoneTimeSuggestion> mSuggestionByPhoneId = + new ArrayMapWithHistory<>(KEEP_SUGGESTION_HISTORY_SIZE); @Override public void initialize(@NonNull Callback callback) { @@ -179,16 +175,7 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy { ipw.println("Phone suggestion history:"); ipw.increaseIndent(); // level 2 - for (Map.Entry<Integer, LinkedList<PhoneTimeSuggestion>> entry - : mSuggestionByPhoneId.entrySet()) { - ipw.println("Phone " + entry.getKey()); - - ipw.increaseIndent(); // level 3 - for (PhoneTimeSuggestion suggestion : entry.getValue()) { - ipw.println(suggestion); - } - ipw.decreaseIndent(); // level 3 - } + mSuggestionByPhoneId.dump(ipw); ipw.decreaseIndent(); // level 2 ipw.decreaseIndent(); // level 1 @@ -205,20 +192,10 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy { } int phoneId = suggestion.getPhoneId(); - LinkedList<PhoneTimeSuggestion> phoneSuggestions = mSuggestionByPhoneId.get(phoneId); - if (phoneSuggestions == null) { - // The first time we've seen this phoneId. - phoneSuggestions = new LinkedList<>(); - mSuggestionByPhoneId.put(phoneId, phoneSuggestions); - } else if (phoneSuggestions.isEmpty()) { - Slog.w(LOG_TAG, "Suggestions unexpectedly empty when adding suggestion=" + suggestion); - } - - if (!phoneSuggestions.isEmpty()) { + PhoneTimeSuggestion previousSuggestion = mSuggestionByPhoneId.get(phoneId); + if (previousSuggestion != null) { // We can log / discard suggestions with obvious issues with the reference time clock. - PhoneTimeSuggestion previousSuggestion = phoneSuggestions.getFirst(); - if (previousSuggestion == null - || previousSuggestion.getUtcTime() == null + if (previousSuggestion.getUtcTime() == null || previousSuggestion.getUtcTime().getValue() == null) { // This should be impossible given we only store validated suggestions. Slog.w(LOG_TAG, "Previous suggestion is null or has a null time." @@ -240,10 +217,7 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy { } // Store the latest suggestion. - phoneSuggestions.addFirst(suggestion); - if (phoneSuggestions.size() > KEEP_SUGGESTION_HISTORY_SIZE) { - phoneSuggestions.removeLast(); - } + mSuggestionByPhoneId.put(phoneId, suggestion); return true; } @@ -331,15 +305,7 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy { int bestScore = PHONE_INVALID_SCORE; for (int i = 0; i < mSuggestionByPhoneId.size(); i++) { Integer phoneId = mSuggestionByPhoneId.keyAt(i); - LinkedList<PhoneTimeSuggestion> phoneSuggestions = mSuggestionByPhoneId.valueAt(i); - if (phoneSuggestions == null) { - // Unexpected - map is missing a value. - Slog.w(LOG_TAG, "Suggestions unexpectedly missing for phoneId." - + " phoneId=" + phoneId); - continue; - } - - PhoneTimeSuggestion candidateSuggestion = phoneSuggestions.getFirst(); + PhoneTimeSuggestion candidateSuggestion = mSuggestionByPhoneId.valueAt(i); if (candidateSuggestion == null) { // Unexpected - null suggestions should never be stored. Slog.w(LOG_TAG, "Latest suggestion unexpectedly null for phoneId." @@ -540,10 +506,6 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy { @VisibleForTesting @Nullable public synchronized PhoneTimeSuggestion getLatestPhoneSuggestion(int phoneId) { - LinkedList<PhoneTimeSuggestion> suggestions = mSuggestionByPhoneId.get(phoneId); - if (suggestions == null) { - return null; - } - return suggestions.getFirst(); + return mSuggestionByPhoneId.get(phoneId); } } diff --git a/services/core/java/com/android/server/timezonedetector/ArrayMapWithHistory.java b/services/core/java/com/android/server/timezonedetector/ArrayMapWithHistory.java new file mode 100644 index 000000000000..3274f0e1112f --- /dev/null +++ b/services/core/java/com/android/server/timezonedetector/ArrayMapWithHistory.java @@ -0,0 +1,187 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.timezonedetector; + +import android.annotation.IntRange; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.util.ArrayMap; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.IndentingPrintWriter; + +/** + * A partial decorator for {@link ArrayMap} that records historic values for each mapping for + * debugging later with {@link #dump(IndentingPrintWriter)}. + * + * <p>This class is only intended for use in {@link TimeZoneDetectorStrategy} and + * {@link com.android.server.timedetector.TimeDetectorStrategy} so only provides the parts of the + * {@link ArrayMap} API needed. If it is ever extended to include deletion methods like + * {@link ArrayMap#remove(Object)} some thought would need to be given to the correct + * {@link ArrayMap#containsKey(Object)} behavior for the history. Like {@link ArrayMap}, it is not + * thread-safe. + * + * @param <K> the type of the key + * @param <V> the type of the value + */ +public final class ArrayMapWithHistory<K, V> { + private static final String TAG = "ArrayMapWithHistory"; + + /** The size the linked list against each value is allowed to grow to. */ + private final int mMaxHistorySize; + + @Nullable + private ArrayMap<K, ReferenceWithHistory<V>> mMap; + + /** + * Creates an instance that records, at most, the specified number of values against each key. + */ + public ArrayMapWithHistory(@IntRange(from = 1) int maxHistorySize) { + if (maxHistorySize < 1) { + throw new IllegalArgumentException("maxHistorySize < 1: " + maxHistorySize); + } + mMaxHistorySize = maxHistorySize; + } + + /** + * See {@link ArrayMap#put(K, V)}. + */ + @Nullable + public V put(@Nullable K key, @Nullable V value) { + if (mMap == null) { + mMap = new ArrayMap<>(); + } + + ReferenceWithHistory<V> valueHolder = mMap.get(key); + if (valueHolder == null) { + valueHolder = new ReferenceWithHistory<>(mMaxHistorySize); + mMap.put(key, valueHolder); + } else if (valueHolder.getHistoryCount() == 0) { + Log.w(TAG, "History for \"" + key + "\" was unexpectedly empty"); + } + + return valueHolder.set(value); + } + + /** + * See {@link ArrayMap#get(Object)}. + */ + @Nullable + public V get(@Nullable Object key) { + if (mMap == null) { + return null; + } + + ReferenceWithHistory<V> valueHolder = mMap.get(key); + if (valueHolder == null) { + return null; + } else if (valueHolder.getHistoryCount() == 0) { + Log.w(TAG, "History for \"" + key + "\" was unexpectedly empty"); + } + return valueHolder.get(); + } + + /** + * See {@link ArrayMap#size()}. + */ + public int size() { + return mMap == null ? 0 : mMap.size(); + } + + /** + * See {@link ArrayMap#keyAt(int)}. + */ + @Nullable + public K keyAt(int index) { + if (mMap == null) { + throw new ArrayIndexOutOfBoundsException(index); + } + return mMap.keyAt(index); + } + + /** + * See {@link ArrayMap#valueAt(int)}. + */ + @Nullable + public V valueAt(int index) { + if (mMap == null) { + throw new ArrayIndexOutOfBoundsException(index); + } + + ReferenceWithHistory<V> valueHolder = mMap.valueAt(index); + if (valueHolder == null || valueHolder.getHistoryCount() == 0) { + Log.w(TAG, "valueAt(" + index + ") was unexpectedly null or empty"); + return null; + } + return valueHolder.get(); + } + + /** + * Dumps the content of the map, including historic values, using the supplied writer. + */ + public void dump(@NonNull IndentingPrintWriter ipw) { + if (mMap == null) { + ipw.println("{Empty}"); + } else { + for (int i = 0; i < mMap.size(); i++) { + ipw.println("key idx: " + i + "=" + mMap.keyAt(i)); + ReferenceWithHistory<V> value = mMap.valueAt(i); + ipw.println("val idx: " + i + "=" + value); + ipw.increaseIndent(); + + ipw.println("Historic values=["); + ipw.increaseIndent(); + value.dump(ipw); + ipw.decreaseIndent(); + ipw.println("]"); + + ipw.decreaseIndent(); + } + } + ipw.flush(); + } + + /** + * Internal method intended for tests that returns the number of historic values associated with + * the supplied key currently. If there is no mapping for the key then {@code 0} is returned. + */ + @VisibleForTesting + public int getHistoryCountForKeyForTests(@Nullable K key) { + if (mMap == null) { + return 0; + } + + ReferenceWithHistory<V> valueHolder = mMap.get(key); + if (valueHolder == null) { + return 0; + } else if (valueHolder.getHistoryCount() == 0) { + Log.w(TAG, "getValuesSizeForKeyForTests(\"" + key + "\") was unexpectedly empty"); + return 0; + } else { + return valueHolder.getHistoryCount(); + } + } + + @Override + public String toString() { + return "ArrayMapWithHistory{" + + "mHistorySize=" + mMaxHistorySize + + ", mMap=" + mMap + + '}'; + } +} diff --git a/services/core/java/com/android/server/timezonedetector/ReferenceWithHistory.java b/services/core/java/com/android/server/timezonedetector/ReferenceWithHistory.java new file mode 100644 index 000000000000..8bd10359bc6c --- /dev/null +++ b/services/core/java/com/android/server/timezonedetector/ReferenceWithHistory.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.timezonedetector; + +import android.annotation.IntRange; +import android.annotation.NonNull; +import android.annotation.Nullable; + +import com.android.internal.util.IndentingPrintWriter; + +import java.util.LinkedList; + +/** + * A class that behaves like the following definition, except it stores the history of values set + * that can be dumped for debugging with {@link #dump(IndentingPrintWriter)}. + * + * <pre>{@code + * private static class Ref<V> { + * private V mValue; + * + * public V get() { + * return mValue; + * } + * + * public V set(V value) { + * V previous = mValue; + * mValue = value; + * return previous; + * } + * } + * }</pre> + * + * <p>This class is not thread-safe. + * + * @param <V> the type of the value + */ +public final class ReferenceWithHistory<V> { + + /** The size the history linked list is allowed to grow to. */ + private final int mMaxHistorySize; + + @Nullable + private LinkedList<V> mValues; + + /** + * Creates an instance that records, at most, the specified number of values. + */ + public ReferenceWithHistory(@IntRange(from = 1) int maxHistorySize) { + if (maxHistorySize < 1) { + throw new IllegalArgumentException("maxHistorySize < 1: " + maxHistorySize); + } + this.mMaxHistorySize = maxHistorySize; + } + + /** Returns the current value, or {@code null} if it has never been set. */ + @Nullable + public V get() { + return (mValues == null || mValues.isEmpty()) ? null : mValues.getFirst(); + } + + /** Sets the current value. Returns the previous value, or {@code null}. */ + @Nullable + public V set(@Nullable V newValue) { + if (mValues == null) { + mValues = new LinkedList<>(); + } + + V previous = get(); + + mValues.addFirst(newValue); + if (mValues.size() > mMaxHistorySize) { + mValues.removeLast(); + } + return previous; + } + + /** + * Dumps the content of the reference, including historic values, using the supplied writer. + */ + public void dump(@NonNull IndentingPrintWriter ipw) { + if (mValues == null) { + ipw.println("{Empty}"); + } else { + int i = 0; + for (V value : mValues) { + ipw.println(i + ": " + value); + i++; + } + } + ipw.flush(); + } + + /** + * Returns the number of historic entries stored currently. + */ + public int getHistoryCount() { + return mValues == null ? 0 : mValues.size(); + } + + @Override + public String toString() { + return String.valueOf(get()); + } +} diff --git a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategy.java b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategy.java index b3013c7e0a5f..b4a439991dd9 100644 --- a/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategy.java +++ b/services/core/java/com/android/server/timezonedetector/TimeZoneDetectorStrategy.java @@ -27,7 +27,6 @@ import android.annotation.Nullable; import android.app.timezonedetector.ManualTimeZoneSuggestion; import android.app.timezonedetector.PhoneTimeZoneSuggestion; import android.content.Context; -import android.util.ArrayMap; import android.util.LocalLog; import android.util.Slog; @@ -38,8 +37,6 @@ import com.android.internal.util.IndentingPrintWriter; import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import java.util.LinkedList; -import java.util.Map; import java.util.Objects; /** @@ -175,14 +172,13 @@ public class TimeZoneDetectorStrategy { private final LocalLog mTimeZoneChangesLog = new LocalLog(30, false /* useLocalTimestamps */); /** - * A mapping from phoneId to a linked list of phone time zone suggestions (the head being the - * latest). We typically expect one or two entries in this Map: devices will have a small number - * of telephony devices and phoneIds are assumed to be stable. The LinkedList associated with - * the ID will not exceed {@link #KEEP_PHONE_SUGGESTION_HISTORY_SIZE} in size. + * A mapping from phoneId to a phone time zone suggestion. We typically expect one or two + * mappings: devices will have a small number of telephony devices and phoneIds are assumed to + * be stable. */ @GuardedBy("this") - private ArrayMap<Integer, LinkedList<QualifiedPhoneTimeZoneSuggestion>> mSuggestionByPhoneId = - new ArrayMap<>(); + private ArrayMapWithHistory<Integer, QualifiedPhoneTimeZoneSuggestion> mSuggestionByPhoneId = + new ArrayMapWithHistory<>(KEEP_PHONE_SUGGESTION_HISTORY_SIZE); /** * Creates a new instance of {@link TimeZoneDetectorStrategy}. @@ -226,16 +222,7 @@ public class TimeZoneDetectorStrategy { new QualifiedPhoneTimeZoneSuggestion(suggestion, score); // Store the suggestion against the correct phoneId. - LinkedList<QualifiedPhoneTimeZoneSuggestion> suggestions = - mSuggestionByPhoneId.get(suggestion.getPhoneId()); - if (suggestions == null) { - suggestions = new LinkedList<>(); - mSuggestionByPhoneId.put(suggestion.getPhoneId(), suggestions); - } - suggestions.addFirst(scoredSuggestion); - if (suggestions.size() > KEEP_PHONE_SUGGESTION_HISTORY_SIZE) { - suggestions.removeLast(); - } + mSuggestionByPhoneId.put(suggestion.getPhoneId(), scoredSuggestion); // Now perform auto time zone detection. The new suggestion may be used to modify the time // zone setting. @@ -398,13 +385,7 @@ public class TimeZoneDetectorStrategy { // rate-limit so age is not a strong indicator of confidence. Instead, the callers are // expected to withdraw suggestions they no longer have confidence in. for (int i = 0; i < mSuggestionByPhoneId.size(); i++) { - LinkedList<QualifiedPhoneTimeZoneSuggestion> phoneSuggestions = - mSuggestionByPhoneId.valueAt(i); - if (phoneSuggestions == null) { - // Unexpected - continue; - } - QualifiedPhoneTimeZoneSuggestion candidateSuggestion = phoneSuggestions.getFirst(); + QualifiedPhoneTimeZoneSuggestion candidateSuggestion = mSuggestionByPhoneId.valueAt(i); if (candidateSuggestion == null) { // Unexpected continue; @@ -474,16 +455,7 @@ public class TimeZoneDetectorStrategy { ipw.println("Phone suggestion history:"); ipw.increaseIndent(); // level 2 - for (Map.Entry<Integer, LinkedList<QualifiedPhoneTimeZoneSuggestion>> entry - : mSuggestionByPhoneId.entrySet()) { - ipw.println("Phone " + entry.getKey()); - - ipw.increaseIndent(); // level 3 - for (QualifiedPhoneTimeZoneSuggestion suggestion : entry.getValue()) { - ipw.println(suggestion); - } - ipw.decreaseIndent(); // level 3 - } + mSuggestionByPhoneId.dump(ipw); ipw.decreaseIndent(); // level 2 ipw.decreaseIndent(); // level 1 ipw.flush(); @@ -494,12 +466,7 @@ public class TimeZoneDetectorStrategy { */ @VisibleForTesting public synchronized QualifiedPhoneTimeZoneSuggestion getLatestPhoneSuggestion(int phoneId) { - LinkedList<QualifiedPhoneTimeZoneSuggestion> suggestions = - mSuggestionByPhoneId.get(phoneId); - if (suggestions == null) { - return null; - } - return suggestions.getFirst(); + return mSuggestionByPhoneId.get(phoneId); } /** diff --git a/services/tests/servicestests/src/com/android/server/timedetector/ArrayMapWithHistoryTest.java b/services/tests/servicestests/src/com/android/server/timedetector/ArrayMapWithHistoryTest.java new file mode 100644 index 000000000000..b6eea461d222 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/timedetector/ArrayMapWithHistoryTest.java @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.timedetector; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + +import android.util.ArrayMap; + +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.util.IndentingPrintWriter; +import com.android.server.timezonedetector.ArrayMapWithHistory; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.StringWriter; +import java.util.concurrent.Callable; + +@RunWith(AndroidJUnit4.class) +public class ArrayMapWithHistoryTest { + + @Test + public void testValueHistoryBehavior() { + // Create a map that will retain 2 values per key. + ArrayMapWithHistory<String, String> historyMap = new ArrayMapWithHistory<>(2 /* history */); + ArrayMap<String, String> arrayMap = new ArrayMap<>(); + + compareGetAndSizeForKeys(historyMap, arrayMap, entry("K1", null)); + + assertEquals(0, historyMap.getHistoryCountForKeyForTests("K1")); + assertToStringAndDumpNotNull(historyMap); + + putAndCompareReturnValue(historyMap, arrayMap, "K1", "V1"); + compareGetAndSizeForKeys(historyMap, arrayMap, entry("K1", "V1")); + compareKeyAtAndValueAtForIndex(0, historyMap, arrayMap); + + assertEquals(1, historyMap.getHistoryCountForKeyForTests("K1")); + assertToStringAndDumpNotNull(historyMap); + + // put() a new value for the same key. + putAndCompareReturnValue(historyMap, arrayMap, "K1", "V2"); + compareGetAndSizeForKeys(historyMap, arrayMap, entry("K1", "V2")); + compareKeyAtAndValueAtForIndex(0, historyMap, arrayMap); + + assertEquals(2, historyMap.getHistoryCountForKeyForTests("K1")); + assertToStringAndDumpNotNull(historyMap); + + // put() a new value for the same key. We should have hit the limit of "2 values retained + // per key". + putAndCompareReturnValue(historyMap, arrayMap, "K1", "V3"); + compareGetAndSizeForKeys(historyMap, arrayMap, entry("K1", "V3")); + compareKeyAtAndValueAtForIndex(0, historyMap, arrayMap); + + assertEquals(2, historyMap.getHistoryCountForKeyForTests("K1")); + assertToStringAndDumpNotNull(historyMap); + } + + @Test + public void testMapBehavior() throws Exception { + ArrayMapWithHistory<String, String> historyMap = new ArrayMapWithHistory<>(2); + ArrayMap<String, String> arrayMap = new ArrayMap<>(); + + compareGetAndSizeForKeys(historyMap, arrayMap, entry("K1", null), entry("K2", null)); + assertIndexAccessThrowsException(0, historyMap, arrayMap); + + assertEquals(0, historyMap.getHistoryCountForKeyForTests("K1")); + assertEquals(0, historyMap.getHistoryCountForKeyForTests("K2")); + + putAndCompareReturnValue(historyMap, arrayMap, "K1", "V1"); + compareGetAndSizeForKeys(historyMap, arrayMap, entry("K1", "V1"), entry("K2", null)); + compareKeyAtAndValueAtForIndex(0, historyMap, arrayMap); + // TODO Restore after http://b/146563025 is fixed and ArrayMap behaves properly in tests. + // assertIndexAccessThrowsException(1, historyMap, arrayMap); + + assertEquals(1, historyMap.getHistoryCountForKeyForTests("K1")); + assertToStringAndDumpNotNull(historyMap); + + putAndCompareReturnValue(historyMap, arrayMap, "K2", "V2"); + compareGetAndSizeForKeys(historyMap, arrayMap, entry("K1", "V1"), entry("K2", "V2")); + compareKeyAtAndValueAtForIndex(0, historyMap, arrayMap); + compareKeyAtAndValueAtForIndex(1, historyMap, arrayMap); + // TODO Restore after http://b/146563025 is fixed and ArrayMap behaves properly in tests. + // assertIndexAccessThrowsException(2, historyMap, arrayMap); + + assertEquals(1, historyMap.getHistoryCountForKeyForTests("K1")); + assertEquals(1, historyMap.getHistoryCountForKeyForTests("K2")); + assertToStringAndDumpNotNull(historyMap); + } + + private static String dumpHistoryMap(ArrayMapWithHistory<?, ?> historyMap) { + StringWriter stringWriter = new StringWriter(); + try (IndentingPrintWriter ipw = new IndentingPrintWriter(stringWriter, " ")) { + historyMap.dump(ipw); + return stringWriter.toString(); + } + } + + private static <K, V> void putAndCompareReturnValue(ArrayMapWithHistory<K, V> historyMap, + ArrayMap<K, V> arrayMap, K key, V value) { + assertEquals(arrayMap.put(key, value), historyMap.put(key, value)); + } + + private static class Entry<K, V> { + public final K key; + public final V value; + + Entry(K key, V value) { + this.key = key; + this.value = value; + } + } + + private static <K, V> Entry<K, V> entry(K key, V value) { + return new Entry<>(key, value); + } + + @SafeVarargs + private static <K, V> void compareGetAndSizeForKeys(ArrayMapWithHistory<K, V> historyMap, + ArrayMap<K, V> arrayMap, Entry<K, V>... expectedEntries) { + for (Entry<K, V> expectedEntry : expectedEntries) { + assertEquals(arrayMap.get(expectedEntry.key), historyMap.get(expectedEntry.key)); + assertEquals(expectedEntry.value, historyMap.get(expectedEntry.key)); + } + assertEquals(arrayMap.size(), historyMap.size()); + } + + private static void compareKeyAtAndValueAtForIndex( + int index, ArrayMapWithHistory<?, ?> historyMap, ArrayMap<?, ?> arrayMap) { + assertEquals(arrayMap.keyAt(index), historyMap.keyAt(index)); + assertEquals(arrayMap.valueAt(index), historyMap.valueAt(index)); + } + + private static void assertIndexAccessThrowsException( + int index, ArrayMapWithHistory<?, ?> historyMap, ArrayMap<?, ?> arrayMap) + throws Exception { + assertThrowsArrayIndexOutOfBoundsException( + "ArrayMap.keyAt(" + index + ")", () -> arrayMap.keyAt(index)); + assertThrowsArrayIndexOutOfBoundsException( + "ArrayMapWithHistory.keyAt(" + index + ")", () -> historyMap.keyAt(index)); + assertThrowsArrayIndexOutOfBoundsException( + "ArrayMap.keyAt(" + index + ")", () -> arrayMap.valueAt(index)); + assertThrowsArrayIndexOutOfBoundsException( + "ArrayMapWithHistory.keyAt(" + index + ")", () -> historyMap.valueAt(index)); + } + + private static void assertThrowsArrayIndexOutOfBoundsException( + String description, Callable<?> callable) throws Exception { + try { + callable.call(); + fail("Expected exception for " + description); + } catch (ArrayIndexOutOfBoundsException expected) { + // This is fine. + } catch (Exception e) { + // Any other exception is just rethrown. + throw e; + } + } + + private static void assertToStringAndDumpNotNull(ArrayMapWithHistory<?, ?> historyMap) { + assertNotNull(historyMap.toString()); + assertNotNull(dumpHistoryMap(historyMap)); + } +} diff --git a/services/tests/servicestests/src/com/android/server/timedetector/ReferenceWithHistoryTest.java b/services/tests/servicestests/src/com/android/server/timedetector/ReferenceWithHistoryTest.java new file mode 100644 index 000000000000..ce72499007ba --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/timedetector/ReferenceWithHistoryTest.java @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.timedetector; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.util.IndentingPrintWriter; +import com.android.server.timezonedetector.ReferenceWithHistory; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.StringWriter; + +@RunWith(AndroidJUnit4.class) +public class ReferenceWithHistoryTest { + + @Test + public void testBasicReferenceBehavior() { + // Create a reference that will retain 2 history values. + ReferenceWithHistory<String> referenceWithHistory = + new ReferenceWithHistory<>(2 /* history */); + TestRef<String> reference = new TestRef<>(); + + // Check unset behavior. + compareGet(referenceWithHistory, reference, null); + assertNotNull(dumpReferenceWithHistory(referenceWithHistory)); + compareToString(referenceWithHistory, reference, "null"); + + // Try setting null. + setAndCompareReturnValue(referenceWithHistory, reference, null); + compareGet(referenceWithHistory, reference, null); + assertNotNull(dumpReferenceWithHistory(referenceWithHistory)); + compareToString(referenceWithHistory, reference, "null"); + + // Try setting a non-null value. + setAndCompareReturnValue(referenceWithHistory, reference, "Foo"); + compareGet(referenceWithHistory, reference, "Foo"); + assertNotNull(dumpReferenceWithHistory(referenceWithHistory)); + compareToString(referenceWithHistory, reference, "Foo"); + + // Try setting null again. + setAndCompareReturnValue(referenceWithHistory, reference, "Foo"); + compareGet(referenceWithHistory, reference, "Foo"); + assertNotNull(dumpReferenceWithHistory(referenceWithHistory)); + compareToString(referenceWithHistory, reference, "Foo"); + + // Try a non-null value again. + setAndCompareReturnValue(referenceWithHistory, reference, "Bar"); + compareGet(referenceWithHistory, reference, "Bar"); + assertNotNull(dumpReferenceWithHistory(referenceWithHistory)); + compareToString(referenceWithHistory, reference, "Bar"); + } + + @Test + public void testValueHistoryBehavior() { + // Create a reference that will retain 2 history values. + ReferenceWithHistory<String> referenceWithHistory = + new ReferenceWithHistory<>(2 /* history */); + TestRef<String> reference = new TestRef<>(); + + // Assert behavior before anything is set. + assertEquals(0, referenceWithHistory.getHistoryCount()); + + // Set a value (1). + setAndCompareReturnValue(referenceWithHistory, reference, "V1"); + assertEquals(1, referenceWithHistory.getHistoryCount()); + + // Set a value (2). + setAndCompareReturnValue(referenceWithHistory, reference, "V2"); + assertEquals(2, referenceWithHistory.getHistoryCount()); + + // Set a value (3). + // We should have hit the limit of "2 history values retained per key". + setAndCompareReturnValue(referenceWithHistory, reference, "V3"); + assertEquals(2, referenceWithHistory.getHistoryCount()); + } + + /** + * A simple class that has the same behavior as ReferenceWithHistory without the history. Used + * in tests for comparison. + */ + private static class TestRef<V> { + private V mValue; + + public V get() { + return mValue; + } + + public V set(V value) { + V previous = mValue; + mValue = value; + return previous; + } + + public String toString() { + return String.valueOf(mValue); + } + } + + private static void compareGet( + ReferenceWithHistory<?> referenceWithHistory, TestRef<?> reference, Object value) { + assertEquals(reference.get(), referenceWithHistory.get()); + assertEquals(value, reference.get()); + } + + private static <T> void setAndCompareReturnValue( + ReferenceWithHistory<T> referenceWithHistory, TestRef<T> reference, T newValue) { + assertEquals(reference.set(newValue), referenceWithHistory.set(newValue)); + } + + private static void compareToString( + ReferenceWithHistory<?> referenceWithHistory, TestRef<?> reference, String expected) { + assertEquals(reference.toString(), referenceWithHistory.toString()); + assertEquals(expected, referenceWithHistory.toString()); + } + + private static String dumpReferenceWithHistory(ReferenceWithHistory<?> referenceWithHistory) { + StringWriter stringWriter = new StringWriter(); + try (IndentingPrintWriter ipw = new IndentingPrintWriter(stringWriter, " ")) { + referenceWithHistory.dump(ipw); + return stringWriter.toString(); + } + } +} |