-
Notifications
You must be signed in to change notification settings - Fork 615
[Firebase Segmentation] Add custom installation id cache layer and tests for it. #524
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 7 commits
1962431
b69b2c8
8881325
771d23c
dc3f410
864748f
0a3ebf6
2d158ed
18d4e7e
d65c21c
3e2cc89
f118d39
4da5d31
c1f7644
d1ff0ec
2c5102c
41fbfee
097ff36
5fd2fa0
e950003
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
// Copyright 2019 Google LLC | ||
// | ||
// 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.google.firebase.segmentation; | ||
|
||
import static com.google.common.truth.Truth.assertThat; | ||
import static org.junit.Assert.assertNull; | ||
import static org.junit.Assert.assertTrue; | ||
|
||
import androidx.test.InstrumentationRegistry; | ||
import androidx.test.ext.junit.runners.AndroidJUnit4; | ||
import com.google.android.gms.tasks.Tasks; | ||
import com.google.firebase.FirebaseApp; | ||
import com.google.firebase.FirebaseOptions; | ||
import org.junit.After; | ||
import org.junit.Before; | ||
import org.junit.Test; | ||
import org.junit.runner.RunWith; | ||
|
||
/** Instrumented tests for {@link CustomInstallationIdCache} */ | ||
@RunWith(AndroidJUnit4.class) | ||
public class CustomInstallationIdCacheTest { | ||
|
||
private FirebaseApp firebaseApp0; | ||
private FirebaseApp firebaseApp1; | ||
private CustomInstallationIdCache cache; | ||
|
||
@Before | ||
public void setUp() { | ||
FirebaseApp.clearInstancesForTest(); | ||
firebaseApp0 = | ||
FirebaseApp.initializeApp( | ||
InstrumentationRegistry.getContext(), | ||
new FirebaseOptions.Builder().setApplicationId("1:123456789:android:abcdef").build()); | ||
firebaseApp1 = | ||
FirebaseApp.initializeApp( | ||
InstrumentationRegistry.getContext(), | ||
new FirebaseOptions.Builder().setApplicationId("1:987654321:android:abcdef").build(), | ||
"firebase_app_1"); | ||
cache = CustomInstallationIdCache.getInstance(); | ||
} | ||
|
||
@After | ||
public void cleanUp() throws Exception { | ||
Tasks.await(cache.clearAll()); | ||
} | ||
|
||
@Test | ||
public void testReadCacheEntry_Null() { | ||
assertNull(cache.readCacheEntryValue(firebaseApp0)); | ||
assertNull(cache.readCacheEntryValue(firebaseApp1)); | ||
} | ||
|
||
@Test | ||
public void testUpdateAndReadCacheEntry() throws Exception { | ||
assertTrue( | ||
Tasks.await( | ||
cache.insertOrUpdateCacheEntry( | ||
firebaseApp0, | ||
CustomInstallationIdCacheEntryValue.create( | ||
"123456", "cAAAAAAAAAA", CustomInstallationIdCache.CacheStatus.PENDING)))); | ||
CustomInstallationIdCacheEntryValue entryValue = cache.readCacheEntryValue(firebaseApp0); | ||
assertThat(entryValue.getCustomInstallationId()).isEqualTo("123456"); | ||
assertThat(entryValue.getFirebaseInstanceId()).isEqualTo("cAAAAAAAAAA"); | ||
assertThat(entryValue.getCacheStatus()) | ||
.isEqualTo(CustomInstallationIdCache.CacheStatus.PENDING); | ||
assertNull(cache.readCacheEntryValue(firebaseApp1)); | ||
|
||
assertTrue( | ||
Tasks.await( | ||
cache.insertOrUpdateCacheEntry( | ||
firebaseApp0, | ||
CustomInstallationIdCacheEntryValue.create( | ||
"123456", "cAAAAAAAAAA", CustomInstallationIdCache.CacheStatus.SYNCED)))); | ||
entryValue = cache.readCacheEntryValue(firebaseApp0); | ||
assertThat(entryValue.getCustomInstallationId()).isEqualTo("123456"); | ||
assertThat(entryValue.getFirebaseInstanceId()).isEqualTo("cAAAAAAAAAA"); | ||
assertThat(entryValue.getCacheStatus()).isEqualTo(CustomInstallationIdCache.CacheStatus.SYNCED); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
// Copyright 2019 Google LLC | ||
// | ||
// 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.google.firebase.segmentation; | ||
|
||
import android.content.Context; | ||
import android.content.SharedPreferences; | ||
import androidx.annotation.Nullable; | ||
import androidx.annotation.VisibleForTesting; | ||
import com.google.android.gms.common.util.Strings; | ||
import com.google.android.gms.tasks.Task; | ||
import com.google.android.gms.tasks.TaskCompletionSource; | ||
import com.google.firebase.FirebaseApp; | ||
import java.util.concurrent.Executor; | ||
import java.util.concurrent.Executors; | ||
|
||
class CustomInstallationIdCache { | ||
|
||
// Status of each cache entry | ||
// NOTE: never change the ordinal of the enum values because the enum values are stored in cache | ||
// as their ordinal numbers. | ||
enum CacheStatus { | ||
// Cache entry is synced to Firebase backend | ||
SYNCED, | ||
// Cache entry is waiting for Firebase backend response or pending internal retry for retryable | ||
// errors. | ||
PENDING, | ||
// Cache entry is not accepted by Firebase backend. | ||
ERROR, | ||
} | ||
|
||
private static final String SHARED_PREFS_NAME = "CustomInstallationIdCache"; | ||
|
||
private static final String CUSTOM_INSTALLATION_ID_KEY = "Cid"; | ||
private static final String INSTANCE_ID_KEY = "Iid"; | ||
private static final String CACHE_STATUS_KEY = "Status"; | ||
|
||
private static CustomInstallationIdCache singleton = null; | ||
private final Executor ioExecuter; | ||
private final SharedPreferences prefs; | ||
|
||
static CustomInstallationIdCache getInstance() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Due to the way SharedPrefs are implemented, this class does not have to be a singleton, as multiple instances of sharedPrefs that point to the same file share the same thread-safe in-memory backing structure. As a side-effect of doing it your interface will become much simpler, as you won't have to pass FirebaseApp around to be able to read/write to the cache, e.g. something along these lines: class Cache {
// note: injecting direct deps only https://github.com/google/guice/wiki/InjectOnlyDirectDependencies
Cache(String persistenceKey, SharedPreferences prefs) {}
Value read();
void write(Value value);
} When your SDK initializes it can then construct this cache as There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't feel strongly re 1.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changing to thread-safe singleton. If we find singleton pattern doesn't work well later, we can always change it. |
||
if (singleton == null) { | ||
singleton = new CustomInstallationIdCache(); | ||
} | ||
return singleton; | ||
} | ||
|
||
private CustomInstallationIdCache() { | ||
// Since different FirebaseApp in the same Android application should have the same application | ||
// context and same dir path, so that use the context of the default FirebaseApp to create the | ||
// shared preferences. | ||
prefs = | ||
FirebaseApp.getInstance() | ||
.getApplicationContext() | ||
.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE); | ||
|
||
ioExecuter = Executors.newFixedThreadPool(2); | ||
} | ||
|
||
@Nullable | ||
synchronized CustomInstallationIdCacheEntryValue readCacheEntryValue(FirebaseApp firebaseApp) { | ||
String cid = | ||
prefs.getString(getSharedPreferencesKey(firebaseApp, CUSTOM_INSTALLATION_ID_KEY), null); | ||
String iid = prefs.getString(getSharedPreferencesKey(firebaseApp, INSTANCE_ID_KEY), null); | ||
int status = prefs.getInt(getSharedPreferencesKey(firebaseApp, CACHE_STATUS_KEY), -1); | ||
|
||
if (Strings.isEmptyOrWhitespace(cid) || Strings.isEmptyOrWhitespace(iid) || status == -1) { | ||
return null; | ||
} | ||
|
||
return CustomInstallationIdCacheEntryValue.create(cid, iid, CacheStatus.values()[status]); | ||
} | ||
|
||
synchronized Task<Boolean> insertOrUpdateCacheEntry( | ||
FirebaseApp firebaseApp, CustomInstallationIdCacheEntryValue entryValue) { | ||
SharedPreferences.Editor editor = prefs.edit(); | ||
editor.putString( | ||
getSharedPreferencesKey(firebaseApp, CUSTOM_INSTALLATION_ID_KEY), | ||
entryValue.getCustomInstallationId()); | ||
editor.putString( | ||
getSharedPreferencesKey(firebaseApp, INSTANCE_ID_KEY), entryValue.getFirebaseInstanceId()); | ||
editor.putInt( | ||
getSharedPreferencesKey(firebaseApp, CACHE_STATUS_KEY), | ||
entryValue.getCacheStatus().ordinal()); | ||
return commitSharedPreferencesEditAsync(editor); | ||
} | ||
|
||
synchronized Task<Boolean> clear(FirebaseApp firebaseApp) { | ||
SharedPreferences.Editor editor = prefs.edit(); | ||
editor.remove(getSharedPreferencesKey(firebaseApp, CUSTOM_INSTALLATION_ID_KEY)); | ||
editor.remove(getSharedPreferencesKey(firebaseApp, INSTANCE_ID_KEY)); | ||
editor.remove(getSharedPreferencesKey(firebaseApp, CACHE_STATUS_KEY)); | ||
diwu-arete marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return commitSharedPreferencesEditAsync(editor); | ||
} | ||
|
||
@VisibleForTesting | ||
diwu-arete marked this conversation as resolved.
Show resolved
Hide resolved
|
||
synchronized Task<Boolean> clearAll() { | ||
SharedPreferences.Editor editor = prefs.edit(); | ||
editor.clear(); | ||
return commitSharedPreferencesEditAsync(editor); | ||
} | ||
|
||
private static String getSharedPreferencesKey(FirebaseApp firebaseApp, String key) { | ||
return String.format("%s|%s", firebaseApp.getPersistenceKey(), key); | ||
} | ||
|
||
private Task<Boolean> commitSharedPreferencesEditAsync(SharedPreferences.Editor editor) { | ||
TaskCompletionSource<Boolean> result = new TaskCompletionSource<Boolean>(); | ||
ioExecuter.execute( | ||
new Runnable() { | ||
@Override | ||
public void run() { | ||
result.setResult(editor.commit()); | ||
} | ||
}); | ||
return result.getTask(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
// Copyright 2019 Google LLC | ||
// | ||
// 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.google.firebase.segmentation; | ||
|
||
import com.google.auto.value.AutoValue; | ||
import com.google.firebase.segmentation.CustomInstallationIdCache.CacheStatus; | ||
|
||
/** | ||
* This class represents a cache entry value in {@link CustomInstallationIdCache}, which contains a | ||
* Firebase instance id, a custom installation id and the cache status of this entry. | ||
*/ | ||
@AutoValue | ||
abstract class CustomInstallationIdCacheEntryValue { | ||
abstract String getCustomInstallationId(); | ||
|
||
abstract String getFirebaseInstanceId(); | ||
|
||
abstract CacheStatus getCacheStatus(); | ||
|
||
static CustomInstallationIdCacheEntryValue create( | ||
String customInstallationId, String firebaseInstanceId, CacheStatus cacheStatus) { | ||
return new AutoValue_CustomInstallationIdCacheEntryValue( | ||
customInstallationId, firebaseInstanceId, cacheStatus); | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.