Loading core/res/AndroidManifest.xml +7 −4 Original line number Diff line number Diff line Loading @@ -3436,12 +3436,15 @@ android:permission="android.permission.BIND_JOB_SERVICE" > </service> <service android:name="com.android.server.pm.BackgroundDexOptService" <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> </manifest> services/core/java/com/android/server/DiskStatsService.java +56 −0 Original line number Diff line number Diff line Loading @@ -22,6 +22,15 @@ import android.os.Environment; 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; Loading @@ -35,11 +44,13 @@ import java.io.PrintWriter; */ 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 Loading Loading @@ -84,6 +95,10 @@ public class DiskStatsService extends Binder { 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. } Loading Loading @@ -114,4 +129,45 @@ public class DiskStatsService extends Binder { 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); } } } services/core/java/com/android/server/storage/DiskStatsFileLogger.java 0 → 100644 +166 −0 Original line number Diff line number Diff line /* * 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 services/core/java/com/android/server/storage/DiskStatsLoggingService.java 0 → 100644 +170 −0 Original line number Diff line number Diff line /* * 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 services/core/java/com/android/server/storage/FileCollector.java 0 → 100644 +247 −0 Original line number Diff line number Diff line /* * 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.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; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Map; /** * FileCollector walks over a directory and categorizes storage usage by their type. */ public class FileCollector { private static final int UNRECOGNIZED = -1; private static final int IMAGES = 0; private static final int VIDEO = 1; private static final int AUDIO = 2; @Retention(RetentionPolicy.SOURCE) @IntDef({ UNRECOGNIZED, IMAGES, VIDEO, AUDIO }) private @interface FileTypes {} private static final Map<String, Integer> EXTENSION_MAP = new ArrayMap<String, Integer>(); static { // Audio EXTENSION_MAP.put("aac", AUDIO); EXTENSION_MAP.put("amr", AUDIO); EXTENSION_MAP.put("awb", AUDIO); EXTENSION_MAP.put("snd", AUDIO); EXTENSION_MAP.put("flac", AUDIO); EXTENSION_MAP.put("mp3", AUDIO); EXTENSION_MAP.put("mpga", AUDIO); EXTENSION_MAP.put("mpega", AUDIO); EXTENSION_MAP.put("mp2", AUDIO); EXTENSION_MAP.put("m4a", AUDIO); EXTENSION_MAP.put("aif", AUDIO); EXTENSION_MAP.put("aiff", AUDIO); EXTENSION_MAP.put("aifc", AUDIO); EXTENSION_MAP.put("gsm", AUDIO); EXTENSION_MAP.put("mka", AUDIO); EXTENSION_MAP.put("m3u", AUDIO); EXTENSION_MAP.put("wma", AUDIO); EXTENSION_MAP.put("wax", AUDIO); EXTENSION_MAP.put("ra", AUDIO); EXTENSION_MAP.put("rm", AUDIO); EXTENSION_MAP.put("ram", AUDIO); EXTENSION_MAP.put("pls", AUDIO); EXTENSION_MAP.put("sd2", AUDIO); EXTENSION_MAP.put("wav", AUDIO); EXTENSION_MAP.put("ogg", AUDIO); EXTENSION_MAP.put("oga", AUDIO); // Video EXTENSION_MAP.put("3gpp", VIDEO); EXTENSION_MAP.put("3gp", VIDEO); EXTENSION_MAP.put("3gpp2", VIDEO); EXTENSION_MAP.put("3g2", VIDEO); EXTENSION_MAP.put("avi", VIDEO); EXTENSION_MAP.put("dl", VIDEO); EXTENSION_MAP.put("dif", VIDEO); EXTENSION_MAP.put("dv", VIDEO); EXTENSION_MAP.put("fli", VIDEO); EXTENSION_MAP.put("m4v", VIDEO); EXTENSION_MAP.put("ts", VIDEO); EXTENSION_MAP.put("mpeg", VIDEO); EXTENSION_MAP.put("mpg", VIDEO); EXTENSION_MAP.put("mpe", VIDEO); EXTENSION_MAP.put("mp4", VIDEO); EXTENSION_MAP.put("vob", VIDEO); EXTENSION_MAP.put("qt", VIDEO); EXTENSION_MAP.put("mov", VIDEO); EXTENSION_MAP.put("mxu", VIDEO); EXTENSION_MAP.put("webm", VIDEO); EXTENSION_MAP.put("lsf", VIDEO); EXTENSION_MAP.put("lsx", VIDEO); EXTENSION_MAP.put("mkv", VIDEO); EXTENSION_MAP.put("mng", VIDEO); EXTENSION_MAP.put("asf", VIDEO); EXTENSION_MAP.put("asx", VIDEO); EXTENSION_MAP.put("wm", VIDEO); EXTENSION_MAP.put("wmv", VIDEO); EXTENSION_MAP.put("wmx", VIDEO); EXTENSION_MAP.put("wvx", VIDEO); EXTENSION_MAP.put("movie", VIDEO); EXTENSION_MAP.put("wrf", VIDEO); // Images EXTENSION_MAP.put("bmp", IMAGES); EXTENSION_MAP.put("gif", IMAGES); EXTENSION_MAP.put("jpg", IMAGES); EXTENSION_MAP.put("jpeg", IMAGES); EXTENSION_MAP.put("jpe", IMAGES); EXTENSION_MAP.put("pcx", IMAGES); EXTENSION_MAP.put("png", IMAGES); EXTENSION_MAP.put("svg", IMAGES); EXTENSION_MAP.put("svgz", IMAGES); EXTENSION_MAP.put("tiff", IMAGES); EXTENSION_MAP.put("tif", IMAGES); EXTENSION_MAP.put("wbmp", IMAGES); EXTENSION_MAP.put("webp", IMAGES); EXTENSION_MAP.put("dng", IMAGES); EXTENSION_MAP.put("cr2", IMAGES); EXTENSION_MAP.put("ras", IMAGES); EXTENSION_MAP.put("art", IMAGES); EXTENSION_MAP.put("jng", IMAGES); EXTENSION_MAP.put("nef", IMAGES); EXTENSION_MAP.put("nrw", IMAGES); EXTENSION_MAP.put("orf", IMAGES); EXTENSION_MAP.put("rw2", IMAGES); EXTENSION_MAP.put("pef", IMAGES); EXTENSION_MAP.put("psd", IMAGES); EXTENSION_MAP.put("pnm", IMAGES); EXTENSION_MAP.put("pbm", IMAGES); EXTENSION_MAP.put("pgm", IMAGES); EXTENSION_MAP.put("ppm", IMAGES); EXTENSION_MAP.put("srw", IMAGES); EXTENSION_MAP.put("arw", IMAGES); EXTENSION_MAP.put("rgb", IMAGES); EXTENSION_MAP.put("xbm", IMAGES); EXTENSION_MAP.put("xpm", IMAGES); EXTENSION_MAP.put("xwd", IMAGES); } /** * Returns the file categorization measurement result. * @param path Directory to collect and categorize storage in. */ public static MeasurementResult getMeasurementResult(File path) { return collectFiles(StorageManager.maybeTranslateEmulatedPathToInternal(path), 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(); if (files == null) { return result; } for (File f : files) { if (f.isDirectory()) { try { collectFiles(f, result); } catch (StackOverflowError e) { return result; } } else { handleFile(result, f); } } return result; } private static void handleFile(MeasurementResult result, File f) { long fileSize = f.length(); int fileType = EXTENSION_MAP.getOrDefault(getExtensionForFile(f), UNRECOGNIZED); switch (fileType) { case AUDIO: result.audioSize += fileSize; break; case VIDEO: result.videosSize += fileSize; break; case IMAGES: result.imagesSize += fileSize; break; default: result.miscSize += fileSize; } } private static String getExtensionForFile(File file) { String fileName = file.getName(); int index = fileName.lastIndexOf('.'); if (index == -1) { return ""; } return fileName.substring(index + 1).toLowerCase(); } /** * MeasurementResult contains a storage categorization result. */ public static class MeasurementResult { public long imagesSize; public long videosSize; public long miscSize; public long audioSize; /** * Sums up the storage taken by all of the categorizable sizes in the measurement. */ public long totalAccountedSize() { return imagesSize + videosSize + miscSize + audioSize; } } } Loading
core/res/AndroidManifest.xml +7 −4 Original line number Diff line number Diff line Loading @@ -3436,12 +3436,15 @@ android:permission="android.permission.BIND_JOB_SERVICE" > </service> <service android:name="com.android.server.pm.BackgroundDexOptService" <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> </manifest>
services/core/java/com/android/server/DiskStatsService.java +56 −0 Original line number Diff line number Diff line Loading @@ -22,6 +22,15 @@ import android.os.Environment; 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; Loading @@ -35,11 +44,13 @@ import java.io.PrintWriter; */ 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 Loading Loading @@ -84,6 +95,10 @@ public class DiskStatsService extends Binder { 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. } Loading Loading @@ -114,4 +129,45 @@ public class DiskStatsService extends Binder { 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); } } }
services/core/java/com/android/server/storage/DiskStatsFileLogger.java 0 → 100644 +166 −0 Original line number Diff line number Diff line /* * 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
services/core/java/com/android/server/storage/DiskStatsLoggingService.java 0 → 100644 +170 −0 Original line number Diff line number Diff line /* * 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
services/core/java/com/android/server/storage/FileCollector.java 0 → 100644 +247 −0 Original line number Diff line number Diff line /* * 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.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; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Map; /** * FileCollector walks over a directory and categorizes storage usage by their type. */ public class FileCollector { private static final int UNRECOGNIZED = -1; private static final int IMAGES = 0; private static final int VIDEO = 1; private static final int AUDIO = 2; @Retention(RetentionPolicy.SOURCE) @IntDef({ UNRECOGNIZED, IMAGES, VIDEO, AUDIO }) private @interface FileTypes {} private static final Map<String, Integer> EXTENSION_MAP = new ArrayMap<String, Integer>(); static { // Audio EXTENSION_MAP.put("aac", AUDIO); EXTENSION_MAP.put("amr", AUDIO); EXTENSION_MAP.put("awb", AUDIO); EXTENSION_MAP.put("snd", AUDIO); EXTENSION_MAP.put("flac", AUDIO); EXTENSION_MAP.put("mp3", AUDIO); EXTENSION_MAP.put("mpga", AUDIO); EXTENSION_MAP.put("mpega", AUDIO); EXTENSION_MAP.put("mp2", AUDIO); EXTENSION_MAP.put("m4a", AUDIO); EXTENSION_MAP.put("aif", AUDIO); EXTENSION_MAP.put("aiff", AUDIO); EXTENSION_MAP.put("aifc", AUDIO); EXTENSION_MAP.put("gsm", AUDIO); EXTENSION_MAP.put("mka", AUDIO); EXTENSION_MAP.put("m3u", AUDIO); EXTENSION_MAP.put("wma", AUDIO); EXTENSION_MAP.put("wax", AUDIO); EXTENSION_MAP.put("ra", AUDIO); EXTENSION_MAP.put("rm", AUDIO); EXTENSION_MAP.put("ram", AUDIO); EXTENSION_MAP.put("pls", AUDIO); EXTENSION_MAP.put("sd2", AUDIO); EXTENSION_MAP.put("wav", AUDIO); EXTENSION_MAP.put("ogg", AUDIO); EXTENSION_MAP.put("oga", AUDIO); // Video EXTENSION_MAP.put("3gpp", VIDEO); EXTENSION_MAP.put("3gp", VIDEO); EXTENSION_MAP.put("3gpp2", VIDEO); EXTENSION_MAP.put("3g2", VIDEO); EXTENSION_MAP.put("avi", VIDEO); EXTENSION_MAP.put("dl", VIDEO); EXTENSION_MAP.put("dif", VIDEO); EXTENSION_MAP.put("dv", VIDEO); EXTENSION_MAP.put("fli", VIDEO); EXTENSION_MAP.put("m4v", VIDEO); EXTENSION_MAP.put("ts", VIDEO); EXTENSION_MAP.put("mpeg", VIDEO); EXTENSION_MAP.put("mpg", VIDEO); EXTENSION_MAP.put("mpe", VIDEO); EXTENSION_MAP.put("mp4", VIDEO); EXTENSION_MAP.put("vob", VIDEO); EXTENSION_MAP.put("qt", VIDEO); EXTENSION_MAP.put("mov", VIDEO); EXTENSION_MAP.put("mxu", VIDEO); EXTENSION_MAP.put("webm", VIDEO); EXTENSION_MAP.put("lsf", VIDEO); EXTENSION_MAP.put("lsx", VIDEO); EXTENSION_MAP.put("mkv", VIDEO); EXTENSION_MAP.put("mng", VIDEO); EXTENSION_MAP.put("asf", VIDEO); EXTENSION_MAP.put("asx", VIDEO); EXTENSION_MAP.put("wm", VIDEO); EXTENSION_MAP.put("wmv", VIDEO); EXTENSION_MAP.put("wmx", VIDEO); EXTENSION_MAP.put("wvx", VIDEO); EXTENSION_MAP.put("movie", VIDEO); EXTENSION_MAP.put("wrf", VIDEO); // Images EXTENSION_MAP.put("bmp", IMAGES); EXTENSION_MAP.put("gif", IMAGES); EXTENSION_MAP.put("jpg", IMAGES); EXTENSION_MAP.put("jpeg", IMAGES); EXTENSION_MAP.put("jpe", IMAGES); EXTENSION_MAP.put("pcx", IMAGES); EXTENSION_MAP.put("png", IMAGES); EXTENSION_MAP.put("svg", IMAGES); EXTENSION_MAP.put("svgz", IMAGES); EXTENSION_MAP.put("tiff", IMAGES); EXTENSION_MAP.put("tif", IMAGES); EXTENSION_MAP.put("wbmp", IMAGES); EXTENSION_MAP.put("webp", IMAGES); EXTENSION_MAP.put("dng", IMAGES); EXTENSION_MAP.put("cr2", IMAGES); EXTENSION_MAP.put("ras", IMAGES); EXTENSION_MAP.put("art", IMAGES); EXTENSION_MAP.put("jng", IMAGES); EXTENSION_MAP.put("nef", IMAGES); EXTENSION_MAP.put("nrw", IMAGES); EXTENSION_MAP.put("orf", IMAGES); EXTENSION_MAP.put("rw2", IMAGES); EXTENSION_MAP.put("pef", IMAGES); EXTENSION_MAP.put("psd", IMAGES); EXTENSION_MAP.put("pnm", IMAGES); EXTENSION_MAP.put("pbm", IMAGES); EXTENSION_MAP.put("pgm", IMAGES); EXTENSION_MAP.put("ppm", IMAGES); EXTENSION_MAP.put("srw", IMAGES); EXTENSION_MAP.put("arw", IMAGES); EXTENSION_MAP.put("rgb", IMAGES); EXTENSION_MAP.put("xbm", IMAGES); EXTENSION_MAP.put("xpm", IMAGES); EXTENSION_MAP.put("xwd", IMAGES); } /** * Returns the file categorization measurement result. * @param path Directory to collect and categorize storage in. */ public static MeasurementResult getMeasurementResult(File path) { return collectFiles(StorageManager.maybeTranslateEmulatedPathToInternal(path), 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(); if (files == null) { return result; } for (File f : files) { if (f.isDirectory()) { try { collectFiles(f, result); } catch (StackOverflowError e) { return result; } } else { handleFile(result, f); } } return result; } private static void handleFile(MeasurementResult result, File f) { long fileSize = f.length(); int fileType = EXTENSION_MAP.getOrDefault(getExtensionForFile(f), UNRECOGNIZED); switch (fileType) { case AUDIO: result.audioSize += fileSize; break; case VIDEO: result.videosSize += fileSize; break; case IMAGES: result.imagesSize += fileSize; break; default: result.miscSize += fileSize; } } private static String getExtensionForFile(File file) { String fileName = file.getName(); int index = fileName.lastIndexOf('.'); if (index == -1) { return ""; } return fileName.substring(index + 1).toLowerCase(); } /** * MeasurementResult contains a storage categorization result. */ public static class MeasurementResult { public long imagesSize; public long videosSize; public long miscSize; public long audioSize; /** * Sums up the storage taken by all of the categorizable sizes in the measurement. */ public long totalAccountedSize() { return imagesSize + videosSize + miscSize + audioSize; } } }