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