From 6e1b689fa91d518c3a7183eceaa19e5465dc9e40 Mon Sep 17 00:00:00 2001 From: Paul Annekov Date: Wed, 29 Mar 2017 10:36:16 +0300 Subject: [PATCH] [WIP] Ability to share to syncthing (#843) * increased folder button size * increased folder button size * code formatting * init * added copy * get name from content provider * implemented file sharing, multiple sharing and display name getter * putting everything in order * cancel button added * ignore existing files, translation, styling * fixed file copy * Updated UI, moved copy to background, fixed content:// copying * Fixed possible crash when displayName == null * Fixed lint issues, removed ru translations * Fixed plural params * Escaped quotes to display them in Toast * Changed spinner design to material one * Changed copy code, moved credits to code, fixed members naming * Fallback to id if label is not set * Added doc for some methods * Removed try-catch as there are no exceptions here * Removed try-catch as there are no exceptions here * Fixed general exception type * Removed ShareActivity from recents * Moved style to appropriate place * Small fixes for toString() * Moved credits from comment to javadoc of the class --- src/main/AndroidManifest.xml | 16 + .../activities/ShareActivity.java | 276 ++++++++++++++++++ .../syncthingandroid/model/Folder.java | 7 + src/main/res/layout/activity_share.xml | 91 ++++++ src/main/res/values/strings.xml | 43 +++ src/main/res/values/styles.xml | 6 + src/main/res/values/themes.xml | 3 + 7 files changed, 442 insertions(+) create mode 100644 src/main/java/com/nutomic/syncthingandroid/activities/ShareActivity.java create mode 100644 src/main/res/layout/activity_share.xml diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index 163340a8..6fa78593 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -50,6 +50,22 @@ android:name="android.support.PARENT_ACTIVITY" android:value=".activities.MainActivity" /> + + + + + + + + + + + + folders = getApi().getFolders(); + + ArrayAdapter adapter = new ArrayAdapter<>( + this, android.R.layout.simple_spinner_item, folders); + + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + Spinner sItems = (Spinner) findViewById(R.id.folders); + sItems.setAdapter(adapter); + } + + @Override + public void onServiceConnected() { + getService().registerOnApiChangeListener(this); + } + + @Override + protected void onPostCreate(Bundle savedInstanceState) { + super.onPostCreate(savedInstanceState); + + //noinspection ConstantConditions + getSupportActionBar().setDisplayHomeAsUpEnabled(false); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_share); + + registerOnServiceConnectedListener(this); + + Spinner mFoldersSpinner = (Spinner) findViewById(R.id.folders); + Button mShareButton = (Button) findViewById(R.id.share_button); + Button mCancelButton = (Button) findViewById(R.id.cancel_button); + EditText mShareName = (EditText) findViewById(R.id.name); + TextView mShareTitle = (TextView) findViewById(R.id.namesTitle); + + // TODO: add support for EXTRA_TEXT (notes, memos sharing) + ArrayList extrasToCopy = new ArrayList<>(); + if (getIntent().getAction().equals(Intent.ACTION_SEND)) { + Uri uri = getIntent().getParcelableExtra(Intent.EXTRA_STREAM); + if (uri != null) + extrasToCopy.add(uri); + } else if (getIntent().getAction().equals(Intent.ACTION_SEND_MULTIPLE)) { + ArrayList extras = getIntent().getParcelableArrayListExtra(Intent.EXTRA_STREAM); + if (extras != null) + extrasToCopy = extras; + } + + if (extrasToCopy.isEmpty()) { + Toast.makeText(this, getString(R.string.nothing_share), Toast.LENGTH_SHORT).show(); + finish(); + } + + Map files = new HashMap<>(); + for (Uri sourceUri : extrasToCopy) { + String displayName = getDisplayNameForUri(sourceUri); + if (displayName == null) { + displayName = generateDisplayName(); + } + files.put(sourceUri, displayName); + } + + mShareName.setText(TextUtils.join("\n", files.values())); + if (files.size() > 1) { + mShareName.setFocusable(false); + mShareName.setKeyListener(null); + } + mShareTitle.setText(getResources().getQuantityString(R.plurals.file_name_title, + files.size() > 1 ? 2 : 1)); + mShareButton.setOnClickListener(view -> { + if (files.size() == 1) + files.entrySet().iterator().next().setValue(mShareName.getText().toString()); + Folder folder = (Folder) mFoldersSpinner.getSelectedItem(); + new CopyFilesTask(files, folder).execute(); + }); + mCancelButton.setOnClickListener(view -> finish()); + } + + /** + * Generate file name for new file. + */ + private String generateDisplayName() { + Date date = new Date(System.currentTimeMillis()); + DateFormat df = DateFormat.getDateTimeInstance(); + return String.format(getResources().getString(R.string.file_name_template), + df.format(date)); + } + + /** + * Get file name from uri. + */ + private String getDisplayNameForUri(Uri uri) { + String displayName; + + if (!ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) { + displayName = uri.getLastPathSegment(); + } else { + displayName = getDisplayNameFromContentResolver(uri); + if (displayName == null) { + // last chance to have a name + displayName = uri.getLastPathSegment().replaceAll("\\s", ""); + } + + // Add best possible extension + int index = displayName.lastIndexOf("."); + if (index == -1 || MimeTypeMap.getSingleton() + .getMimeTypeFromExtension(displayName.substring(index + 1)) == null) { + String mimeType = this.getContentResolver().getType(uri); + String extension = MimeTypeMap.getSingleton() + .getExtensionFromMimeType(mimeType); + if (extension != null) { + displayName += "." + extension; + } + } + } + + // Replace path separator characters to avoid inconsistent paths + return displayName != null ? displayName.replaceAll("/", "-") : null; + } + + /** + * Get file name from content uri (content://). + */ + private String getDisplayNameFromContentResolver(Uri uri) { + String displayName = null; + String mimeType = getContentResolver().getType(uri); + if (mimeType != null) { + String displayNameColumn; + if (mimeType.startsWith("image/")) { + displayNameColumn = MediaStore.Images.ImageColumns.DISPLAY_NAME; + } else if (mimeType.startsWith("video/")) { + displayNameColumn = MediaStore.Video.VideoColumns.DISPLAY_NAME; + + } else if (mimeType.startsWith("audio/")) { + displayNameColumn = MediaStore.Audio.AudioColumns.DISPLAY_NAME; + + } else { + displayNameColumn = MediaStore.Files.FileColumns.DISPLAY_NAME; + } + + Cursor cursor = getContentResolver().query( + uri, + new String[]{displayNameColumn}, + null, + null, + null + ); + if (cursor != null) { + cursor.moveToFirst(); + displayName = cursor.getString(cursor.getColumnIndex(displayNameColumn)); + } + if (cursor != null) { + cursor.close(); + } + } + return displayName; + } + + private class CopyFilesTask extends AsyncTask { + private ProgressDialog mProgress; + private Map mFiles; + private Folder mFolder; + private int mCopied = 0, mIgnored = 0; + + CopyFilesTask(Map files, Folder folder) { + this.mFiles = files; + this.mFolder = folder; + } + + protected void onPreExecute() { + mProgress = ProgressDialog.show(ShareActivity.this, null, + getString(R.string.copy_progress), true); + } + + protected Boolean doInBackground(Void... params) { + boolean isError = false; + for (Map.Entry entry : mFiles.entrySet()) { + InputStream inputStream = null; + String outPath = mFolder.path + entry.getValue(); + try { + File outFile = new File(outPath); + if (outFile.isFile()) { + mIgnored++; + continue; + } + inputStream = getContentResolver().openInputStream(entry.getKey()); + Files.asByteSink(outFile).writeFrom(inputStream); + mCopied++; + } catch (FileNotFoundException e) { + Log.e(TAG, String.format("Can't find input file \"%s\" to copy", + entry.getKey()), e); + isError = true; + } catch (IOException e) { + Log.e(TAG, String.format("IO exception during file \"%s\" sharing", + entry.getKey()), e); + isError = true; + } finally { + try { + if (inputStream != null) + inputStream.close(); + } catch (IOException e) { + Log.w(TAG, "Exception on input/output stream close", e); + } + } + } + return isError; + } + + protected void onPostExecute(Boolean isError) { + mProgress.dismiss(); + Toast.makeText(ShareActivity.this, mIgnored > 0 ? + getResources().getQuantityString(R.plurals.copy_success_partially, mCopied, + mCopied, mFolder.label, mIgnored) : + getResources().getQuantityString(R.plurals.copy_success, mCopied, mCopied, + mFolder.label), + Toast.LENGTH_LONG).show(); + if (isError) { + Toast.makeText(ShareActivity.this, getString(R.string.copy_exception), + Toast.LENGTH_SHORT).show(); + } + finish(); + } + } +} diff --git a/src/main/java/com/nutomic/syncthingandroid/model/Folder.java b/src/main/java/com/nutomic/syncthingandroid/model/Folder.java index 33f5e8df..db1e0911 100644 --- a/src/main/java/com/nutomic/syncthingandroid/model/Folder.java +++ b/src/main/java/com/nutomic/syncthingandroid/model/Folder.java @@ -1,5 +1,7 @@ package com.nutomic.syncthingandroid.model; +import android.text.TextUtils; + import java.io.Serializable; import java.util.ArrayList; import java.util.HashMap; @@ -60,4 +62,9 @@ public class Folder { } } } + + @Override + public String toString() { + return !TextUtils.isEmpty(label) ? label : id; + } } diff --git a/src/main/res/layout/activity_share.xml b/src/main/res/layout/activity_share.xml new file mode 100644 index 00000000..24f251c2 --- /dev/null +++ b/src/main/res/layout/activity_share.xml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + +