~xdavidwu/saf-sftp

ca25ed35d864c42a2b45951cc1d1d25a4f711fbe — xdavidwu 4 years ago
init commit
A  => .gitignore +82 -0
@@ 1,82 @@
# Built application files
*.apk
*.ap_
*.aab

# Files for the ART/Dalvik VM
*.dex

# Java class files
*.class

# Generated files
bin/
gen/
out/
release/

# Gradle files
.gradle/
build/

# Local configuration file (sdk path, etc)
local.properties

# Proguard folder generated by Eclipse
proguard/

# Log Files
*.log

# Android Studio Navigation editor temp files
.navigation/

# Android Studio captures folder
captures/

# IntelliJ
*.iml
.idea/workspace.xml
.idea/tasks.xml
.idea/gradle.xml
.idea/assetWizardSettings.xml
.idea/dictionaries
.idea/libraries
# Android Studio 3 in .gitignore file.
.idea/caches
.idea/modules.xml
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
.idea/navEditor.xml

# Keystore files
# Uncomment the following lines if you do not want to check your keystore files in.
#*.jks
#*.keystore

# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild

# Google Services (e.g. APIs or Firebase)
# google-services.json

# Freeline
freeline.py
freeline/
freeline_project_description.json

# fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
fastlane/readme.md

# Version control
vcs.xml

# lint
lint/intermediates/
lint/generated/
lint/outputs/
lint/tmp/
# lint/reports/

A  => build.gradle +41 -0
@@ 1,41 @@
buildscript {
	repositories {
		google()
		jcenter()
		maven {
			url 'https://oss.jfrog.org/artifactory/oss-snapshot-local/'
		}
	}
	dependencies {
		classpath 'com.android.tools.build:gradle:3.4.1'
	}
}
apply plugin: 'com.android.application'

android {
	compileSdkVersion 'android-28'
	buildToolsVersion '28.0.3'

	defaultConfig {
		minSdkVersion 19
		targetSdkVersion 28
	}

	buildTypes {
		release {
			minifyEnabled false
			proguardFile getDefaultProguardFile('proguard-android.txt')
		}
	}
}

dependencies {
	implementation "org.connectbot:sshlib:2.2.9"
}

allprojects {
	repositories {
		google()
		jcenter()
	}
}

A  => gradle/wrapper/gradle-wrapper.jar +0 -0
A  => gradle/wrapper/gradle-wrapper.properties +6 -0
@@ 1,6 @@
#Wed Apr 10 15:27:10 PDT 2013
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip

A  => gradlew +164 -0
@@ 1,164 @@
#!/usr/bin/env bash

##############################################################################
##
##  Gradle start up script for UN*X
##
##############################################################################

# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""

APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`

# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"

warn ( ) {
    echo "$*"
}

die ( ) {
    echo
    echo "$*"
    echo
    exit 1
}

# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
case "`uname`" in
  CYGWIN* )
    cygwin=true
    ;;
  Darwin* )
    darwin=true
    ;;
  MINGW* )
    msys=true
    ;;
esac

# For Cygwin, ensure paths are in UNIX format before anything is touched.
if $cygwin ; then
    [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
fi

# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
    ls=`ls -ld "$PRG"`
    link=`expr "$ls" : '.*-> \(.*\)$'`
    if expr "$link" : '/.*' > /dev/null; then
        PRG="$link"
    else
        PRG=`dirname "$PRG"`"/$link"
    fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >&-
APP_HOME="`pwd -P`"
cd "$SAVED" >&-

CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar

# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
        # IBM's JDK on AIX uses strange locations for the executables
        JAVACMD="$JAVA_HOME/jre/sh/java"
    else
        JAVACMD="$JAVA_HOME/bin/java"
    fi
    if [ ! -x "$JAVACMD" ] ; then
        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME

Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
    fi
else
    JAVACMD="java"
    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.

Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi

# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
    MAX_FD_LIMIT=`ulimit -H -n`
    if [ $? -eq 0 ] ; then
        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
            MAX_FD="$MAX_FD_LIMIT"
        fi
        ulimit -n $MAX_FD
        if [ $? -ne 0 ] ; then
            warn "Could not set maximum file descriptor limit: $MAX_FD"
        fi
    else
        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
    fi
fi

# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi

# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`

    # We build the pattern for arguments to be converted via cygpath
    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
    SEP=""
    for dir in $ROOTDIRSRAW ; do
        ROOTDIRS="$ROOTDIRS$SEP$dir"
        SEP="|"
    done
    OURCYGPATTERN="(^($ROOTDIRS))"
    # Add a user-defined pattern to the cygpath arguments
    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
    fi
    # Now convert the arguments - kludge to limit ourselves to /bin/sh
    i=0
    for arg in "$@" ; do
        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option

        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
        else
            eval `echo args$i`="\"$arg\""
        fi
        i=$((i+1))
    done
    case $i in
        (0) set -- ;;
        (1) set -- "$args0" ;;
        (2) set -- "$args0" "$args1" ;;
        (3) set -- "$args0" "$args1" "$args2" ;;
        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
    esac
