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