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