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

fix ANR issue caused by MediaDRM api #791

Merged
merged 9 commits into from
Feb 17, 2022
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1467,6 +1467,7 @@ public Analytics build() {
AnalyticsContext.create(application, traitsCache.get(), collectDeviceID);
CountDownLatch advertisingIdLatch = new CountDownLatch(1);
analyticsContext.attachAdvertisingId(application, advertisingIdLatch, logger);
analyticsContext.attachDeviceId(getSegmentSharedPreferences(application, tag));

List<Integration.Factory> factories = new ArrayList<>(1 + this.factories.size());
factories.add(SegmentIntegration.FACTORY);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
import static android.net.ConnectivityManager.TYPE_WIFI;
import static com.segment.analytics.internal.Utils.NullableConcurrentHashMap;
import static com.segment.analytics.internal.Utils.createMap;
import static com.segment.analytics.internal.Utils.getDeviceId;
import static com.segment.analytics.internal.Utils.getSystemService;
import static com.segment.analytics.internal.Utils.hasPermission;
import static com.segment.analytics.internal.Utils.isNullOrEmpty;
Expand All @@ -40,6 +39,7 @@

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.net.ConnectivityManager;
Expand Down Expand Up @@ -95,7 +95,7 @@ public class AnalyticsContext extends ValueMap {
// Campaign
private static final String CAMPAIGN_KEY = "campaign";
// Device
private static final String DEVICE_KEY = "device";
static final String DEVICE_KEY = "device";
// Library
private static final String LIBRARY_KEY = "library";
private static final String LIBRARY_NAME_KEY = "name";
Expand Down Expand Up @@ -131,7 +131,7 @@ static synchronized AnalyticsContext create(
new AnalyticsContext(new NullableConcurrentHashMap<String, Object>());
analyticsContext.putApp(context);
analyticsContext.setTraits(traits);
analyticsContext.putDevice(context, collectDeviceId);
analyticsContext.putDevice(collectDeviceId);
analyticsContext.putLibrary();
analyticsContext.put(
LOCALE_KEY,
Expand Down Expand Up @@ -172,6 +172,10 @@ void attachAdvertisingId(Context context, CountDownLatch latch, Logger logger) {
}
}

void attachDeviceId(SharedPreferences segmentSharedPreference) {
new GetDeviceIdTask(this, segmentSharedPreference, new CountDownLatch(1)).execute();
}

@Override
public AnalyticsContext putValue(String key, Object value) {
super.putValue(key, value);
Expand Down Expand Up @@ -234,9 +238,12 @@ public Campaign campaign() {
}

/** Fill this instance with device info from the provided {@link Context}. */
void putDevice(Context context, boolean collectDeviceID) {
void putDevice(boolean collectDeviceID) {
Device device = new Device();
String identifier = collectDeviceID ? getDeviceId() : traits().anonymousId();

// use empty string to indicate device id is not yet ready.
// the device id will be populated async (see `attachDeviceId`)
String identifier = collectDeviceID ? "" : 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
188 changes: 188 additions & 0 deletions analytics/src/main/java/com/segment/analytics/GetDeviceIdTask.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
/**
* The MIT License (MIT)
*
* Copyright (c) 2014 Segment.io, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.segment.analytics;

import static com.segment.analytics.internal.Utils.isNullOrEmpty;

import android.content.SharedPreferences;
import android.media.MediaDrm;
import android.os.Build;
import java.security.MessageDigest;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

public class GetDeviceIdTask {

private final ExecutorService executor = Executors.newFixedThreadPool(2);

private final AnalyticsContext analyticsContext;

private final SharedPreferences segmentSharedPreference;

private final CountDownLatch latch;

private static final String DEVICE_ID_CACHE_KEY = "device.id";

public GetDeviceIdTask(
AnalyticsContext analyticsContext,
SharedPreferences segmentSharedPreference,
CountDownLatch latch) {
this.analyticsContext = analyticsContext;
this.segmentSharedPreference = segmentSharedPreference;
this.latch = latch;
}

public void execute() {
if (cacheHit()) {
return;
}

// since getDeviceId causes ANR (i.e. the function hangs forever),
// we need it in a separate task
// here we use Future, because it has built-in cancel mechanism
final Future<?> future =
executor.submit(
new Runnable() {
@Override
public void run() {
String deviceId = getDeviceId();

// the function may come back after a long time,
// (since thread can't guaranteed to be cancelled),
// we need to check if an interrupt signal has been raised
if (!Thread.currentThread().isInterrupted()) {
updateDeviceId(deviceId);
updateCache(deviceId);
}
}
});

// since Future.get is a blocking call,
// we need to run it on a different thread.
executor.execute(
new Runnable() {
@Override
public void run() {
try {
future.get(2, TimeUnit.SECONDS);
} catch (Exception e) {
// if any exception happens (timeout, interrupt, etc),
// cancel the task (raise an interrupt signal)
future.cancel(true);
String fallbackDeviceId = UUID.randomUUID().toString();
updateDeviceId(fallbackDeviceId);
updateCache(fallbackDeviceId);
}

// too bad we have to have a latch here just for unit tests
latch.countDown();
executor.shutdownNow();
}
});
}

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();
}

private boolean cacheHit() {
String cache = segmentSharedPreference.getString(DEVICE_ID_CACHE_KEY, null);

if (cache != null) {
updateDeviceId(cache);
return true;
} else {
return false;
}
}

private void updateDeviceId(String deviceId) {
synchronized (analyticsContext) {
if (!analyticsContext.containsKey(AnalyticsContext.DEVICE_KEY)) {
analyticsContext.put(AnalyticsContext.DEVICE_KEY, new AnalyticsContext.Device());
}

AnalyticsContext.Device device =
(AnalyticsContext.Device) analyticsContext.get(AnalyticsContext.DEVICE_KEY);
device.put(AnalyticsContext.Device.DEVICE_ID_KEY, deviceId);
}
}

private void updateCache(String deviceId) {
SharedPreferences.Editor editor = segmentSharedPreference.edit();
editor.putString(DEVICE_ID_CACHE_KEY, deviceId);
editor.apply();
}

/**
* 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 String getUniqueID() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) return null;

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 String byteArrayToHexString(byte[] bytes) {
StringBuilder buffer = new StringBuilder();
for (byte element : bytes) {
buffer.append(String.format("%02x", element));
}

return buffer.toString();
}
}
55 changes: 0 additions & 55 deletions analytics/src/main/java/com/segment/analytics/internal/Utils.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@
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;
Expand All @@ -53,7 +52,6 @@
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 All @@ -64,7 +62,6 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
Expand Down Expand Up @@ -298,58 +295,6 @@ public static <T> List<T> immutableCopyOf(@Nullable List<T> list) {
return Collections.unmodifiableList(new ArrayList<>(list));
}

/** 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;

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
Loading