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