• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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