mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-06-16 10:00:08 +02:00
Compare commits
14 Commits
7d38d4e3e3
...
6ae409a213
Author | SHA1 | Date | |
---|---|---|---|
|
6ae409a213 | ||
|
7358cb7ba8 | ||
|
b909e123a4 | ||
|
39ea1774a4 | ||
|
f2c360ae8a | ||
|
41aab5135f | ||
|
690d01dcac | ||
|
02b052fcaf | ||
|
ac8d1ed6a0 | ||
|
508a86b8ed | ||
|
f581d57c01 | ||
|
bed67ef1fb | ||
|
04237b7727 | ||
|
dc1ffdafcd |
|
@ -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"
|
||||
|
|
|
@ -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>
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 -> {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
|
@ -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){
|
||||
|
|
|
@ -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) {
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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) {}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
}};
|
||||
|
|
11
app/src/main/res/layout/activity_camera.xml
Normal file
11
app/src/main/res/layout/activity_camera.xml
Normal 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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
10
app/src/main/res/xml/devicesettings_fossilqhybrid_legacy.xml
Normal file
10
app/src/main/res/xml/devicesettings_fossilqhybrid_legacy.xml
Normal 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>
|
|
@ -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>
|
76
app/src/main/res/xml/devicesettings_soundcore_headphones.xml
Normal file
76
app/src/main/res/xml/devicesettings_soundcore_headphones.xml
Normal 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>
|
107
app/src/main/res/xml/devicesettings_soundcore_touch_options.xml
Normal file
107
app/src/main/res/xml/devicesettings_soundcore_touch_options.xml
Normal 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>
|
|
@ -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"
|
||||
|
|
|
@ -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() {
|
||||
|
|
Loading…
Reference in New Issue
Block a user