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 com.android.documentsui.base.DocumentInfo.getCursorInt;
20 import static com.android.documentsui.base.DocumentInfo.getCursorLong;
21 import static com.android.documentsui.base.DocumentInfo.getCursorString;
22 import static com.android.documentsui.base.Shared.VERBOSE;
23 import static com.android.documentsui.base.Shared.compareToIgnoreCaseNullable;
24 
25 import android.annotation.IntDef;
26 import android.annotation.Nullable;
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 com.android.documentsui.DocumentsAccess;
39 import com.android.documentsui.IconUtils;
40 import com.android.documentsui.R;
41 
42 import java.io.DataInputStream;
43 import java.io.DataOutputStream;
44 import java.io.FileNotFoundException;
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 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 
RootInfo()104     public RootInfo() {
105         reset();
106     }
107 
108     @Override
reset()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
read(DataInputStream in)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
write(DataOutputStream out)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
describeContents()162     public int describeContents() {
163         return 0;
164     }
165 
166     @Override
writeToParcel(Parcel dest, int flags)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 
fromRootsCursor(String authority, Cursor cursor)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 
deriveFields()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 
getUri()239     public Uri getUri() {
240         return DocumentsContract.buildRootUri(authority, rootId);
241     }
242 
isRecents()243     public boolean isRecents() {
244         return authority == null && rootId == null;
245     }
246 
isHome()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 
isExternalStorage()254     public boolean isExternalStorage() {
255         return Providers.AUTHORITY_STORAGE.equals(authority);
256     }
257 
isDownloads()258     public boolean isDownloads() {
259         return Providers.AUTHORITY_DOWNLOADS.equals(authority);
260     }
261 
isImages()262     public boolean isImages() {
263         return Providers.AUTHORITY_MEDIA.equals(authority)
264                 && Providers.ROOT_ID_IMAGES.equals(rootId);
265     }
266 
isVideos()267     public boolean isVideos() {
268         return Providers.AUTHORITY_MEDIA.equals(authority)
269                 && Providers.ROOT_ID_VIDEOS.equals(rootId);
270     }
271 
isAudio()272     public boolean isAudio() {
273         return Providers.AUTHORITY_MEDIA.equals(authority)
274                 && Providers.ROOT_ID_AUDIO.equals(rootId);
275     }
276 
isMtp()277     public boolean isMtp() {
278         return Providers.AUTHORITY_MTP.equals(authority);
279     }
280 
isLibrary()281     public boolean isLibrary() {
282         return derivedType == TYPE_IMAGES
283                 || derivedType == TYPE_VIDEO
284                 || derivedType == TYPE_AUDIO
285                 || derivedType == TYPE_RECENTS;
286     }
287 
hasSettings()288     public boolean hasSettings() {
289         return (flags & Root.FLAG_HAS_SETTINGS) != 0;
290     }
291 
supportsChildren()292     public boolean supportsChildren() {
293         return (flags & Root.FLAG_SUPPORTS_IS_CHILD) != 0;
294     }
295 
supportsCreate()296     public boolean supportsCreate() {
297         return (flags & Root.FLAG_SUPPORTS_CREATE) != 0;
298     }
299 
supportsRecents()300     public boolean supportsRecents() {
301         return (flags & Root.FLAG_SUPPORTS_RECENTS) != 0;
302     }
303 
supportsSearch()304     public boolean supportsSearch() {
305         return (flags & Root.FLAG_SUPPORTS_SEARCH) != 0;
306     }
307 
supportsEject()308     public boolean supportsEject() {
309         return (flags & Root.FLAG_SUPPORTS_EJECT) != 0;
310     }
311 
isAdvanced()312     public boolean isAdvanced() {
313         return (flags & Root.FLAG_ADVANCED) != 0;
314     }
315 
isLocalOnly()316     public boolean isLocalOnly() {
317         return (flags & Root.FLAG_LOCAL_ONLY) != 0;
318     }
319 
isEmpty()320     public boolean isEmpty() {
321         return (flags & Root.FLAG_EMPTY) != 0;
322     }
323 
isSd()324     public boolean isSd() {
325         return (flags & Root.FLAG_REMOVABLE_SD) != 0;
326     }
327 
isUsb()328     public boolean isUsb() {
329         return (flags & Root.FLAG_REMOVABLE_USB) != 0;
330     }
331 
loadIcon(Context context)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 
loadDrawerIcon(Context context)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 
loadEjectIcon(Context context)348     public Drawable loadEjectIcon(Context context) {
349         return IconUtils.applyTintColor(context, R.drawable.ic_eject, R.color.item_eject_icon);
350     }
351 
352     @Override
equals(Object o)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
hashCode()372     public int hashCode() {
373         return Objects.hash(authority, rootId);
374     }
375 
376     @Override
compareTo(RootInfo other)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
toString()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 
toDebugString()405     public String toDebugString() {
406         return (TextUtils.isEmpty(summary))
407                 ? "\"" + title + "\" @ " + getUri()
408                 : "\"" + title + " (" + summary + ")\" @ " + getUri();
409     }
410 
getDirectoryString()411     public String getDirectoryString() {
412         return !TextUtils.isEmpty(summary) ? summary : title;
413     }
414 }
415