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