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