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