Java tutorial
/** * Copyright (C) 2015 RasPi Check Contributors * * 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 2 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, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ package de.eidottermihi.rpicheck.activity; import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.os.AsyncTask; import android.os.Bundle; import android.support.v4.app.DialogFragment; import android.support.v4.app.NavUtils; import android.support.v4.widget.CursorAdapter; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; import android.widget.ListView; import android.widget.Toast; import com.google.common.base.Splitter; import com.google.common.base.Strings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import de.eidottermihi.raspicheck.R; import de.eidottermihi.rpicheck.db.CommandBean; import de.eidottermihi.rpicheck.db.DeviceDbHelper; import de.eidottermihi.rpicheck.ssh.beans.Exported; import de.eidottermihi.rpicheck.db.RaspberryDeviceBean; import de.eidottermihi.rpicheck.fragment.CommandPlaceholdersDialog; import de.eidottermihi.rpicheck.fragment.CommandPlaceholdersDialog.PlaceholdersDialogListener; import de.eidottermihi.rpicheck.fragment.PassphraseDialog; import de.eidottermihi.rpicheck.fragment.PassphraseDialog.PassphraseDialogListener; import de.eidottermihi.rpicheck.fragment.RunCommandDialog; import de.fhconfig.android.library.injection.annotation.XmlLayout; import de.fhconfig.android.library.injection.annotation.XmlMenu; import de.fhconfig.android.library.injection.annotation.XmlView; import de.fhconfig.android.library.ui.injection.InjectionActionBarActivity; @XmlLayout(R.layout.activity_commands) @XmlMenu(R.menu.activity_commands) public class CustomCommandActivity extends InjectionActionBarActivity implements OnItemClickListener, PassphraseDialogListener, PlaceholdersDialogListener { private static final Logger LOGGER = LoggerFactory.getLogger(CustomCommandActivity.class); private RaspberryDeviceBean currentDevice; @XmlView(R.id.commandListView) private ListView commandListView; private DeviceDbHelper deviceDb = new DeviceDbHelper(this); private Cursor fullCommandCursor; private Pattern dynamicPlaceHolderPattern = Pattern.compile("(\\$\\{[^*\\}]+\\})"); private Pattern nonPromptingPlaceHolders = Pattern.compile("(\\%\\{[^*\\}]+\\})"); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Show the Up button in the action bar. getSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setDisplayShowTitleEnabled(true); Bundle extras = this.getIntent().getExtras(); if (extras != null && extras.get("pi") != null) { LOGGER.debug("onCreate: get currentDevice out of intent."); currentDevice = (RaspberryDeviceBean) extras.get("pi"); } else if (savedInstanceState.getSerializable("pi") != null) { LOGGER.debug("onCreate: get currentDevice out of savedInstanceState."); currentDevice = (RaspberryDeviceBean) savedInstanceState.getSerializable("pi"); } if (currentDevice != null) { LOGGER.debug("Setting activity title for device."); getSupportActionBar().setTitle(currentDevice.getName()); LOGGER.debug("Initializing ListView"); this.initListView(currentDevice); } else { LOGGER.debug("No current device! Setting no title"); } } /** * Init ListView with commands. * * @param pi */ private void initListView(RaspberryDeviceBean pi) { new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... params) { fullCommandCursor = deviceDb.getFullCommandCursor(); return null; } @Override protected void onPostExecute(Void r) { CommandAdapter commandsAdapter = new CommandAdapter(CustomCommandActivity.this, fullCommandCursor, CursorAdapter.FLAG_AUTO_REQUERY); commandListView.setAdapter(commandsAdapter); commandListView.setOnItemClickListener(CustomCommandActivity.this); // commandListView.setOnItemLongClickListener(CustomCommandActivity.this); registerForContextMenu(commandListView); } }.execute(); } @Override public void onCreateContextMenu(final ContextMenu menu, View v, ContextMenuInfo menuInfo) { if (v.getId() == R.id.commandListView) { final AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo; LOGGER.debug("Creating context menu for command id = {}.", info.id); CommandBean cmd = deviceDb.readCommand(info.id); menu.setHeaderTitle(cmd.getName()); menu.add(Menu.NONE, 1, 1, R.string.command_context_edit); menu.add(Menu.NONE, 2, 2, R.string.command_context_delete); menu.add(Menu.NONE, 3, 3, R.string.command_context_run); } super.onCreateContextMenu(menu, v, menuInfo); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case android.R.id.home: // This ID represents the Home or Up button. In the case of this // activity, the Up button is shown. Use NavUtils to allow users // to navigate up one level in the application structure. For // more details, see the Navigation pattern on Android Design: // // http://developer.android.com/design/patterns/navigation.html#up-vs-back // NavUtils.navigateUpFromSameTask(this); break; case R.id.menu_new_command: // init intent Intent newCommandIntent = new Intent(CustomCommandActivity.this, NewCommandActivity.class); newCommandIntent.putExtras(this.getIntent().getExtras()); this.startActivityForResult(newCommandIntent, NewCommandActivity.REQUEST_NEW); break; } return super.onOptionsItemSelected(item); } @Override public boolean onContextItemSelected(android.view.MenuItem item) { AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo(); LOGGER.debug("Context item selected for command id {}.", info.id); int menuItemIndex = item.getItemId(); switch (menuItemIndex) { case 1: Intent newCommandIntent = new Intent(CustomCommandActivity.this, NewCommandActivity.class); newCommandIntent.putExtras(this.getIntent().getExtras()); newCommandIntent.putExtra(NewCommandActivity.CMD_KEY_EDIT, info.id); this.startActivityForResult(newCommandIntent, NewCommandActivity.REQUEST_EDIT); break; case 2: this.deleteCommand(info.id); break; case 3: this.runCommand(info.id); break; default: break; } return true; } private void deleteCommand(final long id) { new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... params) { deviceDb.deleteCommand(id); return null; } @Override protected void onPostExecute(Void r) { CustomCommandActivity.this.initListView(currentDevice); } }.execute(); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == NewCommandActivity.REQUEST_NEW && resultCode == RESULT_OK) { // new cmd saved, update... Toast.makeText(this, R.string.toast_command_saved, Toast.LENGTH_SHORT).show(); initListView(currentDevice); } else if (requestCode == NewCommandActivity.REQUEST_EDIT && resultCode == RESULT_OK) { Toast.makeText(this, R.string.toast_command_updated, Toast.LENGTH_SHORT).show(); initListView(currentDevice); } super.onActivityResult(requestCode, resultCode, data); } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); if (currentDevice != null) { LOGGER.debug("Writing currentDevice in outState."); outState.putSerializable("pi", currentDevice); } } @Override public void onItemClick(AdapterView<?> arg0, View arg1, int itemPos, long itemId) { LOGGER.debug("Command pos {} clicked. Item _id = {}.", itemPos, itemId); runCommand(itemId); } private void runCommand(long commandId) { ConnectivityManager connMgr = (ConnectivityManager) this.getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo networkInfo = connMgr.getActiveNetworkInfo(); if (networkInfo != null && networkInfo.isConnected()) { if (currentDevice.getAuthMethod().equals(NewRaspiAuthActivity.SPINNER_AUTH_METHODS[2]) && Strings.isNullOrEmpty(currentDevice.getKeyfilePass())) { // must ask for key passphrase first LOGGER.debug("Asking for key passphrase."); // dirty hack, saving commandId as "dialog type" final String dialogType = commandId + ""; final DialogFragment passphraseDialog = new PassphraseDialog(); final Bundle args = new Bundle(); args.putString(PassphraseDialog.KEY_TYPE, dialogType); passphraseDialog.setArguments(args); passphraseDialog.setCancelable(false); passphraseDialog.show(getSupportFragmentManager(), "passphrase"); } else { LOGGER.debug("Opening command dialog."); openCommandDialog(commandId, currentDevice.getKeyfilePass()); } } else { Toast.makeText(this, R.string.no_connection, Toast.LENGTH_SHORT).show(); } } @Override protected void onDestroy() { super.onDestroy(); if (deviceDb != null) { deviceDb.close(); } } /** * Opens the command dialog. * * @param keyPassphrase nullable: key passphrase */ private void openCommandDialog(final long commandId, final String keyPassphrase) { final CommandBean command = deviceDb.readCommand(commandId); final ArrayList<String> dynamicPlaceholders = parseDynamicPlaceholders(command.getCommand()); if (!dynamicPlaceholders.isEmpty()) { // need to get replacements for dynamic placeholders first DialogFragment placeholderDialog = new CommandPlaceholdersDialog(); Bundle args2 = new Bundle(); args2.putStringArrayList(CommandPlaceholdersDialog.ARG_PLACEHOLDERS, dynamicPlaceholders); args2.putSerializable(CommandPlaceholdersDialog.ARG_COMMAND, command); args2.putString(CommandPlaceholdersDialog.ARG_PASSPHRASE, keyPassphrase); placeholderDialog.setArguments(args2); placeholderDialog.show(getSupportFragmentManager(), "placeholders"); return; } parseNonPromptingAndShow(keyPassphrase, command); } private void parseNonPromptingAndShow(String keyPassphrase, CommandBean command) { final DialogFragment runCommandDialog = new RunCommandDialog(); final Bundle args = new Bundle(); String cmdString = command.getCommand(); Map<String, String> nonPromptingPlaceholders = parseNonPromptingPlaceholders(command.getCommand(), currentDevice); for (Map.Entry<String, String> entry : nonPromptingPlaceholders.entrySet()) { cmdString = cmdString.replace(entry.getKey(), entry.getValue()); } command.setCommand(cmdString); args.putSerializable("pi", currentDevice); args.putSerializable("cmd", command); if (keyPassphrase != null) { args.putString("passphrase", keyPassphrase); } runCommandDialog.setArguments(args); runCommandDialog.show(getSupportFragmentManager(), "runCommand"); } private Map<String, String> parseNonPromptingPlaceholders(String command, RaspberryDeviceBean currentDevice) { Map<String, String> nonPromptingPlaceholders = new HashMap<>(); Matcher m = nonPromptingPlaceHolders.matcher(command); while (m.find()) { String placeholder = m.group(); String placeholderValue = placeholder.substring(2, placeholder.length() - 1); LOGGER.debug("Found non-prompting placeholder for: {}", placeholderValue); if (placeholderValue.startsWith("pi.")) { // accessing properties of current pi device List<String> splitToList = Splitter.on('.').splitToList(placeholderValue); if (splitToList.size() == 2) { String accessor = splitToList.get(1); String value = getValueViaReflection(currentDevice, accessor); if (value != null) { LOGGER.debug("Value for '{}' is '{}'", placeholder, value); nonPromptingPlaceholders.put(placeholder, value); } } else { LOGGER.debug("Skipping bad placeholder definition: {}", placeholder); } } else if (placeholderValue.startsWith("date(")) { // parse format in braces final String format = placeholderValue.substring(5, placeholderValue.length() - 1); LOGGER.debug("Trying to get system time with format '{}'...", format); try { SimpleDateFormat simpleDateFormat = new SimpleDateFormat(format); String value = simpleDateFormat.format(new Date()); LOGGER.debug("Value for '{}' is '{}'", placeholder, value); nonPromptingPlaceholders.put(placeholder, value); } catch (IllegalArgumentException e) { LOGGER.warn( "Unparseable Date Format: {} - refer to Java's SimpleDateFormat for a valid format specification.", format); } } } return nonPromptingPlaceholders; } private String getValueViaReflection(RaspberryDeviceBean device, String accessor) { for (Method method : device.getClass().getMethods()) { if (method.isAnnotationPresent(Exported.class)) { if (method.getName().replaceFirst("get", "").toLowerCase().equals(accessor.toLowerCase())) { try { Object result = method.invoke(device, new Object[] {}); if (result != null) { return result.toString(); } } catch (IllegalAccessException e) { } catch (InvocationTargetException e) { } } } } LOGGER.debug("No getter found on DeviceBean. Property is not present."); return null; } private ArrayList<String> parseDynamicPlaceholders(String commandString) { ArrayList<String> placeholders = new ArrayList<>(); Matcher m = dynamicPlaceHolderPattern.matcher(commandString); while (m.find()) { String placeholder = m.group(); placeholders.add(placeholder); } return placeholders; } @Override public void onPassphraseOKClick(DialogFragment dialog, String passphrase, boolean savePassphrase, String type) { LOGGER.debug("Key passphrase entered."); if (savePassphrase) { LOGGER.debug("Saving passphrase.."); currentDevice.setKeyfilePass(passphrase); currentDevice.setModifiedAt(new Date()); new Thread() { @Override public void run() { deviceDb.update(currentDevice); } }.start(); } // dirty hack: type is commandId Long commandId = Long.parseLong(type); LOGGER.debug("Starting command dialog for command id " + commandId); openCommandDialog(commandId, passphrase); } @Override public void onPassphraseCancelClick() { // do nothing } @Override public void onPlaceholdersOKClick(CommandBean command, String keyPassphrase) { parseNonPromptingAndShow(keyPassphrase, command); } @Override public void onPlaceholdersCancelClick() { // do nothing } }