1 /* 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 17 package android.os; 18 19 import android.content.Context; 20 import android.util.Log; 21 22 import com.android.internal.os.IDropBoxManagerService; 23 24 import java.io.ByteArrayInputStream; 25 import java.io.Closeable; 26 import java.io.File; 27 import java.io.IOException; 28 import java.io.InputStream; 29 import 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 * 41 * <p>DropBoxManager entries are not sent anywhere directly, but other system 42 * services and debugging tools may scan and upload entries for processing. 43 */ 44 public class DropBoxManager { 45 private static final String TAG = "DropBoxManager"; 46 47 private final Context mContext; 48 private final IDropBoxManagerService mService; 49 50 /** Flag value: Entry's content was deleted to save space. */ 51 public static final int IS_EMPTY = 1; 52 53 /** Flag value: Content is human-readable UTF-8 text (can be combined with IS_GZIPPED). */ 54 public static final int IS_TEXT = 2; 55 56 /** Flag value: Content can be decompressed with {@link java.util.zip.GZIPOutputStream}. */ 57 public static final int IS_GZIPPED = 4; 58 59 /** Flag value for serialization only: Value is a byte array, not a file descriptor */ 60 private static final int HAS_BYTE_ARRAY = 8; 61 62 /** 63 * Broadcast Action: This is broadcast when a new entry is added in the dropbox. 64 * You must hold the {@link android.Manifest.permission#READ_LOGS} permission 65 * in order to receive this broadcast. 66 * 67 * <p class="note">This is a protected intent that can only be sent 68 * by the system. 69 */ 70 public static final String ACTION_DROPBOX_ENTRY_ADDED = 71 "android.intent.action.DROPBOX_ENTRY_ADDED"; 72 73 /** 74 * Extra for {@link android.os.DropBoxManager#ACTION_DROPBOX_ENTRY_ADDED}: 75 * string containing the dropbox tag. 76 */ 77 public static final String EXTRA_TAG = "tag"; 78 79 /** 80 * Extra for {@link android.os.DropBoxManager#ACTION_DROPBOX_ENTRY_ADDED}: 81 * long integer value containing time (in milliseconds since January 1, 1970 00:00:00 UTC) 82 * when the entry was created. 83 */ 84 public static final String EXTRA_TIME = "time"; 85 86 /** 87 * A single entry retrieved from the drop box. 88 * This may include a reference to a stream, so you must call 89 * {@link #close()} when you are done using it. 90 */ 91 public static class Entry implements Parcelable, Closeable { 92 private final String mTag; 93 private final long mTimeMillis; 94 95 private final byte[] mData; 96 private final ParcelFileDescriptor mFileDescriptor; 97 private final int mFlags; 98 99 /** Create a new empty Entry with no contents. */ 100 public Entry(String tag, long millis) { 101 if (tag == null) throw new NullPointerException("tag == null"); 102 103 mTag = tag; 104 mTimeMillis = millis; 105 mData = null; 106 mFileDescriptor = null; 107 mFlags = IS_EMPTY; 108 } 109 110 /** Create a new Entry with plain text contents. */ 111 public Entry(String tag, long millis, String text) { 112 if (tag == null) throw new NullPointerException("tag == null"); 113 if (text == null) throw new NullPointerException("text == null"); 114 115 mTag = tag; 116 mTimeMillis = millis; 117 mData = text.getBytes(); 118 mFileDescriptor = null; 119 mFlags = IS_TEXT; 120 } 121 122 /** 123 * Create a new Entry with byte array contents. 124 * The data array must not be modified after creating this entry. 125 */ 126 public Entry(String tag, long millis, byte[] data, int flags) { 127 if (tag == null) throw new NullPointerException("tag == null"); 128 if (((flags & IS_EMPTY) != 0) != (data == null)) { 129 throw new IllegalArgumentException("Bad flags: " + flags); 130 } 131 132 mTag = tag; 133 mTimeMillis = millis; 134 mData = data; 135 mFileDescriptor = null; 136 mFlags = flags; 137 } 138 139 /** 140 * Create a new Entry with streaming data contents. 141 * Takes ownership of the ParcelFileDescriptor. 142 */ 143 public Entry(String tag, long millis, ParcelFileDescriptor data, int flags) { 144 if (tag == null) throw new NullPointerException("tag == null"); 145 if (((flags & IS_EMPTY) != 0) != (data == null)) { 146 throw new IllegalArgumentException("Bad flags: " + flags); 147 } 148 149 mTag = tag; 150 mTimeMillis = millis; 151 mData = null; 152 mFileDescriptor = data; 153 mFlags = flags; 154 } 155 156 /** 157 * Create a new Entry with the contents read from a file. 158 * The file will be read when the entry's contents are requested. 159 */ 160 public Entry(String tag, long millis, File data, int flags) throws IOException { 161 if (tag == null) throw new NullPointerException("tag == null"); 162 if ((flags & IS_EMPTY) != 0) throw new IllegalArgumentException("Bad flags: " + flags); 163 164 mTag = tag; 165 mTimeMillis = millis; 166 mData = null; 167 mFileDescriptor = ParcelFileDescriptor.open(data, ParcelFileDescriptor.MODE_READ_ONLY); 168 mFlags = flags; 169 } 170 171 /** Close the input stream associated with this entry. */ 172 public void close() { 173 try { if (mFileDescriptor != null) mFileDescriptor.close(); } catch (IOException e) { } 174 } 175 176 /** @return the tag originally attached to the entry. */ 177 public String getTag() { return mTag; } 178 179 /** @return time when the entry was originally created. */ 180 public long getTimeMillis() { return mTimeMillis; } 181 182 /** @return flags describing the content returned by {@link #getInputStream()}. */ 183 public int getFlags() { return mFlags & ~IS_GZIPPED; } // getInputStream() decompresses. 184 185 /** 186 * @param maxBytes of string to return (will truncate at this length). 187 * @return the uncompressed text contents of the entry, null if the entry is not text. 188 */ 189 public String getText(int maxBytes) { 190 if ((mFlags & IS_TEXT) == 0) return null; 191 if (mData != null) return new String(mData, 0, Math.min(maxBytes, mData.length)); 192 193 InputStream is = null; 194 try { 195 is = getInputStream(); 196 if (is == null) return null; 197 byte[] buf = new byte[maxBytes]; 198 int readBytes = 0; 199 int n = 0; 200 while (n >= 0 && (readBytes += n) < maxBytes) { 201 n = is.read(buf, readBytes, maxBytes - readBytes); 202 } 203 return new String(buf, 0, readBytes); 204 } catch (IOException e) { 205 return null; 206 } finally { 207 try { if (is != null) is.close(); } catch (IOException e) {} 208 } 209 } 210 211 /** @return the uncompressed contents of the entry, or null if the contents were lost */ 212 public InputStream getInputStream() throws IOException { 213 InputStream is; 214 if (mData != null) { 215 is = new ByteArrayInputStream(mData); 216 } else if (mFileDescriptor != null) { 217 is = new ParcelFileDescriptor.AutoCloseInputStream(mFileDescriptor); 218 } else { 219 return null; 220 } 221 return (mFlags & IS_GZIPPED) != 0 ? new GZIPInputStream(is) : is; 222 } 223 224 public static final Parcelable.Creator<Entry> CREATOR = new Parcelable.Creator() { 225 public Entry[] newArray(int size) { return new Entry[size]; } 226 public Entry createFromParcel(Parcel in) { 227 String tag = in.readString(); 228 long millis = in.readLong(); 229 int flags = in.readInt(); 230 if ((flags & HAS_BYTE_ARRAY) != 0) { 231 return new Entry(tag, millis, in.createByteArray(), flags & ~HAS_BYTE_ARRAY); 232 } else { 233 ParcelFileDescriptor pfd = ParcelFileDescriptor.CREATOR.createFromParcel(in); 234 return new Entry(tag, millis, pfd, flags); 235 } 236 } 237 }; 238 239 public int describeContents() { 240 return mFileDescriptor != null ? Parcelable.CONTENTS_FILE_DESCRIPTOR : 0; 241 } 242 243 public void writeToParcel(Parcel out, int flags) { 244 out.writeString(mTag); 245 out.writeLong(mTimeMillis); 246 if (mFileDescriptor != null) { 247 out.writeInt(mFlags & ~HAS_BYTE_ARRAY); // Clear bit just to be safe 248 mFileDescriptor.writeToParcel(out, flags); 249 } else { 250 out.writeInt(mFlags | HAS_BYTE_ARRAY); 251 out.writeByteArray(mData); 252 } 253 } 254 } 255 256 /** {@hide} */ 257 public DropBoxManager(Context context, IDropBoxManagerService service) { 258 mContext = context; 259 mService = service; 260 } 261 262 /** 263 * Create a dummy instance for testing. All methods will fail unless 264 * overridden with an appropriate mock implementation. To obtain a 265 * functional instance, use {@link android.content.Context#getSystemService}. 266 */ 267 protected DropBoxManager() { 268 mContext = null; 269 mService = null; 270 } 271 272 /** 273 * Stores human-readable text. The data may be discarded eventually (or even 274 * immediately) if space is limited, or ignored entirely if the tag has been 275 * blocked (see {@link #isTagEnabled}). 276 * 277 * @param tag describing the type of entry being stored 278 * @param data value to store 279 */ 280 public void addText(String tag, String data) { 281 try { 282 mService.add(new Entry(tag, 0, data)); 283 } catch (RemoteException e) { 284 if (e instanceof TransactionTooLargeException 285 && mContext.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.N) { 286 Log.e(TAG, "App sent too much data, so it was ignored", e); 287 return; 288 } 289 throw e.rethrowFromSystemServer(); 290 } 291 } 292 293 /** 294 * Stores binary data, which may be ignored or discarded as with {@link #addText}. 295 * 296 * @param tag describing the type of entry being stored 297 * @param data value to store 298 * @param flags describing the data 299 */ 300 public void addData(String tag, byte[] data, int flags) { 301 if (data == null) throw new NullPointerException("data == null"); 302 try { 303 mService.add(new Entry(tag, 0, data, flags)); 304 } catch (RemoteException e) { 305 if (e instanceof TransactionTooLargeException 306 && mContext.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.N) { 307 Log.e(TAG, "App sent too much data, so it was ignored", e); 308 return; 309 } 310 throw e.rethrowFromSystemServer(); 311 } 312 } 313 314 /** 315 * Stores the contents of a file, which may be ignored or discarded as with 316 * {@link #addText}. 317 * 318 * @param tag describing the type of entry being stored 319 * @param file to read from 320 * @param flags describing the data 321 * @throws IOException if the file can't be opened 322 */ 323 public void addFile(String tag, File file, int flags) throws IOException { 324 if (file == null) throw new NullPointerException("file == null"); 325 Entry entry = new Entry(tag, 0, file, flags); 326 try { 327 mService.add(entry); 328 } catch (RemoteException e) { 329 throw e.rethrowFromSystemServer(); 330 } finally { 331 entry.close(); 332 } 333 } 334 335 /** 336 * Checks any blacklists (set in system settings) to see whether a certain 337 * tag is allowed. Entries with disabled tags will be dropped immediately, 338 * so you can save the work of actually constructing and sending the data. 339 * 340 * @param tag that would be used in {@link #addText} or {@link #addFile} 341 * @return whether events with that tag would be accepted 342 */ 343 public boolean isTagEnabled(String tag) { 344 try { 345 return mService.isTagEnabled(tag); 346 } catch (RemoteException e) { 347 throw e.rethrowFromSystemServer(); 348 } 349 } 350 351 /** 352 * Gets the next entry from the drop box <em>after</em> the specified time. 353 * Requires <code>android.permission.READ_LOGS</code>. You must always call 354 * {@link Entry#close()} on the return value! 355 * 356 * @param tag of entry to look for, null for all tags 357 * @param msec time of the last entry seen 358 * @return the next entry, or null if there are no more entries 359 */ 360 public Entry getNextEntry(String tag, long msec) { 361 try { 362 return mService.getNextEntry(tag, msec); 363 } catch (RemoteException e) { 364 throw e.rethrowFromSystemServer(); 365 } 366 } 367 368 // TODO: It may be useful to have some sort of notification mechanism 369 // when data is added to the dropbox, for demand-driven readers -- 370 // for now readers need to poll the dropbox to find new data. 371 } 372