/* * 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: * */ 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 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() { @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 listFilesRecursive(File startDir, String exclude, int uid) { final ArrayList files = Lists.newArrayList(); final LinkedList dirs = new LinkedList(); 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(); } } }