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