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