fi

# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
function splitJvmOpts() {
    JVM_OPTS=("$@")
}
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"

exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"

A  => gradlew.bat +90 -0
@@ 1,90 @@
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem  Gradle startup script for Windows
@rem
@rem ##########################################################################

@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal

@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=

set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%

@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome

set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init

echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.

goto fail

:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe

if exist "%JAVA_EXE%" goto init

echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.

goto fail

:init
@rem Get command-line arguments, handling Windowz variants

if not "%OS%" == "Windows_NT" goto win9xME_args
if "%@eval[2+2]" == "4" goto 4NT_args

:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2

:win9xME_args_slurp
if "x%~1" == "x" goto execute

set CMD_LINE_ARGS=%*
goto execute

:4NT_args
@rem Get arguments from the 4NT Shell from JP Software
set CMD_LINE_ARGS=%$

:execute
@rem Setup the command line

set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar

@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%

:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd

:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1

:mainEnd
if "%OS%"=="Windows_NT" endlocal

:omega

A  => src/androidTest/java/org/safsftp/MainActivityTest.java +21 -0
@@ 1,21 @@
package org.safsftp;

import android.test.ActivityInstrumentationTestCase2;

/**
 * This is a simple framework for a test of an Application.	 See
 * {@link android.test.ApplicationTestCase ApplicationTestCase} for more information on
 * how to write and extend Application tests.
 * <p/>
 * To run this test, you can type:
 * adb shell am instrument -w \
 * -e class org.safsftp.MainActivityTest \
 * org.safsftp.tests/android.test.InstrumentationTestRunner
 */
public class MainActivityTest extends ActivityInstrumentationTestCase2<MainActivity> {

	public MainActivityTest() {
		super("org.safsftp", MainActivity.class);
	}

}

A  => src/main/AndroidManifest.xml +30 -0
@@ 1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
	package="org.safsftp"
	android:versionCode="1"
	android:versionName="1.0">

	<uses-permission android:name="android.permission.INTERNET" />

	<application android:label="@string/app_name"
		  android:icon="@mipmap/sym_def_app_icon"
		  android:allowBackup="false">
		<activity android:name="MainActivity"
			android:label="@string/app_name"
			android:theme="@android:style/Theme.DeviceDefault">
			<intent-filter>
				<action android:name="android.intent.action.MAIN" />
				<category android:name="android.intent.category.LAUNCHER" />
			</intent-filter>
		</activity>
		<provider android:name=".SFTPDocumentsProvider"
			android:authorities="org.safsftp"
			android:permission="android.permission.MANAGE_DOCUMENTS"
			android:grantUriPermissions="true"
			android:exported="true">
			<intent-filter>
				<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
			</intent-filter>
		</provider>
	</application>
</manifest>

A  => src/main/java/org/safsftp/MainActivity.java +58 -0
@@ 1,58 @@
package org.safsftp;

