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 
17 package com.android.documentsui.model;
18 
19 import static com.android.documentsui.Shared.DEBUG;
20 import static com.android.documentsui.Shared.compareToIgnoreCaseNullable;
21 import static com.android.documentsui.model.DocumentInfo.getCursorInt;
22 import static com.android.documentsui.model.DocumentInfo.getCursorLong;
23 import static com.android.documentsui.model.DocumentInfo.getCursorString;
24 
25 import android.annotation.IntDef;
26 import android.content.Context;
27 import android.database.Cursor;
28 import android.graphics.drawable.Drawable;
29 import android.net.Uri;
30 import android.os.Parcel;
31 import android.os.Parcelable;
32 import android.provider.DocumentsContract;
33 import android.provider.DocumentsContract.Root;
34 import android.text.TextUtils;
35 import android.util.Log;
36 
37 import com.android.documentsui.IconUtils;
38 import com.android.documentsui.R;
39 
40 import java.io.DataInputStream;
41 import java.io.DataOutputStream;
42 import java.io.IOException;
43 import java.lang.annotation.Retention;
44 import java.lang.annotation.RetentionPolicy;
45 import java.net.ProtocolException;
46 import java.util.Objects;
47 
48 /**
49  * Representation of a {@link Root}.
50  */
51 public 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 
RootInfo()98     public RootInfo() {
99         reset();
100     }
101 
102     @Override
reset()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
read(DataInputStream in)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
write(DataOutputStream out)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
describeContents()155     public int describeContents() {
156         return 0;
157     }
158 
159     @Override
writeToParcel(Parcel dest, int flags)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 
fromRootsCursor(String authority, Cursor cursor)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 
deriveFields()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 = R.drawable.ic_doc_image;
217         } else if (isVideos()) {
218             derivedType = TYPE_VIDEO;
219             derivedIcon = R.drawable.ic_doc_video;
220         } else if (isAudio()) {
221             derivedType = TYPE_AUDIO;
222             derivedIcon = 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 
getUri()232     public Uri getUri() {
233         return DocumentsContract.buildRootUri(authority, rootId);
234     }
235 
isRecents()236     public boolean isRecents() {
237         return authority == null && rootId == null;
238     }
239 
isHome()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 
isExternalStorage()247     public boolean isExternalStorage() {
248         return "com.android.externalstorage.documents".equals(authority);
249     }
250 
isDownloads()251     public boolean isDownloads() {
252         return "com.android.providers.downloads.documents".equals(authority);
253     }
254 
isImages()255     public boolean isImages() {
256         return "com.android.providers.media.documents".equals(authority)
257                 && "images_root".equals(rootId);
258     }
259 
isVideos()260     public boolean isVideos() {
261         return "com.android.providers.media.documents".equals(authority)
262                 && "videos_root".equals(rootId);
263     }
264 
isAudio()265     public boolean isAudio() {
266         return "com.android.providers.media.documents".equals(authority)
267                 && "audio_root".equals(rootId);
268     }
269 
isMtp()270     public boolean isMtp() {
271         return "com.android.mtp.documents".equals(authority);
272     }
273 
isLibrary()274     public boolean isLibrary() {
275         return derivedType == TYPE_IMAGES
276                 || derivedType == TYPE_VIDEO
277                 || derivedType == TYPE_AUDIO
278                 || derivedType == TYPE_RECENTS;
279     }
280 
hasSettings()281     public boolean hasSettings() {
282         return (flags & Root.FLAG_HAS_SETTINGS) != 0;
283     }
284 
supportsChildren()285     public boolean supportsChildren() {
286         return (flags & Root.FLAG_SUPPORTS_IS_CHILD) != 0;
287     }
288 
supportsCreate()289     public boolean supportsCreate() {
290         return (flags & Root.FLAG_SUPPORTS_CREATE) != 0;
291     }
292 
supportsRecents()293     public boolean supportsRecents() {
294         return (flags & Root.FLAG_SUPPORTS_RECENTS) != 0;
295     }
296 
supportsSearch()297     public boolean supportsSearch() {
298         return (flags & Root.FLAG_SUPPORTS_SEARCH) != 0;
299     }
300 
isAdvanced()301     public boolean isAdvanced() {
302         return (flags & Root.FLAG_ADVANCED) != 0;
303     }
304 
isLocalOnly()305     public boolean isLocalOnly() {
306         return (flags & Root.FLAG_LOCAL_ONLY) != 0;
307     }
308 
isEmpty()309     public boolean isEmpty() {
310         return (flags & Root.FLAG_EMPTY) != 0;
311     }
312 
isSd()313     public boolean isSd() {
314         return (flags & Root.FLAG_REMOVABLE_SD) != 0;
315     }
316 
isUsb()317     public boolean isUsb() {
318         return (flags & Root.FLAG_REMOVABLE_USB) != 0;
319     }
320 
loadIcon(Context context)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 
loadDrawerIcon(Context context)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
equals(Object o)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
hashCode()357     public int hashCode() {
358         return Objects.hash(authority, rootId);
359     }
360 
361     @Override
compareTo(RootInfo other)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
toString()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 
getDirectoryString()389     public String getDirectoryString() {
390         return !TextUtils.isEmpty(summary) ? summary : title;
391     }
392 }
393