blob: 1be7b6b68bf0a44c9c31b7a4154fd479109b7790 [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
Jeff Sharkeyc36c3b92018-04-19 15:22:45 -060019import static android.Manifest.permission.PACKAGE_USAGE_STATS;
20import static android.Manifest.permission.READ_LOGS;
21
22import android.annotation.Nullable;
23import android.annotation.RequiresPermission;
Jeff Sharkey0f3f60b2017-04-24 18:06:20 -060024import android.annotation.SdkConstant;
25import android.annotation.SdkConstant.SdkConstantType;
Jeff Sharkeyc36c3b92018-04-19 15:22:45 -060026import android.annotation.SystemService;
Jeff Sharkeyb8e8a912016-03-09 16:27:40 -070027import android.content.Context;
28import android.util.Log;
29
Dan Egnorf18a01c2009-11-12 11:32:50 -080030import com.android.internal.os.IDropBoxManagerService;
Dan Egnor95240272009-10-27 18:23:39 -070031
32import java.io.ByteArrayInputStream;
Brad Fitzpatrickcc792c42010-10-08 09:50:30 -070033import java.io.Closeable;
Dan Egnor95240272009-10-27 18:23:39 -070034import java.io.File;
Dan Egnor95240272009-10-27 18:23:39 -070035import java.io.IOException;
36import java.io.InputStream;
37import java.util.zip.GZIPInputStream;
38
39/**
40 * Enqueues chunks of data (from various sources -- application crashes, kernel
41 * log records, etc.). The queue is size bounded and will drop old data if the
42 * enqueued data exceeds the maximum size. You can think of this as a
43 * persistent, system-wide, blob-oriented "logcat".
44 *
Dan Egnorf18a01c2009-11-12 11:32:50 -080045 * <p>DropBoxManager entries are not sent anywhere directly, but other system
46 * services and debugging tools may scan and upload entries for processing.
Dan Egnor95240272009-10-27 18:23:39 -070047 */
Jeff Sharkeyd86b8fe2017-06-02 17:36:26 -060048@SystemService(Context.DROPBOX_SERVICE)
Dan Egnorf18a01c2009-11-12 11:32:50 -080049public class DropBoxManager {
50 private static final String TAG = "DropBoxManager";
Jeff Sharkeyb8e8a912016-03-09 16:27:40 -070051
52 private final Context mContext;
Dan Egnorf18a01c2009-11-12 11:32:50 -080053 private final IDropBoxManagerService mService;
Dan Egnor95240272009-10-27 18:23:39 -070054
55 /** Flag value: Entry's content was deleted to save space. */
56 public static final int IS_EMPTY = 1;
57
58 /** Flag value: Content is human-readable UTF-8 text (can be combined with IS_GZIPPED). */
59 public static final int IS_TEXT = 2;
60
Dan Egnore3cfe2d2009-11-14 16:20:04 -080061 /** Flag value: Content can be decompressed with {@link java.util.zip.GZIPOutputStream}. */
Dan Egnor95240272009-10-27 18:23:39 -070062 public static final int IS_GZIPPED = 4;
63
Dan Egnor6e6d60d2010-07-20 15:24:09 -070064 /** Flag value for serialization only: Value is a byte array, not a file descriptor */
65 private static final int HAS_BYTE_ARRAY = 8;
66
Dan Egnor95240272009-10-27 18:23:39 -070067 /**
Hakan Stillb2475362010-12-07 14:05:55 +010068 * Broadcast Action: This is broadcast when a new entry is added in the dropbox.
69 * You must hold the {@link android.Manifest.permission#READ_LOGS} permission
70 * in order to receive this broadcast.
71 *
72 * <p class="note">This is a protected intent that can only be sent
73 * by the system.
74 */
Jeff Sharkey0f3f60b2017-04-24 18:06:20 -060075 @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
Hakan Stillb2475362010-12-07 14:05:55 +010076 public static final String ACTION_DROPBOX_ENTRY_ADDED =
77 "android.intent.action.DROPBOX_ENTRY_ADDED";
78
79 /**
80 * Extra for {@link android.os.DropBoxManager#ACTION_DROPBOX_ENTRY_ADDED}:
81 * string containing the dropbox tag.
82 */
83 public static final String EXTRA_TAG = "tag";
84
85 /**
86 * Extra for {@link android.os.DropBoxManager#ACTION_DROPBOX_ENTRY_ADDED}:
87 * long integer value containing time (in milliseconds since January 1, 1970 00:00:00 UTC)
88 * when the entry was created.
89 */
90 public static final String EXTRA_TIME = "time";
91
92 /**
Dan Egnor95240272009-10-27 18:23:39 -070093 * A single entry retrieved from the drop box.
94 * This may include a reference to a stream, so you must call
95 * {@link #close()} when you are done using it.
96 */
Brad Fitzpatrickcc792c42010-10-08 09:50:30 -070097 public static class Entry implements Parcelable, Closeable {
Dan Egnor95240272009-10-27 18:23:39 -070098 private final String mTag;
99 private final long mTimeMillis;
100
101 private final byte[] mData;
102 private final ParcelFileDescriptor mFileDescriptor;
103 private final int mFlags;
104
105 /** Create a new empty Entry with no contents. */
106 public Entry(String tag, long millis) {
Dan Egnor6e6d60d2010-07-20 15:24:09 -0700107 if (tag == null) throw new NullPointerException("tag == null");
108
109 mTag = tag;
110 mTimeMillis = millis;
111 mData = null;
112 mFileDescriptor = null;
113 mFlags = IS_EMPTY;
Dan Egnor95240272009-10-27 18:23:39 -0700114 }
115
116 /** Create a new Entry with plain text contents. */
117 public Entry(String tag, long millis, String text) {
Dan Egnor6e6d60d2010-07-20 15:24:09 -0700118 if (tag == null) throw new NullPointerException("tag == null");
119 if (text == null) throw new NullPointerException("text == null");
120
121 mTag = tag;
122 mTimeMillis = millis;
123 mData = text.getBytes();
124 mFileDescriptor = null;
125 mFlags = IS_TEXT;
Dan Egnor95240272009-10-27 18:23:39 -0700126 }
127
128 /**
129 * Create a new Entry with byte array contents.
130 * The data array must not be modified after creating this entry.
131 */
132 public Entry(String tag, long millis, byte[] data, int flags) {
Dan Egnor6e6d60d2010-07-20 15:24:09 -0700133 if (tag == null) throw new NullPointerException("tag == null");
134 if (((flags & IS_EMPTY) != 0) != (data == null)) {
135 throw new IllegalArgumentException("Bad flags: " + flags);
136 }
137
138 mTag = tag;
139 mTimeMillis = millis;
140 mData = data;
141 mFileDescriptor = null;
142 mFlags = flags;
Dan Egnor95240272009-10-27 18:23:39 -0700143 }
144
145 /**
146 * Create a new Entry with streaming data contents.
147 * Takes ownership of the ParcelFileDescriptor.
148 */
149 public Entry(String tag, long millis, ParcelFileDescriptor data, int flags) {
Dan Egnor6e6d60d2010-07-20 15:24:09 -0700150 if (tag == null) throw new NullPointerException("tag == null");
151 if (((flags & IS_EMPTY) != 0) != (data == null)) {
152 throw new IllegalArgumentException("Bad flags: " + flags);
153 }
154
155 mTag = tag;
156 mTimeMillis = millis;
157 mData = null;
158 mFileDescriptor = data;
159 mFlags = flags;
Dan Egnor95240272009-10-27 18:23:39 -0700160 }
161
162 /**
163 * Create a new Entry with the contents read from a file.
164 * The file will be read when the entry's contents are requested.
165 */
166 public Entry(String tag, long millis, File data, int flags) throws IOException {
Dan Egnor6e6d60d2010-07-20 15:24:09 -0700167 if (tag == null) throw new NullPointerException("tag == null");
168 if ((flags & IS_EMPTY) != 0) throw new IllegalArgumentException("Bad flags: " + flags);
Dan Egnor95240272009-10-27 18:23:39 -0700169
170 mTag = tag;
171 mTimeMillis = millis;
Dan Egnor6e6d60d2010-07-20 15:24:09 -0700172 mData = null;
173 mFileDescriptor = ParcelFileDescriptor.open(data, ParcelFileDescriptor.MODE_READ_ONLY);
Dan Egnor95240272009-10-27 18:23:39 -0700174 mFlags = flags;
Dan Egnor95240272009-10-27 18:23:39 -0700175 }
176
177 /** Close the input stream associated with this entry. */
178 public void close() {
179 try { if (mFileDescriptor != null) mFileDescriptor.close(); } catch (IOException e) { }
180 }
181
182 /** @return the tag originally attached to the entry. */
183 public String getTag() { return mTag; }
184
185 /** @return time when the entry was originally created. */
186 public long getTimeMillis() { return mTimeMillis; }
187
Brad Fitzpatrick95173b12010-10-07 10:07:34 -0700188 /** @return flags describing the content returned by {@link #getInputStream()}. */
Dan Egnor95240272009-10-27 18:23:39 -0700189 public int getFlags() { return mFlags & ~IS_GZIPPED; } // getInputStream() decompresses.
190
191 /**
192 * @param maxBytes of string to return (will truncate at this length).
193 * @return the uncompressed text contents of the entry, null if the entry is not text.
194 */
195 public String getText(int maxBytes) {
196 if ((mFlags & IS_TEXT) == 0) return null;
197 if (mData != null) return new String(mData, 0, Math.min(maxBytes, mData.length));
198
199 InputStream is = null;
200 try {
201 is = getInputStream();
Dan Egnor6e6d60d2010-07-20 15:24:09 -0700202 if (is == null) return null;
Dan Egnor95240272009-10-27 18:23:39 -0700203 byte[] buf = new byte[maxBytes];
Christian Lindeberge9f18812010-11-16 09:57:54 +0100204 int readBytes = 0;
205 int n = 0;
206 while (n >= 0 && (readBytes += n) < maxBytes) {
207 n = is.read(buf, readBytes, maxBytes - readBytes);
208 }
209 return new String(buf, 0, readBytes);
Dan Egnor95240272009-10-27 18:23:39 -0700210 } catch (IOException e) {
211 return null;
212 } finally {
213 try { if (is != null) is.close(); } catch (IOException e) {}
214 }
215 }
216
217 /** @return the uncompressed contents of the entry, or null if the contents were lost */
218 public InputStream getInputStream() throws IOException {
219 InputStream is;
220 if (mData != null) {
221 is = new ByteArrayInputStream(mData);
222 } else if (mFileDescriptor != null) {
223 is = new ParcelFileDescriptor.AutoCloseInputStream(mFileDescriptor);
224 } else {
225 return null;
226 }
227 return (mFlags & IS_GZIPPED) != 0 ? new GZIPInputStream(is) : is;
228 }
229
Jeff Sharkey9e8f83d2019-02-28 12:06:45 -0700230 public static final @android.annotation.NonNull Parcelable.Creator<Entry> CREATOR = new Parcelable.Creator() {
Dan Egnor95240272009-10-27 18:23:39 -0700231 public Entry[] newArray(int size) { return new Entry[size]; }
232 public Entry createFromParcel(Parcel in) {
Dan Egnor6e6d60d2010-07-20 15:24:09 -0700233 String tag = in.readString();
234 long millis = in.readLong();
235 int flags = in.readInt();
236 if ((flags & HAS_BYTE_ARRAY) != 0) {
237 return new Entry(tag, millis, in.createByteArray(), flags & ~HAS_BYTE_ARRAY);
238 } else {
Tim Kilbournba953232015-06-02 15:57:45 -0700239 ParcelFileDescriptor pfd = ParcelFileDescriptor.CREATOR.createFromParcel(in);
240 return new Entry(tag, millis, pfd, flags);
Dan Egnor6e6d60d2010-07-20 15:24:09 -0700241 }
Dan Egnor95240272009-10-27 18:23:39 -0700242 }
243 };
244
245 public int describeContents() {
246 return mFileDescriptor != null ? Parcelable.CONTENTS_FILE_DESCRIPTOR : 0;
247 }
248
249 public void writeToParcel(Parcel out, int flags) {
250 out.writeString(mTag);
251 out.writeLong(mTimeMillis);
252 if (mFileDescriptor != null) {
Dan Egnor6e6d60d2010-07-20 15:24:09 -0700253 out.writeInt(mFlags & ~HAS_BYTE_ARRAY); // Clear bit just to be safe
254 mFileDescriptor.writeToParcel(out, flags);
Dan Egnor95240272009-10-27 18:23:39 -0700255 } else {
Dan Egnor6e6d60d2010-07-20 15:24:09 -0700256 out.writeInt(mFlags | HAS_BYTE_ARRAY);
257 out.writeByteArray(mData);
Dan Egnor95240272009-10-27 18:23:39 -0700258 }
Dan Egnor95240272009-10-27 18:23:39 -0700259 }
260 }
261
262 /** {@hide} */
Jeff Sharkeyb8e8a912016-03-09 16:27:40 -0700263 public DropBoxManager(Context context, IDropBoxManagerService service) {
264 mContext = context;
265 mService = service;
266 }
Dan Egnor95240272009-10-27 18:23:39 -0700267
268 /**
269 * Create a dummy instance for testing. All methods will fail unless
270 * overridden with an appropriate mock implementation. To obtain a
271 * functional instance, use {@link android.content.Context#getSystemService}.
272 */
Jeff Sharkeyb8e8a912016-03-09 16:27:40 -0700273 protected DropBoxManager() {
274 mContext = null;
275 mService = null;
276 }
Dan Egnor95240272009-10-27 18:23:39 -0700277
278 /**
279 * Stores human-readable text. The data may be discarded eventually (or even
280 * immediately) if space is limited, or ignored entirely if the tag has been
281 * blocked (see {@link #isTagEnabled}).
282 *
283 * @param tag describing the type of entry being stored
284 * @param data value to store
285 */
286 public void addText(String tag, String data) {
Jeff Sharkeyf8880562016-02-26 13:03:01 -0700287 try {
288 mService.add(new Entry(tag, 0, data));
289 } catch (RemoteException e) {
Jeff Sharkeyb8e8a912016-03-09 16:27:40 -0700290 if (e instanceof TransactionTooLargeException
291 && mContext.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.N) {
292 Log.e(TAG, "App sent too much data, so it was ignored", e);
293 return;
294 }
Jeff Sharkeyf8880562016-02-26 13:03:01 -0700295 throw e.rethrowFromSystemServer();
296 }
Dan Egnor95240272009-10-27 18:23:39 -0700297 }
298
299 /**
300 * Stores binary data, which may be ignored or discarded as with {@link #addText}.
301 *
302 * @param tag describing the type of entry being stored
303 * @param data value to store
304 * @param flags describing the data
305 */
306 public void addData(String tag, byte[] data, int flags) {
Dan Egnor6e6d60d2010-07-20 15:24:09 -0700307 if (data == null) throw new NullPointerException("data == null");
Jeff Sharkeyf8880562016-02-26 13:03:01 -0700308 try {
309 mService.add(new Entry(tag, 0, data, flags));
310 } catch (RemoteException e) {
Jeff Sharkeyb8e8a912016-03-09 16:27:40 -0700311 if (e instanceof TransactionTooLargeException
312 && mContext.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.N) {
313 Log.e(TAG, "App sent too much data, so it was ignored", e);
314 return;
315 }
Jeff Sharkeyf8880562016-02-26 13:03:01 -0700316 throw e.rethrowFromSystemServer();
317 }
Dan Egnor95240272009-10-27 18:23:39 -0700318 }
319
320 /**
Dan Egnoreb7a7d52009-11-25 12:38:00 -0800321 * Stores the contents of a file, which may be ignored or discarded as with
322 * {@link #addText}.
Dan Egnor95240272009-10-27 18:23:39 -0700323 *
324 * @param tag describing the type of entry being stored
Dan Egnoreb7a7d52009-11-25 12:38:00 -0800325 * @param file to read from
Dan Egnor95240272009-10-27 18:23:39 -0700326 * @param flags describing the data
Dan Egnoreb7a7d52009-11-25 12:38:00 -0800327 * @throws IOException if the file can't be opened
Dan Egnor95240272009-10-27 18:23:39 -0700328 */
Dan Egnoreb7a7d52009-11-25 12:38:00 -0800329 public void addFile(String tag, File file, int flags) throws IOException {
Dan Egnor6e6d60d2010-07-20 15:24:09 -0700330 if (file == null) throw new NullPointerException("file == null");
Dan Egnoreb7a7d52009-11-25 12:38:00 -0800331 Entry entry = new Entry(tag, 0, file, flags);
332 try {
Brad Fitzpatrick14418942010-06-14 08:58:26 -0700333 mService.add(entry);
Dan Egnoreb7a7d52009-11-25 12:38:00 -0800334 } catch (RemoteException e) {
Jeff Sharkeyf8880562016-02-26 13:03:01 -0700335 throw e.rethrowFromSystemServer();
Dan Egnoreb7a7d52009-11-25 12:38:00 -0800336 } finally {
337 entry.close();
338 }
Dan Egnor95240272009-10-27 18:23:39 -0700339 }
340
341 /**
342 * Checks any blacklists (set in system settings) to see whether a certain
343 * tag is allowed. Entries with disabled tags will be dropped immediately,
344 * so you can save the work of actually constructing and sending the data.
345 *
346 * @param tag that would be used in {@link #addText} or {@link #addFile}
347 * @return whether events with that tag would be accepted
348 */
349 public boolean isTagEnabled(String tag) {
Jeff Sharkeyf8880562016-02-26 13:03:01 -0700350 try {
351 return mService.isTagEnabled(tag);
352 } catch (RemoteException e) {
353 throw e.rethrowFromSystemServer();
354 }
Dan Egnor95240272009-10-27 18:23:39 -0700355 }
356
357 /**
Brad Fitzpatrick30f5c8f2010-10-07 10:48:20 -0700358 * Gets the next entry from the drop box <em>after</em> the specified time.
Jeff Sharkeyc36c3b92018-04-19 15:22:45 -0600359 * You must always call {@link Entry#close()} on the return value!
Dan Egnor95240272009-10-27 18:23:39 -0700360 *
361 * @param tag of entry to look for, null for all tags
362 * @param msec time of the last entry seen
363 * @return the next entry, or null if there are no more entries
364 */
Jeff Sharkeyc36c3b92018-04-19 15:22:45 -0600365 @RequiresPermission(allOf = { READ_LOGS, PACKAGE_USAGE_STATS })
366 public @Nullable Entry getNextEntry(String tag, long msec) {
Jeff Sharkeyf8880562016-02-26 13:03:01 -0700367 try {
Jeff Sharkeyc36c3b92018-04-19 15:22:45 -0600368 return mService.getNextEntry(tag, msec, mContext.getOpPackageName());
369 } catch (SecurityException e) {
370 if (mContext.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.P) {
371 throw e;
372 } else {
373 Log.w(TAG, e.getMessage());
374 return null;
375 }
Jeff Sharkeyf8880562016-02-26 13:03:01 -0700376 } catch (RemoteException e) {
377 throw e.rethrowFromSystemServer();
378 }
Dan Egnor95240272009-10-27 18:23:39 -0700379 }
380
381 // TODO: It may be useful to have some sort of notification mechanism
382 // when data is added to the dropbox, for demand-driven readers --
383 // for now readers need to poll the dropbox to find new data.
384}