Home | History | Annotate | Download | only in files
      1 /*
      2  * Copyright (C) 2015 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 com.android.documentsui.files;
     18 
     19 import static com.android.documentsui.base.DocumentInfo.getCursorString;
     20 import static com.android.documentsui.base.Shared.DEBUG;
     21 import static com.android.documentsui.base.Shared.MAX_DOCS_IN_INTENT;
     22 
     23 import android.content.ClipData;
     24 import android.content.ClipDescription;
     25 import android.content.Intent;
     26 import android.content.QuickViewConstants;
     27 import android.content.pm.PackageManager;
     28 import android.content.res.Resources;
     29 import android.database.Cursor;
     30 import android.net.Uri;
     31 import android.os.Build;
     32 import android.provider.DocumentsContract;
     33 import android.provider.DocumentsContract.Document;
     34 import android.support.annotation.Nullable;
     35 import android.text.TextUtils;
     36 import android.util.Log;
     37 import android.util.Range;
     38 
     39 import com.android.documentsui.R;
     40 import com.android.documentsui.base.DebugFlags;
     41 import com.android.documentsui.base.DocumentInfo;
     42 import com.android.documentsui.Model;
     43 import com.android.documentsui.roots.RootCursorWrapper;
     44 
     45 import java.util.ArrayList;
     46 import java.util.List;
     47 
     48 /**
     49  * Provides support for gather a list of quick-viewable files into a quick view intent.
     50  */
     51 public final class QuickViewIntentBuilder {
     52 
     53     // trusted quick view package can be set via system property on debug builds.
     54     // Unfortunately when the value is set, it interferes with testing (supercedes
     55     // any value set in the resource system).
     56     // For that reason when trusted quick view package is set to this magic value
     57     // we won't honor the system property.
     58     public static final String IGNORE_DEBUG_PROP = "*disabled*";
     59     private static final String TAG = "QuickViewIntentBuilder";
     60 
     61     private static final String[] IN_ARCHIVE_FEATURES = {};
     62     private static final String[] FULL_FEATURES = {
     63             QuickViewConstants.FEATURE_VIEW,
     64             QuickViewConstants.FEATURE_EDIT,
     65             QuickViewConstants.FEATURE_SEND,
     66             QuickViewConstants.FEATURE_DOWNLOAD,
     67             QuickViewConstants.FEATURE_PRINT
     68     };
     69 
     70     private final DocumentInfo mDocument;
     71     private final Model mModel;
     72 
     73     private final PackageManager mPackageMgr;
     74     private final Resources mResources;
     75 
     76     public QuickViewIntentBuilder(
     77             PackageManager packageMgr,
     78             Resources resources,
     79             DocumentInfo doc,
     80             Model model) {
     81 
     82         assert(packageMgr != null);
     83         assert(resources != null);
     84         assert(doc != null);
     85         assert(model != null);
     86 
     87         mPackageMgr = packageMgr;
     88         mResources = resources;
     89         mDocument = doc;
     90         mModel = model;
     91     }
     92 
     93     /**
     94      * Builds the intent for quick viewing. Short circuits building if a handler cannot
     95      * be resolved; in this case {@code null} is returned.
     96      */
     97     @Nullable Intent build() {
     98         if (DEBUG) Log.d(TAG, "Preparing intent for doc:" + mDocument.documentId);
     99 
    100         String trustedPkg = getQuickViewPackage();
    101 
    102         if (!TextUtils.isEmpty(trustedPkg)) {
    103             Intent intent = new Intent(Intent.ACTION_QUICK_VIEW);
    104             intent.setDataAndType(mDocument.derivedUri, mDocument.mimeType);
    105             intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
    106                     | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
    107             intent.setPackage(trustedPkg);
    108             if (hasRegisteredHandler(intent)) {
    109                 includeQuickViewFeaturesFlag(intent, mDocument);
    110 
    111                 final ArrayList<Uri> uris = new ArrayList<>();
    112                 final int documentLocation = collectViewableUris(uris);
    113                 final Range<Integer> range = computeSiblingsRange(uris, documentLocation);
    114 
    115                 ClipData clipData = null;
    116                 ClipData.Item item;
    117                 Uri uri;
    118                 for (int i = range.getLower(); i <= range.getUpper(); i++) {
    119                     uri = uris.get(i);
    120                     item = new ClipData.Item(uri);
    121                     if (DEBUG) Log.d(TAG, "Including file: " + uri);
    122                     if (clipData == null) {
    123                         clipData = new ClipData(
    124                                 "URIs", new String[] { ClipDescription.MIMETYPE_TEXT_URILIST },
    125                                 item);
    126                     } else {
    127                         clipData.addItem(item);
    128                     }
    129                 }
    130 
    131                 // The documentLocation variable contains an index in "uris". However,
    132                 // ClipData contains a slice of "uris", so we need to shift the location
    133                 // so it points to the same Uri.
    134                 intent.putExtra(Intent.EXTRA_INDEX, documentLocation - range.getLower());
    135                 intent.setClipData(clipData);
    136 
    137                 return intent;
    138             } else {
    139                 Log.e(TAG, "Can't resolve trusted quick view package: " + trustedPkg);
    140             }
    141         }
    142 
    143         return null;
    144     }
    145 
    146     private String getQuickViewPackage() {
    147         String resValue = mResources.getString(R.string.trusted_quick_viewer_package);
    148 
    149         // Allow automated tests to hard-disable quick viewing.
    150         if (IGNORE_DEBUG_PROP.equals(resValue)) {
    151             return "";
    152         }
    153 
    154         // Allow users of debug devices to override default quick viewer
    155         // for the purposes of testing.
    156         if (Build.IS_DEBUGGABLE) {
    157             String quickViewer = DebugFlags.getQuickViewer();
    158             if (quickViewer != null) {
    159                 return quickViewer;
    160             }
    161             return android.os.SystemProperties.get("debug.quick_viewer", resValue);
    162         }
    163         return resValue;
    164     }
    165 
    166     private int collectViewableUris(ArrayList<Uri> uris) {
    167         final String[] siblingIds = mModel.getModelIds();
    168         uris.ensureCapacity(siblingIds.length);
    169 
    170         int documentLocation = 0;
    171         Cursor cursor;
    172         String mimeType;
    173         String id;
    174         String authority;
    175         Uri uri;
    176 
    177         // Cursor's are not guaranteed to be immutable. Hence, traverse it only once.
    178         for (int i = 0; i < siblingIds.length; i++) {
    179             cursor = mModel.getItem(siblingIds[i]);
    180 
    181             if (cursor == null) {
    182                 if (DEBUG) Log.d(TAG,
    183                         "Unable to obtain cursor for sibling document, modelId: "
    184                         + siblingIds[i]);
    185                 continue;
    186             }
    187 
    188             mimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
    189             if (Document.MIME_TYPE_DIR.equals(mimeType)) {
    190                 if (DEBUG) Log.d(TAG,
    191                         "Skipping directory, not supported by quick view. modelId: "
    192                         + siblingIds[i]);
    193                 continue;
    194             }
    195 
    196             id = getCursorString(cursor, Document.COLUMN_DOCUMENT_ID);
    197             authority = getCursorString(cursor, RootCursorWrapper.COLUMN_AUTHORITY);
    198             uri = DocumentsContract.buildDocumentUri(authority, id);
    199 
    200             uris.add(uri);
    201 
    202             if (id.equals(mDocument.documentId)) {
    203                 documentLocation = uris.size() - 1;  // Position in "uris", not in the model.
    204                 if (DEBUG) Log.d(TAG, "Found starting point for QV. " + documentLocation);
    205             }
    206         }
    207 
    208         return documentLocation;
    209     }
    210 
    211     private boolean hasRegisteredHandler(Intent intent) {
    212         // Try to resolve the intent. If a matching app isn't installed, it won't resolve.
    213         return intent.resolveActivity(mPackageMgr) != null;
    214     }
    215 
    216     private static void includeQuickViewFeaturesFlag(Intent intent, DocumentInfo doc) {
    217         intent.putExtra(
    218                 Intent.EXTRA_QUICK_VIEW_FEATURES,
    219                 doc.isInArchive() ? IN_ARCHIVE_FEATURES : FULL_FEATURES);
    220     }
    221 
    222     private static Range<Integer> computeSiblingsRange(List<Uri> uris, int documentLocation) {
    223         // Restrict number of siblings to avoid hitting the IPC limit.
    224         // TODO: Remove this restriction once ClipData can hold an arbitrary number of
    225         // items.
    226         int firstSibling;
    227         int lastSibling;
    228         if (documentLocation < uris.size() / 2) {
    229             firstSibling = Math.max(0, documentLocation - MAX_DOCS_IN_INTENT / 2);
    230             lastSibling = Math.min(uris.size() - 1, firstSibling + MAX_DOCS_IN_INTENT - 1);
    231         } else {
    232             lastSibling = Math.min(uris.size() - 1, documentLocation + MAX_DOCS_IN_INTENT / 2);
    233             firstSibling = Math.max(0, lastSibling - MAX_DOCS_IN_INTENT + 1);
    234         }
    235 
    236         if (DEBUG) Log.d(TAG, "Copmuted siblings from index: " + firstSibling
    237                 + " to: " + lastSibling);
    238 
    239         return new Range(firstSibling, lastSibling);
    240     }
    241 }
    242