blob: 245448748766437e6d7f49c94358afbcb7040aaf [file] [log] [blame]
Dan Egnor4410ec82009-09-11 16:40:01 -07001/*
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;
Doug Zongker43866e02010-01-07 12:09:54 -080025import android.database.ContentObserver;
Dan Egnor4410ec82009-09-11 16:40:01 -070026import android.net.Uri;
Jeff Sharkey911d7f42013-09-05 18:11:45 -070027import android.os.Binder;
Dan Egnor3d40df32009-11-17 13:36:31 -080028import android.os.Debug;
Dan Egnorf18a01c2009-11-12 11:32:50 -080029import android.os.DropBoxManager;
Dianne Hackborn8bdf5932010-10-15 12:54:40 -070030import android.os.FileUtils;
Doug Zongker43866e02010-01-07 12:09:54 -080031import android.os.Handler;
Craig Mautner26caf7a2012-03-04 17:17:59 -080032import android.os.Message;
Dan Egnor4410ec82009-09-11 16:40:01 -070033import android.os.StatFs;
34import android.os.SystemClock;
Dianne Hackborn5ac72a22012-08-29 18:32:08 -070035import android.os.UserHandle;
Dan Egnor4410ec82009-09-11 16:40:01 -070036import android.provider.Settings;
Dan Egnor3d40df32009-11-17 13:36:31 -080037import android.text.format.Time;
Joe Onorato8a9b2202010-02-26 18:56:32 -080038import android.util.Slog;
Dan Egnor4410ec82009-09-11 16:40:01 -070039
Tim Kilbourn0935f3c2015-05-28 11:48:43 -070040import libcore.io.IoUtils;
41
Dan Egnorf18a01c2009-11-12 11:32:50 -080042import com.android.internal.os.IDropBoxManagerService;
Dan Egnor95240272009-10-27 18:23:39 -070043
Brad Fitzpatrick89647b12010-09-22 17:49:16 -070044import java.io.BufferedOutputStream;
Dan Egnor4410ec82009-09-11 16:40:01 -070045import java.io.File;
46import java.io.FileDescriptor;
Dan Egnor4410ec82009-09-11 16:40:01 -070047import java.io.FileOutputStream;
48import java.io.IOException;
Dan Egnor95240272009-10-27 18:23:39 -070049import java.io.InputStream;
Dan Egnor4410ec82009-09-11 16:40:01 -070050import java.io.InputStreamReader;
51import java.io.OutputStream;
Dan Egnor4410ec82009-09-11 16:40:01 -070052import java.io.PrintWriter;
Dan Egnor4410ec82009-09-11 16:40:01 -070053import java.util.ArrayList;
Dan Egnor4410ec82009-09-11 16:40:01 -070054import java.util.HashMap;
Dan Egnor4410ec82009-09-11 16:40:01 -070055import java.util.SortedSet;
56import java.util.TreeSet;
57import java.util.zip.GZIPOutputStream;
58
59/**
Dan Egnorf18a01c2009-11-12 11:32:50 -080060 * Implementation of {@link IDropBoxManagerService} using the filesystem.
61 * Clients use {@link DropBoxManager} to access this service.
Dan Egnor4410ec82009-09-11 16:40:01 -070062 */
Tim Kilbourn0935f3c2015-05-28 11:48:43 -070063public final class DropBoxManagerService extends SystemService {
Dan Egnorf18a01c2009-11-12 11:32:50 -080064 private static final String TAG = "DropBoxManagerService";
Dan Egnor4410ec82009-09-11 16:40:01 -070065 private static final int DEFAULT_AGE_SECONDS = 3 * 86400;
Dan Egnor3a8b0c12010-03-24 17:48:20 -070066 private static final int DEFAULT_MAX_FILES = 1000;
67 private static final int DEFAULT_QUOTA_KB = 5 * 1024;
68 private static final int DEFAULT_QUOTA_PERCENT = 10;
69 private static final int DEFAULT_RESERVE_PERCENT = 10;
Dan Egnor4410ec82009-09-11 16:40:01 -070070 private static final int QUOTA_RESCAN_MILLIS = 5000;
71
Craig Mautner26caf7a2012-03-04 17:17:59 -080072 // mHandler 'what' value.
73 private static final int MSG_SEND_BROADCAST = 1;
74
Dan Egnor3d40df32009-11-17 13:36:31 -080075 private static final boolean PROFILE_DUMP = false;
76
Dan Egnor4410ec82009-09-11 16:40:01 -070077 // TODO: This implementation currently uses one file per entry, which is
78 // inefficient for smallish entries -- consider using a single queue file
79 // per tag (or even globally) instead.
80
81 // The cached context and derived objects
82
Dan Egnor4410ec82009-09-11 16:40:01 -070083 private final ContentResolver mContentResolver;
84 private final File mDropBoxDir;
85
86 // Accounting of all currently written log files (set in init()).
87
88 private FileList mAllFiles = null;
89 private HashMap<String, FileList> mFilesByTag = null;
90
91 // Various bits of disk information
92
93 private StatFs mStatFs = null;
94 private int mBlockSize = 0;
95 private int mCachedQuotaBlocks = 0; // Space we can use: computed from free space, etc.
96 private long mCachedQuotaUptimeMillis = 0;
97
Brad Fitzpatrick34165c62011-01-17 18:14:18 -080098 private volatile boolean mBooted = false;
99
Craig Mautner26caf7a2012-03-04 17:17:59 -0800100 // Provide a way to perform sendBroadcast asynchronously to avoid deadlocks.
101 private final Handler mHandler;
102
Dan Egnor4410ec82009-09-11 16:40:01 -0700103 /** Receives events that might indicate a need to clean up files. */
104 private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
105 @Override
106 public void onReceive(Context context, Intent intent) {
Tim Kilbourn0935f3c2015-05-28 11:48:43 -0700107 // For ACTION_DEVICE_STORAGE_LOW:
Dan Egnor4410ec82009-09-11 16:40:01 -0700108 mCachedQuotaUptimeMillis = 0; // Force a re-check of quota size
Dan Egnor3a8b0c12010-03-24 17:48:20 -0700109
110 // Run the initialization in the background (not this main thread).
111 // The init() and trimToFit() methods are synchronized, so they still
112 // block other users -- but at least the onReceive() call can finish.
113 new Thread() {
114 public void run() {
115 try {
116 init();
117 trimToFit();
118 } catch (IOException e) {
119 Slog.e(TAG, "Can't init", e);
120 }
121 }
122 }.start();
Dan Egnor4410ec82009-09-11 16:40:01 -0700123 }
124 };
125
Tim Kilbourn0935f3c2015-05-28 11:48:43 -0700126 private final IDropBoxManagerService.Stub mStub = new IDropBoxManagerService.Stub() {
127 @Override
128 public void add(DropBoxManager.Entry entry) {
129 DropBoxManagerService.this.add(entry);
130 }
131
132 @Override
133 public boolean isTagEnabled(String tag) {
134 return DropBoxManagerService.this.isTagEnabled(tag);
135 }
136
137 @Override
138 public DropBoxManager.Entry getNextEntry(String tag, long millis) {
139 return DropBoxManagerService.this.getNextEntry(tag, millis);
140 }
141
142 @Override
143 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
144 DropBoxManagerService.this.dump(fd, pw, args);
145 }
146 };
147
148 /**
149 * Creates an instance of managed drop box storage using the default dropbox
150 * directory.
151 *
152 * @param context to use for receiving free space & gservices intents
153 */
154 public DropBoxManagerService(final Context context) {
155 this(context, new File("/data/system/dropbox"));
156 }
157
Dan Egnor4410ec82009-09-11 16:40:01 -0700158 /**
159 * Creates an instance of managed drop box storage. Normally there is one of these
160 * run by the system, but others can be created for testing and other purposes.
161 *
162 * @param context to use for receiving free space & gservices intents
163 * @param path to store drop box entries in
164 */
Doug Zongker43866e02010-01-07 12:09:54 -0800165 public DropBoxManagerService(final Context context, File path) {
Tim Kilbourn0935f3c2015-05-28 11:48:43 -0700166 super(context);
Dan Egnor4410ec82009-09-11 16:40:01 -0700167 mDropBoxDir = path;
Tim Kilbourn0935f3c2015-05-28 11:48:43 -0700168 mContentResolver = getContext().getContentResolver();
169 mHandler = new Handler() {
170 @Override
171 public void handleMessage(Message msg) {
172 if (msg.what == MSG_SEND_BROADCAST) {
Xiaohui Chene4de5a02015-09-22 15:33:31 -0700173 getContext().sendBroadcastAsUser((Intent)msg.obj, UserHandle.SYSTEM,
Tim Kilbourn0935f3c2015-05-28 11:48:43 -0700174 android.Manifest.permission.READ_LOGS);
175 }
176 }
177 };
178 }
Dan Egnor4410ec82009-09-11 16:40:01 -0700179
Tim Kilbourn0935f3c2015-05-28 11:48:43 -0700180 @Override
181 public void onStart() {
Dan Egnor4410ec82009-09-11 16:40:01 -0700182 // Set up intent receivers
Brad Fitzpatrick34165c62011-01-17 18:14:18 -0800183 IntentFilter filter = new IntentFilter();
184 filter.addAction(Intent.ACTION_DEVICE_STORAGE_LOW);
Tim Kilbourn0935f3c2015-05-28 11:48:43 -0700185 getContext().registerReceiver(mReceiver, filter);
Doug Zongker43866e02010-01-07 12:09:54 -0800186
187 mContentResolver.registerContentObserver(
Jeff Sharkey625239a2012-09-26 22:03:49 -0700188 Settings.Global.CONTENT_URI, true,
Doug Zongker43866e02010-01-07 12:09:54 -0800189 new ContentObserver(new Handler()) {
Craig Mautner26caf7a2012-03-04 17:17:59 -0800190 @Override
Doug Zongker43866e02010-01-07 12:09:54 -0800191 public void onChange(boolean selfChange) {
Tim Kilbourn0935f3c2015-05-28 11:48:43 -0700192 mReceiver.onReceive(getContext(), (Intent) null);
Doug Zongker43866e02010-01-07 12:09:54 -0800193 }
194 });
Dan Egnor4410ec82009-09-11 16:40:01 -0700195
Tim Kilbourn0935f3c2015-05-28 11:48:43 -0700196 publishBinderService(Context.DROPBOX_SERVICE, mStub);
Craig Mautner26caf7a2012-03-04 17:17:59 -0800197
Dan Egnor4410ec82009-09-11 16:40:01 -0700198 // The real work gets done lazily in init() -- that way service creation always
199 // succeeds, and things like disk problems cause individual method failures.
200 }
201
Tim Kilbourn0935f3c2015-05-28 11:48:43 -0700202 @Override
203 public void onBootPhase(int phase) {
204 switch (phase) {
205 case PHASE_BOOT_COMPLETED:
206 mBooted = true;
207 break;
208 }
Dan Egnor4410ec82009-09-11 16:40:01 -0700209 }
210
Tim Kilbourn0935f3c2015-05-28 11:48:43 -0700211 /** Retrieves the binder stub -- for test instances */
212 public IDropBoxManagerService getServiceStub() {
213 return mStub;
214 }
215
Dan Egnorf18a01c2009-11-12 11:32:50 -0800216 public void add(DropBoxManager.Entry entry) {
Dan Egnor4410ec82009-09-11 16:40:01 -0700217 File temp = null;
Tim Kilbourn0935f3c2015-05-28 11:48:43 -0700218 InputStream input = null;
Dan Egnor4410ec82009-09-11 16:40:01 -0700219 OutputStream output = null;
Dan Egnor95240272009-10-27 18:23:39 -0700220 final String tag = entry.getTag();
Dan Egnor4410ec82009-09-11 16:40:01 -0700221 try {
Dan Egnor95240272009-10-27 18:23:39 -0700222 int flags = entry.getFlags();
Dan Egnorf18a01c2009-11-12 11:32:50 -0800223 if ((flags & DropBoxManager.IS_EMPTY) != 0) throw new IllegalArgumentException();
Dan Egnor4410ec82009-09-11 16:40:01 -0700224
225 init();
226 if (!isTagEnabled(tag)) return;
227 long max = trimToFit();
228 long lastTrim = System.currentTimeMillis();
229
230 byte[] buffer = new byte[mBlockSize];
Tim Kilbourn0935f3c2015-05-28 11:48:43 -0700231 input = entry.getInputStream();
Dan Egnor4410ec82009-09-11 16:40:01 -0700232
233 // First, accumulate up to one block worth of data in memory before
234 // deciding whether to compress the data or not.
235
236 int read = 0;
237 while (read < buffer.length) {
238 int n = input.read(buffer, read, buffer.length - read);
239 if (n <= 0) break;
240 read += n;
241 }
242
243 // If we have at least one block, compress it -- otherwise, just write
244 // the data in uncompressed form.
245
246 temp = new File(mDropBoxDir, "drop" + Thread.currentThread().getId() + ".tmp");
Brad Fitzpatrick89647b12010-09-22 17:49:16 -0700247 int bufferSize = mBlockSize;
248 if (bufferSize > 4096) bufferSize = 4096;
249 if (bufferSize < 512) bufferSize = 512;
Dianne Hackborn8bdf5932010-10-15 12:54:40 -0700250 FileOutputStream foutput = new FileOutputStream(temp);
251 output = new BufferedOutputStream(foutput, bufferSize);
Dan Egnorf18a01c2009-11-12 11:32:50 -0800252 if (read == buffer.length && ((flags & DropBoxManager.IS_GZIPPED) == 0)) {
Dan Egnor4410ec82009-09-11 16:40:01 -0700253 output = new GZIPOutputStream(output);
Dan Egnorf18a01c2009-11-12 11:32:50 -0800254 flags = flags | DropBoxManager.IS_GZIPPED;
Dan Egnor4410ec82009-09-11 16:40:01 -0700255 }
256
257 do {
258 output.write(buffer, 0, read);
259
260 long now = System.currentTimeMillis();
261 if (now - lastTrim > 30 * 1000) {
262 max = trimToFit(); // In case data dribbles in slowly
263 lastTrim = now;
264 }
265
266 read = input.read(buffer);
267 if (read <= 0) {
Dianne Hackborn8bdf5932010-10-15 12:54:40 -0700268 FileUtils.sync(foutput);
Dan Egnor4410ec82009-09-11 16:40:01 -0700269 output.close(); // Get a final size measurement
270 output = null;
271 } else {
272 output.flush(); // So the size measurement is pseudo-reasonable
273 }
274
275 long len = temp.length();
276 if (len > max) {
Joe Onorato8a9b2202010-02-26 18:56:32 -0800277 Slog.w(TAG, "Dropping: " + tag + " (" + temp.length() + " > " + max + " bytes)");
Dan Egnor4410ec82009-09-11 16:40:01 -0700278 temp.delete();
279 temp = null; // Pass temp = null to createEntry() to leave a tombstone
280 break;
281 }
282 } while (read > 0);
283
Hakan Stillb2475362010-12-07 14:05:55 +0100284 long time = createEntry(temp, tag, flags);
Dan Egnor4410ec82009-09-11 16:40:01 -0700285 temp = null;
Hakan Stillb2475362010-12-07 14:05:55 +0100286
Craig Mautner26caf7a2012-03-04 17:17:59 -0800287 final Intent dropboxIntent = new Intent(DropBoxManager.ACTION_DROPBOX_ENTRY_ADDED);
Hakan Stillb2475362010-12-07 14:05:55 +0100288 dropboxIntent.putExtra(DropBoxManager.EXTRA_TAG, tag);
289 dropboxIntent.putExtra(DropBoxManager.EXTRA_TIME, time);
Brad Fitzpatrick34165c62011-01-17 18:14:18 -0800290 if (!mBooted) {
291 dropboxIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
292 }
Craig Mautner26caf7a2012-03-04 17:17:59 -0800293 // Call sendBroadcast after returning from this call to avoid deadlock. In particular
294 // the caller may be holding the WindowManagerService lock but sendBroadcast requires a
295 // lock in ActivityManagerService. ActivityManagerService has been caught holding that
296 // very lock while waiting for the WindowManagerService lock.
297 mHandler.sendMessage(mHandler.obtainMessage(MSG_SEND_BROADCAST, dropboxIntent));
Dan Egnor4410ec82009-09-11 16:40:01 -0700298 } catch (IOException e) {
Joe Onorato8a9b2202010-02-26 18:56:32 -0800299 Slog.e(TAG, "Can't write: " + tag, e);
Dan Egnor4410ec82009-09-11 16:40:01 -0700300 } finally {
Tim Kilbourn0935f3c2015-05-28 11:48:43 -0700301 IoUtils.closeQuietly(output);
302 IoUtils.closeQuietly(input);
Dan Egnor95240272009-10-27 18:23:39 -0700303 entry.close();
Dan Egnor4410ec82009-09-11 16:40:01 -0700304 if (temp != null) temp.delete();
305 }
306 }
307
308 public boolean isTagEnabled(String tag) {
Jeff Sharkey911d7f42013-09-05 18:11:45 -0700309 final long token = Binder.clearCallingIdentity();
310 try {
311 return !"disabled".equals(Settings.Global.getString(
312 mContentResolver, Settings.Global.DROPBOX_TAG_PREFIX + tag));
313 } finally {
314 Binder.restoreCallingIdentity(token);
315 }
Dan Egnor4410ec82009-09-11 16:40:01 -0700316 }
317
Dan Egnorf18a01c2009-11-12 11:32:50 -0800318 public synchronized DropBoxManager.Entry getNextEntry(String tag, long millis) {
Tim Kilbourn0935f3c2015-05-28 11:48:43 -0700319 if (getContext().checkCallingOrSelfPermission(android.Manifest.permission.READ_LOGS)
Dan Egnor4410ec82009-09-11 16:40:01 -0700320 != PackageManager.PERMISSION_GRANTED) {
321 throw new SecurityException("READ_LOGS permission required");
322 }
323
324 try {
325 init();
326 } catch (IOException e) {
Joe Onorato8a9b2202010-02-26 18:56:32 -0800327 Slog.e(TAG, "Can't init", e);
Dan Egnor4410ec82009-09-11 16:40:01 -0700328 return null;
329 }
330
Dan Egnorb3b06fc2009-10-20 13:05:17 -0700331 FileList list = tag == null ? mAllFiles : mFilesByTag.get(tag);
332 if (list == null) return null;
333
334 for (EntryFile entry : list.contents.tailSet(new EntryFile(millis + 1))) {
Dan Egnor4410ec82009-09-11 16:40:01 -0700335 if (entry.tag == null) continue;
Dan Egnorf18a01c2009-11-12 11:32:50 -0800336 if ((entry.flags & DropBoxManager.IS_EMPTY) != 0) {
337 return new DropBoxManager.Entry(entry.tag, entry.timestampMillis);
Dan Egnor95240272009-10-27 18:23:39 -0700338 }
Dan Egnor4410ec82009-09-11 16:40:01 -0700339 try {
Dan Egnorf18a01c2009-11-12 11:32:50 -0800340 return new DropBoxManager.Entry(
341 entry.tag, entry.timestampMillis, entry.file, entry.flags);
Dan Egnor4410ec82009-09-11 16:40:01 -0700342 } catch (IOException e) {
Joe Onorato8a9b2202010-02-26 18:56:32 -0800343 Slog.e(TAG, "Can't read: " + entry.file, e);
Dan Egnor4410ec82009-09-11 16:40:01 -0700344 // Continue to next file
345 }
346 }
347
348 return null;
349 }
350
351 public synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
Tim Kilbourn0935f3c2015-05-28 11:48:43 -0700352 if (getContext().checkCallingOrSelfPermission(android.Manifest.permission.DUMP)
Dan Egnor4410ec82009-09-11 16:40:01 -0700353 != PackageManager.PERMISSION_GRANTED) {
Dan Egnorf18a01c2009-11-12 11:32:50 -0800354 pw.println("Permission Denial: Can't dump DropBoxManagerService");
Dan Egnor4410ec82009-09-11 16:40:01 -0700355 return;
356 }
357
358 try {
359 init();
360 } catch (IOException e) {
361 pw.println("Can't initialize: " + e);
Joe Onorato8a9b2202010-02-26 18:56:32 -0800362 Slog.e(TAG, "Can't init", e);
Dan Egnor4410ec82009-09-11 16:40:01 -0700363 return;
364 }
365
Dan Egnor3d40df32009-11-17 13:36:31 -0800366 if (PROFILE_DUMP) Debug.startMethodTracing("/data/trace/dropbox.dump");
367
Dan Egnor5ec249a2009-11-25 13:16:47 -0800368 StringBuilder out = new StringBuilder();
Dan Egnor4410ec82009-09-11 16:40:01 -0700369 boolean doPrint = false, doFile = false;
370 ArrayList<String> searchArgs = new ArrayList<String>();
371 for (int i = 0; args != null && i < args.length; i++) {
372 if (args[i].equals("-p") || args[i].equals("--print")) {
373 doPrint = true;
374 } else if (args[i].equals("-f") || args[i].equals("--file")) {
375 doFile = true;
376 } else if (args[i].startsWith("-")) {
Dan Egnor5ec249a2009-11-25 13:16:47 -0800377 out.append("Unknown argument: ").append(args[i]).append("\n");
Dan Egnor4410ec82009-09-11 16:40:01 -0700378 } else {
379 searchArgs.add(args[i]);
380 }
381 }
382
Dan Egnor5ec249a2009-11-25 13:16:47 -0800383 out.append("Drop box contents: ").append(mAllFiles.contents.size()).append(" entries\n");
Dan Egnor4410ec82009-09-11 16:40:01 -0700384
385 if (!searchArgs.isEmpty()) {
Dan Egnor5ec249a2009-11-25 13:16:47 -0800386 out.append("Searching for:");
387 for (String a : searchArgs) out.append(" ").append(a);
388 out.append("\n");
Dan Egnor4410ec82009-09-11 16:40:01 -0700389 }
390
Dan Egnor3d40df32009-11-17 13:36:31 -0800391 int numFound = 0, numArgs = searchArgs.size();
392 Time time = new Time();
Dan Egnor5ec249a2009-11-25 13:16:47 -0800393 out.append("\n");
Dan Egnor4410ec82009-09-11 16:40:01 -0700394 for (EntryFile entry : mAllFiles.contents) {
Dan Egnor3d40df32009-11-17 13:36:31 -0800395 time.set(entry.timestampMillis);
396 String date = time.format("%Y-%m-%d %H:%M:%S");
Dan Egnor4410ec82009-09-11 16:40:01 -0700397 boolean match = true;
Dan Egnor3d40df32009-11-17 13:36:31 -0800398 for (int i = 0; i < numArgs && match; i++) {
399 String arg = searchArgs.get(i);
400 match = (date.contains(arg) || arg.equals(entry.tag));
401 }
Dan Egnor4410ec82009-09-11 16:40:01 -0700402 if (!match) continue;
403
404 numFound++;
Dan Egnor42471dd2010-01-07 17:25:22 -0800405 if (doPrint) out.append("========================================\n");
Dan Egnor5ec249a2009-11-25 13:16:47 -0800406 out.append(date).append(" ").append(entry.tag == null ? "(no tag)" : entry.tag);
Dan Egnor4410ec82009-09-11 16:40:01 -0700407 if (entry.file == null) {
Dan Egnor5ec249a2009-11-25 13:16:47 -0800408 out.append(" (no file)\n");
Dan Egnor4410ec82009-09-11 16:40:01 -0700409 continue;
Dan Egnorf18a01c2009-11-12 11:32:50 -0800410 } else if ((entry.flags & DropBoxManager.IS_EMPTY) != 0) {
Dan Egnor5ec249a2009-11-25 13:16:47 -0800411 out.append(" (contents lost)\n");
Dan Egnor4410ec82009-09-11 16:40:01 -0700412 continue;
413 } else {
Dan Egnor5ec249a2009-11-25 13:16:47 -0800414 out.append(" (");
415 if ((entry.flags & DropBoxManager.IS_GZIPPED) != 0) out.append("compressed ");
416 out.append((entry.flags & DropBoxManager.IS_TEXT) != 0 ? "text" : "data");
417 out.append(", ").append(entry.file.length()).append(" bytes)\n");
Dan Egnor4410ec82009-09-11 16:40:01 -0700418 }
419
Dan Egnorf18a01c2009-11-12 11:32:50 -0800420 if (doFile || (doPrint && (entry.flags & DropBoxManager.IS_TEXT) == 0)) {
Dan Egnor5ec249a2009-11-25 13:16:47 -0800421 if (!doPrint) out.append(" ");
422 out.append(entry.file.getPath()).append("\n");
Dan Egnor4410ec82009-09-11 16:40:01 -0700423 }
424
Dan Egnorf18a01c2009-11-12 11:32:50 -0800425 if ((entry.flags & DropBoxManager.IS_TEXT) != 0 && (doPrint || !doFile)) {
426 DropBoxManager.Entry dbe = null;
Brad Fitzpatrick0c822402010-11-23 09:17:56 -0800427 InputStreamReader isr = null;
Dan Egnor4410ec82009-09-11 16:40:01 -0700428 try {
Dan Egnorf18a01c2009-11-12 11:32:50 -0800429 dbe = new DropBoxManager.Entry(
Dan Egnor4410ec82009-09-11 16:40:01 -0700430 entry.tag, entry.timestampMillis, entry.file, entry.flags);
431
432 if (doPrint) {
Brad Fitzpatrick0c822402010-11-23 09:17:56 -0800433 isr = new InputStreamReader(dbe.getInputStream());
Dan Egnor4410ec82009-09-11 16:40:01 -0700434 char[] buf = new char[4096];
435 boolean newline = false;
436 for (;;) {
Brad Fitzpatrick0c822402010-11-23 09:17:56 -0800437 int n = isr.read(buf);
Dan Egnor4410ec82009-09-11 16:40:01 -0700438 if (n <= 0) break;
Dan Egnor5ec249a2009-11-25 13:16:47 -0800439 out.append(buf, 0, n);
Dan Egnor4410ec82009-09-11 16:40:01 -0700440 newline = (buf[n - 1] == '\n');
Dan Egnor42471dd2010-01-07 17:25:22 -0800441
442 // Flush periodically when printing to avoid out-of-memory.
443 if (out.length() > 65536) {
444 pw.write(out.toString());
445 out.setLength(0);
446 }
Dan Egnor4410ec82009-09-11 16:40:01 -0700447 }
Dan Egnor5ec249a2009-11-25 13:16:47 -0800448 if (!newline) out.append("\n");
Dan Egnor4410ec82009-09-11 16:40:01 -0700449 } else {
450 String text = dbe.getText(70);
Jeff Sharkey22510ef2014-11-13 12:28:46 -0800451 out.append(" ");
452 if (text == null) {
453 out.append("[null]");
454 } else {
455 boolean truncated = (text.length() == 70);
456 out.append(text.trim().replace('\n', '/'));
457 if (truncated) out.append(" ...");
458 }
Dan Egnor5ec249a2009-11-25 13:16:47 -0800459 out.append("\n");
Dan Egnor4410ec82009-09-11 16:40:01 -0700460 }
461 } catch (IOException e) {
Dan Egnor5ec249a2009-11-25 13:16:47 -0800462 out.append("*** ").append(e.toString()).append("\n");
Joe Onorato8a9b2202010-02-26 18:56:32 -0800463 Slog.e(TAG, "Can't read: " + entry.file, e);
Dan Egnor4410ec82009-09-11 16:40:01 -0700464 } finally {
465 if (dbe != null) dbe.close();
Brad Fitzpatrick0c822402010-11-23 09:17:56 -0800466 if (isr != null) {
467 try {
468 isr.close();
469 } catch (IOException unused) {
470 }
471 }
Dan Egnor4410ec82009-09-11 16:40:01 -0700472 }
473 }
474
Dan Egnor5ec249a2009-11-25 13:16:47 -0800475 if (doPrint) out.append("\n");
Dan Egnor4410ec82009-09-11 16:40:01 -0700476 }
477
Dan Egnor5ec249a2009-11-25 13:16:47 -0800478 if (numFound == 0) out.append("(No entries found.)\n");
Dan Egnor4410ec82009-09-11 16:40:01 -0700479
480 if (args == null || args.length == 0) {
Dan Egnor5ec249a2009-11-25 13:16:47 -0800481 if (!doPrint) out.append("\n");
482 out.append("Usage: dumpsys dropbox [--print|--file] [YYYY-mm-dd] [HH:MM:SS] [tag]\n");
Dan Egnor4410ec82009-09-11 16:40:01 -0700483 }
Dan Egnor3d40df32009-11-17 13:36:31 -0800484
485 pw.write(out.toString());
486 if (PROFILE_DUMP) Debug.stopMethodTracing();
Dan Egnor4410ec82009-09-11 16:40:01 -0700487 }
488
489 ///////////////////////////////////////////////////////////////////////////
490
Xiaohui Chene4de5a02015-09-22 15:33:31 -0700491 /** Chronologically sorted list of {@link EntryFile} */
Dan Egnor4410ec82009-09-11 16:40:01 -0700492 private static final class FileList implements Comparable<FileList> {
493 public int blocks = 0;
494 public final TreeSet<EntryFile> contents = new TreeSet<EntryFile>();
495
496 /** Sorts bigger FileList instances before smaller ones. */
497 public final int compareTo(FileList o) {
498 if (blocks != o.blocks) return o.blocks - blocks;
499 if (this == o) return 0;
500 if (hashCode() < o.hashCode()) return -1;
501 if (hashCode() > o.hashCode()) return 1;
502 return 0;
503 }
504 }
505
506 /** Metadata describing an on-disk log file. */
507 private static final class EntryFile implements Comparable<EntryFile> {
508 public final String tag;
509 public final long timestampMillis;
510 public final int flags;
511 public final File file;
512 public final int blocks;
513
514 /** Sorts earlier EntryFile instances before later ones. */
515 public final int compareTo(EntryFile o) {
516 if (timestampMillis < o.timestampMillis) return -1;
517 if (timestampMillis > o.timestampMillis) return 1;
518 if (file != null && o.file != null) return file.compareTo(o.file);
519 if (o.file != null) return -1;
520 if (file != null) return 1;
521 if (this == o) return 0;
522 if (hashCode() < o.hashCode()) return -1;
523 if (hashCode() > o.hashCode()) return 1;
524 return 0;
525 }
526
527 /**
528 * Moves an existing temporary file to a new log filename.
529 * @param temp file to rename
530 * @param dir to store file in
531 * @param tag to use for new log file name
532 * @param timestampMillis of log entry
Dan Egnor95240272009-10-27 18:23:39 -0700533 * @param flags for the entry data
Dan Egnor4410ec82009-09-11 16:40:01 -0700534 * @param blockSize to use for space accounting
535 * @throws IOException if the file can't be moved
536 */
537 public EntryFile(File temp, File dir, String tag,long timestampMillis,
538 int flags, int blockSize) throws IOException {
Dan Egnorf18a01c2009-11-12 11:32:50 -0800539 if ((flags & DropBoxManager.IS_EMPTY) != 0) throw new IllegalArgumentException();
Dan Egnor4410ec82009-09-11 16:40:01 -0700540
541 this.tag = tag;
542 this.timestampMillis = timestampMillis;
543 this.flags = flags;
544 this.file = new File(dir, Uri.encode(tag) + "@" + timestampMillis +
Dan Egnorf18a01c2009-11-12 11:32:50 -0800545 ((flags & DropBoxManager.IS_TEXT) != 0 ? ".txt" : ".dat") +
546 ((flags & DropBoxManager.IS_GZIPPED) != 0 ? ".gz" : ""));
Dan Egnor4410ec82009-09-11 16:40:01 -0700547
548 if (!temp.renameTo(this.file)) {
549 throw new IOException("Can't rename " + temp + " to " + this.file);
550 }
551 this.blocks = (int) ((this.file.length() + blockSize - 1) / blockSize);
552 }
553
554 /**
555 * Creates a zero-length tombstone for a file whose contents were lost.
556 * @param dir to store file in
557 * @param tag to use for new log file name
558 * @param timestampMillis of log entry
559 * @throws IOException if the file can't be created.
560 */
561 public EntryFile(File dir, String tag, long timestampMillis) throws IOException {
562 this.tag = tag;
563 this.timestampMillis = timestampMillis;
Dan Egnorf18a01c2009-11-12 11:32:50 -0800564 this.flags = DropBoxManager.IS_EMPTY;
Dan Egnor4410ec82009-09-11 16:40:01 -0700565 this.file = new File(dir, Uri.encode(tag) + "@" + timestampMillis + ".lost");
566 this.blocks = 0;
567 new FileOutputStream(this.file).close();
568 }
569
570 /**
571 * Extracts metadata from an existing on-disk log filename.
572 * @param file name of existing log file
573 * @param blockSize to use for space accounting
574 */
575 public EntryFile(File file, int blockSize) {
576 this.file = file;
577 this.blocks = (int) ((this.file.length() + blockSize - 1) / blockSize);
578
579 String name = file.getName();
580 int at = name.lastIndexOf('@');
581 if (at < 0) {
582 this.tag = null;
583 this.timestampMillis = 0;
Dan Egnorf18a01c2009-11-12 11:32:50 -0800584 this.flags = DropBoxManager.IS_EMPTY;
Dan Egnor4410ec82009-09-11 16:40:01 -0700585 return;
586 }
587
588 int flags = 0;
589 this.tag = Uri.decode(name.substring(0, at));
590 if (name.endsWith(".gz")) {
Dan Egnorf18a01c2009-11-12 11:32:50 -0800591 flags |= DropBoxManager.IS_GZIPPED;
Dan Egnor4410ec82009-09-11 16:40:01 -0700592 name = name.substring(0, name.length() - 3);
593 }
594 if (name.endsWith(".lost")) {
Dan Egnorf18a01c2009-11-12 11:32:50 -0800595 flags |= DropBoxManager.IS_EMPTY;
Dan Egnor4410ec82009-09-11 16:40:01 -0700596 name = name.substring(at + 1, name.length() - 5);
597 } else if (name.endsWith(".txt")) {
Dan Egnorf18a01c2009-11-12 11:32:50 -0800598 flags |= DropBoxManager.IS_TEXT;
Dan Egnor4410ec82009-09-11 16:40:01 -0700599 name = name.substring(at + 1, name.length() - 4);
600 } else if (name.endsWith(".dat")) {
601 name = name.substring(at + 1, name.length() - 4);
602 } else {
Dan Egnorf18a01c2009-11-12 11:32:50 -0800603 this.flags = DropBoxManager.IS_EMPTY;
Dan Egnor4410ec82009-09-11 16:40:01 -0700604 this.timestampMillis = 0;
605 return;
606 }
607 this.flags = flags;
608
609 long millis;
610 try { millis = Long.valueOf(name); } catch (NumberFormatException e) { millis = 0; }
611 this.timestampMillis = millis;
612 }
613
614 /**
615 * Creates a EntryFile object with only a timestamp for comparison purposes.
Xiaohui Chene4de5a02015-09-22 15:33:31 -0700616 * @param millis to compare with.
Dan Egnor4410ec82009-09-11 16:40:01 -0700617 */
618 public EntryFile(long millis) {
619 this.tag = null;
620 this.timestampMillis = millis;
Dan Egnorf18a01c2009-11-12 11:32:50 -0800621 this.flags = DropBoxManager.IS_EMPTY;
Dan Egnor4410ec82009-09-11 16:40:01 -0700622 this.file = null;
623 this.blocks = 0;
624 }
625 }
626
627 ///////////////////////////////////////////////////////////////////////////
628
629 /** If never run before, scans disk contents to build in-memory tracking data. */
630 private synchronized void init() throws IOException {
631 if (mStatFs == null) {
632 if (!mDropBoxDir.isDirectory() && !mDropBoxDir.mkdirs()) {
633 throw new IOException("Can't mkdir: " + mDropBoxDir);
634 }
635 try {
636 mStatFs = new StatFs(mDropBoxDir.getPath());
637 mBlockSize = mStatFs.getBlockSize();
638 } catch (IllegalArgumentException e) { // StatFs throws this on error
639 throw new IOException("Can't statfs: " + mDropBoxDir);
640 }
641 }
642
643 if (mAllFiles == null) {
644 File[] files = mDropBoxDir.listFiles();
645 if (files == null) throw new IOException("Can't list files: " + mDropBoxDir);
646
647 mAllFiles = new FileList();
648 mFilesByTag = new HashMap<String, FileList>();
649
650 // Scan pre-existing files.
651 for (File file : files) {
652 if (file.getName().endsWith(".tmp")) {
Joe Onorato8a9b2202010-02-26 18:56:32 -0800653 Slog.i(TAG, "Cleaning temp file: " + file);
Dan Egnor4410ec82009-09-11 16:40:01 -0700654 file.delete();
655 continue;
656 }
657
658 EntryFile entry = new EntryFile(file, mBlockSize);
659 if (entry.tag == null) {
Joe Onorato8a9b2202010-02-26 18:56:32 -0800660 Slog.w(TAG, "Unrecognized file: " + file);
Dan Egnor4410ec82009-09-11 16:40:01 -0700661 continue;
662 } else if (entry.timestampMillis == 0) {
Joe Onorato8a9b2202010-02-26 18:56:32 -0800663 Slog.w(TAG, "Invalid filename: " + file);
Dan Egnor4410ec82009-09-11 16:40:01 -0700664 file.delete();
665 continue;
666 }
667
668 enrollEntry(entry);
669 }
670 }
671 }
672
673 /** Adds a disk log file to in-memory tracking for accounting and enumeration. */
674 private synchronized void enrollEntry(EntryFile entry) {
675 mAllFiles.contents.add(entry);
676 mAllFiles.blocks += entry.blocks;
677
678 // mFilesByTag is used for trimming, so don't list empty files.
679 // (Zero-length/lost files are trimmed by date from mAllFiles.)
680
681 if (entry.tag != null && entry.file != null && entry.blocks > 0) {
682 FileList tagFiles = mFilesByTag.get(entry.tag);
683 if (tagFiles == null) {
684 tagFiles = new FileList();
685 mFilesByTag.put(entry.tag, tagFiles);
686 }
687 tagFiles.contents.add(entry);
688 tagFiles.blocks += entry.blocks;
689 }
690 }
691
692 /** Moves a temporary file to a final log filename and enrolls it. */
Hakan Stillb2475362010-12-07 14:05:55 +0100693 private synchronized long createEntry(File temp, String tag, int flags) throws IOException {
Dan Egnor4410ec82009-09-11 16:40:01 -0700694 long t = System.currentTimeMillis();
695
696 // Require each entry to have a unique timestamp; if there are entries
697 // >10sec in the future (due to clock skew), drag them back to avoid
698 // keeping them around forever.
699
700 SortedSet<EntryFile> tail = mAllFiles.contents.tailSet(new EntryFile(t + 10000));
701 EntryFile[] future = null;
702 if (!tail.isEmpty()) {
703 future = tail.toArray(new EntryFile[tail.size()]);
704 tail.clear(); // Remove from mAllFiles
705 }
706
707 if (!mAllFiles.contents.isEmpty()) {
708 t = Math.max(t, mAllFiles.contents.last().timestampMillis + 1);
709 }
710
711 if (future != null) {
712 for (EntryFile late : future) {
713 mAllFiles.blocks -= late.blocks;
714 FileList tagFiles = mFilesByTag.get(late.tag);
Dan Egnorf283e362010-03-10 16:49:55 -0800715 if (tagFiles != null && tagFiles.contents.remove(late)) {
716 tagFiles.blocks -= late.blocks;
717 }
Dan Egnorf18a01c2009-11-12 11:32:50 -0800718 if ((late.flags & DropBoxManager.IS_EMPTY) == 0) {
Dan Egnor4410ec82009-09-11 16:40:01 -0700719 enrollEntry(new EntryFile(
720 late.file, mDropBoxDir, late.tag, t++, late.flags, mBlockSize));
721 } else {
722 enrollEntry(new EntryFile(mDropBoxDir, late.tag, t++));
723 }
724 }
725 }
726
727 if (temp == null) {
728 enrollEntry(new EntryFile(mDropBoxDir, tag, t));
729 } else {
730 enrollEntry(new EntryFile(temp, mDropBoxDir, tag, t, flags, mBlockSize));
731 }
Hakan Stillb2475362010-12-07 14:05:55 +0100732 return t;
Dan Egnor4410ec82009-09-11 16:40:01 -0700733 }
734
735 /**
736 * Trims the files on disk to make sure they aren't using too much space.
737 * @return the overall quota for storage (in bytes)
738 */
739 private synchronized long trimToFit() {
740 // Expunge aged items (including tombstones marking deleted data).
741
Jeff Sharkey625239a2012-09-26 22:03:49 -0700742 int ageSeconds = Settings.Global.getInt(mContentResolver,
743 Settings.Global.DROPBOX_AGE_SECONDS, DEFAULT_AGE_SECONDS);
744 int maxFiles = Settings.Global.getInt(mContentResolver,
745 Settings.Global.DROPBOX_MAX_FILES, DEFAULT_MAX_FILES);
Dan Egnor4410ec82009-09-11 16:40:01 -0700746 long cutoffMillis = System.currentTimeMillis() - ageSeconds * 1000;
747 while (!mAllFiles.contents.isEmpty()) {
748 EntryFile entry = mAllFiles.contents.first();
Dan Egnor3a8b0c12010-03-24 17:48:20 -0700749 if (entry.timestampMillis > cutoffMillis && mAllFiles.contents.size() < maxFiles) break;
Dan Egnor4410ec82009-09-11 16:40:01 -0700750
751 FileList tag = mFilesByTag.get(entry.tag);
752 if (tag != null && tag.contents.remove(entry)) tag.blocks -= entry.blocks;
753 if (mAllFiles.contents.remove(entry)) mAllFiles.blocks -= entry.blocks;
754 if (entry.file != null) entry.file.delete();
755 }
756
757 // Compute overall quota (a fraction of available free space) in blocks.
758 // The quota changes dynamically based on the amount of free space;
759 // that way when lots of data is available we can use it, but we'll get
760 // out of the way if storage starts getting tight.
761
762 long uptimeMillis = SystemClock.uptimeMillis();
763 if (uptimeMillis > mCachedQuotaUptimeMillis + QUOTA_RESCAN_MILLIS) {
Jeff Sharkey625239a2012-09-26 22:03:49 -0700764 int quotaPercent = Settings.Global.getInt(mContentResolver,
765 Settings.Global.DROPBOX_QUOTA_PERCENT, DEFAULT_QUOTA_PERCENT);
766 int reservePercent = Settings.Global.getInt(mContentResolver,
767 Settings.Global.DROPBOX_RESERVE_PERCENT, DEFAULT_RESERVE_PERCENT);
768 int quotaKb = Settings.Global.getInt(mContentResolver,
769 Settings.Global.DROPBOX_QUOTA_KB, DEFAULT_QUOTA_KB);
Dan Egnor4410ec82009-09-11 16:40:01 -0700770
771 mStatFs.restat(mDropBoxDir.getPath());
772 int available = mStatFs.getAvailableBlocks();
773 int nonreserved = available - mStatFs.getBlockCount() * reservePercent / 100;
774 int maximum = quotaKb * 1024 / mBlockSize;
775 mCachedQuotaBlocks = Math.min(maximum, Math.max(0, nonreserved * quotaPercent / 100));
776 mCachedQuotaUptimeMillis = uptimeMillis;
777 }
778
779 // If we're using too much space, delete old items to make room.
780 //
781 // We trim each tag independently (this is why we keep per-tag lists).
782 // Space is "fairly" shared between tags -- they are all squeezed
783 // equally until enough space is reclaimed.
784 //
785 // A single circular buffer (a la logcat) would be simpler, but this
786 // way we can handle fat/bursty data (like 1MB+ bugreports, 300KB+
787 // kernel crash dumps, and 100KB+ ANR reports) without swamping small,
Dan Egnor3a8b0c12010-03-24 17:48:20 -0700788 // well-behaved data streams (event statistics, profile data, etc).
Dan Egnor4410ec82009-09-11 16:40:01 -0700789 //
790 // Deleted files are replaced with zero-length tombstones to mark what
791 // was lost. Tombstones are expunged by age (see above).
792
793 if (mAllFiles.blocks > mCachedQuotaBlocks) {
Dan Egnor4410ec82009-09-11 16:40:01 -0700794 // Find a fair share amount of space to limit each tag
795 int unsqueezed = mAllFiles.blocks, squeezed = 0;
796 TreeSet<FileList> tags = new TreeSet<FileList>(mFilesByTag.values());
797 for (FileList tag : tags) {
798 if (squeezed > 0 && tag.blocks <= (mCachedQuotaBlocks - unsqueezed) / squeezed) {
799 break;
800 }
801 unsqueezed -= tag.blocks;
802 squeezed++;
803 }
804 int tagQuota = (mCachedQuotaBlocks - unsqueezed) / squeezed;
805
806 // Remove old items from each tag until it meets the per-tag quota.
807 for (FileList tag : tags) {
808 if (mAllFiles.blocks < mCachedQuotaBlocks) break;
809 while (tag.blocks > tagQuota && !tag.contents.isEmpty()) {
810 EntryFile entry = tag.contents.first();
811 if (tag.contents.remove(entry)) tag.blocks -= entry.blocks;
812 if (mAllFiles.contents.remove(entry)) mAllFiles.blocks -= entry.blocks;
813
814 try {
815 if (entry.file != null) entry.file.delete();
816 enrollEntry(new EntryFile(mDropBoxDir, entry.tag, entry.timestampMillis));
817 } catch (IOException e) {
Joe Onorato8a9b2202010-02-26 18:56:32 -0800818 Slog.e(TAG, "Can't write tombstone file", e);
Dan Egnor4410ec82009-09-11 16:40:01 -0700819 }
820 }
821 }
822 }
823
824 return mCachedQuotaBlocks * mBlockSize;
825 }
826}