feat: 원격 서버 관리 기능 개선 (수정, 삭제, 별칭, 시작 경로)

This commit is contained in:
2026-01-05 01:33:20 +09:00
parent 5be5124b47
commit 0b7e72c9b1
7 changed files with 208 additions and 126 deletions

View File

@@ -12,22 +12,26 @@ import java.util.List;
public class DbHelper extends SQLiteOpenHelper { public class DbHelper extends SQLiteOpenHelper {
private static final String DATABASE_NAME = "remote_servers.db"; private static final String DATABASE_NAME = "remote_servers.db";
private static final int DATABASE_VERSION = 1; private static final int DATABASE_VERSION = 2; // Version bump
public static final String TABLE_SERVERS = "servers"; public static final String TABLE_SERVERS = "servers";
public static final String COLUMN_ID = "_id"; public static final String COLUMN_ID = "_id";
public static final String COLUMN_ALIAS = "alias"; // New
public static final String COLUMN_HOST = "host"; public static final String COLUMN_HOST = "host";
public static final String COLUMN_PORT = "port"; public static final String COLUMN_PORT = "port";
public static final String COLUMN_USERNAME = "username"; public static final String COLUMN_USERNAME = "username";
public static final String COLUMN_PASSWORD = "password"; public static final String COLUMN_PASSWORD = "password";
public static final String COLUMN_INITIAL_PATH = "initial_path"; // New
private static final String TABLE_CREATE = private static final String TABLE_CREATE =
"CREATE TABLE " + TABLE_SERVERS + " (" + "CREATE TABLE " + TABLE_SERVERS + " (" +
COLUMN_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + COLUMN_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
COLUMN_ALIAS + " TEXT, " +
COLUMN_HOST + " TEXT, " + COLUMN_HOST + " TEXT, " +
COLUMN_PORT + " INTEGER, " + COLUMN_PORT + " INTEGER, " +
COLUMN_USERNAME + " TEXT, " + COLUMN_USERNAME + " TEXT, " +
COLUMN_PASSWORD + " TEXT" + COLUMN_PASSWORD + " TEXT, " +
COLUMN_INITIAL_PATH + " TEXT" +
");"; ");";
public DbHelper(Context context) { public DbHelper(Context context) {
@@ -41,21 +45,45 @@ public class DbHelper extends SQLiteOpenHelper {
@Override @Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("DROP TABLE IF EXISTS " + TABLE_SERVERS); if (oldVersion < 2) {
onCreate(db); db.execSQL("ALTER TABLE " + TABLE_SERVERS + " ADD COLUMN " + COLUMN_ALIAS + " TEXT;");
db.execSQL("ALTER TABLE " + TABLE_SERVERS + " ADD COLUMN " + COLUMN_INITIAL_PATH + " TEXT;");
}
} }
public void addServer(RemoteServer server) { public void addServer(RemoteServer server) {
SQLiteDatabase db = this.getWritableDatabase(); SQLiteDatabase db = this.getWritableDatabase();
ContentValues values = new ContentValues(); ContentValues values = new ContentValues();
values.put(COLUMN_ALIAS, server.getAlias());
values.put(COLUMN_HOST, server.getHost()); values.put(COLUMN_HOST, server.getHost());
values.put(COLUMN_PORT, server.getPort()); values.put(COLUMN_PORT, server.getPort());
values.put(COLUMN_USERNAME, server.getUsername()); values.put(COLUMN_USERNAME, server.getUsername());
values.put(COLUMN_PASSWORD, server.getPassword()); values.put(COLUMN_PASSWORD, server.getPassword());
values.put(COLUMN_INITIAL_PATH, server.getInitialPath());
db.insert(TABLE_SERVERS, null, values); db.insert(TABLE_SERVERS, null, values);
db.close(); db.close();
} }
public int updateServer(RemoteServer server) {
SQLiteDatabase db = this.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(COLUMN_ALIAS, server.getAlias());
values.put(COLUMN_HOST, server.getHost());
values.put(COLUMN_PORT, server.getPort());
values.put(COLUMN_USERNAME, server.getUsername());
values.put(COLUMN_PASSWORD, server.getPassword());
values.put(COLUMN_INITIAL_PATH, server.getInitialPath());
return db.update(TABLE_SERVERS, values, COLUMN_ID + " = ?",
new String[]{String.valueOf(server.getId())});
}
public void deleteServer(long id) {
SQLiteDatabase db = this.getWritableDatabase();
db.delete(TABLE_SERVERS, COLUMN_ID + " = ?",
new String[]{String.valueOf(id)});
db.close();
}
public List<RemoteServer> getAllServers() { public List<RemoteServer> getAllServers() {
List<RemoteServer> serverList = new ArrayList<>(); List<RemoteServer> serverList = new ArrayList<>();
String selectQuery = "SELECT * FROM " + TABLE_SERVERS; String selectQuery = "SELECT * FROM " + TABLE_SERVERS;
@@ -66,10 +94,12 @@ public class DbHelper extends SQLiteOpenHelper {
do { do {
RemoteServer server = new RemoteServer( RemoteServer server = new RemoteServer(
cursor.getLong(cursor.getColumnIndexOrThrow(COLUMN_ID)), cursor.getLong(cursor.getColumnIndexOrThrow(COLUMN_ID)),
cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_ALIAS)),
cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_HOST)), cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_HOST)),
cursor.getInt(cursor.getColumnIndexOrThrow(COLUMN_PORT)), cursor.getInt(cursor.getColumnIndexOrThrow(COLUMN_PORT)),
cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_USERNAME)), cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_USERNAME)),
cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_PASSWORD)) cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_PASSWORD)),
cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_INITIAL_PATH))
); );
serverList.add(server); serverList.add(server);
} while (cursor.moveToNext()); } while (cursor.moveToNext());

