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