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