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