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