Enable directory selection. Add an IntentService to copy files.
Change-Id: I0bec0224aa1b52766664c23f77d60affec702111
This commit is contained in:
@@ -65,5 +65,10 @@
|
||||
<data android:scheme="package" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<service
|
||||
android:name=".CopyService"
|
||||
android:exported="false">
|
||||
</service>
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
@@ -33,4 +33,8 @@
|
||||
android:id="@+id/menu_select_all"
|
||||
android:title="@string/menu_select_all"
|
||||
android:showAsAction="never" />
|
||||
<item
|
||||
android:id="@+id/menu_copy"
|
||||
android:title="@string/menu_copy"
|
||||
android:showAsAction="never" />
|
||||
</menu>
|
||||
|
||||
@@ -48,6 +48,8 @@
|
||||
<string name="menu_select">Select \"<xliff:g id="directory" example="My Directory">^1</xliff:g>\"</string>
|
||||
<!-- Menu item title that selects all documents in the current directory [CHAR LIMIT=24] -->
|
||||
<string name="menu_select_all">Select All</string>
|
||||
<!-- Menu item title that copies the selected documents [CHAR LIMIT=24] -->
|
||||
<string name="menu_copy">Copy to\u2026</string>
|
||||
|
||||
<!-- Menu item that reveals internal storage built into the device [CHAR LIMIT=24] -->
|
||||
<string name="menu_advanced_show" product="nosdcard">Show internal storage</string>
|
||||
@@ -110,4 +112,16 @@
|
||||
<!-- Title of dialog when prompting user to select an app to share documents with [CHAR LIMIT=32] -->
|
||||
<string name="share_via">Share via</string>
|
||||
|
||||
<!-- Title of the cancel button [CHAR LIMIT=24] -->
|
||||
<string name="cancel">Cancel</string>
|
||||
<!-- Title of the copy notification [CHAR LIMIT=24] -->
|
||||
<string name="copy_notification_title">Copying files</string>
|
||||
<!-- Text shown on the copy notification to indicate remaining time, in minutes [CHAR LIMIT=24] -->
|
||||
<string name="copy_remaining"><xliff:g id="duration" example="3 minutes">%s</xliff:g> left</string>
|
||||
<!-- Toast shown when a file copy is kicked off -->
|
||||
<plurals name="copy_begin">
|
||||
<item quantity="one">Copying <xliff:g id="count" example="1">%1$d</xliff:g> file.</item>
|
||||
<item quantity="other">Copying <xliff:g id="count" example="3">%1$d</xliff:g> files.</item>
|
||||
</plurals>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,7 @@ import android.os.Bundle;
|
||||
import android.os.CancellationSignal;
|
||||
import android.os.OperationCanceledException;
|
||||
import android.os.Parcelable;
|
||||
import android.os.SystemProperties;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.provider.DocumentsContract.Document;
|
||||
import android.text.format.DateUtils;
|
||||
@@ -77,6 +78,7 @@ import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.android.documentsui.BaseActivity.State;
|
||||
import com.android.documentsui.CopyService;
|
||||
import com.android.documentsui.ProviderExecutor.Preemptable;
|
||||
import com.android.documentsui.RecentsProvider.StateColumns;
|
||||
import com.android.documentsui.model.DocumentInfo;
|
||||
@@ -463,11 +465,14 @@ public class DirectoryFragment extends Fragment {
|
||||
final MenuItem open = menu.findItem(R.id.menu_open);
|
||||
final MenuItem share = menu.findItem(R.id.menu_share);
|
||||
final MenuItem delete = menu.findItem(R.id.menu_delete);
|
||||
final MenuItem copy = menu.findItem(R.id.menu_copy);
|
||||
|
||||
final boolean manageMode = state.action == ACTION_MANAGE;
|
||||
open.setVisible(!manageMode);
|
||||
share.setVisible(manageMode);
|
||||
delete.setVisible(manageMode);
|
||||
// Hide the copy feature by default.
|
||||
copy.setVisible(SystemProperties.getBoolean("debug.documentsui.enable_copy", false));
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -501,6 +506,11 @@ public class DirectoryFragment extends Fragment {
|
||||
mode.finish();
|
||||
return true;
|
||||
|
||||
} else if (id == R.id.menu_copy) {
|
||||
onCopyDocuments(docs);
|
||||
mode.finish();
|
||||
return true;
|
||||
|
||||
} else if (id == R.id.menu_select_all) {
|
||||
int count = mCurrentView.getCount();
|
||||
for (int i = 0; i < count; i++) {
|
||||
@@ -623,6 +633,20 @@ public class DirectoryFragment extends Fragment {
|
||||
}
|
||||
}
|
||||
|
||||
private void onCopyDocuments(List<DocumentInfo> docs) {
|
||||
final Context context = getActivity();
|
||||
final Resources res = context.getResources();
|
||||
|
||||
Intent copyIntent = new Intent(context, CopyService.class);
|
||||
copyIntent.putParcelableArrayListExtra(CopyService.EXTRA_SRC_LIST,
|
||||
new ArrayList<DocumentInfo>(docs));
|
||||
|
||||
Toast.makeText(context,
|
||||
res.getQuantityString(R.plurals.copy_begin, docs.size(), docs.size()),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
context.startService(copyIntent);
|
||||
}
|
||||
|
||||
private static State getDisplayState(Fragment fragment) {
|
||||
return ((BaseActivity) fragment.getActivity()).getDisplayState();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user