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.model;
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.DocumentsContract.Root;
28 import android.provider.DocumentsProvider;
29 import android.text.TextUtils;
30 
31 import com.android.documentsui.DocumentsApplication;
32 import com.android.documentsui.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.text.Collator;
42 
43 /**
44  * Representation of a {@link Document}.
45  */
46 public class DocumentInfo implements Durable, Parcelable {
47     private static final int VERSION_INIT = 1;
48     private static final int VERSION_SPLIT_URI = 2;
49 
50     private static final Collator sCollator;
51 
52     static {
53         sCollator = Collator.getInstance();
54         sCollator.setStrength(Collator.SECONDARY);
55     }
56 
57     public String authority;
58     public String documentId;
59     public String mimeType;
60     public String displayName;
61     public long lastModified;
62     public int flags;
63     public String summary;
64     public long size;
65     public int icon;
66 
67     /** Derived fields that aren't persisted */
68     public Uri derivedUri;
69 
DocumentInfo()70     public DocumentInfo() {
71         reset();
72     }
73 
74     @Override
reset()75     public void reset() {
76         authority = null;
77         documentId = null;
78         mimeType = null;
79         displayName = null;
80         lastModified = -1;
81         flags = 0;
82         summary = null;
83         size = -1;
84         icon = 0;
85 
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         final String authority = getCursorString(cursor, RootCursorWrapper.COLUMN_AUTHORITY);
152         return fromCursor(cursor, authority);
153     }
154 
fromCursor(Cursor cursor, String authority)155     public static DocumentInfo fromCursor(Cursor cursor, String authority) {
156         final DocumentInfo info = new DocumentInfo();
157         info.updateFromCursor(cursor, authority);
158         return info;
159     }
160 
updateFromCursor(Cursor cursor, String authority)161     public void updateFromCursor(Cursor cursor, String authority) {
162         this.authority = authority;
163         this.documentId = getCursorString(cursor, Document.COLUMN_DOCUMENT_ID);
164         this.mimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
165         this.displayName = getCursorString(cursor, Document.COLUMN_DISPLAY_NAME);
166         this.lastModified = getCursorLong(cursor, Document.COLUMN_LAST_MODIFIED);
167         this.flags = getCursorInt(cursor, Document.COLUMN_FLAGS);
168         this.summary = getCursorString(cursor, Document.COLUMN_SUMMARY);
169         this.size = getCursorLong(cursor, Document.COLUMN_SIZE);
170         this.icon = getCursorInt(cursor, Document.COLUMN_ICON);
171         this.deriveFields();
172     }
173 
fromUri(ContentResolver resolver, Uri uri)174     public static DocumentInfo fromUri(ContentResolver resolver, Uri uri)
175             throws FileNotFoundException {
176         final DocumentInfo info = new DocumentInfo();
177         info.updateFromUri(resolver, uri);
178         return info;
179     }
180 
181     /**
182      * Update a possibly stale restored document against a live
183      * {@link DocumentsProvider}.
184      */
updateSelf(ContentResolver resolver)185     public void updateSelf(ContentResolver resolver) throws FileNotFoundException {
186         updateFromUri(resolver, derivedUri);
187     }
188 
updateFromUri(ContentResolver resolver, Uri uri)189     public void updateFromUri(ContentResolver resolver, Uri uri) throws FileNotFoundException {
190         ContentProviderClient client = null;
191         Cursor cursor = null;
192         try {
193             client = DocumentsApplication.acquireUnstableProviderOrThrow(
194                     resolver, uri.getAuthority());
195             cursor = client.query(uri, null, null, null, null);
196             if (!cursor.moveToFirst()) {
197                 throw new FileNotFoundException("Missing details for " + uri);
198             }
199             updateFromCursor(cursor, uri.getAuthority());
200         } catch (Throwable t) {
201             throw asFileNotFoundException(t);
202         } finally {
203             IoUtils.closeQuietly(cursor);
204             ContentProviderClient.releaseQuietly(client);
205         }
206     }
207 
deriveFields()208     private void deriveFields() {
209         derivedUri = DocumentsContract.buildDocumentUri(authority, documentId);
210     }
211 
212     @Override
toString()213     public String toString() {
214         return "Document{docId=" + documentId + ", name=" + displayName + "}";
215     }
216 
isCreateSupported()217     public boolean isCreateSupported() {
218         return (flags & Document.FLAG_DIR_SUPPORTS_CREATE) != 0;
219     }
220 
isThumbnailSupported()221     public boolean isThumbnailSupported() {
222         return (flags & Document.FLAG_SUPPORTS_THUMBNAIL) != 0;
223     }
224 
isDirectory()225     public boolean isDirectory() {
226         return Document.MIME_TYPE_DIR.equals(mimeType);
227     }
228 
isGridPreferred()229     public boolean isGridPreferred() {
230         return (flags & Document.FLAG_DIR_PREFERS_GRID) != 0;
231     }
232 
isDeleteSupported()233     public boolean isDeleteSupported() {
234         return (flags & Document.FLAG_SUPPORTS_DELETE) != 0;
235     }
236 
isGridTitlesHidden()237     public boolean isGridTitlesHidden() {
238         return (flags & Document.FLAG_DIR_HIDE_GRID_TITLES) != 0;
239     }
240 
getCursorString(Cursor cursor, String columnName)241     public static String getCursorString(Cursor cursor, String columnName) {
242         final int index = cursor.getColumnIndex(columnName);
243         return (index != -1) ? cursor.getString(index) : null;
244     }
245 
246     /**
247      * Missing or null values are returned as -1.
248      */
getCursorLong(Cursor cursor, String columnName)249     public static long getCursorLong(Cursor cursor, String columnName) {
250         final int index = cursor.getColumnIndex(columnName);
251         if (index == -1) return -1;
252         final String value = cursor.getString(index);
253         if (value == null) return -1;
254         try {
255             return Long.parseLong(value);
256         } catch (NumberFormatException e) {
257             return -1;
258         }
259     }
260 
261     /**
262      * Missing or null values are returned as 0.
263      */
getCursorInt(Cursor cursor, String columnName)264     public static int getCursorInt(Cursor cursor, String columnName) {
265         final int index = cursor.getColumnIndex(columnName);
266         return (index != -1) ? cursor.getInt(index) : 0;
267     }
268 
asFileNotFoundException(Throwable t)269     public static FileNotFoundException asFileNotFoundException(Throwable t)
270             throws FileNotFoundException {
271         if (t instanceof FileNotFoundException) {
272             throw (FileNotFoundException) t;
273         }
274         final FileNotFoundException fnfe = new FileNotFoundException(t.getMessage());
275         fnfe.initCause(t);
276         throw fnfe;
277     }
278 
279     /**
280      * String prefix used to indicate the document is a directory.
281      */
282     public static final char DIR_PREFIX = '\001';
283 
284     /**
285      * Compare two strings against each other using system default collator in a
286      * case-insensitive mode. Clusters strings prefixed with {@link #DIR_PREFIX}
287      * before other items.
288      */
compareToIgnoreCaseNullable(String lhs, String rhs)289     public static int compareToIgnoreCaseNullable(String lhs, String rhs) {
290         final boolean leftEmpty = TextUtils.isEmpty(lhs);
291         final boolean rightEmpty = TextUtils.isEmpty(rhs);
292 
293         if (leftEmpty && rightEmpty) return 0;
294         if (leftEmpty) return -1;
295         if (rightEmpty) return 1;
296 
297         final boolean leftDir = (lhs.charAt(0) == DIR_PREFIX);
298         final boolean rightDir = (rhs.charAt(0) == DIR_PREFIX);
299 
300         if (leftDir && !rightDir) return -1;
301         if (rightDir && !leftDir) return 1;
302 
303         return sCollator.compare(lhs, rhs);
304     }
305 }
306