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 17package com.android.providers.downloads; 18 19import static android.net.TrafficStats.MB_IN_BYTES; 20import static android.provider.Downloads.Impl.STATUS_INSUFFICIENT_SPACE_ERROR; 21import static android.text.format.DateUtils.DAY_IN_MILLIS; 22import static com.android.providers.downloads.Constants.TAG; 23 24import android.app.DownloadManager; 25import android.content.ContentResolver; 26import android.content.ContentUris; 27import android.content.Context; 28import android.content.pm.IPackageDataObserver; 29import android.content.pm.PackageManager; 30import android.database.Cursor; 31import android.os.Environment; 32import android.provider.Downloads; 33import android.system.ErrnoException; 34import android.system.Os; 35import android.system.StructStat; 36import android.system.StructStatVfs; 37import android.text.TextUtils; 38import android.util.Slog; 39 40import com.android.internal.annotations.VisibleForTesting; 41import com.google.android.collect.Lists; 42import com.google.android.collect.Sets; 43 44import libcore.io.IoUtils; 45 46import java.io.File; 47import java.io.FileDescriptor; 48import java.io.IOException; 49import java.util.ArrayList; 50import java.util.Collections; 51import java.util.Comparator; 52import java.util.HashSet; 53import java.util.LinkedList; 54import java.util.List; 55import java.util.Objects; 56import java.util.concurrent.CountDownLatch; 57import java.util.concurrent.TimeUnit; 58 59/** 60 * Utility methods for managing storage space related to 61 * {@link DownloadManager}. 62 */ 63public class StorageUtils { 64 65 /** 66 * Minimum age for a file to be considered for deletion. 67 */ 68 static final long MIN_DELETE_AGE = DAY_IN_MILLIS; 69 70 /** 71 * Reserved disk space to avoid filling disk. 72 */ 73 static final long RESERVED_BYTES = 32 * MB_IN_BYTES; 74 75 @VisibleForTesting 76 static boolean sForceFullEviction = false; 77 78 /** 79 * Ensure that requested free space exists on the partition backing the 80 * given {@link FileDescriptor}. If not enough space is available, it tries 81 * freeing up space as follows: 82 * <ul> 83 * <li>If backed by the data partition (including emulated external 84 * storage), then ask {@link PackageManager} to free space from cache 85 * directories. 86 * <li>If backed by the cache partition, then try deleting older downloads 87 * to free space. 88 * </ul> 89 */ 90 public static void ensureAvailableSpace(Context context, FileDescriptor fd, long bytes) 91 throws IOException, StopRequestException { 92 93 long availBytes = getAvailableBytes(fd); 94 if (availBytes >= bytes) { 95 // Underlying partition has enough space; go ahead 96 return; 97 } 98 99 // Not enough space, let's try freeing some up. Start by tracking down 100 // the backing partition. 101 final long dev; 102 try { 103 dev = Os.fstat(fd).st_dev; 104 } catch (ErrnoException e) { 105 throw e.rethrowAsIOException(); 106 } 107 108 final long dataDev = getDeviceId(Environment.getDataDirectory()); 109 final long cacheDev = getDeviceId(Environment.getDownloadCacheDirectory()); 110 final long externalDev = getDeviceId(Environment.getExternalStorageDirectory()); 111 112 if (dev == dataDev || (dev == externalDev && Environment.isExternalStorageEmulated())) { 113 // File lives on internal storage; ask PackageManager to try freeing 114 // up space from cache directories. 115 final PackageManager pm = context.getPackageManager(); 116 final ObserverLatch observer = new ObserverLatch(); 117 pm.freeStorageAndNotify(sForceFullEviction ? Long.MAX_VALUE : bytes, observer); 118 119 try { 120 if (!observer.latch.await(30, TimeUnit.SECONDS)) { 121 throw new IOException("Timeout while freeing disk space"); 122 } 123 } catch (InterruptedException e) { 124 Thread.currentThread().interrupt(); 125 } 126 127 } else if (dev == cacheDev) { 128 // Try removing old files on cache partition 129 freeCacheStorage(bytes); 130 } 131 132 // Did we free enough space? 133 availBytes = getAvailableBytes(fd); 134 if (availBytes < bytes) { 135 throw new StopRequestException(STATUS_INSUFFICIENT_SPACE_ERROR, 136 "Not enough free space; " + bytes + " requested, " + availBytes + " available"); 137 } 138 } 139 140 /** 141 * Free requested space on cache partition, deleting oldest files first. 142 * We're only focused on freeing up disk space, and rely on the next orphan 143 * pass to clean up database entries. 144 */ 145 private static void freeCacheStorage(long bytes) { 146 // Only consider finished downloads 147 final List<ConcreteFile> files = listFilesRecursive( 148 Environment.getDownloadCacheDirectory(), Constants.DIRECTORY_CACHE_RUNNING, 149 android.os.Process.myUid()); 150 151 Slog.d(TAG, "Found " + files.size() + " downloads on cache"); 152 153 Collections.sort(files, new Comparator<ConcreteFile>() { 154 @Override 155 public int compare(ConcreteFile lhs, ConcreteFile rhs) { 156 return (int) (lhs.file.lastModified() - rhs.file.lastModified()); 157 } 158 }); 159 160 final long now = System.currentTimeMillis(); 161 for (ConcreteFile file : files) { 162 if (bytes <= 0) break; 163 164 if (now - file.file.lastModified() < MIN_DELETE_AGE) { 165 Slog.d(TAG, "Skipping recently modified " + file.file); 166 } else { 167 final long len = file.file.length(); 168 Slog.d(TAG, "Deleting " + file.file + " to reclaim " + len); 169 bytes -= len; 170 file.file.delete(); 171 } 172 } 173 } 174 175 /** 176 * Return number of available bytes on the filesystem backing the given 177 * {@link FileDescriptor}, minus any {@link #RESERVED_BYTES} buffer. 178 */ 179 private static long getAvailableBytes(FileDescriptor fd) throws IOException { 180 try { 181 final StructStatVfs stat = Os.fstatvfs(fd); 182 return (stat.f_bavail * stat.f_bsize) - RESERVED_BYTES; 183 } catch (ErrnoException e) { 184 throw e.rethrowAsIOException(); 185 } 186 } 187 188 private static long getDeviceId(File file) { 189 try { 190 return Os.stat(file.getAbsolutePath()).st_dev; 191 } catch (ErrnoException e) { 192 // Safe since dev_t is uint 193 return -1; 194 } 195 } 196 197 /** 198 * Return list of all normal files under the given directory, traversing 199 * directories recursively. 200 * 201 * @param exclude ignore dirs with this name, or {@code null} to ignore. 202 * @param uid only return files owned by this UID, or {@code -1} to ignore. 203 */ 204 static List<ConcreteFile> listFilesRecursive(File startDir, String exclude, int uid) { 205 final ArrayList<ConcreteFile> files = Lists.newArrayList(); 206 final LinkedList<File> dirs = new LinkedList<File>(); 207 dirs.add(startDir); 208 while (!dirs.isEmpty()) { 209 final File dir = dirs.removeFirst(); 210 if (Objects.equals(dir.getName(), exclude)) continue; 211 212 final File[] children = dir.listFiles(); 213 if (children == null) continue; 214 215 for (File child : children) { 216 if (child.isDirectory()) { 217 dirs.add(child); 218 } else if (child.isFile()) { 219 try { 220 final ConcreteFile file = new ConcreteFile(child); 221 if (uid == -1 || file.stat.st_uid == uid) { 222 files.add(file); 223 } 224 } catch (ErrnoException ignored) { 225 } 226 } 227 } 228 } 229 return files; 230 } 231 232 /** 233 * Concrete file on disk that has a backing device and inode. Faster than 234 * {@code realpath()} when looking for identical files. 235 */ 236 static class ConcreteFile { 237 public final File file; 238 public final StructStat stat; 239 240 public ConcreteFile(File file) throws ErrnoException { 241 this.file = file; 242 this.stat = Os.lstat(file.getAbsolutePath()); 243 } 244 245 @Override 246 public int hashCode() { 247 int result = 1; 248 result = 31 * result + (int) (stat.st_dev ^ (stat.st_dev >>> 32)); 249 result = 31 * result + (int) (stat.st_ino ^ (stat.st_ino >>> 32)); 250 return result; 251 } 252 253 @Override 254 public boolean equals(Object o) { 255 if (o instanceof ConcreteFile) { 256 final ConcreteFile f = (ConcreteFile) o; 257 return (f.stat.st_dev == stat.st_dev) && (f.stat.st_ino == stat.st_ino); 258 } 259 return false; 260 } 261 } 262 263 static class ObserverLatch extends IPackageDataObserver.Stub { 264 public final CountDownLatch latch = new CountDownLatch(1); 265 266 @Override 267 public void onRemoveCompleted(String packageName, boolean succeeded) { 268 latch.countDown(); 269 } 270 } 271} 272