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