blob: 27001dc5bde8f7fb5f8232e02bcde315c8be289d [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
Dan Egnorf18a01c2009-11-12 11:32:50 -080019import com.android.internal.os.IDropBoxManagerService;
Dan Egnor95240272009-10-27 18:23:39 -070020
21import java.io.ByteArrayInputStream;
Brad Fitzpatrickcc792c42010-10-08 09:50:30 -070022import java.io.Closeable;
Dan Egnor95240272009-10-27 18:23:39 -070023import java.io.File;
Dan Egnor95240272009-10-27 18:23:39 -070024import java.io.IOException;
25import java.io.InputStream;
26import java.util.zip.GZIPInputStream;
27
28/**
29 * Enqueues chunks of data (from various sources -- application crashes, kernel
30 * log records, etc.). The queue is size bounded and will drop old data if the
31 * enqueued data exceeds the maximum size. You can think of this as a
32 * persistent, system-wide, blob-oriented "logcat".
33 *
34 * <p>You can obtain an instance of this class by calling
35 * {@link android.content.Context#getSystemService}
36 * with {@link android.content.Context#DROPBOX_SERVICE}.
37 *
Dan Egnorf18a01c2009-11-12 11:32:50 -080038 * <p>DropBoxManager entries are not sent anywhere directly, but other system
39 * services and debugging tools may scan and upload entries for processing.
Dan Egnor95240272009-10-27 18:23:39 -070040 */
Dan Egnorf18a01c2009-11-12 11:32:50 -080041public class DropBoxManager {
42 private static final String TAG = "DropBoxManager";
43 private final IDropBoxManagerService mService;
Dan Egnor95240272009-10-27 18:23:39 -070044
45 /** Flag value: Entry's content was deleted to save space. */
46 public static final int IS_EMPTY = 1;
47
48 /** Flag value: Content is human-readable UTF-8 text (can be combined with IS_GZIPPED). */
49 public static final int IS_TEXT = 2;
50
Dan Egnore3cfe2d2009-11-14 16:20:04 -080051 /** Flag value: Content can be decompressed with {@link java.util.zip.GZIPOutputStream}. */
Dan Egnor95240272009-10-27 18:23:39 -070052 public static final int IS_GZIPPED = 4;
53
Dan Egnor6e6d60d2010-07-20 15:24:09 -070054 /** Flag value for serialization only: Value is a byte array, not a file descriptor */
55 private static final int HAS_BYTE_ARRAY = 8;
56
Dan Egnor95240272009-10-27 18:23:39 -070057 /**
Hakan Stillb2475362010-12-07 14:05:55 +010058 * Broadcast Action: This is broadcast when a new entry is added in the dropbox.
59 * You must hold the {@link android.Manifest.permission#READ_LOGS} permission
60 * in order to receive this broadcast.
61 *
62 * <p class="note">This is a protected intent that can only be sent
63 * by the system.
64 */
65 public static final String ACTION_DROPBOX_ENTRY_ADDED =
66 "android.intent.action.DROPBOX_ENTRY_ADDED";
67
68 /**
69 * Extra for {@link android.os.DropBoxManager#ACTION_DROPBOX_ENTRY_ADDED}:
70 * string containing the dropbox tag.
71 */
72 public static final String EXTRA_TAG = "tag";
73
74 /**
75 * Extra for {@link android.os.DropBoxManager#ACTION_DROPBOX_ENTRY_ADDED}:
76 * long integer value containing time (in milliseconds since January 1, 1970 00:00:00 UTC)
77 * when the entry was created.
78 */
79 public static final String EXTRA_TIME = "time";
80
81 /**
Dan Egnor95240272009-10-27 18:23:39 -070082 * A single entry retrieved from the drop box.
83 * This may include a reference to a stream, so you must call
84 * {@link #close()} when you are done using it.
85 */
Brad Fitzpatrickcc792c42010-10-08 09:50:30 -070086 public static class Entry implements Parcelable, Closeable {
Dan Egnor95240272009-10-27 18:23:39 -070087 private final String mTag;
88 private final long mTimeMillis;
89
90 private final byte[] mData;
91 private final ParcelFileDescriptor mFileDescriptor;
92 private final int mFlags;
93
94 /** Create a new empty Entry with no contents. */
95 public Entry(String tag, long millis) {
Dan Egnor6e6d60d2010-07-20 15:24:09 -070096 if (tag == null) throw new NullPointerException("tag == null");
97
98 mTag = tag;
99 mTimeMillis = millis;
100 mData = null;
101 mFileDescriptor = null;
102 mFlags = IS_EMPTY;
Dan Egnor95240272009-10-27 18:23:39 -0700103 }
104
105 /** Create a new Entry with plain text contents. */
106 public Entry(String tag, long millis, String text) {
Dan Egnor6e6d60d2010-07-20 15:24:09 -0700107 if (tag == null) throw new NullPointerException("tag == null");
108 if (text == null) throw new NullPointerException("text == null");
109
110 mTag = tag;
111 mTimeMillis = millis;
112 mData = text.getBytes();
113 mFileDescriptor = null;
114 mFlags = IS_TEXT;
Dan Egnor95240272009-10-27 18:23:39 -0700115 }
116
117 /**
118 * Create a new Entry with byte array contents.
119 * The data array must not be modified after creating this entry.
120 */
121 public Entry(String tag, long millis, byte[] data, int flags) {
Dan Egnor6e6d60d2010-07-20 15:24:09 -0700122 if (tag == null) throw new NullPointerException("tag == null");
123 if (((flags & IS_EMPTY) != 0) != (data == null)) {
124 throw new IllegalArgumentException("Bad flags: " + flags);
125 }
126
127 mTag = tag;
128 mTimeMillis = millis;
129 mData = data;
130 mFileDescriptor = null;
131 mFlags = flags;
Dan Egnor95240272009-10-27 18:23:39 -0700132 }
133
134 /**
135 * Create a new Entry with streaming data contents.
136 * Takes ownership of the ParcelFileDescriptor.
137 */
138 public Entry(String tag, long millis, ParcelFileDescriptor data, int flags) {
Dan Egnor6e6d60d2010-07-20 15:24:09 -0700139 if (tag == null) throw new NullPointerException("tag == null");
140 if (((flags & IS_EMPTY) != 0) != (data == null)) {
141 throw new IllegalArgumentException("Bad flags: " + flags);
142 }
143
144 mTag = tag;
145 mTimeMillis = millis;
146 mData = null;
147 mFileDescriptor = data;
148 mFlags = flags;
Dan Egnor95240272009-10-27 18:23:39 -0700149 }
150
151 /**
152 * Create a new Entry with the contents read from a file.
153 * The file will be read when the entry's contents are requested.
154 */
155 public Entry(String tag, long millis, File data, int flags) throws IOException {
Dan Egnor6e6d60d2010-07-20 15:24:09 -0700156 if (tag == null) throw new NullPointerException("tag == null");
157 if ((flags & IS_EMPTY) != 0) throw new IllegalArgumentException("Bad flags: " + flags);
Dan Egnor95240272009-10-27 18:23:39 -0700158
159 mTag = tag;
160 mTimeMillis = millis;
Dan Egnor6e6d60d2010-07-20 15:24:09 -0700161 mData = null;
162 mFileDescriptor = ParcelFileDescriptor.open(data, ParcelFileDescriptor.MODE_READ_ONLY);
Dan Egnor95240272009-10-27 18:23:39 -0700163 mFlags = flags;
Dan Egnor95240272009-10-27 18:23:39 -0700164 }
165
166 /** Close the input stream associated with this entry. */
167 public void close() {
168 try { if (mFileDescriptor != null) mFileDescriptor.close(); } catch (IOException e) { }
169 }
170
171 /** @return the tag originally attached to the entry. */
172 public String getTag() { return mTag; }
173
174 /** @return time when the entry was originally created. */
175 public long getTimeMillis() { return mTimeMillis; }
176
Brad Fitzpatrick95173b12010-10-07 10:07:34 -0700177 /** @return flags describing the content returned by {@link #getInputStream()}. */
Dan Egnor95240272009-10-27 18:23:39 -0700178 public int getFlags() { return mFlags & ~IS_GZIPPED; } // getInputStream() decompresses.
179
180 /**
181 * @param maxBytes of string to return (will truncate at this length).
182 * @return the uncompressed text contents of the entry, null if the entry is not text.
183 */
184 public String getText(int maxBytes) {
185 if ((mFlags & IS_TEXT) == 0) return null;
186 if (mData != null) return new String(mData, 0, Math.min(maxBytes, mData.length));
187
188 InputStream is = null;
189 try {
190 is = getInputStream();
Dan Egnor6e6d60d2010-07-20 15:24:09 -0700191 if (is == null) return null;
Dan Egnor95240272009-10-27 18:23:39 -0700192 byte[] buf = new byte[maxBytes];
Christian Lindeberge9f18812010-11-16 09:57:54 +0100193 int readBytes = 0;
194 int n = 0;
195 while (n >= 0 && (readBytes += n) < maxBytes) {
196 n = is.read(buf, readBytes, maxBytes - readBytes);
197 }
198 return new String(buf, 0, readBytes);
Dan Egnor95240272009-10-27 18:23:39 -0700199 } catch (IOException e) {
200 return null;
201 } finally {
202 try { if (is != null) is.close(); } catch (IOException e) {}
203 }
204 }
205
206 /** @return the uncompressed contents of the entry, or null if the contents were lost */
207 public InputStream getInputStream() throws IOException {
208 InputStream is;
209 if (mData != null) {
210 is = new ByteArrayInputStream(mData);
211 } else if (mFileDescriptor != null) {
212 is = new ParcelFileDescriptor.AutoCloseInputStream(mFileDescriptor);
213 } else {
214 return null;
215 }
216 return (mFlags & IS_GZIPPED) != 0 ? new GZIPInputStream(is) : is;
217 }
218
219 public static final Parcelable.Creator<Entry> CREATOR = new Parcelable.Creator() {
220 public Entry[] newArray(int size) { return new Entry[size]; }
221 public Entry createFromParcel(Parcel in) {
Dan Egnor6e6d60d2010-07-20 15:24:09 -0700222 String tag = in.readString();
223 long millis = in.readLong();
224 int flags = in.readInt();
225 if ((flags & HAS_BYTE_ARRAY) != 0) {
226 return new Entry(tag, millis, in.createByteArray(), flags & ~HAS_BYTE_ARRAY);
227 } else {
228 return new Entry(tag, millis, in.readFileDescriptor(), flags);
229 }
Dan Egnor95240272009-10-27 18:23:39 -0700230 }
231 };
232
233 public int describeContents() {
234 return mFileDescriptor != null ? Parcelable.CONTENTS_FILE_DESCRIPTOR : 0;
235 }
236
237 public void writeToParcel(Parcel out, int flags) {
238 out.writeString(mTag);
239 out.writeLong(mTimeMillis);
240 if (mFileDescriptor != null) {
Dan Egnor6e6d60d2010-07-20 15:24:09 -0700241 out.writeInt(mFlags & ~HAS_BYTE_ARRAY); // Clear bit just to be safe
242 mFileDescriptor.writeToParcel(out, flags);
Dan Egnor95240272009-10-27 18:23:39 -0700243 } else {
Dan Egnor6e6d60d2010-07-20 15:24:09 -0700244 out.writeInt(mFlags | HAS_BYTE_ARRAY);
245 out.writeByteArray(mData);
Dan Egnor95240272009-10-27 18:23:39 -0700246 }
Dan Egnor95240272009-10-27 18:23:39 -0700247 }
248 }
249
250 /** {@hide} */
Dan Egnorf18a01c2009-11-12 11:32:50 -0800251 public DropBoxManager(IDropBoxManagerService service) { mService = service; }
Dan Egnor95240272009-10-27 18:23:39 -0700252
253 /**
254 * Create a dummy instance for testing. All methods will fail unless
255 * overridden with an appropriate mock implementation. To obtain a
256 * functional instance, use {@link android.content.Context#getSystemService}.
257 */
Dan Egnorf18a01c2009-11-12 11:32:50 -0800258 protected DropBoxManager() { mService = null; }
Dan Egnor95240272009-10-27 18:23:39 -0700259
260 /**
261 * Stores human-readable text. The data may be discarded eventually (or even
262 * immediately) if space is limited, or ignored entirely if the tag has been
263 * blocked (see {@link #isTagEnabled}).
264 *
265 * @param tag describing the type of entry being stored
266 * @param data value to store
267 */
268 public void addText(String tag, String data) {
269 try { mService.add(new Entry(tag, 0, data)); } catch (RemoteException e) {}
270 }
271
272 /**
273 * Stores binary data, which may be ignored or discarded as with {@link #addText}.
274 *
275 * @param tag describing the type of entry being stored
276 * @param data value to store
277 * @param flags describing the data
278 */
279 public void addData(String tag, byte[] data, int flags) {
Dan Egnor6e6d60d2010-07-20 15:24:09 -0700280 if (data == null) throw new NullPointerException("data == null");
Dan Egnor95240272009-10-27 18:23:39 -0700281 try { mService.add(new Entry(tag, 0, data, flags)); } catch (RemoteException e) {}
282 }
283
284 /**
Dan Egnoreb7a7d52009-11-25 12:38:00 -0800285 * Stores the contents of a file, which may be ignored or discarded as with
286 * {@link #addText}.
Dan Egnor95240272009-10-27 18:23:39 -0700287 *
288 * @param tag describing the type of entry being stored
Dan Egnoreb7a7d52009-11-25 12:38:00 -0800289 * @param file to read from
Dan Egnor95240272009-10-27 18:23:39 -0700290 * @param flags describing the data
Dan Egnoreb7a7d52009-11-25 12:38:00 -0800291 * @throws IOException if the file can't be opened
Dan Egnor95240272009-10-27 18:23:39 -0700292 */
Dan Egnoreb7a7d52009-11-25 12:38:00 -0800293 public void addFile(String tag, File file, int flags) throws IOException {
Dan Egnor6e6d60d2010-07-20 15:24:09 -0700294 if (file == null) throw new NullPointerException("file == null");
Dan Egnoreb7a7d52009-11-25 12:38:00 -0800295 Entry entry = new Entry(tag, 0, file, flags);
296 try {
Brad Fitzpatrick14418942010-06-14 08:58:26 -0700297 mService.add(entry);
Dan Egnoreb7a7d52009-11-25 12:38:00 -0800298 } catch (RemoteException e) {
299 // ignore
300 } finally {
301 entry.close();
302 }
Dan Egnor95240272009-10-27 18:23:39 -0700303 }
304
305 /**
306 * Checks any blacklists (set in system settings) to see whether a certain
307 * tag is allowed. Entries with disabled tags will be dropped immediately,
308 * so you can save the work of actually constructing and sending the data.
309 *
310 * @param tag that would be used in {@link #addText} or {@link #addFile}
311 * @return whether events with that tag would be accepted
312 */
313 public boolean isTagEnabled(String tag) {
314 try { return mService.isTagEnabled(tag); } catch (RemoteException e) { return false; }
315 }
316
317 /**
Brad Fitzpatrick30f5c8f2010-10-07 10:48:20 -0700318 * Gets the next entry from the drop box <em>after</em> the specified time.
319 * Requires <code>android.permission.READ_LOGS</code>. You must always call
Dan Egnor95240272009-10-27 18:23:39 -0700320 * {@link Entry#close()} on the return value!
321 *
322 * @param tag of entry to look for, null for all tags
323 * @param msec time of the last entry seen
324 * @return the next entry, or null if there are no more entries
325 */
326 public Entry getNextEntry(String tag, long msec) {
327 try { return mService.getNextEntry(tag, msec); } catch (RemoteException e) { return null; }
328 }
329
330 // TODO: It may be useful to have some sort of notification mechanism
331 // when data is added to the dropbox, for demand-driven readers --
332 // for now readers need to poll the dropbox to find new data.
333}