blob: e1c16780d68d29260c65d6f17287a3434d1cc4b5 [file] [log] [blame]
Dan Egnor95240272009-10-27 18:23:39 -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 android.os;
18
19import android.util.Log;
20
Dan Egnorf18a01c2009-11-12 11:32:50 -080021import com.android.internal.os.IDropBoxManagerService;
Dan Egnor95240272009-10-27 18:23:39 -070022
23import java.io.ByteArrayInputStream;
Brad Fitzpatrickcc792c42010-10-08 09:50:30 -070024import java.io.Closeable;
Dan Egnor95240272009-10-27 18:23:39 -070025import java.io.File;
26import java.io.FileInputStream;
27import java.io.IOException;
28import java.io.InputStream;
29import java.util.zip.GZIPInputStream;
30
31/**
32 * Enqueues chunks of data (from various sources -- application crashes, kernel
33 * log records, etc.). The queue is size bounded and will drop old data if the
34 * enqueued data exceeds the maximum size. You can think of this as a
35 * persistent, system-wide, blob-oriented "logcat".
36 *
37 * <p>You can obtain an instance of this class by calling
38 * {@link android.content.Context#getSystemService}
39 * with {@link android.content.Context#DROPBOX_SERVICE}.
40 *
Dan Egnorf18a01c2009-11-12 11:32:50 -080041 * <p>DropBoxManager entries are not sent anywhere directly, but other system
42 * services and debugging tools may scan and upload entries for processing.
Dan Egnor95240272009-10-27 18:23:39 -070043 */
Dan Egnorf18a01c2009-11-12 11:32:50 -080044public class DropBoxManager {
45 private static final String TAG = "DropBoxManager";
46 private final IDropBoxManagerService mService;
Dan Egnor95240272009-10-27 18:23:39 -070047
48 /** Flag value: Entry's content was deleted to save space. */
49 public static final int IS_EMPTY = 1;
50
51 /** Flag value: Content is human-readable UTF-8 text (can be combined with IS_GZIPPED). */
52 public static final int IS_TEXT = 2;
53
Dan Egnore3cfe2d2009-11-14 16:20:04 -080054 /** Flag value: Content can be decompressed with {@link java.util.zip.GZIPOutputStream}. */
Dan Egnor95240272009-10-27 18:23:39 -070055 public static final int IS_GZIPPED = 4;
56
Dan Egnor6e6d60d2010-07-20 15:24:09 -070057 /** Flag value for serialization only: Value is a byte array, not a file descriptor */
58 private static final int HAS_BYTE_ARRAY = 8;
59
Dan Egnor95240272009-10-27 18:23:39 -070060 /**
Hakan Stillb2475362010-12-07 14:05:55 +010061 * Broadcast Action: This is broadcast when a new entry is added in the dropbox.
62 * You must hold the {@link android.Manifest.permission#READ_LOGS} permission
63 * in order to receive this broadcast.
64 *
65 * <p class="note">This is a protected intent that can only be sent
66 * by the system.
67 */
68 public static final String ACTION_DROPBOX_ENTRY_ADDED =
69 "android.intent.action.DROPBOX_ENTRY_ADDED";
70
71 /**
72 * Extra for {@link android.os.DropBoxManager#ACTION_DROPBOX_ENTRY_ADDED}:
73 * string containing the dropbox tag.
74 */
75 public static final String EXTRA_TAG = "tag";
76
77 /**
78 * Extra for {@link android.os.DropBoxManager#ACTION_DROPBOX_ENTRY_ADDED}:
79 * long integer value containing time (in milliseconds since January 1, 1970 00:00:00 UTC)
80 * when the entry was created.
81 */
82 public static final String EXTRA_TIME = "time";
83
84 /**
Dan Egnor95240272009-10-27 18:23:39 -070085 * A single entry retrieved from the drop box.
86 * This may include a reference to a stream, so you must call
87 * {@link #close()} when you are done using it.
88 */
Brad Fitzpatrickcc792c42010-10-08 09:50:30 -070089 public static class Entry implements Parcelable, Closeable {
Dan Egnor95240272009-10-27 18:23:39 -070090 private final String mTag;
91 private final long mTimeMillis;
92
93 private final byte[] mData;
94 private final ParcelFileDescriptor mFileDescriptor;
95 private final int mFlags;
96
97 /** Create a new empty Entry with no contents. */
98 public Entry(String tag, long millis) {
Dan Egnor6e6d60d2010-07-20 15:24:09 -070099 if (tag == null) throw new NullPointerException("tag == null");
100
101 mTag = tag;
102 mTimeMillis = millis;
103 mData = null;
104 mFileDescriptor = null;
105 mFlags = IS_EMPTY;
Dan Egnor95240272009-10-27 18:23:39 -0700106 }
107
108 /** Create a new Entry with plain text contents. */
109 public Entry(String tag, long millis, String text) {
Dan Egnor6e6d60d2010-07-20 15:24:09 -0700110 if (tag == null) throw new NullPointerException("tag == null");
111 if (text == null) throw new NullPointerException("text == null");
112
113 mTag = tag;
114 mTimeMillis = millis;
115 mData = text.getBytes();
116 mFileDescriptor = null;
117 mFlags = IS_TEXT;
Dan Egnor95240272009-10-27 18:23:39 -0700118 }
119
120 /**
121 * Create a new Entry with byte array contents.
122 * The data array must not be modified after creating this entry.
123 */
124 public Entry(String tag, long millis, byte[] data, int flags) {
Dan Egnor6e6d60d2010-07-20 15:24:09 -0700125 if (tag == null) throw new NullPointerException("tag == null");
126 if (((flags & IS_EMPTY) != 0) != (data == null)) {
127 throw new IllegalArgumentException("Bad flags: " + flags);
128 }
129
130 mTag = tag;
131 mTimeMillis = millis;
132 mData = data;
133 mFileDescriptor = null;
134 mFlags = flags;
Dan Egnor95240272009-10-27 18:23:39 -0700135 }
136
137 /**
138 * Create a new Entry with streaming data contents.
139 * Takes ownership of the ParcelFileDescriptor.
140 */
141 public Entry(String tag, long millis, ParcelFileDescriptor data, int flags) {
Dan Egnor6e6d60d2010-07-20 15:24:09 -0700142 if (tag == null) throw new NullPointerException("tag == null");
143 if (((flags & IS_EMPTY) != 0) != (data == null)) {
144 throw new IllegalArgumentException("Bad flags: " + flags);
145 }
146
147 mTag = tag;
148 mTimeMillis = millis;
149 mData = null;
150 mFileDescriptor = data;
151 mFlags = flags;
Dan Egnor95240272009-10-27 18:23:39 -0700152 }
153
154 /**
155 * Create a new Entry with the contents read from a file.
156 * The file will be read when the entry's contents are requested.
157 */
158 public Entry(String tag, long millis, File data, int flags) throws IOException {
Dan Egnor6e6d60d2010-07-20 15:24:09 -0700159 if (tag == null) throw new NullPointerException("tag == null");
160 if ((flags & IS_EMPTY) != 0) throw new IllegalArgumentException("Bad flags: " + flags);
Dan Egnor95240272009-10-27 18:23:39 -0700161
162 mTag = tag;
163 mTimeMillis = millis;
Dan Egnor6e6d60d2010-07-20 15:24:09 -0700164 mData = null;
165 mFileDescriptor = ParcelFileDescriptor.open(data, ParcelFileDescriptor.MODE_READ_ONLY);
Dan Egnor95240272009-10-27 18:23:39 -0700166 mFlags = flags;
Dan Egnor95240272009-10-27 18:23:39 -0700167 }
168
169 /** Close the input stream associated with this entry. */
170 public void close() {
171 try { if (mFileDescriptor != null) mFileDescriptor.close(); } catch (IOException e) { }
172 }
173
174 /** @return the tag originally attached to the entry. */
175 public String getTag() { return mTag; }
176
177 /** @return time when the entry was originally created. */
178 public long getTimeMillis() { return mTimeMillis; }
179
Brad Fitzpatrick95173b12010-10-07 10:07:34 -0700180 /** @return flags describing the content returned by {@link #getInputStream()}. */
Dan Egnor95240272009-10-27 18:23:39 -0700181 public int getFlags() { return mFlags & ~IS_GZIPPED; } // getInputStream() decompresses.
182
183 /**
184 * @param maxBytes of string to return (will truncate at this length).
185 * @return the uncompressed text contents of the entry, null if the entry is not text.
186 */
187 public String getText(int maxBytes) {
188 if ((mFlags & IS_TEXT) == 0) return null;
189 if (mData != null) return new String(mData, 0, Math.min(maxBytes, mData.length));
190
191 InputStream is = null;
192 try {
193 is = getInputStream();
Dan Egnor6e6d60d2010-07-20 15:24:09 -0700194 if (is == null) return null;
Dan Egnor95240272009-10-27 18:23:39 -0700195 byte[] buf = new byte[maxBytes];
Christian Lindeberge9f18812010-11-16 09:57:54 +0100196 int readBytes = 0;
197 int n = 0;
198 while (n >= 0 && (readBytes += n) < maxBytes) {
199 n = is.read(buf, readBytes, maxBytes - readBytes);
200 }
201 return new String(buf, 0, readBytes);
Dan Egnor95240272009-10-27 18:23:39 -0700202 } catch (IOException e) {
203 return null;
204 } finally {
205 try { if (is != null) is.close(); } catch (IOException e) {}
206 }
207 }
208
209 /** @return the uncompressed contents of the entry, or null if the contents were lost */
210 public InputStream getInputStream() throws IOException {
211 InputStream is;
212 if (mData != null) {
213 is = new ByteArrayInputStream(mData);
214 } else if (mFileDescriptor != null) {
215 is = new ParcelFileDescriptor.AutoCloseInputStream(mFileDescriptor);
216 } else {
217 return null;
218 }
219 return (mFlags & IS_GZIPPED) != 0 ? new GZIPInputStream(is) : is;
220 }
221
222 public static final Parcelable.Creator<Entry> CREATOR = new Parcelable.Creator() {
223 public Entry[] newArray(int size) { return new Entry[size]; }
224 public Entry createFromParcel(Parcel in) {
Dan Egnor6e6d60d2010-07-20 15:24:09 -0700225 String tag = in.readString();
226 long millis = in.readLong();
227 int flags = in.readInt();
228 if ((flags & HAS_BYTE_ARRAY) != 0) {
229 return new Entry(tag, millis, in.createByteArray(), flags & ~HAS_BYTE_ARRAY);
230 } else {
231 return new Entry(tag, millis, in.readFileDescriptor(), flags);
232 }
Dan Egnor95240272009-10-27 18:23:39 -0700233 }
234 };
235
236 public int describeContents() {
237 return mFileDescriptor != null ? Parcelable.CONTENTS_FILE_DESCRIPTOR : 0;
238 }
239
240 public void writeToParcel(Parcel out, int flags) {
241 out.writeString(mTag);
242 out.writeLong(mTimeMillis);
243 if (mFileDescriptor != null) {
Dan Egnor6e6d60d2010-07-20 15:24:09 -0700244 out.writeInt(mFlags & ~HAS_BYTE_ARRAY); // Clear bit just to be safe
245 mFileDescriptor.writeToParcel(out, flags);
Dan Egnor95240272009-10-27 18:23:39 -0700246 } else {
Dan Egnor6e6d60d2010-07-20 15:24:09 -0700247 out.writeInt(mFlags | HAS_BYTE_ARRAY);
248 out.writeByteArray(mData);
Dan Egnor95240272009-10-27 18:23:39 -0700249 }
Dan Egnor95240272009-10-27 18:23:39 -0700250 }
251 }
252
253 /** {@hide} */
Dan Egnorf18a01c2009-11-12 11:32:50 -0800254 public DropBoxManager(IDropBoxManagerService service) { mService = service; }
Dan Egnor95240272009-10-27 18:23:39 -0700255
256 /**
257 * Create a dummy instance for testing. All methods will fail unless
258 * overridden with an appropriate mock implementation. To obtain a
259 * functional instance, use {@link android.content.Context#getSystemService}.
260 */
Dan Egnorf18a01c2009-11-12 11:32:50 -0800261 protected DropBoxManager() { mService = null; }
Dan Egnor95240272009-10-27 18:23:39 -0700262
263 /**
264 * Stores human-readable text. The data may be discarded eventually (or even
265 * immediately) if space is limited, or ignored entirely if the tag has been
266 * blocked (see {@link #isTagEnabled}).
267 *
268 * @param tag describing the type of entry being stored
269 * @param data value to store
270 */
271 public void addText(String tag, String data) {
272 try { mService.add(new Entry(tag, 0, data)); } catch (RemoteException e) {}
273 }
274
275 /**
276 * Stores binary data, which may be ignored or discarded as with {@link #addText}.
277 *
278 * @param tag describing the type of entry being stored
279 * @param data value to store
280 * @param flags describing the data
281 */
282 public void addData(String tag, byte[] data, int flags) {
Dan Egnor6e6d60d2010-07-20 15:24:09 -0700283 if (data == null) throw new NullPointerException("data == null");
Dan Egnor95240272009-10-27 18:23:39 -0700284 try { mService.add(new Entry(tag, 0, data, flags)); } catch (RemoteException e) {}
285 }
286
287 /**
Dan Egnoreb7a7d52009-11-25 12:38:00 -0800288 * Stores the contents of a file, which may be ignored or discarded as with
289 * {@link #addText}.
Dan Egnor95240272009-10-27 18:23:39 -0700290 *
291 * @param tag describing the type of entry being stored
Dan Egnoreb7a7d52009-11-25 12:38:00 -0800292 * @param file to read from
Dan Egnor95240272009-10-27 18:23:39 -0700293 * @param flags describing the data
Dan Egnoreb7a7d52009-11-25 12:38:00 -0800294 * @throws IOException if the file can't be opened
Dan Egnor95240272009-10-27 18:23:39 -0700295 */
Dan Egnoreb7a7d52009-11-25 12:38:00 -0800296 public void addFile(String tag, File file, int flags) throws IOException {
Dan Egnor6e6d60d2010-07-20 15:24:09 -0700297 if (file == null) throw new NullPointerException("file == null");
Dan Egnoreb7a7d52009-11-25 12:38:00 -0800298 Entry entry = new Entry(tag, 0, file, flags);
299 try {
Brad Fitzpatrick14418942010-06-14 08:58:26 -0700300 mService.add(entry);
Dan Egnoreb7a7d52009-11-25 12:38:00 -0800301 } catch (RemoteException e) {
302 // ignore
303 } finally {
304 entry.close();
305 }
Dan Egnor95240272009-10-27 18:23:39 -0700306 }
307
308 /**
309 * Checks any blacklists (set in system settings) to see whether a certain
310 * tag is allowed. Entries with disabled tags will be dropped immediately,
311 * so you can save the work of actually constructing and sending the data.
312 *
313 * @param tag that would be used in {@link #addText} or {@link #addFile}
314 * @return whether events with that tag would be accepted
315 */
316 public boolean isTagEnabled(String tag) {
317 try { return mService.isTagEnabled(tag); } catch (RemoteException e) { return false; }
318 }
319
320 /**
Brad Fitzpatrick30f5c8f2010-10-07 10:48:20 -0700321 * Gets the next entry from the drop box <em>after</em> the specified time.
322 * Requires <code>android.permission.READ_LOGS</code>. You must always call
Dan Egnor95240272009-10-27 18:23:39 -0700323 * {@link Entry#close()} on the return value!
324 *
325 * @param tag of entry to look for, null for all tags
326 * @param msec time of the last entry seen
327 * @return the next entry, or null if there are no more entries
328 */
329 public Entry getNextEntry(String tag, long msec) {
330 try { return mService.getNextEntry(tag, msec); } catch (RemoteException e) { return null; }
331 }
332
333 // TODO: It may be useful to have some sort of notification mechanism
334 // when data is added to the dropbox, for demand-driven readers --
335 // for now readers need to poll the dropbox to find new data.
336}