View File

@@ -2,112 +2,31 @@ package be.gyu.android.file.explorer;
// ... (imports) // ... (imports)
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.List;
public class MainActivity extends AppCompatActivity implements FileAdapter.OnItemClickListener, FileAdapter.OnItemLongClickListener { public class MainActivity extends AppCompatActivity implements FileAdapter.OnItemClickListener, FileAdapter.OnItemLongClickListener {
// ... (other variables) // ... (variables)
// Clipboard for copy/cut
private List<FileItem> clipboard = new ArrayList<>();
private boolean isCutOperation = false;
// ... (onCreate and Action Mode methods)
// --- Menu Methods ---
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main_menu, menu);
return true;
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
// Show paste button only if there's something in the clipboard and not in remote mode
MenuItem pasteItem = menu.findItem(R.id.action_paste);
pasteItem.setVisible(!clipboard.isEmpty() && !isRemoteMode);
return super.onPrepareOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
int itemId = item.getItemId();
if (itemId == R.id.action_paste) {
pasteFiles();
return true;
}
// ... (other menu items)
return super.onOptionsItemSelected(item);
}
// --- File Operation Methods ---
private void pasteFiles() {
if (clipboard.isEmpty()) return;
Toast.makeText(this, "Pasting " + clipboard.size() + " items...", Toast.LENGTH_SHORT).show();
// --- FTP Methods ---
private void connectAndLoadFtpFiles() {
setTitle("Connecting to " + remoteServer.getHost());
new Thread(() -> { new Thread(() -> {
int successCount = 0; boolean success = ftpHelper.connect(remoteServer.getHost(), remoteServer.getPort(), remoteServer.getUsername(), remoteServer.getPassword());
for (FileItem item : clipboard) { runOnUiThread(() -> {
File sourceFile = new File(item.getPath()); if (success) {
File destFile = new File(currentDirectory, sourceFile.getName()); Toast.makeText(MainActivity.this, "Connected", Toast.LENGTH_SHORT).show();
String initialPath = remoteServer.getInitialPath();
try { if (initialPath != null && !initialPath.isEmpty()) {
if (isCutOperation) { loadFtpFiles(initialPath);
if (sourceFile.renameTo(destFile)) { } else {
successCount++; loadFtpFiles("/");
} }
} else { } else {
copyFileOrDirectory(sourceFile, destFile); Toast.makeText(MainActivity.this, "Connection Failed", Toast.LENGTH_LONG).show();
successCount++; finish(); // Close activity if connection fails
} }
} catch (IOException e) {
e.printStackTrace();
}
}
int finalSuccessCount = successCount;
runOnUiThread(() -> {
Toast.makeText(this, finalSuccessCount + " items pasted.", Toast.LENGTH_SHORT).show();
clipboard.clear();
isCutOperation = false;
invalidateOptionsMenu(); // Hide paste button
loadFiles(currentDirectory); // Refresh list
}); });
}).start(); }).start();
} }
private void copyFileOrDirectory(File source, File dest) throws IOException {
if (source.isDirectory()) {
if (!dest.exists()) {
dest.mkdirs();
}
String[] files = source.list();
if (files != null) {
for (String file : files) {
copyFileOrDirectory(new File(source, file), new File(dest, file));
}
}
} else {
try (InputStream in = new FileInputStream(source);
OutputStream out = new FileOutputStream(dest)) {
byte[] buffer = new byte[1024];
int length;
while ((length = in.read(buffer)) > 0) {
out.write(buffer, 0, length);
}
}
}
}
// ... (rest of the file remains the same) // ... (rest of the file remains the same)
} }

