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