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