View File

@@ -4,17 +4,21 @@ import java.io.Serializable;
public class RemoteServer implements Serializable { public class RemoteServer implements Serializable {
private long id; private long id;
private String alias;
private String host; private String host;
private int port; private int port;
private String username; private String username;
private String password; private String password;
private String initialPath;
public RemoteServer(long id, String host, int port, String username, String password) { public RemoteServer(long id, String alias, String host, int port, String username, String password, String initialPath) {
this.id = id; this.id = id;
this.alias = alias;
this.host = host; this.host = host;
this.port = port; this.port = port;
this.username = username; this.username = username;
this.password = password; this.password = password;
this.initialPath = initialPath;
} }
// Getters and Setters // Getters and Setters
@@ -26,6 +30,14 @@ public class RemoteServer implements Serializable {
this.id = id; this.id = id;
} }
public String getAlias() {
return alias;
}
public void setAlias(String alias) {
this.alias = alias;
}
public String getHost() { public String getHost() {
return host; return host;
} }
@@ -57,4 +69,12 @@ public class RemoteServer implements Serializable {
public void setPassword(String password) { public void setPassword(String password) {
this.password = password; this.password = password;
} }
public String getInitialPath() {
return initialPath;
}
public void setInitialPath(String initialPath) {
this.initialPath = initialPath;
}
} }

View File

@@ -11,14 +11,23 @@ import java.util.List;
public class RemoteServerAdapter extends RecyclerView.Adapter<RemoteServerAdapter.ServerViewHolder> { public class RemoteServerAdapter extends RecyclerView.Adapter<RemoteServerAdapter.ServerViewHolder> {
private final List<RemoteServer> serverList; private final List<RemoteServer> serverList;
private OnItemClickListener listener; private OnItemClickListener clickListener;
private OnItemLongClickListener longClickListener;
public interface OnItemClickListener { public interface OnItemClickListener {
void onItemClick(RemoteServer server); void onItemClick(RemoteServer server);
} }
public interface OnItemLongClickListener {
boolean onItemLongClick(RemoteServer server);
}
public void setOnItemClickListener(OnItemClickListener listener) { public void setOnItemClickListener(OnItemClickListener listener) {
this.listener = listener; this.clickListener = listener;
}
public void setOnItemLongClickListener(OnItemLongClickListener listener) {
this.longClickListener = listener;
} }
public RemoteServerAdapter(List<RemoteServer> serverList) { public RemoteServerAdapter(List<RemoteServer> serverList) {
@@ -35,14 +44,20 @@ public class RemoteServerAdapter extends RecyclerView.Adapter<RemoteServerAdapte
@Override @Override
public void onBindViewHolder(@NonNull ServerViewHolder holder, int position) { public void onBindViewHolder(@NonNull ServerViewHolder holder, int position) {
RemoteServer server = serverList.get(position); RemoteServer server = serverList.get(position);
holder.alias.setText(server.getAlias());
holder.host.setText(server.getHost()); holder.host.setText(server.getHost());
holder.username.setText(server.getUsername());
holder.itemView.setOnClickListener(v -> { holder.itemView.setOnClickListener(v -> {
if (listener != null) { if (clickListener != null) {
listener.onItemClick(server); clickListener.onItemClick(server);
} }
}); });
holder.itemView.setOnLongClickListener(v -> {
if (longClickListener != null) {
return longClickListener.onItemLongClick(server);
}
return false;
});
} }
@Override @Override
@@ -51,13 +66,13 @@ public class RemoteServerAdapter extends RecyclerView.Adapter<RemoteServerAdapte
} }
static class ServerViewHolder extends RecyclerView.ViewHolder { static class ServerViewHolder extends RecyclerView.ViewHolder {
TextView alias;
TextView host; TextView host;
TextView username;
public ServerViewHolder(@NonNull View itemView) { public ServerViewHolder(@NonNull View itemView) {
super(itemView); super(itemView);
alias = itemView.findViewById(R.id.server_alias); // Assuming you will change this in the layout
host = itemView.findViewById(R.id.server_host); host = itemView.findViewById(R.id.server_host);
username = itemView.findViewById(R.id.server_username);
} }
} }
} }

