android:permission="android.permission.BIND_JOB_SERVICE" >
</service>
- <service
- android:name="com.android.server.pm.BackgroundDexOptService"
- android:exported="true"
- android:permission="android.permission.BIND_JOB_SERVICE">
+ <service android:name="com.android.server.pm.BackgroundDexOptService"
+ android:exported="true"
+ android:permission="android.permission.BIND_JOB_SERVICE">
+ </service>
+
+ <service android:name="com.android.server.storage.DiskStatsLoggingService"
+ android:permission="android.permission.BIND_JOB_SERVICE" >
</service>
</application>
import android.os.StatFs;
import android.os.SystemClock;
import android.os.storage.StorageManager;
+import android.util.Log;
+
+import com.android.server.storage.DiskStatsFileLogger;
+import com.android.server.storage.DiskStatsLoggingService;
+
+import libcore.io.IoUtils;
+
+import org.json.JSONException;
+import org.json.JSONObject;
import java.io.File;
import java.io.FileDescriptor;
*/
public class DiskStatsService extends Binder {
private static final String TAG = "DiskStatsService";
+ private static final String DISKSTATS_DUMP_FILE = "/data/system/diskstats_cache.json";
private final Context mContext;
public DiskStatsService(Context context) {
mContext = context;
+ DiskStatsLoggingService.schedule(context);
}
@Override
pw.println("File-based Encryption: true");
}
+ if (isCheckin(args)) {
+ reportCachedValues(pw);
+ }
+
// TODO: Read /proc/yaffs and report interesting values;
// add configurable (through args) performance test parameters.
}
return;
}
}
+
+ private boolean isCheckin(String[] args) {
+ for (String opt : args) {
+ if ("--checkin".equals(opt)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void reportCachedValues(PrintWriter pw) {
+ try {
+ String jsonString = IoUtils.readFileAsString(DISKSTATS_DUMP_FILE);
+ JSONObject json = new JSONObject(jsonString);
+ pw.print("App Size: ");
+ pw.println(json.getLong(DiskStatsFileLogger.APP_SIZE_AGG_KEY));
+ pw.print("App Cache Size: ");
+ pw.println(json.getLong(DiskStatsFileLogger.APP_CACHE_AGG_KEY));
+ pw.print("Photos Size: ");
+ pw.println(json.getLong(DiskStatsFileLogger.PHOTOS_KEY));
+ pw.print("Videos Size: ");
+ pw.println(json.getLong(DiskStatsFileLogger.VIDEOS_KEY));
+ pw.print("Audio Size: ");
+ pw.println(json.getLong(DiskStatsFileLogger.AUDIO_KEY));
+ pw.print("Downloads Size: ");
+ pw.println(json.getLong(DiskStatsFileLogger.DOWNLOADS_KEY));
+ pw.print("System Size: ");
+ pw.println(json.getLong(DiskStatsFileLogger.SYSTEM_KEY));
+ pw.print("Other Size: ");
+ pw.println(json.getLong(DiskStatsFileLogger.MISC_KEY));
+ pw.print("Package Names: ");
+ pw.println(json.getJSONArray(DiskStatsFileLogger.PACKAGE_NAMES_KEY));
+ pw.print("App Sizes: ");
+ pw.println(json.getJSONArray(DiskStatsFileLogger.APP_SIZES_KEY));
+ pw.print("Cache Sizes: ");
+ pw.println(json.getJSONArray(DiskStatsFileLogger.APP_CACHES_KEY));
+ } catch (IOException | JSONException e) {
+ Log.w(TAG, "exception reading diskstats cache file", e);
+ }
+ }
+
}
--- /dev/null
+/*
+ * Copyright (C) 2016 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/LICENSE2.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.server.storage;
+
+import android.content.pm.PackageStats;
+import android.os.Environment;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.android.server.storage.FileCollector.MeasurementResult;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.PrintWriter;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * DiskStatsFileLogger logs collected storage information to a file in a JSON format.
+ *
+ * The following information is cached in the file:
+ * 1. Size of images on disk.
+ * 2. Size of videos on disk.
+ * 3. Size of audio on disk.
+ * 4. Size of the downloads folder.
+ * 5. System size.
+ * 6. Aggregate and individual app and app cache sizes.
+ * 7. How much storage couldn't be categorized in one of the above categories.
+ */
+public class DiskStatsFileLogger {
+ private static final String TAG = "DiskStatsLogger";
+
+ public static final String PHOTOS_KEY = "photosSize";
+ public static final String VIDEOS_KEY = "videosSize";
+ public static final String AUDIO_KEY = "audioSize";
+ public static final String DOWNLOADS_KEY = "downloadsSize";
+ public static final String SYSTEM_KEY = "systemSize";
+ public static final String MISC_KEY = "otherSize";
+ public static final String APP_SIZE_AGG_KEY = "appSize";
+ public static final String APP_CACHE_AGG_KEY = "cacheSize";
+ public static final String PACKAGE_NAMES_KEY = "packageNames";
+ public static final String APP_SIZES_KEY = "appSizes";
+ public static final String APP_CACHES_KEY = "cacheSizes";
+ public static final String LAST_QUERY_TIMESTAMP_KEY = "queryTime";
+
+ private MeasurementResult mResult;
+ private long mDownloadsSize;
+ private long mSystemSize;
+ private List<PackageStats> mPackageStats;
+
+ /**
+ * Constructs a DiskStatsFileLogger with calculated measurement results.
+ */
+ public DiskStatsFileLogger(MeasurementResult result, MeasurementResult downloadsResult,
+ List<PackageStats> stats, long systemSize) {
+ mResult = result;
+ mDownloadsSize = downloadsResult.totalAccountedSize();
+ mSystemSize = systemSize;
+ mPackageStats = stats;
+ }
+
+ /**
+ * Dumps the storage collection output to a file.
+ * @param file File to write the output into.
+ * @throws FileNotFoundException
+ */
+ public void dumpToFile(File file) throws FileNotFoundException {
+ PrintWriter pw = new PrintWriter(file);
+ JSONObject representation = getJsonRepresentation();
+ if (representation != null) {
+ pw.println(representation);
+ }
+ pw.close();
+ }
+
+ private JSONObject getJsonRepresentation() {
+ JSONObject json = new JSONObject();
+ try {
+ json.put(LAST_QUERY_TIMESTAMP_KEY, System.currentTimeMillis());
+ json.put(PHOTOS_KEY, mResult.imagesSize);
+ json.put(VIDEOS_KEY, mResult.videosSize);
+ json.put(AUDIO_KEY, mResult.audioSize);
+ json.put(DOWNLOADS_KEY, mDownloadsSize);
+ json.put(SYSTEM_KEY, mSystemSize);
+ json.put(MISC_KEY, mResult.miscSize);
+ addAppsToJson(json);
+ } catch (JSONException e) {
+ Log.e(TAG, e.toString());
+ return null;
+ }
+
+ return json;
+ }
+
+ private void addAppsToJson(JSONObject json) throws JSONException {
+ JSONArray names = new JSONArray();
+ JSONArray appSizeList = new JSONArray();
+ JSONArray cacheSizeList = new JSONArray();
+
+ long appSizeSum = 0L;
+ long cacheSizeSum = 0L;
+ boolean isExternal = Environment.isExternalStorageEmulated();
+ for (Map.Entry<String, PackageStats> entry : mergePackagesAcrossUsers().entrySet()) {
+ PackageStats stat = entry.getValue();
+ long appSize = stat.codeSize + stat.dataSize;
+ long cacheSize = stat.cacheSize;
+ if (isExternal) {
+ appSize += stat.externalCodeSize + stat.externalDataSize;
+ cacheSize += stat.externalCacheSize;
+ }
+ appSizeSum += appSize;
+ cacheSizeSum += cacheSize;
+
+ names.put(stat.packageName);
+ appSizeList.put(appSize);
+ cacheSizeList.put(cacheSize);
+ }
+ json.put(PACKAGE_NAMES_KEY, names);
+ json.put(APP_SIZES_KEY, appSizeList);
+ json.put(APP_CACHES_KEY, cacheSizeList);
+ json.put(APP_SIZE_AGG_KEY, appSizeSum);
+ json.put(APP_CACHE_AGG_KEY, cacheSizeSum);
+ }
+
+ /**
+ * A given package may exist for multiple users with distinct sizes. This function merges
+ * the duplicated packages together and sums up their sizes to get the actual totals for the
+ * package.
+ * @return A mapping of package name to merged package stats.
+ */
+ private ArrayMap<String, PackageStats> mergePackagesAcrossUsers() {
+ ArrayMap<String, PackageStats> packageMap = new ArrayMap<>();
+ for (PackageStats stat : mPackageStats) {
+ PackageStats existingStats = packageMap.get(stat.packageName);
+ if (existingStats != null) {
+ existingStats.cacheSize += stat.cacheSize;
+ existingStats.codeSize += stat.codeSize;
+ existingStats.dataSize += stat.dataSize;
+ existingStats.externalCacheSize += stat.externalCacheSize;
+ existingStats.externalCodeSize += stat.externalCodeSize;
+ existingStats.externalDataSize += stat.externalDataSize;
+ } else {
+ packageMap.put(stat.packageName, new PackageStats(stat));
+ }
+ }
+ return packageMap;
+ }
+}
\ No newline at end of file
--- /dev/null
+/*
+ * Copyright (C) 2016 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/LICENSE2.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.server.storage;
+
+import android.app.job.JobInfo;
+import android.app.job.JobParameters;
+import android.app.job.JobScheduler;
+import android.app.job.JobService;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.PackageStats;
+import android.os.AsyncTask;
+import android.os.BatteryManager;
+import android.os.Environment;
+import android.os.Environment.UserEnvironment;
+import android.os.UserHandle;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.storage.FileCollector.MeasurementResult;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * DiskStatsLoggingService is a JobService which collects storage categorization information and
+ * app size information on a roughly daily cadence.
+ */
+public class DiskStatsLoggingService extends JobService {
+ private static final String TAG = "DiskStatsLogService";
+ public static final String DUMPSYS_CACHE_PATH = "/data/system/diskstats_cache.json";
+ private static final int JOB_DISKSTATS_LOGGING = 0x4449534b; // DISK
+ private static ComponentName sDiskStatsLoggingService = new ComponentName(
+ "android",
+ DiskStatsLoggingService.class.getName());
+
+ @Override
+ public boolean onStartJob(JobParameters params) {
+ // We need to check the preconditions again because they may not be enforced for
+ // subsequent runs.
+ if (!isCharging(this)) {
+ jobFinished(params, true);
+ return false;
+ }
+
+ final int userId = UserHandle.myUserId();
+ UserEnvironment environment = new UserEnvironment(userId);
+ AppCollector collector = new AppCollector(this,
+ getPackageManager().getPrimaryStorageCurrentVolume());
+ LogRunnable task = new LogRunnable();
+ task.setRootDirectory(environment.getExternalStorageDirectory());
+ task.setDownloadsDirectory(
+ environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS));
+ task.setSystemSize(FileCollector.getSystemSize(this));
+ task.setLogOutputFile(new File(DUMPSYS_CACHE_PATH));
+ task.setAppCollector(collector);
+ task.setJobService(this, params);
+ AsyncTask.execute(task);
+ return true;
+ }
+
+ @Override
+ public boolean onStopJob(JobParameters params) {
+ // TODO: Try to stop being handled.
+ return false;
+ }
+
+ /**
+ * Schedules a DiskStats collection task. This task only runs on device idle while charging
+ * once every 24 hours.
+ * @param context Context to use to get a job scheduler.
+ */
+ public static void schedule(Context context) {
+ JobScheduler js = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
+
+ js.schedule(new JobInfo.Builder(JOB_DISKSTATS_LOGGING, sDiskStatsLoggingService)
+ .setRequiresDeviceIdle(true)
+ .setRequiresCharging(true)
+ .setPeriodic(TimeUnit.DAYS.toMillis(1))
+ .build());
+ }
+
+ private static boolean isCharging(Context context) {
+ BatteryManager batteryManager = context.getSystemService(BatteryManager.class);
+ if (batteryManager != null) {
+ return batteryManager.isCharging();
+ }
+ return false;
+ }
+
+ @VisibleForTesting
+ static class LogRunnable implements Runnable {
+ private static final long TIMEOUT_MILLIS = TimeUnit.MINUTES.toMillis(5);
+
+ private JobService mJobService;
+ private JobParameters mParams;
+ private AppCollector mCollector;
+ private File mOutputFile;
+ private File mRootDirectory;
+ private File mDownloadsDirectory;
+ private long mSystemSize;
+
+ public void setRootDirectory(File file) {
+ mRootDirectory = file;
+ }
+
+ public void setDownloadsDirectory(File file) {
+ mDownloadsDirectory = file;
+ }
+
+ public void setAppCollector(AppCollector collector) {
+ mCollector = collector;
+ }
+
+ public void setLogOutputFile(File file) {
+ mOutputFile = file;
+ }
+
+ public void setSystemSize(long size) {
+ mSystemSize = size;
+ }
+
+ public void setJobService(JobService jobService, JobParameters params) {
+ mJobService = jobService;
+ mParams = params;
+ }
+
+ public void run() {
+ FileCollector.MeasurementResult mainCategories =
+ FileCollector.getMeasurementResult(mRootDirectory);
+ FileCollector.MeasurementResult downloads =
+ FileCollector.getMeasurementResult(mDownloadsDirectory);
+
+ logToFile(mainCategories, downloads, mCollector.getPackageStats(TIMEOUT_MILLIS),
+ mSystemSize);
+
+ if (mJobService != null) {
+ mJobService.jobFinished(mParams, false);
+ }
+ }
+
+ private void logToFile(MeasurementResult mainCategories, MeasurementResult downloads,
+ List<PackageStats> stats, long systemSize) {
+ DiskStatsFileLogger logger = new DiskStatsFileLogger(mainCategories, downloads, stats,
+ systemSize);
+ try {
+ mOutputFile.createNewFile();
+ logger.dumpToFile(mOutputFile);
+ } catch (IOException e) {
+ Log.e(TAG, "Exception while writing opportunistic disk file cache.", e);
+ }
+ }
+ }
+}
\ No newline at end of file
package com.android.server.storage;
import android.annotation.IntDef;
+import android.content.Context;
+import android.content.pm.PackageManager;
import android.os.storage.StorageManager;
+import android.os.storage.VolumeInfo;
import android.util.ArrayMap;
import java.io.File;
new MeasurementResult());
}
+ /**
+ * Returns the size of a system for a given context. This is done by finding the difference
+ * between the shared data and the total primary storage size.
+ * @param context Context to use to get storage information.
+ */
+ public static long getSystemSize(Context context) {
+ PackageManager pm = context.getPackageManager();
+ VolumeInfo primaryVolume = pm.getPrimaryStorageCurrentVolume();
+
+ StorageManager sm = context.getSystemService(StorageManager.class);
+ VolumeInfo shared = sm.findEmulatedForPrivate(primaryVolume);
+ if (shared == null) {
+ return 0;
+ }
+
+ final long sharedDataSize = shared.getPath().getTotalSpace();
+ long systemSize = sm.getPrimaryStorageSize() - sharedDataSize;
+
+ // This case is not exceptional -- we just fallback to the shared data volume in this case.
+ if (systemSize <= 0) {
+ return 0;
+ }
+
+ return systemSize;
+ }
+
private static MeasurementResult collectFiles(File file, MeasurementResult result) {
File[] files = file.listFiles();
--- /dev/null
+/*
+ * Copyright (C) 2016 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.server.storage;
+
+import android.content.pm.PackageStats;
+import android.test.AndroidTestCase;
+import android.util.ArraySet;
+import libcore.io.IoUtils;
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.File;
+import java.util.ArrayList;
+
+import static com.google.common.truth.Truth.assertThat;
+
+@RunWith(JUnit4.class)
+public class DiskStatsFileLoggerTest extends AndroidTestCase {
+ @Rule public TemporaryFolder temporaryFolder;
+ public FileCollector.MeasurementResult mMainResult;
+ public FileCollector.MeasurementResult mDownloadsResult;
+ private ArrayList<PackageStats> mPackages;
+ private File mOutputFile;
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ temporaryFolder = new TemporaryFolder();
+ temporaryFolder.create();
+ mOutputFile = temporaryFolder.newFile();
+ mMainResult = new FileCollector.MeasurementResult();
+ mDownloadsResult = new FileCollector.MeasurementResult();
+ mPackages = new ArrayList<>();
+ }
+
+ @Test
+ public void testEmptyStorage() throws Exception {
+ DiskStatsFileLogger logger = new DiskStatsFileLogger(
+ mMainResult, mDownloadsResult,mPackages, 0L);
+
+ logger.dumpToFile(mOutputFile);
+
+ JSONObject output = getOutputFileAsJson();
+ assertThat(output.getLong(DiskStatsFileLogger.PHOTOS_KEY)).isEqualTo(0L);
+ assertThat(output.getLong(DiskStatsFileLogger.VIDEOS_KEY)).isEqualTo(0L);
+ assertThat(output.getLong(DiskStatsFileLogger.AUDIO_KEY)).isEqualTo(0L);
+ assertThat(output.getLong(DiskStatsFileLogger.DOWNLOADS_KEY)).isEqualTo(0L);
+ assertThat(output.getLong(DiskStatsFileLogger.SYSTEM_KEY)).isEqualTo(0L);
+ assertThat(output.getLong(DiskStatsFileLogger.MISC_KEY)).isEqualTo(0L);
+ assertThat(output.getLong(DiskStatsFileLogger.APP_SIZE_AGG_KEY)).isEqualTo(0L);
+ assertThat(output.getLong(DiskStatsFileLogger.APP_CACHE_AGG_KEY)).isEqualTo(0L);
+ assertThat(
+ output.getJSONArray(DiskStatsFileLogger.PACKAGE_NAMES_KEY).length()).isEqualTo(0L);
+ assertThat(output.getJSONArray(DiskStatsFileLogger.APP_SIZES_KEY).length()).isEqualTo(0L);
+ assertThat(output.getJSONArray(DiskStatsFileLogger.APP_CACHES_KEY).length()).isEqualTo(0L);
+ }
+
+ @Test
+ public void testMeasurementResultsReported() throws Exception {
+ mMainResult.audioSize = 1;
+ mMainResult.imagesSize = 10;
+ mMainResult.miscSize = 100;
+ mDownloadsResult.miscSize = 1000;
+ DiskStatsFileLogger logger = new DiskStatsFileLogger(
+ mMainResult, mDownloadsResult,mPackages, 3L);
+
+ logger.dumpToFile(mOutputFile);
+
+ JSONObject output = getOutputFileAsJson();
+ assertThat(output.getLong(DiskStatsFileLogger.AUDIO_KEY)).isEqualTo(1L);
+ assertThat(output.getLong(DiskStatsFileLogger.PHOTOS_KEY)).isEqualTo(10L);
+ assertThat(output.getLong(DiskStatsFileLogger.MISC_KEY)).isEqualTo(100L);
+ assertThat(output.getLong(DiskStatsFileLogger.DOWNLOADS_KEY)).isEqualTo(1000L);
+ assertThat(output.getLong(DiskStatsFileLogger.SYSTEM_KEY)).isEqualTo(3L);
+ }
+
+ @Test
+ public void testAppsReported() throws Exception {
+ PackageStats firstPackage = new PackageStats("com.test.app");
+ firstPackage.codeSize = 100;
+ firstPackage.dataSize = 1000;
+ firstPackage.cacheSize = 20;
+ mPackages.add(firstPackage);
+
+ PackageStats secondPackage = new PackageStats("com.test.app2");
+ secondPackage.codeSize = 10;
+ secondPackage.dataSize = 1;
+ secondPackage.cacheSize = 2;
+ mPackages.add(secondPackage);
+
+ DiskStatsFileLogger logger = new DiskStatsFileLogger(
+ mMainResult, mDownloadsResult, mPackages, 0L);
+ logger.dumpToFile(mOutputFile);
+
+ JSONObject output = getOutputFileAsJson();
+ assertThat(output.getLong(DiskStatsFileLogger.APP_SIZE_AGG_KEY)).isEqualTo(1111);
+ assertThat(output.getLong(DiskStatsFileLogger.APP_CACHE_AGG_KEY)).isEqualTo(22);
+
+ JSONArray packageNames = output.getJSONArray(DiskStatsFileLogger.PACKAGE_NAMES_KEY);
+ assertThat(packageNames.length()).isEqualTo(2);
+ JSONArray appSizes = output.getJSONArray(DiskStatsFileLogger.APP_SIZES_KEY);
+ assertThat(appSizes.length()).isEqualTo(2);
+ JSONArray cacheSizes = output.getJSONArray(DiskStatsFileLogger.APP_CACHES_KEY);
+ assertThat(cacheSizes.length()).isEqualTo(2);
+
+ // We need to do this crazy Set over this because the DiskStatsFileLogger provides no
+ // guarantee of the ordering of the apps in its output. By using a set, we avoid any order
+ // problems.
+ ArraySet<AppSizeGrouping> apps = new ArraySet<>();
+ for (int i = 0; i < packageNames.length(); i++) {
+ AppSizeGrouping app = new AppSizeGrouping(packageNames.getString(i),
+ appSizes.getLong(i), cacheSizes.getLong(i));
+ apps.add(app);
+ }
+ assertThat(apps).containsAllOf(new AppSizeGrouping("com.test.app", 1100, 20),
+ new AppSizeGrouping("com.test.app2", 11, 2));
+ }
+
+ @Test
+ public void testEmulatedExternalStorageCounted() throws Exception {
+ PackageStats app = new PackageStats("com.test.app");
+ app.dataSize = 1000;
+ app.externalDataSize = 1000;
+ app.cacheSize = 20;
+ mPackages.add(app);
+
+ DiskStatsFileLogger logger = new DiskStatsFileLogger(
+ mMainResult, mDownloadsResult, mPackages, 0L);
+ logger.dumpToFile(mOutputFile);
+
+ JSONObject output = getOutputFileAsJson();
+ JSONArray appSizes = output.getJSONArray(DiskStatsFileLogger.APP_SIZES_KEY);
+ assertThat(appSizes.length()).isEqualTo(1);
+ assertThat(appSizes.getLong(0)).isEqualTo(2000);
+ }
+
+ @Test
+ public void testDuplicatePackageNameIsMergedAcrossMultipleUsers() throws Exception {
+ PackageStats app = new PackageStats("com.test.app");
+ app.dataSize = 1000;
+ app.externalDataSize = 1000;
+ app.cacheSize = 20;
+ app.userHandle = 0;
+ mPackages.add(app);
+
+ PackageStats secondApp = new PackageStats("com.test.app");
+ secondApp.dataSize = 100;
+ secondApp.externalDataSize = 100;
+ secondApp.cacheSize = 2;
+ secondApp.userHandle = 1;
+ mPackages.add(secondApp);
+
+ DiskStatsFileLogger logger = new DiskStatsFileLogger(
+ mMainResult, mDownloadsResult, mPackages, 0L);
+ logger.dumpToFile(mOutputFile);
+
+ JSONObject output = getOutputFileAsJson();
+ assertThat(output.getLong(DiskStatsFileLogger.APP_SIZE_AGG_KEY)).isEqualTo(2200);
+ assertThat(output.getLong(DiskStatsFileLogger.APP_CACHE_AGG_KEY)).isEqualTo(22);
+ JSONArray packageNames = output.getJSONArray(DiskStatsFileLogger.PACKAGE_NAMES_KEY);
+ assertThat(packageNames.length()).isEqualTo(1);
+ assertThat(packageNames.getString(0)).isEqualTo("com.test.app");
+
+ JSONArray appSizes = output.getJSONArray(DiskStatsFileLogger.APP_SIZES_KEY);
+ assertThat(appSizes.length()).isEqualTo(1);
+ assertThat(appSizes.getLong(0)).isEqualTo(2200);
+
+ JSONArray cacheSizes = output.getJSONArray(DiskStatsFileLogger.APP_CACHES_KEY);
+ assertThat(cacheSizes.length()).isEqualTo(1);
+ assertThat(cacheSizes.getLong(0)).isEqualTo(22);
+ }
+
+ private JSONObject getOutputFileAsJson() throws Exception {
+ return new JSONObject(IoUtils.readFileAsString(mOutputFile.getAbsolutePath()));
+ }
+
+ /**
+ * This class exists for putting zipped app size information arrays into a set for comparison
+ * purposes.
+ */
+ private class AppSizeGrouping {
+ public String packageName;
+ public long appSize;
+ public long cacheSize;
+
+ public AppSizeGrouping(String packageName, long appSize, long cacheSize) {
+ this.packageName = packageName;
+ this.appSize = appSize;
+ this.cacheSize = cacheSize;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result = 37 * result + (int)(appSize ^ (appSize >>> 32));
+ result = 37 * result + (int)(cacheSize ^ (cacheSize >>> 32));
+ result = 37 * result + packageName.hashCode();
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof AppSizeGrouping)) {
+ return false;
+ }
+ if (this == o) {
+ return true;
+ }
+ AppSizeGrouping grouping = (AppSizeGrouping) o;
+ return packageName.equals(grouping.packageName) && appSize == grouping.appSize &&
+ cacheSize == grouping.cacheSize;
+ }
+
+ @Override
+ public String toString() {
+ return packageName + " " + appSize + " " + cacheSize;
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+/*
+ * Copyright (C) 2016 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.server.storage;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Mockito.when;
+
+import android.content.pm.PackageStats;
+import android.test.AndroidTestCase;
+
+import com.android.server.storage.DiskStatsLoggingService.LogRunnable;
+
+import libcore.io.IoUtils;
+
+import org.json.JSONObject;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.File;
+import java.io.PrintStream;
+import java.util.ArrayList;
+
+@RunWith(JUnit4.class)
+public class DiskStatsLoggingServiceTest extends AndroidTestCase {
+ @Rule public TemporaryFolder mTemporaryFolder;
+ @Rule public TemporaryFolder mDownloads;
+ @Rule public TemporaryFolder mRootDirectory;
+ @Mock private AppCollector mCollector;
+ private File mInputFile;
+
+
+ @Before
+ public void setUp() throws Exception {
+ super.setUp();
+ MockitoAnnotations.initMocks(this);
+ mTemporaryFolder = new TemporaryFolder();
+ mTemporaryFolder.create();
+ mInputFile = mTemporaryFolder.newFile();
+ mDownloads = new TemporaryFolder();
+ mDownloads.create();
+ mRootDirectory = new TemporaryFolder();
+ mRootDirectory.create();
+ }
+
+ @Test
+ public void testEmptyLog() throws Exception {
+ LogRunnable task = new LogRunnable();
+ task.setAppCollector(mCollector);
+ task.setDownloadsDirectory(mDownloads.getRoot());
+ task.setRootDirectory(mRootDirectory.getRoot());
+ task.setLogOutputFile(mInputFile);
+ task.setSystemSize(0L);
+ task.run();
+
+ JSONObject json = getJsonOutput();
+ assertThat(json.getLong(DiskStatsFileLogger.PHOTOS_KEY)).isEqualTo(0L);
+ assertThat(json.getLong(DiskStatsFileLogger.VIDEOS_KEY)).isEqualTo(0L);
+ assertThat(json.getLong(DiskStatsFileLogger.AUDIO_KEY)).isEqualTo(0L);
+ assertThat(json.getLong(DiskStatsFileLogger.DOWNLOADS_KEY)).isEqualTo(0L);
+ assertThat(json.getLong(DiskStatsFileLogger.SYSTEM_KEY)).isEqualTo(0L);
+ assertThat(json.getLong(DiskStatsFileLogger.MISC_KEY)).isEqualTo(0L);
+ assertThat(json.getLong(DiskStatsFileLogger.APP_SIZE_AGG_KEY)).isEqualTo(0L);
+ assertThat(json.getLong(DiskStatsFileLogger.APP_CACHE_AGG_KEY)).isEqualTo(0L);
+ assertThat(
+ json.getJSONArray(DiskStatsFileLogger.PACKAGE_NAMES_KEY).length()).isEqualTo(0L);
+ assertThat(json.getJSONArray(DiskStatsFileLogger.APP_SIZES_KEY).length()).isEqualTo(0L);
+ assertThat(json.getJSONArray(DiskStatsFileLogger.APP_CACHES_KEY).length()).isEqualTo(0L);
+ }
+
+ @Test
+ public void testPopulatedLogTask() throws Exception {
+ // Write data to directories.
+ writeDataToFile(mDownloads.newFile(), "lol");
+ writeDataToFile(mRootDirectory.newFile("test.jpg"), "1234");
+ writeDataToFile(mRootDirectory.newFile("test.mp4"), "12345");
+ writeDataToFile(mRootDirectory.newFile("test.mp3"), "123456");
+ writeDataToFile(mRootDirectory.newFile("test.whatever"), "1234567");
+
+ // Write apps.
+ ArrayList<PackageStats> apps = new ArrayList<>();
+ PackageStats testApp = new PackageStats("com.test.app");
+ testApp.dataSize = 5L;
+ testApp.cacheSize = 55L;
+ testApp.codeSize = 10L;
+ apps.add(testApp);
+ when(mCollector.getPackageStats(anyInt())).thenReturn(apps);
+
+ LogRunnable task = new LogRunnable();
+ task.setAppCollector(mCollector);
+ task.setDownloadsDirectory(mDownloads.getRoot());
+ task.setRootDirectory(mRootDirectory.getRoot());
+ task.setLogOutputFile(mInputFile);
+ task.setSystemSize(10L);
+ task.run();
+
+ JSONObject json = getJsonOutput();
+ assertThat(json.getLong(DiskStatsFileLogger.PHOTOS_KEY)).isEqualTo(4L);
+ assertThat(json.getLong(DiskStatsFileLogger.VIDEOS_KEY)).isEqualTo(5L);
+ assertThat(json.getLong(DiskStatsFileLogger.AUDIO_KEY)).isEqualTo(6L);
+ assertThat(json.getLong(DiskStatsFileLogger.DOWNLOADS_KEY)).isEqualTo(3L);
+ assertThat(json.getLong(DiskStatsFileLogger.SYSTEM_KEY)).isEqualTo(10L);
+ assertThat(json.getLong(DiskStatsFileLogger.MISC_KEY)).isEqualTo(7L);
+ assertThat(json.getLong(DiskStatsFileLogger.APP_SIZE_AGG_KEY)).isEqualTo(15L);
+ assertThat(json.getLong(DiskStatsFileLogger.APP_CACHE_AGG_KEY)).isEqualTo(55L);
+ assertThat(
+ json.getJSONArray(DiskStatsFileLogger.PACKAGE_NAMES_KEY).length()).isEqualTo(1L);
+ assertThat(json.getJSONArray(DiskStatsFileLogger.APP_SIZES_KEY).length()).isEqualTo(1L);
+ assertThat(json.getJSONArray(DiskStatsFileLogger.APP_CACHES_KEY).length()).isEqualTo(1L);
+ }
+
+ private void writeDataToFile(File f, String data) throws Exception{
+ PrintStream out = new PrintStream(f);
+ out.print(data);
+ out.close();
+ }
+
+ private JSONObject getJsonOutput() throws Exception {
+ return new JSONObject(IoUtils.readFileAsString(mInputFile.getAbsolutePath()));
+ }
+}