Java tutorial
/* * DD-WRT Companion is a mobile app that lets you connect to, * monitor and manage your DD-WRT routers on the go. * * Copyright (C) 2014 Armel Soro * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * * Contact Info: Armel Soro <apps+ddwrt@rm3l.org> */ package org.rm3l.ddwrt.mgmt; import android.annotation.TargetApi; import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; import android.content.ContentResolver; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.database.Cursor; import android.net.Uri; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.provider.OpenableColumns; import android.support.annotation.Nullable; import android.support.v4.app.FragmentActivity; import android.text.Editable; import android.util.Log; import android.util.Patterns; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.inputmethod.InputMethodManager; import android.widget.AdapterView; import android.widget.Button; import android.widget.CheckBox; import android.widget.EditText; import android.widget.RadioGroup; import android.widget.Spinner; import android.widget.TextView; import com.actionbarsherlock.app.SherlockDialogFragment; import com.google.common.base.Strings; import com.google.common.base.Throwables; import org.apache.commons.io.IOUtils; import org.jetbrains.annotations.NotNull; import org.rm3l.ddwrt.R; import org.rm3l.ddwrt.exceptions.DDWRTCompanionException; import org.rm3l.ddwrt.mgmt.dao.DDWRTCompanionDAO; import org.rm3l.ddwrt.resources.conn.Router; import org.rm3l.ddwrt.utils.SSHUtils; import org.rm3l.ddwrt.utils.Utils; import java.io.IOException; import de.keyboardsurfer.android.widget.crouton.Crouton; import de.keyboardsurfer.android.widget.crouton.Style; import static com.google.common.base.Strings.isNullOrEmpty; import static de.keyboardsurfer.android.widget.crouton.Style.ALERT; import static org.rm3l.ddwrt.utils.DDWRTCompanionConstants.ALWAYS_CHECK_CONNECTION_PREF_KEY; import static org.rm3l.ddwrt.utils.DDWRTCompanionConstants.DEFAULT_SHARED_PREFERENCES_KEY; import static org.rm3l.ddwrt.utils.DDWRTCompanionConstants.MAX_PRIVKEY_SIZE_BYTES; import static org.rm3l.ddwrt.utils.Utils.toHumanReadableByteCount; public abstract class AbstractRouterMgmtDialogFragment extends SherlockDialogFragment implements AdapterView.OnItemSelectedListener { private static final String LOG_TAG = AbstractRouterMgmtDialogFragment.class.getSimpleName(); private static final int READ_REQUEST_CODE = 42; protected DDWRTCompanionDAO dao; private RouterMgmtDialogListener mListener; protected abstract CharSequence getDialogMessage(); @org.jetbrains.annotations.Nullable protected abstract CharSequence getDialogTitle(); protected abstract CharSequence getPositiveButtonMsg(); protected abstract void onPositiveButtonActionSuccess(@NotNull RouterMgmtDialogListener mListener, @Nullable Router router, boolean error); protected SharedPreferences mGlobalSharedPreferences; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); this.dao = RouterManagementActivity.getDao(getActivity()); mGlobalSharedPreferences = getActivity().getSharedPreferences(DEFAULT_SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE); } @NotNull @Override public Dialog onCreateDialog(Bundle savedInstanceState) { final FragmentActivity activity = getActivity(); @NotNull AlertDialog.Builder builder = new AlertDialog.Builder(activity); // Get the layout inflater @NotNull final LayoutInflater inflater = activity.getLayoutInflater(); // Inflate and set the layout for the dialog // Pass null as the parent view because its going in the dialog layout final View view = inflater.inflate(R.layout.activity_router_add, null); ((Spinner) view.findViewById(R.id.router_add_proto)).setOnItemSelectedListener(this); ((RadioGroup) view.findViewById(R.id.router_add_ssh_auth_method)) .setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() { @Override public void onCheckedChanged(RadioGroup group, int checkedId) { final View privkeyHdrView = view.findViewById(R.id.router_add_privkey_hdr); final Button privkeyView = (Button) view.findViewById(R.id.router_add_privkey); final TextView privkeyPathView = (TextView) view.findViewById(R.id.router_add_privkey_path); final TextView pwdHdrView = (TextView) view.findViewById(R.id.router_add_password_hdr); final EditText pwdView = (EditText) view.findViewById(R.id.router_add_password); switch (checkedId) { case R.id.router_add_ssh_auth_method_none: privkeyPathView.setText(null); privkeyHdrView.setVisibility(View.GONE); privkeyView.setVisibility(View.GONE); pwdHdrView.setVisibility(View.GONE); pwdView.setText(null); pwdView.setVisibility(View.GONE); break; case R.id.router_add_ssh_auth_method_password: privkeyPathView.setText(null); privkeyHdrView.setVisibility(View.GONE); privkeyView.setVisibility(View.GONE); pwdHdrView.setText("Password"); pwdHdrView.setVisibility(View.VISIBLE); pwdView.setVisibility(View.VISIBLE); pwdView.setHint("e.g., 'default' (may be empty) "); break; case R.id.router_add_ssh_auth_method_privkey: pwdView.setText(null); privkeyView.setHint(getString(R.string.router_add_path_to_privkey)); pwdHdrView.setText("Passphrase (if applicable)"); pwdHdrView.setVisibility(View.VISIBLE); pwdView.setVisibility(View.VISIBLE); pwdView.setHint("Key passphrase, if applicable"); privkeyHdrView.setVisibility(View.VISIBLE); privkeyView.setVisibility(View.VISIBLE); break; default: break; } } }); builder.setMessage(this.getDialogMessage()).setView(view) // Add action buttons .setPositiveButton(this.getPositiveButtonMsg(), new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int id) { //Do nothing here because we override this button later to change the close behaviour. //However, we still need this because on older versions of Android unless we //pass a handler the button doesn't get instantiated } }).setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { AbstractRouterMgmtDialogFragment.this.getDialog().cancel(); } }); if (!(this.getDialogTitle() == null || this.getDialogTitle().toString().isEmpty())) { builder.setTitle(this.getDialogTitle()); } return builder.create(); } /** * Receive the result from a previous call to * {@link #startActivityForResult(android.content.Intent, int)}. This follows the * related Activity API as described there in * {@link android.app.Activity#onActivityResult(int, int, android.content.Intent)}. * * @param requestCode The integer request code originally supplied to * startActivityForResult(), allowing you to identify who this * result came from. * @param resultCode The integer result code returned by the child activity * through its setResult(). * @param resultData An Intent, which can return result data to the caller */ @Override public void onActivityResult(int requestCode, int resultCode, Intent resultData) { // The ACTION_OPEN_DOCUMENT intent was sent with the request code // READ_REQUEST_CODE. If the request code seen here doesn't match, it's the // response to some other intent, and the code below shouldn't run at all. if (requestCode == READ_REQUEST_CODE && resultCode == Activity.RESULT_OK) { // The document selected by the user won't be returned in the intent. // Instead, a URI to that document will be contained in the return intent // provided to this method as a parameter. // Pull that URI using resultData.getData(). Uri uri; if (resultData != null) { uri = resultData.getData(); Log.i(LOG_TAG, "Uri: " + uri.toString()); final AlertDialog d = (AlertDialog) getDialog(); if (d != null) { final ContentResolver contentResolver = this.getSherlockActivity().getContentResolver(); final Cursor uriCursor = contentResolver.query(uri, null, null, null, null); /* * Get the column indexes of the data in the Cursor, * move to the first row in the Cursor, get the data, * and display it. */ final int nameIndex = uriCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME); final int sizeIndex = uriCursor.getColumnIndex(OpenableColumns.SIZE); uriCursor.moveToFirst(); //File size in bytes final long fileSize = uriCursor.getLong(sizeIndex); final String filename = uriCursor.getString(nameIndex); //Check file size if (fileSize > MAX_PRIVKEY_SIZE_BYTES) { displayMessage(String.format("File '%s' too big (%s). Limit is %s", filename, toHumanReadableByteCount(fileSize), toHumanReadableByteCount(MAX_PRIVKEY_SIZE_BYTES)), ALERT); return; } //Replace button hint message with file name final Button fileSelectorButton = (Button) d.findViewById(R.id.router_add_privkey); final CharSequence fileSelectorOriginalHint = fileSelectorButton.getHint(); if (!Strings.isNullOrEmpty(filename)) { fileSelectorButton.setHint(filename); } //Set file actual content in hidden field final TextView privKeyPath = (TextView) d.findViewById(R.id.router_add_privkey_path); try { privKeyPath.setText(IOUtils.toString(contentResolver.openInputStream(uri))); } catch (IOException e) { displayMessage("Error: " + e.getMessage(), ALERT); e.printStackTrace(); fileSelectorButton.setHint(fileSelectorOriginalHint); } } } } } @Override public void onAttach(@NotNull Activity activity) { super.onAttach(activity); // Verify that the host activity implements the callback interface try { // Instantiate the NoticeDialogListener so we can send events to the host mListener = (RouterMgmtDialogListener) activity; } catch (ClassCastException e) { // The activity doesn't implement the interface, throw exception throw new ClassCastException(activity.toString() + " must implement NoticeDialogListener"); } } @Override public void onStart() { super.onStart(); //super.onStart() is where dialog.show() is actually called on the underlying dialog, so we have to do it after this point final AlertDialog d = (AlertDialog) getDialog(); if (d != null) { d.findViewById(R.id.router_add_privkey).setOnClickListener(new View.OnClickListener() { @TargetApi(Build.VERSION_CODES.KITKAT) @Override public void onClick(View view) { //Open up file picker // ACTION_OPEN_DOCUMENT is the intent to choose a file via the system's file // browser. final Intent intent = new Intent(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { intent.setAction(Intent.ACTION_OPEN_DOCUMENT); } else { intent.setAction(Intent.ACTION_GET_CONTENT); } // Filter to only show results that can be "opened", such as a // file (as opposed to a list of contacts or timezones) intent.addCategory(Intent.CATEGORY_OPENABLE); // search for all documents available via installed storage providers intent.setType("*/*"); AbstractRouterMgmtDialogFragment.this.startActivityForResult(intent, READ_REQUEST_CODE); } }); d.getButton(Dialog.BUTTON_POSITIVE).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { //Validate form boolean validForm = validateForm(d); if (validForm) { // Now check actual connection to router ... new CheckRouterConnectionAsyncTask( ((EditText) d.findViewById(R.id.router_add_ip)).getText().toString(), getSherlockActivity() .getSharedPreferences(DEFAULT_SHARED_PREFERENCES_KEY, Context.MODE_PRIVATE) .getBoolean(ALWAYS_CHECK_CONNECTION_PREF_KEY, true)).execute(d); } ///else dialog stays open. 'Cancel' button can still close it. } }); } } @Nullable private Router doCheckConnectionToRouter(@NotNull AlertDialog d) throws Exception { @NotNull final Router router = buildRouter(d); //This will throw an exception if connection could not be established! SSHUtils.checkConnection(mGlobalSharedPreferences, router, 10000); return router; } private static Router buildRouter(AlertDialog d) throws IOException { @NotNull final Router router = new Router(); final String uuid = ((TextView) d.findViewById(R.id.router_add_uuid)).getText().toString(); if (!isNullOrEmpty(uuid)) { router.setUuid(uuid); } router.setName(((EditText) d.findViewById(R.id.router_add_name)).getText().toString()); router.setRemoteIpAddress(((EditText) d.findViewById(R.id.router_add_ip)).getText().toString()); router.setRemotePort( Integer.parseInt(((EditText) d.findViewById(R.id.router_add_port)).getText().toString())); router.setRouterConnectionProtocol(Router.RouterConnectionProtocol .valueOf((((Spinner) d.findViewById(R.id.router_add_proto))).getSelectedItem().toString())); router.setUsername(((EditText) d.findViewById(R.id.router_add_username)).getText().toString(), true); router.setStrictHostKeyChecking( ((CheckBox) d.findViewById(R.id.router_add_is_strict_host_key_checking)).isChecked()); final String password = ((EditText) d.findViewById(R.id.router_add_password)).getText().toString(); final String privkey = ((TextView) d.findViewById(R.id.router_add_privkey_path)).getText().toString(); if (!isNullOrEmpty(password)) { router.setPassword(password, true); } if (!isNullOrEmpty(privkey)) { // //Convert privkey into a format accepted by JSCh //Causes a build issue with SpongyCastle // final PEMParser pemParser = new PEMParser(new StringReader(privkey)); // Object object = pemParser.readObject(); // PEMDecryptorProvider decProv = new JcePEMDecryptorProviderBuilder().build(nullToEmpty(password).toCharArray()); // JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("SC"); // KeyPair kp; // if (object instanceof PEMEncryptedKeyPair) { // Log.d(LOG_TAG, "Encrypted key - we will use provided password"); // kp = converter.getKeyPair(((PEMEncryptedKeyPair) object).decryptKeyPair(decProv)); // } else { // Log.d(LOG_TAG, "Unencrypted key - no password needed"); // kp = converter.getKeyPair((PEMKeyPair) object); // } // final PrivateKey privateKey = kp.getPrivate(); // StringWriter stringWriter = new StringWriter(); // JcaPEMWriter pemWriter = new JcaPEMWriter(stringWriter); // pemWriter.writeObject(privateKey); // pemWriter.close(); router.setPrivKey(privkey, true); } return router; } private boolean validateForm(@NotNull AlertDialog d) { @NotNull final EditText ipAddrView = (EditText) d.findViewById(R.id.router_add_ip); final Editable ipAddrViewText = ipAddrView.getText(); if (!(Patterns.IP_ADDRESS.matcher(ipAddrViewText).matches() || Patterns.DOMAIN_NAME.matcher(ipAddrViewText).matches())) { displayMessage(getString(R.string.router_add_dns_or_ip_invalid) + ":" + ipAddrViewText, ALERT); ipAddrView.requestFocus(); openKeyboard(ipAddrView); return false; } boolean validPort; @NotNull final EditText portView = (EditText) d.findViewById(R.id.router_add_port); try { final String portStr = portView.getText().toString(); validPort = (!isNullOrEmpty(portStr) && (Integer.parseInt(portStr) > 0)); } catch (@NotNull final Exception e) { e.printStackTrace(); validPort = false; } if (!validPort) { displayMessage(getString(R.string.router_add_port_invalid) + ":" + portView.getText(), ALERT); portView.requestFocus(); openKeyboard(portView); return false; } @NotNull final EditText sshUsernameView = (EditText) d.findViewById(R.id.router_add_username); if (isNullOrEmpty(sshUsernameView.getText().toString())) { displayMessage(getString(R.string.router_add_username_invalid), ALERT); sshUsernameView.requestFocus(); openKeyboard(sshUsernameView); return false; } final int checkedAuthMethodRadioButtonId = ((RadioGroup) d.findViewById(R.id.router_add_ssh_auth_method)) .getCheckedRadioButtonId(); if (checkedAuthMethodRadioButtonId == R.id.router_add_ssh_auth_method_password) { //Check password @NotNull final EditText sshPasswordView = (EditText) d.findViewById(R.id.router_add_password); if (isNullOrEmpty(sshPasswordView.getText().toString())) { displayMessage(getString(R.string.router_add_password_invalid), ALERT); sshPasswordView.requestFocus(); openKeyboard(sshPasswordView); return false; } } else if (checkedAuthMethodRadioButtonId == R.id.router_add_ssh_auth_method_privkey) { //Check privkey @NotNull final TextView sshPrivKeyView = (TextView) d.findViewById(R.id.router_add_privkey_path); if (isNullOrEmpty(sshPrivKeyView.getText().toString())) { displayMessage(getString(R.string.router_add_privkey_invalid), ALERT); sshPrivKeyView.requestFocus(); return false; } } return true; } private void openKeyboard(final TextView mTextView) { final InputMethodManager imm = (InputMethodManager) getActivity() .getSystemService(Context.INPUT_METHOD_SERVICE); if (imm != null) { // only will trigger it if no physical keyboard is open imm.showSoftInput(mTextView, 0); } } private void displayMessage(final String msg, final Style style) { if (isNullOrEmpty(msg)) { return; } @org.jetbrains.annotations.Nullable final AlertDialog d = (AlertDialog) getDialog(); Crouton.makeText(getActivity(), msg, style, (ViewGroup) (d == null ? getView() : d.findViewById(R.id.router_add_notification_viewgroup))) .show(); } @Override public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) { // An item was selected. You can retrieve the selected item using // parent.getItemAtPosition(pos) //Since there is only one connection method for now, we won't do anything, but we may display only the relevant //form items, and hide the others. } @Override public void onNothingSelected(AdapterView<?> adapterView) { } @org.jetbrains.annotations.Nullable protected abstract Router onPositiveButtonClickHandler(@NotNull final Router router); protected class CheckRouterConnectionAsyncTask extends AsyncTask<AlertDialog, Void, CheckRouterConnectionAsyncTask.CheckRouterConnectionAsyncTaskResult<Router>> { private final String routerIpOrDns; @org.jetbrains.annotations.Nullable private AlertDialog checkingConnectionDialog = null; private boolean checkActualConnection; public CheckRouterConnectionAsyncTask(String routerIpOrDns, boolean checkActualConnection) { this.routerIpOrDns = routerIpOrDns; this.checkActualConnection = checkActualConnection; } @Override protected void onPreExecute() { if (checkActualConnection) { checkingConnectionDialog = Utils.buildAlertDialog(getActivity(), null, String.format("Hold on - checking connection to router '%s'...", routerIpOrDns), false, false); checkingConnectionDialog.show(); } } @org.jetbrains.annotations.Nullable @Override protected CheckRouterConnectionAsyncTask.CheckRouterConnectionAsyncTaskResult<Router> doInBackground( AlertDialog... dialogs) { if (!checkActualConnection) { try { return new CheckRouterConnectionAsyncTaskResult<>(buildRouter(dialogs[0]), null); } catch (IOException e) { e.printStackTrace(); //No worries, as we should not check actual connection return new CheckRouterConnectionAsyncTaskResult<>(null, e); } } @org.jetbrains.annotations.Nullable Router result = null; @org.jetbrains.annotations.Nullable Exception exception = null; try { result = doCheckConnectionToRouter(dialogs[0]); } catch (Exception e) { e.printStackTrace(); exception = e; } return new CheckRouterConnectionAsyncTask.CheckRouterConnectionAsyncTaskResult<>(result, exception); } @Override protected void onPostExecute( @NotNull CheckRouterConnectionAsyncTask.CheckRouterConnectionAsyncTaskResult<Router> result) { if (checkingConnectionDialog != null) { checkingConnectionDialog.cancel(); } final Exception e = result.getException(); @org.jetbrains.annotations.Nullable Router router = result.getResult(); if (e != null) { final Throwable rootCause = Throwables.getRootCause(e); displayMessage(getString(R.string.router_add_connection_unsuccessful) + ": " + (rootCause != null ? rootCause.getMessage() : e.getMessage()), Style.ALERT); Utils.reportException(new DDWRTCompanionExceptionForConnectionChecksException( router != null ? router.toString() : e.getMessage(), e)); } else { if (router != null) { @org.jetbrains.annotations.Nullable AlertDialog daoAlertDialog = null; try { //Register or update router daoAlertDialog = Utils.buildAlertDialog(getActivity(), null, String.format("Registering (or updating) router '%s'...", routerIpOrDns), false, false); daoAlertDialog.show(); router = AbstractRouterMgmtDialogFragment.this.onPositiveButtonClickHandler(router); dismiss(); } finally { if (daoAlertDialog != null) { daoAlertDialog.cancel(); } } } else { displayMessage(getString(R.string.router_add_internal_error), Style.ALERT); } } if (AbstractRouterMgmtDialogFragment.this.mListener != null) { AbstractRouterMgmtDialogFragment.this.onPositiveButtonActionSuccess( AbstractRouterMgmtDialogFragment.this.mListener, router, e != null); } } @Override protected void onCancelled( CheckRouterConnectionAsyncTask.CheckRouterConnectionAsyncTaskResult<Router> router) { super.onCancelled(router); if (checkingConnectionDialog != null) { checkingConnectionDialog.cancel(); } } @Override protected void onCancelled() { super.onCancelled(); if (checkingConnectionDialog != null) { checkingConnectionDialog.cancel(); } } class CheckRouterConnectionAsyncTaskResult<T> { private final T result; private final Exception exception; private CheckRouterConnectionAsyncTaskResult(T result, Exception exception) { this.result = result; this.exception = exception; } public T getResult() { return result; } public Exception getException() { return exception; } } } private class DDWRTCompanionExceptionForConnectionChecksException extends DDWRTCompanionException { private DDWRTCompanionExceptionForConnectionChecksException( @org.jetbrains.annotations.Nullable String detailMessage, @org.jetbrains.annotations.Nullable Throwable throwable) { super(detailMessage, throwable); } } }