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 com.android.internal.os.IDropBoxManagerService; 20 21 import java.io.ByteArrayInputStream; 22 import java.io.Closeable; 23 import java.io.File; 24 import java.io.IOException; 25 import java.io.InputStream; 26 import 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 * 38 * <p>DropBoxManager entries are not sent anywhere directly, but other system 39 * services and debugging tools may scan and upload entries for processing. 40 */ 41 public class DropBoxManager { 42 private static final String TAG = "DropBoxManager"; 43 private final IDropBoxManagerService mService; 44 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 51 /** Flag value: Content can be decompressed with {@link java.util.zip.GZIPOutputStream}. */ 52 public static final int IS_GZIPPED = 4; 53 54 /** Flag value for serialization only: Value is a byte array, not a file descriptor */ 55 private static final int HAS_BYTE_ARRAY = 8; 56 57 /** 58 * 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 /** 82 * 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 */ 86 public static class Entry implements Parcelable, Closeable { 87 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. */ Entry(String tag, long millis)95 public Entry(String tag, long millis) { 96 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; 103 } 104 105 /** Create a new Entry with plain text contents. */ Entry(String tag, long millis, String text)106 public Entry(String tag, long millis, String text) { 107 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; 115 } 116 117 /** 118 * Create a new Entry with byte array contents. 119 * The data array must not be modified after creating this entry. 120 */ Entry(String tag, long millis, byte[] data, int flags)121 public Entry(String tag, long millis, byte[] data, int flags) { 122 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; 132 } 133 134 /** 135 * Create a new Entry with streaming data contents. 136 * Takes ownership of the ParcelFileDescriptor. 137 */ Entry(String tag, long millis, ParcelFileDescriptor data, int flags)138 public Entry(String tag, long millis, ParcelFileDescriptor data, int flags) { 139 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; 149 } 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 */ Entry(String tag, long millis, File data, int flags)155 public Entry(String tag, long millis, File data, int flags) throws IOException { 156 if (tag == null) throw new NullPointerException("tag == null"); 157 if ((flags & IS_EMPTY) != 0) throw new IllegalArgumentException("Bad flags: " + flags); 158 159 mTag = tag; 160 mTimeMillis = millis; 161 mData = null; 162 mFileDescriptor = ParcelFileDescriptor.open(data, ParcelFileDescriptor.MODE_READ_ONLY); 163 mFlags = flags; 164 } 165 166 /** Close the input stream associated with this entry. */ close()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. */ getTag()172 public String getTag() { return mTag; } 173 174 /** @return time when the entry was originally created. */ getTimeMillis()175 public long getTimeMillis() { return mTimeMillis; } 176 177 /** @return flags describing the content returned by {@link #getInputStream()}. */ getFlags()178 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 */ getText(int maxBytes)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(); 191 if (is == null) return null; 192 byte[] buf = new byte[maxBytes]; 193 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); 199 } 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 */ getInputStream()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) { 222 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 ParcelFileDescriptor pfd = ParcelFileDescriptor.CREATOR.createFromParcel(in); 229 return new Entry(tag, millis, pfd, flags); 230 } 231 } 232 }; 233 describeContents()234 public int describeContents() { 235 return mFileDescriptor != null ? Parcelable.CONTENTS_FILE_DESCRIPTOR : 0; 236 } 237 writeToParcel(Parcel out, int flags)238 public void writeToParcel(Parcel out, int flags) { 239 out.writeString(mTag); 240 out.writeLong(mTimeMillis); 241 if (mFileDescriptor != null) { 242 out.writeInt(mFlags & ~HAS_BYTE_ARRAY); // Clear bit just to be safe 243 mFileDescriptor.writeToParcel(out, flags); 244 } else { 245 out.writeInt(mFlags | HAS_BYTE_ARRAY); 246 out.writeByteArray(mData); 247 } 248 } 249 } 250 251 /** {@hide} */ DropBoxManager(IDropBoxManagerService service)252 public DropBoxManager(IDropBoxManagerService service) { mService = service; } 253 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 */ DropBoxManager()259 protected DropBoxManager() { mService = null; } 260 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 */ addText(String tag, String data)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 */ addData(String tag, byte[] data, int flags)280 public void addData(String tag, byte[] data, int flags) { 281 if (data == null) throw new NullPointerException("data == null"); 282 try { mService.add(new Entry(tag, 0, data, flags)); } catch (RemoteException e) {} 283 } 284 285 /** 286 * Stores the contents of a file, which may be ignored or discarded as with 287 * {@link #addText}. 288 * 289 * @param tag describing the type of entry being stored 290 * @param file to read from 291 * @param flags describing the data 292 * @throws IOException if the file can't be opened 293 */ addFile(String tag, File file, int flags)294 public void addFile(String tag, File file, int flags) throws IOException { 295 if (file == null) throw new NullPointerException("file == null"); 296 Entry entry = new Entry(tag, 0, file, flags); 297 try { 298 mService.add(entry); 299 } catch (RemoteException e) { 300 // ignore 301 } finally { 302 entry.close(); 303 } 304 } 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 */ isTagEnabled(String tag)314 public boolean isTagEnabled(String tag) { 315 try { return mService.isTagEnabled(tag); } catch (RemoteException e) { return false; } 316 } 317 318 /** 319 * 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 321 * {@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 */ getNextEntry(String tag, long msec)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 } 335