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.text.TextUtils;
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.text.Collator;
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    private static final Collator sCollator;
50
51    static {
52        sCollator = Collator.getInstance();
53        sCollator.setStrength(Collator.SECONDARY);
54    }
55
56    public String authority;
57    public String documentId;
58    public String mimeType;
59    public String displayName;
60    public long lastModified;
61    public int flags;
62    public String summary;
63    public long size;
64    public int icon;
65
66    /** Derived fields that aren't persisted */
67    public Uri derivedUri;
68
69    public DocumentInfo() {
70        reset();
71    }
72
73    @Override
74    public void reset() {
75        authority = null;
76        documentId = null;
77        mimeType = null;
78        displayName = null;
79        lastModified = -1;
80        flags = 0;
81        summary = null;
82        size = -1;
83        icon = 0;
84
85        derivedUri = null;
86    }
87
88    @Override
89    public void read(DataInputStream in) throws IOException {
90        final int version = in.readInt();
91        switch (version) {
92            case VERSION_INIT:
93                throw new ProtocolException("Ignored upgrade");
94            case VERSION_SPLIT_URI:
95                authority = DurableUtils.readNullableString(in);
96                documentId = DurableUtils.readNullableString(in);
97                mimeType = DurableUtils.readNullableString(in);
98                displayName = DurableUtils.readNullableString(in);
99                lastModified = in.readLong();
100                flags = in.readInt();
101                summary = DurableUtils.readNullableString(in);
102                size = in.readLong();
103                icon = in.readInt();
104                deriveFields();
105                break;
106            default:
107                throw new ProtocolException("Unknown version " + version);
108        }
109    }
110
111    @Override
112    public void write(DataOutputStream out) throws IOException {
113        out.writeInt(VERSION_SPLIT_URI);
114        DurableUtils.writeNullableString(out, authority);
115        DurableUtils.writeNullableString(out, documentId);
116        DurableUtils.writeNullableString(out, mimeType);
117        DurableUtils.writeNullableString(out, displayName);
118        out.writeLong(lastModified);
119        out.writeInt(flags);
120        DurableUtils.writeNullableString(out, summary);
121        out.writeLong(size);
122        out.writeInt(icon);
123    }
124
125    @Override
126    public int describeContents() {
127        return 0;
128    }
129
130    @Override
131    public void writeToParcel(Parcel dest, int flags) {
132        DurableUtils.writeToParcel(dest, this);
133    }
134
135    public static final Creator<DocumentInfo> CREATOR = new Creator<DocumentInfo>() {
136        @Override
137        public DocumentInfo createFromParcel(Parcel in) {
138            final DocumentInfo doc = new DocumentInfo();
139            DurableUtils.readFromParcel(in, doc);
140            return doc;
141        }
142
143        @Override
144        public DocumentInfo[] newArray(int size) {
145            return new DocumentInfo[size];
146        }
147    };
148
149    public static DocumentInfo fromDirectoryCursor(Cursor cursor) {
150        final String authority = getCursorString(cursor, RootCursorWrapper.COLUMN_AUTHORITY);
151        return fromCursor(cursor, authority);
152    }
153
154    public static DocumentInfo fromCursor(Cursor cursor, String authority) {
155        final DocumentInfo info = new DocumentInfo();
156        info.updateFromCursor(cursor, authority);
157        return info;
158    }
159
160    public void updateFromCursor(Cursor cursor, String authority) {
161        this.authority = authority;
162        this.documentId = getCursorString(cursor, Document.COLUMN_DOCUMENT_ID);
163        this.mimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
164        this.documentId = getCursorString(cursor, Document.COLUMN_DOCUMENT_ID);
165        this.mimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
166        this.displayName = getCursorString(cursor, Document.COLUMN_DISPLAY_NAME);
167        this.lastModified = getCursorLong(cursor, Document.COLUMN_LAST_MODIFIED);
168        this.flags = getCursorInt(cursor, Document.COLUMN_FLAGS);
169        this.summary = getCursorString(cursor, Document.COLUMN_SUMMARY);
170        this.size = getCursorLong(cursor, Document.COLUMN_SIZE);
171        this.icon = getCursorInt(cursor, Document.COLUMN_ICON);
172        this.deriveFields();
173    }
174
175    public static DocumentInfo fromUri(ContentResolver resolver, Uri uri)
176            throws FileNotFoundException {
177        final DocumentInfo info = new DocumentInfo();
178        info.updateFromUri(resolver, uri);
179        return info;
180    }
181
182    /**
183     * Update a possibly stale restored document against a live
184     * {@link DocumentsProvider}.
185     */
186    public void updateSelf(ContentResolver resolver) throws FileNotFoundException {
187        updateFromUri(resolver, derivedUri);
188    }
189
190    public void updateFromUri(ContentResolver resolver, Uri uri) throws FileNotFoundException {
191        ContentProviderClient client = null;
192        Cursor cursor = null;
193        try {
194            client = DocumentsApplication.acquireUnstableProviderOrThrow(
195                    resolver, uri.getAuthority());
196            cursor = client.query(uri, null, null, null, null);
197            if (!cursor.moveToFirst()) {
198                throw new FileNotFoundException("Missing details for " + uri);
199            }
200            updateFromCursor(cursor, uri.getAuthority());
201        } catch (Throwable t) {
202            throw asFileNotFoundException(t);
203        } finally {
204            IoUtils.closeQuietly(cursor);
205            ContentProviderClient.releaseQuietly(client);
206        }
207    }
208
209    private void deriveFields() {
210        derivedUri = DocumentsContract.buildDocumentUri(authority, documentId);
211    }
212
213    @Override
214    public String toString() {
215        return "Document{docId=" + documentId + ", name=" + displayName + "}";
216    }
217
218    public boolean isCreateSupported() {
219        return (flags & Document.FLAG_DIR_SUPPORTS_CREATE) != 0;
220    }
221
222    public boolean isThumbnailSupported() {
223        return (flags & Document.FLAG_SUPPORTS_THUMBNAIL) != 0;
224    }
225
226    public boolean isDirectory() {
227        return Document.MIME_TYPE_DIR.equals(mimeType);
228    }
229
230    public boolean isGridPreferred() {
231        return (flags & Document.FLAG_DIR_PREFERS_GRID) != 0;
232    }
233
234    public boolean isDeleteSupported() {
235        return (flags & Document.FLAG_SUPPORTS_DELETE) != 0;
236    }
237
238    public boolean isGridTitlesHidden() {
239        return (flags & Document.FLAG_DIR_HIDE_GRID_TITLES) != 0;
240    }
241
242    public static String getCursorString(Cursor cursor, String columnName) {
243        final int index = cursor.getColumnIndex(columnName);
244        return (index != -1) ? cursor.getString(index) : null;
245    }
246
247    /**
248     * Missing or null values are returned as -1.
249     */
250    public static long getCursorLong(Cursor cursor, String columnName) {
251        final int index = cursor.getColumnIndex(columnName);
252        if (index == -1) return -1;
253        final String value = cursor.getString(index);
254        if (value == null) return -1;
255        try {
256            return Long.parseLong(value);
257        } catch (NumberFormatException e) {
258            return -1;
259        }
260    }
261
262    /**
263     * Missing or null values are returned as 0.
264     */
265    public static int getCursorInt(Cursor cursor, String columnName) {
266        final int index = cursor.getColumnIndex(columnName);
267        return (index != -1) ? cursor.getInt(index) : 0;
268    }
269
270    public static FileNotFoundException asFileNotFoundException(Throwable t)
271            throws FileNotFoundException {
272        if (t instanceof FileNotFoundException) {
273            throw (FileNotFoundException) t;
274        }
275        final FileNotFoundException fnfe = new FileNotFoundException(t.getMessage());
276        fnfe.initCause(t);
277        throw fnfe;
278    }
279
280    /**
281     * String prefix used to indicate the document is a directory.
282     */
283    public static final char DIR_PREFIX = '\001';
284
285    /**
286     * Compare two strings against each other using system default collator in a
287     * case-insensitive mode. Clusters strings prefixed with {@link #DIR_PREFIX}
288     * before other items.
289     */
290    public static int compareToIgnoreCaseNullable(String lhs, String rhs) {
291        final boolean leftEmpty = TextUtils.isEmpty(lhs);
292        final boolean rightEmpty = TextUtils.isEmpty(rhs);
293
294        if (leftEmpty && rightEmpty) return 0;
295        if (leftEmpty) return -1;
296        if (rightEmpty) return 1;
297
298        final boolean leftDir = (lhs.charAt(0) == DIR_PREFIX);
299        final boolean rightDir = (rhs.charAt(0) == DIR_PREFIX);
300
301        if (leftDir && !rightDir) return -1;
302        if (rightDir && !leftDir) return 1;
303
304        return sCollator.compare(lhs, rhs);
305    }
306}
307