/* * (C) Copyright 2015 by fr3ts0n <erwin.scheuch-heilig@gmx.at> * * 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., 59 Temple Place, Suite 330, Boston, * MA 02111-1307 USA */ package com.fr3ts0n.ecu.gui.androbd; import android.Manifest; import android.app.ActionBar; import android.app.Activity; import android.app.AlertDialog; import android.app.ListActivity; import android.app.SearchManager; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.graphics.Color; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.os.StrictMode; import android.preference.PreferenceManager; import android.util.SparseBooleanArray; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.Window; import android.view.WindowManager; import android.widget.AdapterView; import android.widget.ListView; import android.widget.Spinner; import android.widget.Toast; import com.fr3ts0n.ecu.EcuCodeItem; import com.fr3ts0n.ecu.EcuDataItem; import com.fr3ts0n.ecu.EcuDataItems; import com.fr3ts0n.ecu.EcuDataPv; import com.fr3ts0n.ecu.prot.obd.ElmProt; import com.fr3ts0n.ecu.prot.obd.ObdProt; import com.fr3ts0n.pvs.PvChangeEvent; import com.fr3ts0n.pvs.PvChangeListener; import com.fr3ts0n.pvs.PvList; import org.apache.log4j.Level; import org.apache.log4j.Logger; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.File; import java.io.InputStream; import java.util.Arrays; import java.util.HashSet; import java.util.Set; import java.util.Timer; import java.util.TimerTask; import java.util.TreeSet; import de.mindpipe.android.logging.log4j.LogConfigurator; /** * Main Activity for AndrOBD app */ public class MainActivity extends ListActivity implements PvChangeListener, AdapterView.OnItemLongClickListener, PropertyChangeListener, SharedPreferences.OnSharedPreferenceChangeListener { /** * operating modes */ public enum MODE { OFFLINE, ONLINE, DEMO, FILE } /** * data view modes */ public enum DATA_VIEW_MODE { LIST, //< data list (un-filtered) FILTERED, //< data list (filtered) DASHBOARD, //< dashboard HEADUP, //< Head up display CHART //< chart display } /** * Preselection types */ public enum PRESELECT { LAST_DEV_ADDRESS, LAST_ECU_ADDRESS, LAST_SERVICE, LAST_ITEMS, LAST_VIEW_MODE, } /** * Key names for preferences */ public static final String DEVICE_NAME = "device_name"; public static final String DEVICE_ADDRESS = "device_address"; public static final String DEVICE_PORT = "device_port"; public static final String TOAST = "toast"; public static final String MEASURE_SYSTEM = "measure_system"; public static final String NIGHT_MODE = "night_mode"; public static final String ELM_ADAPTIVE_TIMING = "elm_adaptive_timing"; public static final String ELM_RESET_ON_NRC = "elm_reset_on_nrc"; public static final String PREF_USE_LAST = "USE_LAST_SETTINGS"; public static final String PREF_AUTOHIDE = "autohide_toolbar"; public static final String PREF_AUTOHIDE_DELAY = "autohide_delay"; /** * Message types sent from the BluetoothChatService Handler */ public static final int MESSAGE_STATE_CHANGE = 1; public static final int MESSAGE_FILE_READ = 2; public static final int MESSAGE_FILE_WRITTEN = 3; public static final int MESSAGE_DEVICE_NAME = 4; public static final int MESSAGE_TOAST = 5; public static final int MESSAGE_DATA_ITEMS_CHANGED = 6; public static final int MESSAGE_UPDATE_VIEW = 7; public static final int MESSAGE_OBD_STATE_CHANGED = 8; public static final int MESSAGE_OBD_NUMCODES = 9; public static final int MESSAGE_OBD_ECUS = 10; public static final int MESSAGE_OBD_NRC = 11; public static final int MESSAGE_TOOLBAR_VISIBLE = 12; private static final String TAG = "AndrOBD"; /** * internal Intent request codes */ private static final int REQUEST_CONNECT_DEVICE_SECURE = 1; private static final int REQUEST_CONNECT_DEVICE_INSECURE = 2; private static final int REQUEST_ENABLE_BT = 3; private static final int REQUEST_SELECT_FILE = 4; private static final int REQUEST_SETTINGS = 5; private static final int REQUEST_CONNECT_DEVICE_USB = 6; private static final int REQUEST_GRAPH_DISPLAY_DONE = 7; /** * app exit parameters */ private static final int EXIT_TIMEOUT = 2500; /** * time between display updates to represent data changes */ private static final int DISPLAY_UPDATE_TIME = 200; public static final String LOG_MASTER = "log_master"; public static final String KEEP_SCREEN_ON = "keep_screen_on"; public static final String ELM_CUSTOM_INIT_CMDS = "elm_custom_init_cmds"; public static final Logger log = Logger.getLogger(TAG); /** dialog builder */ private static AlertDialog.Builder dlgBuilder; /** * app preferences ... */ protected static SharedPreferences prefs; /** * Member object for the BT comm services */ private static CommService mCommService = null; /** * Local Bluetooth adapter */ private static BluetoothAdapter mBluetoothAdapter = null; /** * Name of the connected BT device */ private static String mConnectedDeviceName = null; /** * log4j configurator */ private static LogConfigurator logCfg; private static Menu menu; /** * Data list adapters */ private static ObdItemAdapter mPidAdapter; private static VidItemAdapter mVidAdapter; private static DfcItemAdapter mDfcAdapter; private static ObdItemAdapter currDataAdapter; /** * Timer for display updates */ private static Timer updateTimer = new Timer(); /** * initial state of bluetooth adapter */ private static boolean initialBtStateEnabled = false; /** * last time of back key pressed */ private static long lastBackPressTime = 0; /** * toast for showing exit message */ private static Toast exitToast = null; /** file helper */ private static FileHelper fileHelper; /** the local list view */ protected View mListView; /** current data view mode */ private DATA_VIEW_MODE dataViewMode = DATA_VIEW_MODE.LIST; /** AutoHider for the toolbar */ private AutoHider toolbarAutoHider; /** handler for freeze frame selection */ AdapterView.OnItemSelectedListener ff_selected = new AdapterView.OnItemSelectedListener() { @Override public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { CommService.elm.setFreezeFrame_Id(position); } @Override public void onNothingSelected(AdapterView<?> parent) { } }; /** * activation of night mode */ private boolean nightMode = false; /** current OBD service */ private int obdService = ElmProt.OBD_SVC_NONE; /** * current operating mode */ private MODE mode = MODE.OFFLINE; /** empty string set as default parameter*/ static final Set<String> emptyStringSet = new HashSet<String>(); /** * Check if restore of specified preselection is wanted from settings * @param preselect specified preselect * @return flag if preselection shall be restored */ boolean istRestoreWanted(PRESELECT preselect) { return prefs.getStringSet(PREF_USE_LAST, emptyStringSet).contains(preselect.toString()); } /** * Check if last data selection shall be restored * * check if previously selected items shall be re-selected */ public void checkToRestoreLastDataSelection() { // if last data items shall be restored if(istRestoreWanted(PRESELECT.LAST_ITEMS)) { // get preference for last seleted items int[] lastSelectedItems = toIntArray(prefs.getString(PRESELECT.LAST_ITEMS.toString(), "")); // select last selected items if(lastSelectedItems.length > 0) { if(!selectDataItems(lastSelectedItems, true)) { // if items could not be applied // remove invalid preselection prefs.edit().remove(PRESELECT.LAST_ITEMS.toString()).apply(); log.warn(String.format("Invalid preselection: %s", Arrays.toString(lastSelectedItems))); } } } // if last view mode shall be restored if(istRestoreWanted(PRESELECT.LAST_VIEW_MODE)) { // set last data view mode DATA_VIEW_MODE lastMode = DATA_VIEW_MODE.valueOf(prefs.getString(PRESELECT.LAST_VIEW_MODE.toString(),DATA_VIEW_MODE.LIST.toString())); setDataViewMode(lastMode); } } /** * Handle message requests */ private transient final Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { PropertyChangeEvent evt; switch (msg.what) { case MESSAGE_STATE_CHANGE: switch ((CommService.STATE) msg.obj) { case CONNECTED: onConnect(); break; case CONNECTING: setStatus(R.string.title_connecting); break; default: onDisconnect(); break; } break; case MESSAGE_FILE_WRITTEN: break; // data has been read - finish up case MESSAGE_FILE_READ: // set listeners for data structure changes setDataListeners(); // set adapters data source to loaded list instances mPidAdapter.setPvList(ObdProt.PidPvs); mVidAdapter.setPvList(ObdProt.VidPvs); mDfcAdapter.setPvList(ObdProt.tCodes); // set OBD data mode to the one selected by input file setObdService(CommService.elm.getService(), getString(R.string.saved_data)); // Check if last data selection shall be restored if(obdService == ObdProt.OBD_SVC_DATA) checkToRestoreLastDataSelection(); break; case MESSAGE_DEVICE_NAME: // save the connected device's name mConnectedDeviceName = msg.getData().getString(DEVICE_NAME); Toast.makeText(getApplicationContext(), getString(R.string.connected_to) + mConnectedDeviceName, Toast.LENGTH_SHORT).show(); break; case MESSAGE_TOAST: Toast.makeText(getApplicationContext(), msg.getData().getString(TOAST), Toast.LENGTH_SHORT).show(); break; case MESSAGE_DATA_ITEMS_CHANGED: PvChangeEvent event = (PvChangeEvent) msg.obj; switch (event.getType()) { case PvChangeEvent.PV_ADDED: currDataAdapter.setPvList(currDataAdapter.pvs); try { if(event.getSource() == ObdProt.PidPvs) { // Check if last data selection shall be restored checkToRestoreLastDataSelection(); } // set up data update timer updateTimer.schedule(updateTask, 0, DISPLAY_UPDATE_TIME); } catch (Exception ignored) { } break; case PvChangeEvent.PV_CLEARED: currDataAdapter.clear(); break; } break; case MESSAGE_UPDATE_VIEW: getListView().invalidateViews(); break; // handle state change in OBD protocol case MESSAGE_OBD_STATE_CHANGED: evt = (PropertyChangeEvent) msg.obj; ElmProt.STAT state = (ElmProt.STAT)evt.getNewValue(); /* Show ELM status only in ONLINE mode */ if (getMode() != MODE.DEMO) { setStatus(getResources().getStringArray(R.array.elmcomm_states)[state.ordinal()]); } // if last selection shall be restored ... if(istRestoreWanted(PRESELECT.LAST_SERVICE)) { if(state == ElmProt.STAT.ECU_DETECTED) { setObdService(prefs.getInt(PRESELECT.LAST_SERVICE.toString(),0), null); } } break; // handle change in number of fault codes case MESSAGE_OBD_NUMCODES: evt = (PropertyChangeEvent) msg.obj; setNumCodes((Integer) evt.getNewValue()); break; // handle ECU detection event case MESSAGE_OBD_ECUS: evt = (PropertyChangeEvent) msg.obj; selectEcu((Set<Integer>) evt.getNewValue()); break; // handle negative result code from OBD protocol case MESSAGE_OBD_NRC: // reset OBD mode to prevent infinite error loop setObdService(ObdProt.OBD_SVC_NONE, getText(R.string.obd_error)); // show error dialog ... evt = (PropertyChangeEvent) msg.obj; String nrcMessage = (String)evt.getNewValue(); dlgBuilder .setIcon(android.R.drawable.ic_dialog_alert) .setTitle(R.string.obd_error) .setMessage(nrcMessage) .setPositiveButton(null,null) .show(); break; // set toolbar visibility case MESSAGE_TOOLBAR_VISIBLE: Boolean visible = (Boolean)msg.obj; // log action log.debug(String.format("ActionBar: %s", visible ? "show" : "hide")); // set action bar visibility ActionBar ab = getActionBar(); if(ab != null) { if(visible) { ab.show(); } else { ab.hide(); } } break; } } }; /** * convert result of Arrays.toString(int[]) back into int[] * @param input String of array * @return int[] of String value */ private int[] toIntArray(String input) { int[] result = {}; if(input.length() > 0) { String beforeSplit = input.replaceAll("\\[|\\]|\\s", ""); String[] split = beforeSplit.split("\\,"); result = new int[split.length]; for (int i = 0; i < split.length; i++) { result[i] = Integer.parseInt(split[i]); } } return result; } /** * Prompt for selection of a single ECU from list of available ECUs * @param ecuAdresses List of available ECUs */ protected void selectEcu(final Set<Integer> ecuAdresses) { // if more than one ECUs available ... if(ecuAdresses.size() > 1) { int preferredAddress = prefs.getInt(PRESELECT.LAST_ECU_ADDRESS.toString(), 0); // check if last preferred address matches any of the reported addresses if(istRestoreWanted(PRESELECT.LAST_ECU_ADDRESS) && ecuAdresses.contains(preferredAddress)) { // set addrerss CommService.elm.setEcuAddress(preferredAddress); } else { // NO match with preference -> allow selection // .. allow selection of single ECU address ... final CharSequence[] entries = new CharSequence[ecuAdresses.size()]; // create list of entries int i = 0; for (Integer addr : ecuAdresses) { entries[i++] = String.format("0x%X", addr); } // show dialog ... dlgBuilder .setTitle(R.string.select_ecu_addr) .setItems(entries, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { int address = Integer.parseInt(entries[which].toString().substring(2), 16); // set address CommService.elm.setEcuAddress(address); // set this as preference (preference change will trigger ELM command) prefs.edit().putInt(PRESELECT.LAST_ECU_ADDRESS.toString(), address).apply(); } }) .show(); } } } /** * OnClick handler - Browse URL from content description * @param view view source of click event */ public void browseClickedUrl(View view) { String url = view.getContentDescription().toString(); startActivity(new Intent(Intent.ACTION_VIEW).setData(Uri.parse(url))); } /** * OnClick handler - Unhide action bar * @param view view source of click event */ public void unHideActionBar(View view) { if(toolbarAutoHider != null) toolbarAutoHider.showComponent(); } /** * Timer Task to cyclically update data screen */ private transient final TimerTask updateTask = new TimerTask() { @Override public void run() { /* forward message to update the view */ Message msg = mHandler.obtainMessage(MainActivity.MESSAGE_UPDATE_VIEW); mHandler.sendMessage(msg); } }; public boolean isNightMode() { return nightMode; } public void setNightMode(boolean nightMode) { this.nightMode = nightMode; setTheme(nightMode ? R.style.AppTheme_Dark : R.style.AppTheme); getWindow().getDecorView().setBackgroundColor(nightMode ? Color.BLACK : Color.WHITE); setObdService(obdService, null); } private void setNumCodes(int newNumCodes) { // set list background based on MIL status View list = findViewById(R.id.obd_list); if(list != null) { list.setBackgroundResource((newNumCodes & 0x80) != 0 ? R.drawable.mil_on : R.drawable.mil_off); } // enable / disable freeze frames based on number of codes setMenuItemEnable(R.id.service_freezeframes, (newNumCodes != 0)); } /** * Set enabled state for a specified menu item * * this includes shading disabled items to visualize state * * @param id ID of menu item * @param enabled flag if to be enabled/disabled */ private void setMenuItemEnable(int id, boolean enabled) { if (menu != null) { MenuItem item = menu.findItem(id); if (item != null) { item.setEnabled(enabled); // if menu item has icon ... Drawable icon = item.getIcon(); if (icon != null) { // set it's shading icon.setAlpha(enabled ? 255 : 127); } } } } /** * Set enabled state for a specified menu item * * this includes shading disabled items to visualize state * * @param id ID of menu item * @param enabled flag if to be visible/invisible */ private void setMenuItemVisible(int id, boolean enabled) { if (menu != null) { MenuItem item = menu.findItem(id); if (item != null) { item.setVisible(enabled); } } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // requestWindowFeature(Window.FEATURE_NO_TITLE); getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); requestWindowFeature(Window.FEATURE_PROGRESS); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { // Storage Permissions final int REQUEST_EXTERNAL_STORAGE = 1; final String[] PERMISSIONS_STORAGE = { Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE }; requestPermissions(PERMISSIONS_STORAGE, REQUEST_EXTERNAL_STORAGE); // Workaround for FileUriExposedException in Android >= M StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder(); StrictMode.setVmPolicy(builder.build()); } dlgBuilder = new AlertDialog.Builder(this); // get preferences prefs = PreferenceManager.getDefaultSharedPreferences(this); // register for later changes prefs.registerOnSharedPreferenceChangeListener(this); // Overlay feature has to be set before window content is set if(prefs.getBoolean(PREF_AUTOHIDE,false)) getWindow().requestFeature(Window.FEATURE_ACTION_BAR_OVERLAY); // set up log4j logging ... logCfg = new LogConfigurator(); logCfg.setUseLogCatAppender(true); logCfg.setUseFileAppender(true); logCfg.setFileName( FileHelper.getPath(this).concat(File.separator).concat("log/AndrOBD.log")); setLogLevels(); log.info(String.format("%s %s starting", getString(R.string.app_name), getString(R.string.app_version))); // Set up all data adapters mPidAdapter = new ObdItemAdapter(this, R.layout.obd_item, ObdProt.PidPvs); mVidAdapter = new VidItemAdapter(this, R.layout.obd_item, ObdProt.VidPvs); mDfcAdapter = new DfcItemAdapter(this, R.layout.obd_item, ObdProt.tCodes); currDataAdapter = mPidAdapter; // get list view mListView = getWindow().getLayoutInflater().inflate(R.layout.obd_list, null); // update all settings from preferences onSharedPreferenceChanged(prefs, null); // set up action bar ActionBar actionBar = getActionBar(); if (actionBar != null) { actionBar.setDisplayShowTitleEnabled(true); } setContentView(R.layout.startup_layout); // create file helper instance fileHelper = new FileHelper(this, CommService.elm); // set listeners for data structure changes setDataListeners(); // automate elm status display CommService.elm.addPropertyChangeListener(this); // override comm medium with USB connect intent if ("android.hardware.usb.action.USB_DEVICE_ATTACHED".equals(getIntent().getAction())) { CommService.medium = CommService.MEDIUM.USB; } switch (CommService.medium) { case BLUETOOTH: // Get local Bluetooth adapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); log.debug("Adapter: " + mBluetoothAdapter); // If BT is not on, request that it be enabled. if (getMode() != MODE.DEMO && mBluetoothAdapter != null) { // remember initial bluetooth state initialBtStateEnabled = mBluetoothAdapter.isEnabled(); if (!initialBtStateEnabled) { Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); startActivityForResult(enableIntent, REQUEST_ENABLE_BT); } } break; case USB: case NETWORK: setMode(MODE.ONLINE); break; } // start automatich toolbar hider setAutoHider(prefs.getBoolean(PREF_AUTOHIDE,false)); } /** * start/stop the autmatic toolbar hider */ void setAutoHider(boolean active) { // disable existing hider if(toolbarAutoHider != null) { // cancel auto hider toolbarAutoHider.cancel(); // forget about it toolbarAutoHider = null; } // if new hider shall be activated if(active) { // enable new hider int timeout = Integer.valueOf( prefs.getString(MainActivity.PREF_AUTOHIDE_DELAY,"15") ); toolbarAutoHider = new AutoHider( this, mHandler, MESSAGE_TOOLBAR_VISIBLE, timeout * 1000); // start with update resolution of 1 second toolbarAutoHider.start(1000); } } @Override public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { // keep main display on? if (key==null || KEEP_SCREEN_ON.equals(key)) { getWindow().addFlags(prefs.getBoolean(KEEP_SCREEN_ON, false) ? WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON : 0); } // night mode if(key==null || NIGHT_MODE.equals(key)) setNightMode(prefs.getBoolean(NIGHT_MODE, false)); // set default comm medium if(key==null || SettingsActivity.KEY_COMM_MEDIUM.equals(key)) CommService.medium = CommService.MEDIUM.values()[ Integer.valueOf(prefs.getString(SettingsActivity.KEY_COMM_MEDIUM, "0"))]; // enable/disable ELM adaptive timing if(key==null || ELM_ADAPTIVE_TIMING.equals(key)) CommService.elm.mAdaptiveTiming.setEnabled(prefs.getBoolean(ELM_ADAPTIVE_TIMING, true)); // set protocol flag to initiate immediate reset on NRC reception if(key==null || ELM_RESET_ON_NRC.equals(key)) CommService.elm.setResetOnNrc(prefs.getBoolean(ELM_RESET_ON_NRC, false)); // set custom ELM init commands if(key==null || ELM_CUSTOM_INIT_CMDS.equals(key)) { String value = prefs.getString(ELM_CUSTOM_INIT_CMDS, null); if(value != null && value.length() > 0) CommService.elm.setCustomInitCommands(value.split("\n")); } // ELM timeout if(key==null || SettingsActivity.ELM_MIN_TIMEOUT.equals(key)) CommService.elm.mAdaptiveTiming.setElmTimeoutMin( Integer.valueOf(prefs.getString(SettingsActivity.ELM_MIN_TIMEOUT, String.valueOf(CommService.elm.mAdaptiveTiming.getElmTimeoutMin())))); // ... measurement system if(key==null || MEASURE_SYSTEM.equals(key)) setConversionSystem(Integer.valueOf( prefs.getString(MEASURE_SYSTEM, String.valueOf(EcuDataItem.SYSTEM_METRIC))) ); // ... preferred protocol if(key==null || SettingsActivity.KEY_PROT_SELECT.equals(key)) ElmProt.setPreferredProtocol( Integer.valueOf(prefs.getString(SettingsActivity.KEY_PROT_SELECT, "0"))); // log levels if(key==null || LOG_MASTER.equals(key)) setLogLevels(); // update from protocol extensions if(key==null || key.startsWith("ext_file-")) loadPreferredExtensions(); // set disabled ELM commands if(key==null || SettingsActivity.ELM_CMD_DISABLE.equals(key)) { ElmProt.disableCommands(prefs.getStringSet(SettingsActivity.ELM_CMD_DISABLE, null)); } // AutoHide ToolBar if(key==null || PREF_AUTOHIDE.equals(key) || PREF_AUTOHIDE_DELAY.equals(key)) setAutoHider(prefs.getBoolean(PREF_AUTOHIDE,false)); } /** * set listeners for data structure changes */ private void setDataListeners() { // add pv change listeners to trigger model updates ObdProt.PidPvs.addPvChangeListener(this, PvChangeEvent.PV_ADDED | PvChangeEvent.PV_CLEARED ); ObdProt.VidPvs.addPvChangeListener(this, PvChangeEvent.PV_ADDED | PvChangeEvent.PV_CLEARED ); ObdProt.tCodes.addPvChangeListener(this, PvChangeEvent.PV_ADDED | PvChangeEvent.PV_CLEARED ); } /** * set listeners for data structure changes */ private void removeDataListeners() { // remove pv change listeners ObdProt.PidPvs.removePvChangeListener(this); ObdProt.VidPvs.removePvChangeListener(this); ObdProt.tCodes.removePvChangeListener(this); } /** * get current operating mode */ public MODE getMode() { return mode; } /** * set new operating mode * * @param mode new mode */ public void setMode(MODE mode) { // if this is a mode change, or file reload ... if (mode != this.mode || mode == MODE.FILE) { if (mode != MODE.DEMO) stopDemoService(); switch (mode) { case OFFLINE: // update menu item states setMenuItemVisible(R.id.disconnect, false); setMenuItemVisible(R.id.secure_connect_scan, true); setMenuItemEnable(R.id.obd_services, false); setMenuItemEnable(R.id.graph_actions, false); break; case ONLINE: switch (CommService.medium) { case BLUETOOTH: // if pre-settings shall be used ... String address = prefs.getString(PRESELECT.LAST_DEV_ADDRESS.toString(), null); if(istRestoreWanted(PRESELECT.LAST_DEV_ADDRESS) && address != null) { // ... connect with previously connected device connectBtDevice(address, prefs.getBoolean("bt_secure_connection", false)); } else { // ... otherwise launch the BtDeviceListActivity to see devices and do scan Intent serverIntent = new Intent(this, BtDeviceListActivity.class); startActivityForResult(serverIntent, prefs.getBoolean("bt_secure_connection", false) ? REQUEST_CONNECT_DEVICE_SECURE : REQUEST_CONNECT_DEVICE_INSECURE ); } break; case USB: Intent enableIntent = new Intent(this, UsbDeviceListActivity.class); startActivityForResult(enableIntent, REQUEST_CONNECT_DEVICE_USB); break; case NETWORK: connectNetworkDevice(prefs.getString(DEVICE_ADDRESS, null), Integer.valueOf(prefs.getString(DEVICE_PORT, "23"))); break; } break; case DEMO: startDemoService(); break; case FILE: setStatus(R.string.saved_data); selectFileToLoad(); break; } // set new mode this.mode = mode; setStatus(mode.toString()); } } /** * set mesaurement conversion system to metric/imperial * * @param cnvId ID for metric/imperial conversion */ void setConversionSystem(int cnvId) { log.info("Conversion: " + getResources().getStringArray(R.array.measure_options)[cnvId]); if (EcuDataItem.cnvSystem != cnvId) { // set coversion system EcuDataItem.cnvSystem = cnvId; } } /** * Setr logging levels from shared preferences */ private void setLogLevels() { logCfg.setRootLevel(Level.toLevel(prefs.getString(LOG_MASTER, "INFO"))); logCfg.configure(); } /** * Load optional extension files which may have * been defined in preferences */ public void loadPreferredExtensions() { String errors = ""; // custom conversions try { String filePath = prefs.getString(SettingsActivity.extKeys[0], null); if (filePath != null) { log.info("Load ext. conversions: " + filePath); Uri uri = Uri.parse(filePath); InputStream inStr = getContentResolver().openInputStream(uri); EcuDataItems.cnv.loadFromStream(inStr); } } catch (Exception e) { log.error("Load ext. conversions: ", e); e.printStackTrace(); errors += e.getLocalizedMessage() + "\n"; } // custom PIDs try { String filePath = prefs.getString(SettingsActivity.extKeys[1], null); if (filePath != null) { log.info("Load ext. conversions: " + filePath); Uri uri = Uri.parse(filePath); InputStream inStr = getContentResolver().openInputStream(uri); ObdProt.dataItems.loadFromStream(inStr); } } catch (Exception e) { log.error("Load ext. PIDs: ", e); e.printStackTrace(); errors += e.getLocalizedMessage() + "\n"; } if (errors.length() != 0) { dlgBuilder .setIcon(android.R.drawable.ic_dialog_alert) .setTitle(R.string.extension_loading) .setMessage(getString(R.string.check_cust_settings) + errors) .show(); } } /** * Stop demo mode Thread */ private void stopDemoService() { if (getMode() == MODE.DEMO) { ElmProt.runDemo = false; Toast.makeText(this, getString(R.string.demo_stopped), Toast.LENGTH_SHORT).show(); } } /** * Start demo mode Thread */ private void startDemoService() { if (getMode() != MODE.DEMO) { setStatus(getString(R.string.demo)); Toast.makeText(this, getString(R.string.demo_started), Toast.LENGTH_SHORT).show(); boolean allowConnect = mBluetoothAdapter != null && mBluetoothAdapter.isEnabled(); setMenuItemVisible(R.id.secure_connect_scan, allowConnect); setMenuItemVisible(R.id.disconnect, !allowConnect); setMenuItemEnable(R.id.obd_services, true); setMenuItemEnable(R.id.graph_actions, false); /* The Thread object for processing the demo mode loop */ Thread demoThread = new Thread(CommService.elm); demoThread.start(); } } /** * set status message in status bar * * @param resId Resource ID of the text to be displayed */ private void setStatus(int resId) { final ActionBar actionBar = getActionBar(); if (actionBar != null) { actionBar.setSubtitle(resId); } } /** * Select file to be loaded */ public void selectFileToLoad() { File file = new File(FileHelper.getPath(this)); Intent intent = new Intent(Intent.ACTION_GET_CONTENT); intent.addCategory(Intent.CATEGORY_OPENABLE); Uri data = Uri.fromFile(file); String type = "*/*"; intent.setDataAndType(data, type); startActivityForResult(intent, REQUEST_SELECT_FILE); } /** * set status message in status bar * * @param subTitle status text to be set */ private void setStatus(CharSequence subTitle) { final ActionBar actionBar = getActionBar(); if (actionBar != null) { actionBar.setSubtitle(subTitle); } } /** * Handler for application start event */ @Override public void onStart() { super.onStart(); // If the adapter is null, then Bluetooth is not supported if (CommService.medium == CommService.MEDIUM.BLUETOOTH && mBluetoothAdapter == null) { // start ELM protocol demo loop setMode(MODE.DEMO); } } /** * handle pressing of the BACK-KEY */ @Override public void onBackPressed() { if (CommService.elm.getService() != ObdProt.OBD_SVC_NONE) { if(dataViewMode == DATA_VIEW_MODE.FILTERED) { setDataViewMode(DATA_VIEW_MODE.LIST); } else { setObdService(ObdProt.OBD_SVC_NONE, null); } } else { if (lastBackPressTime < System.currentTimeMillis() - EXIT_TIMEOUT) { exitToast = Toast.makeText(this, R.string.back_again_to_exit, Toast.LENGTH_SHORT); exitToast.show(); lastBackPressTime = System.currentTimeMillis(); } else { if (exitToast != null) { exitToast.cancel(); } super.onBackPressed(); } } } /** * Handler for options menu creation event */ @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.main, menu); MainActivity.menu = menu; // update menu item status for current conversion setConversionSystem(EcuDataItem.cnvSystem); return true; } /** * Handler for Options menu selection */ @Override public boolean onOptionsItemSelected(MenuItem item) { // Handle presses on the action bar items updateTimer.purge(); switch (item.getItemId()) { case R.id.day_night_mode: // toggle night mode setting prefs.edit().putBoolean(NIGHT_MODE, !isNightMode()).apply(); return true; case R.id.secure_connect_scan: setMode(MODE.ONLINE); return true; case R.id.reset_preselections: clearPreselections(); recreate(); return true; case R.id.disconnect: // stop communication service if (mCommService != null) mCommService.stop(); setMode(MODE.OFFLINE); return true; case R.id.settings: // Launch the BtDeviceListActivity to see devices and do scan Intent settingsIntent = new Intent(this, SettingsActivity.class); startActivityForResult(settingsIntent, REQUEST_SETTINGS); return true; case R.id.chart_selected: setDataViewMode(DATA_VIEW_MODE.CHART); return true; case R.id.hud_selected: setDataViewMode(DATA_VIEW_MODE.HEADUP); return true; case R.id.dashboard_selected: setDataViewMode(DATA_VIEW_MODE.DASHBOARD); return true; case R.id.filter_selected: setDataViewMode(DATA_VIEW_MODE.FILTERED); return true; case R.id.unfilter_selected: setDataViewMode(DATA_VIEW_MODE.LIST); return true; case R.id.save: // save recorded data (threaded) fileHelper.saveDataThreaded(); return true; case R.id.load: setMode(MODE.FILE); return true; case R.id.service_none: setObdService(ObdProt.OBD_SVC_NONE, item.getTitle()); return true; case R.id.service_data: setObdService(ObdProt.OBD_SVC_DATA, item.getTitle()); return true; case R.id.service_vid_data: setObdService(ObdProt.OBD_SVC_VEH_INFO, item.getTitle()); return true; case R.id.service_freezeframes: setObdService(ObdProt.OBD_SVC_FREEZEFRAME, item.getTitle()); return true; case R.id.service_codes: setObdService(ObdProt.OBD_SVC_READ_CODES, item.getTitle()); return true; case R.id.service_permacodes: setObdService(ObdProt.OBD_SVC_PERMACODES, item.getTitle()); return true; case R.id.service_pendingcodes: setObdService(ObdProt.OBD_SVC_PENDINGCODES, item.getTitle()); return true; case R.id.service_clearcodes: clearObdFaultCodes(); setObdService(ObdProt.OBD_SVC_READ_CODES, item.getTitle()); return true; } return super.onOptionsItemSelected(item); } /** * clear all preselections */ private void clearPreselections() { for(PRESELECT selection : PRESELECT.values()) prefs.edit().remove(selection.toString()).apply(); } /** * Handler for result messages from other activities */ @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { boolean secureConnection = false; switch (requestCode) { // device is connected case REQUEST_CONNECT_DEVICE_SECURE: secureConnection = true; // no break here ... case REQUEST_CONNECT_DEVICE_INSECURE: // When BtDeviceListActivity returns with a device to connect if (resultCode == Activity.RESULT_OK) { // Get the device MAC address String address = data.getExtras().getString( BtDeviceListActivity.EXTRA_DEVICE_ADDRESS); // save reported address as last setting prefs.edit().putString(PRESELECT.LAST_DEV_ADDRESS.toString(), address).apply(); connectBtDevice(address, secureConnection); } else { setMode(MODE.OFFLINE); } break; // USB device selected case REQUEST_CONNECT_DEVICE_USB: // DeviceListActivity returns with a device to connect if (resultCode == Activity.RESULT_OK) { mCommService = new UsbCommService(this, mHandler); mCommService.connect(UsbDeviceListActivity.selectedPort, true); } else { setMode(MODE.OFFLINE); } break; // bluetooth enabled case REQUEST_ENABLE_BT: // When the request to enable Bluetooth returns if (resultCode == Activity.RESULT_OK) { // Start online mode setMode(MODE.ONLINE); } else { // Start demo service Thread setMode(MODE.DEMO); } break; // file selected case REQUEST_SELECT_FILE: if (resultCode == RESULT_OK) { // Get the Uri of the selected file Uri uri = data.getData(); log.info("Load content: " + uri); // load data ... fileHelper.loadDataThreaded(uri, mHandler, MESSAGE_FILE_READ); // don't allow saving it again setMenuItemEnable(R.id.save, false); setMenuItemEnable(R.id.obd_services, true); } break; // settings finished case REQUEST_SETTINGS: { // change handling done by callbacks } break; // graphical data view finished case REQUEST_GRAPH_DISPLAY_DONE: // let context know that we are in list mode again ... dataViewMode = DATA_VIEW_MODE.LIST; break; } } /** * Initiate a connect to the selected bluetooth device * * @param address bluetooth device address * @param secure flag to indicate if the connection shall be secure, or not */ private void connectBtDevice(String address, boolean secure) { // Get the BluetoothDevice object BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address); // Attempt to connect to the device mCommService = new BtCommService(this, mHandler); mCommService.connect(device, secure); } /** * Initiate a connect to the selected network device * * @param address IP device address * @param port IP port to connect to */ private void connectNetworkDevice(String address, int port) { // Attempt to connect to the device mCommService = new NetworkCommService(this, mHandler); ((NetworkCommService)mCommService).connect(address, port); } /** * Activate desired OBD service * * @param newObdService OBD service ID to be activated */ public void setObdService(int newObdService, CharSequence menuTitle) { // remember this as current OBD service obdService = newObdService; // set list view setContentView(mListView); // un-filter display setFiltered(false); // set title ActionBar ab = getActionBar(); if (ab != null) { // title specified ... show it if (menuTitle != null) { ab.setTitle(menuTitle); } else { // no title specified, set to app name if no service set if (newObdService == ElmProt.OBD_SVC_NONE) { ab.setTitle(getString(R.string.app_name)); } } } // update controls setMenuItemEnable(R.id.graph_actions, false); getListView().setOnItemLongClickListener(this); // set protocol service CommService.elm.setService(newObdService, (getMode() != MODE.FILE)); // show / hide freeze frame selector */ Spinner ff_selector = (Spinner) findViewById(R.id.ff_selector); ff_selector.setOnItemSelectedListener(ff_selected); ff_selector.setAdapter(mDfcAdapter); ff_selector.setVisibility( newObdService == ObdProt.OBD_SVC_FREEZEFRAME ? View.VISIBLE : View.GONE); // set corresponding list adapter switch (newObdService) { case ObdProt.OBD_SVC_DATA: case ObdProt.OBD_SVC_FREEZEFRAME: currDataAdapter = mPidAdapter; break; case ObdProt.OBD_SVC_PENDINGCODES: case ObdProt.OBD_SVC_PERMACODES: case ObdProt.OBD_SVC_READ_CODES: currDataAdapter = mDfcAdapter; break; case ObdProt.OBD_SVC_NONE: setContentView(R.layout.startup_layout); // intentionally no break to initialize adapter case ObdProt.OBD_SVC_VEH_INFO: currDataAdapter = mVidAdapter; break; } setListAdapter(currDataAdapter); // remember this as last selected service if(newObdService > 0) prefs.edit().putInt(PRESELECT.LAST_SERVICE.toString(), newObdService).apply(); } /** * Filter display items to just the selected ones */ private void setFiltered(boolean filtered) { if (filtered) { PvList filteredList = new PvList(); TreeSet<Integer> selPids = new TreeSet<Integer>(); for (int pos : getSelectedPositions()) { EcuDataPv pv = (EcuDataPv) mPidAdapter.getItem(pos); selPids.add(pv.getAsInt(EcuDataPv.FID_PID)); filteredList.put(pv.toString(), pv); } mPidAdapter.setPvList(filteredList); setFixedPids(selPids); } else { ObdProt.resetFixedPid(); mPidAdapter.setPvList(ObdProt.PidPvs); } setMenuItemEnable(R.id.filter_selected, !filtered); setMenuItemEnable(R.id.unfilter_selected, filtered); setMenuItemEnable(R.id.chart_selected, !filtered); setMenuItemEnable(R.id.dashboard_selected, !filtered); setMenuItemEnable(R.id.hud_selected, !filtered); } /** * get the Position in model of the selected items * * @return Array of selected item positions */ private int[] getSelectedPositions() { int selectedPositions[]; // SparseBoolArray - what a garbage data type to return ... final SparseBooleanArray checkedItems = getListView().getCheckedItemPositions(); // get number of items int checkedItemsCount = getListView().getCheckedItemCount(); // dimension array selectedPositions = new int[checkedItemsCount]; if (checkedItemsCount > 0) { int j = 0; // loop through findings for (int i = 0; i < checkedItems.size(); i++) { // Item position in adapter if (checkedItems.valueAt(i)) { selectedPositions[j++] = checkedItems.keyAt(i); } } // trim to really detected value (workaround for invalid length reported) selectedPositions = Arrays.copyOf(selectedPositions,j); } // save this as last seleted positions prefs.edit().putString(PRESELECT.LAST_ITEMS.toString(), Arrays.toString(selectedPositions)).apply(); return selectedPositions; } /** * Set selection status on specified list item positions * @param positions list of positions to be set * @param selectionStatus status to be set * @return flag if selections could be applied */ private boolean selectDataItems(int[] positions, boolean selectionStatus) { int count; int max; boolean positionsValid; Arrays.sort(positions); max = positions.length > 0 ? positions[positions.length-1] : 0; count = getListAdapter().getCount(); positionsValid = (max < count); // if all positions are valid for current list ... if(positionsValid) { // set list items as selected for(int i : positions) { getListView().setItemChecked(i, selectionStatus); } } // enable graphic actions only on DATA service if min 1 item selected setMenuItemEnable(R.id.graph_actions, positions.length > 0 && selectionStatus); // return validity of positions return positionsValid; } /** * Set fixed PIDs for protocol to specified list of PIDs * * @param pidNumbers List of PIDs */ public static void setFixedPids(Set<Integer> pidNumbers) { int[] pids = new int[pidNumbers.size()]; int i = 0; for (Integer pidNum : pidNumbers) pids[i++] = pidNum; Arrays.sort(pids); // set protocol fixed PIDs ObdProt.setFixedPid(pids); } /** * Handle short clicks in OBD data list items */ @Override public void onListItemClick(ListView l, View v, int position, long id) { if(DATA_VIEW_MODE.LIST == dataViewMode && ObdProt.OBD_SVC_DATA == CommService.elm.getService()) { super.onListItemClick(l, v, position, id); } // enable graphic actions only on DATA service if min 1 item selected setMenuItemEnable(R.id.graph_actions, ((CommService.elm.getService() == ObdProt.OBD_SVC_DATA) && (getListView().getCheckedItemCount() > 0) ) ); } /* * (non-Javadoc) * * @see android.app.Activity#onDestroy() */ @Override protected void onDestroy() { // Stop toolbar hider thread setAutoHider(false); try { // Reduce ELM power consumption by setting it to sleep CommService.elm.goToSleep(); // wait until message is out ... Thread.sleep(100, 0); } catch (InterruptedException e) { // do nothing } /* don't listen to ELM data changes any more */ removeDataListeners(); // don't listen to ELM property changes any more CommService.elm.removePropertyChangeListener(this); // stop demo service if it was started setMode(MODE.OFFLINE); // stop communication service if (mCommService != null) mCommService.stop(); // if bluetooth adapter was switched OFF before ... if (mBluetoothAdapter != null && !initialBtStateEnabled) { // ... turn it OFF again mBluetoothAdapter.disable(); } log.info(String.format("%s %s finished", getString(R.string.app_name), getString(R.string.app_version))); super.onDestroy(); } /** * Handle long licks on OBD data list items */ @Override public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) { Intent intent; switch (CommService.elm.getService()) { /* if we are in OBD data mode: * ->Long click on an item starts the single item dashboard activity */ case ObdProt.OBD_SVC_DATA: EcuDataPv pv = (EcuDataPv) getListAdapter().getItem(position); /* only numeric values may be shown as graph/dashboard */ if (pv.get(EcuDataPv.FID_VALUE) instanceof Number) { DashBoardActivity.setAdapter(getListAdapter()); intent = new Intent(this, DashBoardActivity.class); intent.putExtra(DashBoardActivity.POSITIONS, new int[]{position}); startActivity(intent); } break; /* If we are in DFC mode of any kind * -> Long click leads to a web search for selected DFC */ case ObdProt.OBD_SVC_READ_CODES: case ObdProt.OBD_SVC_PERMACODES: case ObdProt.OBD_SVC_PENDINGCODES: intent = new Intent(Intent.ACTION_WEB_SEARCH); EcuCodeItem dfc = (EcuCodeItem) getListAdapter().getItem(position); intent.putExtra(SearchManager.QUERY, "OBD " + String.valueOf(dfc.get(EcuCodeItem.FID_CODE))); startActivity(intent); break; } return true; } /** * Handle bluetooth connection established ... */ private void onConnect() { stopDemoService(); mode = MODE.ONLINE; // handle further initialisations setMenuItemVisible(R.id.secure_connect_scan, false); setMenuItemVisible(R.id.disconnect, true); setMenuItemEnable(R.id.obd_services, true); setMenuItemEnable(R.id.graph_actions, false); // display connection status setStatus(getString(R.string.title_connected_to, mConnectedDeviceName)); // send RESET to Elm adapter CommService.elm.reset(); } /** * Handle bluetooth connection lost ... */ private void onDisconnect() { // handle further initialisations setMode(MODE.OFFLINE); } /** * Handler for PV change events This handler just forwards the PV change * events to the android handler, since all adapter / GUI actions have to be * performed from the main handler * * @param event PvChangeEvent which is reported */ @Override public synchronized void pvChanged(PvChangeEvent event) { // forward PV change to the UI Activity Message msg = mHandler.obtainMessage(MainActivity.MESSAGE_DATA_ITEMS_CHANGED); if(!event.isChildEvent()) { msg.obj = event; mHandler.sendMessage(msg); } } /** * Property change listener to ELM-Protocol * * @param evt the property change event to be handled */ public void propertyChange(PropertyChangeEvent evt) { /* handle protocol status changes */ if (ElmProt.PROP_STATUS.equals(evt.getPropertyName())) { // forward property change to the UI Activity Message msg = mHandler.obtainMessage(MESSAGE_OBD_STATE_CHANGED); msg.obj = evt; mHandler.sendMessage(msg); } else if (ElmProt.PROP_NUM_CODES.equals(evt.getPropertyName())) { // forward property change to the UI Activity Message msg = mHandler.obtainMessage(MESSAGE_OBD_NUMCODES); msg.obj = evt; mHandler.sendMessage(msg); } else if (ElmProt.PROP_ECU_ADDRESS.equals(evt.getPropertyName())) { // forward property change to the UI Activity Message msg = mHandler.obtainMessage(MESSAGE_OBD_ECUS); msg.obj = evt; mHandler.sendMessage(msg); } else if (ObdProt.PROP_NRC.equals(evt.getPropertyName())) { // forward property change to the UI Activity Message msg = mHandler.obtainMessage(MESSAGE_OBD_NRC); msg.obj = evt; mHandler.sendMessage(msg); } } /** * clear OBD fault codes after a warning * confirmation dialog is shown and the operation is confirmed */ protected void clearObdFaultCodes() { dlgBuilder .setIcon(android.R.drawable.ic_dialog_alert) .setTitle(R.string.obd_clearcodes) .setMessage(R.string.obd_clear_info) .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { // set service CLEAR_CODES to clear the codes CommService.elm.setService(ObdProt.OBD_SVC_CLEAR_CODES); // set service READ_CODES to re-read the codes CommService.elm.setService(ObdProt.OBD_SVC_READ_CODES); } }) .setNegativeButton(android.R.string.no, null) .show(); } /** * Set new data view mode * @param dataViewMode new data view mode */ private void setDataViewMode(DATA_VIEW_MODE dataViewMode) { // if this is a real change ... if(dataViewMode != this.dataViewMode) { log.info(String.format("Set view mode: %s -> %s", this.dataViewMode, dataViewMode)); switch(dataViewMode) { case LIST: setFiltered(false); break; case FILTERED: setFiltered(true); break; case HEADUP: case DASHBOARD: /* if we are in OBD data mode: * -> Short click on an item starts the readout activity */ if (CommService.elm.getService() == ObdProt.OBD_SVC_DATA) { if (getListView().getCheckedItemCount() > 0) { DashBoardActivity.setAdapter(getListAdapter()); Intent intent = new Intent(this, DashBoardActivity.class); intent.putExtra(DashBoardActivity.POSITIONS, getSelectedPositions()); intent.putExtra(DashBoardActivity.RES_ID, dataViewMode == DATA_VIEW_MODE.DASHBOARD ? R.layout.dashboard : R.layout.head_up); startActivityForResult(intent, REQUEST_GRAPH_DISPLAY_DONE); } else { setMenuItemEnable(R.id.graph_actions, false); } } break; case CHART: /* if we are in OBD data mode: * -> Short click on an item starts the readout activity */ if (CommService.elm.getService() == ObdProt.OBD_SVC_DATA) { if (getListView().getCheckedItemCount() > 0) { ChartActivity.setAdapter(getListAdapter()); Intent intent = new Intent(this, ChartActivity.class); intent.putExtra(ChartActivity.POSITIONS, getSelectedPositions()); startActivityForResult(intent, REQUEST_GRAPH_DISPLAY_DONE); } else { setMenuItemEnable(R.id.chart_selected, false); } } break; } this.dataViewMode = dataViewMode; // remember this as the last data view mode (if not regular list) if(dataViewMode != DATA_VIEW_MODE.LIST) prefs.edit().putString(PRESELECT.LAST_VIEW_MODE.toString(), dataViewMode.toString()).apply(); } } }