blob: ad09c45cfa9202dfbbf0ba2c45bc7b8a8a8026e3 [file] [log] [blame]
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.documentsui.base;
import static com.android.documentsui.base.SharedMinimal.DEBUG;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.database.Cursor;
import android.net.Uri;
import android.os.FileUtils;
import android.os.Parcel;
import android.os.Parcelable;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Document;
import android.provider.DocumentsProvider;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
import com.android.documentsui.DocumentsApplication;
import com.android.documentsui.archives.ArchivesProvider;
import com.android.documentsui.roots.RootCursorWrapper;
import com.android.documentsui.util.VersionUtils;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.ProtocolException;
import java.util.Arrays;
import java.util.Objects;
import java.util.Set;
import javax.annotation.Nullable;
/**
* Representation of a {@link Document}.
*/
public class DocumentInfo implements Durable, Parcelable {
private static final String TAG = "DocumentInfo";
private static final int VERSION_INIT = 1;
private static final int VERSION_SPLIT_URI = 2;
private static final int VERSION_USER_ID = 3;
public UserId userId;
public String authority;
public String documentId;
public String mimeType;
public String displayName;
public long lastModified;
public int flags;
public String summary;
public long size;
public int icon;
/** Derived fields that aren't persisted */
public Uri derivedUri;
public DocumentInfo() {
reset();
}
@Override
public void reset() {
userId = UserId.UNSPECIFIED_USER;
authority = null;
documentId = null;
mimeType = null;
displayName = null;
lastModified = -1;
flags = 0;
summary = null;
size = -1;
icon = 0;
derivedUri = null;
}
@Override
public void read(DataInputStream in) throws IOException {
final int version = in.readInt();
switch (version) {
case VERSION_USER_ID:
userId = UserId.read(in);
case VERSION_SPLIT_URI:
if (version < VERSION_USER_ID) {
userId = UserId.CURRENT_USER;
}
authority = DurableUtils.readNullableString(in);
documentId = DurableUtils.readNullableString(in);
mimeType = DurableUtils.readNullableString(in);
displayName = DurableUtils.readNullableString(in);
lastModified = in.readLong();
flags = in.readInt();
summary = DurableUtils.readNullableString(in);
size = in.readLong();
icon = in.readInt();
deriveFields();
break;
case VERSION_INIT:
throw new ProtocolException("Ignored upgrade");
default:
throw new ProtocolException("Unknown version " + version);
}
}
@Override
public void write(DataOutputStream out) throws IOException {
out.writeInt(VERSION_USER_ID);
UserId.write(out, userId);
DurableUtils.writeNullableString(out, authority);
DurableUtils.writeNullableString(out, documentId);
DurableUtils.writeNullableString(out, mimeType);
DurableUtils.writeNullableString(out, displayName);
out.writeLong(lastModified);
out.writeInt(flags);
DurableUtils.writeNullableString(out, summary);
out.writeLong(size);
out.writeInt(icon);
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
DurableUtils.writeToParcel(dest, this);
}
public static final Creator<DocumentInfo> CREATOR = new Creator<DocumentInfo>() {
@Override
public DocumentInfo createFromParcel(Parcel in) {
final DocumentInfo doc = new DocumentInfo();
DurableUtils.readFromParcel(in, doc);
return doc;
}
@Override
public DocumentInfo[] newArray(int size) {
return new DocumentInfo[size];
}
};
public static DocumentInfo fromDirectoryCursor(Cursor cursor) {
assert (cursor != null);
assert (cursor.getColumnIndex(RootCursorWrapper.COLUMN_USER_ID) >= 0);
final UserId userId = UserId.of(getCursorInt(cursor, RootCursorWrapper.COLUMN_USER_ID));
final String authority = getCursorString(cursor, RootCursorWrapper.COLUMN_AUTHORITY);
return fromCursor(cursor, userId, authority);
}
public static DocumentInfo fromCursor(Cursor cursor, UserId userId, String authority) {
assert(cursor != null);
final DocumentInfo info = new DocumentInfo();
info.updateFromCursor(cursor, userId, authority);
return info;
}
public void updateFromCursor(Cursor cursor, UserId userId, String authority) {
this.userId = userId;
this.authority = authority;
this.documentId = getCursorString(cursor, Document.COLUMN_DOCUMENT_ID);
this.mimeType = getCursorString(cursor, Document.COLUMN_MIME_TYPE);
this.displayName = getCursorString(cursor, Document.COLUMN_DISPLAY_NAME);
this.lastModified = getCursorLong(cursor, Document.COLUMN_LAST_MODIFIED);
this.flags = getCursorInt(cursor, Document.COLUMN_FLAGS);
this.summary = getCursorString(cursor, Document.COLUMN_SUMMARY);
this.size = getCursorLong(cursor, Document.COLUMN_SIZE);
this.icon = getCursorInt(cursor, Document.COLUMN_ICON);
this.deriveFields();
}
/**
* Resolves a document info from the uri. The caller should specify the user of the resolver
* by providing a {@link UserId}.
*/
public static DocumentInfo fromUri(ContentResolver resolver, Uri uri, UserId userId)
throws FileNotFoundException {
final DocumentInfo info = new DocumentInfo();
info.updateFromUri(resolver, uri, userId);
return info;
}
/**
* Update a possibly stale restored document against a live {@link DocumentsProvider}. The
* caller should specify the user of the resolver by providing a {@link UserId}.
*/
public void updateSelf(ContentResolver resolver, UserId userId) throws FileNotFoundException {
updateFromUri(resolver, derivedUri, userId);
}
private void updateFromUri(ContentResolver resolver, Uri uri, UserId userId)
throws FileNotFoundException {
ContentProviderClient client = null;
Cursor cursor = null;
try {
client = DocumentsApplication.acquireUnstableProviderOrThrow(
resolver, uri.getAuthority());
cursor = client.query(uri, null, null, null, null);
if (!cursor.moveToFirst()) {
throw new FileNotFoundException("Missing details for " + uri);
}
updateFromCursor(cursor, userId, uri.getAuthority());
} catch (Throwable t) {
throw asFileNotFoundException(t);
} finally {
FileUtils.closeQuietly(cursor);
FileUtils.closeQuietly(client);
}
}
@VisibleForTesting
void deriveFields() {
derivedUri = DocumentsContract.buildDocumentUri(authority, documentId);
}
@Override
public String toString() {
return "DocumentInfo{"
+ "docId=" + documentId
+ ", userId=" + userId
+ ", name=" + displayName
+ ", mimeType=" + mimeType
+ ", isContainer=" + isContainer()
+ ", isDirectory=" + isDirectory()
+ ", isArchive=" + isArchive()
+ ", isInArchive=" + isInArchive()
+ ", isPartial=" + isPartial()
+ ", isVirtual=" + isVirtual()
+ ", isDeleteSupported=" + isDeleteSupported()
+ ", isCreateSupported=" + isCreateSupported()
+ ", isMoveSupported=" + isMoveSupported()
+ ", isRenameSupported=" + isRenameSupported()
+ ", isMetadataSupported=" + isMetadataSupported()
+ ", isBlockedFromTree=" + isBlockedFromTree()
+ "} @ "
+ derivedUri;
}
public boolean isCreateSupported() {
return (flags & Document.FLAG_DIR_SUPPORTS_CREATE) != 0;
}
public boolean isDeleteSupported() {
return (flags & Document.FLAG_SUPPORTS_DELETE) != 0;
}
public boolean isMetadataSupported() {
return (flags & Document.FLAG_SUPPORTS_METADATA) != 0;
}
public boolean isMoveSupported() {
return (flags & Document.FLAG_SUPPORTS_MOVE) != 0;
}
public boolean isRemoveSupported() {
return (flags & Document.FLAG_SUPPORTS_REMOVE) != 0;
}
public boolean isRenameSupported() {
return (flags & Document.FLAG_SUPPORTS_RENAME) != 0;
}
public boolean isSettingsSupported() {
return (flags & Document.FLAG_SUPPORTS_SETTINGS) != 0;
}
public boolean isThumbnailSupported() {
return (flags & Document.FLAG_SUPPORTS_THUMBNAIL) != 0;
}
public boolean isWeblinkSupported() {
return (flags & Document.FLAG_WEB_LINKABLE) != 0;
}
public boolean isWriteSupported() {
return (flags & Document.FLAG_SUPPORTS_WRITE) != 0;
}
public boolean isDirectory() {
return Document.MIME_TYPE_DIR.equals(mimeType);
}
public boolean isArchive() {
return ArchivesProvider.isSupportedArchiveType(mimeType);
}
public boolean isInArchive() {
return ArchivesProvider.AUTHORITY.equals(authority);
}
public boolean isPartial() {
return (flags & Document.FLAG_PARTIAL) != 0;
}
public boolean isBlockedFromTree() {
if (VersionUtils.isAtLeastR()) {
return (flags & Document.FLAG_DIR_BLOCKS_OPEN_DOCUMENT_TREE) != 0;
} else {
return false;
}
}
// Containers are documents which can be opened in DocumentsUI as folders.
public boolean isContainer() {
return isDirectory() || (isArchive() && !isInArchive() && !isPartial());
}
public boolean isVirtual() {
return (flags & Document.FLAG_VIRTUAL_DOCUMENT) != 0;
}
public boolean prefersSortByLastModified() {
return (flags & Document.FLAG_DIR_PREFERS_LAST_MODIFIED) != 0;
}
/**
* Returns a document uri representing this {@link DocumentInfo}. The URI may contain user
* information. Use this when uri is needed externally. For usage within DocsUI, use
* {@link #derivedUri}.
*/
public Uri getDocumentUri() {
if (UserId.CURRENT_USER.equals(userId)) {
return derivedUri;
}
return userId.buildDocumentUriAsUser(authority, documentId);
}
/**
* Returns a tree document uri representing this {@link DocumentInfo}. The URI may contain user
* information. Use this when uri is needed externally.
*/
public Uri getTreeDocumentUri() {
if (UserId.CURRENT_USER.equals(userId)) {
return DocumentsContract.buildTreeDocumentUri(authority, documentId);
}
return userId.buildTreeDocumentUriAsUser(authority, documentId);
}
@Override
public int hashCode() {
return userId.hashCode() + derivedUri.hashCode() + mimeType.hashCode();
}
@Override
public boolean equals(Object o) {
if (o == null) {
return false;
}
if (this == o) {
return true;
}
if (o instanceof DocumentInfo) {
DocumentInfo other = (DocumentInfo) o;
// Uri + mime type should be totally unique.
return Objects.equals(userId, other.userId)
&& Objects.equals(derivedUri, other.derivedUri)
&& Objects.equals(mimeType, other.mimeType);
}
return false;
}
public static String getCursorString(Cursor cursor, String columnName) {
if (cursor == null) {
return null;
}
final int index = cursor.getColumnIndex(columnName);
return (index != -1) ? cursor.getString(index) : null;
}
/**
* Missing or null values are returned as -1.
*/
public static long getCursorLong(Cursor cursor, String columnName) {
if (cursor == null) {
return -1;
}
final int index = cursor.getColumnIndex(columnName);
if (index == -1) return -1;
final String value = cursor.getString(index);
if (value == null) return -1;
try {
return Long.parseLong(value);
} catch (NumberFormatException e) {
return -1;
}
}
/**
* Missing or null values are returned as 0.
*/
public static int getCursorInt(Cursor cursor, String columnName) {
if (cursor == null) {
return 0;
}
final int index = cursor.getColumnIndex(columnName);
return (index != -1) ? cursor.getInt(index) : 0;
}
public static FileNotFoundException asFileNotFoundException(Throwable t)
throws FileNotFoundException {
if (t instanceof FileNotFoundException) {
throw (FileNotFoundException) t;
}
final FileNotFoundException fnfe = new FileNotFoundException(t.getMessage());
fnfe.initCause(t);
throw fnfe;
}
public static Uri getUri(Cursor cursor) {
return DocumentsContract.buildDocumentUri(
getCursorString(cursor, RootCursorWrapper.COLUMN_AUTHORITY),
getCursorString(cursor, Document.COLUMN_DOCUMENT_ID));
}
public static UserId getUserId(Cursor cursor) {
return UserId.of(getCursorInt(cursor, RootCursorWrapper.COLUMN_USER_ID));
}
public static void addMimeTypes(ContentResolver resolver, Uri uri, Set<String> mimeTypes) {
assert(uri != null);
if ("content".equals(uri.getScheme())) {
final String type = resolver.getType(uri);
if (type != null) {
mimeTypes.add(type);
} else {
if (DEBUG) {
Log.d(TAG, "resolver.getType(uri) return null, url:" + uri.toSafeString());
}
}
final String[] streamTypes = resolver.getStreamTypes(uri, "*/*");
if (streamTypes != null) {
mimeTypes.addAll(Arrays.asList(streamTypes));
}
}
}
public static String debugString(@Nullable DocumentInfo doc) {
if (doc == null) {
return "<null DocumentInfo>";
}
if (doc.derivedUri == null) {
return "<DocumentInfo null derivedUri>";
}
return doc.derivedUri.toString();
}
}