import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.preference.EditTextPreference;
import android.preference.PreferenceActivity;
import android.os.Bundle;

public class MainActivity extends PreferenceActivity
	implements OnSharedPreferenceChangeListener {

	private EditTextPreference hostText, portText, usernameText, passwdText;
	
	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		addPreferencesFromResource(R.xml.main_pre);

		hostText=(EditTextPreference)findPreference("host");
		portText=(EditTextPreference)findPreference("port");
		usernameText=(EditTextPreference)findPreference("username");
		passwdText=(EditTextPreference)findPreference("passwd");

		getPreferenceScreen().getSharedPreferences()
			.registerOnSharedPreferenceChangeListener(this);
	}

	@Override
	public void onSharedPreferenceChanged(SharedPreferences settings,
			String key) {
		switch(key){
		case "host":
			if (settings.getString("host", "").equals(""))
				hostText.setSummary(getString(R.string.host_summary));
			else
				hostText.setSummary(settings.getString("host", ""));
			break;
		case "port":
			if (settings.getString("port", "").equals(""))
				portText.setSummary(getString(R.string.port_summary));
			else
				portText.setSummary(settings.getString("port", ""));
			break;
		case "username":
			if (settings.getString("username", "").equals(""))
				usernameText.setSummary(getString(R.string.username_summary));
			else
				usernameText.setSummary(settings.getString("username", ""));
			break;
		case "passwd":
			if (settings.getString("passwd", "").equals(""))
				passwdText.setSummary(getString(R.string.passwd_summary));
			else
				passwdText.setSummary(getString(R.string.passwd_filled));
			break;
		}
	}
}

A  => src/main/java/org/safsftp/ReadTask.java +71 -0
@@ 1,71 @@
package org.safsftp;

import android.os.AsyncTask;
import android.os.Message;
import android.os.ParcelFileDescriptor;
import android.os.ParcelFileDescriptor.AutoCloseOutputStream;
import android.util.Log;

import com.trilead.ssh2.Connection;
import com.trilead.ssh2.SFTPv3Client;
import com.trilead.ssh2.SFTPv3FileHandle;

import org.safsftp.ToastThread;

public class ReadTask extends AsyncTask<Void,Void,Void> {
	private Connection connection;
	private SFTPv3Client sftp;
	private SFTPv3FileHandle file;
	private ToastThread lthread;
	private ParcelFileDescriptor fd;

	public ReadTask(String host,String port,String username,String passwd,
			String filename,ParcelFileDescriptor fd,ToastThread lthread) {
		this.fd=fd;
		try {
			connection=new Connection(host,Integer.parseInt(port));
			connection.connect(null,10000,10000);
			if(!connection.authenticateWithPassword(username,passwd)){
				Message msg=lthread.handler.obtainMessage();
				msg.obj="SFTP auth failed.";
				lthread.handler.sendMessage(msg);
			}
			sftp=new SFTPv3Client(connection);
			sftp.setCharset(null);
			Message msg=lthread.handler.obtainMessage();
			msg.obj="SFTP connect succeed.";
			lthread.handler.sendMessage(msg);
			file=sftp.openFileRO(filename);
		}
		catch(Exception e){
			Message msg=lthread.handler.obtainMessage();
			msg.obj=e.toString();
			lthread.handler.sendMessage(msg);
			sftp.close();
			connection.close();
		}
	}

	@Override
	public Void doInBackground(Void... args) {
		AutoCloseOutputStream acos=new AutoCloseOutputStream(fd);
		int size,offset=0;
		byte[] buf=new byte[32768];
		try{
			while((size=sftp.read(file,offset,buf,0,32768))>0) {
				acos.write(buf,0,size);
				offset+=size;
			}
			sftp.closeFile(file);
			sftp.close();
			connection.close();
			acos.close();
		}
		catch(Exception e){
			Log.e("SFTP","read file "+e.toString());
			sftp.close();
			connection.close();
		}
		return null;
	}
}

