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