Home | History | Annotate | Download | only in documentsui
      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;
     18 
     19 import static com.android.documentsui.Shared.DEBUG;
     20 import static com.android.documentsui.Shared.MAX_DOCS_IN_INTENT;
     21 import static com.android.documentsui.model.DocumentInfo.getCursorString;
     22 
     23 import android.content.ClipData;
     24 import android.content.ClipDescription;
     25 import android.content.ComponentName;
     26 import android.content.Intent;
     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.dirlist.Model;
     40 import com.android.documentsui.model.DocumentInfo;
     41 
     42 import java.util.ArrayList;
     43 import java.util.List;
     44 
     45 /**
     46  * Provides support for gather a list of quick-viewable files into a quick view intent.
     47  */
     48 final class QuickViewIntentBuilder {
     49 
     50     private static final String TAG = "QuickViewIntentBuilder";
     51 
     52     private final DocumentInfo mDocument;
     53     private final Model mModel;
     54 
     55     private final PackageManager mPkgManager;
     56     private final Resources mResources;
     57 
     58     public QuickViewIntentBuilder(
     59             PackageManager pkgManager,
     60             Resources resources,
     61             DocumentInfo doc,
     62             Model model) {
     63 
     64         mPkgManager = pkgManager;
     65         mResources = resources;
     66         mDocument = doc;
     67         mModel = model;
     68     }
     69 
     70     /**
     71      * Builds the intent for quick viewing. Short circuits building if a handler cannot
     72      * be resolved; in this case {@code null} is returned.
     73      */
     74     @Nullable Intent build() {
     75         if (DEBUG) Log.d(TAG, "Preparing intent for doc:" + mDocument.documentId);
     76 
     77         String trustedPkg = getQuickViewPackage();
     78 
     79         if (!TextUtils.isEmpty(trustedPkg)) {
     80             Intent intent = new Intent(Intent.ACTION_QUICK_VIEW);
     81             intent.setDataAndType(mDocument.derivedUri, mDocument.mimeType);
     82             intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
     83             intent.setPackage(trustedPkg);
     84             if (hasRegisteredHandler(intent)) {
     85                 final ArrayList<Uri> uris = new ArrayList<Uri>();
     86                 final int documentLocation = collectViewableUris(uris);
     87                 final Range<Integer> range = computeSiblingsRange(uris, documentLocation);
     88 
     89                 ClipData clipData = null;
     90                 ClipData.Item item;
     91                 Uri uri;
     92                 for (int i = range.getLower(); i <= range.getUpper(); i++) {
     93                     uri = uris.get(i);
     94                     item = new ClipData.Item(uri);
     95                     if (DEBUG) Log.d(TAG, "Including file: " + uri);
     96                     if (clipData == null) {
     97                         clipData = new ClipData(
     98                                 "URIs", new String[] { ClipDescription.MIMETYPE_TEXT_URILIST },
     99                                 item);
    100                     } else {
    101                         clipData.addItem(item);
    102                     }
    103                 }
    104 
    105                 // The documentLocation variable contains an index in "uris". However,
    106                 // ClipData contains a slice of "uris", so we need to shift the location
    107                 // so it points to the same Uri.
    108                 intent.putExtra(Intent.EXTRA_INDEX, documentLocation - range.getLower());
    109                 intent.setClipData(clipData);
    110 
    111                 return intent;
    112             } else {
    113                 Log.e(TAG, "Can't resolve trusted quick view package: " + trustedPkg);
    114             }
    115         }
    116 
    117         return null;
    118     }
    119 
    120     private String getQuickViewPackage() {
    121         String resValue = mResources.getString(R.string.trusted_quick_viewer_package);
    122         if (Build.IS_DEBUGGABLE ) {
    123             // Allow users of debug devices to override default quick viewer
    124             // for the purposes of testing.
    125             return android.os.SystemProperties.get("debug.quick_viewer", resValue);
    126         }
    127         return resValue;
    128     }
    129 
    130     private int collectViewableUris(ArrayList<Uri> uris) {
    131         final String[] siblingIds = mModel.getModelIds();
    132         uris.ensureCapacity(siblingIds.length);
    133 
    134         int documentLocation = 0;
    135         Cursor cursor;
    136         String mimeType;
    137         String id;
    138         String authority;
    139         Uri uri;
    140 
    141         // Cursor's are not guaranteed to be immutable. Hence, traverse it only once.
    142         for (int i = 0; i < siblingIds.length; i++) {
    143             cursor = mModel.getItem(siblingIds[i]);
    144 
    145             if (cursor == null) {
    146                 if (DEBUG) Log.d(TAG,
    147                         "Unable to obtain cursor for sibling document, modelId: "
    148                         + siblingIds[i]);
    149                 continue;
    150             }
    151 
    152             mimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
    153             if (Document.MIME_TYPE_DIR.equals(mimeType)) {
    154                 if (DEBUG) Log.d(TAG,
    155                         "Skipping directory, not supported by quick view. modelId: "
    156                         + siblingIds[i]);
    157                 continue;
    158             }
    159 
    160             id = getCursorString(cursor, Document.COLUMN_DOCUMENT_ID);
    161             authority = getCursorString(cursor, RootCursorWrapper.COLUMN_AUTHORITY);
    162             uri = DocumentsContract.buildDocumentUri(authority, id);
    163 
    164             uris.add(uri);
    165 
    166             if (id.equals(mDocument.documentId)) {
    167                 documentLocation = uris.size() - 1;  // Position in "uris", not in the model.
    168                 if (DEBUG) Log.d(TAG, "Found starting point for QV. " + documentLocation);
    169             }
    170         }
    171 
    172         return documentLocation;
    173     }
    174 
    175     private static Range<Integer> computeSiblingsRange(List<Uri> uris, int documentLocation) {
    176         // Restrict number of siblings to avoid hitting the IPC limit.
    177         // TODO: Remove this restriction once ClipData can hold an arbitrary number of
    178         // items.
    179         int firstSibling;
    180         int lastSibling;
    181         if (documentLocation < uris.size() / 2) {
    182             firstSibling = Math.max(0, documentLocation - MAX_DOCS_IN_INTENT / 2);
    183             lastSibling = Math.min(uris.size() - 1, firstSibling + MAX_DOCS_IN_INTENT - 1);
    184         } else {
    185             lastSibling = Math.min(uris.size() - 1, documentLocation + MAX_DOCS_IN_INTENT / 2);
    186             firstSibling = Math.max(0, lastSibling - MAX_DOCS_IN_INTENT + 1);
    187         }
    188 
    189         if (DEBUG) Log.d(TAG, "Copmuted siblings from index: " + firstSibling
    190                 + " to: " + lastSibling);
    191 
    192         return new Range(firstSibling, lastSibling);
    193     }
    194 
    195     private boolean hasRegisteredHandler(Intent intent) {
    196         // Try to resolve the intent. If a matching app isn't installed, it won't resolve.
    197         return intent.resolveActivity(mPkgManager) != null;
    198     }
    199 }
    200