Compare commits

...

14 Commits

Author SHA1 Message Date
Martin.JM 6ae409a213 [Huawei] Implement remote camera shutter 2024-05-09 14:09:21 +02:00
Martin.JM 7358cb7ba8 Add camera implementation
This is to support watches with remote shutter applets that do not
act as remote triggers for other apps automatically.
2024-05-09 14:09:21 +02:00
José Rebelo b909e123a4 Huawei Band 9: Fix device name 2024-05-08 22:49:57 +01:00
Damien 'Psolyca' Gaignon 39ea1774a4 [Huawei] Add Huawei Band 9 gadget 2024-05-08 21:49:12 +00:00
Damien 'Psolyca' Gaignon f2c360ae8a [Huawei] Add Huawei Watch Fit 3 gadget 2024-05-08 21:49:12 +00:00
José Rebelo 41aab5135f Fossil Q Hybrid: Migrate global preferences to device-specific 2024-05-08 21:42:29 +00:00
Damien 'Psolyca' Gaignon 690d01dcac
[Huawei] Remove unneeded data 2024-05-08 22:48:47 +02:00
Damien 'Psolyca' Gaignon 02b052fcaf
[Huawei] Add Huawei Watch 4 Pro gadget
fix
2024-05-08 22:48:46 +02:00
ahormann ac8d1ed6a0 New Device Soundcore Liberty 3 Pro (#3753)
Reviewed-on: https://codeberg.org/Freeyourgadget/Gadgetbridge/pulls/3753
Co-authored-by: ahormann <ahormann@gmx.net>
Co-committed-by: ahormann <ahormann@gmx.net>
2024-05-07 22:39:13 +00:00
MrYoranimo 508a86b8ed Xiaomi: fix determining fall asleep time
Because the previous implementation of determining the time the user
falls asleep in a given time range would take the 24 hours in advance
into account, graphs displaying sleep data would erroneously indicate
that the user has been asleep since the start of the timeframe if
the user was asleep during the rollover of the time frame 24 hours
before.

This commit change the algorithm to only fetch the last sleep stage
sample and sleep range sample from the database that occurred before
the given time range. This saves having to process 24 hours worth of
samples before the time range in both cases, and prevents taking into
account irrelevant sleep ranges.
2024-05-07 13:33:52 +02:00
MrYoranimo f581d57c01 Xiaomi: fix sleep stages not getting parsed from sleep details files
Not all packets use the payload length byte/short for the payload
length. Instead, some packets do not carry a payload, in which case
the payload length bytes are assumed to represent some state or flag.
Therefore, for packets with a type known not to carry a payload, the
payload extraction is skipped, allowing other packets to get
successfully parsed again.
2024-05-07 13:17:10 +02:00
José Rebelo bed67ef1fb Xiaomi: Allow transliteration 2024-05-05 12:36:23 +01:00
José Rebelo 04237b7727 Prevent query for devices that have activity card disabled
Even if the activity card was disabled, all devices would be queried for
data. This slows down the UI when there are a lot of devices, especially
if multiple of them have data and only a few have the card enabled.
2024-05-04 23:51:19 +01:00
Alik Aslanyan dc1ffdafcd Rework Armenian transliteration to handle more edge cases around mixed letters 2024-05-04 20:45:35 +04:00
57 changed files with 2142 additions and 238 deletions

View File

@ -212,6 +212,10 @@ dependencies {
// testImplementation "ch.qos.logback:logback-classic:1.1.3"
// testImplementation "ch.qos.logback:logback-core:1.1.3"
implementation 'com.android.support.constraint:constraint-layout:2.0.4'
implementation "androidx.camera:camera-core:1.2.3"
implementation "androidx.camera:camera-camera2:1.2.3"
implementation 'androidx.camera:camera-view:1.2.3'
implementation 'androidx.camera:camera-lifecycle:1.2.3'
testImplementation "junit:junit:4.13.2"
testImplementation "org.mockito:mockito-core:2.28.2"
testImplementation "org.robolectric:robolectric:4.12"

View File

@ -74,6 +74,8 @@
<!-- Used for starting activities from the background with intents -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.CAMERA"/>
<!--
SDK 30 & Android 11 - Used for getting app name from notifications, and for starting
services from other packages via intents, when targeting Android API level 30 or later
@ -99,6 +101,9 @@
<uses-feature
android:name="android.software.companion_device_setup"
android:required="false" />
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
<application
android:name=".GBApplication"
@ -764,15 +769,15 @@
</activity>
<activity
android:name=".devices.qhybrid.ConfigActivity"
android:name=".devices.qhybrid.QHybridConfigActivity"
android:label="@string/qhybrid_title_watchface"
android:exported="true"
android:parentActivityName=".activities.ControlCenterv2" />
android:parentActivityName=".activities.devicesettings.DeviceSettingsActivity" />
<activity
android:name=".devices.qhybrid.QHybridAppChoserActivity"
android:label="@string/qhybrid_title_apps"
android:exported="true"
android:parentActivityName=".devices.qhybrid.ConfigActivity" />
android:parentActivityName=".devices.qhybrid.QHybridConfigActivity" />
<activity
android:name=".devices.qhybrid.HRConfigActivity"
android:label="@string/qhybrid_title_watchface"
@ -875,6 +880,11 @@
android:name=".activities.dashboard.DashboardCalendarActivity"
android:label="@string/menuitem_calendar"
android:parentActivityName=".activities.ControlCenterv2" />
<activity
android:name=".activities.CameraActivity"
android:launchMode="singleInstance"
android:exported="false" />
</application>
</manifest>

View File

@ -124,7 +124,7 @@ public class GBApplication extends Application {
private static SharedPreferences sharedPrefs;
private static final String PREFS_VERSION = "shared_preferences_version";
//if preferences have to be migrated, increment the following and add the migration logic in migratePrefs below; see http://stackoverflow.com/questions/16397848/how-can-i-migrate-android-preferences-with-a-new-version
private static final int CURRENT_PREFS_VERSION = 29;
private static final int CURRENT_PREFS_VERSION = 30;
private static final LimitedQueue<Integer, String> mIDSenderLookup = new LimitedQueue<>(16);
private static Prefs prefs;
@ -1468,6 +1468,28 @@ public class GBApplication extends Application {
}
}
if (oldVersion < 30) {
// Migrate QHybrid preferences to device-specific
try (DBHandler db = acquireDB()) {
final DaoSession daoSession = db.getDaoSession();
final List<Device> activeDevices = DBHelper.getActiveDevices(daoSession);
for (Device dbDevice : activeDevices) {
final DeviceType deviceType = DeviceType.fromName(dbDevice.getTypeName());
if (deviceType == DeviceType.FOSSILQHYBRID) {
final SharedPreferences deviceSharedPrefs = GBApplication.getDeviceSpecificSharedPrefs(dbDevice.getIdentifier());
final SharedPreferences.Editor deviceSharedPrefsEdit = deviceSharedPrefs.edit();
deviceSharedPrefsEdit.putInt("QHYBRID_TIME_OFFSET", sharedPrefs.getInt("QHYBRID_TIME_OFFSET", 0));
deviceSharedPrefsEdit.putInt("QHYBRID_TIMEZONE_OFFSET", sharedPrefs.getInt("QHYBRID_TIMEZONE_OFFSET", 0));
deviceSharedPrefsEdit.apply();
}
}
} catch (Exception e) {
Log.w(TAG, "error acquiring DB lock");
}
}
editor.putString(PREFS_VERSION, Integer.toString(CURRENT_PREFS_VERSION));
editor.apply();
}

View File

@ -0,0 +1,218 @@
/* Copyright (C) 2024 Martin.JM
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.Manifest;
import android.content.ContentValues;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.provider.MediaStore;
import android.widget.Toast;
import androidx.activity.result.ActivityResultCallback;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.ImageCaptureException;
import androidx.camera.core.Preview;
import androidx.camera.lifecycle.ProcessCameraProvider;
import androidx.camera.view.PreviewView;
import androidx.core.content.ContextCompat;
import com.google.common.util.concurrent.ListenableFuture;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.ExecutionException;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCameraRemote;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class CameraActivity extends AppCompatActivity {
private static final Logger LOG = LoggerFactory.getLogger(CameraActivity.class);
public static final String intentExtraEvent = "EVENT";
private ListenableFuture<ProcessCameraProvider> cameraProviderListenableFuture;
private ImageCapture imageCapture;
private boolean reportClosing = true;
public static boolean supportsCamera() {
return GBApplication.getContext().getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY);
}
public static boolean hasCameraPermission() {
return GBApplication.getContext().checkCallingOrSelfPermission(Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED;
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_camera);
if (!supportsCamera()) {
LOG.error("No camera support");
GB.toast(getString(R.string.toast_camera_support_required), Toast.LENGTH_SHORT, GB.ERROR);
GBApplication.deviceService().onCameraStatusChange(GBDeviceEventCameraRemote.Event.EXCEPTION, null);
reportClosing = false;
finish();
return;
}
if (!hasCameraPermission()) {
LOG.info("Requesting camera permission");
ActivityResultLauncher<String> requestPermissionLauncher =
registerForActivityResult(new ActivityResultContracts.RequestPermission(), new ActivityResultCallback<Boolean>() {
@Override
public void onActivityResult(Boolean isGranted) {
if (isGranted) {
initCamera();
} else {
LOG.error("Did not receive camera permission");
GB.toast(getString(R.string.toast_camera_permission_required), Toast.LENGTH_SHORT, GB.ERROR);
GBApplication.deviceService().onCameraStatusChange(GBDeviceEventCameraRemote.Event.EXCEPTION, null);
reportClosing = false;
finish();
}
}
});
requestPermissionLauncher.launch(Manifest.permission.CAMERA);
return;
}
initCamera();
}
private void initCamera() {
cameraProviderListenableFuture = ProcessCameraProvider.getInstance(this);
cameraProviderListenableFuture.addListener(new Runnable() {
@Override
public void run() {
try {
ProcessCameraProvider cameraProvider = cameraProviderListenableFuture.get();
PreviewView previewView = findViewById(R.id.preview);
Preview preview = new Preview.Builder().build();
CameraSelector cameraSelector = new CameraSelector.Builder()
.requireLensFacing(CameraSelector.LENS_FACING_BACK) // TODO: make setting
.build();
preview.setSurfaceProvider(previewView.getSurfaceProvider());
imageCapture = new ImageCapture.Builder()
.setTargetRotation(preview.getTargetRotation())
.build();
cameraProvider.bindToLifecycle(
CameraActivity.this,
cameraSelector,
imageCapture,
preview
);
} catch (ExecutionException | InterruptedException e) {
throw new RuntimeException(e);
}
}
}, ContextCompat.getMainExecutor(this));
handleIntent(getIntent());
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
handleIntent(intent);
}
private void handleIntent(Intent intent) {
if (!intent.hasExtra(intentExtraEvent)) {
this.finish();
return;
}
GBDeviceEventCameraRemote.Event event = GBDeviceEventCameraRemote.intToEvent(intent.getIntExtra(intentExtraEvent, 0));
LOG.info("Camera received event: " + event.name());
// Nothing to do for unknown events
if (event == GBDeviceEventCameraRemote.Event.CLOSE_CAMERA) {
finish();
} else if (event == GBDeviceEventCameraRemote.Event.OPEN_CAMERA) {
GBApplication.deviceService().onCameraStatusChange(GBDeviceEventCameraRemote.Event.OPEN_CAMERA, null);
} else if (event == GBDeviceEventCameraRemote.Event.TAKE_PICTURE) {
ContentValues contentValues = new ContentValues();
contentValues.put(MediaStore.Images.Media.TITLE, "Gadgetbridge photo");
contentValues.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
ImageCapture.OutputFileOptions outputFileOptions = new ImageCapture.OutputFileOptions.Builder(
getContentResolver(),
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
contentValues
).build();
imageCapture.takePicture(outputFileOptions, ContextCompat.getMainExecutor(this), new ImageCapture.OnImageSavedCallback() {
@Override
public void onImageSaved(@NonNull ImageCapture.OutputFileResults outputFileResults) {
if (outputFileResults.getSavedUri() == null) {
// Shouldn't ever happen
GBApplication.deviceService().onCameraStatusChange(GBDeviceEventCameraRemote.Event.EXCEPTION, null);
return;
}
// TODO: improve feedback that the photo has been taken
GB.toast(
String.format(getString(R.string.toast_camera_photo_taken),
outputFileResults.getSavedUri().getPath()),
Toast.LENGTH_LONG,
GB.INFO
);
GBApplication.deviceService().onCameraStatusChange(
GBDeviceEventCameraRemote.Event.TAKE_PICTURE,
outputFileResults.getSavedUri().getPath()
);
}
@Override
public void onError(@NonNull ImageCaptureException exception) {
LOG.error("Failed to save image", exception);
GBApplication.deviceService().onCameraStatusChange(GBDeviceEventCameraRemote.Event.EXCEPTION, null);
}
});
}
}
@Override
protected void onStop() {
super.onStop();
if (reportClosing)
GBApplication.deviceService().onCameraStatusChange(GBDeviceEventCameraRemote.Event.CLOSE_CAMERA, null);
}
}

View File

@ -102,6 +102,7 @@ import nodomain.freeyourgadget.gadgetbridge.adapter.SpinnerWithIconAdapter;
import nodomain.freeyourgadget.gadgetbridge.adapter.SpinnerWithIconItem;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCameraRemote;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
@ -752,6 +753,18 @@ public class DebugActivity extends AbstractGBActivity {
handler.postDelayed(runnable, delay);
}
});
Button cameraOpenButton = findViewById(R.id.cameraOpen);
cameraOpenButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent cameraIntent = new Intent(getApplicationContext(), CameraActivity.class);
cameraIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
cameraIntent.putExtra(CameraActivity.intentExtraEvent, GBDeviceEventCameraRemote.eventToInt(GBDeviceEventCameraRemote.Event.OPEN_CAMERA));
getApplicationContext().startActivity(cameraIntent);
}
});
}
@RequiresApi(Build.VERSION_CODES.O)

View File

@ -46,6 +46,7 @@ import java.util.Objects;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
import nodomain.freeyourgadget.gadgetbridge.activities.discovery.DiscoveryActivityV2;
import nodomain.freeyourgadget.gadgetbridge.adapter.GBDeviceAdapterv2;
import nodomain.freeyourgadget.gadgetbridge.database.DBAccess;
@ -233,7 +234,8 @@ public class DevicesFragment extends Fragment {
protected void doInBackground(DBHandler db) {
for (GBDevice gbDevice : deviceList) {
final DeviceCoordinator coordinator = gbDevice.getDeviceCoordinator();
if (coordinator.supportsActivityTracking()) {
boolean showActivityCard = GBApplication.getDevicePrefs(gbDevice.getAddress()).getBoolean(DeviceSettingsPreferenceConst.PREFS_ACTIVITY_IN_DEVICE_CARD, true);
if (coordinator.supportsActivityTracking() && showActivityCard) {
long[] stepsAndSleepData = getSteps(gbDevice, db);
deviceActivityHashMap.put(gbDevice.getAddress(), stepsAndSleepData);
}

View File

@ -68,7 +68,6 @@ import nodomain.freeyourgadget.gadgetbridge.activities.discovery.DiscoveryPairin
import nodomain.freeyourgadget.gadgetbridge.database.PeriodicExporter;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandPreferencesActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleSettingsActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.ConfigActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.zetime.ZeTimePreferenceActivity;
import nodomain.freeyourgadget.gadgetbridge.externalevents.TimeChangeReceiver;
import nodomain.freeyourgadget.gadgetbridge.model.Weather;
@ -138,14 +137,6 @@ public class SettingsActivity extends AbstractSettingsActivityV2 {
});
}
pref = findPreference("pref_key_qhybrid");
if (pref != null) {
pref.setOnPreferenceClickListener(preference -> {
startActivity(new Intent(requireContext(), ConfigActivity.class));
return true;
});
}
pref = findPreference("pref_key_pebble");
if (pref != null) {
pref.setOnPreferenceClickListener(preference -> {

View File

@ -395,7 +395,7 @@ public class DashboardTodayWidget extends AbstractDashboardWidget {
if (firstTimestamp == 0) firstTimestamp = sample.getTimestamp();
if (lastTimestamp == 0) lastTimestamp = sample.getTimestamp();
if ((sample.getHeartRate() < 10 || sample.getTimestamp() > lastTimestamp + dashboardData.hrIntervalSecs) && firstTimestamp != lastTimestamp) {
LOG.info("Registered worn session from " + firstTimestamp + " to " + lastTimestamp);
LOG.debug("Registered worn session from {} to {}", firstTimestamp, lastTimestamp);
addActivity(firstTimestamp, lastTimestamp, ActivityKind.TYPE_NOT_MEASURED);
if (sample.getHeartRate() < 10) {
firstTimestamp = 0;
@ -409,7 +409,7 @@ public class DashboardTodayWidget extends AbstractDashboardWidget {
lastTimestamp = sample.getTimestamp();
}
if (firstTimestamp != lastTimestamp) {
LOG.info("Registered worn session from " + firstTimestamp + " to " + lastTimestamp);
LOG.debug("Registered worn session from {} to {}", firstTimestamp, lastTimestamp);
addActivity(firstTimestamp, lastTimestamp, ActivityKind.TYPE_NOT_MEASURED);
}
}

View File

@ -343,6 +343,25 @@ public class DeviceSettingsPreferenceConst {
public static final String PREF_SONY_PROTOCOL_VERSION = "pref_protocol_version";
public static final String PREF_SONY_ACTUAL_PROTOCOL_VERSION = "pref_actual_protocol_version";
public static final String PREF_SONY_AMBIENT_SOUND_CONTROL = "pref_sony_ambient_sound_control";
public static final String PREF_SOUNDCORE_AMBIENT_SOUND_CONTROL = "pref_soundcore_ambient_sound_control";
public static final String PREF_SOUNDCORE_ADAPTIVE_NOISE_CANCELLING = "pref_adaptive_noise_cancelling";
public static final String PREF_SOUNDCORE_WIND_NOISE_REDUCTION= "pref_soundcore_wind_noise_reduction";
public static final String PREF_SOUNDCORE_TRANSPARENCY_VOCAL_MODE = "pref_soundcore_transparency_vocal_mode";
public static final String PREF_SOUNDCORE_WEARING_DETECTION = "pref_soundcore_wearing_detection";
public static final String PREF_SOUNDCORE_WEARING_TONE = "pref_soundcore_wearing_tone";
public static final String PREF_SOUNDCORE_TOUCH_TONE = "pref_soundcore_touch_tone";
public static final String PREF_SOUNDCORE_CONTROL_SINGLE_TAP_DISABLED = "pref_soundcore_control_single_tap_disabled";
public static final String PREF_SOUNDCORE_CONTROL_DOUBLE_TAP_DISABLED = "pref_soundcore_control_double_tap_disabled";
public static final String PREF_SOUNDCORE_CONTROL_TRIPLE_TAP_DISABLED = "pref_soundcore_control_triple_tap_disabled";
public static final String PREF_SOUNDCORE_CONTROL_LONG_PRESS_DISABLED = "pref_soundcore_control_long_press_disabled";
public static final String PREF_SOUNDCORE_CONTROL_SINGLE_TAP_ACTION_LEFT = "pref_soundcore_control_single_tap_action_left";
public static final String PREF_SOUNDCORE_CONTROL_DOUBLE_TAP_ACTION_LEFT = "pref_soundcore_control_double_tap_action_left";
public static final String PREF_SOUNDCORE_CONTROL_TRIPLE_TAP_ACTION_LEFT = "pref_soundcore_control_triple_tap_action_left";
public static final String PREF_SOUNDCORE_CONTROL_LONG_PRESS_ACTION_LEFT = "pref_soundcore_control_long_press_action_left";
public static final String PREF_SOUNDCORE_CONTROL_SINGLE_TAP_ACTION_RIGHT = "pref_soundcore_control_single_tap_action_right";
public static final String PREF_SOUNDCORE_CONTROL_DOUBLE_TAP_ACTION_RIGHT = "pref_soundcore_control_double_tap_action_right";
public static final String PREF_SOUNDCORE_CONTROL_TRIPLE_TAP_ACTION_RIGHT = "pref_soundcore_control_triple_tap_action_right";
public static final String PREF_SOUNDCORE_CONTROL_LONG_PRESS_ACTION_RIGHT = "pref_soundcore_control_long_press_action_right";
public static final String PREF_SONY_AMBIENT_SOUND_CONTROL_BUTTON_MODE = "pref_sony_ambient_sound_control_button_mode";
public static final String PREF_SONY_FOCUS_VOICE = "pref_sony_focus_voice";
public static final String PREF_SONY_AMBIENT_SOUND_LEVEL = "pref_sony_ambient_sound_level";

View File

@ -563,6 +563,27 @@ public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment i
addPreferenceHandlerFor(PREF_SONY_CONNECT_TWO_DEVICES);
addPreferenceHandlerFor(PREF_SONY_ADAPTIVE_VOLUME_CONTROL);
addPreferenceHandlerFor(PREF_SONY_WIDE_AREA_TAP);
addPreferenceHandlerFor(PREF_SOUNDCORE_AMBIENT_SOUND_CONTROL);
addPreferenceHandlerFor(PREF_SOUNDCORE_WIND_NOISE_REDUCTION);
addPreferenceHandlerFor(PREF_SOUNDCORE_TRANSPARENCY_VOCAL_MODE);
addPreferenceHandlerFor(PREF_SOUNDCORE_ADAPTIVE_NOISE_CANCELLING);
addPreferenceHandlerFor(PREF_SOUNDCORE_TOUCH_TONE);
addPreferenceHandlerFor(PREF_SOUNDCORE_WEARING_TONE);
addPreferenceHandlerFor(PREF_SOUNDCORE_WEARING_DETECTION);
addPreferenceHandlerFor(PREF_SOUNDCORE_CONTROL_SINGLE_TAP_DISABLED);
addPreferenceHandlerFor(PREF_SOUNDCORE_CONTROL_DOUBLE_TAP_DISABLED);
addPreferenceHandlerFor(PREF_SOUNDCORE_CONTROL_TRIPLE_TAP_DISABLED);
addPreferenceHandlerFor(PREF_SOUNDCORE_CONTROL_LONG_PRESS_DISABLED);
addPreferenceHandlerFor(PREF_SOUNDCORE_CONTROL_SINGLE_TAP_ACTION_LEFT);
addPreferenceHandlerFor(PREF_SOUNDCORE_CONTROL_SINGLE_TAP_ACTION_RIGHT);
addPreferenceHandlerFor(PREF_SOUNDCORE_CONTROL_DOUBLE_TAP_ACTION_LEFT);
addPreferenceHandlerFor(PREF_SOUNDCORE_CONTROL_DOUBLE_TAP_ACTION_RIGHT);
addPreferenceHandlerFor(PREF_SOUNDCORE_CONTROL_TRIPLE_TAP_ACTION_LEFT);
addPreferenceHandlerFor(PREF_SOUNDCORE_CONTROL_TRIPLE_TAP_ACTION_RIGHT);
addPreferenceHandlerFor(PREF_SOUNDCORE_CONTROL_LONG_PRESS_ACTION_LEFT);
addPreferenceHandlerFor(PREF_SOUNDCORE_CONTROL_LONG_PRESS_ACTION_RIGHT);
addPreferenceHandlerFor(PREF_FEMOMETER_MEASUREMENT_MODE);
addPreferenceHandlerFor(PREF_QC35_NOISE_CANCELLING_LEVEL);

View File

@ -34,6 +34,7 @@ public enum DeviceSpecificSettingsScreen {
DATE_TIME("pref_screen_date_time", R.xml.devicesettings_root_date_time),
WORKOUT("pref_screen_workout", R.xml.devicesettings_root_workout),
HEALTH("pref_screen_health", R.xml.devicesettings_root_health),
TOUCH_OPTIONS("pref_screen_touch_options", R.xml.devicesettings_root_touch_options),
;
private final String key;

View File

@ -1316,6 +1316,13 @@ public class GBDeviceAdapterv2 extends ListAdapter<GBDevice, GBDeviceAdapterv2.V
}
private void setActivityCard(ViewHolder holder, final GBDevice device, long[] dailyTotals) {
boolean showActivityCard = GBApplication.getDeviceSpecificSharedPrefs(device.getAddress()).getBoolean(DeviceSettingsPreferenceConst.PREFS_ACTIVITY_IN_DEVICE_CARD, true);
holder.cardViewActivityCardLayout.setVisibility(showActivityCard ? View.VISIBLE : View.GONE);
if (!showActivityCard) {
return;
}
int steps = (int) dailyTotals[0];
int sleep = (int) dailyTotals[1];
ActivityUser activityUser = new ActivityUser();
@ -1336,8 +1343,6 @@ public class GBDeviceAdapterv2 extends ListAdapter<GBDevice, GBDeviceAdapterv2.V
setUpChart(holder.SleepTimeChart);
setChartsData(holder.SleepTimeChart, sleep, sleepGoalMinutes, context.getString(R.string.prefs_activity_in_device_card_sleep_title), String.format("%1s", getHM(sleep)), context);
boolean showActivityCard = GBApplication.getDeviceSpecificSharedPrefs(device.getAddress()).getBoolean(DeviceSettingsPreferenceConst.PREFS_ACTIVITY_IN_DEVICE_CARD, true);
holder.cardViewActivityCardLayout.setVisibility(showActivityCard ? View.VISIBLE : View.GONE);
boolean showActivitySteps = GBApplication.getDeviceSpecificSharedPrefs(device.getAddress()).getBoolean(DeviceSettingsPreferenceConst.PREFS_ACTIVITY_IN_DEVICE_CARD_STEPS, true);
boolean showActivitySleep = GBApplication.getDeviceSpecificSharedPrefs(device.getAddress()).getBoolean(DeviceSettingsPreferenceConst.PREFS_ACTIVITY_IN_DEVICE_CARD_SLEEP, true);

View File

@ -0,0 +1,57 @@
/* Copyright (C) 2024 Martin.JM
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.deviceevents;
public class GBDeviceEventCameraRemote extends GBDeviceEvent {
public Event event = Event.UNKNOWN;
public enum Event {
UNKNOWN,
OPEN_CAMERA,
TAKE_PICTURE,
CLOSE_CAMERA,
EXCEPTION
}
static public int eventToInt(Event event) {
switch (event) {
case UNKNOWN:
return 0;
case OPEN_CAMERA:
return 1;
case TAKE_PICTURE:
return 2;
case CLOSE_CAMERA:
return 3;
}
return -1;
}
static public Event intToEvent(int event) {
switch (event) {
case 0:
return Event.UNKNOWN;
case 1:
return Event.OPEN_CAMERA;
case 2:
return Event.TAKE_PICTURE;
case 3:
return Event.CLOSE_CAMERA;
}
return Event.EXCEPTION;
}
}

View File

@ -100,6 +100,25 @@ public abstract class AbstractTimeSampleProvider<T extends AbstractTimeSample> i
return samples.get(0);
}
public T getLastSampleBefore(final long timestampTo) {
final Device dbDevice = DBHelper.findDevice(getDevice(), getSession());
if (dbDevice == null) {
// no device, no sample
return null;
}
final Property deviceIdSampleProp = getDeviceIdentifierSampleProperty();
final Property timestampSampleProp = getTimestampSampleProperty();
final List<T> samples = getSampleDao().queryBuilder()
.where(deviceIdSampleProp.eq(dbDevice.getId()),
timestampSampleProp.le(timestampTo))
.orderDesc(getTimestampSampleProperty())
.limit(1)
.list();
return !samples.isEmpty() ? samples.get(0) : null;
}
@Nullable
@Override
public T getFirstSample() {

View File

@ -26,6 +26,7 @@ import java.util.ArrayList;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.capabilities.loyaltycards.LoyaltyCard;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCameraRemote;
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
@ -151,4 +152,6 @@ public interface EventHandler {
void onSetGpsLocation(Location location);
void onSleepAsAndroidAction(String action, Bundle extras);
void onCameraStatusChange(GBDeviceEventCameraRemote.Event event, String filename);
}

View File

@ -62,12 +62,15 @@ public final class HuaweiConstants {
public static final String HU_TALKBANDB6_NAME = "huawei b6-";
public static final String HU_BAND7_NAME = "huawei band 7-";
public static final String HU_BAND8_NAME = "huawei band 8-";
public static final String HU_BAND9_NAME = "huawei band 9-";
public static final String HU_WATCHGT3_NAME = "huawei watch gt 3-";
public static final String HU_WATCHGT3PRO_NAME = "huawei watch gt 3 pro-";
public static final String HU_WATCHGT4_NAME = "huawei watch gt 4-";
public static final String HU_WATCHFIT_NAME = "huawei watch fit-";
public static final String HU_WATCHFIT2_NAME = "huawei watch fit 2-";
public static final String HU_WATCHFIT3_NAME = "huawei watch fit 3-";
public static final String HU_WATCHULTIMATE_NAME = "huawei watch ultimate-";
public static final String HU_WATCH4PRO_NAME = "huawei watch 4 pro-";
public static final String PREF_HUAWEI_ADDRESS = "huawei_address";
public static final String PREF_HUAWEI_WORKMODE = "workmode";

View File

@ -32,6 +32,7 @@ import org.slf4j.Logger;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.CameraActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.appmanager.AppManagerActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettings;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsScreen;
@ -220,6 +221,10 @@ public class HuaweiCoordinator {
deviceSpecificSettings.addRootScreen(R.xml.devicesettings_disable_find_phone_with_dnd);
deviceSpecificSettings.addRootScreen(R.xml.devicesettings_allow_accept_reject_calls);
// Camera control
if (supportsCameraRemote())
deviceSpecificSettings.addRootScreen(R.xml.devicesettings_camera_remote);
// Time
if (supportsDateFormat()) {
final List<Integer> dateTime = deviceSpecificSettings.addRootScreen(DeviceSpecificSettingsScreen.DATE_TIME);
@ -281,6 +286,10 @@ public class HuaweiCoordinator {
return supportsCommandForService(0x01, 0x1d);
}
public boolean supportsCameraRemote() {
return supportsCommandForService(0x01, 0x29) && CameraActivity.supportsCamera();
}
public boolean supportsAcceptAgreement() {
return supportsCommandForService(0x01, 0x30);
}

View File

@ -32,6 +32,7 @@ import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Alarms;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.AccountRelated;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Calls;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.CameraRemote;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.GpsAndTime;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Watchface;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Weather;
@ -430,6 +431,11 @@ public class HuaweiPacket {
return new DeviceConfig.SecurityNegotiation.Response(paramsProvider).fromPacket(this);
case DeviceConfig.WearStatus.id:
return new DeviceConfig.WearStatus.Response(paramsProvider).fromPacket(this);
// Camera remote has same ID as DeviceConfig
case CameraRemote.CameraRemoteStatus.id:
return new CameraRemote.CameraRemoteStatus.Response(paramsProvider).fromPacket(this);
default:
this.isEncrypted = this.attemptDecrypt(); // Helps with debugging
return this;

View File

@ -0,0 +1,71 @@
/* Copyright (C) 2024 Damien Gaignon, Martin.JM
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.huawei.huaweiband9;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.regex.Pattern;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiLECoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiSpo2SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
public class HuaweiBand9Coordinator extends HuaweiLECoordinator {
private static final Logger LOG = LoggerFactory.getLogger(HuaweiBand9Coordinator.class);
@Override
public boolean isExperimental() {
return true;
}
@Override
public DeviceType getDeviceType() {
return DeviceType.HUAWEIBAND9;
}
@Override
protected Pattern getSupportedDeviceName() {
return Pattern.compile(HuaweiConstants.HU_BAND9_NAME + ".*", Pattern.CASE_INSENSITIVE);
}
@Override
public boolean supportsHeartRateMeasurement(GBDevice device) {
return true;
}
@Override
public boolean supportsSpo2() {
return true;
}
@Override
public TimeSampleProvider<? extends Spo2Sample> getSpo2SampleProvider(GBDevice device, DaoSession session) {
return new HuaweiSpo2SampleProvider(device, session);
}
@Override
public int getDeviceNameResource() {
return R.string.devicetype_huawei_band9;
}
}

View File

@ -0,0 +1,51 @@
/* Copyright (C) 2024 Damien Gaignon
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.huawei.huaweiwatch4pro;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.regex.Pattern;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiBRCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
public class HuaweiWatch4ProCoordinator extends HuaweiBRCoordinator {
private static final Logger LOG = LoggerFactory.getLogger(HuaweiWatch4ProCoordinator.class);
public HuaweiWatch4ProCoordinator() {
super();
getHuaweiCoordinator().setTransactionCrypted(true);
}
@Override
public DeviceType getDeviceType() {
return DeviceType.HUAWEIWATCH4PRO;
}
@Override
protected Pattern getSupportedDeviceName() {
return Pattern.compile("(" + HuaweiConstants.HU_WATCH4PRO_NAME + ").*", Pattern.CASE_INSENSITIVE);
}
@Override
public int getDeviceNameResource() {
return R.string.devicetype_huawei_watch4pro;
}
}

View File

@ -0,0 +1,56 @@
/* Copyright (C) 2024 Damien Gaignon
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.huawei.huaweiwatchfit3;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.regex.Pattern;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiBRCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
public class HuaweiWatchFit3Coordinator extends HuaweiBRCoordinator {
private static final Logger LOG = LoggerFactory.getLogger(HuaweiWatchFit3Coordinator.class);
public HuaweiWatchFit3Coordinator() {
super();
getHuaweiCoordinator().setTransactionCrypted(true);
}
@Override
public boolean isExperimental() {
return true;
}
@Override
public DeviceType getDeviceType() {
return DeviceType.HUAWEIWATCHFIT3;
}
@Override
protected Pattern getSupportedDeviceName() {
return Pattern.compile("(" + HuaweiConstants.HU_WATCHFIT3_NAME + ").*", Pattern.CASE_INSENSITIVE);
}
@Override
public int getDeviceNameResource() {
return R.string.devicetype_huawei_watchfit3;
}
}

View File

@ -0,0 +1,110 @@
/* Copyright (C) 2024 Martin.JM
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV;
public class CameraRemote {
public static final byte id = 0x01;
public static class CameraRemoteSetup {
public static final byte id = 0x2a;
public static class Request extends HuaweiPacket {
public enum Event {
ENABLE_CAMERA,
CAMERA_STARTED,
CAMERA_STOPPED
}
public Request(ParamsProvider paramsProvider, Event event) {
super(paramsProvider);
this.serviceId = CameraRemote.id;
this.commandId = id;
this.tlv = new HuaweiTLV();
switch (event) {
case ENABLE_CAMERA:
this.tlv.put(0x01, (byte) 0x00);
break;
case CAMERA_STARTED:
this.tlv.put(0x01, (byte) 0x01);
break;
case CAMERA_STOPPED:
this.tlv.put(0x01, (byte) 0x02);
break;
}
this.complete = true;
this.isEncrypted = true;
}
}
}
public static class CameraRemoteStatus {
public static final byte id = 0x29;
public static class Request extends HuaweiPacket {
// All responses are async, and must be ACK-ed
public Request(ParamsProvider paramsProvider) {
super(paramsProvider);
this.serviceId = CameraRemote.id;
this.commandId = id;
this.tlv = new HuaweiTLV()
.put(0x7f, 0x186A0);
this.complete = true;
this.isEncrypted = true;
}
}
public static class Response extends HuaweiPacket {
public enum Event {
OPEN_CAMERA,
TAKE_PICTURE,
CLOSE_CAMERA
}
public Event event;
public Response(ParamsProvider paramsProvider) {
super(paramsProvider);
this.serviceId = CameraRemote.id;
this.commandId = id;
}
@Override
public void parseTlv() throws ParseException {
switch (this.tlv.getByte(0x01)) {
case 1:
this.event = Event.OPEN_CAMERA;
break;
case 2:
this.event = Event.TAKE_PICTURE;
break;
case 3:
this.event = Event.CLOSE_CAMERA;
break;
}
}
}
}
}

View File

@ -1178,7 +1178,6 @@ public class DeviceConfig {
public JSONObject value;
public JSONObject payload;
public JSONObject version;
public byte step;
// public int operationCode; // TODO
@ -1203,7 +1202,6 @@ public class DeviceConfig {
try {
this.value = new JSONObject(this.tlv.getString(0x01));
this.payload = value.getJSONObject("payload");
this.version = payload.getJSONObject("version");
// Ugly, but should work
if (payload.has("isoSalt")) {
@ -1322,7 +1320,6 @@ public class DeviceConfig {
public long requestId;
public byte[] selfAuthId;
public String groupId;
public JSONObject version = null;
public JSONObject payload = null;
public JSONObject value = null;
@ -1341,7 +1338,6 @@ public class DeviceConfig {
try {
value = new JSONObject(this.tlv.getString(0x01));
payload = value.getJSONObject("payload");
version = payload.getJSONObject("version");
if (payload.has("isoSalt")) {
this.step = 1;

View File

@ -80,7 +80,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.foss
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.buttonconfig.ConfigPayload;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class ConfigActivity extends AbstractGBActivity {
public class QHybridConfigActivity extends AbstractGBActivity {
PackageAdapter adapter;
ArrayList<NotificationConfiguration> list;
PackageConfigHelper helper;
@ -97,45 +97,53 @@ public class ConfigActivity extends AbstractGBActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
device = getIntent().getParcelableExtra(GBDevice.EXTRA_DEVICE);
super.onCreate(savedInstanceState);
if (device == null) {
GB.toast(this, "Device is null", Toast.LENGTH_LONG, GB.ERROR);
finish();
return;
}
setContentView(R.layout.activity_qhybrid_settings);
findViewById(R.id.buttonOverwriteButtons).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
LocalBroadcastManager.getInstance(ConfigActivity.this).sendBroadcast(new Intent(QHybridSupport.QHYBRID_COMMAND_OVERWRITE_BUTTONS));
LocalBroadcastManager.getInstance(QHybridConfigActivity.this).sendBroadcast(new Intent(QHybridSupport.QHYBRID_COMMAND_OVERWRITE_BUTTONS));
}
});
prefs = getSharedPreferences(getPackageName(), MODE_PRIVATE);
prefs = GBApplication.getDeviceSpecificSharedPrefs(device.getAddress());
timeOffsetView = findViewById(R.id.qhybridTimeOffset);
timeOffsetView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
int timeOffset = prefs.getInt("QHYBRID_TIME_OFFSET", 0);
LinearLayout layout2 = new LinearLayout(ConfigActivity.this);
LinearLayout layout2 = new LinearLayout(QHybridConfigActivity.this);
layout2.setOrientation(LinearLayout.HORIZONTAL);
final NumberPicker hourPicker = new NumberPicker(ConfigActivity.this);
final NumberPicker hourPicker = new NumberPicker(QHybridConfigActivity.this);
hourPicker.setMinValue(0);
hourPicker.setMaxValue(23);
hourPicker.setValue(timeOffset / 60);
final NumberPicker minPicker = new NumberPicker(ConfigActivity.this);
final NumberPicker minPicker = new NumberPicker(QHybridConfigActivity.this);
minPicker.setMinValue(0);
minPicker.setMaxValue(59);
minPicker.setValue(timeOffset % 60);
layout2.addView(hourPicker);
TextView tw = new TextView(ConfigActivity.this);
TextView tw = new TextView(QHybridConfigActivity.this);
tw.setText(":");
layout2.addView(tw);
layout2.addView(minPicker);
layout2.setGravity(Gravity.CENTER);
new MaterialAlertDialogBuilder(ConfigActivity.this)
new MaterialAlertDialogBuilder(QHybridConfigActivity.this)
.setTitle(getString(R.string.qhybrid_offset_time_by))
.setView(layout2)
.setPositiveButton("ok", new DialogInterface.OnClickListener() {
@ -143,7 +151,7 @@ public class ConfigActivity extends AbstractGBActivity {
public void onClick(DialogInterface dialogInterface, int i) {
prefs.edit().putInt("QHYBRID_TIME_OFFSET", hourPicker.getValue() * 60 + minPicker.getValue()).apply();
updateTimeOffset();
LocalBroadcastManager.getInstance(ConfigActivity.this).sendBroadcast(new Intent(QHybridSupport.QHYBRID_COMMAND_UPDATE));
LocalBroadcastManager.getInstance(QHybridConfigActivity.this).sendBroadcast(new Intent(QHybridSupport.QHYBRID_COMMAND_UPDATE));
GB.toast(getString(R.string.qhybrid_changes_delay_prompt), Toast.LENGTH_SHORT, GB.INFO);
}
})
@ -159,28 +167,28 @@ public class ConfigActivity extends AbstractGBActivity {
@Override
public void onClick(View view) {
int timeOffset = prefs.getInt("QHYBRID_TIMEZONE_OFFSET", 0);
LinearLayout layout2 = new LinearLayout(ConfigActivity.this);
LinearLayout layout2 = new LinearLayout(QHybridConfigActivity.this);
layout2.setOrientation(LinearLayout.HORIZONTAL);
final NumberPicker hourPicker = new NumberPicker(ConfigActivity.this);
final NumberPicker hourPicker = new NumberPicker(QHybridConfigActivity.this);
hourPicker.setMinValue(0);
hourPicker.setMaxValue(23);
hourPicker.setValue(timeOffset / 60);
final NumberPicker minPicker = new NumberPicker(ConfigActivity.this);
final NumberPicker minPicker = new NumberPicker(QHybridConfigActivity.this);
minPicker.setMinValue(0);
minPicker.setMaxValue(59);
minPicker.setValue(timeOffset % 60);
layout2.addView(hourPicker);
TextView tw = new TextView(ConfigActivity.this);
TextView tw = new TextView(QHybridConfigActivity.this);
tw.setText(":");
layout2.addView(tw);
layout2.addView(minPicker);
layout2.setGravity(Gravity.CENTER);
new MaterialAlertDialogBuilder(ConfigActivity.this)
new MaterialAlertDialogBuilder(QHybridConfigActivity.this)
.setTitle(getString(R.string.qhybrid_offset_timezone))
.setView(layout2)
.setPositiveButton("ok", new DialogInterface.OnClickListener() {
@ -188,7 +196,7 @@ public class ConfigActivity extends AbstractGBActivity {
public void onClick(DialogInterface dialogInterface, int i) {
prefs.edit().putInt("QHYBRID_TIMEZONE_OFFSET", hourPicker.getValue() * 60 + minPicker.getValue()).apply();
updateTimezoneOffset();
LocalBroadcastManager.getInstance(ConfigActivity.this).sendBroadcast(new Intent(QHybridSupport.QHYBRID_COMMAND_UPDATE_TIMEZONE));
LocalBroadcastManager.getInstance(QHybridConfigActivity.this).sendBroadcast(new Intent(QHybridSupport.QHYBRID_COMMAND_UPDATE_TIMEZONE));
GB.toast(getString(R.string.qhybrid_changes_delay_prompt), Toast.LENGTH_SHORT, GB.INFO);
}
})
@ -215,7 +223,7 @@ public class ConfigActivity extends AbstractGBActivity {
appList.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
@Override
public boolean onItemLongClick(final AdapterView<?> adapterView, View view, final int i, long l) {
PopupMenu menu = new PopupMenu(ConfigActivity.this, view);
PopupMenu menu = new PopupMenu(QHybridConfigActivity.this, view);
menu.getMenu().add(0, 0, 0, "edit");
menu.getMenu().add(0, 1, 1, "delete");
menu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
@ -223,7 +231,7 @@ public class ConfigActivity extends AbstractGBActivity {
public boolean onMenuItemClick(MenuItem menuItem) {
switch (menuItem.getItemId()) {
case 0: {
TimePicker picker = new TimePicker(ConfigActivity.this, (NotificationConfiguration) adapterView.getItemAtPosition(i));
TimePicker picker = new TimePicker(QHybridConfigActivity.this, (NotificationConfiguration) adapterView.getItemAtPosition(i));
picker.finishListener = new TimePicker.OnFinishListener() {
@Override
public void onFinish(boolean success, NotificationConfiguration config) {
@ -231,7 +239,7 @@ public class ConfigActivity extends AbstractGBActivity {
if (success) {
try {
helper.saveNotificationConfiguration(config);
LocalBroadcastManager.getInstance(ConfigActivity.this).sendBroadcast(new Intent(QHybridSupport.QHYBRID_COMMAND_NOTIFICATION_CONFIG_CHANGED));
LocalBroadcastManager.getInstance(QHybridConfigActivity.this).sendBroadcast(new Intent(QHybridSupport.QHYBRID_COMMAND_NOTIFICATION_CONFIG_CHANGED));
} catch (Exception e) {
GB.toast("error saving notification", Toast.LENGTH_SHORT, GB.ERROR, e);
}
@ -257,7 +265,7 @@ public class ConfigActivity extends AbstractGBActivity {
case 1: {
try {
helper.deleteNotificationConfiguration((NotificationConfiguration) adapterView.getItemAtPosition(i));
LocalBroadcastManager.getInstance(ConfigActivity.this).sendBroadcast(new Intent(QHybridSupport.QHYBRID_COMMAND_NOTIFICATION_CONFIG_CHANGED));
LocalBroadcastManager.getInstance(QHybridConfigActivity.this).sendBroadcast(new Intent(QHybridSupport.QHYBRID_COMMAND_NOTIFICATION_CONFIG_CHANGED));
} catch (Exception e) {
GB.toast("error deleting setting", Toast.LENGTH_SHORT, GB.ERROR, e);
}
@ -278,7 +286,7 @@ public class ConfigActivity extends AbstractGBActivity {
public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
Intent notificationIntent = new Intent(QHybridSupport.QHYBRID_COMMAND_NOTIFICATION);
notificationIntent.putExtra("CONFIG", (NotificationConfiguration) adapterView.getItemAtPosition(i));
LocalBroadcastManager.getInstance(ConfigActivity.this).sendBroadcast(notificationIntent);
LocalBroadcastManager.getInstance(QHybridConfigActivity.this).sendBroadcast(notificationIntent);
}
});
SeekBar vibeBar = findViewById(R.id.vibrationStrengthBar);
@ -302,22 +310,15 @@ public class ConfigActivity extends AbstractGBActivity {
device.addDeviceInfo(new GenericItem(QHybridSupport.ITEM_VIBRATION_STRENGTH, values[progress]));
Intent intent = new Intent(QHybridSupport.QHYBRID_COMMAND_UPDATE_SETTINGS);
intent.putExtra("EXTRA_SETTING", QHybridSupport.ITEM_VIBRATION_STRENGTH);
LocalBroadcastManager.getInstance(ConfigActivity.this).sendBroadcast(intent);
LocalBroadcastManager.getInstance(QHybridConfigActivity.this).sendBroadcast(intent);
}
});
// NOTE: this code always selects the first connected Q Hybrid device
// because currently this class is unable to handle multiple
// connected Q Hybrid devices
List<GBDevice> devices = GBApplication.app().getDeviceManager().getSelectedDevices();
for(GBDevice candidate : devices){
if (candidate.getType() == DeviceType.FOSSILQHYBRID && candidate.getFirmwareVersion().charAt(2) == '0') {
device = candidate;
updateSettings();
return;
}
if (device.getType() == DeviceType.FOSSILQHYBRID && device.isInitialized() && device.getFirmwareVersion().charAt(2) == '0') {
updateSettings();
} else {
setSettingsError(getString(R.string.watch_not_connected));
}
setSettingsError(getString(R.string.watch_not_connected));
}
private void updateTimeOffset() {
@ -362,7 +363,7 @@ public class ConfigActivity extends AbstractGBActivity {
device.addDeviceInfo(new GenericItem(QHybridSupport.ITEM_STEP_GOAL, t));
Intent intent = new Intent(QHybridSupport.QHYBRID_COMMAND_UPDATE_SETTINGS);
intent.putExtra("EXTRA_SETTING", QHybridSupport.ITEM_STEP_GOAL);
LocalBroadcastManager.getInstance(ConfigActivity.this).sendBroadcast(intent);
LocalBroadcastManager.getInstance(QHybridConfigActivity.this).sendBroadcast(intent);
updateSettings();
}
((InputMethodManager) getApplicationContext().getSystemService(Activity.INPUT_METHOD_SERVICE)).hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0);
@ -390,7 +391,7 @@ public class ConfigActivity extends AbstractGBActivity {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean checked) {
if (!device.getDeviceInfo(QHybridSupport.ITEM_STEP_GOAL).getDetails().equals("1000000")) {
new MaterialAlertDialogBuilder(ConfigActivity.this)
new MaterialAlertDialogBuilder(QHybridConfigActivity.this)
.setMessage(getString(R.string.qhybrid_prompt_million_steps))
.setPositiveButton("ok", null)
.show();
@ -400,7 +401,7 @@ public class ConfigActivity extends AbstractGBActivity {
device.addDeviceInfo(new GenericItem(QHybridSupport.ITEM_USE_ACTIVITY_HAND, String.valueOf(checked)));
Intent intent = new Intent(QHybridSupport.QHYBRID_COMMAND_UPDATE_SETTINGS);
intent.putExtra("EXTRA_SETTING", QHybridSupport.ITEM_USE_ACTIVITY_HAND);
LocalBroadcastManager.getInstance(ConfigActivity.this).sendBroadcast(intent);
LocalBroadcastManager.getInstance(QHybridConfigActivity.this).sendBroadcast(intent);
}
});
} else {
@ -440,7 +441,7 @@ public class ConfigActivity extends AbstractGBActivity {
for (int i = 0; i < buttonConfig.length(); i++) {
final int currentIndex = i;
String configName = buttonConfig.getString(i);
TextView buttonTextView = new TextView(ConfigActivity.this);
TextView buttonTextView = new TextView(QHybridConfigActivity.this);
buttonTextView.setTextSize(20);
try {
ConfigPayload payload = ConfigPayload.valueOf(configName);
@ -452,7 +453,7 @@ public class ConfigActivity extends AbstractGBActivity {
buttonTextView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
AlertDialog dialog = new MaterialAlertDialogBuilder(ConfigActivity.this)
AlertDialog dialog = new MaterialAlertDialogBuilder(QHybridConfigActivity.this)
.setItems(names, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
@ -465,7 +466,7 @@ public class ConfigActivity extends AbstractGBActivity {
updateSettings();
Intent buttonIntent = new Intent(QHybridSupport.QHYBRID_COMMAND_OVERWRITE_BUTTONS);
buttonIntent.putExtra(FossilWatchAdapter.ITEM_BUTTONS, buttonConfig.toString());
LocalBroadcastManager.getInstance(ConfigActivity.this).sendBroadcast(buttonIntent);
LocalBroadcastManager.getInstance(QHybridConfigActivity.this).sendBroadcast(buttonIntent);
} catch (JSONException e) {
GB.log("error", GB.ERROR, e);
}
@ -547,6 +548,16 @@ public class ConfigActivity extends AbstractGBActivity {
super.onActivityResult(requestCode, resultCode, data);
}
@Override
public boolean onOptionsItemSelected(@NonNull final MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
this.onBackPressed();
return true;
}
return super.onOptionsItemSelected(item);
}
private void setSettingsError(final String error) {
runOnUiThread(new Runnable() {
@ -575,13 +586,13 @@ public class ConfigActivity extends AbstractGBActivity {
NotificationConfiguration settings = getItem(position);
if (settings == null) {
Button addButton = new Button(ConfigActivity.this);
Button addButton = new Button(QHybridConfigActivity.this);
addButton.setText("+");
addButton.setLayoutParams(new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
addButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
startActivityForResult(new Intent(ConfigActivity.this, QHybridAppChoserActivity.class), REQUEST_CODE_ADD_APP);
startActivityForResult(new Intent(QHybridConfigActivity.this, QHybridAppChoserActivity.class), REQUEST_CODE_ADD_APP);
}
});
return addButton;

View File

@ -41,10 +41,12 @@ import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.appmanager.AppManagerActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettings;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsScreen;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLEDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.cmfwatchpro.CmfWatchProSettingsCustomizer;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
@ -150,7 +152,7 @@ public class QHybridCoordinator extends AbstractBLEDeviceCoordinator {
@Override
public int getCannedRepliesSlotCount(final GBDevice device) {
if (isHybridHR()) {
if (isHybridHR(device)) {
return 16;
}
@ -159,17 +161,17 @@ public class QHybridCoordinator extends AbstractBLEDeviceCoordinator {
@Override
public boolean supportsAlarmTitle(GBDevice device) {
return isHybridHR();
return isHybridHR(device);
}
@Override
public boolean supportsAlarmDescription(GBDevice device) {
return isHybridHR();
return isHybridHR(device);
}
@Override
public boolean supportsHeartRateMeasurement(GBDevice device) {
return this.isHybridHR();
return isHybridHR(device);
}
@Override
@ -189,7 +191,7 @@ public class QHybridCoordinator extends AbstractBLEDeviceCoordinator {
@Override
public Class<? extends Activity> getAppsManagementActivity() {
return isHybridHR() ? AppManagerActivity.class : ConfigActivity.class;
return isHybridHR() ? AppManagerActivity.class : QHybridConfigActivity.class;
}
@Override
@ -249,7 +251,8 @@ public class QHybridCoordinator extends AbstractBLEDeviceCoordinator {
@Override
public DeviceSpecificSettings getDeviceSpecificSettings(final GBDevice device) {
final DeviceSpecificSettings deviceSpecificSettings = new DeviceSpecificSettings();
if (!isHybridHR()) {
if (!isHybridHR(device)) {
deviceSpecificSettings.addRootScreen(R.xml.devicesettings_fossilqhybrid_legacy);
return deviceSpecificSettings;
}
final List<Integer> generic = deviceSpecificSettings.addRootScreen(DeviceSpecificSettingsScreen.GENERIC);
@ -280,6 +283,11 @@ public class QHybridCoordinator extends AbstractBLEDeviceCoordinator {
return deviceSpecificSettings;
}
@Override
public DeviceSpecificSettingsCustomizer getDeviceSpecificSettingsCustomizer(final GBDevice device) {
return new QHybridSettingsCustomizer();
}
@NonNull
@Override
public Class<? extends DeviceSupport> getDeviceSupportClass() {
@ -293,6 +301,7 @@ public class QHybridCoordinator extends AbstractBLEDeviceCoordinator {
};
}
@Deprecated // we should use the isHybridHR(GBDevice) instead of iterating every single device
private boolean isHybridHR() {
List<GBDevice> devices = GBApplication.app().getDeviceManager().getSelectedDevices();
for(GBDevice device : devices){

View File

@ -0,0 +1,60 @@
package nodomain.freeyourgadget.gadgetbridge.devices.qhybrid;
import android.content.Intent;
import android.os.Parcel;
import androidx.annotation.NonNull;
import androidx.preference.Preference;
import java.util.Collections;
import java.util.Set;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsHandler;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public class QHybridSettingsCustomizer implements DeviceSpecificSettingsCustomizer {
@Override
public void onPreferenceChange(final Preference preference, final DeviceSpecificSettingsHandler handler) {
}
@Override
public void customizeSettings(final DeviceSpecificSettingsHandler handler, final Prefs prefs) {
final Preference pref = handler.findPreference("pref_key_qhybrid_legacy");
if (pref != null) {
pref.setOnPreferenceClickListener(preference -> {
final Intent intent = new Intent(handler.getContext(), QHybridConfigActivity.class);
intent.putExtra(GBDevice.EXTRA_DEVICE, handler.getDevice());
handler.getContext().startActivity(intent);
return true;
});
}
}
@Override
public Set<String> getPreferenceKeysWithSummary() {
return Collections.emptySet();
}
public static final Creator<QHybridSettingsCustomizer> CREATOR = new Creator<QHybridSettingsCustomizer>() {
@Override
public QHybridSettingsCustomizer createFromParcel(final Parcel in) {
return new QHybridSettingsCustomizer();
}
@Override
public QHybridSettingsCustomizer[] newArray(final int size) {
return new QHybridSettingsCustomizer[size];
}
};
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(@NonNull final Parcel dest, final int flags) {
}
}

View File

@ -0,0 +1,83 @@
package nodomain.freeyourgadget.gadgetbridge.devices.soundcore;
import androidx.annotation.NonNull;
import java.util.regex.Pattern;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettings;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsScreen;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.BatteryConfig;
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore.SoundcoreLiberty3ProDeviceSupport;
public class SoundcoreLiberty3ProCoordinator extends AbstractDeviceCoordinator {
@Override
public int getDeviceNameResource() {
return R.string.devicetype_soundcore_liberty3_pro;
}
@Override
public int getDefaultIconResource() {
return R.drawable.ic_device_galaxy_buds;
}
@Override
public int getDisabledIconResource() {
return R.drawable.ic_device_galaxy_buds_disabled;
}
@Override
public String getManufacturer() {
return "Anker";
}
@Override
protected Pattern getSupportedDeviceName() {
return Pattern.compile("Soundcore Liberty 3 Pro");
}
@Override
public int getBondingStyle(){
return BONDING_STYLE_NONE;
}
@Override
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException {
}
@Override
public int getBatteryCount() {
return 3;
}
@Override
public BatteryConfig[] getBatteryConfig() {
BatteryConfig battery1 = new BatteryConfig(0, R.drawable.ic_buds_pro_case, R.string.battery_case);
BatteryConfig battery2 = new BatteryConfig(1, R.drawable.ic_nothing_ear_l, R.string.left_earbud);
BatteryConfig battery3 = new BatteryConfig(2, R.drawable.ic_nothing_ear_r, R.string.right_earbud);
return new BatteryConfig[]{battery1, battery2, battery3};
}
@Override
public DeviceSpecificSettings getDeviceSpecificSettings(final GBDevice device) {
final DeviceSpecificSettings deviceSpecificSettings = new DeviceSpecificSettings();
deviceSpecificSettings.addRootScreen(DeviceSpecificSettingsScreen.TOUCH_OPTIONS);
deviceSpecificSettings.addSubScreen(DeviceSpecificSettingsScreen.TOUCH_OPTIONS, R.xml.devicesettings_sony_headphones_ambient_sound_control_button_modes);
deviceSpecificSettings.addSubScreen(DeviceSpecificSettingsScreen.TOUCH_OPTIONS, R.xml.devicesettings_soundcore_touch_options);
deviceSpecificSettings.addRootScreen(R.xml.devicesettings_soundcore_headphones);
return deviceSpecificSettings;
}
@NonNull
@Override
public Class<? extends DeviceSupport> getDeviceSupportClass() {
return SoundcoreLiberty3ProDeviceSupport.class;
}
}

View File

@ -457,6 +457,7 @@ public abstract class XiaomiCoordinator extends AbstractBLEDeviceCoordinator {
if (getCannedRepliesSlotCount(device) > 0) {
notifications.add(R.xml.devicesettings_canned_dismisscall_16);
}
notifications.add(R.xml.devicesettings_transliteration);
//
// Calendar

View File

@ -97,60 +97,80 @@ public class XiaomiSampleProvider extends AbstractSampleProvider<XiaomiActivityS
return samples;
}
/**
* See {@link nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.activity.impl.SleepDetailsParser}
*/
private static int getActivityKindForSample(final XiaomiSleepStageSample sample) {
switch (sample.getStage()) {
case 2:
return ActivityKind.TYPE_DEEP_SLEEP;
case 3:
return ActivityKind.TYPE_LIGHT_SLEEP;
case 4:
return ActivityKind.TYPE_REM_SLEEP;
default: // default to awake
return ActivityKind.TYPE_UNKNOWN;
}
}
/**
* Overlay sleep states on activity samples, since they are stored on a separate table.
*
* @implNote This currently needs to look back a further 24h, so that we are sure that we
* got the sleep start of a sleep session at the start of the samples, if any. This is especially
* noticeable if the charts are configured in a noon-to-noon setting. FIXME: This is not ideal,
* and we may need to rethink the way sleep samples are persisted in the database for Xiaomi devices.
* @implNote In order to determine whether a sleep session was ongoing at the start of the
* given range and what the detected sleep stage was at that time, the last sleep stage and
* sleep time sample before the given range will be queried and included in the results if
* found.
*/
public void overlaySleep(final List<XiaomiActivitySample> samples, final int timestamp_from, final int timestamp_to) {
final RangeMap<Long, Integer> stagesMap = new RangeMap<>();
final XiaomiSleepStageSampleProvider sleepStagesSampleProvider = new XiaomiSleepStageSampleProvider(getDevice(), getSession());
final List<XiaomiSleepStageSample> stageSamples = sleepStagesSampleProvider.getAllSamples(
timestamp_from * 1000L - 86400000L,
// Retrieve the last stage before this time range, as the user could have been asleep during
// the range transition
final XiaomiSleepStageSample lastSleepStageBeforeRange = sleepStagesSampleProvider.getLastSampleBefore(timestamp_from * 1000L);
if (lastSleepStageBeforeRange != null) {
LOG.debug("Last sleep stage before range: ts={}, stage={}", lastSleepStageBeforeRange.getTimestamp(), lastSleepStageBeforeRange.getStage());
stagesMap.put(lastSleepStageBeforeRange.getTimestamp(), getActivityKindForSample(lastSleepStageBeforeRange));
}
// Retrieve all sleep stage samples during the range
final List<XiaomiSleepStageSample> sleepStagesInRange = sleepStagesSampleProvider.getAllSamples(
timestamp_from * 1000L,
timestamp_to * 1000L
);
if (!stageSamples.isEmpty()) {
if (!sleepStagesInRange.isEmpty()) {
// We got actual sleep stages
LOG.debug("Found {} sleep stage samples between {} and {}", stageSamples.size(), timestamp_from, timestamp_to);
LOG.debug("Found {} sleep stage samples between {} and {}", sleepStagesInRange.size(), timestamp_from, timestamp_to);
for (final XiaomiSleepStageSample stageSample : stageSamples) {
final int activityKind;
switch (stageSample.getStage()) {
case 2: // deep
activityKind = ActivityKind.TYPE_DEEP_SLEEP;
break;
case 3: // light
activityKind = ActivityKind.TYPE_LIGHT_SLEEP;
break;
case 4: // rem
activityKind = ActivityKind.TYPE_REM_SLEEP;
break;
case 0: // final awake
case 1: // ?
case 5: // awake during the night
default:
activityKind = ActivityKind.TYPE_UNKNOWN;
break;
}
stagesMap.put(stageSample.getTimestamp(), activityKind);
for (final XiaomiSleepStageSample stageSample : sleepStagesInRange) {
stagesMap.put(stageSample.getTimestamp(), getActivityKindForSample(stageSample));
}
}
// Fetch bed and wakeup times as well.
final XiaomiSleepTimeSampleProvider sleepTimeSampleProvider = new XiaomiSleepTimeSampleProvider(getDevice(), getSession());
final List<XiaomiSleepTimeSample> sleepTimeSamples = sleepTimeSampleProvider.getAllSamples(
timestamp_from * 1000L - 86400000L,
// Find last sleep sample before the requested range, as the recorded wake up time may be
// in the current range
final XiaomiSleepTimeSample lastSleepTimesBeforeRange = sleepTimeSampleProvider.getLastSampleBefore(timestamp_from * 1000L);
if (lastSleepTimesBeforeRange != null) {
stagesMap.put(lastSleepTimesBeforeRange.getWakeupTime(), ActivityKind.TYPE_UNKNOWN);
stagesMap.put(lastSleepTimesBeforeRange.getTimestamp(), ActivityKind.TYPE_LIGHT_SLEEP);
}
// Find all wake up and sleep samples in the current time range
final List<XiaomiSleepTimeSample> sleepTimesInRange = sleepTimeSampleProvider.getAllSamples(
timestamp_from * 1000L,
timestamp_to * 1000L
);
if (!sleepTimeSamples.isEmpty()) {
LOG.debug("Found {} sleep samples between {} and {}", sleepTimeSamples.size(), timestamp_from, timestamp_to);
for (final XiaomiSleepTimeSample stageSample : sleepTimeSamples) {
if (stageSamples.isEmpty()) {
if (!sleepTimesInRange.isEmpty()) {
LOG.debug("Found {} sleep samples between {} and {}", sleepTimesInRange.size(), timestamp_from, timestamp_to);
for (final XiaomiSleepTimeSample stageSample : sleepTimesInRange) {
if (sleepStagesInRange.isEmpty()) {
// Only overlay them as light sleep if we don't have actual sleep stages
stagesMap.put(stageSample.getTimestamp(), ActivityKind.TYPE_LIGHT_SLEEP);
}

View File

@ -35,6 +35,7 @@ import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.capabilities.loyaltycards.LoyaltyCard;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCameraRemote;
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
@ -558,4 +559,13 @@ public class GBDeviceService implements DeviceService {
}
invokeService(intent);
}
@Override
public void onCameraStatusChange(GBDeviceEventCameraRemote.Event event, String filename) {
Intent intent = createIntent().setAction(ACTION_CAMERA_STATUS_CHANGE);
intent.putExtra(EXTRA_CAMERA_EVENT, GBDeviceEventCameraRemote.eventToInt(event));
if (event == GBDeviceEventCameraRemote.Event.TAKE_PICTURE)
intent.putExtra(EXTRA_CAMERA_FILENAME, filename);
invokeService(intent);
}
}

View File

@ -79,6 +79,7 @@ public interface DeviceService extends EventHandler {
String ACTION_SET_GPS_LOCATION = PREFIX + ".action.set_gps_location";
String ACTION_SET_LED_COLOR = PREFIX + ".action.set_led_color";
String ACTION_POWER_OFF = PREFIX + ".action.power_off";
String ACTION_CAMERA_STATUS_CHANGE = PREFIX + ".action.camera_status_change";
String ACTION_SLEEP_AS_ANDROID = ".action.sleep_as_android";
String EXTRA_SLEEP_AS_ANDROID_ACTION = "sleepasandroid_action";
@ -144,6 +145,8 @@ public interface DeviceService extends EventHandler {
String EXTRA_LED_COLOR = "led_color";
String EXTRA_GPS_LOCATION = "gps_location";
String EXTRA_RESET_FLAGS = "reset_flags";
String EXTRA_CAMERA_EVENT = "event";
String EXTRA_CAMERA_FILENAME = "filename";
/**
* Use EXTRA_REALTIME_SAMPLE instead

View File

@ -116,10 +116,13 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huawei.huaweiband4pro.Huawei
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.huaweiband6.HuaweiBand6Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.huaweiband7.HuaweiBand7Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.huaweiband8.HuaweiBand8Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.huaweiband9.HuaweiBand9Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.huaweibandaw70.HuaweiBandAw70Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.huaweitalkbandb6.HuaweiTalkBandB6Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.huaweiwatch4pro.HuaweiWatch4ProCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.huaweiwatchfit.HuaweiWatchFitCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.huaweiwatchfit2.HuaweiWatchFit2Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.huaweiwatchfit3.HuaweiWatchFit3Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.huaweiwatchgt.HuaweiWatchGTCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.huaweiwatchgt2.HuaweiWatchGT2Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.huaweiwatchgt2e.HuaweiWatchGT2eCoordinator;
@ -168,6 +171,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators.SonyWH1000XM5Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.wena3.SonyWena3Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.sonyswr12.SonySWR12DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.soundcore.SoundcoreLiberty3ProCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.supercars.SuperCarsCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.test.TestDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.tlw64.TLW64Coordinator;
@ -348,6 +352,7 @@ public enum DeviceType {
SONY_LINKBUDS_S(SonyLinkBudsSCoordinator.class),
SONY_WH_1000XM5(SonyWH1000XM5Coordinator.class),
SONY_WF_1000XM5(SonyWF1000XM5Coordinator.class),
SOUNDCORE_LIBERTY3_PRO(SoundcoreLiberty3ProCoordinator.class),
BOSE_QC35(QC35Coordinator.class),
HONORBAND3(HonorBand3Coordinator.class),
HONORBAND4(HonorBand4Coordinator.class),
@ -366,9 +371,12 @@ public enum DeviceType {
HUAWEIWATCHGT3(HuaweiWatchGT3Coordinator.class),
HUAWEIWATCHGT4(HuaweiWatchGT4Coordinator.class),
HUAWEIBAND8(HuaweiBand8Coordinator.class),
HUAWEIBAND9(HuaweiBand9Coordinator.class),
HUAWEIWATCHFIT(HuaweiWatchFitCoordinator.class),
HUAWEIWATCHFIT2(HuaweiWatchFit2Coordinator.class),
HUAWEIWATCHFIT3(HuaweiWatchFit3Coordinator.class),
HUAWEIWATCHULTIMATE(HuaweiWatchUltimateCoordinator.class),
HUAWEIWATCH4PRO(HuaweiWatch4ProCoordinator.class),
VESC(VescCoordinator.class),
BINARY_SENSOR(BinarySensorCoordinator.class),
FLIPPER_ZERO(FlipperZeroCoordinator.class),

View File

@ -58,6 +58,7 @@ import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.CameraActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.FindPhoneActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.appmanager.AbstractAppManagerFragment;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
@ -69,6 +70,7 @@ import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventAppInfo;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCallControl;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCameraRemote;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventDisplayMessage;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFindPhone;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFmFrequency;
@ -207,6 +209,8 @@ public abstract class AbstractDeviceSupport implements DeviceSupport {
handleGBDeviceEvent((GBDeviceEventMusicControl) deviceEvent);
} else if (deviceEvent instanceof GBDeviceEventCallControl) {
handleGBDeviceEvent((GBDeviceEventCallControl) deviceEvent);
} else if (deviceEvent instanceof GBDeviceEventCameraRemote) {
handleGBDeviceEvent((GBDeviceEventCameraRemote) deviceEvent);
} else if (deviceEvent instanceof GBDeviceEventVersionInfo) {
handleGBDeviceEvent((GBDeviceEventVersionInfo) deviceEvent);
} else if (deviceEvent instanceof GBDeviceEventAppInfo) {
@ -338,6 +342,13 @@ public abstract class AbstractDeviceSupport implements DeviceSupport {
context.sendBroadcast(callIntent);
}
protected void handleGBDeviceEvent(GBDeviceEventCameraRemote cameraRemoteEvent) {
Intent cameraIntent = new Intent(getContext(), CameraActivity.class);
cameraIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
cameraIntent.putExtra(CameraActivity.intentExtraEvent, GBDeviceEventCameraRemote.eventToInt(cameraRemoteEvent.event));
getContext().startActivity(cameraIntent);
}
protected void handleGBDeviceEvent(GBDeviceEventVersionInfo infoEvent) {
Context context = getContext();
LOG.info("Got event for VERSION_INFO: " + infoEvent);
@ -1187,4 +1198,7 @@ public abstract class AbstractDeviceSupport implements DeviceSupport {
public void onSleepAsAndroidAction(String action, Bundle extras) {
}
@Override
public void onCameraStatusChange(GBDeviceEventCameraRemote.Event event, String filename) {}
}

View File

@ -62,7 +62,9 @@ import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.HeartRateUtils;
import nodomain.freeyourgadget.gadgetbridge.capabilities.loyaltycards.LoyaltyCard;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCameraRemote;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.CameraRemote;
import nodomain.freeyourgadget.gadgetbridge.externalevents.AlarmClockReceiver;
import nodomain.freeyourgadget.gadgetbridge.externalevents.AlarmReceiver;
import nodomain.freeyourgadget.gadgetbridge.externalevents.BluetoothConnectReceiver;
@ -1093,6 +1095,14 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
deviceSupport.onSleepAsAndroidAction(sleepAsAndroidAction, intent.getExtras());
}
break;
case ACTION_CAMERA_STATUS_CHANGE:
final GBDeviceEventCameraRemote.Event event = GBDeviceEventCameraRemote.intToEvent(intent.getIntExtra(EXTRA_CAMERA_EVENT, -1));
String filename = null;
if (event == GBDeviceEventCameraRemote.Event.TAKE_PICTURE) {
filename = intent.getStringExtra(EXTRA_CAMERA_FILENAME);
}
deviceSupport.onCameraStatusChange(event, filename);
break;
}
}

View File

@ -32,6 +32,7 @@ import java.util.EnumSet;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.capabilities.loyaltycards.LoyaltyCard;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCameraRemote;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
@ -520,4 +521,12 @@ public class ServiceDeviceSupport implements DeviceSupport {
}
delegate.onSleepAsAndroidAction(action, extras);
}
@Override
public void onCameraStatusChange(GBDeviceEventCameraRemote.Event event, String filename) {
if (checkBusy("camera status")) {
return;
}
delegate.onCameraStatusChange(event, filename);
}
}

View File

@ -37,12 +37,15 @@ import java.util.HashMap;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.CameraActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCallControl;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCameraRemote;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFindPhone;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventMusicControl;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Calls;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.CameraRemote;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.DeviceConfig;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FindPhone;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.GpsAndTime;
@ -110,6 +113,7 @@ public class AsynchronousResponse {
handleGpsRequest(response);
handleFileUpload(response);
handleWatchface(response);
handleCameraRemote(response);
} catch (Request.ResponseParseException e) {
LOG.error("Response parse exception", e);
}
@ -490,4 +494,37 @@ public class AsynchronousResponse {
support.setGps(((GpsAndTime.GpsStatus.Response) response).enableGps);
}
}
private void handleCameraRemote(HuaweiPacket response) {
if (response.serviceId == CameraRemote.id && response.commandId == CameraRemote.CameraRemoteStatus.id) {
if (!(response instanceof CameraRemote.CameraRemoteStatus.Response)) {
// TODO: exception?
return;
}
if (!CameraActivity.supportsCamera()) {
LOG.error("No camera present");
// TODO: Toast?
return;
}
switch (((CameraRemote.CameraRemoteStatus.Response) response).event) {
case OPEN_CAMERA:
GBDeviceEventCameraRemote openCameraEvent = new GBDeviceEventCameraRemote();
openCameraEvent.event = GBDeviceEventCameraRemote.Event.OPEN_CAMERA;
support.evaluateGBDeviceEvent(openCameraEvent);
break;
case TAKE_PICTURE:
GBDeviceEventCameraRemote takePictureEvent = new GBDeviceEventCameraRemote();
takePictureEvent.event = GBDeviceEventCameraRemote.Event.TAKE_PICTURE;
support.evaluateGBDeviceEvent(takePictureEvent);
break;
case CLOSE_CAMERA:
GBDeviceEventCameraRemote closeCameraEvent = new GBDeviceEventCameraRemote();
closeCameraEvent.event = GBDeviceEventCameraRemote.Event.CLOSE_CAMERA;
support.evaluateGBDeviceEvent(closeCameraEvent);
break;
}
}
}
}

View File

@ -24,6 +24,7 @@ import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCameraRemote;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
@ -155,4 +156,8 @@ public class HuaweiBRSupport extends AbstractBTBRDeviceSupport {
supportProvider.onAppDelete(uuid);
}
@Override
public void onCameraStatusChange(GBDeviceEventCameraRemote.Event event, String filename) {
supportProvider.onCameraStatusChange(event, filename);
}
}

View File

@ -28,6 +28,7 @@ import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCameraRemote;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
@ -163,5 +164,8 @@ public class HuaweiLESupport extends AbstractBTLEDeviceSupport {
supportProvider.onAppDelete(uuid);
}
@Override
public void onCameraStatusChange(GBDeviceEventCameraRemote.Event event, String filename) {
supportProvider.onCameraStatusChange(event, filename);
}
}

View File

@ -21,6 +21,8 @@ import android.content.Context;
import android.content.SharedPreferences;
import android.location.Location;
import android.net.Uri;
import android.os.Handler;
import android.os.SystemClock;
import android.widget.Toast;
import androidx.annotation.NonNull;
@ -44,6 +46,7 @@ import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSett
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCameraRemote;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiCoordinator;
@ -52,6 +55,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiCoordinatorSupp
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiCrypto;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.CameraRemote;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.GpsAndTime;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Weather;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Workout;
@ -87,6 +91,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetG
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetNotificationConstraintsRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetSmartAlarmList;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetWatchfaceParams;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendCameraRemoteSetupEvent;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendExtendedAccountRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendGpsAndTimeToDeviceRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendGpsDataRequest;
@ -740,6 +745,11 @@ public class HuaweiSupportProvider {
GetWatchfaceParams getWatchfaceParams = new GetWatchfaceParams(this);
getWatchfaceParams.doPerform();
}
if (getHuaweiCoordinator().supportsCameraRemote() && GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getBoolean(DeviceSettingsPreferenceConst.PREF_CAMERA_REMOTE, false)) {
SendCameraRemoteSetupEvent sendCameraRemoteSetupEvent = new SendCameraRemoteSetupEvent(this, CameraRemote.CameraRemoteSetup.Request.Event.ENABLE_CAMERA);
sendCameraRemoteSetupEvent.doPerform();
}
} catch (IOException e) {
GB.toast(getContext(), "Initialize dynamic services of Huawei device failed", Toast.LENGTH_SHORT, GB.ERROR,
e);
@ -963,6 +973,15 @@ public class HuaweiSupportProvider {
case ActivityUser.PREF_USER_STEPS_GOAL:
new SendFitnessGoalRequest(this).doPerform();
break;
case DeviceSettingsPreferenceConst.PREF_CAMERA_REMOTE:
if (GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getBoolean(DeviceSettingsPreferenceConst.PREF_CAMERA_REMOTE, false)) {
SendCameraRemoteSetupEvent sendCameraRemoteSetupEvent = new SendCameraRemoteSetupEvent(this, CameraRemote.CameraRemoteSetup.Request.Event.ENABLE_CAMERA);
sendCameraRemoteSetupEvent.doPerform();
} else {
// Somehow it is impossible to disable the camera remote
// But it will disappear after reconnection - until it is enabled again
GB.toast(context, context.getString(R.string.toast_setting_requires_reconnect), Toast.LENGTH_SHORT, GB.INFO);
}
}
} catch (IOException e) {
// TODO: Use translatable string
@ -1903,4 +1922,32 @@ public class HuaweiSupportProvider {
huaweiWatchfaceManager.deleteWatchface(uuid);
}
public void onCameraStatusChange(GBDeviceEventCameraRemote.Event event, String filename) {
if (event == GBDeviceEventCameraRemote.Event.OPEN_CAMERA) {
// Somehow a delay is necessary for the watch
new Handler(GBApplication.getContext().getMainLooper()).postDelayed(
new Runnable() {
@Override
public void run() {
SendCameraRemoteSetupEvent sendCameraRemoteSetupEvent = new SendCameraRemoteSetupEvent(HuaweiSupportProvider.this, CameraRemote.CameraRemoteSetup.Request.Event.CAMERA_STARTED);
try {
sendCameraRemoteSetupEvent.doPerform();
} catch (IOException e) {
GB.toast("Failed to send open camera request", Toast.LENGTH_SHORT, GB.ERROR, e);
LOG.error("Failed to send open camera request", e);
}
}
},
3000
);
} else if (event == GBDeviceEventCameraRemote.Event.CLOSE_CAMERA) {
SendCameraRemoteSetupEvent sendCameraRemoteSetupEvent2 = new SendCameraRemoteSetupEvent(this, CameraRemote.CameraRemoteSetup.Request.Event.CAMERA_STOPPED);
try {
sendCameraRemoteSetupEvent2.doPerform();
} catch (IOException e) {
GB.toast("Failed to send open camera request", Toast.LENGTH_SHORT, GB.ERROR, e);
LOG.error("Failed to send open camera request", e);
}
}
}
}

View File

@ -0,0 +1,47 @@
/* Copyright (C) 2024 Martin.JM
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.CameraRemote;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
public class SendCameraRemoteSetupEvent extends Request {
CameraRemote.CameraRemoteSetup.Request.Event event;
public SendCameraRemoteSetupEvent(HuaweiSupportProvider support, CameraRemote.CameraRemoteSetup.Request.Event event) {
super(support);
this.serviceId = CameraRemote.id;
this.commandId = CameraRemote.CameraRemoteSetup.id;
this.event = event;
}
@Override
protected List<byte[]> createRequest() throws RequestCreationException {
try {
return new CameraRemote.CameraRemoteSetup.Request(
supportProvider.getParamsProvider(),
this.event
).serialize();
} catch (HuaweiPacket.CryptoException e) {
throw new RequestCreationException(e);
}
}
}

View File

@ -0,0 +1,33 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore;
import nodomain.freeyourgadget.gadgetbridge.service.btbr.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.serial.AbstractSerialDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceIoThread;
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
public class SoundcoreLiberty3ProDeviceSupport extends AbstractSerialDeviceSupport {
@Override
public boolean connect() {
getDeviceIOThread().start();
return true;
}
@Override
public boolean useAutoConnect() {
return false;
}
@Override
protected GBDeviceProtocol createDeviceProtocol() {
return new SoundcoreLibertyProtocol(getDevice());
}
@Override
protected synchronized GBDeviceIoThread createDeviceIOThread() {
return new SoundcoreLibertyIOThread(getDevice(), getContext(),
(SoundcoreLibertyProtocol) getDeviceProtocol(),
SoundcoreLiberty3ProDeviceSupport.this, getBluetoothAdapter());
}
}

View File

@ -0,0 +1,49 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore;
import static nodomain.freeyourgadget.gadgetbridge.util.GB.hexdump;
import android.bluetooth.BluetoothAdapter;
import android.content.Context;
import android.os.ParcelUuid;
import androidx.annotation.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.btclassic.BtClassicIoThread;
public class SoundcoreLibertyIOThread extends BtClassicIoThread {
private static final Logger LOG = LoggerFactory.getLogger(SoundcoreLibertyIOThread.class);
private final SoundcoreLibertyProtocol mSoundcoreProtocol;
public SoundcoreLibertyIOThread(GBDevice gbDevice, Context context, SoundcoreLibertyProtocol deviceProtocol, SoundcoreLiberty3ProDeviceSupport deviceSupport, BluetoothAdapter btAdapter) {
super(gbDevice, context, deviceProtocol, deviceSupport, btAdapter);
mSoundcoreProtocol = deviceProtocol;
}
@Override
protected void initialize() {
write(mSoundcoreProtocol.encodeDeviceInfoRequest());
setUpdateState(GBDevice.State.INITIALIZED);
}
@NonNull
protected UUID getUuidToConnect(@NonNull ParcelUuid[] uuids) {
return mSoundcoreProtocol.UUID_DEVICE_CTRL;
}
@Override
protected byte[] parseIncoming(InputStream inStream) throws IOException {
byte[] buffer = new byte[1048576]; //HUGE read
int bytes = inStream.read(buffer);
LOG.debug("read " + bytes + " bytes. " + hexdump(buffer, 0, bytes));
return Arrays.copyOf(buffer, bytes);
}
}

View File

@ -0,0 +1,377 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore;
import static nodomain.freeyourgadget.gadgetbridge.util.GB.hexdump;
import android.content.SharedPreferences;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.AmbientSoundControlButtonMode;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public class SoundcoreLibertyProtocol extends GBDeviceProtocol {
private static final Logger LOG = LoggerFactory.getLogger(SoundcoreLibertyProtocol.class);
private static final int battery_case = 0;
private static final int battery_earphone_left = 1;
private static final int battery_earphone_right = 2;
final UUID UUID_DEVICE_CTRL = UUID.fromString("0cf12d31-fac3-4553-bd80-d6832e7b3952");
protected SoundcoreLibertyProtocol(GBDevice device) {
super(device);
}
private GBDeviceEventBatteryInfo buildBatteryInfo(int batteryIndex, int level) {
GBDeviceEventBatteryInfo info = new GBDeviceEventBatteryInfo();
info.batteryIndex = batteryIndex;
info.level = level;
return info;
}
private GBDeviceEventVersionInfo buildVersionInfo(String firmware1, String firmware2, String serialNumber) {
GBDeviceEventVersionInfo info = new GBDeviceEventVersionInfo();
info.hwVersion = serialNumber;
info.fwVersion = firmware1;
info.fwVersion2 = firmware2;
return info;
}
private String readString(byte[] data, int position, int size) {
if (position + size > data.length) throw new IllegalStateException();
return new String(data, position, size, StandardCharsets.UTF_8);
}
@Override
public GBDeviceEvent[] decodeResponse(byte[] responseData) {
// Byte 0-4: Header
// Byte 5-6: Command (Audio-Mode)
// Byte 7: Size of data
// Byte 8-(x-1): Data
// Byte x: Checksum
if (responseData.length == 0) return null;
List<GBDeviceEvent> devEvts = new ArrayList<>();
byte[] command = Arrays.copyOfRange(responseData, 5, 7);
byte[] data = Arrays.copyOfRange(responseData, 8, responseData.length-1);
if (Arrays.equals(command, new byte[]{0x01, 0x01})) {
// a lot of other data is in here, anything interesting?
String firmware1 = readString(data, 7, 5);
String firmware2 = readString(data, 12, 5);
String serialNumber = readString(data, 17, 16);
devEvts.add(buildVersionInfo(firmware1, firmware2, serialNumber));
} else if (Arrays.equals(command, new byte[]{0x01, (byte) 0x8d})) {
LOG.debug("Unknown incoming message - command: " + hexdump(command) + ", dump: " + hexdump(responseData));
} else if (Arrays.equals(command, new byte[]{0x05, (byte) 0x82})) {
LOG.debug("Unknown incoming message - command: " + hexdump(command) + ", dump: " + hexdump(responseData));
} else if (Arrays.equals(command, new byte[]{0x05, 0x01})) {
LOG.debug("Unknown incoming message - command: " + hexdump(command) + ", dump: " + hexdump(responseData));
} else if (Arrays.equals(command, new byte[]{0x06, 0x01})) { //Sound Mode Update
decodeAudioMode(data);
} else if (Arrays.equals(command, new byte[]{0x01, 0x03})) { // Battery Update
int batteryLeft = data[1] * 20;
int batteryRight = data[2] * 20;
int batteryCase = data[3] * 20;
devEvts.add(buildBatteryInfo(battery_case, batteryCase));
devEvts.add(buildBatteryInfo(battery_earphone_left, batteryLeft));
devEvts.add(buildBatteryInfo(battery_earphone_right, batteryRight));
} else {
// see https://github.com/gmallios/SoundcoreManager/blob/master/soundcore-lib/src/models/packet_kind.rs
// for a mapping for other soundcore devices (similar protocol?)
LOG.debug("Unknown incoming message - command: " + hexdump(command) + ", dump: " + hexdump(responseData));
}
return devEvts.toArray(new GBDeviceEvent[devEvts.size()]);
}
private void decodeAudioMode(byte[] payload) {
SharedPreferences prefs = getDevicePrefs().getPreferences();
SharedPreferences.Editor editor = prefs.edit();
String soundmode = "off";
int anc_strength = 0;
if (payload[1] == 0x00) {
soundmode = "noise_cancelling";
} else if (payload[1] == 0x01) {
soundmode = "ambient_sound";
} else if (payload[1] == 0x02) {
soundmode = "off";
}
if (payload[2] == 0x10) {
anc_strength = 0;
} else if (payload[2] == 0x20) {
anc_strength = 1;
} else if (payload[2] == 0x30) {
anc_strength = 2;
}
boolean vocal_mode = (payload[3] == 0x01);
boolean adaptive_anc = (payload[4] == 0x01);
boolean windnoiseReduction = (payload[5] == 0x01);
editor.putString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_AMBIENT_SOUND_CONTROL, soundmode);
editor.putInt(DeviceSettingsPreferenceConst.PREF_SONY_AMBIENT_SOUND_LEVEL, anc_strength);
editor.putBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_TRANSPARENCY_VOCAL_MODE, vocal_mode);
editor.putBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_ADAPTIVE_NOISE_CANCELLING, adaptive_anc);
editor.putBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_WIND_NOISE_REDUCTION, windnoiseReduction);
editor.apply();
}
@Override
public byte[] encodeSendConfiguration(String config) {
Prefs prefs = getDevicePrefs();
String pref_string;
switch (config) {
// Ambient Sound Modes
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_AMBIENT_SOUND_CONTROL:
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_WIND_NOISE_REDUCTION:
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_TRANSPARENCY_VOCAL_MODE:
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_ADAPTIVE_NOISE_CANCELLING:
case DeviceSettingsPreferenceConst.PREF_SONY_AMBIENT_SOUND_LEVEL:
return encodeAudioMode();
// Control
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_SINGLE_TAP_DISABLED:
return encodeControlTouchLockMessage(TapAction.SINGLE_TAP, prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_SINGLE_TAP_DISABLED, false));
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_DOUBLE_TAP_DISABLED:
return encodeControlTouchLockMessage(TapAction.DOUBLE_TAP, prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_DOUBLE_TAP_DISABLED, false));
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_TRIPLE_TAP_DISABLED:
return encodeControlTouchLockMessage(TapAction.TRIPLE_TAP, prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_TRIPLE_TAP_DISABLED, false));
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_LONG_PRESS_DISABLED:
return encodeControlTouchLockMessage(TapAction.LONG_PRESS, prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_LONG_PRESS_DISABLED, false));
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_SINGLE_TAP_ACTION_LEFT:
pref_string = prefs.getString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_SINGLE_TAP_ACTION_LEFT, "");
return encodeControlFunctionMessage(TapAction.SINGLE_TAP, false, TapFunction.valueOf(pref_string));
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_SINGLE_TAP_ACTION_RIGHT:
pref_string = prefs.getString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_SINGLE_TAP_ACTION_RIGHT, "");
return encodeControlFunctionMessage(TapAction.SINGLE_TAP, true, TapFunction.valueOf(pref_string));
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_DOUBLE_TAP_ACTION_LEFT:
pref_string = prefs.getString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_DOUBLE_TAP_ACTION_LEFT, "");
return encodeControlFunctionMessage(TapAction.DOUBLE_TAP, false, TapFunction.valueOf(pref_string));
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_DOUBLE_TAP_ACTION_RIGHT:
pref_string = prefs.getString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_DOUBLE_TAP_ACTION_RIGHT, "");
return encodeControlFunctionMessage(TapAction.DOUBLE_TAP, true, TapFunction.valueOf(pref_string));
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_TRIPLE_TAP_ACTION_LEFT:
pref_string = prefs.getString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_TRIPLE_TAP_ACTION_LEFT, "");
return encodeControlFunctionMessage(TapAction.TRIPLE_TAP, false, TapFunction.valueOf(pref_string));
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_TRIPLE_TAP_ACTION_RIGHT:
pref_string = prefs.getString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_TRIPLE_TAP_ACTION_RIGHT, "");
return encodeControlFunctionMessage(TapAction.TRIPLE_TAP, true, TapFunction.valueOf(pref_string));
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_LONG_PRESS_ACTION_LEFT:
pref_string = prefs.getString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_LONG_PRESS_ACTION_LEFT, "");
return encodeControlFunctionMessage(TapAction.LONG_PRESS, false, TapFunction.valueOf(pref_string));
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_LONG_PRESS_ACTION_RIGHT:
pref_string = prefs.getString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_LONG_PRESS_ACTION_RIGHT, "");
return encodeControlFunctionMessage(TapAction.LONG_PRESS, true, TapFunction.valueOf(pref_string));
case DeviceSettingsPreferenceConst.PREF_SONY_AMBIENT_SOUND_CONTROL_BUTTON_MODE:
AmbientSoundControlButtonMode modes = AmbientSoundControlButtonMode.fromPreferences(prefs.getPreferences());
switch (modes) {
case NC_AS_OFF:
return encodeControlAmbientModeMessage(true, true, true);
case NC_AS:
return encodeControlAmbientModeMessage(true, true, false);
case NC_OFF:
return encodeControlAmbientModeMessage(true, false, true);
case AS_OFF:
return encodeControlAmbientModeMessage(false, true, true);
}
// Miscellaneous Settings
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_WEARING_DETECTION:
boolean wearingDetection = prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_WEARING_DETECTION, false);
return encodeMessage((byte) 0x01, (byte) 0x81, new byte[]{0x00, encodeBoolean(wearingDetection)});
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_WEARING_TONE:
boolean wearingTone = prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_WEARING_TONE, false);
return encodeMessage((byte) 0x01, (byte) 0x8c, new byte[]{0x00, encodeBoolean(wearingTone)});
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_TOUCH_TONE:
boolean touchTone = prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_TOUCH_TONE, false);
return encodeMessage((byte) 0x01, (byte) 0x83, new byte[]{0x00, encodeBoolean(touchTone)});
default:
LOG.debug("Unsupported CONFIG: " + config);
}
return super.encodeSendConfiguration(config);
}
byte[] encodeDeviceInfoRequest() {
byte[] payload = new byte[]{0x00};
return encodeMessage((byte) 0x01, (byte) 0x01, payload);
}
byte[] encodeMysteryDataRequest1() {
byte[] payload = new byte[]{0x00, 0x00};
return encodeMessage((byte) 0x01, (byte) 0x8d, payload);
}
byte[] encodeMysteryDataRequest2() {
byte[] payload = new byte[]{0x00};
return encodeMessage((byte) 0x05, (byte) 0x01, payload);
}
byte[] encodeMysteryDataRequest3() {
byte[] payload = new byte[]{0x00, 0x00};
return encodeMessage((byte) 0x05, (byte) 0x82, payload);
}
/**
* Encodes the following settings to a payload to set the audio-mode on the headphones:
* PREF_SOUNDCORE_AMBIENT_SOUND_CONTROL If ANC, Transparent or neither should be active
* PREF_SOUNDCORE_ADAPTIVE_NOISE_CANCELLING If the strenght of the ANC should be set manual or adaptively according to ambient noise
* PREF_SONY_AMBIENT_SOUND_LEVEL How strong the ANC should be in manual mode
* PREF_SOUNDCORE_TRANSPARENCY_VOCAL_MODE If the Transparency should focus on vocals or should be fully transparent
* PREF_SOUNDCORE_WIND_NOISE_REDUCTION If Transparency or ANC should reduce Wind Noise
* @return The payload
*/
private byte[] encodeAudioMode() {
Prefs prefs = getDevicePrefs();
byte anc_mode;
switch (prefs.getString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_AMBIENT_SOUND_CONTROL, "off")) {
case "noise_cancelling":
anc_mode = 0x00;
break;
case "ambient_sound":
anc_mode = 0x01;
break;
case "off":
anc_mode = 0x02;
break;
default:
LOG.error("Invalid Audio Mode selected");
return null;
}
byte anc_strength;
switch (prefs.getInt(DeviceSettingsPreferenceConst.PREF_SONY_AMBIENT_SOUND_LEVEL, 0)) {
case 0:
anc_strength = 0x10;
break;
case 1:
anc_strength = 0x20;
break;
case 2:
anc_strength = 0x30;
break;
default:
LOG.error("Invalid ANC Strength selected");
return null;
}
byte adaptive_anc = encodeBoolean(prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_ADAPTIVE_NOISE_CANCELLING, true));
byte vocal_mode = encodeBoolean(prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_TRANSPARENCY_VOCAL_MODE, false));
byte windnoise_reduction = encodeBoolean(prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_WIND_NOISE_REDUCTION, false));
byte[] payload = new byte[]{0x00, anc_mode, anc_strength, vocal_mode, adaptive_anc, windnoise_reduction, 0x01};
return encodeMessage((byte) 0x06, (byte) 0x81, payload);
}
/**
* Enables or disables a tap-action
* @param action The byte that encodes the action (single/double/triple or long tap)
* @param disabled If the action should be enabled or disabled
* @return
*/
private byte[] encodeControlTouchLockMessage(TapAction action, boolean disabled) {
boolean enabled = !disabled;
byte enabled_byte;
byte[] payload;
switch (action) {
case SINGLE_TAP:
case TRIPLE_TAP:
enabled_byte = encodeBoolean(enabled);
break;
case DOUBLE_TAP:
case LONG_PRESS:
enabled_byte = enabled?(byte) 0x11: (byte) 0x10;
break;
default:
LOG.error("Invalid Tap action");
return null;
}
payload = new byte[]{0x00, 0x00, action.getCode(), enabled_byte};
return encodeMessage((byte) 0x04, (byte) 0x83, payload);
}
/**
* Assigns a function (eg play/pause) to an action (eg single tap on right bud)
* @param action The byte that encodes the action (single/double/triple or long tap)
* @param right If the right or left earbud is meant
* @param function The byte that encodes the triggered function (eg play/pause)
* @return The encoded message
*/
private byte[] encodeControlFunctionMessage(TapAction action, boolean right, TapFunction function) {
byte function_byte;
switch (action) {
case SINGLE_TAP:
case DOUBLE_TAP:
function_byte = (byte) (16*6 + function.getCode());
break;
case TRIPLE_TAP:
function_byte = (byte) (16*4 + function.getCode());
break;
case LONG_PRESS:
function_byte = (byte) (16*5 + function.getCode());
break;
default:
LOG.error("Invalid Tap action");
return null;
}
byte[] payload = new byte[] {0x00, encodeBoolean(right), action.getCode(), function_byte};
return encodeMessage((byte) 0x04, (byte) 0x81, payload);
}
/**
* Encodes between which Audio Modes a tap should switch, if it is set to switch the Audio Mode.
* Zb ANC -> -> Transparency -> Normal -> ANC -> ....
*/
private byte[] encodeControlAmbientModeMessage(boolean anc, boolean transparency, boolean normal) {
// Original app does not allow only one true flag. Unsure if Earbuds accept this state.
byte ambientModes = (byte) (4 * (normal?1:0) + 2 * (transparency?1:0) + (anc?1:0));
return encodeMessage((byte) 0x06, (byte) 0x82, new byte[] {0x00, ambientModes});
}
private byte encodeBoolean(boolean bool) {
if (bool) return 0x01;
else return 0x00;
}
private byte[] encodeMessage(byte command1, byte command2, byte[] payload) {
int size = 8 + payload.length + 1;
ByteBuffer msgBuf = ByteBuffer.allocate(size);
msgBuf.order(ByteOrder.BIG_ENDIAN);
msgBuf.put(new byte[] {0x08, (byte) 0xee, 0x00, 0x00, 0x00}); // header
msgBuf.put(command1);
msgBuf.put(command2);
msgBuf.put((byte) size);
msgBuf.put(payload);
byte checksum = -10;
checksum += command1 + command2 + size;
for (int b : payload) {
checksum += b;
}
msgBuf.put(checksum);
return msgBuf.array();
}
}

View File

@ -0,0 +1,19 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore;
enum TapAction {
SINGLE_TAP((byte) 0x02),
DOUBLE_TAP((byte) 0x00),
TRIPLE_TAP((byte) 0x05),
LONG_PRESS((byte) 0x01)
;
private final byte code;
TapAction(final byte code) {
this.code = code;
}
public byte getCode() {
return code;
}
}

View File

@ -0,0 +1,21 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore;
enum TapFunction {
VOLUME_DOWN(1),
VOLUME_UP(0),
MEDIA_NEXT( 3),
MEDIA_PREV(2),
PLAYPAUSE(6),
VOICE_ASSISTANT(5),
AMBIENT_SOUND_CONTROL(4)
;
private final int code;
TapFunction(final int code) {
this.code = code;
}
public int getCode() {
return code;
}
}

View File

@ -81,6 +81,7 @@ public class SleepDetailsParser extends XiaomiActivityParser {
// Heart rate samples
if ((header & (1 << (5 - versionDependentFields))) != 0) {
LOG.debug("Heart rate samples from offset {}", Integer.toHexString(buf.position()));
final int unit = buf.getShort(); // Time unit (i.e sample rate)
final int count = buf.getShort();
@ -98,6 +99,7 @@ public class SleepDetailsParser extends XiaomiActivityParser {
// SpO2 samples
if ((header & (1 << (4 - versionDependentFields))) != 0) {
LOG.debug("SpO₂ samples from offset {}", Integer.toHexString(buf.position()));
final int unit = buf.getShort(); // Time unit (i.e sample rate)
final int count = buf.getShort();
@ -115,6 +117,7 @@ public class SleepDetailsParser extends XiaomiActivityParser {
// snore samples
if (fileId.getVersion() >= 3 && (header & (1 << (3 - versionDependentFields))) != 0) {
LOG.debug("Snore level samples from offset {}", Integer.toHexString(buf.position()));
final int unit = buf.getShort(); // Time unit (i.e sample rate)
final int count = buf.getShort();
@ -131,27 +134,26 @@ public class SleepDetailsParser extends XiaomiActivityParser {
}
final List<XiaomiSleepStageSample> stages = new ArrayList<>();
LOG.debug("Sleep stage packets from offset {}", Integer.toHexString(buf.position()));
// Do not crash if we face a buffer underflow, as the next parsing is not 100% fool-proof,
// and we still want to persist whatever we got so far
boolean stagesParseFailed = false;
try {
while (buf.remaining() >= 17 && buf.getInt() == 0xFFFCFAFB) {
while (buf.remaining() >= 17) {
if (!readStagePacketHeader(buf)) {
break;
}
final int headerLen = buf.get() & 0xFF; // this seems to always be 17
// This timestamp is kind of weird, is seems to sometimes be in seconds
// and other times in nanoseconds. Message types 16 and 17 are in seconds
final long ts = buf.getLong();
final int unk = buf.get() & 0xFF;
final int parity = buf.get() & 0xFF; // sum of stage bit count should be uneven
final int type = buf.get() & 0xFF;
final int dataLen = ((buf.get() & 0xFF) << 8) | (buf.get() & 0xFF);
final byte[] data = new byte[dataLen];
buf.get(data);
final ByteBuffer dataBuf = ByteBuffer.wrap(data).order(ByteOrder.BIG_ENDIAN);
// Known types:
// - acc_unk = 0,
// - ppg_unk = 1,
@ -162,6 +164,17 @@ public class SleepDetailsParser extends XiaomiActivityParser {
// - Summary = 16,
// - Stages = 17
if (type == 0x2 || type == 0x3 || type == 0x9 || type == 0xc || type == 0xd || type == 0xe || type == 0xf) {
// the bytes reserved for the data length are believed to be flags, as they
// do not actually have any data following the headers
continue;
}
final byte[] data = new byte[dataLen];
buf.get(data);
final ByteBuffer dataBuf = ByteBuffer.wrap(data).order(ByteOrder.BIG_ENDIAN);
if (type == 16) {
final int data_0 = dataBuf.get() & 0xFF;
final int sleep_index = data_0 >> 4;
@ -193,11 +206,10 @@ public class SleepDetailsParser extends XiaomiActivityParser {
sample.setAwakeDuration(wake_duration);
// FIXME: This is an array, but we end up persisting only the last sample, since
// the timestamp is the primary key
// the timestamp is the primary key
summaries.add(sample);
sample = null;
}
else if (type == 17) { // Stages
} else if (type == 17) { // Stages
long currentTime = ts * 1000;
for (int i = 0; i < dataLen / 2; i++) {
// when the change to the phase occurs
@ -250,7 +262,6 @@ public class SleepDetailsParser extends XiaomiActivityParser {
sampleProvider.addSample(summary);
}
} catch (final Exception e) {
GB.toast(support.getContext(), "Error saving sleep sample", Toast.LENGTH_LONG, GB.ERROR);
LOG.error("Error saving sleep sample", e);
@ -282,10 +293,23 @@ public class SleepDetailsParser extends XiaomiActivityParser {
}
}
return stagesParseFailed;
return !stagesParseFailed;
}
static private int decodeStage(int rawStage) {
private static boolean readStagePacketHeader(final ByteBuffer buffer) {
while (buffer.remaining() >= 17) {
if (buffer.getInt() != 0xfffcfafb) {
// rollback to second byte of header
buffer.position(buffer.position() - 3);
continue;
}
return true;
}
return false;
}
private static int decodeStage(int rawStage) {
switch (rawStage) {
case 0:
return 5; // AWAKE

View File

@ -26,6 +26,7 @@ import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
@ -36,134 +37,181 @@ public class ArmenianTransliterator implements Transliterator {
// Or if it has 'ւ' symbol after it, then we should read it as 'u' (as double o in booze)
private static final Map<String, String> transliterateMap = new LinkedHashMap<String, String>() {
{
// Simple substitutions
Map<String, String> simpleSubstitions = new HashMap<String, String>() {
{
put("ա","a");
put("բ","b");
put("գ","g");
put("դ","d");
put("ե","e");
put("զ","z");
put("է","e");
put("ը","y");
put("թ","t");
put("ժ","j");
put("ի","i");
put("լ","l");
put("խ","x");
put("ծ","c");
put("կ","k");
put("հ","h");
put("ձ","dz");
put("ղ","x");
put("ճ","c");
put("մ","m");
put("յ","y");
put("ն","n");
put("շ","sh");
put("չ","ch");
put("պ","p");
put("ջ","j");
put("ռ","r");
put("ս","s");
put("վ","v");
put("տ","t");
put("ր","r");
put("ց","c");
put("փ","p");
put("ք","q");
put("օ","o");
put("և","ev");
put("ֆ","f");
}
};
// Letter + 'ու'
put("աու","au");
put("բու","bu");
put("գու","gu");
put("դու","du");
put("եու","eu");
put("զու","zu");
put("էու","eu");
put("ըու","yu");
put("թու","tu");
put("ժու","ju");
put("իու","iu");
put("լու","lu");
put("խու","xu");
put("ծու","cu");
put("կու","ku");
put("հու","hu");
put("ձու","dzu");
put("ղու","xu");
put("ճու","cu");
put("մու","mu");
put("յու","yu");
put("նու","nu");
put("շու","shu");
put("չու","chu");
put("պու","pu");
put("ջու","ju");
put("ռու","ru");
put("սու","su");
put("վու","vu");
put("տու","tu");
put("րու","ru");
put("ցու","cu");
put("փու","pu");
put("քու","qu");
put("օու","ou");
put("ևու","eu");
put("ֆու","fu");
put("ոու","vou");
char[] letterMapU = {
'ա',
'բ',
'գ',
'դ',
'ե',
'զ',
'է',
'ը',
'թ',
'ժ',
'ի',
'լ',
'խ',
'ծ',
'կ',
'հ',
'ձ',
'ղ',
'ճ',
'մ',
'յ',
'ն',
'շ',
'չ',
'պ',
'ջ',
'ռ',
'ս',
'վ',
'տ',
'ր',
'ց',
'փ',
'ք',
'օ',
'և',
'ֆ',
'ո',
};
for(char letter : letterMapU) {
char capitalLetter = Character.toUpperCase(letter);
final String transliteratedLetter = simpleSubstitions.get(Character.toString(letter));
final String transliteratedCapitalLetter = simpleSubstitions.get(Character.toString(capitalLetter));
put(Character.toString(letter) + "ու", transliteratedLetter + "u");
put(Character.toString(capitalLetter) + "ու", transliteratedCapitalLetter + "u");
put(Character.toString(letter) + "ՈՒ", transliteratedLetter + "U");
put(Character.toString(capitalLetter) + "ՈՒ", transliteratedCapitalLetter + "U");
put(Character.toString(letter) + "Ու", transliteratedLetter + "U");
put(Character.toString(capitalLetter) + "Ու", transliteratedCapitalLetter + "U");
put(Character.toString(letter) + "ոՒ", transliteratedLetter + "U");
put(Character.toString(capitalLetter) + "ոՒ", transliteratedCapitalLetter + "U");
}
put("ու","u");
put("Ու","U");
put("ոՒ","U");
put("ՈՒ","U");
// Letter + 'ո'
put("բո","bo");
put("գո","go");
put("դո","do");
put("զո","zo");
put("թո","to");
put("ժո","jo");
put("լո","lo");
put("խո","xo");
put("ծո","co");
put("կո","ko");
put("հո","ho");
put("ձո","dzo");
put("ղո","xo");
put("ճո","co");
put("մո","mo");
put("յո","yo");
put("նո","no");
put("շո","so");
put("չո","co");
put("պո","po");
put("ջո","jo");
put("ռո","ro");
put("սո","so");
put("վո","vo");
put("տո","to");
put("րո","ro");
put("ցո","co");
put("փո","po");
put("քո","qo");
put("ևո","eo");
put("ֆո","fo");
char[] letterMapVo = {
'բ',
'գ',
'դ',
'զ',
'թ',
'ժ',
'լ',
'խ',
'ծ',
'կ',
'հ',
'ձ',
'ղ',
'ճ',
'մ',
'յ',
'ն',
'շ',
'չ',
'պ',
'ջ',
'ռ',
'ս',
'վ',
'տ',
'ր',
'ց',
'փ',
'ք',
'և',
'ֆ',
};
for(char letter : letterMapVo) {
char capitalLetter = Character.toUpperCase(letter);
final String transliteratedLetter = simpleSubstitions.get(Character.toString(letter));
final String transliteratedCapitalLetter = simpleSubstitions.get(Character.toString(capitalLetter));
put(Character.toString(letter) + "ո", transliteratedLetter + "o");
put(Character.toString(capitalLetter) + "ո", transliteratedCapitalLetter + "o");
put(Character.toString(letter) + "Ո", transliteratedLetter + "Օ");
put(Character.toString(capitalLetter) + "Ո", transliteratedCapitalLetter + "Օ");
}
put("ո","vo");
put("Ո","VO");
// Two different ways to write, we support all.
put("եւ","ev");
put("եվ","ev");
// Simple substitutions
put("ա","a");
put("բ","b");
put("գ","g");
put("դ","d");
put("ե","e");
put("զ","z");
put("է","e");
put("ը","y");
put("թ","t");
put("ժ","j");
put("ի","i");
put("լ","l");
put("խ","x");
put("ծ","c");
put("կ","k");
put("հ","h");
put("ձ","dz");
put("ղ","x");
put("ճ","c");
put("մ","m");
put("յ","y");
put("ն","n");
put("շ","sh");
put("չ","ch");
put("պ","p");
put("ջ","j");
put("ռ","r");
put("ս","s");
put("վ","v");
put("տ","t");
put("ր","r");
put("ց","c");
put("փ","p");
put("ք","q");
put("օ","o");
put("և","ev");
put("ֆ","f");
put("Եւ","Ev");
put("Եվ","Ev");
put("ԵՒ","EV");
put("ԵՎ","EV");
// If this symbol wasn't used in the combination with others, then it's meaningless
put("ւ","");
put("Ւ","");
// Add support for capitilazed words
for (final Map.Entry<String,String> entry : ((Map<String, String>)this.clone()).entrySet()) {
final String capitalKey = WordUtils.capitalize(entry.getKey());
if(!capitalKey.equals(entry.getKey())) {
put(capitalKey, WordUtils.capitalize(entry.getValue()));
}
// Simple substitutions have last priority
for (final Map.Entry<String,String> entry : simpleSubstitions.entrySet()) {
put(entry.getKey(), entry.getValue());
put(entry.getKey().toUpperCase(), entry.getValue().toUpperCase());
}
}};

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.camera.view.PreviewView
android:id="@+id/preview"
android:layout_height="match_parent"
android:layout_width="match_parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -301,6 +301,14 @@
grid:layout_columnSpan="2"
grid:layout_gravity="fill_horizontal"
android:text="@string/debug_companion_pair_current" />
<Button
android:id="@+id/cameraOpen"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
grid:layout_columnSpan="2"
grid:layout_gravity="fill_horizontal"
android:text="@string/open_camera" />
</androidx.gridlayout.widget.GridLayout>
</ScrollView>

View File

@ -3441,6 +3441,27 @@
<item>as_off</item>
</string-array>
<string-array name="soundcore_button_function_names">
<item>@string/pref_media_volumedown</item>
<item>@string/pref_media_volumeup</item>
<item>@string/pref_media_next</item>
<item>@string/pref_media_previous</item>
<item>@string/pref_media_playpause</item>
<item>@string/pref_title_touch_voice_assistant</item>
<item>@string/sony_button_mode_ambient_sound_control</item>
</string-array>
<string-array name="soundcore_button_function_values">
<item>VOLUME_DOWN</item>
<item>VOLUME_UP</item>
<item>MEDIA_NEXT</item>
<item>MEDIA_PREV</item>
<item>PLAYPAUSE</item>
<item>VOICE_ASSISTANT</item>
<item>AMBIENT_SOUND_CONTROL</item>
</string-array>
<string-array name="fitness_tracking_apps_package_names">
<item>de.dennisguse.opentracks</item>
<item>de.dennisguse.opentracks.playStore</item>

View File

@ -520,6 +520,10 @@
<string name="pref_gps_satellite_search">Satellite Search</string>
<string name="pref_crown_vibration">Crown Vibration</string>
<string name="pref_alert_tone">Alert Tone</string>
<string name="pref_touch_tone">Touch Tone</string>
<string name="pref_touch_tone_summary">Plays a tone when the earbud is touched</string>
<string name="pref_wearing_tone">Wearing Tone</string>
<string name="pref_wearing_tone_summary">Plays a tone when the earbud is inserted</string>
<string name="pref_cover_to_mute">Cover to Mute</string>
<string name="pref_vibrate_for_alert">Vibrate for Alert</string>
<string name="pref_text_to_speech">Text to Speech</string>
@ -1533,6 +1537,7 @@
<string name="devicetype_sony_wi_sp600n">Sony WI-SP600N</string>
<string name="devicetype_sony_linkbuds">Sony LinkBuds</string>
<string name="devicetype_sony_linkbuds_s">Sony LinkBuds S</string>
<string name="devicetype_soundcore_liberty3_pro">Soundcore Liberty 3 Pro</string>
<string name="devicetype_binary_sensor">Binary sensor</string>
<string name="devicetype_honor_band3">Honor Band 3</string>
<string name="devicetype_honor_band4">Honor Band 4</string>
@ -1544,6 +1549,7 @@
<string name="devicetype_huawei_band6">Huawei Band 6</string>
<string name="devicetype_huawei_band7">Huawei Band 7</string>
<string name="devicetype_huawei_band8">Huawei Band 8</string>
<string name="devicetype_huawei_band9">Huawei Band 9</string>
<string name="devicetype_huawei_watch_gt">Huawei Watch GT</string>
<string name="devicetype_huawei_band4pro">Huawei Band 4 (Pro)</string>
<string name="devicetype_huawei_watchgt2">Huawei Watch GT 2 (Pro)</string>
@ -1553,7 +1559,9 @@
<string name="devicetype_huawei_watchgt4">Huawei Watch GT 4</string>
<string name="devicetype_huawei_watchfit">Huawei Watch Fit</string>
<string name="devicetype_huawei_watchfit2">Huawei Watch Fit 2</string>
<string name="devicetype_huawei_watchfit3">Huawei Watch Fit 3</string>
<string name="devicetype_huawei_watchultimate">Huawei Watch Ultimate</string>
<string name="devicetype_huawei_watch4pro">Huawei Watch 4 Pro</string>
<string name="devicetype_femometer_vinca2">Femometer Vinca II</string>
<string name="devicetype_xiaomi_watch_lite">Xiaomi Watch Lite</string>
<string name="devicetype_redmiwatch3active">Redmi Watch 3 Active</string>
@ -1606,6 +1614,7 @@
<string name="menuitem_cards">Cards</string>
<string name="menuitem_mi_ai">MI AI</string>
<string name="preferences_qhybrid_settings">Q Hybrid Settings</string>
<string name="preferences_qhybrid_settings_summary">Legacy settings for Q Hybrid watches</string>
<string name="menuitem_music">Music</string>
<string name="menuitem_more">More</string>
<string name="menuitem_nfc">NFC</string>
@ -2284,6 +2293,8 @@
<string name="pref_wide_area_tap_title">Wide area tap</string>
<string name="pref_adaptive_volume_control_summary">Increase volume automatically when ambient sound is loud</string>
<string name="pref_adaptive_volume_control_title">Adaptive volume control</string>
<string name="pref_adaptive_noise_cancelling_title">Adaptive ANC</string>
<string name="pref_adaptive_noise_cancelling_summary">Set the strength of the ANC automatically depending on the ambient sound level</string>
<string name="sony_speak_to_chat">Speak-to-chat</string>
<string name="sony_speak_to_chat_summary">Turn off noise cancelling automatically when you start talking.</string>
<string name="sony_speak_to_chat_sensitivity">Voice Detection Sensitivity</string>
@ -2821,4 +2832,11 @@
<string name="pref_title_huawei_account">Huawei Account</string>
<string name="pref_summary_huawei_account">Huawei account used in pairing process. Setting it allows to pair without factory reset.</string>
<string name="watchface_resolution_doesnt_match">Watchface resolution doesnt match device screen. Watchface is %1$s device screen is %2$s</string>
<string name="toast_setting_requires_reconnect">This setting will take effect after a reconnect</string>
<!-- Camera strings -->
<string name="open_camera">Open Camera</string>
<string name="toast_camera_permission_required">Camera permission is required for this function.</string>
<string name="toast_camera_support_required">Camera support is required for this function.</string>
<string name="toast_camera_photo_taken">Photo has been taken and saved at: %s</string>
</resources>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<Preference
android:icon="@drawable/ic_settings"
android:key="pref_key_qhybrid_legacy"
android:summary="@string/preferences_qhybrid_settings_summary"
android:title="@string/preferences_qhybrid_settings" />
</androidx.preference.PreferenceScreen>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceScreen
android:icon="@drawable/ic_touch"
android:key="pref_screen_touch_options"
android:persistent="false"
android:title="@string/prefs_galaxy_touch_options">
</PreferenceScreen>
</androidx.preference.PreferenceScreen>

View File

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory
android:key="pref_key_header_soundcore_ambient_sound_control"
android:title="@string/pref_header_sony_ambient_sound_control">
<ListPreference
android:defaultValue="noise_cancelling"
android:entries="@array/sony_ambient_sound_control_names"
android:entryValues="@array/sony_ambient_sound_control_values"
android:icon="@drawable/ic_hearing"
android:key="pref_soundcore_ambient_sound_control"
android:summary="%s"
android:title="@string/sony_ambient_sound" />
<SwitchPreferenceCompat
android:defaultValue="true"
android:disableDependentsState="true"
android:icon="@drawable/ic_hearing"
android:key="pref_adaptive_noise_cancelling"
android:layout="@layout/preference_checkbox"
android:summary="@string/pref_adaptive_noise_cancelling_summary"
android:title="@string/pref_adaptive_noise_cancelling_title" />
<!-- [0, 2], low moderate and high -->
<SeekBarPreference
android:dependency="pref_adaptive_noise_cancelling"
android:defaultValue="0"
android:icon="@drawable/ic_hearing"
android:key="pref_sony_ambient_sound_level"
android:max="2"
android:title="@string/prefs_active_noise_cancelling_level" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:icon="@drawable/ic_block"
android:key="pref_soundcore_wind_noise_reduction"
android:layout="@layout/preference_checkbox"
android:title="@string/sony_ambient_sound_wind_noise_reduction" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:icon="@drawable/ic_voice"
android:key="pref_soundcore_transparency_vocal_mode"
android:layout="@layout/preference_checkbox"
android:title="@string/sony_ambient_sound_focus_voice" />
</PreferenceCategory>
<PreferenceCategory
android:key="pref_key_header_soundcore_other"
android:title="@string/pref_header_other">
<SwitchPreferenceCompat
android:defaultValue="false"
android:layout="@layout/preference_checkbox"
android:summary="@string/nothing_prefs_inear_summary"
android:title="@string/nothing_prefs_inear_title"/>
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="pref_soundcore_wearing_tone"
android:layout="@layout/preference_checkbox"
android:summary="@string/pref_wearing_tone_summary"
android:title="@string/pref_wearing_tone"/>
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="pref_soundcore_touch_tone"
android:layout="@layout/preference_checkbox"
android:summary="@string/pref_touch_tone_summary"
android:title="@string/pref_touch_tone"/>
</PreferenceCategory>
</androidx.preference.PreferenceScreen>

View File

@ -0,0 +1,107 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory android:title="@string/single_tap">
<SwitchPreferenceCompat
android:defaultValue="false"
android:disableDependentsState="true"
android:icon="@drawable/ic_lock_open"
android:key="pref_soundcore_control_single_tap_disabled"
android:layout="@layout/preference_checkbox"
android:summary="@string/prefs_touch_lock_summary"
android:title="@string/prefs_touch_lock" />
<ListPreference
android:dependency="pref_soundcore_control_single_tap_disabled"
android:entries="@array/soundcore_button_function_names"
android:entryValues="@array/soundcore_button_function_values"
android:icon="@drawable/ic_touch"
android:key="pref_soundcore_control_single_tap_action_left"
android:summary="%s"
android:title="@string/prefs_left" />
<ListPreference
android:dependency="pref_soundcore_control_single_tap_disabled"
android:entries="@array/soundcore_button_function_names"
android:entryValues="@array/soundcore_button_function_values"
android:icon="@drawable/ic_touch"
android:key="pref_soundcore_control_single_tap_action_right"
android:summary="%s"
android:title="@string/prefs_right" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/double_tap">
<SwitchPreferenceCompat
android:defaultValue="false"
android:disableDependentsState="true"
android:icon="@drawable/ic_lock_open"
android:key="pref_soundcore_control_double_tap_disabled"
android:layout="@layout/preference_checkbox"
android:summary="@string/prefs_touch_lock_summary"
android:title="@string/prefs_touch_lock" />
<ListPreference
android:dependency="pref_soundcore_control_double_tap_disabled"
android:entries="@array/soundcore_button_function_names"
android:entryValues="@array/soundcore_button_function_values"
android:icon="@drawable/ic_touch"
android:key="pref_soundcore_control_double_tap_action_left"
android:summary="%s"
android:title="@string/prefs_left" />
<ListPreference
android:dependency="pref_soundcore_control_double_tap_disabled"
android:entries="@array/soundcore_button_function_names"
android:entryValues="@array/soundcore_button_function_values"
android:icon="@drawable/ic_touch"
android:key="pref_soundcore_control_double_tap_action_right"
android:summary="%s"
android:title="@string/prefs_right" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/triple_tap">
<SwitchPreferenceCompat
android:defaultValue="false"
android:disableDependentsState="true"
android:icon="@drawable/ic_lock_open"
android:key="pref_soundcore_control_triple_tap_disabled"
android:layout="@layout/preference_checkbox"
android:summary="@string/prefs_touch_lock_summary"
android:title="@string/prefs_touch_lock" />
<ListPreference
android:dependency="pref_soundcore_control_triple_tap_disabled"
android:entries="@array/soundcore_button_function_names"
android:entryValues="@array/soundcore_button_function_values"
android:icon="@drawable/ic_touch"
android:key="pref_soundcore_control_triple_tap_action_left"
android:summary="%s"
android:title="@string/prefs_left" />
<ListPreference
android:dependency="pref_soundcore_control_triple_tap_disabled"
android:entries="@array/soundcore_button_function_names"
android:entryValues="@array/soundcore_button_function_values"
android:icon="@drawable/ic_touch"
android:key="pref_soundcore_control_triple_tap_action_right"
android:summary="%s"
android:title="@string/prefs_right" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/long_press">
<SwitchPreferenceCompat
android:defaultValue="false"
android:disableDependentsState="true"
android:icon="@drawable/ic_lock_open"
android:key="pref_soundcore_control_long_press_disabled"
android:layout="@layout/preference_checkbox"
android:summary="@string/prefs_touch_lock_summary"
android:title="@string/prefs_touch_lock" />
<ListPreference
android:dependency="pref_soundcore_control_long_press_disabled"
android:entries="@array/soundcore_button_function_names"
android:entryValues="@array/soundcore_button_function_values"
android:icon="@drawable/ic_touch"
android:key="pref_soundcore_control_long_press_action_left"
android:summary="%s"
android:title="@string/prefs_left" />
<ListPreference
android:dependency="pref_soundcore_control_long_press_disabled"
android:entries="@array/soundcore_button_function_names"
android:entryValues="@array/soundcore_button_function_values"
android:icon="@drawable/ic_touch"
android:key="pref_soundcore_control_long_press_action_right"
android:summary="%s"
android:title="@string/prefs_right" />
</PreferenceCategory>
</androidx.preference.PreferenceScreen>

View File

@ -270,11 +270,6 @@
android:title="@string/preferences_category_device_specific_settings"
app:iconSpaceReserved="false">
<Preference
android:icon="@drawable/ic_device_pebble"
android:key="pref_key_qhybrid"
android:title="@string/preferences_qhybrid_settings" />
<Preference
android:icon="@drawable/ic_device_miband"
android:key="pref_key_miband"

View File

@ -33,6 +33,19 @@ public class ArmenianTransliteratorTest extends TestCase {
new ArmenianTransliterator().transliterate("որը jet iridescent կառուցում են sheen Վիքիպեդիա կայքից օգտվողները and a distinctive ազատ խմբագրման ձևաչափով"));
}
@Test
public void testMixedCaseWords() {
Assert.assertEquals(
"Inchpes", new ArmenianTransliterator().transliterate("Ինչպես")
);
Assert.assertEquals(
"VOrՕSHEL", new ArmenianTransliterator().transliterate("ՈրՈՇԵԼ")
);
Assert.assertEquals(
"Ushadir", new ArmenianTransliterator().transliterate("Ուշադիր")
);
}
@Test
public void testTop100Words() {