A  => src/main/java/org/safsftp/SFTPDocumentsProvider.java +225 -0
@@ 1,225 @@
package org.safsftp;

import android.content.SharedPreferences;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.os.CancellationSignal;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.ParcelFileDescriptor;
import android.preference.PreferenceManager;
import android.provider.DocumentsContract.Document;
import android.provider.DocumentsContract.Root;
import android.provider.DocumentsProvider;
import android.webkit.MimeTypeMap;
import android.widget.Toast;
import android.util.Log;

import com.trilead.ssh2.Connection;
import com.trilead.ssh2.SFTPv3Client;
import com.trilead.ssh2.SFTPv3DirectoryEntry;
import com.trilead.ssh2.SFTPv3FileAttributes;

import java.io.IOException;
import java.util.Vector;

import org.safsftp.ToastThread;
import org.safsftp.ReadTask;

public class SFTPDocumentsProvider extends DocumentsProvider {

	private ToastThread lthread;
	
	private static final String[] DEFAULT_ROOT_PROJECTION=new String[]{
		Root.COLUMN_ROOT_ID,
		Root.COLUMN_FLAGS,
		Root.COLUMN_ICON,
		Root.COLUMN_TITLE,
		Root.COLUMN_DOCUMENT_ID,
	};

	private static final String[] DEFAULT_DOC_PROJECTION=new String[]{
		Document.COLUMN_DOCUMENT_ID,
		Document.COLUMN_DISPLAY_NAME,
		Document.COLUMN_MIME_TYPE,
		Document.COLUMN_LAST_MODIFIED,
		Document.COLUMN_SIZE
	};

	private static String getMime(String filename) {
		int idx=filename.lastIndexOf(".");
		if(idx>0){
			String mime=MimeTypeMap.getSingleton()
				.getMimeTypeFromExtension(filename
				.substring(idx+1).toLowerCase());
			if(mime!=null)return mime;
		}
		return "application/octet-stream";
	}

	@Override
	public boolean onCreate() {
		lthread=new ToastThread(getContext());
		lthread.start();
		return true;
	}

	public ParcelFileDescriptor openDocument(String documentId,
			String mode,CancellationSignal cancellationSignal) {
		if (!"r".equals(mode)) {
			throw new UnsupportedOperationException("Mode "+mode+" is not supported yet.");
		}
		SharedPreferences settings=PreferenceManager
			.getDefaultSharedPreferences(getContext());
		String host=documentId.substring(0,documentId.indexOf(":"));
		String tmp=documentId.substring(0,documentId.indexOf("/"));
		String port=tmp.substring(tmp.indexOf(":")+1);
		String filename=documentId.substring(documentId.indexOf("/")+1);
		Log.e("SFTP","od "+documentId+" on "+host+":"+port);
		ParcelFileDescriptor[] fd;
		try {
			fd=ParcelFileDescriptor.createReliablePipe();
		}
		catch(IOException e) {
			return null;
		}
		new ReadTask(host,port,settings.getString("username",""),
				settings.getString("passwd",""),
				filename,fd[1],lthread).execute();
		Log.e("SFTP","od "+documentId+" on "+host+":"+port+" return");
		return fd[0];
	}

