/* CanZE Take a closer look at your ZE car Copyright (C) 2015 - The CanZE Team http://canze.fisch.lu 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 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/>. */ package lu.fisch.canze.activities; import android.annotation.SuppressLint; import android.app.AlertDialog; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothSocket; import android.content.BroadcastReceiver; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.res.Resources; import android.graphics.Point; import android.graphics.drawable.AnimationDrawable; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentTransaction; import android.support.v4.content.ContextCompat; import android.support.v4.view.ViewPager; import android.support.v7.app.ActionBar; import android.support.v7.app.AppCompatActivity; import android.text.Html; import android.util.Log; import android.view.Display; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.Window; import android.widget.ImageView; import android.widget.Toast; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Locale; import lu.fisch.canze.R; import lu.fisch.canze.actors.Field; import lu.fisch.canze.actors.Fields; import lu.fisch.canze.actors.Frames; import lu.fisch.canze.bluetooth.BluetoothManager; import lu.fisch.canze.classes.DataLogger; import lu.fisch.canze.classes.DebugLogger; import lu.fisch.canze.database.CanzeDataSource; import lu.fisch.canze.devices.BobDue; import lu.fisch.canze.devices.Device; import lu.fisch.canze.devices.ELM327; import lu.fisch.canze.devices.ELM327OverHttp; import lu.fisch.canze.fragments.ExperimentalFragment; import lu.fisch.canze.fragments.MainFragment; import lu.fisch.canze.fragments.TechnicalFragment; import lu.fisch.canze.interfaces.BluetoothEvent; import lu.fisch.canze.interfaces.DebugListener; import lu.fisch.canze.interfaces.FieldListener; import lu.fisch.canze.ui.AppSectionsPagerAdapter; public class MainActivity extends AppCompatActivity implements FieldListener /*, android.support.v7.app.ActionBar.TabListener */{ public static final String TAG = " CanZE"; // SPP UUID service // private static final UUID MY_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB"); public final static String PREFERENCES_FILE = "lu.fisch.canze.settings"; public final static String DATA_FILE = "lu.fisch.canze.data"; // MAC-address of Bluetooth module (you must edit this line) private static String bluetoothDeviceAddress = null; private static String bluetoothDeviceName = null; // url of gateway if in use private static String gatewayUrl = null; // public final static int RECEIVE_MESSAGE = 1; public final static int REQUEST_ENABLE_BT = 3; public final static int SETTINGS_ACTIVITY = 7; public final static int LEAVE_BLUETOOTH_ON = 11; // note that the CAR constants are stored in the option property of the field object // this is a short // public static final short CAR_MASK = 0xff; public static final short CAR_NONE = 0x000; //public static final int CAR_ANY = 0x0ff; public static final short CAR_FLUENCE = 0x001; public static final short CAR_ZOE_Q210 = 0x002; public static final short CAR_KANGOO = 0x004; // public static final short CAR_TWIZY = 0x008; // you'll never know ;-) public static final short CAR_X10 = 0x010; // not used public static final short CAR_ZOE_R240 = 0x020; public static final short CAR_ZOE_Q90 = 0x040; public static final short CAR_ZOE_R90 = 0x080; public static final short FIELD_TYPE_MASK = 0x700; //public static final short FIELD_TYPE_UNSIGNED = 0x000; public static final short FIELD_TYPE_SIGNED = 0x100; public static final short FIELD_TYPE_STRING = 0x200; // not implemented yet public static final short TOAST_NONE = 0; public static final short TOAST_ELM = 1; public static final short TOAST_ELMCAR = 2; public static final double reduction = 9.32; // update suggested by Loc Dao // private StringBuilder sb = new StringBuilder(); // private String buffer = ""; // private int count; // private long start; private boolean visible = true; public boolean leaveBluetoothOn = false; private boolean returnFromWidget = false; public static Fields fields = Fields.getInstance(); public static Device device = null; private static MainActivity instance = null; public static boolean safeDrivingMode = true; public static boolean bluetoothBackgroundMode = false; public static boolean debugLogMode = false; public static boolean fieldLogMode = false; public static boolean dataExportMode = false; public static DataLogger dataLogger = null; // rather use singleton in onCreate public static int car = CAR_NONE; private static boolean isDriving = false; public static boolean milesMode = false; public static int toastLevel = 1; private DebugListener debugListener = null; // private Fragment actualFragment; static private Resources res; // bluetooth stuff private MenuItem bluetoothMenutItem = null; public final static int BLUETOOTH_DISCONNECTED = 21; public final static int BLUETOOTH_SEARCH = 22; public final static int BLUETOOTH_CONNECTED = 23; //The BroadcastReceiver that listens for bluetooth broadcasts private final BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); // BluetoothDevice bluetoothDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); if (BluetoothDevice.ACTION_ACL_DISCONNECTED.equals(action)) { //Device has disconnected // only resume if this activity is also visible if(visible) { // stop reading if (device!=null) { device.stopAndJoin(); } // inform user setTitle(TAG + " - disconnected"); setBluetoothState(BLUETOOTH_DISCONNECTED); toast (R.string.toast_BluetoothLost); // try to reconnect onResume(); } } } }; public static MainActivity getInstance() { return instance; } public static void debug(String text) { Log.d(TAG, text); if(debugLogMode) { SimpleDateFormat sdf = new SimpleDateFormat(getStringSingle(R.string.format_YMDHMSs), Locale.getDefault()); DebugLogger.getInstance().log(sdf.format(Calendar.getInstance().getTime()) + ": " + text); } } /* TODO we should move to simply always provide the level in the toast() call instead of all those if's in the code */ public static void toast(int level, final String message) { if (level > toastLevel) return; if(instance!=null) instance.runOnUiThread(new Runnable() { @Override public void run() { Toast.makeText(instance, message, Toast.LENGTH_SHORT).show(); } }); } public static void toast(final String message) { if(instance!=null) instance.runOnUiThread(new Runnable() { @Override public void run() { Toast.makeText(instance, message, Toast.LENGTH_SHORT).show(); } }); } public static void toast(String format, Object... arguments) { final String finalMessage = String.format(Locale.getDefault(), format, arguments); if(instance!=null) instance.runOnUiThread(new Runnable() { @Override public void run() { Toast.makeText(instance, finalMessage, Toast.LENGTH_SHORT).show(); } }); } public static void toast(final int resource) { if(instance!=null) instance.runOnUiThread(new Runnable() { @Override public void run() { final String finalMessage = getStringSingle (resource); Toast.makeText(instance, finalMessage, Toast.LENGTH_SHORT).show(); } }); } public void loadSettings() { debug("MainActivity: loadSettings"); try { SharedPreferences settings = getSharedPreferences(PREFERENCES_FILE, 0); bluetoothDeviceName = settings.getString("deviceName", null); bluetoothDeviceAddress = settings.getString("deviceAddress", null); gatewayUrl = settings.getString("gatewayUrl", null); // String dataFormat = settings.getString("dataFormat", "crdt"); String deviceType = settings.getString("device", "Arduino"); safeDrivingMode = settings.getBoolean("optSafe", true); bluetoothBackgroundMode = settings.getBoolean("optBTBackground", false); milesMode = settings.getBoolean("optMiles", false); dataExportMode = settings.getBoolean("optDataExport", false); debugLogMode = settings.getBoolean("optDebugLog", false); fieldLogMode = settings.getBoolean("optFieldLog", false); toastLevel = settings.getInt("optToast", 1); if (bluetoothDeviceName != null && !bluetoothDeviceName.isEmpty() && bluetoothDeviceName.length()>4) BluetoothManager.getInstance().setDummyMode(bluetoothDeviceName.substring(0, 4).compareTo("HTTP") == 0); String carStr = settings.getString("car", "None"); switch (carStr) { case "None": car = CAR_NONE; break; case "ZoƩ": case "ZOE": case "ZOE Q210": car = CAR_ZOE_Q210; break; case "ZOE R240": car = CAR_ZOE_R240; break; case "ZOE Q90": car = CAR_ZOE_Q90; break; case "ZOE R90": car = CAR_ZOE_R90; break; case "Fluence": car = CAR_FLUENCE; break; case "Kangoo": car = CAR_KANGOO; break; case "X10": car = CAR_X10; break; } // as the settings may have changed, we need to reload different things // create a new device switch (deviceType) { case "Bob Due": device = new BobDue(); break; case "ELM327": device = new ELM327(); break; case "ELM327Http": device = new ELM327OverHttp(); break; default: device = null; break; } // since the car type may have changed, reload the frame timings and fields Frames.getInstance().load(); fields.load(); if (device != null) { // initialise the connection device.initConnection(); // register application wide fields // registerApplicationFields(); // now done in Fields.load } // after loading PREFERENCES we may have new values for "dataExportMode" dataExportMode = dataLogger.activate(dataExportMode); } catch(Exception e) { MainActivity.debug(e.getMessage()); StackTraceElement[] st = e.getStackTrace(); for(int i=0; i<st.length; i++) MainActivity.debug(st[i].toString()); } } public void registerApplicationFields() { if (safeDrivingMode) { // speed Field field = fields.getBySID("5d7.0"); if (field != null) { field.addListener(MainActivity.getInstance()); // callback is onFieldUpdateEvent if (device != null) device.addApplicationField(field, 1000); // query every second } } else { Field field = fields.getBySID("5d7.0"); if (field != null) { field.removeListener(MainActivity.getInstance()); if (device != null) device.removeApplicationField(field); } } } protected void updateActionBar() { switch (viewPager.getCurrentItem()) { case 0: actionBar.setIcon(R.mipmap.ic_launcher); break; case 1: actionBar.setIcon(R.mipmap.fragement_technical); break; case 2: actionBar.setIcon(R.mipmap.fragement_experimental); break; default: break; } } private ViewPager viewPager; private ActionBar actionBar ; @Override protected void onCreate(Bundle savedInstanceState) { // always create an instance // needed to get strings from resources in non-Activity classes res = getResources(); // dataLogger = DataLogger.getInstance(); dataLogger = new DataLogger(); debug("MainActivity: onCreate"); instance = this; getWindow().requestFeature(Window.FEATURE_ACTION_BAR); super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // navigation bar AppSectionsPagerAdapter appSectionsPagerAdapter = new AppSectionsPagerAdapter(getSupportFragmentManager()); actionBar = getSupportActionBar(); actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_HOME | ActionBar.DISPLAY_SHOW_TITLE); //actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS); viewPager = (ViewPager) findViewById(R.id.main); viewPager.setAdapter(appSectionsPagerAdapter); viewPager.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() { @Override public void onPageSelected(int position) { //actionBar.setSelectedNavigationItem(position); updateActionBar(); } }); updateActionBar(); /* for (int i = 0; i < appSectionsPagerAdapter.getCount(); i++) { actionBar.addTab( actionBar.newTab() .setText(appSectionsPagerAdapter.getPageTitle(i)) .setTabListener(MainActivity.this)); } */ // load the initial "main" fragment //loadFragement(new MainFragment()); setTitle(TAG + " - not connected"); setBluetoothState(BLUETOOTH_DISCONNECTED); // tabs //final ActionBar actionBar = getSupportActionBar(); // Specify that tabs should be displayed in the action bar. // open the database CanzeDataSource.getInstance(getBaseContext()).open(); // cleanup CanzeDataSource.getInstance().cleanUp(); // setup cleaning (once every hour) Runnable cleanUpRunnable = new Runnable() { @Override public void run() { CanzeDataSource.getInstance().cleanUp(); } }; Handler handler = new Handler(); handler.postDelayed(cleanUpRunnable, 60*1000); // register for bluetooth changes IntentFilter intentFilter = new IntentFilter(BluetoothDevice.ACTION_ACL_DISCONNECTED); this.registerReceiver(broadcastReceiver, intentFilter); // configure Bluetooth manager BluetoothManager.getInstance().setBluetoothEvent(new BluetoothEvent() { @Override public void onBeforeConnect() { setBluetoothState(BLUETOOTH_SEARCH); } @Override public void onAfterConnect(BluetoothSocket bluetoothSocket) { device.init(visible); device.registerFilters(); // set title debug("MainActivity: onAfterConnect > set title"); runOnUiThread(new Runnable() { @Override public void run() { setTitle(TAG + " - connected to <" + bluetoothDeviceName + "@" + bluetoothDeviceAddress + ">"); setBluetoothState(BLUETOOTH_CONNECTED); } }); } @Override public void onBeforeDisconnect(BluetoothSocket bluetoothSocket) { } @Override public void onAfterDisconnect() { runOnUiThread(new Runnable() { @Override public void run() { setTitle(TAG + " - disconnected"); } }); } }); // detect hardware status int BT_STATE = BluetoothManager.getInstance().getHardwareState(); if(BT_STATE==BluetoothManager.STATE_BLUETOOTH_NOT_AVAILABLE) toast ("Sorry, but your device doesn't seem to have Bluetooth support!"); else if (BT_STATE==BluetoothManager.STATE_BLUETOOTH_NOT_ACTIVE) { Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); startActivityForResult(enableBtIntent, 1); } // load settings // - includes the reader // - includes the decoder //loadSettings(); --> done in onResume // load fields from static code debug("Loaded fields: " + fields.size()); // load fields //final SharedPreferences settings = getSharedPreferences(PREFERENCES_FILE, 0); (new Thread(new Runnable() { @Override public void run() { debug("Loading fields last field values from database"); for(int i=0; i<fields.size(); i++) { Field field = fields.get(i); field.setCalculatedValue(CanzeDataSource.getInstance().getLast(field.getSID())); //debug("MainActivity: Setting "+field.getSID()+" = "+field.getValue()); //f.setValue(settings.getFloat(f.getUniqueID(), 0)); } debug("Loading fields last field values from database (done)"); } })).start(); } @Override public void onResume() { debug("MainActivity: onResume"); instance = this; visible=true; super.onResume(); // if returning from a single widget activity, we have to leave here! if(returnFromWidget) { returnFromWidget=false; return; } if(!leaveBluetoothOn) { runOnUiThread(new Runnable() { @Override public void run() { setBluetoothState(BLUETOOTH_DISCONNECTED); } }); (new Thread(new Runnable() { @Override public void run() { reloadBluetooth(); } })).start(); } final SharedPreferences settings = getSharedPreferences(PREFERENCES_FILE, 0); if(!settings.getBoolean("disclaimer",false)) { AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(this); // set title alertDialogBuilder.setTitle(R.string.prompt_Disclaimer); // set dialog message String yes = getStringSingle(R.string.prompt_Accept); String no = getStringSingle(R.string.prompt_Decline); Display display = getWindowManager().getDefaultDisplay(); Point size = new Point(); display.getSize(size); float width = size.x; // int height = size.y; width = width / getResources().getDisplayMetrics().scaledDensity; if(width<=480) { yes=getStringSingle(R.string.default_Yes); no =getStringSingle(R.string.default_No); } alertDialogBuilder .setMessage(Html.fromHtml(getStringSingle(R.string.prompt_DisclaimerText))) .setCancelable(true) .setPositiveButton(yes, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { // if this button is clicked, close SharedPreferences.Editor editor = settings.edit(); editor.putBoolean("disclaimer", true); // editor.commit(); editor.apply(); // current activity dialog.cancel(); } }) .setNegativeButton(no, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { // if this button is clicked, just close // the dialog box and do nothing dialog.cancel(); //MainActivity.this.finishAffinity(); requires API16 MainActivity.this.finish(); android.os.Process.killProcess(android.os.Process.myPid()); System.exit(0); } }); // create alert dialog AlertDialog alertDialog = alertDialogBuilder.create(); // show it alertDialog.show(); } } public void reloadBluetooth() { reloadBluetooth(true); } public void reloadBluetooth(boolean reloadSettings) { // re-load the settings if asked to if(reloadSettings) loadSettings(); // try to get a new BT thread BluetoothManager.getInstance().connect(bluetoothDeviceAddress, true, BluetoothManager.RETRIES_INFINITE); } @Override public void onPause() { debug("MainActivity: onPause"); debug("MainActivity: onPause > leaveBluetoothOn = "+leaveBluetoothOn); visible=false; // stop here if BT should stay on! if(bluetoothBackgroundMode) { super.onPause(); return; } if(!leaveBluetoothOn) { if(device!=null) device.clearFields(); debug("MainActivity: stopping BT"); stopBluetooth(); } super.onPause(); } public void stopBluetooth() { stopBluetooth(true); } public void stopBluetooth(boolean reset) { if(device!=null) { // stop the device debug("MainActivity: stopBluetooth > stopAndJoin"); device.stopAndJoin(); // remove reference if(reset) { device.clearFields(); device.registerFilters(); } } // disconnect BT debug("MainActivity: stopBluetooth > BT disconnect"); BluetoothManager.getInstance().disconnect(); } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { MainActivity.debug("MainActivity: onActivityResult"); MainActivity.debug("MainActivity: onActivityResult > requestCode = " + requestCode); MainActivity.debug("MainActivity: onActivityResult > resultCode = " + resultCode); // this must be set in any case leaveBluetoothOn=false; if(requestCode==SETTINGS_ACTIVITY) { // load settings loadSettings(); } else if(requestCode==LEAVE_BLUETOOTH_ON) { MainActivity.debug("MainActivity: onActivityResult > "+LEAVE_BLUETOOTH_ON); returnFromWidget=true; // register fields this activity needs /* registerFields(); */ } else super.onActivityResult(requestCode, resultCode, data); } /* public void saveFields() { // safe fields SharedPreferences settings = getSharedPreferences(PREFERENCES_FILE, 0); SharedPreferences.Editor editor = settings.edit(); for(int i=0; i<fields.size(); i++) { Field f = fields.get(i); editor.putFloat(f.getUniqueID(),(float) f.getRawValue()); //debug("Setting "+f.getUniqueID()+" = "+f.getRawValue()); } editor.commit(); } */ @Override protected void onDestroy() { debug("MainActivity: onDestroy"); dataLogger.destroy(); // clean up if(device!=null) { // stop the device nicely device.stopAndJoin(); device.clearFields(); device.registerFilters(); } // disconnect the bluetooth BluetoothManager.getInstance().disconnect(); // un-register for bluetooth changes this.unregisterReceiver(broadcastReceiver); super.onDestroy(); } @Override public boolean onCreateOptionsMenu(final Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.menu_main, menu); // get a reference to the bluetooth action button bluetoothMenutItem = menu.findItem(R.id.action_bluetooth); // and put the right view on it bluetoothMenutItem.setActionView(R.layout.animated_menu_item); // set the correct initial state setBluetoothState(BLUETOOTH_DISCONNECTED); // get access to the image view ImageView imageView = (ImageView) bluetoothMenutItem.getActionView().findViewById(R.id.animated_menu_item_action); // define an action imageView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { (new Thread(new Runnable() { @Override public void run() { toast(getStringSingle(R.string.toast_Reconnecting)); stopBluetooth(); reloadBluetooth(); } })).start(); } }); return true; } private void setBluetoothState(int btState) { if(bluetoothMenutItem!=null) { final ImageView imageView = (ImageView) bluetoothMenutItem.getActionView().findViewById(R.id.animated_menu_item_action); // stop the animation if there is one running AnimationDrawable frameAnimation; if(imageView.getBackground() instanceof AnimationDrawable) { frameAnimation = (AnimationDrawable) imageView.getBackground(); if (frameAnimation.isRunning()) frameAnimation.stop(); } switch (btState) { case BLUETOOTH_DISCONNECTED: imageView.setBackgroundResource(R.mipmap.bluetooth_none); break; case BLUETOOTH_CONNECTED: imageView.setBackgroundResource(R.mipmap.bluetooth_3); break; case BLUETOOTH_SEARCH: runOnUiThread(new Runnable() { @SuppressLint("NewApi") @Override public void run() { AnimationDrawable drawable = (AnimationDrawable) ContextCompat.getDrawable(getApplicationContext(), R.anim.animation_bluetooth); // Use setBackgroundDrawable() for API 14 and 15 and setBackground() for API 16+: if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { imageView.setBackground(drawable); } else { //noinspection deprecation imageView.setBackgroundDrawable(drawable); } AnimationDrawable frameAnimation = (AnimationDrawable) imageView.getBackground(); frameAnimation.start(); } }); break; default: break; } } } @Override public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); // start the settings activity if (id == R.id.action_settings) { if(isSafe()) { // run a toast toast (R.string.toast_WaitingSettings); (new Thread(new Runnable() { @Override public void run() { // give the toast a moment to appear try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } if (device != null) { // stop the BT device device.stopAndJoin(); device.clearFields(); device.registerFilters(); BluetoothManager.getInstance().disconnect(); } // load the activity Intent intent = new Intent(MainActivity.this, SettingsActivity.class); startActivityForResult(intent, SETTINGS_ACTIVITY); } })).start(); return true; } } // see AppSectionsPagerAdapter for the right sequence else if (id == R.id.action_main) { //loadFragement(new MainFragment()); viewPager.setCurrentItem(0,true); updateActionBar(); } else if (id == R.id.action_technical) { //loadFragement(new TechnicalFragment()); viewPager.setCurrentItem(1,true); updateActionBar(); } else if (id == R.id.action_experimental) { //loadFragement(new ExperimentalFragment()); viewPager.setCurrentItem(2,true); updateActionBar(); } //else if (id == R.id.action_bluetooth) { //} return super.onOptionsItemSelected(item); } @Override public void onFieldUpdateEvent(Field field) { if(field.getSID().equals("5d7.0")) { //debug("Speed "+field.getValue()); isDriving = (field.getValue()>10); } } public static boolean isSafe() { boolean safe = !isDriving || !safeDrivingMode; if(!safe) { Toast.makeText(MainActivity.instance, R.string.toast_NotWhileDriving,Toast.LENGTH_LONG).show(); } return safe; } public static boolean isZOE () { return (car == CAR_X10 || car == CAR_ZOE_Q90 || car == CAR_ZOE_Q210 || car == CAR_ZOE_R90 || car == CAR_ZOE_R240); } public static boolean isFluKan () { return (car == CAR_FLUENCE || car == CAR_KANGOO); } public static String getBluetoothDeviceAddress() { if("HTTP Gateway".equals(bluetoothDeviceName)) return gatewayUrl; return bluetoothDeviceAddress; } public static String getStringSingle (int resId) { return res.getString(resId); } public static String [] getStringList (int resId) { return res.getStringArray(resId); } public void setDebugListener (DebugListener debugListener) { this.debugListener = debugListener; } public void dropDebugMessage (String msg) { if (debugListener != null) debugListener.dropDebugMessage (msg); } }