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