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 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.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. */
    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. */
    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          */
    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          */
    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          */
    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. */
    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. */
    193         public String getTag() { return mTag; }
    194 
    195         /** @return time when the entry was originally created. */
    196         public long getTimeMillis() { return mTimeMillis; }
    197 
    198         /** @return flags describing the content returned by {@link #getInputStream()}. */
    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          */
    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 */
    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 
    255         public int describeContents() {
    256             return mFileDescriptor != null ? Parcelable.CONTENTS_FILE_DESCRIPTOR : 0;
    257         }
    258 
    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} */
    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      */
    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      */
    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      */
    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      */
    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      */
    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 })
    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