RootInfo.java revision a5588b65d55bc1b8b5ba943f8b660db26a7eac5c
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 static com.android.documentsui.Shared.DEBUG;
20import static com.android.documentsui.Shared.compareToIgnoreCaseNullable;
21import static com.android.documentsui.model.DocumentInfo.getCursorInt;
22import static com.android.documentsui.model.DocumentInfo.getCursorLong;
23import static com.android.documentsui.model.DocumentInfo.getCursorString;
24
25import android.annotation.IntDef;
26import android.annotation.Nullable;
27import android.content.Context;
28import android.database.Cursor;
29import android.graphics.drawable.Drawable;
30import android.net.Uri;
31import android.os.Parcel;
32import android.os.Parcelable;
33import android.provider.DocumentsContract;
34import android.provider.DocumentsContract.Root;
35import android.text.TextUtils;
36import android.util.Log;
37
38import com.android.documentsui.IconUtils;
39import com.android.documentsui.R;
40
41import java.io.DataInputStream;
42import java.io.DataOutputStream;
43import java.io.FileNotFoundException;
44import java.io.IOException;
45import java.lang.annotation.Retention;
46import java.lang.annotation.RetentionPolicy;
47import java.net.ProtocolException;
48import java.util.Objects;
49
50/**
51 * Representation of a {@link Root}.
52 */
53public class RootInfo implements Durable, Parcelable, Comparable<RootInfo> {
54
55    private static final String TAG = "RootInfo";
56    private static final int VERSION_INIT = 1;
57    private static final int VERSION_DROP_TYPE = 2;
58
59    // The values of these constants determine the sort order of various roots in the RootsFragment.
60    @IntDef(flag = false, value = {
61            TYPE_IMAGES,
62            TYPE_VIDEO,
63            TYPE_AUDIO,
64            TYPE_RECENTS,
65            TYPE_DOWNLOADS,
66            TYPE_LOCAL,
67            TYPE_MTP,
68            TYPE_SD,
69            TYPE_USB,
70            TYPE_OTHER
71    })
72    @Retention(RetentionPolicy.SOURCE)
73    public @interface RootType {}
74    public static final int TYPE_IMAGES = 1;
75    public static final int TYPE_VIDEO = 2;
76    public static final int TYPE_AUDIO = 3;
77    public static final int TYPE_RECENTS = 4;
78    public static final int TYPE_DOWNLOADS = 5;
79    public static final int TYPE_LOCAL = 6;
80    public static final int TYPE_MTP = 7;
81    public static final int TYPE_SD = 8;
82    public static final int TYPE_USB = 9;
83    public static final int TYPE_OTHER = 10;
84
85    public String authority;
86    public String rootId;
87    public int flags;
88    public int icon;
89    public String title;
90    public String summary;
91    public String documentId;
92    public long availableBytes;
93    public String mimeTypes;
94
95    /** Derived fields that aren't persisted */
96    public String[] derivedMimeTypes;
97    public int derivedIcon;
98    public @RootType int derivedType;
99    // Currently, we are not persisting this and we should be asking Provider whether a Root
100    // is in the process of eject. Provider does not have this available yet.
101    public transient boolean ejecting;
102
103    public RootInfo() {
104        reset();
105    }
106
107    @Override
108    public void reset() {
109        authority = null;
110        rootId = null;
111        flags = 0;
112        icon = 0;
113        title = null;
114        summary = null;
115        documentId = null;
116        availableBytes = -1;
117        mimeTypes = null;
118        ejecting = false;
119
120        derivedMimeTypes = null;
121        derivedIcon = 0;
122        derivedType = 0;
123    }
124
125    @Override
126    public void read(DataInputStream in) throws IOException {
127        final int version = in.readInt();
128        switch (version) {
129            case VERSION_DROP_TYPE:
130                authority = DurableUtils.readNullableString(in);
131                rootId = DurableUtils.readNullableString(in);
132                flags = in.readInt();
133                icon = in.readInt();
134                title = DurableUtils.readNullableString(in);
135                summary = DurableUtils.readNullableString(in);
136                documentId = DurableUtils.readNullableString(in);
137                availableBytes = in.readLong();
138                mimeTypes = DurableUtils.readNullableString(in);
139                deriveFields();
140                break;
141            default:
142                throw new ProtocolException("Unknown version " + version);
143        }
144    }
145
146    @Override
147    public void write(DataOutputStream out) throws IOException {
148        out.writeInt(VERSION_DROP_TYPE);
149        DurableUtils.writeNullableString(out, authority);
150        DurableUtils.writeNullableString(out, rootId);
151        out.writeInt(flags);
152        out.writeInt(icon);
153        DurableUtils.writeNullableString(out, title);
154        DurableUtils.writeNullableString(out, summary);
155        DurableUtils.writeNullableString(out, documentId);
156        out.writeLong(availableBytes);
157        DurableUtils.writeNullableString(out, mimeTypes);
158    }
159
160    @Override
161    public int describeContents() {
162        return 0;
163    }
164
165    @Override
166    public void writeToParcel(Parcel dest, int flags) {
167        DurableUtils.writeToParcel(dest, this);
168    }
169
170    public static final Creator<RootInfo> CREATOR = new Creator<RootInfo>() {
171        @Override
172        public RootInfo createFromParcel(Parcel in) {
173            final RootInfo root = new RootInfo();
174            DurableUtils.readFromParcel(in, root);
175            return root;
176        }
177
178        @Override
179        public RootInfo[] newArray(int size) {
180            return new RootInfo[size];
181        }
182    };
183
184    public static RootInfo fromRootsCursor(String authority, Cursor cursor) {
185        final RootInfo root = new RootInfo();
186        root.authority = authority;
187        root.rootId = getCursorString(cursor, Root.COLUMN_ROOT_ID);
188        root.flags = getCursorInt(cursor, Root.COLUMN_FLAGS);
189        root.icon = getCursorInt(cursor, Root.COLUMN_ICON);
190        root.title = getCursorString(cursor, Root.COLUMN_TITLE);
191        root.summary = getCursorString(cursor, Root.COLUMN_SUMMARY);
192        root.documentId = getCursorString(cursor, Root.COLUMN_DOCUMENT_ID);
193        root.availableBytes = getCursorLong(cursor, Root.COLUMN_AVAILABLE_BYTES);
194        root.mimeTypes = getCursorString(cursor, Root.COLUMN_MIME_TYPES);
195        root.deriveFields();
196        return root;
197    }
198
199    private void deriveFields() {
200        derivedMimeTypes = (mimeTypes != null) ? mimeTypes.split("\n") : null;
201
202        if (isHome()) {
203            derivedType = TYPE_LOCAL;
204            derivedIcon = R.drawable.ic_root_documents;
205        } else if (isMtp()) {
206            derivedType = TYPE_MTP;
207            derivedIcon = R.drawable.ic_usb_storage;
208        } else if (isUsb()) {
209            derivedType = TYPE_USB;
210            derivedIcon = R.drawable.ic_usb_storage;
211        } else if (isSd()) {
212            derivedType = TYPE_SD;
213            derivedIcon = R.drawable.ic_sd_storage;
214        } else if (isExternalStorage()) {
215            derivedType = TYPE_LOCAL;
216            derivedIcon = R.drawable.ic_root_smartphone;
217        } else if (isDownloads()) {
218            derivedType = TYPE_DOWNLOADS;
219            derivedIcon = R.drawable.ic_root_download;
220        } else if (isImages()) {
221            derivedType = TYPE_IMAGES;
222            derivedIcon = com.android.internal.R.drawable.ic_doc_image;
223        } else if (isVideos()) {
224            derivedType = TYPE_VIDEO;
225            derivedIcon = com.android.internal.R.drawable.ic_doc_video;
226        } else if (isAudio()) {
227            derivedType = TYPE_AUDIO;
228            derivedIcon = com.android.internal.R.drawable.ic_doc_audio;
229        } else if (isRecents()) {
230            derivedType = TYPE_RECENTS;
231        } else {
232            derivedType = TYPE_OTHER;
233        }
234
235        if (DEBUG) Log.d(TAG, "Finished deriving fields: " + this);
236    }
237
238    public Uri getUri() {
239        return DocumentsContract.buildRootUri(authority, rootId);
240    }
241
242    public boolean isRecents() {
243        return authority == null && rootId == null;
244    }
245
246    public boolean isHome() {
247        // Note that "home" is the expected root id for the auto-created
248        // user home directory on external storage. The "home" value should
249        // match ExternalStorageProvider.ROOT_ID_HOME.
250        return isExternalStorage() && "home".equals(rootId);
251    }
252
253    public boolean isExternalStorage() {
254        return "com.android.externalstorage.documents".equals(authority);
255    }
256
257    public boolean isDownloads() {
258        return "com.android.providers.downloads.documents".equals(authority);
259    }
260
261    public boolean isImages() {
262        return "com.android.providers.media.documents".equals(authority)
263                && "images_root".equals(rootId);
264    }
265
266    public boolean isVideos() {
267        return "com.android.providers.media.documents".equals(authority)
268                && "videos_root".equals(rootId);
269    }
270
271    public boolean isAudio() {
272        return "com.android.providers.media.documents".equals(authority)
273                && "audio_root".equals(rootId);
274    }
275
276    public boolean isMtp() {
277        return "com.android.mtp.documents".equals(authority);
278    }
279
280    public boolean isLibrary() {
281        return derivedType == TYPE_IMAGES
282                || derivedType == TYPE_VIDEO
283                || derivedType == TYPE_AUDIO
284                || derivedType == TYPE_RECENTS;
285    }
286
287    public boolean hasSettings() {
288        return (flags & Root.FLAG_HAS_SETTINGS) != 0;
289    }
290
291    public boolean supportsChildren() {
292        return (flags & Root.FLAG_SUPPORTS_IS_CHILD) != 0;
293    }
294
295    public boolean supportsCreate() {
296        return (flags & Root.FLAG_SUPPORTS_CREATE) != 0;
297    }
298
299    public boolean supportsRecents() {
300        return (flags & Root.FLAG_SUPPORTS_RECENTS) != 0;
301    }
302
303    public boolean supportsSearch() {
304        return (flags & Root.FLAG_SUPPORTS_SEARCH) != 0;
305    }
306
307    public boolean supportsEject() {
308        return (flags & Root.FLAG_SUPPORTS_EJECT) != 0;
309    }
310
311    public boolean isAdvanced() {
312        return (flags & Root.FLAG_ADVANCED) != 0;
313    }
314
315    public boolean isLocalOnly() {
316        return (flags & Root.FLAG_LOCAL_ONLY) != 0;
317    }
318
319    public boolean isEmpty() {
320        return (flags & Root.FLAG_EMPTY) != 0;
321    }
322
323    public boolean isSd() {
324        return (flags & Root.FLAG_REMOVABLE_SD) != 0;
325    }
326
327    public boolean isUsb() {
328        return (flags & Root.FLAG_REMOVABLE_USB) != 0;
329    }
330
331    public Drawable loadIcon(Context context) {
332        if (derivedIcon != 0) {
333            return context.getDrawable(derivedIcon);
334        } else {
335            return IconUtils.loadPackageIcon(context, authority, icon);
336        }
337    }
338
339    public Drawable loadDrawerIcon(Context context) {
340        if (derivedIcon != 0) {
341            return IconUtils.applyTintColor(context, derivedIcon, R.color.item_root_icon);
342        } else {
343            return IconUtils.loadPackageIcon(context, authority, icon);
344        }
345    }
346
347    public Drawable loadEjectIcon(Context context) {
348        return IconUtils.applyTintColor(context, R.drawable.ic_eject, R.color.item_eject_icon);
349    }
350
351    /**
352     * Gets the {@link DocumentInfo} of the root folder of this root.
353     */
354    public @Nullable DocumentInfo getRootDocumentBlocking(Context context) {
355        try {
356            final Uri uri = DocumentsContract.buildDocumentUri(authority, documentId);
357            return DocumentInfo.fromUri(context.getContentResolver(), uri);
358        } catch (FileNotFoundException e) {
359            Log.w(TAG, "Failed to find root", e);
360            return null;
361        }
362    }
363
364    @Override
365    public boolean equals(Object o) {
366        if (o == null) {
367            return false;
368        }
369
370        if (this == o) {
371            return true;
372        }
373
374        if (o instanceof RootInfo) {
375            RootInfo other = (RootInfo) o;
376            return Objects.equals(authority, other.authority)
377                    && Objects.equals(rootId, other.rootId);
378        }
379
380        return false;
381    }
382
383    @Override
384    public int hashCode() {
385        return Objects.hash(authority, rootId);
386    }
387
388    @Override
389    public int compareTo(RootInfo other) {
390        // Sort by root type, then title, then summary.
391        int score = derivedType - other.derivedType;
392        if (score != 0) {
393            return score;
394        }
395
396        score = compareToIgnoreCaseNullable(title, other.title);
397        if (score != 0) {
398            return score;
399        }
400
401        return compareToIgnoreCaseNullable(summary, other.summary);
402    }
403
404    @Override
405    public String toString() {
406        return "Root{"
407                + "authority=" + authority
408                + ", rootId=" + rootId
409                + ", title=" + title
410                + ", isUsb=" + isUsb()
411                + ", isSd=" + isSd()
412                + ", isMtp=" + isMtp()
413                + "}";
414    }
415
416    public String getDirectoryString() {
417        return !TextUtils.isEmpty(summary) ? summary : title;
418    }
419}
420