1 /*
2  * Copyright (C) 2013 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.base;
18 
19 import static com.android.documentsui.base.SharedMinimal.DEBUG;
20 
21 import android.content.ContentProviderClient;
22 import android.content.ContentResolver;
23 import android.database.Cursor;
24 import android.net.Uri;
25 import android.os.FileUtils;
26 import android.os.Parcel;
27 import android.os.Parcelable;
28 import android.provider.DocumentsContract;
29 import android.provider.DocumentsContract.Document;
30 import android.provider.DocumentsProvider;
31 import android.util.Log;
32 
33 import androidx.annotation.VisibleForTesting;
34 
35 import com.android.documentsui.DocumentsApplication;
36 import com.android.documentsui.archives.ArchivesProvider;
37 import com.android.documentsui.roots.RootCursorWrapper;
38 import com.android.documentsui.util.VersionUtils;
39 
40 import java.io.DataInputStream;
41 import java.io.DataOutputStream;
42 import java.io.FileNotFoundException;
43 import java.io.IOException;
44 import java.net.ProtocolException;
45 import java.util.Arrays;
46 import java.util.Objects;
47 import java.util.Set;
48 
49 import javax.annotation.Nullable;
50 
51 /**
52  * Representation of a {@link Document}.
53  */
54 public class DocumentInfo implements Durable, Parcelable {
55     private static final String TAG = "DocumentInfo";
56     private static final int VERSION_INIT = 1;
57     private static final int VERSION_SPLIT_URI = 2;
58     private static final int VERSION_USER_ID = 3;
59 
60     public UserId userId;
61     public String authority;
62     public String documentId;
63     public String mimeType;
64     public String displayName;
65     public long lastModified;
66     public int flags;
67     public String summary;
68     public long size;
69     public int icon;
70 
71     /** Derived fields that aren't persisted */
72     public Uri derivedUri;
73 
DocumentInfo()74     public DocumentInfo() {
75         reset();
76     }
77 
78     @Override
reset()79     public void reset() {
80         userId = UserId.UNSPECIFIED_USER;
81         authority = null;
82         documentId = null;
83         mimeType = null;
84         displayName = null;
85         lastModified = -1;
86         flags = 0;
87         summary = null;
88         size = -1;
89         icon = 0;
90         derivedUri = null;
91     }
92 
93     @Override
read(DataInputStream in)94     public void read(DataInputStream in) throws IOException {
95         final int version = in.readInt();
96         switch (version) {
97             case VERSION_USER_ID:
98                 userId = UserId.read(in);
99             case VERSION_SPLIT_URI:
100                 if (version < VERSION_USER_ID) {
101                     userId = UserId.CURRENT_USER;
102                 }
103                 authority = DurableUtils.readNullableString(in);
104                 documentId = DurableUtils.readNullableString(in);
105                 mimeType = DurableUtils.readNullableString(in);
106                 displayName = DurableUtils.readNullableString(in);
107                 lastModified = in.readLong();
108                 flags = in.readInt();
109                 summary = DurableUtils.readNullableString(in);
110                 size = in.readLong();
111                 icon = in.readInt();
112                 deriveFields();
113                 break;
114             case VERSION_INIT:
115                 throw new ProtocolException("Ignored upgrade");
116             default:
117                 throw new ProtocolException("Unknown version " + version);
118         }
119     }
120 
121     @Override
write(DataOutputStream out)122     public void write(DataOutputStream out) throws IOException {
123         out.writeInt(VERSION_USER_ID);
124         UserId.write(out, userId);
125         DurableUtils.writeNullableString(out, authority);
126         DurableUtils.writeNullableString(out, documentId);
127         DurableUtils.writeNullableString(out, mimeType);
128         DurableUtils.writeNullableString(out, displayName);
129         out.writeLong(lastModified);
130         out.writeInt(flags);
131         DurableUtils.writeNullableString(out, summary);
132         out.writeLong(size);
133         out.writeInt(icon);
134     }
135 
136     @Override
describeContents()137     public int describeContents() {
138         return 0;
139     }
140 
141     @Override
writeToParcel(Parcel dest, int flags)142     public void writeToParcel(Parcel dest, int flags) {
143         DurableUtils.writeToParcel(dest, this);
144     }
145 
146     public static final Creator<DocumentInfo> CREATOR = new Creator<DocumentInfo>() {
147         @Override
148         public DocumentInfo createFromParcel(Parcel in) {
149             final DocumentInfo doc = new DocumentInfo();
150             DurableUtils.readFromParcel(in, doc);
151             return doc;
152         }
153 
154         @Override
155         public DocumentInfo[] newArray(int size) {
156             return new DocumentInfo[size];
157         }
158     };
159 
fromDirectoryCursor(Cursor cursor)160     public static DocumentInfo fromDirectoryCursor(Cursor cursor) {
161         assert (cursor != null);
162         assert (cursor.getColumnIndex(RootCursorWrapper.COLUMN_USER_ID) >= 0);
163         final UserId userId = UserId.of(getCursorInt(cursor, RootCursorWrapper.COLUMN_USER_ID));
164         final String authority = getCursorString(cursor, RootCursorWrapper.COLUMN_AUTHORITY);
165         return fromCursor(cursor, userId, authority);
166     }
167 
fromCursor(Cursor cursor, UserId userId, String authority)168     public static DocumentInfo fromCursor(Cursor cursor, UserId userId, String authority) {
169         assert(cursor != null);
170         final DocumentInfo info = new DocumentInfo();
171         info.updateFromCursor(cursor, userId, authority);
172         return info;
173     }
174 
updateFromCursor(Cursor cursor, UserId userId, String authority)175     public void updateFromCursor(Cursor cursor, UserId userId, String authority) {
176         this.userId = userId;
177         this.authority = authority;
178         this.documentId = getCursorString(cursor, Document.COLUMN_DOCUMENT_ID);
179         this.mimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
180         this.displayName = getCursorString(cursor, Document.COLUMN_DISPLAY_NAME);
181         this.lastModified = getCursorLong(cursor, Document.COLUMN_LAST_MODIFIED);
182         this.flags = getCursorInt(cursor, Document.COLUMN_FLAGS);
183         this.summary = getCursorString(cursor, Document.COLUMN_SUMMARY);
184         this.size = getCursorLong(cursor, Document.COLUMN_SIZE);
185         this.icon = getCursorInt(cursor, Document.COLUMN_ICON);
186         this.deriveFields();
187     }
188 
189     /**
190      * Resolves a document info from the uri. The caller should specify the user of the resolver
191      * by providing a {@link UserId}.
192      */
fromUri(ContentResolver resolver, Uri uri, UserId userId)193     public static DocumentInfo fromUri(ContentResolver resolver, Uri uri, UserId userId)
194             throws FileNotFoundException {
195         final DocumentInfo info = new DocumentInfo();
196         info.updateFromUri(resolver, uri, userId);
197         return info;
198     }
199 
200     /**
201      * Update a possibly stale restored document against a live {@link DocumentsProvider}.  The
202      * caller should specify the user of the resolver by providing a {@link UserId}.
203      */
updateSelf(ContentResolver resolver, UserId userId)204     public void updateSelf(ContentResolver resolver, UserId userId) throws FileNotFoundException {
205         updateFromUri(resolver, derivedUri, userId);
206     }
207 
updateFromUri(ContentResolver resolver, Uri uri, UserId userId)208     private void updateFromUri(ContentResolver resolver, Uri uri, UserId userId)
209             throws FileNotFoundException {
210         ContentProviderClient client = null;
211         Cursor cursor = null;
212         try {
213             client = DocumentsApplication.acquireUnstableProviderOrThrow(
214                     resolver, uri.getAuthority());
215             cursor = client.query(uri, null, null, null, null);
216             if (!cursor.moveToFirst()) {
217                 throw new FileNotFoundException("Missing details for " + uri);
218             }
219             updateFromCursor(cursor, userId, uri.getAuthority());
220         } catch (Throwable t) {
221             throw asFileNotFoundException(t);
222         } finally {
223             FileUtils.closeQuietly(cursor);
224             FileUtils.closeQuietly(client);
225         }
226     }
227 
228     @VisibleForTesting
deriveFields()229     void deriveFields() {
230         derivedUri = DocumentsContract.buildDocumentUri(authority, documentId);
231     }
232 
233     @Override
toString()234     public String toString() {
235         return "DocumentInfo{"
236                 + "docId=" + documentId
237                 + ", userId=" + userId
238                 + ", name=" + displayName
239                 + ", mimeType=" + mimeType
240                 + ", isContainer=" + isContainer()
241                 + ", isDirectory=" + isDirectory()
242                 + ", isArchive=" + isArchive()
243                 + ", isInArchive=" + isInArchive()
244                 + ", isPartial=" + isPartial()
245                 + ", isVirtual=" + isVirtual()
246                 + ", isDeleteSupported=" + isDeleteSupported()
247                 + ", isCreateSupported=" + isCreateSupported()
248                 + ", isMoveSupported=" + isMoveSupported()
249                 + ", isRenameSupported=" + isRenameSupported()
250                 + ", isMetadataSupported=" + isMetadataSupported()
251                 + ", isBlockedFromTree=" + isBlockedFromTree()
252                 + "} @ "
253                 + derivedUri;
254     }
255 
isCreateSupported()256     public boolean isCreateSupported() {
257         return (flags & Document.FLAG_DIR_SUPPORTS_CREATE) != 0;
258     }
259 
isDeleteSupported()260     public boolean isDeleteSupported() {
261         return (flags & Document.FLAG_SUPPORTS_DELETE) != 0;
262     }
263 
isMetadataSupported()264     public boolean isMetadataSupported() {
265         return (flags & Document.FLAG_SUPPORTS_METADATA) != 0;
266     }
267 
isMoveSupported()268     public boolean isMoveSupported() {
269         return (flags & Document.FLAG_SUPPORTS_MOVE) != 0;
270     }
271 
isRemoveSupported()272     public boolean isRemoveSupported() {
273         return (flags & Document.FLAG_SUPPORTS_REMOVE) != 0;
274     }
275 
isRenameSupported()276     public boolean isRenameSupported() {
277         return (flags & Document.FLAG_SUPPORTS_RENAME) != 0;
278     }
279 
isSettingsSupported()280     public boolean isSettingsSupported() {
281         return (flags & Document.FLAG_SUPPORTS_SETTINGS) != 0;
282     }
283 
isThumbnailSupported()284     public boolean isThumbnailSupported() {
285         return (flags & Document.FLAG_SUPPORTS_THUMBNAIL) != 0;
286     }
287 
isWeblinkSupported()288     public boolean isWeblinkSupported() {
289         return (flags & Document.FLAG_WEB_LINKABLE) != 0;
290     }
291 
isWriteSupported()292     public boolean isWriteSupported() {
293         return (flags & Document.FLAG_SUPPORTS_WRITE) != 0;
294     }
295 
isDirectory()296     public boolean isDirectory() {
297         return Document.MIME_TYPE_DIR.equals(mimeType);
298     }
299 
isArchive()300     public boolean isArchive() {
301         return ArchivesProvider.isSupportedArchiveType(mimeType);
302     }
303 
isInArchive()304     public boolean isInArchive() {
305         return ArchivesProvider.AUTHORITY.equals(authority);
306     }
307 
isPartial()308     public boolean isPartial() {
309         return (flags & Document.FLAG_PARTIAL) != 0;
310     }
311 
isBlockedFromTree()312     public boolean isBlockedFromTree() {
313         if (VersionUtils.isAtLeastR()) {
314             return (flags & Document.FLAG_DIR_BLOCKS_OPEN_DOCUMENT_TREE) != 0;
315         } else {
316             return false;
317         }
318     }
319 
320     // Containers are documents which can be opened in DocumentsUI as folders.
isContainer()321     public boolean isContainer() {
322         return isDirectory() || (isArchive() && !isInArchive() && !isPartial());
323     }
324 
isVirtual()325     public boolean isVirtual() {
326         return (flags & Document.FLAG_VIRTUAL_DOCUMENT) != 0;
327     }
328 
prefersSortByLastModified()329     public boolean prefersSortByLastModified() {
330         return (flags & Document.FLAG_DIR_PREFERS_LAST_MODIFIED) != 0;
331     }
332 
333     /**
334      * Returns a document uri representing this {@link DocumentInfo}. The URI may contain user
335      * information. Use this when uri is needed externally. For usage within DocsUI, use
336      * {@link #derivedUri}.
337      */
getDocumentUri()338     public Uri getDocumentUri() {
339         if (UserId.CURRENT_USER.equals(userId)) {
340             return derivedUri;
341         }
342         return userId.buildDocumentUriAsUser(authority, documentId);
343     }
344 
345 
346     /**
347      * Returns a tree document uri representing this {@link DocumentInfo}. The URI may contain user
348      * information. Use this when uri is needed externally.
349      */
getTreeDocumentUri()350     public Uri getTreeDocumentUri() {
351         if (UserId.CURRENT_USER.equals(userId)) {
352             return DocumentsContract.buildTreeDocumentUri(authority, documentId);
353         }
354         return userId.buildTreeDocumentUriAsUser(authority, documentId);
355     }
356 
357     @Override
hashCode()358     public int hashCode() {
359         return userId.hashCode() + derivedUri.hashCode() + mimeType.hashCode();
360     }
361 
362     @Override
equals(Object o)363     public boolean equals(Object o) {
364         if (o == null) {
365             return false;
366         }
367 
368         if (this == o) {
369             return true;
370         }
371 
372         if (o instanceof DocumentInfo) {
373             DocumentInfo other = (DocumentInfo) o;
374             // Uri + mime type should be totally unique.
375             return Objects.equals(userId, other.userId)
376                     && Objects.equals(derivedUri, other.derivedUri)
377                     && Objects.equals(mimeType, other.mimeType);
378         }
379 
380         return false;
381     }
382 
getCursorString(Cursor cursor, String columnName)383     public static String getCursorString(Cursor cursor, String columnName) {
384         if (cursor == null) {
385             return null;
386         }
387         final int index = cursor.getColumnIndex(columnName);
388         return (index != -1) ? cursor.getString(index) : null;
389     }
390 
391     /**
392      * Missing or null values are returned as -1.
393      */
getCursorLong(Cursor cursor, String columnName)394     public static long getCursorLong(Cursor cursor, String columnName) {
395         if (cursor == null) {
396             return -1;
397         }
398 
399         final int index = cursor.getColumnIndex(columnName);
400         if (index == -1) return -1;
401         final String value = cursor.getString(index);
402         if (value == null) return -1;
403         try {
404             return Long.parseLong(value);
405         } catch (NumberFormatException e) {
406             return -1;
407         }
408     }
409 
410     /**
411      * Missing or null values are returned as 0.
412      */
getCursorInt(Cursor cursor, String columnName)413     public static int getCursorInt(Cursor cursor, String columnName) {
414         if (cursor == null) {
415             return 0;
416         }
417 
418         final int index = cursor.getColumnIndex(columnName);
419         return (index != -1) ? cursor.getInt(index) : 0;
420     }
421 
asFileNotFoundException(Throwable t)422     public static FileNotFoundException asFileNotFoundException(Throwable t)
423             throws FileNotFoundException {
424         if (t instanceof FileNotFoundException) {
425             throw (FileNotFoundException) t;
426         }
427         final FileNotFoundException fnfe = new FileNotFoundException(t.getMessage());
428         fnfe.initCause(t);
429         throw fnfe;
430     }
431 
getUri(Cursor cursor)432     public static Uri getUri(Cursor cursor) {
433         return DocumentsContract.buildDocumentUri(
434             getCursorString(cursor, RootCursorWrapper.COLUMN_AUTHORITY),
435             getCursorString(cursor, Document.COLUMN_DOCUMENT_ID));
436     }
437 
getUserId(Cursor cursor)438     public static UserId getUserId(Cursor cursor) {
439         return UserId.of(getCursorInt(cursor, RootCursorWrapper.COLUMN_USER_ID));
440     }
441 
addMimeTypes(ContentResolver resolver, Uri uri, Set<String> mimeTypes)442     public static void addMimeTypes(ContentResolver resolver, Uri uri, Set<String> mimeTypes) {
443         assert(uri != null);
444         if ("content".equals(uri.getScheme())) {
445             final String type = resolver.getType(uri);
446             if (type != null) {
447                 mimeTypes.add(type);
448             } else {
449                 if (DEBUG) {
450                     Log.d(TAG, "resolver.getType(uri) return null, url:" + uri.toSafeString());
451                 }
452             }
453             final String[] streamTypes = resolver.getStreamTypes(uri, "*/*");
454             if (streamTypes != null) {
455                 mimeTypes.addAll(Arrays.asList(streamTypes));
456             }
457         }
458     }
459 
debugString(@ullable DocumentInfo doc)460     public static String debugString(@Nullable DocumentInfo doc) {
461         if (doc == null) {
462             return "<null DocumentInfo>";
463         }
464 
465         if (doc.derivedUri == null) {
466             return "<DocumentInfo null derivedUri>";
467         }
468         return doc.derivedUri.toString();
469     }
470 }
471