blob: 586427e0ee50018328fe7dc58c7b5471b92be106 [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.google.common.annotations.VisibleForTesting
import com.google.wireless.android.sdk.stats.AndroidStudioEvent
import java.nio.file.Paths
import java.util.*
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.
*/
object UsageTracker {
private val gate = Any()
private var initialized = false
@VisibleForTesting
@JvmStatic
var sessionId = UUID.randomUUID().toString()
@JvmStatic
private var writer: UsageTrackerWriter = NullUsageTracker
private var isTesting: Boolean = false
/**
* 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.
*/
/*
* Sets a maximum size at which point logs need to be flushed. Zero or less indicates no
* flushing until @{link #close()} is called.
*/
@JvmStatic
var maxJournalSize: Int = 0
/**
* 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.
*/
@JvmStatic
var maxJournalTime: Long = 0
private 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.
*/
@JvmStatic
var version: String? = null
@JvmStatic
/**
* Set when Android Studio is running in development mode.
*/
var ideaIsInternal = false
/**
* Gets the ide brand specified for this UsageTracker.
*/
/**
* Set the ide brand specified for this UsageTracker.
*/
@JvmStatic
var ideBrand: AndroidStudioEvent.IdeBrand = AndroidStudioEvent.IdeBrand.UNKNOWN_IDE_BRAND
/**
* Gets the global writer to the provided tracker writer so tests can provide their own UsageTrackerWriter
* implementation. NOTE: Should only be used from Usage Tracker tests.
*/
@JvmStatic
val writerForTest: UsageTrackerWriter
@VisibleForTesting
get() {
synchronized(gate) {
return writer
}
}
/**
* Sets a timeout at which point logs need to be flushed. Zero or less indicates no timeout
* should be used.
*/
@JvmStatic
fun setMaxJournalTime(duration: Long, unit: TimeUnit) {
synchronized(gate) {
ensureInitialized()
maxJournalTime = unit.toNanos(duration)
writer.scheduleJournalTimeout(maxJournalTime)
}
}
/** Logs usage data provided in the @{link AndroidStudioEvent}. */
@JvmStatic
fun log(studioEvent: AndroidStudioEvent.Builder) {
synchronized(gate) {
ensureInitialized()
writer.logNow(studioEvent)
}
}
/** Logs usage data provided in the @{link AndroidStudioEvent} with provided event time. */
@JvmStatic
fun log(eventTimeMs: Long, studioEvent: AndroidStudioEvent.Builder) {
synchronized(gate) {
ensureInitialized()
writer.logAt(eventTimeMs, studioEvent)
}
}
private fun ensureInitialized() {
if (!initialized && java.lang.Boolean.getBoolean("idea.is.internal")) {
// Android Studio Developers: If you hit this exception, you're trying to log metrics before
// our metrics system has been initialized. Please reach out the the owners of this code
// to figure out how best to do your logging instead of sending it into the void.
throw RuntimeException("call to UsageTracker before initialization")
}
}
/**
* Initializes a [UsageTrackerWriter] for use throughout this process based on user opt-in and
* other settings.
*/
@JvmStatic
fun initialize(scheduler: ScheduledExecutorService): UsageTrackerWriter {
if (isTesting) {
return writer
}
synchronized(gate) {
val oldInstance = writer
if (AnalyticsSettings.optedIn) {
try {
writer = JournalingUsageTracker(
scheduler,
Paths.get(AnalyticsPaths.spoolDirectory)
)
}
catch (ex: RuntimeException) {
writer = NullUsageTracker
throw ex
}
}
else {
writer = NullUsageTracker
}
try {
oldInstance.close()
}
catch (ex: Exception) {
throw RuntimeException("Unable to close usage tracker", ex)
}
initialized = true
return writer
}
}
/**
* initializes or updates AnalyticsSettings into a disabled state.
*/
@JvmStatic
fun disable() {
deinitialize()
initialized = true
}
@JvmStatic
fun deinitialize() {
synchronized(gate) {
initialized = false
try {
// The writer may have pending events which will be dropped by close
// call flush() to write them before closing.
writer.flush()
writer.close()
} catch (ex: Exception) {
throw RuntimeException("Unable to close usage tracker", ex)
} finally {
writer = NullUsageTracker
}
}
}
/**
* Sets the global writer to the provided tracker so tests can provide their own UsageTracker
* implementation. NOTE: Should only be used from tests.
*/
@VisibleForTesting
@JvmStatic
fun setWriterForTest(tracker: UsageTrackerWriter): UsageTrackerWriter {
synchronized(gate) {
isTesting = true
initialized = true
val old = writer
writer = tracker
return old
}
}
/**
* resets the global writer to the null usage tracker, to clean state in tests. NOTE: Should
* only be used from tests.
*/
@VisibleForTesting
@JvmStatic
fun cleanAfterTesting() {
isTesting = false
writer = NullUsageTracker
initialized = false
}
}