blob: 4e15225cd612ba11c9212d8eb2a151166142c93d [file] [log] [blame]
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
*
* 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.android.tools.analytics;
import com.android.annotations.NonNull;
import com.android.annotations.VisibleForTesting;
import com.android.utils.DateProvider;
import com.android.utils.ILogger;
import com.google.wireless.android.play.playlog.proto.ClientAnalytics;
import com.google.wireless.android.sdk.stats.AndroidStudioEvent;
import com.google.wireless.android.sdk.stats.ProductDetails;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.UUID;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* UsageTracker is an api to report usage of features. This data is used to improve
* future versions of Android Studio and related tools.
*
* The tracker has an API to logDetails usage (in the form of protobuf messages).
* A separate system called the Analytics Publisher takes the logs and sends them
* to Google's servers for analysis.
*/
public abstract class UsageTracker implements AutoCloseable {
private static final Object sGate = new Object();
@VisibleForTesting static String sSessionId = UUID.randomUUID().toString();
@VisibleForTesting public static DateProvider sDateProvider = DateProvider.SYSTEM;
private static UsageTracker sInstance = new NullUsageTracker(new AnalyticsSettings(), null);
private final AnalyticsSettings mAnalyticsSettings;
private final ScheduledExecutorService mScheduler;
private int mMaxJournalSize;
private long mMaxJournalTime;
private String mVersion;
@VisibleForTesting protected long mStartTimeMs = sDateProvider.now().getTime();
protected UsageTracker(
AnalyticsSettings analyticsSettings, ScheduledExecutorService scheduler) {
this.mAnalyticsSettings = analyticsSettings;
this.mScheduler = scheduler;
}
/**
* Indicates whether this UsageTracker has a maximum size at which point logs need to be flushed.
* Zero or less indicates no maximum size at which to flush.
*/
public int getMaxJournalSize() {
return mMaxJournalSize;
}
/*
* Sets a maximum size at which point logs need to be flushed. Zero or less indicates no
* flushing until @{link #close()} is called.
*/
public void setMaxJournalSize(int maxJournalSize) {
this.mMaxJournalSize = maxJournalSize;
}
/**
* Indicates whether this UsageTracker has a timeout at which point logs need to be flushed.
* Zero or less indicates no timeout is set.
*
* @return timeout in nano-seconds.
*/
public long getMaxJournalTime() {
return mMaxJournalTime;
}
/**
* Sets a timeout at which point logs need to be flushed. Zero or less indicates no timeout
* should be used.
*/
public void setMaxJournalTime(long duration, TimeUnit unit) {
this.mMaxJournalTime = unit.toNanos(duration);
}
/**
* Gets the version specified for this UsageTracker. This version when specified is used
* to populate the product_details.version field of AndroidStudioEvent at time of logging
* As the version of the product generating the event can be different of the version uploading
* the event.
*/
@NonNull public String getVersion() {
return mVersion;
}
/**
* Set the version specified for this UsageTracker. This version when specified is used
* to populate the product_details.version field of AndroidStudioEvent at time of logging
* As the version of the product generating the event can be different of the version uploading
* the event.
*/
public void setVersion(@NonNull String version) {
mVersion = version;
}
/** Gets the analytics settings used by this tracker. */
public AnalyticsSettings getAnalyticsSettings() {
return mAnalyticsSettings;
}
/** Gets the scheduler used by this tracker. */
public ScheduledExecutorService getScheduler() {
return mScheduler;
}
/** Logs usage data provided in the @{link AndroidStudioEvent}. */
public void log(@NonNull AndroidStudioEvent.Builder studioEvent) {
studioEvent.setStudioSessionId(sSessionId);
if (mVersion != null && !studioEvent.hasProductDetails()) {
studioEvent.setProductDetails(ProductDetails.newBuilder().setVersion(mVersion));
}
long now = sDateProvider.now().getTime();
try {
logDetails(
ClientAnalytics.LogEvent.newBuilder()
.setEventTimeMs(now)
.setEventUptimeMs(now - mStartTimeMs)
.setSourceExtension(studioEvent.build().toByteString()));
} catch (NullPointerException exception) {
// TODO: Temporary fix for http://b.android.com/224994. We should remove this try-catch
// block once there is a permanent fix.
logDetails(
ClientAnalytics.LogEvent.newBuilder()
.setEventTimeMs(now)
.setEventUptimeMs(now - mStartTimeMs));
}
}
/**
* Logs usage data provided in the @{link ClientAnalytics.LogEvent}. Normally using {#log} is
* preferred please talk to this code's author if you need {@link #logDetails} instead.
*/
public abstract void logDetails(@NonNull ClientAnalytics.LogEvent.Builder logEvent);
/**
* Gets an instance of the {@link UsageTracker} that has been initialized correctly for this process.
*/
@NonNull
public static UsageTracker getInstance() {
synchronized (sGate) {
return sInstance;
}
}
/**
* Initializes a {@link UsageTracker} for use throughout this process based on user opt-in and
* other settings.
*/
public static UsageTracker initialize(
@NonNull AnalyticsSettings analyticsSettings,
@NonNull ScheduledExecutorService scheduler) {
synchronized (sGate) {
if (analyticsSettings.hasOptedIn()) {
sInstance =
new JournalingUsageTracker(
analyticsSettings,
scheduler,
Paths.get(AnalyticsPaths.getSpoolDirectory()));
} else {
sInstance = new NullUsageTracker(analyticsSettings, scheduler);
}
return sInstance;
}
}
/**
* Sets the global instance to the provided tracker so tests can provide their own UsageTracker
* implementation. NOTE: Should only be used from tests.
*/
@VisibleForTesting
public static UsageTracker setInstanceForTest(UsageTracker tracker) {
return sInstance = tracker;
}
/**
* resets the global instance to the null usage tracker, to clean state in tests. NOTE: Should
* only be used from tests.
*/
@VisibleForTesting
public static void cleanAfterTesting() {
sInstance = new NullUsageTracker(new AnalyticsSettings(), null);
}
public static AnalyticsSettings updateSettingsAndTracker(
boolean optIn, @NonNull ILogger logger, @NonNull ScheduledExecutorService scheduler) {
UsageTracker current = getInstance();
AnalyticsSettings settings = AnalyticsSettings.getInstance(logger);
if (optIn != settings.hasOptedIn()) {
settings.setHasOptedIn(optIn);
try {
settings.saveSettings();
} catch (IOException e) {
logger.error(e, "Unable to save analytics settings");
}
}
try {
current.close();
} catch (Exception e) {
logger.error(e, "Unable to close existing analytics tracker");
}
initialize(settings, scheduler);
return settings;
}
}