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