blob: 3bb57c8ee7aed73f0c7623ec36d75807cfcc91af [file] [log] [blame]
/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.providers.downloads;
import static android.net.TrafficStats.MB_IN_BYTES;
import static android.provider.Downloads.Impl.STATUS_INSUFFICIENT_SPACE_ERROR;
import static android.text.format.DateUtils.DAY_IN_MILLIS;
import static com.android.providers.downloads.Constants.TAG;
import android.app.DownloadManager;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
import android.content.pm.IPackageDataObserver;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.os.Environment;
import android.provider.Downloads;
import android.system.ErrnoException;
import android.system.Os;
import android.system.StructStat;
import android.system.StructStatVfs;
import android.text.TextUtils;
import android.util.Slog;
import com.android.internal.annotations.VisibleForTesting;
import com.google.android.collect.Lists;
import com.google.android.collect.Sets;
import libcore.io.IoUtils;
import java.io.File;
import java.io.FileDescriptor;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
* Utility methods for managing storage space related to
* {@link DownloadManager}.
*/
public class StorageUtils {
/**
* Minimum age for a file to be considered for deletion.
*/
static final long MIN_DELETE_AGE = DAY_IN_MILLIS;
/**
* Reserved disk space to avoid filling disk.
*/
static final long RESERVED_BYTES = 32 * MB_IN_BYTES;
@VisibleForTesting
static boolean sForceFullEviction = false;
/**
* Ensure that requested free space exists on the partition backing the
* given {@link FileDescriptor}. If not enough space is available, it tries
* freeing up space as follows:
* <ul>
* <li>If backed by the data partition (including emulated external
* storage), then ask {@link PackageManager} to free space from cache
* directories.
* <li>If backed by the cache partition, then try deleting older downloads
* to free space.
* </ul>
*/
public static void ensureAvailableSpace(Context context, FileDescriptor fd, long bytes)
throws IOException, StopRequestException {
long availBytes = getAvailableBytes(fd);
if (availBytes >= bytes) {
// Underlying partition has enough space; go ahead
return;
}
// Not enough space, let's try freeing some up. Start by tracking down
// the backing partition.
final long dev;
try {
dev = Os.fstat(fd).st_dev;
} catch (ErrnoException e) {
throw e.rethrowAsIOException();
}
// TODO: teach about evicting caches on adopted secondary storage devices
final long dataDev = getDeviceId(Environment.getDataDirectory());
final long cacheDev = getDeviceId(Environment.getDownloadCacheDirectory());
final long externalDev = getDeviceId(Environment.getExternalStorageDirectory());
if (dev == dataDev || (dev == externalDev && Environment.isExternalStorageEmulated())) {
// File lives on internal storage; ask PackageManager to try freeing
// up space from cache directories.
final PackageManager pm = context.getPackageManager();
final ObserverLatch observer = new ObserverLatch();
pm.freeStorageAndNotify(sForceFullEviction ? Long.MAX_VALUE : bytes, observer);
try {
if (!observer.latch.await(30, TimeUnit.SECONDS)) {
throw new IOException("Timeout while freeing disk space");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
} else if (dev == cacheDev) {
// Try removing old files on cache partition
freeCacheStorage(bytes);
}
// Did we free enough space?
availBytes = getAvailableBytes(fd);
if (availBytes < bytes) {
throw new StopRequestException(STATUS_INSUFFICIENT_SPACE_ERROR,
"Not enough free space; " + bytes + " requested, " + availBytes + " available");
}
}
/**
* Free requested space on cache partition, deleting oldest files first.
* We're only focused on freeing up disk space, and rely on the next orphan
* pass to clean up database entries.
*/
private static void freeCacheStorage(long bytes) {
// Only consider finished downloads
final List<ConcreteFile> files = listFilesRecursive(
Environment.getDownloadCacheDirectory(), Constants.DIRECTORY_CACHE_RUNNING,
android.os.Process.myUid());
Slog.d(TAG, "Found " + files.size() + " downloads on cache");
Collections.sort(files, new Comparator<ConcreteFile>() {
@Override
public int compare(ConcreteFile lhs, ConcreteFile rhs) {
return (int) (lhs.file.lastModified() - rhs.file.lastModified());
}
});
final long now = System.currentTimeMillis();
for (ConcreteFile file : files) {
if (bytes <= 0) break;
if (now - file.file.lastModified() < MIN_DELETE_AGE) {
Slog.d(TAG, "Skipping recently modified " + file.file);
} else {
final long len = file.file.length();
Slog.d(TAG, "Deleting " + file.file + " to reclaim " + len);
bytes -= len;
file.file.delete();
}
}
}
/**
* Return number of available bytes on the filesystem backing the given
* {@link FileDescriptor}, minus any {@link #RESERVED_BYTES} buffer.
*/
private static long getAvailableBytes(FileDescriptor fd) throws IOException {
try {
final StructStatVfs stat = Os.fstatvfs(fd);
return (stat.f_bavail * stat.f_bsize) - RESERVED_BYTES;
} catch (ErrnoException e) {
throw e.rethrowAsIOException();
}
}
private static long getDeviceId(File file) {
try {
return Os.stat(file.getAbsolutePath()).st_dev;
} catch (ErrnoException e) {
// Safe since dev_t is uint
return -1;
}
}
/**
* Return list of all normal files under the given directory, traversing
* directories recursively.
*
* @param exclude ignore dirs with this name, or {@code null} to ignore.
* @param uid only return files owned by this UID, or {@code -1} to ignore.
*/
static List<ConcreteFile> listFilesRecursive(File startDir, String exclude, int uid) {
final ArrayList<ConcreteFile> files = Lists.newArrayList();
final LinkedList<File> dirs = new LinkedList<File>();
dirs.add(startDir);
while (!dirs.isEmpty()) {
final File dir = dirs.removeFirst();
if (Objects.equals(dir.getName(), exclude)) continue;
final File[] children = dir.listFiles();
if (children == null) continue;
for (File child : children) {
if (child.isDirectory()) {
dirs.add(child);
} else if (child.isFile()) {
try {
final ConcreteFile file = new ConcreteFile(child);
if (uid == -1 || file.stat.st_uid == uid) {
files.add(file);
}
} catch (ErrnoException ignored) {
}
}
}
}
return files;
}
/**
* Concrete file on disk that has a backing device and inode. Faster than
* {@code realpath()} when looking for identical files.
*/
static class ConcreteFile {
public final File file;
public final StructStat stat;
public ConcreteFile(File file) throws ErrnoException {
this.file = file;
this.stat = Os.lstat(file.getAbsolutePath());
}
@Override
public int hashCode() {
int result = 1;
result = 31 * result + (int) (stat.st_dev ^ (stat.st_dev >>> 32));
result = 31 * result + (int) (stat.st_ino ^ (stat.st_ino >>> 32));
return result;
}
@Override
public boolean equals(Object o) {
if (o instanceof ConcreteFile) {
final ConcreteFile f = (ConcreteFile) o;
return (f.stat.st_dev == stat.st_dev) && (f.stat.st_ino == stat.st_ino);
}
return false;
}
}
static class ObserverLatch extends IPackageDataObserver.Stub {
public final CountDownLatch latch = new CountDownLatch(1);
@Override
public void onRemoveCompleted(String packageName, boolean succeeded) {
latch.countDown();
}
}
}