com.leanplum.internal.Util Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of leanplum-core Show documentation
Show all versions of leanplum-core Show documentation
The Leanplum SDK messaging platform
The newest version!
/*
* Copyright 2021, Leanplum, Inc. All rights reserved.
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.leanplum.internal;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Resources;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.Build;
import android.os.Looper;
import android.provider.Settings.Secure;
import android.text.TextUtils;
import android.util.TypedValue;
import com.leanplum.Leanplum;
import com.leanplum.LeanplumActivityHelper;
import com.leanplum.LeanplumDeviceIdMode;
import com.leanplum.internal.Constants.Params;
import com.leanplum.monitoring.ExceptionHandler;
import com.leanplum.utils.SharedPreferencesUtil;
import java.io.File;
import java.lang.reflect.Method;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.UnsupportedCharsetException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import androidx.annotation.RequiresPermission;
/**
* Leanplum utilities.
*
* @author Andrew First
*/
public class Util {
private static final String ACCESS_WIFI_STATE_PERMISSION = "android.permission.ACCESS_WIFI_STATE";
private static String appName = null;
private static String versionName = null;
private static boolean hasPlayServicesCalled = false;
private static boolean hasPlayServices = false;
public static class DeviceIdInfo {
public final String id;
public boolean limitAdTracking;
public DeviceIdInfo(String id) {
this.id = id;
}
public DeviceIdInfo(String id, boolean limitAdTracking) {
this.id = id;
this.limitAdTracking = limitAdTracking;
}
}
/**
* Gets MD5 hash of given string.
*
* @param string String for which want to have MD5 hash.
* @return String with MD5 hash of given string.
*/
private static String md5(String string) throws Exception {
MessageDigest messageDigest = MessageDigest.getInstance("MD5");
messageDigest.update(string.getBytes(Charset.forName("UTF-8")));
byte digest[] = messageDigest.digest();
StringBuilder result = new StringBuilder();
for (byte dig : digest) {
result.append(String.format("%02x", dig));
}
return result.toString();
}
/**
* Gets SHA-256 hash of given string.
*/
public static String sha256(String string) throws NoSuchAlgorithmException {
MessageDigest messageDigest = MessageDigest.getInstance("SHA256");
messageDigest.update(string.getBytes(Charset.forName("UTF-8")));
byte digest[] = messageDigest.digest();
StringBuilder result = new StringBuilder();
for (byte dig : digest) {
result.append(String.format("%02x", dig));
}
return result.toString();
}
private static String checkDeviceId(String deviceIdMethod, String deviceId) {
if (deviceId != null) {
if (!isValidDeviceId(deviceId)) {
Log.e("Invalid device id generated (" + deviceIdMethod + "): " + deviceId);
return null;
}
}
return deviceId;
}
@RequiresPermission(ACCESS_WIFI_STATE_PERMISSION)
private static String getWifiMacAddressHash(Context context) {
String logPrefix = "Skipping wifi device id; ";
if (context.checkCallingOrSelfPermission(ACCESS_WIFI_STATE_PERMISSION) !=
PackageManager.PERMISSION_GRANTED) {
Log.d(logPrefix + "no wifi state permissions.");
return null;
}
try {
WifiManager manager = (WifiManager) context.getApplicationContext()
.getSystemService(Context.WIFI_SERVICE);
WifiInfo wifiInfo = manager.getConnectionInfo();
if (wifiInfo == null) {
Log.d(logPrefix + "null WifiInfo.");
return null;
}
@SuppressLint("HardwareIds")
String macAddress = wifiInfo.getMacAddress();
if (macAddress == null || macAddress.isEmpty()) {
Log.d(logPrefix + "no mac address returned.");
return null;
}
if (Constants.INVALID_MAC_ADDRESS.equals(macAddress)) {
// Note(ed): this is the expected case for Marshmallow and later, as they return
// INVALID_MAC_ADDRESS; we intend to fall back to the Android id for Marshmallow devices.
Log.d(logPrefix + "Marshmallow and later returns a fake MAC address.");
return null;
}
@SuppressLint("HardwareIds")
String deviceId = md5(wifiInfo.getMacAddress());
Log.d("Using wifi device id: " + deviceId);
return checkDeviceId("mac address", deviceId);
} catch (Exception e) {
Log.d("Error getting wifi MAC address.");
}
return null;
}
/**
* Retrieves the advertising ID. Requires Google Play Services or androidX. Note: This method must
* not run on the main thread.
*/
private static DeviceIdInfo getAdvertisingId(Context caller) {
try {
final String[] classNames = {
"androidx.ads.identifier.AdvertisingIdClient",
"com.google.android.gms.ads.identifier.AdvertisingIdClient"
};
for (String name : classNames) {
try {
Object adInfo = Class.forName(name)
.getMethod("getAdvertisingIdInfo", Context.class)
.invoke(null, caller);
if (name.equals(classNames[0])) {
Method get = adInfo.getClass().getMethod("get", long.class, TimeUnit.class);
adInfo = get.invoke(adInfo, 5, TimeUnit.SECONDS);
}
String id = checkDeviceId("advertising id", (String) adInfo.getClass().getMethod("getId")
.invoke(adInfo));
if (id != null) {
boolean limitTracking = (Boolean) adInfo.getClass()
.getMethod("isLimitAdTrackingEnabled")
.invoke(adInfo);
Log.d("Using advertising device id: " + id);
return new DeviceIdInfo(id, limitTracking);
}
} catch (Throwable t) {
Log.i("Couldn't get AdvertisingID using class: " + name);
}
}
} catch (Throwable t) {
Log.e("Error getting advertising ID. Google Play Services are not available: ", t);
}
return null;
}
private static String getAndroidId(Context context) {
@SuppressLint("HardwareIds")
String androidId = Secure.getString(context.getContentResolver(), Secure.ANDROID_ID);
if (androidId == null || androidId.isEmpty()) {
Log.d("Skipping Android device id; no id returned.");
return null;
}
if (Constants.INVALID_ANDROID_ID.equals(androidId)) {
Log.d("Skipping Android device id; got invalid " + "device id: " + androidId);
return null;
}
Log.d("Using Android device id: " + androidId);
return checkDeviceId("android id", androidId);
}
/**
* Final fallback device id -- generate a random device id.
*/
private static String generateRandomDeviceId() {
// Mark random IDs to be able to identify them.
String randomId = UUID.randomUUID().toString() + "-LP";
Log.d("Using generated device id: " + randomId);
return randomId;
}
private static boolean isValidForCharset(String id, String charsetName) {
CharsetEncoder encoder = null;
try {
Charset charset = Charset.forName(charsetName);
encoder = charset.newEncoder();
} catch (UnsupportedCharsetException e) {
Log.d("Unsupported charset: " + charsetName);
}
if (encoder != null && !encoder.canEncode(id)) {
Log.d("Invalid id (contains invalid characters): " + id);
return false;
}
return true;
}
public static boolean isValidUserId(String userId) {
String logPrefix = "Invalid user id ";
if (userId == null || userId.isEmpty()) {
Log.d(logPrefix + "(sentinel): " + userId);
return false;
}
if (Constants.INVALID_UUID.equals(userId)) {
Log.d(logPrefix + "(zero uuid): " + userId);
return false;
}
if (userId.length() > Constants.MAX_USER_ID_LENGTH) {
Log.d(logPrefix + "(too long): " + userId);
return false;
}
if (userId.contains("\n")) {
Log.d(logPrefix + "(contains newline): " + userId);
return false;
}
if (userId.contains("\"") || userId.contains("\'")) {
Log.d(logPrefix + "(contains quotes): " + userId);
return false;
}
return isValidForCharset(userId, "UTF-8");
}
public static boolean isValidDeviceId(String deviceId) {
String logPrefix = "Invalid device id ";
if (deviceId == null || deviceId.isEmpty() ||
Constants.INVALID_ANDROID_ID.equals(deviceId) ||
Constants.INVALID_MAC_ADDRESS_HASH.equals(deviceId) ||
Constants.OLD_INVALID_MAC_ADDRESS_HASH.equals(deviceId) ||
Constants.INVALID_UUID.equals(deviceId)) {
Log.d(logPrefix + "(sentinel): " + deviceId);
return false;
}
if (deviceId.length() > Constants.MAX_DEVICE_ID_LENGTH) {
Log.d(logPrefix + "(too long): " + deviceId);
return false;
}
if (deviceId.contains("[")) {
Log.d(logPrefix + "(contains brackets): " + deviceId);
return false;
}
if (deviceId.contains("\n")) {
Log.d(logPrefix + "(contains newline): " + deviceId);
return false;
}
if (deviceId.contains(",")) {
Log.d(logPrefix + "(contains comma): " + deviceId);
return false;
}
if (deviceId.contains("\"") || deviceId.contains("\'")) {
Log.d(logPrefix + "(contains quotes): " + deviceId);
return false;
}
return isValidForCharset(deviceId, "US-ASCII");
}
@RequiresPermission(ACCESS_WIFI_STATE_PERMISSION)
public static DeviceIdInfo getDeviceId(LeanplumDeviceIdMode mode) {
Context context = Leanplum.getContext();
if (mode.equals(LeanplumDeviceIdMode.ADVERTISING_ID)) {
try {
DeviceIdInfo info = getAdvertisingId(context);
if (info != null) {
return info;
}
} catch (Exception e) {
Log.e("Error getting advertising ID: %s", e);
}
}
if (isSimulator() || mode.equals(LeanplumDeviceIdMode.ANDROID_ID)) {
String androidId = getAndroidId(context);
if (androidId != null) {
return new DeviceIdInfo(getAndroidId(context));
}
}
String macAddressHash = getWifiMacAddressHash(context);
if (macAddressHash != null) {
return new DeviceIdInfo(macAddressHash);
}
String androidId = getAndroidId(context);
if (androidId != null) {
return new DeviceIdInfo(androidId);
}
return new DeviceIdInfo(generateRandomDeviceId());
}
public static String getVersionName() {
if (versionName != null) {
return versionName;
}
Context context = Leanplum.getContext();
try {
if (TextUtils.isEmpty(versionName)) {
PackageInfo pInfo = context.getPackageManager().getPackageInfo(
context.getPackageName(), 0);
versionName = pInfo.versionName;
}
} catch (Exception e) {
Log.d("Could not extract versionName from Manifest or PackageInfo.");
}
return versionName;
}
public static String getDeviceModel() {
if (isSimulator()) {
return "Android Emulator";
}
String manufacturer = Build.MANUFACTURER;
String model = Build.MODEL;
if (model.startsWith(manufacturer)) {
return capitalize(model);
} else {
return capitalize(manufacturer) + " " + model;
}
}
public static String getApplicationName(Context context) {
if (appName != null) {
return appName;
}
int stringId = context.getApplicationInfo().labelRes;
if (stringId == 0) {
appName = context.getApplicationInfo().loadLabel(context.getPackageManager()).toString();
} else {
appName = context.getString(stringId);
}
return appName;
}
private static String capitalize(String s) {
if (s == null || s.length() == 0) {
return "";
}
char first = s.charAt(0);
if (Character.isUpperCase(first)) {
return s;
} else {
return Character.toUpperCase(first) + s.substring(1);
}
}
@SuppressWarnings("SameReturnValue")
public static String getSystemName() {
return "Android OS";
}
@SuppressWarnings("SameReturnValue")
public static String getSystemVersion() {
return Build.VERSION.RELEASE;
}
public static boolean isSimulator() {
String model = android.os.Build.MODEL.toLowerCase(Locale.getDefault());
return model.contains("google_sdk")
|| model.contains("emulator")
|| model.contains("sdk");
}
public static String getDeviceName() {
if (isSimulator()) {
return "Android Emulator";
}
return getDeviceModel();
}
public static String getLocale() {
String language = Locale.getDefault().getLanguage();
if ("".equals(language)) {
language = "xx";
}
String country = Locale.getDefault().getCountry();
if ("".equals(country)) {
country = "XX";
}
return language + "_" + country;
}
/**
* Check whether the device has a network connection. WARNING: Does not check for available
* internet connection! use isOnline()
*
* @return Whether a network connection is available or not.
*/
public static boolean isConnected() {
try {
Context context = Leanplum.getContext();
ConnectivityManager manager = (ConnectivityManager) context.getSystemService(
Context.CONNECTIVITY_SERVICE);
if (manager == null) {
return false;
}
NetworkInfo netInfo = manager.getActiveNetworkInfo();
return !(netInfo == null || !netInfo.isConnectedOrConnecting());
} catch (Exception e) {
Log.d("Error getting connectivity info", e);
return false;
}
}
public static T multiIndex(Map, ?> map, Object... indices) {
if (map == null) {
return null;
}
Object current = map;
for (Object index : indices) {
if (!((Map, ?>) current).containsKey(index)) {
return null;
}
current = ((Map, ?>) current).get(index);
}
return CollectionUtil.uncheckedCast(current);
}
/**
* Check the device to make sure it has the Google Play Services APK. If it doesn't, display a
* dialog that allows users to download the APK from the Google Play Store or enable it in the
* device's system settings.
*/
public static boolean hasPlayServices() {
if (hasPlayServicesCalled) {
return hasPlayServices;
}
Context context = Leanplum.getContext();
PackageManager packageManager = context.getPackageManager();
PackageInfo packageInfo;
try {
packageInfo = packageManager.getPackageInfo("com.google.android.gms",
PackageManager.GET_SIGNATURES);
} catch (PackageManager.NameNotFoundException e) {
hasPlayServicesCalled = true;
hasPlayServices = false;
return false;
}
if (packageInfo.versionCode < 4242000) {
Log.i("Google Play services version is too old: " + packageInfo.versionCode);
hasPlayServicesCalled = true;
hasPlayServices = false;
return false;
}
ApplicationInfo info;
try {
info = packageManager.getApplicationInfo("com.google.android.gms", 0);
} catch (PackageManager.NameNotFoundException e) {
hasPlayServicesCalled = true;
hasPlayServices = false;
return false;
}
hasPlayServicesCalled = true;
hasPlayServices = info.enabled;
return info.enabled;
}
public static boolean isInBackground() {
return (LeanplumActivityHelper.getCurrentActivity() == null ||
LeanplumActivityHelper.isActivityPaused());
}
/**
* Include install time and last app update time in start API params the first time that the app
* runs with Leanplum.
*/
public static void initializePreLeanplumInstall(Map params) {
Context context = Leanplum.getContext();
SharedPreferences preferences = context.getSharedPreferences("__leanplum__",
Context.MODE_PRIVATE);
if (preferences.getBoolean(Constants.Keys.INSTALL_TIME_INITIALIZED, false)) {
return;
}
PackageManager packageManager = context.getPackageManager();
String packageName = context.getPackageName();
setInstallTime(params, packageManager, packageName);
setUpdateTime(params, packageManager, packageName);
SharedPreferences.Editor editor = preferences.edit();
editor.putBoolean(Constants.Keys.INSTALL_TIME_INITIALIZED, true);
SharedPreferencesUtil.commitChanges(editor);
}
/**
* Set install time from package manager and update time from apk file modification time.
*/
private static void setInstallTime(Map params, PackageManager packageManager,
String packageName) {
try {
PackageInfo info = packageManager.getPackageInfo(packageName, 0);
params.put(Params.INSTALL_DATE, "" + (info.firstInstallTime / 1000.0));
} catch (NameNotFoundException e) {
Log.d("Failed to find package info: " + e);
}
}
/**
* Set update time from apk file modification time.
*/
private static void setUpdateTime(Map params, PackageManager packageManager,
String packageName) {
try {
ApplicationInfo info = packageManager.getApplicationInfo(packageName, 0);
File apkFile = new File(info.sourceDir);
if (apkFile.exists()) {
params.put(Constants.Params.UPDATE_DATE, "" + (apkFile.lastModified() / 1000.0));
}
} catch (Throwable t) {
Log.d("Failed to find package info: " + t);
}
}
/**
* Initialize exception handling in the SDK.
*/
public static void initExceptionHandling(Context context) {
ExceptionHandler.getInstance().setContext(context);
}
/**
* Constructs a {@link HashMap} with the given keys and values.
*/
public static Map newMap(K firstKey, V firstValue, Object... otherValues) {
if (otherValues.length % 2 == 1) {
throw new IllegalArgumentException("Must supply an even number of values.");
}
Map map = new HashMap<>();
map.put(firstKey, firstValue);
for (int i = 0; i < otherValues.length; i += 2) {
K otherKey = CollectionUtil.uncheckedCast(otherValues[i]);
V otherValue = CollectionUtil.uncheckedCast(otherValues[i + 1]);
map.put(otherKey, otherValue);
}
return map;
}
/**
* Generates a Resource name from resourceId located in res/ folder.
*
* @param resourceId id of the resource, must be greater then 0.
* @return resourceName in format folder/file.extension.
*/
public static String generateResourceNameFromId(int resourceId) {
try {
if (resourceId <= 0) {
Log.d("Provided resource id is invalid.");
return null;
}
Resources resources = Leanplum.getContext().getResources();
// Get entryName from resourceId, which represents a file name in res/ directory.
String entryName = resources.getResourceEntryName(resourceId);
// Get typeName from resourceId, which represents a folder where file is located in
// res/ directory.
String typeName = resources.getResourceTypeName(resourceId);
// By using TypedValue we can get full path of a file with extension.
TypedValue value = new TypedValue();
resources.getValue(resourceId, value, true);
// Regex matching to find real file extension, "image.img.png" will produce "png".
String[] fullFileName = value.string.toString().split("\\.(?=[^\\.]+$)");
String extension = "";
// If extension is found, we will append dot before it.
if (fullFileName.length == 2) {
extension = "." + fullFileName[1];
}
// Return full resource name in format: drawable/image.png
return typeName + "/" + entryName + extension;
} catch (Exception e) {
Log.e("Failed to generate resource name from provided resource id: %s", e.getMessage());
Log.exception(e);
}
return null;
}
/**
* Generates resource Id based on Resource name.
*
* @param resourceName name of the resource including folder and file extension.
* @return id of the resource if found, 0 otherwise.
*/
public static int generateIdFromResourceName(String resourceName) {
// Split resource name to extract folder and file name.
String[] parts = resourceName.split("/");
if (parts.length == 2) {
Resources resources = Leanplum.getContext().getResources();
// Type name represents folder where file is contained.
String typeName = parts[0];
String fileName = parts[1];
String entryName = fileName;
// Since fileName contains extension we have to remove it,
// to be able to get resource id.
String[] fileParts = fileName.split("\\.(?=[^\\.]+$)");
if (fileParts.length == 2) {
entryName = fileParts[0];
}
// Get identifier for a file in specified directory
if (!TextUtils.isEmpty(typeName) && !TextUtils.isEmpty(entryName)) {
return resources.getIdentifier(entryName, typeName, Leanplum.getContext().getPackageName());
}
}
Log.d("Could not extract resource id from provided resource name: ", resourceName);
return 0;
}
/**
* Checks if device is manufactured by Xiaomi.
*
* @return True if device is manufactured by Xiaomi.
*/
public static boolean isXiaomiDevice() {
return Build.MANUFACTURER != null && Build.MANUFACTURER.toLowerCase().contains("xiaomi");
}
/**
* Checks for Huawei Mobile Services.
*
* @return True if Huawei Mobile Services exist.
*/
public static boolean isHuaweiServicesAvailable(Context context) {
try {
Class> clazz = Class.forName("com.huawei.hms.api.HuaweiApiAvailability");
Object instance = clazz.getMethod("getInstance").invoke(null);
Method isAvailable = clazz.getMethod("isHuaweiMobileServicesAvailable", Context.class);
int statusCode = (int) isAvailable.invoke(instance, context);
return statusCode == 0;
} catch (Throwable ignore) {
}
return false;
}
/**
* Checks if current thread is the main (UI) thread.
*
* @return True if the current thread is the main thread.
*/
public static boolean isMainThread() {
return Thread.currentThread() == Looper.getMainLooper().getThread();
}
public static String getThread() {
return isMainThread() ? "UI" : "BG";
}
}