/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.documentsui; import static com.android.documentsui.Shared.DEBUG; import static com.android.documentsui.Shared.MAX_DOCS_IN_INTENT; import static com.android.documentsui.model.DocumentInfo.getCursorString; import android.content.ClipData; import android.content.ClipDescription; import android.content.ComponentName; import android.content.Intent; import android.content.pm.PackageManager; import android.content.res.Resources; import android.database.Cursor; import android.net.Uri; import android.os.Build; import android.provider.DocumentsContract; import android.provider.DocumentsContract.Document; import android.support.annotation.Nullable; import android.text.TextUtils; import android.util.Log; import android.util.Range; import com.android.documentsui.dirlist.Model; import com.android.documentsui.model.DocumentInfo; import java.util.ArrayList; import java.util.List; /** * Provides support for gather a list of quick-viewable files into a quick view intent. */ final class QuickViewIntentBuilder { private static final String TAG = "QuickViewIntentBuilder"; private final DocumentInfo mDocument; private final Model mModel; private final PackageManager mPkgManager; private final Resources mResources; public QuickViewIntentBuilder( PackageManager pkgManager, Resources resources, DocumentInfo doc, Model model) { mPkgManager = pkgManager; mResources = resources; mDocument = doc; mModel = model; } /** * Builds the intent for quick viewing. Short circuits building if a handler cannot * be resolved; in this case {@code null} is returned. */ @Nullable Intent build() { if (DEBUG) Log.d(TAG, "Preparing intent for doc:" + mDocument.documentId); String trustedPkg = getQuickViewPackage(); if (!TextUtils.isEmpty(trustedPkg)) { Intent intent = new Intent(Intent.ACTION_QUICK_VIEW); intent.setDataAndType(mDocument.derivedUri, mDocument.mimeType); intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); intent.setPackage(trustedPkg); if (hasRegisteredHandler(intent)) { final ArrayList<Uri> uris = new ArrayList<Uri>(); final int documentLocation = collectViewableUris(uris); final Range<Integer> range = computeSiblingsRange(uris, documentLocation); ClipData clipData = null; ClipData.Item item; Uri uri; for (int i = range.getLower(); i <= range.getUpper(); i++) { uri = uris.get(i); item = new ClipData.Item(uri); if (DEBUG) Log.d(TAG, "Including file: " + uri); if (clipData == null) { clipData = new ClipData( "URIs", new String[] { ClipDescription.MIMETYPE_TEXT_URILIST }, item); } else { clipData.addItem(item); } } // The documentLocation variable contains an index in "uris". However, // ClipData contains a slice of "uris", so we need to shift the location // so it points to the same Uri. intent.putExtra(Intent.EXTRA_INDEX, documentLocation - range.getLower()); intent.setClipData(clipData); return intent; } else { Log.e(TAG, "Can't resolve trusted quick view package: " + trustedPkg); } } return null; } private String getQuickViewPackage() { String resValue = mResources.getString(R.string.trusted_quick_viewer_package); if (Build.IS_DEBUGGABLE ) { // Allow users of debug devices to override default quick viewer // for the purposes of testing. return android.os.SystemProperties.get("debug.quick_viewer", resValue); } return resValue; } private int collectViewableUris(ArrayList<Uri> uris) { final String[] siblingIds = mModel.getModelIds(); uris.ensureCapacity(siblingIds.length); int documentLocation = 0; Cursor cursor; String mimeType; String id; String authority; Uri uri; // Cursor's are not guaranteed to be immutable. Hence, traverse it only once. for (int i = 0; i < siblingIds.length; i++) { cursor = mModel.getItem(siblingIds[i]); if (cursor == null) { if (DEBUG) Log.d(TAG, "Unable to obtain cursor for sibling document, modelId: " + siblingIds[i]); continue; } mimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE); if (Document.MIME_TYPE_DIR.equals(mimeType)) { if (DEBUG) Log.d(TAG, "Skipping directory, not supported by quick view. modelId: " + siblingIds[i]); continue; } id = getCursorString(cursor, Document.COLUMN_DOCUMENT_ID); authority = getCursorString(cursor, RootCursorWrapper.COLUMN_AUTHORITY); uri = DocumentsContract.buildDocumentUri(authority, id); uris.add(uri); if (id.equals(mDocument.documentId)) { documentLocation = uris.size() - 1; // Position in "uris", not in the model. if (DEBUG) Log.d(TAG, "Found starting point for QV. " + documentLocation); } } return documentLocation; } private static Range<Integer> computeSiblingsRange(List<Uri> uris, int documentLocation) { // Restrict number of siblings to avoid hitting the IPC limit. // TODO: Remove this restriction once ClipData can hold an arbitrary number of // items. int firstSibling; int lastSibling; if (documentLocation < uris.size() / 2) { firstSibling = Math.max(0, documentLocation - MAX_DOCS_IN_INTENT / 2); lastSibling = Math.min(uris.size() - 1, firstSibling + MAX_DOCS_IN_INTENT - 1); } else { lastSibling = Math.min(uris.size() - 1, documentLocation + MAX_DOCS_IN_INTENT / 2); firstSibling = Math.max(0, lastSibling - MAX_DOCS_IN_INTENT + 1); } if (DEBUG) Log.d(TAG, "Copmuted siblings from index: " + firstSibling + " to: " + lastSibling); return new Range(firstSibling, lastSibling); } private boolean hasRegisteredHandler(Intent intent) { // Try to resolve the intent. If a matching app isn't installed, it won't resolve. return intent.resolveActivity(mPkgManager) != null; } }