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.SharedMinimal.DEBUG; 20 21 import android.content.ContentProviderClient; 22 import android.content.ContentResolver; 23 import android.database.Cursor; 24 import android.net.Uri; 25 import android.os.FileUtils; 26 import android.os.Parcel; 27 import android.os.Parcelable; 28 import android.provider.DocumentsContract; 29 import android.provider.DocumentsContract.Document; 30 import android.provider.DocumentsProvider; 31 import android.util.Log; 32 33 import androidx.annotation.VisibleForTesting; 34 35 import com.android.documentsui.DocumentsApplication; 36 import com.android.documentsui.archives.ArchivesProvider; 37 import com.android.documentsui.roots.RootCursorWrapper; 38 import com.android.documentsui.util.VersionUtils; 39 40 import java.io.DataInputStream; 41 import java.io.DataOutputStream; 42 import java.io.FileNotFoundException; 43 import java.io.IOException; 44 import java.net.ProtocolException; 45 import java.util.Arrays; 46 import java.util.Objects; 47 import java.util.Set; 48 49 import javax.annotation.Nullable; 50 51 /** 52 * Representation of a {@link Document}. 53 */ 54 public class DocumentInfo implements Durable, Parcelable { 55 private static final String TAG = "DocumentInfo"; 56 private static final int VERSION_INIT = 1; 57 private static final int VERSION_SPLIT_URI = 2; 58 private static final int VERSION_USER_ID = 3; 59 60 public UserId userId; 61 public String authority; 62 public String documentId; 63 public String mimeType; 64 public String displayName; 65 public long lastModified; 66 public int flags; 67 public String summary; 68 public long size; 69 public int icon; 70 71 /** Derived fields that aren't persisted */ 72 public Uri derivedUri; 73 DocumentInfo()74 public DocumentInfo() { 75 reset(); 76 } 77 78 @Override reset()79 public void reset() { 80 userId = UserId.UNSPECIFIED_USER; 81 authority = null; 82 documentId = null; 83 mimeType = null; 84 displayName = null; 85 lastModified = -1; 86 flags = 0; 87 summary = null; 88 size = -1; 89 icon = 0; 90 derivedUri = null; 91 } 92 93 @Override read(DataInputStream in)94 public void read(DataInputStream in) throws IOException { 95 final int version = in.readInt(); 96 switch (version) { 97 case VERSION_USER_ID: 98 userId = UserId.read(in); 99 case VERSION_SPLIT_URI: 100 if (version < VERSION_USER_ID) { 101 userId = UserId.CURRENT_USER; 102 } 103 authority = DurableUtils.readNullableString(in); 104 documentId = DurableUtils.readNullableString(in); 105 mimeType = DurableUtils.readNullableString(in); 106 displayName = DurableUtils.readNullableString(in); 107 lastModified = in.readLong(); 108 flags = in.readInt(); 109 summary = DurableUtils.readNullableString(in); 110 size = in.readLong(); 111 icon = in.readInt(); 112 deriveFields(); 113 break; 114 case VERSION_INIT: 115 throw new ProtocolException("Ignored upgrade"); 116 default: 117 throw new ProtocolException("Unknown version " + version); 118 } 119 } 120 121 @Override write(DataOutputStream out)122 public void write(DataOutputStream out) throws IOException { 123 out.writeInt(VERSION_USER_ID); 124 UserId.write(out, userId); 125 DurableUtils.writeNullableString(out, authority); 126 DurableUtils.writeNullableString(out, documentId); 127 DurableUtils.writeNullableString(out, mimeType); 128 DurableUtils.writeNullableString(out, displayName); 129 out.writeLong(lastModified); 130 out.writeInt(flags); 131 DurableUtils.writeNullableString(out, summary); 132 out.writeLong(size); 133 out.writeInt(icon); 134 } 135 136 @Override describeContents()137 public int describeContents() { 138 return 0; 139 } 140 141 @Override writeToParcel(Parcel dest, int flags)142 public void writeToParcel(Parcel dest, int flags) { 143 DurableUtils.writeToParcel(dest, this); 144 } 145 146 public static final Creator<DocumentInfo> CREATOR = new Creator<DocumentInfo>() { 147 @Override 148 public DocumentInfo createFromParcel(Parcel in) { 149 final DocumentInfo doc = new DocumentInfo(); 150 DurableUtils.readFromParcel(in, doc); 151 return doc; 152 } 153 154 @Override 155 public DocumentInfo[] newArray(int size) { 156 return new DocumentInfo[size]; 157 } 158 }; 159 fromDirectoryCursor(Cursor cursor)160 public static DocumentInfo fromDirectoryCursor(Cursor cursor) { 161 assert (cursor != null); 162 assert (cursor.getColumnIndex(RootCursorWrapper.COLUMN_USER_ID) >= 0); 163 final UserId userId = UserId.of(getCursorInt(cursor, RootCursorWrapper.COLUMN_USER_ID)); 164 final String authority = getCursorString(cursor, RootCursorWrapper.COLUMN_AUTHORITY); 165 return fromCursor(cursor, userId, authority); 166 } 167 fromCursor(Cursor cursor, UserId userId, String authority)168 public static DocumentInfo fromCursor(Cursor cursor, UserId userId, String authority) { 169 assert(cursor != null); 170 final DocumentInfo info = new DocumentInfo(); 171 info.updateFromCursor(cursor, userId, authority); 172 return info; 173 } 174 updateFromCursor(Cursor cursor, UserId userId, String authority)175 public void updateFromCursor(Cursor cursor, UserId userId, String authority) { 176 this.userId = userId; 177 this.authority = authority; 178 this.documentId = getCursorString(cursor, Document.COLUMN_DOCUMENT_ID); 179 this.mimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE); 180 this.displayName = getCursorString(cursor, Document.COLUMN_DISPLAY_NAME); 181 this.lastModified = getCursorLong(cursor, Document.COLUMN_LAST_MODIFIED); 182 this.flags = getCursorInt(cursor, Document.COLUMN_FLAGS); 183 this.summary = getCursorString(cursor, Document.COLUMN_SUMMARY); 184 this.size = getCursorLong(cursor, Document.COLUMN_SIZE); 185 this.icon = getCursorInt(cursor, Document.COLUMN_ICON); 186 this.deriveFields(); 187 } 188 189 /** 190 * Resolves a document info from the uri. The caller should specify the user of the resolver 191 * by providing a {@link UserId}. 192 */ fromUri(ContentResolver resolver, Uri uri, UserId userId)193 public static DocumentInfo fromUri(ContentResolver resolver, Uri uri, UserId userId) 194 throws FileNotFoundException { 195 final DocumentInfo info = new DocumentInfo(); 196 info.updateFromUri(resolver, uri, userId); 197 return info; 198 } 199 200 /** 201 * Update a possibly stale restored document against a live {@link DocumentsProvider}. The 202 * caller should specify the user of the resolver by providing a {@link UserId}. 203 */ updateSelf(ContentResolver resolver, UserId userId)204 public void updateSelf(ContentResolver resolver, UserId userId) throws FileNotFoundException { 205 updateFromUri(resolver, derivedUri, userId); 206 } 207 updateFromUri(ContentResolver resolver, Uri uri, UserId userId)208 private void updateFromUri(ContentResolver resolver, Uri uri, UserId userId) 209 throws FileNotFoundException { 210 ContentProviderClient client = null; 211 Cursor cursor = null; 212 try { 213 client = DocumentsApplication.acquireUnstableProviderOrThrow( 214 resolver, uri.getAuthority()); 215 cursor = client.query(uri, null, null, null, null); 216 if (!cursor.moveToFirst()) { 217 throw new FileNotFoundException("Missing details for " + uri); 218 } 219 updateFromCursor(cursor, userId, uri.getAuthority()); 220 } catch (Throwable t) { 221 throw asFileNotFoundException(t); 222 } finally { 223 FileUtils.closeQuietly(cursor); 224 FileUtils.closeQuietly(client); 225 } 226 } 227 228 @VisibleForTesting deriveFields()229 void deriveFields() { 230 derivedUri = DocumentsContract.buildDocumentUri(authority, documentId); 231 } 232 233 @Override toString()234 public String toString() { 235 return "DocumentInfo{" 236 + "docId=" + documentId 237 + ", userId=" + userId 238 + ", name=" + displayName 239 + ", mimeType=" + mimeType 240 + ", isContainer=" + isContainer() 241 + ", isDirectory=" + isDirectory() 242 + ", isArchive=" + isArchive() 243 + ", isInArchive=" + isInArchive() 244 + ", isPartial=" + isPartial() 245 + ", isVirtual=" + isVirtual() 246 + ", isDeleteSupported=" + isDeleteSupported() 247 + ", isCreateSupported=" + isCreateSupported() 248 + ", isMoveSupported=" + isMoveSupported() 249 + ", isRenameSupported=" + isRenameSupported() 250 + ", isMetadataSupported=" + isMetadataSupported() 251 + ", isBlockedFromTree=" + isBlockedFromTree() 252 + "} @ " 253 + derivedUri; 254 } 255 isCreateSupported()256 public boolean isCreateSupported() { 257 return (flags & Document.FLAG_DIR_SUPPORTS_CREATE) != 0; 258 } 259 isDeleteSupported()260 public boolean isDeleteSupported() { 261 return (flags & Document.FLAG_SUPPORTS_DELETE) != 0; 262 } 263 isMetadataSupported()264 public boolean isMetadataSupported() { 265 return (flags & Document.FLAG_SUPPORTS_METADATA) != 0; 266 } 267 isMoveSupported()268 public boolean isMoveSupported() { 269 return (flags & Document.FLAG_SUPPORTS_MOVE) != 0; 270 } 271 isRemoveSupported()272 public boolean isRemoveSupported() { 273 return (flags & Document.FLAG_SUPPORTS_REMOVE) != 0; 274 } 275 isRenameSupported()276 public boolean isRenameSupported() { 277 return (flags & Document.FLAG_SUPPORTS_RENAME) != 0; 278 } 279 isSettingsSupported()280 public boolean isSettingsSupported() { 281 return (flags & Document.FLAG_SUPPORTS_SETTINGS) != 0; 282 } 283 isThumbnailSupported()284 public boolean isThumbnailSupported() { 285 return (flags & Document.FLAG_SUPPORTS_THUMBNAIL) != 0; 286 } 287 isWeblinkSupported()288 public boolean isWeblinkSupported() { 289 return (flags & Document.FLAG_WEB_LINKABLE) != 0; 290 } 291 isWriteSupported()292 public boolean isWriteSupported() { 293 return (flags & Document.FLAG_SUPPORTS_WRITE) != 0; 294 } 295 isDirectory()296 public boolean isDirectory() { 297 return Document.MIME_TYPE_DIR.equals(mimeType); 298 } 299 isArchive()300 public boolean isArchive() { 301 return ArchivesProvider.isSupportedArchiveType(mimeType); 302 } 303 isInArchive()304 public boolean isInArchive() { 305 return ArchivesProvider.AUTHORITY.equals(authority); 306 } 307 isPartial()308 public boolean isPartial() { 309 return (flags & Document.FLAG_PARTIAL) != 0; 310 } 311 isBlockedFromTree()312 public boolean isBlockedFromTree() { 313 if (VersionUtils.isAtLeastR()) { 314 return (flags & Document.FLAG_DIR_BLOCKS_OPEN_DOCUMENT_TREE) != 0; 315 } else { 316 return false; 317 } 318 } 319 320 // Containers are documents which can be opened in DocumentsUI as folders. isContainer()321 public boolean isContainer() { 322 return isDirectory() || (isArchive() && !isInArchive() && !isPartial()); 323 } 324 isVirtual()325 public boolean isVirtual() { 326 return (flags & Document.FLAG_VIRTUAL_DOCUMENT) != 0; 327 } 328 prefersSortByLastModified()329 public boolean prefersSortByLastModified() { 330 return (flags & Document.FLAG_DIR_PREFERS_LAST_MODIFIED) != 0; 331 } 332 333 /** 334 * Returns a document uri representing this {@link DocumentInfo}. The URI may contain user 335 * information. Use this when uri is needed externally. For usage within DocsUI, use 336 * {@link #derivedUri}. 337 */ getDocumentUri()338 public Uri getDocumentUri() { 339 if (UserId.CURRENT_USER.equals(userId)) { 340 return derivedUri; 341 } 342 return userId.buildDocumentUriAsUser(authority, documentId); 343 } 344 345 346 /** 347 * Returns a tree document uri representing this {@link DocumentInfo}. The URI may contain user 348 * information. Use this when uri is needed externally. 349 */ getTreeDocumentUri()350 public Uri getTreeDocumentUri() { 351 if (UserId.CURRENT_USER.equals(userId)) { 352 return DocumentsContract.buildTreeDocumentUri(authority, documentId); 353 } 354 return userId.buildTreeDocumentUriAsUser(authority, documentId); 355 } 356 357 @Override hashCode()358 public int hashCode() { 359 return userId.hashCode() + derivedUri.hashCode() + mimeType.hashCode(); 360 } 361 362 @Override equals(Object o)363 public boolean equals(Object o) { 364 if (o == null) { 365 return false; 366 } 367 368 if (this == o) { 369 return true; 370 } 371 372 if (o instanceof DocumentInfo) { 373 DocumentInfo other = (DocumentInfo) o; 374 // Uri + mime type should be totally unique. 375 return Objects.equals(userId, other.userId) 376 && Objects.equals(derivedUri, other.derivedUri) 377 && Objects.equals(mimeType, other.mimeType); 378 } 379 380 return false; 381 } 382 getCursorString(Cursor cursor, String columnName)383 public static String getCursorString(Cursor cursor, String columnName) { 384 if (cursor == null) { 385 return null; 386 } 387 final int index = cursor.getColumnIndex(columnName); 388 return (index != -1) ? cursor.getString(index) : null; 389 } 390 391 /** 392 * Missing or null values are returned as -1. 393 */ getCursorLong(Cursor cursor, String columnName)394 public static long getCursorLong(Cursor cursor, String columnName) { 395 if (cursor == null) { 396 return -1; 397 } 398 399 final int index = cursor.getColumnIndex(columnName); 400 if (index == -1) return -1; 401 final String value = cursor.getString(index); 402 if (value == null) return -1; 403 try { 404 return Long.parseLong(value); 405 } catch (NumberFormatException e) { 406 return -1; 407 } 408 } 409 410 /** 411 * Missing or null values are returned as 0. 412 */ getCursorInt(Cursor cursor, String columnName)413 public static int getCursorInt(Cursor cursor, String columnName) { 414 if (cursor == null) { 415 return 0; 416 } 417 418 final int index = cursor.getColumnIndex(columnName); 419 return (index != -1) ? cursor.getInt(index) : 0; 420 } 421 asFileNotFoundException(Throwable t)422 public static FileNotFoundException asFileNotFoundException(Throwable t) 423 throws FileNotFoundException { 424 if (t instanceof FileNotFoundException) { 425 throw (FileNotFoundException) t; 426 } 427 final FileNotFoundException fnfe = new FileNotFoundException(t.getMessage()); 428 fnfe.initCause(t); 429 throw fnfe; 430 } 431 getUri(Cursor cursor)432 public static Uri getUri(Cursor cursor) { 433 return DocumentsContract.buildDocumentUri( 434 getCursorString(cursor, RootCursorWrapper.COLUMN_AUTHORITY), 435 getCursorString(cursor, Document.COLUMN_DOCUMENT_ID)); 436 } 437 getUserId(Cursor cursor)438 public static UserId getUserId(Cursor cursor) { 439 return UserId.of(getCursorInt(cursor, RootCursorWrapper.COLUMN_USER_ID)); 440 } 441 addMimeTypes(ContentResolver resolver, Uri uri, Set<String> mimeTypes)442 public static void addMimeTypes(ContentResolver resolver, Uri uri, Set<String> mimeTypes) { 443 assert(uri != null); 444 if ("content".equals(uri.getScheme())) { 445 final String type = resolver.getType(uri); 446 if (type != null) { 447 mimeTypes.add(type); 448 } else { 449 if (DEBUG) { 450 Log.d(TAG, "resolver.getType(uri) return null, url:" + uri.toSafeString()); 451 } 452 } 453 final String[] streamTypes = resolver.getStreamTypes(uri, "*/*"); 454 if (streamTypes != null) { 455 mimeTypes.addAll(Arrays.asList(streamTypes)); 456 } 457 } 458 } 459 debugString(@ullable DocumentInfo doc)460 public static String debugString(@Nullable DocumentInfo doc) { 461 if (doc == null) { 462 return "<null DocumentInfo>"; 463 } 464 465 if (doc.derivedUri == null) { 466 return "<DocumentInfo null derivedUri>"; 467 } 468 return doc.derivedUri.toString(); 469 } 470 } 471