This commit is contained in:
aleph-null
2016-12-15 21:24:06 +01:00
parent 353fc4f9e9
commit 738aaf3ea9
43 changed files with 1865 additions and 1 deletions
+23 -1
View File
@@ -1,2 +1,24 @@
# a-sync-browser
A Sync Browser: a browser app for Syncthing-compatible shares
[![MPLv2 License](https://img.shields.io/badge/license-MPLv2-blue.svg?style=flat-square)](https://www.mozilla.org/MPL/2.0/)
This project is an android app, that works as a client for a [Syncthing][1] share (accessing syncthing devices in the same way a client-server file sharing app access its proprietary server).
This project is based on [a-sync][2], a java implementation of Syncthing protocols (bep, discovery, relay).
NOTE: this is a client-oriented implementation, designed to work online by downloading and uploading files from an active device on the network (instead of synchronizing a local copy of the entire repository). This is quite different from the way the original Syncthing app works, and its useful from those devices that cannot or wish not to download the entire repository (for example, mobile devices with limited storage available, wishing to access a syncthing share).
DISCLAMER: I'm not the owner fo Syncthing. The Syncthing name, logo and reference documentation is property of the Syncthing team, and licensed under the MPLv2 License. This project is not affiliated with Syncthing, and uses its public avaliable documentation and protocol specification as a reference to implement protocol compatibility.
Documentation is non-existing as know, if you have any question feel free to contact the developer (me).
All code is licensed under the [MPLv2 License][3].
If you like this work, please donate something to help its development!
[![paypal](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=FVB93HNCH5NL8)
[1]: https://syncthing.net/
[2]: https://github.com/davide-imbriaco/a-sync
[3]: https://github.com/davide-imbriaco/a-sync-browser/blob/master/LICENSE
+39
View File
@@ -0,0 +1,39 @@
apply plugin: 'com.android.application'
android {
compileSdkVersion 25
buildToolsVersion "25.0.0"
defaultConfig {
applicationId "it.anyplace.syncbrowser"
minSdkVersion 19
targetSdkVersion 25
versionCode 1
versionName "1.0"
jackOptions {
enabled true
}
multiDexEnabled true
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:25.0.1'
compile ('it.anyplace.sync:a-sync-client:1.0') {
exclude group: 'commons-logging', module:'commons-logging'
exclude group: 'commons-cli'
exclude group: 'commons-codec'
exclude group: 'org.apache.httpcomponents', module:'httpclient'
exclude group: 'org.slf4j'
exclude group: 'ch.qos.logback'
}
compile 'org.apache.httpcomponents:httpclient-android:4.3.5.1'
compile 'org.slf4j:slf4j-android:1.7.13'
compile 'com.google.zxing:android-integration:3.3.0'
compile 'com.nononsenseapps:filepicker:2.5.2'
}
@@ -0,0 +1,26 @@
package it.anyplace.syncbrowser;
import android.content.Context;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
/**
* Instrumentation test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() throws Exception {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getTargetContext();
assertEquals("it.anyplace.syncthingbrowser", appContext.getPackageName());
}
}
+42
View File
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="it.anyplace.syncbrowser">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:banner="@drawable/banner"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/Theme.Syncthing">
<activity android:name="it.anyplace.syncbrowser.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<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=".filepicker.MIVFilePickerActivity"
android:label="@string/app_name"
android:theme="@style/FilePickerTheme">
<intent-filter>
<action android:name="android.intent.action.GET_CONTENT" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
</application>
</manifest>
Binary file not shown.
@@ -0,0 +1,968 @@
package it.anyplace.syncbrowser;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.ActivityNotFoundException;
import android.content.DialogInterface;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.Typeface;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Environment;
import android.provider.MediaStore;
import android.support.annotation.NonNull;
import android.support.v4.widget.DrawerLayout;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.text.format.DateFormat;
import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.MimeTypeMap;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Strings;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Ordering;
import com.google.common.collect.Sets;
import com.google.common.eventbus.Subscribe;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.google.zxing.integration.android.IntentIntegrator;
import com.google.zxing.integration.android.IntentResult;
import com.nononsenseapps.filepicker.FilePickerActivity;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.tuple.Pair;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Set;
import javax.annotation.Nullable;
import it.anyplace.sync.bep.BlockPuller;
import it.anyplace.sync.bep.BlockPusher;
import it.anyplace.sync.bep.FolderBrowser;
import it.anyplace.sync.bep.IndexBrowser;
import it.anyplace.sync.bep.IndexHandler;
import it.anyplace.sync.client.SyncthingClient;
import it.anyplace.sync.core.Configuration;
import it.anyplace.sync.core.beans.FileInfo;
import it.anyplace.sync.core.beans.FolderInfo;
import it.anyplace.sync.core.beans.FolderStats;
import it.anyplace.sync.core.beans.IndexInfo;
import it.anyplace.sync.core.security.KeystoreHandler;
import it.anyplace.sync.core.utils.PathUtils;
import it.anyplace.syncbrowser.filepicker.MIVFilePickerActivity;
import static com.google.common.base.Objects.equal;
import static com.google.common.base.Preconditions.checkArgument;
import static org.apache.commons.io.FileUtils.getFile;
import static org.apache.commons.lang3.StringUtils.capitalize;
import static org.apache.commons.lang3.StringUtils.isBlank;
public class MainActivity extends AppCompatActivity {
private Configuration configuration;
private SyncthingClient syncthingClient;
private Exception statupError;
private void closeClient(){
if(indexBrowser!=null){
indexBrowser.close();
indexBrowser=null;
}
if(folderBrowser!=null){
folderBrowser.close();
folderBrowser=null;
}
if(syncthingClient!=null){
syncthingClient.close();
syncthingClient=null;
}
}
private void initClient() {
closeClient();
try {
final String configurationFile="config.properties";
Reader configReader;
try{
configReader=new InputStreamReader(openFileInput(configurationFile)) ;
}catch (FileNotFoundException e) {
Log.w("initClient","config file "+configurationFile+" not found",e);
configReader=null;
}
File configFile = new File(getExternalFilesDir(null),"config.properties");
configuration = new Configuration(configFile);
configuration.setCache(new File(getExternalCacheDir(),"cache"));
configuration.setDatabase(new File(getExternalFilesDir(null),"database"));
configuration.setDeviceName(getDeviceName());
configuration.clearTempDir();
KeystoreHandler keystoreHandler=new KeystoreHandler(configuration);
Log.i("initClient","loaded configuration = "+configuration.dumpToString());
Log.i("initClient","storage space = "+configuration.getStorageInfo().dumpAvailableSpace());
syncthingClient=new SyncthingClient(configuration);
syncthingClient.getIndexHandler().getEventBus().register(new Object(){
@Subscribe
public void handleIndexRecordAquiredEvent(IndexHandler.IndexRecordAquiredEvent event){
final String label=syncthingClient.getIndexHandler().getFolderInfo(event.getFolder()).getLabel();
final IndexInfo indexInfo=event.getIndexInfo();
final long count=event.getNewRecords().size();
runOnUiThread(new Runnable() {
@Override
public void run() {
Log.i("handleIndexRecordEvent","trigger folder list update from index record acquired");
((TextView)findViewById(R.id.index_update_message)).setText("index update, folder "
+label+" "+((int)(indexInfo.getCompleted()*100))+ "% synchronized");
updateFolderListView();
}
});
}
@Subscribe
public void handleRemoteIndexAquiredEvent(IndexHandler.RemoteIndexAquiredEvent event){
runOnUiThread(new Runnable() {
@Override
public void run() {
Log.i("handleIndexAquiredEvent","trigger folder list update from index acquired");
findViewById(R.id.index_loading_bar).setVisibility(View.GONE);
updateFolderListView();
}
});
}
});
//TODO listen for device events, update device list
folderBrowser=syncthingClient.getIndexHandler().newFolderBrowser();
statupError=null;
}catch(Exception ex){
Log.e("Main","error",ex);
statupError=ex;
closeClient();
}
}
private Typeface fontAwesome;
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.i("onCreate","BEGIN");
super.onCreate(savedInstanceState);
fontAwesome = Typeface.createFromAsset(getAssets(), "fontawesome-webfont.ttf");
setContentView(R.layout.activity_main);
((TextView)findViewById(R.id.current_folder_name_header)).setText(R.string.app_name);
for(int id:Arrays.asList(R.id.file_upload_button,
R.id.cancel_file_upload_button,
R.id.show_menu_button,
R.id.exit_menu_button_icon,
R.id.qr_button_icon,
R.id.cleanup_button_icon,
R.id.update_index_button_icon,
R.id.add_device_here_button,
R.id.upload_here_button)){
((TextView)findViewById(id)).setTypeface(fontAwesome);
}
((View)findViewById(R.id.show_menu_button)).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
((DrawerLayout)findViewById(R.id.activity_main)).openDrawer(Gravity.LEFT);
}
});
((View)findViewById(R.id.exitMenuButton)).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
((DrawerLayout)findViewById(R.id.activity_main)).closeDrawer(Gravity.LEFT);
}
});
((View)findViewById(R.id.qrButton)).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
openQrcode();
((DrawerLayout)findViewById(R.id.activity_main)).closeDrawer(Gravity.LEFT);
}
});
((View)findViewById(R.id.add_device_here_button)).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
openQrcode();
}
});
((View)findViewById(R.id.cleanupButton)).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
new AlertDialog.Builder(MainActivity.this)
.setTitle("clear cache and index")
.setMessage("clear all cache data and index data?")
.setIcon(android.R.drawable.ic_dialog_alert)
.setPositiveButton("yes", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
cleanCacheAndIndex();
}
})
.setNegativeButton("no",null)
.show();
((DrawerLayout)findViewById(R.id.activity_main)).closeDrawer(Gravity.LEFT);
}
});
((View)findViewById(R.id.updateIndexButton)).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
updateIndexFromRemote();
((DrawerLayout)findViewById(R.id.activity_main)).closeDrawer(Gravity.LEFT);
}
});
((View)findViewById(R.id.upload_here_button)).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
showUploadHereDialog();
}
});
((DrawerLayout)findViewById(R.id.activity_main)).addDrawerListener(new DrawerLayout.SimpleDrawerListener() {
@Override
public void onDrawerOpened(View drawerView) {
if(drawerView.getId()==R.id.devicesDrawer){
updateDeviceList();
}
}
});
new AsyncTask<Void,Void,Void>(){
private ProgressDialog progressDialog;
@Override
protected void onPreExecute() {
progressDialog = new ProgressDialog(MainActivity.this);
progressDialog.setMessage("loading config, starting syncthing client");
progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
progressDialog.setCancelable(false);
progressDialog.setIndeterminate(true);
progressDialog.show();
}
@Override
protected Void doInBackground(Void... voidd) {
initClient();
return null;
}
@Override
protected void onPostExecute (Void voidd){
progressDialog.dismiss();
if(syncthingClient==null){
Toast.makeText(MainActivity.this,"error starting syncthing client: "+statupError , Toast.LENGTH_LONG).show();
}else{
restoreBrowserFolderFromPref();
Date lastUpdate = getLastIndexUpdateFromPref();
if(lastUpdate==null || new Date().getTime()-lastUpdate.getTime() > 10*60*1000) { //trigger update if last was more than 10mins ago
Log.d("onCreate", "trigger index update, last was "+lastUpdate);
updateIndexFromRemote();
}
initDeviceList();
}
}
}.execute();
Log.i("onCreate","app ready, scanning intent");
Intent intent = getIntent();
if (equal(Intent.ACTION_SEND, intent.getAction())) {
handleSendSingle(intent);
} else if (equal(Intent.ACTION_SEND_MULTIPLE, intent.getAction())) {
handleSendMany(intent);
}
Log.i("onCreate","END");
}
private void showUploadHereDialog() {
if(indexBrowser==null){
Log.w("showUploadHereDialog","unable to open dialog, null index browser");
}else{
Intent i = new Intent(this, MIVFilePickerActivity.class);
String path=Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath();
Log.i("showUploadHereDialog","path = "+path);
i.putExtra(FilePickerActivity.EXTRA_START_PATH, path);
startActivityForResult(i, 0);
}
}
private void cleanCacheAndIndex() {
if(syncthingClient!=null){
syncthingClient.clearCacheAndIndex();
recreate();
}
}
private void handleSendSingle(Intent intent){
Uri fileUri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
if(fileUri!=null){
handleSend(Collections.singletonList(fileUri));
}
}
private void handleSendMany(Intent intent){
List<Uri> fileUris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
if(fileUris!=null && !fileUris.isEmpty()){
handleSend(fileUris);
}
}
private void updateButtonsVisibility(){
if(isHandlingUploadIntent){
findViewById(R.id.upload_here_button).setVisibility(View.GONE);
findViewById(R.id.file_upload_bar).setVisibility(View.VISIBLE);//todo lock if not in folder
if(isBrowsingFolder){
((TextView)findViewById(R.id.file_upload_button)).setEnabled(true);
}else{
((TextView)findViewById(R.id.file_upload_button)).setEnabled(false);
}
}else{
if(isBrowsingFolder){
findViewById(R.id.upload_here_button).setVisibility(View.VISIBLE);
}else{
findViewById(R.id.upload_here_button).setVisibility(View.GONE);
}
findViewById(R.id.file_upload_bar).setVisibility(View.GONE);
}
}
private List<Uri> filesToUpload;
private void handleSend(List<Uri> list){
Log.i("Main","handle send of files = "+list);
isHandlingUploadIntent=true;
filesToUpload=list;
updateButtonsVisibility();
((TextView)findViewById(R.id.file_upload_message)).setText(Joiner.on(", ").join(Iterables.transform(list,new Function<Uri,String>(){
@Override
public String apply( Uri input) {
return getContentFileName(input);
}
})));
((TextView)findViewById(R.id.file_upload_button)).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if(indexBrowser!=null) {
doUpload(indexBrowser.getFolder(), indexBrowser.getCurrentPath(), filesToUpload);
filesToUpload = null;
isHandlingUploadIntent=false;
updateButtonsVisibility();
}else{
Toast.makeText(MainActivity.this,"choose a folder for upload" , Toast.LENGTH_SHORT).show();
}
}
});
((TextView)findViewById(R.id.cancel_file_upload_button)).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
filesToUpload = null;
isHandlingUploadIntent=false;
updateButtonsVisibility();
Toast.makeText(MainActivity.this,"file upload cancelled" , Toast.LENGTH_SHORT).show();
}
});
}
private String getContentFileName(Uri contentUri){
String fileName = contentUri.getLastPathSegment();
if(equal(contentUri.getScheme(),"content")) {
try (Cursor cursor = MainActivity.this.getContentResolver().query(contentUri, new String[]{MediaStore.Images.Media.DATA}, null, null, null)) {
int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
cursor.moveToFirst();
String path = cursor.getString(column_index);
Log.d("Main", "recovered 'content' uri real path = " + path);
fileName = Uri.parse(path).getLastPathSegment();
}
}
return fileName;
}
private void doUpload(final String folder,final String dir,final List<Uri> filesToUpload) {
if(!filesToUpload.isEmpty()) {
new AsyncTask<Void, BlockPusher.FileUploadObserver, Exception>() {
private ProgressDialog progressDialog;
private Thread thread;
private boolean cancelled = false;
private Uri fileToUpload = filesToUpload.iterator().next();
private List<Uri> nextFilesToUpload = filesToUpload.subList(1, filesToUpload.size());
private String fileName = getContentFileName(fileToUpload);
private String path = PathUtils.buildPath(dir, fileName);
@Override
protected void onPreExecute() {
Log.i("doUpload","upload of file "+fileName+" to folder "+folder+":"+dir);
progressDialog = new ProgressDialog(MainActivity.this);
progressDialog.setMessage("uploading file " + fileName);
progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
progressDialog.setCancelable(true);
progressDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialogInterface) {
cancelled = true;
if (thread != null) {
thread.interrupt();
}
Toast.makeText(MainActivity.this, "upload aborted by user", Toast.LENGTH_SHORT).show();
}
});
progressDialog.setIndeterminate(true);
progressDialog.show();
}
@Override
protected Exception doInBackground(Void... voidd) {
try {
try (BlockPusher.FileUploadObserver observer = syncthingClient.pushFile(getContentResolver().openInputStream(fileToUpload), folder, path)) {
Log.i("doUpload","pushing file "+fileName+" to folder "+folder+":"+dir);
publishProgress(observer);
while (!observer.isCompleted() && !cancelled) {
observer.waitForProgressUpdate();
Log.i("Main", "upload progress = " + observer.getProgressMessage());
publishProgress(observer);
}
if (cancelled) {
return null;
}
Log.i("Main", "uploaded file = " + path);
return null;
}
} catch (Exception ex) {
if (cancelled) {
return null;
}
Log.e("Main", "file upload exception", ex);
return ex;
}
}
@Override
protected void onProgressUpdate(BlockPusher.FileUploadObserver... observer) {
if (observer[0].getProgress() > 0) {
progressDialog.setIndeterminate(false);
progressDialog.setMax((int)observer[0].getDataSource().getSize());
progressDialog.setProgress((int) (observer[0].getProgress() * observer[0].getDataSource().getSize()));
}
}
@Override
protected void onPostExecute(Exception res) {
progressDialog.dismiss();
if (cancelled) {
// do nothing
} else if (res != null) {
Toast.makeText(MainActivity.this, "error uploading file: " + res, Toast.LENGTH_LONG).show();
} else {
Log.i("doUpload","uploaded file "+fileName+" to folder "+folder+":"+dir);
Toast.makeText(MainActivity.this, "uploaded file: " + fileName, Toast.LENGTH_SHORT).show();
updateFolderListView();
if (!nextFilesToUpload.isEmpty()) {
doUpload(folder, dir, nextFilesToUpload);
}
}
}
}.execute();
}
}
private FolderBrowser folderBrowser;
private IndexBrowser indexBrowser;
private boolean isBrowsingFolder=false, isHandlingUploadIntent=false, indexUpdateInProgress = false;
private final static String CURRENT_FOLDER_PREF="CURRENT_FOLDER";
private void saveCurrentFolder(){
Log.d("saveCurrentFolder","saveCurrentFolder");
if(isBrowsingFolder){
getPreferences(MODE_PRIVATE).edit()
.putString(CURRENT_FOLDER_PREF,new Gson().toJson(Arrays.asList(indexBrowser.getFolder(),indexBrowser.getCurrentPath())))
.apply();
}else{
getPreferences(MODE_PRIVATE).edit().remove(CURRENT_FOLDER_PREF).apply();
}
}
private void restoreBrowserFolderFromPref(){
Log.d("restoreBrowserFolder...","restoreBrowserFolderFromPref");
String value = getPreferences(MODE_PRIVATE).getString(CURRENT_FOLDER_PREF,null);
if(isBlank(value)){
showAllFoldersListView();
}else{
try {
List<String> list = new Gson().fromJson(value, new TypeToken<List<String>>() {
}.getType());
checkArgument(list.size() == 2);
showFolderListView(list.get(0), list.get(1));
}catch(Exception ex){
Log.e("restoreBrowserFolder...","error restoring browser folder from preferences",ex);
showAllFoldersListView();
}
}
}
private void showAllFoldersListView(){
Log.d("Main","showAllFoldersListView BEGIN");
if(indexBrowser!=null) {
indexBrowser.close();
indexBrowser = null;
}
ListView listView=(ListView)findViewById(R.id.listView);
List<Pair<FolderInfo, FolderStats>> list=Lists.newArrayList(folderBrowser.getFolderInfoAndStatsList());
Collections.sort(list, Ordering.natural().onResultOf(new Function<Pair<FolderInfo, FolderStats>, String>() {
@Override
public String apply(Pair<FolderInfo, FolderStats> input) {
return input.getLeft().getLabel();
}
}));
Log.i("Main","list folders = "+list+" ("+list.size()+" records");
ArrayAdapter adapter = new ArrayAdapter<Pair<FolderInfo, FolderStats>>(this,R.layout.listview_folder,list){
@NonNull
@Override
public View getView(int position, View v, ViewGroup parent) {
if (v==null) {
v= LayoutInflater.from(getContext()).inflate(R.layout.listview_folder, null);
}
FolderInfo folderInfo = getItem(position).getLeft();
FolderStats folderStats = getItem(position).getRight();
((TextView)v.findViewById(R.id.folder_icon)).setTypeface(fontAwesome);
((TextView)v.findViewById(R.id.folder_name)).setText(folderInfo.getLabel()+" ("+folderInfo.getFolder()+")");
((TextView)v.findViewById(R.id.folder_lastmod_info)).setText(folderStats.getLastUpdate()==null?"last change: unknown":("last change: "+folderStats.getLastUpdate().toLocaleString()));
((TextView)v.findViewById(R.id.folder_content_info)).setText(folderStats.describeSize()+", "+folderStats.getFileCount()+" files, "+folderStats.getDirCount()+" dirs");
return v;
}
};
listView.setAdapter(adapter);
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> adapterView, View view, int position, long l) {
String folder= ((Pair<FolderInfo, FolderStats>)listView.getItemAtPosition(position)).getLeft().getFolder();
showFolderListView(folder,null);
}
});
findViewById(R.id.no_folder_shared_message).setVisibility(list.isEmpty()?View.VISIBLE:View.GONE);
isBrowsingFolder=false;
updateButtonsVisibility();
saveCurrentFolder();
Log.d("Main","showAllFoldersListView END");
}
private void showFolderListView(String folder, @Nullable String previousPath){
Log.d("showFolderListView","showFolderListView BEGIN");
if(indexBrowser!=null &&equal(folder,indexBrowser.getFolder())){
Log.d("showFolderListView","reuse current index browser");
indexBrowser.navigateToNearestPath(previousPath);
}else {
if (indexBrowser != null) {
indexBrowser.close();
}
Log.d("showFolderListView","open new index browser");
indexBrowser = syncthingClient.getIndexHandler()
.newIndexBrowserBuilder().setFolder(folder)
.includeParentInList(true).allowParentInRoot(true)
.buildToNearestPath(previousPath);
}
ListView listView=(ListView)findViewById(R.id.listView);
ArrayAdapter adapter = new ArrayAdapter<FileInfo>(this,R.layout.listview_file, Lists.newArrayList()){
@NonNull
@Override
public View getView(int position, View v, ViewGroup parent) {
if (v==null) {
v= LayoutInflater.from(getContext()).inflate(R.layout.listview_file, null);
}
FileInfo fileInfo = getItem(position);
((TextView)v.findViewById(R.id.file_label)).setText(fileInfo.getFileName());
((TextView)v.findViewById(R.id.file_icon)).setTypeface(fontAwesome);
if(fileInfo.isDirectory()){
((TextView)v.findViewById(R.id.file_icon)).setText(R.string.icon_folder_o);
((TextView)v.findViewById(R.id.file_size)).setVisibility(View.GONE);
}else{
((TextView)v.findViewById(R.id.file_icon)).setText(R.string.icon_file_o);
((TextView)v.findViewById(R.id.file_size)).setVisibility(View.VISIBLE);
((TextView)v.findViewById(R.id.file_size)).setText(FileUtils.byteCountToDisplaySize(fileInfo.getSize()));
}
return v;
}
};
listView.setAdapter(adapter);
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> adapterView, View view, int position, long l) {
FileInfo fileInfo= (FileInfo)listView.getItemAtPosition(position);
Log.d("showFolderListView","navigate to path = '"+fileInfo.getPath()+"' from path = '"+indexBrowser.getCurrentPath()+"'");
navigateToFolder(fileInfo);
}
});
isBrowsingFolder=true;
findViewById(R.id.no_folder_shared_message).setVisibility(View.GONE);
navigateToFolder(indexBrowser.getCurrentPathInfo());
updateButtonsVisibility();
Log.d("Main","showFolderListView END");
}
private void navigateToFolder(FileInfo fileInfo){
if(indexBrowser.isRoot() && PathUtils.isParent(fileInfo.getPath())) {
showAllFoldersListView(); //navigate back to folder list
}else{
if (fileInfo.isDirectory()) {
indexBrowser.navigateTo(fileInfo);
if(!indexBrowser.isCacheReady()){
new AsyncTask<Void,Void,Void>(){
@Override
protected void onPreExecute() {
findViewById(R.id.mainProgressBarHolder).setVisibility(View.VISIBLE);
}
@Override
protected Void doInBackground(Void... voids) {
indexBrowser.waitForCacheReady();
return null;
}
@Override
protected void onPostExecute(Void aVoid) {
findViewById(R.id.mainProgressBarHolder).setVisibility(View.GONE);
navigateToFolder(indexBrowser.getCurrentPathInfo());
}
}.execute();
}else {
List<FileInfo> list = indexBrowser.listFiles();
Log.i("showFolderListView", "list for path = '" + indexBrowser.getCurrentPath() + "' list = " + list + " (" + list.size() + " records)");
checkArgument(!list.isEmpty());//list must contain at least the 'parent' path
ListView listView = (ListView) findViewById(R.id.listView);
ArrayAdapter adapter = (ArrayAdapter) listView.getAdapter();
adapter.clear();
adapter.addAll(list);
adapter.notifyDataSetChanged();
listView.setSelection(0);
saveCurrentFolder();
((TextView) findViewById(R.id.current_folder_name_header)).setText(indexBrowser.isRoot() ? folderBrowser.getFolderInfo(indexBrowser.getFolder()).getLabel() : fileInfo.getFileName());
}
} else {
pullFile(fileInfo);
}
}
}
private void updateFolderListView(){
Log.d("updateFolderListView","BEGIN");
if(indexBrowser==null){
showAllFoldersListView();
}else{
showFolderListView(indexBrowser.getFolder(), indexBrowser.getCurrentPath());
}
Log.d("updateFolderListView","END");
}
private void initDeviceList(){
ListView listView=(ListView)findViewById(R.id.devicesListView);
ArrayAdapter adapter = new ArrayAdapter<String>(this,R.layout.listview_device, Lists.newArrayList()){ //TODO replace string with some sort of 'Device' bean
@NonNull
@Override
public View getView(int position, View v, ViewGroup parent) {
if (v==null) {
v= LayoutInflater.from(getContext()).inflate(R.layout.listview_device, null);
}
String deviceId = getItem(position);
((TextView)v.findViewById(R.id.device_name)).setText(deviceId.substring(0,7));
((TextView)v.findViewById(R.id.device_icon)).setTypeface(fontAwesome);
//TODO show more data; show status in icon color
return v;
}
};
listView.setAdapter(adapter);
// listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
// @Override
// public void onItemClick(AdapterView<?> adapterView, View view, int position, long l) {
// FileInfo fileInfo= (FileInfo)listView.getItemAtPosition(position);
// Log.d("showFolderListView","navigate to path = '"+fileInfo.getPath()+"' from path = '"+indexBrowser.getCurrentPath()+"'");
// navigateToFolder(fileInfo);
// }
// });
//TODO add button for device removal
listView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
@Override
public boolean onItemLongClick(AdapterView<?> adapterView, View view, int position, long l) {
String deviceId= (String)listView.getItemAtPosition(position);
new AlertDialog.Builder(MainActivity.this)
.setTitle("remove device "+deviceId.substring(0,7))
.setMessage("remove device"+deviceId.substring(0,7)+" from list of known devices?")
.setIcon(android.R.drawable.ic_dialog_alert)
.setPositiveButton("yes", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Set<String> peers= Sets.newHashSet(configuration.getPeers());
peers.remove(deviceId);
configuration.setPeers(peers);
configuration.storeConfiguration();//TODO add 'store configuration bg' method
updateDeviceList();
}
})
.setNegativeButton("no",null)
.show();
Log.d("showFolderListView","delete device = '"+deviceId+"'");
return false;
}
});
updateDeviceList();
}
private void updateDeviceList(){
ListView listView=(ListView)findViewById(R.id.devicesListView);
List<String> peerList = configuration.getPeers();
((ArrayAdapter)listView.getAdapter()).clear();
((ArrayAdapter)listView.getAdapter()).addAll(peerList);
((ArrayAdapter)listView.getAdapter()).notifyDataSetChanged();
listView.setSelection(0);
//TODO update devices from some sort of device info repository/handler, not from config
}
private void pullFile(final FileInfo fileInfo){
Log.i("pullFile","pulling file = "+fileInfo);
new AsyncTask<Void,BlockPuller.FileDownloadObserver,Pair<File,Exception>>(){
private ProgressDialog progressDialog;
private Thread thread;
private boolean cancelled=false;
@Override
protected void onPreExecute() {
progressDialog = new ProgressDialog(MainActivity.this);
progressDialog.setMessage("downloading file "+fileInfo.getFileName());
progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
progressDialog.setCancelable(true);
progressDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialogInterface) {
cancelled=true;
if(thread!=null){
thread.interrupt();
}
Toast.makeText(MainActivity.this, "download aborted by user", Toast.LENGTH_SHORT).show();
}
});
progressDialog.setIndeterminate(true);
progressDialog.show();
}
@Override
protected Pair<File,Exception> doInBackground(Void... voidd) {
try {
try (BlockPuller.FileDownloadObserver fileDownloadObserver = syncthingClient.pullFile(fileInfo.getFolder(), fileInfo.getPath())) {
publishProgress(fileDownloadObserver);
while (!fileDownloadObserver.isCompleted() &&!cancelled) {
fileDownloadObserver.waitForProgressUpdate();
Log.i("pullFile", "download progress = " + fileDownloadObserver.getProgressMessage());
publishProgress(fileDownloadObserver);
}
if(cancelled){
return null;
}
File outputDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
File outputFile = new File(outputDir, fileInfo.getFileName());
FileUtils.copyInputStreamToFile(fileDownloadObserver.getInputStream(), outputFile);
Log.i("pullFile", "downloaded file = " + fileInfo.getPath());
return Pair.of(outputFile, null);
}
}catch(Exception ex){
if(cancelled){
return null;
}
Log.e("pullFile","file download exception",ex);
return Pair.of(null,ex);
}
}
@Override
protected void onProgressUpdate(BlockPuller.FileDownloadObserver... fileDownloadObserver) {
if(fileDownloadObserver[0].getProgress()>0){
progressDialog.setIndeterminate(false);
progressDialog.setMax((int)(long)fileInfo.getSize());
progressDialog.setProgress((int)(fileDownloadObserver[0].getProgress()*fileInfo.getSize()));
}
}
@Override
protected void onPostExecute (Pair<File,Exception> res){
progressDialog.dismiss();
if(cancelled){
// do nothing
}else if(res.getLeft()==null){
Toast.makeText(MainActivity.this, "error downloading file: " + res.getRight(), Toast.LENGTH_LONG).show();
}else{
String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(FilenameUtils.getExtension(res.getLeft().getName()));
Intent newIntent = new Intent(Intent.ACTION_VIEW);
Log.i("Main", "open file = "+ res.getLeft().getName()+" ("+mimeType+")");
newIntent.setDataAndType(Uri.fromFile(res.getLeft()),mimeType);
newIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
Intent chooser = Intent.createChooser(newIntent, null);
try {
startActivity(chooser);
} catch (ActivityNotFoundException e) {
Toast.makeText(MainActivity.this, "no handler found for this file: "+ res.getLeft().getName()+" ("+mimeType+")", Toast.LENGTH_LONG).show();
}
}
}
}.execute();
}
private final static String LAST_INDEX_UPDATE_TS_PREF="LAST_INDEX_UPDATE_TS";
private @Nullable Date getLastIndexUpdateFromPref(){
long lastUpdate = getPreferences(MODE_PRIVATE).getLong(LAST_INDEX_UPDATE_TS_PREF,-1);
if(lastUpdate<0){
return null;
}else{
return new Date(lastUpdate);
}
}
private void updateIndexFromRemote(){
Log.d("Main","updateIndexFromRemote BEGIN");
if(indexUpdateInProgress) {
Toast.makeText(MainActivity.this, "index update already in progress", Toast.LENGTH_SHORT).show();
}else{
indexUpdateInProgress = true;
new AsyncTask<Void, Void, Exception>() {
private View indexLoadingBar = (View) findViewById(R.id.index_loading_bar);
@Override
protected void onPreExecute() {
indexLoadingBar.setVisibility(View.VISIBLE);
}
@Override
protected Exception doInBackground(Void... voidd) {
try {
syncthingClient.waitForRemoteIndexAquired();
return null;
} catch (Exception ex) {
Log.e("Main", "index dump exception", ex);
return ex;
}
}
@Override
protected void onPostExecute(Exception ex) {
indexLoadingBar.setVisibility(View.GONE);
if (ex != null) {
Toast.makeText(MainActivity.this, "error updating index: " + ex.toString(), Toast.LENGTH_LONG).show();
}
updateFolderListView();
indexUpdateInProgress = false;
getPreferences(MODE_PRIVATE).edit()
.putLong(LAST_INDEX_UPDATE_TS_PREF,new Date().getTime())
.apply();
}
}.execute();
}
Log.d("Main","updateIndexFromRemote END (running bg)");
}
@Override
protected void onDestroy() {
super.onDestroy();
try {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... voids) {
closeClient();
return null;
}
}.execute().get();
}catch(Exception ex){
Log.w("Main","error closing client",ex);
}
}
public void openQrcode(){
IntentIntegrator integrator = new IntentIntegrator(MainActivity.this);
integrator.initiateScan();
}
/**
* Receives value of scanned QR code and sets it as device ID.
*/
@Override
public void onActivityResult(int requestCode, int resultCode, Intent intent) {
IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent);
if (scanResult != null) {
String deviceId = scanResult.getContents();
if(!isBlank(deviceId)) {
Log.i("Main", "qrcode text = " + deviceId);
importDeviceId(deviceId);
return;
}
}
if (resultCode == Activity.RESULT_OK) {
Uri fileUri = intent.getData();
doUpload(indexBrowser.getFolder(),indexBrowser.getCurrentPath(), Arrays.asList(fileUri));
}
}
@Override
public void onBackPressed() {
if(indexBrowser!=null){
ListView listView=(ListView)findViewById(R.id.listView);
listView.performItemClick(listView.getAdapter().getView(0, null, null), 0, listView.getItemIdAtPosition(0)); //click item '0', ie '..' (go to parent)
}else {
super.onBackPressed();
}
}
private void importDeviceId(String deviceId){
try {
KeystoreHandler.validateDeviceId(deviceId);
boolean modified = configuration.addPeers(deviceId);
if(modified) {
configuration.storeConfiguration();
Toast.makeText(this, "successfully imported device: " + deviceId, Toast.LENGTH_SHORT).show();
updateDeviceList();//TODO remove this if event triggered (and handler trigger update)
updateIndexFromRemote();
}else{
Toast.makeText(this, "device already present: " + deviceId, Toast.LENGTH_SHORT).show();
}
}catch(Exception ex){
Log.e("Main","error importing deviceId = "+deviceId,ex);
Toast.makeText(this, "error importing device: "+ex , Toast.LENGTH_LONG).show();
}
}
private String getDeviceName() {
String manufacturer = Build.MANUFACTURER;
String model = Build.MODEL;
if (model.startsWith(manufacturer)) {
return capitalize(model);
} else {
return capitalize(manufacturer) + " " + model;
}
}
}
@@ -0,0 +1,24 @@
package it.anyplace.syncbrowser.filepicker;
import com.nononsenseapps.filepicker.AbstractFilePickerActivity;
import com.nononsenseapps.filepicker.AbstractFilePickerFragment;
import java.io.File;
/**
* Created by aleph on 27/05/16.
*/
public class MIVFilePickerActivity extends AbstractFilePickerActivity<File> {
@Override
protected AbstractFilePickerFragment<File> getFragment(
final String startPath, final int mode, final boolean allowMultiple,
final boolean allowCreateDir) {
// Only the fragment in this line needs to be changed
AbstractFilePickerFragment<File> fragment = new MIVFilePickerFragment();
fragment.setArgs(startPath, mode, allowMultiple, allowCreateDir);
return fragment;
}
}
@@ -0,0 +1,35 @@
package it.anyplace.syncbrowser.filepicker;
import android.view.View;
import com.nononsenseapps.filepicker.FilePickerFragment;
import java.io.File;
/**
* Created by aleph on 27/05/16.
*/
public class MIVFilePickerFragment extends FilePickerFragment {
@Override
public void onClickCheckable(View v, CheckableViewHolder vh) {
// auto open file on click
if (!allowMultiple) {
// Clear is necessary, in case user clicked some checkbox directly
mCheckedItems.clear();
mCheckedItems.add(vh.file);
onClickOk(null);
} else {
super.onClickCheckable(v, vh);
}
}
// private static final String EXTENSION = ".*[.](jpg|png|jpeg)";
@Override
protected boolean isItemVisible(final File file) {
// return isDir(file) || file.getName().toLowerCase().matches(EXTENSION);
return true;
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 137 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/ic_cellphone_black_24dp_inactive" android:state_enabled="false" />
<item android:drawable="@drawable/ic_cellphone_black_24dp_active" />
</selector>
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

+341
View File
@@ -0,0 +1,341 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- The main content view -->
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/topBar"
android:layout_width="match_parent"
android:layout_height="40dp"
android:orientation="horizontal"
android:paddingBottom="8dp"
android:paddingTop="8dp"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:background="@color/primary">
<TextView
android:id="@+id/show_menu_button"
android:layout_width="32dp"
android:layout_height="match_parent"
android:textSize="24dp"
android:textColor="@color/white_on_primary"
android:text="@string/icon_fa_bars" />
<TextView
android:id="@+id/current_folder_name_header"
android:layout_height="match_parent"
android:layout_width="0dp"
android:layout_weight="1"
android:maxLines="1"
android:textSize="18dp"
android:textStyle="bold"
android:textColor="@color/white_on_primary"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/topBar"
android:orientation="vertical"
android:divider="?android:listDivider"
android:showDividers="middle">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="40dp"
android:padding="8dp"
android:id="@+id/index_loading_bar"
android:orientation="horizontal"
android:background="@color/primary"
android:visibility="gone">
<TextView
android:layout_width="0dp"
android:layout_weight="0.5"
android:layout_height="match_parent"
/>
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:indeterminate="true"/>
<TextView
android:id="@+id/index_update_message"
android:layout_width="wrap_content"
android:layout_weight="1"
android:layout_height="match_parent"
android:textSize="18dp"
android:textStyle="bold"
android:textColor="@color/white_on_primary"
android:text="index update..."
android:layout_gravity="center_vertical"
android:textAlignment="gravity"
/>
<TextView
android:layout_width="0dp"
android:layout_weight="0.5"
android:layout_height="match_parent"
/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/no_folder_shared_message"
android:orientation="horizontal"
>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="20dp"
android:padding="8dp"
android:text="no shared folders available..."
android:layout_gravity="center_vertical"
android:textAlignment="gravity"
/>
</LinearLayout>
<ListView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:id="@+id/listView"
android:divider="@color/divider"
android:dividerHeight="2dp">
</ListView>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/file_upload_bar"
android:orientation="horizontal"
android:padding="8dp"
android:visibility="gone" >
<TextView
android:id="@+id/file_upload_message"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"
android:text="file upload..."
android:textSize="20dp"
android:layout_gravity="center_vertical"
android:textAlignment="gravity"
android:paddingRight="8dp"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/cancel_file_upload_button"
android:textSize="32dp"
android:textColor="@color/primary"
android:background="?selectableItemBackground"
android:text="@string/icon_fa_times"
android:layout_marginRight="8dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/file_upload_button"
android:textSize="32dp"
android:textColor="@color/primary"
android:background="?selectableItemBackground"
android:text="@string/icon_fa_cloud_upload"/>
</LinearLayout>
</LinearLayout>
<TextView
android:id="@+id/upload_here_button"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginRight="24dp"
android:layout_marginBottom="24dp"
android:layout_alignParentRight="true"
android:layout_alignParentBottom="true"
android:textSize="48dp"
android:textColor="@color/primary"
android:background="?selectableItemBackground"
android:text="@string/icon_plus_circle"/>
<FrameLayout
android:id="@+id/mainProgressBarHolder"
android:animateLayoutChanges="true"
android:visibility="gone"
android:alpha="0.4"
android:background="#000000"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ProgressBar
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"
android:layout_gravity="center" />
</FrameLayout>
</RelativeLayout>
<!-- The navigation drawer -->
<ScrollView
android:background="?android:windowBackground"
android:layout_width="240dp"
android:layout_height="match_parent"
android:layout_gravity="start"
android:fadeScrollbars="false">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:id="@+id/qrButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:orientation="horizontal"
android:padding="12dp">
<TextView
android:id="@+id/qr_button_icon"
android:layout_width="32dp"
android:layout_height="match_parent"
android:textSize="24dp"
android:text="@string/icon_qrcode"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true" />
<TextView
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"
android:textSize="14dp"
android:text="add device"
android:layout_gravity="center_vertical"
android:textAlignment="gravity"
/>
</LinearLayout>
<LinearLayout
android:id="@+id/cleanupButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:orientation="horizontal"
android:padding="12dp">
<TextView
android:id="@+id/cleanup_button_icon"
android:layout_width="32dp"
android:layout_height="match_parent"
android:textSize="24dp"
android:text="@string/icon_cleanup"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"/>
<TextView
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"
android:textSize="14dp"
android:text="clear local cache/index"
android:layout_gravity="center_vertical"
android:textAlignment="gravity"
/>
</LinearLayout>
<LinearLayout
android:id="@+id/updateIndexButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:orientation="horizontal"
android:padding="12dp">
<TextView
android:id="@+id/update_index_button_icon"
android:layout_width="32dp"
android:layout_height="match_parent"
android:textSize="24dp"
android:text="@string/icon_update"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true" />
<TextView
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"
android:textSize="14dp"
android:text="update remote index"
android:layout_gravity="center_vertical"
android:textAlignment="gravity"
/>
</LinearLayout>
<LinearLayout
android:id="@+id/exitMenuButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:orientation="horizontal"
android:padding="12dp">
<TextView
android:id="@+id/exit_menu_button_icon"
android:layout_width="32dp"
android:layout_height="match_parent"
android:textSize="24dp"
android:text="@string/icon_fa_sign_out"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true" />
<TextView
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"
android:textSize="14dp"
android:text="exit menu"
android:layout_gravity="center_vertical"
android:textAlignment="gravity"
/>
</LinearLayout>
</LinearLayout>
</ScrollView>
<!-- device drawer (right) -->
<RelativeLayout
android:layout_height="match_parent"
android:orientation="vertical"
android:id="@+id/devicesDrawer"
android:background="?android:windowBackground"
android:layout_width="280dp"
android:layout_gravity="end">
<ListView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/devicesListView"
android:divider="@color/divider"
android:dividerHeight="2dp">
</ListView>
<TextView
android:id="@+id/add_device_here_button"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginRight="18dp"
android:layout_marginBottom="18dp"
android:layout_alignParentRight="true"
android:layout_alignParentBottom="true"
android:textSize="48dp"
android:textColor="@color/primary"
android:background="?selectableItemBackground"
android:text="@string/icon_qrcode"/>
</RelativeLayout>
</android.support.v4.widget.DrawerLayout>
@@ -0,0 +1,33 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="32dp"
android:paddingBottom="12dp"
android:paddingLeft="@dimen/abc_action_bar_content_inset_material"
android:paddingRight="@dimen/abc_action_bar_content_inset_material"
android:paddingTop="12dp">
<TextView
android:id="@+id/device_icon"
android:layout_width="32dp"
android:layout_height="match_parent"
android:textSize="32dp"
android:textColor="@color/primary"
android:text="@string/icon_fa_laptop"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"/>
<TextView
android:id="@+id/device_name"
android:layout_width="match_parent"
android:layout_alignParentRight="true"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:layout_alignParentTop="true"
android:gravity="top"
android:textAlignment="gravity"
android:paddingLeft="40dp"
android:textSize="18dp"
android:textStyle="bold"/>
</RelativeLayout>
+38
View File
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:paddingLeft="12dp"
android:paddingRight="12dp">
<TextView
android:id="@+id/file_icon"
android:layout_width="48dp"
android:layout_height="match_parent"
android:textSize="32dp"
android:textColor="@color/primary"
android:text="@string/icon_folder"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true" />
<TextView
android:id="@+id/file_label"
android:maxLines="1"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_toEndOf="@+id/file_icon"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:textSize="22dp" />
<TextView
android:id="@+id/file_size"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:layout_below="@id/file_label"
android:layout_toEndOf="@+id/file_icon"/>
</RelativeLayout>
@@ -0,0 +1,56 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="32dp"
android:paddingBottom="12dp"
android:paddingLeft="@dimen/abc_action_bar_content_inset_material"
android:paddingRight="@dimen/abc_action_bar_content_inset_material"
android:paddingTop="12dp">
<TextView
android:id="@+id/folder_icon"
android:layout_width="60dp"
android:layout_height="match_parent"
android:textSize="60dp"
android:textColor="@color/primary"
android:text="@string/icon_fa_share_alt"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"/>
<TextView
android:id="@+id/folder_name"
android:layout_width="match_parent"
android:layout_alignParentRight="true"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:layout_alignParentTop="true"
android:gravity="top"
android:textAlignment="gravity"
android:paddingLeft="75dp"
android:textSize="20dp"
android:textStyle="bold"/>
<TextView
android:id="@+id/folder_lastmod_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:layout_below="@id/folder_name"
android:paddingLeft="75dp"
android:textSize="14dp"
android:layout_alignParentStart="true" />
<TextView
android:id="@+id/folder_content_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:layout_below="@id/folder_lastmod_info"
android:paddingLeft="75dp"
android:textSize="14dp"
android:layout_alignParentStart="true" />
</RelativeLayout>
Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

+13
View File
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="primary">#f43703</color>
<color name="primary_dark">#d13602</color>
<color name="white_on_primary">#fefefe</color>
<color name="accent">#FFC107</color>
<color name="divider">#1F000000</color>
<color name="text_red">#ffff4444</color>
<color name="text_blue">#ff33b5e5</color>
<color name="text_green">#ff99cc00</color>
<color name="light_grey">#cccccc</color>
</resources>
+17
View File
@@ -0,0 +1,17 @@
<resources>
<string name="app_name">a-sync-browser</string>
<string name="icon_cleanup">&#xf014;</string>
<string name="icon_qrcode">&#xf029;</string>
<string name="icon_folder">&#xf07b;</string>
<string name="icon_folder_o">&#xf114;</string>
<string name="icon_file_o">&#xf016;</string>
<string name="icon_update">&#xf079;</string>
<string name="icon_plus_circle">&#xf055;</string>
<string name="icon_fa_bars">&#xf0c9;</string>
<string name="icon_fa_share_alt">&#xf1e0;</string>
<string name="icon_fa_sign_out">&#xf08b;</string>
<string name="icon_fa_cloud_upload">&#xf0ee;</string>
<string name="icon_fa_times">&#xf00d;</string>
<string name="icon_fa_laptop">&#xf109;</string>
</resources>
+79
View File
@@ -0,0 +1,79 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Widget" />
<style name="Widget.Syncthing" />
<style name="Widget.Syncthing.ListView" parent="Widget.AppCompat.ListView">
<item name="android:paddingTop">8dp</item>
<item name="android:paddingBottom">8dp</item>
<item name="android:clipToPadding">false</item>
<item name="android:scrollbarStyle">outsideOverlay</item>
<item name="android:divider">@android:color/transparent</item>
</style>
<style name="Widget.Syncthing.TextView" />
<style name="Widget.Syncthing.TextView.Label" parent="@android:style/Widget.TextView">
<item name="android:background">?selectableItemBackground</item>
<item name="android:drawablePadding">32dp</item>
<item name="android:gravity">start|center_vertical</item>
<item name="android:textAppearance">@style/TextAppearance.AppCompat.Body1</item>
<item name="android:paddingLeft">@dimen/abc_action_bar_content_inset_material</item>
<item name="android:paddingRight">@dimen/abc_action_bar_content_inset_material</item>
</style>
<style name="Widget.Syncthing.TextView.Label.Details">
<item name="android:textColor">?attr/editTextColor</item>
<item name="android:minHeight">56dp</item>
<item name="android:paddingTop">8dp</item>
<item name="android:paddingBottom">8dp</item>
</style>
<style name="Widget.Syncthing.TextView.Label.Details.DeviceList">
<item name="android:paddingLeft">4dp</item>
<item name="android:minHeight">48dp</item>
</style>
<style name="Widget.Syncthing.TextView.Label.Details.Field">
<item name="android:background">@null</item>
</style>
<style name="Widget.Syncthing.DrawerArrowToggle" parent="Widget.AppCompat.DrawerArrowToggle">
<item name="spinBars">false</item>
<item name="color">@android:color/white</item>
</style>
<style name="TextAppearance" />
<style name="TextAppearance.Syncthing" />
<style name="TextAppearance.Syncthing.ListItemPrimary" parent="TextAppearance.AppCompat.Subhead">
<item name="android:textColor">?android:textColorPrimary</item>
</style>
<style name="TextAppearance.Syncthing.ListItemSecondary" parent="TextAppearance.AppCompat.Body1">
<item name="android:textColor">?android:textColorSecondary</item>
</style>
<style name="TextAppearance.Syncthing.ListItemSmall" parent="TextAppearance.AppCompat.Caption" />
<!-- You can also inherit from NNF_BaseTheme.Light -->
<style name="FilePickerTheme" parent="NNF_BaseTheme">
<!-- Set these to match your theme -->
<item name="colorPrimary">@color/primary</item>
<item name="colorPrimaryDark">@color/primary_dark</item>
<item name="colorAccent">@color/accent</item>
<!-- Need to set this also to style create folder dialog -->
<item name="alertDialogTheme">@style/FilePickerAlertDialogTheme</item>
<!-- If you want to set a specific toolbar theme, do it here -->
<!-- <item name="nnf_toolbarTheme">@style/ThemeOverlay.AppCompat.Dark.ActionBar</item> -->
</style>
<style name="FilePickerAlertDialogTheme" parent="Theme.AppCompat.Dialog.Alert">
<item name="colorPrimary">@color/primary</item>
<item name="colorPrimaryDark">@color/primary_dark</item>
<item name="colorAccent">@color/accent</item>
</style>
</resources>
+47
View File
@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Activity themes -->
<eat-comment/>
<style name="Theme.Syncthing.Base" parent="@style/Theme.AppCompat.Light.NoActionBar">
<item name="colorPrimary">@color/primary</item>
<item name="colorPrimaryDark">@color/primary_dark</item>
<item name="colorAccent">@color/accent</item>
<!--<item name="textAppearanceListItemPrimary">@style/TextAppearance.Syncthing.ListItemPrimary</item>-->
<!--<item name="textAppearanceListItemSecondary">@style/TextAppearance.Syncthing.ListItemSecondary</item>-->
<!--<item name="textAppearanceListItemSmall">@style/TextAppearance.Syncthing.ListItemSmall</item>-->
<item name="windowActionModeOverlay">true</item>
<item name="android:listViewStyle">@style/Widget.Syncthing.ListView</item>
<!--<item name="android:listDivider">@drawable/list_divider_inset</item>-->
<item name="drawerArrowStyle">@style/Widget.Syncthing.DrawerArrowToggle</item>
<!--<item name="android:actionBarSize">@dimen/abc_action_bar_default_height_material</item>-->
</style>
<style name="Theme.Syncthing" parent="Theme.Syncthing.Base"/>
<!--<style name="Theme.Syncthing.Translucent">-->
<!--<item name="android:windowBackground">@android:color/transparent</item>-->
<!--<item name="android:colorBackgroundCacheHint">@null</item>-->
<!--<item name="android:windowIsTranslucent">true</item>-->
<!--<item name="android:windowAnimationStyle">@android:style/Animation</item>-->
<!--</style>-->
<!--&lt;!&ndash; Dialog themes &ndash;&gt;-->
<!--<eat-comment/>-->
<style name="Theme.Syncthing.Dialog" parent="Theme.AppCompat.Light.Dialog.Alert">
<item name="colorAccent">@color/primary_dark</item>
</style>
<!--&lt;!&ndash; Toolbar themes &ndash;&gt;-->
<!--<eat-comment/>-->
<!--<style name="ThemeOverlay.Syncthing.Toolbar" parent="@style/ThemeOverlay.AppCompat.Dark.ActionBar">-->
<!--<item name="android:windowBackground">@null</item>-->
<!--<item name="colorAccent">@android:color/white</item>-->
<!--</style>-->
</resources>
@@ -0,0 +1,17 @@
package it.anyplace.syncbrowser;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() throws Exception {
assertEquals(4, 2 + 2);
}
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

BIN
View File
Binary file not shown.
+25
View File
@@ -0,0 +1,25 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
mavenLocal()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.2.2'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
mavenLocal()
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
+19
View File
@@ -0,0 +1,19 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
#org.gradle.jvmargs=-Xmx1536m
#org.gradle.jvmargs=-Xmx4096m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
org.gradle.jvmargs=-Xmx4g -XX:MaxPermSize=1g
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
Binary file not shown.
+6
View File
@@ -0,0 +1,6 @@
#Mon Dec 28 10:00:20 PST 2015
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

+11
View File
@@ -0,0 +1,11 @@
## This file is automatically generated by Android Studio.
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
#
# This file must *NOT* be checked into Version Control Systems,
# as it contains information specific to your local configuration.
#
# Location of the SDK. This is only used by Gradle.
# For customization when using a Version Control System, please read the
# header note.
#Sun Nov 20 21:34:36 CET 2016
sdk.dir=/home/aleph/android-sdk
+1
View File
@@ -0,0 +1 @@
include ':app'