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 17 package com.android.settings.deviceinfo; 18 19 import android.app.ActivityManager; 20 import android.content.ComponentName; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.content.ServiceConnection; 24 import android.content.pm.ApplicationInfo; 25 import android.content.pm.IPackageStatsObserver; 26 import android.content.pm.PackageManager; 27 import android.content.pm.PackageStats; 28 import android.content.pm.UserInfo; 29 import android.os.Environment; 30 import android.os.Environment.UserEnvironment; 31 import android.os.Handler; 32 import android.os.HandlerThread; 33 import android.os.IBinder; 34 import android.os.Looper; 35 import android.os.Message; 36 import android.os.UserHandle; 37 import android.os.UserManager; 38 import android.os.storage.StorageVolume; 39 import android.util.Log; 40 import android.util.SparseLongArray; 41 42 import com.android.internal.app.IMediaContainerService; 43 import com.google.android.collect.Maps; 44 import com.google.android.collect.Sets; 45 46 import java.io.File; 47 import java.lang.ref.WeakReference; 48 import java.util.ArrayList; 49 import java.util.Collections; 50 import java.util.HashMap; 51 import java.util.List; 52 import java.util.Set; 53 54 import javax.annotation.concurrent.GuardedBy; 55 56 /** 57 * Utility for measuring the disk usage of internal storage or a physical 58 * {@link StorageVolume}. Connects with a remote {@link IMediaContainerService} 59 * and delivers results to {@link MeasurementReceiver}. 60 */ 61 public class StorageMeasurement { 62 private static final String TAG = "StorageMeasurement"; 63 64 private static final boolean LOCAL_LOGV = true; 65 static final boolean LOGV = LOCAL_LOGV && Log.isLoggable(TAG, Log.VERBOSE); 66 67 private static final String DEFAULT_CONTAINER_PACKAGE = "com.android.defcontainer"; 68 69 public static final ComponentName DEFAULT_CONTAINER_COMPONENT = new ComponentName( 70 DEFAULT_CONTAINER_PACKAGE, "com.android.defcontainer.DefaultContainerService"); 71 72 /** Media types to measure on external storage. */ 73 private static final Set<String> sMeasureMediaTypes = Sets.newHashSet( 74 Environment.DIRECTORY_DCIM, Environment.DIRECTORY_MOVIES, 75 Environment.DIRECTORY_PICTURES, Environment.DIRECTORY_MUSIC, 76 Environment.DIRECTORY_ALARMS, Environment.DIRECTORY_NOTIFICATIONS, 77 Environment.DIRECTORY_RINGTONES, Environment.DIRECTORY_PODCASTS, 78 Environment.DIRECTORY_DOWNLOADS, Environment.DIRECTORY_ANDROID); 79 80 @GuardedBy("sInstances") 81 private static HashMap<StorageVolume, StorageMeasurement> sInstances = Maps.newHashMap(); 82 83 /** 84 * Obtain shared instance of {@link StorageMeasurement} for given physical 85 * {@link StorageVolume}, or internal storage if {@code null}. 86 */ 87 public static StorageMeasurement getInstance(Context context, StorageVolume volume) { 88 synchronized (sInstances) { 89 StorageMeasurement value = sInstances.get(volume); 90 if (value == null) { 91 value = new StorageMeasurement(context.getApplicationContext(), volume); 92 sInstances.put(volume, value); 93 } 94 return value; 95 } 96 } 97 98 public static class MeasurementDetails { 99 public long totalSize; 100 public long availSize; 101 102 /** 103 * Total apps disk usage. 104 * <p> 105 * When measuring internal storage, this value includes the code size of 106 * all apps (regardless of install status for current user), and 107 * internal disk used by the current user's apps. When the device 108 * emulates external storage, this value also includes emulated storage 109 * used by the current user's apps. 110 * <p> 111 * When measuring a physical {@link StorageVolume}, this value includes 112 * usage by all apps on that volume. 113 */ 114 public long appsSize; 115 116 /** 117 * Total cache disk usage by apps. 118 */ 119 public long cacheSize; 120 121 /** 122 * Total media disk usage, categorized by types such as 123 * {@link Environment#DIRECTORY_MUSIC}. 124 * <p> 125 * When measuring internal storage, this reflects media on emulated 126 * storage for the current user. 127 * <p> 128 * When measuring a physical {@link StorageVolume}, this reflects media 129 * on that volume. 130 */ 131 public HashMap<String, Long> mediaSize = Maps.newHashMap(); 132 133 /** 134 * Misc external disk usage for the current user, unaccounted in 135 * {@link #mediaSize}. 136 */ 137 public long miscSize; 138 139 /** 140 * Total disk usage for users, which is only meaningful for emulated 141 * internal storage. Key is {@link UserHandle}. 142 */ 143 public SparseLongArray usersSize = new SparseLongArray(); 144 } 145 146 public interface MeasurementReceiver { 147 public void updateApproximate(StorageMeasurement meas, long totalSize, long availSize); 148 public void updateDetails(StorageMeasurement meas, MeasurementDetails details); 149 } 150 151 private volatile WeakReference<MeasurementReceiver> mReceiver; 152 153 /** Physical volume being measured, or {@code null} for internal. */ 154 private final StorageVolume mVolume; 155 156 private final boolean mIsInternal; 157 private final boolean mIsPrimary; 158 159 private final MeasurementHandler mHandler; 160 161 private long mTotalSize; 162 private long mAvailSize; 163 164 List<FileInfo> mFileInfoForMisc; 165 166 private StorageMeasurement(Context context, StorageVolume volume) { 167 mVolume = volume; 168 mIsInternal = volume == null; 169 mIsPrimary = volume != null ? volume.isPrimary() : false; 170 171 // Start the thread that will measure the disk usage. 172 final HandlerThread handlerThread = new HandlerThread("MemoryMeasurement"); 173 handlerThread.start(); 174 mHandler = new MeasurementHandler(context, handlerThread.getLooper()); 175 } 176 177 public void setReceiver(MeasurementReceiver receiver) { 178 if (mReceiver == null || mReceiver.get() == null) { 179 mReceiver = new WeakReference<MeasurementReceiver>(receiver); 180 } 181 } 182 183 public void measure() { 184 if (!mHandler.hasMessages(MeasurementHandler.MSG_MEASURE)) { 185 mHandler.sendEmptyMessage(MeasurementHandler.MSG_MEASURE); 186 } 187 } 188 189 public void cleanUp() { 190 mReceiver = null; 191 mHandler.removeMessages(MeasurementHandler.MSG_MEASURE); 192 mHandler.sendEmptyMessage(MeasurementHandler.MSG_DISCONNECT); 193 } 194 195 public void invalidate() { 196 mHandler.sendEmptyMessage(MeasurementHandler.MSG_INVALIDATE); 197 } 198 199 private void sendInternalApproximateUpdate() { 200 MeasurementReceiver receiver = (mReceiver != null) ? mReceiver.get() : null; 201 if (receiver == null) { 202 return; 203 } 204 receiver.updateApproximate(this, mTotalSize, mAvailSize); 205 } 206 207 private void sendExactUpdate(MeasurementDetails details) { 208 MeasurementReceiver receiver = (mReceiver != null) ? mReceiver.get() : null; 209 if (receiver == null) { 210 if (LOGV) { 211 Log.i(TAG, "measurements dropped because receiver is null! wasted effort"); 212 } 213 return; 214 } 215 receiver.updateDetails(this, details); 216 } 217 218 private static class StatsObserver extends IPackageStatsObserver.Stub { 219 private final boolean mIsInternal; 220 private final MeasurementDetails mDetails; 221 private final int mCurrentUser; 222 private final Message mFinished; 223 224 private int mRemaining; 225 226 public StatsObserver(boolean isInternal, MeasurementDetails details, int currentUser, 227 Message finished, int remaining) { 228 mIsInternal = isInternal; 229 mDetails = details; 230 mCurrentUser = currentUser; 231 mFinished = finished; 232 mRemaining = remaining; 233 } 234 235 @Override 236 public void onGetStatsCompleted(PackageStats stats, boolean succeeded) { 237 synchronized (mDetails) { 238 if (succeeded) { 239 addStatsLocked(stats); 240 } 241 if (--mRemaining == 0) { 242 mFinished.sendToTarget(); 243 } 244 } 245 } 246 247 private void addStatsLocked(PackageStats stats) { 248 if (mIsInternal) { 249 long codeSize = stats.codeSize; 250 long dataSize = stats.dataSize; 251 long cacheSize = stats.cacheSize; 252 if (Environment.isExternalStorageEmulated()) { 253 // Include emulated storage when measuring internal. OBB is 254 // shared on emulated storage, so treat as code. 255 codeSize += stats.externalCodeSize + stats.externalObbSize; 256 dataSize += stats.externalDataSize + stats.externalMediaSize; 257 cacheSize += stats.externalCacheSize; 258 } 259 260 // Count code and data for current user 261 if (stats.userHandle == mCurrentUser) { 262 mDetails.appsSize += codeSize; 263 mDetails.appsSize += dataSize; 264 } 265 266 // User summary only includes data (code is only counted once 267 // for the current user) 268 addValue(mDetails.usersSize, stats.userHandle, dataSize); 269 270 // Include cache for all users 271 mDetails.cacheSize += cacheSize; 272 273 } else { 274 // Physical storage; only count external sizes 275 mDetails.appsSize += stats.externalCodeSize + stats.externalDataSize 276 + stats.externalMediaSize + stats.externalObbSize; 277 mDetails.cacheSize += stats.externalCacheSize; 278 } 279 } 280 } 281 282 private class MeasurementHandler extends Handler { 283 public static final int MSG_MEASURE = 1; 284 public static final int MSG_CONNECTED = 2; 285 public static final int MSG_DISCONNECT = 3; 286 public static final int MSG_COMPLETED = 4; 287 public static final int MSG_INVALIDATE = 5; 288 289 private Object mLock = new Object(); 290 291 private IMediaContainerService mDefaultContainer; 292 293 private volatile boolean mBound = false; 294 295 private MeasurementDetails mCached; 296 297 private final WeakReference<Context> mContext; 298 299 private final ServiceConnection mDefContainerConn = new ServiceConnection() { 300 @Override 301 public void onServiceConnected(ComponentName name, IBinder service) { 302 final IMediaContainerService imcs = IMediaContainerService.Stub.asInterface( 303 service); 304 mDefaultContainer = imcs; 305 mBound = true; 306 sendMessage(obtainMessage(MSG_CONNECTED, imcs)); 307 } 308 309 @Override 310 public void onServiceDisconnected(ComponentName name) { 311 mBound = false; 312 removeMessages(MSG_CONNECTED); 313 } 314 }; 315 316 public MeasurementHandler(Context context, Looper looper) { 317 super(looper); 318 mContext = new WeakReference<Context>(context); 319 } 320 321 @Override 322 public void handleMessage(Message msg) { 323 switch (msg.what) { 324 case MSG_MEASURE: { 325 if (mCached != null) { 326 sendExactUpdate(mCached); 327 break; 328 } 329 330 final Context context = (mContext != null) ? mContext.get() : null; 331 if (context == null) { 332 return; 333 } 334 335 synchronized (mLock) { 336 if (mBound) { 337 removeMessages(MSG_DISCONNECT); 338 sendMessage(obtainMessage(MSG_CONNECTED, mDefaultContainer)); 339 } else { 340 Intent service = new Intent().setComponent(DEFAULT_CONTAINER_COMPONENT); 341 context.bindServiceAsUser(service, mDefContainerConn, Context.BIND_AUTO_CREATE, 342 UserHandle.OWNER); 343 } 344 } 345 break; 346 } 347 case MSG_CONNECTED: { 348 IMediaContainerService imcs = (IMediaContainerService) msg.obj; 349 measureApproximateStorage(imcs); 350 measureExactStorage(imcs); 351 break; 352 } 353 case MSG_DISCONNECT: { 354 synchronized (mLock) { 355 if (mBound) { 356 final Context context = (mContext != null) ? mContext.get() : null; 357 if (context == null) { 358 return; 359 } 360 361 mBound = false; 362 context.unbindService(mDefContainerConn); 363 } 364 } 365 break; 366 } 367 case MSG_COMPLETED: { 368 mCached = (MeasurementDetails) msg.obj; 369 sendExactUpdate(mCached); 370 break; 371 } 372 case MSG_INVALIDATE: { 373 mCached = null; 374 break; 375 } 376 } 377 } 378 379 private void measureApproximateStorage(IMediaContainerService imcs) { 380 final String path = mVolume != null ? mVolume.getPath() 381 : Environment.getDataDirectory().getPath(); 382 try { 383 final long[] stats = imcs.getFileSystemStats(path); 384 mTotalSize = stats[0]; 385 mAvailSize = stats[1]; 386 } catch (Exception e) { 387 Log.w(TAG, "Problem in container service", e); 388 } 389 390 sendInternalApproximateUpdate(); 391 } 392 393 private void measureExactStorage(IMediaContainerService imcs) { 394 final Context context = mContext != null ? mContext.get() : null; 395 if (context == null) { 396 return; 397 } 398 399 final MeasurementDetails details = new MeasurementDetails(); 400 final Message finished = obtainMessage(MSG_COMPLETED, details); 401 402 details.totalSize = mTotalSize; 403 details.availSize = mAvailSize; 404 405 final UserManager userManager = (UserManager) context.getSystemService( 406 Context.USER_SERVICE); 407 final List<UserInfo> users = userManager.getUsers(); 408 409 final int currentUser = ActivityManager.getCurrentUser(); 410 final UserEnvironment currentEnv = new UserEnvironment(currentUser); 411 412 // Measure media types for emulated storage, or for primary physical 413 // external volume 414 final boolean measureMedia = (mIsInternal && Environment.isExternalStorageEmulated()) 415 || mIsPrimary; 416 if (measureMedia) { 417 for (String type : sMeasureMediaTypes) { 418 final File path = currentEnv.getExternalStoragePublicDirectory(type); 419 final long size = getDirectorySize(imcs, path); 420 details.mediaSize.put(type, size); 421 } 422 } 423 424 // Measure misc files not counted under media 425 if (measureMedia) { 426 final File path = mIsInternal ? currentEnv.getExternalStorageDirectory() 427 : mVolume.getPathFile(); 428 details.miscSize = measureMisc(imcs, path); 429 } 430 431 // Measure total emulated storage of all users; internal apps data 432 // will be spliced in later 433 for (UserInfo user : users) { 434 final UserEnvironment userEnv = new UserEnvironment(user.id); 435 final long size = getDirectorySize(imcs, userEnv.getExternalStorageDirectory()); 436 addValue(details.usersSize, user.id, size); 437 } 438 439 // Measure all apps for all users 440 final PackageManager pm = context.getPackageManager(); 441 if (mIsInternal || mIsPrimary) { 442 final List<ApplicationInfo> apps = pm.getInstalledApplications( 443 PackageManager.GET_UNINSTALLED_PACKAGES 444 | PackageManager.GET_DISABLED_COMPONENTS); 445 446 final int count = users.size() * apps.size(); 447 final StatsObserver observer = new StatsObserver( 448 mIsInternal, details, currentUser, finished, count); 449 450 for (UserInfo user : users) { 451 for (ApplicationInfo app : apps) { 452 pm.getPackageSizeInfo(app.packageName, user.id, observer); 453 } 454 } 455 456 } else { 457 finished.sendToTarget(); 458 } 459 } 460 } 461 462 private static long getDirectorySize(IMediaContainerService imcs, File path) { 463 try { 464 final long size = imcs.calculateDirectorySize(path.toString()); 465 Log.d(TAG, "getDirectorySize(" + path + ") returned " + size); 466 return size; 467 } catch (Exception e) { 468 Log.w(TAG, "Could not read memory from default container service for " + path, e); 469 return 0; 470 } 471 } 472 473 private long measureMisc(IMediaContainerService imcs, File dir) { 474 mFileInfoForMisc = new ArrayList<FileInfo>(); 475 476 final File[] files = dir.listFiles(); 477 if (files == null) return 0; 478 479 // Get sizes of all top level nodes except the ones already computed 480 long counter = 0; 481 long miscSize = 0; 482 483 for (File file : files) { 484 final String path = file.getAbsolutePath(); 485 final String name = file.getName(); 486 if (sMeasureMediaTypes.contains(name)) { 487 continue; 488 } 489 490 if (file.isFile()) { 491 final long fileSize = file.length(); 492 mFileInfoForMisc.add(new FileInfo(path, fileSize, counter++)); 493 miscSize += fileSize; 494 } else if (file.isDirectory()) { 495 final long dirSize = getDirectorySize(imcs, file); 496 mFileInfoForMisc.add(new FileInfo(path, dirSize, counter++)); 497 miscSize += dirSize; 498 } else { 499 // Non directory, non file: not listed 500 } 501 } 502 503 // sort the list of FileInfo objects collected above in descending order of their sizes 504 Collections.sort(mFileInfoForMisc); 505 506 return miscSize; 507 } 508 509 static class FileInfo implements Comparable<FileInfo> { 510 final String mFileName; 511 final long mSize; 512 final long mId; 513 514 FileInfo(String fileName, long size, long id) { 515 mFileName = fileName; 516 mSize = size; 517 mId = id; 518 } 519 520 @Override 521 public int compareTo(FileInfo that) { 522 if (this == that || mSize == that.mSize) return 0; 523 else return (mSize < that.mSize) ? 1 : -1; // for descending sort 524 } 525 526 @Override 527 public String toString() { 528 return mFileName + " : " + mSize + ", id:" + mId; 529 } 530 } 531 532 private static void addValue(SparseLongArray array, int key, long value) { 533 array.put(key, array.get(key) + value); 534 } 535 } 536