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