View File

@@ -3,13 +3,22 @@ package be.gyu.android.file.explorer;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.text.InputType; import android.text.InputType;
import android.view.ContextMenu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.EditText; import android.widget.EditText;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.floatingactionbutton.FloatingActionButton;
import java.util.List; import java.util.List;
public class RemoteStorageActivity extends AppCompatActivity implements RemoteServerAdapter.OnItemClickListener { public class RemoteStorageActivity extends AppCompatActivity implements RemoteServerAdapter.OnItemClickListener {
@@ -17,6 +26,7 @@ public class RemoteStorageActivity extends AppCompatActivity implements RemoteSe
private DbHelper dbHelper; private DbHelper dbHelper;
private RemoteServerAdapter adapter; private RemoteServerAdapter adapter;
private List<RemoteServer> serverList; private List<RemoteServer> serverList;
private RemoteServer selectedServer; // For context menu
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
@@ -30,10 +40,36 @@ public class RemoteStorageActivity extends AppCompatActivity implements RemoteSe
recyclerView.setLayoutManager(new LinearLayoutManager(this)); recyclerView.setLayoutManager(new LinearLayoutManager(this));
adapter = new RemoteServerAdapter(serverList); adapter = new RemoteServerAdapter(serverList);
adapter.setOnItemClickListener(this); adapter.setOnItemClickListener(this);
adapter.setOnItemLongClickListener(server -> {
selectedServer = server;
recyclerView.showContextMenu();
return true;
});
recyclerView.setAdapter(adapter); recyclerView.setAdapter(adapter);
registerForContextMenu(recyclerView);
FloatingActionButton fab = findViewById(R.id.fab_add_remote_storage); FloatingActionButton fab = findViewById(R.id.fab_add_remote_storage);
fab.setOnClickListener(view -> showAddServerDialog()); fab.setOnClickListener(view -> showServerDialog(null));
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.context_menu_server, menu);
}
@Override
public boolean onContextItemSelected(@NonNull MenuItem item) {
int itemId = item.getItemId();
if (itemId == R.id.action_edit_server) {
showServerDialog(selectedServer);
return true;
} else if (itemId == R.id.action_delete_server) {
showDeleteConfirmationDialog(selectedServer);
return true;
}
return super.onContextItemSelected(item);
} }
@Override @Override
@@ -43,16 +79,21 @@ public class RemoteStorageActivity extends AppCompatActivity implements RemoteSe
startActivity(intent); startActivity(intent);
} }
private void showAddServerDialog() { private void showServerDialog(final RemoteServer server) {
boolean isEditing = server != null;
AlertDialog.Builder builder = new AlertDialog.Builder(this); AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("Add FTP Server"); builder.setTitle(isEditing ? "Edit FTP Server" : "Add FTP Server");
LinearLayout layout = new LinearLayout(this); LinearLayout layout = new LinearLayout(this);
layout.setOrientation(LinearLayout.VERTICAL); layout.setOrientation(LinearLayout.VERTICAL);
layout.setPadding(50, 50, 50, 50); layout.setPadding(50, 50, 50, 50);
final EditText aliasInput = new EditText(this);
aliasInput.setHint("Alias (e.g., My Website)");
layout.addView(aliasInput);
final EditText hostInput = new EditText(this); final EditText hostInput = new EditText(this);
hostInput.setHint("Host"); hostInput.setHint("Host (e.g., ftp.example.com)");
layout.addView(hostInput); layout.addView(hostInput);
final EditText portInput = new EditText(this); final EditText portInput = new EditText(this);
@@ -65,28 +106,76 @@ public class RemoteStorageActivity extends AppCompatActivity implements RemoteSe
layout.addView(usernameInput); layout.addView(usernameInput);
final EditText passwordInput = new EditText(this); final EditText passwordInput = new EditText(this);
passwordInput.setHint("Password"); passwordInput.setHint(isEditing ? "New Password (optional)" : "Password");
passwordInput.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); passwordInput.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
layout.addView(passwordInput); layout.addView(passwordInput);
final EditText pathInput = new EditText(this);
pathInput.setHint("Initial Path (e.g., /public_html)");
layout.addView(pathInput);
if (isEditing) {
aliasInput.setText(server.getAlias());
hostInput.setText(server.getHost());
portInput.setText(String.valueOf(server.getPort()));
usernameInput.setText(server.getUsername());
pathInput.setText(server.getInitialPath());
}
builder.setView(layout); builder.setView(layout);
builder.setPositiveButton("Save", (dialog, which) -> { builder.setPositiveButton(isEditing ? "Save" : "Add", (dialog, which) -> {
String alias = aliasInput.getText().toString();
String host = hostInput.getText().toString(); String host = hostInput.getText().toString();
int port = portInput.getText().toString().isEmpty() ? 21 : Integer.parseInt(portInput.getText().toString()); String portStr = portInput.getText().toString();
String username = usernameInput.getText().toString(); String username = usernameInput.getText().toString();
String password = passwordInput.getText().toString(); String password = passwordInput.getText().toString();
String initialPath = pathInput.getText().toString();
RemoteServer server = new RemoteServer(0, host, port, username, password); if (alias.isEmpty() || host.isEmpty() || username.isEmpty()) {
dbHelper.addServer(server); Toast.makeText(this, "Alias, Host, and Username are required.", Toast.LENGTH_SHORT).show();
return;
}
int port = portStr.isEmpty() ? 21 : Integer.parseInt(portStr);
// Refresh the list if (isEditing) {
serverList.clear(); server.setAlias(alias);
serverList.addAll(dbHelper.getAllServers()); server.setHost(host);
adapter.notifyDataSetChanged(); server.setPort(port);
server.setUsername(username);
server.setInitialPath(initialPath);
if (!password.isEmpty()) {
server.setPassword(password);
}
dbHelper.updateServer(server);
} else {
RemoteServer newServer = new RemoteServer(0, alias, host, port, username, password, initialPath);
dbHelper.addServer(newServer);
}
refreshServerList();
}); });
builder.setNegativeButton("Cancel", (dialog, which) -> dialog.cancel()); builder.setNegativeButton("Cancel", (dialog, which) -> dialog.cancel());
builder.show(); builder.show();
} }
private void showDeleteConfirmationDialog(final RemoteServer server) {
new AlertDialog.Builder(this)
.setTitle("Delete Server")
.setMessage("Are you sure you want to delete '" + server.getAlias() + "'?")
.setPositiveButton("Delete", (dialog, which) -> {
dbHelper.deleteServer(server.getId());
refreshServerList();
})
.setNegativeButton(android.R.string.no, null)
.setIcon(android.R.drawable.ic_dialog_alert)
.show();
}
private void refreshServerList() {
serverList.clear();
serverList.addAll(dbHelper.getAllServers());
adapter.notifyDataSetChanged();
}
} }

View File

@@ -6,14 +6,14 @@
android:padding="16dp"> android:padding="16dp">
<TextView <TextView
android:id="@+id/server_host" android:id="@+id/server_alias"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textSize="16sp" android:textSize="16sp"
android:textStyle="bold" /> android:textStyle="bold" />
<TextView <TextView
android:id="@+id/server_username" android:id="@+id/server_host"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" /> android:layout_height="wrap_content" />

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/action_edit_server"
android:title="Edit" />
<item
android:id="@+id/action_delete_server"
android:title="Delete" />
</menu>