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.content.ComponentName; 20 import android.content.Context; 21 import android.content.Intent; 22 import android.content.ServiceConnection; 23 import android.content.pm.ApplicationInfo; 24 import android.content.pm.IPackageStatsObserver; 25 import android.content.pm.PackageManager; 26 import android.content.pm.PackageStats; 27 import android.os.Bundle; 28 import android.os.Environment; 29 import android.os.Handler; 30 import android.os.HandlerThread; 31 import android.os.IBinder; 32 import android.os.Looper; 33 import android.os.Message; 34 import android.os.RemoteException; 35 import android.os.storage.StorageVolume; 36 import android.util.Log; 37 38 import com.android.internal.app.IMediaContainerService; 39 40 import java.io.File; 41 import java.lang.ref.WeakReference; 42 import java.util.ArrayList; 43 import java.util.Collections; 44 import java.util.List; 45 import java.util.Map; 46 import java.util.concurrent.ConcurrentHashMap; 47 48 /** 49 * Measure the memory for various systems. 50 * 51 * TODO: This class should ideally have less knowledge about what the context 52 * it's measuring is. In the future, reduce the amount of stuff it needs to 53 * know about by just keeping an array of measurement types of the following 54 * properties: 55 * 56 * Filesystem stats (using DefaultContainerService) 57 * Directory measurements (using DefaultContainerService.measureDir) 58 * Application measurements (using PackageManager) 59 * 60 * Then the calling application would just specify the type and an argument. 61 * This class would keep track of it while the calling application would 62 * decide on how to use it. 63 */ 64 public class StorageMeasurement { 65 private static final String TAG = "StorageMeasurement"; 66 67 private static final boolean LOCAL_LOGV = true; 68 static final boolean LOGV = LOCAL_LOGV && Log.isLoggable(TAG, Log.VERBOSE); 69 70 public static final String TOTAL_SIZE = "total_size"; 71 72 public static final String AVAIL_SIZE = "avail_size"; 73 74 public static final String APPS_USED = "apps_used"; 75 76 public static final String DOWNLOADS_SIZE = "downloads_size"; 77 78 public static final String MISC_SIZE = "misc_size"; 79 80 public static final String MEDIA_SIZES = "media_sizes"; 81 82 private static final String DEFAULT_CONTAINER_PACKAGE = "com.android.defcontainer"; 83 84 public static final ComponentName DEFAULT_CONTAINER_COMPONENT = new ComponentName( 85 DEFAULT_CONTAINER_PACKAGE, "com.android.defcontainer.DefaultContainerService"); 86 87 private final MeasurementHandler mHandler; 88 89 private static Map<StorageVolume, StorageMeasurement> sInstances = 90 new ConcurrentHashMap<StorageVolume, StorageMeasurement>(); 91 private static StorageMeasurement sInternalInstance; 92 93 private volatile WeakReference<MeasurementReceiver> mReceiver; 94 95 private long mTotalSize; 96 private long mAvailSize; 97 private long mAppsSize; 98 private long mDownloadsSize; 99 private long mMiscSize; 100 private long[] mMediaSizes = new long[StorageVolumePreferenceCategory.sMediaCategories.length]; 101 102 final private StorageVolume mStorageVolume; 103 final private boolean mIsPrimary; 104 final private boolean mIsInternal; 105 106 List<FileInfo> mFileInfoForMisc; 107 108 public interface MeasurementReceiver { 109 public void updateApproximate(Bundle bundle); 110 public void updateExact(Bundle bundle); 111 } 112 113 private StorageMeasurement(Context context, StorageVolume storageVolume, boolean isPrimary) { 114 mStorageVolume = storageVolume; 115 mIsInternal = storageVolume == null; 116 mIsPrimary = !mIsInternal && isPrimary; 117 118 // Start the thread that will measure the disk usage. 119 final HandlerThread handlerThread = new HandlerThread("MemoryMeasurement"); 120 handlerThread.start(); 121 mHandler = new MeasurementHandler(context, handlerThread.getLooper()); 122 } 123 124 /** 125 * Get the singleton of the StorageMeasurement class. The application 126 * context is used to avoid leaking activities. 127 * @param storageVolume The {@link StorageVolume} that will be measured 128 * @param isPrimary true when this storage volume is the primary volume 129 */ 130 public static StorageMeasurement getInstance(Context context, StorageVolume storageVolume, 131 boolean isPrimary) { 132 if (storageVolume == null) { 133 if (sInternalInstance == null) { 134 sInternalInstance = 135 new StorageMeasurement(context.getApplicationContext(), storageVolume, isPrimary); 136 } 137 return sInternalInstance; 138 } 139 if (sInstances.containsKey(storageVolume)) { 140 return sInstances.get(storageVolume); 141 } else { 142 StorageMeasurement storageMeasurement = 143 new StorageMeasurement(context.getApplicationContext(), storageVolume, isPrimary); 144 sInstances.put(storageVolume, storageMeasurement); 145 return storageMeasurement; 146 } 147 } 148 149 public void setReceiver(MeasurementReceiver receiver) { 150 if (mReceiver == null || mReceiver.get() == null) { 151 mReceiver = new WeakReference<MeasurementReceiver>(receiver); 152 } 153 } 154 155 public void measure() { 156 if (!mHandler.hasMessages(MeasurementHandler.MSG_MEASURE)) { 157 mHandler.sendEmptyMessage(MeasurementHandler.MSG_MEASURE); 158 } 159 } 160 161 public void cleanUp() { 162 mReceiver = null; 163 mHandler.removeMessages(MeasurementHandler.MSG_MEASURE); 164 mHandler.sendEmptyMessage(MeasurementHandler.MSG_DISCONNECT); 165 } 166 167 public void invalidate() { 168 mHandler.sendEmptyMessage(MeasurementHandler.MSG_INVALIDATE); 169 } 170 171 private void sendInternalApproximateUpdate() { 172 MeasurementReceiver receiver = (mReceiver != null) ? mReceiver.get() : null; 173 if (receiver == null) { 174 return; 175 } 176 177 Bundle bundle = new Bundle(); 178 bundle.putLong(TOTAL_SIZE, mTotalSize); 179 bundle.putLong(AVAIL_SIZE, mAvailSize); 180 181 receiver.updateApproximate(bundle); 182 } 183 184 private void sendExactUpdate() { 185 MeasurementReceiver receiver = (mReceiver != null) ? mReceiver.get() : null; 186 if (receiver == null) { 187 if (LOGV) { 188 Log.i(TAG, "measurements dropped because receiver is null! wasted effort"); 189 } 190 return; 191 } 192 193 Bundle bundle = new Bundle(); 194 bundle.putLong(TOTAL_SIZE, mTotalSize); 195 bundle.putLong(AVAIL_SIZE, mAvailSize); 196 bundle.putLong(APPS_USED, mAppsSize); 197 bundle.putLong(DOWNLOADS_SIZE, mDownloadsSize); 198 bundle.putLong(MISC_SIZE, mMiscSize); 199 bundle.putLongArray(MEDIA_SIZES, mMediaSizes); 200 201 receiver.updateExact(bundle); 202 } 203 204 private class MeasurementHandler extends Handler { 205 public static final int MSG_MEASURE = 1; 206 207 public static final int MSG_CONNECTED = 2; 208 209 public static final int MSG_DISCONNECT = 3; 210 211 public static final int MSG_COMPLETED = 4; 212 213 public static final int MSG_INVALIDATE = 5; 214 215 private Object mLock = new Object(); 216 217 private IMediaContainerService mDefaultContainer; 218 219 private volatile boolean mBound = false; 220 221 private volatile boolean mMeasured = false; 222 223 private StatsObserver mStatsObserver; 224 225 private final WeakReference<Context> mContext; 226 227 final private ServiceConnection mDefContainerConn = new ServiceConnection() { 228 public void onServiceConnected(ComponentName name, IBinder service) { 229 final IMediaContainerService imcs = IMediaContainerService.Stub 230 .asInterface(service); 231 mDefaultContainer = imcs; 232 mBound = true; 233 sendMessage(obtainMessage(MSG_CONNECTED, imcs)); 234 } 235 236 public void onServiceDisconnected(ComponentName name) { 237 mBound = false; 238 removeMessages(MSG_CONNECTED); 239 } 240 }; 241 242 public MeasurementHandler(Context context, Looper looper) { 243 super(looper); 244 mContext = new WeakReference<Context>(context); 245 } 246 247 @Override 248 public void handleMessage(Message msg) { 249 switch (msg.what) { 250 case MSG_MEASURE: { 251 if (mMeasured) { 252 sendExactUpdate(); 253 break; 254 } 255 256 final Context context = (mContext != null) ? mContext.get() : null; 257 if (context == null) { 258 return; 259 } 260 261 synchronized (mLock) { 262 if (mBound) { 263 removeMessages(MSG_DISCONNECT); 264 sendMessage(obtainMessage(MSG_CONNECTED, mDefaultContainer)); 265 } else { 266 Intent service = new Intent().setComponent(DEFAULT_CONTAINER_COMPONENT); 267 context.bindService(service, mDefContainerConn, 268 Context.BIND_AUTO_CREATE); 269 } 270 } 271 break; 272 } 273 case MSG_CONNECTED: { 274 IMediaContainerService imcs = (IMediaContainerService) msg.obj; 275 measureApproximateStorage(imcs); 276 measureExactStorage(imcs); 277 break; 278 } 279 case MSG_DISCONNECT: { 280 synchronized (mLock) { 281 if (mBound) { 282 final Context context = (mContext != null) ? mContext.get() : null; 283 if (context == null) { 284 return; 285 } 286 287 mBound = false; 288 context.unbindService(mDefContainerConn); 289 } 290 } 291 break; 292 } 293 case MSG_COMPLETED: { 294 mMeasured = true; 295 sendExactUpdate(); 296 break; 297 } 298 case MSG_INVALIDATE: { 299 mMeasured = false; 300 break; 301 } 302 } 303 } 304 305 /** 306 * Request measurement of each package. 307 * 308 * @param pm PackageManager instance to query 309 */ 310 public void requestQueuedMeasurementsLocked(PackageManager pm) { 311 final String[] appsList = mStatsObserver.getAppsList(); 312 final int N = appsList.length; 313 for (int i = 0; i < N; i++) { 314 pm.getPackageSizeInfo(appsList[i], mStatsObserver); 315 } 316 } 317 318 private class StatsObserver extends IPackageStatsObserver.Stub { 319 private long mAppsSizeForThisStatsObserver = 0; 320 private final List<String> mAppsList = new ArrayList<String>(); 321 322 public void onGetStatsCompleted(PackageStats stats, boolean succeeded) { 323 if (!mStatsObserver.equals(this)) { 324 // this callback's class object is no longer in use. ignore this callback. 325 return; 326 } 327 328 if (succeeded) { 329 if (mIsInternal) { 330 mAppsSizeForThisStatsObserver += stats.codeSize + stats.dataSize; 331 } else if (!Environment.isExternalStorageEmulated()) { 332 mAppsSizeForThisStatsObserver += stats.externalObbSize + 333 stats.externalCodeSize + stats.externalDataSize + 334 stats.externalCacheSize + stats.externalMediaSize; 335 } else { 336 mAppsSizeForThisStatsObserver += stats.codeSize + stats.dataSize + 337 stats.externalCodeSize + stats.externalDataSize + 338 stats.externalCacheSize + stats.externalMediaSize + 339 stats.externalObbSize; 340 } 341 } 342 343 synchronized (mAppsList) { 344 mAppsList.remove(stats.packageName); 345 if (mAppsList.size() > 0) return; 346 } 347 348 mAppsSize = mAppsSizeForThisStatsObserver; 349 onInternalMeasurementComplete(); 350 } 351 352 public void queuePackageMeasurementLocked(String packageName) { 353 synchronized (mAppsList) { 354 mAppsList.add(packageName); 355 } 356 } 357 358 public String[] getAppsList() { 359 synchronized (mAppsList) { 360 return mAppsList.toArray(new String[mAppsList.size()]); 361 } 362 } 363 } 364 365 private void onInternalMeasurementComplete() { 366 sendEmptyMessage(MSG_COMPLETED); 367 } 368 369 private void measureApproximateStorage(IMediaContainerService imcs) { 370 final String path = mStorageVolume != null ? mStorageVolume.getPath() 371 : Environment.getDataDirectory().getPath(); 372 try { 373 final long[] stats = imcs.getFileSystemStats(path); 374 mTotalSize = stats[0]; 375 mAvailSize = stats[1]; 376 } catch (RemoteException e) { 377 Log.w(TAG, "Problem in container service", e); 378 } 379 380 sendInternalApproximateUpdate(); 381 } 382 383 private void measureExactStorage(IMediaContainerService imcs) { 384 Context context = mContext != null ? mContext.get() : null; 385 if (context == null) { 386 return; 387 } 388 389 // Media 390 for (int i = 0; i < StorageVolumePreferenceCategory.sMediaCategories.length; i++) { 391 if (mIsPrimary) { 392 String[] dirs = StorageVolumePreferenceCategory.sMediaCategories[i].mDirPaths; 393 final int length = dirs.length; 394 mMediaSizes[i] = 0; 395 for (int d = 0; d < length; d++) { 396 final String path = dirs[d]; 397 mMediaSizes[i] += getDirectorySize(imcs, path); 398 } 399 } else { 400 // TODO Compute sizes using the MediaStore 401 mMediaSizes[i] = 0; 402 } 403 } 404 405 /* Compute sizes using the media provider 406 // Media sizes are measured by the MediaStore. Query database. 407 ContentResolver contentResolver = context.getContentResolver(); 408 // TODO "external" as a static String from MediaStore? 409 Uri audioUri = MediaStore.Files.getContentUri("external"); 410 final String[] projection = 411 new String[] { "sum(" + MediaStore.Files.FileColumns.SIZE + ")" }; 412 final String selection = 413 MediaStore.Files.FileColumns.STORAGE_ID + "=" + 414 Integer.toString(mStorageVolume.getStorageId()) + " AND " + 415 MediaStore.Files.FileColumns.MEDIA_TYPE + "=?"; 416 417 for (int i = 0; i < StorageVolumePreferenceCategory.sMediaCategories.length; i++) { 418 mMediaSizes[i] = 0; 419 int mediaType = StorageVolumePreferenceCategory.sMediaCategories[i].mediaType; 420 Cursor c = null; 421 try { 422 c = contentResolver.query(audioUri, projection, selection, 423 new String[] { Integer.toString(mediaType) } , null); 424 425 if (c != null && c.moveToNext()) { 426 long size = c.getLong(0); 427 mMediaSizes[i] = size; 428 } 429 } finally { 430 if (c != null) c.close(); 431 } 432 } 433 */ 434 435 // Downloads (primary volume only) 436 if (mIsPrimary) { 437 final String downloadsPath = Environment.getExternalStoragePublicDirectory( 438 Environment.DIRECTORY_DOWNLOADS).getAbsolutePath(); 439 mDownloadsSize = getDirectorySize(imcs, downloadsPath); 440 } else { 441 mDownloadsSize = 0; 442 } 443 444 // Misc 445 mMiscSize = 0; 446 if (mIsPrimary) { 447 measureSizesOfMisc(imcs); 448 } 449 450 // Apps 451 // We have to get installd to measure the package sizes. 452 PackageManager pm = context.getPackageManager(); 453 if (pm == null) { 454 return; 455 } 456 final List<ApplicationInfo> apps; 457 if (mIsPrimary || mIsInternal) { 458 apps = pm.getInstalledApplications(PackageManager.GET_UNINSTALLED_PACKAGES | 459 PackageManager.GET_DISABLED_COMPONENTS); 460 } else { 461 // TODO also measure apps installed on the SD card 462 apps = Collections.emptyList(); 463 } 464 465 if (apps != null && apps.size() > 0) { 466 // initiate measurement of all package sizes. need new StatsObserver object. 467 mStatsObserver = new StatsObserver(); 468 synchronized (mStatsObserver.mAppsList) { 469 for (int i = 0; i < apps.size(); i++) { 470 final ApplicationInfo info = apps.get(i); 471 mStatsObserver.queuePackageMeasurementLocked(info.packageName); 472 } 473 } 474 475 requestQueuedMeasurementsLocked(pm); 476 // Sending of the message back to the MeasurementReceiver is 477 // completed in the PackageObserver 478 } else { 479 onInternalMeasurementComplete(); 480 } 481 } 482 } 483 484 private long getDirectorySize(IMediaContainerService imcs, String dir) { 485 try { 486 return imcs.calculateDirectorySize(dir); 487 } catch (Exception e) { 488 Log.w(TAG, "Could not read memory from default container service for " + dir, e); 489 return 0; 490 } 491 } 492 493 long getMiscSize() { 494 return mMiscSize; 495 } 496 497 private void measureSizesOfMisc(IMediaContainerService imcs) { 498 File top = new File(mStorageVolume.getPath()); 499 mFileInfoForMisc = new ArrayList<FileInfo>(); 500 File[] files = top.listFiles(); 501 if (files == null) return; 502 final int len = files.length; 503 // Get sizes of all top level nodes except the ones already computed... 504 long counter = 0; 505 for (int i = 0; i < len; i++) { 506 String path = files[i].getAbsolutePath(); 507 if (StorageVolumePreferenceCategory.sPathsExcludedForMisc.contains(path)) { 508 continue; 509 } 510 if (files[i].isFile()) { 511 final long fileSize = files[i].length(); 512 mFileInfoForMisc.add(new FileInfo(path, fileSize, counter++)); 513 mMiscSize += fileSize; 514 } else if (files[i].isDirectory()) { 515 final long dirSize = getDirectorySize(imcs, path); 516 mFileInfoForMisc.add(new FileInfo(path, dirSize, counter++)); 517 mMiscSize += dirSize; 518 } else { 519 // Non directory, non file: not listed 520 } 521 } 522 // sort the list of FileInfo objects collected above in descending order of their sizes 523 Collections.sort(mFileInfoForMisc); 524 } 525 526 static class FileInfo implements Comparable<FileInfo> { 527 final String mFileName; 528 final long mSize; 529 final long mId; 530 531 FileInfo(String fileName, long size, long id) { 532 mFileName = fileName; 533 mSize = size; 534 mId = id; 535 } 536 537 @Override 538 public int compareTo(FileInfo that) { 539 if (this == that || mSize == that.mSize) return 0; 540 else return (mSize < that.mSize) ? 1 : -1; // for descending sort 541 } 542 543 @Override 544 public String toString() { 545 return mFileName + " : " + mSize + ", id:" + mId; 546 } 547 } 548 549 /** 550 * TODO remove this method, only used because external SD Card needs a special treatment. 551 */ 552 boolean isExternalSDCard() { 553 return !mIsPrimary && !mIsInternal; 554 } 555 } 556