1
0
Fork 0
mirror of https://github.com/syncthing/syncthing-android.git synced 2024-11-30 00:01:19 +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:
Paul Annekov 2017-03-29 10:36:16 +03:00 committed by Felix Ableitner
parent 76b3ddad55
commit 6e1b689fa9
7 changed files with 442 additions and 0 deletions

View file

@ -50,6 +50,22 @@
android:name="android.support.PARENT_ACTIVITY" android:name="android.support.PARENT_ACTIVITY"
android:value=".activities.MainActivity" /> android:value=".activities.MainActivity" />
</activity> </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 <activity
android:name=".activities.LogActivity" android:name=".activities.LogActivity"
android:label="@string/log_title" android:label="@string/log_title"

View file

@ -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();
}
}
}

View file

@ -1,5 +1,7 @@
package com.nutomic.syncthingandroid.model; package com.nutomic.syncthingandroid.model;
import android.text.TextUtils;
import java.io.Serializable; import java.io.Serializable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
@ -60,4 +62,9 @@ public class Folder {
} }
} }
} }
@Override
public String toString() {
return !TextUtils.isEmpty(label) ? label : id;
}
} }

View 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>

View file

@ -421,6 +421,49 @@ Please report any problems you encounter via Github.</string>
<!-- Title of the "share log" menu button --> <!-- Title of the "share log" menu button -->
<string name="share_title">Share</string> <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 --> <!-- SyncthingService -->

View file

@ -58,4 +58,10 @@
</style> </style>
<style name="TextAppearance.Syncthing.ListItemSmall" parent="TextAppearance.AppCompat.Caption" /> <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> </resources>

View file

@ -19,6 +19,9 @@
<item name="drawerArrowStyle">@style/Widget.Syncthing.DrawerArrowToggle</item> <item name="drawerArrowStyle">@style/Widget.Syncthing.DrawerArrowToggle</item>
<item name="android:actionBarSize">@dimen/abc_action_bar_default_height_material</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>
<style name="Theme.Syncthing" parent="Theme.Syncthing.Base"/> <style name="Theme.Syncthing" parent="Theme.Syncthing.Base"/>