• 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_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