1 /*
2  * Copyright (C) 2017 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.services;
18 
19 import static android.content.ContentResolver.wrap;
20 
21 import static com.android.documentsui.services.FileOperationService.OPERATION_MOVE;
22 
23 import android.app.Notification;
24 import android.app.Notification.Builder;
25 import android.content.ContentResolver;
26 import android.content.Context;
27 import android.net.Uri;
28 import android.os.Messenger;
29 import android.os.ParcelFileDescriptor;
30 import android.os.RemoteException;
31 import android.provider.DocumentsContract;
32 import android.util.Log;
33 
34 import com.android.documentsui.R;
35 import com.android.documentsui.archives.ArchivesProvider;
36 import com.android.documentsui.base.DocumentInfo;
37 import com.android.documentsui.base.DocumentStack;
38 import com.android.documentsui.base.Features;
39 import com.android.documentsui.base.UserId;
40 import com.android.documentsui.clipping.UrisSupplier;
41 
42 import java.io.FileNotFoundException;
43 
44 // TODO: Stop extending CopyJob.
45 final class CompressJob extends CopyJob {
46 
47     private static final String TAG = "CompressJob";
48     private static final String NEW_ARCHIVE_EXTENSION = ".zip";
49 
50     private Uri mArchiveUri;
51 
52     /**
53      * Moves files to a destination identified by {@code destination}.
54      * Performs most work by delegating to CopyJob, then deleting
55      * a file after it has been copied.
56      *
57      * @see @link {@link Job} constructor for most param descriptions.
58      */
CompressJob(Context service, Listener listener, String id, DocumentStack destination, UrisSupplier srcs, Messenger messenger, Features features)59     CompressJob(Context service, Listener listener, String id, DocumentStack destination,
60             UrisSupplier srcs, Messenger messenger, Features features) {
61         super(service, listener, id, OPERATION_MOVE, destination, srcs, messenger, features);
62     }
63 
64     @Override
createProgressBuilder()65     Builder createProgressBuilder() {
66         return super.createProgressBuilder(
67                 service.getString(R.string.compress_notification_title),
68                 R.drawable.ic_menu_compress,
69                 service.getString(android.R.string.cancel),
70                 R.drawable.ic_cab_cancel);
71     }
72 
73     @Override
getSetupNotification()74     public Notification getSetupNotification() {
75         return getSetupNotification(service.getString(R.string.compress_preparing));
76     }
77 
78     @Override
getProgressNotification()79     public Notification getProgressNotification() {
80         return getProgressNotification(R.string.copy_remaining);
81     }
82 
83     @Override
getFailureNotification()84     Notification getFailureNotification() {
85         return getFailureNotification(
86                 R.plurals.compress_error_notification_title, R.drawable.ic_menu_compress);
87     }
88 
89     @Override
setUp()90     public boolean setUp() {
91         if (!super.setUp()) {
92             return false;
93         }
94 
95         final ContentResolver resolver = appContext.getContentResolver();
96 
97         // TODO: Move this to DocumentsProvider.
98 
99         String displayName;
100         if (mResolvedDocs.size() == 1) {
101             displayName = mResolvedDocs.get(0).displayName + NEW_ARCHIVE_EXTENSION;
102         } else {
103             displayName = service.getString(R.string.new_archive_file_name, NEW_ARCHIVE_EXTENSION);
104         }
105 
106         try {
107             mArchiveUri = DocumentsContract.createDocument(
108                     resolver, mDstInfo.derivedUri, "application/zip", displayName);
109         } catch (Exception e) {
110             mArchiveUri = null;
111         }
112 
113         try {
114             mDstInfo = DocumentInfo.fromUri(resolver, ArchivesProvider.buildUriForArchive(
115                     mArchiveUri, ParcelFileDescriptor.MODE_WRITE_ONLY), UserId.DEFAULT_USER);
116             ArchivesProvider.acquireArchive(getClient(mDstInfo), mDstInfo.derivedUri);
117         } catch (FileNotFoundException e) {
118             Log.e(TAG, "Failed to create dstInfo.", e);
119             failureCount = mResourceUris.getItemCount();
120             return false;
121         } catch (RemoteException e) {
122             Log.e(TAG, "Failed to acquire the archive.", e);
123             failureCount = mResourceUris.getItemCount();
124             return false;
125         }
126 
127         return true;
128     }
129 
130     @Override
finish()131     void finish() {
132         try {
133             ArchivesProvider.releaseArchive(getClient(mDstInfo), mDstInfo.derivedUri);
134         } catch (RemoteException e) {
135             Log.e(TAG, "Failed to release the archive.");
136         }
137 
138         // Remove the archive file in case of an error.
139         try {
140             if (!isFinished() || isCanceled()) {
141                 DocumentsContract.deleteDocument(wrap(getClient(mArchiveUri)), mArchiveUri);
142             }
143         } catch (RemoteException | FileNotFoundException e) {
144             Log.w(TAG, "Failed to cleanup after compress error: " + mDstInfo.toString(), e);
145         }
146 
147         super.finish();
148     }
149 
150     /**
151      * {@inheritDoc}
152      *
153      * Only check space for moves across authorities. For now we don't know if the doc in
154      * {@link #mSrcs} is in the same root of destination, and if it's optimized move in the same
155      * root it should succeed regardless of free space, but it's for sure a failure if there is no
156      * enough free space if docs are moved from another authority.
157      */
158     @Override
checkSpace()159     boolean checkSpace() {
160         // We're unable to say how much space the archive will take, so assume
161         // it will fit.
162         return true;
163     }
164 
processDocument(DocumentInfo src, DocumentInfo dest)165     void processDocument(DocumentInfo src, DocumentInfo dest) throws ResourceException {
166         byteCopyDocument(src, dest);
167     }
168 
169     @Override
toString()170     public String toString() {
171         return new StringBuilder()
172                 .append("CompressJob")
173                 .append("{")
174                 .append("id=" + id)
175                 .append(", uris=" + mResourceUris)
176                 .append(", docs=" + mResolvedDocs)
177                 .append(", destination=" + stack)
178                 .append("}")
179                 .toString();
180     }
181 }
182