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