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