/* * Copyright (C) 2013 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.stats; import com.android.annotations.NonNull; import com.intellij.ide.util.PropertiesComponent; import com.intellij.internal.statistic.CollectUsagesException; import com.intellij.internal.statistic.UsagesCollector; import com.intellij.internal.statistic.beans.GroupDescriptor; import com.intellij.internal.statistic.beans.UsageDescriptor; import com.intellij.internal.statistic.connect.StatisticsConnectionService; import com.intellij.internal.statistic.connect.StatisticsResult; import com.intellij.internal.statistic.connect.StatisticsService; import com.intellij.notification.Notification; import com.intellij.notification.NotificationListener; import com.intellij.notification.NotificationType; import com.intellij.openapi.application.ApplicationInfo; import com.intellij.openapi.application.ApplicationNamesInfo; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.extensions.Extensions; import com.intellij.openapi.project.Project; import com.intellij.openapi.project.ProjectManager; import com.intellij.openapi.updateSettings.impl.UpdateChecker; import com.intellij.util.net.HttpConfigurable; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.io.OutputStream; import java.net.HttpURLConnection; import java.util.*; /** * Android Statistics Service. * Based on idea's RemotelyConfigurableStatisticsService. * Also sends a legacy ping using ADT's LegacySdkStatsService. */ @SuppressWarnings("MethodMayBeStatic") public class AndroidStatisticsService implements StatisticsService { private static final Logger LOG = Logger.getInstance("#" + AndroidStatisticsService.class.getName()); private static final String CONTENT_TYPE = "Content-Type"; private static final String HTTP_POST = "POST"; private static final int HTTP_STATUS_OK = 200; private static final String PROTOBUF_CONTENT = "application/x-protobuf"; @NonNull @Override public Notification createNotification(@NotNull final String groupDisplayId, @Nullable NotificationListener listener) { final String fullProductName = ApplicationNamesInfo.getInstance().getFullProductName(); final String companyName = ApplicationInfo.getInstance().getCompanyName(); String text = "<html>Please click <a href='allow'>I agree</a> if you want to help make " + fullProductName + " better or <a href='decline'>I don't agree</a> otherwise. <a href='settings'>more...</a></html>"; String title = "Help improve " + fullProductName + " by sending usage statistics to " + companyName; return new Notification(groupDisplayId, title, text, NotificationType.INFORMATION, listener); } @Nullable @Override public Map<String, String> getStatisticsConfigurationLabels() { Map<String, String> labels = new HashMap<String, String>(); final String fullProductName = ApplicationNamesInfo.getInstance().getFullProductName(); final String companyName = ApplicationInfo.getInstance().getCompanyName(); labels.put("title", "Help improve " + fullProductName + " by sending usage statistics to " + companyName); labels.put("allow-checkbox", "Send usage statistics to " + companyName); labels.put("details", "<html>This allows " + companyName + " to collect information about your plugins configuration (what is enabled and what is not)" + "<br/>and feature usage statistics (e.g. how frequently you're using code completion)." + "<br/>This data is collected in accordance with " + companyName + "'s privacy policy.</html>"); return labels; } @SuppressWarnings("ConstantConditions") @Override public StatisticsResult send() { LegacySdkStatsService sdkstats = sendLegacyPing(); StatisticsResult result = sendUsageStats(sdkstats); result = sendBuildStats(sdkstats); return result; } /** * Checks whether the statistics service has a service URL and is authorized * to send statistics. * * @return A {@link StatisticsResult} with a * {@link com.intellij.internal.statistic.connect.StatisticsResult.ResultCode#SEND} result code * on success, otherwise one of the error result codes. */ static StatisticsResult areStatisticsAuthorized() { // Get the redirected URL final StatisticsConnectionService service = new StatisticsConnectionService(); final String serviceUrl = service.getServiceUrl(); // Check server provided an URL and enabled sending stats. if (serviceUrl == null) { return new StatisticsResult(StatisticsResult.ResultCode.ERROR_IN_CONFIG, "ERROR"); } if (!service.isTransmissionPermitted()) { return new StatisticsResult(StatisticsResult.ResultCode.NOT_PERMITTED_SERVER, "NOT_PERMITTED"); } return new StatisticsResult(StatisticsResult.ResultCode.SEND, "OK"); } /** * Sends one LogRequests with multiple build records. * Does nothing if there are no records pending. */ private StatisticsResult sendBuildStats(LegacySdkStatsService sdkstats) { StatisticsResult code = areStatisticsAuthorized(); if (code.getCode() != StatisticsResult.ResultCode.SEND) { return code; } StudioBuildStatsPersistenceComponent records = StudioBuildStatsPersistenceComponent.getInstance(); if (records == null || !records.hasRecords()) { return new StatisticsResult(StatisticsResult.ResultCode.NOTHING_TO_SEND, "NOTHING_TO_SEND"); } StatsProto.LogRequest data = getRecordData(sdkstats, records); String error = null; try { error = sendData(data); } catch (Exception e) { error = e.getClass().getSimpleName() + " " + (e.getMessage() != null ? e.getMessage() : e.toString()); } if (error != null) { LOG.debug("[SendStats/AS-2] Error " + (error == null ? "None" : error)); } if (error == null) { return new StatisticsResult(StatisticsResult.ResultCode.SEND, "OK"); } else { return new StatisticsResult(StatisticsResult.ResultCode.SENT_WITH_ERRORS, error); } } /** * Send IJ-style "usage" stats using our format. The idea is to deactivate this eventually. */ @Deprecated private StatisticsResult sendUsageStats(LegacySdkStatsService sdkstats) { StatisticsResult code = areStatisticsAuthorized(); if (code.getCode() != StatisticsResult.ResultCode.SEND) { return code; } StatisticsConnectionService service = new StatisticsConnectionService(); StatsProto.LogRequest data = getUsageData(sdkstats, service.getDisabledGroups()); String error = null; try { error = sendData(data); } catch (Exception e) { error = e.getClass().getSimpleName() + " " + (e.getMessage() != null ? e.getMessage() : e.toString()); } if (error != null) { LOG.debug("[SendStats/AS-1] Error " + (error == null ? "None" : error)); } if (error == null) { return new StatisticsResult(StatisticsResult.ResultCode.SEND, "OK"); } else { return new StatisticsResult(StatisticsResult.ResultCode.SENT_WITH_ERRORS, error); } } private LegacySdkStatsService sendLegacyPing() { // Legacy ADT-compatible stats service. LegacySdkStatsService sdkstats = new LegacySdkStatsService(); sdkstats.ping("studio", ApplicationInfo.getInstance().getFullVersion()); return sdkstats; } /** * Transforms one or more BuildRecords into as many LogRequest.LogEvents as needed, each with their * own timestamp. The wrapper LogRequest has a "now" timestamp. */ private StatsProto.LogRequest getRecordData(@NotNull LegacySdkStatsService sdkstats, @NotNull StudioBuildStatsPersistenceComponent records) { StatsProto.LogRequest.Builder request = StatsProto.LogRequest.newBuilder(); request.setLogSource(StatsProto.LogRequest.LogSource.ANDROID_STUDIO); request.setRequestTimeMs(System.currentTimeMillis()); String uuid = UpdateChecker.getInstallationUID(PropertiesComponent.getInstance()); String appVersion = ApplicationInfo.getInstance().getFullVersion(); request.setClientInfo(createClientInfo(sdkstats, uuid, appVersion)); while (records.hasRecords()) { BuildRecord record = records.getFirstRecord(); if (record == null) { break; } StatsProto.LogEvent.Builder evtBuilder = StatsProto.LogEvent.newBuilder(); evtBuilder.setEventTimeMs(record.getUtcTimestampMs()); evtBuilder.setTag("build"); for (KeyString value : record.getData()) { StatsProto.LogEventKeyValues.Builder kvBuilder = StatsProto.LogEventKeyValues.newBuilder(); kvBuilder.setKey(value.getKey()); kvBuilder.setValue(value.getValue()); evtBuilder.addValue(kvBuilder); } request.addLogEvent(evtBuilder.build()); } return request.build(); } private StatsProto.LogRequest getUsageData(@NotNull LegacySdkStatsService sdkstats, @NotNull Set<String> disabledGroups) { Project[] openProjects = ProjectManager.getInstance().getOpenProjects(); Map<String, KeyString[]> usages = new LinkedHashMap<String, KeyString[]>(); for (Project project : openProjects) { final Map<String, KeyString[]> allUsages = getAllUsages(project, disabledGroups); usages.putAll(allUsages); } String uuid = UpdateChecker.getInstallationUID(PropertiesComponent.getInstance()); String appVersion = ApplicationInfo.getInstance().getFullVersion(); return createRequest(sdkstats, uuid, appVersion, usages); } @NotNull public Map<String, KeyString[]> getAllUsages(@Nullable Project project, @NotNull Set<String> disabledGroups) { Map<String, KeyString[]> allUsages = new LinkedHashMap<String, KeyString[]>(); for (UsagesCollector usagesCollector : Extensions.getExtensions(UsagesCollector.EP_NAME)) { final GroupDescriptor groupDescriptor = usagesCollector.getGroupId(); final String groupId = groupDescriptor.getId(); if (!disabledGroups.contains(groupId)) { try { final Set<UsageDescriptor> usages = usagesCollector.getUsages(project); final Set<Counter> counters = new TreeSet<Counter>(); for (UsageDescriptor usage : usages) { Counter counter = new Counter(usage.getKey(), usage.getValue()); counters.add(counter); } allUsages.put(groupId, counters.toArray(new Counter[counters.size()])); } catch (CollectUsagesException e) { LOG.info(e); } } } return allUsages; } /** * Sends data. Returns an error if something occurred. * * TODO: the server send a reply that tells us how long to wait before sending the next one. * Capture that and report it to the caller. */ @Nullable public String sendData(@NotNull StatsProto.LogRequest request) throws IOException { if (request == null) { return "[SendStats] Invalid arguments"; } String url = "https://play.google.com/log"; byte[] data = request.toByteArray(); HttpURLConnection connection = HttpConfigurable.getInstance().openHttpConnection(url); connection.setConnectTimeout(2000); connection.setReadTimeout(2000); connection.setDoOutput(true); connection.setRequestMethod(HTTP_POST); connection.setRequestProperty(CONTENT_TYPE, PROTOBUF_CONTENT); OutputStream os = connection.getOutputStream(); try { os.write(data); } finally { os.close(); } int code = connection.getResponseCode(); if (code == HTTP_STATUS_OK) { return null; // no error } return "[SendStats] Error " + code; } public StatsProto.LogRequest createRequest(@NotNull LegacySdkStatsService sdkstats, @NotNull String uuid, @NotNull String appVersion, @NotNull Map<String, KeyString[]> usages) { StatsProto.LogRequest.Builder request = StatsProto.LogRequest.newBuilder(); request.setLogSource(StatsProto.LogRequest.LogSource.ANDROID_STUDIO); request.setRequestTimeMs(System.currentTimeMillis()); request.setClientInfo(createClientInfo(sdkstats, uuid, appVersion)); for (Map.Entry<String, KeyString[]> entry : usages.entrySet()) { request.addLogEvent(createEvent(entry.getKey(), entry.getValue())); } request.addLogEvent(createEvent("jvm", new KeyString[] { new KeyString("jvm-info", sdkstats.getJvmInfo()), new KeyString("jvm-vers", sdkstats.getJvmVersion()), new KeyString("jvm-arch", sdkstats.getJvmArch()) } )); return request.build(); } private StatsProto.LogEvent createEvent(@NotNull String groupId, @NotNull KeyString[] values) { StatsProto.LogEvent.Builder evtBuilder = StatsProto.LogEvent.newBuilder(); evtBuilder.setEventTimeMs(System.currentTimeMillis()); evtBuilder.setTag(groupId); for (KeyString value : values) { StatsProto.LogEventKeyValues.Builder kvBuilder = StatsProto.LogEventKeyValues.newBuilder(); kvBuilder.setKey(value.getKey()); kvBuilder.setValue(value.getValue()); evtBuilder.addValue(kvBuilder); } return evtBuilder.build(); } private StatsProto.ClientInfo createClientInfo(@NotNull LegacySdkStatsService sdkstats, @NotNull String uuid, @NotNull String appVersion) { StatsProto.DesktopClientInfo.Builder desktop = StatsProto.DesktopClientInfo.newBuilder(); desktop.setClientId(uuid); OsInfo info = sdkstats.getOsName(); desktop.setOs(info.getOsName()); String os_vers = info.getOsVersion(); if (os_vers != null) { desktop.setOsMajorVersion(os_vers); } desktop.setOsFullVersion(info.getOsFull()); desktop.setApplicationBuild(appVersion); StatsProto.ClientInfo.Builder cinfo = StatsProto.ClientInfo.newBuilder(); cinfo.setClientType(StatsProto.ClientInfo.ClientType.DESKTOP); cinfo.setDesktopClientInfo(desktop); return cinfo.build(); } }