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.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. */
    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. */
    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          */
    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          */
    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          */
    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. */
    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. */
    178         public String getTag() { return mTag; }
    179 
    180         /** @return time when the entry was originally created. */
    181         public long getTimeMillis() { return mTimeMillis; }
    182 
    183         /** @return flags describing the content returned by {@link #getInputStream()}. */
    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          */
    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 */
    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 
    240         public int describeContents() {
    241             return mFileDescriptor != null ? Parcelable.CONTENTS_FILE_DESCRIPTOR : 0;
    242         }
    243 
    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} */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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