Skip to content
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

address violation of retrieving unsettable device #781

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion .circleci/config.yml
Expand Up @@ -5,8 +5,9 @@ jobs:
docker:
- image: circleci/android:api-28-alpha
environment:
JVM_OPTS: -Xmx6400m
JVM_OPTS: -Xmx8192m
CIRCLE_JDK_VERSION: oraclejdk8
resource_class: large
steps:
- checkout
- restore_cache:
Expand Down
Expand Up @@ -236,7 +236,7 @@ public Campaign campaign() {
/** Fill this instance with device info from the provided {@link Context}. */
void putDevice(Context context, boolean collectDeviceID) {
Device device = new Device();
String identifier = collectDeviceID ? getDeviceId(context) : traits().anonymousId();
String identifier = collectDeviceID ? getDeviceId() : traits().anonymousId();
device.put(Device.DEVICE_ID_KEY, identifier);
device.put(Device.DEVICE_MANUFACTURER_KEY, Build.MANUFACTURER);
device.put(Device.DEVICE_MODEL_KEY, Build.MODEL);
Expand Down
80 changes: 47 additions & 33 deletions analytics/src/main/java/com/segment/analytics/internal/Utils.java
Expand Up @@ -24,27 +24,22 @@
package com.segment.analytics.internal;

import static android.Manifest.permission.ACCESS_NETWORK_STATE;
import static android.Manifest.permission.READ_PHONE_STATE;
import static android.content.Context.CONNECTIVITY_SERVICE;
import static android.content.Context.MODE_PRIVATE;
import static android.content.Context.TELEPHONY_SERVICE;
import static android.content.pm.PackageManager.FEATURE_TELEPHONY;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.os.Process.THREAD_PRIORITY_BACKGROUND;
import static android.provider.Settings.Secure.ANDROID_ID;
import static android.provider.Settings.Secure.getString;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.media.MediaDrm;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.os.Build;
import android.os.Process;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
Expand All @@ -58,6 +53,7 @@
import java.io.InputStreamReader;
import java.lang.reflect.Array;
import java.net.HttpURLConnection;
import java.security.MessageDigest;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collection;
Expand Down Expand Up @@ -302,40 +298,58 @@ public static <T> List<T> immutableCopyOf(@Nullable List<T> list) {
return Collections.unmodifiableList(new ArrayList<>(list));
}

/**
* Creates a unique device id. Suppresses `HardwareIds` lint warnings as we don't use this ID
* for identifying specific users. This is also what is required by the Segment spec.
*/
@SuppressLint("HardwareIds")
public static String getDeviceId(Context context) {
String androidId = getString(context.getContentResolver(), ANDROID_ID);
if (!isNullOrEmpty(androidId)
&& !"9774d56d682e549c".equals(androidId)
&& !"unknown".equals(androidId)
&& !"000000000000000".equals(androidId)) {
return androidId;
}

// Serial number, guaranteed to be on all non phones in 2.3+.
if (!isNullOrEmpty(Build.SERIAL)) {
return Build.SERIAL;
}

// Telephony ID, guaranteed to be on all phones, requires READ_PHONE_STATE permission
if (hasPermission(context, READ_PHONE_STATE) && hasFeature(context, FEATURE_TELEPHONY)) {
TelephonyManager telephonyManager = getSystemService(context, TELEPHONY_SERVICE);
@SuppressLint("MissingPermission")
String telephonyId = telephonyManager.getDeviceId();
if (!isNullOrEmpty(telephonyId)) {
return telephonyId;
}
/** Creates a unique device id. */
public static String getDeviceId() {
// unique id generated from DRM API
String uniqueID = getUniqueID();
if (!isNullOrEmpty(uniqueID)) {
return uniqueID;
}

// If this still fails, generate random identifier that does not persist across
// installations
return UUID.randomUUID().toString();
}

/**
* Workaround for not able to get device id on Android 10 or above using DRM API {@see
* https://stackoverflow.com/questions/58103580/android-10-imei-no-longer-available-on-api-29-looking-for-alternatives}
* {@see https://developer.android.com/training/articles/user-data-ids}
*/
private static String getUniqueID() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) return null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any reason to return null if we are lower than API 18?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, the drm api is only available in 18+. so it's either we bump the min sdk to 18 or we do a check. and the getDeviceID method does a null check and returns random uuid as fallback.


UUID wideVineUuid = new UUID(-0x121074568629b532L, -0x5c37d8232ae2de13L);
MediaDrm wvDrm = null;
try {
wvDrm = new MediaDrm(wideVineUuid);
byte[] wideVineId = wvDrm.getPropertyByteArray(MediaDrm.PROPERTY_DEVICE_UNIQUE_ID);
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(wideVineId);
return byteArrayToHexString(md.digest());
} catch (Exception e) {
// Inspect exception
return null;
} finally {
if (wvDrm == null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
wvDrm.close();
} else {
wvDrm.release();
}
}
}
}

private static String byteArrayToHexString(byte[] bytes) {
StringBuilder buffer = new StringBuilder();
for (byte element : bytes) {
buffer.append(String.format("%02x", element));
}

return buffer.toString();
}

/** Returns a shared preferences for storing any library preferences. */
public static SharedPreferences getSegmentSharedPreferences(Context context, String tag) {
return context.getSharedPreferences("analytics-android-" + tag, MODE_PRIVATE);
Expand Down
Expand Up @@ -70,8 +70,10 @@ class AnalyticsContextTest {
assertThat(context.getValueMap("app"))
.containsEntry("build", "0")

// only check esistence of device id, since we don't know the value
// and we can't mock static method in mockito 2
assertThat(context.getValueMap("device"))
.containsEntry("id", "unknown")
.containsKey("id")
assertThat(context.getValueMap("device"))
.containsEntry("manufacturer", "unknown")
assertThat(context.getValueMap("device"))
Expand Down
4 changes: 2 additions & 2 deletions gradle.properties
Expand Up @@ -18,7 +18,7 @@ POM_LICENCE_DIST=repo
POM_DEVELOPER_ID=segmentio
POM_DEVELOPER_NAME=Segment, Inc.

org.gradle.jvmargs=-Xmx1536m

org.gradle.jvmargs=-Xmx2048m
org.gradle.daemon=false
android.useAndroidX=true
android.enableJetifier=true