Port most Android TestApp utils to kotlin (#668)

This commit is contained in:
Birk Skyum
2023-01-09 08:51:27 +01:00
committed by GitHub
parent 3b4bf343f5
commit 9d99adc431
18 changed files with 408 additions and 448 deletions

View File

@@ -57,7 +57,9 @@ class MapLibreApplication : MultiDexApplication() {
private fun initializeMapbox() {
val apiKey = ApiKeyUtils.getApiKey(applicationContext)
validateApiKey(apiKey)
if (apiKey != null) {
validateApiKey(apiKey)
}
Mapbox.getInstance(applicationContext, apiKey, TILE_SERVER)
TileLoadingMeasurementUtils.setUpTileLoadingMeasurement()
MapStrictMode.setStrictModeEnabled(true)

View File

@@ -18,7 +18,7 @@ import com.mapbox.mapboxsdk.style.layers.SymbolLayer
import com.mapbox.mapboxsdk.style.sources.GeoJsonSource
import com.mapbox.mapboxsdk.testapp.R
import com.mapbox.mapboxsdk.testapp.databinding.ActivityLatlngboundsBinding
import com.mapbox.mapboxsdk.testapp.utils.GeoParseUtil.loadStringFromAssets
import com.mapbox.mapboxsdk.testapp.utils.GeoParseUtil
import com.mapbox.mapboxsdk.utils.BitmapUtils
import java.net.URISyntaxException
@@ -52,7 +52,7 @@ class LatLngBoundsActivity : AppCompatActivity() {
mapboxMap = map
val featureCollection: FeatureCollection =
fromJson(loadStringFromAssets(this, "points-sf.geojson"))
fromJson(GeoParseUtil.loadStringFromAssets(this, "points-sf.geojson"))
bounds = createBounds(featureCollection)
map.getCameraForLatLngBounds(bounds, createPadding(peekHeight))?.let {

View File

@@ -209,21 +209,23 @@ class OfflineActivity : AppCompatActivity(), DownloadRegionDialogListener {
val metadata = OfflineUtils.convertRegionName(regionName)
// Create region
offlineManager!!.createOfflineRegion(
definition,
metadata,
object : CreateOfflineRegionCallback {
override fun onCreate(offlineRegion: OfflineRegion) {
Timber.d("Offline region created: %s", regionName)
this@OfflineActivity.offlineRegion = offlineRegion
launchDownload()
}
if (metadata != null) {
offlineManager!!.createOfflineRegion(
definition,
metadata,
object : CreateOfflineRegionCallback {
override fun onCreate(offlineRegion: OfflineRegion) {
Timber.d("Offline region created: %s", regionName)
this@OfflineActivity.offlineRegion = offlineRegion
launchDownload()
}
override fun onError(error: String) {
Timber.e("Error: %s", error)
override fun onError(error: String) {
Timber.e("Error: %s", error)
}
}
}
)
)
}
}
private fun launchDownload() {
@@ -308,7 +310,7 @@ class OfflineActivity : AppCompatActivity(), DownloadRegionDialogListener {
companion object {
// JSON encoding/decoding
const val JSON_CHARSET = "UTF-8"
val JSON_CHARSET = Charsets.UTF_8
const val JSON_FIELD_REGION_NAME = "FIELD_REGION_NAME"
// Style URL

View File

@@ -55,10 +55,12 @@ class UpdateMetadataActivity :
builder.setPositiveButton(
"OK"
) { dialog: DialogInterface?, which: Int ->
updateMetadata(
region,
OfflineUtils.convertRegionName(input.text.toString())
)
OfflineUtils.convertRegionName(input.text.toString())?.let {
updateMetadata(
region,
it
)
}
}
builder.setNegativeButton("Cancel") { dialog: DialogInterface, which: Int -> dialog.cancel() }
builder.show()

View File

@@ -1,39 +0,0 @@
package com.mapbox.mapboxsdk.testapp.utils;
import android.content.Context;
import androidx.annotation.NonNull;
import com.mapbox.mapboxsdk.Mapbox;
public class ApiKeyUtils {
/**
* <p>
* Returns the ApiKey set in the app resources.
* </p>
* It will first search for a api key in the Mapbox object. If not found it
* will then attempt to load the api key from the
* {@code res/values/dev.xml} development file.
*
* @param context The {@link Context} of the {@link android.app.Activity} or {@link android.app.Fragment}.
* @return The api key or null if not found.
*/
public static String getApiKey(@NonNull Context context) {
try {
// Read out AndroidManifest
String apiKey = Mapbox.getApiKey();
if (apiKey == null || apiKey.isEmpty()) {
throw new IllegalArgumentException();
}
return apiKey;
} catch (Exception exception) {
// Use fallback on string resource, used for development
//TODO:PP
int apiKeyResId = context.getResources()
.getIdentifier("api_key", "string", context.getPackageName());
return apiKeyResId != 0 ? context.getString(apiKeyResId) : null;
}
}
}

View File

@@ -0,0 +1,34 @@
package com.mapbox.mapboxsdk.testapp.utils
import android.content.Context
import com.mapbox.mapboxsdk.Mapbox
import java.lang.Exception
object ApiKeyUtils {
/**
*
*
* Returns the ApiKey set in the app resources.
*
* It will first search for a api key in the Mapbox object. If not found it
* will then attempt to load the api key from the
* `res/values/dev.xml` development file.
*
* @param context The [Context] of the [android.app.Activity] or [android.app.Fragment].
* @return The api key or null if not found.
*/
fun getApiKey(context: Context): String? {
return try {
// Read out AndroidManifest
val apiKey = Mapbox.getApiKey()
require(!(apiKey == null || apiKey.isEmpty()))
apiKey
} catch (exception: Exception) {
// Use fallback on string resource, used for development
// TODO:PP
val apiKeyResId = context.resources
.getIdentifier("api_key", "string", context.packageName)
if (apiKeyResId != 0) context.getString(apiKeyResId) else null
}
}
}

View File

@@ -1,26 +0,0 @@
package com.mapbox.mapboxsdk.testapp.utils;
import android.content.Context;
import android.graphics.Typeface;
import java.util.Hashtable;
import timber.log.Timber;
public class FontCache {
private static Hashtable<String, Typeface> fontCache = new Hashtable<>();
public static Typeface get(String name, Context context) {
Typeface tf = fontCache.get(name);
if (tf == null) {
try {
tf = Typeface.createFromAsset(context.getAssets(), name);
fontCache.put(name, tf);
} catch (Exception exception) {
Timber.e("Font not found");
}
}
return tf;
}
}

View File

@@ -0,0 +1,25 @@
package com.mapbox.mapboxsdk.testapp.utils
import android.content.Context
import android.graphics.Typeface
import timber.log.Timber
import java.lang.Exception
import java.util.*
object FontCache {
private val fontCache = Hashtable<String, Typeface?>()
@JvmStatic
operator fun get(name: String, context: Context): Typeface? {
var tf = fontCache[name]
if (tf == null) {
try {
tf = Typeface.createFromAsset(context.assets, name)
fontCache[name] = tf
} catch (exception: Exception) {
Timber.e("Font not found")
}
}
return tf
}
}

View File

@@ -1,51 +0,0 @@
package com.mapbox.mapboxsdk.testapp.utils;
import android.content.Context;
import android.text.TextUtils;
import com.mapbox.geojson.Feature;
import com.mapbox.geojson.FeatureCollection;
import com.mapbox.geojson.Point;
import com.mapbox.mapboxsdk.geometry.LatLng;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
public class GeoParseUtil {
public static String loadStringFromAssets(final Context context, final String fileName) throws IOException {
if (TextUtils.isEmpty(fileName)) {
throw new NullPointerException("No GeoJSON File Name passed in.");
}
InputStream is = context.getAssets().open(fileName);
BufferedReader rd = new BufferedReader(new InputStreamReader(is, Charset.forName("UTF-8")));
return readAll(rd);
}
public static List<LatLng> parseGeoJsonCoordinates(String geojsonStr) {
List<LatLng> latLngs = new ArrayList<>();
FeatureCollection featureCollection = FeatureCollection.fromJson(geojsonStr);
for (Feature feature : featureCollection.features()) {
if (feature.geometry() instanceof Point) {
Point point = (Point) feature.geometry();
latLngs.add(new LatLng(point.latitude(), point.longitude()));
}
}
return latLngs;
}
private static String readAll(Reader rd) throws IOException {
StringBuilder sb = new StringBuilder();
int cp;
while ((cp = rd.read()) != -1) {
sb.append((char) cp);
}
return sb.toString();
}
}

View File

@@ -0,0 +1,52 @@
package com.mapbox.mapboxsdk.testapp.utils
import android.content.Context
import android.text.TextUtils
import com.mapbox.geojson.FeatureCollection
import com.mapbox.geojson.Point
import com.mapbox.mapboxsdk.geometry.LatLng
import java.io.BufferedReader
import java.io.IOException
import java.io.InputStreamReader
import java.io.Reader
import java.lang.NullPointerException
import java.lang.StringBuilder
import java.util.ArrayList
import kotlin.Throws
class GeoParseUtil {
companion object {
@Throws(IOException::class)
fun loadStringFromAssets(context: Context, fileName: String?): String {
if (TextUtils.isEmpty(fileName)) {
throw NullPointerException("No GeoJSON File Name passed in.")
}
val `is` = context.assets.open(fileName!!)
val rd = BufferedReader(InputStreamReader(`is`, Charsets.UTF_8))
return readAll(rd)
}
fun parseGeoJsonCoordinates(geojsonStr: String?): List<LatLng> {
val latLngs: MutableList<LatLng> = ArrayList()
val featureCollection = FeatureCollection.fromJson(geojsonStr!!)
for (feature in featureCollection.features()!!) {
if (feature.geometry() is Point) {
val point = feature.geometry() as Point?
latLngs.add(LatLng(point!!.latitude(), point.longitude()))
}
}
return latLngs
}
@Throws(IOException::class)
private fun readAll(rd: Reader): String {
val sb = StringBuilder()
var cp: Int
while (rd.read().also { cp = it } != -1) {
sb.append(cp.toChar())
}
return sb.toString()
}
}
}

View File

@@ -1,32 +0,0 @@
package com.mapbox.mapboxsdk.testapp.utils;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import androidx.annotation.ColorInt;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.core.content.res.ResourcesCompat;
import androidx.core.graphics.drawable.DrawableCompat;
import com.mapbox.mapboxsdk.annotations.Icon;
import com.mapbox.mapboxsdk.annotations.IconFactory;
public class IconUtils {
/**
* Demonstrates converting any Drawable to an Icon, for use as a marker icon.
*/
public static Icon drawableToIcon(@NonNull Context context, @DrawableRes int id, @ColorInt int colorRes) {
Drawable vectorDrawable = ResourcesCompat.getDrawable(context.getResources(), id, context.getTheme());
Bitmap bitmap = Bitmap.createBitmap(vectorDrawable.getIntrinsicWidth(),
vectorDrawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
vectorDrawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
DrawableCompat.setTint(vectorDrawable, colorRes);
vectorDrawable.draw(canvas);
return IconFactory.getInstance(context).fromBitmap(bitmap);
}
}

View File

@@ -0,0 +1,31 @@
package com.mapbox.mapboxsdk.testapp.utils
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.drawable.DrawableCompat
import com.mapbox.mapboxsdk.annotations.Icon
import com.mapbox.mapboxsdk.annotations.IconFactory
object IconUtils {
/**
* Demonstrates converting any Drawable to an Icon, for use as a marker icon.
*/
@JvmStatic
fun drawableToIcon(context: Context, @DrawableRes id: Int, @ColorInt colorRes: Int): Icon {
val vectorDrawable = ResourcesCompat.getDrawable(context.resources, id, context.theme)
val bitmap = Bitmap.createBitmap(
vectorDrawable!!.intrinsicWidth,
vectorDrawable.intrinsicHeight,
Bitmap.Config.ARGB_8888
)
val canvas = Canvas(bitmap)
vectorDrawable.setBounds(0, 0, canvas.width, canvas.height)
DrawableCompat.setTint(vectorDrawable, colorRes)
vectorDrawable.draw(canvas)
return IconFactory.getInstance(context).fromBitmap(bitmap)
}
}

View File

@@ -1,26 +0,0 @@
package com.mapbox.mapboxsdk.testapp.utils;
import android.content.Context;
import android.widget.TextView;
import com.mapbox.mapboxsdk.camera.CameraPosition;
import com.mapbox.mapboxsdk.maps.MapboxMap;
import com.mapbox.mapboxsdk.testapp.R;
public class IdleZoomListener implements MapboxMap.OnCameraIdleListener {
private MapboxMap mapboxMap;
private TextView textView;
public IdleZoomListener(MapboxMap mapboxMap, TextView textView) {
this.mapboxMap = mapboxMap;
this.textView = textView;
}
@Override
public void onCameraIdle() {
Context context = textView.getContext();
CameraPosition position = mapboxMap.getCameraPosition();
textView.setText(String.format(context.getString(R.string.debug_zoom), position.zoom));
}
}

View File

@@ -0,0 +1,15 @@
package com.mapbox.mapboxsdk.testapp.utils
import android.widget.TextView
import com.mapbox.mapboxsdk.maps.MapboxMap
import com.mapbox.mapboxsdk.maps.MapboxMap.OnCameraIdleListener
import com.mapbox.mapboxsdk.testapp.R
class IdleZoomListener(private val mapboxMap: MapboxMap, private val textView: TextView) :
OnCameraIdleListener {
override fun onCameraIdle() {
val context = textView.context
val position = mapboxMap.cameraPosition
textView.text = String.format(context.getString(R.string.debug_zoom), position.zoom)
}
}

View File

@@ -1,37 +0,0 @@
package com.mapbox.mapboxsdk.testapp.utils;
import androidx.annotation.NonNull;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import timber.log.Timber;
import static com.mapbox.mapboxsdk.testapp.activity.offline.OfflineActivity.JSON_CHARSET;
import static com.mapbox.mapboxsdk.testapp.activity.offline.OfflineActivity.JSON_FIELD_REGION_NAME;
public class OfflineUtils {
public static String convertRegionName(@NonNull byte[] metadata) {
try {
String json = new String(metadata, JSON_CHARSET);
JsonObject jsonObject = new Gson().fromJson(json, JsonObject.class);
String name = jsonObject.get(JSON_FIELD_REGION_NAME).getAsString();
return name != null ? name : "";
} catch (Exception exception) {
return "";
}
}
public static byte[] convertRegionName(String regionName) {
try {
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty(JSON_FIELD_REGION_NAME, regionName);
return jsonObject.toString().getBytes(JSON_CHARSET);
} catch (Exception exception) {
Timber.e(exception, "Failed to encode metadata: ");
}
return null;
}
}

View File

@@ -0,0 +1,32 @@
package com.mapbox.mapboxsdk.testapp.utils
import com.google.gson.Gson
import com.google.gson.JsonObject
import com.mapbox.mapboxsdk.testapp.activity.offline.OfflineActivity
import timber.log.Timber
import java.lang.Exception
object OfflineUtils {
fun convertRegionName(metadata: ByteArray): String {
return try {
val json = String(metadata, OfflineActivity.JSON_CHARSET)
val jsonObject = Gson().fromJson(json, JsonObject::class.java)
val name = jsonObject[OfflineActivity.JSON_FIELD_REGION_NAME].asString
name ?: ""
} catch (exception: Exception) {
""
}
}
@JvmStatic
fun convertRegionName(regionName: String?): ByteArray? {
try {
val jsonObject = JsonObject()
jsonObject.addProperty(OfflineActivity.JSON_FIELD_REGION_NAME, regionName)
return jsonObject.toString().toByteArray(OfflineActivity.JSON_CHARSET)
} catch (exception: Exception) {
Timber.e(exception, "Failed to encode metadata: ")
}
return null
}
}

View File

@@ -1,216 +0,0 @@
package com.mapbox.mapboxsdk.testapp.utils;
import android.app.ActivityManager;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.net.ConnectivityManager;
import android.net.NetworkCapabilities;
import android.net.NetworkInfo;
import android.os.Build;
import android.os.Bundle;
import android.util.DisplayMetrics;
import android.view.Display;
import android.view.WindowManager;
import androidx.annotation.NonNull;
import androidx.annotation.StringDef;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.mapbox.mapboxsdk.MapStrictMode;
import com.mapbox.mapboxsdk.Mapbox;
import com.mapbox.mapboxsdk.module.http.HttpRequestUtil;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import timber.log.Timber;
import static com.mapbox.mapboxsdk.constants.MapboxConstants.DEFAULT_MEASURE_TILE_DOWNLOAD_ON;
import static com.mapbox.mapboxsdk.constants.MapboxConstants.KEY_META_DATA_MEASURE_TILE_DOWNLOAD_ON;
public class TileLoadingMeasurementUtils {
private static final String ATTRIBUTE_REQUEST_URL = "requestUrl";
public static void setUpTileLoadingMeasurement() {
if (isTileLoadingMeasurementOn()) {
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.addNetworkInterceptor(new TileLoadingInterceptor())
.build();
HttpRequestUtil.setOkHttpClient(okHttpClient);
}
}
private static boolean isTileLoadingMeasurementOn() {
return isBooleanMetaDataValueOn(KEY_META_DATA_MEASURE_TILE_DOWNLOAD_ON,
DEFAULT_MEASURE_TILE_DOWNLOAD_ON);
}
private static boolean isBooleanMetaDataValueOn(@NonNull String propKey, boolean defaultValue) {
try {
// Try getting a custom value from the app Manifest
Context context = Mapbox.getApplicationContext();
ApplicationInfo appInfo = context.getPackageManager().getApplicationInfo(context.getPackageName(),
PackageManager.GET_META_DATA);
if (appInfo.metaData != null) {
return appInfo.metaData.getBoolean(propKey, defaultValue);
}
} catch (PackageManager.NameNotFoundException exception) {
Timber.e("Failed to read the package metadata: " + exception);
MapStrictMode.strictModeViolation(exception);
} catch (Exception exception) {
Timber.e("Failed to read key: " + propKey + " " + exception);
MapStrictMode.strictModeViolation(exception);
}
return defaultValue;
}
/**
* This Interceptor allows to measure time spent getting a response object over network.
* The following data will be collected:
* responseCode, elapsedMS
* requestUrl (request string till the question mark),
* and device metadata.
*/
static class TileLoadingInterceptor implements Interceptor {
private static String metadata = null;
@StringDef( {CONNECTION_NONE, CONNECTION_CELLULAR, CONNECTION_WIFI})
@Retention(RetentionPolicy.SOURCE)
@interface ConnectionState {
}
private static final String CONNECTION_NONE = "none";
private static final String CONNECTION_CELLULAR = "cellular";
private static final String CONNECTION_WIFI = "wifi";
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
long elapsed = System.nanoTime();
Response response = chain.proceed(request);
elapsed = System.nanoTime() - elapsed;
triggerPerformanceEvent(response, elapsed / 1000000);
return response;
}
private void triggerPerformanceEvent(Response response, long elapsedMs) {
List<Attribute<String>> attributes = new ArrayList<>();
String request = getUrl(response.request());
attributes.add(new Attribute<>("requestUrl", request));
attributes.add(new Attribute<>("responseCode", String.valueOf(response.code())));
attributes.add(
new Attribute<>("connectionState", getConnectionState()));
List<Attribute<Long>> counters = new ArrayList();
counters.add(new Attribute<>("elapsedMS", elapsedMs));
Bundle bundle = new Bundle();
Gson gson = new Gson();
bundle.putString("attributes", gson.toJson(attributes));
bundle.putString("counters", gson.toJson(counters));
bundle.putString("metadata", getMetadata());
}
private static String getUrl(Request request) {
String url = request.url().toString();
return url.substring(0, url.indexOf('?'));
}
private static String getMetadata() {
if (metadata == null) {
JsonObject metaData = new JsonObject();
metaData.addProperty("os", "android");
metaData.addProperty("manufacturer", Build.MANUFACTURER);
metaData.addProperty("brand", Build.BRAND);
metaData.addProperty("device", Build.MODEL);
metaData.addProperty("version", Build.VERSION.RELEASE);
metaData.addProperty("abi", Build.CPU_ABI);
metaData.addProperty("country", Locale.getDefault().getISO3Country());
metaData.addProperty("ram", getRam());
metaData.addProperty("screenSize", getWindowSize());
metadata = metaData.toString();
}
return metadata;
}
private static String getRam() {
ActivityManager actManager =
(ActivityManager) Mapbox.getApplicationContext().getSystemService(Context.ACTIVITY_SERVICE);
ActivityManager.MemoryInfo memInfo = new ActivityManager.MemoryInfo();
actManager.getMemoryInfo(memInfo);
return String.valueOf(memInfo.totalMem);
}
private static String getWindowSize() {
WindowManager windowManager =
(WindowManager) Mapbox.getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
Display display = windowManager.getDefaultDisplay();
DisplayMetrics metrics = new DisplayMetrics();
display.getMetrics(metrics);
int width = metrics.widthPixels;
int height = metrics.heightPixels;
return "{" + width + "," + height + "}";
}
@ConnectionState
private static String getConnectionState() {
Context appContext = Mapbox.getApplicationContext();
ConnectivityManager connectivityManager =
(ConnectivityManager) appContext.getSystemService(Context.CONNECTIVITY_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (connectivityManager != null) {
NetworkCapabilities capabilities =
connectivityManager.getNetworkCapabilities(connectivityManager.getActiveNetwork());
if (capabilities != null) {
if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
return CONNECTION_WIFI;
} else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
return CONNECTION_CELLULAR;
}
}
}
} else {
if (connectivityManager != null) {
NetworkInfo activeNetwork = connectivityManager.getActiveNetworkInfo();
if (activeNetwork != null) {
if (activeNetwork.getType() == ConnectivityManager.TYPE_WIFI) {
return CONNECTION_WIFI;
} else if (activeNetwork.getType() == ConnectivityManager.TYPE_MOBILE) {
return CONNECTION_CELLULAR;
}
}
}
}
return CONNECTION_NONE;
}
}
private static class Attribute<T> {
private String name;
private T value;
Attribute(String name, T value) {
this.name = name;
this.value = value;
}
}
}

View File

@@ -0,0 +1,192 @@
package com.mapbox.mapboxsdk.testapp.utils
import android.app.ActivityManager
import android.content.Context
import android.content.pm.PackageManager
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.os.Build
import android.os.Bundle
import android.util.DisplayMetrics
import android.view.WindowManager
import androidx.annotation.StringDef
import com.google.gson.Gson
import com.google.gson.JsonObject
import com.mapbox.mapboxsdk.MapStrictMode
import com.mapbox.mapboxsdk.Mapbox
import com.mapbox.mapboxsdk.constants.MapboxConstants
import com.mapbox.mapboxsdk.module.http.HttpRequestUtil
import okhttp3.Interceptor
import okhttp3.Interceptor.Chain
import okhttp3.OkHttpClient
import okhttp3.OkHttpClient.Builder
import okhttp3.Request
import okhttp3.Response
import timber.log.Timber
import java.io.IOException
import java.util.*
class TileLoadingMeasurementUtils {
companion object {
private const val ATTRIBUTE_REQUEST_URL = "requestUrl"
fun setUpTileLoadingMeasurement() {
if (isTileLoadingMeasurementOn) {
val okHttpClient: OkHttpClient = Builder()
.addNetworkInterceptor(TileLoadingInterceptor())
.build()
HttpRequestUtil.setOkHttpClient(okHttpClient)
}
}
private val isTileLoadingMeasurementOn: Boolean
private get() = isBooleanMetaDataValueOn(
MapboxConstants.KEY_META_DATA_MEASURE_TILE_DOWNLOAD_ON,
MapboxConstants.DEFAULT_MEASURE_TILE_DOWNLOAD_ON
)
private fun isBooleanMetaDataValueOn(propKey: String, defaultValue: Boolean): Boolean {
try {
// Try getting a custom value from the app Manifest
val context = Mapbox.getApplicationContext()
val appInfo = context.packageManager.getApplicationInfo(
context.packageName,
PackageManager.GET_META_DATA
)
if (appInfo.metaData != null) {
return appInfo.metaData.getBoolean(propKey, defaultValue)
}
} catch (exception: PackageManager.NameNotFoundException) {
Timber.e("Failed to read the package metadata: $exception")
MapStrictMode.strictModeViolation(exception)
} catch (exception: Exception) {
Timber.e("Failed to read key: $propKey $exception")
MapStrictMode.strictModeViolation(exception)
}
return defaultValue
}
/**
* This Interceptor allows to measure time spent getting a response object over network.
* The following data will be collected:
* responseCode, elapsedMS
* requestUrl (request string till the question mark),
* and device metadata.
*/
internal class TileLoadingInterceptor : Interceptor {
@StringDef(*[CONNECTION_NONE, CONNECTION_CELLULAR, CONNECTION_WIFI])
@kotlin.annotation.Retention(AnnotationRetention.SOURCE)
internal annotation class ConnectionState
@Throws(IOException::class)
override fun intercept(chain: Chain): Response {
val request: Request = chain.request()
var elapsed = System.nanoTime()
val response: Response = chain.proceed(request)
elapsed = System.nanoTime() - elapsed
triggerPerformanceEvent(response, elapsed / 1000000)
return response
}
private fun triggerPerformanceEvent(response: Response, elapsedMs: Long) {
val attributes: MutableList<Attribute<String>> = ArrayList()
val request = getUrl(response.request)
attributes.add(Attribute("requestUrl", request))
attributes.add(Attribute("responseCode", response.code.toString()))
attributes.add(
Attribute("connectionState", connectionState)
)
val counters: MutableList<Attribute<Long>?> = ArrayList()
counters.add(Attribute("elapsedMS", elapsedMs))
val bundle = Bundle()
val gson = Gson()
bundle.putString("attributes", gson.toJson(attributes))
bundle.putString("counters", gson.toJson(counters))
bundle.putString("metadata", metadata)
}
companion object {
private var metadata: String? = null
private get() {
if (field == null) {
val metaData = JsonObject()
metaData.addProperty("os", "android")
metaData.addProperty("manufacturer", Build.MANUFACTURER)
metaData.addProperty("brand", Build.BRAND)
metaData.addProperty("device", Build.MODEL)
metaData.addProperty("version", Build.VERSION.RELEASE)
metaData.addProperty("abi", Build.CPU_ABI)
metaData.addProperty("country", Locale.getDefault().isO3Country)
metaData.addProperty("ram", ram)
metaData.addProperty("screenSize", windowSize)
field = metaData.toString()
}
return field
}
private const val CONNECTION_NONE = "none"
private const val CONNECTION_CELLULAR = "cellular"
private const val CONNECTION_WIFI = "wifi"
private fun getUrl(request: Request): String {
val url = request.url.toString()
return url.substring(0, url.indexOf('?'))
}
private val ram: String
private get() {
val actManager = Mapbox.getApplicationContext()
.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val memInfo = ActivityManager.MemoryInfo()
actManager.getMemoryInfo(memInfo)
return memInfo.totalMem.toString()
}
private val windowSize: String
private get() {
val windowManager = Mapbox.getApplicationContext()
.getSystemService(Context.WINDOW_SERVICE) as WindowManager
val display = windowManager.defaultDisplay
val metrics = DisplayMetrics()
display.getMetrics(metrics)
val width = metrics.widthPixels
val height = metrics.heightPixels
return "{$width,$height}"
}
@get:ConnectionState
private val connectionState: String
private get() {
val appContext = Mapbox.getApplicationContext()
val connectivityManager =
appContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (connectivityManager != null) {
val capabilities =
connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
if (capabilities != null) {
if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
return CONNECTION_WIFI
} else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
return CONNECTION_CELLULAR
}
}
}
} else {
if (connectivityManager != null) {
val activeNetwork = connectivityManager.activeNetworkInfo
if (activeNetwork != null) {
if (activeNetwork.type == ConnectivityManager.TYPE_WIFI) {
return CONNECTION_WIFI
} else if (activeNetwork.type == ConnectivityManager.TYPE_MOBILE) {
return CONNECTION_CELLULAR
}
}
}
}
return CONNECTION_NONE
}
}
}
private class Attribute<T> internal constructor(private val name: String, private val value: T)
}
}