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