mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2025-02-04 14:07:32 +01:00
Even Realities: Add initial support for Even G1 Smart Glasses (#4553)
Co-authored-by: jrthomas270 <jrthomas270@noreply.codeberg.org> Co-committed-by: jrthomas270 <jrthomas270@noreply.codeberg.org>
This commit is contained in:
parent
c08285a356
commit
2d6a7d3866
@ -643,6 +643,10 @@
|
||||
<activity
|
||||
android:name=".devices.pebble.PebblePairingActivity"
|
||||
android:label="@string/title_activity_pebble_pairing" />
|
||||
<activity
|
||||
android:name=".devices.evenrealities.G1PairingActivity"
|
||||
android:label="@string/title_activity_even_realities_g1_pairing"
|
||||
android:parentActivityName=".activities.discovery.DiscoveryActivityV2" />
|
||||
<activity
|
||||
android:name=".devices.watch9.Watch9PairingActivity"
|
||||
android:label="@string/title_activity_watch9_pairing" />
|
||||
|
@ -109,6 +109,7 @@ public class DiscoveryActivityV2 extends AbstractGBActivity implements AdapterVi
|
||||
MenuProvider {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DiscoveryActivityV2.class);
|
||||
|
||||
private static final int CHILD_RESULT = 0x826983; // "RES" as ASCII hex
|
||||
private final Handler handler = new Handler();
|
||||
|
||||
private static final long SCAN_DURATION = 30000; // 30s
|
||||
@ -144,7 +145,13 @@ public class DiscoveryActivityV2 extends AbstractGBActivity implements AdapterVi
|
||||
@Override
|
||||
public void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
BondingUtil.handleActivityResult(this, requestCode, resultCode, data);
|
||||
if (requestCode == CHILD_RESULT && resultCode == RESULT_OK) {
|
||||
// A device with a custom pairing activity has finished and indicated that the discovery activity should be
|
||||
// closed.
|
||||
finish();
|
||||
} else {
|
||||
BondingUtil.handleActivityResult(this, requestCode, resultCode, data);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@ -655,7 +662,8 @@ public class DiscoveryActivityV2 extends AbstractGBActivity implements AdapterVi
|
||||
if (pairingActivity != null) {
|
||||
final Intent intent = new Intent(this, pairingActivity);
|
||||
intent.putExtra(DeviceCoordinator.EXTRA_DEVICE_CANDIDATE, deviceCandidate);
|
||||
startActivity(intent);
|
||||
intent.putParcelableArrayListExtra(DeviceCoordinator.EXTRA_DEVICE_ALL_CANDIDATES, deviceCandidates);
|
||||
startActivityForResult(intent, CHILD_RESULT);
|
||||
} else {
|
||||
if (coordinator.getBondingStyle() == DeviceCoordinator.BONDING_STYLE_NONE ||
|
||||
coordinator.getBondingStyle() == DeviceCoordinator.BONDING_STYLE_LAZY) {
|
||||
|
@ -78,6 +78,8 @@ import nodomain.freeyourgadget.gadgetbridge.service.ServiceDeviceSupport;
|
||||
*/
|
||||
public interface DeviceCoordinator {
|
||||
String EXTRA_DEVICE_CANDIDATE = "nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate.EXTRA_DEVICE_CANDIDATE";
|
||||
String EXTRA_DEVICE_ALL_CANDIDATES =
|
||||
"nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate.EXTRA_DEVICE_ALL_CANDIDATES";
|
||||
/**
|
||||
* Do not attempt to bond after discovery.
|
||||
*/
|
||||
|
@ -0,0 +1,87 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.evenrealities;
|
||||
|
||||
import android.app.Activity;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
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.evenrealities.G1DeviceSupport;
|
||||
|
||||
/**
|
||||
* Coordinator for the Even Realities G1 smart glasses. Describes the supported capabilities of the
|
||||
* device.
|
||||
* <p>
|
||||
* This class partners with G1DeviceSupport.java and G1PairingActivity.java
|
||||
*/
|
||||
public class G1DeviceCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(G1DeviceCoordinator.class);
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Class<? extends DeviceSupport> getDeviceSupportClass() {
|
||||
return G1DeviceSupport.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<? extends Activity> getPairingActivity() {
|
||||
return G1PairingActivity.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Pattern getSupportedDeviceName() {
|
||||
// eg. G1_45_L_F2333, G1_63_R_04935.
|
||||
// Note that the G1_XX_L_YYYYY will have a corresponding G1_XX_R_ZZZZZ. The XX will match,
|
||||
// but the trailing 5 characters will not.
|
||||
return Pattern.compile("Even G1_\\d\\d_[L|R]_\\w+");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDeviceNameResource() {
|
||||
return R.string.devicetype_even_realities_g1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getManufacturer() {
|
||||
return "Even Realities";
|
||||
}
|
||||
|
||||
@Override
|
||||
@DrawableRes
|
||||
public int getDefaultIconResource() {
|
||||
return R.drawable.ic_device_even_realities_g1;
|
||||
}
|
||||
|
||||
@Override
|
||||
@DrawableRes
|
||||
public int getDisabledIconResource() {
|
||||
return R.drawable.ic_device_even_realities_g1_disabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBondingStyle() {
|
||||
return BONDING_STYLE_LAZY;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device,
|
||||
@NonNull DaoSession session) throws GBException {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean addBatteryPollingSettings() {
|
||||
return true;
|
||||
}
|
||||
}
|
@ -0,0 +1,338 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.evenrealities;
|
||||
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.view.View;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ListView;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.adapter.DeviceCandidateAdapter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.evenrealities.G1DeviceConstants;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.BondingInterface;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.BondingUtil;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
/**
|
||||
* This class manages the pairing of both the left and right device for G1 glasses.
|
||||
* The user will select either the left or the right and this activity will search for the other
|
||||
* side pair both.
|
||||
*/
|
||||
public class G1PairingActivity extends AbstractGBActivity
|
||||
implements BondingInterface, AdapterView.OnItemClickListener {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(G1PairingActivity.class);
|
||||
private final ArrayList<GBDeviceCandidate> nextLensCandidates = new ArrayList<>();
|
||||
private final BroadcastReceiver bluetoothReceiver = new G1PairingActivity.BluetoothReceiver();
|
||||
|
||||
// Variables used to determine the initial state. The user can select the left or right lens to
|
||||
// start the pairing so these are used to determine the other device that needs connection.
|
||||
private GBDeviceCandidate initialDeviceCandidate;
|
||||
private G1DeviceConstants.Side initialDeviceCandidateSide;
|
||||
|
||||
// Variables used for tracking the bonding state of both devices. The bonding steps involve
|
||||
// setting the current target to left, then initiating bonding on the current target. When the
|
||||
// bond has completed, the current target is set to the right device then bonding is initiated
|
||||
// on the current target again. currentBondingCompleteFromCallback is used to differentiate
|
||||
// calls to onBondingComplete(). onBondingComplete() will be called prematurely by GB so we need
|
||||
// to ignore that call, however when the BLE api invokes ACTION_BOND_STATE_CHANGED, it is before
|
||||
// the device has been marked as bonded so it's impossible to tell who is calling
|
||||
// onBondingComplete() just from the device state.
|
||||
private GBDeviceCandidate currentBondingCandidate;
|
||||
private boolean currentBondingCompleteFromCallback;
|
||||
private GBDeviceCandidate leftDeviceCandidate;
|
||||
private GBDeviceCandidate rightDeviceCandidate;
|
||||
|
||||
// References to UI elements so that any function can update the interface.
|
||||
private TextView hintTextView;
|
||||
private ProgressBar progressBar;
|
||||
private ListView nextLensCandidatesListView;
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
unregisterBroadcastReceivers();
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
unregisterBroadcastReceivers();
|
||||
super.onStop();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
unregisterBroadcastReceivers();
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
registerBroadcastReceivers();
|
||||
super.onResume();
|
||||
}
|
||||
|
||||
@Override
|
||||
public GBDeviceCandidate getCurrentTarget() {
|
||||
return currentBondingCandidate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMacAddress() {
|
||||
return currentBondingCandidate.getDevice().getAddress();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getAttemptToConnect() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Context getContext() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_even_realities_g1_pairing);
|
||||
|
||||
// Initialize the references to all the UI element objects.
|
||||
hintTextView = findViewById(R.id.even_g1_pairing_status);
|
||||
nextLensCandidatesListView = findViewById(R.id.next_lens_candidates_list);
|
||||
progressBar = findViewById(R.id.pairing_progress_bar);
|
||||
|
||||
// Pull the candidate device out of the intent.
|
||||
Intent intent = getIntent();
|
||||
intent.setExtrasClassLoader(GBDeviceCandidate.class.getClassLoader());
|
||||
initialDeviceCandidate =
|
||||
intent.getParcelableExtra(DeviceCoordinator.EXTRA_DEVICE_CANDIDATE);
|
||||
|
||||
// Extract the name of the device and null check it.
|
||||
String name = initialDeviceCandidate.getName();
|
||||
if (name == null) {
|
||||
GB.toast(getContext(),
|
||||
getString(R.string.pairing_even_realities_g1_invalid_device, "null"),
|
||||
Toast.LENGTH_LONG, GB.ERROR);
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
// The name of the device will be something like 'Even G1_87_L_39E92'.
|
||||
// Extract the Even G1_87 out and null check it.
|
||||
String compositeDeviceName = G1DeviceConstants.getNameFromFullName(name);
|
||||
if (compositeDeviceName == null) {
|
||||
GB.toast(getContext(),
|
||||
getString(R.string.pairing_even_realities_g1_invalid_device, name),
|
||||
Toast.LENGTH_LONG, GB.ERROR);
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
// The name of the device will be something like 'Even G1_87_L_39E92'.
|
||||
// Extract the L or R from out and null check it.
|
||||
initialDeviceCandidateSide =
|
||||
G1DeviceConstants.getSideFromFullName(initialDeviceCandidate.getName());
|
||||
if (initialDeviceCandidateSide == null) {
|
||||
GB.toast(getContext(),
|
||||
getString(R.string.pairing_even_realities_g1_invalid_device, name),
|
||||
Toast.LENGTH_LONG, GB.ERROR);
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine the current and next side. This is used to show the correct UI element to the
|
||||
// user.
|
||||
int currentSide = 0;
|
||||
int nextSide = 0;
|
||||
if (initialDeviceCandidateSide == G1DeviceConstants.Side.LEFT) {
|
||||
currentSide = R.string.watchface_dialog_widget_preset_left;
|
||||
nextSide = R.string.watchface_dialog_widget_preset_right;
|
||||
} else {
|
||||
currentSide = R.string.watchface_dialog_widget_preset_right;
|
||||
nextSide = R.string.watchface_dialog_widget_preset_left;
|
||||
}
|
||||
hintTextView.setText(getString(R.string.pairing_even_realities_g1_select_next_lens,
|
||||
getString(currentSide), getString(nextSide)));
|
||||
|
||||
// Populate the list of next side candidates. We examine all other devices in the discovery
|
||||
// list and filter them based on name.
|
||||
final List<Parcelable> allCandidates =
|
||||
intent.getParcelableArrayListExtra(DeviceCoordinator.EXTRA_DEVICE_ALL_CANDIDATES);
|
||||
if (allCandidates != null) {
|
||||
nextLensCandidates.clear();
|
||||
for (final Parcelable p : allCandidates) {
|
||||
final GBDeviceCandidate nextCandidate = (GBDeviceCandidate) p;
|
||||
// Filter out all devices that don't match the selected device name and also filter
|
||||
// out the selected device.
|
||||
String nextCandidatePrefix =
|
||||
G1DeviceConstants.getNameFromFullName(nextCandidate.getName());
|
||||
if (!initialDeviceCandidate.equals(nextCandidate) &&
|
||||
compositeDeviceName.equals(nextCandidatePrefix)) {
|
||||
nextLensCandidates.add(nextCandidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No matching device found.
|
||||
if (nextLensCandidates.isEmpty()) {
|
||||
GB.toast(getContext(), R.string.pairing_even_realities_g1_find_both_fail,
|
||||
Toast.LENGTH_LONG, GB.ERROR);
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
// Setup the BLE callbacks so we get notified when the devices are done bonding.
|
||||
registerBroadcastReceivers();
|
||||
|
||||
// If there is only one matching device, initiate pairing with it, no need to ask the user.
|
||||
if (nextLensCandidates.size() == 1) {
|
||||
if (initialDeviceCandidateSide == G1DeviceConstants.Side.LEFT) {
|
||||
pairDevices(initialDeviceCandidate, nextLensCandidates.get(0));
|
||||
} else {
|
||||
pairDevices(nextLensCandidates.get(0), initialDeviceCandidate);
|
||||
}
|
||||
} else {
|
||||
// There is more than one matching candidate, display all of the candidates as a list
|
||||
// and let the user choose the correct one. This should be rare an only happen if the
|
||||
// user has multiple pairs of glasses around them. Even then, the two digit id should
|
||||
// not be the same between devices, but since it can only be 00-99, there are only 100
|
||||
// options, so collisions are inevitable. Better to have this and not need it than have
|
||||
// users get stuck.
|
||||
DeviceCandidateAdapter nextLensCandidatesAdapter =
|
||||
new DeviceCandidateAdapter(this, nextLensCandidates);
|
||||
nextLensCandidatesListView.setAdapter(nextLensCandidatesAdapter);
|
||||
nextLensCandidatesListView.setOnItemClickListener(this);
|
||||
|
||||
// Hide the progress bar. The list is visible by default, so it will be shown.
|
||||
progressBar.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBondingComplete(boolean success) {
|
||||
// On error, just exit. There will be a toast from the bonding code to says what went wrong.
|
||||
if (!success) {
|
||||
finish();
|
||||
}
|
||||
|
||||
// Left device is done, start pairing the right device. This function will be called again
|
||||
// when the right device is finished.
|
||||
if (currentBondingCandidate == leftDeviceCandidate && currentBondingCompleteFromCallback) {
|
||||
currentBondingCandidate = rightDeviceCandidate;
|
||||
currentBondingCompleteFromCallback = false;
|
||||
String displayName =
|
||||
G1DeviceConstants.getNameFromFullName(rightDeviceCandidate.getName()) + " " +
|
||||
getString(R.string.watchface_dialog_widget_preset_right);
|
||||
hintTextView.setText(
|
||||
getString(R.string.pairing_even_realities_g1_working, displayName));
|
||||
BondingUtil.connectThenComplete(this, currentBondingCandidate);
|
||||
}
|
||||
|
||||
// Both devices are bonded. Finish up.
|
||||
if (currentBondingCandidate == rightDeviceCandidate && currentBondingCompleteFromCallback) {
|
||||
// The initial connection prompts the bonding, but it will be a generic GATT connection.
|
||||
// Now that the device is bonded, we need to disconnect and reconnect one more time to
|
||||
// have full access to all GATT attributes.
|
||||
BondingUtil.attemptToFirstConnect(leftDeviceCandidate.getDevice());
|
||||
BondingUtil.attemptToFirstConnect(rightDeviceCandidate.getDevice());
|
||||
setResult(RESULT_OK, null);
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||
final GBDeviceCandidate nextDeviceCandidate = nextLensCandidates.get(position);
|
||||
// The user may have selected either the right or the left lens. We have both devices, pair
|
||||
// them as left and right.
|
||||
if (initialDeviceCandidateSide == G1DeviceConstants.Side.LEFT) {
|
||||
pairDevices(initialDeviceCandidate, nextDeviceCandidate);
|
||||
} else {
|
||||
pairDevices(nextDeviceCandidate, initialDeviceCandidate);
|
||||
}
|
||||
}
|
||||
|
||||
private void pairDevices(GBDeviceCandidate leftCandidate, GBDeviceCandidate rightCandidate) {
|
||||
// Change the UI to pairing in progress mode.
|
||||
progressBar.setVisibility(View.VISIBLE);
|
||||
nextLensCandidatesListView.setVisibility(View.GONE);
|
||||
String displayName = G1DeviceConstants.getNameFromFullName(leftCandidate.getName()) + " " +
|
||||
getString(R.string.watchface_dialog_widget_preset_left);
|
||||
hintTextView.setText(getString(R.string.pairing_even_realities_g1_working, displayName));
|
||||
|
||||
// Set the global left and right for the callback to use later.
|
||||
leftDeviceCandidate = leftCandidate;
|
||||
rightDeviceCandidate = rightCandidate;
|
||||
|
||||
// Bond the left device. When it is completed, onBondingComplete() will be called which will
|
||||
// bond the right.
|
||||
currentBondingCandidate = leftDeviceCandidate;
|
||||
currentBondingCompleteFromCallback = false;
|
||||
BondingUtil.connectThenComplete(this, currentBondingCandidate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerBroadcastReceivers() {
|
||||
final IntentFilter bluetoothIntents = new IntentFilter();
|
||||
bluetoothIntents.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
|
||||
ContextCompat.registerReceiver(this, bluetoothReceiver, bluetoothIntents,
|
||||
ContextCompat.RECEIVER_EXPORTED);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregisterBroadcastReceivers() {
|
||||
AndroidUtils.safeUnregisterBroadcastReceiver(this, bluetoothReceiver);
|
||||
}
|
||||
|
||||
private final class BluetoothReceiver extends BroadcastReceiver {
|
||||
@Override
|
||||
public void onReceive(final Context context, final Intent intent) {
|
||||
if (Objects.requireNonNull(intent.getAction())
|
||||
.equals(BluetoothDevice.ACTION_BOND_STATE_CHANGED)) {
|
||||
LOG.debug("ACTION_BOND_STATE_CHANGED");
|
||||
final BluetoothDevice device =
|
||||
intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
|
||||
|
||||
if (device != null) {
|
||||
final int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE,
|
||||
BluetoothDevice.BOND_NONE);
|
||||
LOG.debug("{} Bond state: {}", device.getAddress(), bondState);
|
||||
|
||||
if (bondState == BluetoothDevice.BOND_BONDED) {
|
||||
if (device.getAddress().equals(currentBondingCandidate.getMacAddress())) {
|
||||
currentBondingCompleteFromCallback = true;
|
||||
((BondingInterface) context).onBondingComplete(true);
|
||||
} else {
|
||||
// We got a callback from the wrong device. This shouldn't be possible.
|
||||
GB.toast(getContext(),
|
||||
getString(R.string.pairing_even_realities_g1_invalid_device,
|
||||
device.getAddress()), Toast.LENGTH_LONG, GB.ERROR);
|
||||
((BondingInterface) context).onBondingComplete(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -48,6 +48,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.colmi.ColmiR10Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.cycling_sensor.coordinator.CyclingSensorCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.divoom.PixooCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.domyos.DomyosT540Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.evenrealities.G1DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.femometer.FemometerVinca2DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProDeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.fitpro.colacao.ColaCao21Coordinator;
|
||||
@ -517,6 +518,7 @@ public enum DeviceType {
|
||||
WASPOS(WaspOSCoordinator.class),
|
||||
UM25(UM25Coordinator.class),
|
||||
DOMYOS_T540(DomyosT540Coordinator.class),
|
||||
EVEN_REALITIES_G_1(G1DeviceCoordinator.class),
|
||||
NOTHING_EAR1(Ear1Coordinator.class),
|
||||
NOTHING_EAR2(Ear2Coordinator.class),
|
||||
NOTHING_EAR_STICK(EarStickCoordinator.class),
|
||||
|
@ -0,0 +1,72 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.evenrealities;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class G1DeviceConstants {
|
||||
public static final UUID UUID_SERVICE_NORDIC_UART =
|
||||
UUID.fromString("6e400001-b5a3-f393-e0a9-e50e24dcca9e");
|
||||
public static final UUID UUID_CHARACTERISTIC_NORDIC_UART_TX =
|
||||
UUID.fromString("6e400002-b5a3-f393-e0a9-e50e24dcca9e");
|
||||
public static final UUID UUID_CHARACTERISTIC_NORDIC_UART_RX =
|
||||
UUID.fromString("6e400003-b5a3-f393-e0a9-e50e24dcca9e");
|
||||
public static final int MTU = 251;
|
||||
|
||||
// Extract the L or R at the end of the device prefix.
|
||||
public static Side getSideFromFullName(String deviceName) {
|
||||
int prefixSize = "Even G1_XX_X".length();
|
||||
|
||||
if (deviceName.length() < prefixSize) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String prefix = deviceName.substring(0, prefixSize);
|
||||
char side = prefix.charAt(prefix.length() - 1);
|
||||
if (side == 'L' || side == 'R') {
|
||||
return side == 'L' ? Side.LEFT : Side.RIGHT;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static String getNameFromFullName(String deviceName) {
|
||||
int prefixSize = "Even G1_XX".length();
|
||||
|
||||
if (deviceName.length() < prefixSize) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return deviceName.substring(0, prefixSize);
|
||||
}
|
||||
|
||||
public enum Side {
|
||||
LEFT,
|
||||
RIGHT;
|
||||
}
|
||||
|
||||
|
||||
// TODO: Lifted these from a different project, some of them are wrong.
|
||||
public enum CommandId {
|
||||
BATTERY_LEVEL((byte) 0x2C),
|
||||
WEATHER_AND_TIME((byte) 0x06),
|
||||
START_AI((byte) 0xF5),
|
||||
OPEN_MIC((byte) 0x0E),
|
||||
MIC_RESPONSE((byte) 0x0E),
|
||||
RECEIVE_MIC_DATA((byte) 0xF1),
|
||||
INIT((byte) 0x4D),
|
||||
HEARTBEAT((byte) 0x25),
|
||||
SEND_RESULT((byte) 0x4E),
|
||||
QUICK_NOTE((byte) 0x21),
|
||||
DASHBOARD((byte) 0x22),
|
||||
NOTIFICATION((byte) 0x4B),
|
||||
BMP((byte) 0x15),
|
||||
FW_INFO_REQUEST((byte) 0x23),
|
||||
FW_INFO_RESPONSE((byte) 0x6E),
|
||||
CRC((byte) 0x16);
|
||||
|
||||
final public byte id;
|
||||
|
||||
CommandId(byte id) {
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,215 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.evenrealities;
|
||||
|
||||
import android.bluetooth.BluetoothGatt;
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
|
||||
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.util.GB;
|
||||
|
||||
/**
|
||||
* Support class for the Even Realities G1. This sends and receives commands to and from the device.
|
||||
* The Protocol is mostly defined in G1Constants.java right now. In the future the protocol will be
|
||||
* broken out to a different class.
|
||||
* One interesting point about this device is that it requires a constant BLE connection which is
|
||||
* contrary to the way BLE is supposed to work. Unfortunately the device will show the disconnected
|
||||
* icon and stop displaying any information when it is in the disconnected state. Because of this,
|
||||
* we need to send a heartbeat ever 30 seconds, otherwise the device will disconnect and reconnect
|
||||
* every 32 seconds per the BLE spec.
|
||||
*/
|
||||
public class G1DeviceSupport extends AbstractBTLEDeviceSupport {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(G1DeviceSupport.class);
|
||||
private final Handler backgroundTasksHandler = new Handler(Looper.getMainLooper());
|
||||
private BluetoothGattCharacteristic rxCharacteristic;
|
||||
private BluetoothGattCharacteristic txCharacteristic;
|
||||
private int heartBeatSequence;
|
||||
|
||||
public G1DeviceSupport() {
|
||||
this(LOG);
|
||||
}
|
||||
|
||||
public G1DeviceSupport(Logger logger) {
|
||||
super(logger);
|
||||
addSupportedService(G1DeviceConstants.UUID_SERVICE_NORDIC_UART);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected TransactionBuilder initializeDevice(TransactionBuilder builder) {//}, int deviceIdx) {
|
||||
this.heartBeatSequence = 0;
|
||||
builder.add(
|
||||
new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext()));
|
||||
|
||||
rxCharacteristic = getCharacteristic(G1DeviceConstants.UUID_CHARACTERISTIC_NORDIC_UART_RX);
|
||||
txCharacteristic = getCharacteristic(G1DeviceConstants.UUID_CHARACTERISTIC_NORDIC_UART_TX);
|
||||
builder.requestMtu(G1DeviceConstants.MTU);
|
||||
|
||||
if (rxCharacteristic == null || txCharacteristic == null) {
|
||||
// If the characteristics are not received from the device reconnect and try again.
|
||||
LOG.warn("RX/TX characteristics are null, will attempt to reconnect");
|
||||
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.WAITING_FOR_RECONNECT,
|
||||
getContext()));
|
||||
GB.toast(getContext(), "Failed to connect to Glasses, waiting for reconnect.",
|
||||
Toast.LENGTH_LONG, GB.ERROR);
|
||||
return builder;
|
||||
}
|
||||
|
||||
// Register callbacks for this device.
|
||||
builder.setCallback(this);
|
||||
builder.notify(rxCharacteristic, true);
|
||||
|
||||
// Send the command to fetch FW version.
|
||||
byte[] packet = new byte[2];
|
||||
packet[0] = G1DeviceConstants.CommandId.FW_INFO_REQUEST.id;
|
||||
packet[1] = 0x74;
|
||||
builder.write(txCharacteristic, packet);
|
||||
|
||||
// Send the command to fetch battery info.
|
||||
packet = new byte[2];
|
||||
packet[0] = G1DeviceConstants.CommandId.BATTERY_LEVEL.id;
|
||||
packet[1] = 0x01;
|
||||
builder.write(txCharacteristic, packet);
|
||||
|
||||
if (getDevice().getFirmwareVersion() == null) {
|
||||
getDevice().setFirmwareVersion("N/A");
|
||||
getDevice().setFirmwareVersion2("N/A");
|
||||
}
|
||||
|
||||
// The glasses will auto disconnect after 30 seconds of no data on the wire.
|
||||
// Schedule a heartbeat task. If this is not enabled, button presses on the glasses will not
|
||||
// be sent to the phone, so realtime interactions won't work.
|
||||
scheduleHeatBeat();
|
||||
|
||||
// Schedule the battery polling.
|
||||
scheduleBatteryPolling();
|
||||
|
||||
// Device is ready for use.
|
||||
builder.add(
|
||||
new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext()));
|
||||
gbDevice.sendDeviceUpdateIntent(getContext());
|
||||
return builder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
// Remove all callbacks
|
||||
backgroundTasksHandler.removeCallbacksAndMessages(null);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean useAutoConnect() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCharacteristicChanged(BluetoothGatt gatt,
|
||||
BluetoothGattCharacteristic characteristic) {
|
||||
// Super already handled this.
|
||||
if (super.onCharacteristicChanged(gatt, characteristic)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If this is the correct UART RX message, parse it.
|
||||
if (G1DeviceConstants.UUID_CHARACTERISTIC_NORDIC_UART_RX.equals(characteristic.getUuid())) {
|
||||
byte[] payload = characteristic.getValue();
|
||||
if (payload[0] == G1DeviceConstants.CommandId.BATTERY_LEVEL.id) {
|
||||
GBDeviceEventBatteryInfo batteryInfo = new GBDeviceEventBatteryInfo();
|
||||
batteryInfo.state = BatteryState.BATTERY_NORMAL;
|
||||
batteryInfo.level = payload[2];
|
||||
handleGBDeviceEvent(batteryInfo);
|
||||
} else if (payload[0] == G1DeviceConstants.CommandId.FW_INFO_RESPONSE.id) {
|
||||
// FW info string
|
||||
String fwInfo = new String(payload, StandardCharsets.US_ASCII).trim();
|
||||
LOG.debug("Got FW: " + fwInfo);
|
||||
int versionStart = fwInfo.lastIndexOf(" ver ") + " ver ".length();
|
||||
int versionEnd = fwInfo.indexOf(',', versionStart);
|
||||
if (versionStart > -1 && versionEnd > versionStart) {
|
||||
String version = fwInfo.substring(versionStart, versionEnd);
|
||||
LOG.debug("Parsed fw version: " + version);
|
||||
getDevice().setFirmwareVersion(version);
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* If configuration options can be set on the device, this method
|
||||
* can be overridden and implemented by the device support class.
|
||||
*
|
||||
* @param config the device specific option to set on the device
|
||||
*/
|
||||
@Override
|
||||
public void onSendConfiguration(String config) {
|
||||
switch (config) {
|
||||
// Reschedule battery polling. The new schedule may be disabled.
|
||||
case DeviceSettingsPreferenceConst.PREF_BATTERY_POLLING_ENABLE:
|
||||
case DeviceSettingsPreferenceConst.PREF_BATTERY_POLLING_INTERVAL:
|
||||
scheduleBatteryPolling();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void scheduleHeatBeat() {
|
||||
backgroundTasksHandler.removeCallbacksAndMessages(heartBeatRunner);
|
||||
backgroundTasksHandler.postDelayed(heartBeatRunner, 30 * 1000);
|
||||
}
|
||||
|
||||
private void scheduleBatteryPolling() {
|
||||
backgroundTasksHandler.removeCallbacksAndMessages(batteryRunner);
|
||||
if (GBApplication.getDevicePrefs(gbDevice).getBatteryPollingEnabled()) {
|
||||
int interval_minutes =
|
||||
GBApplication.getDevicePrefs(gbDevice).getBatteryPollingIntervalMinutes();
|
||||
int interval = interval_minutes * 60 * 1000;
|
||||
LOG.debug("Starting battery runner delayed by {} ({} minutes)", interval,
|
||||
interval_minutes);
|
||||
backgroundTasksHandler.postDelayed(batteryRunner, interval);
|
||||
}
|
||||
}
|
||||
|
||||
private final Runnable batteryRunner = () -> {
|
||||
TransactionBuilder builder = new TransactionBuilder("battery_request");
|
||||
byte[] packet = new byte[2];
|
||||
packet[0] = G1DeviceConstants.CommandId.BATTERY_LEVEL.id;
|
||||
packet[1] = 0x01;
|
||||
builder.write(txCharacteristic, packet);
|
||||
builder.queue(getQueue());
|
||||
|
||||
// Schedule the next check.
|
||||
scheduleBatteryPolling();
|
||||
};
|
||||
|
||||
|
||||
private final Runnable heartBeatRunner = () -> {
|
||||
TransactionBuilder builder = new TransactionBuilder("heart_beat");
|
||||
int length = 6;
|
||||
byte[] packet = new byte[length];
|
||||
packet[0] = G1DeviceConstants.CommandId.HEARTBEAT.id;
|
||||
packet[1] = (byte) (length & 0xFF);
|
||||
packet[2] = 0x00; //(byte)((length >> 8) & 0xFF);
|
||||
packet[3] = (byte) (heartBeatSequence % 0xFF);
|
||||
packet[4] = 0x04;
|
||||
packet[5] = (byte) (heartBeatSequence % 0xFF);
|
||||
builder.write(txCharacteristic, packet);
|
||||
builder.queue(getQueue());
|
||||
|
||||
// Schedule the next heartbeat.
|
||||
scheduleHeatBeat();
|
||||
};
|
||||
|
||||
|
||||
}
|
30
app/src/main/res/drawable/ic_device_even_realities_g1.xml
Normal file
30
app/src/main/res/drawable/ic_device_even_realities_g1.xml
Normal file
@ -0,0 +1,30 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="45sp"
|
||||
android:height="45sp"
|
||||
android:viewportWidth="30"
|
||||
android:viewportHeight="30">
|
||||
<path
|
||||
android:fillColor="?attr/deviceIconLight"
|
||||
android:pathData="M3.871 3.877h20.925a0.947 0.947 0 0 1 0.948 0.947v20.01a0.947 0.947 0 0 1-0.948 0.948H3.871a0.947 0.947 0 0 1-0.947-0.948V4.824a0.947 0.947 0 0 1 0.947-0.947z"
|
||||
android:strokeWidth="3.57" />
|
||||
<path
|
||||
android:fillColor="?attr/deviceIconDark"
|
||||
android:pathData="M3.879 3.035h20.925a0.947 0.947 0 0 1 0.947 0.947v20.01a0.947 0.947 0 0 1-0.947 0.948H3.88a0.947 0.947 0 0 1-0.947-0.948V3.982A0.947 0.947 0 0 1 3.88 3.035z"
|
||||
android:strokeWidth="3.57" />
|
||||
<path
|
||||
android:fillColor="?attr/deviceIconPrimary"
|
||||
android:pathData="M3.871 3.413h20.925a0.947 0.947 0 0 1 0.948 0.947v20.01a0.947 0.947 0 0 1-0.948 0.948H3.871a0.947 0.947 0 0 1-0.947-0.948V4.36A0.947 0.947 0 0 1 3.87 3.413z"
|
||||
android:strokeWidth="3.57" />
|
||||
<path
|
||||
android:fillColor="?attr/deviceIconOnPrimary"
|
||||
android:pathData= "m 9.26,11.01c -2.72,-0.06 -5.67,0.62 -5.83,0.92 -0.11,0.21 -0.11,0.21 0.56,0.21 0.72,0 0.72,0 0.64,0.41 -0.07,0.36 -0.12,0.39 -0.76,0.39 -0.29,0 -0.52,0.01 -0.52,0.02 0,0.74 1.18,0.05 1.44,1.27 0.63,2.91 1.4,3.08 3.11,3.26 3.47,0.35 4.4,-0.32 5.47,-3.94 0.28,-0.97 1.65,-0.97 1.94,0 1.07,3.63 1.92,4.25 5.37,3.94 2.39,-0.26 2.81,-1.38 3.21,-3.24 0.27,-1.25 1.44,-0.53 1.44,-1.29 0,-0.01 -0.23,-0.02 -0.52,-0.02 -0.64,0 -0.69,-0.02 -0.76,-0.39 -0.07,-0.41 -0.08,-0.41 0.64,-0.41 0.67,0 0.67,0 0.56,-0.21 -0.22,-0.43 -6.36,-1.68 -9.12,-0.27 -0.94,0.48 -2.94,0.36 -3.58,0 -0.86,-0.44 -2.05,-0.62 -3.29,-0.65z"
|
||||
/>
|
||||
<path
|
||||
android:fillColor="?attr/deviceIconPrimary"
|
||||
android:pathData="m 8.84,11.43c -0.32,0 -0.66,0.01 -1.01,0.04 -1.69,0.11 -3.13,0.27 -2.51,2.79 0.48,1.94 0.38,2.48 2.1,2.79 1.99,0.36 4.07,0.33 4.93,-1.77 0.32,-0.78 0.54,-2.15 0.41,-2.54 -0.29,-0.87 -1.69,-1.32 -3.92,-1.3z"
|
||||
/>
|
||||
<path
|
||||
android:fillColor="?attr/deviceIconPrimary"
|
||||
android:pathData="m 19.85,11.44c -2.23,-0.02 -3.63,0.44 -3.92,1.31 -0.13,0.39 0.09,1.75 0.41,2.54 0.67,1.87 1.72,1.98 3.55,1.88 2.7,-0.16 2.88,-0.62 3.49,-2.95 0.58,-2.07 -0.75,-2.62 -2.52,-2.74 -0.35,-0.02 -0.69,-0.04 -1.01,-0.04z"
|
||||
/>
|
||||
</vector>
|
@ -0,0 +1,30 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="45sp"
|
||||
android:height="45sp"
|
||||
android:viewportWidth="30"
|
||||
android:viewportHeight="30">
|
||||
<path
|
||||
android:fillColor="#7a7a7a"
|
||||
android:pathData="M3.871 3.877h20.925a0.947 0.947 0 0 1 0.948 0.947v20.01a0.947 0.947 0 0 1-0.948 0.948H3.871a0.947 0.947 0 0 1-0.947-0.948V4.824a0.947 0.947 0 0 1 0.947-0.947z"
|
||||
android:strokeWidth="3.57" />
|
||||
<path
|
||||
android:fillColor="#9f9f9f"
|
||||
android:pathData="M3.879 3.035h20.925a0.947 0.947 0 0 1 0.947 0.947v20.01a0.947 0.947 0 0 1-0.947 0.948H3.88a0.947 0.947 0 0 1-0.947-0.948V3.982A0.947 0.947 0 0 1 3.88 3.035z"
|
||||
android:strokeWidth="3.57" />
|
||||
<path
|
||||
android:fillColor="#8a8a8a"
|
||||
android:pathData="M3.871 3.413h20.925a0.947 0.947 0 0 1 0.948 0.947v20.01a0.947 0.947 0 0 1-0.948 0.948H3.871a0.947 0.947 0 0 1-0.947-0.948V4.36A0.947 0.947 0 0 1 3.87 3.413z"
|
||||
android:strokeWidth="3.57" />
|
||||
<path
|
||||
android:fillColor="#fcfcfc"
|
||||
android:pathData= "m 9.26,11.01c -2.72,-0.06 -5.67,0.62 -5.83,0.92 -0.11,0.21 -0.11,0.21 0.56,0.21 0.72,0 0.72,0 0.64,0.41 -0.07,0.36 -0.12,0.39 -0.76,0.39 -0.29,0 -0.52,0.01 -0.52,0.02 0,0.74 1.18,0.05 1.44,1.27 0.63,2.91 1.4,3.08 3.11,3.26 3.47,0.35 4.4,-0.32 5.47,-3.94 0.28,-0.97 1.65,-0.97 1.94,0 1.07,3.63 1.92,4.25 5.37,3.94 2.39,-0.26 2.81,-1.38 3.21,-3.24 0.27,-1.25 1.44,-0.53 1.44,-1.29 0,-0.01 -0.23,-0.02 -0.52,-0.02 -0.64,0 -0.69,-0.02 -0.76,-0.39 -0.07,-0.41 -0.08,-0.41 0.64,-0.41 0.67,0 0.67,0 0.56,-0.21 -0.22,-0.43 -6.36,-1.68 -9.12,-0.27 -0.94,0.48 -2.94,0.36 -3.58,0 -0.86,-0.44 -2.05,-0.62 -3.29,-0.65z"
|
||||
/>
|
||||
<path
|
||||
android:fillColor="#8a8a8a"
|
||||
android:pathData="m 8.84,11.43c -0.32,0 -0.66,0.01 -1.01,0.04 -1.69,0.11 -3.13,0.27 -2.51,2.79 0.48,1.94 0.38,2.48 2.1,2.79 1.99,0.36 4.07,0.33 4.93,-1.77 0.32,-0.78 0.54,-2.15 0.41,-2.54 -0.29,-0.87 -1.69,-1.32 -3.92,-1.3z"
|
||||
/>
|
||||
<path
|
||||
android:fillColor="#8a8a8a"
|
||||
android:pathData="m 19.85,11.44c -2.23,-0.02 -3.63,0.44 -3.92,1.31 -0.13,0.39 0.09,1.75 0.41,2.54 0.67,1.87 1.72,1.98 3.55,1.88 2.7,-0.16 2.88,-0.62 3.49,-2.95 0.58,-2.07 -0.75,-2.62 -2.52,-2.74 -0.35,-0.02 -0.69,-0.04 -1.01,-0.04z"
|
||||
/>
|
||||
</vector>
|
@ -0,0 +1,57 @@
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:paddingLeft="@dimen/activity_horizontal_margin"
|
||||
android:paddingTop="@dimen/activity_vertical_margin"
|
||||
android:paddingRight="@dimen/activity_horizontal_margin"
|
||||
android:paddingBottom="@dimen/activity_vertical_margin"
|
||||
tools:context="nodomain.freeyourgadget.gadgetbridge.activities.discovery.DiscoveryActivityV2">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/even_g1_pairing_status"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="5dp"
|
||||
android:text="@string/pairing_even_realities_g1_select_next_lens"
|
||||
android:textIsSelectable="true"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/pairing_progress_bar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<ListView
|
||||
android:id="@+id/next_lens_candidates_list"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="0dp"
|
||||
android:layout_gravity="bottom|top"
|
||||
android:layout_weight="1">
|
||||
|
||||
</ListView>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/even_g1_discovery_note"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_horizontal|bottom"
|
||||
android:text="@string/discovery_note"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textColor="@color/secondarytext"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/even_g1_pairing_hint"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|center_horizontal"
|
||||
android:text="@string/pairing_even_realities_g1_bottom_hint"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textColor="@color/secondarytext"
|
||||
android:textIsSelectable="true" />
|
||||
|
||||
</LinearLayout>
|
@ -680,6 +680,12 @@
|
||||
<string name="discovery_need_to_enter_authkey">This device needs a secret auth key, long press on the device to enter it. Read the wiki.</string>
|
||||
<string name="discovery_entered_invalid_authkey">The secret auth key you entered is invalid! Long press on the device to edit.</string>
|
||||
<string name="discovery_note">Note:</string>
|
||||
<string name="title_activity_even_realities_g1_pairing">Even G1 Pairing</string>
|
||||
<string name="pairing_even_realities_g1_select_next_lens">%1$s lens selected for pairing. Multiple candidates for the %2$s lens found. Please select the %2$s lens from the list below.</string>
|
||||
<string name="pairing_even_realities_g1_working">Pairing with %1s...</string>
|
||||
<string name="pairing_even_realities_g1_find_both_fail">Failed to find both the right and left lens. Please scan again.</string>
|
||||
<string name="pairing_even_realities_g1_invalid_device">Even G1 Device Malformed: %1$s.</string>
|
||||
<string name="pairing_even_realities_g1_bottom_hint">Several pairing dialogs will pop up on your Android device. If not, look in the notification drawer and accept the pairing request. Both the left and right lens will need to be paired.</string>
|
||||
<string name="candidate_item_device_image">Device image</string>
|
||||
<string name="miband_prefs_alias">Name/Alias</string>
|
||||
<string name="pref_header_vibration_count">Vibration count</string>
|
||||
@ -1851,6 +1857,7 @@
|
||||
<string name="devicetype_colacao21">ColaCao 2021</string>
|
||||
<string name="devicetype_colacao23">ColaCao 2023</string>
|
||||
<string name="devicetype_domyos_t540">Domyos T540</string>
|
||||
<string name="devicetype_even_realities_g1">Even Realities G1</string>
|
||||
<string name="devicetype_sony_wh_1000xm2">Sony WH-1000XM2</string>
|
||||
<string name="devicetype_sony_wh_1000xm3">Sony WH-1000XM3</string>
|
||||
<string name="devicetype_sony_wh_1000xm4">Sony WH-1000XM4</string>
|
||||
|
Loading…
x
Reference in New Issue
Block a user