summaryrefslogtreecommitdiffstats
path: root/packages/DocumentsUI
diff options
context:
space:
mode:
authorBen Kwa <kenobi@google.com>2015-03-31 10:11:43 -0700
committerBen Kwa <kenobi@google.com>2015-04-03 13:49:24 -0700
commitd99109fca847895233b0bdfafa131ebca8dfe3d5 (patch)
treea72b11439c756a9137fb9b11144980d098dea1b5 /packages/DocumentsUI
parent3bcc9488a17b25e569c982e9e7596e6d34384c5b (diff)
downloadframeworks_base-d99109fca847895233b0bdfafa131ebca8dfe3d5.zip
frameworks_base-d99109fca847895233b0bdfafa131ebca8dfe3d5.tar.gz
frameworks_base-d99109fca847895233b0bdfafa131ebca8dfe3d5.tar.bz2
Enable directory selection. Add an IntentService to copy files.
Change-Id: I0bec0224aa1b52766664c23f77d60affec702111
Diffstat (limited to 'packages/DocumentsUI')
-rw-r--r--packages/DocumentsUI/AndroidManifest.xml5
-rw-r--r--packages/DocumentsUI/res/menu/mode_directory.xml4
-rw-r--r--packages/DocumentsUI/res/values/strings.xml14
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/CopyService.java280
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java24
5 files changed, 327 insertions, 0 deletions
diff --git a/packages/DocumentsUI/AndroidManifest.xml b/packages/DocumentsUI/AndroidManifest.xml
index 165b11e..a6f7a26 100644
--- a/packages/DocumentsUI/AndroidManifest.xml
+++ b/packages/DocumentsUI/AndroidManifest.xml
@@ -65,5 +65,10 @@
<data android:scheme="package" />
</intent-filter>
</receiver>
+
+ <service
+ android:name=".CopyService"
+ android:exported="false">
+ </service>
</application>
</manifest>
diff --git a/packages/DocumentsUI/res/menu/mode_directory.xml b/packages/DocumentsUI/res/menu/mode_directory.xml
index 695060d..4b89823 100644
--- a/packages/DocumentsUI/res/menu/mode_directory.xml
+++ b/packages/DocumentsUI/res/menu/mode_directory.xml
@@ -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>
diff --git a/packages/DocumentsUI/res/values/strings.xml b/packages/DocumentsUI/res/values/strings.xml
index 4ad337d..310ccf0 100644
--- a/packages/DocumentsUI/res/values/strings.xml
+++ b/packages/DocumentsUI/res/values/strings.xml
@@ -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>
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);
+ }
+ }
+ }
+}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
index a75dc42..d6d691f 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java
@@ -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();
}