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