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.base;
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.archives.ArchivesProvider;
32import com.android.documentsui.roots.RootCursorWrapper;
33
34import libcore.io.IoUtils;
35
36import java.io.DataInputStream;
37import java.io.DataOutputStream;
38import java.io.FileNotFoundException;
39import java.io.IOException;
40import java.net.ProtocolException;
41import java.util.Arrays;
42import java.util.Objects;
43import java.util.Set;
44
45import javax.annotation.Nullable;
46
47/**
48 * Representation of a {@link Document}.
49 */
50public 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
67    public DocumentInfo() {
68        reset();
69    }
70
71    @Override
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
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
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
123    public int describeContents() {
124        return 0;
125    }
126
127    @Override
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
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
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
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
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     */
183    public void updateSelf(ContentResolver resolver) throws FileNotFoundException {
184        updateFromUri(resolver, derivedUri);
185    }
186
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
207    void deriveFields() {
208        derivedUri = DocumentsContract.buildDocumentUri(authority, documentId);
209    }
210
211    @Override
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
230    public boolean isCreateSupported() {
231        return (flags & Document.FLAG_DIR_SUPPORTS_CREATE) != 0;
232    }
233
234    public boolean isDirectory() {
235        return Document.MIME_TYPE_DIR.equals(mimeType);
236    }
237
238    public boolean isWriteSupported() {
239        return (flags & Document.FLAG_SUPPORTS_WRITE) != 0;
240    }
241
242    public boolean isDeleteSupported() {
243        return (flags & Document.FLAG_SUPPORTS_DELETE) != 0;
244    }
245
246    public boolean isRemoveSupported() {
247        return (flags & Document.FLAG_SUPPORTS_REMOVE) != 0;
248    }
249
250    public boolean isMoveSupported() {
251        return (flags & Document.FLAG_SUPPORTS_MOVE) != 0;
252    }
253
254    public boolean isRenameSupported() {
255        return (flags & Document.FLAG_SUPPORTS_RENAME) != 0;
256    }
257
258    public boolean isThumbnailSupported() {
259        return (flags & Document.FLAG_SUPPORTS_THUMBNAIL) != 0;
260    }
261
262    public boolean isWeblinkSupported() {
263        return (flags & Document.FLAG_WEB_LINKABLE) != 0;
264    }
265
266    public boolean isArchive() {
267        return ArchivesProvider.isSupportedArchiveType(mimeType);
268    }
269
270    public boolean isInArchive() {
271        return ArchivesProvider.AUTHORITY.equals(authority);
272    }
273
274    public boolean isPartial() {
275        return (flags & Document.FLAG_PARTIAL) != 0;
276    }
277
278    // Containers are documents which can be opened in DocumentsUI as folders.
279    public boolean isContainer() {
280        return isDirectory() || (isArchive() && !isInArchive() && !isPartial());
281    }
282
283    public boolean isVirtual() {
284        return (flags & Document.FLAG_VIRTUAL_DOCUMENT) != 0;
285    }
286
287    public boolean isSettingsSupported() {
288        return (flags & Document.FLAG_SUPPORTS_SETTINGS) != 0;
289    }
290
291    public boolean prefersSortByLastModified() {
292        return (flags & Document.FLAG_DIR_PREFERS_LAST_MODIFIED) != 0;
293    }
294
295    @Override
296    public int hashCode() {
297        return derivedUri.hashCode() + mimeType.hashCode();
298    }
299
300    @Override
301    public boolean equals(Object o) {
302        if (o == null) {
303            return false;
304        }
305
306        if (this == o) {
307            return true;
308        }
309
310        if (o instanceof DocumentInfo) {
311            DocumentInfo other = (DocumentInfo) o;
312            // Uri + mime type should be totally unique.
313            return Objects.equals(derivedUri, other.derivedUri)
314                    && Objects.equals(mimeType, other.mimeType);
315        }
316
317        return false;
318    }
319
320    public static String getCursorString(Cursor cursor, String columnName) {
321        final int index = cursor.getColumnIndex(columnName);
322        return (index != -1) ? cursor.getString(index) : null;
323    }
324
325    /**
326     * Missing or null values are returned as -1.
327     */
328    public static long getCursorLong(Cursor cursor, String columnName) {
329        final int index = cursor.getColumnIndex(columnName);
330        if (index == -1) return -1;
331        final String value = cursor.getString(index);
332        if (value == null) return -1;
333        try {
334            return Long.parseLong(value);
335        } catch (NumberFormatException e) {
336            return -1;
337        }
338    }
339
340    /**
341     * Missing or null values are returned as 0.
342     */
343    public static int getCursorInt(Cursor cursor, String columnName) {
344        final int index = cursor.getColumnIndex(columnName);
345        return (index != -1) ? cursor.getInt(index) : 0;
346    }
347
348    public static FileNotFoundException asFileNotFoundException(Throwable t)
349            throws FileNotFoundException {
350        if (t instanceof FileNotFoundException) {
351            throw (FileNotFoundException) t;
352        }
353        final FileNotFoundException fnfe = new FileNotFoundException(t.getMessage());
354        fnfe.initCause(t);
355        throw fnfe;
356    }
357
358    public static Uri getUri(Cursor cursor) {
359        return DocumentsContract.buildDocumentUri(
360            getCursorString(cursor, RootCursorWrapper.COLUMN_AUTHORITY),
361            getCursorString(cursor, Document.COLUMN_DOCUMENT_ID));
362    }
363
364    public static void addMimeTypes(ContentResolver resolver, Uri uri, Set<String> mimeTypes) {
365        assert(uri != null);
366        if ("content".equals(uri.getScheme())) {
367            mimeTypes.add(resolver.getType(uri));
368            final String[] streamTypes = resolver.getStreamTypes(uri, "*/*");
369            if (streamTypes != null) {
370                mimeTypes.addAll(Arrays.asList(streamTypes));
371            }
372        }
373    }
374
375    public static String debugString(@Nullable DocumentInfo doc) {
376        if (doc == null) {
377            return "<null DocumentInfo>";
378        }
379
380        if (doc.derivedUri == null) {
381            doc.deriveFields();
382            assert(doc.derivedUri != null);
383        }
384        return doc.derivedUri.toString();
385    }
386}
387