	public Cursor queryChildDocuments(String parentDocumentId,
			String[] projection,String sortOrder) {
		MatrixCursor result=new MatrixCursor(projection!=null?projection:DEFAULT_DOC_PROJECTION);
		SharedPreferences settings=PreferenceManager
			.getDefaultSharedPreferences(getContext());
		Connection connection=null;
		SFTPv3Client sftp=null;
		String host=parentDocumentId.substring(0,parentDocumentId.indexOf(":"));
		String tmp=parentDocumentId.substring(0,parentDocumentId.indexOf("/"));
		String port=tmp.substring(tmp.indexOf(":")+1);
		Log.e("SFTP","qcf "+parentDocumentId+" on "+host+":"+port);
		try {
			connection=new Connection(host,Integer.parseInt(port));
			connection.connect(null,10000,10000);
			if(!connection.authenticateWithPassword(settings.getString("username",""),
				settings.getString("passwd",""))){
				Message msg=lthread.handler.obtainMessage();
				msg.obj="SFTP auth failed.";
				lthread.handler.sendMessage(msg);
			}
			sftp=new SFTPv3Client(connection);
			sftp.setCharset(null);
			Message msg=lthread.handler.obtainMessage();
			msg.obj="SFTP connect succeed.";
			lthread.handler.sendMessage(msg);
		}
		catch(Exception e){
			Message msg=lthread.handler.obtainMessage();
			msg.obj=e.toString();
			lthread.handler.sendMessage(msg);
			sftp.close();
			connection.close();
			return result;
		}
		String filename=parentDocumentId.substring(parentDocumentId.indexOf("/")+1);
		try{
			Vector<SFTPv3DirectoryEntry> res=sftp.ls(filename);
			for(SFTPv3DirectoryEntry entry : res){
				Log.e("SFTP","qcf "+parentDocumentId+" "+entry.filename+" "+entry.attributes.size+" "+entry.attributes.mtime);
				if(entry.filename.equals(".")||entry.filename.equals(".."))continue;
				MatrixCursor.RowBuilder row=result.newRow();
				row.add(Document.COLUMN_DOCUMENT_ID,parentDocumentId+'/'+entry.filename);
				row.add(Document.COLUMN_DISPLAY_NAME,entry.filename);
				if(entry.attributes.isDirectory()){
					row.add(Document.COLUMN_MIME_TYPE,Document.MIME_TYPE_DIR);
				}
				else if(entry.attributes.isRegularFile()){
					row.add(Document.COLUMN_MIME_TYPE,getMime(entry.filename));
				}
				row.add(Document.COLUMN_SIZE,entry.attributes.size);
				row.add(Document.COLUMN_LAST_MODIFIED,entry.attributes.mtime*1000);
			}
		}
		catch(Exception e){
		Log.e("SFTP","qcf "+parentDocumentId+" "+e.toString());
			Message msg=lthread.handler.obtainMessage();
			msg.obj=e.toString();
			lthread.handler.sendMessage(msg);
		}
		sftp.close();
		connection.close();
		return result;
	}

	public Cursor queryDocument(String documentId, String[] projection) {
		MatrixCursor result=new MatrixCursor(projection!=null?projection:DEFAULT_DOC_PROJECTION);
		SharedPreferences settings=PreferenceManager
			.getDefaultSharedPreferences(getContext());
		Connection connection=null;
		SFTPv3Client sftp=null;
		String host=documentId.substring(0,documentId.indexOf(":"));
		String tmp=documentId.substring(0,documentId.indexOf("/"));
		String port=tmp.substring(tmp.indexOf(":")+1);
		Log.e("SFTP","qf "+documentId+" on "+host+":"+port);
		try {
			connection=new Connection(host,Integer.parseInt(port));
			connection.connect(null,10000,10000);
			if(!connection.authenticateWithPassword(settings.getString("username",""),
				settings.getString("passwd",""))){
				Message msg=lthread.handler.obtainMessage();
				msg.obj="SFTP auth failed.";
				lthread.handler.sendMessage(msg);
			}
			sftp=new SFTPv3Client(connection);
			sftp.setCharset(null);
			Message msg=lthread.handler.obtainMessage();
			msg.obj="SFTP connect succeed.";
			lthread.handler.sendMessage(msg);
		}
		catch(Exception e){
			Message msg=lthread.handler.obtainMessage();
			msg.obj=e.toString();
			lthread.handler.sendMessage(msg);
			sftp.close();
			connection.close();
			return result;
		}
		String filename=documentId.substring(documentId.indexOf("/")+1);
		try{
			SFTPv3FileAttributes res=sftp.stat(filename);
			MatrixCursor.RowBuilder row=result.newRow();
			row.add(Document.COLUMN_DOCUMENT_ID,documentId);
			row.add(Document.COLUMN_DISPLAY_NAME,filename.substring(filename.lastIndexOf("/")+1));
			row.add(Document.COLUMN_MIME_TYPE,res.isDirectory()?Document.MIME_TYPE_DIR:getMime(filename));
			row.add(Document.COLUMN_SIZE,res.size);
			row.add(Document.COLUMN_LAST_MODIFIED,res.mtime*1000);
		}
		catch(Exception e){
		Log.e("SFTP","qf "+documentId+" "+e.toString());
			Message msg=lthread.handler.obtainMessage();
			msg.obj=e.toString();
			lthread.handler.sendMessage(msg);
		}
		sftp.close();
		connection.close();
		return result;
	}

