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