• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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