	public Cursor queryRoots(String[] projection) {
		MatrixCursor result=new MatrixCursor(projection!=null?projection:DEFAULT_ROOT_PROJECTION);
		SharedPreferences settings=PreferenceManager
			.getDefaultSharedPreferences(getContext());
		String host=settings.getString("host",""),port=settings.getString("port","22");
		MatrixCursor.RowBuilder row=result.newRow();
		row.add(Root.COLUMN_ROOT_ID,host+":"+port);
		row.add(Root.COLUMN_DOCUMENT_ID,host+":"+port+"/.");
		row.add(Root.COLUMN_FLAGS,0);
		row.add(Root.COLUMN_TITLE,"SFTP "+host+":"+port);
		row.add(Root.COLUMN_ICON,R.mipmap.sym_def_app_icon);
		return result;
	}
}

A  => src/main/java/org/safsftp/ToastThread.java +27 -0
@@ 1,27 @@
package org.safsftp;

import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.widget.Toast;

public class ToastThread extends Thread {
	public Handler handler;
	private Context context;

	public ToastThread(Context context){
		this.context=context;
	}

	public void run() {
		Looper.prepare();
		handler=new Handler() {
			public void handleMessage(Message msg) {
				Toast.makeText(context,(String)msg.obj,Toast.LENGTH_SHORT).show();
			}
		};
		Looper.loop();
	}
}


A  => src/main/res/mipmap-hdpi/sym_def_app_icon.png +0 -0
A  => src/main/res/mipmap-ldpi/sym_def_app_icon.png +0 -0
A  => src/main/res/mipmap-mdpi/sym_def_app_icon.png +0 -0
A  => src/main/res/mipmap-xhdpi/sym_def_app_icon.png +0 -0
A  => src/main/res/mipmap-xxhdpi/sym_def_app_icon.png +0 -0
A  => src/main/res/mipmap-xxxhdpi/sym_def_app_icon.png +0 -0
A  => src/main/res/values/strings.xml +15 -0
@@ 1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
	<string name="app_name">SAF SFTP</string>
	<string name="conn_dest">Connection Destination</string>
	<string name="host">Host</string>
	<string name="host_summary">Host to connect.</string>
	<string name="port">Port</string>
	<string name="port_summary">Port to connect.</string>
	<string name="username">Username</string>
	<string name="username_summary">Username to connect.</string>
	<string name="auth">Authentication Details</string>
	<string name="passwd">Password</string>
	<string name="passwd_summary">Password.</string>
	<string name="passwd_filled">(filled)</string>
</resources>

A  => src/main/res/xml/main_pre.xml +19 -0
@@ 1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
	<PreferenceCategory android:title="@string/conn_dest">
		<EditTextPreference android:key="host"
			android:summary="@string/host_summary"
			android:title="@string/host" />
		<EditTextPreference android:title="@string/port"
			android:summary="@string/port_summary"
			android:key="port" android:defaultValue="22" />
		<EditTextPreference android:key="username"
			android:summary="@string/username_summary"
			android:title="@string/username" />
	</PreferenceCategory>
	<PreferenceCategory android:title="@string/auth">
		<EditTextPreference android:title="@string/passwd"
			android:summary="@string/passwd_summary"
			android:key="passwd" />
	</PreferenceCategory>
</PreferenceScreen>