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