/* * Copyright (c) 2015-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ package com.facebook.common.statfs; import javax.annotation.Nullable; import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.ThreadSafe; import java.io.File; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import android.annotation.SuppressLint; import android.os.Build; import android.os.Environment; import android.os.StatFs; import android.os.SystemClock; import com.facebook.common.internal.Throwables; /** * Helper class that periodically checks the amount of free space available. * <p>To keep the overhead low, it caches the free space information, and * only updates that info after two minutes. * * <p>It is a singleton, and is thread-safe. * * <p>Initialization is delayed until first use, so the first call to any method may incur some * additional cost. */ @ThreadSafe public class StatFsHelper { public enum StorageType { INTERNAL, EXTERNAL }; private static StatFsHelper sStatsFsHelper; // Time interval for updating disk information private static final long RESTAT_INTERVAL_MS = TimeUnit.MINUTES.toMillis(2); private volatile StatFs mInternalStatFs = null; private volatile File mInternalPath; private volatile StatFs mExternalStatFs = null; private volatile File mExternalPath; @GuardedBy("lock") private long mLastRestatTime; private final Lock lock; private volatile boolean mInitialized = false; public synchronized static StatFsHelper getInstance() { if (sStatsFsHelper == null) { sStatsFsHelper = new StatFsHelper(); } return sStatsFsHelper; } /** * Constructor. * * <p>Initialization is delayed until first use, so we must call {@link #ensureInitialized()} * when implementing member methods. */ protected StatFsHelper() { lock = new ReentrantLock(); } /** * Initialization code that can sometimes take a long time. */ private void ensureInitialized() { if (!mInitialized) { lock.lock(); try { if (!mInitialized) { mInternalPath = Environment.getDataDirectory(); mExternalPath = Environment.getExternalStorageDirectory(); updateStats(); mInitialized = true; } } finally { lock.unlock(); } } } /** * Check if available space in the filesystem is greater than the given threshold. * Note that the free space stats are cached and updated in intervals of RESTAT_INTERVAL_MS. * If the amount of free space has crossed over the threshold since the last update, it will * return incorrect results till the space stats are updated again. * * @param storageType StorageType (internal or external) to test * @param freeSpaceThreshold compare the available free space to this size * @return whether free space is lower than the input freeSpaceThreshold, * returns true if disk information is not available */ public boolean testLowDiskSpace(StorageType storageType, long freeSpaceThreshold) { ensureInitialized(); long availableStorageSpace = getAvailableStorageSpace(storageType); if (availableStorageSpace > 0) { return availableStorageSpace < freeSpaceThreshold; } return true; } /** * Gets the information about the free storage space, including reserved blocks, * either internal or external depends on the given input * @param storageType Internal or external storage type * @return available space in bytes, -1 if no information is available */ @SuppressLint("DeprecatedMethod") public long getFreeStorageSpace(StorageType storageType) { ensureInitialized(); maybeUpdateStats(); StatFs statFS = storageType == StorageType.INTERNAL ? mInternalStatFs : mExternalStatFs; if (statFS != null) { long blockSize, availableBlocks; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { blockSize = statFS.getBlockSizeLong(); availableBlocks = statFS.getFreeBlocksLong(); } else { blockSize = statFS.getBlockSize(); availableBlocks = statFS.getFreeBlocks(); } return blockSize * availableBlocks; } return -1; } /** * Gets the information about the total storage space, * either internal or external depends on the given input * @param storageType Internal or external storage type * @return available space in bytes, -1 if no information is available */ @SuppressLint("DeprecatedMethod") public long getTotalStorageSpace(StorageType storageType) { ensureInitialized(); maybeUpdateStats(); StatFs statFS = storageType == StorageType.INTERNAL ? mInternalStatFs : mExternalStatFs; if (statFS != null) { long blockSize, totalBlocks; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { blockSize = statFS.getBlockSizeLong(); totalBlocks = statFS.getBlockCountLong(); } else { blockSize = statFS.getBlockSize(); totalBlocks = statFS.getBlockCount(); } return blockSize * totalBlocks; } return -1; } /** * Gets the information about the available storage space * either internal or external depends on the give input * @param storageType Internal or external storage type * @return available space in bytes, 0 if no information is available */ @SuppressLint("DeprecatedMethod") public long getAvailableStorageSpace(StorageType storageType) { ensureInitialized(); maybeUpdateStats(); StatFs statFS = storageType == StorageType.INTERNAL ? mInternalStatFs : mExternalStatFs; if (statFS != null) { long blockSize, availableBlocks; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { blockSize = statFS.getBlockSizeLong(); availableBlocks = statFS.getAvailableBlocksLong(); } else { blockSize = statFS.getBlockSize(); availableBlocks = statFS.getAvailableBlocks(); } return blockSize * availableBlocks; } return 0; } /** * Thread-safe call to update disk stats. Update occurs if the thread is able to acquire * the lock (i.e., no other thread is updating it at the same time), and it has been * at least RESTAT_INTERVAL_MS since the last update. * Assumes that initialization has been completed before this method is called. */ private void maybeUpdateStats() { // Update the free space if able to get the lock, // with a frequency of once in RESTAT_INTERVAL_MS if (lock.tryLock()) { try { if ((SystemClock.uptimeMillis() - mLastRestatTime) > RESTAT_INTERVAL_MS) { updateStats(); } } finally { lock.unlock(); } } } /** * Thread-safe call to reset the disk stats. * If we know that the free space has changed recently (for example, if we have * deleted files), use this method to reset the internal state and * start tracking disk stats afresh, resetting the internal timer for updating stats. */ public void resetStats() { // Update the free space if able to get the lock if (lock.tryLock()) { try { ensureInitialized(); updateStats(); } finally { lock.unlock(); } } } /** * (Re)calculate the stats. * It is the callers responsibility to ensure thread-safety. * Assumes that it is called after initialization (or at the end of it). */ @GuardedBy("lock") private void updateStats() { mInternalStatFs = updateStatsHelper(mInternalStatFs, mInternalPath); mExternalStatFs = updateStatsHelper(mExternalStatFs, mExternalPath); mLastRestatTime = SystemClock.uptimeMillis(); } /** * Update stats for a single directory and return the StatFs object for that directory. If the * directory does not exist or the StatFs restat() or constructor fails (throws), a null StatFs * object is returned. */ private StatFs updateStatsHelper(@Nullable StatFs statfs, @Nullable File dir) { if(dir == null || !dir.exists()) { // The path does not exist, do not track stats for it. return null; } try { if (statfs == null) { // Create a new StatFs object for this path. statfs = createStatFs(dir.getAbsolutePath()); } else { // Call restat and keep the existing StatFs object. statfs.restat(dir.getAbsolutePath()); } } catch (IllegalArgumentException ex) { // Invalidate the StatFs object for this directory. The native StatFs implementation throws // IllegalArgumentException in the case that the statfs() system call fails and it invalidates // its internal data structures so subsequent calls against the StatFs object will fail or // throw (so we should make no more calls on the object). The most likely reason for this call // to fail is because the provided path no longer exists. The next call to updateStats() will // a new statfs object if the path exists. This will handle the case that a path is unmounted // and later remounted (but it has to have been mounted when this object was initialized). statfs = null; } catch (Throwable ex) { // Any other exception types are not expected and should be propagated as runtime errors. throw Throwables.propagate(ex); } return statfs; } protected static StatFs createStatFs(String path) { return new StatFs(path); } }