From f08d8d66d84a2e0f3b1c3adead3e0082238aa2fd Mon Sep 17 00:00:00 2001 From: Gyubin Han Date: Thu, 1 Jan 2026 02:46:18 +0900 Subject: [PATCH] =?UTF-8?q?Feat:=20FTP=20=EC=84=9C=EB=B2=84=20=EA=B8=B0?= =?UTF-8?q?=EB=B3=B8=20=EA=B5=AC=EC=A1=B0=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AndroidManifest.xml에 필요한 권한 추가 (INTERNET, STORAGE 등) - FTPResponse 클래스 생성: FTP 응답 코드 상수 정의 - FTPSession 클래스 생성: 개별 클라이언트 세션 처리 * 명령어 읽기/응답 보내기 기본 루프 * USER, PASS, QUIT, SYST, PWD, NOOP 명령어 처리 - FTPServer 클래스 생성: 메인 서버 로직 * ServerSocket으로 포트 2121에서 수신 대기 * ExecutorService로 다중 클라이언트 연결 관리 - FTPService 클래스 생성: 백그라운드 포그라운드 서비스 * 서버 시작/중지 Intent 처리 * 알림 채널 및 포그라운드 서비스 구현 - MainActivity UI 업데이트: 서버 제어 기능 * 시작/중지 버튼 추가 * 서버 상태 표시 * 런타임 권한 요청 처리 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- .README.md.un~ | Bin 0 -> 1801 bytes .gitignore | 17 ++ .idea/.gitignore | 3 + .idea/.name | 1 + .idea/AndroidProjectSystem.xml | 6 + .idea/compiler.xml | 6 + .idea/deploymentTargetSelector.xml | 10 + .idea/gradle.xml | 19 ++ .idea/markdown.xml | 8 + .idea/misc.xml | 9 + .idea/runConfigurations.xml | 17 ++ .idea/vcs.xml | 7 + README.md~ | 4 + app/.gitignore | 1 + app/build.gradle | 38 ++++ app/proguard-rules.pro | 21 ++ .../server/ftp/ExampleInstrumentedTest.java | 26 +++ app/src/main/AndroidManifest.xml | 40 ++++ .../gyu/android/server/ftp/FTPResponse.java | 68 +++++++ .../be/gyu/android/server/ftp/FTPServer.java | 109 +++++++++++ .../be/gyu/android/server/ftp/FTPService.java | 120 ++++++++++++ .../be/gyu/android/server/ftp/FTPSession.java | 142 ++++++++++++++ .../gyu/android/server/ftp/MainActivity.java | 122 ++++++++++++ .../drawable-v24/ic_launcher_foreground.xml | 30 +++ .../res/drawable/ic_launcher_background.xml | 170 ++++++++++++++++ app/src/main/res/layout/activity_main.xml | 67 +++++++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + app/src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 0 -> 1404 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 0 -> 2898 bytes app/src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 982 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 0 -> 1772 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 1900 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 0 -> 3918 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 2884 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 0 -> 5914 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 3844 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 0 -> 7778 bytes app/src/main/res/values-night/themes.xml | 16 ++ app/src/main/res/values/colors.xml | 10 + app/src/main/res/values/strings.xml | 3 + app/src/main/res/values/themes.xml | 16 ++ .../android/server/ftp/ExampleUnitTest.java | 17 ++ build.gradle | 9 + gradle.properties | 21 ++ gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59203 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 185 ++++++++++++++++++ gradlew.bat | 89 +++++++++ settings.gradle | 16 ++ 50 files changed, 1459 insertions(+) create mode 100644 .README.md.un~ create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/.name create mode 100644 .idea/AndroidProjectSystem.xml create mode 100644 .idea/compiler.xml create mode 100644 .idea/deploymentTargetSelector.xml create mode 100644 .idea/gradle.xml create mode 100644 .idea/markdown.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/runConfigurations.xml create mode 100644 .idea/vcs.xml create mode 100644 README.md~ create mode 100644 app/.gitignore create mode 100644 app/build.gradle create mode 100644 app/proguard-rules.pro create mode 100644 app/src/androidTest/java/be/gyu/android/server/ftp/ExampleInstrumentedTest.java create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/java/be/gyu/android/server/ftp/FTPResponse.java create mode 100644 app/src/main/java/be/gyu/android/server/ftp/FTPServer.java create mode 100644 app/src/main/java/be/gyu/android/server/ftp/FTPService.java create mode 100644 app/src/main/java/be/gyu/android/server/ftp/FTPSession.java create mode 100644 app/src/main/java/be/gyu/android/server/ftp/MainActivity.java create mode 100644 app/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 app/src/main/res/layout/activity_main.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp create mode 100644 app/src/main/res/values-night/themes.xml create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/themes.xml create mode 100644 app/src/test/java/be/gyu/android/server/ftp/ExampleUnitTest.java create mode 100644 build.gradle create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle diff --git a/.README.md.un~ b/.README.md.un~ new file mode 100644 index 0000000000000000000000000000000000000000..e58b8ebaf931fb494e15020cd34caf714e5d079f GIT binary patch literal 1801 zcmeHIJxc>Y5M86zcA}LFX~j+}6$G&h1W{W-v`C>*V`F6^7mdk*QFA2GO^l%8#}y$O zh!FpPjW$*mR-$C*I<0kPb9V&m2o@Q5H+wrX%iX-0*_puN@$}Mo@Qr!;rn}#~I;?mf z!Xf`>u{QR)^P+u(ht5Xsu6w(lHyC4`h;4aO;Z{nMCjtsND_)WD6rY&#LoUN1cWW89 z@~p~1Xt?4~7O1sH4zhaK>=Rj0Jawg>WFjc} zMsbgEHe0%h83_3HE)*SY?5pS3xr&Cth@y$s4xi*klo8iT6iqH^&TdOLF9X;$^NJ=` zr>Qa7M8y&qF?k?p0ATAL2ySIFwbDJLlb{J+E1PmRiydpv1&*7tq;W6`R!YYnhiVw( zAe!N|qL4*A1dTK<1fN`34u%42!PRvR`KaCO^kb@A8avlrD@$XsG!~NUWhaOHoT0&5 eg=A;t&lDEYqdk6In&=845KaH(DjAHtU;O}ViYeRx literal 0 HcmV?d00001 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..00c55dd --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties + +.temp \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..e03a4f8 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +Gyub_s Android FTP Server \ No newline at end of file diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml new file mode 100644 index 0000000..4a53bee --- /dev/null +++ b/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..fb7f4a8 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..b268ef3 --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..d124cf2 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/markdown.xml b/.idea/markdown.xml new file mode 100644 index 0000000..c61ea33 --- /dev/null +++ b/.idea/markdown.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..c6ac130 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..16660f1 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..8306744 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/README.md~ b/README.md~ new file mode 100644 index 0000000..5bbc122 --- /dev/null +++ b/README.md~ @@ -0,0 +1,4 @@ +# Android FTP Server +안드로이드 환경에서 구동되는 FTP 서버 애플리케이션 개발을 위한 Repository 입니다. + + diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..317bc7e --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,38 @@ +plugins { + id 'com.android.application' +} + +android { + compileSdk 36 + + defaultConfig { + applicationId "be.gyu.android.server.ftp" + minSdk 21 + targetSdk 36 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + + implementation 'androidx.appcompat:appcompat:1.3.0' + implementation 'com.google.android.material:material:1.4.0' + implementation 'androidx.constraintlayout:constraintlayout:2.0.4' + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.3' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/be/gyu/android/server/ftp/ExampleInstrumentedTest.java b/app/src/androidTest/java/be/gyu/android/server/ftp/ExampleInstrumentedTest.java new file mode 100644 index 0000000..672fbf9 --- /dev/null +++ b/app/src/androidTest/java/be/gyu/android/server/ftp/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package be.gyu.android.server.ftp; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + assertEquals("be.gyu.android.server.ftp", appContext.getPackageName()); + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a31e421 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/be/gyu/android/server/ftp/FTPResponse.java b/app/src/main/java/be/gyu/android/server/ftp/FTPResponse.java new file mode 100644 index 0000000..51d938e --- /dev/null +++ b/app/src/main/java/be/gyu/android/server/ftp/FTPResponse.java @@ -0,0 +1,68 @@ +package be.gyu.android.server.ftp; + +public class FTPResponse { + // 1xx - Positive Preliminary reply + public static final int RESTART_MARKER = 110; + public static final int SERVICE_READY_IN_N_MINUTES = 120; + public static final int DATA_CONNECTION_ALREADY_OPEN = 125; + public static final int FILE_STATUS_OK = 150; + + // 2xx - Positive Completion reply + public static final int COMMAND_OK = 200; + public static final int COMMAND_NOT_IMPLEMENTED = 202; + public static final int SYSTEM_STATUS = 211; + public static final int DIRECTORY_STATUS = 212; + public static final int FILE_STATUS = 213; + public static final int HELP_MESSAGE = 214; + public static final int SYSTEM_TYPE = 215; + public static final int SERVICE_READY = 220; + public static final int SERVICE_CLOSING = 221; + public static final int DATA_CONNECTION_OPEN = 225; + public static final int CLOSING_DATA_CONNECTION = 226; + public static final int ENTERING_PASSIVE_MODE = 227; + public static final int USER_LOGGED_IN = 230; + public static final int REQUESTED_FILE_ACTION_OK = 250; + public static final int PATHNAME_CREATED = 257; + + // 3xx - Positive Intermediate reply + public static final int USERNAME_OK_NEED_PASSWORD = 331; + public static final int NEED_ACCOUNT = 332; + public static final int FILE_ACTION_PENDING = 350; + + // 4xx - Transient Negative Completion reply + public static final int SERVICE_NOT_AVAILABLE = 421; + public static final int CANNOT_OPEN_DATA_CONNECTION = 425; + public static final int CONNECTION_CLOSED = 426; + public static final int FILE_ACTION_NOT_TAKEN = 450; + public static final int ACTION_ABORTED = 451; + public static final int INSUFFICIENT_STORAGE = 452; + + // 5xx - Permanent Negative Completion reply + public static final int SYNTAX_ERROR = 500; + public static final int SYNTAX_ERROR_PARAMETERS = 501; + public static final int COMMAND_NOT_IMPLEMENTED_502 = 502; + public static final int BAD_SEQUENCE = 503; + public static final int COMMAND_NOT_IMPLEMENTED_FOR_PARAMETER = 504; + public static final int NOT_LOGGED_IN = 530; + public static final int NEED_ACCOUNT_FOR_STORING = 532; + public static final int FILE_UNAVAILABLE = 550; + public static final int PAGE_TYPE_UNKNOWN = 551; + public static final int EXCEEDED_STORAGE = 552; + public static final int FILE_NAME_NOT_ALLOWED = 553; + + public static String format(int code, String message) { + return code + " " + message + "\r\n"; + } + + public static String formatMultiline(int code, String[] messages) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < messages.length; i++) { + if (i < messages.length - 1) { + sb.append(code).append("-").append(messages[i]).append("\r\n"); + } else { + sb.append(code).append(" ").append(messages[i]).append("\r\n"); + } + } + return sb.toString(); + } +} \ No newline at end of file diff --git a/app/src/main/java/be/gyu/android/server/ftp/FTPServer.java b/app/src/main/java/be/gyu/android/server/ftp/FTPServer.java new file mode 100644 index 0000000..df10bfe --- /dev/null +++ b/app/src/main/java/be/gyu/android/server/ftp/FTPServer.java @@ -0,0 +1,109 @@ +package be.gyu.android.server.ftp; + +import android.util.Log; + +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +public class FTPServer { + private static final String TAG = "FTPServer"; + private static final int DEFAULT_PORT = 2121; + private static final int MAX_CONNECTIONS = 10; + + private ServerSocket serverSocket; + private ExecutorService executorService; + private Thread acceptThread; + private boolean isRunning = false; + private int port; + + public FTPServer() { + this(DEFAULT_PORT); + } + + public FTPServer(int port) { + this.port = port; + this.executorService = Executors.newFixedThreadPool(MAX_CONNECTIONS); + } + + public void start() { + if (isRunning) { + Log.w(TAG, "Server is already running"); + return; + } + + acceptThread = new Thread(() -> { + try { + serverSocket = new ServerSocket(port); + isRunning = true; + Log.i(TAG, "FTP Server started on port " + port); + + while (isRunning && !Thread.currentThread().isInterrupted()) { + try { + Socket clientSocket = serverSocket.accept(); + Log.i(TAG, "New client connection from: " + clientSocket.getInetAddress()); + + FTPSession session = new FTPSession(clientSocket); + executorService.execute(session); + + } catch (IOException e) { + if (isRunning) { + Log.e(TAG, "Error accepting connection: " + e.getMessage()); + } + } + } + } catch (IOException e) { + Log.e(TAG, "Server socket error: " + e.getMessage()); + } finally { + Log.i(TAG, "Server accept thread stopped"); + } + }); + + acceptThread.start(); + } + + public void stop() { + if (!isRunning) { + Log.w(TAG, "Server is not running"); + return; + } + + Log.i(TAG, "Stopping FTP Server..."); + isRunning = false; + + try { + if (serverSocket != null && !serverSocket.isClosed()) { + serverSocket.close(); + } + } catch (IOException e) { + Log.e(TAG, "Error closing server socket: " + e.getMessage()); + } + + if (acceptThread != null) { + acceptThread.interrupt(); + } + + executorService.shutdown(); + try { + if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) { + executorService.shutdownNow(); + } + } catch (InterruptedException e) { + executorService.shutdownNow(); + Thread.currentThread().interrupt(); + } + + Log.i(TAG, "FTP Server stopped"); + } + + public boolean isRunning() { + return isRunning; + } + + public int getPort() { + return port; + } +} \ No newline at end of file diff --git a/app/src/main/java/be/gyu/android/server/ftp/FTPService.java b/app/src/main/java/be/gyu/android/server/ftp/FTPService.java new file mode 100644 index 0000000..3c49321 --- /dev/null +++ b/app/src/main/java/be/gyu/android/server/ftp/FTPService.java @@ -0,0 +1,120 @@ +package be.gyu.android.server.ftp; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Intent; +import android.os.Build; +import android.os.IBinder; +import android.util.Log; + +import androidx.core.app.NotificationCompat; + +public class FTPService extends Service { + private static final String TAG = "FTPService"; + private static final String CHANNEL_ID = "FTPServerChannel"; + private static final int NOTIFICATION_ID = 1; + + public static final String ACTION_START = "be.gyu.android.server.ftp.ACTION_START"; + public static final String ACTION_STOP = "be.gyu.android.server.ftp.ACTION_STOP"; + + private FTPServer ftpServer; + + @Override + public void onCreate() { + super.onCreate(); + Log.d(TAG, "Service created"); + createNotificationChannel(); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if (intent != null) { + String action = intent.getAction(); + + if (ACTION_START.equals(action)) { + startFTPServer(); + } else if (ACTION_STOP.equals(action)) { + stopFTPServer(); + } + } + + return START_STICKY; + } + + private void startFTPServer() { + if (ftpServer != null && ftpServer.isRunning()) { + Log.w(TAG, "FTP Server is already running"); + return; + } + + ftpServer = new FTPServer(); + ftpServer.start(); + + Notification notification = createNotification("FTP Server is running on port " + ftpServer.getPort()); + startForeground(NOTIFICATION_ID, notification); + + Log.i(TAG, "FTP Server started"); + } + + private void stopFTPServer() { + if (ftpServer != null) { + ftpServer.stop(); + ftpServer = null; + } + + stopForeground(true); + stopSelf(); + + Log.i(TAG, "FTP Server stopped"); + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (ftpServer != null) { + ftpServer.stop(); + } + Log.d(TAG, "Service destroyed"); + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + private void createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel channel = new NotificationChannel( + CHANNEL_ID, + "FTP Server Service", + NotificationManager.IMPORTANCE_LOW + ); + channel.setDescription("FTP Server running notification"); + + NotificationManager manager = getSystemService(NotificationManager.class); + if (manager != null) { + manager.createNotificationChannel(channel); + } + } + } + + private Notification createNotification(String contentText) { + Intent notificationIntent = new Intent(this, MainActivity.class); + PendingIntent pendingIntent = PendingIntent.getActivity( + this, + 0, + notificationIntent, + PendingIntent.FLAG_IMMUTABLE + ); + + return new NotificationCompat.Builder(this, CHANNEL_ID) + .setContentTitle("FTP Server") + .setContentText(contentText) + .setSmallIcon(android.R.drawable.stat_sys_upload) + .setContentIntent(pendingIntent) + .build(); + } +} \ No newline at end of file diff --git a/app/src/main/java/be/gyu/android/server/ftp/FTPSession.java b/app/src/main/java/be/gyu/android/server/ftp/FTPSession.java new file mode 100644 index 0000000..210e97b --- /dev/null +++ b/app/src/main/java/be/gyu/android/server/ftp/FTPSession.java @@ -0,0 +1,142 @@ +package be.gyu.android.server.ftp; + +import android.util.Log; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.net.Socket; + +public class FTPSession implements Runnable { + private static final String TAG = "FTPSession"; + + private final Socket controlSocket; + private BufferedReader reader; + private BufferedWriter writer; + + private String username; + private boolean isAuthenticated = false; + private String currentDirectory = "/"; + private boolean isRunning = true; + + public FTPSession(Socket socket) { + this.controlSocket = socket; + } + + @Override + public void run() { + try { + reader = new BufferedReader(new InputStreamReader(controlSocket.getInputStream())); + writer = new BufferedWriter(new OutputStreamWriter(controlSocket.getOutputStream())); + + Log.d(TAG, "Client connected: " + controlSocket.getInetAddress()); + sendResponse(FTPResponse.SERVICE_READY, "FTP Server ready"); + + String line; + while (isRunning && (line = reader.readLine()) != null) { + line = line.trim(); + Log.d(TAG, "Command received: " + line); + + if (line.isEmpty()) { + continue; + } + + handleCommand(line); + } + } catch (IOException e) { + Log.e(TAG, "Session error: " + e.getMessage()); + } finally { + closeSession(); + } + } + + private void handleCommand(String commandLine) throws IOException { + String[] parts = commandLine.split("\\s+", 2); + String command = parts[0].toUpperCase(); + String argument = parts.length > 1 ? parts[1] : ""; + + switch (command) { + case "USER": + handleUser(argument); + break; + case "PASS": + handlePass(argument); + break; + case "QUIT": + handleQuit(); + break; + case "SYST": + handleSyst(); + break; + case "PWD": + handlePwd(); + break; + case "NOOP": + handleNoop(); + break; + default: + sendResponse(FTPResponse.COMMAND_NOT_IMPLEMENTED_502, "Command not implemented"); + break; + } + } + + private void handleUser(String username) throws IOException { + this.username = username; + sendResponse(FTPResponse.USERNAME_OK_NEED_PASSWORD, "Username OK, password required"); + } + + private void handlePass(String password) throws IOException { + if (username == null || username.isEmpty()) { + sendResponse(FTPResponse.BAD_SEQUENCE, "Login with USER first"); + return; + } + + // Simple authentication - accept any password for now + // In production, implement proper authentication + isAuthenticated = true; + sendResponse(FTPResponse.USER_LOGGED_IN, "User logged in"); + } + + private void handleQuit() throws IOException { + sendResponse(FTPResponse.SERVICE_CLOSING, "Goodbye"); + isRunning = false; + } + + private void handleSyst() throws IOException { + sendResponse(FTPResponse.SYSTEM_TYPE, "UNIX Type: L8"); + } + + private void handlePwd() throws IOException { + if (!isAuthenticated) { + sendResponse(FTPResponse.NOT_LOGGED_IN, "Please login first"); + return; + } + sendResponse(FTPResponse.PATHNAME_CREATED, "\"" + currentDirectory + "\" is current directory"); + } + + private void handleNoop() throws IOException { + sendResponse(FTPResponse.COMMAND_OK, "OK"); + } + + private void sendResponse(int code, String message) throws IOException { + String response = FTPResponse.format(code, message); + writer.write(response); + writer.flush(); + Log.d(TAG, "Response sent: " + response.trim()); + } + + private void closeSession() { + try { + if (reader != null) reader.close(); + if (writer != null) writer.close(); + if (controlSocket != null && !controlSocket.isClosed()) { + controlSocket.close(); + } + Log.d(TAG, "Session closed"); + } catch (IOException e) { + Log.e(TAG, "Error closing session: " + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/be/gyu/android/server/ftp/MainActivity.java b/app/src/main/java/be/gyu/android/server/ftp/MainActivity.java new file mode 100644 index 0000000..c4a3001 --- /dev/null +++ b/app/src/main/java/be/gyu/android/server/ftp/MainActivity.java @@ -0,0 +1,122 @@ +package be.gyu.android.server.ftp; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; + +import android.Manifest; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.os.Build; +import android.os.Bundle; +import android.widget.Button; +import android.widget.TextView; +import android.widget.Toast; + +public class MainActivity extends AppCompatActivity { + + private static final int PERMISSION_REQUEST_CODE = 100; + + private TextView statusTextView; + private Button startButton; + private Button stopButton; + + private boolean isServerRunning = false; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + initViews(); + setupListeners(); + requestPermissions(); + } + + private void initViews() { + statusTextView = findViewById(R.id.statusTextView); + startButton = findViewById(R.id.startButton); + stopButton = findViewById(R.id.stopButton); + } + + private void setupListeners() { + startButton.setOnClickListener(v -> startFTPServer()); + stopButton.setOnClickListener(v -> stopFTPServer()); + } + + private void requestPermissions() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + // Android 13+ + if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_MEDIA_IMAGES) + != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(this, + new String[]{ + Manifest.permission.READ_MEDIA_IMAGES, + Manifest.permission.READ_MEDIA_VIDEO, + Manifest.permission.READ_MEDIA_AUDIO + }, + PERMISSION_REQUEST_CODE); + } + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + // Android 6.0 - 12 + if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) + != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(this, + new String[]{ + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE + }, + PERMISSION_REQUEST_CODE); + } + } + } + + private void startFTPServer() { + Intent serviceIntent = new Intent(this, FTPService.class); + serviceIntent.setAction(FTPService.ACTION_START); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startForegroundService(serviceIntent); + } else { + startService(serviceIntent); + } + + isServerRunning = true; + updateUI(); + Toast.makeText(this, "FTP Server Started", Toast.LENGTH_SHORT).show(); + } + + private void stopFTPServer() { + Intent serviceIntent = new Intent(this, FTPService.class); + serviceIntent.setAction(FTPService.ACTION_STOP); + startService(serviceIntent); + + isServerRunning = false; + updateUI(); + Toast.makeText(this, "FTP Server Stopped", Toast.LENGTH_SHORT).show(); + } + + private void updateUI() { + if (isServerRunning) { + statusTextView.setText("Server Status: Running"); + startButton.setEnabled(false); + stopButton.setEnabled(true); + } else { + statusTextView.setText("Server Status: Stopped"); + startButton.setEnabled(true); + stopButton.setEnabled(false); + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if (requestCode == PERMISSION_REQUEST_CODE) { + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + Toast.makeText(this, "Permissions granted", Toast.LENGTH_SHORT).show(); + } else { + Toast.makeText(this, "Permissions denied. App may not work correctly.", Toast.LENGTH_LONG).show(); + } + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..1021401 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,67 @@ + + + + + + + + + +