AnalyticsBasedUsageTracker.java
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed 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 androidx.test.internal.runner.tracker;
import static androidx.test.internal.util.Checks.checkNotNull;
import static java.net.URLEncoder.encode;
import android.content.Context;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.SystemClock;
import android.util.Log;
import android.view.Display;
import android.view.WindowManager;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* Creates a usage tracker that pings google analytics when infra bits get used.
*
* @deprecated obsolete
*/
@Deprecated
public final class AnalyticsBasedUsageTracker implements UsageTracker {
private static final String TAG = "InfraTrack";
private static final String UTF_8 = "UTF-8";
private static final String APP_NAME_PARAM = "an=";
private static final String SCREEN_NAME_PARAM = "&cd=";
private static final String APP_VERSION_PARAM = "&av=";
private static final String TRACKER_ID_PARAM = "&tid=";
private static final String CLIENT_ID_PARAM = "&cid=";
private static final String SCREEN_RESOLUTION_PARAM = "&sr=";
private static final String API_LEVEL_PARAM = "&cd2=";
private static final String MODEL_NAME_PARAM = "&cd3=";
private final String trackingId;
private final String targetPackage;
private final URL analyticsURI;
private final String screenResolution;
private final String apiLevel;
private final String model;
private final String userId;
private final Map<String, String> usageTypeToVersion = new HashMap<>();
private AnalyticsBasedUsageTracker(Builder builder) {
this.trackingId = checkNotNull(builder.trackingId);
this.targetPackage = checkNotNull(builder.targetPackage);
this.analyticsURI = checkNotNull(builder.analyticsURI);
this.apiLevel = checkNotNull(builder.apiLevel);
this.model = checkNotNull(builder.model);
this.screenResolution = checkNotNull(builder.screenResolution);
this.userId = checkNotNull(builder.userId);
}
/** Builder for AnalyticsBasedUsageTracker. */
public static class Builder {
private final Context targetContext;
private Uri analyticsUri =
new Uri.Builder()
.scheme("https")
.authority("www.google-analytics.com")
.path("collect")
.build();
private String trackingId = "UA-36650409-3";
private String apiLevel = String.valueOf(Build.VERSION.SDK_INT);
private String model = Build.MODEL;
private String targetPackage;
private URL analyticsURI;
private String screenResolution;
private String userId;
private boolean hashed;
public Builder(Context targetContext) {
if (targetContext == null) {
throw new NullPointerException("Context null!?");
}
this.targetContext = targetContext;
}
public Builder withTrackingId(String trackingId) {
this.trackingId = trackingId;
return this;
}
public Builder withAnalyticsUri(Uri analyticsUri) {
checkNotNull(analyticsUri);
this.analyticsUri = analyticsUri;
return this;
}
public Builder withApiLevel(String apiLevel) {
this.apiLevel = apiLevel;
return this;
}
public Builder withScreenResolution(String resolutionVal) {
this.screenResolution = resolutionVal;
return this;
}
public Builder withUserId(String userId) {
this.userId = userId;
return this;
}
public Builder withModel(String model) {
this.model = model;
return this;
}
public Builder withTargetPackage(String targetPackage) {
hashed = false;
this.targetPackage = targetPackage;
return this;
}
public UsageTracker buildIfPossible() {
if (!hasInternetPermission()) {
Log.d(TAG, "Tracking disabled due to lack of internet permissions");
return null;
}
if (null == targetPackage) {
withTargetPackage(targetContext.getPackageName());
}
if (targetPackage.contains("com.google.analytics")) {
Log.d(TAG, "Refusing to use analytics while testing analytics.");
return null;
}
try {
if (targetPackage.startsWith("com.google.")
|| targetPackage.startsWith("com.android.")
|| targetPackage.startsWith("android.support.")) {
// track usage of google owned packages...
} else {
if (!hashed) {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
digest.reset();
digest.update(targetPackage.getBytes(UTF_8));
BigInteger hashedPackage = new BigInteger(digest.digest());
targetPackage = "sha256-" + hashedPackage.toString(16);
}
hashed = true;
}
} catch (NoSuchAlgorithmException nsae) {
Log.d(TAG, "Cannot hash package name.", nsae);
return null;
} catch (UnsupportedEncodingException uee) {
Log.d(TAG, "Impossible - no utf-8 encoding?", uee);
return null;
}
try {
analyticsURI = new URL(analyticsUri.toString());
} catch (MalformedURLException mule) {
Log.w(TAG, "Tracking disabled bad url: " + analyticsUri.toString(), mule);
return null;
}
if (null == screenResolution) {
Display display =
((WindowManager) targetContext.getSystemService(Context.WINDOW_SERVICE))
.getDefaultDisplay();
// Headless devices don't have a Display.
if (null == display) {
screenResolution = "0x0";
} else {
screenResolution =
new StringBuilder()
.append(display.getWidth())
.append("x")
.append(display.getHeight())
.toString();
}
}
if (null == userId) {
userId = UUID.randomUUID().toString();
}
return new AnalyticsBasedUsageTracker(this);
}
private boolean hasInternetPermission() {
return PackageManager.PERMISSION_GRANTED
== targetContext.checkCallingOrSelfPermission("android.permission.INTERNET");
}
}
@Override
public void trackUsage(String usageType, String version) {
synchronized (usageTypeToVersion) {
usageTypeToVersion.put(usageType, version);
}
}
@Override
public void sendUsages() {
Map<String, String> myUsages;
synchronized (usageTypeToVersion) {
if (usageTypeToVersion.isEmpty()) {
return;
}
myUsages = new HashMap<>(usageTypeToVersion);
usageTypeToVersion.clear();
}
String baseBody = null;
try {
baseBody =
new StringBuilder()
.append(APP_NAME_PARAM)
.append(encode(targetPackage, UTF_8))
.append(TRACKER_ID_PARAM)
.append(encode(trackingId, UTF_8))
.append("&v=1") // Protocol Version.
.append("&z=") // Cache Buster (optional)
.append(SystemClock.uptimeMillis())
.append(CLIENT_ID_PARAM)
.append(encode(userId, UTF_8))
.append(SCREEN_RESOLUTION_PARAM)
.append(encode(screenResolution, UTF_8))
.append(API_LEVEL_PARAM)
.append(encode(apiLevel, UTF_8))
.append(MODEL_NAME_PARAM)
.append(encode(model, UTF_8))
.append("&t=appview") // Hit type
.append("&sc=start") // Session Control
.toString();
} catch (IOException ioe) {
Log.w(TAG, "Impossible error happened. analytics disabled.", ioe);
}
for (Map.Entry<String, String> usage : myUsages.entrySet()) {
HttpURLConnection analyticsConnection = null;
try {
analyticsConnection = (HttpURLConnection) analyticsURI.openConnection();
byte[] body =
new StringBuilder()
.append(baseBody)
.append(SCREEN_NAME_PARAM)
.append(encode(usage.getKey(), UTF_8))
.append(APP_VERSION_PARAM)
.append(encode(usage.getValue(), UTF_8))
.toString()
// j5 compatibility. this is utf8.
.getBytes();
analyticsConnection.setConnectTimeout(3000); // milliseconds
analyticsConnection.setReadTimeout(5000); // milliseconds
analyticsConnection.setDoOutput(true);
analyticsConnection.setFixedLengthStreamingMode(body.length);
analyticsConnection.getOutputStream().write(body);
int status = analyticsConnection.getResponseCode();
if (status / 100 != 2) {
Log.w(
TAG,
"Analytics post: "
+ usage
+ " failed. code: "
+ analyticsConnection.getResponseCode()
+ " - "
+ analyticsConnection.getResponseMessage());
}
} catch (IOException ioe) {
Log.w(TAG, "Analytics post: " + usage + " failed. ", ioe);
} finally {
if (null != analyticsConnection) {
analyticsConnection.disconnect();
}
}
}
}
}