diff --git a/analytics/BUILD b/analytics/BUILD
new file mode 100644
index 00000000000..f61dc2a1b14
--- /dev/null
+++ b/analytics/BUILD
@@ -0,0 +1,20 @@
+load("//tools/base/bazel:bazel.bzl", "iml_module")
+
+# managed by go/iml_to_build
+iml_module(
+ name = "analytics",
+ srcs = ["src"],
+ iml_files = ["analytics.iml"],
+ test_srcs = ["testSrc"],
+ visibility = ["//visibility:public"],
+ # do not sort: must match IML order
+ deps = [
+ "//tools/idea/.idea/libraries:kotlin-stdlib-jdk8",
+ "//tools/idea/.idea/libraries:studio-analytics-proto",
+ "//tools/idea/.idea/libraries:HdrHistogram",
+ "//tools/analytics-library/tracker:analytics-tracker[module]",
+ "//tools/idea/.idea/libraries:JUnit4[test]",
+ "//tools/idea/.idea/libraries:protobuf",
+ "//tools/idea/platform/core-api:intellij.platform.core[module]",
+ ],
+)
diff --git a/analytics/analytics.iml b/analytics/analytics.iml
new file mode 100644
index 00000000000..ee3e75a2e2e
--- /dev/null
+++ b/analytics/analytics.iml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/analytics/src/com/android/tools/analytics/HighlightingStats.kt b/analytics/src/com/android/tools/analytics/HighlightingStats.kt
new file mode 100644
index 00000000000..69ca29c2c20
--- /dev/null
+++ b/analytics/src/com/android/tools/analytics/HighlightingStats.kt
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2020 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 com.android.tools.analytics
+
+import com.google.wireless.android.sdk.stats.AndroidStudioEvent
+import com.google.wireless.android.sdk.stats.EditorFileType
+import com.google.wireless.android.sdk.stats.EditorFileType.GROOVY
+import com.google.wireless.android.sdk.stats.EditorFileType.JAVA
+import com.google.wireless.android.sdk.stats.EditorFileType.JSON
+import com.google.wireless.android.sdk.stats.EditorFileType.KOTLIN
+import com.google.wireless.android.sdk.stats.EditorFileType.KOTLIN_SCRIPT
+import com.google.wireless.android.sdk.stats.EditorFileType.NATIVE
+import com.google.wireless.android.sdk.stats.EditorFileType.PROPERTIES
+import com.google.wireless.android.sdk.stats.EditorFileType.UNKNOWN
+import com.google.wireless.android.sdk.stats.EditorFileType.XML
+import com.google.wireless.android.sdk.stats.EditorHighlightingStats
+import com.intellij.concurrency.JobScheduler
+import com.intellij.openapi.Disposable
+import com.intellij.openapi.application.ApplicationManager
+import com.intellij.openapi.components.BaseComponent
+import com.intellij.openapi.editor.Document
+import com.intellij.openapi.fileEditor.FileDocumentManager
+import com.intellij.openapi.util.Disposer
+import com.intellij.openapi.vfs.VirtualFile
+import org.HdrHistogram.Recorder
+import java.util.concurrent.ConcurrentHashMap
+import java.util.concurrent.TimeUnit
+
+/**
+ * Tracks highlighting latency across file types.
+ * To log an [AndroidStudioEvent] with the collected data, call [reportHighlightingStats].
+ */
+object HighlightingStats : BaseComponent {
+ private const val MAX_LATENCY_MS = 10 * 60 * 1000 // Limit latencies to 10 minutes to ensure reasonable histogram size.
+
+ override fun initComponent() {
+ // Send reports hourly and on application close.
+ JobScheduler.getScheduler().scheduleWithFixedDelay(this::reportHighlightingStats, 1, 1, TimeUnit.HOURS)
+ Disposer.register(ApplicationManager.getApplication(), Disposable(this::reportHighlightingStats))
+ }
+
+ /**
+ * Maps file types to latency recorders.
+ * We use [Recorder] to allow thread-safe read access from background threads.
+ */
+ private val latencyRecorders = ConcurrentHashMap()
+
+ fun recordHighlightingLatency(document: Document, latencyMs: Long) {
+ if (latencyMs < 0 || latencyMs > MAX_LATENCY_MS) return
+ val file = FileDocumentManager.getInstance().getFile(document) ?: return
+ val fileType = convertFileType(file)
+ val recorder = latencyRecorders.computeIfAbsent(fileType) { Recorder(1) }
+ recorder.recordValue(latencyMs)
+ }
+
+ /**
+ * Logs an [AndroidStudioEvent] with editor highlighting stats.
+ * Resets statistics so that counts are not double-counted in the next report.
+ */
+ fun reportHighlightingStats() {
+ val allStats = EditorHighlightingStats.newBuilder()
+ for ((fileType, recorder) in latencyRecorders) {
+ val histogram = recorder.intervalHistogram // Automatically resets statistics for this recorder.
+ if (histogram.totalCount == 0L) {
+ continue
+ }
+ val record = EditorHighlightingStats.Stats.newBuilder().also {
+ it.fileType = fileType
+ it.histogram = histogram.toProto()
+ }
+ allStats.addByFileType(record.build())
+ }
+
+ if (allStats.byFileTypeCount == 0) {
+ return
+ }
+
+ UsageTracker.log(
+ AndroidStudioEvent.newBuilder().apply {
+ kind = AndroidStudioEvent.EventKind.EDITOR_HIGHLIGHTING_STATS
+ editorHighlightingStats = allStats.build()
+ }
+ )
+ }
+
+ /** Converts from file type name to proto enum value. */
+ private fun convertFileType(file: VirtualFile): EditorFileType = when (file.fileType.name) {
+ // We use string literals here (rather than, e.g., JsonFileType.INSTANCE.name) to avoid unnecessary
+ // dependencies on other plugins. Fortunately, these values are extremely unlikely to change.
+ "JAVA" -> JAVA
+ "Kotlin" -> if (file.extension == "kts") KOTLIN_SCRIPT else KOTLIN
+ "XML" -> XML
+ "Groovy" -> GROOVY
+ "Properties" -> PROPERTIES
+ "JSON" -> JSON
+ "ObjectiveC" -> NATIVE
+ else -> UNKNOWN
+ }
+}
diff --git a/analytics/src/com/android/tools/analytics/HistogramUtil.kt b/analytics/src/com/android/tools/analytics/HistogramUtil.kt
new file mode 100644
index 00000000000..175c5c8eb8f
--- /dev/null
+++ b/analytics/src/com/android/tools/analytics/HistogramUtil.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2020 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.
+ */
+@file:JvmName("HistogramUtil")
+
+package com.android.tools.analytics
+
+import com.google.wireless.android.sdk.stats.HistogramBin
+import org.HdrHistogram.Histogram
+import org.HdrHistogram.HistogramIterationValue
+
+typealias HistogramProto = com.google.wireless.android.sdk.stats.Histogram
+
+/**
+ * Returns the inclusive start value for the given bin.
+ */
+val HistogramIterationValue.start: Long get() {
+ // Special case: HdrHistogram encodes the first bin as 0,0 even though it's supposed to be 0,1
+ if (valueIteratedFrom == 0L && valueIteratedTo == 0L) {
+ return 0L
+ }
+ return valueIteratedFrom + 1
+}
+
+/**
+ * Returns the exclusive end value for the given bin.
+ */
+val HistogramIterationValue.end: Long get() {
+ return valueIteratedTo + 1
+}
+
+/**
+ * Converts a [Histogram] to a proto.
+ */
+fun Histogram.toProto(): HistogramProto {
+ val builder = HistogramProto.newBuilder()
+
+ var total = totalCount
+ builder.totalCount = total
+ for (value in allValues()) {
+ if (value.countAddedInThisIterationStep > 0L) {
+ // HdrHistogram has a special case for it's first bin, which uses the range 0 to 0. Subsequent bins have an inclusive
+ // upper bound and exclusive lower bound. We use inclusive lower bounds and exclusive upper bounds in the proto, so
+ // need to shuffle around some indices here.
+ builder.addBin(HistogramBin.newBuilder()
+ .setStart(value.start)
+ .setEnd(value.end)
+ .setTotalSamples(total)
+ .setSamples(value.countAddedInThisIterationStep))
+ }
+ total -= value.countAddedInThisIterationStep
+ }
+
+ return builder.build()
+}
diff --git a/analytics/src/com/android/tools/analytics/StudioUpdateAnalyticsUtil.kt b/analytics/src/com/android/tools/analytics/StudioUpdateAnalyticsUtil.kt
new file mode 100644
index 00000000000..d83471f7be4
--- /dev/null
+++ b/analytics/src/com/android/tools/analytics/StudioUpdateAnalyticsUtil.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2020 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.
+ */
+@file:JvmName("StudioUpdateAnalyticsUtil")
+
+package com.android.tools.analytics
+
+import com.android.tools.analytics.UsageTracker
+import com.google.wireless.android.sdk.stats.AndroidStudioEvent
+import com.google.wireless.android.sdk.stats.StudioUpdateFlowEvent
+
+fun logClickUpdate(newBuild: String) {
+ log(StudioUpdateFlowEvent.newBuilder().setEventKind(StudioUpdateFlowEvent.Kind.DIALOG_CLICK_UPDATE), newBuild)
+}
+
+fun logClickIgnore(newBuild: String) {
+ log(StudioUpdateFlowEvent.newBuilder().setEventKind(StudioUpdateFlowEvent.Kind.DIALOG_CLICK_IGNORE), newBuild)
+}
+
+fun logClickLater(newBuild: String) {
+ log(StudioUpdateFlowEvent.newBuilder().setEventKind(StudioUpdateFlowEvent.Kind.DIALOG_CLICK_LATER), newBuild)
+}
+
+fun logClickAction(actionName: String, newBuild: String) {
+ log(StudioUpdateFlowEvent.newBuilder()
+ .setEventKind(StudioUpdateFlowEvent.Kind.DIALOG_CLICK_ACTION)
+ .setActionName(actionName), newBuild)
+}
+
+fun logDownloadSuccess(newBuild: String) {
+ log(StudioUpdateFlowEvent.newBuilder().setEventKind(StudioUpdateFlowEvent.Kind.PATCH_DOWNLOAD_SUCCESS), newBuild)
+}
+
+fun logDownloadFailure(newBuild: String) {
+ log(StudioUpdateFlowEvent.newBuilder().setEventKind(StudioUpdateFlowEvent.Kind.PATCH_DOWNLOAD_FAILURE), newBuild)
+}
+
+fun logUpdateDialogOpenManually(newBuild: String) {
+ log(StudioUpdateFlowEvent.newBuilder()
+ .setEventKind(StudioUpdateFlowEvent.Kind.DIALOG_OPEN)
+ .setDialogTrigger(StudioUpdateFlowEvent.DialogTrigger.MANUAL), newBuild)
+}
+
+fun logUpdateDialogOpenFromNotification(newBuild: String) {
+ log(StudioUpdateFlowEvent.newBuilder()
+ .setEventKind(StudioUpdateFlowEvent.Kind.DIALOG_OPEN)
+ .setDialogTrigger(StudioUpdateFlowEvent.DialogTrigger.NOTIFICATION), newBuild)
+}
+
+fun logClickNotification(newBuild: String) {
+ log(StudioUpdateFlowEvent.newBuilder().setEventKind(StudioUpdateFlowEvent.Kind.NOTIFICATION_UPDATE_LINK_CLICKED), newBuild)
+
+}
+
+fun logNotificationShown(newBuild: String) {
+ log(StudioUpdateFlowEvent.newBuilder().setEventKind(StudioUpdateFlowEvent.Kind.NOTIFICATION_SHOWN), newBuild)
+}
+
+fun log(event: StudioUpdateFlowEvent.Builder, newBuild: String) {
+ event.setStudioNewVersion(newBuild)
+ UsageTracker.log(AndroidStudioEvent.newBuilder()
+ .setKind(AndroidStudioEvent.EventKind.STUDIO_UPDATE_FLOW)
+ .setStudioUpdateFlowEvent(event.build())
+ )
+}
diff --git a/analytics/testSrc/com/android/analytics/HistogramUtilTest.kt b/analytics/testSrc/com/android/analytics/HistogramUtilTest.kt
new file mode 100644
index 00000000000..f25218d3f05
--- /dev/null
+++ b/analytics/testSrc/com/android/analytics/HistogramUtilTest.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2020 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 com.android.analytics;
+
+import com.android.tools.analytics.toProto
+import org.HdrHistogram.Histogram
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class HistogramUtilTest {
+ @Test
+ fun testBinBoundaries() {
+ // We repeat the test 100 times to verify bins with different numbers of digits
+ for (i in 0..100) {
+ val hist = Histogram(1)
+ hist.recordValue(i.toLong())
+ val proto = hist.toProto()
+ assertEquals("There should be one sample in the histogram", 1, proto.totalCount)
+ val binList = proto.binList
+ assertEquals("Empty bins should not be converted to protos", 1, binList.size)
+ val bin = binList[0]
+ assertTrue("The bin start value should be inclusive", bin.start <= i)
+ assertTrue("The bin end value should be exclusive", i < bin.end)
+ }
+ }
+
+ @Test
+ fun testEmptyHistogram() {
+ val emptyProto = Histogram(1).toProto()
+ assertEquals("There should be no samples in an empty histogram", 0, emptyProto.totalCount)
+ }
+}
\ No newline at end of file
diff --git a/android-test-framework/testSrc/org/jetbrains/android/AndroidTestBase.java b/android-test-framework/testSrc/org/jetbrains/android/AndroidTestBase.java
index 674a3aba844..f68137a0275 100644
--- a/android-test-framework/testSrc/org/jetbrains/android/AndroidTestBase.java
+++ b/android-test-framework/testSrc/org/jetbrains/android/AndroidTestBase.java
@@ -101,6 +101,7 @@ private static void checkUndisposedAndroidRelatedObjects() {
DisposerExplorer.visitTree(disposable -> {
if (disposable.getClass().getName().equals("com.android.tools.idea.adb.AdbService") ||
disposable.getClass().getName().equals("com.android.tools.idea.adb.AdbOptionsService") ||
+ disposable.getClass().getName().startsWith("com.android.tools.analytics.HighlightingStats") ||
(disposable instanceof ProjectImpl && (((ProjectImpl)disposable).isDefault() || ((ProjectImpl)disposable).isLight())) ||
disposable.toString().startsWith("services of " + ProjectImpl.class.getName()) ||
(disposable instanceof Module && ((Module)disposable).getName().equals(LightProjectDescriptor.TEST_MODULE_NAME)) ||
diff --git a/android/intellij.android.core.iml b/android/intellij.android.core.iml
index 37726378f15..fca1944343d 100755
--- a/android/intellij.android.core.iml
+++ b/android/intellij.android.core.iml
@@ -19,6 +19,9 @@
+
+
+
diff --git a/android/lint_baseline.xml b/android/lint_baseline.xml
index 70ed0887e8e..8ee66aca896 100755
--- a/android/lint_baseline.xml
+++ b/android/lint_baseline.xml
@@ -14,7 +14,7 @@
message="To get local formatting use `getDateInstance()`, `getDateTimeInstance()`, or `getTimeInstance()`, or use `new SimpleDateFormat(String template, Locale locale)` with for example `Locale.US` for ASCII dates.">
+ line="239"/>
+ line="614"/>
+ line="753"/>
+ line="918"/>
+ line="919"/>
+ line="1383"/>
+ line="58"/>
+ line="59"/>
+
+
+
+
+ line="79"/>
+ line="387"/>
+ line="986"/>
-
+
@@ -774,7 +782,7 @@
message="Use `whenCompleteAsync` overload with an explicit Executor instead. See `go/do-not-freeze`.">
+ line="802"/>
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/src/META-INF/android-plugin.xml b/android/src/META-INF/android-plugin.xml
index 3fc1925bf2b..a39077dfbf7 100644
--- a/android/src/META-INF/android-plugin.xml
+++ b/android/src/META-INF/android-plugin.xml
@@ -39,7 +39,11 @@
messages.AndroidBundle
-
+
+
+ com.android.tools.analytics.HighlightingStats
+
+
org.jetbrains.android.AndroidProjectComponent
@@ -315,6 +319,7 @@
+
diff --git a/android/src/com/android/tools/idea/diagnostics/AndroidStudioSystemHealthMonitor.java b/android/src/com/android/tools/idea/diagnostics/AndroidStudioSystemHealthMonitor.java
index b1bd623858e..a958b132d32 100755
--- a/android/src/com/android/tools/idea/diagnostics/AndroidStudioSystemHealthMonitor.java
+++ b/android/src/com/android/tools/idea/diagnostics/AndroidStudioSystemHealthMonitor.java
@@ -16,6 +16,7 @@
package com.android.tools.idea.diagnostics;
import com.android.tools.analytics.AnalyticsSettings;
+import com.android.tools.analytics.HistogramUtil;
import com.android.tools.analytics.UsageTracker;
import com.android.tools.idea.diagnostics.crash.StudioCrashReporter;
import com.android.tools.idea.diagnostics.hprof.action.AnalysisRunnable;
@@ -95,7 +96,6 @@
import com.intellij.openapi.util.SystemInfoRt;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
-import com.intellij.util.analytics.HistogramUtil;
import com.intellij.util.messages.MessageBusConnection;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;
diff --git a/android/src/com/android/tools/idea/startup/AndroidStudioAnalyticsImpl.java b/android/src/com/android/tools/idea/startup/AndroidStudioAnalyticsImpl.java
new file mode 100644
index 00000000000..1deb9c1027a
--- /dev/null
+++ b/android/src/com/android/tools/idea/startup/AndroidStudioAnalyticsImpl.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2020 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 com.android.tools.idea.startup;
+
+import com.android.tools.analytics.AnalyticsPublisher;
+import com.android.tools.analytics.AnalyticsSettings;
+import com.android.tools.analytics.AnalyticsSettingsData;
+import com.android.tools.analytics.HighlightingStats;
+import com.android.tools.analytics.StudioUpdateAnalyticsUtil;
+import com.android.tools.analytics.UsageTracker;
+import com.android.utils.ILogger;
+import com.intellij.analytics.AndroidStudioAnalytics;
+import com.intellij.concurrency.JobScheduler;
+import com.intellij.ide.ConsentOptionsProvider;
+import com.intellij.internal.statistic.persistence.UsageStatisticsPersistenceComponent;
+import com.intellij.openapi.application.Application;
+import com.intellij.openapi.application.ApplicationInfo;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.editor.Document;
+import java.io.IOException;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public class AndroidStudioAnalyticsImpl extends AndroidStudioAnalytics {
+ private ILogger androidLogger;
+
+ @Override
+ public boolean isAllowed() {
+ // As we cannot control when IJ calls into this code, we need to load the AnalyticsSettings if
+ // we're not initialized yet, to ensure we properly return opt-in status.
+ if (!AnalyticsSettings.getInitialized()) {
+ Application application = ApplicationManager.getApplication();
+ if (application != null && application.isUnitTestMode()) {
+ AnalyticsSettingsData analyticsSettings = new AnalyticsSettingsData();
+ analyticsSettings.setOptedIn(false);
+ AnalyticsSettings.setInstanceForTest(analyticsSettings);
+ } else {
+ AnalyticsSettings.initialize(getAndroidLogger());
+ }
+ }
+ return AnalyticsSettings.getOptedIn();
+
+ }
+
+
+ @Override
+ public void recordHighlightingLatency(Document document, long latencyMs) {
+ HighlightingStats.INSTANCE.recordHighlightingLatency(document, latencyMs);
+ }
+
+ @Override
+ public void logUpdateDialogOpenManually(@NotNull String newBuild) {
+ StudioUpdateAnalyticsUtil.logUpdateDialogOpenManually(newBuild);
+ }
+
+ @Override
+ public void logNotificationShown(@NotNull String newBuild) {
+ StudioUpdateAnalyticsUtil.logNotificationShown(newBuild);
+ }
+
+ @Override
+ public void logClickNotification(@NotNull String newBuild) {
+ StudioUpdateAnalyticsUtil.logClickNotification(newBuild);
+ }
+
+ @Override
+ public void logUpdateDialogOpenFromNotification(@NotNull String newBuild) {
+ StudioUpdateAnalyticsUtil.logUpdateDialogOpenFromNotification(newBuild);
+ }
+
+ @Override
+ public void logClickIgnore(String newBuild) {
+ StudioUpdateAnalyticsUtil.logClickIgnore(newBuild);
+ }
+
+ @Override
+ public void logClickLater(String newBuild) {
+ StudioUpdateAnalyticsUtil.logClickLater(newBuild);
+ }
+
+ @Override
+ public void logDownloadSuccess(String newBuild) {
+ StudioUpdateAnalyticsUtil.logDownloadSuccess(newBuild);
+ }
+
+ @Override
+ public void logDownloadFailure(String newBuild) {
+ StudioUpdateAnalyticsUtil.logDownloadFailure(newBuild);
+ }
+
+ @Override
+ public void updateAndroidStudioMetrics() {
+ updateAndroidStudioMetrics(getConsentOptionsProvider().isSendingUsageStatsAllowed());
+ }
+
+ private @Nullable ConsentOptionsProvider getConsentOptionsProvider() {
+ return UsageStatisticsPersistenceComponent.getConsentOptionsProvider();
+ }
+
+ private void updateAndroidStudioMetrics(boolean allowed) {
+
+ // Update the settings & tracker based on allowed state, will initialize on first call.
+ boolean updated = false;
+ try {
+ if (allowed == AnalyticsSettings.getOptedIn()) {
+ updated = false;
+ } else {
+ AnalyticsSettings.setOptedIn(allowed);
+ AnalyticsSettings.saveSettings();
+ updated = true;
+ }
+ } catch (IOException e) {
+ getAndroidLogger().error(e, "Unable to update analytics settings");
+ }
+ if (updated) {
+ initializeAndroidStudioUsageTrackerAndPublisher();
+ }
+ }
+
+ @Override
+ public void initializeAndroidStudioUsageTrackerAndPublisher() {
+ ILogger logger = getAndroidLogger();
+
+ ScheduledExecutorService scheduler = JobScheduler.getScheduler();
+ AnalyticsSettings.initialize(logger, scheduler);
+
+ try {
+ // If AnalyticsSettings and IJ opt-in status disagree, then we assume IJ is correct.
+ // This catches cornercases such as manual modifications as well as deal with the
+ // incorrect rename of "hasOptedIn" to "optedIn" in some early 3.3 canary builds.
+ boolean ijOptedIn = getConsentOptionsProvider().isSendingUsageStatsAllowed();
+ if (AnalyticsSettings.getOptedIn() != ijOptedIn) {
+ AnalyticsSettings.setOptedIn(ijOptedIn);
+ AnalyticsSettings.saveSettings();
+ }
+ UsageTracker.initialize(scheduler);
+ } catch (Exception e) {
+ logger.warning("Unable to initialize analytics tracker: " + e.getMessage());
+ return;
+ }
+ // Update usage tracker maximums for long-lived process.
+ UsageTracker.setMaxJournalTime(10, TimeUnit.MINUTES);
+ UsageTracker.setMaxJournalSize(1000);
+
+ ApplicationInfo application = ApplicationInfo.getInstance();
+ AnalyticsPublisher.updatePublisher(logger, scheduler, application.getStrictVersion());
+ }
+
+ private ILogger getAndroidLogger() {
+ if (androidLogger == null) {
+ Logger intelliJLogger = Logger.getInstance("#com.intellij.internal.statistic.persistence.UsageStatisticsPersistenceComponent");
+ // Create logger & scheduler based on IntelliJ/ADT helpers.
+ androidLogger = new ILogger() {
+ @Override
+ public void error(@com.android.annotations.Nullable Throwable t,
+ @com.android.annotations.Nullable String msgFormat,
+ Object... args) {
+ intelliJLogger.error(String.format(msgFormat, args), t);
+ }
+
+ @Override
+ public void warning(String msgFormat, Object... args) {
+ intelliJLogger.warn(String.format(msgFormat, args));
+ }
+
+ @Override
+ public void info(String msgFormat, Object... args) {
+ intelliJLogger.info(String.format(msgFormat, args));
+ }
+
+ @Override
+ public void verbose(String msgFormat, Object... args) {
+ info(msgFormat, args);
+ }
+ };
+ }
+ return androidLogger;
+ }
+}
diff --git a/android/src/com/android/tools/idea/startup/AndroidStudioInitializer.java b/android/src/com/android/tools/idea/startup/AndroidStudioInitializer.java
index 0843092ede0..e42abb20a88 100644
--- a/android/src/com/android/tools/idea/startup/AndroidStudioInitializer.java
+++ b/android/src/com/android/tools/idea/startup/AndroidStudioInitializer.java
@@ -33,11 +33,13 @@
import com.android.tools.idea.testartifacts.junit.AndroidJUnitConfigurationType;
import com.android.tools.idea.ui.resourcemanager.actions.ShowFileInResourceManagerAction;
import com.google.wireless.android.sdk.stats.AndroidStudioEvent;
+import com.intellij.analytics.AndroidStudioAnalytics;
import com.intellij.concurrency.JobScheduler;
import com.intellij.execution.actions.RunConfigurationProducer;
import com.intellij.execution.configurations.ConfigurationType;
import com.intellij.execution.junit.JUnitConfigurationProducer;
import com.intellij.execution.junit.JUnitConfigurationType;
+import com.intellij.ide.ApplicationLoadListener;
import com.intellij.ide.fileTemplates.FileTemplate;
import com.intellij.ide.fileTemplates.FileTemplateManager;
import com.intellij.ide.plugins.PluginManagerCore;
@@ -50,6 +52,7 @@
import com.intellij.openapi.application.ApplicationInfo;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.PathManager;
+import com.intellij.openapi.application.PreloadingActivity;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.HighlighterColors;
import com.intellij.openapi.editor.XmlHighlighterColors;
@@ -69,7 +72,6 @@
import org.jetbrains.annotations.NotNull;
import org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer;
import org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer;
-import org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer;
/**
* Performs Android Studio specific initialization tasks that are build-system-independent.
@@ -79,6 +81,15 @@
*
*/
public class AndroidStudioInitializer implements ActionConfigurationCustomizer {
+
+ public static class AndroidStudioLoadListener implements ApplicationLoadListener {
+
+ @Override
+ public void beforeApplicationLoaded(@NotNull Application application, @NotNull String configPath) {
+ AndroidStudioAnalytics.initialize(new AndroidStudioAnalyticsImpl());
+ }
+ }
+
@Override
public void customize(@NotNull ActionManager actionManager) {
checkInstallation();
@@ -121,7 +132,7 @@ private static void setupResourceManagerActions(ActionManager actionManager) {
* sets up collection of Android Studio specific analytics.
*/
private static void setupAnalytics() {
- UsageStatisticsPersistenceComponent.getInstance().initializeAndroidStudioUsageTrackerAndPublisher();
+ AndroidStudioAnalytics.getInstance().initializeAndroidStudioUsageTrackerAndPublisher();
// If the user hasn't opted in, we will ask IJ to check if the user has
// provided a decision on the statistics consent. If the user hasn't made a
diff --git a/android/src/com/android/tools/idea/stats/CompletionStats.kt b/android/src/com/android/tools/idea/stats/CompletionStats.kt
index b0ba7848133..e90b101a3c0 100644
--- a/android/src/com/android/tools/idea/stats/CompletionStats.kt
+++ b/android/src/com/android/tools/idea/stats/CompletionStats.kt
@@ -16,6 +16,7 @@
package com.android.tools.idea.stats
import com.android.tools.analytics.UsageTracker
+import com.android.tools.analytics.toProto
import com.android.tools.idea.stats.CompletionStats.reportCompletionStats
import com.google.wireless.android.sdk.stats.AndroidStudioEvent
import com.google.wireless.android.sdk.stats.EditorCompletionStats
@@ -30,7 +31,6 @@ import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.project.Project
import com.intellij.openapi.startup.StartupActivity
-import com.intellij.util.analytics.toProto
import org.HdrHistogram.SingleWriterRecorder
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentMap
diff --git a/android/src/com/android/tools/idea/stats/TypingLatencyTracker.kt b/android/src/com/android/tools/idea/stats/TypingLatencyTracker.kt
index 48aa3c4d5f7..9c1d058a83b 100644
--- a/android/src/com/android/tools/idea/stats/TypingLatencyTracker.kt
+++ b/android/src/com/android/tools/idea/stats/TypingLatencyTracker.kt
@@ -16,6 +16,7 @@
package com.android.tools.idea.stats
import com.android.tools.analytics.UsageTracker
+import com.android.tools.analytics.toProto
import com.android.tools.idea.stats.TypingLatencyTracker.reportTypingLatency
import com.google.wireless.android.sdk.stats.AndroidStudioEvent
import com.google.wireless.android.sdk.stats.EditorFileType
@@ -23,7 +24,6 @@ import com.google.wireless.android.sdk.stats.TypingLatencyStats
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.actionSystem.LatencyListener
import com.intellij.openapi.fileEditor.FileDocumentManager
-import com.intellij.util.analytics.toProto
import org.HdrHistogram.SingleWriterRecorder
import java.util.concurrent.ConcurrentHashMap
diff --git a/lint/tests/testSrc/com/android/tools/idea/lint/common/LintIdeTest.kt b/lint/tests/testSrc/com/android/tools/idea/lint/common/LintIdeTest.kt
index 4ccd950a57b..29fbd4306b8 100644
--- a/lint/tests/testSrc/com/android/tools/idea/lint/common/LintIdeTest.kt
+++ b/lint/tests/testSrc/com/android/tools/idea/lint/common/LintIdeTest.kt
@@ -22,6 +22,8 @@ import com.google.common.collect.Lists
import com.google.common.collect.Sets
import com.google.common.truth.Truth.assertThat
import com.intellij.analysis.AnalysisScope
+import com.intellij.analytics.AndroidStudioAnalytics
+import com.intellij.analytics.NullAndroidStudioAnalytics
import com.intellij.codeInsight.daemon.impl.ShowIntentionsPass
import com.intellij.codeInsight.daemon.impl.ShowIntentionsPass.IntentionsInfo
import com.intellij.codeInsight.intention.IntentionAction
@@ -85,6 +87,7 @@ class LintIdeTest : UsefulTestCase() {
myModule = moduleFixtureBuilder.fixture!!.module
AndroidLintInspectionBase.setRegisterDynamicToolsFromTests(false)
fixture.allowTreeAccessForAllFiles()
+ AndroidStudioAnalytics.initialize(NullAndroidStudioAnalytics());
}
override fun tearDown() {
diff --git a/studio-updater/BUILD b/studio-updater/BUILD
new file mode 100644
index 00000000000..eabed269689
--- /dev/null
+++ b/studio-updater/BUILD
@@ -0,0 +1,63 @@
+load("//tools/base/bazel:bazel.bzl", "iml_module")
+load("//tools/base/bazel:coverage.bzl", "coverage_java_test")
+
+# managed by go/iml_to_build
+iml_module(
+ name = "intellij.android.updater.studio-updater",
+ srcs = ["src"],
+ iml_files = ["intellij.android.updater.studio-updater.iml"],
+ # do not sort: must match IML order
+ test_runtime_deps = [
+ "//tools/idea/xml/dom-openapi:intellij.xml.dom",
+ "//tools/idea/platform/testRunner:intellij.platform.testRunner",
+ "//tools/idea/xml/xml-structure-view-impl:intellij.xml.structureView.impl",
+ "//tools/idea/xml/dom-impl:intellij.xml.dom.impl",
+ "//tools/idea/spellchecker:intellij.spellchecker",
+ "//tools/idea/platform/lvcs-impl:intellij.platform.lvcs.impl",
+ "//tools/idea/platform/testFramework/extensions:intellij.platform.testExtensions",
+ "//tools/idea/platform/statistics/devkit:intellij.platform.statistics.devkit",
+ "//tools/idea/platform/credential-store:intellij.platform.credentialStore",
+ "//tools/idea/images:intellij.platform.images",
+ "//tools/idea/platform/external-system-impl:intellij.platform.externalSystem.impl",
+ "//tools/idea/platform/built-in-server:intellij.platform.builtInServer.impl",
+ "//tools/idea/platform/tasks-platform-impl:intellij.platform.tasks.impl",
+ "//tools/idea/json:intellij.json",
+ "//tools/idea/.idea/libraries:delight-rhino-sandbox",
+ "//tools/idea/.idea/libraries:rhino",
+ "//tools/idea/.idea/libraries:netty-handler-proxy",
+ "//tools/idea/.idea/libraries:javassist",
+ "//tools/idea/platform/diagnostic:intellij.platform.diagnostic",
+ "//tools/idea/.idea/libraries:error-prone-annotations",
+ "//tools/idea/.idea/libraries:javax.activation",
+ "//tools/idea/.idea/libraries:jaxb-api",
+ "//tools/idea/.idea/libraries:jaxb-runtime",
+ ],
+ test_srcs = ["testSrc"],
+ test_tags = ["manual"], # Tested via the integration_test target below
+ visibility = ["//visibility:public"],
+ runtime_deps = ["//prebuilts/tools/common/m2/repository/it/unimi/dsi/fastutil/7.2.1:jar"],
+ # do not sort: must match IML order
+ deps = [
+ "//tools/idea/.idea/libraries:JUnit4[test]",
+ "//tools/idea/.idea/libraries:assertJ[test]",
+ "//tools/base/common:studio.android.sdktools.common[module]",
+ "//tools/analytics-library/shared:analytics-shared[module]",
+ "//tools/analytics-library/tracker:analytics-tracker[module]",
+ "//tools/idea/.idea/libraries:studio-analytics-proto",
+ "//tools/idea/.idea/libraries:protobuf",
+ "//tools/base/testutils:studio.android.sdktools.testutils[module, test]",
+ "//tools/idea/.idea/libraries:Guava[test]",
+ "//tools/idea/.idea/libraries:kotlin-stdlib-jdk8",
+ "//tools/idea/updater:intellij.platform.updater[module]",
+ "//tools/idea/platform/util:intellij.platform.util[module, test]",
+ "//tools/idea/platform/testFramework:intellij.platform.testFramework[module, test]",
+ ],
+)
+
+coverage_java_test(
+ name = "integration_test",
+ data = ["//tools/idea/updater:updater_deploy.jar"],
+ tags = ["no_test_windows"], # b/77288863
+ test_class = "com.android.studio.updater.StudioPatchUpdaterIntegrationTest",
+ runtime_deps = [":intellij.android.updater.studio-updater_testlib"],
+)
diff --git a/studio-updater/intellij.android.updater.studio-updater.iml b/studio-updater/intellij.android.updater.studio-updater.iml
new file mode 100644
index 00000000000..fc1f8096eb8
--- /dev/null
+++ b/studio-updater/intellij.android.updater.studio-updater.iml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/studio-updater/src/META-INF/services/com.studio.updater.UpdaterService b/studio-updater/src/META-INF/services/com.studio.updater.UpdaterService
new file mode 100644
index 00000000000..66298308015
--- /dev/null
+++ b/studio-updater/src/META-INF/services/com.studio.updater.UpdaterService
@@ -0,0 +1 @@
+com.studio.updater.StudioUpdaterService
\ No newline at end of file
diff --git a/studio-updater/src/com/studio/updater/StudioUpdaterAnalyticsReportingUI.kt b/studio-updater/src/com/studio/updater/StudioUpdaterAnalyticsReportingUI.kt
new file mode 100644
index 00000000000..b31ffac3c05
--- /dev/null
+++ b/studio-updater/src/com/studio/updater/StudioUpdaterAnalyticsReportingUI.kt
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2019 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 com.studio.updater
+
+import com.android.tools.analytics.UsageTracker
+import com.google.wireless.android.sdk.stats.AndroidStudioEvent
+import com.google.wireless.android.sdk.stats.ProductDetails
+import com.google.wireless.android.sdk.stats.StudioPatchUpdaterEvent
+import com.intellij.updater.OperationCancelledException
+import com.intellij.updater.UpdaterUI
+import com.intellij.updater.ValidationResult
+
+/** A delegating Updater UI that reports events to Android Studio analytics for opted-in users. */
+class StudioUpdaterAnalyticsReportingUI(private val myDelegate: UpdaterUI) : UpdaterUI {
+
+ override fun setDescription(oldBuildDesc: String, newBuildDesc: String) {
+ UsageTracker.version = newBuildDesc
+
+ UsageTracker.log(
+ AndroidStudioEvent.newBuilder().apply {
+ kind = AndroidStudioEvent.EventKind.STUDIO_PATCH_UPDATER
+ productDetails = ProductDetails.newBuilder().apply {
+ product = ProductDetails.ProductKind.STUDIO_PATCH_UPDATER
+ version = newBuildDesc
+ }.build()
+
+ studioPatchUpdaterEvent = StudioPatchUpdaterEvent.newBuilder().apply {
+ kind = StudioPatchUpdaterEvent.Kind.PATCH_DETAILS_SHOW
+ patch = StudioPatchUpdaterEvent.Patch.newBuilder().apply {
+ studioVersionFrom = oldBuildDesc
+ studioVersionTo = newBuildDesc
+ }.build()
+ }.build()
+ }
+ )
+ myDelegate.setDescription(oldBuildDesc, newBuildDesc)
+ }
+
+ override fun setDescription(text: String) {
+ myDelegate.setDescription(text)
+ }
+
+ override fun startProcess(title: String) {
+ UsageTracker.log(
+ AndroidStudioEvent.newBuilder().apply {
+ kind = AndroidStudioEvent.EventKind.STUDIO_PATCH_UPDATER
+ studioPatchUpdaterEvent = StudioPatchUpdaterEvent.newBuilder().apply {
+ kind = toAnalytics(title)
+ }.build()
+ })
+ myDelegate.startProcess(title)
+ }
+
+ override fun setProgress(percentage: Int) {
+ myDelegate.setProgress(percentage)
+ }
+
+ override fun setProgressIndeterminate() {
+ myDelegate.setProgressIndeterminate()
+ }
+
+ @Throws(OperationCancelledException::class)
+ override fun checkCancelled() {
+ myDelegate.checkCancelled()
+ }
+
+ override fun showError(message: String) {
+ UsageTracker.log(
+ AndroidStudioEvent.newBuilder().apply {
+ kind = AndroidStudioEvent.EventKind.STUDIO_PATCH_UPDATER
+ studioPatchUpdaterEvent = StudioPatchUpdaterEvent.newBuilder().apply {
+ kind = StudioPatchUpdaterEvent.Kind.FATAL_ERROR_DIALOG_SHOW
+ }.build()
+ })
+ myDelegate.showError(message)
+ }
+
+ @Throws(OperationCancelledException::class)
+ override fun askUser(message: String) {
+ UsageTracker.log(
+ AndroidStudioEvent.newBuilder().apply {
+ kind = AndroidStudioEvent.EventKind.STUDIO_PATCH_UPDATER
+ studioPatchUpdaterEvent = StudioPatchUpdaterEvent.newBuilder().apply {
+ kind = StudioPatchUpdaterEvent.Kind.RETRYABLE_ERROR_DIALOG_SHOW
+ }.build()
+ })
+ myDelegate.askUser(message)
+ }
+
+ @Throws(OperationCancelledException::class)
+ override fun askUser(validationResults: List): Map {
+ UsageTracker.log(
+ AndroidStudioEvent.newBuilder().apply {
+ kind = AndroidStudioEvent.EventKind.STUDIO_PATCH_UPDATER
+ studioPatchUpdaterEvent = StudioPatchUpdaterEvent.newBuilder().apply {
+ kind = StudioPatchUpdaterEvent.Kind.VALIDATION_PROBLEMS_DIALOG_SHOW
+ issueDialog = toAnalytics(validationResults)
+ }.build()
+ })
+ val result = myDelegate.askUser(validationResults)
+ UsageTracker.log(
+ AndroidStudioEvent.newBuilder().apply {
+ kind = AndroidStudioEvent.EventKind.STUDIO_PATCH_UPDATER
+ studioPatchUpdaterEvent = StudioPatchUpdaterEvent.newBuilder().apply {
+ kind = StudioPatchUpdaterEvent.Kind.VALIDATION_PROBLEMS_DIALOG_CLOSE
+ issueDialogChoices = toAnalytics(result)
+ }.build()
+ })
+ return result
+ }
+
+ override fun bold(text: String): String {
+ return myDelegate.bold(text)
+ }
+}
diff --git a/studio-updater/src/com/studio/updater/StudioUpdaterAnalyticsUtil.kt b/studio-updater/src/com/studio/updater/StudioUpdaterAnalyticsUtil.kt
new file mode 100644
index 00000000000..35b38f0b9a4
--- /dev/null
+++ b/studio-updater/src/com/studio/updater/StudioUpdaterAnalyticsUtil.kt
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+
+@file:JvmName("StudioUpdaterAnalyticsUtil")
+
+package com.studio.updater
+
+
+import com.android.tools.analytics.AnalyticsSettings
+import com.android.tools.analytics.UsageTracker
+import com.android.utils.StdLogger
+import com.google.wireless.android.sdk.stats.AndroidStudioEvent
+import com.google.wireless.android.sdk.stats.StudioPatchUpdaterEvent
+import com.intellij.updater.ValidationResult
+import java.util.concurrent.ScheduledThreadPoolExecutor
+import java.util.concurrent.TimeUnit
+
+fun logProcessStart() {
+ AnalyticsSettings.initialize(StdLogger(StdLogger.Level.VERBOSE))
+ UsageTracker.initialize(ScheduledThreadPoolExecutor(0))
+ UsageTracker.setMaxJournalTime(10, TimeUnit.MINUTES)
+ UsageTracker.maxJournalSize = 1000
+
+ UsageTracker.log(
+ AndroidStudioEvent.newBuilder().apply {
+ kind = AndroidStudioEvent.EventKind.STUDIO_PATCH_UPDATER
+ studioPatchUpdaterEvent = StudioPatchUpdaterEvent.newBuilder().apply {
+ kind = StudioPatchUpdaterEvent.Kind.START
+ }.build()
+ })
+ // Ensure all events are flushed to disk before process exit.
+ Runtime.getRuntime().addShutdownHook(Thread(UsageTracker::deinitialize))
+}
+
+fun logProcessSuccess() {
+ UsageTracker.log(
+ AndroidStudioEvent.newBuilder().apply {
+ kind = AndroidStudioEvent.EventKind.STUDIO_PATCH_UPDATER
+ studioPatchUpdaterEvent = StudioPatchUpdaterEvent.newBuilder().apply {
+ kind = StudioPatchUpdaterEvent.Kind.EXIT_OK
+ }.build()
+ })
+}
+
+fun logProcessAbort() {
+ UsageTracker.log(
+ AndroidStudioEvent.newBuilder().apply {
+ kind = AndroidStudioEvent.EventKind.STUDIO_PATCH_UPDATER
+ studioPatchUpdaterEvent = StudioPatchUpdaterEvent.newBuilder().apply {
+ kind = StudioPatchUpdaterEvent.Kind.EXIT_ABORT
+ }.build()
+ })
+}
+
+fun logException() {
+ UsageTracker.log(
+ AndroidStudioEvent.newBuilder().apply {
+ kind = AndroidStudioEvent.EventKind.STUDIO_PATCH_UPDATER
+ studioPatchUpdaterEvent = StudioPatchUpdaterEvent.newBuilder().apply {
+ kind = StudioPatchUpdaterEvent.Kind.EXIT_EXCEPTION
+ }.build()
+ })
+}
+
+private fun toAnalytics(kind: ValidationResult.Kind): StudioPatchUpdaterEvent.IssueDialog.Issue.Kind {
+ return when (kind) {
+ ValidationResult.Kind.INFO -> StudioPatchUpdaterEvent.IssueDialog.Issue.Kind.INFO
+ ValidationResult.Kind.CONFLICT -> StudioPatchUpdaterEvent.IssueDialog.Issue.Kind.CONFLICT
+ ValidationResult.Kind.ERROR -> StudioPatchUpdaterEvent.IssueDialog.Issue.Kind.ERROR
+ }
+}
+
+private fun toAnalytics(action: ValidationResult.Action): StudioPatchUpdaterEvent.IssueDialog.Issue.Action {
+ return when (action) {
+ ValidationResult.Action.CREATE -> StudioPatchUpdaterEvent.IssueDialog.Issue.Action.CREATE
+ ValidationResult.Action.UPDATE -> StudioPatchUpdaterEvent.IssueDialog.Issue.Action.UPDATE
+ ValidationResult.Action.DELETE -> StudioPatchUpdaterEvent.IssueDialog.Issue.Action.DELETE
+ ValidationResult.Action.NO_ACTION -> StudioPatchUpdaterEvent.IssueDialog.Issue.Action.NO_ACTION
+ ValidationResult.Action.VALIDATE -> StudioPatchUpdaterEvent.IssueDialog.Issue.Action.VALIDATE
+ }
+}
+
+private fun toAnalytics(result: ValidationResult): StudioPatchUpdaterEvent.IssueDialog.Issue.Builder {
+ return StudioPatchUpdaterEvent.IssueDialog.Issue.newBuilder().apply {
+ action = toAnalytics(result.action)
+ kind = toAnalytics(result.kind)
+ result.options.forEach { addPresentedOption(toAnalytics(it)) }
+ }
+}
+
+private fun toAnalytics(value: ValidationResult.Option): StudioPatchUpdaterEvent.ValidationOption {
+ return when (value) {
+ ValidationResult.Option.NONE -> StudioPatchUpdaterEvent.ValidationOption.NONE
+ ValidationResult.Option.IGNORE -> StudioPatchUpdaterEvent.ValidationOption.IGNORE
+ ValidationResult.Option.KEEP -> StudioPatchUpdaterEvent.ValidationOption.KEEP
+ ValidationResult.Option.REPLACE -> StudioPatchUpdaterEvent.ValidationOption.REPLACE
+ ValidationResult.Option.DELETE -> StudioPatchUpdaterEvent.ValidationOption.DELETE
+ ValidationResult.Option.KILL_PROCESS -> StudioPatchUpdaterEvent.ValidationOption.KILL_PROCESS
+ }
+}
+
+fun logProcessFinish(result: Boolean) {
+ if (result) {
+ logProcessSuccess()
+ }
+ else {
+ logProcessAbort()
+ }
+}
+
+internal fun toAnalytics(phase: String): StudioPatchUpdaterEvent.Kind {
+ return when (phase) {
+ "Extracting patch file...", "Extracting patch files..." -> StudioPatchUpdaterEvent.Kind.PHASE_EXTRACTING_PATCH_FILES
+ "Validating installation..." -> StudioPatchUpdaterEvent.Kind.PHASE_VALIDATING_INSTALLATION
+ "Backing up files..." -> StudioPatchUpdaterEvent.Kind.PHASE_BACKING_UP_FILES
+ "Preparing update..." -> StudioPatchUpdaterEvent.Kind.PHASE_PREPARING_UPDATE
+ "Applying patch..." -> StudioPatchUpdaterEvent.Kind.PHASE_APPLYING_PATCH
+ "Reverting..." -> StudioPatchUpdaterEvent.Kind.PHASE_REVERTING
+ "Cleaning up..." -> StudioPatchUpdaterEvent.Kind.PHASE_CLEANING_UP
+ else -> StudioPatchUpdaterEvent.Kind.PHASE_UNKNOWN
+ }
+}
+
+internal fun toAnalytics(results: List): StudioPatchUpdaterEvent.IssueDialog {
+ return StudioPatchUpdaterEvent.IssueDialog.newBuilder().apply {
+ results.forEach { addIssue(toAnalytics(it)) }
+ }.build()
+}
+
+internal fun toAnalytics(result: Map): StudioPatchUpdaterEvent.IssueDialogChoices {
+ return StudioPatchUpdaterEvent.IssueDialogChoices.newBuilder().apply {
+ result.values.forEach { addChoice(validationToAnalytics(it)) }
+ }.build()
+}
+
+private fun validationToAnalytics(value: ValidationResult.Option): StudioPatchUpdaterEvent.IssueDialogChoices.Choice {
+ return StudioPatchUpdaterEvent.IssueDialogChoices.Choice.newBuilder().apply {
+ chosenOption = toAnalytics(value)
+ }.build()
+}
\ No newline at end of file
diff --git a/studio-updater/src/com/studio/updater/StudioUpdaterService.java b/studio-updater/src/com/studio/updater/StudioUpdaterService.java
new file mode 100644
index 00000000000..cf18ef3b206
--- /dev/null
+++ b/studio-updater/src/com/studio/updater/StudioUpdaterService.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2020 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 com.studio.updater;
+
+import com.intellij.updater.UpdaterUI;
+
+public class StudioUpdaterService extends UpdaterService {
+ @Override
+ public void logProcessStart() {
+ StudioUpdaterAnalyticsUtil.logProcessStart();
+ }
+
+ @Override
+ public UpdaterUI wrap(UpdaterUI ui) {
+ return new StudioUpdaterAnalyticsReportingUI(ui);
+ }
+
+ @Override
+ public void logProcessFinish(boolean success) {
+ StudioUpdaterAnalyticsUtil.logProcessFinish(success);
+ }
+
+ @Override
+ public void logException() {
+ StudioUpdaterAnalyticsUtil.logException();
+ }
+}
diff --git a/studio-updater/testSrc/com/android/studio/updater/StudioPatchUpdaterIntegrationTest.kt b/studio-updater/testSrc/com/android/studio/updater/StudioPatchUpdaterIntegrationTest.kt
new file mode 100644
index 00000000000..ceb0d5f0f81
--- /dev/null
+++ b/studio-updater/testSrc/com/android/studio/updater/StudioPatchUpdaterIntegrationTest.kt
@@ -0,0 +1,228 @@
+/*
+ * Copyright (C) 2019 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 com.android.studio.updater
+
+import com.android.testutils.TestUtils
+import com.google.common.collect.MoreCollectors
+import com.google.wireless.android.play.playlog.proto.ClientAnalytics
+import com.google.wireless.android.sdk.stats.AndroidStudioEvent
+import com.google.wireless.android.sdk.stats.StudioPatchUpdaterEvent
+import com.intellij.openapi.util.SystemInfo
+import com.intellij.testFramework.rules.TempDirectory
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotEquals
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import java.io.BufferedInputStream
+import java.nio.charset.StandardCharsets
+import java.nio.file.FileVisitResult
+import java.nio.file.Files
+import java.nio.file.Path
+import java.nio.file.Paths
+import java.nio.file.SimpleFileVisitor
+import java.nio.file.attribute.BasicFileAttributes
+import java.util.ArrayList
+import java.util.HashMap
+
+/**
+ * Integration test for the studio updater.
+ *
+ * This is formulated to check the packaging of the updater that may be changed by intellij merges,
+ * hence uses the final binary, rather than being run as a unit test.
+ */
+@RunWith(JUnit4::class)
+class StudioPatchUpdaterIntegrationTest {
+
+ @get:Rule
+ var myTempDirectory = TempDirectory()
+
+ private lateinit var java: Path
+ private lateinit var patchJar: Path
+
+ enum class ExampleDirectory(private val files: Map) {
+ V1(mapOf("removed" to "v1_removed_later", "changed" to "v1_changed")),
+ V2(mapOf("added" to "v2_added_since_v1", "changed" to "v2_changed")),
+ V3(mapOf("changed" to "v3_changed"));
+
+ internal fun createExampleDir(tempDirectory: TempDirectory): Path {
+ val dir = tempDirectory.newFolder().toPath()
+ for ((path, content) in files) {
+ val file = dir.resolve(path)
+ Files.createDirectories(file.parent)
+ Files.write(file, setOf(content))
+ }
+ // Sanity test
+ verifyDir(dir)
+ return dir
+ }
+
+ internal fun verifyDir(dir: Path) {
+ val actual = readDir(dir)
+ assertEquals(files, actual)
+ }
+
+ private fun readDir(dir: Path): Map {
+ val actual = HashMap()
+ Files.walkFileTree(dir, object : SimpleFileVisitor() {
+ override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult {
+ val filePath = dir.relativize(file).toString().replace(dir.fileSystem.separator, "/")
+ actual[filePath] = Files.readAllLines(file).joinToString("\n")
+ return FileVisitResult.CONTINUE
+ }
+ })
+ return actual
+ }
+
+ }
+
+ @Before
+ fun createPatch() {
+ java = Paths.get(System.getProperty("java.home")).resolve("bin").resolve(if (SystemInfo.isWindows) "java.exe" else "java")
+ patchJar = myTempDirectory.newFolder("patch").toPath().resolve("patch.jar")
+
+ // Build the patch
+
+ val createPatcher = arrayOf(java.toString(), "-cp", updaterFullJar.toString(), UPDATER_MAIN_CLASS, "create", "v1", "v2",
+ ExampleDirectory.V1.createExampleDir(myTempDirectory).toString(),
+ ExampleDirectory.V2.createExampleDir(myTempDirectory).toString(), patchJar.toString(), "--strict")
+ runExpectingOk(createPatcher, mapOf())
+ assertTrue(Files.isRegularFile(patchJar))
+ }
+
+ /**
+ * Smoke test for patch being correctly applied
+ */
+ @Test
+ fun patchApplicationSmokeTest() {
+ val analyticsHome = createAnalyticsHome()
+ val env = mapOf("ANDROID_SDK_HOME" to analyticsHome.toString())
+
+ // When V1 to V2 patch applied to V1
+ val dir = ExampleDirectory.V1.createExampleDir(myTempDirectory)
+ val applyPatch = arrayOf(java.toString(), "-cp", patchJar.toString(), UPDATER_MAIN_CLASS, "apply", dir.toString())
+ // Patcher should succeed.
+ runExpectingOk(applyPatch, env)
+
+ // Result should be V2
+ ExampleDirectory.V2.verifyDir(dir)
+
+ // Check the events produced. This is kept as an integration test to also cover the packaging of the updater binary.
+ val events = readEvents(analyticsHome)
+ assertEquals(8, events.size.toLong())
+ for (event in events) {
+ assertEquals(AndroidStudioEvent.EventKind.STUDIO_PATCH_UPDATER, event.kind)
+ }
+ val expectedEventSequence = listOf(
+ StudioPatchUpdaterEvent.Kind.START,
+ StudioPatchUpdaterEvent.Kind.PHASE_EXTRACTING_PATCH_FILES,
+ StudioPatchUpdaterEvent.Kind.PATCH_DETAILS_SHOW,
+ StudioPatchUpdaterEvent.Kind.PHASE_VALIDATING_INSTALLATION,
+ StudioPatchUpdaterEvent.Kind.PHASE_BACKING_UP_FILES,
+ StudioPatchUpdaterEvent.Kind.PHASE_APPLYING_PATCH,
+ StudioPatchUpdaterEvent.Kind.PHASE_CLEANING_UP,
+ StudioPatchUpdaterEvent.Kind.EXIT_OK
+ )
+ assertEquals(expectedEventSequence, events.map { it.studioPatchUpdaterEvent.kind })
+
+ val details = events.map { it.studioPatchUpdaterEvent }.find { it.kind == StudioPatchUpdaterEvent.Kind.PATCH_DETAILS_SHOW }!!
+ assertEquals("v1", details.patch.studioVersionFrom)
+ assertEquals("v2", details.patch.studioVersionTo)
+ }
+
+ /**
+ * Smoke test for patch failing to be applied.
+ */
+ @Test
+ fun patchApplicationFailureTest() {
+
+ // When V1 to V2 patch applied to some other version
+ val dir = ExampleDirectory.V3.createExampleDir(myTempDirectory)
+ val applyPatch = arrayOf(java.toString(), "-cp", patchJar.toString(), UPDATER_MAIN_CLASS, "apply", dir.toString())
+ // Patcher should fail
+ runExpectingError(applyPatch, mapOf())
+
+ // Version should not be corrupted.
+ ExampleDirectory.V3.verifyDir(dir)
+ }
+
+ private fun readEvents(analyticsHome: Path): List {
+ // Check the analytics were written.
+ val spool = analyticsHome.resolve("metrics/spool")
+ val trackFile: Path = Files.list(spool).use { paths -> paths.collect(MoreCollectors.onlyElement()) }
+ val events = ArrayList()
+
+ BufferedInputStream(Files.newInputStream(trackFile)).use { inputStream ->
+ // read all LogEvents from the trackFile.
+ while (true) {
+ val event = ClientAnalytics.LogEvent.parseDelimitedFrom(inputStream) ?: break
+ val studioEvent = AndroidStudioEvent.parseFrom(event.sourceExtension)
+ events.add(studioEvent)
+ println(studioEvent)
+ println("---")
+ }
+ }
+ return events
+ }
+
+ private fun createAnalyticsHome(): Path {
+ val path = myTempDirectory.newFolder().toPath()
+ val json = "{ userId: \"a4d47d92-8d4c-44bb-a8a4-d2483b6e0c16\", hasOptedIn: true }"
+ Files.write(
+ path.resolve("").resolve("analytics.settings"),
+ json.toByteArray(StandardCharsets.UTF_8))
+ return path
+ }
+
+ // See UpdateInstaller.UPDATER_MAIN_CLASS
+ private val UPDATER_MAIN_CLASS = "com.intellij.updater.Runner"
+
+ private fun runExpectingOk(command: Array, env: Map) {
+ val returnValue = run(command, env)
+ assertEquals("Expected command to run successfully " + command.joinToString(" "), 0, returnValue.toLong())
+ }
+
+ private fun runExpectingError(command: Array, env: Map) {
+ val returnValue = run(command, env)
+ assertNotEquals("Expected command to fail " + command.joinToString(" "), 0, returnValue.toLong())
+ }
+
+ private fun run(createPatcher: Array, env: Map): Int {
+ val builder = ProcessBuilder(*createPatcher)
+ .redirectOutput(ProcessBuilder.Redirect.INHERIT)
+ .redirectError(ProcessBuilder.Redirect.INHERIT)
+ builder.environment().putAll(env)
+ return builder.start().waitFor()
+ }
+
+ private val updaterFullJar: Path
+ get() {
+ val root = TestUtils.getWorkspaceRoot().toPath()
+ val bazelDeployJar = root.resolve("tools/idea/updater/updater_deploy.jar")
+ if (Files.isRegularFile(bazelDeployJar)) {
+ return bazelDeployJar
+ }
+ val antJar = root.resolve("tools/idea/out/studio/artifacts/updater-full.jar")
+ if (Files.isRegularFile(antJar)) {
+ return antJar
+ }
+ throw RuntimeException("Unable to find updater deploy jar. Perhaps run cd tools/idea && ant fullupdater")
+ }
+
+}
\ No newline at end of file