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