Skip to content

Commit ec44c99

Browse files
authored
Merge pull request #93 from Parsely/get_add_key_coroutines
Refactor retrieving advertisement id to Coroutines
2 parents ca714a7 + 0c3102d commit ec44c99

File tree

7 files changed

+253
-123
lines changed

7 files changed

+253
-123
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package com.parsely.parselyandroid
2+
3+
import android.content.Context
4+
import android.provider.Settings
5+
import com.google.android.gms.ads.identifier.AdvertisingIdClient
6+
import kotlinx.coroutines.CoroutineScope
7+
import kotlinx.coroutines.launch
8+
9+
internal class AdvertisementIdProvider(
10+
private val context: Context,
11+
coroutineScope: CoroutineScope
12+
) : IdProvider {
13+
14+
private var adKey: String? = null
15+
16+
init {
17+
coroutineScope.launch {
18+
try {
19+
adKey = AdvertisingIdClient.getAdvertisingIdInfo(context).id
20+
} catch (e: Exception) {
21+
ParselyTracker.PLog("No Google play services or error!")
22+
}
23+
}
24+
}
25+
26+
/**
27+
* @return advertisement id if the coroutine in the constructor finished executing AdvertisingIdClient#getAdvertisingIdInfo
28+
* null otherwise
29+
*/
30+
override fun provide(): String? = adKey
31+
}
32+
33+
internal class AndroidIdProvider(private val context: Context) : IdProvider {
34+
override fun provide(): String? {
35+
val uuid = try {
36+
Settings.Secure.getString(
37+
context.applicationContext.contentResolver,
38+
Settings.Secure.ANDROID_ID
39+
)
40+
} catch (ex: Exception) {
41+
null
42+
}
43+
ParselyTracker.PLog(String.format("Android ID: %s", uuid))
44+
return uuid
45+
}
46+
}
47+
48+
internal fun interface IdProvider {
49+
fun provide(): String?
50+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.parsely.parselyandroid
2+
3+
import android.os.Build
4+
5+
internal interface DeviceInfoRepository{
6+
fun collectDeviceInfo(): Map<String, String>
7+
}
8+
9+
internal open class AndroidDeviceInfoRepository(
10+
private val advertisementIdProvider: IdProvider,
11+
private val androidIdProvider: IdProvider,
12+
): DeviceInfoRepository {
13+
14+
/**
15+
* Collect device-specific info.
16+
*
17+
*
18+
* Collects info about the device and user to use in Parsely events.
19+
*/
20+
override fun collectDeviceInfo(): Map<String, String> {
21+
val dInfo: MutableMap<String, String> = HashMap()
22+
23+
// TODO: screen dimensions (maybe?)
24+
dInfo["parsely_site_uuid"] = parselySiteUuid
25+
dInfo["manufacturer"] = Build.MANUFACTURER
26+
dInfo["os"] = "android"
27+
dInfo["os_version"] = String.format("%d", Build.VERSION.SDK_INT)
28+
29+
return dInfo
30+
}
31+
32+
private val parselySiteUuid: String
33+
get() {
34+
val adKey = advertisementIdProvider.provide()
35+
val androidId = androidIdProvider.provide()
36+
37+
ParselyTracker.PLog("adkey is: %s, uuid is %s", adKey, androidId)
38+
39+
return if (adKey != null) {
40+
adKey
41+
} else {
42+
ParselyTracker.PLog("falling back to device uuid")
43+
androidId .orEmpty()
44+
}
45+
}
46+
}

parsely/src/main/java/com/parsely/parselyandroid/EventsBuilder.java

Lines changed: 8 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -3,41 +3,27 @@
33
import static com.parsely.parselyandroid.ParselyTracker.PLog;
44

55
import android.content.Context;
6-
import android.content.SharedPreferences;
7-
import android.os.AsyncTask;
8-
import android.provider.Settings;
96

107
import androidx.annotation.NonNull;
118
import androidx.annotation.Nullable;
129

13-
import com.google.android.gms.ads.identifier.AdvertisingIdClient;
14-
import com.google.android.gms.common.GooglePlayServicesNotAvailableException;
15-
import com.google.android.gms.common.GooglePlayServicesRepairableException;
16-
17-
import java.io.IOException;
1810
import java.util.Calendar;
1911
import java.util.HashMap;
2012
import java.util.Map;
2113
import java.util.TimeZone;
2214

2315
class EventsBuilder {
24-
private static final String UUID_KEY = "parsely-uuid";
2516
private static final String VIDEO_START_ID_KEY = "vsid";
2617
private static final String PAGE_VIEW_ID_KEY = "pvid";
2718

28-
@NonNull
29-
private final Context context;
30-
private final SharedPreferences settings;
3119
private final String siteId;
3220

33-
private Map<String, String> deviceInfo;
21+
@NonNull
22+
private final DeviceInfoRepository deviceInfoRepository;
3423

35-
public EventsBuilder(@NonNull final Context context, @NonNull final String siteId) {
36-
this.context = context;
24+
public EventsBuilder(@NonNull final DeviceInfoRepository deviceInfoRepository, @NonNull final String siteId) {
3725
this.siteId = siteId;
38-
settings = context.getSharedPreferences("parsely-prefs", 0);
39-
deviceInfo = collectDeviceInfo(null);
40-
new GetAdKey(context).execute();
26+
this.deviceInfoRepository = deviceInfoRepository;
4127
}
4228

4329
/**
@@ -74,11 +60,11 @@ Map<String, Object> buildEvent(
7460
if (extraData != null) {
7561
data.putAll(extraData);
7662
}
77-
data.put("manufacturer", deviceInfo.get("manufacturer"));
78-
data.put("os", deviceInfo.get("os"));
79-
data.put("os_version", deviceInfo.get("os_version"));
63+
64+
final Map<String, String> deviceInfo = deviceInfoRepository.collectDeviceInfo();
8065
data.put("ts", now.getTimeInMillis());
81-
data.put("parsely_site_uuid", deviceInfo.get("parsely_site_uuid"));
66+
data.putAll(deviceInfo);
67+
8268
event.put("data", data);
8369

8470
if (metadata != null) {
@@ -96,90 +82,4 @@ Map<String, Object> buildEvent(
9682
return event;
9783
}
9884

99-
/**
100-
* Collect device-specific info.
101-
* <p>
102-
* Collects info about the device and user to use in Parsely events.
103-
*/
104-
private Map<String, String> collectDeviceInfo(@Nullable final String adKey) {
105-
Map<String, String> dInfo = new HashMap<>();
106-
107-
// TODO: screen dimensions (maybe?)
108-
PLog("adkey is: %s, uuid is %s", adKey, getSiteUuid());
109-
final String uuid = (adKey != null) ? adKey : getSiteUuid();
110-
dInfo.put("parsely_site_uuid", uuid);
111-
dInfo.put("manufacturer", android.os.Build.MANUFACTURER);
112-
dInfo.put("os", "android");
113-
dInfo.put("os_version", String.format("%d", android.os.Build.VERSION.SDK_INT));
114-
115-
// FIXME: Not passed in event or used anywhere else.
116-
CharSequence txt = context.getPackageManager().getApplicationLabel(context.getApplicationInfo());
117-
dInfo.put("appname", txt.toString());
118-
119-
return dInfo;
120-
}
121-
122-
/**
123-
* Get the UUID for this user.
124-
*/
125-
//TODO: docs about where we get this UUID from and how.
126-
private String getSiteUuid() {
127-
String uuid = "";
128-
try {
129-
uuid = settings.getString(UUID_KEY, "");
130-
if (uuid.equals("")) {
131-
uuid = generateSiteUuid();
132-
}
133-
} catch (Exception ex) {
134-
PLog("Exception caught during site uuid generation: %s", ex.toString());
135-
}
136-
return uuid;
137-
}
138-
139-
/**
140-
* Read the Parsely UUID from application context or make a new one.
141-
*
142-
* @return The UUID to use for this user.
143-
*/
144-
private String generateSiteUuid() {
145-
String uuid = Settings.Secure.getString(context.getApplicationContext().getContentResolver(),
146-
Settings.Secure.ANDROID_ID);
147-
PLog(String.format("Generated UUID: %s", uuid));
148-
return uuid;
149-
}
150-
/**
151-
* Async task to get adKey for this device.
152-
*/
153-
private class GetAdKey extends AsyncTask<Void, Void, String> {
154-
private final Context mContext;
155-
156-
public GetAdKey(Context context) {
157-
mContext = context;
158-
}
159-
160-
@Override
161-
protected String doInBackground(Void... params) {
162-
AdvertisingIdClient.Info idInfo = null;
163-
String advertId = null;
164-
try {
165-
idInfo = AdvertisingIdClient.getAdvertisingIdInfo(mContext);
166-
} catch (GooglePlayServicesRepairableException | IOException |
167-
GooglePlayServicesNotAvailableException | IllegalArgumentException e) {
168-
PLog("No Google play services or error! falling back to device uuid");
169-
// fall back to device uuid on google play errors
170-
advertId = getSiteUuid();
171-
}
172-
try {
173-
advertId = idInfo.getId();
174-
} catch (NullPointerException e) {
175-
advertId = getSiteUuid();
176-
}
177-
return advertId;
178-
}
179-
180-
@Override
181-
protected void onPostExecute(String advertId) {
182-
deviceInfo = collectDeviceInfo(advertId);
183-
}
184-
}
18585
}

parsely/src/main/java/com/parsely/parselyandroid/ParselyTracker.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,11 @@ public class ParselyTracker {
7070
*/
7171
protected ParselyTracker(String siteId, int flushInterval, Context c) {
7272
context = c.getApplicationContext();
73-
eventsBuilder = new EventsBuilder(context, siteId);
73+
eventsBuilder = new EventsBuilder(
74+
new AndroidDeviceInfoRepository(
75+
new AdvertisementIdProvider(context, ParselyCoroutineScopeKt.getSdkScope()),
76+
new AndroidIdProvider(context)
77+
), siteId);
7478
localStorageRepository = new LocalStorageRepository(context);
7579
flushManager = new ParselyFlushManager(new Function0<Unit>() {
7680
@Override
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package com.parsely.parselyandroid
2+
3+
import org.assertj.core.api.Assertions.assertThat
4+
import org.junit.Before
5+
import org.junit.Test
6+
import org.junit.runner.RunWith
7+
import org.robolectric.RobolectricTestRunner
8+
import org.robolectric.annotation.Config
9+
import org.robolectric.shadows.ShadowBuild
10+
11+
private const val SDK_VERSION = 33
12+
private const val MANUFACTURER = "test manufacturer"
13+
14+
@RunWith(RobolectricTestRunner::class)
15+
@Config(sdk = [SDK_VERSION])
16+
internal class AndroidDeviceInfoRepositoryTest {
17+
18+
@Before
19+
fun setUp() {
20+
ShadowBuild.setManufacturer(MANUFACTURER)
21+
}
22+
23+
@Test
24+
fun `given the advertisement id exists, when collecting device info, then parsely site uuid is advertisement id`() {
25+
// given
26+
val advertisementId = "ad id"
27+
val sut = AndroidDeviceInfoRepository(
28+
advertisementIdProvider = { advertisementId },
29+
androidIdProvider = { "android id" })
30+
31+
// when
32+
val result = sut.collectDeviceInfo()
33+
34+
// then
35+
assertThat(result).isEqualTo(expectedConstantDeviceInfo + ("parsely_site_uuid" to advertisementId))
36+
}
37+
38+
@Test
39+
fun `given the advertisement is null and android id is not, when collecting device info, then parsely id is android id`() {
40+
// given
41+
val androidId = "android id"
42+
val sut = AndroidDeviceInfoRepository(
43+
advertisementIdProvider = { null },
44+
androidIdProvider = { androidId }
45+
)
46+
47+
// when
48+
val result = sut.collectDeviceInfo()
49+
50+
// then
51+
assertThat(result).isEqualTo(expectedConstantDeviceInfo + ("parsely_site_uuid" to androidId))
52+
}
53+
54+
@Test
55+
fun `given both advertisement id and android id are null, when collecting device info, then parsely id is empty`() {
56+
// given
57+
val sut = AndroidDeviceInfoRepository(
58+
advertisementIdProvider = { null },
59+
androidIdProvider = { null }
60+
)
61+
62+
// when
63+
val result = sut.collectDeviceInfo()
64+
65+
// then
66+
assertThat(result).isEqualTo(expectedConstantDeviceInfo + ("parsely_site_uuid" to ""))
67+
}
68+
69+
private companion object {
70+
val expectedConstantDeviceInfo = mapOf(
71+
"manufacturer" to MANUFACTURER,
72+
"os" to "android",
73+
"os_version" to "$SDK_VERSION"
74+
)
75+
}
76+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package com.parsely.parselyandroid
2+
3+
import android.app.Application
4+
import android.provider.Settings
5+
import androidx.test.core.app.ApplicationProvider
6+
import org.assertj.core.api.Assertions.assertThat
7+
import org.junit.Before
8+
import org.junit.Test
9+
import org.junit.runner.RunWith
10+
import org.robolectric.RobolectricTestRunner
11+
12+
@RunWith(RobolectricTestRunner::class)
13+
internal class AndroidIdProviderTest {
14+
15+
lateinit var sut: AndroidIdProvider
16+
17+
@Before
18+
fun setUp() {
19+
sut = AndroidIdProvider(ApplicationProvider.getApplicationContext())
20+
}
21+
22+
@Test
23+
fun `given no site uuid is stored, when requesting uuid, then return ANDROID_ID value`() {
24+
// given
25+
val fakeAndroidId = "test id"
26+
Settings.Secure.putString(
27+
ApplicationProvider.getApplicationContext<Application>().contentResolver,
28+
Settings.Secure.ANDROID_ID,
29+
fakeAndroidId
30+
)
31+
32+
// when
33+
val result= sut.provide()
34+
35+
// then
36+
assertThat(result).isEqualTo(fakeAndroidId)
37+
}
38+
39+
@Test
40+
fun `given site uuid already requested, when requesting uuid, then return same uuid`() {
41+
// given
42+
val fakeAndroidId = "test id"
43+
Settings.Secure.putString(
44+
ApplicationProvider.getApplicationContext<Application>().contentResolver,
45+
Settings.Secure.ANDROID_ID,
46+
fakeAndroidId
47+
)
48+
val storedValue = sut.provide()
49+
50+
// when
51+
val result = sut.provide()
52+
53+
// then
54+
assertThat(result).isEqualTo(storedValue)
55+
}
56+
}

0 commit comments

Comments
 (0)