/* * Copyright (C) 2014 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.VisibleForTesting; import com.intellij.internal.statistic.connect.StatisticsResult; import com.intellij.openapi.application.Application; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.components.*; import com.intellij.util.concurrency.SequentialTaskExecutor; import org.jdom.Element; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.ide.PooledThreadExecutor; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; /** * Stores "Android Build Statistics" records. * This is a quick throw-away implementation. * * A build record is composed of: * - an UTC epoch long timestamp (System.currentTimeMillis), when the even occurred. * - a list of key/value pairs (string => string). * * This component collects the build records. It merely acts as a synchronized * persistent storage for the records with a maximum upper bound. * * The {@link AndroidStatisticsService} is responsible for actually sending the * records to the statistic collection server based on the current user settings * (i.e. whether stats are enabled and at which frequency they are sent.. this is * not controlled here.) */ @State( name = "StudioBuildStatistic", storages = { @Storage(file = StoragePathMacros.APP_CONFIG + "/studio.build.statistics.xml", roamingType = RoamingType.DISABLED) } ) public class StudioBuildStatsPersistenceComponent implements ApplicationComponent, PersistentStateComponent<Element> { private static final int MAX_RECORDS = 1000; private static final String TAG_RECORD = "record"; private static final String TAG_VALUE = "value"; private static final String ATTR_UTC_MS = "utc_ms"; private static final String ATTR_KEY = "key"; private static final String ATTR_VALUE = "value"; private final LinkedList<BuildRecord> myRecords = new LinkedList<BuildRecord>(); private final SequentialTaskExecutor myTaskExecutor = new SequentialTaskExecutor(PooledThreadExecutor.INSTANCE); /** * Retrieves an instance of the component or null if not available or configured. * * @return an instance of the component or null. */ @Nullable public static StudioBuildStatsPersistenceComponent getInstance() { Application app = ApplicationManager.getApplication(); return app == null ? null : app.getComponent(StudioBuildStatsPersistenceComponent.class); } /** * Adds one build record. * <p/> * This checks the {@link AndroidStatisticsService}, if available: * this does nothing if stats have not been authorized to be collected. * * @param newRecord to be added to the internal queue. */ public void addBuildRecord(@NotNull final BuildRecord newRecord) { myTaskExecutor.execute(new Runnable() { @Override public void run() { addBuildRecordImmediately(newRecord); } }); } @VisibleForTesting void addBuildRecordImmediately(@NotNull BuildRecord newRecord) { // Skip if there is no Application, allowing this to run using non-idea unit tests. Application app = ApplicationManager.getApplication(); if (app != null && !app.isUnitTestMode()) { StatisticsResult code = AndroidStatisticsService.areStatisticsAuthorized(); if (code.getCode() != StatisticsResult.ResultCode.SEND) { // Don't even collect the stats. return; } } synchronized (myRecords) { myRecords.add(newRecord); // Limit the size of the queue to something reasonable. while (myRecords.size() > MAX_RECORDS) { myRecords.removeFirst(); } } } public StudioBuildStatsPersistenceComponent() { // nop } // Visibility set to package-default for testing. Clients should use getFirstRecord(). @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) @NotNull LinkedList<BuildRecord> getRecords() { return myRecords; } /** * Returns the first record or null if there are no records. */ @Nullable BuildRecord getFirstRecord() { synchronized (myRecords) { if (!myRecords.isEmpty()) { return myRecords.removeFirst(); } return null; } } /** * Returns true if the record list is not empty. * When returning true, the next #getFirstRecord() should not return null. */ boolean hasRecords() { synchronized (myRecords) { return !myRecords.isEmpty(); } } // --- ApplicationComponent implementation @Override public void initComponent() { // nop } @Override public void disposeComponent() { // nop } @NotNull @Override public String getComponentName() { return "StudioBuildStatsPersistenceComponent"; } // --- PersistentStateComponent implementation @Override public void loadState(@NotNull Element state) { synchronized (myRecords) { for (Object record : state.getChildren(TAG_RECORD)) { Element recordElement = (Element) record; long timestampMs = Long.valueOf(recordElement.getAttributeValue(ATTR_UTC_MS)); List<KeyString> data = new ArrayList<KeyString>(); for (Object kv : recordElement.getChildren(TAG_VALUE)) { Element valueElement = (Element) kv; data.add(new KeyString(valueElement.getAttributeValue(ATTR_KEY), valueElement.getAttributeValue(ATTR_VALUE))); } myRecords.add(new BuildRecord(timestampMs, data)); } } } @Nullable @Override public Element getState() { Element element = new Element("state"); synchronized (myRecords) { for (BuildRecord record : myRecords) { KeyString[] data = record.getData(); if (data.length == 0) { continue; } Element recordElement = new Element(TAG_RECORD); recordElement.setAttribute(ATTR_UTC_MS, Long.toString(record.getUtcTimestampMs())); for (KeyString kv : data) { Element valueElement = new Element(TAG_VALUE); valueElement.setAttribute(ATTR_KEY, kv.getKey()); valueElement.setAttribute(ATTR_VALUE, kv.getValue()); recordElement.addContent(valueElement); } element.addContent(recordElement); } } return element; } }