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