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