blob: 2b144683dc708095034c436802dd6c687db2878b [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 {
Tim Kilbournba953232015-06-02 15:57:45 -0700228 ParcelFileDescriptor pfd = ParcelFileDescriptor.CREATOR.createFromParcel(in);
229 return new Entry(tag, millis, pfd, flags);
Dan Egnor6e6d60d2010-07-20 15:24:09 -0700230 }
Dan Egnor95240272009-10-27 18:23:39 -0700231 }
232 };
233
234 public int describeContents() {
235 return mFileDescriptor != null ? Parcelable.CONTENTS_FILE_DESCRIPTOR : 0;
236 }
237
238 public void writeToParcel(Parcel out, int flags) {
239 out.writeString(mTag);
240 out.writeLong(mTimeMillis);
241 if (mFileDescriptor != null) {
Dan Egnor6e6d60d2010-07-20 15:24:09 -0700242 out.writeInt(mFlags & ~HAS_BYTE_ARRAY); // Clear bit just to be safe
243 mFileDescriptor.writeToParcel(out, flags);
Dan Egnor95240272009-10-27 18:23:39 -0700244 } else {
Dan Egnor6e6d60d2010-07-20 15:24:09 -0700245 out.writeInt(mFlags | HAS_BYTE_ARRAY);
246 out.writeByteArray(mData);
Dan Egnor95240272009-10-27 18:23:39 -0700247 }
Dan Egnor95240272009-10-27 18:23:39 -0700248 }
249 }
250
251 /** {@hide} */
Dan Egnorf18a01c2009-11-12 11:32:50 -0800252 public DropBoxManager(IDropBoxManagerService service) { mService = service; }
Dan Egnor95240272009-10-27 18:23:39 -0700253
254 /**
255 * Create a dummy instance for testing. All methods will fail unless
256 * overridden with an appropriate mock implementation. To obtain a
257 * functional instance, use {@link android.content.Context#getSystemService}.
258 */
Dan Egnorf18a01c2009-11-12 11:32:50 -0800259 protected DropBoxManager() { mService = null; }
Dan Egnor95240272009-10-27 18:23:39 -0700260
261 /**
262 * Stores human-readable text. The data may be discarded eventually (or even
263 * immediately) if space is limited, or ignored entirely if the tag has been
264 * blocked (see {@link #isTagEnabled}).
265 *
266 * @param tag describing the type of entry being stored
267 * @param data value to store
268 */
269 public void addText(String tag, String data) {
270 try { mService.add(new Entry(tag, 0, data)); } catch (RemoteException e) {}
271 }
272
273 /**
274 * Stores binary data, which may be ignored or discarded as with {@link #addText}.
275 *
276 * @param tag describing the type of entry being stored
277 * @param data value to store
278 * @param flags describing the data
279 */
280 public void addData(String tag, byte[] data, int flags) {
Dan Egnor6e6d60d2010-07-20 15:24:09 -0700281 if (data == null) throw new NullPointerException("data == null");
Dan Egnor95240272009-10-27 18:23:39 -0700282 try { mService.add(new Entry(tag, 0, data, flags)); } catch (RemoteException e) {}
283 }
284
285 /**
Dan Egnoreb7a7d52009-11-25 12:38:00 -0800286 * Stores the contents of a file, which may be ignored or discarded as with
287 * {@link #addText}.
Dan Egnor95240272009-10-27 18:23:39 -0700288 *
289 * @param tag describing the type of entry being stored
Dan Egnoreb7a7d52009-11-25 12:38:00 -0800290 * @param file to read from
Dan Egnor95240272009-10-27 18:23:39 -0700291 * @param flags describing the data
Dan Egnoreb7a7d52009-11-25 12:38:00 -0800292 * @throws IOException if the file can't be opened
Dan Egnor95240272009-10-27 18:23:39 -0700293 */
Dan Egnoreb7a7d52009-11-25 12:38:00 -0800294 public void addFile(String tag, File file, int flags) throws IOException {
Dan Egnor6e6d60d2010-07-20 15:24:09 -0700295 if (file == null) throw new NullPointerException("file == null");
Dan Egnoreb7a7d52009-11-25 12:38:00 -0800296 Entry entry = new Entry(tag, 0, file, flags);
297 try {
Brad Fitzpatrick14418942010-06-14 08:58:26 -0700298 mService.add(entry);
Dan Egnoreb7a7d52009-11-25 12:38:00 -0800299 } catch (RemoteException e) {
300 // ignore
301 } finally {
302 entry.close();
303 }
Dan Egnor95240272009-10-27 18:23:39 -0700304 }
305
306 /**
307 * Checks any blacklists (set in system settings) to see whether a certain
308 * tag is allowed. Entries with disabled tags will be dropped immediately,
309 * so you can save the work of actually constructing and sending the data.
310 *
311 * @param tag that would be used in {@link #addText} or {@link #addFile}
312 * @return whether events with that tag would be accepted
313 */
314 public boolean isTagEnabled(String tag) {
315 try { return mService.isTagEnabled(tag); } catch (RemoteException e) { return false; }
316 }
317
318 /**
Brad Fitzpatrick30f5c8f2010-10-07 10:48:20 -0700319 * Gets the next entry from the drop box <em>after</em> the specified time.
320 * Requires <code>android.permission.READ_LOGS</code>. You must always call
Dan Egnor95240272009-10-27 18:23:39 -0700321 * {@link Entry#close()} on the return value!
322 *
323 * @param tag of entry to look for, null for all tags
324 * @param msec time of the last entry seen
325 * @return the next entry, or null if there are no more entries
326 */
327 public Entry getNextEntry(String tag, long msec) {
328 try { return mService.getNextEntry(tag, msec); } catch (RemoteException e) { return null; }
329 }
330
331 // TODO: It may be useful to have some sort of notification mechanism
332 // when data is added to the dropbox, for demand-driven readers --
333 // for now readers need to poll the dropbox to find new data.
334}