diff --git a/.idea/dictionaries/t.xml b/.idea/dictionaries/t.xml index c82ca81bd..18f9cb697 100644 --- a/.idea/dictionaries/t.xml +++ b/.idea/dictionaries/t.xml @@ -53,6 +53,7 @@ gideão girolamo gobbetti + gree greenberg greenrobot greffier diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 2b33bfa43..9f572d6e3 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -72,6 +72,7 @@ -keepclassmembers,allowobfuscation class * { @com.google.gson.annotations.SerializedName ; } +-keep class nodomain.freeyourgadget.gadgetbridge.service.devices.gree.messages.** {*; } # Somehow the rule above was not enough for some -keep class nodomain.freeyourgadget.gadgetbridge.devices.pinetime.InfiniTimeDFU* { *; } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9534a737d..75bd18f1b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -647,6 +647,10 @@ android:name=".devices.evenrealities.G1PairingActivity" android:label="@string/title_activity_even_realities_g1_pairing" android:parentActivityName=".activities.discovery.DiscoveryActivityV2" /> + diff --git a/app/src/main/assets/ic_device_air_conditioning.svg b/app/src/main/assets/ic_device_air_conditioning.svg new file mode 100644 index 000000000..061a88062 --- /dev/null +++ b/app/src/main/assets/ic_device_air_conditioning.svg @@ -0,0 +1,124 @@ + + + +image/svg+xml diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/gree/GreeAcCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/gree/GreeAcCoordinator.java new file mode 100644 index 000000000..c310a7104 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/gree/GreeAcCoordinator.java @@ -0,0 +1,72 @@ +package nodomain.freeyourgadget.gadgetbridge.devices.gree; + +import android.app.Activity; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.regex.Pattern; + +import nodomain.freeyourgadget.gadgetbridge.GBException; +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLEDeviceCoordinator; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.Device; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.gree.GreeAcSupport; + +public class GreeAcCoordinator extends AbstractBLEDeviceCoordinator { + @Override + protected void deleteDevice(@NonNull final GBDevice gbDevice, @NonNull final Device device, @NonNull final DaoSession session) throws GBException { + } + + @Override + protected Pattern getSupportedDeviceName() { + // GR-AC_10001_09_xxxx_SC + return Pattern.compile("^GR-AC_\\d{5}_\\d{2}_[0-9a-f]{4}_SC$"); + } + + @Override + public String getManufacturer() { + return "Gree"; + } + + @NonNull + @Override + public Class getDeviceSupportClass() { + return GreeAcSupport.class; + } + + @Override + public int getBondingStyle() { + return BONDING_STYLE_NONE; + } + + @Override + public int getDeviceNameResource() { + return R.string.devicetype_gree_ac; + } + + @Override + public int getDefaultIconResource() { + return R.drawable.ic_device_air_conditioning; + } + + @Override + public int getDisabledIconResource() { + return R.drawable.ic_device_air_conditioning_disabled; + } + + @Override + public boolean suggestUnbindBeforePair() { + // shouldn't matter + return false; + } + + @Nullable + @Override + public Class getPairingActivity() { + return GreeAcPairingActivity.class; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/gree/GreeAcPairingActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/gree/GreeAcPairingActivity.java new file mode 100644 index 000000000..d65439c2a --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/gree/GreeAcPairingActivity.java @@ -0,0 +1,265 @@ +package nodomain.freeyourgadget.gadgetbridge.devices.gree; + +import android.content.BroadcastReceiver; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.View; +import android.widget.Button; +import android.widget.ProgressBar; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.appcompat.app.ActionBar; +import androidx.core.content.ContextCompat; + +import com.google.android.material.textfield.TextInputEditText; +import com.google.android.material.textfield.TextInputLayout; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity; +import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate; +import nodomain.freeyourgadget.gadgetbridge.service.devices.gree.GreeAcPrefs; +import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper; +import nodomain.freeyourgadget.gadgetbridge.util.GB; +import nodomain.freeyourgadget.gadgetbridge.util.preferences.DevicePrefs; + +public class GreeAcPairingActivity extends AbstractGBActivity { + private static final Logger LOG = LoggerFactory.getLogger(GreeAcPairingActivity.class); + + public static final String ACTION_BIND_STATUS = "nodomain.freeyourgadget.gadgetbridge.gree.bind_status"; + public static final String EXTRA_BIND_KEY = "extra_bind_key"; + public static final String EXTRA_BIND_MESSAGE = "extra_bind_message"; + + private GBDeviceCandidate deviceCandidate; + + private TextInputLayout textLayoutSsid; + private TextInputLayout textLayoutPassword; + private TextInputLayout textLayoutHost; + private TextInputEditText editTextSsid; + private TextInputEditText editTextPassword; + private TextInputEditText editTextHost; + private ProgressBar progressBar; + private TextView pairResultTextView; + private Button buttonPair; + private Button buttonCopy; + + private GBDevice gbDevice; + private String bindKey; + + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(final Context context, final Intent intent) { + final String action = intent.getAction(); + if (action == null) { + return; + } + + if (ACTION_BIND_STATUS.equals(action)) { + final String bindMessage = intent.getStringExtra(EXTRA_BIND_MESSAGE); + bindKey = intent.getStringExtra(EXTRA_BIND_KEY); + + progressBar.setVisibility(View.GONE); + if ("1".equals(bindMessage)) { + pairResultTextView.setText(getString(R.string.gree_pair_status_success, String.valueOf(bindKey))); + buttonCopy.setVisibility(View.VISIBLE); + } else { + pairResultTextView.setText(getString(R.string.gree_pair_status_failure, bindMessage)); + } + + pairResultTextView.setVisibility(View.VISIBLE); + return; + } + + if (GBDevice.ACTION_DEVICE_CHANGED.equals(action)) { + final GBDevice actionDevice = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE); + if (actionDevice == null || !actionDevice.getAddress().equals(deviceCandidate.getMacAddress())) { + return; + } + + LOG.debug("Got device state: {}", actionDevice.getState()); + + if (actionDevice.getState() == GBDevice.State.NOT_CONNECTED && buttonCopy.getVisibility() != View.VISIBLE) { + pairResultTextView.setText(getString(R.string.gree_pair_status_failure, actionDevice.getState().toString())); + pairResultTextView.setVisibility(View.VISIBLE); + + editTextSsid.setEnabled(true); + editTextPassword.setEnabled(true); + editTextHost.setEnabled(true); + buttonPair.setVisibility(View.VISIBLE); + progressBar.setVisibility(View.GONE); + } + } + + LOG.error("Unknown action {}", action); + } + }; + + @Override + protected void onCreate(final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + final Intent intent = getIntent(); + deviceCandidate = intent.getParcelableExtra(DeviceCoordinator.EXTRA_DEVICE_CANDIDATE); + + if (deviceCandidate == null) { + GB.toast(this, "Device candidate missing", Toast.LENGTH_LONG, GB.ERROR); + finish(); + return; + } + + setContentView(R.layout.activity_gree_ac_pairing); + + final IntentFilter filter = new IntentFilter(); + filter.addAction(ACTION_BIND_STATUS); + filter.addAction(GBDevice.ACTION_DEVICE_CHANGED); + ContextCompat.registerReceiver(this, mReceiver, filter, ContextCompat.RECEIVER_NOT_EXPORTED); + + final TextView textPairInfo = findViewById(R.id.gree_pair_info); + textLayoutSsid = findViewById(R.id.gree_pair_ssid_layout); + textLayoutPassword = findViewById(R.id.gree_pair_password_layout); + textLayoutHost = findViewById(R.id.gree_pair_host_layout); + editTextSsid = findViewById(R.id.gree_pair_ssid_text); + editTextPassword = findViewById(R.id.gree_pair_password_text); + editTextHost = findViewById(R.id.gree_pair_host_text); + progressBar = findViewById(R.id.gree_pair_progress_bar); + pairResultTextView = findViewById(R.id.gree_pair_result); + buttonPair = findViewById(R.id.gree_button_pair); + buttonCopy = findViewById(R.id.gree_button_copy); + + textPairInfo.setText(getString(R.string.gree_pair_info, deviceCandidate.getName(), deviceCandidate.getMacAddress())); + + final DevicePrefs devicePrefs = new DevicePrefs(GBApplication.getDeviceSpecificSharedPrefs(deviceCandidate.getMacAddress()), gbDevice); + editTextSsid.setText(devicePrefs.getString(GreeAcPrefs.PREF_SSID, "")); + editTextPassword.setText(devicePrefs.getString(GreeAcPrefs.PREF_PASSWORD, "")); + editTextHost.setText(devicePrefs.getString(GreeAcPrefs.PREF_HOST, "")); + + editTextSsid.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(final CharSequence s, final int start, final int count, final int after) { + } + + @Override + public void onTextChanged(final CharSequence s, final int start, final int before, final int count) { + } + + @Override + public void afterTextChanged(final Editable s) { + textLayoutSsid.setError(null); + } + }); + editTextPassword.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(final CharSequence s, final int start, final int count, final int after) { + } + + @Override + public void onTextChanged(final CharSequence s, final int start, final int before, final int count) { + } + + @Override + public void afterTextChanged(final Editable s) { + textLayoutPassword.setError(null); + } + }); + editTextHost.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(final CharSequence s, final int start, final int count, final int after) { + } + + @Override + public void onTextChanged(final CharSequence s, final int start, final int before, final int count) { + } + + @Override + public void afterTextChanged(final Editable s) { + textLayoutHost.setError(null); + } + }); + + buttonPair.setOnClickListener(v -> { + savePrefs(); + + final String ssid = editTextSsid.getText() != null ? editTextSsid.getText().toString() : ""; + if (ssid.isEmpty() || ssid.length() > 32) { + textLayoutSsid.setError("Invalid SSID"); + return; + } + final String password = editTextPassword.getText() != null ? editTextPassword.getText().toString() : ""; + if (password.isEmpty() || password.length() < 8 || password.length() > 63) { + textLayoutPassword.setError("Invalid password"); + return; + } + final String host = editTextHost.getText() != null ? editTextHost.getText().toString() : ""; + if (host.isEmpty()) { + textLayoutHost.setError("Invalid host"); + return; + } + + editTextSsid.setEnabled(false); + editTextPassword.setEnabled(false); + editTextHost.setEnabled(false); + buttonPair.setVisibility(View.GONE); + progressBar.setVisibility(View.VISIBLE); + pairResultTextView.setVisibility(View.GONE); + + GBApplication.deviceService().disconnect(); + gbDevice = DeviceHelper.getInstance().toSupportedDevice(deviceCandidate.getDevice()); + GBApplication.deviceService(gbDevice).connect(true); + }); + + buttonCopy.setOnClickListener(v -> { + final ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); + final String clipboardData = getString( + R.string.gree_pair_clipboard, + deviceCandidate.getName(), + deviceCandidate.getMacAddress(), + bindKey + ); + final ClipData clip = ClipData.newPlainText(deviceCandidate.getName(), clipboardData); + clipboard.setPrimaryClip(clip); + GB.toast(getString(R.string.copied_to_clipboard), Toast.LENGTH_LONG, GB.INFO); + }); + + final ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setTitle(deviceCandidate.getName()); + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (gbDevice != null) { + GBApplication.deviceService(gbDevice).disconnect(); + } + unregisterReceiver(mReceiver); + } + + private void savePrefs() { + final DevicePrefs devicePrefs = new DevicePrefs(GBApplication.getDeviceSpecificSharedPrefs(deviceCandidate.getMacAddress()), gbDevice); + final SharedPreferences.Editor editor = devicePrefs.getPreferences().edit(); + if (editTextSsid != null && editTextSsid.getText() != null) { + editor.putString(GreeAcPrefs.PREF_SSID, editTextSsid.getText().toString()); + } + if (editTextPassword != null && editTextPassword.getText() != null) { + editor.putString(GreeAcPrefs.PREF_PASSWORD, editTextPassword.getText().toString()); + } + if (editTextHost != null && editTextHost.getText() != null) { + editor.putString(GreeAcPrefs.PREF_HOST, editTextHost.getText().toString()); + } + editor.apply(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java index 56f77036d..fd7807a5a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java @@ -120,6 +120,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.vivomove.Garm import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.vivomove.GarminVivomoveTrendCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.vivosmart.GarminVivosmart5Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.vivosport.GarminVivosportCoordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.gree.GreeAcCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.hama.fit6900.HamaFit6900DeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.hplus.EXRIZUK8Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.hplus.HPlusCoordinator; @@ -514,6 +515,7 @@ public enum DeviceType { GARMIN_VIVOACTIVE_5(GarminVivoActive5Coordinator.class), GARMIN_VIVOSMART_5(GarminVivosmart5Coordinator.class), GARMIN_VIVOSPORT(GarminVivosportCoordinator.class), + GREE_AC(GreeAcCoordinator.class), VIBRATISSIMO(VibratissimoCoordinator.class), SONY_SWR12(SonySWR12DeviceCoordinator.class), LIVEVIEW(LiveviewCoordinator.class), diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/gree/GreeAcPrefs.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/gree/GreeAcPrefs.java new file mode 100644 index 000000000..07001a1c2 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/gree/GreeAcPrefs.java @@ -0,0 +1,7 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.gree; + +public class GreeAcPrefs { + public static final String PREF_SSID = "gree_ssid"; + public static final String PREF_PASSWORD = "gree_password"; + public static final String PREF_HOST = "gree_host"; +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/gree/GreeAcSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/gree/GreeAcSupport.java new file mode 100644 index 000000000..e6d2d51d3 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/gree/GreeAcSupport.java @@ -0,0 +1,231 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.gree; + +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; +import android.content.Intent; +import android.util.Base64; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.util.Locale; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.BuildConfig; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences; +import nodomain.freeyourgadget.gadgetbridge.devices.gree.GreeAcPairingActivity; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction; +import nodomain.freeyourgadget.gadgetbridge.service.devices.gree.messages.AbstractGreeMessage; +import nodomain.freeyourgadget.gadgetbridge.service.devices.gree.messages.GreeBindMessage; +import nodomain.freeyourgadget.gadgetbridge.service.devices.gree.messages.GreeBleInfoMessage; +import nodomain.freeyourgadget.gadgetbridge.service.devices.gree.messages.GreeBleKeyMessage; +import nodomain.freeyourgadget.gadgetbridge.service.devices.gree.messages.GreePackMessage; +import nodomain.freeyourgadget.gadgetbridge.service.devices.gree.messages.GreeWlanMessage; +import nodomain.freeyourgadget.gadgetbridge.util.CryptoUtils; +import nodomain.freeyourgadget.gadgetbridge.util.GB; +import nodomain.freeyourgadget.gadgetbridge.util.preferences.DevicePrefs; + +public class GreeAcSupport extends AbstractBTLEDeviceSupport { + private static final Logger LOG = LoggerFactory.getLogger(GreeAcSupport.class); + + public static final UUID UUID_SERVICE_GREE_PACK = UUID.fromString("0000fd06-173c-93d2-488e-fe144d2e12a2"); + public static final UUID UUID_CHARACTERISTIC_PACK_TX = UUID.fromString("0000fd03-0000-1000-8000-00805f9b34fb"); + public static final UUID UUID_CHARACTERISTIC_PACK_RX = UUID.fromString("0000fd04-0000-1000-8000-00805f9b34fb"); + + public static final byte[] DEFAULT_KEY = "a3K8Bx%2r8Y7#xDh".getBytes(); + + private byte[] bindKey = null; + + private BluetoothGattCharacteristic characteristicTx; + + public GreeAcSupport() { + super(LOG); + addSupportedService(UUID_SERVICE_GREE_PACK); + } + + @Override + public boolean useAutoConnect() { + return false; + } + + @Override + protected TransactionBuilder initializeDevice(final TransactionBuilder builder) { + characteristicTx = getCharacteristic(UUID_CHARACTERISTIC_PACK_TX); + final BluetoothGattCharacteristic characteristicRx = getCharacteristic(UUID_CHARACTERISTIC_PACK_RX); + + if (characteristicTx == null || characteristicRx == null) { + LOG.warn("Pack characteristics are null"); + builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.NOT_CONNECTED, getContext())); + return builder; + } + + characteristicTx.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT); + + builder.notify(getCharacteristic(UUID_CHARACTERISTIC_PACK_RX), true); + + final String mac = getDevice().getAddress().trim().replace(":", "").toLowerCase(Locale.ROOT); + writeMessage(builder, new GreeBindMessage(mac.substring(mac.length() - 4))); + + return builder; + } + + @Override + public boolean onCharacteristicChanged(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) { + if (super.onCharacteristicChanged(gatt, characteristic)) { + return true; + } + + final UUID characteristicUUID = characteristic.getUuid(); + final byte[] value = characteristic.getValue(); + + if (UUID_CHARACTERISTIC_PACK_RX.equals(characteristicUUID)) { + final String packMessageJson = new String(value, StandardCharsets.UTF_8); + LOG.debug("Got pack: {}", packMessageJson); + + final AbstractGreeMessage message; + try { + message = AbstractGreeMessage.fromJson(packMessageJson); + } catch (final Exception e) { + LOG.error("Failed to deserialize message from json", e); + return true; + } + + handleMessage(message); + + return true; + } + + LOG.warn("Unknown characteristic {} changed: {}", characteristicUUID, GB.hexdump(value)); + + return false; + } + + private void handleMessage(final AbstractGreeMessage message) { + if (message instanceof GreePackMessage) { + final GreePackMessage packMessage = (GreePackMessage) message; + final int encryptionKeyNum = packMessage.getEncryptionKey(); + + final byte[] key; + switch (encryptionKeyNum) { + case GreePackMessage.KEY_BIND: + key = bindKey; + break; + case GreePackMessage.KEY_DEFAULT: + key = DEFAULT_KEY; + break; + default: + LOG.warn("Unknown pack message encryption key {}", encryptionKeyNum); + return; + } + + if (key == null) { + LOG.error("Key {} is not known", encryptionKeyNum); + return; + } + + final byte[] encryptedBytes = Base64.decode(packMessage.getPack(), Base64.DEFAULT); + final byte[] decryptedBytes; + try { + decryptedBytes = CryptoUtils.decryptAES_ECB_Pad(encryptedBytes, key); + } catch (final GeneralSecurityException e) { + LOG.error("Failed to decrypt pack", e); + return; + } + + final AbstractGreeMessage packSubMessage; + try { + final String subMessageJson = new String(decryptedBytes, StandardCharsets.UTF_8); + LOG.debug("Pack sub message: {}", subMessageJson); + + packSubMessage = AbstractGreeMessage.fromJson(subMessageJson); + } catch (final Exception e) { + LOG.error("Failed to deserialize pack sub message from json", e); + return; + } + + handleMessage(packSubMessage); + return; + } + + if (message instanceof GreeBleKeyMessage) { + final GreeBleKeyMessage bleKeyMessage = (GreeBleKeyMessage) message; + + LOG.debug("Got bind key: {}", bleKeyMessage.getKey()); + bindKey = bleKeyMessage.getKey().getBytes(StandardCharsets.UTF_8); + + evaluateGBDeviceEvent(new GBDeviceEventUpdatePreferences("authkey", bleKeyMessage.getKey())); + + final DevicePrefs devicePrefs = getDevicePrefs(); + final String host = devicePrefs.getString(GreeAcPrefs.PREF_HOST, ""); + final String psw = devicePrefs.getString(GreeAcPrefs.PREF_PASSWORD, ""); + final String ssid = devicePrefs.getString(GreeAcPrefs.PREF_SSID, ""); + if (host.isEmpty() || psw.isEmpty() || ssid.isEmpty()) { + final Intent intent = new Intent(GreeAcPairingActivity.ACTION_BIND_STATUS); + intent.setPackage(BuildConfig.APPLICATION_ID); + intent.putExtra(GreeAcPairingActivity.EXTRA_BIND_MESSAGE, "missing wlan params"); + getContext().sendBroadcast(intent); + return; + } + + final TransactionBuilder builder = createTransactionBuilder("setup wifi"); + writeMessage(builder, new GreeWlanMessage(host, psw, ssid, 1)); + builder.queue(getQueue()); + + return; + } + + if (message instanceof GreeBleInfoMessage) { + final GreeBleInfoMessage bleInfoMessage = (GreeBleInfoMessage) message; + + LOG.debug("Got ble info, wificon = {}", bleInfoMessage.getWificon()); + + final Intent intent = new Intent(GreeAcPairingActivity.ACTION_BIND_STATUS); + intent.setPackage(BuildConfig.APPLICATION_ID); + intent.putExtra(GreeAcPairingActivity.EXTRA_BIND_KEY, new String(bindKey)); + intent.putExtra(GreeAcPairingActivity.EXTRA_BIND_MESSAGE, String.valueOf(bleInfoMessage.getWificon())); + getContext().sendBroadcast(intent); + + // TODO disconnect - not much else we can do + } + + LOG.warn("Unhandled message: {}", message); + } + + private void writeMessage(final TransactionBuilder builder, final AbstractGreeMessage message) { + final String messageJson = message.toString(); + + LOG.debug("Will send: {}", messageJson); + + if (message instanceof GreePackMessage) { + builder.write(characteristicTx, messageJson.getBytes(StandardCharsets.UTF_8)); + return; + } + + final int key; + final byte[] encryptedBytes; + try { + if (message instanceof GreeBindMessage) { + key = GreePackMessage.KEY_DEFAULT; + encryptedBytes = CryptoUtils.encryptAES_ECB_Pad(messageJson.getBytes(StandardCharsets.UTF_8), DEFAULT_KEY); + } else { + if (bindKey == null) { + LOG.error("No bind key, unable to encrypt"); + return; + } + key = GreePackMessage.KEY_BIND; + encryptedBytes = CryptoUtils.encryptAES_ECB_Pad(messageJson.getBytes(StandardCharsets.UTF_8), bindKey); + } + } catch (final GeneralSecurityException e) { + LOG.error("Failed to encrypt message", e); + return; + } + + final GreePackMessage packMessage = new GreePackMessage(Base64.encodeToString(encryptedBytes, Base64.DEFAULT).replace("\n", "").trim(), key); + writeMessage(builder, packMessage); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/gree/messages/AbstractGreeMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/gree/messages/AbstractGreeMessage.java new file mode 100644 index 000000000..3443cf087 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/gree/messages/AbstractGreeMessage.java @@ -0,0 +1,37 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.gree.messages; + +import androidx.annotation.NonNull; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.typeadapters.RuntimeTypeAdapterFactory; + +public abstract class AbstractGreeMessage { + private static final Gson GSON = new GsonBuilder() + .registerTypeAdapterFactory(getTypeAdapterFactory()) + .serializeNulls() + .disableHtmlEscaping() + .create(); + + @NonNull + @Override + public String toString() { + return GSON.toJson(this); + } + + public static TypeAdapterFactory getTypeAdapterFactory() { + return RuntimeTypeAdapterFactory + .of(AbstractGreeMessage.class, "t") + .registerSubtype(GreePackMessage.class, GreePackMessage.TYPE) + .registerSubtype(GreeBindMessage.class, GreeBindMessage.TYPE) + .registerSubtype(GreeBleInfoMessage.class, GreeBleInfoMessage.TYPE) + .registerSubtype(GreeBleKeyMessage.class, GreeBleKeyMessage.TYPE) + .registerSubtype(GreeWlanMessage.class, GreeWlanMessage.TYPE) + .recognizeSubtypes(); + } + + public static AbstractGreeMessage fromJson(final String json) { + return GSON.fromJson(json, AbstractGreeMessage.class); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/gree/messages/GreeBindMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/gree/messages/GreeBindMessage.java new file mode 100644 index 000000000..f108ad28d --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/gree/messages/GreeBindMessage.java @@ -0,0 +1,13 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.gree.messages; + +public class GreeBindMessage extends AbstractGreeMessage { + public static final String TYPE = "bind"; + + private final String mac; + private final int IsCP; + + public GreeBindMessage(final String mac) { + this.mac = mac; + this.IsCP = 1; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/gree/messages/GreeBleInfoMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/gree/messages/GreeBleInfoMessage.java new file mode 100644 index 000000000..57571141a --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/gree/messages/GreeBleInfoMessage.java @@ -0,0 +1,33 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.gree.messages; + +public class GreeBleInfoMessage extends AbstractGreeMessage { + public static final String TYPE = "bleinfo"; + + private final int wificon; + private final String mac; + private final String mid; + private final String ver; + + public GreeBleInfoMessage(final int wificon, final String mac, final String mid, final String ver) { + this.wificon = wificon; + this.mac = mac; + this.mid = mid; + this.ver = ver; + } + + public int getWificon() { + return wificon; + } + + public String getMac() { + return mac; + } + + public String getMid() { + return mid; + } + + public String getVer() { + return ver; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/gree/messages/GreeBleKeyMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/gree/messages/GreeBleKeyMessage.java new file mode 100644 index 000000000..0d8fa8b5c --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/gree/messages/GreeBleKeyMessage.java @@ -0,0 +1,15 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.gree.messages; + +public class GreeBleKeyMessage extends AbstractGreeMessage { + public static final String TYPE = "blekey"; + + private final String key; + + public GreeBleKeyMessage(final String key) { + this.key = key; + } + + public String getKey() { + return key; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/gree/messages/GreePackMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/gree/messages/GreePackMessage.java new file mode 100644 index 000000000..85317adc1 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/gree/messages/GreePackMessage.java @@ -0,0 +1,26 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.gree.messages; + +public class GreePackMessage extends AbstractGreeMessage { + public static final String TYPE = "pack"; + + public static final int KEY_DEFAULT = 1; + public static final int KEY_BIND = 0; + + private final String pack; + private final int i; + private final int pIn; + + public GreePackMessage(final String pack, final int encryptionKey) { + this.pack = pack; + this.i = encryptionKey; + this.pIn = 0; + } + + public String getPack() { + return pack; + } + + public int getEncryptionKey() { + return i; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/gree/messages/GreeWlanMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/gree/messages/GreeWlanMessage.java new file mode 100644 index 000000000..1f52335f0 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/gree/messages/GreeWlanMessage.java @@ -0,0 +1,17 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.gree.messages; + +public class GreeWlanMessage extends AbstractGreeMessage { + public static final String TYPE = "wlan"; + + private final String host; + private final String psw; + private final String ssid; + private final int num; + + public GreeWlanMessage(final String host, final String psw, final String ssid, final int num) { + this.host = host; + this.psw = psw; + this.ssid = ssid; + this.num = num; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/CryptoUtils.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/CryptoUtils.java index c5e4cc686..6b99dd3b5 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/CryptoUtils.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/CryptoUtils.java @@ -21,6 +21,7 @@ import android.annotation.SuppressLint; import java.io.UnsupportedEncodingException; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.MessageDigest; @@ -71,6 +72,20 @@ public class CryptoUtils { return cipher.doFinal(data); } + public static byte[] encryptAES_ECB_Pad(byte[] data, byte[] key) throws GeneralSecurityException { + final Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); + final SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES"); + cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec); + return cipher.doFinal(data); + } + + public static byte[] decryptAES_ECB_Pad(byte[] data, byte[] key) throws GeneralSecurityException { + final Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); + final SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES"); + cipher.init(Cipher.DECRYPT_MODE, secretKeySpec); + return cipher.doFinal(data); + } + public static byte[] encryptAES_GCM_NoPad(byte[] data, byte[] key, byte[] iv, byte[] aad) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException { Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES"); diff --git a/app/src/main/res/drawable/ic_device_air_conditioning.xml b/app/src/main/res/drawable/ic_device_air_conditioning.xml new file mode 100644 index 000000000..c1fb8d6aa --- /dev/null +++ b/app/src/main/res/drawable/ic_device_air_conditioning.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_device_air_conditioning_disabled.xml b/app/src/main/res/drawable/ic_device_air_conditioning_disabled.xml new file mode 100644 index 000000000..708f09c04 --- /dev/null +++ b/app/src/main/res/drawable/ic_device_air_conditioning_disabled.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_gree_ac_pairing.xml b/app/src/main/res/layout/activity_gree_ac_pairing.xml new file mode 100644 index 000000000..cf08303dc --- /dev/null +++ b/app/src/main/res/layout/activity_gree_ac_pairing.xml @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + +