This is a follow-up question for Android APP FTP host profile class implementation and Android APP connect to FTP server in Java. I am attempting to perform the upload operation to specified FTP server in Android APP. With referring to the answer of FtpClient storeFile always return False and List of FTP server return codes on wikipedia, the returned reply code is translated to string
message.
The experimental implementation
FtpConnection
class implementation:package com.example.ftpreplycodeclass; import android.util.Log; import org.apache.commons.net.ftp.FTP; import org.apache.commons.net.ftp.FTPClient; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.net.SocketException; import java.net.UnknownHostException; public final class FtpConnection { private final FtpHostProfiles ftpHostProfiles = new FtpHostProfiles(); public FtpConnection(FtpHostProfiles input) { this.ftpHostProfiles.addProfiles(input); } public boolean uploadFile(String profileName, String fullFilename) { File targetFile = new File(fullFilename); if (!targetFile.exists()) { Log.e("FTPconnection_uploadFile", "File " + fullFilename + " isn't existed!"); return false; } // Reference: https://stackoverflow.com/a/8761268/6667035 FTPClient ftp = new FTPClient(); try { FtpHostProfile profile = ftpHostProfiles.getProfile(profileName); // Reference: https://stackoverflow.com/a/55950845/6667035 // The argument of `FTPClient.connect` method is hostname, not URL. ftp.connect(profile.getHostname(), profile.getPort()); boolean status = ftp.login(profile.getUsername(), profile.getPassword()); if (status) { ftp.enterLocalPassiveMode(); ftp.setFileType(FTP.BINARY_FILE_TYPE); ftp.sendCommand("OPTS UTF8 ON"); } System.out.println("status : " + ftp.getStatus()); Log.d("FTPTask", ftp.getStatus()); FileInputStream in = new FileInputStream(fullFilename); String filename = new File(fullFilename).getName(); boolean result = ftp.storeFile(filename, in); var detailedInfo = ftp.getReplyCode(); Log.d("FTP Reply Code", String.valueOf(detailedInfo)); Log.d("FTP Reply Message", FtpReplyCode.getReplyMessage(detailedInfo)); in.close(); ftp.logout(); ftp.disconnect(); return result; } catch (UnknownHostException ex) { ex.printStackTrace(); } catch (SocketException en) { // TODO Auto-generated catch block en.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } return false; } public FTPClient connectftp(String profileName) { // Reference: https://stackoverflow.com/a/8761268/6667035 FTPClient ftp = new FTPClient(); try { FtpHostProfile profile = ftpHostProfiles.getProfile(profileName); // Reference: https://stackoverflow.com/a/55950845/6667035 // The argument of `FtpClient.connect` method is hostname, not URL. ftp.connect(profile.getHostname(), profile.getPort()); boolean loggedIn = ftp.login(profile.getUsername(), profile.getPassword()); if (loggedIn) { ftp.enterLocalPassiveMode(); ftp.setFileType(FTP.BINARY_FILE_TYPE); ftp.sendCommand("OPTS UTF8 ON"); } System.out.println("status : " + ftp.getStatus()); } catch (UnknownHostException ex) { ex.printStackTrace(); } catch (SocketException en) { en.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return ftp; } private static class FtpReplyCode { private static String getReplyMessage(int input) { if (input == 100) { return "The requested action is being initiated, expect another reply before proceeding with a new command."; } if (input == 110) { return "Restart marker replay."; } if (input == 120) { return "Service ready in nnn minutes."; } if (input == 125) { return "Data connection already open; transfer starting."; } if (input == 150) { return "File status okay; about to open data connection."; } if (input == 200) { return "The requested action has been successfully completed."; } if (input == 202) { return "Command not implemented, superfluous at this site."; } if (input == 211) { return "System status, or system help reply."; } if (input == 212) { return "Directory status."; } if (input == 213) { return "File status."; } if (input == 214) { return "Help message. Explains how to use the server or the meaning of a particular non-standard command. This reply is useful only to the human user."; } if (input == 215) { return "NAME system type. Where NAME is an official system name from the registry kept by IANA."; } if (input == 220) { return "Service ready for new user."; } if (input == 221) { return "Service closing control connection."; } if (input == 225) { return "Data connection open; no transfer in progress."; } if (input == 226) { return "Closing data connection. Requested file action successful (for example, file transfer or file abort)."; } if (input == 227) { return "Entering Passive Mode (h1,h2,h3,h4,p1,p2)."; } if (input == 228) { return "Entering Long Passive Mode (long address, port)."; } if (input == 229) { return "Entering Extended Passive Mode (|||port|)."; } if (input == 230) { return "User logged in, proceed. Logged out if appropriate."; } if (input == 231) { return "User logged out; service terminated."; } if (input == 232) { return "Logout command noted, will complete when transfer done."; } if (input == 234) { return "Specifies that the server accepts the authentication mechanism specified by the client, and the exchange of security data is complete. A higher level nonstandard code created by Microsoft."; } if (input == 250) { return "Requested file action okay, completed."; } if (input == 257) { return "\"PATHNAME\" created"; } if (input == 300) { return "The command has been accepted, but the requested action is on hold, pending receipt of further information."; } if (input == 331) { return "User name okay, need password."; } if (input == 332) { return "Need account for login."; } if (input == 350) { return "Requested file action pending further information"; } if (input == 400) { return "The command was not accepted and the requested action did not take place, but the error condition is temporary and the action may be requested again."; } if (input == 421) { return "Service not available, closing control connection. This may be a reply to any command if the service knows it must shut down."; } if (input == 425) { return "Can\'t open data connection."; } if (input == 426) { return "Connection closed; transfer aborted."; } if (input == 430) { return "Invalid username or password"; } if (input == 434) { return "Requested host unavailable."; } if (input == 450) { return "Requested file action not taken."; } if (input == 451) { return "Requested action aborted. Local error in processing."; } if (input == 452) { return "Requested action not taken. Insufficient storage space in system. File unavailable (e.g., file busy)."; } if (input == 500) { return "Syntax error, command unrecognized and the requested action did not take place. This may include errors such as command line too long."; } if (input == 501) { return "Syntax error in parameters or arguments."; } if (input == 502) { return "Command not implemented."; } if (input == 503) { return "Bad sequence of commands."; } if (input == 504) { return "Command not implemented for that parameter."; } if (input == 530) { return "Not logged in."; } if (input == 532) { return "Need account for storing files."; } if (input == 534) { return "Could Not Connect to Server - Policy Requires SSL"; } if (input == 550) { return "Requested action not taken. File unavailable (e.g., file not found, no access)."; } if (input == 551) { return "Requested action aborted. Page type unknown."; } if (input == 552) { return "Requested file action aborted. Exceeded storage allocation (for current directory or dataset)."; } if (input == 553) { return "Requested action not taken. File name not allowed."; } if (input == 600) { return "Replies regarding confidentiality and integrity"; } if (input == 631) { return "Integrity protected reply."; } if (input == 632) { return "Confidentiality and integrity protected reply."; } if (input == 633) { return "Confidentiality protected reply."; } if (input == 10000) { return "Common Winsock Error Codes"; } if (input == 10054) { return "Connection reset by peer. The connection was forcibly closed by the remote host."; } if (input == 10060) { return "Cannot connect to remote server."; } if (input == 10061) { return "Cannot connect to remote server. The connection is actively refused by the server."; } if (input == 10066) { return "Directory not empty."; } if (input == 10068) { return "Too many users, server is full."; } return ""; } } }
Full Testing Code
MainActivity.java
implementation:package com.example.ftpreplycodeclass; import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import android.Manifest; import android.content.Context; import android.content.pm.PackageManager; import android.os.AsyncTask; import android.os.Bundle; import android.util.Log; import android.widget.Toast; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.TimeZone; public class MainActivity extends AppCompatActivity { private static final int READ_EXTERNAL_STORAGE_CODE = 1; private static final int WRITE_EXTERNAL_STORAGE_CODE = 2; private static final int ACCESS_NETWORK_STATE_CODE = 3; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); checkPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE_CODE); checkPermission(Manifest.permission.READ_EXTERNAL_STORAGE, READ_EXTERNAL_STORAGE_CODE); checkPermission(Manifest.permission.ACCESS_NETWORK_STATE, ACCESS_NETWORK_STATE_CODE); User currentUser = new User( "Mike", "M12345678", "1990/10/13", "(555) 555-1234", "[email protected]", "password"); // Using GMT (UTC±00:00) time var gmtTimeZone = new SimpleDateFormat("yyyyMMdd'T'HHmmss"); gmtTimeZone.setTimeZone(TimeZone.getTimeZone("GMT")); String currentTime = gmtTimeZone.format(Calendar.getInstance().getTime()); String newUserRegistrationFilename = currentTime + "_NewUser.ser"; boolean isSaveSuccessfully = Serialization.Save(this, newUserRegistrationFilename, currentUser); if (isSaveSuccessfully) { showToast("Save current user information successfully", Toast.LENGTH_SHORT); Log.d("Save operation", "Save current user information successfully"); } showToast(Serialization.Load(this, newUserRegistrationFilename).getFullName(), Toast.LENGTH_SHORT); try { Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } new FtpTask().execute(this.getApplicationInfo().dataDir + "/files/" + newUserRegistrationFilename); } // Function to check and request permission. public void checkPermission(String permission, int requestCode) { if (ContextCompat.checkSelfPermission(MainActivity.this, permission) == PackageManager.PERMISSION_DENIED) { // Requesting the permission ActivityCompat.requestPermissions(MainActivity.this, new String[] { permission }, requestCode); } else { Toast.makeText(MainActivity.this, "Permission already granted", Toast.LENGTH_SHORT).show(); } } void showToast(String textInput, int duration) { Context context = getApplicationContext(); CharSequence text = textInput; Toast toast = Toast.makeText(context, text, duration); toast.show(); } // AsyncTask must be subclassed to be used. The subclass will override at least one method // `doInBackground(Params...)`, and most often will override a second one `onPostExecute(Result)` // Reference: https://developer.android.com/reference/android/os/AsyncTask?authuser=4 // Reference: https://stackoverflow.com/a/12447497/6667035 private class FtpTask extends AsyncTask<String, Void, Boolean> { // `doInBackground` invoked on the background thread immediately after `onPreExecute()` // finishes executing. This step is used to perform background computation // that can take a long time. The parameters of the asynchronous task are // passed to this step. The result of the computation must be returned by // this step and will be passed back to the last step. This step can also use // `publishProgress(Progress...)` to publish one or more units of progress. // These values are published on the UI thread, in the `onProgressUpdate(Progress...)` // step. protected Boolean doInBackground(String... filenames) { FtpHostProfiles ftpHostProfiles = new FtpHostProfiles(); ftpHostProfiles.addProfile(new FtpHostProfile( "Profile1", "Hostname1", 21, "Username1", "Password1")); ftpHostProfiles.addProfile(new FtpHostProfile( "Profile2", "Hostname2", 21, "Username2", "Password2")); FtpConnection ftpConnect = new FtpConnection(ftpHostProfiles); for (int i = 0; i < filenames.length; i++) { Log.v("FTPTask","Preparing to upload " + filenames[i]); var result = ftpConnect.uploadFile("Profile1", filenames[i]); if (result == false) { Log.v("FTPTask","Fail to upload " + filenames[i] + "!"); return false; } else { Log.v("FTPTask","Upload " + filenames[i] + " successfully!"); } if (isCancelled()) break; } return true; } // `onPostExecute` invoked on the UI thread after the background computation finishes. // The result of the background computation is passed to this step as // a parameter. protected void onPostExecute(boolean result) { if (result) { Log.v("FTPTask","FTP upload complete"); } return; } } }
User
class implementation:package com.example.ftpreplycodeclass; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; public class User implements java.io.Serializable { private String fullName; private String personalID; private String dateOfBirth; private String cellPhoneNumber; private String emailInfo; private String password; public User(final String fullNameInput, final String personalIDInput, final String dateOfBirthInput, final String cellPhoneNumberInput, final String emailInfoInput, final String passwordInput) { fullName = fullNameInput; personalID = personalIDInput; dateOfBirth = dateOfBirthInput; cellPhoneNumber = cellPhoneNumberInput; emailInfo = emailInfoInput; try { password = hashingMethod(passwordInput); } catch (Exception ex) { password = passwordInput; } return; } public String getFullName() { return fullName; } public String getPersonalID() { return personalID; } public String getDateOfBirth() { return dateOfBirth; } public String getCellPhoneNumber() { return cellPhoneNumber; } public String getEmailInfo() { return emailInfo; } public String getHash() throws NoSuchAlgorithmException { return hashingMethod(fullName + personalID); } public String getHashedPassword() throws NoSuchAlgorithmException { return password; } public boolean checkPassword(String password) { boolean result = false; try { result = password.equals(hashingMethod(password)); } catch (Exception e) { e.printStackTrace(); } return result; } //********************************************************************************************** // Reference: https://stackoverflow.com/a/2624385/6667035 private String hashingMethod(String inputString) throws NoSuchAlgorithmException { MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); String stringToHash = inputString; messageDigest.update(stringToHash.getBytes()); String stringHash = new String(messageDigest.digest()); return stringHash; } }
Serialization
class implementation:package com.example.ftpreplycodeclass; import android.content.Context; import android.util.Log; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; // Reference: https://codereview.stackexchange.com/q/260909/231235 public class Serialization { // Reference: https://stackoverflow.com/a/4118917/6667035 // fileName cannot contain any path separator public static boolean Save(Context context, String fileName, User user) { try { FileOutputStream fos = context.openFileOutput(fileName, Context.MODE_PRIVATE); String fullFilename = context.getFilesDir() + "/" + fileName; Log.d("Save", "Save file in " + fullFilename); ObjectOutputStream os = new ObjectOutputStream(fos); os.writeObject(user); os.close(); fos.close(); if (new File(fullFilename).exists()) { return true; } else { return false; } } catch (IOException i) { i.printStackTrace(); return false; } } public static User Load(Context context, String fileName) { try { FileInputStream fis = context.openFileInput(fileName); ObjectInputStream is = new ObjectInputStream(fis); User simpleClass = (User) is.readObject(); is.close(); fis.close(); return simpleClass; } catch (Exception e) { e.printStackTrace(); } return null; } }
FtpHostProfile
class implementation:package com.example.ftpreplycodeclass; public final class FtpHostProfile { private final String profileName; private final String ftpHostName; private final int ftpPort; private final String ftpUserName; private final String ftpPassword; public FtpHostProfile(String hostNameInput, int portInput, String userNameInput, String passwordInput) { this.profileName = ""; this.ftpHostName = hostNameInput; this.ftpPort = portInput; this.ftpUserName = userNameInput; this.ftpPassword = passwordInput; } public FtpHostProfile( String profileNameInput, String hostnameInput, int portInput, String usernameInput, String passwordInput) { this.profileName = profileNameInput; this.ftpHostName = hostnameInput; this.ftpPort = portInput; this.ftpUserName = usernameInput; this.ftpPassword = passwordInput; } public String getProfileName() { return this.profileName; } public String getHostname() { return this.ftpHostName; } public int getPort() { return this.ftpPort; } public String getUsername() { return this.ftpUserName; } public String getPassword() { return this.ftpPassword; } }
FtpHostProfiles
class implementation:package com.example.ftpreplycodeclass; import android.util.Log; import java.util.Collection; import java.util.HashMap; import java.util.Map; public final class FtpHostProfiles { private final Map<String, FtpHostProfile> profiles = new HashMap<>(); public FtpHostProfiles() { // Empty constructor } public FtpHostProfiles addProfile(FtpHostProfile input) { this.profiles.put(input.getProfileName(), input); return this; } public FtpHostProfiles addProfiles(Collection<FtpHostProfile> input) { input.stream().forEach(profile -> profiles.put(profile.getProfileName(), profile)); return this; } public FtpHostProfiles addProfiles(FtpHostProfiles input) { this.profiles.putAll(input.profiles); return this; } public FtpHostProfile getProfile(String profileNameInput) { FtpHostProfile profile = profiles.get(profileNameInput); if (profile == null) { Log.d("GetProfile", "No such item in stored profiles"); throw new IllegalStateException("No such item in stored profiles"); } return profile; } }
AndroidManifest.xml
:<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.ftpreplycodeclass"> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.FtpReplyCodeClass"> <activity android:name=".MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> </manifest>
All suggestions are welcome.
The summary information:
Which question it is a follow-up to?
What changes has been made in the code since last question?
I am attempting to perform the upload operation to specified FTP server in Android APP in this post.
Why a new review is being asked for?
If there is any possible improvement, please let me know.