DocumentArchive.java revision 735fc5f89ffb4c2c9e96e370cda7208b222f0773
164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski/* 264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * Copyright (C) 2015 The Android Open Source Project 364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * 464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * Licensed under the Apache License, Version 2.0 (the "License"); 564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * you may not use this file except in compliance with the License. 664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * You may obtain a copy of the License at 764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * 864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * http://www.apache.org/licenses/LICENSE-2.0 964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * 1064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * Unless required by applicable law or agreed to in writing, software 1164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * distributed under the License is distributed on an "AS IS" BASIS, 1264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 1364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * See the License for the specific language governing permissions and 1464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * limitations under the License. 1564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski */ 1664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski 1764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskipackage android.support.provider; 1864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski 1964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport android.content.Context; 2009deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewskiimport android.content.res.AssetFileDescriptor; 2164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport android.database.Cursor; 2264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport android.database.MatrixCursor; 2309deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewskiimport android.graphics.Point; 2474f89c87908ad510160083202c3825a880c18905Tomasz Mikolajewskiimport android.media.ExifInterface; 2564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport android.net.Uri; 2674f89c87908ad510160083202c3825a880c18905Tomasz Mikolajewskiimport android.os.Bundle; 2764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport android.os.CancellationSignal; 2809deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewskiimport android.os.OperationCanceledException; 2964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport android.os.ParcelFileDescriptor; 3074f89c87908ad510160083202c3825a880c18905Tomasz Mikolajewskiimport android.provider.DocumentsContract; 3164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport android.provider.DocumentsContract.Document; 3264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport android.provider.DocumentsProvider; 33369746f1bd8e90fb04e11f181623413b7ae8e063Tomasz Mikolajewskiimport android.support.annotation.Nullable; 3464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport android.util.Log; 3564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport android.webkit.MimeTypeMap; 3664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski 3764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.io.Closeable; 3864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.io.File; 3964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.io.FileNotFoundException; 4064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.io.FileOutputStream; 4164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.io.IOException; 4264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.io.InputStream; 4393307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewskiimport java.util.ArrayList; 4464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.lang.IllegalArgumentException; 4564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.lang.IllegalStateException; 4664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.lang.UnsupportedOperationException; 4764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.util.Collections; 4893307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewskiimport java.util.HashMap; 4964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.util.Iterator; 5093307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewskiimport java.util.List; 51aaf68419351d65048712c636cbe107c42cdb6844Tomasz Mikolajewskiimport java.util.Locale; 5293307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewskiimport java.util.Map; 5379ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewskiimport java.util.Stack; 5464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.util.concurrent.ExecutorService; 5564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.util.concurrent.Executors; 5664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.util.zip.ZipEntry; 5764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.util.zip.ZipFile; 5864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskiimport java.util.zip.ZipInputStream; 5964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski 6064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski/** 6164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * Provides basic implementation for creating, extracting and accessing 6264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * files within archives exposed by a document provider. The id delimiter 6364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * must be a character which is not used in document ids generated by the 6464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * document provider. 6564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * 6664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * <p>This class is thread safe. 6764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * 6864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * @hide 6964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski */ 7064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewskipublic class DocumentArchive implements Closeable { 7164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski private static final String TAG = "DocumentArchive"; 7264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski 7364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski private static final String[] DEFAULT_PROJECTION = new String[] { 7464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski Document.COLUMN_DOCUMENT_ID, 7564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski Document.COLUMN_DISPLAY_NAME, 7664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski Document.COLUMN_MIME_TYPE, 7709deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski Document.COLUMN_SIZE, 7809deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski Document.COLUMN_FLAGS 7964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski }; 8064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski 8164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski private final Context mContext; 8264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski private final String mDocumentId; 8364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski private final char mIdDelimiter; 8464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski private final Uri mNotificationUri; 8564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski private final ZipFile mZipFile; 8664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski private final ExecutorService mExecutor; 8779ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski private final Map<String, ZipEntry> mEntries; 8893307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski private final Map<String, List<ZipEntry>> mTree; 8964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski 9064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski private DocumentArchive( 9164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski Context context, 9264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski File file, 9364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski String documentId, 9464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski char idDelimiter, 95369746f1bd8e90fb04e11f181623413b7ae8e063Tomasz Mikolajewski @Nullable Uri notificationUri) 9664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski throws IOException { 9764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski mContext = context; 9864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski mDocumentId = documentId; 9964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski mIdDelimiter = idDelimiter; 10064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski mNotificationUri = notificationUri; 10164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski mZipFile = new ZipFile(file); 10264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski mExecutor = Executors.newSingleThreadExecutor(); 10393307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski 10493307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski // Build the tree structure in memory. 10593307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski mTree = new HashMap<String, List<ZipEntry>>(); 10693307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski mTree.put("/", new ArrayList<ZipEntry>()); 10793307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski 10879ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski mEntries = new HashMap<String, ZipEntry>(); 10979ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski ZipEntry entry; 11079ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski final List<? extends ZipEntry> entries = Collections.list(mZipFile.entries()); 11179ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski final Stack<ZipEntry> stack = new Stack<>(); 11279ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski for (int i = entries.size() - 1; i >= 0; i--) { 11379ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski entry = entries.get(i); 114e7cddbdd108544a329500ccb02fdc3ef0fca94d7Tomasz Mikolajewski if (entry.isDirectory() != entry.getName().endsWith("/")) { 115e7cddbdd108544a329500ccb02fdc3ef0fca94d7Tomasz Mikolajewski throw new IOException( 116e7cddbdd108544a329500ccb02fdc3ef0fca94d7Tomasz Mikolajewski "Directories must have a trailing slash, and files must not."); 117e7cddbdd108544a329500ccb02fdc3ef0fca94d7Tomasz Mikolajewski } 1183bf877d757a981d73fb85d46c7f77231ea03a801Tomasz Mikolajewski if (mEntries.containsKey(entry.getName())) { 1193bf877d757a981d73fb85d46c7f77231ea03a801Tomasz Mikolajewski throw new IOException("Multiple entries with the same name are not supported."); 1203bf877d757a981d73fb85d46c7f77231ea03a801Tomasz Mikolajewski } 12179ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski mEntries.put(entry.getName(), entry); 12293307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski if (entry.isDirectory()) { 12393307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski mTree.put(entry.getName(), new ArrayList<ZipEntry>()); 12493307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski } 12579ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski stack.push(entry); 12693307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski } 12793307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski 12893307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski int delimiterIndex; 12993307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski String parentPath; 13079ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski ZipEntry parentEntry; 13179ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski List<ZipEntry> parentList; 13279ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski 13379ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski while (stack.size() > 0) { 13479ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski entry = stack.pop(); 13579ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski 13693307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski delimiterIndex = entry.getName().lastIndexOf('/', entry.isDirectory() 13793307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski ? entry.getName().length() - 2 : entry.getName().length() - 1); 13893307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski parentPath = 13993307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski delimiterIndex != -1 ? entry.getName().substring(0, delimiterIndex) + "/" : "/"; 14093307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski parentList = mTree.get(parentPath); 14179ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski 14279ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski if (parentList == null) { 14379ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski parentEntry = mEntries.get(parentPath); 14479ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski if (parentEntry == null) { 14579ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski // The ZIP file doesn't contain all directories leading to the entry. 14679ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski // It's rare, but can happen in a valid ZIP archive. In such case create a 14779ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski // fake ZipEntry and add it on top of the stack to process it next. 14879ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski parentEntry = new ZipEntry(parentPath); 14979ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski parentEntry.setSize(0); 15079ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski parentEntry.setTime(entry.getTime()); 15179ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski mEntries.put(parentPath, parentEntry); 15279ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski stack.push(parentEntry); 15379ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski } 15479ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski parentList = new ArrayList<ZipEntry>(); 15579ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski mTree.put(parentPath, parentList); 15693307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski } 15779ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski 15879ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski parentList.add(entry); 15993307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski } 16064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski } 16164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski 16264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski /** 16364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * Creates a DocumentsArchive instance for opening, browsing and accessing 16464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * documents within the archive passed as a local file. 16564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * 16664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * @param context Context of the provider. 16764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * @param File Local file containing the archive. 16864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * @param documentId ID of the archive document. 16964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * @param idDelimiter Delimiter for constructing IDs of documents within the archive. 17064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * The delimiter must never be used for IDs of other documents. 17164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * @param Uri notificationUri Uri for notifying that the archive file has changed. 17264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * @see createForParcelFileDescriptor(DocumentsProvider, ParcelFileDescriptor, String, char, 17364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * Uri) 17464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski */ 17564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski public static DocumentArchive createForLocalFile( 176369746f1bd8e90fb04e11f181623413b7ae8e063Tomasz Mikolajewski Context context, File file, String documentId, char idDelimiter, 177369746f1bd8e90fb04e11f181623413b7ae8e063Tomasz Mikolajewski @Nullable Uri notificationUri) 17864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski throws IOException { 17964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski return new DocumentArchive(context, file, documentId, idDelimiter, notificationUri); 18064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski } 18164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski 18264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski /** 18364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * Creates a DocumentsArchive instance for opening, browsing and accessing 18464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * documents within the archive passed as a file descriptor. 18564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * 18664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * <p>Note, that this method should be used only if the document does not exist 18764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * on the local storage. A snapshot file will be created, which may be slower 18864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * and consume significant resources, in contrast to using 18964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * {@see createForLocalFile(Context, File, String, char, Uri}. 19064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * 19164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * @param context Context of the provider. 19264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * @param descriptor File descriptor for the archive's contents. 19364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * @param documentId ID of the archive document. 19464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * @param idDelimiter Delimiter for constructing IDs of documents within the archive. 19564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * The delimiter must never be used for IDs of other documents. 19664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * @param Uri notificationUri Uri for notifying that the archive file has changed. 19764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * @see createForLocalFile(Context, File, String, char, Uri) 19864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski */ 19964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski public static DocumentArchive createForParcelFileDescriptor( 20064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski Context context, ParcelFileDescriptor descriptor, String documentId, 201369746f1bd8e90fb04e11f181623413b7ae8e063Tomasz Mikolajewski char idDelimiter, @Nullable Uri notificationUri) 20264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski throws IOException { 20364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski File snapshotFile = null; 20464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski try { 20564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski // Create a copy of the archive, as ZipFile doesn't operate on streams. 20664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski // Moreover, ZipInputStream would be inefficient for large files on 20764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski // pipes. 20864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski snapshotFile = File.createTempFile("android.support.provider.snapshot{", 20964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski "}.zip", context.getCacheDir()); 21064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski 21164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski try ( 21264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski final FileOutputStream outputStream = 21364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski new ParcelFileDescriptor.AutoCloseOutputStream( 21464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski ParcelFileDescriptor.open( 21564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski snapshotFile, ParcelFileDescriptor.MODE_WRITE_ONLY)); 21664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski final ParcelFileDescriptor.AutoCloseInputStream inputStream = 21764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski new ParcelFileDescriptor.AutoCloseInputStream(descriptor); 21864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski ) { 21964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski final byte[] buffer = new byte[32 * 1024]; 22064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski int bytes; 22164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski while ((bytes = inputStream.read(buffer)) != -1) { 22264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski outputStream.write(buffer, 0, bytes); 22364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski } 22464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski outputStream.flush(); 22564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski return new DocumentArchive(context, snapshotFile, documentId, idDelimiter, 22664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski notificationUri); 22764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski } 22864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski } finally { 22964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski // On UNIX the file will be still available for processes which opened it, even 23064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski // after deleting it. Remove it ASAP, as it won't be used by anyone else. 23164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski if (snapshotFile != null) { 23264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski snapshotFile.delete(); 23364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski } 23464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski } 23564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski } 23664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski 23764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski /** 23864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * Lists child documents of an archive or a directory within an 23964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * archive. Must be called only for archives with supported mime type, 24064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * or for documents within archives. 24164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * 24264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * @see DocumentsProvider.queryChildDocuments(String, String[], String) 24364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski */ 244369746f1bd8e90fb04e11f181623413b7ae8e063Tomasz Mikolajewski public Cursor queryChildDocuments(String documentId, @Nullable String[] projection, 24530a5cbb8727113e2a640d4256d5c0c0435f3421aTomasz Mikolajewski @Nullable String sortOrder) throws FileNotFoundException { 24664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski final ParsedDocumentId parsedParentId = ParsedDocumentId.fromDocumentId( 24764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski documentId, mIdDelimiter); 24864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski Preconditions.checkArgumentEquals(mDocumentId, parsedParentId.mArchiveId, 24964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski "Mismatching document ID. Expected: %s, actual: %s."); 25064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski 25193307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski final String parentPath = parsedParentId.mPath != null ? parsedParentId.mPath : "/"; 25264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski final MatrixCursor result = new MatrixCursor( 25364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski projection != null ? projection : DEFAULT_PROJECTION); 25464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski if (mNotificationUri != null) { 25564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski result.setNotificationUri(mContext.getContentResolver(), mNotificationUri); 25664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski } 25764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski 25893307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski final List<ZipEntry> parentList = mTree.get(parentPath); 25930a5cbb8727113e2a640d4256d5c0c0435f3421aTomasz Mikolajewski if (parentList == null) { 26030a5cbb8727113e2a640d4256d5c0c0435f3421aTomasz Mikolajewski throw new FileNotFoundException(); 26130a5cbb8727113e2a640d4256d5c0c0435f3421aTomasz Mikolajewski } 26293307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski for (final ZipEntry entry : parentList) { 26393307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski addCursorRow(result, entry); 26464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski } 26564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski return result; 26664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski } 26764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski 26864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski /** 26964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * Returns a MIME type of a document within an archive. 27064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * 27164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * @see DocumentsProvider.getDocumentType(String) 27264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski */ 27364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski public String getDocumentType(String documentId) throws FileNotFoundException { 27464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski final ParsedDocumentId parsedId = ParsedDocumentId.fromDocumentId( 27564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski documentId, mIdDelimiter); 27664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski Preconditions.checkArgumentEquals(mDocumentId, parsedId.mArchiveId, 27764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski "Mismatching document ID. Expected: %s, actual: %s."); 27864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski Preconditions.checkArgumentNotNull(parsedId.mPath, "Not a document within an archive."); 27964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski 28079ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski final ZipEntry entry = mEntries.get(parsedId.mPath); 28164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski if (entry == null) { 28264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski throw new FileNotFoundException(); 28364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski } 28464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski return getMimeTypeForEntry(entry); 28564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski } 28664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski 28764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski /** 28864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * Returns true if a document within an archive is a child or any descendant of the archive 28964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * document or another document within the archive. 29064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * 29164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * @see DocumentsProvider.isChildDocument(String, String) 29264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski */ 29364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski public boolean isChildDocument(String parentDocumentId, String documentId) { 29464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski final ParsedDocumentId parsedParentId = ParsedDocumentId.fromDocumentId( 29564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski parentDocumentId, mIdDelimiter); 29664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski final ParsedDocumentId parsedId = ParsedDocumentId.fromDocumentId( 29764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski documentId, mIdDelimiter); 29864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski Preconditions.checkArgumentEquals(mDocumentId, parsedParentId.mArchiveId, 29964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski "Mismatching document ID. Expected: %s, actual: %s."); 30064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski Preconditions.checkArgumentNotNull(parsedId.mPath, 30164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski "Not a document within an archive."); 30264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski 30379ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski final ZipEntry entry = mEntries.get(parsedId.mPath); 30464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski if (entry == null) { 30564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski return false; 30664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski } 30764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski 30864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski if (parsedParentId.mPath == null) { 30964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski // No need to compare paths. Every file in the archive is a child of the archive 31064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski // file. 31164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski return true; 31264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski } 31364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski 31479ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski final ZipEntry parentEntry = mEntries.get(parsedParentId.mPath); 31564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski if (parentEntry == null || !parentEntry.isDirectory()) { 31664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski return false; 31764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski } 31864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski 31993307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski final String parentPath = entry.getName(); 32064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski 32164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski // Add a trailing slash even if it's not a directory, so it's easy to check if the 32264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski // entry is a descendant. 32393307b9bd6d6c127d85bee61f42ba1bf0f5d979aTomasz Mikolajewski final String pathWithSlash = entry.isDirectory() ? entry.getName() : entry.getName() + "/"; 32464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski return pathWithSlash.startsWith(parentPath) && !parentPath.equals(pathWithSlash); 32564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski } 32664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski 32764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski /** 32864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * Returns metadata of a document within an archive. 32964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * 33064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * @see DocumentsProvider.queryDocument(String, String[]) 33164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski */ 332369746f1bd8e90fb04e11f181623413b7ae8e063Tomasz Mikolajewski public Cursor queryDocument(String documentId, @Nullable String[] projection) 33364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski throws FileNotFoundException { 33464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski final ParsedDocumentId parsedId = ParsedDocumentId.fromDocumentId( 33564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski documentId, mIdDelimiter); 33664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski Preconditions.checkArgumentEquals(mDocumentId, parsedId.mArchiveId, 33764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski "Mismatching document ID. Expected: %s, actual: %s."); 33864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski Preconditions.checkArgumentNotNull(parsedId.mPath, "Not a document within an archive."); 33964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski 34079ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski final ZipEntry entry = mEntries.get(parsedId.mPath); 34164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski if (entry == null) { 34264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski throw new FileNotFoundException(); 34364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski } 34464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski 34564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski final MatrixCursor result = new MatrixCursor( 34664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski projection != null ? projection : DEFAULT_PROJECTION); 34764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski if (mNotificationUri != null) { 34864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski result.setNotificationUri(mContext.getContentResolver(), mNotificationUri); 34964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski } 35064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski addCursorRow(result, entry); 35164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski return result; 35264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski } 35364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski 35464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski /** 35564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * Opens a file within an archive. 35664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * 35764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * @see DocumentsProvider.openDocument(String, String, CancellationSignal)) 35864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski */ 35964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski public ParcelFileDescriptor openDocument( 360369746f1bd8e90fb04e11f181623413b7ae8e063Tomasz Mikolajewski String documentId, String mode, @Nullable final CancellationSignal signal) 36164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski throws FileNotFoundException { 36264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski Preconditions.checkArgumentEquals("r", mode, 36364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski "Invalid mode. Only reading \"r\" supported, but got: \"%s\"."); 36464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski final ParsedDocumentId parsedId = ParsedDocumentId.fromDocumentId( 36564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski documentId, mIdDelimiter); 36664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski Preconditions.checkArgumentEquals(mDocumentId, parsedId.mArchiveId, 36764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski "Mismatching document ID. Expected: %s, actual: %s."); 36864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski Preconditions.checkArgumentNotNull(parsedId.mPath, "Not a document within an archive."); 36964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski 37079ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski final ZipEntry entry = mEntries.get(parsedId.mPath); 37164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski if (entry == null) { 37264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski throw new FileNotFoundException(); 37364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski } 37464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski 37564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski ParcelFileDescriptor[] pipe; 37664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski InputStream inputStream = null; 37764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski try { 37864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski pipe = ParcelFileDescriptor.createReliablePipe(); 37964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski inputStream = mZipFile.getInputStream(entry); 38064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski } catch (IOException e) { 38164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski if (inputStream != null) { 38264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski IoUtils.closeQuietly(inputStream); 38364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski } 38464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski // Ideally we'd simply throw IOException to the caller, but for consistency 38564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski // with DocumentsProvider::openDocument, converting it to IllegalStateException. 38664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski throw new IllegalStateException("Failed to open the document.", e); 38764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski } 38864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski final ParcelFileDescriptor outputPipe = pipe[1]; 38964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski final InputStream finalInputStream = inputStream; 39064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski mExecutor.execute( 39164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski new Runnable() { 39264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski @Override 39364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski public void run() { 39464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski try (final ParcelFileDescriptor.AutoCloseOutputStream outputStream = 39564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski new ParcelFileDescriptor.AutoCloseOutputStream(outputPipe)) { 39664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski try { 39764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski final byte buffer[] = new byte[32 * 1024]; 39864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski int bytes; 39964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski while ((bytes = finalInputStream.read(buffer)) != -1) { 40064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski if (Thread.interrupted()) { 40164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski throw new InterruptedException(); 40264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski } 40364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski if (signal != null) { 40464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski signal.throwIfCanceled(); 40564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski } 40664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski outputStream.write(buffer, 0, bytes); 40764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski } 40864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski } catch (IOException | InterruptedException e) { 40964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski // Catch the exception before the outer try-with-resource closes the 41064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski // pipe with close() instead of closeWithError(). 41164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski try { 41264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski outputPipe.closeWithError(e.getMessage()); 41364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski } catch (IOException e2) { 41464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski Log.e(TAG, "Failed to close the pipe after an error.", e2); 41564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski } 41664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski } 41709deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski } catch (OperationCanceledException e) { 41809deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski // Cancelled gracefully. 41964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski } catch (IOException e) { 42064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski Log.e(TAG, "Failed to close the output stream gracefully.", e); 42164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski } finally { 42264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski IoUtils.closeQuietly(finalInputStream); 42364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski } 42464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski } 42564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski }); 42664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski 42764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski return pipe[0]; 42864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski } 42964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski 43064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski /** 43109deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski * Opens a thumbnail of a file within an archive. 43209deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski * 43309deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski * @see DocumentsProvider.openDocumentThumbnail(String, Point, CancellationSignal)) 43409deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski */ 43509deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski public AssetFileDescriptor openDocumentThumbnail( 43609deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski String documentId, Point sizeHint, final CancellationSignal signal) 43709deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski throws FileNotFoundException { 43809deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski final ParsedDocumentId parsedId = ParsedDocumentId.fromDocumentId(documentId, mIdDelimiter); 43909deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski Preconditions.checkArgumentEquals(mDocumentId, parsedId.mArchiveId, 44009deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski "Mismatching document ID. Expected: %s, actual: %s."); 44109deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski Preconditions.checkArgumentNotNull(parsedId.mPath, "Not a document within an archive."); 44209deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski Preconditions.checkArgument(getDocumentType(documentId).startsWith("image/"), 44309deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski "Thumbnails only supported for image/* MIME type."); 44409deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski 44579ef8424ea33627ca941fbab83bfb2fa24b7ce98Tomasz Mikolajewski final ZipEntry entry = mEntries.get(parsedId.mPath); 44609deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski if (entry == null) { 44709deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski throw new FileNotFoundException(); 44809deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski } 44909deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski 450735fc5f89ffb4c2c9e96e370cda7208b222f0773Tomasz Mikolajewski InputStream inputStream = null; 45174f89c87908ad510160083202c3825a880c18905Tomasz Mikolajewski try { 452735fc5f89ffb4c2c9e96e370cda7208b222f0773Tomasz Mikolajewski inputStream = mZipFile.getInputStream(entry); 45374f89c87908ad510160083202c3825a880c18905Tomasz Mikolajewski final ExifInterface exif = new ExifInterface(inputStream); 45474f89c87908ad510160083202c3825a880c18905Tomasz Mikolajewski if (exif.hasThumbnail()) { 45574f89c87908ad510160083202c3825a880c18905Tomasz Mikolajewski Bundle extras = null; 45674f89c87908ad510160083202c3825a880c18905Tomasz Mikolajewski switch (exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, -1)) { 45774f89c87908ad510160083202c3825a880c18905Tomasz Mikolajewski case ExifInterface.ORIENTATION_ROTATE_90: 45874f89c87908ad510160083202c3825a880c18905Tomasz Mikolajewski extras = new Bundle(1); 45974f89c87908ad510160083202c3825a880c18905Tomasz Mikolajewski extras.putInt(DocumentsContract.EXTRA_ORIENTATION, 90); 46074f89c87908ad510160083202c3825a880c18905Tomasz Mikolajewski break; 46174f89c87908ad510160083202c3825a880c18905Tomasz Mikolajewski case ExifInterface.ORIENTATION_ROTATE_180: 46274f89c87908ad510160083202c3825a880c18905Tomasz Mikolajewski extras = new Bundle(1); 46374f89c87908ad510160083202c3825a880c18905Tomasz Mikolajewski extras.putInt(DocumentsContract.EXTRA_ORIENTATION, 180); 46474f89c87908ad510160083202c3825a880c18905Tomasz Mikolajewski break; 46574f89c87908ad510160083202c3825a880c18905Tomasz Mikolajewski case ExifInterface.ORIENTATION_ROTATE_270: 46674f89c87908ad510160083202c3825a880c18905Tomasz Mikolajewski extras = new Bundle(1); 46774f89c87908ad510160083202c3825a880c18905Tomasz Mikolajewski extras.putInt(DocumentsContract.EXTRA_ORIENTATION, 270); 46874f89c87908ad510160083202c3825a880c18905Tomasz Mikolajewski break; 46974f89c87908ad510160083202c3825a880c18905Tomasz Mikolajewski } 47074f89c87908ad510160083202c3825a880c18905Tomasz Mikolajewski final long[] range = exif.getThumbnailRange(); 47174f89c87908ad510160083202c3825a880c18905Tomasz Mikolajewski return new AssetFileDescriptor( 47274f89c87908ad510160083202c3825a880c18905Tomasz Mikolajewski openDocument(documentId, "r", signal), range[0], range[1], extras); 47374f89c87908ad510160083202c3825a880c18905Tomasz Mikolajewski } 47474f89c87908ad510160083202c3825a880c18905Tomasz Mikolajewski } catch (IOException e) { 47574f89c87908ad510160083202c3825a880c18905Tomasz Mikolajewski // Ignore the exception, as reading the EXIF may legally fail. 47674f89c87908ad510160083202c3825a880c18905Tomasz Mikolajewski Log.e(TAG, "Failed to obtain thumbnail from EXIF.", e); 477735fc5f89ffb4c2c9e96e370cda7208b222f0773Tomasz Mikolajewski } finally { 478735fc5f89ffb4c2c9e96e370cda7208b222f0773Tomasz Mikolajewski IoUtils.closeQuietly(inputStream); 47974f89c87908ad510160083202c3825a880c18905Tomasz Mikolajewski } 48074f89c87908ad510160083202c3825a880c18905Tomasz Mikolajewski 48109deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski return new AssetFileDescriptor( 48209deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski openDocument(documentId, "r", signal), 0, entry.getSize(), null); 48309deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski } 48409deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski 48509deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski /** 48664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * Schedules a gracefully close of the archive after any opened files are closed. 48764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * 48864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * <p>This method does not block until shutdown. Once called, other methods should not be 48964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski * called. 49064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski */ 49164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski @Override 49264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski public void close() { 49364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski mExecutor.execute(new Runnable() { 49464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski @Override 49564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski public void run() { 49664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski IoUtils.closeQuietly(mZipFile); 49764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski } 49864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski }); 49964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski mExecutor.shutdown(); 50064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski } 50164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski 50264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski private void addCursorRow(MatrixCursor cursor, ZipEntry entry) { 50364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski final MatrixCursor.RowBuilder row = cursor.newRow(); 50464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski final ParsedDocumentId parsedId = new ParsedDocumentId(mDocumentId, entry.getName()); 50564ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski row.add(Document.COLUMN_DOCUMENT_ID, parsedId.toDocumentId(mIdDelimiter)); 50609deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski 50764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski final File file = new File(entry.getName()); 50864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski row.add(Document.COLUMN_DISPLAY_NAME, file.getName()); 50964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski row.add(Document.COLUMN_SIZE, entry.getSize()); 51009deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski 51109deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski final String mimeType = getMimeTypeForEntry(entry); 51209deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski row.add(Document.COLUMN_MIME_TYPE, mimeType); 51309deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski 51409deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski final int flags = mimeType.startsWith("image/") ? Document.FLAG_SUPPORTS_THUMBNAIL : 0; 51509deb05d980153aa6d1a3696aca74f49d46caa94Tomasz Mikolajewski row.add(Document.COLUMN_FLAGS, flags); 51664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski } 51764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski 51864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski private String getMimeTypeForEntry(ZipEntry entry) { 51964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski if (entry.isDirectory()) { 52064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski return Document.MIME_TYPE_DIR; 52164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski } 52264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski 52364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski final int lastDot = entry.getName().lastIndexOf('.'); 52464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski if (lastDot >= 0) { 525aaf68419351d65048712c636cbe107c42cdb6844Tomasz Mikolajewski final String extension = entry.getName().substring(lastDot + 1).toLowerCase(Locale.US); 52664ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski final String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); 52764ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski if (mimeType != null) { 52864ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski return mimeType; 52964ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski } 53064ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski } 53164ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski 53264ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski return "application/octet-stream"; 53364ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski } 53464ce8c2e2085a0d5ff3e69ba5520873d41c76af5Tomasz Mikolajewski}; 535