1 /* 2 * Copyright 2022 Google LLC 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.google.android.libraries.mobiledatadownload.internal.logging; 17 18 import static com.google.android.libraries.mobiledatadownload.internal.MddConstants.SPLIT_CHAR; 19 import static com.google.common.truth.Truth.assertThat; 20 import static java.util.concurrent.TimeUnit.DAYS; 21 import static java.util.concurrent.TimeUnit.HOURS; 22 import static java.util.concurrent.TimeUnit.MINUTES; 23 24 import android.content.Context; 25 import android.content.SharedPreferences; 26 import android.net.Uri; 27 import androidx.test.core.app.ApplicationProvider; 28 import com.google.mobiledatadownload.internal.MetadataProto.FileGroupLoggingState; 29 import com.google.mobiledatadownload.internal.MetadataProto.GroupKey; 30 import com.google.mobiledatadownload.internal.MetadataProto.SamplingInfo; 31 import com.google.android.libraries.mobiledatadownload.file.SynchronousFileStorage; 32 import com.google.android.libraries.mobiledatadownload.file.common.testing.FakeFileBackend; 33 import com.google.android.libraries.mobiledatadownload.file.common.testing.TemporaryUri; 34 import com.google.android.libraries.mobiledatadownload.testing.FakeTimeSource; 35 import com.google.common.base.Preconditions; 36 import com.google.common.collect.ImmutableList; 37 import com.google.common.collect.ImmutableMap; 38 import com.google.common.util.concurrent.ListeningExecutorService; 39 import com.google.common.util.concurrent.MoreExecutors; 40 import com.google.protobuf.util.Timestamps; 41 import java.util.Arrays; 42 import java.util.List; 43 import java.util.Map; 44 import java.util.Random; 45 import java.util.concurrent.Executors; 46 import org.junit.After; 47 import org.junit.Before; 48 import org.junit.Rule; 49 import org.junit.Test; 50 import org.junit.runner.RunWith; 51 import org.robolectric.ParameterizedRobolectricTestRunner; 52 import org.robolectric.ParameterizedRobolectricTestRunner.Parameters; 53 54 @RunWith(ParameterizedRobolectricTestRunner.class) 55 public final class LoggingStateStoreTest { 56 57 private static final String OWNER_PACKAGE = "owner-package"; 58 private static final String VARIANT_ID = "variant-id-1"; 59 60 private static final String GROUP_NAME_1 = "group-name-1"; 61 private static final String GROUP_NAME_2 = "group-name-2"; 62 63 private static final int BUILD_ID_1 = 1; 64 65 private static final int VERSION_NUMBER_1 = 1; 66 private static final int VERSION_NUMBER_2 = 2; 67 68 private static final String INSTANCE_ID = "instance-id"; 69 70 private static final ListeningExecutorService executorService = 71 MoreExecutors.listeningDecorator(Executors.newCachedThreadPool()); 72 73 private static final long RANDOM_TESTING_SEED = 1234; 74 // First long that seed "1234" generates: 75 private static final long RANDOM_FIRST_SEEDED_LONG = -6519408338692630574L; 76 77 @Rule public final TemporaryUri tmpUri = new TemporaryUri(); 78 79 private Uri uri; 80 private LoggingStateStore loggingStateStore; 81 private SharedPreferences loggingStateSharedPrefs; 82 83 private FakeTimeSource timeSource; 84 private FakeFileBackend fakeFileBackend; 85 86 private Context context; 87 88 /* Run the same test suite on two implementations of the same interface. */ 89 private enum Implementation { 90 SHARED_PREFERENCES, 91 } 92 93 @Parameters(name = "implementation={0}") data()94 public static ImmutableList<Object[]> data() { 95 return ImmutableList.of(new Object[] {Implementation.SHARED_PREFERENCES}); 96 } 97 98 private final Implementation implUnderTest; 99 LoggingStateStoreTest(Implementation impl)100 public LoggingStateStoreTest(Implementation impl) { 101 this.implUnderTest = impl; 102 } 103 104 @Before setUp()105 public void setUp() throws Exception { 106 context = ApplicationProvider.getApplicationContext(); 107 108 fakeFileBackend = new FakeFileBackend(); 109 110 SynchronousFileStorage fileStorage = new SynchronousFileStorage(Arrays.asList(fakeFileBackend)); 111 112 Uri uriWithoutPb = tmpUri.newUri(); 113 114 uri = uriWithoutPb.buildUpon().path(uriWithoutPb.getPath() + ".pb").build(); 115 timeSource = new FakeTimeSource(); 116 117 loggingStateSharedPrefs = context.getSharedPreferences("loggingStateSharedPrefs", 0); 118 119 loggingStateStore = createLoggingStateStore(); 120 } 121 122 @After cleanUp()123 public void cleanUp() throws Exception {} 124 125 @Test testGetAndReset_onFirstRun_returnAbsent()126 public void testGetAndReset_onFirstRun_returnAbsent() throws Exception { 127 assertThat(loggingStateStore.getAndResetDaysSinceLastMaintenance().get()).isAbsent(); 128 } 129 130 @Test testGetAndReset_returnsCorrectNumber()131 public void testGetAndReset_returnsCorrectNumber() throws Exception { 132 assertThat(loggingStateStore.getAndResetDaysSinceLastMaintenance().get()).isAbsent(); 133 timeSource.advance(5, DAYS); 134 assertThat(loggingStateStore.getAndResetDaysSinceLastMaintenance().get()).hasValue(5); 135 } 136 137 @Test testGetAndReset_onSameDay_returns0()138 public void testGetAndReset_onSameDay_returns0() throws Exception { 139 assertThat(loggingStateStore.getAndResetDaysSinceLastMaintenance().get()).isAbsent(); 140 timeSource.advance(1, HOURS); 141 assertThat(loggingStateStore.getAndResetDaysSinceLastMaintenance().get()).hasValue(0); 142 timeSource.advance(22, HOURS); 143 assertThat(loggingStateStore.getAndResetDaysSinceLastMaintenance().get()).hasValue(0); 144 timeSource.advance(59, MINUTES); 145 assertThat(loggingStateStore.getAndResetDaysSinceLastMaintenance().get()).hasValue(0); 146 timeSource.advance(1, MINUTES); 147 assertThat(loggingStateStore.getAndResetDaysSinceLastMaintenance().get()).hasValue(1); 148 } 149 150 @Test testGetAndReset_resetsForFuturedays()151 public void testGetAndReset_resetsForFuturedays() throws Exception { 152 assertThat(loggingStateStore.getAndResetDaysSinceLastMaintenance().get()).isAbsent(); 153 154 timeSource.advance(1, DAYS); 155 assertThat(loggingStateStore.getAndResetDaysSinceLastMaintenance().get()).hasValue(1); 156 timeSource.advance(1, DAYS); 157 assertThat(loggingStateStore.getAndResetDaysSinceLastMaintenance().get()).hasValue(1); 158 } 159 160 @Test testGetAndReset_usesUtcTime()161 public void testGetAndReset_usesUtcTime() throws Exception { 162 timeSource.set(1623455940000L); // June 11th 11:59 pm 163 assertThat(loggingStateStore.getAndResetDaysSinceLastMaintenance().get()).isAbsent(); 164 timeSource.advance(1, MINUTES); // advance to june 12th 165 assertThat(loggingStateStore.getAndResetDaysSinceLastMaintenance().get()).hasValue(1); 166 } 167 168 @Test testGetAndReset_returnsNegativeValue_ifGoesBackInTime()169 public void testGetAndReset_returnsNegativeValue_ifGoesBackInTime() throws Exception { 170 timeSource.set(1623369600000L); // June 11th 2021 12:00 am 171 assertThat(loggingStateStore.getAndResetDaysSinceLastMaintenance().get()).isAbsent(); 172 timeSource.set(1623283200000L); // June 10th 2021 12:00 am 173 assertThat(loggingStateStore.getAndResetDaysSinceLastMaintenance().get()).hasValue(-1); 174 } 175 176 @Test testStateIsStoredAcrossRestarts()177 public void testStateIsStoredAcrossRestarts() throws Exception { 178 assertThat(loggingStateStore.getAndResetDaysSinceLastMaintenance().get()).isAbsent(); 179 timeSource.advance(20, DAYS); 180 loggingStateStore = createLoggingStateStore(); 181 182 assertThat(loggingStateStore.getAndResetDaysSinceLastMaintenance().get()).hasValue(20); 183 } 184 185 @Test testIncrementDataUsage()186 public void testIncrementDataUsage() throws Exception { 187 FileGroupLoggingState group1FileGroupLoggingState = 188 FileGroupLoggingState.newBuilder() 189 .setGroupKey( 190 GroupKey.newBuilder() 191 .setGroupName(GROUP_NAME_1) 192 .setOwnerPackage(OWNER_PACKAGE) 193 .setVariantId(VARIANT_ID) 194 .build()) 195 .setFileGroupVersionNumber(VERSION_NUMBER_1) 196 .setBuildId(BUILD_ID_1) 197 .setCellularUsage(123) 198 .setWifiUsage(456) 199 .build(); 200 201 loggingStateStore.incrementDataUsage(group1FileGroupLoggingState).get(); 202 203 assertThat(loggingStateStore.getAndResetAllDataUsage().get()) 204 .containsExactly(group1FileGroupLoggingState); 205 } 206 207 @Test testIncrementDataUsage_mergesDuplicateEntries()208 public void testIncrementDataUsage_mergesDuplicateEntries() throws Exception { 209 FileGroupLoggingState group1FileGroupLoggingState = 210 FileGroupLoggingState.newBuilder() 211 .setGroupKey( 212 GroupKey.newBuilder() 213 .setGroupName(GROUP_NAME_1) 214 .setOwnerPackage(OWNER_PACKAGE) 215 .setVariantId(VARIANT_ID) 216 .build()) 217 .setFileGroupVersionNumber(VERSION_NUMBER_1) 218 .setBuildId(BUILD_ID_1) 219 .setCellularUsage(123) 220 .setWifiUsage(456) 221 .build(); 222 223 FileGroupLoggingState withDifferentIncrements = 224 group1FileGroupLoggingState.toBuilder().setCellularUsage(5).setWifiUsage(10).build(); 225 226 // Increment with build 1 twice 227 loggingStateStore.incrementDataUsage(group1FileGroupLoggingState).get(); 228 loggingStateStore.incrementDataUsage(group1FileGroupLoggingState).get(); 229 230 // Increment with varying group name, owner package, variant id, version number, build id. None 231 // of them should be joined with the unmodified group. 232 loggingStateStore 233 .incrementDataUsage(withDifferentIncrements.toBuilder().setBuildId(789).build()) 234 .get(); 235 236 loggingStateStore 237 .incrementDataUsage( 238 withDifferentIncrements.toBuilder().setFileGroupVersionNumber(789).build()) 239 .get(); 240 241 loggingStateStore 242 .incrementDataUsage( 243 withDifferentIncrements.toBuilder() 244 .setGroupKey( 245 withDifferentIncrements.getGroupKey().toBuilder() 246 .setOwnerPackage("someotherpackage")) 247 .build()) 248 .get(); 249 250 loggingStateStore 251 .incrementDataUsage( 252 withDifferentIncrements.toBuilder() 253 .setGroupKey( 254 withDifferentIncrements.getGroupKey().toBuilder().setGroupName("someothername")) 255 .build()) 256 .get(); 257 258 loggingStateStore 259 .incrementDataUsage( 260 withDifferentIncrements.toBuilder() 261 .setGroupKey( 262 withDifferentIncrements.getGroupKey().toBuilder() 263 .setVariantId("someothervariant")) 264 .build()) 265 .get(); 266 267 List<FileGroupLoggingState> allDataUsage = loggingStateStore.getAndResetAllDataUsage().get(); 268 269 assertThat(allDataUsage) 270 .contains( 271 group1FileGroupLoggingState.toBuilder() 272 .setCellularUsage(group1FileGroupLoggingState.getCellularUsage() * 2) 273 .setWifiUsage(group1FileGroupLoggingState.getWifiUsage() * 2) 274 .build()); 275 276 assertThat(allDataUsage).hasSize(6); 277 } 278 279 @Test testGetAndResetDataUsage_resetsAllDataUsage()280 public void testGetAndResetDataUsage_resetsAllDataUsage() throws Exception { 281 FileGroupLoggingState group1FileGroupLoggingState = 282 FileGroupLoggingState.newBuilder() 283 .setGroupKey( 284 GroupKey.newBuilder() 285 .setGroupName(GROUP_NAME_1) 286 .setOwnerPackage(OWNER_PACKAGE) 287 .setVariantId(VARIANT_ID) 288 .build()) 289 .setFileGroupVersionNumber(VERSION_NUMBER_1) 290 .setBuildId(BUILD_ID_1) 291 .setCellularUsage(123) 292 .setWifiUsage(456) 293 .build(); 294 295 loggingStateStore.incrementDataUsage(group1FileGroupLoggingState).get(); 296 297 assertThat(loggingStateStore.getAndResetAllDataUsage().get()) 298 .containsExactly(group1FileGroupLoggingState); 299 300 assertThat(loggingStateStore.getAndResetAllDataUsage().get()).isEmpty(); 301 } 302 303 @Test testClear_clearsAllState()304 public void testClear_clearsAllState() throws Exception { 305 assertThat(loggingStateStore.getAndResetDaysSinceLastMaintenance().get()).isAbsent(); 306 timeSource.advance(20, DAYS); 307 308 FileGroupLoggingState group1FileGroupLoggingState = 309 FileGroupLoggingState.newBuilder() 310 .setGroupKey( 311 GroupKey.newBuilder() 312 .setGroupName(GROUP_NAME_1) 313 .setOwnerPackage(OWNER_PACKAGE) 314 .setVariantId(VARIANT_ID) 315 .build()) 316 .setFileGroupVersionNumber(VERSION_NUMBER_1) 317 .setBuildId(BUILD_ID_1) 318 .setCellularUsage(123) 319 .setWifiUsage(456) 320 .build(); 321 322 loggingStateStore.incrementDataUsage(group1FileGroupLoggingState).get(); 323 324 loggingStateStore.clear().get(); 325 326 assertThat(loggingStateStore.getAndResetDaysSinceLastMaintenance().get()).isAbsent(); 327 assertThat(loggingStateStore.getAndResetAllDataUsage().get()).isEmpty(); 328 } 329 330 @Test testGetSamplingInfo_returnsPopulatedSamplingInfo()331 public void testGetSamplingInfo_returnsPopulatedSamplingInfo() throws Exception { 332 long timeMillis = 1234567890L; 333 timeSource.set(timeMillis); 334 335 SamplingInfo samplingInfo = loggingStateStore.getStableSamplingInfo().get(); 336 337 assertThat(samplingInfo.getStableLogSamplingSalt()).isEqualTo(RANDOM_FIRST_SEEDED_LONG); 338 assertThat(samplingInfo.getLogSamplingSaltSetTimestamp()) 339 .isEqualTo(Timestamps.fromMillis(timeMillis)); 340 } 341 342 @Test testGetSamplingInfo_seedsWithProvidedRngAndTimestamp()343 public void testGetSamplingInfo_seedsWithProvidedRngAndTimestamp() throws Exception { 344 timeSource.set(12345L); 345 loggingStateStore.getAndResetDaysSinceLastMaintenance().get(); // Should not be affected 346 347 long timeMillis = 1234567890L; 348 timeSource.set(timeMillis); 349 350 SamplingInfo samplingInfo = loggingStateStore.getStableSamplingInfo().get(); 351 352 assertThat(samplingInfo) 353 .isEqualTo( 354 SamplingInfo.newBuilder() 355 .setStableLogSamplingSalt(RANDOM_FIRST_SEEDED_LONG) 356 .setLogSamplingSaltSetTimestamp(Timestamps.fromMillis(timeMillis)) 357 .build()); 358 // 1234567890 - 12345 millis = 14 days 359 assertThat(loggingStateStore.getAndResetDaysSinceLastMaintenance().get()).hasValue(14); 360 } 361 362 @Test testGetSamplingInfo_doesNotModifyExistingSamplingData()363 public void testGetSamplingInfo_doesNotModifyExistingSamplingData() throws Exception { 364 timeSource.set(12345L); 365 LoggingStateStore existingStore = createLoggingStateStore(); 366 existingStore.getStableSamplingInfo().get(); // Should not be affected 367 368 long timeMillis = 1234567890L; 369 timeSource.set(timeMillis); 370 371 SamplingInfo samplingInfo = loggingStateStore.getStableSamplingInfo().get(); 372 373 assertThat(samplingInfo.getStableLogSamplingSalt()).isEqualTo(RANDOM_FIRST_SEEDED_LONG); 374 assertThat(samplingInfo.getLogSamplingSaltSetTimestamp()) 375 .isEqualTo(Timestamps.fromMillis(12345L)); 376 } 377 getFileGroupKey( String ownerPackage, String groupName, int versionNumber, String networkType)378 private static String getFileGroupKey( 379 String ownerPackage, String groupName, int versionNumber, String networkType) { 380 // Format of shared preferences key is: ownerPackage|groupName|versionNumber|networkType, value 381 // is: long. 382 return new StringBuilder(ownerPackage) 383 .append(SPLIT_CHAR) 384 .append(groupName) 385 .append(SPLIT_CHAR) 386 .append(versionNumber) 387 .append(SPLIT_CHAR) 388 .append(networkType) 389 .toString(); 390 } 391 392 /** 393 * Adds the preferences from {@code prefsToAdd} to {@code prefs}. Throws an Exception if it fails 394 * to write to the SharedPreferences (e.g. to IO errors). 395 */ addPreferencesOrThrow( SharedPreferences prefs, ImmutableMap<String, Long> prefsToAdd)396 private static void addPreferencesOrThrow( 397 SharedPreferences prefs, ImmutableMap<String, Long> prefsToAdd) { 398 SharedPreferences.Editor editor = prefs.edit(); 399 for (Map.Entry<String, Long> entryToWrite : prefsToAdd.entrySet()) { 400 editor.putLong(entryToWrite.getKey(), entryToWrite.getValue()); 401 } 402 403 Preconditions.checkState( 404 editor.commit(), "Unable to write to shared prefs when setting up test."); 405 } 406 createLoggingStateStore()407 private LoggingStateStore createLoggingStateStore() throws Exception { 408 switch (implUnderTest) { 409 case SHARED_PREFERENCES: 410 return SharedPreferencesLoggingState.create( 411 () -> loggingStateSharedPrefs, 412 timeSource, 413 executorService, 414 new Random(RANDOM_TESTING_SEED)); 415 } 416 throw new AssertionError(); // Exhaustive switch 417 } 418 } 419