mirror of
https://github.com/syncthing/syncthing-android.git
synced 2024-12-27 05:11:30 +00:00
[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
This commit is contained in:
parent
76b3ddad55
commit
6e1b689fa9
7 changed files with 442 additions and 0 deletions
|
@ -50,6 +50,22 @@
|
|||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".activities.MainActivity" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".activities.ShareActivity"
|
||||
android:label="@string/share_activity_title"
|
||||
android:excludeFromRecents="true"
|
||||
android:taskAffinity="">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="*/*" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="*/*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".activities.LogActivity"
|
||||
android:label="@string/log_title"
|
||||
|
|
|
@ -0,0 +1,276 @@
|
|||
package com.nutomic.syncthingandroid.activities;
|
||||
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.provider.MediaStore;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.webkit.MimeTypeMap;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.google.common.io.Files;
|
||||
import com.nutomic.syncthingandroid.R;
|
||||
import com.nutomic.syncthingandroid.model.Folder;
|
||||
import com.nutomic.syncthingandroid.service.SyncthingService;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.text.DateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Shares incoming files to syncthing folders.
|
||||
*
|
||||
* {@link #getDisplayNameForUri} and {@link #getDisplayNameFromContentResolver} are taken from
|
||||
* ownCloud Android {@see https://github.com/owncloud/android/blob/79664304fdb762b2e04f1ac505f50d0923ddd212/src/com/owncloud/android/utils/UriUtils.java#L193}
|
||||
*/
|
||||
public class ShareActivity extends SyncthingActivity
|
||||
implements SyncthingActivity.OnServiceConnectedListener, SyncthingService.OnApiChangeListener {
|
||||
|
||||
private static final String TAG = "ShareActivity";
|
||||
|
||||
@Override
|
||||
public void onApiChange(SyncthingService.State currentState) {
|
||||
if (currentState != SyncthingService.State.ACTIVE || getApi() == null)
|
||||
return;
|
||||
|
||||
List<Folder> folders = getApi().getFolders();
|
||||
|
||||
ArrayAdapter<Folder> 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<Uri> 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<Uri> 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<Uri, String> 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<Void, Void, Boolean> {
|
||||
private ProgressDialog mProgress;
|
||||
private Map<Uri, String> mFiles;
|
||||
private Folder mFolder;
|
||||
private int mCopied = 0, mIgnored = 0;
|
||||
|
||||
CopyFilesTask(Map<Uri, String> 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<Uri, String> 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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
91
src/main/res/layout/activity_share.xml
Normal file
91
src/main/res/layout/activity_share.xml
Normal file
|
@ -0,0 +1,91 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:context="com.nutomic.syncthingandroid.activities.ShareActivity"
|
||||
android:id="@+id/drawer_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<include layout="@layout/widget_toolbar" />
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="8dp"
|
||||
android:paddingLeft="@dimen/abc_action_bar_content_inset_material"
|
||||
android:paddingRight="@dimen/abc_action_bar_content_inset_material"
|
||||
android:paddingTop="8dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/namesTitle"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_marginTop="8dp" />
|
||||
|
||||
<EditText
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ems="10"
|
||||
android:id="@+id/name"
|
||||
android:inputType="text|textMultiLine"
|
||||
android:layout_below="@+id/namesTitle"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true" />
|
||||
|
||||
<TextView
|
||||
android:text="@string/folder_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/folder_title"
|
||||
android:layout_below="@+id/name"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_marginTop="8dp" />
|
||||
|
||||
<Spinner
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/folders"
|
||||
android:layout_below="@+id/folder_title"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_marginTop="8dp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/share_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/save_title"
|
||||
android:background="?android:selectableItemBackground"
|
||||
android:textColor="@color/accent"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:textSize="14sp"
|
||||
android:minWidth="60dip" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/cancel_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/cancel_title"
|
||||
android:background="?android:selectableItemBackground"
|
||||
android:textColor="@color/accent"
|
||||
android:minWidth="50dip"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_toLeftOf="@+id/share_button"
|
||||
android:layout_toStartOf="@+id/share_button"
|
||||
android:layout_marginRight="14dp"
|
||||
android:layout_marginEnd="14dp" />
|
||||
|
||||
</RelativeLayout>
|
||||
</LinearLayout>
|
||||
</android.support.v4.widget.DrawerLayout>
|
|
@ -421,6 +421,49 @@ Please report any problems you encounter via Github.</string>
|
|||
<!-- Title of the "share log" menu button -->
|
||||
<string name="share_title">Share</string>
|
||||
|
||||
<!-- ShareActivity -->
|
||||
|
||||
|
||||
<!-- Title of the "share" activity -->
|
||||
<string name="share_activity_title">Save to Syncthing</string>
|
||||
|
||||
<!-- Title of the "save" button -->
|
||||
<string name="save_title">Save</string>
|
||||
|
||||
<!-- Title of the "cancel" button -->
|
||||
<string name="cancel_title">Cancel</string>
|
||||
|
||||
<!-- Title of the "save" button -->
|
||||
<string name="folder_title">Folder</string>
|
||||
|
||||
<!-- New file name template -->
|
||||
<string name="file_name_template">New file %1$s</string>
|
||||
|
||||
<!-- Nothing to share toast -->
|
||||
<string name="nothing_share">Nothing to share</string>
|
||||
|
||||
<!-- Copy success toast partially -->
|
||||
<plurals name="copy_success_partially">
|
||||
<item quantity="one">%1$d file copied to folder \"%2$s\", %3$d already exist</item>
|
||||
<item quantity="other">%1$d files copied to folder \"%2$s\", %3$d already exist</item>
|
||||
</plurals>
|
||||
|
||||
<!-- Copy success toast -->
|
||||
<plurals name="copy_success">
|
||||
<item quantity="one">%1$d file copied to folder \"%2$s\"</item>
|
||||
<item quantity="other">%1$d files copied to folder \"%2$s\"</item>
|
||||
</plurals>
|
||||
|
||||
<!-- Copy exception toast -->
|
||||
<string name="copy_exception">There was an error during sharing, check application logs</string>
|
||||
|
||||
<!-- Copy progress dialog text -->
|
||||
<string name="copy_progress">Sharing files…</string>
|
||||
|
||||
<plurals name="file_name_title">
|
||||
<item quantity="one">File Name</item>
|
||||
<item quantity="other">Files List</item>
|
||||
</plurals>
|
||||
|
||||
<!-- SyncthingService -->
|
||||
|
||||
|
|
|
@ -58,4 +58,10 @@
|
|||
</style>
|
||||
|
||||
<style name="TextAppearance.Syncthing.ListItemSmall" parent="TextAppearance.AppCompat.Caption" />
|
||||
|
||||
<style name="Widget.Syncthing.TextView.SpinnerItem" parent="Widget.AppCompat.TextView.SpinnerItem">
|
||||
<item name="android:textSize">18sp</item>
|
||||
<item name="android:paddingTop">5dp</item>
|
||||
<item name="android:paddingBottom">5dp</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
|
|
@ -19,6 +19,9 @@
|
|||
<item name="drawerArrowStyle">@style/Widget.Syncthing.DrawerArrowToggle</item>
|
||||
|
||||
<item name="android:actionBarSize">@dimen/abc_action_bar_default_height_material</item>
|
||||
|
||||
<item name="android:spinnerStyle">@style/Widget.AppCompat.Spinner.Underlined</item>
|
||||
<item name="android:spinnerItemStyle">@style/Widget.Syncthing.TextView.SpinnerItem</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Syncthing" parent="Theme.Syncthing.Base"/>
|
||||
|
|
Loading…
Reference in a new issue