Compare commits
2 Commits
e6e4ef7df5
...
3357fe76d4
| Author | SHA1 | Date | |
|---|---|---|---|
| 3357fe76d4 | |||
| 12f27e9f13 |
@@ -9,8 +9,12 @@ public class FTPConfig {
|
||||
private static final String KEY_PORT = "ftp_port";
|
||||
private static final String KEY_ROOT_DIR_URI = "root_directory_uri";
|
||||
private static final String KEY_ROOT_DIR_PATH = "root_directory_path";
|
||||
private static final String KEY_MIN_DATA_PORT = "min_data_port";
|
||||
private static final String KEY_MAX_DATA_PORT = "max_data_port";
|
||||
|
||||
private static final int DEFAULT_PORT = 2121;
|
||||
private static final int DEFAULT_MIN_DATA_PORT = 50000;
|
||||
private static final int DEFAULT_MAX_DATA_PORT = 50100;
|
||||
|
||||
private final SharedPreferences preferences;
|
||||
|
||||
@@ -60,4 +64,27 @@ public class FTPConfig {
|
||||
.putString(KEY_ROOT_DIR_PATH, rootDirPath)
|
||||
.apply();
|
||||
}
|
||||
|
||||
public int getMinDataPort() {
|
||||
return preferences.getInt(KEY_MIN_DATA_PORT, DEFAULT_MIN_DATA_PORT);
|
||||
}
|
||||
|
||||
public void setMinDataPort(int port) {
|
||||
preferences.edit().putInt(KEY_MIN_DATA_PORT, port).apply();
|
||||
}
|
||||
|
||||
public int getMaxDataPort() {
|
||||
return preferences.getInt(KEY_MAX_DATA_PORT, DEFAULT_MAX_DATA_PORT);
|
||||
}
|
||||
|
||||
public void setMaxDataPort(int port) {
|
||||
preferences.edit().putInt(KEY_MAX_DATA_PORT, port).apply();
|
||||
}
|
||||
|
||||
public void setDataPortRange(int minPort, int maxPort) {
|
||||
preferences.edit()
|
||||
.putInt(KEY_MIN_DATA_PORT, minPort)
|
||||
.putInt(KEY_MAX_DATA_PORT, maxPort)
|
||||
.apply();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,14 +20,41 @@ public class FTPDataConnection {
|
||||
private int passivePort;
|
||||
|
||||
public boolean openPassiveMode(InetAddress bindAddress) {
|
||||
try {
|
||||
// Use port 0 to get a random available port
|
||||
passiveSocket = new ServerSocket(0, 1, bindAddress);
|
||||
passiveSocket.setSoTimeout(DATA_CONNECTION_TIMEOUT);
|
||||
passivePort = passiveSocket.getLocalPort();
|
||||
return openPassiveMode(bindAddress, 0, 0);
|
||||
}
|
||||
|
||||
Log.i(TAG, "Passive mode enabled on port: " + passivePort);
|
||||
return true;
|
||||
public boolean openPassiveMode(InetAddress bindAddress, int minPort, int maxPort) {
|
||||
try {
|
||||
if (minPort <= 0 || maxPort <= 0 || minPort > maxPort) {
|
||||
// Use port 0 to get a random available port
|
||||
passiveSocket = new ServerSocket(0, 1, bindAddress);
|
||||
passiveSocket.setSoTimeout(DATA_CONNECTION_TIMEOUT);
|
||||
passivePort = passiveSocket.getLocalPort();
|
||||
Log.i(TAG, "Passive mode enabled on random port: " + passivePort);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Try to find an available port in the specified range
|
||||
IOException lastException = null;
|
||||
for (int port = minPort; port <= maxPort; port++) {
|
||||
try {
|
||||
passiveSocket = new ServerSocket(port, 1, bindAddress);
|
||||
passiveSocket.setSoTimeout(DATA_CONNECTION_TIMEOUT);
|
||||
passivePort = passiveSocket.getLocalPort();
|
||||
Log.i(TAG, "Passive mode enabled on port: " + passivePort + " (range: " + minPort + "-" + maxPort + ")");
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
lastException = e;
|
||||
// Port is in use, try next port
|
||||
}
|
||||
}
|
||||
|
||||
// No available port found in range
|
||||
Log.e(TAG, "No available port in range " + minPort + "-" + maxPort);
|
||||
if (lastException != null) {
|
||||
Log.e(TAG, "Last error: " + lastException.getMessage());
|
||||
}
|
||||
return false;
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Error opening passive mode: " + e.getMessage());
|
||||
return false;
|
||||
|
||||
@@ -23,19 +23,27 @@ public class FTPServer {
|
||||
private int port;
|
||||
private Context context;
|
||||
private Uri rootDirectoryUri;
|
||||
private int minDataPort = 0;
|
||||
private int maxDataPort = 0;
|
||||
|
||||
public FTPServer(Context context) {
|
||||
this(context, DEFAULT_PORT, null);
|
||||
this(context, DEFAULT_PORT, null, 0, 0);
|
||||
}
|
||||
|
||||
public FTPServer(Context context, int port) {
|
||||
this(context, port, null);
|
||||
this(context, port, null, 0, 0);
|
||||
}
|
||||
|
||||
public FTPServer(Context context, int port, Uri rootDirectoryUri) {
|
||||
this(context, port, rootDirectoryUri, 0, 0);
|
||||
}
|
||||
|
||||
public FTPServer(Context context, int port, Uri rootDirectoryUri, int minDataPort, int maxDataPort) {
|
||||
this.context = context;
|
||||
this.port = port;
|
||||
this.rootDirectoryUri = rootDirectoryUri;
|
||||
this.minDataPort = minDataPort;
|
||||
this.maxDataPort = maxDataPort;
|
||||
this.executorService = Executors.newFixedThreadPool(MAX_CONNECTIONS);
|
||||
}
|
||||
|
||||
@@ -56,7 +64,7 @@ public class FTPServer {
|
||||
Socket clientSocket = serverSocket.accept();
|
||||
Log.i(TAG, "New client connection from: " + clientSocket.getInetAddress());
|
||||
|
||||
FTPSession session = new FTPSession(clientSocket, context, rootDirectoryUri);
|
||||
FTPSession session = new FTPSession(clientSocket, context, rootDirectoryUri, minDataPort, maxDataPort);
|
||||
executorService.execute(session);
|
||||
|
||||
} catch (IOException e) {
|
||||
|
||||
@@ -37,11 +37,13 @@ public class FTPService extends Service {
|
||||
if (ACTION_START.equals(action)) {
|
||||
int port = intent.getIntExtra("port", 2121);
|
||||
String rootDirUriString = intent.getStringExtra("rootDirUri");
|
||||
int minDataPort = intent.getIntExtra("minDataPort", 0);
|
||||
int maxDataPort = intent.getIntExtra("maxDataPort", 0);
|
||||
android.net.Uri rootDirUri = null;
|
||||
if (rootDirUriString != null && !rootDirUriString.isEmpty()) {
|
||||
rootDirUri = android.net.Uri.parse(rootDirUriString);
|
||||
}
|
||||
startFTPServer(port, rootDirUri);
|
||||
startFTPServer(port, rootDirUri, minDataPort, maxDataPort);
|
||||
} else if (ACTION_STOP.equals(action)) {
|
||||
stopFTPServer();
|
||||
}
|
||||
@@ -50,19 +52,24 @@ public class FTPService extends Service {
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
private void startFTPServer(int port, android.net.Uri rootDirUri) {
|
||||
private void startFTPServer(int port, android.net.Uri rootDirUri, int minDataPort, int maxDataPort) {
|
||||
if (ftpServer != null && ftpServer.isRunning()) {
|
||||
Log.w(TAG, "FTP Server is already running");
|
||||
return;
|
||||
}
|
||||
|
||||
ftpServer = new FTPServer(this, port, rootDirUri);
|
||||
ftpServer = new FTPServer(this, port, rootDirUri, minDataPort, maxDataPort);
|
||||
ftpServer.start();
|
||||
|
||||
Notification notification = createNotification("FTP Server is running on port " + ftpServer.getPort());
|
||||
String notificationText = "FTP Server is running on port " + ftpServer.getPort();
|
||||
if (minDataPort > 0 && maxDataPort > 0) {
|
||||
notificationText += " (Data ports: " + minDataPort + "-" + maxDataPort + ")";
|
||||
}
|
||||
Notification notification = createNotification(notificationText);
|
||||
startForeground(NOTIFICATION_ID, notification);
|
||||
|
||||
Log.i(TAG, "FTP Server started on port " + port + " with root URI: " + rootDirUri);
|
||||
Log.i(TAG, "FTP Server started on port " + port + " with root URI: " + rootDirUri +
|
||||
", data port range: " + minDataPort + "-" + maxDataPort);
|
||||
}
|
||||
|
||||
private void stopFTPServer() {
|
||||
|
||||
@@ -31,15 +31,23 @@ public class FTPSession implements Runnable {
|
||||
private FTPDataConnection dataConnection;
|
||||
private String transferType = "A"; // A = ASCII, I = Binary
|
||||
private boolean useUtf8 = true; // UTF-8 enabled by default for better compatibility
|
||||
private int minDataPort = 0;
|
||||
private int maxDataPort = 0;
|
||||
|
||||
public FTPSession(Socket socket, Context context) {
|
||||
this(socket, context, null);
|
||||
this(socket, context, null, 0, 0);
|
||||
}
|
||||
|
||||
public FTPSession(Socket socket, Context context, Uri rootDirectoryUri) {
|
||||
this(socket, context, rootDirectoryUri, 0, 0);
|
||||
}
|
||||
|
||||
public FTPSession(Socket socket, Context context, Uri rootDirectoryUri, int minDataPort, int maxDataPort) {
|
||||
this.controlSocket = socket;
|
||||
this.fileSystem = new FTPFileSystem(context, rootDirectoryUri);
|
||||
this.dataConnection = null;
|
||||
this.minDataPort = minDataPort;
|
||||
this.maxDataPort = maxDataPort;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -405,7 +413,7 @@ public class FTPSession implements Runnable {
|
||||
// Get server address from control socket
|
||||
String serverAddress = controlSocket.getLocalAddress().getHostAddress();
|
||||
|
||||
if (dataConnection.openPassiveMode(controlSocket.getLocalAddress())) {
|
||||
if (dataConnection.openPassiveMode(controlSocket.getLocalAddress(), minDataPort, maxDataPort)) {
|
||||
int port = dataConnection.getPassivePort();
|
||||
|
||||
// Format: h1,h2,h3,h4,p1,p2
|
||||
|
||||
@@ -8,6 +8,8 @@ import androidx.core.content.ContextCompat;
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.ActivityManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
@@ -33,6 +35,8 @@ public class MainActivity extends AppCompatActivity {
|
||||
private TextView rootDirPathTextView;
|
||||
private Button selectDirButton;
|
||||
private Button saveSettingsButton;
|
||||
private EditText minDataPortEditText;
|
||||
private EditText maxDataPortEditText;
|
||||
|
||||
private FTPConfig config;
|
||||
private ActivityResultLauncher<Uri> directoryPickerLauncher;
|
||||
@@ -89,11 +93,15 @@ public class MainActivity extends AppCompatActivity {
|
||||
rootDirPathTextView = findViewById(R.id.rootDirPathTextView);
|
||||
selectDirButton = findViewById(R.id.selectDirButton);
|
||||
saveSettingsButton = findViewById(R.id.saveSettingsButton);
|
||||
minDataPortEditText = findViewById(R.id.minDataPortEditText);
|
||||
maxDataPortEditText = findViewById(R.id.maxDataPortEditText);
|
||||
}
|
||||
|
||||
private void loadSettings() {
|
||||
int port = config.getPort();
|
||||
String rootPath = config.getRootDirectoryPath();
|
||||
int minDataPort = config.getMinDataPort();
|
||||
int maxDataPort = config.getMaxDataPort();
|
||||
|
||||
portEditText.setText(String.valueOf(port));
|
||||
portTextView.setText("Port: " + port);
|
||||
@@ -103,6 +111,9 @@ public class MainActivity extends AppCompatActivity {
|
||||
} else {
|
||||
rootDirPathTextView.setText("Not selected");
|
||||
}
|
||||
|
||||
minDataPortEditText.setText(String.valueOf(minDataPort));
|
||||
maxDataPortEditText.setText(String.valueOf(maxDataPort));
|
||||
}
|
||||
|
||||
private void setupListeners() {
|
||||
@@ -140,7 +151,45 @@ public class MainActivity extends AppCompatActivity {
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate data port range
|
||||
String minPortStr = minDataPortEditText.getText().toString().trim();
|
||||
String maxPortStr = maxDataPortEditText.getText().toString().trim();
|
||||
|
||||
int minDataPort = 0;
|
||||
int maxDataPort = 0;
|
||||
|
||||
if (!minPortStr.isEmpty() || !maxPortStr.isEmpty()) {
|
||||
if (minPortStr.isEmpty() || maxPortStr.isEmpty()) {
|
||||
Toast.makeText(this, "Please enter both min and max data ports or leave both empty", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
minDataPort = Integer.parseInt(minPortStr);
|
||||
maxDataPort = Integer.parseInt(maxPortStr);
|
||||
|
||||
if (minDataPort < 1024 || minDataPort > 65535 || maxDataPort < 1024 || maxDataPort > 65535) {
|
||||
Toast.makeText(this, "Data ports must be between 1024 and 65535", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
if (minDataPort >= maxDataPort) {
|
||||
Toast.makeText(this, "Min data port must be less than max data port", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
if (maxDataPort - minDataPort < 10) {
|
||||
Toast.makeText(this, "Data port range should be at least 10 ports", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
Toast.makeText(this, "Invalid data port number", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
config.setPort(port);
|
||||
config.setDataPortRange(minDataPort, maxDataPort);
|
||||
portTextView.setText("Port: " + port);
|
||||
|
||||
Toast.makeText(this, "Settings saved", Toast.LENGTH_SHORT).show();
|
||||
@@ -194,6 +243,8 @@ public class MainActivity extends AppCompatActivity {
|
||||
serviceIntent.setAction(FTPService.ACTION_START);
|
||||
serviceIntent.putExtra("port", config.getPort());
|
||||
serviceIntent.putExtra("rootDirUri", rootDirUri);
|
||||
serviceIntent.putExtra("minDataPort", config.getMinDataPort());
|
||||
serviceIntent.putExtra("maxDataPort", config.getMaxDataPort());
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
startForegroundService(serviceIntent);
|
||||
@@ -239,4 +290,27 @@ public class MainActivity extends AppCompatActivity {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if FTPService is currently running in the background
|
||||
*/
|
||||
private boolean isServiceRunning() {
|
||||
ActivityManager manager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
|
||||
if (manager != null) {
|
||||
for (ActivityManager.RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) {
|
||||
if (FTPService.class.getName().equals(service.service.getClassName())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
// Sync UI with actual service state when activity resumes
|
||||
isServerRunning = isServiceRunning();
|
||||
updateUI();
|
||||
}
|
||||
}
|
||||
@@ -144,6 +144,62 @@
|
||||
app:layout_constraintTop_toBottomOf="@id/rootDirPathTextView"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/dataPortRangeLabel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Data Port Range:"
|
||||
android:textSize="16sp"
|
||||
android:layout_marginTop="16dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/selectDirButton"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/minDataPortLabel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Min:"
|
||||
android:textSize="14sp"
|
||||
android:layout_marginTop="8dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/dataPortRangeLabel"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/minDataPortEditText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="number"
|
||||
android:hint="50000"
|
||||
android:layout_marginStart="16dp"
|
||||
app:layout_constraintBaseline_toBaselineOf="@id/minDataPortLabel"
|
||||
app:layout_constraintStart_toEndOf="@id/minDataPortLabel"
|
||||
app:layout_constraintEnd_toStartOf="@id/maxDataPortLabel"
|
||||
app:layout_constraintWidth_default="percent"
|
||||
app:layout_constraintHorizontal_weight="1" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/maxDataPortLabel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Max:"
|
||||
android:textSize="14sp"
|
||||
android:layout_marginStart="16dp"
|
||||
app:layout_constraintBaseline_toBaselineOf="@id/minDataPortLabel"
|
||||
app:layout_constraintStart_toEndOf="@id/minDataPortEditText" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/maxDataPortEditText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="number"
|
||||
android:hint="50100"
|
||||
android:layout_marginStart="16dp"
|
||||
app:layout_constraintBaseline_toBaselineOf="@id/minDataPortLabel"
|
||||
app:layout_constraintStart_toEndOf="@id/maxDataPortLabel"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintWidth_default="percent"
|
||||
app:layout_constraintHorizontal_weight="1" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/saveSettingsButton"
|
||||
android:layout_width="wrap_content"
|
||||
@@ -151,7 +207,7 @@
|
||||
android:text="Save Settings"
|
||||
android:minWidth="150dp"
|
||||
android:layout_marginTop="24dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/selectDirButton"
|
||||
app:layout_constraintTop_toBottomOf="@id/minDataPortLabel"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
|
||||
Reference in New Issue
Block a user