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