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