diff options
Diffstat (limited to 'packages/DocumentsUI/src/com/android/documentsui/CopyService.java')
| -rw-r--r-- | packages/DocumentsUI/src/com/android/documentsui/CopyService.java | 280 |
1 files changed, 280 insertions, 0 deletions
diff --git a/packages/DocumentsUI/src/com/android/documentsui/CopyService.java b/packages/DocumentsUI/src/com/android/documentsui/CopyService.java new file mode 100644 index 0000000..f7d8cc4 --- /dev/null +++ b/packages/DocumentsUI/src/com/android/documentsui/CopyService.java @@ -0,0 +1,280 @@ +/* + * Copyright (C) 2015 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; + +import android.app.IntentService; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.SystemClock; +import android.text.format.DateUtils; +import android.util.Log; + +import com.android.documentsui.model.DocumentInfo; + +import libcore.io.IoUtils; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.text.NumberFormat; +import java.util.ArrayList; + +public class CopyService extends IntentService { + public static final String TAG = "CopyService"; + public static final String EXTRA_SRC_LIST = "com.android.documentsui.SRC_LIST"; + private static final String EXTRA_CANCEL = "com.android.documentsui.CANCEL"; + + private NotificationManager mNotificationManager; + private Notification.Builder mProgressBuilder; + + // Jobs are serialized but a job ID is used, to avoid mixing up cancellation requests. + private String mJobId; + private volatile boolean mIsCancelled; + // Parameters of the copy job. Requests to an IntentService are serialized so this code only + // needs to deal with one job at a time. + private long mBatchSize; + private long mBytesCopied; + private long mStartTime; + private long mLastNotificationTime; + // Speed estimation + private long mBytesCopiedSample; + private long mSampleTime; + private long mSpeed; + private long mRemainingTime; + + public CopyService() { + super("CopyService"); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if (intent.hasExtra(EXTRA_CANCEL)) { + handleCancel(intent); + } + return super.onStartCommand(intent, flags, startId); + } + + @Override + protected void onHandleIntent(Intent intent) { + if (intent.hasExtra(EXTRA_CANCEL)) { + handleCancel(intent); + return; + } + + ArrayList<DocumentInfo> srcs = intent.getParcelableArrayListExtra(EXTRA_SRC_LIST); + // Use the app local files dir as a copy destination for now. This resolves to + // /data/data/com.android.documentsui/files. + // TODO: Add actual destination picking. + File destinationDir = getFilesDir(); + + setupCopyJob(srcs, destinationDir); + + ArrayList<String> failedFilenames = new ArrayList<String>(); + for (int i = 0; i < srcs.size() && !mIsCancelled; ++i) { + DocumentInfo src = srcs.get(i); + try { + copyFile(src, destinationDir); + } catch (IOException e) { + Log.e(TAG, "Failed to copy " + src.displayName, e); + failedFilenames.add(src.displayName); + } + } + + if (failedFilenames.size() > 0) { + // TODO: Display a notification when an error has occurred. + } + + // Dismiss the ongoing copy notification when the copy is done. + mNotificationManager.cancel(mJobId, 0); + + // TODO: Display a toast if the copy was cancelled. + } + + @Override + public void onCreate() { + super.onCreate(); + mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + } + + /** + * Sets up the CopyService to start tracking and sending notifications for the given batch of + * files. + * + * @param srcs A list of src files to copy. + */ + private void setupCopyJob(ArrayList<DocumentInfo> srcs, File destinationDir) { + // Create an ID for this copy job. Use the timestamp. + mJobId = String.valueOf(SystemClock.elapsedRealtime()); + // Reset the cancellation flag. + mIsCancelled = false; + + mProgressBuilder = new Notification.Builder(this) + .setContentTitle(getString(R.string.copy_notification_title)) + .setCategory(Notification.CATEGORY_PROGRESS) + .setSmallIcon(R.drawable.ic_menu_copy).setOngoing(true); + + Intent cancelIntent = new Intent(this, CopyService.class); + cancelIntent.putExtra(EXTRA_CANCEL, mJobId); + mProgressBuilder.addAction(R.drawable.ic_cab_cancel, + getString(R.string.cancel), PendingIntent.getService(this, 0, + cancelIntent, PendingIntent.FLAG_ONE_SHOT)); + + // TODO: Add a content intent to open the destination folder. + + // Send an initial progress notification. + mNotificationManager.notify(mJobId, 0, mProgressBuilder.build()); + + // Reset batch parameters. + mBatchSize = 0; + for (DocumentInfo doc : srcs) { + mBatchSize += doc.size; + } + mBytesCopied = 0; + mStartTime = SystemClock.elapsedRealtime(); + mLastNotificationTime = 0; + mBytesCopiedSample = 0; + mSampleTime = 0; + mSpeed = 0; + mRemainingTime = 0; + + // TODO: Check preconditions for copy. + // - check that the destination has enough space and is writeable? + // - check MIME types? + } + + /** + * Cancels the current copy job, if its ID matches the given ID. + * + * @param intent The cancellation intent. + */ + private void handleCancel(Intent intent) { + final String cancelledId = intent.getStringExtra(EXTRA_CANCEL); + // Do nothing if the cancelled ID doesn't match the current job ID. This prevents racey + // cancellation requests from affecting unrelated copy jobs. + if (java.util.Objects.equals(mJobId, cancelledId)) { + // Set the cancel flag. This causes the copy loops to exit. + mIsCancelled = true; + // Dismiss the progress notification here rather than in the copy loop. This preserves + // interactivity for the user in case the copy loop is stalled. + mNotificationManager.cancel(mJobId, 0); + } + } + + /** + * Logs progress on the current copy operation. Displays/Updates the progress notification. + * + * @param bytesCopied + */ + private void makeProgress(long bytesCopied) { + mBytesCopied += bytesCopied; + double done = (double) mBytesCopied / mBatchSize; + String percent = NumberFormat.getPercentInstance().format(done); + + // Update time estimate + long currentTime = SystemClock.elapsedRealtime(); + long elapsedTime = currentTime - mStartTime; + + // Send out progress notifications once a second. + if (currentTime - mLastNotificationTime > 1000) { + updateRemainingTimeEstimate(elapsedTime); + mProgressBuilder.setProgress(100, (int) (done * 100), false); + mProgressBuilder.setContentInfo(percent); + if (mRemainingTime > 0) { + mProgressBuilder.setContentText(getString(R.string.copy_remaining, + DateUtils.formatDuration(mRemainingTime))); + } else { + mProgressBuilder.setContentText(null); + } + mNotificationManager.notify(mJobId, 0, mProgressBuilder.build()); + mLastNotificationTime = currentTime; + } + } + + /** + * Generates an estimate of the remaining time in the copy. + * + * @param elapsedTime The time elapsed so far. + */ + private void updateRemainingTimeEstimate(long elapsedTime) { + final long sampleDuration = elapsedTime - mSampleTime; + final long sampleSpeed = ((mBytesCopied - mBytesCopiedSample) * 1000) / sampleDuration; + if (mSpeed == 0) { + mSpeed = sampleSpeed; + } else { + mSpeed = ((3 * mSpeed) + sampleSpeed) / 4; + } + + if (mSampleTime > 0 && mSpeed > 0) { + mRemainingTime = ((mBatchSize - mBytesCopied) * 1000) / mSpeed; + } else { + mRemainingTime = 0; + } + + mSampleTime = elapsedTime; + mBytesCopiedSample = mBytesCopied; + } + + /** + * Copies a file to a given location. + * + * @param srcInfo The source file. + * @param destination The directory to copy into. + * @throws IOException + */ + private void copyFile(DocumentInfo srcInfo, File destinationDir) + throws IOException { + final Context context = getApplicationContext(); + final ContentResolver resolver = context.getContentResolver(); + final File destinationFile = new File(destinationDir, srcInfo.displayName); + final Uri destinationUri = Uri.fromFile(destinationFile); + + InputStream source = null; + OutputStream destination = null; + + boolean errorOccurred = false; + try { + source = resolver.openInputStream(srcInfo.derivedUri); + destination = resolver.openOutputStream(destinationUri); + + byte[] buffer = new byte[8192]; + int len; + while (!mIsCancelled && ((len = source.read(buffer)) != -1)) { + destination.write(buffer, 0, len); + makeProgress(len); + } + } catch (IOException e) { + errorOccurred = true; + Log.e(TAG, "Error while copying " + srcInfo.displayName, e); + } finally { + IoUtils.closeQuietly(source); + IoUtils.closeQuietly(destination); + } + + if (errorOccurred || mIsCancelled) { + // Clean up half-copied files. + if (!destinationFile.delete()) { + Log.w(TAG, "Failed to clean up partially copied file " + srcInfo.displayName); + } + } + } +} |
