DropBoxManagerService.java revision 8a9b22056b13477f59df934928c00c58b5871c95
1/*
2 * Copyright (C) 2009 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.server;
18
19import android.content.BroadcastReceiver;
20import android.content.ContentResolver;
21import android.content.Context;
22import android.content.Intent;
23import android.content.IntentFilter;
24import android.content.pm.PackageManager;
25import android.database.ContentObserver;
26import android.net.Uri;
27import android.os.Debug;
28import android.os.DropBoxManager;
29import android.os.Handler;
30import android.os.ParcelFileDescriptor;
31import android.os.StatFs;
32import android.os.SystemClock;
33import android.provider.Settings;
34import android.text.format.Time;
35import android.util.Slog;
36
37import com.android.internal.os.IDropBoxManagerService;
38
39import java.io.File;
40import java.io.FileDescriptor;
41import java.io.FileOutputStream;
42import java.io.IOException;
43import java.io.InputStream;
44import java.io.InputStreamReader;
45import java.io.OutputStream;
46import java.io.OutputStreamWriter;
47import java.io.PrintWriter;
48import java.io.UnsupportedEncodingException;
49import java.util.ArrayList;
50import java.util.Comparator;
51import java.util.HashMap;
52import java.util.Iterator;
53import java.util.Map;
54import java.util.SortedSet;
55import java.util.TreeSet;
56import java.util.zip.GZIPOutputStream;
57
58/**
59 * Implementation of {@link IDropBoxManagerService} using the filesystem.
60 * Clients use {@link DropBoxManager} to access this service.
61 */
62public final class DropBoxManagerService extends IDropBoxManagerService.Stub {
63    private static final String TAG = "DropBoxManagerService";
64    private static final int DEFAULT_RESERVE_PERCENT = 10;
65    private static final int DEFAULT_QUOTA_PERCENT = 10;
66    private static final int DEFAULT_QUOTA_KB = 5 * 1024;
67    private static final int DEFAULT_AGE_SECONDS = 3 * 86400;
68    private static final int QUOTA_RESCAN_MILLIS = 5000;
69
70    private static final boolean PROFILE_DUMP = false;
71
72    // TODO: This implementation currently uses one file per entry, which is
73    // inefficient for smallish entries -- consider using a single queue file
74    // per tag (or even globally) instead.
75
76    // The cached context and derived objects
77
78    private final Context mContext;
79    private final ContentResolver mContentResolver;
80    private final File mDropBoxDir;
81
82    // Accounting of all currently written log files (set in init()).
83
84    private FileList mAllFiles = null;
85    private HashMap<String, FileList> mFilesByTag = null;
86
87    // Various bits of disk information
88
89    private StatFs mStatFs = null;
90    private int mBlockSize = 0;
91    private int mCachedQuotaBlocks = 0;  // Space we can use: computed from free space, etc.
92    private long mCachedQuotaUptimeMillis = 0;
93
94    // Ensure that all log entries have a unique timestamp
95    private long mLastTimestamp = 0;
96
97    /** Receives events that might indicate a need to clean up files. */
98    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
99        @Override
100        public void onReceive(Context context, Intent intent) {
101            mCachedQuotaUptimeMillis = 0;  // Force a re-check of quota size
102            try {
103                init();
104                trimToFit();
105            } catch (IOException e) {
106                Slog.e(TAG, "Can't init", e);
107            }
108        }
109    };
110
111    /**
112     * Creates an instance of managed drop box storage.  Normally there is one of these
113     * run by the system, but others can be created for testing and other purposes.
114     *
115     * @param context to use for receiving free space & gservices intents
116     * @param path to store drop box entries in
117     */
118    public DropBoxManagerService(final Context context, File path) {
119        mDropBoxDir = path;
120
121        // Set up intent receivers
122        mContext = context;
123        mContentResolver = context.getContentResolver();
124        context.registerReceiver(mReceiver, new IntentFilter(Intent.ACTION_DEVICE_STORAGE_LOW));
125
126        mContentResolver.registerContentObserver(
127            Settings.Secure.CONTENT_URI, true,
128            new ContentObserver(new Handler()) {
129                public void onChange(boolean selfChange) {
130                    mReceiver.onReceive(context, (Intent) null);
131                }
132            });
133
134        // The real work gets done lazily in init() -- that way service creation always
135        // succeeds, and things like disk problems cause individual method failures.
136    }
137
138    /** Unregisters broadcast receivers and any other hooks -- for test instances */
139    public void stop() {
140        mContext.unregisterReceiver(mReceiver);
141    }
142
143    public void add(DropBoxManager.Entry entry) {
144        File temp = null;
145        OutputStream output = null;
146        final String tag = entry.getTag();
147        try {
148            int flags = entry.getFlags();
149            if ((flags & DropBoxManager.IS_EMPTY) != 0) throw new IllegalArgumentException();
150
151            init();
152            if (!isTagEnabled(tag)) return;
153            long max = trimToFit();
154            long lastTrim = System.currentTimeMillis();
155
156            byte[] buffer = new byte[mBlockSize];
157            InputStream input = entry.getInputStream();
158
159            // First, accumulate up to one block worth of data in memory before
160            // deciding whether to compress the data or not.
161
162            int read = 0;
163            while (read < buffer.length) {
164                int n = input.read(buffer, read, buffer.length - read);
165                if (n <= 0) break;
166                read += n;
167            }
168
169            // If we have at least one block, compress it -- otherwise, just write
170            // the data in uncompressed form.
171
172            temp = new File(mDropBoxDir, "drop" + Thread.currentThread().getId() + ".tmp");
173            output = new FileOutputStream(temp);
174            if (read == buffer.length && ((flags & DropBoxManager.IS_GZIPPED) == 0)) {
175                output = new GZIPOutputStream(output);
176                flags = flags | DropBoxManager.IS_GZIPPED;
177            }
178
179            do {
180                output.write(buffer, 0, read);
181
182                long now = System.currentTimeMillis();
183                if (now - lastTrim > 30 * 1000) {
184                    max = trimToFit();  // In case data dribbles in slowly
185                    lastTrim = now;
186                }
187
188                read = input.read(buffer);
189                if (read <= 0) {
190                    output.close();  // Get a final size measurement
191                    output = null;
192                } else {
193                    output.flush();  // So the size measurement is pseudo-reasonable
194                }
195
196                long len = temp.length();
197                if (len > max) {
198                    Slog.w(TAG, "Dropping: " + tag + " (" + temp.length() + " > " + max + " bytes)");
199                    temp.delete();
200                    temp = null;  // Pass temp = null to createEntry() to leave a tombstone
201                    break;
202                }
203            } while (read > 0);
204
205            createEntry(temp, tag, flags);
206            temp = null;
207        } catch (IOException e) {
208            Slog.e(TAG, "Can't write: " + tag, e);
209        } finally {
210            try { if (output != null) output.close(); } catch (IOException e) {}
211            entry.close();
212            if (temp != null) temp.delete();
213        }
214    }
215
216    public boolean isTagEnabled(String tag) {
217        return !"disabled".equals(Settings.Secure.getString(
218                mContentResolver, Settings.Secure.DROPBOX_TAG_PREFIX + tag));
219    }
220
221    public synchronized DropBoxManager.Entry getNextEntry(String tag, long millis) {
222        if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.READ_LOGS)
223                != PackageManager.PERMISSION_GRANTED) {
224            throw new SecurityException("READ_LOGS permission required");
225        }
226
227        try {
228            init();
229        } catch (IOException e) {
230            Slog.e(TAG, "Can't init", e);
231            return null;
232        }
233
234        FileList list = tag == null ? mAllFiles : mFilesByTag.get(tag);
235        if (list == null) return null;
236
237        for (EntryFile entry : list.contents.tailSet(new EntryFile(millis + 1))) {
238            if (entry.tag == null) continue;
239            if ((entry.flags & DropBoxManager.IS_EMPTY) != 0) {
240                return new DropBoxManager.Entry(entry.tag, entry.timestampMillis);
241            }
242            try {
243                return new DropBoxManager.Entry(
244                        entry.tag, entry.timestampMillis, entry.file, entry.flags);
245            } catch (IOException e) {
246                Slog.e(TAG, "Can't read: " + entry.file, e);
247                // Continue to next file
248            }
249        }
250
251        return null;
252    }
253
254    public synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
255        if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP)
256                != PackageManager.PERMISSION_GRANTED) {
257            pw.println("Permission Denial: Can't dump DropBoxManagerService");
258            return;
259        }
260
261        try {
262            init();
263        } catch (IOException e) {
264            pw.println("Can't initialize: " + e);
265            Slog.e(TAG, "Can't init", e);
266            return;
267        }
268
269        if (PROFILE_DUMP) Debug.startMethodTracing("/data/trace/dropbox.dump");
270
271        StringBuilder out = new StringBuilder();
272        boolean doPrint = false, doFile = false;
273        ArrayList<String> searchArgs = new ArrayList<String>();
274        for (int i = 0; args != null && i < args.length; i++) {
275            if (args[i].equals("-p") || args[i].equals("--print")) {
276                doPrint = true;
277            } else if (args[i].equals("-f") || args[i].equals("--file")) {
278                doFile = true;
279            } else if (args[i].startsWith("-")) {
280                out.append("Unknown argument: ").append(args[i]).append("\n");
281            } else {
282                searchArgs.add(args[i]);
283            }
284        }
285
286        out.append("Drop box contents: ").append(mAllFiles.contents.size()).append(" entries\n");
287
288        if (!searchArgs.isEmpty()) {
289            out.append("Searching for:");
290            for (String a : searchArgs) out.append(" ").append(a);
291            out.append("\n");
292        }
293
294        int numFound = 0, numArgs = searchArgs.size();
295        Time time = new Time();
296        out.append("\n");
297        for (EntryFile entry : mAllFiles.contents) {
298            time.set(entry.timestampMillis);
299            String date = time.format("%Y-%m-%d %H:%M:%S");
300            boolean match = true;
301            for (int i = 0; i < numArgs && match; i++) {
302                String arg = searchArgs.get(i);
303                match = (date.contains(arg) || arg.equals(entry.tag));
304            }
305            if (!match) continue;
306
307            numFound++;
308            if (doPrint) out.append("========================================\n");
309            out.append(date).append(" ").append(entry.tag == null ? "(no tag)" : entry.tag);
310            if (entry.file == null) {
311                out.append(" (no file)\n");
312                continue;
313            } else if ((entry.flags & DropBoxManager.IS_EMPTY) != 0) {
314                out.append(" (contents lost)\n");
315                continue;
316            } else {
317                out.append(" (");
318                if ((entry.flags & DropBoxManager.IS_GZIPPED) != 0) out.append("compressed ");
319                out.append((entry.flags & DropBoxManager.IS_TEXT) != 0 ? "text" : "data");
320                out.append(", ").append(entry.file.length()).append(" bytes)\n");
321            }
322
323            if (doFile || (doPrint && (entry.flags & DropBoxManager.IS_TEXT) == 0)) {
324                if (!doPrint) out.append("    ");
325                out.append(entry.file.getPath()).append("\n");
326            }
327
328            if ((entry.flags & DropBoxManager.IS_TEXT) != 0 && (doPrint || !doFile)) {
329                DropBoxManager.Entry dbe = null;
330                try {
331                    dbe = new DropBoxManager.Entry(
332                             entry.tag, entry.timestampMillis, entry.file, entry.flags);
333
334                    if (doPrint) {
335                        InputStreamReader r = new InputStreamReader(dbe.getInputStream());
336                        char[] buf = new char[4096];
337                        boolean newline = false;
338                        for (;;) {
339                            int n = r.read(buf);
340                            if (n <= 0) break;
341                            out.append(buf, 0, n);
342                            newline = (buf[n - 1] == '\n');
343
344                            // Flush periodically when printing to avoid out-of-memory.
345                            if (out.length() > 65536) {
346                                pw.write(out.toString());
347                                out.setLength(0);
348                            }
349                        }
350                        if (!newline) out.append("\n");
351                    } else {
352                        String text = dbe.getText(70);
353                        boolean truncated = (text.length() == 70);
354                        out.append("    ").append(text.trim().replace('\n', '/'));
355                        if (truncated) out.append(" ...");
356                        out.append("\n");
357                    }
358                } catch (IOException e) {
359                    out.append("*** ").append(e.toString()).append("\n");
360                    Slog.e(TAG, "Can't read: " + entry.file, e);
361                } finally {
362                    if (dbe != null) dbe.close();
363                }
364            }
365
366            if (doPrint) out.append("\n");
367        }
368
369        if (numFound == 0) out.append("(No entries found.)\n");
370
371        if (args == null || args.length == 0) {
372            if (!doPrint) out.append("\n");
373            out.append("Usage: dumpsys dropbox [--print|--file] [YYYY-mm-dd] [HH:MM:SS] [tag]\n");
374        }
375
376        pw.write(out.toString());
377        if (PROFILE_DUMP) Debug.stopMethodTracing();
378    }
379
380    ///////////////////////////////////////////////////////////////////////////
381
382    /** Chronologically sorted list of {@link #EntryFile} */
383    private static final class FileList implements Comparable<FileList> {
384        public int blocks = 0;
385        public final TreeSet<EntryFile> contents = new TreeSet<EntryFile>();
386
387        /** Sorts bigger FileList instances before smaller ones. */
388        public final int compareTo(FileList o) {
389            if (blocks != o.blocks) return o.blocks - blocks;
390            if (this == o) return 0;
391            if (hashCode() < o.hashCode()) return -1;
392            if (hashCode() > o.hashCode()) return 1;
393            return 0;
394        }
395    }
396
397    /** Metadata describing an on-disk log file. */
398    private static final class EntryFile implements Comparable<EntryFile> {
399        public final String tag;
400        public final long timestampMillis;
401        public final int flags;
402        public final File file;
403        public final int blocks;
404
405        /** Sorts earlier EntryFile instances before later ones. */
406        public final int compareTo(EntryFile o) {
407            if (timestampMillis < o.timestampMillis) return -1;
408            if (timestampMillis > o.timestampMillis) return 1;
409            if (file != null && o.file != null) return file.compareTo(o.file);
410            if (o.file != null) return -1;
411            if (file != null) return 1;
412            if (this == o) return 0;
413            if (hashCode() < o.hashCode()) return -1;
414            if (hashCode() > o.hashCode()) return 1;
415            return 0;
416        }
417
418        /**
419         * Moves an existing temporary file to a new log filename.
420         * @param temp file to rename
421         * @param dir to store file in
422         * @param tag to use for new log file name
423         * @param timestampMillis of log entry
424         * @param flags for the entry data
425         * @param blockSize to use for space accounting
426         * @throws IOException if the file can't be moved
427         */
428        public EntryFile(File temp, File dir, String tag,long timestampMillis,
429                         int flags, int blockSize) throws IOException {
430            if ((flags & DropBoxManager.IS_EMPTY) != 0) throw new IllegalArgumentException();
431
432            this.tag = tag;
433            this.timestampMillis = timestampMillis;
434            this.flags = flags;
435            this.file = new File(dir, Uri.encode(tag) + "@" + timestampMillis +
436                    ((flags & DropBoxManager.IS_TEXT) != 0 ? ".txt" : ".dat") +
437                    ((flags & DropBoxManager.IS_GZIPPED) != 0 ? ".gz" : ""));
438
439            if (!temp.renameTo(this.file)) {
440                throw new IOException("Can't rename " + temp + " to " + this.file);
441            }
442            this.blocks = (int) ((this.file.length() + blockSize - 1) / blockSize);
443        }
444
445        /**
446         * Creates a zero-length tombstone for a file whose contents were lost.
447         * @param dir to store file in
448         * @param tag to use for new log file name
449         * @param timestampMillis of log entry
450         * @throws IOException if the file can't be created.
451         */
452        public EntryFile(File dir, String tag, long timestampMillis) throws IOException {
453            this.tag = tag;
454            this.timestampMillis = timestampMillis;
455            this.flags = DropBoxManager.IS_EMPTY;
456            this.file = new File(dir, Uri.encode(tag) + "@" + timestampMillis + ".lost");
457            this.blocks = 0;
458            new FileOutputStream(this.file).close();
459        }
460
461        /**
462         * Extracts metadata from an existing on-disk log filename.
463         * @param file name of existing log file
464         * @param blockSize to use for space accounting
465         */
466        public EntryFile(File file, int blockSize) {
467            this.file = file;
468            this.blocks = (int) ((this.file.length() + blockSize - 1) / blockSize);
469
470            String name = file.getName();
471            int at = name.lastIndexOf('@');
472            if (at < 0) {
473                this.tag = null;
474                this.timestampMillis = 0;
475                this.flags = DropBoxManager.IS_EMPTY;
476                return;
477            }
478
479            int flags = 0;
480            this.tag = Uri.decode(name.substring(0, at));
481            if (name.endsWith(".gz")) {
482                flags |= DropBoxManager.IS_GZIPPED;
483                name = name.substring(0, name.length() - 3);
484            }
485            if (name.endsWith(".lost")) {
486                flags |= DropBoxManager.IS_EMPTY;
487                name = name.substring(at + 1, name.length() - 5);
488            } else if (name.endsWith(".txt")) {
489                flags |= DropBoxManager.IS_TEXT;
490                name = name.substring(at + 1, name.length() - 4);
491            } else if (name.endsWith(".dat")) {
492                name = name.substring(at + 1, name.length() - 4);
493            } else {
494                this.flags = DropBoxManager.IS_EMPTY;
495                this.timestampMillis = 0;
496                return;
497            }
498            this.flags = flags;
499
500            long millis;
501            try { millis = Long.valueOf(name); } catch (NumberFormatException e) { millis = 0; }
502            this.timestampMillis = millis;
503        }
504
505        /**
506         * Creates a EntryFile object with only a timestamp for comparison purposes.
507         * @param timestampMillis to compare with.
508         */
509        public EntryFile(long millis) {
510            this.tag = null;
511            this.timestampMillis = millis;
512            this.flags = DropBoxManager.IS_EMPTY;
513            this.file = null;
514            this.blocks = 0;
515        }
516    }
517
518    ///////////////////////////////////////////////////////////////////////////
519
520    /** If never run before, scans disk contents to build in-memory tracking data. */
521    private synchronized void init() throws IOException {
522        if (mStatFs == null) {
523            if (!mDropBoxDir.isDirectory() && !mDropBoxDir.mkdirs()) {
524                throw new IOException("Can't mkdir: " + mDropBoxDir);
525            }
526            try {
527                mStatFs = new StatFs(mDropBoxDir.getPath());
528                mBlockSize = mStatFs.getBlockSize();
529            } catch (IllegalArgumentException e) {  // StatFs throws this on error
530                throw new IOException("Can't statfs: " + mDropBoxDir);
531            }
532        }
533
534        if (mAllFiles == null) {
535            File[] files = mDropBoxDir.listFiles();
536            if (files == null) throw new IOException("Can't list files: " + mDropBoxDir);
537
538            mAllFiles = new FileList();
539            mFilesByTag = new HashMap<String, FileList>();
540
541            // Scan pre-existing files.
542            for (File file : files) {
543                if (file.getName().endsWith(".tmp")) {
544                    Slog.i(TAG, "Cleaning temp file: " + file);
545                    file.delete();
546                    continue;
547                }
548
549                EntryFile entry = new EntryFile(file, mBlockSize);
550                if (entry.tag == null) {
551                    Slog.w(TAG, "Unrecognized file: " + file);
552                    continue;
553                } else if (entry.timestampMillis == 0) {
554                    Slog.w(TAG, "Invalid filename: " + file);
555                    file.delete();
556                    continue;
557                }
558
559                enrollEntry(entry);
560            }
561        }
562    }
563
564    /** Adds a disk log file to in-memory tracking for accounting and enumeration. */
565    private synchronized void enrollEntry(EntryFile entry) {
566        mAllFiles.contents.add(entry);
567        mAllFiles.blocks += entry.blocks;
568
569        // mFilesByTag is used for trimming, so don't list empty files.
570        // (Zero-length/lost files are trimmed by date from mAllFiles.)
571
572        if (entry.tag != null && entry.file != null && entry.blocks > 0) {
573            FileList tagFiles = mFilesByTag.get(entry.tag);
574            if (tagFiles == null) {
575                tagFiles = new FileList();
576                mFilesByTag.put(entry.tag, tagFiles);
577            }
578            tagFiles.contents.add(entry);
579            tagFiles.blocks += entry.blocks;
580        }
581    }
582
583    /** Moves a temporary file to a final log filename and enrolls it. */
584    private synchronized void createEntry(File temp, String tag, int flags) throws IOException {
585        long t = System.currentTimeMillis();
586
587        // Require each entry to have a unique timestamp; if there are entries
588        // >10sec in the future (due to clock skew), drag them back to avoid
589        // keeping them around forever.
590
591        SortedSet<EntryFile> tail = mAllFiles.contents.tailSet(new EntryFile(t + 10000));
592        EntryFile[] future = null;
593        if (!tail.isEmpty()) {
594            future = tail.toArray(new EntryFile[tail.size()]);
595            tail.clear();  // Remove from mAllFiles
596        }
597
598        if (!mAllFiles.contents.isEmpty()) {
599            t = Math.max(t, mAllFiles.contents.last().timestampMillis + 1);
600        }
601
602        if (future != null) {
603            for (EntryFile late : future) {
604                mAllFiles.blocks -= late.blocks;
605                FileList tagFiles = mFilesByTag.get(late.tag);
606                if (tagFiles.contents.remove(late)) tagFiles.blocks -= late.blocks;
607                if ((late.flags & DropBoxManager.IS_EMPTY) == 0) {
608                    enrollEntry(new EntryFile(
609                            late.file, mDropBoxDir, late.tag, t++, late.flags, mBlockSize));
610                } else {
611                    enrollEntry(new EntryFile(mDropBoxDir, late.tag, t++));
612                }
613            }
614        }
615
616        if (temp == null) {
617            enrollEntry(new EntryFile(mDropBoxDir, tag, t));
618        } else {
619            enrollEntry(new EntryFile(temp, mDropBoxDir, tag, t, flags, mBlockSize));
620        }
621    }
622
623    /**
624     * Trims the files on disk to make sure they aren't using too much space.
625     * @return the overall quota for storage (in bytes)
626     */
627    private synchronized long trimToFit() {
628        // Expunge aged items (including tombstones marking deleted data).
629
630        int ageSeconds = Settings.Secure.getInt(mContentResolver,
631                Settings.Secure.DROPBOX_AGE_SECONDS, DEFAULT_AGE_SECONDS);
632        long cutoffMillis = System.currentTimeMillis() - ageSeconds * 1000;
633        while (!mAllFiles.contents.isEmpty()) {
634            EntryFile entry = mAllFiles.contents.first();
635            if (entry.timestampMillis > cutoffMillis) break;
636
637            FileList tag = mFilesByTag.get(entry.tag);
638            if (tag != null && tag.contents.remove(entry)) tag.blocks -= entry.blocks;
639            if (mAllFiles.contents.remove(entry)) mAllFiles.blocks -= entry.blocks;
640            if (entry.file != null) entry.file.delete();
641        }
642
643        // Compute overall quota (a fraction of available free space) in blocks.
644        // The quota changes dynamically based on the amount of free space;
645        // that way when lots of data is available we can use it, but we'll get
646        // out of the way if storage starts getting tight.
647
648        long uptimeMillis = SystemClock.uptimeMillis();
649        if (uptimeMillis > mCachedQuotaUptimeMillis + QUOTA_RESCAN_MILLIS) {
650            int quotaPercent = Settings.Secure.getInt(mContentResolver,
651                    Settings.Secure.DROPBOX_QUOTA_PERCENT, DEFAULT_QUOTA_PERCENT);
652            int reservePercent = Settings.Secure.getInt(mContentResolver,
653                    Settings.Secure.DROPBOX_RESERVE_PERCENT, DEFAULT_RESERVE_PERCENT);
654            int quotaKb = Settings.Secure.getInt(mContentResolver,
655                    Settings.Secure.DROPBOX_QUOTA_KB, DEFAULT_QUOTA_KB);
656
657            mStatFs.restat(mDropBoxDir.getPath());
658            int available = mStatFs.getAvailableBlocks();
659            int nonreserved = available - mStatFs.getBlockCount() * reservePercent / 100;
660            int maximum = quotaKb * 1024 / mBlockSize;
661            mCachedQuotaBlocks = Math.min(maximum, Math.max(0, nonreserved * quotaPercent / 100));
662            mCachedQuotaUptimeMillis = uptimeMillis;
663        }
664
665        // If we're using too much space, delete old items to make room.
666        //
667        // We trim each tag independently (this is why we keep per-tag lists).
668        // Space is "fairly" shared between tags -- they are all squeezed
669        // equally until enough space is reclaimed.
670        //
671        // A single circular buffer (a la logcat) would be simpler, but this
672        // way we can handle fat/bursty data (like 1MB+ bugreports, 300KB+
673        // kernel crash dumps, and 100KB+ ANR reports) without swamping small,
674        // well-behaved data // streams (event statistics, profile data, etc).
675        //
676        // Deleted files are replaced with zero-length tombstones to mark what
677        // was lost.  Tombstones are expunged by age (see above).
678
679        if (mAllFiles.blocks > mCachedQuotaBlocks) {
680            Slog.i(TAG, "Usage (" + mAllFiles.blocks + ") > Quota (" + mCachedQuotaBlocks + ")");
681
682            // Find a fair share amount of space to limit each tag
683            int unsqueezed = mAllFiles.blocks, squeezed = 0;
684            TreeSet<FileList> tags = new TreeSet<FileList>(mFilesByTag.values());
685            for (FileList tag : tags) {
686                if (squeezed > 0 && tag.blocks <= (mCachedQuotaBlocks - unsqueezed) / squeezed) {
687                    break;
688                }
689                unsqueezed -= tag.blocks;
690                squeezed++;
691            }
692            int tagQuota = (mCachedQuotaBlocks - unsqueezed) / squeezed;
693
694            // Remove old items from each tag until it meets the per-tag quota.
695            for (FileList tag : tags) {
696                if (mAllFiles.blocks < mCachedQuotaBlocks) break;
697                while (tag.blocks > tagQuota && !tag.contents.isEmpty()) {
698                    EntryFile entry = tag.contents.first();
699                    if (tag.contents.remove(entry)) tag.blocks -= entry.blocks;
700                    if (mAllFiles.contents.remove(entry)) mAllFiles.blocks -= entry.blocks;
701
702                    try {
703                        if (entry.file != null) entry.file.delete();
704                        enrollEntry(new EntryFile(mDropBoxDir, entry.tag, entry.timestampMillis));
705                    } catch (IOException e) {
706                        Slog.e(TAG, "Can't write tombstone file", e);
707                    }
708                }
709            }
710        }
711
712        return mCachedQuotaBlocks * mBlockSize;
713    }
714}
715