Compare commits

..

2 Commits

Author SHA1 Message Date
3357fe76d4 Feat: 데이터 포트 범위 지정 기능 추가
방화벽 환경에서 특정 포트 범위만 개방하여 사용할 수 있도록 데이터 포트 범위 설정 기능 추가.
- FTPConfig에 minDataPort, maxDataPort 설정 추가 (기본값: 50000-50100)
- FTPDataConnection에서 지정된 범위 내 사용 가능한 포트 자동 할당
- MainActivity UI에 데이터 포트 범위 설정 필드 추가
- 포트 범위 유효성 검증 (최소 10개 포트, 1024-65535 범위)
- 범위 미지정 시 기존처럼 랜덤 포트 사용

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-02 04:33:53 +09:00
12f27e9f13 Fix: 앱 재시작 시 UI 상태와 서비스 상태 동기화 문제 해결
onResume()에서 실제 FTPService 실행 상태를 확인하여 UI를 동기화함으로써 화면 전환 또는 앱 재시작 시에도 올바른 버튼 상태가 표시되도록 수정.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-02 02:10:39 +09:00
7 changed files with 225 additions and 18 deletions

View File

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

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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() {

View File

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

View File

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

View File

@@ -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" />