1/* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.settingslib.deviceinfo; 18 19import android.app.ActivityManager; 20import android.content.ComponentName; 21import android.content.Context; 22import android.content.Intent; 23import android.content.ServiceConnection; 24import android.content.pm.ApplicationInfo; 25import android.content.pm.IPackageStatsObserver; 26import android.content.pm.PackageManager; 27import android.content.pm.PackageStats; 28import android.content.pm.UserInfo; 29import android.os.Environment; 30import android.os.Handler; 31import android.os.HandlerThread; 32import android.os.IBinder; 33import android.os.Looper; 34import android.os.Message; 35import android.os.UserHandle; 36import android.os.UserManager; 37import android.os.storage.StorageVolume; 38import android.os.storage.VolumeInfo; 39import android.util.Log; 40import android.util.SparseArray; 41import android.util.SparseLongArray; 42 43import com.android.internal.app.IMediaContainerService; 44import com.android.internal.util.ArrayUtils; 45import com.google.android.collect.Sets; 46 47import java.io.File; 48import java.lang.ref.WeakReference; 49import java.util.ArrayList; 50import java.util.HashMap; 51import java.util.List; 52import java.util.Objects; 53import java.util.Set; 54 55/** 56 * Utility for measuring the disk usage of internal storage or a physical 57 * {@link StorageVolume}. Connects with a remote {@link IMediaContainerService} 58 * and delivers results to {@link MeasurementReceiver}. 59 */ 60public class StorageMeasurement { 61 private static final String TAG = "StorageMeasurement"; 62 63 private static final boolean LOCAL_LOGV = true; 64 static final boolean LOGV = LOCAL_LOGV && Log.isLoggable(TAG, Log.VERBOSE); 65 66 private static final String DEFAULT_CONTAINER_PACKAGE = "com.android.defcontainer"; 67 68 public static final ComponentName DEFAULT_CONTAINER_COMPONENT = new ComponentName( 69 DEFAULT_CONTAINER_PACKAGE, "com.android.defcontainer.DefaultContainerService"); 70 71 /** Media types to measure on external storage. */ 72 private static final Set<String> sMeasureMediaTypes = Sets.newHashSet( 73 Environment.DIRECTORY_DCIM, Environment.DIRECTORY_MOVIES, 74 Environment.DIRECTORY_PICTURES, Environment.DIRECTORY_MUSIC, 75 Environment.DIRECTORY_ALARMS, Environment.DIRECTORY_NOTIFICATIONS, 76 Environment.DIRECTORY_RINGTONES, Environment.DIRECTORY_PODCASTS, 77 Environment.DIRECTORY_DOWNLOADS, Environment.DIRECTORY_ANDROID); 78 79 public static class MeasurementDetails { 80 public long totalSize; 81 public long availSize; 82 83 /** 84 * Total apps disk usage per profiles of the current user. 85 * <p> 86 * When measuring internal storage, this value includes the code size of 87 * all apps (regardless of install status for the given profile), and 88 * internal disk used by the profile's apps. When the device 89 * emulates external storage, this value also includes emulated storage 90 * used by the profile's apps. 91 * <p> 92 * When measuring a physical {@link StorageVolume}, this value includes 93 * usage by all apps on that volume and only for the primary profile. 94 * <p> 95 * Key is {@link UserHandle}. 96 */ 97 public SparseLongArray appsSize = new SparseLongArray(); 98 99 /** 100 * Total cache disk usage by apps (over all users and profiles). 101 */ 102 public long cacheSize; 103 104 /** 105 * Total media disk usage, categorized by types such as 106 * {@link Environment#DIRECTORY_MUSIC} for every user profile of the current user. 107 * <p> 108 * When measuring internal storage, this reflects media on emulated 109 * storage for the respective profile. 110 * <p> 111 * When measuring a physical {@link StorageVolume}, this reflects media 112 * on that volume. 113 * <p> 114 * Key of the {@link SparseArray} is {@link UserHandle}. 115 */ 116 public SparseArray<HashMap<String, Long>> mediaSize = new SparseArray<>(); 117 118 /** 119 * Misc external disk usage for the current user's profiles, unaccounted in 120 * {@link #mediaSize}. Key is {@link UserHandle}. 121 */ 122 public SparseLongArray miscSize = new SparseLongArray(); 123 124 /** 125 * Total disk usage for users, which is only meaningful for emulated 126 * internal storage. Key is {@link UserHandle}. 127 */ 128 public SparseLongArray usersSize = new SparseLongArray(); 129 } 130 131 public interface MeasurementReceiver { 132 void onDetailsChanged(MeasurementDetails details); 133 } 134 135 private WeakReference<MeasurementReceiver> mReceiver; 136 137 private final Context mContext; 138 139 private final VolumeInfo mVolume; 140 private final VolumeInfo mSharedVolume; 141 142 private final MainHandler mMainHandler; 143 private final MeasurementHandler mMeasurementHandler; 144 145 public StorageMeasurement(Context context, VolumeInfo volume, VolumeInfo sharedVolume) { 146 mContext = context.getApplicationContext(); 147 148 mVolume = volume; 149 mSharedVolume = sharedVolume; 150 151 // Start the thread that will measure the disk usage. 152 final HandlerThread handlerThread = new HandlerThread("MemoryMeasurement"); 153 handlerThread.start(); 154 155 mMainHandler = new MainHandler(); 156 mMeasurementHandler = new MeasurementHandler(handlerThread.getLooper()); 157 } 158 159 public void setReceiver(MeasurementReceiver receiver) { 160 if (mReceiver == null || mReceiver.get() == null) { 161 mReceiver = new WeakReference<MeasurementReceiver>(receiver); 162 } 163 } 164 165 public void forceMeasure() { 166 invalidate(); 167 measure(); 168 } 169 170 public void measure() { 171 if (!mMeasurementHandler.hasMessages(MeasurementHandler.MSG_MEASURE)) { 172 mMeasurementHandler.sendEmptyMessage(MeasurementHandler.MSG_MEASURE); 173 } 174 } 175 176 public void onDestroy() { 177 mReceiver = null; 178 mMeasurementHandler.removeMessages(MeasurementHandler.MSG_MEASURE); 179 mMeasurementHandler.sendEmptyMessage(MeasurementHandler.MSG_DISCONNECT); 180 } 181 182 private void invalidate() { 183 mMeasurementHandler.sendEmptyMessage(MeasurementHandler.MSG_INVALIDATE); 184 } 185 186 private static class StatsObserver extends IPackageStatsObserver.Stub { 187 private final boolean mIsPrivate; 188 private final MeasurementDetails mDetails; 189 private final int mCurrentUser; 190 private final Message mFinished; 191 192 private int mRemaining; 193 194 public StatsObserver(boolean isPrivate, MeasurementDetails details, int currentUser, 195 List<UserInfo> profiles, Message finished, int remaining) { 196 mIsPrivate = isPrivate; 197 mDetails = details; 198 mCurrentUser = currentUser; 199 if (isPrivate) { 200 // Add the profile ids as keys to detail's app sizes. 201 for (UserInfo userInfo : profiles) { 202 mDetails.appsSize.put(userInfo.id, 0); 203 } 204 } 205 mFinished = finished; 206 mRemaining = remaining; 207 } 208 209 @Override 210 public void onGetStatsCompleted(PackageStats stats, boolean succeeded) { 211 synchronized (mDetails) { 212 if (succeeded) { 213 addStatsLocked(stats); 214 } 215 if (--mRemaining == 0) { 216 mFinished.sendToTarget(); 217 } 218 } 219 } 220 221 private void addStatsLocked(PackageStats stats) { 222 if (mIsPrivate) { 223 long codeSize = stats.codeSize; 224 long dataSize = stats.dataSize; 225 long cacheSize = stats.cacheSize; 226 if (Environment.isExternalStorageEmulated()) { 227 // Include emulated storage when measuring internal. OBB is 228 // shared on emulated storage, so treat as code. 229 codeSize += stats.externalCodeSize + stats.externalObbSize; 230 dataSize += stats.externalDataSize + stats.externalMediaSize; 231 cacheSize += stats.externalCacheSize; 232 } 233 234 // Count code and data for current user's profiles (keys prepared in constructor) 235 addValueIfKeyExists(mDetails.appsSize, stats.userHandle, codeSize + dataSize); 236 237 // User summary only includes data (code is only counted once 238 // for the current user) 239 addValue(mDetails.usersSize, stats.userHandle, dataSize); 240 241 // Include cache for all users 242 mDetails.cacheSize += cacheSize; 243 244 } else { 245 // Physical storage; only count external sizes 246 addValue(mDetails.appsSize, mCurrentUser, 247 stats.externalCodeSize + stats.externalDataSize 248 + stats.externalMediaSize + stats.externalObbSize); 249 mDetails.cacheSize += stats.externalCacheSize; 250 } 251 } 252 } 253 254 private class MainHandler extends Handler { 255 @Override 256 public void handleMessage(Message msg) { 257 final MeasurementDetails details = (MeasurementDetails) msg.obj; 258 final MeasurementReceiver receiver = (mReceiver != null) ? mReceiver.get() : null; 259 if (receiver != null) { 260 receiver.onDetailsChanged(details); 261 } 262 } 263 } 264 265 private class MeasurementHandler extends Handler { 266 public static final int MSG_MEASURE = 1; 267 public static final int MSG_CONNECTED = 2; 268 public static final int MSG_DISCONNECT = 3; 269 public static final int MSG_COMPLETED = 4; 270 public static final int MSG_INVALIDATE = 5; 271 272 private Object mLock = new Object(); 273 274 private IMediaContainerService mDefaultContainer; 275 276 private volatile boolean mBound = false; 277 278 private MeasurementDetails mCached; 279 280 private final ServiceConnection mDefContainerConn = new ServiceConnection() { 281 @Override 282 public void onServiceConnected(ComponentName name, IBinder service) { 283 final IMediaContainerService imcs = IMediaContainerService.Stub.asInterface( 284 service); 285 mDefaultContainer = imcs; 286 mBound = true; 287 sendMessage(obtainMessage(MSG_CONNECTED, imcs)); 288 } 289 290 @Override 291 public void onServiceDisconnected(ComponentName name) { 292 mBound = false; 293 removeMessages(MSG_CONNECTED); 294 } 295 }; 296 297 public MeasurementHandler(Looper looper) { 298 super(looper); 299 } 300 301 @Override 302 public void handleMessage(Message msg) { 303 switch (msg.what) { 304 case MSG_MEASURE: { 305 if (mCached != null) { 306 mMainHandler.obtainMessage(0, mCached).sendToTarget(); 307 break; 308 } 309 310 synchronized (mLock) { 311 if (mBound) { 312 removeMessages(MSG_DISCONNECT); 313 sendMessage(obtainMessage(MSG_CONNECTED, mDefaultContainer)); 314 } else { 315 Intent service = new Intent().setComponent(DEFAULT_CONTAINER_COMPONENT); 316 mContext.bindServiceAsUser(service, mDefContainerConn, 317 Context.BIND_AUTO_CREATE, UserHandle.OWNER); 318 } 319 } 320 break; 321 } 322 case MSG_CONNECTED: { 323 final IMediaContainerService imcs = (IMediaContainerService) msg.obj; 324 measureExactStorage(imcs); 325 break; 326 } 327 case MSG_DISCONNECT: { 328 synchronized (mLock) { 329 if (mBound) { 330 mBound = false; 331 mContext.unbindService(mDefContainerConn); 332 } 333 } 334 break; 335 } 336 case MSG_COMPLETED: { 337 mCached = (MeasurementDetails) msg.obj; 338 mMainHandler.obtainMessage(0, mCached).sendToTarget(); 339 break; 340 } 341 case MSG_INVALIDATE: { 342 mCached = null; 343 break; 344 } 345 } 346 } 347 } 348 349 private void measureExactStorage(IMediaContainerService imcs) { 350 final UserManager userManager = mContext.getSystemService(UserManager.class); 351 final PackageManager packageManager = mContext.getPackageManager(); 352 353 final List<UserInfo> users = userManager.getUsers(); 354 final List<UserInfo> currentProfiles = userManager.getEnabledProfiles( 355 ActivityManager.getCurrentUser()); 356 357 final MeasurementDetails details = new MeasurementDetails(); 358 final Message finished = mMeasurementHandler.obtainMessage(MeasurementHandler.MSG_COMPLETED, 359 details); 360 361 if (mVolume == null || !mVolume.isMountedReadable()) { 362 finished.sendToTarget(); 363 return; 364 } 365 366 if (mSharedVolume != null && mSharedVolume.isMountedReadable()) { 367 for (UserInfo currentUserInfo : currentProfiles) { 368 final int userId = currentUserInfo.id; 369 final File basePath = mSharedVolume.getPathForUser(userId); 370 HashMap<String, Long> mediaMap = new HashMap<>(sMeasureMediaTypes.size()); 371 details.mediaSize.put(userId, mediaMap); 372 373 // Measure media types for emulated storage, or for primary physical 374 // external volume 375 for (String type : sMeasureMediaTypes) { 376 final File path = new File(basePath, type); 377 final long size = getDirectorySize(imcs, path); 378 mediaMap.put(type, size); 379 } 380 381 // Measure misc files not counted under media 382 addValue(details.miscSize, userId, measureMisc(imcs, basePath)); 383 } 384 385 if (mSharedVolume.getType() == VolumeInfo.TYPE_EMULATED) { 386 // Measure total emulated storage of all users; internal apps data 387 // will be spliced in later 388 for (UserInfo user : users) { 389 final File userPath = mSharedVolume.getPathForUser(user.id); 390 final long size = getDirectorySize(imcs, userPath); 391 addValue(details.usersSize, user.id, size); 392 } 393 } 394 } 395 396 final File file = mVolume.getPath(); 397 if (file != null) { 398 details.totalSize = file.getTotalSpace(); 399 details.availSize = file.getFreeSpace(); 400 } 401 402 // Measure all apps hosted on this volume for all users 403 if (mVolume.getType() == VolumeInfo.TYPE_PRIVATE) { 404 final List<ApplicationInfo> apps = packageManager.getInstalledApplications( 405 PackageManager.GET_UNINSTALLED_PACKAGES 406 | PackageManager.GET_DISABLED_COMPONENTS); 407 408 final List<ApplicationInfo> volumeApps = new ArrayList<>(); 409 for (ApplicationInfo app : apps) { 410 if (Objects.equals(app.volumeUuid, mVolume.getFsUuid())) { 411 volumeApps.add(app); 412 } 413 } 414 415 final int count = users.size() * volumeApps.size(); 416 if (count == 0) { 417 finished.sendToTarget(); 418 return; 419 } 420 421 final StatsObserver observer = new StatsObserver(true, details, 422 ActivityManager.getCurrentUser(), currentProfiles, finished, count); 423 for (UserInfo user : users) { 424 for (ApplicationInfo app : volumeApps) { 425 packageManager.getPackageSizeInfo(app.packageName, user.id, observer); 426 } 427 } 428 429 } else { 430 finished.sendToTarget(); 431 return; 432 } 433 } 434 435 private static long getDirectorySize(IMediaContainerService imcs, File path) { 436 try { 437 final long size = imcs.calculateDirectorySize(path.toString()); 438 Log.d(TAG, "getDirectorySize(" + path + ") returned " + size); 439 return size; 440 } catch (Exception e) { 441 Log.w(TAG, "Could not read memory from default container service for " + path, e); 442 return 0; 443 } 444 } 445 446 private long measureMisc(IMediaContainerService imcs, File dir) { 447 final File[] files = dir.listFiles(); 448 if (ArrayUtils.isEmpty(files)) return 0; 449 450 // Get sizes of all top level nodes except the ones already computed 451 long miscSize = 0; 452 for (File file : files) { 453 final String name = file.getName(); 454 if (sMeasureMediaTypes.contains(name)) { 455 continue; 456 } 457 458 if (file.isFile()) { 459 miscSize += file.length(); 460 } else if (file.isDirectory()) { 461 miscSize += getDirectorySize(imcs, file); 462 } 463 } 464 return miscSize; 465 } 466 467 private static void addValue(SparseLongArray array, int key, long value) { 468 array.put(key, array.get(key) + value); 469 } 470 471 private static void addValueIfKeyExists(SparseLongArray array, int key, long value) { 472 final int index = array.indexOfKey(key); 473 if (index >= 0) { 474 array.put(key, array.valueAt(index) + value); 475 } 476 } 477} 478