DropBoxManagerServicepublic final class DropBoxManagerService extends IDropBoxManagerService.Stub Implementation of {@link IDropBoxManagerService} using the filesystem.
Clients use {@link DropBoxManager} to access this service. |
Fields Summary |
---|
private static final String | TAG | private static final int | DEFAULT_AGE_SECONDS | private static final int | DEFAULT_MAX_FILES | private static final int | DEFAULT_QUOTA_KB | private static final int | DEFAULT_QUOTA_PERCENT | private static final int | DEFAULT_RESERVE_PERCENT | private static final int | QUOTA_RESCAN_MILLIS | private static final int | MSG_SEND_BROADCAST | private static final boolean | PROFILE_DUMP | private final android.content.Context | mContext | private final android.content.ContentResolver | mContentResolver | private final File | mDropBoxDir | private FileList | mAllFiles | private HashMap | mFilesByTag | private android.os.StatFs | mStatFs | private int | mBlockSize | private int | mCachedQuotaBlocks | private long | mCachedQuotaUptimeMillis | private volatile boolean | mBooted | private final android.os.Handler | mHandler | private final android.content.BroadcastReceiver | mReceiverReceives events that might indicate a need to clean up files. |
Constructors Summary |
---|
public DropBoxManagerService(android.content.Context context, File path)Creates an instance of managed drop box storage. Normally there is one of these
run by the system, but others can be created for testing and other purposes.
mDropBoxDir = path;
// Set up intent receivers
mContext = context;
mContentResolver = context.getContentResolver();
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_DEVICE_STORAGE_LOW);
filter.addAction(Intent.ACTION_BOOT_COMPLETED);
context.registerReceiver(mReceiver, filter);
mContentResolver.registerContentObserver(
Settings.Global.CONTENT_URI, true,
new ContentObserver(new Handler()) {
@Override
public void onChange(boolean selfChange) {
mReceiver.onReceive(context, (Intent) null);
}
});
mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
if (msg.what == MSG_SEND_BROADCAST) {
mContext.sendBroadcastAsUser((Intent)msg.obj, UserHandle.OWNER,
android.Manifest.permission.READ_LOGS);
}
}
};
// The real work gets done lazily in init() -- that way service creation always
// succeeds, and things like disk problems cause individual method failures.
|
Methods Summary |
---|
public void | add(DropBoxManager.Entry entry)
File temp = null;
OutputStream output = null;
final String tag = entry.getTag();
try {
int flags = entry.getFlags();
if ((flags & DropBoxManager.IS_EMPTY) != 0) throw new IllegalArgumentException();
init();
if (!isTagEnabled(tag)) return;
long max = trimToFit();
long lastTrim = System.currentTimeMillis();
byte[] buffer = new byte[mBlockSize];
InputStream input = entry.getInputStream();
// First, accumulate up to one block worth of data in memory before
// deciding whether to compress the data or not.
int read = 0;
while (read < buffer.length) {
int n = input.read(buffer, read, buffer.length - read);
if (n <= 0) break;
read += n;
}
// If we have at least one block, compress it -- otherwise, just write
// the data in uncompressed form.
temp = new File(mDropBoxDir, "drop" + Thread.currentThread().getId() + ".tmp");
int bufferSize = mBlockSize;
if (bufferSize > 4096) bufferSize = 4096;
if (bufferSize < 512) bufferSize = 512;
FileOutputStream foutput = new FileOutputStream(temp);
output = new BufferedOutputStream(foutput, bufferSize);
if (read == buffer.length && ((flags & DropBoxManager.IS_GZIPPED) == 0)) {
output = new GZIPOutputStream(output);
flags = flags | DropBoxManager.IS_GZIPPED;
}
do {
output.write(buffer, 0, read);
long now = System.currentTimeMillis();
if (now - lastTrim > 30 * 1000) {
max = trimToFit(); // In case data dribbles in slowly
lastTrim = now;
}
read = input.read(buffer);
if (read <= 0) {
FileUtils.sync(foutput);
output.close(); // Get a final size measurement
output = null;
} else {
output.flush(); // So the size measurement is pseudo-reasonable
}
long len = temp.length();
if (len > max) {
Slog.w(TAG, "Dropping: " + tag + " (" + temp.length() + " > " + max + " bytes)");
temp.delete();
temp = null; // Pass temp = null to createEntry() to leave a tombstone
break;
}
} while (read > 0);
long time = createEntry(temp, tag, flags);
temp = null;
final Intent dropboxIntent = new Intent(DropBoxManager.ACTION_DROPBOX_ENTRY_ADDED);
dropboxIntent.putExtra(DropBoxManager.EXTRA_TAG, tag);
dropboxIntent.putExtra(DropBoxManager.EXTRA_TIME, time);
if (!mBooted) {
dropboxIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
}
// Call sendBroadcast after returning from this call to avoid deadlock. In particular
// the caller may be holding the WindowManagerService lock but sendBroadcast requires a
// lock in ActivityManagerService. ActivityManagerService has been caught holding that
// very lock while waiting for the WindowManagerService lock.
mHandler.sendMessage(mHandler.obtainMessage(MSG_SEND_BROADCAST, dropboxIntent));
} catch (IOException e) {
Slog.e(TAG, "Can't write: " + tag, e);
} finally {
try { if (output != null) output.close(); } catch (IOException e) {}
entry.close();
if (temp != null) temp.delete();
}
| private synchronized long | createEntry(java.io.File temp, java.lang.String tag, int flags)Moves a temporary file to a final log filename and enrolls it.
long t = System.currentTimeMillis();
// Require each entry to have a unique timestamp; if there are entries
// >10sec in the future (due to clock skew), drag them back to avoid
// keeping them around forever.
SortedSet<EntryFile> tail = mAllFiles.contents.tailSet(new EntryFile(t + 10000));
EntryFile[] future = null;
if (!tail.isEmpty()) {
future = tail.toArray(new EntryFile[tail.size()]);
tail.clear(); // Remove from mAllFiles
}
if (!mAllFiles.contents.isEmpty()) {
t = Math.max(t, mAllFiles.contents.last().timestampMillis + 1);
}
if (future != null) {
for (EntryFile late : future) {
mAllFiles.blocks -= late.blocks;
FileList tagFiles = mFilesByTag.get(late.tag);
if (tagFiles != null && tagFiles.contents.remove(late)) {
tagFiles.blocks -= late.blocks;
}
if ((late.flags & DropBoxManager.IS_EMPTY) == 0) {
enrollEntry(new EntryFile(
late.file, mDropBoxDir, late.tag, t++, late.flags, mBlockSize));
} else {
enrollEntry(new EntryFile(mDropBoxDir, late.tag, t++));
}
}
}
if (temp == null) {
enrollEntry(new EntryFile(mDropBoxDir, tag, t));
} else {
enrollEntry(new EntryFile(temp, mDropBoxDir, tag, t, flags, mBlockSize));
}
return t;
| public synchronized void | dump(java.io.FileDescriptor fd, java.io.PrintWriter pw, java.lang.String[] args)
if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP)
!= PackageManager.PERMISSION_GRANTED) {
pw.println("Permission Denial: Can't dump DropBoxManagerService");
return;
}
try {
init();
} catch (IOException e) {
pw.println("Can't initialize: " + e);
Slog.e(TAG, "Can't init", e);
return;
}
if (PROFILE_DUMP) Debug.startMethodTracing("/data/trace/dropbox.dump");
StringBuilder out = new StringBuilder();
boolean doPrint = false, doFile = false;
ArrayList<String> searchArgs = new ArrayList<String>();
for (int i = 0; args != null && i < args.length; i++) {
if (args[i].equals("-p") || args[i].equals("--print")) {
doPrint = true;
} else if (args[i].equals("-f") || args[i].equals("--file")) {
doFile = true;
} else if (args[i].startsWith("-")) {
out.append("Unknown argument: ").append(args[i]).append("\n");
} else {
searchArgs.add(args[i]);
}
}
out.append("Drop box contents: ").append(mAllFiles.contents.size()).append(" entries\n");
if (!searchArgs.isEmpty()) {
out.append("Searching for:");
for (String a : searchArgs) out.append(" ").append(a);
out.append("\n");
}
int numFound = 0, numArgs = searchArgs.size();
Time time = new Time();
out.append("\n");
for (EntryFile entry : mAllFiles.contents) {
time.set(entry.timestampMillis);
String date = time.format("%Y-%m-%d %H:%M:%S");
boolean match = true;
for (int i = 0; i < numArgs && match; i++) {
String arg = searchArgs.get(i);
match = (date.contains(arg) || arg.equals(entry.tag));
}
if (!match) continue;
numFound++;
if (doPrint) out.append("========================================\n");
out.append(date).append(" ").append(entry.tag == null ? "(no tag)" : entry.tag);
if (entry.file == null) {
out.append(" (no file)\n");
continue;
} else if ((entry.flags & DropBoxManager.IS_EMPTY) != 0) {
out.append(" (contents lost)\n");
continue;
} else {
out.append(" (");
if ((entry.flags & DropBoxManager.IS_GZIPPED) != 0) out.append("compressed ");
out.append((entry.flags & DropBoxManager.IS_TEXT) != 0 ? "text" : "data");
out.append(", ").append(entry.file.length()).append(" bytes)\n");
}
if (doFile || (doPrint && (entry.flags & DropBoxManager.IS_TEXT) == 0)) {
if (!doPrint) out.append(" ");
out.append(entry.file.getPath()).append("\n");
}
if ((entry.flags & DropBoxManager.IS_TEXT) != 0 && (doPrint || !doFile)) {
DropBoxManager.Entry dbe = null;
InputStreamReader isr = null;
try {
dbe = new DropBoxManager.Entry(
entry.tag, entry.timestampMillis, entry.file, entry.flags);
if (doPrint) {
isr = new InputStreamReader(dbe.getInputStream());
char[] buf = new char[4096];
boolean newline = false;
for (;;) {
int n = isr.read(buf);
if (n <= 0) break;
out.append(buf, 0, n);
newline = (buf[n - 1] == '\n");
// Flush periodically when printing to avoid out-of-memory.
if (out.length() > 65536) {
pw.write(out.toString());
out.setLength(0);
}
}
if (!newline) out.append("\n");
} else {
String text = dbe.getText(70);
out.append(" ");
if (text == null) {
out.append("[null]");
} else {
boolean truncated = (text.length() == 70);
out.append(text.trim().replace('\n", '/"));
if (truncated) out.append(" ...");
}
out.append("\n");
}
} catch (IOException e) {
out.append("*** ").append(e.toString()).append("\n");
Slog.e(TAG, "Can't read: " + entry.file, e);
} finally {
if (dbe != null) dbe.close();
if (isr != null) {
try {
isr.close();
} catch (IOException unused) {
}
}
}
}
if (doPrint) out.append("\n");
}
if (numFound == 0) out.append("(No entries found.)\n");
if (args == null || args.length == 0) {
if (!doPrint) out.append("\n");
out.append("Usage: dumpsys dropbox [--print|--file] [YYYY-mm-dd] [HH:MM:SS] [tag]\n");
}
pw.write(out.toString());
if (PROFILE_DUMP) Debug.stopMethodTracing();
| private synchronized void | enrollEntry(com.android.server.DropBoxManagerService$EntryFile entry)Adds a disk log file to in-memory tracking for accounting and enumeration.
mAllFiles.contents.add(entry);
mAllFiles.blocks += entry.blocks;
// mFilesByTag is used for trimming, so don't list empty files.
// (Zero-length/lost files are trimmed by date from mAllFiles.)
if (entry.tag != null && entry.file != null && entry.blocks > 0) {
FileList tagFiles = mFilesByTag.get(entry.tag);
if (tagFiles == null) {
tagFiles = new FileList();
mFilesByTag.put(entry.tag, tagFiles);
}
tagFiles.contents.add(entry);
tagFiles.blocks += entry.blocks;
}
| public synchronized DropBoxManager.Entry | getNextEntry(java.lang.String tag, long millis)
if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.READ_LOGS)
!= PackageManager.PERMISSION_GRANTED) {
throw new SecurityException("READ_LOGS permission required");
}
try {
init();
} catch (IOException e) {
Slog.e(TAG, "Can't init", e);
return null;
}
FileList list = tag == null ? mAllFiles : mFilesByTag.get(tag);
if (list == null) return null;
for (EntryFile entry : list.contents.tailSet(new EntryFile(millis + 1))) {
if (entry.tag == null) continue;
if ((entry.flags & DropBoxManager.IS_EMPTY) != 0) {
return new DropBoxManager.Entry(entry.tag, entry.timestampMillis);
}
try {
return new DropBoxManager.Entry(
entry.tag, entry.timestampMillis, entry.file, entry.flags);
} catch (IOException e) {
Slog.e(TAG, "Can't read: " + entry.file, e);
// Continue to next file
}
}
return null;
| private synchronized void | init()If never run before, scans disk contents to build in-memory tracking data.
if (mStatFs == null) {
if (!mDropBoxDir.isDirectory() && !mDropBoxDir.mkdirs()) {
throw new IOException("Can't mkdir: " + mDropBoxDir);
}
try {
mStatFs = new StatFs(mDropBoxDir.getPath());
mBlockSize = mStatFs.getBlockSize();
} catch (IllegalArgumentException e) { // StatFs throws this on error
throw new IOException("Can't statfs: " + mDropBoxDir);
}
}
if (mAllFiles == null) {
File[] files = mDropBoxDir.listFiles();
if (files == null) throw new IOException("Can't list files: " + mDropBoxDir);
mAllFiles = new FileList();
mFilesByTag = new HashMap<String, FileList>();
// Scan pre-existing files.
for (File file : files) {
if (file.getName().endsWith(".tmp")) {
Slog.i(TAG, "Cleaning temp file: " + file);
file.delete();
continue;
}
EntryFile entry = new EntryFile(file, mBlockSize);
if (entry.tag == null) {
Slog.w(TAG, "Unrecognized file: " + file);
continue;
} else if (entry.timestampMillis == 0) {
Slog.w(TAG, "Invalid filename: " + file);
file.delete();
continue;
}
enrollEntry(entry);
}
}
| public boolean | isTagEnabled(java.lang.String tag)
final long token = Binder.clearCallingIdentity();
try {
return !"disabled".equals(Settings.Global.getString(
mContentResolver, Settings.Global.DROPBOX_TAG_PREFIX + tag));
} finally {
Binder.restoreCallingIdentity(token);
}
| public void | stop()Unregisters broadcast receivers and any other hooks -- for test instances
mContext.unregisterReceiver(mReceiver);
| private synchronized long | trimToFit()Trims the files on disk to make sure they aren't using too much space.
// Expunge aged items (including tombstones marking deleted data).
int ageSeconds = Settings.Global.getInt(mContentResolver,
Settings.Global.DROPBOX_AGE_SECONDS, DEFAULT_AGE_SECONDS);
int maxFiles = Settings.Global.getInt(mContentResolver,
Settings.Global.DROPBOX_MAX_FILES, DEFAULT_MAX_FILES);
long cutoffMillis = System.currentTimeMillis() - ageSeconds * 1000;
while (!mAllFiles.contents.isEmpty()) {
EntryFile entry = mAllFiles.contents.first();
if (entry.timestampMillis > cutoffMillis && mAllFiles.contents.size() < maxFiles) break;
FileList tag = mFilesByTag.get(entry.tag);
if (tag != null && tag.contents.remove(entry)) tag.blocks -= entry.blocks;
if (mAllFiles.contents.remove(entry)) mAllFiles.blocks -= entry.blocks;
if (entry.file != null) entry.file.delete();
}
// Compute overall quota (a fraction of available free space) in blocks.
// The quota changes dynamically based on the amount of free space;
// that way when lots of data is available we can use it, but we'll get
// out of the way if storage starts getting tight.
long uptimeMillis = SystemClock.uptimeMillis();
if (uptimeMillis > mCachedQuotaUptimeMillis + QUOTA_RESCAN_MILLIS) {
int quotaPercent = Settings.Global.getInt(mContentResolver,
Settings.Global.DROPBOX_QUOTA_PERCENT, DEFAULT_QUOTA_PERCENT);
int reservePercent = Settings.Global.getInt(mContentResolver,
Settings.Global.DROPBOX_RESERVE_PERCENT, DEFAULT_RESERVE_PERCENT);
int quotaKb = Settings.Global.getInt(mContentResolver,
Settings.Global.DROPBOX_QUOTA_KB, DEFAULT_QUOTA_KB);
mStatFs.restat(mDropBoxDir.getPath());
int available = mStatFs.getAvailableBlocks();
int nonreserved = available - mStatFs.getBlockCount() * reservePercent / 100;
int maximum = quotaKb * 1024 / mBlockSize;
mCachedQuotaBlocks = Math.min(maximum, Math.max(0, nonreserved * quotaPercent / 100));
mCachedQuotaUptimeMillis = uptimeMillis;
}
// If we're using too much space, delete old items to make room.
//
// We trim each tag independently (this is why we keep per-tag lists).
// Space is "fairly" shared between tags -- they are all squeezed
// equally until enough space is reclaimed.
//
// A single circular buffer (a la logcat) would be simpler, but this
// way we can handle fat/bursty data (like 1MB+ bugreports, 300KB+
// kernel crash dumps, and 100KB+ ANR reports) without swamping small,
// well-behaved data streams (event statistics, profile data, etc).
//
// Deleted files are replaced with zero-length tombstones to mark what
// was lost. Tombstones are expunged by age (see above).
if (mAllFiles.blocks > mCachedQuotaBlocks) {
// Find a fair share amount of space to limit each tag
int unsqueezed = mAllFiles.blocks, squeezed = 0;
TreeSet<FileList> tags = new TreeSet<FileList>(mFilesByTag.values());
for (FileList tag : tags) {
if (squeezed > 0 && tag.blocks <= (mCachedQuotaBlocks - unsqueezed) / squeezed) {
break;
}
unsqueezed -= tag.blocks;
squeezed++;
}
int tagQuota = (mCachedQuotaBlocks - unsqueezed) / squeezed;
// Remove old items from each tag until it meets the per-tag quota.
for (FileList tag : tags) {
if (mAllFiles.blocks < mCachedQuotaBlocks) break;
while (tag.blocks > tagQuota && !tag.contents.isEmpty()) {
EntryFile entry = tag.contents.first();
if (tag.contents.remove(entry)) tag.blocks -= entry.blocks;
if (mAllFiles.contents.remove(entry)) mAllFiles.blocks -= entry.blocks;
try {
if (entry.file != null) entry.file.delete();
enrollEntry(new EntryFile(mDropBoxDir, entry.tag, entry.timestampMillis));
} catch (IOException e) {
Slog.e(TAG, "Can't write tombstone file", e);
}
}
}
}
return mCachedQuotaBlocks * mBlockSize;
|
|