diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..5be4791 Binary files /dev/null and b/.DS_Store differ diff --git a/AndroidClientApp/.gitignore b/AndroidClientApp/.gitignore new file mode 100644 index 0000000..d509498 --- /dev/null +++ b/AndroidClientApp/.gitignore @@ -0,0 +1,11 @@ +*.iml +.gradle +/local.properties +/.idea/caches/build_file_checksums.ser +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +.DS_Store +/build +/captures +.externalNativeBuild \ No newline at end of file diff --git a/AndroidClientApp/.idea/codeStyles/Project.xml b/AndroidClientApp/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..30aa626 --- /dev/null +++ b/AndroidClientApp/.idea/codeStyles/Project.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/AndroidClientApp/.idea/gradle.xml b/AndroidClientApp/.idea/gradle.xml new file mode 100644 index 0000000..7ac24c7 --- /dev/null +++ b/AndroidClientApp/.idea/gradle.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/AndroidClientApp/.idea/misc.xml b/AndroidClientApp/.idea/misc.xml new file mode 100644 index 0000000..e0d5b93 --- /dev/null +++ b/AndroidClientApp/.idea/misc.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/AndroidClientApp/.idea/runConfigurations.xml b/AndroidClientApp/.idea/runConfigurations.xml new file mode 100644 index 0000000..7f68460 --- /dev/null +++ b/AndroidClientApp/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/AndroidClientApp/app/.gitignore b/AndroidClientApp/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/AndroidClientApp/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/AndroidClientApp/app/build.gradle b/AndroidClientApp/app/build.gradle new file mode 100644 index 0000000..bb79dbe --- /dev/null +++ b/AndroidClientApp/app/build.gradle @@ -0,0 +1,33 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 28 + defaultConfig { + applicationId "bleconfd.tc.com.bleconfd_example" + minSdkVersion 26 + targetSdkVersion 28 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + lintOptions { + disable 'Deprecation' + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation 'com.android.support:appcompat-v7:28.0.0' + implementation 'com.android.support.constraint:constraint-layout:1.1.3' + implementation 'com.android.support:support-v4:28.0.0' + implementation 'com.android.support:design:28.0.0' + testImplementation 'junit:junit:4.12' + androidTestImplementation 'com.android.support.test:runner:1.0.2' + androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' +} diff --git a/AndroidClientApp/app/proguard-rules.pro b/AndroidClientApp/app/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/AndroidClientApp/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/AndroidClientApp/app/src/main/AndroidManifest.xml b/AndroidClientApp/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..41e9952 --- /dev/null +++ b/AndroidClientApp/app/src/main/AndroidManifest.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/BLE/BLEListener.java b/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/BLE/BLEListener.java new file mode 100644 index 0000000..7d34c9e --- /dev/null +++ b/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/BLE/BLEListener.java @@ -0,0 +1,26 @@ +package bleconfd.tc.com.bleconfd_example.BLE; + +import org.json.JSONObject; + +/** + * BLE connection event listener + */ +public interface BLEListener { + + /** + * when connection state update + */ + void onUpdate(); + + /** + * when connection closed + */ + void onClose(); + + /** + * when message from BLE server + * + * @param message the message object + */ + void onMessage(JSONObject message); +} diff --git a/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/BLE/BLELogger.java b/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/BLE/BLELogger.java new file mode 100644 index 0000000..3c900c9 --- /dev/null +++ b/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/BLE/BLELogger.java @@ -0,0 +1,43 @@ +package bleconfd.tc.com.bleconfd_example.BLE; + +import android.app.Activity; +import android.widget.TextView; + +/** + * Logger used by BLE Manager + */ +public class BLELogger { + private TextView view; + private Activity activity; + + public BLELogger(TextView view, Activity activity) { + this.view = view; + this.activity = activity; + } + + /** + * debug message + * + * @param msg the message value + */ + public void debug(final String msg) { + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + view.setText(view.getText() + "\n" + msg); + } + }); + } + + /** + * clear logs + */ + public void clear() { + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + view.setText(""); + } + }); + } +} diff --git a/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/BLE/BLEManager.java b/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/BLE/BLEManager.java new file mode 100644 index 0000000..56371ed --- /dev/null +++ b/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/BLE/BLEManager.java @@ -0,0 +1,317 @@ +package bleconfd.tc.com.bleconfd_example.BLE; + + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCallback; +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattService; +import android.bluetooth.BluetoothProfile; +import android.content.Context; +import android.util.Log; + +import java.nio.ByteBuffer; +import java.util.UUID; + +/** + * BLE manager, singleton pattern + */ +public class BLEManager { + + private static BLEManager that = null; + private BLELogger bleLogger = null; + private Context mContext; + private BluetoothDevice mmDevice = null; + + + private UUID rpcServiceUUID = UUID.fromString("503553ca-eb90-11e8-ac5b-bb7e434023e8"); + private UUID rpcInboxUUId = UUID.fromString("510c87c8-eb90-11e8-b3dc-17292c2ecc2d"); + private UUID ePollUUID = UUID.fromString("5140f882-eb90-11e8-a835-13d2bd922d3f"); + + private String TAG = "BLEManager"; + + private BluetoothGatt mBluetoothGatt = null; + private Thread readThread = null; + private BLEPacket blePacket = new BLEPacket(); + private BluetoothAdapter mBluetoothAdapter = null; + private BLEListener bleListener = null; + + + private BLEManager() { + + } + + /** + * get BLE manager instance + */ + public static BLEManager getInstance() { + if (that == null) { + that = new BLEManager(); + } + return that; + } + + /** + * set logger + * + * @param logger the ble logger + */ + public void setLogger(BLELogger logger) { + this.bleLogger = logger; + } + + /** + * debug message + * + * @param msg the message + */ + private void pushMessage(String msg) { + if (this.bleLogger != null) { + this.bleLogger.debug(msg); + } + } + + + public void init(Context context, BluetoothDevice bluetoothDevice) { + if (mmDevice != null && bluetoothDevice.getAddress().equals(mmDevice.getAddress())) { + return; + } + + this.close(); + this.mContext = context; + this.mmDevice = bluetoothDevice; + this.mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + } + + /** + * get current device + * + * @return the current device + */ + public BluetoothDevice getMmDevice() { + return mmDevice; + } + + /** + * check the ble is closed or not + * + * @return + */ + public boolean isClosed() { + return mBluetoothGatt == null; + } + + /** + * set ble listener + * + * @param bleListener the ble listener + */ + public void setBleListener(BLEListener bleListener) { + this.bleListener = bleListener; + } + + /** + * close BLE instance + */ + public void close() { + if (mBluetoothGatt == null) { + return; + } + mBluetoothGatt.disconnect(); + mBluetoothGatt.close(); + cancelReadThread(); + mBluetoothGatt = null; + if (bleListener != null) { + bleListener.onClose(); + } + } + + /** + * connect to BLE + */ + public void connect() { + mBluetoothGatt = mmDevice.connectGatt(mContext, false, new BluetoothGattCallback() { + @Override + public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { + + if (characteristic.getValue() == null || characteristic.getValue().length <= 0) { + return; + } + + if (characteristic.getUuid().equals(ePollUUID)) { + ByteBuffer byteBuffer = ByteBuffer.allocate(characteristic.getValue().length); + byteBuffer.position(0); + byteBuffer.put(characteristic.getValue()); + byteBuffer.position(0); + int length = byteBuffer.getInt(0); + if (length > 0) { + Log.d(TAG, "got content notification, content length = " + length); + readInbox(); + } + } else if (characteristic.getUuid().equals(rpcInboxUUId)) { + pushMessage("receive bytes, length = " + characteristic.getValue().length); + blePacket.unPack(characteristic.getValue()); + while (blePacket.getMessages().size() > 0) { + if (bleListener != null) { + bleListener.onMessage(blePacket.getMessages().poll()); + } + } + } + characteristic.setValue(new byte[]{}); + } + + @Override + public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { + super.onCharacteristicWrite(gatt, characteristic, status); + } + + @Override + public void onServicesDiscovered(BluetoothGatt gatt, int status) { + pushMessage("All done, BLE connection create successful. started ePoll thread"); + ePollThread(); + if (bleListener != null) { + bleListener.onUpdate(); + } + } + + @Override + public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { + super.onCharacteristicChanged(gatt, characteristic); + pushMessage("onCharacteristicChanged ???"); + } + + @Override + public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { + if (newState == BluetoothProfile.STATE_CONNECTED) { + pushMessage("start discoverServices..."); + mBluetoothGatt.discoverServices(); + } else { + pushMessage("connection closed."); + close(); + } + } + }, BluetoothDevice.TRANSPORT_LE); + + if (mBluetoothGatt.connect()) { + pushMessage("start connecting ..."); + } else { + pushMessage("connect failed"); + close(); + } + } + + /** + * cancel read thread + */ + private void cancelReadThread() { + if (readThread != null) { + readThread.interrupt(); + } + readThread = null; + } + + /** + * read inbox message + */ + void readInbox() { + BluetoothGattCharacteristic readCharacteristic = + getCharacteristic(rpcServiceUUID.toString(), rpcInboxUUId.toString()); + mBluetoothGatt.readCharacteristic(readCharacteristic); + } + + /** + * start a thread to read message + */ + void ePollThread() { + this.readThread = new Thread(new Runnable() { + @Override + public void run() { + while (true) { + try { + if (mBluetoothAdapter == null || mBluetoothGatt == null) { + return; + } + if (readThread.isInterrupted()) { + return; + } + Thread.sleep(1000); + BluetoothGattCharacteristic readCharacteristic = + getCharacteristic(rpcServiceUUID.toString(), ePollUUID.toString()); + mBluetoothGatt.readCharacteristic(readCharacteristic); + } catch (Exception e) { + e.printStackTrace(); + return; + } + } + } + }); + this.readThread.start(); + } + + /** + * get service + * + * @param uuid the service uuid + */ + BluetoothGattService getService(UUID uuid) { + if (mBluetoothAdapter == null || mBluetoothGatt == null) { + return null; + } + return mBluetoothGatt.getService(uuid); + } + + /** + * get Characteristic + * + * @param serviceUUID the service uuid + * @param characteristicUUID the Characteristic uuid + */ + private BluetoothGattCharacteristic getCharacteristic(String serviceUUID, String characteristicUUID) { + BluetoothGattService service = getService(UUID.fromString(serviceUUID)); + + if (service == null) { + return null; + } + final BluetoothGattCharacteristic gattCharacteristic = service.getCharacteristic(UUID.fromString(characteristicUUID)); + if (gattCharacteristic != null) { + return gattCharacteristic; + } else { + pushMessage("Can not find 'BluetoothGattCharacteristic'"); + return null; + } + } + + /** + * is BLE service found or not + */ + public boolean isFoundService() { + return mBluetoothGatt != null && mBluetoothGatt.getServices().size() > 0 + && mBluetoothGatt.getService(rpcServiceUUID) != null; + } + + /** + * write data to rpc inbox, we don't care the data size, system will auto split it + * + * @param data the binary data + */ + public boolean write(byte[] data) { + // write into rpi service in box + BluetoothGattCharacteristic writeCharacteristic = getCharacteristic(rpcServiceUUID.toString(), + rpcInboxUUId.toString()); + if (writeCharacteristic == null) { + pushMessage("Write failed. GattCharacteristic is null."); + return false; + } + writeCharacteristic.setValue(data); + boolean result = writeCharacteristicWrite(writeCharacteristic); + pushMessage("write to rpc inbox length = " + data.length + ", result = " + result); + return result; + } + + + boolean writeCharacteristicWrite(BluetoothGattCharacteristic characteristic) { + return mBluetoothGatt.writeCharacteristic(characteristic); + } + +} diff --git a/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/BLE/BLEPacket.java b/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/BLE/BLEPacket.java new file mode 100644 index 0000000..24d4839 --- /dev/null +++ b/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/BLE/BLEPacket.java @@ -0,0 +1,79 @@ +package bleconfd.tc.com.bleconfd_example.BLE; + + +import org.json.JSONException; +import org.json.JSONObject; + +import java.nio.ByteBuffer; +import java.util.LinkedList; +import java.util.List; +import java.util.PriorityQueue; +import java.util.Queue; + + +/** + * rpc BLE packet class + */ +public class BLEPacket { + private final byte kRecordDelimiter = (short) 30; + + private List byteLinkedList = new LinkedList<>(); + + private Queue messages = new PriorityQueue<>(); + + /** + * pack json message to rpc packet + * @param message the json message + * @return the packed bytes + */ + public byte[] pack(String message) { + byte[] contentBytes = message.getBytes(); + int totalLength = contentBytes.length + 1; + + ByteBuffer byteBuffer = ByteBuffer.allocateDirect(totalLength); + byteBuffer.position(0); + byteBuffer.put(contentBytes); + byteBuffer.position(contentBytes.length); + byteBuffer.put(kRecordDelimiter); + + byte[] results = new byte[totalLength]; + byteBuffer.position(0); + byteBuffer.get(results, 0, totalLength); + return results; + } + + + private void parseMessage() { + byte[] buffer = new byte[byteLinkedList.size()]; + for (int i = 0; i < byteLinkedList.size(); i++) { + buffer[i] = byteLinkedList.get(i); + } + byteLinkedList = new LinkedList<>(); + String jsonStr = new String(buffer); + try { + JSONObject jsonObject = new JSONObject(jsonStr); + messages.add(jsonObject); + } catch (JSONException e) { + e.printStackTrace(); + } + } + + /** + * return the JSON object from packet + * + * @param bytes the bytes from rpc + */ + public void unPack(byte[] bytes) { + for (int i = 0; i < bytes.length; i++) { + if (bytes[i] == kRecordDelimiter) { + parseMessage(); + } else { + byteLinkedList.add(bytes[i]); + } + } + } + + public Queue getMessages() { + return messages; + } +} diff --git a/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/BlePacket.java b/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/BlePacket.java new file mode 100644 index 0000000..e0ce0d2 --- /dev/null +++ b/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/BlePacket.java @@ -0,0 +1,81 @@ +package bleconfd.tc.com.bleconfd_example; + + +import org.json.JSONException; +import org.json.JSONObject; + +import java.nio.ByteBuffer; +import java.util.LinkedList; +import java.util.List; +import java.util.PriorityQueue; +import java.util.Queue; + + +/** + * rpc BLE packet class + */ +public class BlePacket { + private final byte kRecordDelimiter = (short) 30; + + private List byteLinkedList = new LinkedList<>(); + + private Queue messages = new PriorityQueue<>(); + + /** + * pack json message to rpc packet + * the header struct see here https://github.com/jmgasper/bleconfd/blob/master/README.md + * + * @param message the json message + * @return the packed bytes + */ + public byte[] pack(String message) { + byte[] contentBytes = message.getBytes(); + int totalLength = contentBytes.length + 1; + + ByteBuffer byteBuffer = ByteBuffer.allocateDirect(totalLength); + byteBuffer.position(0); + byteBuffer.put(contentBytes); + byteBuffer.position(contentBytes.length); + byteBuffer.put(kRecordDelimiter); + + byte[] results = new byte[totalLength]; + byteBuffer.position(0); + byteBuffer.get(results, 0, totalLength); + return results; + } + + + private void parseMessage() { + byte[] buffer = new byte[byteLinkedList.size()]; + for (int i = 0; i < byteLinkedList.size(); i++) { + buffer[i] = byteLinkedList.get(i); + } + byteLinkedList = new LinkedList<>(); + String jsonStr = new String(buffer); + try { + JSONObject jsonObject = new JSONObject(jsonStr); + messages.add(jsonObject); + } catch (JSONException e) { + e.printStackTrace(); + } + } + + /** + * return the JSON object from packet + * + * @param bytes the bytes from rpc + */ + public void unPack(byte[] bytes) { + for (int i = 0; i < bytes.length; i++) { + if (bytes[i] == kRecordDelimiter) { + parseMessage(); + } else { + byteLinkedList.add(bytes[i]); + } + } + } + + public Queue getMessages() { + return messages; + } +} diff --git a/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/Const.java b/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/Const.java new file mode 100644 index 0000000..6f95b49 --- /dev/null +++ b/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/Const.java @@ -0,0 +1,8 @@ +package bleconfd.tc.com.bleconfd_example; + +/** + * Const values + */ +public class Const { + public static String ARG_PAGE_NUMBER = "ARG_PAGE_NUMBER"; +} diff --git a/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/DialogSetWifi.java b/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/DialogSetWifi.java new file mode 100644 index 0000000..99108cf --- /dev/null +++ b/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/DialogSetWifi.java @@ -0,0 +1,80 @@ +package bleconfd.tc.com.bleconfd_example; + +import android.app.Dialog; +import android.content.Context; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import android.support.v4.app.DialogFragment; +import android.support.v7.app.AlertDialog; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.EditText; +import android.widget.Toast; + + +/**Test + * set wifi dialog class + */ +public class DialogSetWifi extends DialogFragment { + + + /** + * click listener + */ + public interface DialogListener { + boolean onConnect(DialogFragment dialog, String ssid, String password); + } + + DialogListener mListener; + + @Override + public void onAttach(Context context) { + super.onAttach(context); + mListener = (DialogListener) context; + } + + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + // Get the layout inflater + LayoutInflater inflater = getActivity().getLayoutInflater(); + + + View view = inflater.inflate(R.layout.dialog_wifi, null); + builder.setView(view) + .setTitle("SET RPI WIFI CONNECTION"); + + final EditText ssid = view.findViewById(R.id.wifi_ssid); + final EditText password = view.findViewById(R.id.wifi_password); + + + view.findViewById(R.id.wifi_connect).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (ssid.getText().toString().equals("") || password.getText().toString().equals("")) { + Toast.makeText(getActivity(), "SSID and password cannot be empty", Toast.LENGTH_SHORT).show(); + return; + } + boolean r = mListener.onConnect(DialogSetWifi.this, + ssid.getText().toString(), + password.getText().toString()); + if (r) { + DialogSetWifi.this.getDialog().cancel(); + } else { + Toast.makeText(getActivity(), "write message to rpc failed.", Toast.LENGTH_SHORT).show(); + } + } + }); + + view.findViewById(R.id.wifi_cancel).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + DialogSetWifi.this.getDialog().cancel(); + } + }); + return builder.create(); + } +} diff --git a/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/DrawerActivity.java b/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/DrawerActivity.java new file mode 100755 index 0000000..e8147c2 --- /dev/null +++ b/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/DrawerActivity.java @@ -0,0 +1,150 @@ + +package bleconfd.tc.com.bleconfd_example; + +import android.app.Activity; +import android.app.Fragment; +import android.app.FragmentManager; +import android.app.FragmentTransaction; +import android.content.res.Configuration; +import android.os.Bundle; +import android.support.v4.app.ActionBarDrawerToggle; +import android.support.v4.view.GravityCompat; +import android.support.v4.widget.DrawerLayout; +import android.support.v7.widget.RecyclerView; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; + +import bleconfd.tc.com.bleconfd_example.adapters.DrawerMenuItemAdapter; +import bleconfd.tc.com.bleconfd_example.fragments.HomeFragment; +import bleconfd.tc.com.bleconfd_example.fragments.INIFileFragment; +import bleconfd.tc.com.bleconfd_example.fragments.WifiFragment; + + +/** + * the main activity include drawer + */ +public class DrawerActivity extends Activity + implements DrawerMenuItemAdapter.OnItemClickListener { + private DrawerLayout mDrawerLayout; + private RecyclerView mDrawerList; + private ActionBarDrawerToggle mDrawerToggle; + private String[] mPlanetTitles; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_navigation_drawer); + + mPlanetTitles = getResources().getStringArray(R.array.drawer_item_array); + mDrawerLayout = findViewById(R.id.drawer_layout); + mDrawerList = findViewById(R.id.left_drawer); + + // set a custom shadow that overlays the main content when the drawer opens + mDrawerLayout.setDrawerShadow(R.drawable.drawer_shadow, GravityCompat.START); + // improve performance by indicating the list if fixed size. + mDrawerList.setHasFixedSize(true); + + // set up the drawer's list view with items and click listener + mDrawerList.setAdapter(new DrawerMenuItemAdapter(mPlanetTitles, this)); + // enable ActionBar app icon to behave as action to toggle nav drawer + getActionBar().setDisplayHomeAsUpEnabled(true); + getActionBar().setHomeButtonEnabled(true); + + // ActionBarDrawerToggle ties together the the proper interactions + // between the sliding drawer and the action bar app icon + mDrawerToggle = new ActionBarDrawerToggle( + this, /* host Activity */ + mDrawerLayout, /* DrawerLayout object */ + R.drawable.ic_drawer, /* nav drawer image to replace 'Up' caret */ + R.string.drawer_open, /* "open drawer" description for accessibility */ + R.string.drawer_close /* "close drawer" description for accessibility */ + ) { + public void onDrawerClosed(View view) { + invalidateOptionsMenu(); // creates call to onPrepareOptionsMenu() + } + + public void onDrawerOpened(View drawerView) { + invalidateOptionsMenu(); // creates call to onPrepareOptionsMenu() + } + }; + mDrawerLayout.setDrawerListener(mDrawerToggle); + + if (savedInstanceState == null) { + selectItem(0); + } + } + + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + return false; + } + + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + return mDrawerToggle.onOptionsItemSelected(item); + } + + /* The click listener for RecyclerView in the navigation drawer */ + @Override + public void onClick(View view, int position) { + selectItem(position); + } + + /** + * when drawer item clicked + * + * @param position the drawer position + */ + private void selectItem(int position) { + // update the main content by replacing fragments + Fragment fragment = null; + switch (position) { + case 0: + fragment = HomeFragment.newInstance(position); + break; + case 1: + fragment = WifiFragment.newInstance(position); + break; + case 2: + fragment = INIFileFragment.newInstance(position); + break; + } + + FragmentManager fragmentManager = getFragmentManager(); + FragmentTransaction ft = fragmentManager.beginTransaction(); + ft.replace(R.id.content_frame, fragment); + ft.commit(); + + // update selected item title, then close the drawer + setTitle(mPlanetTitles[position]); + mDrawerLayout.closeDrawer(mDrawerList); + } + + /** + * set navigation bar title + * + * @param title the title + */ + @Override + public void setTitle(CharSequence title) { + getActionBar().setTitle(title); + } + + + @Override + protected void onPostCreate(Bundle savedInstanceState) { + super.onPostCreate(savedInstanceState); + // Sync the toggle state after onRestoreInstanceState has occurred. + mDrawerToggle.syncState(); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + // Pass any configuration change to the drawer toggls + mDrawerToggle.onConfigurationChanged(newConfig); + } +} diff --git a/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/MainActivity.java b/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/MainActivity.java new file mode 100644 index 0000000..6551554 --- /dev/null +++ b/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/MainActivity.java @@ -0,0 +1,465 @@ +package bleconfd.tc.com.bleconfd_example; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCallback; +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattService; +import android.bluetooth.BluetoothProfile; +import android.content.Context; +import android.support.v4.app.DialogFragment; +import android.support.v7.app.AppCompatActivity; +import android.os.Bundle; +import android.util.Log; +import android.view.View; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.Spinner; +import android.widget.TextView; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Set; +import java.util.UUID; + +public class MainActivity extends AppCompatActivity implements DialogSetWifi.DialogListener { + BluetoothAdapter mBluetoothAdapter = null; + + private TextView logTxt = null; + private Spinner spinner = null; + private Button connectBtn = null; + private Button disConnectBtn = null; + private Button setWifiBtn = null; + private final String TAG = "BLE-EXAMPLE"; + + private BluetoothDevice currentDevice = null; + private Context mContext; + private BLEInstance bLEInstance = null; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + this.mContext = this; + + spinner = findViewById(R.id.pairedBleSpinner); + logTxt = findViewById(R.id.logTxt); + connectBtn = findViewById(R.id.connectBtn); + disConnectBtn = findViewById(R.id.disConnectBtn); + setWifiBtn = findViewById(R.id.setWifiBtn); + + + mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + if (mBluetoothAdapter == null) { + // Device does not support Bluetooth + logTxt.setText("Device does not support Bluetooth ..."); + return; + } + + initUI(); + } + + /** + * update buttons status + */ + private void updateUI() { + runOnUiThread(new Runnable() { + @Override + public void run() { + if (currentDevice == null || bLEInstance != null) { + connectBtn.setEnabled(false); + } else { + connectBtn.setEnabled(true); + } + + if (bLEInstance != null) { + disConnectBtn.setEnabled(true); + spinner.setEnabled(false); + } else { + spinner.setEnabled(true); + disConnectBtn.setEnabled(false); + } + + if (bLEInstance != null && bLEInstance.isFoundService()) { + setWifiBtn.setEnabled(true); + } else { + setWifiBtn.setEnabled(false); + } + } + }); + } + + /** + * init UI + */ + private void initUI() { + final ArrayList list = new ArrayList<>(); + list.add("No Devices"); + + this.updateUI(); + + final Set pairedDevices = mBluetoothAdapter.getBondedDevices(); + if (pairedDevices.size() <= 0) { + logTxt.setText("You need paired rpi3 bluetooth device in android system setting first"); + return; + } + + for (BluetoothDevice device : pairedDevices) { + list.add(device.getName()); + } + + ArrayAdapter adapter = new ArrayAdapter(this, R.layout.spinner_item, R.id.spinnerTxt, list); + spinner.setAdapter(adapter); + spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + Log.d(TAG, "selected position = " + position); + currentDevice = null; + for (BluetoothDevice device : pairedDevices) { + if (device.getName().equals(list.get(position))) { + currentDevice = device; + } + } + updateUI(); + } + + @Override + public void onNothingSelected(AdapterView parent) { + + } + }); + + + // connect button button click event + connectBtn.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + closeInstance(); + logTxt.setText(""); + bLEInstance = new BLEInstance(currentDevice); + bLEInstance.connect(); + updateUI(); + } + }); + + //disconnect button click event + disConnectBtn.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + closeInstance(); + logTxt.setText(""); + pushMessage("ble connection closed"); + } + }); + + // set wifi button click event + setWifiBtn.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + DialogSetWifi dialog = new DialogSetWifi(); + dialog.show(getSupportFragmentManager(), "SetWifiDialog"); + } + }); + } + + @Override + public void onBackPressed() { + moveTaskToBack(true); + android.os.Process.killProcess(android.os.Process.myPid()); + System.exit(1); + } + + /** + * close ble connection instance + */ + private void closeInstance() { + if (bLEInstance != null) { + bLEInstance.close(); + } + bLEInstance = null; + updateUI(); + } + + /** + * show message + * + * @param msg the mssage + */ + private void pushMessage(final String msg) { + runOnUiThread(new Runnable() { + @Override + public void run() { + logTxt.setText(logTxt.getText() + "\n" + msg); + } + }); + } + + /** + * create + * + * @param ssid + * @param password + * @return + * @throws JSONException + */ + private byte[] connectWifiBody(String ssid, String password) throws JSONException { + + JSONObject wifiObj = new JSONObject(); + wifiObj.put("jsonrpc", "2.0"); + wifiObj.put("method", "wifi-connect"); + wifiObj.put("id", (int) (Math.random() * 100)); + wifiObj.put("wi-fi_tech", "infra"); + + JSONObject disco = new JSONObject(); + disco.put("ssid", ssid); + wifiObj.put("discovery", disco); + + JSONObject creds = new JSONObject(); + creds.put("akm", "psk"); + creds.put("pass", password); + wifiObj.put("cred", creds); + + BlePacket blePacket = new BlePacket(); + return blePacket.pack(wifiObj.toString()); + } + + + @Override + public boolean onConnect(DialogFragment dialog, String ssid, String password) { + if (bLEInstance == null) { + return false; + } + try { + boolean ret = bLEInstance.write(connectWifiBody(ssid, password)); + if (ret) { + pushMessage("rpc(wifi-connect) invoked, waiting response..."); + } + return ret; + } catch (JSONException e) { + e.printStackTrace(); + return false; + } + } + + /** + * BLE class instance + */ + class BLEInstance { + private final BluetoothDevice mmDevice; + private UUID rpcServiceUUID = UUID.fromString("503553ca-eb90-11e8-ac5b-bb7e434023e8"); + private UUID rpcInboxUUId = UUID.fromString("510c87c8-eb90-11e8-b3dc-17292c2ecc2d"); + private UUID ePollUUID = UUID.fromString("5140f882-eb90-11e8-a835-13d2bd922d3f"); + + private BluetoothGatt mBluetoothGatt = null; + private Thread readThread = null; + private BlePacket blePacket = new BlePacket(); + + BLEInstance(BluetoothDevice device) { + mmDevice = device; + } + + + /** + * close BLE instance + */ + public void close() { + if (mBluetoothGatt == null) { + return; + } + mBluetoothGatt.disconnect(); + mBluetoothGatt.close(); + cancelReadThread(); + mBluetoothGatt = null; + } + + /** + * connect to BLE + */ + public void connect() { + + mBluetoothGatt = mmDevice.connectGatt(mContext, false, new BluetoothGattCallback() { + @Override + public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { + + if (characteristic.getValue() == null || characteristic.getValue().length <= 0) { + return; + } + + if (characteristic.getUuid().equals(ePollUUID)) { + ByteBuffer byteBuffer = ByteBuffer.allocate(characteristic.getValue().length); + byteBuffer.position(0); + byteBuffer.put(characteristic.getValue()); + byteBuffer.position(0); + int length = byteBuffer.getInt(0); + Log.d(TAG, "notification got, start read ... length = " + length); + if (length > 0) { + readInbox(); + } + } else if (characteristic.getUuid().equals(rpcInboxUUId)) { + pushMessage("receive bytes, length = " + characteristic.getValue().length); + blePacket.unPack(characteristic.getValue()); + while (blePacket.getMessages().size() > 0) { + pushMessage(blePacket.getMessages().poll().toString()); + } + } + characteristic.setValue(new byte[]{}); + } + + @Override + public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { + super.onCharacteristicWrite(gatt, characteristic, status); + } + + @Override + public void onServicesDiscovered(BluetoothGatt gatt, int status) { + pushMessage("All done, BLE connection create successful. started ePoll thread"); + ePollThread(); + updateUI(); + } + + @Override + public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { + super.onCharacteristicChanged(gatt, characteristic); + pushMessage("onCharacteristicChanged ???"); + } + + @Override + public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { + if (newState == BluetoothProfile.STATE_CONNECTED) { + pushMessage("start discoverServices..."); + mBluetoothGatt.discoverServices(); + } else { + pushMessage("connection closed."); + closeInstance(); + } + } + }, BluetoothDevice.TRANSPORT_LE); + + if (mBluetoothGatt.connect()) { + pushMessage("start connecting ..."); + } else { + pushMessage("connect failed"); + closeInstance(); + } + } + + /** + * cancel read thread + */ + private void cancelReadThread() { + if (readThread != null) { + readThread.interrupt(); + } + readThread = null; + } + + /** + * read inbox message + */ + void readInbox() { + BluetoothGattCharacteristic readCharacteristic = + getCharacteristic(rpcServiceUUID.toString(), rpcInboxUUId.toString()); + mBluetoothGatt.readCharacteristic(readCharacteristic); + } + + /** + * start a thread to read message + */ + void ePollThread() { + this.readThread = new Thread(new Runnable() { + @Override + public void run() { + while (true) { + try { + if (mBluetoothAdapter == null || mBluetoothGatt == null) { + return; + } + if (readThread.isInterrupted()) { + return; + } + Thread.sleep(500); + BluetoothGattCharacteristic readCharacteristic = + getCharacteristic(rpcServiceUUID.toString(), ePollUUID.toString()); + mBluetoothGatt.readCharacteristic(readCharacteristic); + } catch (Exception e) { + e.printStackTrace(); + return; + } + } + } + }); + this.readThread.start(); + } + + /** + * get service + * + * @param uuid the service uuid + */ + BluetoothGattService getService(UUID uuid) { + if (mBluetoothAdapter == null || mBluetoothGatt == null) { + return null; + } + return mBluetoothGatt.getService(uuid); + } + + /** + * get Characteristic + * + * @param serviceUUID the service uuid + * @param characteristicUUID the Characteristic uuid + */ + private BluetoothGattCharacteristic getCharacteristic(String serviceUUID, String characteristicUUID) { + BluetoothGattService service = getService(UUID.fromString(serviceUUID)); + + if (service == null) { + return null; + } + final BluetoothGattCharacteristic gattCharacteristic = service.getCharacteristic(UUID.fromString(characteristicUUID)); + if (gattCharacteristic != null) { + return gattCharacteristic; + } else { + pushMessage("Can not find 'BluetoothGattCharacteristic'"); + return null; + } + } + + /** + * is BLE service found or not + */ + public boolean isFoundService() { + return mBluetoothGatt != null && mBluetoothGatt.getServices().size() > 0 + && mBluetoothGatt.getService(rpcServiceUUID) != null; + } + + /** + * write data to rpc inbox, we don't care the data size, system will auto split it + * + * @param data the binary data + */ + boolean write(byte[] data) { + // write into rpi service in box + BluetoothGattCharacteristic writeCharacteristic = getCharacteristic(rpcServiceUUID.toString(), + rpcInboxUUId.toString()); + if (writeCharacteristic == null) { + pushMessage("Write failed. GattCharacteristic is null."); + return false; + } + writeCharacteristic.setValue(data); + boolean result = writeCharacteristicWrite(writeCharacteristic); + pushMessage("write to rpc inbox length = " + data.length + ", result = " + result); + return result; + } + + + boolean writeCharacteristicWrite(BluetoothGattCharacteristic characteristic) { + return mBluetoothGatt.writeCharacteristic(characteristic); + } + } +} + diff --git a/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/adapters/DrawerMenuItemAdapter.java b/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/adapters/DrawerMenuItemAdapter.java new file mode 100755 index 0000000..9dd3afd --- /dev/null +++ b/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/adapters/DrawerMenuItemAdapter.java @@ -0,0 +1,67 @@ + + +package bleconfd.tc.com.bleconfd_example.adapters; + +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import bleconfd.tc.com.bleconfd_example.R; + +/** + * Adapter for the drawer data used in our drawer menu, + */ +public class DrawerMenuItemAdapter extends RecyclerView.Adapter { + private String[] mDataset; + private OnItemClickListener mListener; + + /** + * Interface for receiving click events from cells. + */ + public interface OnItemClickListener { + public void onClick(View view, int position); + } + + /** + * Custom viewholder for our planet views. + */ + public static class ViewHolder extends RecyclerView.ViewHolder { + public final TextView mTextView; + + public ViewHolder(TextView v) { + super(v); + mTextView = v; + } + } + + public DrawerMenuItemAdapter(String[] myDataset, OnItemClickListener listener) { + mDataset = myDataset; + mListener = listener; + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + LayoutInflater vi = LayoutInflater.from(parent.getContext()); + View v = vi.inflate(R.layout.drawer_list_item, parent, false); + TextView tv = v.findViewById(android.R.id.text1); + return new ViewHolder(tv); + } + + @Override + public void onBindViewHolder(ViewHolder holder, final int position) { + holder.mTextView.setText(mDataset[position]); + holder.mTextView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + mListener.onClick(view, position); + } + }); + } + + @Override + public int getItemCount() { + return mDataset.length; + } +} diff --git a/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/adapters/INIGroupItemAdapter.java b/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/adapters/INIGroupItemAdapter.java new file mode 100644 index 0000000..614583b --- /dev/null +++ b/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/adapters/INIGroupItemAdapter.java @@ -0,0 +1,101 @@ +package bleconfd.tc.com.bleconfd_example.adapters; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.Button; +import android.widget.ImageButton; +import android.widget.ListView; +import android.widget.TextView; + +import java.util.List; + +import bleconfd.tc.com.bleconfd_example.R; +import bleconfd.tc.com.bleconfd_example.models.INIGroup; +import bleconfd.tc.com.bleconfd_example.ui.TouchEffect; + +/** + * adapter for group list in INI file page + */ +public class INIGroupItemAdapter extends BaseAdapter { + + private Context context; + private List groups; + private OnClickListener mListener; + + public INIGroupItemAdapter(Context context, List groups, OnClickListener mListener) { + this.context = context; + this.groups = groups; + this.mListener = mListener; + } + + @Override + public int getCount() { + return groups.size(); + } + + @Override + public INIGroup getItem(int position) { + return groups.get(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(final int position, View convertView, ViewGroup parent) { + View view = LayoutInflater.from(context).inflate(R.layout.ini_group_item, null); + final INIGroup group = getItem(position); + ImageButton editGroupBtn = view.findViewById(R.id.edit_group_btn); + editGroupBtn.setOnTouchListener(new TouchEffect()); + editGroupBtn.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + mListener.onGroupModifyClick(group.getName()); + } + }); + + ImageButton deleteGroupBtn = view.findViewById(R.id.delete_group_btn); + deleteGroupBtn.setOnTouchListener(new TouchEffect()); + deleteGroupBtn.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + mListener.onGroupDeleteClick(group.getName()); + } + }); + + ((TextView) view.findViewById(R.id.group_name)).setText("[" + group.getName() + "]"); + + ListView listView = view.findViewById(R.id.value_listview); + listView.setAdapter(new INIKeyValueItemAdapter(context, group.getKeyValues(), new INIKeyValueItemAdapter.OnClickListener() { + @Override + public void onValueDeleteClick(String key) { + mListener.onValueDeleteClick(group.getName(), key); + } + + @Override + public void onValueModifyClick(String key, String value) { + mListener.onValueModifyClick(group.getName(), key, value); + } + })); + return view; + } + + + /** + * Interface for receiving click events from cells. + */ + public interface OnClickListener { + void onGroupDeleteClick(String groupName); + + void onGroupModifyClick(String groupName); + + void onValueDeleteClick(String group, String key); + + void onValueModifyClick(String group, String key, String value); + } +} diff --git a/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/adapters/INIKeyValueItemAdapter.java b/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/adapters/INIKeyValueItemAdapter.java new file mode 100644 index 0000000..55f2d94 --- /dev/null +++ b/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/adapters/INIKeyValueItemAdapter.java @@ -0,0 +1,85 @@ +package bleconfd.tc.com.bleconfd_example.adapters; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.ImageButton; +import android.widget.TextView; + +import java.util.List; + +import bleconfd.tc.com.bleconfd_example.R; +import bleconfd.tc.com.bleconfd_example.models.INIKeyValue; +import bleconfd.tc.com.bleconfd_example.ui.TouchEffect; + +/** + * adapter for value list in INI file page + */ +public class INIKeyValueItemAdapter extends BaseAdapter { + + private Context context; + private List keyValues; + private OnClickListener mListener; + + public INIKeyValueItemAdapter(Context context, List keyValues, OnClickListener mListener) { + this.context = context; + this.keyValues = keyValues; + this.mListener = mListener; + } + + @Override + public int getCount() { + return keyValues.size(); + } + + @Override + public INIKeyValue getItem(int position) { + return keyValues.get(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(final int position, View convertView, ViewGroup parent) { + View view = LayoutInflater.from(context).inflate(R.layout.ini_value_item, null); + final INIKeyValue keyValue = getItem(position); + + ImageButton editBtn = view.findViewById(R.id.edit_btn); + editBtn.setOnTouchListener(new TouchEffect()); + editBtn.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + mListener.onValueModifyClick(keyValue.getKey(), keyValue.getValue()); + } + }); + + ImageButton deleteBtn = view.findViewById(R.id.delete_btn); + deleteBtn.setOnTouchListener(new TouchEffect()); + deleteBtn.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + mListener.onValueDeleteClick(keyValue.getKey()); + } + }); + + + ((TextView) view.findViewById(R.id.key_value_txt)).setText(keyValue.getKey() + " = " + keyValue.getValue()); + return view; + } + + /** + * Interface for receiving click events from cells. + */ + public interface OnClickListener { + void onValueDeleteClick(String key); + + void onValueModifyClick(String key, String value); + } + + +} diff --git a/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/dialogs/DialogAddOrEditGroup.java b/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/dialogs/DialogAddOrEditGroup.java new file mode 100644 index 0000000..2e567fa --- /dev/null +++ b/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/dialogs/DialogAddOrEditGroup.java @@ -0,0 +1,87 @@ +package bleconfd.tc.com.bleconfd_example.dialogs; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.Toast; + +import bleconfd.tc.com.bleconfd_example.R; + +/** + * add or edit group dialog for ini file page + */ +public class DialogAddOrEditGroup extends DialogFragment { + + /** + * click listener + */ + public interface Listener { + boolean onAddOrEditGroup(String originName, String newName); + } + + Listener mListener; + + + public void setmListener(Listener mListener) { + this.mListener = mListener; + } + + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + final String groupName = getArguments().getString("group"); + + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + + // Get the layout inflater + LayoutInflater inflater = getActivity().getLayoutInflater(); + + + View view = inflater.inflate(R.layout.dialog_add_group, null); + builder.setView(view) + .setTitle(groupName == null ? "ADD GROUP" : "EDIT GROUP"); + + final EditText groupInput = view.findViewById(R.id.group_name); + + // set group name + if (groupName != null) { + groupInput.setText(groupName); + } + + groupInput.requestFocus(); + + Button okButton = view.findViewById(R.id.ok); + okButton.setText(groupName == null ? "ADD" : "EDIT"); + okButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + + if (groupInput.getText().toString().trim().isEmpty()) { + Toast.makeText(getContext(), "group name cannot be empty", Toast.LENGTH_LONG).show(); + return; + } + + if (mListener != null) { + mListener.onAddOrEditGroup(groupName, groupInput.getText().toString()); + } + DialogAddOrEditGroup.this.getDialog().cancel(); + } + }); + + view.findViewById(R.id.cancel).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + DialogAddOrEditGroup.this.getDialog().cancel(); + } + }); + + return builder.create(); + } +} diff --git a/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/dialogs/DialogAddOrEditValue.java b/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/dialogs/DialogAddOrEditValue.java new file mode 100644 index 0000000..32eaaa2 --- /dev/null +++ b/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/dialogs/DialogAddOrEditValue.java @@ -0,0 +1,113 @@ +package bleconfd.tc.com.bleconfd_example.dialogs; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.Toast; + +import bleconfd.tc.com.bleconfd_example.R; + +/** + * add or edit key value dialog for ini file page + */ +public class DialogAddOrEditValue extends DialogFragment { + + /** + * click listener + */ + public interface Listener { + boolean onAddOrEditValue(String group, String key, String value); + } + + Listener mListener; + + + public void setmListener(Listener mListener) { + this.mListener = mListener; + } + + + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + final String groupName = getArguments().getString("group"); + final String keyName = getArguments().getString("key"); + final String value = getArguments().getString("value"); + + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + + // Get the layout inflater + LayoutInflater inflater = getActivity().getLayoutInflater(); + + + View view = inflater.inflate(R.layout.dialog_add_ini_value, null); + builder.setView(view) + .setTitle(groupName == null ? "ADD VALUE" : "EDIT VALUE"); + + final EditText groupInput = view.findViewById(R.id.group_name); + final EditText keyInput = view.findViewById(R.id.key); + final EditText valueInput = view.findViewById(R.id.value); + + + // set values + if (groupName != null) { + groupInput.setText(groupName); + groupInput.setEnabled(false); + } else { + groupInput.requestFocus(); + } + + if (keyName != null) { + keyInput.setText(keyName); + keyInput.setEnabled(false); + } + + if (value != null) { + valueInput.setText(value); + valueInput.requestFocus(); + } + + groupInput.requestFocus(); + + Button okButton = view.findViewById(R.id.ok); + okButton.setText(groupName == null ? "ADD" : "EDIT"); + okButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (groupInput.getText().toString().trim().isEmpty()) { + Toast.makeText(getContext(), "group name cannot be empty", Toast.LENGTH_LONG).show(); + return; + } + if (keyInput.getText().toString().trim().isEmpty()) { + Toast.makeText(getContext(), "key cannot be empty", Toast.LENGTH_LONG).show(); + return; + } + if (valueInput.getText().toString().trim().isEmpty()) { + Toast.makeText(getContext(), "value cannot be empty", Toast.LENGTH_LONG).show(); + return; + } + if (mListener != null) { + mListener.onAddOrEditValue(groupInput.getText().toString(), + keyInput.getText().toString(), valueInput.getText().toString()); + } + DialogAddOrEditValue.this.getDialog().cancel(); + } + }); + + view.findViewById(R.id.cancel).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + DialogAddOrEditValue.this.getDialog().cancel(); + } + }); + + return builder.create(); + } +} diff --git a/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/dialogs/DialogSetWifi.java b/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/dialogs/DialogSetWifi.java new file mode 100644 index 0000000..f902f6d --- /dev/null +++ b/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/dialogs/DialogSetWifi.java @@ -0,0 +1,86 @@ +package bleconfd.tc.com.bleconfd_example.dialogs; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.content.Context; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + + +import android.view.LayoutInflater; +import android.view.View; +import android.widget.EditText; +import android.widget.Toast; + +import bleconfd.tc.com.bleconfd_example.R; + + +/** + * set wifi dialog class + */ +public class DialogSetWifi extends DialogFragment { + + + /** + * click listener + */ + public interface DialogListener { + boolean onConnect(DialogFragment dialog, String ssid, String password); + } + + DialogListener mListener; + + @Override + public void onAttach(Context context) { + super.onAttach(context); + } + + public void setmListener(DialogListener mListener) { + this.mListener = mListener; + } + + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + // Get the layout inflater + LayoutInflater inflater = getActivity().getLayoutInflater(); + + + View view = inflater.inflate(R.layout.dialog_wifi, null); + builder.setView(view) + .setTitle("SET RPI WIFI CONNECTION"); + + final EditText ssid = view.findViewById(R.id.wifi_ssid); + final EditText password = view.findViewById(R.id.wifi_password); + + + view.findViewById(R.id.wifi_connect).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (ssid.getText().toString().equals("") || password.getText().toString().equals("")) { + Toast.makeText(getActivity(), "SSID and password cannot be empty", Toast.LENGTH_SHORT).show(); + return; + } + boolean r = mListener.onConnect(DialogSetWifi.this, + ssid.getText().toString(), + password.getText().toString()); + if (r) { + DialogSetWifi.this.getDialog().cancel(); + } else { + Toast.makeText(getActivity(), "write message to rpc failed.", Toast.LENGTH_SHORT).show(); + } + } + }); + + view.findViewById(R.id.wifi_cancel).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + DialogSetWifi.this.getDialog().cancel(); + } + }); + return builder.create(); + } +} diff --git a/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/fragments/HomeFragment.java b/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/fragments/HomeFragment.java new file mode 100644 index 0000000..3e1e70c --- /dev/null +++ b/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/fragments/HomeFragment.java @@ -0,0 +1,211 @@ +package bleconfd.tc.com.bleconfd_example.fragments; + +import android.app.Fragment; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.Spinner; +import android.widget.TextView; + +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; + +import bleconfd.tc.com.bleconfd_example.BLE.BLEListener; +import bleconfd.tc.com.bleconfd_example.BLE.BLELogger; +import bleconfd.tc.com.bleconfd_example.BLE.BLEManager; +import bleconfd.tc.com.bleconfd_example.Const; +import bleconfd.tc.com.bleconfd_example.R; + +/** + * home fragment page, used to create connection for BLE + */ +public class HomeFragment extends Fragment { + + BluetoothAdapter mBluetoothAdapter = null; + + private TextView logTxt = null; + private Spinner spinner = null; + private Button connectBtn = null; + private Button disConnectBtn = null; + private final String TAG = "BLE-EXAMPLE"; + private BluetoothDevice currentDevice = null; + private BLELogger bleLogger = null; + + public HomeFragment() { + } + + /** + * create new home page + * + * @param position the drawer position + */ + public static android.app.Fragment newInstance(int position) { + android.app.Fragment fragment = new HomeFragment(); + Bundle args = new Bundle(); + args.putInt(Const.ARG_PAGE_NUMBER, position); + fragment.setArguments(args); + return fragment; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View rootView = inflater.inflate(R.layout.fragment_home, container, false); + int i = getArguments().getInt(Const.ARG_PAGE_NUMBER); + String title = getResources().getStringArray(R.array.drawer_item_array)[i]; + getActivity().setTitle(title); + + spinner = rootView.findViewById(R.id.pairedBleSpinner); + logTxt = rootView.findViewById(R.id.logTxt); + connectBtn = rootView.findViewById(R.id.connectBtn); + disConnectBtn = rootView.findViewById(R.id.disConnectBtn); + + mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + + if (mBluetoothAdapter == null) { + // Device does not support Bluetooth + logTxt.setText("Device does not support Bluetooth ..."); + } + + bleLogger = new BLELogger(logTxt, getActivity()); + BLEManager.getInstance().setLogger(bleLogger); + BLEManager.getInstance().setBleListener(new BLEListener() { + @Override + public void onUpdate() { + updateUI(); + } + + @Override + public void onClose() { + updateUI(); + } + + @Override + public void onMessage(JSONObject message) { + bleLogger.debug(message.toString()); + } + }); + + initUI(); + return rootView; + } + + + /** + * init UI + */ + private void initUI() { + final ArrayList list = new ArrayList<>(); + list.add("No Devices"); + + Set pairedDevices = new HashSet<>(); + if (mBluetoothAdapter != null) { + pairedDevices = mBluetoothAdapter.getBondedDevices(); + if (pairedDevices.size() <= 0) { + logTxt.setText("You need paired rpi3 bluetooth device in android system setting first"); + return; + } + + for (BluetoothDevice device : pairedDevices) { + list.add(device.getName()); + } + } + + + ArrayAdapter adapter = new ArrayAdapter(getContext(), R.layout.spinner_item, R.id.spinnerTxt, list); + spinner.setAdapter(adapter); + + final Set finalPairedDevices = pairedDevices; + spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + Log.d(TAG, "selected position = " + position); + currentDevice = null; + for (BluetoothDevice device : finalPairedDevices) { + if (device.getName().equals(list.get(position))) { + currentDevice = device; + BLEManager.getInstance().init(getContext(), currentDevice); + } + } + updateUI(); + } + + @Override + public void onNothingSelected(AdapterView parent) { + + } + }); + + + // connect button button click event + connectBtn.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + BLEManager.getInstance().close(); + bleLogger.clear(); + BLEManager.getInstance().connect(); + updateUI(); + } + }); + + //disconnect button click event + disConnectBtn.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + BLEManager.getInstance().close(); + bleLogger.clear(); + bleLogger.debug("ble connection closed"); + } + }); + + + if (BLEManager.getInstance().getMmDevice() != null) { + int index = 1; + for (BluetoothDevice device : pairedDevices) { + if (device.getAddress().equals(BLEManager.getInstance().getMmDevice().getAddress())) { + spinner.setSelection(index); + currentDevice = device; + } + index += 1; + } + } + + if (!BLEManager.getInstance().isClosed() && BLEManager.getInstance().isFoundService()) { + bleLogger.debug("connection worked as expected"); + } + + this.updateUI(); + } + + + /** + * update buttons status + */ + private void updateUI() { + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + + connectBtn.setEnabled(currentDevice != null && BLEManager.getInstance().isClosed()); + + if (!BLEManager.getInstance().isClosed()) { + disConnectBtn.setEnabled(true); + spinner.setEnabled(false); + } else { + spinner.setEnabled(true); + disConnectBtn.setEnabled(false); + } + } + }); + } +} diff --git a/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/fragments/INIFileFragment.java b/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/fragments/INIFileFragment.java new file mode 100644 index 0000000..0c79b17 --- /dev/null +++ b/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/fragments/INIFileFragment.java @@ -0,0 +1,475 @@ +package bleconfd.tc.com.bleconfd_example.fragments; + +import android.app.AlertDialog; +import android.app.Fragment; +import android.app.ProgressDialog; +import android.content.DialogInterface; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.Toast; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import bleconfd.tc.com.bleconfd_example.BLE.BLEListener; +import bleconfd.tc.com.bleconfd_example.BLE.BLELogger; +import bleconfd.tc.com.bleconfd_example.BLE.BLEManager; +import bleconfd.tc.com.bleconfd_example.BLE.BLEPacket; +import bleconfd.tc.com.bleconfd_example.Const; +import bleconfd.tc.com.bleconfd_example.R; +import bleconfd.tc.com.bleconfd_example.adapters.INIGroupItemAdapter; +import bleconfd.tc.com.bleconfd_example.dialogs.DialogAddOrEditGroup; +import bleconfd.tc.com.bleconfd_example.dialogs.DialogAddOrEditValue; +import bleconfd.tc.com.bleconfd_example.models.ApiResponse; +import bleconfd.tc.com.bleconfd_example.models.INIFile; +import bleconfd.tc.com.bleconfd_example.models.INIGroup; +import bleconfd.tc.com.bleconfd_example.models.Task; + +/** + * ini file page + */ +public class INIFileFragment extends Fragment implements INIGroupItemAdapter.OnClickListener { + + + private ListView groupListView; + private String TAG = "INIFileFragment"; + + private Button getAllBtn; + private Button addGroupBtn; + private Button addValueBtn; + private List tasks; + private ProgressDialog progressDialog; + private BLELogger bleLogger; + private TextView logTextView; + private BLEPacket blePacket = new BLEPacket(); + + public INIFileFragment() { + tasks = new LinkedList<>(); + } + + /** + * create new ini file instance + * + * @param position the fragment position + * @return the home fragment + */ + public static Fragment newInstance(int position) { + Fragment fragment = new INIFileFragment(); + Bundle args = new Bundle(); + args.putInt(Const.ARG_PAGE_NUMBER, position); + fragment.setArguments(args); + return fragment; + } + + /** + * create new view + */ + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View rootView = inflater.inflate(R.layout.fragment_ini_file, container, false); + int i = getArguments().getInt(Const.ARG_PAGE_NUMBER); + String title = getResources().getStringArray(R.array.drawer_item_array)[i]; + getActivity().setTitle(title); + + getAllBtn = rootView.findViewById(R.id.get_all_btn); + addGroupBtn = rootView.findViewById(R.id.add_group_btn); + addValueBtn = rootView.findViewById(R.id.add_value_btn); + groupListView = rootView.findViewById(R.id.listview); + logTextView = rootView.findViewById(R.id.logTxt); + bleLogger = new BLELogger(logTextView, getActivity()); + + + // set empty list + INIFile iniFile = new INIFile(); + groupListView.setAdapter(new INIGroupItemAdapter(getContext(), iniFile.getGroups(), this)); + + // setup BLEManager + BLEManager.getInstance().setLogger(bleLogger); + BLEManager.getInstance().setBleListener(new BLEListener() { + @Override + public void onUpdate() { + updateUI(); + } + + @Override + public void onClose() { + updateUI(); + } + + @Override + public void onMessage(final JSONObject message) { + Log.d(TAG, "onMessage: " + message.toString()); + + // found task + Task callbackTask = null; + for (Task task : tasks) { + if (task.isSameTask(message)) { + callbackTask = task; + break; + } + } + + if (callbackTask != null) { + // remove it first + tasks.remove(callbackTask); + + final Task finalCallbackTask = callbackTask; + + // invoke callback + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + progressDialog.hide(); + ApiResponse response = new ApiResponse(message); + if (response.getCode() == 0) { + finalCallbackTask.getListener().onDone(response); + } else { + showToast(response.getError()); + } + } + }); + } + } + }); + + progressDialog = ProgressDialog.show(getActivity(), "", "", true); + progressDialog.hide(); + + this.initUI(); + return rootView; + } + + /** + * show toast message in UI thread + * + * @param message the message + */ + private void showToast(final String message) { + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(getContext(), message, Toast.LENGTH_LONG).show(); + } + }); + } + + /** + * init UI elements + */ + private void initUI() { + addGroupBtn.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + DialogAddOrEditGroup dialogAddOrEditGroup = new DialogAddOrEditGroup(); + dialogAddOrEditGroup.setArguments(new Bundle()); + dialogAddOrEditGroup.setmListener(new DialogAddOrEditGroup.Listener() { + @Override + public boolean onAddOrEditGroup(String originName, String newName) { + return addOrEditGroup(originName, newName); + } + }); + dialogAddOrEditGroup.show(getFragmentManager(), "addOrEditGroup"); + } + }); + + addValueBtn.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + DialogAddOrEditValue dialogAddOrEditValue = new DialogAddOrEditValue(); + dialogAddOrEditValue.setArguments(new Bundle()); + dialogAddOrEditValue.setmListener(new DialogAddOrEditValue.Listener() { + @Override + public boolean onAddOrEditValue(String group, String key, String value) { + return addOrEditValue(group, key, value); + } + }); + dialogAddOrEditValue.show(getFragmentManager(), "addOrEditValue"); + } + }); + + getAllBtn.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + fetchAllValue(); + } + }); + + this.updateUI(); + } + + /** + * update ui elements + */ + private void updateUI() { + final INIGroupItemAdapter.OnClickListener that = this; + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + boolean canSend = false; + String msg = ""; + + if (BLEManager.getInstance().isClosed()) { + msg = "BLE connection didn't create, please connect it in Home page"; + } else if (!BLEManager.getInstance().isFoundService()) { + msg = "BLE connected, but service not found."; + } else { + canSend = true; + } + + if (canSend) { + logTextView.setVisibility(View.GONE); + getAllBtn.setEnabled(true); + addGroupBtn.setEnabled(true); + addValueBtn.setEnabled(true); + } else { + logTextView.setVisibility(View.VISIBLE); + bleLogger.clear(); + bleLogger.debug(msg); + getAllBtn.setEnabled(false); + addGroupBtn.setEnabled(false); + addValueBtn.setEnabled(false); + groupListView.setAdapter(new INIGroupItemAdapter(getContext(), new LinkedList(), that)); + } + } + }); + } + + /** + * send request to BLE server + * + * @param method the rpc method + * @param params the method params + * @param listener the returned callback + */ + private boolean sendRequest(String method, Map params, Task.TaskListener listener) { + try { + int id = (int) (Math.random() * 100000000); + JSONObject object = new JSONObject(); + object.put("jsonrpc", "2.0"); + object.put("method", method); + object.put("id", id); + JSONObject paramsObj = new JSONObject(); + for (String key : params.keySet()) { + paramsObj.put(key, params.get(key)); + } + object.put("params", paramsObj); + + boolean writeResult = BLEManager.getInstance().write(blePacket.pack(object.toString())); + if (!writeResult) { // write failed + Toast.makeText(getContext(), "BLE reject this send, maybe it working ? please try again later!", Toast.LENGTH_SHORT).show(); + return false; + } + tasks.add(new Task(id, listener)); + + progressDialog.setTitle("Invoke BLE API"); + progressDialog.setMessage("invoke " + method + ", send and wait response from server..."); + progressDialog.show(); + } catch (JSONException e) { + e.printStackTrace(); + } + return true; + } + + /** + * fetch all values + */ + private void fetchAllValue() { + final INIGroupItemAdapter.OnClickListener listener = this; + + sendRequest( + "app-settings-get-all", + new HashMap(), new Task.TaskListener() { + @Override + public void onDone(ApiResponse response) { + try { + final INIFile iniFile = INIFile.fromJSON(response.getResult()); + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + groupListView.setAdapter(new INIGroupItemAdapter(getContext(), iniFile.getGroups(), listener)); + } + }); + } catch (JSONException e) { + e.printStackTrace(); + } + } + }); + } + + /** + * invoke add or edit group method + * + * @param originName the origin group name + * @param newName the new name + * @return result + */ + private boolean addOrEditGroup(String originName, String newName) { + Map params = new HashMap<>(); + String method = "app-settings-group-add"; + if (originName == null) { // new group + params.put("group", newName); + } else { + params.put("originGroup", originName); + params.put("newGroup", newName); + method = "app-settings-group-modify"; + } + return sendRequest(method, params, new Task.TaskListener() { + @Override + public void onDone(ApiResponse response) { + fetchAllValue(); + } + }); + } + + /** + * invoke add or edit value method + * + * @param groupName the group name + * @param key the key + * @param value the value + * @return the result + */ + private boolean addOrEditValue(String groupName, String key, String value) { + Map params = new HashMap<>(); + + String method = "app-settings-set"; + params.put("group", groupName); + params.put("name", key); + params.put("value", value); + + return sendRequest(method, params, new Task.TaskListener() { + @Override + public void onDone(ApiResponse response) { + fetchAllValue(); + } + }); + } + + /** + * popup delete confirm delete dialog + * + * @param msg the delete message + * @param onClickListener the callback when click YES + */ + private AlertDialog deleteConfirm(String msg, final DialogInterface.OnClickListener onClickListener) { + return new AlertDialog.Builder(getContext()) + .setTitle("Delete") + .setMessage(msg) + .setPositiveButton("Delete", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + dialog.dismiss(); + onClickListener.onClick(dialog, whichButton); + } + + }) + .setNegativeButton("cancel", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + } + }) + .create(); + } + + /** + * on delete group button click + * + * @param groupName the group name + */ + @Override + public void onGroupDeleteClick(final String groupName) { + deleteConfirm("Are you sure you want delete group " + groupName + " ?", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Map params = new HashMap<>(); + params.put("group", groupName); + sendRequest("app-settings-group-delete", params, new Task.TaskListener() { + @Override + public void onDone(ApiResponse response) { + fetchAllValue(); + } + }); + } + }).show(); + } + + /** + * on edit group name button click + * + * @param groupName the group name + */ + @Override + public void onGroupModifyClick(final String groupName) { + DialogAddOrEditGroup dialogAddOrEditGroup = new DialogAddOrEditGroup(); + Bundle bundle = new Bundle(); + bundle.putString("group", groupName); + dialogAddOrEditGroup.setArguments(bundle); + dialogAddOrEditGroup.setmListener(new DialogAddOrEditGroup.Listener() { + @Override + public boolean onAddOrEditGroup(String originName, String newName) { + return addOrEditGroup(originName, newName); + } + }); + dialogAddOrEditGroup.show(getFragmentManager(), "addOrEditGroup"); + } + + /** + * on value delete button click + * + * @param group the group name + * @param key the key name + */ + @Override + public void onValueDeleteClick(final String group, final String key) { + deleteConfirm("Are you sure you want delete this value (key=" + key + ") ?", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + Map params = new HashMap<>(); + params.put("group", group); + params.put("name", key); + sendRequest("app-settings-delete", params, new Task.TaskListener() { + @Override + public void onDone(ApiResponse response) { + fetchAllValue(); + } + }); + } + }).show(); + } + + /** + * on value modify click + * + * @param group the group name + * @param key the key name + * @param value the value + */ + @Override + public void onValueModifyClick(String group, String key, String value) { + DialogAddOrEditValue dialogAddOrEditValue = new DialogAddOrEditValue(); + Bundle bundle = new Bundle(); + bundle.putString("group", group); + bundle.putString("key", key); + bundle.putString("value", value); + dialogAddOrEditValue.setArguments(bundle); + + dialogAddOrEditValue.setmListener(new DialogAddOrEditValue.Listener() { + @Override + public boolean onAddOrEditValue(String group, String key, String value) { + return addOrEditValue(group, key, value); + } + }); + dialogAddOrEditValue.show(getFragmentManager(), "addOrEditValue"); + } +} diff --git a/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/fragments/WifiFragment.java b/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/fragments/WifiFragment.java new file mode 100644 index 0000000..1e1d70e --- /dev/null +++ b/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/fragments/WifiFragment.java @@ -0,0 +1,169 @@ +package bleconfd.tc.com.bleconfd_example.fragments; + +import android.app.DialogFragment; +import android.app.Fragment; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.TextView; + +import org.json.JSONException; +import org.json.JSONObject; + +import bleconfd.tc.com.bleconfd_example.BLE.BLEListener; +import bleconfd.tc.com.bleconfd_example.BLE.BLELogger; +import bleconfd.tc.com.bleconfd_example.BLE.BLEManager; +import bleconfd.tc.com.bleconfd_example.BLE.BLEPacket; +import bleconfd.tc.com.bleconfd_example.Const; +import bleconfd.tc.com.bleconfd_example.R; +import bleconfd.tc.com.bleconfd_example.dialogs.DialogSetWifi; + +/** + * set wifi page + */ +public class WifiFragment extends Fragment { + + private Button setWifiButton = null; + private TextView logTxt = null; + private BLELogger bleLogger = null; + + public WifiFragment() { + } + + /** + * create new set wifi page + * + * @param position the drawer position + */ + public static Fragment newInstance(int position) { + Fragment fragment = new WifiFragment(); + Bundle args = new Bundle(); + args.putInt(Const.ARG_PAGE_NUMBER, position); + fragment.setArguments(args); + return fragment; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View rootView = inflater.inflate(R.layout.fragment_wifi, container, false); + int i = getArguments().getInt(Const.ARG_PAGE_NUMBER); + String title = getResources().getStringArray(R.array.drawer_item_array)[i]; + getActivity().setTitle(title); + + setWifiButton = rootView.findViewById(R.id.setWifiBtn); + logTxt = rootView.findViewById(R.id.logTxt); + + bleLogger = new BLELogger(logTxt, getActivity()); + BLEManager.getInstance().setLogger(bleLogger); + BLEManager.getInstance().setBleListener(new BLEListener() { + @Override + public void onUpdate() { + updateUI(); + } + + @Override + public void onClose() { + updateUI(); + } + + @Override + public void onMessage(JSONObject message) { + bleLogger.debug(message.toString()); + } + }); + + this.initUI(); + return rootView; + } + + /** + * init set wifi UI + */ + private void initUI() { + // set wifi button click event + setWifiButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + DialogSetWifi dialog = new DialogSetWifi(); + dialog.setmListener(dialogListener); + dialog.show(getFragmentManager(), "SetWifiDialog"); + } + }); + this.updateUI(); + } + + /** + * update wifi page ui + */ + private void updateUI() { + getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + if (BLEManager.getInstance().isClosed()) { + bleLogger.debug("BLE connection didn't create, please connect it in Home page"); + setWifiButton.setEnabled(false); + } else if (!BLEManager.getInstance().isFoundService()) { + bleLogger.debug("BLE connected, but service not found."); + setWifiButton.setEnabled(false); + } else { + setWifiButton.setEnabled(true); + } + } + }); + } + + + /** + * create set wifi json body + * + * @param ssid the ssid + * @param password the password + * @return + * @throws JSONException + */ + private byte[] connectWifiBody(String ssid, String password) throws JSONException { + JSONObject wifiObj = new JSONObject(); + wifiObj.put("jsonrpc", "2.0"); + wifiObj.put("method", "wifi-connect"); + wifiObj.put("id", (int) (Math.random() * 100)); + wifiObj.put("wi-fi_tech", "infra"); + + JSONObject disco = new JSONObject(); + disco.put("ssid", ssid); + wifiObj.put("discovery", disco); + + JSONObject creds = new JSONObject(); + creds.put("akm", "psk"); + creds.put("pass", password); + wifiObj.put("cred", creds); + + BLEPacket blePacket = new BLEPacket(); + return blePacket.pack(wifiObj.toString()); + } + + /** + * wifi click connect listener + */ + private DialogSetWifi.DialogListener dialogListener = new DialogSetWifi.DialogListener() { + @Override + public boolean onConnect(DialogFragment dialog, String ssid, String password) { + if (BLEManager.getInstance().isClosed() || !BLEManager.getInstance().isFoundService()) { + return false; + } + + try { + boolean ret = BLEManager.getInstance().write(connectWifiBody(ssid, password)); + if (ret) { + bleLogger.debug("rpc(wifi-connect) invoked, waiting response..."); + } + return ret; + } catch (JSONException e) { + e.printStackTrace(); + return false; + } + } + }; +} diff --git a/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/models/ApiResponse.java b/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/models/ApiResponse.java new file mode 100644 index 0000000..157f908 --- /dev/null +++ b/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/models/ApiResponse.java @@ -0,0 +1,46 @@ +package bleconfd.tc.com.bleconfd_example.models; + +import org.json.JSONException; +import org.json.JSONObject; + +/** + * the rpc api response + */ +public class ApiResponse { + + private int id = -1; + private int code = -1; + private String error = "parse response failed"; + private JSONObject result; + + public ApiResponse(JSONObject res) { + + try { + id = res.getInt("id"); + code = res.getInt("code"); + if (code != 0) { + error = res.getJSONObject("error").getString("message"); + } else { + result = res.getJSONObject("result"); + } + } catch (JSONException e) { + e.printStackTrace(); + } + } + + public int getId() { + return id; + } + + public int getCode() { + return code; + } + + public String getError() { + return error; + } + + public JSONObject getResult() { + return result; + } +} diff --git a/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/models/INIFile.java b/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/models/INIFile.java new file mode 100644 index 0000000..164372d --- /dev/null +++ b/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/models/INIFile.java @@ -0,0 +1,63 @@ +package bleconfd.tc.com.bleconfd_example.models; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +/** + * ini file obj + */ +public class INIFile { + + + private List groups = new LinkedList<>(); + + + public List getGroups() { + return groups; + } + + public void setGroups(List groups) { + this.groups = groups; + } + + /** + * create ini file obj from json object + * + * @param jsonObject the json object from rpc server + * @return + * @throws JSONException if error happened + */ + public static INIFile fromJSON(JSONObject jsonObject) throws JSONException { + List iniGroups = new ArrayList<>(); + + Iterator groupIt = jsonObject.keys(); + while (groupIt.hasNext()) { + String groupName = groupIt.next(); + JSONObject keyValuesObj = jsonObject.getJSONObject(groupName); + Iterator keyIt = keyValuesObj.keys(); + + INIGroup iniGroup = new INIGroup(); + iniGroup.setName(groupName); + List keyValues = new ArrayList<>(); + + while (keyIt.hasNext()) { + String key = keyIt.next(); + INIKeyValue iniKeyValue = new INIKeyValue(); + iniKeyValue.setKey(key); + iniKeyValue.setValue(keyValuesObj.getString(key)); + keyValues.add(iniKeyValue); + } + iniGroup.setKeyValues(keyValues); + iniGroups.add(iniGroup); + } + + INIFile iniFile = new INIFile(); + iniFile.setGroups(iniGroups); + return iniFile; + } +} diff --git a/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/models/INIGroup.java b/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/models/INIGroup.java new file mode 100644 index 0000000..36abb44 --- /dev/null +++ b/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/models/INIGroup.java @@ -0,0 +1,27 @@ +package bleconfd.tc.com.bleconfd_example.models; + +import java.util.List; + +/** + * INI file group object + */ +public class INIGroup { + private String name; + private List keyValues; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getKeyValues() { + return keyValues; + } + + public void setKeyValues(List keyValues) { + this.keyValues = keyValues; + } +} diff --git a/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/models/INIKeyValue.java b/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/models/INIKeyValue.java new file mode 100644 index 0000000..128d7ff --- /dev/null +++ b/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/models/INIKeyValue.java @@ -0,0 +1,25 @@ +package bleconfd.tc.com.bleconfd_example.models; + +/** + * INI file key-value object + */ +public class INIKeyValue { + private String key; + private String value; + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/models/Task.java b/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/models/Task.java new file mode 100644 index 0000000..4a13bed --- /dev/null +++ b/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/models/Task.java @@ -0,0 +1,57 @@ +package bleconfd.tc.com.bleconfd_example.models; + +import org.json.JSONObject; + +/** + * rpc request task + */ +public class Task { + + /** + * task onDone callback + */ + public interface TaskListener { + void onDone(ApiResponse response); + } + + private int requestId; + private TaskListener listener; + + /** + * create new task + * + * @param requestId the request id + * @param listener the callback listener + */ + public Task(int requestId, TaskListener listener) { + this.requestId = requestId; + this.listener = listener; + } + + /** + * check returned response the belongs to this task + * + * @param object the json object from BLE server + * @return the result + */ + public boolean isSameTask(JSONObject object) { + try { + if (object.getInt("id") == requestId) { + return true; + } + } catch (Exception e) { + return false; + } + return false; + } + + public TaskListener getListener() { + return listener; + } + + public void setListener(TaskListener listener) { + this.listener = listener; + } + + +} diff --git a/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/ui/INIKeyValueListView.java b/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/ui/INIKeyValueListView.java new file mode 100644 index 0000000..660f77f --- /dev/null +++ b/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/ui/INIKeyValueListView.java @@ -0,0 +1,31 @@ +package bleconfd.tc.com.bleconfd_example.ui; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.ListView; + +/** + * INI key value list view + * this listView will auto set height to show all items + */ +public class INIKeyValueListView extends ListView { + + public INIKeyValueListView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public INIKeyValueListView(Context context) { + super(context); + } + + public INIKeyValueListView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, + MeasureSpec.AT_MOST); + super.onMeasure(widthMeasureSpec, expandSpec); + } +} \ No newline at end of file diff --git a/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/ui/TouchEffect.java b/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/ui/TouchEffect.java new file mode 100644 index 0000000..94f9060 --- /dev/null +++ b/AndroidClientApp/app/src/main/java/bleconfd/tc/com/bleconfd_example/ui/TouchEffect.java @@ -0,0 +1,20 @@ +package bleconfd.tc.com.bleconfd_example.ui; + +import android.view.MotionEvent; +import android.view.View; + +/** + * add touch effect for image button + */ +public class TouchEffect implements View.OnTouchListener { + + @Override + public boolean onTouch(View v, MotionEvent event) { + if (event.getAction() == MotionEvent.ACTION_DOWN) { + v.setAlpha(.5f); + } else if (event.getAction() == MotionEvent.ACTION_UP) { + v.setAlpha(1f); + } + return false; + } +} diff --git a/AndroidClientApp/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/AndroidClientApp/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..1f6bb29 --- /dev/null +++ b/AndroidClientApp/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/AndroidClientApp/app/src/main/res/drawable-xxhdpi/drawer_shadow.9.png b/AndroidClientApp/app/src/main/res/drawable-xxhdpi/drawer_shadow.9.png new file mode 100644 index 0000000..b91e9d7 Binary files /dev/null and b/AndroidClientApp/app/src/main/res/drawable-xxhdpi/drawer_shadow.9.png differ diff --git a/AndroidClientApp/app/src/main/res/drawable-xxhdpi/ic_drawer.png b/AndroidClientApp/app/src/main/res/drawable-xxhdpi/ic_drawer.png new file mode 100644 index 0000000..9c4685d Binary files /dev/null and b/AndroidClientApp/app/src/main/res/drawable-xxhdpi/ic_drawer.png differ diff --git a/AndroidClientApp/app/src/main/res/drawable/ic_launcher_background.xml b/AndroidClientApp/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..0d025f9 --- /dev/null +++ b/AndroidClientApp/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AndroidClientApp/app/src/main/res/layout/activity_main.xml b/AndroidClientApp/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..c81d561 --- /dev/null +++ b/AndroidClientApp/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + +