mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-11-23 02:16:48 +01:00
Compare commits
51 Commits
d9b59df637
...
97cba3f5e6
Author | SHA1 | Date | |
---|---|---|---|
|
97cba3f5e6 | ||
|
0c984ad400 | ||
|
8b1a061e4c | ||
|
40c89c7512 | ||
|
256d3938a5 | ||
|
19ef63073d | ||
|
d870175d25 | ||
|
a4bfe080b1 | ||
|
8deba9e111 | ||
|
ca1d1bc2f7 | ||
|
2dabffa798 | ||
|
4e172429fc | ||
|
239391d7bc | ||
|
143fad2cf9 | ||
|
47f31e6040 | ||
|
5f19155a7e | ||
|
8b11e5eda0 | ||
|
16aed1364b | ||
|
212289645f | ||
|
6b5c5ae0ac | ||
|
9d1a57b6c2 | ||
|
b56ed974a3 | ||
|
b5bd4da9b1 | ||
|
1d2404a4e6 | ||
|
39e7bd8c62 | ||
|
5f91715c89 | ||
|
1618fda418 | ||
|
e453855e88 | ||
|
dc1533b4ed | ||
|
1a3a7dec05 | ||
|
87bc2e6ed7 | ||
|
9bd828814e | ||
|
6aa7280967 | ||
|
f16e2eeabb | ||
|
e83555f099 | ||
|
9b6fce566d | ||
|
de37e5b6fd | ||
|
cbb710abe7 | ||
|
31b8fd683d | ||
|
82f221752e | ||
|
c2c1e48c85 | ||
|
810df3055c | ||
|
a72de07d2a | ||
|
7a0e43a4de | ||
|
ce32ac7272 | ||
|
2a865fe498 | ||
|
f3185f1acb | ||
|
6bb93bef89 | ||
|
7c1d44fcd3 | ||
|
a2323ce845 | ||
|
5a0f1e46db |
@ -124,6 +124,7 @@
|
||||
<w>protomors</w>
|
||||
<w>qhybrid</w>
|
||||
<w>quallenauge</w>
|
||||
<w>realme</w>
|
||||
<w>rebelo</w>
|
||||
<w>roidmi</w>
|
||||
<w>romanization</w>
|
||||
|
15
CHANGELOG.md
15
CHANGELOG.md
@ -1,5 +1,20 @@
|
||||
### Changelog
|
||||
|
||||
#### Next Release (WIP)
|
||||
* Initial support for Bowers and Wilkins P Series
|
||||
* Initial support for Garmin Fenix 6S Pro, Forerunner 55/235/620
|
||||
* Initial support for Huawei Band 3 Pro
|
||||
* Initial support for Oppo Enco Air
|
||||
* Huawei: Display high-resolution heart rate
|
||||
* Huawei: Improve activity parsing
|
||||
* Huawei: Sync skin temperature
|
||||
|
||||
#### 0.82.1
|
||||
* Huawei: Improve activity parsing
|
||||
* Huawei Watch GT: Fix connection failure
|
||||
* Withings: Fix crash on connection
|
||||
* Improve Armenian transliterator for mixed-case words
|
||||
|
||||
#### 0.82.0
|
||||
* Initial support for Anker Soundcore Liberty 4 NC
|
||||
* Initial support for CMF Buds Pro 2 / Watch Pro 2
|
||||
|
@ -79,8 +79,8 @@ android {
|
||||
minSdkVersion 21
|
||||
|
||||
// Note: always bump BOTH versionCode and versionName!
|
||||
versionName "0.82.0"
|
||||
versionCode 233
|
||||
versionName "0.82.1"
|
||||
versionCode 234
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
buildConfigField "String", "GIT_HASH_SHORT", "\"${getGitHashShort()}\""
|
||||
buildConfigField "boolean", "INTERNET_ACCESS", "false"
|
||||
@ -198,7 +198,7 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.2'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.3'
|
||||
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.2.0'
|
||||
implementation "androidx.camera:camera-core:1.4.0"
|
||||
|
@ -150,6 +150,14 @@
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".activities.welcome.WelcomeActivity"
|
||||
android:label="@string/first_start_welcome_title"
|
||||
android:parentActivityName=".activities.ControlCenterv2" />
|
||||
<activity
|
||||
android:name=".activities.PermissionsActivity"
|
||||
android:label="@string/first_start_permissions_title"
|
||||
android:parentActivityName=".activities.ControlCenterv2" />
|
||||
<activity
|
||||
android:name=".activities.SettingsActivity"
|
||||
android:label="@string/title_activity_settings"
|
||||
@ -199,6 +207,10 @@
|
||||
android:label="@string/title_activity_appmanager"
|
||||
android:launchMode="singleTop"
|
||||
android:parentActivityName=".activities.ControlCenterv2" />
|
||||
<activity
|
||||
android:name=".activities.musicmanager.MusicManagerActivity"
|
||||
android:label="@string/title_activity_musicmanager"
|
||||
android:parentActivityName=".activities.SettingsActivity" />
|
||||
<activity
|
||||
android:name=".activities.AppBlacklistActivity"
|
||||
android:label="@string/title_activity_notification_management"
|
||||
|
@ -127,7 +127,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 = 42;
|
||||
private static final int CURRENT_PREFS_VERSION = 43;
|
||||
|
||||
private static final LimitedQueue<Integer, String> mIDSenderLookup = new LimitedQueue<>(16);
|
||||
private static GBPrefs prefs;
|
||||
@ -1838,6 +1838,20 @@ public class GBApplication extends Application {
|
||||
}
|
||||
}
|
||||
|
||||
if (oldVersion < 43) {
|
||||
// Users upgrading to this version don't need to see the welcome screen
|
||||
try (DBHandler db = acquireDB()) {
|
||||
final DaoSession daoSession = db.getDaoSession();
|
||||
final List<Device> activeDevices = DBHelper.getActiveDevices(daoSession);
|
||||
|
||||
if (!activeDevices.isEmpty()) {
|
||||
editor.putBoolean("first_run", false);
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
Log.e(TAG, "Failed to migrate prefs to version 42", e);
|
||||
}
|
||||
}
|
||||
|
||||
editor.putString(PREFS_VERSION, Integer.toString(CURRENT_PREFS_VERSION));
|
||||
editor.apply();
|
||||
}
|
||||
|
@ -21,46 +21,29 @@ package nodomain.freeyourgadget.gadgetbridge.activities;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_CONNECT;
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Dialog;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.res.Resources;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
import android.telephony.PhoneStateListener;
|
||||
import android.telephony.TelephonyManager;
|
||||
import android.util.TypedValue;
|
||||
import android.view.MenuItem;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.appcompat.app.ActionBarDrawerToggle;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.view.GravityCompat;
|
||||
import androidx.core.view.MenuProvider;
|
||||
import androidx.drawerlayout.widget.DrawerLayout;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
@ -70,26 +53,22 @@ import androidx.viewpager2.widget.ViewPager2;
|
||||
|
||||
import com.google.android.material.appbar.MaterialToolbar;
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationView;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.navigation.NavigationView;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.BuildConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.discovery.DiscoveryActivityV2;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.welcome.WelcomeActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
|
||||
@ -98,6 +77,7 @@ import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GBChangeLog;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.PermissionsUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
|
||||
//TODO: extend AbstractGBActivity, but it requires actionbar that is not available
|
||||
@ -105,16 +85,11 @@ public class ControlCenterv2 extends AppCompatActivity
|
||||
implements NavigationView.OnNavigationItemSelectedListener, GBActivity {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ControlCenterv2.class);
|
||||
public static final int MENU_REFRESH_CODE = 1;
|
||||
public static final String ACTION_REQUEST_PERMISSIONS
|
||||
= "nodomain.freeyourgadget.gadgetbridge.activities.controlcenter.requestpermissions";
|
||||
public static final String ACTION_REQUEST_LOCATION_PERMISSIONS
|
||||
= "nodomain.freeyourgadget.gadgetbridge.activities.controlcenter.requestlocationpermissions";
|
||||
private boolean isLanguageInvalid = false;
|
||||
private boolean isThemeInvalid = false;
|
||||
private ViewPager2 viewPager;
|
||||
private FragmentStateAdapter pagerAdapter;
|
||||
private SwipeRefreshLayout swipeLayout;
|
||||
private static PhoneStateListener fakeStateListener;
|
||||
private AlertDialog clDialog;
|
||||
|
||||
//needed for KK compatibility
|
||||
@ -140,12 +115,6 @@ public class ControlCenterv2 extends AppCompatActivity
|
||||
final GBDevice device = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
|
||||
handleRealtimeSample(device, intent.getSerializableExtra(DeviceService.EXTRA_REALTIME_SAMPLE));
|
||||
break;
|
||||
case ACTION_REQUEST_PERMISSIONS:
|
||||
checkAndRequestPermissions();
|
||||
break;
|
||||
case ACTION_REQUEST_LOCATION_PERMISSIONS:
|
||||
checkAndRequestLocationPermissions();
|
||||
break;
|
||||
case GBDevice.ACTION_DEVICE_CHANGED:
|
||||
GBDevice dev = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
|
||||
if (dev != null && !dev.isBusy()) {
|
||||
@ -310,79 +279,21 @@ public class ControlCenterv2 extends AppCompatActivity
|
||||
filterLocal.addAction(GBApplication.ACTION_THEME_CHANGE);
|
||||
filterLocal.addAction(GBApplication.ACTION_QUIT);
|
||||
filterLocal.addAction(DeviceService.ACTION_REALTIME_SAMPLES);
|
||||
filterLocal.addAction(ACTION_REQUEST_PERMISSIONS);
|
||||
filterLocal.addAction(ACTION_REQUEST_LOCATION_PERMISSIONS);
|
||||
filterLocal.addAction(GBDevice.ACTION_DEVICE_CHANGED);
|
||||
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filterLocal);
|
||||
|
||||
/*
|
||||
* Ask for permission to intercept notifications on first run.
|
||||
*/
|
||||
pesterWithPermissions = prefs.getBoolean("permission_pestering", true);
|
||||
|
||||
boolean displayPermissionDialog = !prefs.getBoolean("permission_dialog_displayed", false);
|
||||
prefs.getPreferences().edit().putBoolean("permission_dialog_displayed", true).apply();
|
||||
|
||||
|
||||
Set<String> set = NotificationManagerCompat.getEnabledListenerPackages(this);
|
||||
if (pesterWithPermissions) {
|
||||
if (!set.contains(this.getPackageName())) { // If notification listener access hasn't been granted
|
||||
// Put up a dialog explaining why we need permissions (Polite, but also Play Store policy)
|
||||
// When accepted, we open the Activity for Notification access
|
||||
DialogFragment dialog = new NotifyListenerPermissionsDialogFragment();
|
||||
dialog.show(getSupportFragmentManager(), "NotifyListenerPermissionsDialogFragment");
|
||||
// Open the Welcome flow on first run, only check permissions on next runs
|
||||
boolean firstRun = prefs.getBoolean("first_run", true);
|
||||
if (firstRun) {
|
||||
launchWelcomeActivity();
|
||||
} else {
|
||||
pesterWithPermissions = prefs.getBoolean("permission_pestering", true);
|
||||
if (pesterWithPermissions && !PermissionsUtils.checkAllPermissions(this)) {
|
||||
Intent permissionsIntent = new Intent(this, PermissionsActivity.class);
|
||||
startActivity(permissionsIntent);
|
||||
}
|
||||
}
|
||||
|
||||
/* We not put up dialogs explaining why we need permissions (Polite, but also Play Store policy).
|
||||
|
||||
Rather than chaining the calls, we just open a bunch of dialogs. Last in this list = first
|
||||
on the page, and as they are accepted the permissions are requested in turn.
|
||||
|
||||
When accepted, we request it or open the Activity for permission to display over other apps. */
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
/* In order to be able to set ringer mode to silent in GB's PhoneCallReceiver
|
||||
the permission to access notifications is needed above Android M
|
||||
ACCESS_NOTIFICATION_POLICY is also needed in the manifest */
|
||||
if (pesterWithPermissions) {
|
||||
if (!((NotificationManager) this.getSystemService(Context.NOTIFICATION_SERVICE)).isNotificationPolicyAccessGranted()) {
|
||||
// Put up a dialog explaining why we need permissions (Polite, but also Play Store policy)
|
||||
// When accepted, we open the Activity for Notification access
|
||||
DialogFragment dialog = new NotifyPolicyPermissionsDialogFragment();
|
||||
dialog.show(getSupportFragmentManager(), "NotifyPolicyPermissionsDialogFragment");
|
||||
}
|
||||
}
|
||||
|
||||
if (!Settings.canDrawOverlays(getApplicationContext())) {
|
||||
// If diplay over other apps access hasn't been granted
|
||||
// Put up a dialog explaining why we need permissions (Polite, but also Play Store policy)
|
||||
// When accepted, we open the Activity for permission to display over other apps.
|
||||
if (pesterWithPermissions) {
|
||||
DialogFragment dialog = new DisplayOverOthersPermissionsDialogFragment();
|
||||
dialog.show(getSupportFragmentManager(), "DisplayOverOthersPermissionsDialogFragment");
|
||||
}
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q &&
|
||||
ContextCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.ACCESS_BACKGROUND_LOCATION) == PackageManager.PERMISSION_DENIED) {
|
||||
if (pesterWithPermissions) {
|
||||
DialogFragment dialog = new LocationPermissionsDialogFragment();
|
||||
dialog.show(getSupportFragmentManager(), "LocationPermissionsDialogFragment");
|
||||
}
|
||||
}
|
||||
|
||||
// Check all the other permissions that we need to for Android M + later
|
||||
if (getWantedPermissions().isEmpty())
|
||||
displayPermissionDialog = false;
|
||||
if (displayPermissionDialog && pesterWithPermissions) {
|
||||
DialogFragment dialog = new PermissionsDialogFragment();
|
||||
dialog.show(getSupportFragmentManager(), "PermissionsDialogFragment");
|
||||
// when 'ok' clicked, checkAndRequestPermissions() is called
|
||||
} else
|
||||
checkAndRequestPermissions();
|
||||
}
|
||||
|
||||
GBChangeLog cl = GBChangeLog.createChangeLog(this);
|
||||
boolean showChangelog = prefs.getBoolean("show_changelog", true);
|
||||
if (showChangelog && cl.isFirstRun() && cl.hasChanges(cl.isFirstRunEver())) {
|
||||
@ -473,6 +384,10 @@ public class ControlCenterv2 extends AppCompatActivity
|
||||
}
|
||||
|
||||
|
||||
private void launchWelcomeActivity() {
|
||||
startActivity(new Intent(this, WelcomeActivity.class));
|
||||
}
|
||||
|
||||
private void launchDiscoveryActivity() {
|
||||
startActivity(new Intent(this, DiscoveryActivityV2.class));
|
||||
}
|
||||
@ -489,145 +404,6 @@ public class ControlCenterv2 extends AppCompatActivity
|
||||
}
|
||||
}
|
||||
|
||||
private void checkAndRequestLocationPermissions() {
|
||||
if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.ACCESS_BACKGROUND_LOCATION) != PackageManager.PERMISSION_GRANTED) {
|
||||
LOG.error("No permission to access background location!");
|
||||
GB.toast(getString(R.string.error_no_location_access), Toast.LENGTH_SHORT, GB.ERROR);
|
||||
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_BACKGROUND_LOCATION}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.M)
|
||||
private List<String> getWantedPermissions() {
|
||||
List<String> wantedPermissions = new ArrayList<>();
|
||||
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH) == PackageManager.PERMISSION_DENIED)
|
||||
wantedPermissions.add(Manifest.permission.BLUETOOTH);
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_ADMIN) == PackageManager.PERMISSION_DENIED)
|
||||
wantedPermissions.add(Manifest.permission.BLUETOOTH_ADMIN);
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_DENIED)
|
||||
wantedPermissions.add(Manifest.permission.READ_CONTACTS);
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE) == PackageManager.PERMISSION_DENIED)
|
||||
wantedPermissions.add(Manifest.permission.CALL_PHONE);
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CALL_LOG) == PackageManager.PERMISSION_DENIED)
|
||||
wantedPermissions.add(Manifest.permission.READ_CALL_LOG);
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_DENIED)
|
||||
wantedPermissions.add(Manifest.permission.READ_PHONE_STATE);
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.PROCESS_OUTGOING_CALLS) == PackageManager.PERMISSION_DENIED)
|
||||
wantedPermissions.add(Manifest.permission.PROCESS_OUTGOING_CALLS);
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECEIVE_SMS) == PackageManager.PERMISSION_DENIED)
|
||||
wantedPermissions.add(Manifest.permission.RECEIVE_SMS);
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_SMS) == PackageManager.PERMISSION_DENIED)
|
||||
wantedPermissions.add(Manifest.permission.READ_SMS);
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.SEND_SMS) == PackageManager.PERMISSION_DENIED)
|
||||
wantedPermissions.add(Manifest.permission.SEND_SMS);
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED)
|
||||
wantedPermissions.add(Manifest.permission.READ_EXTERNAL_STORAGE);
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CALENDAR) == PackageManager.PERMISSION_DENIED)
|
||||
wantedPermissions.add(Manifest.permission.READ_CALENDAR);
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_DENIED)
|
||||
wantedPermissions.add(Manifest.permission.ACCESS_FINE_LOCATION);
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_DENIED)
|
||||
wantedPermissions.add(Manifest.permission.ACCESS_COARSE_LOCATION);
|
||||
|
||||
try {
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.MEDIA_CONTENT_CONTROL) == PackageManager.PERMISSION_DENIED)
|
||||
wantedPermissions.add(Manifest.permission.MEDIA_CONTENT_CONTROL);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
if (pesterWithPermissions) {
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ANSWER_PHONE_CALLS) == PackageManager.PERMISSION_DENIED) {
|
||||
wantedPermissions.add(Manifest.permission.ANSWER_PHONE_CALLS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && Build.VERSION.SDK_INT <= Build.VERSION_CODES.S) {
|
||||
if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.ACCESS_BACKGROUND_LOCATION) == PackageManager.PERMISSION_DENIED) {
|
||||
wantedPermissions.add(Manifest.permission.ACCESS_BACKGROUND_LOCATION);
|
||||
}
|
||||
if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_DENIED) {
|
||||
wantedPermissions.add(Manifest.permission.ACCESS_FINE_LOCATION);
|
||||
}
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.QUERY_ALL_PACKAGES) == PackageManager.PERMISSION_DENIED) {
|
||||
wantedPermissions.add(Manifest.permission.QUERY_ALL_PACKAGES);
|
||||
}
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.BLUETOOTH_SCAN) == PackageManager.PERMISSION_DENIED) {
|
||||
wantedPermissions.add(Manifest.permission.BLUETOOTH_SCAN);
|
||||
}
|
||||
if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_DENIED) {
|
||||
wantedPermissions.add(Manifest.permission.BLUETOOTH_CONNECT);
|
||||
}
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_DENIED) {
|
||||
wantedPermissions.add(Manifest.permission.POST_NOTIFICATIONS);
|
||||
}
|
||||
}
|
||||
|
||||
if (BuildConfig.INTERNET_ACCESS) {
|
||||
if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.INTERNET) == PackageManager.PERMISSION_DENIED) {
|
||||
wantedPermissions.add(Manifest.permission.INTERNET);
|
||||
}
|
||||
}
|
||||
|
||||
return wantedPermissions;
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.M)
|
||||
private void checkAndRequestPermissions() {
|
||||
List<String> wantedPermissions = getWantedPermissions();
|
||||
|
||||
if (!wantedPermissions.isEmpty()) {
|
||||
Prefs prefs = GBApplication.getPrefs();
|
||||
// If this is not the first run, we can rely on
|
||||
// shouldShowRequestPermissionRationale(String permission)
|
||||
// and ignore permissions that shouldn't or can't be requested again
|
||||
if (prefs.getBoolean("permissions_asked", false)) {
|
||||
// Don't request permissions that we shouldn't show a prompt for
|
||||
// e.g. permissions that are "Never" granted by the user or never granted by the system
|
||||
Set<String> shouldNotAsk = new HashSet<>();
|
||||
for (String wantedPermission : wantedPermissions) {
|
||||
if (!shouldShowRequestPermissionRationale(wantedPermission)) {
|
||||
shouldNotAsk.add(wantedPermission);
|
||||
}
|
||||
}
|
||||
wantedPermissions.removeAll(shouldNotAsk);
|
||||
} else {
|
||||
// Permissions have not been asked yet, but now will be
|
||||
prefs.getPreferences().edit().putBoolean("permissions_asked", true).apply();
|
||||
}
|
||||
|
||||
if (!wantedPermissions.isEmpty()) {
|
||||
GB.toast(this, getString(R.string.permission_granting_mandatory), Toast.LENGTH_LONG, GB.ERROR);
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
||||
ActivityCompat.requestPermissions(this, wantedPermissions.toArray(new String[0]), 0);
|
||||
} else {
|
||||
requestMultiplePermissionsLauncher.launch(wantedPermissions.toArray(new String[0]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { // The enclosed hack in it's current state cause crash on Banglejs builds tarkgetSDK=31 on a Android 13 device.
|
||||
// HACK: On Lineage we have to do this so that the permission dialog pops up
|
||||
if (fakeStateListener == null) {
|
||||
fakeStateListener = new PhoneStateListener();
|
||||
TelephonyManager telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
|
||||
telephonyManager.listen(fakeStateListener, PhoneStateListener.LISTEN_CALL_STATE);
|
||||
telephonyManager.listen(fakeStateListener, PhoneStateListener.LISTEN_NONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setLanguage(Locale language, boolean invalidateLanguage) {
|
||||
if (invalidateLanguage) {
|
||||
isLanguageInvalid = true;
|
||||
@ -635,137 +411,6 @@ public class ControlCenterv2 extends AppCompatActivity
|
||||
AndroidUtils.setLanguage(this, language);
|
||||
}
|
||||
|
||||
/// Called from onCreate - this puts up a dialog explaining we need permissions, and goes to the correct Activity
|
||||
public static class NotifyPolicyPermissionsDialogFragment extends DialogFragment {
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
// Use the Builder class for convenient dialog construction
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getActivity());
|
||||
final Context context = getContext();
|
||||
builder.setMessage(context.getString(R.string.permission_notification_policy_access,
|
||||
getContext().getString(R.string.app_name),
|
||||
getContext().getString(R.string.ok)))
|
||||
.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
public void onClick(DialogInterface dialog, int id) {
|
||||
try {
|
||||
startActivity(new Intent(android.provider.Settings.ACTION_NOTIFICATION_POLICY_ACCESS_SETTINGS));
|
||||
} catch (ActivityNotFoundException e) {
|
||||
GB.toast(context, "'Notification Policy' activity not found", Toast.LENGTH_LONG, GB.ERROR);
|
||||
}
|
||||
}
|
||||
});
|
||||
return builder.create();
|
||||
}
|
||||
}
|
||||
|
||||
/// Called from onCreate - this puts up a dialog explaining we need permissions, and goes to the correct Activity
|
||||
public static class NotifyListenerPermissionsDialogFragment extends DialogFragment {
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
// Use the Builder class for convenient dialog construction
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getActivity());
|
||||
final Context context = getContext();
|
||||
builder.setMessage(context.getString(R.string.permission_notification_listener,
|
||||
getContext().getString(R.string.app_name),
|
||||
getContext().getString(R.string.ok)))
|
||||
.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int id) {
|
||||
try {
|
||||
startActivity(new Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS"));
|
||||
} catch (ActivityNotFoundException e) {
|
||||
GB.toast(context, "'Notification Listener Settings' activity not found", Toast.LENGTH_LONG, GB.ERROR);
|
||||
}
|
||||
}
|
||||
});
|
||||
return builder.create();
|
||||
}
|
||||
}
|
||||
|
||||
/// Called from onCreate - this puts up a dialog explaining we need permissions, and goes to the correct Activity
|
||||
public static class DisplayOverOthersPermissionsDialogFragment extends DialogFragment {
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
// Use the Builder class for convenient dialog construction
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getActivity());
|
||||
Context context = getContext();
|
||||
builder.setMessage(context.getString(R.string.permission_display_over_other_apps,
|
||||
getContext().getString(R.string.app_name),
|
||||
getContext().getString(R.string.ok)))
|
||||
.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
public void onClick(DialogInterface dialog, int id) {
|
||||
Intent enableIntent = new Intent(android.provider.Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
|
||||
startActivity(enableIntent);
|
||||
}
|
||||
}).setNegativeButton(R.string.dismiss, new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int id) {}
|
||||
});
|
||||
return builder.create();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Called from onCreate - this puts up a dialog explaining we need backgound location permissions, and then requests permissions when 'ok' pressed
|
||||
public static class LocationPermissionsDialogFragment extends DialogFragment {
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
// Use the Builder class for convenient dialog construction
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getActivity());
|
||||
Context context = getContext();
|
||||
builder.setMessage(context.getString(R.string.permission_location,
|
||||
getContext().getString(R.string.app_name),
|
||||
getContext().getString(R.string.ok)))
|
||||
.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int id) {
|
||||
Intent intent = new Intent(ACTION_REQUEST_LOCATION_PERMISSIONS);
|
||||
LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent);
|
||||
}
|
||||
});
|
||||
return builder.create();
|
||||
}
|
||||
}
|
||||
|
||||
// Register the permissions callback, which handles the user's response to the
|
||||
// system permissions dialog. Save the return value, an instance of
|
||||
// ActivityResultLauncher, as an instance variable.
|
||||
// This is required here rather than where it is used because it'll cause a
|
||||
// "LifecycleOwners must call register before they are STARTED" if not called from onCreate
|
||||
public ActivityResultLauncher<String[]> requestMultiplePermissionsLauncher =
|
||||
registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), isGranted -> {
|
||||
if (isGranted.containsValue(true)) {
|
||||
// Permission is granted. Continue the action or workflow in your
|
||||
// app.
|
||||
} else {
|
||||
// Explain to the user that the feature is unavailable because the
|
||||
// feature requires a permission that the user has denied. At the
|
||||
// same time, respect the user's decision. Don't link to system
|
||||
// settings in an effort to convince the user to change their
|
||||
// decision.
|
||||
GB.toast(this, getString(R.string.permission_granting_mandatory), Toast.LENGTH_LONG, GB.ERROR);
|
||||
}
|
||||
});
|
||||
|
||||
/// Called from onCreate - this puts up a dialog explaining we need permissions, and then requests permissions when 'ok' pressed
|
||||
public static class PermissionsDialogFragment extends DialogFragment {
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
// Use the Builder class for convenient dialog construction
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getActivity());
|
||||
Context context = getContext();
|
||||
builder.setMessage(context.getString(R.string.permission_request,
|
||||
getContext().getString(R.string.app_name),
|
||||
getContext().getString(R.string.ok)))
|
||||
.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int id) {
|
||||
Intent intent = new Intent(ACTION_REQUEST_PERMISSIONS);
|
||||
LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent);
|
||||
}
|
||||
});
|
||||
return builder.create();
|
||||
}
|
||||
}
|
||||
|
||||
private class MainFragmentsPagerAdapter extends FragmentStateAdapter {
|
||||
public MainFragmentsPagerAdapter(FragmentActivity fa) {
|
||||
super(fa);
|
||||
|
@ -0,0 +1,38 @@
|
||||
/* Copyright (C) 2024 Arjan Schrijver
|
||||
|
||||
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 <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.welcome.WelcomeFragmentPermissions;
|
||||
|
||||
public class PermissionsActivity extends AbstractGBActivity {
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_permissions);
|
||||
|
||||
WelcomeFragmentPermissions permissionsFragment = new WelcomeFragmentPermissions();
|
||||
FragmentManager fragmentManager = getSupportFragmentManager();
|
||||
FragmentTransaction transaction = fragmentManager.beginTransaction();
|
||||
transaction.replace(R.id.fragment_container, permissionsFragment).commit();
|
||||
}
|
||||
}
|
@ -97,14 +97,14 @@ public class BodyEnergyFragment extends AbstractChartFragment<BodyEnergyFragment
|
||||
|
||||
@Override
|
||||
protected BodyEnergyData refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device) {
|
||||
String formattedDate = new SimpleDateFormat("E, MMM dd").format(getEndDate());
|
||||
mDateView.setText(formattedDate);
|
||||
List<? extends BodyEnergySample> samples = getBodyEnergySamples(db, device, getTSStart(), getTSEnd());
|
||||
return new BodyEnergyData(samples);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateChartsnUIThread(BodyEnergyData bodyEnergyData) {
|
||||
String formattedDate = new SimpleDateFormat("E, MMM dd").format(getEndDate());
|
||||
mDateView.setText(formattedDate);
|
||||
|
||||
List<Entry> lineEntries = new ArrayList<>();
|
||||
final List<ILineDataSet> lineDataSets = new ArrayList<>();
|
||||
|
@ -126,10 +126,8 @@ public class HRVStatusFragment extends AbstractChartFragment<HRVStatusFragment.H
|
||||
@Override
|
||||
protected HRVStatusWeeklyData refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device) {
|
||||
Calendar day = Calendar.getInstance();
|
||||
Date tsEnd = getChartsHost().getEndDate();
|
||||
day.setTime(tsEnd);
|
||||
String formattedDate = new SimpleDateFormat("E, MMM dd").format(tsEnd);
|
||||
mDateView.setText(formattedDate);
|
||||
day.setTime(getEndDate());
|
||||
|
||||
List<HRVStatusDayData> weeklyData = getWeeklyData(db, day, device);
|
||||
return new HRVStatusWeeklyData(weeklyData);
|
||||
}
|
||||
@ -164,6 +162,9 @@ public class HRVStatusFragment extends AbstractChartFragment<HRVStatusFragment.H
|
||||
|
||||
@Override
|
||||
protected void updateChartsnUIThread(HRVStatusWeeklyData weeklyData) {
|
||||
String formattedDate = new SimpleDateFormat("E, MMM dd").format(getEndDate());
|
||||
mDateView.setText(formattedDate);
|
||||
|
||||
mWeeklyHRVStatusChart.setData(null); // workaround for https://github.com/PhilJay/MPAndroidChart/issues/2317
|
||||
List<Entry> lineEntries = new ArrayList<>();
|
||||
final List<ILineDataSet> lineDataSets = new ArrayList<>();
|
||||
|
@ -38,6 +38,7 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.HeartRateSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Accumulator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
|
||||
public class HeartRateDailyFragment extends AbstractChartFragment<HeartRateDailyFragment.HeartRateData> {
|
||||
@ -123,9 +124,7 @@ public class HeartRateDailyFragment extends AbstractChartFragment<HeartRateDaily
|
||||
day.add(Calendar.HOUR, 0);
|
||||
int startTs = (int) (day.getTimeInMillis() / 1000);
|
||||
int endTs = startTs + 24 * 60 * 60 - 1;
|
||||
Date date = new Date((long) endTs * 1000);
|
||||
String formattedDate = new SimpleDateFormat("E, MMM dd").format(date);
|
||||
mDateView.setText(formattedDate);
|
||||
|
||||
List<? extends ActivitySample> samples = getActivitySamples(db, device, startTs, endTs);
|
||||
|
||||
int restingHeartRate = -1;
|
||||
@ -211,20 +210,33 @@ public class HeartRateDailyFragment extends AbstractChartFragment<HeartRateDaily
|
||||
|
||||
@Override
|
||||
protected void updateChartsnUIThread(HeartRateDailyFragment.HeartRateData data) {
|
||||
Calendar day = Calendar.getInstance();
|
||||
day.setTime(getEndDate());
|
||||
day.add(Calendar.DATE, 0);
|
||||
day.set(Calendar.HOUR_OF_DAY, 0);
|
||||
day.set(Calendar.MINUTE, 0);
|
||||
day.set(Calendar.SECOND, 0);
|
||||
day.add(Calendar.HOUR, 0);
|
||||
int startTs = (int) (day.getTimeInMillis() / 1000);
|
||||
int endTs = startTs + 24 * 60 * 60 - 1;
|
||||
Date date = new Date((long) endTs * 1000);
|
||||
String formattedDate = new SimpleDateFormat("E, MMM dd").format(date);
|
||||
mDateView.setText(formattedDate);
|
||||
|
||||
HeartRateUtils heartRateUtilsInstance = HeartRateUtils.getInstance();
|
||||
final TimestampTranslation tsTranslation = new TimestampTranslation();
|
||||
final List<Entry> lineEntries = new ArrayList<>();
|
||||
List<? extends ActivitySample> samples = data.samples;
|
||||
int average = 0;
|
||||
int minimum = 0;
|
||||
int maximum = 0;
|
||||
int sum = 0;
|
||||
int n = 0;
|
||||
final Accumulator accumulator = new Accumulator();
|
||||
|
||||
int lastHrSampleIndex = -1;
|
||||
for (int i =0; i < samples.size(); i++) {
|
||||
ActivitySample sample = samples.get(i);
|
||||
int ts = tsTranslation.shorten(sample.getTimestamp());
|
||||
if (sample.getKind() != ActivityKind.NOT_WORN && heartRateUtilsInstance.isValidHeartRateValue(sample.getHeartRate())) {
|
||||
final ActivitySample sample = samples.get(i);
|
||||
final int ts = tsTranslation.shorten(sample.getTimestamp());
|
||||
if (!heartRateUtilsInstance.isValidHeartRateValue(sample.getHeartRate())) {
|
||||
continue;
|
||||
}
|
||||
if (sample.getKind() != ActivityKind.NOT_WORN) {
|
||||
if (lastHrSampleIndex > -1 && ts - lastHrSampleIndex > 1800 * HeartRateUtils.MAX_HR_MEASUREMENTS_GAP_MINUTES) {
|
||||
lineEntries.add(new Entry(lastHrSampleIndex + 1, 0 ));
|
||||
lineEntries.add(new Entry(ts - 1, 0));
|
||||
@ -232,17 +244,7 @@ public class HeartRateDailyFragment extends AbstractChartFragment<HeartRateDaily
|
||||
lineEntries.add(new Entry(ts, sample.getHeartRate()));
|
||||
lastHrSampleIndex = ts;
|
||||
}
|
||||
if (sample.getHeartRate() <= 0) {
|
||||
continue;
|
||||
}
|
||||
n++;
|
||||
sum += sample.getHeartRate();
|
||||
if (sample.getHeartRate() > maximum) {
|
||||
maximum = sample.getHeartRate();
|
||||
}
|
||||
if (minimum == 0 || sample.getHeartRate() < minimum) {
|
||||
minimum = sample.getHeartRate();
|
||||
}
|
||||
accumulator.add(sample.getHeartRate());
|
||||
}
|
||||
|
||||
LineDataSet dataSet = new LineDataSet(lineEntries, "Heart Rate");
|
||||
@ -255,16 +257,15 @@ public class HeartRateDailyFragment extends AbstractChartFragment<HeartRateDaily
|
||||
dataSet.setColor(HEARTRATE_COLOR);
|
||||
dataSet.setValueTextColor(CHART_TEXT_COLOR);
|
||||
|
||||
if (n > 0 && sum > 0) {
|
||||
average = sum / n;
|
||||
}
|
||||
final int average = accumulator.getCount() > 0 ? (int) Math.round(accumulator.getAverage()) : -1;
|
||||
final int minimum = accumulator.getCount() > 0 ? (int) Math.round(accumulator.getMin()) : -1;
|
||||
final int maximum = accumulator.getCount() > 0 ? (int) Math.round(accumulator.getMax()) : -1;
|
||||
|
||||
hrAverage.setText(average > 0 ? getString(R.string.bpm_value_unit, average) : "-");
|
||||
hrMinimum.setText(minimum > 0 ? getString(R.string.bpm_value_unit, minimum) : "-");
|
||||
hrMaximum.setText(maximum > 0 ? getString(R.string.bpm_value_unit, maximum) : "-");
|
||||
hrResting.setText(data.restingHeartRate > 0 ? getString(R.string.bpm_value_unit, data.restingHeartRate) : "-");
|
||||
|
||||
|
||||
if (minimum > 0) {
|
||||
hrLineChart.getAxisLeft().setAxisMinimum(Math.max(minimum - 30, 0));
|
||||
hrLineChart.getAxisRight().setAxisMinimum(Math.max(minimum - 30, 0));
|
||||
@ -279,7 +280,7 @@ public class HeartRateDailyFragment extends AbstractChartFragment<HeartRateDaily
|
||||
|
||||
hrLineChart.getAxisLeft().removeAllLimitLines();
|
||||
|
||||
if (GBApplication.getPrefs().getBoolean("charts_show_average", true)) {
|
||||
if (average > 0 && GBApplication.getPrefs().getBoolean("charts_show_average", true)) {
|
||||
final LimitLine averageLine = new LimitLine(average);
|
||||
averageLine.setLineWidth(1.5f);
|
||||
averageLine.enableDashedLine(15f, 10f, 0f);
|
||||
|
@ -37,6 +37,7 @@ import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.WorkoutValueFormatter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
|
||||
@ -101,8 +102,6 @@ public class StepsDailyFragment extends StepsFragment<StepsDailyFragment.StepsDa
|
||||
protected StepsDailyFragment.StepsData refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device) {
|
||||
Calendar day = Calendar.getInstance();
|
||||
day.setTime(chartsHost.getEndDate());
|
||||
String formattedDate = new SimpleDateFormat("E, MMM dd").format(chartsHost.getEndDate());
|
||||
mDateView.setText(formattedDate);
|
||||
List<StepsDay> stepsDayList = getMyStepsDaysData(db, day, device);
|
||||
final StepsDay stepsDay;
|
||||
if (stepsDayList.isEmpty()) {
|
||||
@ -117,6 +116,9 @@ public class StepsDailyFragment extends StepsFragment<StepsDailyFragment.StepsDa
|
||||
|
||||
@Override
|
||||
protected void updateChartsnUIThread(StepsDailyFragment.StepsData stepsData) {
|
||||
String formattedDate = new SimpleDateFormat("E, MMM dd").format(getEndDate());
|
||||
mDateView.setText(formattedDate);
|
||||
|
||||
final int width = (int) TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP,
|
||||
300,
|
||||
@ -132,7 +134,9 @@ public class StepsDailyFragment extends StepsFragment<StepsDailyFragment.StepsDa
|
||||
));
|
||||
|
||||
steps.setText(String.format(String.valueOf(stepsData.todayStepsDay.steps)));
|
||||
distance.setText(getString(R.string.steps_distance_unit, stepsData.todayStepsDay.distance));
|
||||
|
||||
final WorkoutValueFormatter valueFormatter = new WorkoutValueFormatter();
|
||||
distance.setText(valueFormatter.formatValue(stepsData.todayStepsDay.distance, "km"));
|
||||
|
||||
// Chart
|
||||
final List<LegendEntry> legendEntries = new ArrayList<>(1);
|
||||
|
@ -31,6 +31,7 @@ import java.util.Locale;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.WorkoutValueFormatter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
|
||||
@ -142,18 +143,19 @@ public class StepsPeriodFragment extends StepsFragment<StepsPeriodFragment.Steps
|
||||
@Override
|
||||
protected StepsData refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device) {
|
||||
Calendar day = Calendar.getInstance();
|
||||
Date to = new Date((long) this.getTSEnd() * 1000);
|
||||
Date from = DateUtils.addDays(to,-(TOTAL_DAYS - 1));
|
||||
String toFormattedDate = new SimpleDateFormat("E, MMM dd").format(to);
|
||||
String fromFormattedDate = new SimpleDateFormat("E, MMM dd").format(from);
|
||||
mDateView.setText(fromFormattedDate + " - " + toFormattedDate);
|
||||
day.setTime(to);
|
||||
day.setTime(getEndDate());
|
||||
List<StepsDay> stepsDaysData = getMyStepsDaysData(db, day, device);
|
||||
return new StepsData(stepsDaysData);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateChartsnUIThread(StepsData stepsData) {
|
||||
Date to = new Date((long) getTSEnd() * 1000);
|
||||
Date from = DateUtils.addDays(to,-(TOTAL_DAYS - 1));
|
||||
String toFormattedDate = new SimpleDateFormat("E, MMM dd").format(to);
|
||||
String fromFormattedDate = new SimpleDateFormat("E, MMM dd").format(from);
|
||||
mDateView.setText(fromFormattedDate + " - " + toFormattedDate);
|
||||
|
||||
stepsChart.setData(null);
|
||||
|
||||
List<BarEntry> entries = new ArrayList<>();
|
||||
@ -177,9 +179,10 @@ public class StepsPeriodFragment extends StepsFragment<StepsPeriodFragment.Steps
|
||||
}
|
||||
stepsChart.setData(barData);
|
||||
stepsAvg.setText(String.format(String.valueOf(stepsData.stepsDailyAvg)));
|
||||
distanceAvg.setText(getString(R.string.steps_distance_unit, stepsData.distanceDailyAvg));
|
||||
final WorkoutValueFormatter valueFormatter = new WorkoutValueFormatter();
|
||||
distanceAvg.setText(valueFormatter.formatValue(stepsData.distanceDailyAvg, "km"));
|
||||
stepsTotal.setText(String.format(String.valueOf(stepsData.totalSteps)));
|
||||
distanceTotal.setText(getString(R.string.steps_distance_unit, stepsData.totalDistance));
|
||||
distanceTotal.setText(valueFormatter.formatValue(stepsData.totalDistance, "km"));
|
||||
}
|
||||
|
||||
ValueFormatter getStepsChartDayValueFormatter(StepsPeriodFragment.StepsData stepsData) {
|
||||
|
@ -118,8 +118,6 @@ public class VO2MaxFragment extends AbstractChartFragment<VO2MaxFragment.VO2MaxD
|
||||
|
||||
@Override
|
||||
protected VO2MaxData refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device) {
|
||||
String formattedDate = new SimpleDateFormat("E, MMM dd").format(getEndDate());
|
||||
mDateView.setText(formattedDate);
|
||||
List<VO2MaxRecord> records = new ArrayList<>();
|
||||
int tsEnd = getTSEnd();
|
||||
Calendar day = Calendar.getInstance();
|
||||
@ -145,7 +143,9 @@ public class VO2MaxFragment extends AbstractChartFragment<VO2MaxFragment.VO2MaxD
|
||||
|
||||
@Override
|
||||
protected void updateChartsnUIThread(VO2MaxData vo2MaxData) {
|
||||
TimestampTranslation tsTranslation = new TimestampTranslation();
|
||||
String formattedDate = new SimpleDateFormat("E, MMM dd").format(getEndDate());
|
||||
mDateView.setText(formattedDate);
|
||||
|
||||
List<Entry> runningEntries = new ArrayList<>();
|
||||
List<Entry> cyclingEntries = new ArrayList<>();
|
||||
vo2MaxData.records.forEach((record) -> {
|
||||
|
@ -106,6 +106,12 @@ public class DeviceSettingsPreferenceConst {
|
||||
public static final String PREF_DEVICE_INTERNET_ACCESS = "device_internet_access";
|
||||
public static final String PREF_DEVICE_INTENTS = "device_intents";
|
||||
|
||||
public static final String PREF_ACTIVE_NOISE_CANCELLING_TOGGLE = "active_noise_cancelling_toggle";
|
||||
public static final String PREF_WEAR_SENSOR_TOGGLE = "wear_sensor_toggle";
|
||||
public static final String PREF_BANDW_PSERIES_VPT_ENABLED = "bandw_pseries_vpt_enabled";
|
||||
public static final String PREF_BANDW_PSERIES_VPT_LEVEL = "bandw_pseries_vpt_level";
|
||||
public static final String PREF_BANDW_PSERIES_GUI_VPT_LEVEL = "bandw_pseries_gui_vpt_level";
|
||||
|
||||
public static final String PREF_BANGLEJS_TEXT_BITMAP = "banglejs_text_bitmap";
|
||||
public static final String PREF_BANGLEJS_TEXT_BITMAP_SIZE = "banglejs_txt_bitmap_size";
|
||||
public static final String PREF_BANGLEJS_WEBVIEW_URL = "banglejs_webview_url";
|
||||
@ -279,6 +285,8 @@ public class DeviceSettingsPreferenceConst {
|
||||
public static final String PREF_CONTACTS = "pref_contacts";
|
||||
public static final String PREF_WIDGETS = "pref_widgets";
|
||||
|
||||
public static final String PREF_MUSIC_MANAGEMENT = "pref_music_management";
|
||||
|
||||
public static final String PREF_ANTILOST_ENABLED = "pref_antilost_enabled";
|
||||
public static final String PREF_HYDRATION_SWITCH = "pref_hydration_switch";
|
||||
public static final String PREF_HYDRATION_PERIOD = "pref_hydration_period";
|
||||
|
@ -70,6 +70,7 @@ import nodomain.freeyourgadget.gadgetbridge.activities.ConfigureWorldClocks;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.app_specific_notifications.AppSpecificNotificationSettingsActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.loyaltycards.LoyaltyCardsSettingsActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.loyaltycards.LoyaltyCardsSettingsConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.musicmanager.MusicManagerActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.widgets.WidgetScreensListActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.capabilities.HeartRateCapability;
|
||||
import nodomain.freeyourgadget.gadgetbridge.capabilities.password.PasswordCapabilityImpl;
|
||||
@ -611,6 +612,10 @@ public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment i
|
||||
addPreferenceHandlerFor(PREF_SLEEP_MODE_SLEEP_SCREEN);
|
||||
addPreferenceHandlerFor(PREF_SLEEP_MODE_SMART_ENABLE);
|
||||
|
||||
addPreferenceHandlerFor(PREF_ACTIVE_NOISE_CANCELLING_TOGGLE);
|
||||
addPreferenceHandlerFor(PREF_WEAR_SENSOR_TOGGLE);
|
||||
addPreferenceHandlerFor(PREF_BANDW_PSERIES_GUI_VPT_LEVEL);
|
||||
|
||||
addPreferenceHandlerFor(PREF_HYBRID_HR_DRAW_WIDGET_CIRCLES);
|
||||
addPreferenceHandlerFor(PREF_HYBRID_HR_FORCE_WHITE_COLOR);
|
||||
addPreferenceHandlerFor(PREF_HYBRID_HR_SAVE_RAW_ACTIVITY_FILES);
|
||||
@ -1047,6 +1052,19 @@ public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment i
|
||||
});
|
||||
}
|
||||
|
||||
final Preference music_management = findPreference(PREF_MUSIC_MANAGEMENT);
|
||||
if (music_management != null) {
|
||||
music_management.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
final Intent intent = new Intent(getContext(), MusicManagerActivity.class);
|
||||
intent.putExtra(GBDevice.EXTRA_DEVICE, device);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
final Preference widgets = findPreference(PREF_WIDGETS);
|
||||
if (widgets != null) {
|
||||
widgets.setOnPreferenceClickListener(preference -> {
|
||||
|
@ -0,0 +1,632 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.musicmanager;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.text.InputType;
|
||||
import android.text.TextUtils;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.EditText;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.result.ActivityResult;
|
||||
import androidx.activity.result.ActivityResultCallback;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.FwAppInstallerActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.adapter.MusicListAdapter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceMusic;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceMusicPlaylist;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GridAutoFitLayoutManager;
|
||||
|
||||
public class MusicManagerActivity extends AbstractGBActivity {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(MusicManagerActivity.class);
|
||||
|
||||
public static final String ACTION_MUSIC_DATA
|
||||
= "nodomain.freeyourgadget.gadgetbridge.musicmanager.action.music_data";
|
||||
public static final String ACTION_MUSIC_UPDATE
|
||||
= "nodomain.freeyourgadget.gadgetbridge.musicmanager.action.music_update";
|
||||
|
||||
protected GBDevice mGBDevice = null;
|
||||
|
||||
private View loadingView = null;
|
||||
private TextView musicDeviceInfo = null;
|
||||
|
||||
private final List<GBDeviceMusic> allMusic = new ArrayList<>();
|
||||
|
||||
private final List<GBDeviceMusic> musicList = new ArrayList<>();
|
||||
private MusicListAdapter musicAdapter;
|
||||
|
||||
private final List<GBDeviceMusicPlaylist> playlists = new ArrayList<>();
|
||||
private ArrayAdapter<GBDeviceMusicPlaylist> playlistAdapter;
|
||||
|
||||
private View playlistSpinnerLayout;
|
||||
private Spinner playlistsSpinner;
|
||||
|
||||
private FloatingActionButton fabMusicUpload;
|
||||
private FloatingActionButton fabMusicPlaylistAdd;
|
||||
|
||||
private int maxMusicCount = 0;
|
||||
private int maxPlaylistCount = 0;
|
||||
|
||||
public GBDevice getGBDevice() {
|
||||
return mGBDevice;
|
||||
}
|
||||
|
||||
Handler loadingTimeout = new Handler();
|
||||
Runnable loadingRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
GB.toast(getString(R.string.music_error), Toast.LENGTH_SHORT, GB.ERROR);
|
||||
stopLoading();
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setContentView(R.layout.activity_musicmanager);
|
||||
|
||||
Bundle extras = getIntent().getExtras();
|
||||
if (extras != null) {
|
||||
mGBDevice = extras.getParcelable(GBDevice.EXTRA_DEVICE);
|
||||
}
|
||||
if (mGBDevice == null) {
|
||||
throw new IllegalArgumentException("Must provide a device when invoking this activity");
|
||||
}
|
||||
|
||||
fabMusicUpload = findViewById(R.id.fab_music_upload);
|
||||
assert fabMusicUpload != null;
|
||||
fabMusicUpload.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
intent.setType("audio/*");
|
||||
openAudioActivityResultLauncher.launch(intent);
|
||||
}
|
||||
});
|
||||
|
||||
fabMusicPlaylistAdd = findViewById(R.id.fab_music_playlist_add);
|
||||
assert fabMusicPlaylistAdd != null;
|
||||
fabMusicPlaylistAdd.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
addMusicPlaylist();
|
||||
}
|
||||
});
|
||||
|
||||
hideActionButtons();
|
||||
|
||||
RecyclerView musicListView = findViewById(R.id.music_songs_list);
|
||||
loadingView = findViewById(R.id.music_loading);
|
||||
|
||||
musicDeviceInfo = findViewById(R.id.music_device_info);
|
||||
|
||||
musicListView.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||
@Override
|
||||
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
|
||||
if (dy > 0) {
|
||||
hideActionButtons();
|
||||
} else if (dy < 0) {
|
||||
showActionButtons();
|
||||
}
|
||||
}
|
||||
});
|
||||
musicListView.setLayoutManager(new GridAutoFitLayoutManager(this, 300));
|
||||
|
||||
musicAdapter = new MusicListAdapter(
|
||||
musicList,
|
||||
new MusicListAdapter.onItemAction() {
|
||||
@Override
|
||||
public void onItemClick(View view, GBDeviceMusic music) {
|
||||
openPopupMenu(view, music);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onItemLongClick(View view, GBDeviceMusic music) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
);
|
||||
musicListView.setAdapter(musicAdapter);
|
||||
|
||||
playlistSpinnerLayout = findViewById(R.id.music_playlists_layout);
|
||||
|
||||
playlistsSpinner = findViewById(R.id.music_playlists);
|
||||
|
||||
ImageButton renamePlaylist = findViewById(R.id.music_playlist_rename);
|
||||
assert renamePlaylist != null;
|
||||
renamePlaylist.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
renameMusicPlaylist((GBDeviceMusicPlaylist) playlistsSpinner.getSelectedItem());
|
||||
}
|
||||
});
|
||||
|
||||
ImageButton deletePlaylist = findViewById(R.id.music_playlist_delete);
|
||||
assert deletePlaylist != null;
|
||||
deletePlaylist.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
deleteMusicPlaylist((GBDeviceMusicPlaylist) playlistsSpinner.getSelectedItem());
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
playlistsSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
|
||||
GBDeviceMusicPlaylist item = (GBDeviceMusicPlaylist) adapterView.getItemAtPosition(i);
|
||||
if (item.getId() == 0) {
|
||||
deletePlaylist.setVisibility(View.GONE);
|
||||
renamePlaylist.setVisibility(View.GONE);
|
||||
|
||||
} else {
|
||||
deletePlaylist.setVisibility(View.VISIBLE);
|
||||
renamePlaylist.setVisibility(View.VISIBLE);
|
||||
}
|
||||
updateCurrentMusicList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> adapterView) {
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
playlistAdapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, playlists);
|
||||
initPlaylists();
|
||||
|
||||
playlistAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
playlistsSpinner.setAdapter(playlistAdapter);
|
||||
}
|
||||
|
||||
private void hideActionButtons() {
|
||||
fabMusicUpload.hide();
|
||||
fabMusicPlaylistAdd.hide();
|
||||
|
||||
}
|
||||
|
||||
private void showActionButtons() {
|
||||
fabMusicUpload.show();
|
||||
if(maxPlaylistCount > 0) {
|
||||
fabMusicPlaylistAdd.show();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void startLoading(long timeout) {
|
||||
hideActionButtons();
|
||||
loadingView.setVisibility(View.VISIBLE);
|
||||
if(timeout > 0) {
|
||||
loadingTimeout.postDelayed(loadingRunnable, timeout);
|
||||
}
|
||||
}
|
||||
private void startLoading() {
|
||||
startLoading(4000);
|
||||
}
|
||||
|
||||
private void stopLoading() {
|
||||
loadingTimeout.removeCallbacks(loadingRunnable);
|
||||
loadingView.setVisibility(View.GONE);
|
||||
showActionButtons();
|
||||
}
|
||||
|
||||
private void updateCurrentMusicList() {
|
||||
GBDeviceMusicPlaylist current = (GBDeviceMusicPlaylist) playlistsSpinner.getSelectedItem();
|
||||
musicList.clear();
|
||||
if (current.getId() == 0) {
|
||||
musicList.addAll(allMusic);
|
||||
} else {
|
||||
List<GBDeviceMusic> filtered = allMusic.stream().filter(m -> current.getMusicIds().contains(m.getId())).collect(Collectors.toList());
|
||||
musicList.addAll(filtered);
|
||||
}
|
||||
musicAdapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
private void initPlaylists() {
|
||||
playlists.clear();
|
||||
playlists.add(new GBDeviceMusicPlaylist(0,this.getString(R.string.music_all_songs),null));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(ACTION_MUSIC_DATA);
|
||||
filter.addAction(ACTION_MUSIC_UPDATE);
|
||||
|
||||
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filter);
|
||||
|
||||
// Load music data without timeout
|
||||
startLoading(0);
|
||||
GBApplication.deviceService(mGBDevice).onMusicListReq();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
|
||||
super.onStop();
|
||||
}
|
||||
|
||||
ActivityResultLauncher<Intent> openAudioActivityResultLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(),
|
||||
new ActivityResultCallback<ActivityResult>() {
|
||||
@Override
|
||||
public void onActivityResult(ActivityResult result) {
|
||||
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
|
||||
Intent startIntent = new Intent(MusicManagerActivity.this, FwAppInstallerActivity.class);
|
||||
startIntent.setAction(Intent.ACTION_VIEW);
|
||||
startIntent.setDataAndType(result.getData().getData(), null);
|
||||
startActivity(startIntent);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
public boolean openPopupMenu(View view, GBDeviceMusic music) {
|
||||
PopupMenu popupMenu = new PopupMenu(this, view);
|
||||
popupMenu.getMenuInflater().inflate(R.menu.musicmanager_context, popupMenu.getMenu());
|
||||
Menu menu = popupMenu.getMenu();
|
||||
|
||||
if (playlists.size() <= 1) {
|
||||
menu.removeItem(R.id.musicmanager_add_to_playlist);
|
||||
}
|
||||
|
||||
GBDeviceMusicPlaylist current = (GBDeviceMusicPlaylist) playlistsSpinner.getSelectedItem();
|
||||
musicList.clear();
|
||||
if (current.getId() == 0) {
|
||||
menu.removeItem(R.id.musicmanager_delete_from_playlist);
|
||||
} else {
|
||||
menu.removeItem(R.id.musicmanager_delete);
|
||||
}
|
||||
|
||||
popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
|
||||
public boolean onMenuItemClick(MenuItem item) {
|
||||
return onPopupItemSelected(item, music);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
popupMenu.show();
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean onPopupItemSelected(final MenuItem item, final GBDeviceMusic music) {
|
||||
final int itemId = item.getItemId();
|
||||
if (itemId == R.id.musicmanager_delete || itemId == R.id.musicmanager_delete_from_playlist) {
|
||||
deleteMusicConfirm(music);
|
||||
return true;
|
||||
} else if (itemId == R.id.musicmanager_add_to_playlist) {
|
||||
addMusicSongToPlaylist(music);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void deleteMusicConfirm(final GBDeviceMusic music) {
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.Delete)
|
||||
.setMessage(this.getString(R.string.music_delete_confirm_description, music.getTitle()))
|
||||
.setIcon(R.drawable.ic_warning)
|
||||
.setPositiveButton(android.R.string.yes, (dialog, whichButton) -> {
|
||||
deleteMusicFromDevice((GBDeviceMusicPlaylist) playlistsSpinner.getSelectedItem(), music);
|
||||
})
|
||||
.setNegativeButton(android.R.string.no, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void addPlaylistToDevice(final String playlistName) {
|
||||
startLoading();
|
||||
GBApplication.deviceService(mGBDevice).onMusicOperation(0, -1, playlistName, null);
|
||||
}
|
||||
|
||||
private void deletePlaylistFromDevice(final GBDeviceMusicPlaylist playlist) {
|
||||
startLoading();
|
||||
GBApplication.deviceService(mGBDevice).onMusicOperation(1, playlist.getId(), null, null);
|
||||
}
|
||||
|
||||
private void renamePlaylistOnDevice(final GBDeviceMusicPlaylist playlist, String newPlaylistName) {
|
||||
startLoading();
|
||||
GBApplication.deviceService(mGBDevice).onMusicOperation(2, playlist.getId(), newPlaylistName, null);
|
||||
}
|
||||
|
||||
private void addMusicToDevicePlaylist(GBDeviceMusicPlaylist playlist, final GBDeviceMusic music) {
|
||||
startLoading();
|
||||
ArrayList<Integer> list = new ArrayList<>();
|
||||
list.add(music.getId());
|
||||
GBApplication.deviceService(mGBDevice).onMusicOperation(3, playlist.getId(), null, list);
|
||||
}
|
||||
|
||||
private void deleteMusicFromDevice(GBDeviceMusicPlaylist playlist, final GBDeviceMusic music) {
|
||||
startLoading();
|
||||
ArrayList<Integer> list = new ArrayList<>();
|
||||
list.add(music.getId());
|
||||
GBApplication.deviceService(mGBDevice).onMusicOperation(4, playlist.getId(), null, list);
|
||||
}
|
||||
|
||||
private void addMusicPlaylist() {
|
||||
final EditText input = new EditText(this);
|
||||
input.setInputType(InputType.TYPE_CLASS_TEXT);
|
||||
|
||||
FrameLayout container = new FrameLayout(this);
|
||||
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
params.leftMargin = getResources().getDimensionPixelSize(R.dimen.dialog_margin);
|
||||
params.rightMargin = getResources().getDimensionPixelSize(R.dimen.dialog_margin);
|
||||
input.setLayoutParams(params);
|
||||
container.addView(input);
|
||||
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.music_new_playlist)
|
||||
.setView(container)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, whichButton) -> {
|
||||
String playlistName = input.getText().toString();
|
||||
addPlaylistToDevice(playlistName);
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void renameMusicPlaylist(GBDeviceMusicPlaylist playlist) {
|
||||
if(playlist.getId() == 0)
|
||||
return;
|
||||
final EditText input = new EditText(this);
|
||||
input.setInputType(InputType.TYPE_CLASS_TEXT);
|
||||
input.setText(playlist.getName());
|
||||
|
||||
FrameLayout container = new FrameLayout(this);
|
||||
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
params.leftMargin = getResources().getDimensionPixelSize(R.dimen.dialog_margin);
|
||||
params.rightMargin = getResources().getDimensionPixelSize(R.dimen.dialog_margin);
|
||||
input.setLayoutParams(params);
|
||||
container.addView(input);
|
||||
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.music_rename_playlist)
|
||||
.setView(container)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, whichButton) -> {
|
||||
String playlistName = input.getText().toString();
|
||||
renamePlaylistOnDevice(playlist, playlistName);
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void deleteMusicPlaylist(GBDeviceMusicPlaylist playlist) {
|
||||
if(playlist.getId() == 0)
|
||||
return;
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.Delete)
|
||||
.setMessage(this.getString(R.string.music_delete_confirm_description, playlist.getName()))
|
||||
.setIcon(R.drawable.ic_warning)
|
||||
.setPositiveButton(android.R.string.yes, (dialog, whichButton) -> {
|
||||
deletePlaylistFromDevice(playlist);
|
||||
})
|
||||
.setNegativeButton(android.R.string.no, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void addMusicSongToPlaylist(final GBDeviceMusic music) {
|
||||
final Spinner dPlaylists = new Spinner(this);
|
||||
|
||||
List<GBDeviceMusicPlaylist> dialogPlaylists = new ArrayList<>();
|
||||
for (GBDeviceMusicPlaylist playlist : playlists) {
|
||||
if (playlist.getId() != 0) {
|
||||
dialogPlaylists.add(playlist);
|
||||
}
|
||||
}
|
||||
|
||||
ArrayAdapter<GBDeviceMusicPlaylist> dialogPlaylistAdapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, dialogPlaylists);
|
||||
dialogPlaylistAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
dPlaylists.setAdapter(dialogPlaylistAdapter);
|
||||
|
||||
FrameLayout container = new FrameLayout(this);
|
||||
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
params.leftMargin = getResources().getDimensionPixelSize(R.dimen.dialog_margin);
|
||||
params.rightMargin = getResources().getDimensionPixelSize(R.dimen.dialog_margin);
|
||||
dPlaylists.setLayoutParams(params);
|
||||
container.addView(dPlaylists);
|
||||
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.music_add_to_playlist)
|
||||
.setView(container)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, whichButton) -> {
|
||||
GBDeviceMusicPlaylist playlist = (GBDeviceMusicPlaylist) dPlaylists.getSelectedItem();
|
||||
addMusicToDevicePlaylist(playlist, music);
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void startSyncFromDevice(Intent intent) {
|
||||
String info = intent.getStringExtra("deviceInfo");
|
||||
if (!TextUtils.isEmpty(info)) {
|
||||
musicDeviceInfo.setText(info);
|
||||
} else {
|
||||
musicDeviceInfo.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
maxMusicCount = intent.getIntExtra("maxMusicCount", 0);
|
||||
maxPlaylistCount = intent.getIntExtra("maxPlaylistCount", 0);
|
||||
|
||||
// Hide playlist if device does not support it.
|
||||
playlistSpinnerLayout.setVisibility(maxPlaylistCount>0?View.VISIBLE:View.GONE);
|
||||
|
||||
allMusic.clear();
|
||||
musicList.clear();
|
||||
initPlaylists();
|
||||
}
|
||||
|
||||
private void musicListFromDevice(Intent intent) {
|
||||
ArrayList<GBDeviceMusic> list = (ArrayList<GBDeviceMusic>) intent.getSerializableExtra("musicList");
|
||||
if (list != null && !list.isEmpty()) {
|
||||
allMusic.addAll(list);
|
||||
}
|
||||
|
||||
ArrayList<GBDeviceMusicPlaylist> devicePlaylist = (ArrayList<GBDeviceMusicPlaylist>) intent.getSerializableExtra("musicPlaylist");
|
||||
if (devicePlaylist != null && !devicePlaylist.isEmpty()) {
|
||||
playlists.addAll(devicePlaylist);
|
||||
playlistAdapter.notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void musicOperationResponse(Intent intent) {
|
||||
int operation = intent.getIntExtra("operation", -1);
|
||||
if (operation == 0) {
|
||||
int playlistIndex = intent.getIntExtra("playlistIndex", -1);
|
||||
String playlistName = intent.getStringExtra("playlistName");
|
||||
|
||||
if (playlistIndex != -1 && !TextUtils.isEmpty(playlistName)) {
|
||||
playlists.add(new GBDeviceMusicPlaylist(playlistIndex, playlistName, new ArrayList<>()));
|
||||
playlistAdapter.notifyDataSetChanged();
|
||||
}
|
||||
} else if (operation == 1) {
|
||||
int playlistIndex = intent.getIntExtra("playlistIndex", -1);
|
||||
if (playlistIndex != -1) {
|
||||
for (Iterator<GBDeviceMusicPlaylist> iterator = playlists.iterator(); iterator.hasNext(); ) {
|
||||
GBDeviceMusicPlaylist playlist = iterator.next();
|
||||
if (playlist.getId() == playlistIndex) {
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
playlistAdapter.notifyDataSetChanged();
|
||||
}
|
||||
} else if (operation == 2) {
|
||||
int playlistIndex = intent.getIntExtra("playlistIndex", -1);
|
||||
String playlistName = intent.getStringExtra("playlistName");
|
||||
if (playlistIndex != -1 && !TextUtils.isEmpty(playlistName)) {
|
||||
for (GBDeviceMusicPlaylist playlist : playlists) {
|
||||
if (playlist.getId() == playlistIndex) {
|
||||
playlist.setName(playlistName);
|
||||
break;
|
||||
}
|
||||
}
|
||||
playlistAdapter.notifyDataSetChanged();
|
||||
}
|
||||
} else if (operation == 3) {
|
||||
int playlistIndex = intent.getIntExtra("playlistIndex", -1);
|
||||
ArrayList<Integer> ids = (ArrayList<Integer>) intent.getSerializableExtra("musicIds");
|
||||
if (playlistIndex != -1 && ids != null && !ids.isEmpty()) {
|
||||
for (GBDeviceMusicPlaylist playlist : playlists) {
|
||||
if (playlist.getId() == playlistIndex) {
|
||||
ArrayList<Integer> currentList = playlist.getMusicIds();
|
||||
for (Integer id : ids) {
|
||||
if (!currentList.contains(id))
|
||||
currentList.add(id);
|
||||
}
|
||||
playlist.setMusicIds(currentList);
|
||||
break;
|
||||
}
|
||||
}
|
||||
playlistAdapter.notifyDataSetChanged();
|
||||
updateCurrentMusicList();
|
||||
}
|
||||
|
||||
} else if (operation == 4) {
|
||||
ArrayList<Integer> ids = (ArrayList<Integer>) intent.getSerializableExtra("musicIds");
|
||||
int playlistIndex = intent.getIntExtra("playlistIndex", 0);
|
||||
if (ids != null && !ids.isEmpty()) {
|
||||
if (playlistIndex == 0) {
|
||||
for (Iterator<GBDeviceMusic> iterator = musicList.iterator(); iterator.hasNext(); ) {
|
||||
GBDeviceMusic music = iterator.next();
|
||||
if (ids.contains(music.getId())) {
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
for (Iterator<GBDeviceMusic> iterator = allMusic.iterator(); iterator.hasNext(); ) {
|
||||
GBDeviceMusic music = iterator.next();
|
||||
if (ids.contains(music.getId())) {
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (GBDeviceMusicPlaylist playlist : playlists) {
|
||||
if (playlist.getId() == playlistIndex) {
|
||||
ArrayList<Integer> currentList = playlist.getMusicIds();
|
||||
for (Integer id : ids) {
|
||||
currentList.remove(id);
|
||||
}
|
||||
playlist.setMusicIds(currentList);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
playlistAdapter.notifyDataSetChanged();
|
||||
updateCurrentMusicList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String action = intent.getAction();
|
||||
switch (action) {
|
||||
case ACTION_MUSIC_DATA: {
|
||||
if (!intent.hasExtra("type"))
|
||||
break;
|
||||
int type = intent.getIntExtra("type", -1);
|
||||
|
||||
LOG.info("UPDATE type: {}", type);
|
||||
if (type == 1) {
|
||||
startSyncFromDevice(intent);
|
||||
} else if (type == 2) {
|
||||
LOG.info("got music list or playlist from device");
|
||||
musicListFromDevice(intent);
|
||||
} else if (type == 10) {
|
||||
updateCurrentMusicList();
|
||||
stopLoading();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ACTION_MUSIC_UPDATE: {
|
||||
boolean success = intent.getBooleanExtra("success", false);
|
||||
if (intent.hasExtra("operation") && success) {
|
||||
musicOperationResponse(intent);
|
||||
}
|
||||
stopLoading();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
/* Copyright (C) 2024 Arjan Schrijver
|
||||
|
||||
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.welcome;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity;
|
||||
|
||||
public class WelcomeActivity extends AbstractGBActivity {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(WelcomeActivity.class);
|
||||
|
||||
private ViewPager2 viewPager;
|
||||
private WelcomeFragmentsPagerAdapter pagerAdapter;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
AbstractGBActivity.init(this, AbstractGBActivity.NO_ACTIONBAR);
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_welcome);
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().hide();
|
||||
}
|
||||
|
||||
// Configure ViewPager2 with fragment adapter and default fragment
|
||||
viewPager = findViewById(R.id.welcome_viewpager);
|
||||
pagerAdapter = new WelcomeFragmentsPagerAdapter(this);
|
||||
viewPager.setAdapter(pagerAdapter);
|
||||
|
||||
// Set up welcome page indicator
|
||||
WelcomePageIndicator pageIndicator = findViewById(R.id.welcome_page_indicator);
|
||||
pageIndicator.setViewPager(viewPager);
|
||||
}
|
||||
|
||||
private class WelcomeFragmentsPagerAdapter extends FragmentStateAdapter {
|
||||
public WelcomeFragmentsPagerAdapter(FragmentActivity fa) {
|
||||
super(fa);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Fragment createFragment(int position) {
|
||||
switch (position) {
|
||||
case 0:
|
||||
return new WelcomeFragmentIntro();
|
||||
case 1:
|
||||
return new WelcomeFragmentOverview();
|
||||
case 2:
|
||||
return new WelcomeFragmentDocsSource();
|
||||
case 3:
|
||||
return new WelcomeFragmentPermissions();
|
||||
default:
|
||||
return new WelcomeFragmentGetStarted();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return 5;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
/* Copyright (C) 2024 Arjan Schrijver
|
||||
|
||||
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.welcome;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
|
||||
public class WelcomeFragmentDocsSource extends Fragment {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(WelcomeFragmentDocsSource.class);
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
super.onCreateView(inflater, container, savedInstanceState);
|
||||
return inflater.inflate(R.layout.fragment_welcome_docs_source, container, false);
|
||||
}
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
/* Copyright (C) 2024 Arjan Schrijver
|
||||
|
||||
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.welcome;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.DataManagementActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.discovery.DiscoveryActivityV2;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
|
||||
public class WelcomeFragmentGetStarted extends Fragment {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(WelcomeFragmentGetStarted.class);
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
super.onCreateView(inflater, container, savedInstanceState);
|
||||
View view = inflater.inflate(R.layout.fragment_welcome_get_started, container, false);
|
||||
|
||||
Button firstDevice = view.findViewById(R.id.welcome_button_add_device);
|
||||
firstDevice.setOnClickListener(firstDeviceButton -> startActivity(new Intent(requireActivity(), DiscoveryActivityV2.class)));
|
||||
Button restore = view.findViewById(R.id.welcome_button_restore);
|
||||
restore.setOnClickListener(restoreButton -> startActivity(new Intent(requireActivity(), DataManagementActivity.class)));
|
||||
Button toApp = view.findViewById(R.id.welcome_button_to_app);
|
||||
toApp.setOnClickListener(toAppButton -> {
|
||||
Prefs prefs = GBApplication.getPrefs();
|
||||
prefs.getPreferences().edit().putBoolean("first_run", false).apply();
|
||||
requireActivity().finish();
|
||||
});
|
||||
|
||||
return view;
|
||||
}
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
/* Copyright (C) 2024 Arjan Schrijver
|
||||
|
||||
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.welcome;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
|
||||
import com.google.android.material.textfield.MaterialAutoCompleteTextView;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
|
||||
public class WelcomeFragmentIntro extends Fragment {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(WelcomeFragmentIntro.class);
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
super.onCreateView(inflater, container, savedInstanceState);
|
||||
|
||||
final View view = inflater.inflate(R.layout.fragment_welcome_intro, container, false);
|
||||
final String[] themes = getResources().getStringArray(R.array.pref_theme_values);
|
||||
final Prefs prefs = GBApplication.getPrefs();
|
||||
final String currentTheme = prefs.getString("pref_key_theme", getString(R.string.pref_theme_value_system));
|
||||
final int currentThemeIndex = Arrays.asList(themes).indexOf(currentTheme);
|
||||
|
||||
final MaterialAutoCompleteTextView themeMenu = view.findViewById(R.id.app_theme_dropdown_menu);
|
||||
themeMenu.setSaveEnabled(false); // https://github.com/material-components/material-components-android/issues/1464#issuecomment-1258051448
|
||||
themeMenu.setText(getResources().getStringArray(R.array.pref_theme_options)[currentThemeIndex], false);
|
||||
themeMenu.setOnItemClickListener((adapterView, view1, i, l) -> {
|
||||
final SharedPreferences.Editor editor = prefs.getPreferences().edit();
|
||||
editor.putString("pref_key_theme", themes[i]).apply();
|
||||
final Handler handler = new Handler();
|
||||
handler.postDelayed(() -> {
|
||||
// Delay recreation of the Activity to give the dropdown some time to settle.
|
||||
// If we recreate it immediately, the theme popup will reopen, which is not what the user expects.
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(GBApplication.ACTION_THEME_CHANGE);
|
||||
LocalBroadcastManager.getInstance(requireContext()).sendBroadcast(intent);
|
||||
}, 500);
|
||||
});
|
||||
return view;
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
/* Copyright (C) 2024 Arjan Schrijver
|
||||
|
||||
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.welcome;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
|
||||
public class WelcomeFragmentOverview extends Fragment {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(WelcomeFragmentOverview.class);
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
super.onCreateView(inflater, container, savedInstanceState);
|
||||
return inflater.inflate(R.layout.fragment_welcome_overview, container, false);
|
||||
}
|
||||
}
|
@ -0,0 +1,173 @@
|
||||
/* Copyright (C) 2024 Arjan Schrijver
|
||||
|
||||
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.welcome;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.PermissionsUtils;
|
||||
|
||||
public class WelcomeFragmentPermissions extends Fragment {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(WelcomeFragmentPermissions.class);
|
||||
|
||||
private RecyclerView permissionsListView;
|
||||
private PermissionAdapter permissionAdapter;
|
||||
private Button requestAllButton;
|
||||
private List<String> requestingPermissions = new ArrayList<>();
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
super.onCreateView(inflater, container, savedInstanceState);
|
||||
View view = inflater.inflate(R.layout.fragment_welcome_permissions, container, false);
|
||||
|
||||
requestAllButton = view.findViewById(R.id.button_request_all);
|
||||
requestAllButton.setOnClickListener(v -> {
|
||||
List<PermissionsUtils.PermissionDetails> wantedPermissions = PermissionsUtils.getRequiredPermissionsList(requireActivity());
|
||||
requestingPermissions = new ArrayList<>();
|
||||
for (PermissionsUtils.PermissionDetails wantedPermission : wantedPermissions) {
|
||||
requestingPermissions.add(wantedPermission.getPermission());
|
||||
}
|
||||
requestAllPermissions();
|
||||
});
|
||||
|
||||
if (((AppCompatActivity)getActivity()).getSupportActionBar().isShowing()) {
|
||||
// Hide title when the Action Bar is visible (i.e. when not in the first run flow)
|
||||
view.findViewById(R.id.permissions_title).setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
// Initialize RecyclerView and data
|
||||
permissionsListView = view.findViewById(R.id.permissions_list);
|
||||
|
||||
// Set up RecyclerView
|
||||
permissionAdapter = new PermissionAdapter(PermissionsUtils.getRequiredPermissionsList(requireActivity()), requireContext());
|
||||
permissionsListView.setLayoutManager(new LinearLayoutManager(requireContext()));
|
||||
permissionsListView.setAdapter(permissionAdapter);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
permissionAdapter.notifyDataSetChanged();
|
||||
if (PermissionsUtils.checkAllPermissions(requireActivity())) {
|
||||
requestAllButton.setEnabled(false);
|
||||
}
|
||||
if (!requestingPermissions.isEmpty()) {
|
||||
requestAllPermissions();
|
||||
}
|
||||
}
|
||||
|
||||
public void requestAllPermissions() {
|
||||
if (!requestingPermissions.isEmpty()) {
|
||||
Iterator<String> it = requestingPermissions.iterator();
|
||||
while (it.hasNext()) {
|
||||
String currentPermission = it.next();
|
||||
if (PermissionsUtils.specialPermissions.contains(currentPermission)) {
|
||||
it.remove();
|
||||
if (!PermissionsUtils.checkPermission(requireActivity(), currentPermission)) {
|
||||
PermissionsUtils.requestPermission(requireActivity(), currentPermission);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
String[] combinedPermissions = requestingPermissions.toArray(new String[0]);
|
||||
requestingPermissions.clear();
|
||||
ActivityCompat.requestPermissions(requireActivity(), combinedPermissions, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private class PermissionHolder extends RecyclerView.ViewHolder {
|
||||
TextView titleTextView;
|
||||
TextView summaryTextView;
|
||||
ImageView checkmarkImageView;
|
||||
Button requestButton;
|
||||
|
||||
public PermissionHolder(View itemView) {
|
||||
super(itemView);
|
||||
titleTextView = itemView.findViewById(R.id.permission_title);
|
||||
summaryTextView = itemView.findViewById(R.id.permission_summary);
|
||||
checkmarkImageView = itemView.findViewById(R.id.permission_check);
|
||||
requestButton = itemView.findViewById(R.id.permission_request);
|
||||
}
|
||||
}
|
||||
|
||||
private class PermissionAdapter extends RecyclerView.Adapter<PermissionHolder> {
|
||||
private List<PermissionsUtils.PermissionDetails> permissionList;
|
||||
private Context context;
|
||||
|
||||
public PermissionAdapter(List<PermissionsUtils.PermissionDetails> permissionList, Context context) {
|
||||
this.permissionList = permissionList;
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public PermissionHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View itemView = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.fragment_welcome_permission_row, parent, false);
|
||||
return new PermissionHolder(itemView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull PermissionHolder holder, int position) {
|
||||
PermissionsUtils.PermissionDetails permissionData = permissionList.get(position);
|
||||
holder.titleTextView.setText(permissionData.getTitle());
|
||||
holder.summaryTextView.setText(permissionData.getSummary());
|
||||
if (PermissionsUtils.checkPermission(requireContext(), permissionData.getPermission())) {
|
||||
holder.requestButton.setVisibility(View.INVISIBLE);
|
||||
holder.requestButton.setEnabled(false);
|
||||
holder.checkmarkImageView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
holder.requestButton.setVisibility(View.VISIBLE);
|
||||
holder.requestButton.setEnabled(true);
|
||||
holder.checkmarkImageView.setVisibility(View.GONE);
|
||||
holder.requestButton.setOnClickListener(view -> {
|
||||
PermissionsUtils.requestPermission(requireActivity(), permissionData.getPermission());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return permissionList.size();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,135 @@
|
||||
/* Copyright (C) 2024 Arjan Schrijver
|
||||
|
||||
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.welcome;
|
||||
|
||||
import android.animation.ValueAnimator;
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
|
||||
public class WelcomePageIndicator extends View {
|
||||
private ViewPager2 viewPager;
|
||||
private int pageCount;
|
||||
private int dotRadius = 15;
|
||||
private int color;
|
||||
|
||||
private Paint outlinePaint;
|
||||
private Paint filledPaint;
|
||||
private float currentX = 0.0f;
|
||||
|
||||
private ValueAnimator dotAnimator;
|
||||
|
||||
public WelcomePageIndicator(Context context) {
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public WelcomePageIndicator(Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
determineColor(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
public WelcomePageIndicator(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
determineColor(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
private void determineColor(Context context, @Nullable AttributeSet attrs) {
|
||||
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.WelcomePageIndicator);
|
||||
color = a.getColor(R.styleable.WelcomePageIndicator_page_indicator_color, Color.BLACK);
|
||||
a.recycle();
|
||||
}
|
||||
|
||||
private void init() {
|
||||
outlinePaint = new Paint();
|
||||
outlinePaint.setColor(color);
|
||||
outlinePaint.setStyle(Paint.Style.STROKE);
|
||||
outlinePaint.setStrokeWidth(4);
|
||||
outlinePaint.setAntiAlias(true);
|
||||
filledPaint = new Paint();
|
||||
filledPaint.setColor(color);
|
||||
filledPaint.setStyle(Paint.Style.FILL);
|
||||
outlinePaint.setAntiAlias(true);
|
||||
}
|
||||
|
||||
public void setViewPager(ViewPager2 viewPager) {
|
||||
this.viewPager = viewPager;
|
||||
this.pageCount = viewPager.getAdapter().getItemCount();
|
||||
viewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
|
||||
@Override
|
||||
public void onPageSelected(int position) {
|
||||
animateIndicator(position);
|
||||
}
|
||||
});
|
||||
invalidate();
|
||||
}
|
||||
|
||||
private int getHorizontalMargin() {
|
||||
int dotDiameter = dotRadius * 2;
|
||||
int dotSpaces = pageCount * 2 - 1;
|
||||
return (getWidth() - dotSpaces * dotDiameter) / 2 + dotRadius;
|
||||
}
|
||||
|
||||
private void animateIndicator(int position) {
|
||||
float horizontalMargin = getHorizontalMargin();
|
||||
if (horizontalMargin <= 0.0f) {
|
||||
// Not animating because the drawable is not ready yet
|
||||
return;
|
||||
}
|
||||
float targetX = horizontalMargin + 4 * dotRadius * position;
|
||||
if (dotAnimator != null && dotAnimator.isRunning()) {
|
||||
dotAnimator.cancel();
|
||||
}
|
||||
if (currentX == 0.0f) currentX = horizontalMargin;
|
||||
dotAnimator = ValueAnimator.ofFloat(currentX, targetX);
|
||||
dotAnimator.addUpdateListener(animation -> {
|
||||
currentX = (float) animation.getAnimatedValue();
|
||||
invalidate();
|
||||
});
|
||||
dotAnimator.setDuration(300);
|
||||
dotAnimator.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
|
||||
if (viewPager == null || pageCount == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
float horizontalMargin = getHorizontalMargin();
|
||||
if (currentX == 0.0f && horizontalMargin != 0.0f) currentX = horizontalMargin;
|
||||
float circleY = getHeight() / 2f;
|
||||
for (int i = 0; i < pageCount; i++) {
|
||||
float circleX = horizontalMargin + 4 * dotRadius * i;
|
||||
canvas.drawCircle(circleX, circleY, dotRadius, outlinePaint);
|
||||
}
|
||||
canvas.drawCircle(currentX, circleY, dotRadius, filledPaint);
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@ package nodomain.freeyourgadget.gadgetbridge.activities.workouts;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_CM;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_KG;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_KILOMETERS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_METERS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_METERS_PER_SECOND;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_SECONDS_PER_KM;
|
||||
@ -106,6 +107,12 @@ public class WorkoutValueFormatter {
|
||||
unit = "minutes_km";
|
||||
}
|
||||
break;
|
||||
case UNIT_KILOMETERS:
|
||||
if (units.equals(UNIT_IMPERIAL)) {
|
||||
value = value * 0.621371D;
|
||||
unit = "mi";
|
||||
}
|
||||
break;
|
||||
case UNIT_METERS:
|
||||
if (units.equals(UNIT_IMPERIAL)) {
|
||||
value = value * 3.28084D;
|
||||
|
@ -0,0 +1,85 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.adapter;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceMusic;
|
||||
|
||||
public class MusicListAdapter extends RecyclerView.Adapter<MusicListAdapter.MusicViewHolder> {
|
||||
|
||||
public interface onItemAction {
|
||||
void onItemClick(View view, GBDeviceMusic music);
|
||||
boolean onItemLongClick(View view, GBDeviceMusic music);
|
||||
}
|
||||
|
||||
private final List<GBDeviceMusic> musicList;
|
||||
private final onItemAction callback;
|
||||
|
||||
public MusicListAdapter(List<GBDeviceMusic> list, onItemAction callback) {
|
||||
this.musicList = list;
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return musicList.get(position).getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return musicList.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MusicListAdapter.MusicViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_musicmanager_song, parent, false);
|
||||
return new MusicViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(final MusicListAdapter.MusicViewHolder holder, int position) {
|
||||
final GBDeviceMusic music = musicList.get(position);
|
||||
|
||||
holder.musicTitle.setText(music.getTitle());
|
||||
holder.musicArtist.setText(music.getArtist());
|
||||
|
||||
if(callback != null) {
|
||||
holder.itemView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
callback.onItemClick(view, music);
|
||||
}
|
||||
});
|
||||
|
||||
holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
|
||||
@Override
|
||||
public boolean onLongClick(View view) {
|
||||
return callback.onItemLongClick(view, music);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class MusicViewHolder extends RecyclerView.ViewHolder {
|
||||
final TextView musicArtist;
|
||||
final TextView musicTitle;
|
||||
|
||||
MusicViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
musicArtist = itemView.findViewById(R.id.item_details);
|
||||
musicTitle = itemView.findViewById(R.id.item_name);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -20,11 +20,16 @@ import android.content.Context;
|
||||
import android.os.AsyncTask;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
public abstract class DBAccess extends AsyncTask {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DBAccess.class);
|
||||
|
||||
private final String mTask;
|
||||
private final Context mContext;
|
||||
private Exception mError;
|
||||
@ -45,6 +50,7 @@ public abstract class DBAccess extends AsyncTask {
|
||||
try (DBHandler db = GBApplication.acquireDB()) {
|
||||
doInBackground(db);
|
||||
} catch (Exception e) {
|
||||
LOG.error("Error during DBAccess for {}", mTask, e);
|
||||
mError = e;
|
||||
}
|
||||
return null;
|
||||
|
@ -0,0 +1,15 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.deviceevents;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceMusic;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceMusicPlaylist;
|
||||
|
||||
public class GBDeviceMusicData extends GBDeviceEvent {
|
||||
public int type = 0; // 1 - sync start, 2 - music list, 10 - end sync
|
||||
public List<GBDeviceMusic> list = null;
|
||||
public List<GBDeviceMusicPlaylist> playlists = null;
|
||||
public String deviceInfo = null;
|
||||
public int maxMusicCount = 0;
|
||||
public int maxPlaylistCount = 0;
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.deviceevents;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class GBDeviceMusicUpdate extends GBDeviceEvent {
|
||||
public boolean success = false;
|
||||
public int operation = -1;
|
||||
public int playlistIndex = -1;
|
||||
public String playlistName;
|
||||
public ArrayList<Integer> musicIds = null;
|
||||
}
|
@ -95,7 +95,7 @@ public interface EventHandler {
|
||||
|
||||
void onAppConfiguration(UUID appUuid, String config, Integer id);
|
||||
|
||||
void onAppReorder(UUID uuids[]);
|
||||
void onAppReorder(UUID[] uuids);
|
||||
|
||||
void onFetchRecordedData(int dataTypes);
|
||||
|
||||
@ -154,4 +154,9 @@ public interface EventHandler {
|
||||
void onSleepAsAndroidAction(String action, Bundle extras);
|
||||
|
||||
void onCameraStatusChange(GBDeviceEventCameraRemote.Event event, String filename);
|
||||
|
||||
|
||||
void onMusicListReq();
|
||||
|
||||
void onMusicOperation(int operation, int playlistIndex, String playlistName, ArrayList<Integer> musicIds);
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import androidx.annotation.NonNull;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLEDeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
@ -13,7 +13,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.BatteryConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.bandwpseries.BandWPSeriesDeviceSupport;
|
||||
|
||||
public class BandWPSeriesDeviceCoordinator extends AbstractDeviceCoordinator {
|
||||
public class BandWPSeriesDeviceCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
@Override
|
||||
public int getDeviceNameResource() {
|
||||
return R.string.devicetype_bandw_pseries;
|
||||
@ -51,5 +51,13 @@ public class BandWPSeriesDeviceCoordinator extends AbstractDeviceCoordinator {
|
||||
return new BatteryConfig[]{battery0, battery1, battery2};
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
|
||||
return new int[] {
|
||||
R.xml.devicesettings_active_noise_cancelling_toggle,
|
||||
R.xml.devicesettings_bandw_pseries,
|
||||
R.xml.devicesettings_wear_sensor_toggle
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
@ -36,6 +37,7 @@ import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpec
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.agps.GarminAgpsStatus;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
|
||||
@ -142,7 +144,10 @@ public class GarminSettingsCustomizer implements DeviceSpecificSettingsCustomize
|
||||
prefUpdateTime.setTitle(R.string.pref_agps_update_time);
|
||||
final long ts = prefs.getLong(GarminPreferences.agpsUpdateTime(url), 0L);
|
||||
if (ts > 0) {
|
||||
prefUpdateTime.setSummary(SDF.format(new Date(ts)));
|
||||
prefUpdateTime.setSummary(String.format("%s (%s)",
|
||||
SDF.format(new Date(ts)),
|
||||
DateTimeUtils.formatDurationHoursMinutes(System.currentTimeMillis() - ts, TimeUnit.MILLISECONDS)
|
||||
));
|
||||
} else {
|
||||
prefUpdateTime.setSummary(handler.getContext().getString(R.string.unknown));
|
||||
}
|
||||
|
@ -0,0 +1,18 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.fenix;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminCoordinator;
|
||||
|
||||
public class GarminFenix6SProCoordinator extends GarminCoordinator {
|
||||
@Override
|
||||
protected Pattern getSupportedDeviceName() {
|
||||
return Pattern.compile("^fenix 6S Pro$");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDeviceNameResource() {
|
||||
return R.string.devicetype_garmin_fenix_6s_pro;
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
/* Copyright (C) 2024 José Rebelo
|
||||
|
||||
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.garmin.watches.forerunner;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminCoordinator;
|
||||
|
||||
public class GarminForerunner55Coordinator extends GarminCoordinator {
|
||||
@Override
|
||||
protected Pattern getSupportedDeviceName() {
|
||||
return Pattern.compile("^Forerunner 55$");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDeviceNameResource() {
|
||||
return R.string.devicetype_garmin_forerunner_55;
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
/* Copyright (C) 2024 José Rebelo
|
||||
|
||||
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.garmin.watches.forerunner;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminCoordinator;
|
||||
|
||||
public class GarminForerunner620Coordinator extends GarminCoordinator {
|
||||
@Override
|
||||
protected Pattern getSupportedDeviceName() {
|
||||
return Pattern.compile("^Forerunner 620$");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDeviceNameResource() {
|
||||
return R.string.devicetype_garmin_forerunner_620;
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.instinct;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminCoordinator;
|
||||
|
||||
public class GarminInstinct2Coordinator extends GarminCoordinator {
|
||||
@Override
|
||||
protected Pattern getSupportedDeviceName() {
|
||||
return Pattern.compile("^Instinct 2$");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDeviceNameResource() {
|
||||
return R.string.devicetype_garmin_instinct_2;
|
||||
}
|
||||
}
|
@ -56,7 +56,9 @@ public final class HuaweiConstants {
|
||||
public static final String HU_BAND4E_NAME = "huawei band 4e-";
|
||||
public static final String HU_BAND6_NAME = "huawei band 6-";
|
||||
public static final String HU_WATCHGT_NAME = "huawei watch gt-";
|
||||
public static final String HU_BAND3_NAME = "huawei band 3-";
|
||||
public static final String HU_BAND4_NAME = "huawei band 4-";
|
||||
public static final String HU_BAND3PRO_NAME = "huawei band 3 pro-";
|
||||
public static final String HU_BAND4PRO_NAME = "huawei band 4 pro-";
|
||||
public static final String HU_WATCHGT2_NAME = "huawei watch gt 2-";
|
||||
public static final String HU_WATCHGT2E_NAME = "huawei watch gt 2e-";
|
||||
|
@ -305,6 +305,11 @@ public class HuaweiCoordinator {
|
||||
deviceSpecificSettings.addRootScreen(R.xml.devicesettings_contacts);
|
||||
}
|
||||
|
||||
//Music
|
||||
if (supportsMusicUploading() && getMusicInfoParams() != null && device.isConnected()) {
|
||||
deviceSpecificSettings.addRootScreen(R.xml.devicesettings_musicmanagement);
|
||||
}
|
||||
|
||||
// Time
|
||||
if (supportsDateFormat()) {
|
||||
final List<Integer> dateTime = deviceSpecificSettings.addRootScreen(DeviceSpecificSettingsScreen.DATE_TIME);
|
||||
@ -448,6 +453,14 @@ public class HuaweiCoordinator {
|
||||
return supportsHeartRate() || getForceOption(gbDevice, PREF_FORCE_ENABLE_HEARTRATE_SUPPORT);
|
||||
}
|
||||
|
||||
public boolean supportsHeartRateZones() {
|
||||
return supportsCommandForService(0x07, 0x13);
|
||||
}
|
||||
|
||||
public boolean supportsExtendedHeartRateZones() {
|
||||
return supportsCommandForService(0x07, 0x21);
|
||||
}
|
||||
|
||||
public boolean supportsFitnessRestHeartRate() {
|
||||
return supportsCommandForService(0x07, 0x23);
|
||||
}
|
||||
@ -602,8 +615,6 @@ public class HuaweiCoordinator {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public boolean supportsCalendar() {
|
||||
if (supportsExpandCapability())
|
||||
return supportsExpandCapability(171) || supportsExpandCapability(184);
|
||||
@ -628,6 +639,12 @@ public class HuaweiCoordinator {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean supportsMoreMusic() {
|
||||
if (supportsExpandCapability())
|
||||
return supportsExpandCapability(122);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
public boolean supportsPromptPushMessage () {
|
||||
// do not ask for capabilities under specific condition
|
||||
|
@ -7,16 +7,16 @@ import java.util.List;
|
||||
public class HuaweiMusicUtils {
|
||||
|
||||
public static class PageStruct {
|
||||
public short startIndex = 0;
|
||||
public short endIndex = 0;
|
||||
public short startFrame = 0;
|
||||
public short endFrame = 0;
|
||||
public short count = 0;
|
||||
public byte[] hashCode = null;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
final StringBuffer sb = new StringBuffer("PageStruct{");
|
||||
sb.append("startIndex=").append(startIndex);
|
||||
sb.append(", endIndex=").append(endIndex);
|
||||
sb.append("startFrame=").append(startFrame);
|
||||
sb.append(", endFrame=").append(endFrame);
|
||||
sb.append(", count=").append(count);
|
||||
sb.append(", hashCode=");
|
||||
if (hashCode == null) sb.append("null");
|
||||
@ -68,7 +68,6 @@ public class HuaweiMusicUtils {
|
||||
public int currentMusicCount = 0; // TODO: not sure
|
||||
public int unknown = 0; // TODO: not sure
|
||||
public List<FormatRestrictions> formatsRestrictions = null;
|
||||
public List<PageStruct> pageStruct = null;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
@ -80,7 +79,6 @@ public class HuaweiMusicUtils {
|
||||
sb.append(", currentMusicCount=").append(currentMusicCount);
|
||||
sb.append(", unknown=").append(unknown);
|
||||
sb.append(", formatsRestrictions=").append(formatsRestrictions);
|
||||
sb.append(", pageStruct=").append(pageStruct);
|
||||
sb.append('}');
|
||||
return sb.toString();
|
||||
}
|
||||
|
@ -598,6 +598,14 @@ public class HuaweiPacket {
|
||||
return new MusicControl.Control.Response(paramsProvider).fromPacket(this);
|
||||
case MusicControl.MusicInfoParams.id:
|
||||
return new MusicControl.MusicInfoParams.Response(paramsProvider).fromPacket(this);
|
||||
case MusicControl.MusicList.id:
|
||||
return new MusicControl.MusicList.Response(paramsProvider).fromPacket(this);
|
||||
case MusicControl.MusicPlaylists.id:
|
||||
return new MusicControl.MusicPlaylists.Response(paramsProvider).fromPacket(this);
|
||||
case MusicControl.MusicPlaylistMusics.id:
|
||||
return new MusicControl.MusicPlaylistMusics.Response(paramsProvider).fromPacket(this);
|
||||
case MusicControl.MusicOperation.id:
|
||||
return new MusicControl.MusicOperation.Response(paramsProvider).fromPacket(this);
|
||||
case MusicControl.UploadMusicFileInfo.id:
|
||||
return new MusicControl.UploadMusicFileInfo.UploadMusicFileInfoRequest(paramsProvider).fromPacket(this);
|
||||
case MusicControl.ExtendedMusicInfoParams.id:
|
||||
|
@ -223,10 +223,26 @@ public class HuaweiTLV {
|
||||
return getBytes(tag)[0];
|
||||
}
|
||||
|
||||
public Byte getByte(int tag, Byte defaultValue) {
|
||||
try {
|
||||
return getByte(tag);
|
||||
} catch (HuaweiPacket.MissingTagException e) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
public Boolean getBoolean(int tag) throws HuaweiPacket.MissingTagException {
|
||||
return getBytes(tag)[0] == 1;
|
||||
}
|
||||
|
||||
public Boolean getBoolean(int tag, Boolean defaultValue) {
|
||||
try {
|
||||
return getBoolean(tag);
|
||||
} catch (HuaweiPacket.MissingTagException e) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
public Integer getInteger(int tag) throws HuaweiPacket.MissingTagException {
|
||||
return ByteBuffer.wrap(getBytes(tag)).getInt();
|
||||
}
|
||||
@ -243,6 +259,14 @@ public class HuaweiTLV {
|
||||
return ByteBuffer.wrap(getBytes(tag)).getShort();
|
||||
}
|
||||
|
||||
public Short getShort(int tag, Short defaultValue) {
|
||||
try {
|
||||
return getShort(tag);
|
||||
} catch (HuaweiPacket.MissingTagException e) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
public Long getLong(int tag) throws HuaweiPacket.MissingTagException {
|
||||
return ByteBuffer.wrap(getBytes(tag)).getLong();
|
||||
}
|
||||
|
@ -0,0 +1,41 @@
|
||||
/* Copyright (C) 2024 Damien Gaignon, Guido Jäkel, 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.huaweiband3pro;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiLECoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
|
||||
public class HuaweiBand3ProCoordinator extends HuaweiLECoordinator {
|
||||
@Override
|
||||
public DeviceType getDeviceType() {
|
||||
return DeviceType.HUAWEIBAND3PRO;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Pattern getSupportedDeviceName() {
|
||||
return Pattern.compile("(" + HuaweiConstants.HU_BAND3_NAME + "|" + HuaweiConstants.HU_BAND3PRO_NAME + ").*", Pattern.CASE_INSENSITIVE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDeviceNameResource() {
|
||||
return R.string.devicetype_huawei_band3pro;
|
||||
}
|
||||
}
|
@ -92,11 +92,11 @@ public class Alarms {
|
||||
|
||||
public SmartAlarm(HuaweiTLV tlv) throws ParseException {
|
||||
this.index = tlv.getByte(0x03);
|
||||
this.status = tlv.getBoolean(0x04);
|
||||
this.startHour = (byte) ((tlv.getShort(0x05) >> 8) & 0xFF);
|
||||
this.startMinute = (byte) (tlv.getShort(0x05) & 0xFF);
|
||||
this.repeat = tlv.getByte(0x06);
|
||||
this.aheadTime = tlv.getByte(0x07);
|
||||
this.status = tlv.getBoolean(0x04, false);
|
||||
this.startHour = (byte) ((tlv.getShort(0x05, (short) 0) >> 8) & 0xFF);
|
||||
this.startMinute = (byte) (tlv.getShort(0x05, (short) 0) & 0xFF);
|
||||
this.repeat = tlv.getByte(0x06, (byte) 0);
|
||||
this.aheadTime = tlv.getByte(0x07, (byte) 0);
|
||||
}
|
||||
|
||||
public SmartAlarm(boolean status, byte startHour, byte startMinute, byte repeat, byte aheadTime) {
|
||||
|
@ -20,6 +20,7 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HeartRateZonesConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiReportThreshold;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV;
|
||||
@ -597,6 +598,71 @@ public class FitnessData {
|
||||
}
|
||||
}
|
||||
|
||||
public static class HeartRateZoneConfigPacket {
|
||||
// It can use two IDs with basically the same format.
|
||||
public static final byte id_simple = 0x13;
|
||||
public static final byte id_extended = 0x21;
|
||||
|
||||
public static class Request extends HuaweiPacket {
|
||||
private Request(
|
||||
ParamsProvider paramsProvider,
|
||||
byte id,
|
||||
HeartRateZonesConfig heartRateZonesConfig
|
||||
) {
|
||||
super(paramsProvider);
|
||||
|
||||
this.serviceId = FitnessData.id;
|
||||
this.commandId = id;
|
||||
|
||||
HuaweiTLV subTlv = new HuaweiTLV().
|
||||
put(0x08, heartRateZonesConfig.getWarningEnable());
|
||||
|
||||
if (
|
||||
heartRateZonesConfig.hasValidMHRData() &&
|
||||
heartRateZonesConfig.getWarningHRLimit() > 0 &&
|
||||
heartRateZonesConfig.getMaxHRThreshold() > 0
|
||||
) {
|
||||
subTlv
|
||||
.put(0x09, (byte) heartRateZonesConfig.getWarningHRLimit())
|
||||
.put(0x02, (byte) heartRateZonesConfig.getMHRWarmUp())
|
||||
.put(0x03, (byte) heartRateZonesConfig.getMHRFatBurning())
|
||||
.put(0x04, (byte) heartRateZonesConfig.getMHRAerobic())
|
||||
.put(0x05, (byte) heartRateZonesConfig.getMHRAnaerobic())
|
||||
.put(0x06, (byte) heartRateZonesConfig.getMHRExtreme())
|
||||
.put(0x07, (byte) heartRateZonesConfig.getMaxHRThreshold())
|
||||
.put(0x0b, (byte) heartRateZonesConfig.getMaxHRThreshold());
|
||||
}
|
||||
|
||||
if (id == id_extended && heartRateZonesConfig.hasValidHRRData()) {
|
||||
subTlv
|
||||
.put(0x0d, (byte) heartRateZonesConfig.getHRRBasicAerobic())
|
||||
.put(0x0e, (byte) heartRateZonesConfig.getHRRAdvancedAerobic())
|
||||
.put(0x0f, (byte) heartRateZonesConfig.getHRRLactate())
|
||||
.put(0x10, (byte) heartRateZonesConfig.getHRRBasicAnaerobic())
|
||||
.put(0x11, (byte) heartRateZonesConfig.getHRRAdvancedAnaerobic());
|
||||
}
|
||||
|
||||
if (id == id_extended && heartRateZonesConfig.getRestHeartRate() > 0) {
|
||||
subTlv
|
||||
.put(0x0a, (byte) heartRateZonesConfig.getCalculateMethod())
|
||||
.put(0x0c, (byte) heartRateZonesConfig.getRestHeartRate());
|
||||
}
|
||||
|
||||
this.tlv = new HuaweiTLV().put(0x81, subTlv);
|
||||
|
||||
this.complete = true;
|
||||
}
|
||||
|
||||
public static Request requestSimple(ParamsProvider paramsProvider, HeartRateZonesConfig heartRateZonesConfig) {
|
||||
return new Request(paramsProvider, id_simple, heartRateZonesConfig);
|
||||
}
|
||||
|
||||
public static Request requestExtended(ParamsProvider paramsProvider, HeartRateZonesConfig heartRateZonesConfig) {
|
||||
return new Request(paramsProvider, id_extended, heartRateZonesConfig);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class TruSleep {
|
||||
public static final byte id = 0x16;
|
||||
|
||||
|
@ -18,12 +18,14 @@ package nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiMusicUtils.parseFormatBits;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiMusicUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceMusic;
|
||||
|
||||
public class MusicControl {
|
||||
public static final byte id = 0x25;
|
||||
@ -251,16 +253,16 @@ public class MusicControl {
|
||||
public static class Response extends HuaweiPacket {
|
||||
public HuaweiMusicUtils.MusicCapabilities params = new HuaweiMusicUtils.MusicCapabilities();
|
||||
|
||||
public int frameCount = 0;
|
||||
public List<HuaweiMusicUtils.PageStruct> pageStruct = null;
|
||||
|
||||
public Response(ParamsProvider paramsProvider) {
|
||||
super(paramsProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parseTlv() throws ParseException {
|
||||
|
||||
//TODO: unknown TLV
|
||||
// if (this.tlv.contains(0x01))
|
||||
// LOG.info("Unknown: " + this.tlv.getShort(0x01));
|
||||
this.frameCount = this.tlv.getAsInteger(0x01);
|
||||
|
||||
if (this.tlv.contains(0x02))
|
||||
params.availableSpace = this.tlv.getAsInteger(0x02);
|
||||
@ -274,25 +276,214 @@ public class MusicControl {
|
||||
params.currentMusicCount = this.tlv.getAsInteger(0x05);
|
||||
|
||||
if (this.tlv.contains(0x86)) {
|
||||
params.pageStruct = new ArrayList<>();
|
||||
this.pageStruct = new ArrayList<>();
|
||||
List<HuaweiTLV> subTlvs = this.tlv.getObject(0x86).getObjects(0x87);
|
||||
for (HuaweiTLV subTlv : subTlvs) {
|
||||
HuaweiMusicUtils.PageStruct pageStruct = new HuaweiMusicUtils.PageStruct();
|
||||
if (subTlv.contains(0x08))
|
||||
pageStruct.startIndex = subTlv.getShort(0x08);
|
||||
pageStruct.startFrame = subTlv.getShort(0x08);
|
||||
if (subTlv.contains(0x09))
|
||||
pageStruct.endIndex = subTlv.getShort(0x09);
|
||||
pageStruct.endFrame = subTlv.getShort(0x09);
|
||||
if (subTlv.contains(0x0a))
|
||||
pageStruct.count = subTlv.getShort(0x0a);
|
||||
if (subTlv.contains(0x0b))
|
||||
pageStruct.hashCode = subTlv.getBytes(0x0b);
|
||||
params.pageStruct.add(pageStruct);
|
||||
this.pageStruct.add(pageStruct);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class MusicList {
|
||||
public static final byte id = 0x05;
|
||||
|
||||
public static class Request extends HuaweiPacket {
|
||||
public Request(ParamsProvider paramsProvider, short startFrame, short endIndex) {
|
||||
super(paramsProvider);
|
||||
this.serviceId = MusicControl.id;
|
||||
this.commandId = id;
|
||||
this.tlv = new HuaweiTLV()
|
||||
.put(0x01, startFrame)
|
||||
.put(0x04, endIndex);
|
||||
}
|
||||
}
|
||||
|
||||
public static class Response extends HuaweiPacket {
|
||||
|
||||
public short startFrame = 0;
|
||||
public short endIndex = 0;
|
||||
|
||||
public List<GBDeviceMusic> musicList;
|
||||
public Response (ParamsProvider paramsProvider) {
|
||||
super(paramsProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parseTlv() throws HuaweiPacket.ParseException {
|
||||
if(tlv.contains(0x1))
|
||||
startFrame = tlv.getShort(0x1);
|
||||
if(tlv.contains(0x4))
|
||||
endIndex = tlv.getShort(0x4);
|
||||
musicList = new ArrayList<>();
|
||||
if(this.tlv.contains(0x82)) {
|
||||
for (HuaweiTLV subTlv : this.tlv.getObject(0x82).getObjects(0x83)) {
|
||||
int index = subTlv.getAsInteger(0x4);
|
||||
String title = subTlv.getString(0x5);
|
||||
String artist = subTlv.getString(0x6);
|
||||
String fileName = subTlv.getString(0x7);
|
||||
musicList.add(new GBDeviceMusic(index, title, artist, fileName));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class MusicPlaylists {
|
||||
public static final byte id = 0x06;
|
||||
|
||||
public static class Request extends HuaweiPacket {
|
||||
public Request(ParamsProvider paramsProvider) {
|
||||
super(paramsProvider);
|
||||
this.serviceId = MusicControl.id;
|
||||
this.commandId = id;
|
||||
this.tlv = new HuaweiTLV()
|
||||
.put(0x01);
|
||||
}
|
||||
}
|
||||
|
||||
public static class Response extends HuaweiPacket {
|
||||
|
||||
public static class PlaylistData {
|
||||
public int id;
|
||||
public String name;
|
||||
public int frameCount;
|
||||
}
|
||||
|
||||
public List<PlaylistData> playlists = new ArrayList<>();
|
||||
|
||||
public Response (ParamsProvider paramsProvider) {
|
||||
super(paramsProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parseTlv() throws HuaweiPacket.ParseException {
|
||||
if(this.tlv.contains(0x81)) {
|
||||
for (HuaweiTLV subTlv : this.tlv.getObject(0x81).getObjects(0x82)) {
|
||||
PlaylistData data = new PlaylistData();
|
||||
data.id = subTlv.getAsInteger(0x3);
|
||||
data.name = subTlv.getString(0x4);
|
||||
data.frameCount = subTlv.getAsInteger(0x5);
|
||||
playlists.add(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class MusicPlaylistMusics {
|
||||
public static final byte id = 0x07;
|
||||
|
||||
public static class Request extends HuaweiPacket {
|
||||
public Request(ParamsProvider paramsProvider, short playlist, short index) {
|
||||
super(paramsProvider);
|
||||
this.serviceId = MusicControl.id;
|
||||
this.commandId = id;
|
||||
this.tlv = new HuaweiTLV()
|
||||
.put(0x01, playlist)
|
||||
.put(0x02, index);
|
||||
}
|
||||
}
|
||||
|
||||
public static class Response extends HuaweiPacket {
|
||||
|
||||
public int id = -1;
|
||||
public int index = -1;
|
||||
public ArrayList<Integer> musicIds = null;
|
||||
|
||||
public Response (ParamsProvider paramsProvider) {
|
||||
super(paramsProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parseTlv() throws HuaweiPacket.ParseException {
|
||||
if(this.tlv.contains(0x1))
|
||||
id = tlv.getAsInteger(0x1);
|
||||
if(this.tlv.contains(0x2))
|
||||
index = tlv.getAsInteger(0x2);
|
||||
|
||||
if(this.tlv.contains(0x3)) {
|
||||
musicIds = new ArrayList<>();
|
||||
ByteBuffer dt = ByteBuffer.wrap(this.tlv.getBytes(0x3));
|
||||
while (dt.hasRemaining())
|
||||
musicIds.add((int) dt.getShort());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class MusicOperation {
|
||||
public static final byte id = 0x08;
|
||||
|
||||
public static class Request extends HuaweiPacket {
|
||||
public Request(ParamsProvider paramsProvider, int operation, int playlistIndex, String playlistName, ArrayList<Integer> musicIds) {
|
||||
super(paramsProvider);
|
||||
this.serviceId = MusicControl.id;
|
||||
this.commandId = id;
|
||||
this.tlv = new HuaweiTLV()
|
||||
.put(0x01, (byte)operation);
|
||||
|
||||
if(operation == 1 || operation == 2 || operation == 3 || operation == 4) {
|
||||
this.tlv.put(0x02, (short)playlistIndex);
|
||||
}
|
||||
if (operation == 0 || operation == 2) {
|
||||
this.tlv.put(0x03, playlistName);
|
||||
}
|
||||
|
||||
if (operation == 3 || operation == 4) {
|
||||
ByteBuffer ids = ByteBuffer.allocate(musicIds.size() * 2);
|
||||
for (Integer id : musicIds) {
|
||||
ids.putShort(id.shortValue());
|
||||
}
|
||||
this.tlv.put(0x04, ids.array());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class Response extends HuaweiPacket {
|
||||
|
||||
public int operation = -1;
|
||||
public int playlistIndex = -1;
|
||||
public String playlistName;
|
||||
public ArrayList<Integer> musicIds = null;
|
||||
public int resultCode = -1;
|
||||
|
||||
public Response (ParamsProvider paramsProvider) {
|
||||
super(paramsProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parseTlv() throws HuaweiPacket.ParseException {
|
||||
if(this.tlv.contains(0x7f))
|
||||
resultCode = tlv.getInteger(0x7f);
|
||||
if(this.tlv.contains(0x1))
|
||||
operation = tlv.getByte(0x1);
|
||||
if(this.tlv.contains(0x2))
|
||||
playlistIndex = tlv.getAsInteger(0x2);
|
||||
if(this.tlv.contains(0x3))
|
||||
playlistName = tlv.getString(0x3);
|
||||
|
||||
if(this.tlv.contains(0x4)) {
|
||||
musicIds = new ArrayList<>();
|
||||
ByteBuffer dt = ByteBuffer.wrap(this.tlv.getBytes(0x4));
|
||||
while (dt.hasRemaining())
|
||||
musicIds.add((int) dt.getShort());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static class ExtendedMusicInfoParams {
|
||||
public static final byte id = 0x0d;
|
||||
|
||||
|
@ -0,0 +1,75 @@
|
||||
/* Copyright (C) 2024 José Rebelo
|
||||
|
||||
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.oppo;
|
||||
|
||||
import android.util.Pair;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.TouchConfigSide;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.TouchConfigType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.TouchConfigValue;
|
||||
|
||||
public class OppoEncoAirCoordinator extends OppoHeadphonesCoordinator {
|
||||
@Override
|
||||
protected Pattern getSupportedDeviceName() {
|
||||
return Pattern.compile("OPPO Enco Air", Pattern.LITERAL);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDeviceNameResource() {
|
||||
return R.string.devicetype_oppo_enco_air;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsFindDevice() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<Pair<TouchConfigSide, TouchConfigType>, List<TouchConfigValue>> getTouchOptions() {
|
||||
return new LinkedHashMap<Pair<TouchConfigSide, TouchConfigType>, List<TouchConfigValue>>() {{
|
||||
put(Pair.create(TouchConfigSide.LEFT, TouchConfigType.TAP_2), Arrays.asList(
|
||||
TouchConfigValue.OFF,
|
||||
TouchConfigValue.PLAY_PAUSE,
|
||||
TouchConfigValue.PREVIOUS,
|
||||
TouchConfigValue.NEXT,
|
||||
TouchConfigValue.VOICE_ASSISTANT
|
||||
));
|
||||
put(Pair.create(TouchConfigSide.LEFT, TouchConfigType.TAP_3), Arrays.asList(
|
||||
TouchConfigValue.OFF,
|
||||
TouchConfigValue.VOICE_ASSISTANT,
|
||||
TouchConfigValue.GAME_MODE
|
||||
));
|
||||
put(Pair.create(TouchConfigSide.LEFT, TouchConfigType.HOLD), Arrays.asList(
|
||||
TouchConfigValue.OFF,
|
||||
TouchConfigValue.VOLUME_UP,
|
||||
TouchConfigValue.VOLUME_DOWN
|
||||
));
|
||||
|
||||
// Right side is the same
|
||||
put(Pair.create(TouchConfigSide.RIGHT, TouchConfigType.TAP_2), get(Pair.create(TouchConfigSide.LEFT, TouchConfigType.TAP_2)));
|
||||
put(Pair.create(TouchConfigSide.RIGHT, TouchConfigType.TAP_3), get(Pair.create(TouchConfigSide.LEFT, TouchConfigType.TAP_3)));
|
||||
put(Pair.create(TouchConfigSide.RIGHT, TouchConfigType.HOLD), get(Pair.create(TouchConfigSide.LEFT, TouchConfigType.HOLD)));
|
||||
}};
|
||||
}
|
||||
}
|
@ -0,0 +1,101 @@
|
||||
/* Copyright (C) 2024 José Rebelo
|
||||
|
||||
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.oppo;
|
||||
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBException;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
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.AbstractBLClassicDeviceCoordinator;
|
||||
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.oppo.OppoHeadphonesSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.TouchConfigSide;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.TouchConfigType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.TouchConfigValue;
|
||||
|
||||
public abstract class OppoHeadphonesCoordinator extends AbstractBLClassicDeviceCoordinator {
|
||||
@Override
|
||||
protected void deleteDevice(@NonNull final GBDevice gbDevice, @NonNull final Device device, @NonNull final DaoSession session) throws GBException {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getManufacturer() {
|
||||
return "Oppo";
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Class<? extends DeviceSupport> getDeviceSupportClass() {
|
||||
return OppoHeadphonesSupport.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDefaultIconResource() {
|
||||
return R.drawable.ic_device_nothingear;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDisabledIconResource() {
|
||||
return R.drawable.ic_device_nothingear_disabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBatteryCount() {
|
||||
return 3;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BatteryConfig[] getBatteryConfig(final GBDevice device) {
|
||||
final BatteryConfig battery1 = new BatteryConfig(0, R.drawable.ic_nothing_ear_l, R.string.left_earbud);
|
||||
final BatteryConfig battery2 = new BatteryConfig(1, R.drawable.ic_nothing_ear_r, R.string.right_earbud);
|
||||
final BatteryConfig battery3 = new BatteryConfig(2, R.drawable.ic_tws_case, R.string.battery_case);
|
||||
return new BatteryConfig[]{battery1, battery2, battery3};
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeviceSpecificSettings getDeviceSpecificSettings(final GBDevice device) {
|
||||
final DeviceSpecificSettings settings = new DeviceSpecificSettings();
|
||||
|
||||
settings.addRootScreen(DeviceSpecificSettingsScreen.TOUCH_OPTIONS);
|
||||
settings.addSubScreen(DeviceSpecificSettingsScreen.TOUCH_OPTIONS, R.xml.devicesettings_oppo_headphones_touch_options);
|
||||
|
||||
settings.addRootScreen(DeviceSpecificSettingsScreen.CALLS_AND_NOTIFICATIONS);
|
||||
settings.addSubScreen(DeviceSpecificSettingsScreen.CALLS_AND_NOTIFICATIONS, R.xml.devicesettings_headphones);
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeviceSpecificSettingsCustomizer getDeviceSpecificSettingsCustomizer(final GBDevice device) {
|
||||
return new OppoHeadphonesSettingsCustomizer(getTouchOptions());
|
||||
}
|
||||
|
||||
protected abstract Map<Pair<TouchConfigSide, TouchConfigType>, List<TouchConfigValue>> getTouchOptions();
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
/* Copyright (C) 2024 José Rebelo
|
||||
|
||||
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.oppo;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.TouchConfigSide;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.TouchConfigType;
|
||||
|
||||
public class OppoHeadphonesPreferences {
|
||||
public static String getKey(final TouchConfigSide side, final TouchConfigType type) {
|
||||
return String.format(
|
||||
Locale.ROOT,
|
||||
"oppo_touch__%s__%s",
|
||||
side.name().toLowerCase(Locale.ROOT),
|
||||
type.name().toLowerCase(Locale.ROOT)
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,152 @@
|
||||
/* Copyright (C) 2024 José Rebelo
|
||||
|
||||
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.oppo;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.preference.ListPreference;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.TouchConfigSide;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.TouchConfigType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.TouchConfigValue;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
|
||||
public class OppoHeadphonesSettingsCustomizer implements DeviceSpecificSettingsCustomizer {
|
||||
private final Map<Pair<TouchConfigSide, TouchConfigType>, List<TouchConfigValue>> touchOptions;
|
||||
|
||||
public static final Creator<OppoHeadphonesSettingsCustomizer> CREATOR = new Creator<OppoHeadphonesSettingsCustomizer>() {
|
||||
@Override
|
||||
public OppoHeadphonesSettingsCustomizer createFromParcel(final Parcel in) {
|
||||
final Map<Pair<TouchConfigSide, TouchConfigType>, List<TouchConfigValue>> touchOptions = new LinkedHashMap<>();
|
||||
final int numOptions = in.readInt();
|
||||
for (int i = 0; i < numOptions; i++) {
|
||||
final TouchConfigSide touchConfigSide = TouchConfigSide.valueOf(in.readString());
|
||||
final TouchConfigType touchConfigType = TouchConfigType.valueOf(in.readString());
|
||||
final List<TouchConfigValue> values = new ArrayList<>();
|
||||
in.readList(values, TouchConfigValue.class.getClassLoader());
|
||||
touchOptions.put(Pair.create(touchConfigSide, touchConfigType), values);
|
||||
}
|
||||
return new OppoHeadphonesSettingsCustomizer(touchOptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OppoHeadphonesSettingsCustomizer[] newArray(final int size) {
|
||||
return new OppoHeadphonesSettingsCustomizer[size];
|
||||
}
|
||||
};
|
||||
|
||||
public OppoHeadphonesSettingsCustomizer(final Map<Pair<TouchConfigSide, TouchConfigType>, List<TouchConfigValue>> touchOptions) {
|
||||
this.touchOptions = touchOptions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPreferenceChange(final Preference preference, final DeviceSpecificSettingsHandler handler) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void customizeSettings(final DeviceSpecificSettingsHandler handler, final Prefs prefs, final String rootKey) {
|
||||
final Set<TouchConfigSide> knownSides = new HashSet<>();
|
||||
final Set<TouchConfigType> knownTypes = new HashSet<>();
|
||||
|
||||
for (final Map.Entry<Pair<TouchConfigSide, TouchConfigType>, List<TouchConfigValue>> e : touchOptions.entrySet()) {
|
||||
final TouchConfigSide side = e.getKey().first;
|
||||
final TouchConfigType type = e.getKey().second;
|
||||
final Set<TouchConfigValue> possibleValues = new HashSet<>(e.getValue());
|
||||
|
||||
knownSides.add(side);
|
||||
knownTypes.add(type);
|
||||
|
||||
final String key = OppoHeadphonesPreferences.getKey(side, type);
|
||||
final ListPreference pref = handler.findPreference(key);
|
||||
if (pref == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final CharSequence[] originalEntries = pref.getEntries();
|
||||
final CharSequence[] originalValues = pref.getEntryValues();
|
||||
final CharSequence[] entries = new CharSequence[possibleValues.size()];
|
||||
final CharSequence[] values = new CharSequence[possibleValues.size()];
|
||||
int j = 0;
|
||||
for (int i = 0; i < originalValues.length; i++) {
|
||||
if (possibleValues.contains(TouchConfigValue.valueOf(originalValues[i].toString().toUpperCase(Locale.ROOT)))) {
|
||||
entries[j] = originalEntries[i];
|
||||
values[j] = originalValues[i];
|
||||
j++;
|
||||
}
|
||||
}
|
||||
|
||||
pref.setEntries(entries);
|
||||
pref.setEntryValues(values);
|
||||
|
||||
handler.addPreferenceHandlerFor(key);
|
||||
}
|
||||
|
||||
for (final TouchConfigSide side : TouchConfigSide.values()) {
|
||||
if (!knownSides.contains(side)) {
|
||||
// Side not configurable, hide it completely
|
||||
final Preference header = handler.findPreference("oppo_touch_header_" + side.name().toLowerCase(Locale.ROOT));
|
||||
if (header != null) {
|
||||
header.setVisible(false);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
for (final TouchConfigType type : TouchConfigType.values()) {
|
||||
if (!knownTypes.contains(type)) {
|
||||
final String key = OppoHeadphonesPreferences.getKey(side, type);
|
||||
final Preference pref = handler.findPreference(key);
|
||||
if (pref != null) {
|
||||
pref.setVisible(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getPreferenceKeysWithSummary() {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel dest, final int flags) {
|
||||
dest.writeInt(touchOptions.size());
|
||||
for (final Map.Entry<Pair<TouchConfigSide, TouchConfigType>, List<TouchConfigValue>> e : touchOptions.entrySet()) {
|
||||
dest.writeString(e.getKey().first.name());
|
||||
dest.writeString(e.getKey().second.name());
|
||||
dest.writeList(e.getValue());
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
/* Copyright (C) 2024 José Rebelo
|
||||
|
||||
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.realme;
|
||||
|
||||
import android.util.Pair;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.oppo.OppoHeadphonesCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.TouchConfigSide;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.TouchConfigType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.TouchConfigValue;
|
||||
|
||||
public class RealmeBudsT110Coordinator extends OppoHeadphonesCoordinator {
|
||||
@Override
|
||||
protected Pattern getSupportedDeviceName() {
|
||||
return Pattern.compile("realme Buds T110", Pattern.LITERAL);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getManufacturer() {
|
||||
return "Realme";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDeviceNameResource() {
|
||||
return R.string.devicetype_realme_buds_t110;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<Pair<TouchConfigSide, TouchConfigType>, List<TouchConfigValue>> getTouchOptions() {
|
||||
return new LinkedHashMap<Pair<TouchConfigSide, TouchConfigType>, List<TouchConfigValue>>() {{
|
||||
final List<TouchConfigValue> options = Arrays.asList(
|
||||
TouchConfigValue.OFF,
|
||||
TouchConfigValue.PLAY_PAUSE,
|
||||
TouchConfigValue.PREVIOUS,
|
||||
TouchConfigValue.NEXT,
|
||||
TouchConfigValue.VOLUME_UP,
|
||||
TouchConfigValue.VOLUME_DOWN,
|
||||
TouchConfigValue.VOICE_ASSISTANT_REALME
|
||||
);
|
||||
put(Pair.create(TouchConfigSide.LEFT, TouchConfigType.TAP_2), options);
|
||||
put(Pair.create(TouchConfigSide.LEFT, TouchConfigType.TAP_3), options);
|
||||
put(Pair.create(TouchConfigSide.LEFT, TouchConfigType.HOLD), options);
|
||||
put(Pair.create(TouchConfigSide.RIGHT, TouchConfigType.TAP_2), options);
|
||||
put(Pair.create(TouchConfigSide.RIGHT, TouchConfigType.TAP_3), options);
|
||||
put(Pair.create(TouchConfigSide.RIGHT, TouchConfigType.HOLD), options);
|
||||
put(Pair.create(TouchConfigSide.BOTH, TouchConfigType.HOLD), Arrays.asList(
|
||||
TouchConfigValue.OFF,
|
||||
TouchConfigValue.GAME_MODE
|
||||
));
|
||||
}};
|
||||
}
|
||||
}
|
@ -38,7 +38,9 @@ import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
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.capabilities.HeartRateCapability;
|
||||
import nodomain.freeyourgadget.gadgetbridge.capabilities.password.PasswordCapabilityImpl;
|
||||
import nodomain.freeyourgadget.gadgetbridge.capabilities.widgets.WidgetManager;
|
||||
@ -502,26 +504,26 @@ public class TestDeviceCoordinator extends AbstractDeviceCoordinator {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getSupportedDeviceSpecificSettings(final GBDevice device) {
|
||||
final List<Integer> settings = new ArrayList<>();
|
||||
public DeviceSpecificSettings getDeviceSpecificSettings(final GBDevice device) {
|
||||
final DeviceSpecificSettings deviceSpecificSettings = new DeviceSpecificSettings();
|
||||
|
||||
settings.add(R.xml.devicesettings_header_apps);
|
||||
settings.add(R.xml.devicesettings_loyalty_cards);
|
||||
deviceSpecificSettings.addRootScreen(R.xml.devicesettings_loyalty_cards);
|
||||
|
||||
if (getWorldClocksSlotCount() > 0) {
|
||||
settings.add(R.xml.devicesettings_header_time);
|
||||
settings.add(R.xml.devicesettings_world_clocks);
|
||||
final List<Integer> dateTime = deviceSpecificSettings.addRootScreen(DeviceSpecificSettingsScreen.DATE_TIME);
|
||||
dateTime.add(R.xml.devicesettings_world_clocks);
|
||||
}
|
||||
|
||||
if (getContactsSlotCount(device) > 0) {
|
||||
settings.add(R.xml.devicesettings_header_other);
|
||||
settings.add(R.xml.devicesettings_contacts);
|
||||
deviceSpecificSettings.addRootScreen(R.xml.devicesettings_contacts);
|
||||
}
|
||||
|
||||
settings.add(R.xml.devicesettings_header_developer);
|
||||
settings.add(R.xml.devicesettings_test_features);
|
||||
deviceSpecificSettings.addRootScreen(R.xml.devicesettings_test_features);
|
||||
|
||||
return ArrayUtils.toPrimitive(settings.toArray(new Integer[0]));
|
||||
final List<Integer> developer = deviceSpecificSettings.addRootScreen(DeviceSpecificSettingsScreen.DEVELOPER);
|
||||
developer.add(R.xml.devicesettings_developer_add_test_activities);
|
||||
|
||||
return deviceSpecificSettings;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -17,15 +17,30 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.test;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.preference.MultiSelectListPreference;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.User;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
|
||||
public class TestDeviceSpecificSettingsCustomizer implements DeviceSpecificSettingsCustomizer {
|
||||
@ -39,20 +54,55 @@ public class TestDeviceSpecificSettingsCustomizer implements DeviceSpecificSetti
|
||||
@Override
|
||||
public void customizeSettings(final DeviceSpecificSettingsHandler handler, final Prefs prefs, final String rootKey) {
|
||||
final Preference pref = handler.findPreference(TestDeviceConst.PREF_TEST_FEATURES);
|
||||
if (pref == null) {
|
||||
return;
|
||||
if (pref != null) {
|
||||
// Populate the preference directly from the enum
|
||||
final CharSequence[] entries = new CharSequence[TestFeature.values().length];
|
||||
final CharSequence[] values = new CharSequence[TestFeature.values().length];
|
||||
for (int i = 0; i < TestFeature.values().length; i++) {
|
||||
entries[i] = TestFeature.values()[i].name();
|
||||
values[i] = TestFeature.values()[i].name();
|
||||
}
|
||||
if (pref instanceof MultiSelectListPreference) {
|
||||
((MultiSelectListPreference) pref).setEntries(entries);
|
||||
((MultiSelectListPreference) pref).setEntryValues(values);
|
||||
}
|
||||
}
|
||||
|
||||
// Populate the preference directly from the enum
|
||||
final CharSequence[] entries = new CharSequence[TestFeature.values().length];
|
||||
final CharSequence[] values = new CharSequence[TestFeature.values().length];
|
||||
for (int i = 0; i < TestFeature.values().length; i++) {
|
||||
entries[i] = TestFeature.values()[i].name();
|
||||
values[i] = TestFeature.values()[i].name();
|
||||
}
|
||||
if (pref instanceof MultiSelectListPreference) {
|
||||
((MultiSelectListPreference) pref).setEntries(entries);
|
||||
((MultiSelectListPreference) pref).setEntryValues(values);
|
||||
final Preference addTestActivities = handler.findPreference("pref_developer_add_test_activities");
|
||||
if (addTestActivities != null) {
|
||||
addTestActivities.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
|
||||
@Override
|
||||
public boolean onPreferenceClick(@NonNull final Preference preference) {
|
||||
try (DBHandler dbHandler = GBApplication.acquireDB()) {
|
||||
final DaoSession session = dbHandler.getDaoSession();
|
||||
final Device device = DBHelper.getDevice(handler.getDevice(), session);
|
||||
final User user = DBHelper.getUser(session);
|
||||
|
||||
//final QueryBuilder<?> qb = session.getBaseActivitySummaryDao().queryBuilder();
|
||||
//qb.where(BaseActivitySummaryDao.Properties.DeviceId.eq(device.getId())).buildDelete().executeDeleteWithoutDetachingEntities();
|
||||
|
||||
final List<BaseActivitySummary> summaries = new ArrayList<>();
|
||||
|
||||
for (final ActivityKind activityKind : ActivityKind.values()) {
|
||||
final BaseActivitySummary summary = new BaseActivitySummary();
|
||||
summary.setStartTime(new Date(System.currentTimeMillis() - new Random().nextInt(31 * 24 * 60 * 60) * 1000L));
|
||||
summary.setEndTime(new Date(summary.getStartTime().getTime() + new Random().nextInt(60 * 60 * 2) * 1000L));
|
||||
summary.setDevice(device);
|
||||
summary.setUser(user);
|
||||
summary.setActivityKind(activityKind.getCode());
|
||||
// TODO data
|
||||
summaries.add(summary);
|
||||
}
|
||||
|
||||
session.getBaseActivitySummaryDao().insertOrReplaceInTx(summaries);
|
||||
} catch (final Exception e) {
|
||||
GB.toast(handler.getContext(), "Error saving activity summary", Toast.LENGTH_LONG, GB.ERROR, e);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -455,10 +455,14 @@ public class NotificationListener extends NotificationListenerService {
|
||||
mPackageLookup.add(notificationSpec.getId(), sbn.getPackageName()); // for MUTE
|
||||
|
||||
notificationBurstPrevention.put(source, curTime);
|
||||
if (0 != notification.when) {
|
||||
notificationOldRepeatPrevention.put(source, notification.when);
|
||||
if (notification.when == 0) {
|
||||
LOG.info("This app might show old/duplicate notifications. notification.when is 0 for {}", source);
|
||||
} else if ((notification.when - System.currentTimeMillis()) > 30_000L) {
|
||||
// #4327 - Some apps such as outlook send reminder notifications in the future
|
||||
// If we add them to the oldRepeatPrevention, they never show up again
|
||||
LOG.info("This app might show old/duplicate notifications. notification.when is in the future for {}", source);
|
||||
} else {
|
||||
LOG.info("This app might show old/duplicate notifications. notification.when is 0 for " + source);
|
||||
notificationOldRepeatPrevention.put(source, notification.when);
|
||||
}
|
||||
notificationsActive.add(notificationSpec.getId());
|
||||
// NOTE for future developers: this call goes to implementations of DeviceService.onNotification(NotificationSpec), like in GBDeviceService
|
||||
@ -813,10 +817,11 @@ public class NotificationListener extends NotificationListenerService {
|
||||
|
||||
private void logNotification(StatusBarNotification sbn, boolean posted) {
|
||||
LOG.debug(
|
||||
"Notification {} {}: packageName={}, priority={}, category={}",
|
||||
"Notification {} {}: packageName={}, when={}, priority={}, category={}",
|
||||
sbn.getId(),
|
||||
posted ? "posted" : "removed",
|
||||
sbn.getPackageName(),
|
||||
sbn.getNotification().when,
|
||||
sbn.getNotification().priority,
|
||||
sbn.getNotification().category
|
||||
);
|
||||
|
@ -400,15 +400,12 @@ public class GBDevice implements Parcelable {
|
||||
}
|
||||
|
||||
private void unsetDynamicState() {
|
||||
|
||||
setBatteryLevel(BATTERY_UNKNOWN, 0);
|
||||
setBatteryLevel(BATTERY_UNKNOWN, 1);
|
||||
setBatteryLevel(BATTERY_UNKNOWN, 2);
|
||||
setBatteryState(UNKNOWN, 0);
|
||||
setBatteryState(UNKNOWN, 1);
|
||||
setBatteryState(UNKNOWN, 2);
|
||||
setFirmwareVersion(null);
|
||||
setFirmwareVersion2(null);
|
||||
setRssi(RSSI_UNKNOWN);
|
||||
resetExtraInfos();
|
||||
if (mBusyTask != null) {
|
||||
|
@ -0,0 +1,33 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.impl;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
public class GBDeviceMusic implements Serializable {
|
||||
private final int id;
|
||||
private final String title;
|
||||
private final String artist;
|
||||
private final String fileName;
|
||||
|
||||
public GBDeviceMusic(int id, String title, String artist, String fileName) {
|
||||
this.id = id;
|
||||
this.title = title;
|
||||
this.artist = artist;
|
||||
this.fileName = fileName;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public String getArtist() {
|
||||
return artist;
|
||||
}
|
||||
|
||||
public String getFileName() {
|
||||
return fileName;
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.impl;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class GBDeviceMusicPlaylist implements Serializable {
|
||||
private final int id;
|
||||
private String name;
|
||||
private ArrayList<Integer> musicIds;
|
||||
|
||||
public GBDeviceMusicPlaylist(int id, String name, ArrayList<Integer> musicIds) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.musicIds = musicIds;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public ArrayList<Integer> getMusicIds() {
|
||||
return musicIds;
|
||||
}
|
||||
|
||||
public void setMusicIds(ArrayList<Integer> musicIds) {
|
||||
this.musicIds = musicIds;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name;
|
||||
}
|
||||
}
|
@ -568,4 +568,20 @@ public class GBDeviceService implements DeviceService {
|
||||
intent.putExtra(EXTRA_CAMERA_FILENAME, filename);
|
||||
invokeService(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMusicListReq() {
|
||||
Intent intent = createIntent().setAction(ACTION_REQUEST_MUSIC_LIST);
|
||||
invokeService(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMusicOperation(int operation, int playlistIndex, String playlistName, ArrayList<Integer> musicIds) {
|
||||
Intent intent = createIntent().setAction(ACTION_REQUEST_MUSIC_OPERATION);
|
||||
intent.putExtra("operation", operation);
|
||||
intent.putExtra("playlistIndex", playlistIndex);
|
||||
intent.putExtra("playlistName", playlistName);
|
||||
intent.putExtra("musicIds", musicIds);
|
||||
invokeService(intent);
|
||||
}
|
||||
}
|
||||
|
@ -67,8 +67,8 @@ public enum ActivityKind {
|
||||
HANDCYCLING_INDOOR(0x04000005, R.string.activity_type_handcycling_indoor),
|
||||
TRANSITION(0x04000006, R.string.activity_type_transition),
|
||||
FITNESS_EQUIPMENT(0x04000007, R.string.activity_type_fitness_equipment),
|
||||
STAIR_STEPPER(0x04000008, R.string.activity_type_stair_stepper),
|
||||
PILATES(0x04000009, R.string.activity_type_pilates),
|
||||
STAIR_STEPPER(0x04000008, R.string.activity_type_stair_stepper, R.drawable.ic_activity_stair_stepper),
|
||||
PILATES(0x04000009, R.string.activity_type_pilates, R.drawable.ic_activity_pilates),
|
||||
POOL_SWIM(0x0400000a, R.string.activity_type_pool_swimming, R.drawable.ic_activity_swimming),
|
||||
TENNIS(0x0400000b, R.string.activity_type_tennis),
|
||||
PLATFORM_TENNIS(0x0400000c, R.string.activity_type_platform_tennis),
|
||||
@ -94,23 +94,23 @@ public enum ActivityKind {
|
||||
HUNTING(0x04000023, R.string.activity_type_hunting),
|
||||
FISHING(0x04000024, R.string.activity_type_fishing),
|
||||
INLINE_SKATING(0x04000025, R.string.activity_type_inline_skating),
|
||||
ROCK_CLIMBING(0x04000026, R.string.activity_type_rock_climbing),
|
||||
ROCK_CLIMBING(0x04000026, R.string.activity_type_rock_climbing, R.drawable.ic_activity_rock_climbing),
|
||||
CLIMB_INDOOR(0x04000027, R.string.activity_type_climb_indoor),
|
||||
BOULDERING(0x04000028, R.string.activity_type_bouldering),
|
||||
SAIL_RACE(0x0400002a, R.string.activity_type_sail_race, R.drawable.ic_activity_sailing),
|
||||
SAIL_EXPEDITION(0x0400002b, R.string.activity_type_sail_expedition, R.drawable.ic_activity_sailing),
|
||||
ICE_SKATING(0x0400002c, R.string.activity_type_ice_skating),
|
||||
ICE_SKATING(0x0400002c, R.string.activity_type_ice_skating, R.drawable.ic_activity_ice_skating),
|
||||
SKY_DIVING(0x0400002d, R.string.activity_type_sky_diving),
|
||||
SNOWSHOE(0x0400002e, R.string.activity_type_snowshoe),
|
||||
SNOWMOBILING(0x0400002f, R.string.activity_type_snowmobiling),
|
||||
STAND_UP_PADDLEBOARDING(0x04000030, R.string.activity_type_stand_up_paddleboarding),
|
||||
SURFING(0x04000031, R.string.activity_type_surfing),
|
||||
WAKEBOARDING(0x04000032, R.string.activity_type_wakeboarding),
|
||||
WATER_SKIING(0x04000033, R.string.activity_type_water_skiing),
|
||||
STAND_UP_PADDLEBOARDING(0x04000030, R.string.activity_type_stand_up_paddleboarding, R.drawable.ic_activity_sup),
|
||||
SURFING(0x04000031, R.string.activity_type_surfing, R.drawable.ic_activity_surfing),
|
||||
WAKEBOARDING(0x04000032, R.string.activity_type_wakeboarding, R.drawable.ic_activity_wakeboarding),
|
||||
WATER_SKIING(0x04000033, R.string.activity_type_water_skiing, R.drawable.ic_activity_waterskiing),
|
||||
KAYAKING(0x04000034, R.string.activity_type_kayaking, R.drawable.ic_activity_rowing),
|
||||
RAFTING(0x04000035, R.string.activity_type_rafting, R.drawable.ic_activity_rowing),
|
||||
WINDSURFING(0x04000036, R.string.activity_type_windsurfing),
|
||||
KITESURFING(0x04000037, R.string.activity_type_kitesurfing),
|
||||
WINDSURFING(0x04000036, R.string.activity_type_windsurfing, R.drawable.ic_activity_windsurfing),
|
||||
KITESURFING(0x04000037, R.string.activity_type_kitesurfing, R.drawable.ic_activity_kitesurfing),
|
||||
TACTICAL(0x04000038, R.string.activity_type_tactical),
|
||||
JUMPMASTER(0x04000039, R.string.activity_type_jumpmaster),
|
||||
BOXING(0x0400003a, R.string.activity_type_boxing),
|
||||
@ -144,7 +144,7 @@ public enum ActivityKind {
|
||||
HOCKEY(0x04000056, R.string.activity_type_hockey),
|
||||
LACROSSE(0x04000057, R.string.activity_type_lacrosse),
|
||||
VOLLEYBALL(0x04000058, R.string.activity_type_volleyball),
|
||||
WATER_TUBING(0x04000059, R.string.activity_type_water_tubing),
|
||||
WATER_TUBING(0x04000059, R.string.activity_type_water_tubing, R.drawable.ic_activity_watertubing),
|
||||
WAKESURFING(0x0400005a, R.string.activity_type_wakesurfing),
|
||||
MIXED_MARTIAL_ARTS(0x0400005b, R.string.activity_type_mixed_martial_arts), // aka MMA
|
||||
DANCE(0x0400005c, R.string.activity_type_dance),
|
||||
@ -194,20 +194,20 @@ public enum ActivityKind {
|
||||
ROLLER_SKATING(0x04000087, R.string.activity_type_roller_skating),
|
||||
MARTIAL_ARTS(0x04000088, R.string.activity_type_martial_arts),
|
||||
TAI_CHI(0x04000089, R.string.activity_type_tai_chi),
|
||||
HULA_HOOPING(0x0400008a, R.string.activity_type_hula_hooping),
|
||||
HULA_HOOPING(0x0400008a, R.string.activity_type_hula_hooping, R.drawable.ic_activity_hula_hoop),
|
||||
DISC_SPORTS(0x0400008b, R.string.activity_type_disc_sports),
|
||||
DARTS(0x0400008c, R.string.activity_type_darts),
|
||||
ARCHERY(0x0400008d, R.string.activity_type_archery),
|
||||
ARCHERY(0x0400008d, R.string.activity_type_archery, R.drawable.ic_activity_archery),
|
||||
HORSE_RIDING(0x0400008e, R.string.activity_type_horse_riding),
|
||||
KITE_FLYING(0x0400008f, R.string.activity_type_kite_flying),
|
||||
SWING(0x04000090, R.string.activity_type_swing),
|
||||
STAIRS(0x04000091, R.string.activity_type_stairs),
|
||||
STAIRS(0x04000091, R.string.activity_type_stairs, R.drawable.ic_activity_stairs),
|
||||
MIND_AND_BODY(0x04000092, R.string.activity_type_mind_and_body),
|
||||
WRESTLING(0x04000093, R.string.activity_type_wrestling),
|
||||
KABADDI(0x04000094, R.string.activity_type_kabaddi),
|
||||
KARTING(0x04000095, R.string.activity_type_karting),
|
||||
BILLIARDS(0x04000096, R.string.activity_type_billiards),
|
||||
BOWLING(0x04000097, R.string.activity_type_bowling),
|
||||
BOWLING(0x04000097, R.string.activity_type_bowling, R.drawable.ic_activity_bowling),
|
||||
SHUTTLECOCK(0x04000098, R.string.activity_type_shuttlecock),
|
||||
HANDBALL(0x04000099, R.string.activity_type_handball),
|
||||
DODGEBALL(0x0400009a, R.string.activity_type_dodgeball),
|
||||
@ -222,14 +222,14 @@ public enum ActivityKind {
|
||||
JET_SKIING(0x040000a3, R.string.activity_type_jet_skiing),
|
||||
SKATING(0x040000a4, R.string.activity_type_skating),
|
||||
ICE_HOCKEY(0x040000a5, R.string.activity_type_ice_hockey),
|
||||
CURLING(0x040000a6, R.string.activity_type_curling),
|
||||
CURLING(0x040000a6, R.string.activity_type_curling, R.drawable.ic_activity_curling),
|
||||
CROSS_COUNTRY_SKIING(0x040000a8, R.string.activity_type_cross_country_skiing),
|
||||
SNOW_SPORTS(0x040000a9, R.string.activity_type_snow_sports),
|
||||
LUGE(0x040000ab, R.string.activity_type_luge),
|
||||
SKATEBOARDING(0x040000ac, R.string.activity_type_skateboarding),
|
||||
PARACHUTING(0x040000ae, R.string.activity_type_parachuting),
|
||||
PARKOUR(0x040000af, R.string.activity_type_parkour),
|
||||
INDOOR_RUNNING(0x040000b0, R.string.activity_type_indoor_running),
|
||||
INDOOR_RUNNING(0x040000b0, R.string.activity_type_indoor_running, R.drawable.ic_activity_indoor_running),
|
||||
OUTDOOR_RUNNING(0x040000b1, R.string.activity_type_outdoor_running, R.drawable.ic_activity_running),
|
||||
OUTDOOR_WALKING(0x040000b2, R.string.activity_type_outdoor_walking, R.drawable.ic_activity_hiking),
|
||||
OUTDOOR_CYCLING(0x040000b3, R.string.activity_type_outdoor_cycling, R.drawable.ic_activity_biking),
|
||||
@ -251,13 +251,13 @@ public enum ActivityKind {
|
||||
FINSWIMMING(0x040000c3, R.string.activity_type_finswimming),
|
||||
FLOWRIDING(0x040000c4, R.string.activity_type_flowriding),
|
||||
FOLK_DANCE(0x040000c5, R.string.activity_type_folk_dance),
|
||||
FRISBEE(0x040000c6, R.string.activity_type_frisbee),
|
||||
FRISBEE(0x040000c6, R.string.activity_type_frisbee, R.drawable.ic_activity_frisbee),
|
||||
FUTSAL(0x040000c7, R.string.activity_type_futsal),
|
||||
HACKY_SACK(0x040000c8, R.string.activity_type_hacky_sack),
|
||||
HIP_HOP(0x040000c9, R.string.activity_type_hip_hop),
|
||||
HULA_HOOP(0x040000ca, R.string.activity_type_hula_hoop),
|
||||
HULA_HOOP(0x040000ca, R.string.activity_type_hula_hoop, R.drawable.ic_activity_hula_hoop),
|
||||
INDOOR_FITNESS(0x040000cb, R.string.activity_type_indoor_fitness),
|
||||
INDOOR_ICE_SKATING(0x040000cc, R.string.activity_type_indoor_ice_skating),
|
||||
INDOOR_ICE_SKATING(0x040000cc, R.string.activity_type_indoor_ice_skating, R.drawable.ic_activity_ice_skating),
|
||||
JAI_ALAI(0x040000cd, R.string.activity_type_jai_alai),
|
||||
JUDO(0x040000ce, R.string.activity_type_judo),
|
||||
JUJITSU(0x040000cf, R.string.activity_type_jujitsu),
|
||||
@ -284,8 +284,8 @@ public enum ActivityKind {
|
||||
BODY_COMBAT(0x040000e5, R.string.activity_type_body_combat),
|
||||
PLAZA_DANCING(0x040000e6, R.string.activity_type_plaza_dancing),
|
||||
LASER_TAG(0x040000e7, R.string.activity_type_laser_tag),
|
||||
OBSTACLE_RACE(0x040000e8, R.string.activity_type_obstacle_race),
|
||||
BILLIARD_POOL(0x040000e9, R.string.activity_type_billiard_pool),
|
||||
OBSTACLE_RACE(0x040000e8, R.string.activity_type_obstacle_race, R.drawable.ic_activity_obstacle_race),
|
||||
BILLIARD_POOL(0x040000e9, R.string.activity_type_billiard_pool, R.drawable.ic_activity_billiard_pool),
|
||||
CANOEING(0x040000ea, R.string.activity_type_canoeing),
|
||||
WATER_SCOOTER(0x040000eb, R.string.activity_type_water_scooter),
|
||||
BOBSLEIGH(0x040000ec, R.string.activity_type_bobsleigh),
|
||||
|
@ -57,7 +57,7 @@ public class ActivitySummaryJsonSummary {
|
||||
summary.add("baseAltitude", item.getBaseAltitude(), UNIT_METERS);
|
||||
}
|
||||
|
||||
if (!summary.has("averageKMPaceSeconds") && !summary.has("averageSpeed") && summary.has("distanceMeters") && summary.has("activeSeconds")) {
|
||||
if (!summary.has("averageSpeed") && summary.has("distanceMeters") && summary.has("activeSeconds")) {
|
||||
double distance = summary.getNumber("distanceMeters", 0).doubleValue();
|
||||
double duration = summary.getNumber("activeSeconds", 1).doubleValue();
|
||||
summary.add("averageSpeed", distance / duration, UNIT_METERS_PER_SECOND);
|
||||
|
@ -80,6 +80,8 @@ public interface DeviceService extends EventHandler {
|
||||
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_REQUEST_MUSIC_LIST = PREFIX + ".action.request_music_list";
|
||||
String ACTION_REQUEST_MUSIC_OPERATION = PREFIX + ".action.request_music_operation";
|
||||
|
||||
String ACTION_SLEEP_AS_ANDROID = ".action.sleep_as_android";
|
||||
String EXTRA_SLEEP_AS_ANDROID_ACTION = "sleepasandroid_action";
|
||||
|
@ -2,7 +2,7 @@
|
||||
Andreas Böhler, Andreas Shimokawa, Andrew Watkins, angelpup, Carsten Pfeiffer,
|
||||
Cre3per, Damien Gaignon, DanialHanif, Daniel Dakhno, Daniele Gobbetti, Daniel
|
||||
Thompson, Da Pa, Dmytro Bielik, Frank Ertl, Gabriele Monaco, GeekosaurusR3x,
|
||||
Gordon Williams, Jean-François Greffier, jfgreffier, jhey, João Paulo
|
||||
Guido Jäkel, Gordon Williams, Jean-François Greffier, jfgreffier, jhey, João Paulo
|
||||
Barraca, Jochen S, Johannes Krude, José Rebelo, ksiwczynski, ladbsoft,
|
||||
Lesur Frederic, Maciej Kuśnierz, mamucho, Manuel Ruß, Maxime Reyrolle,
|
||||
maxirnilian, Michael, narektor, Noodlez, odavo32nof, opavlov, pangwalla,
|
||||
@ -62,6 +62,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.fenix.GarminF
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.fenix.GarminFenix5PlusCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.fenix.GarminFenix5XPlusCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.fenix.GarminFenix6Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.fenix.GarminFenix6SProCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.fenix.GarminFenix6SSapphireCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.fenix.GarminFenix6SapphireCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.fenix.GarminFenix7Coordinator;
|
||||
@ -78,8 +79,11 @@ import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.forerunner.Ga
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.forerunner.GarminForerunner255SMusicCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.forerunner.GarminForerunner265Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.forerunner.GarminForerunner265SCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.forerunner.GarminForerunner55Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.forerunner.GarminForerunner620Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.forerunner.GarminForerunner955Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.forerunner.GarminForerunner965Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.instinct.GarminInstinct2Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.instinct.GarminInstinct2SCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.instinct.GarminInstinct2SSolarCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.instinct.GarminInstinct2SolTacCoordinator;
|
||||
@ -174,6 +178,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huawei.honorband7.HonorBand7
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.honormagicwatch2.HonorMagicWatch2Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.honorwatchgs3.HonorWatchGS3Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.honorwatchgspro.HonorWatchGSProCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.huaweiband3pro.HuaweiBand3ProCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.huaweiband4pro.HuaweiBand4ProCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.huaweiband6.HuaweiBand6Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.huaweiband7.HuaweiBand7Coordinator;
|
||||
@ -222,10 +227,12 @@ import nodomain.freeyourgadget.gadgetbridge.devices.nothing.Ear1Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.nothing.Ear2Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.nothing.EarStickCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.nut.NutCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.oppo.OppoEncoAirCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.pinetime.PineTimeJFCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.qc35.QC35Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.QHybridCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.realme.RealmeBudsT110Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.roidmi.Roidmi1Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.roidmi.Roidmi3Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.scannable.ScannableDeviceCoordinator;
|
||||
@ -421,11 +428,13 @@ public enum DeviceType {
|
||||
GARMIN_FENIX_5X_PLUS(GarminFenix5XPlusCoordinator.class),
|
||||
GARMIN_FENIX_6(GarminFenix6Coordinator.class),
|
||||
GARMIN_FENIX_6_SAPPHIRE(GarminFenix6SapphireCoordinator.class),
|
||||
GARMIN_FENIX_6S_PRO(GarminFenix6SProCoordinator.class),
|
||||
GARMIN_FENIX_6S_SAPPHIRE(GarminFenix6SSapphireCoordinator.class),
|
||||
GARMIN_FENIX_7(GarminFenix7Coordinator.class),
|
||||
GARMIN_FENIX_7S(GarminFenix7SCoordinator.class),
|
||||
GARMIN_FENIX_7_PRO(GarminFenix7ProCoordinator.class),
|
||||
GARMIN_FENIX_8(GarminFenix8Coordinator.class),
|
||||
GARMIN_FORERUNNER_55(GarminForerunner55Coordinator.class),
|
||||
GARMIN_FORERUNNER_165(GarminForerunner165Coordinator.class),
|
||||
GARMIN_FORERUNNER_235(GarminForerunner235Coordinator.class),
|
||||
GARMIN_FORERUNNER_245(GarminForerunner245Coordinator.class),
|
||||
@ -436,11 +445,13 @@ public enum DeviceType {
|
||||
GARMIN_FORERUNNER_255S_MUSIC(GarminForerunner255SMusicCoordinator.class),
|
||||
GARMIN_FORERUNNER_265(GarminForerunner265Coordinator.class),
|
||||
GARMIN_FORERUNNER_265S(GarminForerunner265SCoordinator.class),
|
||||
GARMIN_FORERUNNER_620(GarminForerunner620Coordinator.class),
|
||||
GARMIN_FORERUNNER_955(GarminForerunner955Coordinator.class),
|
||||
GARMIN_FORERUNNER_965(GarminForerunner965Coordinator.class),
|
||||
GARMIN_SWIM_2(GarminSwim2Coordinator.class),
|
||||
GARMIN_INSTINCT(GarminInstinctCoordinator.class),
|
||||
GARMIN_INSTINCT_SOLAR(GarminInstinctSolarCoordinator.class),
|
||||
GARMIN_INSTINCT_2(GarminInstinct2Coordinator.class),
|
||||
GARMIN_INSTINCT_2S(GarminInstinct2SCoordinator.class),
|
||||
GARMIN_INSTINCT_2S_SOLAR(GarminInstinct2SSolarCoordinator.class),
|
||||
GARMIN_INSTINCT_2X_SOLAR(GarminInstinct2XSolarCoordinator.class),
|
||||
@ -505,6 +516,7 @@ public enum DeviceType {
|
||||
HUAWEIBANDAW70(HuaweiBandAw70Coordinator.class),
|
||||
HUAWEIBAND6(HuaweiBand6Coordinator.class),
|
||||
HUAWEIWATCHGT(HuaweiWatchGTCoordinator.class),
|
||||
HUAWEIBAND3PRO(HuaweiBand3ProCoordinator.class),
|
||||
HUAWEIBAND4PRO(HuaweiBand4ProCoordinator.class),
|
||||
HUAWEIWATCHGT2(HuaweiWatchGT2Coordinator.class),
|
||||
HUAWEIWATCHGT2E(HuaweiWatchGT2eCoordinator.class),
|
||||
@ -534,6 +546,8 @@ public enum DeviceType {
|
||||
FLIPPER_ZERO(FlipperZeroCoordinator.class),
|
||||
SUPER_CARS(SuperCarsCoordinator.class),
|
||||
ASTEROIDOS(AsteroidOSDeviceCoordinator.class),
|
||||
OPPO_ENCO_AIR(OppoEncoAirCoordinator.class),
|
||||
REALME_BUDS_T110(RealmeBudsT110Coordinator.class),
|
||||
SOFLOW_SO6(SoFlowCoordinator.class),
|
||||
WITHINGS_STEEL_HR(WithingsSteelHRDeviceCoordinator.class),
|
||||
SONY_WENA_3(SonyWena3Coordinator.class),
|
||||
|
@ -63,6 +63,7 @@ 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;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.musicmanager.MusicManagerActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.capabilities.loyaltycards.LoyaltyCard;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBAccess;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
@ -86,12 +87,16 @@ import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePref
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventScreenshot;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventWearState;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceMusicData;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceMusicUpdate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.BatteryLevel;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.NotificationListener;
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.opentracks.OpenTracksController;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceMusic;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceMusicPlaylist;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.BatteryConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
|
||||
@ -236,7 +241,12 @@ public abstract class AbstractDeviceSupport implements DeviceSupport {
|
||||
handleGBDeviceEvent((GBDeviceEventWearState) deviceEvent);
|
||||
} else if (deviceEvent instanceof GBDeviceEventSleepStateDetection) {
|
||||
handleGBDeviceEvent((GBDeviceEventSleepStateDetection) deviceEvent);
|
||||
} else if (deviceEvent instanceof GBDeviceMusicData) {
|
||||
handleGBDeviceEvent((GBDeviceMusicData) deviceEvent);
|
||||
} else if (deviceEvent instanceof GBDeviceMusicUpdate) {
|
||||
handleGBDeviceEvent((GBDeviceMusicUpdate) deviceEvent);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void handleGBDeviceEvent(GBDeviceEventSilentMode deviceEvent) {
|
||||
@ -751,6 +761,53 @@ public abstract class AbstractDeviceSupport implements DeviceSupport {
|
||||
handleDeviceAction(actionOnUnwear, broadcastMessage);
|
||||
}
|
||||
|
||||
private void handleGBDeviceEvent(GBDeviceMusicData deviceEvent) {
|
||||
Context context = getContext();
|
||||
LOG.info("Got event for ACTION_MUSIC_DATA");
|
||||
|
||||
Intent intent = new Intent(MusicManagerActivity.ACTION_MUSIC_DATA);
|
||||
|
||||
intent.putExtra("type", deviceEvent.type);
|
||||
|
||||
if(deviceEvent.list != null) {
|
||||
ArrayList<GBDeviceMusic> list = new ArrayList<>(deviceEvent.list);
|
||||
intent.putExtra("musicList", list);
|
||||
}
|
||||
|
||||
if(deviceEvent.playlists != null) {
|
||||
ArrayList<GBDeviceMusicPlaylist> list = new ArrayList<>(deviceEvent.playlists);
|
||||
intent.putExtra("musicPlaylist", list);
|
||||
}
|
||||
|
||||
if(!TextUtils.isEmpty(deviceEvent.deviceInfo)) {
|
||||
intent.putExtra("deviceInfo", deviceEvent.deviceInfo);
|
||||
}
|
||||
|
||||
if(deviceEvent.maxMusicCount > 0) {
|
||||
intent.putExtra("maxMusicCount", deviceEvent.maxMusicCount);
|
||||
}
|
||||
if(deviceEvent.maxPlaylistCount > 0) {
|
||||
intent.putExtra("maxPlaylistCount", deviceEvent.maxPlaylistCount);
|
||||
}
|
||||
|
||||
LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
|
||||
}
|
||||
|
||||
private void handleGBDeviceEvent(GBDeviceMusicUpdate deviceEvent) {
|
||||
Context context = getContext();
|
||||
LOG.info("Got event for ACTION_MUSIC_UPDATE");
|
||||
|
||||
Intent intent = new Intent(MusicManagerActivity.ACTION_MUSIC_UPDATE);
|
||||
|
||||
intent.putExtra("success", deviceEvent.success);
|
||||
intent.putExtra("operation", deviceEvent.operation);
|
||||
intent.putExtra("playlistIndex", deviceEvent.playlistIndex);
|
||||
intent.putExtra("playlistName", deviceEvent.playlistName);
|
||||
intent.putExtra("musicIds", deviceEvent.musicIds);
|
||||
|
||||
LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
|
||||
}
|
||||
|
||||
private StoreDataTask createStoreTask(String task, Context context, GBDeviceEventBatteryInfo deviceEvent) {
|
||||
return new StoreDataTask(task, context, deviceEvent);
|
||||
}
|
||||
@ -1233,4 +1290,10 @@ public abstract class AbstractDeviceSupport implements DeviceSupport {
|
||||
|
||||
@Override
|
||||
public void onCameraStatusChange(GBDeviceEventCameraRemote.Event event, String filename) {}
|
||||
|
||||
@Override
|
||||
public void onMusicListReq() {}
|
||||
|
||||
@Override
|
||||
public void onMusicOperation(int operation, int playlistIndex, String playlistName, ArrayList<Integer> musicIds) {}
|
||||
}
|
||||
|
@ -108,7 +108,6 @@ public abstract class AbstractHeadphoneDeviceSupport extends AbstractSerialDevic
|
||||
|
||||
@Override
|
||||
public void onSendConfiguration(String config) {
|
||||
LOG.warn("ONSENDCONFIGURATION");
|
||||
if (PREF_SPEAK_NOTIFICATIONS_FOCUS_EXCLUSIVE.equals(config)) {
|
||||
final SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress());
|
||||
gbTextToSpeech.setAudioFocus(prefs.getBoolean(PREF_SPEAK_NOTIFICATIONS_FOCUS_EXCLUSIVE, false) ?
|
||||
|
@ -87,6 +87,7 @@ import nodomain.freeyourgadget.gadgetbridge.externalevents.TinyWeatherForecastGe
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.sleepasandroid.SleepAsAndroidReceiver;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceMusic;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
|
||||
@ -1137,6 +1138,16 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
|
||||
}
|
||||
deviceSupport.onCameraStatusChange(event, filename);
|
||||
break;
|
||||
case ACTION_REQUEST_MUSIC_LIST:
|
||||
deviceSupport.onMusicListReq();
|
||||
break;
|
||||
case ACTION_REQUEST_MUSIC_OPERATION:
|
||||
int operation = intentCopy.getIntExtra("operation", -1);
|
||||
int playlistIndex = intentCopy.getIntExtra("playlistIndex", -1);
|
||||
String playlistName = intentCopy.getStringExtra("playlistName");
|
||||
ArrayList<Integer> musics = (ArrayList<Integer>) intentCopy.getSerializableExtra("musicIds");
|
||||
deviceSupport.onMusicOperation(operation, playlistIndex, playlistName, musics);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -34,6 +34,7 @@ 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.impl.GBDeviceMusic;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
|
||||
@ -524,4 +525,20 @@ public class ServiceDeviceSupport implements DeviceSupport {
|
||||
}
|
||||
delegate.onCameraStatusChange(event, filename);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMusicListReq() {
|
||||
if (checkBusy("music list request")) {
|
||||
return;
|
||||
}
|
||||
delegate.onMusicListReq();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMusicOperation(int operation, int playlistIndex, String playlistName, ArrayList<Integer> musicIds) {
|
||||
if (checkBusy("music operation")) {
|
||||
return;
|
||||
}
|
||||
delegate.onMusicOperation(operation, playlistIndex, playlistName, musicIds);
|
||||
}
|
||||
}
|
||||
|
@ -18,12 +18,18 @@ public class BandWBLEProfile<T extends AbstractBTLEDeviceSupport> extends Abstra
|
||||
public static final String ACTION_DEVICE_INFO = ACTION_PREFIX + "DEVICE_INFO";
|
||||
public static final String EXTRA_DEVICE_INFO = "DEVICE_INFO";
|
||||
|
||||
public static final byte ANC_MODE_OFF = 0x01;
|
||||
public static final byte ANC_MODE_ON = 0x03;
|
||||
|
||||
public static final UUID UUID_RPC_REQUEST_CHARACTERISTIC = UUID.fromString("ada50ce9-67b8-4a97-9d8e-37e1d083156c");
|
||||
|
||||
public BandWBLEProfile(final T support) {
|
||||
super(support);
|
||||
}
|
||||
|
||||
public void requestAncModeState(final TransactionBuilder builder) {
|
||||
sendRequest(builder, (byte) 0x03, (byte) 0x01);
|
||||
}
|
||||
public void requestDeviceName(final TransactionBuilder builder) {
|
||||
sendRequest(builder, (byte) 0x05, (byte) 0x01);
|
||||
}
|
||||
@ -36,6 +42,38 @@ public class BandWBLEProfile<T extends AbstractBTLEDeviceSupport> extends Abstra
|
||||
sendRequest(builder, (byte) 0x08, (byte) 0x17);
|
||||
}
|
||||
|
||||
public void requestVptEnabled(final TransactionBuilder builder) {
|
||||
sendRequest(builder, (byte) 0x03, (byte) 0x05);
|
||||
}
|
||||
|
||||
public void requestVptLevel(final TransactionBuilder builder) {
|
||||
sendRequest(builder, (byte) 0x03, (byte) 0x03);
|
||||
}
|
||||
|
||||
public void requestWearSensorEnabled(final TransactionBuilder builder) {
|
||||
sendRequest(builder, (byte) 0x0a, (byte) 0x01);
|
||||
}
|
||||
|
||||
public void setAncModeState(final TransactionBuilder builder, final boolean mode) throws IOException {
|
||||
BandWPSeriesRequest req = new BandWPSeriesRequest((byte) 0x03, (byte) 0x02).addToPayload(mode ? ANC_MODE_ON : ANC_MODE_OFF);
|
||||
builder.write(getCharacteristic(UUID_RPC_REQUEST_CHARACTERISTIC), req.finishAndGetBytes());
|
||||
}
|
||||
|
||||
public void setVptLevel(final TransactionBuilder builder, final int level) throws IOException {
|
||||
BandWPSeriesRequest req = new BandWPSeriesRequest((byte) 0x03, (byte) 0x04).addToPayload(level);
|
||||
builder.write(getCharacteristic(UUID_RPC_REQUEST_CHARACTERISTIC), req.finishAndGetBytes());
|
||||
}
|
||||
|
||||
public void setVptEnabled(final TransactionBuilder builder, final boolean mode) throws IOException {
|
||||
BandWPSeriesRequest req = new BandWPSeriesRequest((byte) 0x03, (byte) 0x06).addToPayload(mode);
|
||||
builder.write(getCharacteristic(UUID_RPC_REQUEST_CHARACTERISTIC), req.finishAndGetBytes());
|
||||
}
|
||||
|
||||
public void setWearSensorEnabled(final TransactionBuilder builder, final boolean mode) throws IOException {
|
||||
BandWPSeriesRequest req = new BandWPSeriesRequest((byte) 0x0a, (byte) 0x02).addToPayload(mode);
|
||||
builder.write(getCharacteristic(UUID_RPC_REQUEST_CHARACTERISTIC), req.finishAndGetBytes());
|
||||
}
|
||||
|
||||
private void sendRequest(final TransactionBuilder builder, byte namespace, byte commandID) {
|
||||
BandWPSeriesRequest req;
|
||||
try {
|
||||
|
@ -1,15 +1,26 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.bandwpseries;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_ACTIVE_NOISE_CANCELLING_TOGGLE;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_BANDW_PSERIES_GUI_VPT_LEVEL;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_BANDW_PSERIES_VPT_ENABLED;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_BANDW_PSERIES_VPT_LEVEL;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_WEAR_SENSOR_TOGGLE;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.impl.GBDevice.BATTERY_UNKNOWN;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.service.devices.bandwpseries.BandWBLEProfile.ANC_MODE_ON;
|
||||
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
import android.bluetooth.BluetoothGatt;
|
||||
import android.content.SharedPreferences.Editor;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.UUID;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
|
||||
@ -18,6 +29,7 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
public class BandWPSeriesDeviceSupport extends AbstractBTLEDeviceSupport {
|
||||
|
||||
@ -25,6 +37,7 @@ public class BandWPSeriesDeviceSupport extends AbstractBTLEDeviceSupport {
|
||||
|
||||
private static final UUID UUID_RPC_SERVICE = UUID.fromString("85ba93a5-09ac-439a-8cc4-1c3f0cb4f29f");
|
||||
private static final UUID UUID_RPC_RESPONSE_CHARACTERISTIC = UUID.fromString("cb909093-3559-4b0c-9a7f-3f1773122fdc");
|
||||
private static final UUID UUID_RPC_NOTIFICATION_CHARACTERISTIC = UUID.fromString("df55d475-9a32-457a-9e20-38cf14e853fb");
|
||||
|
||||
private final BandWBLEProfile<BandWPSeriesDeviceSupport> BandWBLEProfile;
|
||||
private final GBDeviceEventBatteryInfo[] batteryInfo = new GBDeviceEventBatteryInfo[3];
|
||||
@ -59,9 +72,14 @@ public class BandWPSeriesDeviceSupport extends AbstractBTLEDeviceSupport {
|
||||
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext()));
|
||||
|
||||
builder.notify(getCharacteristic(UUID_RPC_RESPONSE_CHARACTERISTIC), true);
|
||||
builder.notify(getCharacteristic(UUID_RPC_NOTIFICATION_CHARACTERISTIC), true);
|
||||
BandWBLEProfile.requestFirmware(builder);
|
||||
BandWBLEProfile.requestDeviceName(builder);
|
||||
BandWBLEProfile.requestBatteryLevels(builder);
|
||||
BandWBLEProfile.requestAncModeState(builder);
|
||||
BandWBLEProfile.requestVptEnabled(builder);
|
||||
BandWBLEProfile.requestVptLevel(builder);
|
||||
BandWBLEProfile.requestWearSensorEnabled(builder);
|
||||
return builder;
|
||||
}
|
||||
|
||||
@ -70,7 +88,7 @@ public class BandWPSeriesDeviceSupport extends AbstractBTLEDeviceSupport {
|
||||
|
||||
UUID characteristicUUID = characteristic.getUuid();
|
||||
|
||||
if (UUID_RPC_RESPONSE_CHARACTERISTIC.equals(characteristicUUID)) {
|
||||
if (UUID_RPC_RESPONSE_CHARACTERISTIC.equals(characteristicUUID) || UUID_RPC_NOTIFICATION_CHARACTERISTIC.equals(characteristicUUID)) {
|
||||
return handleRPCResponse(characteristic);
|
||||
}
|
||||
return false;
|
||||
@ -91,6 +109,20 @@ public class BandWPSeriesDeviceSupport extends AbstractBTLEDeviceSupport {
|
||||
if (response.commandId == 0x01) {
|
||||
return handleFirmwareVersionResponse(response);
|
||||
}
|
||||
} else if (response.namespace == 0x03) {
|
||||
switch (response.commandId) {
|
||||
case 0x01:
|
||||
return handleGetAncModeStateResponse(response);
|
||||
case 0x02:
|
||||
case 0x04:
|
||||
return getIntResponseStatus(response);
|
||||
case 0x03:
|
||||
return handleGetVptLevelResponse(response);
|
||||
case 0x05:
|
||||
return handleGetVptEnabledResponse(response);
|
||||
case 0x06:
|
||||
return getBooleanResponseStatus(response);
|
||||
}
|
||||
} else if (response.namespace == 0x05) {
|
||||
if (response.commandId == 0x01) {
|
||||
return handleDeviceNameResponse(response);
|
||||
@ -99,10 +131,34 @@ public class BandWPSeriesDeviceSupport extends AbstractBTLEDeviceSupport {
|
||||
if (response.commandId == 0x17) {
|
||||
return handleBatteryLevels(response);
|
||||
}
|
||||
} else if (response.namespace == 0x0a) {
|
||||
if (response.commandId == 0x01) {
|
||||
return handleGetWearSensorEnabledResponse(response);
|
||||
} else if (response.commandId == 0x02) {
|
||||
return getBooleanResponseStatus(response);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean handleGetAncModeStateResponse(BandWPSeriesResponse response) {
|
||||
if (!response.messageType.hasPayload) {
|
||||
GB.toast("No payload in response!", Toast.LENGTH_SHORT, GB.ERROR);
|
||||
return false;
|
||||
}
|
||||
int payloadValue;
|
||||
try {
|
||||
payloadValue = response.payloadUnpacker.unpackInt();
|
||||
} catch (IOException e) {
|
||||
GB.toast("Could not extract ancMode from payload: " + Arrays.toString(response.payload), Toast.LENGTH_SHORT, GB.ERROR);
|
||||
return false;
|
||||
}
|
||||
Editor editor = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).edit();
|
||||
editor.putBoolean(PREF_ACTIVE_NOISE_CANCELLING_TOGGLE, payloadValue == ANC_MODE_ON);
|
||||
editor.apply();
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean handleBatteryLevels(BandWPSeriesResponse response) {
|
||||
int[] levels = response.getPayloadFixArray();
|
||||
if (levels == null) {
|
||||
@ -120,6 +176,24 @@ public class BandWPSeriesDeviceSupport extends AbstractBTLEDeviceSupport {
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean handleGetWearSensorEnabledResponse(BandWPSeriesResponse response) {
|
||||
if (!response.messageType.hasPayload) {
|
||||
GB.toast("No payload in response!", Toast.LENGTH_SHORT, GB.ERROR);
|
||||
return false;
|
||||
}
|
||||
boolean wearSensorEnabled;
|
||||
try {
|
||||
wearSensorEnabled = response.getPayloadBoolean();
|
||||
} catch (IOException e) {
|
||||
GB.toast("Failed to unpack wear sensor status from payload " + Arrays.toString(response.payload), Toast.LENGTH_SHORT, GB.ERROR);
|
||||
return false;
|
||||
}
|
||||
Editor editor = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).edit();
|
||||
editor.putBoolean(PREF_WEAR_SENSOR_TOGGLE, wearSensorEnabled);
|
||||
editor.apply();
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean handleFirmwareVersionResponse(BandWPSeriesResponse response) {
|
||||
String firmwareString = response.getPayloadString();
|
||||
if (firmwareString == null) {
|
||||
@ -146,8 +220,96 @@ public class BandWPSeriesDeviceSupport extends AbstractBTLEDeviceSupport {
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean handleGetVptEnabledResponse(BandWPSeriesResponse response) {
|
||||
if (!response.messageType.hasPayload) {
|
||||
GB.toast("No payload in response!", Toast.LENGTH_SHORT, GB.ERROR);
|
||||
return false;
|
||||
}
|
||||
boolean payloadValue;
|
||||
try {
|
||||
payloadValue = response.payloadUnpacker.unpackBoolean();
|
||||
} catch (IOException e) {
|
||||
GB.toast("Could not extract vptEnabled from payload: " + Arrays.toString(response.payload), Toast.LENGTH_SHORT, GB.ERROR);
|
||||
return false;
|
||||
}
|
||||
int vptLevel = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getInt(PREF_BANDW_PSERIES_VPT_LEVEL, 0);
|
||||
Editor editor = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).edit();
|
||||
editor.putBoolean(PREF_BANDW_PSERIES_VPT_ENABLED, payloadValue);
|
||||
editor.putInt(PREF_BANDW_PSERIES_GUI_VPT_LEVEL, payloadValue ? vptLevel + 1 : 0);
|
||||
editor.apply();
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean handleGetVptLevelResponse(BandWPSeriesResponse response) {
|
||||
if (!response.messageType.hasPayload) {
|
||||
GB.toast("No payload in response!", Toast.LENGTH_SHORT, GB.ERROR);
|
||||
return false;
|
||||
}
|
||||
int payloadValue;
|
||||
try {
|
||||
payloadValue = response.payloadUnpacker.unpackInt();
|
||||
} catch (IOException e) {
|
||||
GB.toast("Could not extract vptLevel from payload: " + Arrays.toString(response.payload), Toast.LENGTH_SHORT, GB.ERROR);
|
||||
return false;
|
||||
}
|
||||
boolean vptEnabled = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getBoolean(PREF_BANDW_PSERIES_VPT_ENABLED, false);
|
||||
Editor editor = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).edit();
|
||||
editor.putInt(PREF_BANDW_PSERIES_VPT_LEVEL, payloadValue);
|
||||
editor.putInt(PREF_BANDW_PSERIES_GUI_VPT_LEVEL, vptEnabled ? payloadValue + 1 : 0);
|
||||
editor.apply();
|
||||
return true;
|
||||
}
|
||||
|
||||
public void onSendConfiguration(String config) {
|
||||
try {
|
||||
TransactionBuilder builder = performInitialized("sendConfig");
|
||||
switch (config) {
|
||||
case PREF_ACTIVE_NOISE_CANCELLING_TOGGLE:
|
||||
boolean ancMode = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getBoolean(PREF_ACTIVE_NOISE_CANCELLING_TOGGLE, true);
|
||||
BandWBLEProfile.setAncModeState(builder, ancMode);
|
||||
break;
|
||||
case PREF_BANDW_PSERIES_GUI_VPT_LEVEL:
|
||||
int level = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getInt(PREF_BANDW_PSERIES_GUI_VPT_LEVEL, 0);
|
||||
BandWBLEProfile.setVptEnabled(builder, level != 0);
|
||||
if (level != 0) {
|
||||
BandWBLEProfile.setVptLevel(builder, level - 1);
|
||||
}
|
||||
break;
|
||||
case PREF_WEAR_SENSOR_TOGGLE:
|
||||
boolean wearSensorEnabled = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getBoolean(PREF_WEAR_SENSOR_TOGGLE, true);
|
||||
BandWBLEProfile.setWearSensorEnabled(builder, wearSensorEnabled);
|
||||
break;
|
||||
}
|
||||
performImmediately(builder);
|
||||
} catch (IOException e) {
|
||||
GB.toast("Failed to send settings update", Toast.LENGTH_SHORT, GB.ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean useAutoConnect() {
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean getBooleanResponseStatus(BandWPSeriesResponse response) {
|
||||
boolean payloadValue;
|
||||
try {
|
||||
payloadValue = response.payloadUnpacker.unpackBoolean();
|
||||
} catch (IOException e) {
|
||||
GB.toast("Could not extract response from payload: " + Arrays.toString(response.payload), Toast.LENGTH_SHORT, GB.ERROR);
|
||||
return false;
|
||||
}
|
||||
return payloadValue;
|
||||
}
|
||||
|
||||
private boolean getIntResponseStatus(BandWPSeriesResponse response) {
|
||||
int payloadValue;
|
||||
try {
|
||||
payloadValue = response.payloadUnpacker.unpackInt();
|
||||
} catch (IOException e) {
|
||||
GB.toast("Could not extract response from payload: " + Arrays.toString(response.payload), Toast.LENGTH_SHORT, GB.ERROR);
|
||||
return false;
|
||||
}
|
||||
return payloadValue == 0;
|
||||
}
|
||||
}
|
||||
|
@ -21,7 +21,6 @@ public class BandWPSeriesRequest {
|
||||
messageType = BandWMessageType.REQUEST_WITHOUT_PAYLOAD;
|
||||
namespace = mNamespace;
|
||||
commandId = mCommandId;
|
||||
payloadPacker.packInt(0);
|
||||
}
|
||||
|
||||
public BandWPSeriesRequest addToPayload(int value) throws IOException {
|
||||
@ -36,6 +35,12 @@ public class BandWPSeriesRequest {
|
||||
return this;
|
||||
}
|
||||
|
||||
public BandWPSeriesRequest addToPayload(boolean value) throws IOException {
|
||||
payloadPacker.packBoolean(value);
|
||||
messageType = BandWMessageType.REQUEST_WITH_PAYLOAD;
|
||||
return this;
|
||||
}
|
||||
|
||||
public BandWPSeriesRequest addToPayload(String value) throws IOException {
|
||||
payloadPacker.packString(value);
|
||||
messageType = BandWMessageType.REQUEST_WITH_PAYLOAD;
|
||||
@ -43,13 +48,15 @@ public class BandWPSeriesRequest {
|
||||
}
|
||||
|
||||
public byte[] finishAndGetBytes() {
|
||||
byte len = (byte) ((this.messageType == BandWMessageType.REQUEST_WITHOUT_PAYLOAD) ? 4 : 4 + payloadPacker.getBufferSize());
|
||||
byte[] payload = payloadPacker.toByteArray();
|
||||
byte len = (byte) ((this.messageType == BandWMessageType.REQUEST_WITHOUT_PAYLOAD) ? 4 : 6 + payload.length);
|
||||
byte[] out = addMessageType(new byte[len+1], messageType.value);
|
||||
out[0] = len;
|
||||
out[3] = commandId;
|
||||
out[4] = namespace;
|
||||
if (messageType == BandWMessageType.REQUEST_WITH_PAYLOAD) {
|
||||
System.arraycopy(payloadPacker.toByteArray(), 0, out, 5, len - 5);
|
||||
addShort(out, 5, payload.length);
|
||||
System.arraycopy(payload, 0, out, 7, payload.length);
|
||||
}
|
||||
try {
|
||||
payloadPacker.close();
|
||||
@ -60,11 +67,14 @@ public class BandWPSeriesRequest {
|
||||
}
|
||||
|
||||
private byte[] addMessageType(byte[] target, int value) {
|
||||
byte valueLo = (byte) (value & 0xff);
|
||||
byte valueHi = (byte) (value >> 8);
|
||||
target[1] = valueLo;
|
||||
target[2] = valueHi;
|
||||
return target;
|
||||
return addShort(target, 1, value);
|
||||
}
|
||||
|
||||
private byte[] addShort(byte[] target, int position, int value) {
|
||||
byte valueLo = (byte) (value & 0xff);
|
||||
byte valueHi = (byte) (value >> 8);
|
||||
target[position] = valueLo;
|
||||
target[position+1] = valueHi;
|
||||
return target;
|
||||
}
|
||||
}
|
||||
|
@ -77,4 +77,8 @@ public class BandWPSeriesResponse {
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
public boolean getPayloadBoolean() throws IOException{
|
||||
return payloadUnpacker.unpackBoolean();
|
||||
}
|
||||
}
|
||||
|
@ -300,7 +300,7 @@ public class CasioGBX100DeviceSupport extends Casio2C2DSupport implements Shared
|
||||
// If not a call or email, check the sender and if null, promote the title and message preview
|
||||
// as subtitle
|
||||
if (showMessagePreview && icon != CasioConstants.CATEGORY_INCOMING_CALL && icon != CasioConstants.CATEGORY_EMAIL) {
|
||||
if (!StringUtils.isNullOrEmpty(sender)) {
|
||||
if (StringUtils.isNullOrEmpty(sender)) {
|
||||
// Shift title to sender slot
|
||||
sender = title;
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes
|
||||
public class FieldDefinitionTemperature extends FieldDefinition {
|
||||
|
||||
public FieldDefinitionTemperature(int localNumber, int size, BaseType baseType, String name) {
|
||||
// #4313 - We do a "wrong" conversion to celsius on purpose
|
||||
super(localNumber, size, baseType, name, 1, -273);
|
||||
}
|
||||
|
||||
|
@ -273,7 +273,8 @@ public class WeatherHandler {
|
||||
return new WeatherValue(kelvin, "KELVIN");
|
||||
case "CELSIUS":
|
||||
default:
|
||||
return new WeatherValue(kelvin - 273.15, "CELSIUS");
|
||||
// #4313 - We do a "wrong" conversion to celsius on purpose
|
||||
return new WeatherValue(kelvin - 273, "CELSIUS");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -30,6 +30,7 @@ import java.util.UUID;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCameraRemote;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceMusic;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Contact;
|
||||
@ -198,4 +199,14 @@ public class HuaweiBRSupport extends AbstractBTBRDeviceSupport {
|
||||
public void onTestNewFunction() {
|
||||
supportProvider.onTestNewFunction();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMusicListReq() {
|
||||
supportProvider.onMusicListReq();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMusicOperation(int operation, int playlistIndex, String playlistName, ArrayList<Integer> musicIds) {
|
||||
supportProvider.onMusicOperation(operation, playlistIndex, playlistName, musicIds);
|
||||
}
|
||||
}
|
||||
|
@ -232,7 +232,7 @@ public class HuaweiEphemerisManager {
|
||||
LOG.info("Ephemeris Time: {} ConfigData: {}", fileTime, availableDataConfig.toString());
|
||||
|
||||
} catch (Exception e) {
|
||||
LOG.error("Ephemeris exception file or config processing", e);
|
||||
LOG.info("Ephemeris exception file or config processing: {}", e.getMessage());
|
||||
availableDataConfig = null;
|
||||
//responseCode = 100007; //no network connection
|
||||
return; // NOTE: just ignore request if something wrong with data.
|
||||
|
@ -33,6 +33,7 @@ import java.util.UUID;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCameraRemote;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceMusic;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Contact;
|
||||
@ -214,4 +215,14 @@ public class HuaweiLESupport extends AbstractBTLEDeviceSupport {
|
||||
public boolean getSendWriteRequestResponse() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMusicListReq() {
|
||||
supportProvider.onMusicListReq();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMusicOperation(int operation, int playlistIndex, String playlistName, ArrayList<Integer> musicIds) {
|
||||
supportProvider.onMusicOperation(operation, playlistIndex, playlistName, musicIds);
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,28 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei;
|
||||
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceMusicData;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceMusicUpdate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiMusicUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.MusicControl;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceMusic;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceMusicPlaylist;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetMusicInfoParams;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetMusicList;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetMusicPlaylist;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetMusicPlaylistMusics;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendMusicOperation;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendUploadMusicFileInfoResponse;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
public class HuaweiMusicManager {
|
||||
static Logger LOG = LoggerFactory.getLogger(HuaweiMusicManager.class);
|
||||
@ -125,4 +142,236 @@ public class HuaweiMusicManager {
|
||||
LOG.error("Could not send sendUploadMusicFileInfoResponse", e);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean syncMusicData = false;
|
||||
private int frameCount = 0;
|
||||
private int endFrame = 65535;
|
||||
private int currentFrame = 0;
|
||||
|
||||
|
||||
public void startSyncMusicData() {
|
||||
syncMusicData = true;
|
||||
try {
|
||||
GetMusicInfoParams getMusicInfoParams = new GetMusicInfoParams(this.support);
|
||||
getMusicInfoParams.doPerform();
|
||||
} catch (IOException e) {
|
||||
LOG.error("Get music info: {}", e.getMessage());
|
||||
syncMusicData = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void syncMusicList() {
|
||||
if (!syncMusicData) {
|
||||
this.currentFrame = 0;
|
||||
return;
|
||||
}
|
||||
int count = this.frameCount;
|
||||
if (support.getHuaweiCoordinator().supportsMoreMusic()) {
|
||||
count = Math.min(this.frameCount, 250);
|
||||
}
|
||||
if (this.currentFrame < count) {
|
||||
try {
|
||||
GetMusicList getMusicList = new GetMusicList(this.support, this.currentFrame, this.endFrame);
|
||||
getMusicList.doPerform();
|
||||
} catch (IOException e) {
|
||||
LOG.error("Get music list: {}", e.getMessage());
|
||||
endMusicListSync();
|
||||
}
|
||||
} else {
|
||||
endMusicListSync();
|
||||
}
|
||||
}
|
||||
|
||||
private void endMusicListSync() {
|
||||
this.currentFrame = 0;
|
||||
try {
|
||||
GetMusicPlaylist getMusicPlaylist = new GetMusicPlaylist(this.support);
|
||||
getMusicPlaylist.doPerform();
|
||||
} catch (IOException e) {
|
||||
LOG.error("Get music playlist: {}", e.getMessage());
|
||||
endMusicPlaylistSync();
|
||||
}
|
||||
}
|
||||
|
||||
private void endMusicPlaylistSync() {
|
||||
this.currentPlaylistIndex = 0;
|
||||
this.currentPlaylistFrame = 0;
|
||||
tempPlaylistMusic.clear();
|
||||
|
||||
musicPlaylistMusicSync();
|
||||
}
|
||||
|
||||
private final List<MusicControl.MusicPlaylists.Response.PlaylistData> devicePlaylists = new ArrayList<>();
|
||||
|
||||
private int currentPlaylistIndex = 0;
|
||||
private int currentPlaylistFrame = 0;
|
||||
private final List<List<Integer>> tempPlaylistMusic = new ArrayList<>();
|
||||
|
||||
private void musicPlaylistMusicSync() {
|
||||
if (this.currentPlaylistIndex < devicePlaylists.size()) {
|
||||
MusicControl.MusicPlaylists.Response.PlaylistData playlist = devicePlaylists.get(this.currentPlaylistIndex);
|
||||
syncPlaylistMusicsOne(playlist.id, playlist.frameCount);
|
||||
} else {
|
||||
musicPlaylistMusicDone();
|
||||
}
|
||||
}
|
||||
|
||||
private void syncPlaylistMusicsOne(int id, int frameCount) {
|
||||
if (this.currentPlaylistFrame < frameCount) {
|
||||
try {
|
||||
GetMusicPlaylistMusics getMusicPlaylistMusics = new GetMusicPlaylistMusics(this.support, id, this.currentPlaylistFrame);
|
||||
getMusicPlaylistMusics.doPerform();
|
||||
} catch (IOException e) {
|
||||
LOG.error("Get music playlist musics: {}", e.getMessage());
|
||||
musicPlaylistMusicDone();
|
||||
}
|
||||
} else {
|
||||
syncPlayListMusicIndexDone(id, frameCount);
|
||||
}
|
||||
}
|
||||
|
||||
public void syncNextPlaylistMusicIndex() {
|
||||
this.currentPlaylistFrame++;
|
||||
MusicControl.MusicPlaylists.Response.PlaylistData playlist = devicePlaylists.get(this.currentPlaylistIndex);
|
||||
syncPlaylistMusicsOne(playlist.id, playlist.frameCount);
|
||||
}
|
||||
|
||||
private void syncPlayListMusicIndexDone(int id, int frameCount) {
|
||||
MusicControl.MusicPlaylists.Response.PlaylistData playlist = devicePlaylists.get(this.currentPlaylistIndex);
|
||||
|
||||
ArrayList<Integer> musics = new ArrayList<>();
|
||||
if (this.tempPlaylistMusic.size() == frameCount) {
|
||||
for (int i = 0; i < frameCount; i++) {
|
||||
musics.addAll(this.tempPlaylistMusic.get(i));
|
||||
}
|
||||
}
|
||||
|
||||
GBDeviceMusicPlaylist pl = new GBDeviceMusicPlaylist(playlist.id, playlist.name, musics);
|
||||
List<GBDeviceMusicPlaylist> list = new ArrayList<>();
|
||||
list.add(pl);
|
||||
sendMusicPlaylist(list);
|
||||
this.currentPlaylistIndex++;
|
||||
this.currentPlaylistFrame = 0;
|
||||
this.tempPlaylistMusic.clear();
|
||||
musicPlaylistMusicSync();
|
||||
}
|
||||
|
||||
private void musicPlaylistMusicDone() {
|
||||
this.currentPlaylistIndex = 0;
|
||||
this.currentPlaylistFrame = 0;
|
||||
this.tempPlaylistMusic.clear();
|
||||
|
||||
this.syncMusicData = false;
|
||||
sendMusicSyncDone();
|
||||
}
|
||||
|
||||
public void onMusicMusicInfoParams(HuaweiMusicUtils.MusicCapabilities capabilities, int frameCount, List<HuaweiMusicUtils.PageStruct> pageStruct) {
|
||||
//TODO: research and use pageStruct. It may/should be used to retrieve music data from devices by pages.
|
||||
// without it list can be incomplete, but I can't confirm this.
|
||||
LOG.info("FrameCount: {}, pageStruct: {}", frameCount, pageStruct);
|
||||
support.getHuaweiCoordinator().setMusicInfoParams(capabilities);
|
||||
if(syncMusicData) {
|
||||
this.frameCount = frameCount;
|
||||
this.currentFrame = 0;
|
||||
this.endFrame = 65535;
|
||||
String formats = null;
|
||||
if(capabilities.supportedFormats != null) {
|
||||
formats = String.join(",", capabilities.supportedFormats);
|
||||
}
|
||||
int maxPlaylistCount = 0;
|
||||
if(support.getCoordinator().getHuaweiCoordinator().getExtendedMusicInfoParams() != null) {
|
||||
maxPlaylistCount = support.getCoordinator().getHuaweiCoordinator().getExtendedMusicInfoParams().maxPlaylistCount;
|
||||
}
|
||||
sendMusicSyncStart(support.getContext().getString(R.string.music_huawei_device_info, formats, capabilities.availableSpace), capabilities.maxMusicCount, maxPlaylistCount);
|
||||
syncMusicList();
|
||||
}
|
||||
}
|
||||
|
||||
private void sendMusicSyncStart(final String info, int maxMusicCount, int maxPlaylistCount) {
|
||||
final GBDeviceMusicData musicListCmd = new GBDeviceMusicData();
|
||||
musicListCmd.type = 1;
|
||||
musicListCmd.deviceInfo = info;
|
||||
musicListCmd.maxMusicCount = maxMusicCount;
|
||||
musicListCmd.maxPlaylistCount = maxPlaylistCount;
|
||||
support.evaluateGBDeviceEvent(musicListCmd);
|
||||
}
|
||||
|
||||
|
||||
private void sendMusicList(List<GBDeviceMusic> list) {
|
||||
final GBDeviceMusicData musicListCmd = new GBDeviceMusicData();
|
||||
musicListCmd.type = 2;
|
||||
musicListCmd.list = list;
|
||||
support.evaluateGBDeviceEvent(musicListCmd);
|
||||
}
|
||||
|
||||
private void sendMusicPlaylist(List<GBDeviceMusicPlaylist> list) {
|
||||
final GBDeviceMusicData musicListCmd = new GBDeviceMusicData();
|
||||
musicListCmd.type = 2;
|
||||
musicListCmd.playlists = list;
|
||||
support.evaluateGBDeviceEvent(musicListCmd);
|
||||
}
|
||||
|
||||
private void sendMusicSyncDone() {
|
||||
final GBDeviceMusicData musicListCmd = new GBDeviceMusicData();
|
||||
musicListCmd.type = 10;
|
||||
support.evaluateGBDeviceEvent(musicListCmd);
|
||||
}
|
||||
|
||||
public void onMusicListResponse(int startFrame, int endFrame, List<GBDeviceMusic> list) {
|
||||
sendMusicList(list);
|
||||
if (support.getHuaweiCoordinator().supportsMoreMusic() || !(endFrame == this.endFrame || list.size() == 1)) {
|
||||
if (list.size() == 2) {
|
||||
this.endFrame = list.get(1).getId();
|
||||
}
|
||||
this.currentFrame++;
|
||||
syncMusicList();
|
||||
return;
|
||||
}
|
||||
endMusicListSync();
|
||||
}
|
||||
|
||||
public void onMusicPlaylistResponse(List<MusicControl.MusicPlaylists.Response.PlaylistData> playlists) {
|
||||
this.devicePlaylists.clear();
|
||||
for(MusicControl.MusicPlaylists.Response.PlaylistData pl: playlists) {
|
||||
if(pl.id != 0) {
|
||||
this.devicePlaylists.add(pl);
|
||||
}
|
||||
}
|
||||
endMusicPlaylistSync();
|
||||
}
|
||||
|
||||
public void onMusicPlaylistMusics(int id, int index, List<Integer> musicIds) {
|
||||
this.tempPlaylistMusic.add(musicIds);
|
||||
syncNextPlaylistMusicIndex();
|
||||
}
|
||||
|
||||
public void onMusicOperation(int operation, int playlistIndex, String playlistName, ArrayList<Integer> musicIds) {
|
||||
LOG.info("music operation: {}", operation);
|
||||
try {
|
||||
SendMusicOperation sendMusicOperation = new SendMusicOperation(this.support, operation, playlistIndex, playlistName, musicIds);
|
||||
sendMusicOperation.doPerform();
|
||||
} catch (IOException e) {
|
||||
LOG.error("SendMusicOperation: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public void onMusicOperationResponse(int resultCode, int operation, int playlistIndex, String playlistName, ArrayList<Integer> musicIds) {
|
||||
|
||||
boolean success = true;
|
||||
if (resultCode != 0x000186A0) {
|
||||
GB.toast(support.getContext(), support.getContext().getString(R.string.music_error), Toast.LENGTH_SHORT, GB.ERROR);
|
||||
success = false;
|
||||
}
|
||||
|
||||
LOG.info("music operation response: {} {}", operation, success);
|
||||
final GBDeviceMusicUpdate updateCmd = new GBDeviceMusicUpdate();
|
||||
updateCmd.success = success;
|
||||
updateCmd.operation = operation;
|
||||
updateCmd.playlistIndex = playlistIndex;
|
||||
updateCmd.playlistName = playlistName;
|
||||
updateCmd.musicIds = musicIds;
|
||||
|
||||
support.evaluateGBDeviceEvent(updateCmd);
|
||||
}
|
||||
}
|
||||
|
@ -50,6 +50,7 @@ import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventAppInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCameraRemote;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventDisplayMessage;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceMusicData;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiCoordinator;
|
||||
@ -124,6 +125,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.Send
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendFitnessUserInfoRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendGpsDataRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendFileUploadInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendHeartRateZonesConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendRunPaceConfigRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendSetContactsRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendNotifyHeartRateCapabilityRequest;
|
||||
@ -833,6 +835,7 @@ public class HuaweiSupportProvider {
|
||||
initRequestQueue.add(new SendFitnessUserInfoRequest(this));
|
||||
initRequestQueue.add(new SendRunPaceConfigRequest(this));
|
||||
initRequestQueue.add(new SendDeviceReportThreshold(this));
|
||||
initRequestQueue.add(new SendHeartRateZonesConfig(this));
|
||||
initRequestQueue.add(new SetMediumToStrengthThresholdRequest(this));
|
||||
initRequestQueue.add(new SendFitnessGoalRequest(this));
|
||||
initRequestQueue.add(new GetNotificationCapabilitiesRequest(this));
|
||||
@ -2529,4 +2532,12 @@ public class HuaweiSupportProvider {
|
||||
callback
|
||||
), true);
|
||||
}
|
||||
|
||||
public void onMusicListReq() {
|
||||
getHuaweiMusicManager().startSyncMusicData();
|
||||
}
|
||||
|
||||
public void onMusicOperation(int operation, int playlistIndex, String playlistName, ArrayList<Integer> musicIds) {
|
||||
getHuaweiMusicManager().onMusicOperation(operation, playlistIndex, playlistName, musicIds);
|
||||
}
|
||||
}
|
||||
|
@ -277,6 +277,7 @@ public class HuaweiWeatherManager {
|
||||
if (response.getTlv().getInteger(0x7f, -1) == 0x000186AA) {
|
||||
// Send weather
|
||||
final ArrayList<WeatherSpec> specs = new ArrayList<>(nodomain.freeyourgadget.gadgetbridge.model.Weather.getInstance().getWeatherSpecs());
|
||||
// TODO: could be empty, not really an issue but we need to check what to send back in that case
|
||||
this.sendWeather(specs.get(0));
|
||||
return;
|
||||
}
|
||||
|
@ -39,6 +39,6 @@ public class GetMusicInfoParams extends Request {
|
||||
throw new Request.ResponseTypeMismatchException(receivedPacket, MusicControl.MusicInfoParams.Response.class);
|
||||
|
||||
MusicControl.MusicInfoParams.Response resp = (MusicControl.MusicInfoParams.Response)(receivedPacket);
|
||||
supportProvider.getHuaweiCoordinator().setMusicInfoParams(resp.params);
|
||||
supportProvider.getHuaweiMusicManager().onMusicMusicInfoParams(resp.params, resp.frameCount, resp.pageStruct);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,45 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.MusicControl;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class GetMusicList extends Request {
|
||||
private final Logger LOG = LoggerFactory.getLogger(GetMusicList.class);
|
||||
|
||||
private final int startFrame;
|
||||
private final int endFrame;
|
||||
|
||||
public GetMusicList(HuaweiSupportProvider support, int startFrame, int endFrame) {
|
||||
super(support);
|
||||
this.serviceId = MusicControl.id;
|
||||
this.commandId = MusicControl.MusicList.id;
|
||||
this.startFrame = startFrame;
|
||||
this.endFrame = endFrame;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws Request.RequestCreationException {
|
||||
try {
|
||||
return new MusicControl.MusicList.Request(paramsProvider, (short) this.startFrame, (short) this.endFrame).serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new Request.RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processResponse() throws Request.ResponseParseException {
|
||||
LOG.info("MusicControl.MusicList processResponse");
|
||||
if (!(receivedPacket instanceof MusicControl.MusicList.Response))
|
||||
throw new Request.ResponseTypeMismatchException(receivedPacket, MusicControl.MusicList.Response.class);
|
||||
|
||||
MusicControl.MusicList.Response resp = (MusicControl.MusicList.Response) (receivedPacket);
|
||||
supportProvider.getHuaweiMusicManager().onMusicListResponse(resp.startFrame, resp.endIndex, resp.musicList);
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.MusicControl;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class GetMusicPlaylist extends Request {
|
||||
private final Logger LOG = LoggerFactory.getLogger(GetMusicPlaylist.class);
|
||||
|
||||
public GetMusicPlaylist(HuaweiSupportProvider support) {
|
||||
super(support);
|
||||
this.serviceId = MusicControl.id;
|
||||
this.commandId = MusicControl.MusicPlaylists.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws Request.RequestCreationException {
|
||||
try {
|
||||
return new MusicControl.MusicPlaylists.Request(paramsProvider).serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new Request.RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processResponse() throws Request.ResponseParseException {
|
||||
LOG.info("MusicControl.MusicPlaylists processResponse");
|
||||
if (!(receivedPacket instanceof MusicControl.MusicPlaylists.Response))
|
||||
throw new Request.ResponseTypeMismatchException(receivedPacket, MusicControl.MusicPlaylists.Response.class);
|
||||
|
||||
MusicControl.MusicPlaylists.Response resp = (MusicControl.MusicPlaylists.Response) (receivedPacket);
|
||||
supportProvider.getHuaweiMusicManager().onMusicPlaylistResponse(resp.playlists);
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.MusicControl;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class GetMusicPlaylistMusics extends Request {
|
||||
private final Logger LOG = LoggerFactory.getLogger(GetMusicPlaylistMusics.class);
|
||||
|
||||
private final int playlist;
|
||||
private final int index;
|
||||
|
||||
public GetMusicPlaylistMusics(HuaweiSupportProvider support, int playlist, int index) {
|
||||
super(support);
|
||||
this.serviceId = MusicControl.id;
|
||||
this.commandId = MusicControl.MusicPlaylistMusics.id;
|
||||
this.playlist = playlist;
|
||||
this.index = index;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws Request.RequestCreationException {
|
||||
try {
|
||||
return new MusicControl.MusicPlaylistMusics.Request(paramsProvider, (short) playlist, (short) index).serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new Request.RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processResponse() throws Request.ResponseParseException {
|
||||
LOG.info("MusicControl.GetMusicPlaylistMusics processResponse");
|
||||
if (!(receivedPacket instanceof MusicControl.MusicPlaylistMusics.Response))
|
||||
throw new Request.ResponseTypeMismatchException(receivedPacket, MusicControl.MusicPlaylistMusics.Response.class);
|
||||
|
||||
MusicControl.MusicPlaylistMusics.Response resp = (MusicControl.MusicPlaylistMusics.Response) (receivedPacket);
|
||||
supportProvider.getHuaweiMusicManager().onMusicPlaylistMusics(resp.id, resp.index, resp.musicIds);
|
||||
}
|
||||
}
|
@ -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.service.devices.huawei.requests;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HeartRateZonesConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FitnessData;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class SendHeartRateZonesConfig extends Request {
|
||||
|
||||
public SendHeartRateZonesConfig(HuaweiSupportProvider support) {
|
||||
super(support);
|
||||
this.serviceId = FitnessData.id;
|
||||
this.commandId = supportProvider.getHuaweiCoordinator().supportsExtendedHeartRateZones() ?
|
||||
FitnessData.HeartRateZoneConfigPacket.id_extended :
|
||||
FitnessData.HeartRateZoneConfigPacket.id_simple;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean requestSupported() {
|
||||
return
|
||||
!supportProvider.getHuaweiCoordinator().supportsTrack() && // In this case it uses P2P
|
||||
supportProvider.getHuaweiCoordinator().supportsHeartRateZones();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
try {
|
||||
HeartRateZonesConfig heartRateZonesConfig = new HeartRateZonesConfig(HeartRateZonesConfig.TYPE_UPRIGHT, new ActivityUser().getAge());
|
||||
if (supportProvider.getHuaweiCoordinator().supportsExtendedHeartRateZones()) {
|
||||
return FitnessData.HeartRateZoneConfigPacket.Request.requestExtended(paramsProvider, heartRateZonesConfig).serialize();
|
||||
} else {
|
||||
return FitnessData.HeartRateZoneConfigPacket.Request.requestSimple(paramsProvider, heartRateZonesConfig).serialize();
|
||||
}
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.MusicControl;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class SendMusicOperation extends Request {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SendMusicOperation.class);
|
||||
|
||||
private final int operation;
|
||||
private final int playlistIndex;
|
||||
private final String playlistName;
|
||||
private final ArrayList<Integer> musicIds;
|
||||
|
||||
|
||||
public SendMusicOperation(HuaweiSupportProvider support, int operation, int playlistIndex, String playlistName, ArrayList<Integer> musicIds) {
|
||||
super(support);
|
||||
this.serviceId = MusicControl.id;
|
||||
this.commandId = MusicControl.MusicOperation.id;
|
||||
this.operation = operation;
|
||||
this.playlistIndex = playlistIndex;
|
||||
this.playlistName = playlistName;
|
||||
this.musicIds = musicIds;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws Request.RequestCreationException {
|
||||
try {
|
||||
return new MusicControl.MusicOperation.Request(paramsProvider, operation, playlistIndex, playlistName, musicIds).serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new Request.RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processResponse() throws ResponseTypeMismatchException {
|
||||
LOG.debug("handle Music Operation");
|
||||
if (!(receivedPacket instanceof MusicControl.MusicOperation.Response))
|
||||
throw new Request.ResponseTypeMismatchException(receivedPacket, MusicControl.MusicOperation.Response.class);
|
||||
|
||||
MusicControl.MusicOperation.Response resp = (MusicControl.MusicOperation.Response) (receivedPacket);
|
||||
supportProvider.getHuaweiMusicManager().onMusicOperationResponse(resp.resultCode, resp.operation, resp.playlistIndex, resp.playlistName, resp.musicIds);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
/* Copyright (C) 2024 José Rebelo
|
||||
|
||||
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.oppo;
|
||||
|
||||
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;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.serial.AbstractSerialDeviceSupport;
|
||||
|
||||
public class OppoHeadphonesIoThread extends BtClassicIoThread {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(OppoHeadphonesIoThread.class);
|
||||
|
||||
private final OppoHeadphonesProtocol mProtocol;
|
||||
|
||||
public OppoHeadphonesIoThread(final GBDevice gbDevice,
|
||||
final Context context,
|
||||
final OppoHeadphonesProtocol deviceProtocol,
|
||||
final AbstractSerialDeviceSupport deviceSupport,
|
||||
final BluetoothAdapter btAdapter) {
|
||||
super(gbDevice, context, deviceProtocol, deviceSupport, btAdapter);
|
||||
this.mProtocol = deviceProtocol;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected UUID getUuidToConnect(@NonNull final ParcelUuid[] uuids) {
|
||||
return UUID.fromString("0000079a-d102-11e1-9b23-00025b00a5a5");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initialize() {
|
||||
write(mProtocol.encodeFirmwareVersionReq());
|
||||
write(mProtocol.encodeConfigurationReq());
|
||||
write(mProtocol.encodeBatteryReq());
|
||||
setUpdateState(GBDevice.State.INITIALIZED);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] parseIncoming(final InputStream inStream) throws IOException {
|
||||
final byte[] buffer = new byte[1048576]; //HUGE read
|
||||
final int bytes = inStream.read(buffer);
|
||||
// FIXME: We should buffer this and handle partial commands
|
||||
LOG.debug("Read {} bytes: {}", bytes, hexdump(buffer, 0, bytes));
|
||||
return Arrays.copyOf(buffer, bytes);
|
||||
}
|
||||
}
|
@ -0,0 +1,355 @@
|
||||
/* Copyright (C) 2024 José Rebelo
|
||||
|
||||
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.oppo;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.oppo.OppoHeadphonesPreferences;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.OppoCommand;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.TouchConfigSide;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.TouchConfigType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.TouchConfigValue;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.preferences.DevicePrefs;
|
||||
|
||||
public class OppoHeadphonesProtocol extends GBDeviceProtocol {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(OppoHeadphonesProtocol.class);
|
||||
|
||||
public static final byte CMD_PREAMBLE = (byte) 0xaa;
|
||||
|
||||
private int seqNum = 0;
|
||||
|
||||
protected OppoHeadphonesProtocol(final GBDevice device) {
|
||||
super(device);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GBDeviceEvent[] decodeResponse(final byte[] responseData) {
|
||||
final List<GBDeviceEvent> events = new ArrayList<>();
|
||||
int i = 0;
|
||||
while (i < responseData.length) {
|
||||
if (responseData[i] != CMD_PREAMBLE) {
|
||||
LOG.warn("Unexpected preamble {}", responseData[i]);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
final int totalLength = responseData[i + 1] & 0xff;
|
||||
if (responseData.length - i < totalLength + 2) {
|
||||
LOG.error("Got partial response with {} bytes, expected {}", responseData.length - i, totalLength + 2);
|
||||
break;
|
||||
}
|
||||
|
||||
final byte[] singleResponse = ArrayUtils.subarray(responseData, i, i + totalLength + 3);
|
||||
|
||||
events.addAll(handleSingleResponse(singleResponse));
|
||||
|
||||
i += totalLength + 2;
|
||||
}
|
||||
return events.toArray(new GBDeviceEvent[0]);
|
||||
}
|
||||
|
||||
private static List<GBDeviceEvent> handleSingleResponse(final byte[] responseData) {
|
||||
final List<GBDeviceEvent> events = new ArrayList<>();
|
||||
|
||||
final ByteBuffer responseBuf = ByteBuffer.wrap(responseData).order(ByteOrder.LITTLE_ENDIAN);
|
||||
final byte preamble = responseBuf.get();
|
||||
|
||||
if (preamble != CMD_PREAMBLE) {
|
||||
LOG.error("Unexpected preamble {}", preamble);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
final byte totalLength = responseBuf.get();
|
||||
if (responseData.length != totalLength + 2) {
|
||||
LOG.error("Invalid number of bytes {}, expected {}", responseData.length, totalLength + 2);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
final short zero = responseBuf.getShort();
|
||||
if (zero != 0 && zero != 4) {
|
||||
// 0 on oppo, 4 on realme?
|
||||
LOG.warn("Unexpected bytes: {}, expected 0 or 4", zero);
|
||||
}
|
||||
|
||||
final short code = responseBuf.getShort();
|
||||
final OppoCommand command = OppoCommand.fromCode(code);
|
||||
if (command == null) {
|
||||
LOG.warn("Unknown command code {}", String.format(Locale.ROOT, "0x%04x", code));
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
final int seq = responseBuf.get();
|
||||
final short payloadLength = responseBuf.getShort();
|
||||
final byte[] payload = new byte[payloadLength];
|
||||
responseBuf.get(payload);
|
||||
|
||||
switch (command) {
|
||||
case BATTERY_RET: {
|
||||
if (payload[0] != 0) {
|
||||
LOG.error("Unknown battery ret {}", payload[0]);
|
||||
break;
|
||||
}
|
||||
events.addAll(parseBattery(payload));
|
||||
break;
|
||||
}
|
||||
case DEVICE_INFO: {
|
||||
switch (payload[0]) {
|
||||
case 1: // battery
|
||||
events.addAll(parseBattery(payload));
|
||||
break;
|
||||
case 2: // status
|
||||
LOG.debug("Got status");
|
||||
// TODO handle
|
||||
break;
|
||||
default:
|
||||
LOG.warn("Unknown device info {}", payload[0]);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case FIRMWARE_RET: {
|
||||
if (payload[0] != 0) {
|
||||
LOG.warn("Unexpected firmware ret {}", payload[0]);
|
||||
break;
|
||||
}
|
||||
|
||||
final String fwString;
|
||||
if (payload[payload.length - 1] == 0) {
|
||||
fwString = new String(ArrayUtils.subarray(payload, 2, payload.length - 1)).strip();
|
||||
} else {
|
||||
fwString = new String(ArrayUtils.subarray(payload, 2, payload.length - 2)).strip();
|
||||
}
|
||||
final String[] parts = fwString.split(",");
|
||||
if (parts.length % 3 != 0) {
|
||||
LOG.warn("Fw parts length {} from '{}' is not divisible by 3", parts.length, fwString);
|
||||
break;
|
||||
}
|
||||
final String[] fwVersionParts = new String[3];
|
||||
for (int i = 0; i < parts.length; i += 3) {
|
||||
final String versionPart = parts[i];
|
||||
final String versionType = parts[i + 1];
|
||||
final String version = parts[i + 2];
|
||||
if (!"2".equals(versionType)) {
|
||||
continue; // not fw
|
||||
}
|
||||
|
||||
switch (versionPart) {
|
||||
case "1":
|
||||
fwVersionParts[0] = version;
|
||||
break;
|
||||
case "2":
|
||||
fwVersionParts[1] = version;
|
||||
break;
|
||||
case "3":
|
||||
fwVersionParts[2] = version;
|
||||
break;
|
||||
default:
|
||||
LOG.warn("Unknown firmware version part {}", versionPart);
|
||||
}
|
||||
}
|
||||
|
||||
final List<String> nonNullParts = new ArrayList<>(fwVersionParts.length);
|
||||
for (int i = 0; i < fwVersionParts.length; i++) {
|
||||
if (fwVersionParts[i] == null) {
|
||||
continue;
|
||||
}
|
||||
nonNullParts.add(fwVersionParts[i]);
|
||||
if (fwVersionParts[i].contains(".")) {
|
||||
// Realme devices have the version already with the dots, repeated multiple times
|
||||
break;
|
||||
}
|
||||
}
|
||||
final String fwVersion = String.join(".", nonNullParts);
|
||||
|
||||
final GBDeviceEventVersionInfo eventVersionInfo = new GBDeviceEventVersionInfo();
|
||||
eventVersionInfo.fwVersion = fwVersion;
|
||||
eventVersionInfo.hwVersion = GBApplication.getContext().getString(R.string.n_a);
|
||||
events.add(eventVersionInfo);
|
||||
|
||||
LOG.debug("Got fw version: {}", fwVersion);
|
||||
|
||||
break;
|
||||
}
|
||||
case FIND_DEVICE_ACK: {
|
||||
LOG.debug("Got find device ack, status={}", payload[0]);
|
||||
break;
|
||||
}
|
||||
case TOUCH_CONFIG_RET: {
|
||||
if (payload[0] != 0) {
|
||||
LOG.warn("Unknown config ret {}", payload[0]);
|
||||
break;
|
||||
}
|
||||
if ((payload.length - 2) % 4 != 0) {
|
||||
LOG.warn("Unexpected config ret payload size {}", payload.length);
|
||||
break;
|
||||
}
|
||||
|
||||
final GBDeviceEventUpdatePreferences eventUpdatePreferences = new GBDeviceEventUpdatePreferences();
|
||||
|
||||
for (int i = 2; i < payload.length; i += 4) {
|
||||
final int sideCode = payload[i] & 0xff;
|
||||
final int typeCode = BLETypeConversions.toUint16(payload, i + 1);
|
||||
final int valueCode = payload[i + 3] & 0xff;
|
||||
final TouchConfigSide side = TouchConfigSide.fromCode(sideCode);
|
||||
final TouchConfigType type = TouchConfigType.fromCode(typeCode);
|
||||
final TouchConfigValue value = TouchConfigValue.fromCode(valueCode);
|
||||
|
||||
if (side == null) {
|
||||
LOG.warn("Unknown side code {}", sideCode);
|
||||
continue;
|
||||
}
|
||||
if (type == null) {
|
||||
LOG.warn("Unknown type code {}", typeCode);
|
||||
continue;
|
||||
}
|
||||
if (value == null) {
|
||||
LOG.warn("Unknown value code {}", valueCode);
|
||||
continue;
|
||||
}
|
||||
|
||||
LOG.debug("Got touch config for {} {} = {}", side, type, value);
|
||||
|
||||
eventUpdatePreferences.withPreference(
|
||||
OppoHeadphonesPreferences.getKey(side, type),
|
||||
value.name().toLowerCase(Locale.ROOT)
|
||||
);
|
||||
}
|
||||
|
||||
events.add(eventUpdatePreferences);
|
||||
|
||||
break;
|
||||
}
|
||||
case TOUCH_CONFIG_ACK: {
|
||||
LOG.debug("Got config ack, status={}", payload[0]);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
LOG.warn("Unhandled command {}", command);
|
||||
}
|
||||
|
||||
return events;
|
||||
}
|
||||
|
||||
private static List<GBDeviceEvent> parseBattery(final byte[] payload) {
|
||||
final List<GBDeviceEvent> events = new ArrayList<>();
|
||||
|
||||
final int numBatteries = payload[1] & 0xff;
|
||||
for (int i = 2; i < payload.length; i += 2) {
|
||||
if ((payload[i] & 0xff) == 0xff) {
|
||||
continue;
|
||||
}
|
||||
final int batteryIndex = payload[i] - 1;
|
||||
if (batteryIndex < 0 || batteryIndex > 2) {
|
||||
LOG.error("Unknown battery index {}", payload[i]);
|
||||
break;
|
||||
}
|
||||
|
||||
final int batteryLevel = payload[i + 1] & 0x7f;
|
||||
final BatteryState batteryState = (payload[i + 1] & 0x80) != 0 ? BatteryState.BATTERY_CHARGING : BatteryState.BATTERY_NORMAL;
|
||||
|
||||
LOG.debug("Got battery {}: {}%, {}", batteryIndex, batteryLevel, batteryState);
|
||||
|
||||
final GBDeviceEventBatteryInfo eventBatteryInfo = new GBDeviceEventBatteryInfo();
|
||||
eventBatteryInfo.batteryIndex = batteryIndex;
|
||||
eventBatteryInfo.level = batteryLevel;
|
||||
eventBatteryInfo.state = batteryState;
|
||||
events.add(eventBatteryInfo);
|
||||
}
|
||||
|
||||
return events;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] encodeFirmwareVersionReq() {
|
||||
return encodeMessage(OppoCommand.FIRMWARE_GET, new byte[0]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] encodeFindDevice(final boolean start) {
|
||||
return encodeMessage(OppoCommand.FIND_DEVICE_REQ, new byte[]{(byte) (start ? 0x01 : 0x00)});
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] encodeSendConfiguration(final String config) {
|
||||
final DevicePrefs prefs = getDevicePrefs();
|
||||
|
||||
if (config.startsWith("oppo_touch__")) {
|
||||
final String[] parts = config.split("__");
|
||||
final TouchConfigSide side = TouchConfigSide.valueOf(parts[1].toUpperCase(Locale.ROOT));
|
||||
final TouchConfigType type = TouchConfigType.valueOf(parts[2].toUpperCase(Locale.ROOT));
|
||||
final String valueCode = prefs.getString(OppoHeadphonesPreferences.getKey(side, type), null);
|
||||
if (valueCode == null) {
|
||||
LOG.warn("Failed to get touch option value for {}/{}", side, type);
|
||||
return super.encodeSendConfiguration(config);
|
||||
}
|
||||
|
||||
final TouchConfigValue value = TouchConfigValue.valueOf(valueCode.toUpperCase(Locale.ROOT));
|
||||
|
||||
LOG.debug("Sending {} {} = {}", side, type, value);
|
||||
|
||||
final ByteBuffer buf = ByteBuffer.allocate(5).order(ByteOrder.LITTLE_ENDIAN);
|
||||
buf.put((byte) 0x01);
|
||||
buf.put((byte) side.getCode());
|
||||
buf.putShort((short) type.getCode());
|
||||
buf.put((byte) value.getCode());
|
||||
|
||||
return encodeMessage(OppoCommand.TOUCH_CONFIG_SET, buf.array());
|
||||
}
|
||||
|
||||
return super.encodeSendConfiguration(config);
|
||||
}
|
||||
|
||||
public byte[] encodeBatteryReq() {
|
||||
return encodeMessage(OppoCommand.BATTERY_REQ, new byte[0]);
|
||||
}
|
||||
|
||||
public byte[] encodeConfigurationReq() {
|
||||
return encodeMessage(OppoCommand.TOUCH_CONFIG_REQ, new byte[]{0x02, 0x03, 0x01});
|
||||
}
|
||||
|
||||
private byte[] encodeMessage(final OppoCommand command, final byte[] payload) {
|
||||
final ByteBuffer buf = ByteBuffer.allocate(9 + payload.length).order(ByteOrder.LITTLE_ENDIAN);
|
||||
buf.put(CMD_PREAMBLE);
|
||||
buf.put((byte) (buf.limit() - 2));
|
||||
buf.put((byte) 0);
|
||||
buf.put((byte) 0);
|
||||
buf.putShort(command.getCode());
|
||||
buf.put((byte) seqNum++);
|
||||
buf.putShort((short) payload.length);
|
||||
buf.put(payload);
|
||||
return buf.array();
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
/* Copyright (C) 2024 José Rebelo
|
||||
|
||||
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.oppo;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.AbstractHeadphoneDeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceIoThread;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
|
||||
|
||||
public class OppoHeadphonesSupport extends AbstractHeadphoneDeviceSupport {
|
||||
@Override
|
||||
protected GBDeviceProtocol createDeviceProtocol() {
|
||||
return new OppoHeadphonesProtocol(getDevice());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected GBDeviceIoThread createDeviceIOThread() {
|
||||
return new OppoHeadphonesIoThread(
|
||||
getDevice(),
|
||||
getContext(),
|
||||
(OppoHeadphonesProtocol) getDeviceProtocol(),
|
||||
OppoHeadphonesSupport.this,
|
||||
getBluetoothAdapter()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean useAutoConnect() {
|
||||
return false;
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
/* Copyright (C) 2024 José Rebelo
|
||||
|
||||
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.oppo.commands;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public enum OppoCommand {
|
||||
BATTERY_REQ(0x0106),
|
||||
BATTERY_RET(0x8106),
|
||||
DEVICE_INFO(0x0204),
|
||||
FIRMWARE_GET(0x0105),
|
||||
FIRMWARE_RET(0x8105),
|
||||
TOUCH_CONFIG_REQ(0x0108),
|
||||
TOUCH_CONFIG_SET(0x0401),
|
||||
TOUCH_CONFIG_RET(0x8108),
|
||||
TOUCH_CONFIG_ACK(0x8401),
|
||||
FIND_DEVICE_REQ(0x0400),
|
||||
FIND_DEVICE_ACK(0x8400),
|
||||
;
|
||||
|
||||
private final short code;
|
||||
|
||||
OppoCommand(final int code) {
|
||||
this.code = (short) code;
|
||||
}
|
||||
|
||||
public short getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static OppoCommand fromCode(final short code) {
|
||||
for (final OppoCommand cmd : OppoCommand.values()) {
|
||||
if (cmd.code == code) {
|
||||
return cmd;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
/* Copyright (C) 2024 José Rebelo
|
||||
|
||||
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.oppo.commands;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public enum TouchConfigSide {
|
||||
LEFT(0x01),
|
||||
RIGHT(0x02),
|
||||
BOTH(0x04),
|
||||
;
|
||||
|
||||
private final int code;
|
||||
|
||||
TouchConfigSide(final int code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public int getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static TouchConfigSide fromCode(final int code) {
|
||||
for (final TouchConfigSide param : TouchConfigSide.values()) {
|
||||
if (param.code == code) {
|
||||
return param;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
/* Copyright (C) 2024 José Rebelo
|
||||
|
||||
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.oppo.commands;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public enum TouchConfigType {
|
||||
UNK_1(0x0101),
|
||||
TAP_2(0x0201),
|
||||
TAP_3(0x0301),
|
||||
HOLD(0x0401),
|
||||
;
|
||||
|
||||
private final int code;
|
||||
|
||||
TouchConfigType(final int code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public int getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static TouchConfigType fromCode(final int code) {
|
||||
for (final TouchConfigType param : TouchConfigType.values()) {
|
||||
if (param.code == code) {
|
||||
return param;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
/* Copyright (C) 2024 José Rebelo
|
||||
|
||||
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.oppo.commands;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public enum TouchConfigValue {
|
||||
OFF(0x00),
|
||||
PLAY_PAUSE(0x01),
|
||||
VOICE_ASSISTANT(0x03), // oppo
|
||||
VOICE_ASSISTANT_REALME(0x04),
|
||||
PREVIOUS(0x05),
|
||||
NEXT(0x06),
|
||||
GAME_MODE(0x11),
|
||||
VOLUME_UP(0x0B),
|
||||
VOLUME_DOWN(0x0C),
|
||||
;
|
||||
|
||||
private final int code;
|
||||
|
||||
TouchConfigValue(final int code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public int getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static TouchConfigValue fromCode(final int code) {
|
||||
for (final TouchConfigValue param : TouchConfigValue.values()) {
|
||||
if (param.code == code) {
|
||||
return param;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
@ -25,10 +25,10 @@ public final class WithingsUUID {
|
||||
public static final UUID WITHINGS_APP_CHARACTERISTIC_UUID = UUID.fromString("10000059-5749-5448-0037-000000000000");
|
||||
public static final UUID WITHINGS_APP_CHARACTERISTIC2_UUID = UUID.fromString("10000028-5749-5448-0037-000000000000");
|
||||
public static final UUID CCC_DESCRIPTOR_UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
|
||||
public static final UUID WITHINGS_ANCS_SERVICE_UUID = UUID.fromString("10000057-5749-5448-0037-00000000000000");
|
||||
public static final UUID NOTIFICATION_SOURCE_CHARACTERISTIC_UUID = UUID.fromString("10000059-5749-5448-0037-00000000000000");
|
||||
public static final UUID CONTROL_POINT_CHARACTERISTIC_UUID = UUID.fromString("10000058-5749-5448-0037-00000000000000");
|
||||
public static final UUID DATA_SOURCE_CHARACTERISTIC_UUID = UUID.fromString("1000005a-5749-5448-0037-00000000000000");
|
||||
public static final UUID WITHINGS_ANCS_SERVICE_UUID = UUID.fromString("10000057-5749-5448-0037-000000000000");
|
||||
public static final UUID NOTIFICATION_SOURCE_CHARACTERISTIC_UUID = UUID.fromString("10000059-5749-5448-0037-000000000000");
|
||||
public static final UUID CONTROL_POINT_CHARACTERISTIC_UUID = UUID.fromString("10000058-5749-5448-0037-000000000000");
|
||||
public static final UUID DATA_SOURCE_CHARACTERISTIC_UUID = UUID.fromString("1000005a-5749-5448-0037-000000000000");
|
||||
|
||||
private WithingsUUID() {}
|
||||
}
|
||||
|
@ -105,7 +105,11 @@ public class XiaomiSppProtocolV2 extends AbstractXiaomiSppProtocol {
|
||||
break;
|
||||
case PACKET_TYPE_DATA:
|
||||
XiaomiSppPacketV2.DataPacket dataPacket = (XiaomiSppPacketV2.DataPacket) decodedPacket;
|
||||
support.onPacketReceived(dataPacket.getChannel(), dataPacket.getPayloadBytes(support.getAuthService()));
|
||||
try {
|
||||
support.onPacketReceived(dataPacket.getChannel(), dataPacket.getPayloadBytes(support.getAuthService()));
|
||||
} catch (final Exception ex) {
|
||||
LOG.error("Exception while handling received packet", ex);
|
||||
}
|
||||
// TODO: only directly ack protobuf packets, bulk ack others
|
||||
sendAck(decodedPacket.getSequenceNumber());
|
||||
break;
|
||||
|
@ -454,6 +454,9 @@ public class WorkoutSummaryParser extends XiaomiActivityParser implements Activi
|
||||
case 5:
|
||||
headerSize = 6;
|
||||
break;
|
||||
case 6:
|
||||
headerSize = 7;
|
||||
break;
|
||||
default:
|
||||
LOG.warn("Unable to parse workout summary version {}", fileId.getVersion());
|
||||
return null;
|
||||
|
@ -0,0 +1,328 @@
|
||||
/* Copyright (C) 2024 Arjan Schrijver
|
||||
|
||||
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 <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.util;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.BuildConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.NotificationListener;
|
||||
|
||||
public class PermissionsUtils {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(PermissionsUtils.class);
|
||||
|
||||
public static final String CUSTOM_PERM_NOTIFICATION_LISTENER = "custom_perm_notifications_listener";
|
||||
public static final String CUSTOM_PERM_NOTIFICATION_SERVICE = "custom_perm_notifications_service";
|
||||
public static final String CUSTOM_PERM_DISPLAY_OVER = "custom_perm_display_over";
|
||||
|
||||
public static final List<String> specialPermissions = new ArrayList<String>() {{
|
||||
add(CUSTOM_PERM_NOTIFICATION_LISTENER);
|
||||
add(CUSTOM_PERM_NOTIFICATION_SERVICE);
|
||||
add(CUSTOM_PERM_DISPLAY_OVER);
|
||||
add(Manifest.permission.ACCESS_FINE_LOCATION);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
add(Manifest.permission.ACCESS_BACKGROUND_LOCATION);
|
||||
}
|
||||
}};
|
||||
|
||||
public static ArrayList<PermissionDetails> getRequiredPermissionsList(Activity activity) {
|
||||
ArrayList<PermissionDetails> permissionsList = new ArrayList<>();
|
||||
permissionsList.add(new PermissionDetails(
|
||||
CUSTOM_PERM_NOTIFICATION_LISTENER,
|
||||
activity.getString(R.string.menuitem_notifications),
|
||||
activity.getString(R.string.permission_notifications_summary)));
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
permissionsList.add(new PermissionDetails(
|
||||
CUSTOM_PERM_NOTIFICATION_SERVICE,
|
||||
activity.getString(R.string.permission_manage_dnd_title),
|
||||
activity.getString(R.string.permission_manage_dnd_summary)));
|
||||
permissionsList.add(new PermissionDetails(
|
||||
CUSTOM_PERM_DISPLAY_OVER,
|
||||
activity.getString(R.string.permission_displayover_title),
|
||||
activity.getString(R.string.permission_displayover_summary)));
|
||||
}
|
||||
permissionsList.add(new PermissionDetails(
|
||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||
activity.getString(R.string.permission_fine_location_title),
|
||||
activity.getString(R.string.permission_fine_location_summary)));
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
permissionsList.add(new PermissionDetails(
|
||||
Manifest.permission.ACCESS_BACKGROUND_LOCATION,
|
||||
activity.getString(R.string.permission_background_location_title),
|
||||
activity.getString(R.string.permission_background_location_summary)));
|
||||
}
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
|
||||
permissionsList.add(new PermissionDetails(
|
||||
Manifest.permission.BLUETOOTH,
|
||||
activity.getString(R.string.permission_bluetooth_title),
|
||||
activity.getString(R.string.permission_bluetooth_summary)));
|
||||
permissionsList.add(new PermissionDetails(
|
||||
Manifest.permission.BLUETOOTH_ADMIN,
|
||||
activity.getString(R.string.permission_bluetooth_admin_title),
|
||||
activity.getString(R.string.permission_bluetooth_admin_summary)));
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
permissionsList.add(new PermissionDetails(
|
||||
Manifest.permission.BLUETOOTH_SCAN,
|
||||
activity.getString(R.string.permission_bluetooth_scan_title),
|
||||
activity.getString(R.string.permission_bluetooth_scan_summary)));
|
||||
permissionsList.add(new PermissionDetails(
|
||||
Manifest.permission.BLUETOOTH_CONNECT,
|
||||
activity.getString(R.string.permission_bluetooth_connect_title),
|
||||
activity.getString(R.string.permission_bluetooth_connect_summary)));
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
permissionsList.add(new PermissionDetails(
|
||||
Manifest.permission.POST_NOTIFICATIONS,
|
||||
activity.getString(R.string.permission_post_notification_title),
|
||||
activity.getString(R.string.permission_post_notification_summary)));
|
||||
}
|
||||
if (BuildConfig.INTERNET_ACCESS) {
|
||||
permissionsList.add(new PermissionDetails(
|
||||
Manifest.permission.INTERNET,
|
||||
activity.getString(R.string.permission_internet_access_title),
|
||||
activity.getString(R.string.permission_internet_access_summary)));
|
||||
}
|
||||
// permissionsList.add(new PermissionDetails( // NOTE: can't request this, it's only allowed for system apps
|
||||
// Manifest.permission.MEDIA_CONTENT_CONTROL,
|
||||
// "Media content control",
|
||||
// "Read and control media playback"));
|
||||
permissionsList.add(new PermissionDetails(
|
||||
Manifest.permission.READ_CONTACTS,
|
||||
activity.getString(R.string.permission_contacts_title),
|
||||
activity.getString(R.string.permission_contacts_summary)));
|
||||
permissionsList.add(new PermissionDetails(
|
||||
Manifest.permission.READ_CALENDAR,
|
||||
activity.getString(R.string.permission_calendar_title),
|
||||
activity.getString(R.string.permission_calendar_summary)));
|
||||
permissionsList.add(new PermissionDetails(
|
||||
Manifest.permission.RECEIVE_SMS,
|
||||
activity.getString(R.string.permission_receive_sms_title),
|
||||
activity.getString(R.string.permission_receive_sms_summary)));
|
||||
permissionsList.add(new PermissionDetails(
|
||||
Manifest.permission.SEND_SMS,
|
||||
activity.getString(R.string.permission_send_sms_title),
|
||||
activity.getString(R.string.permission_send_sms_summary)));
|
||||
permissionsList.add(new PermissionDetails(
|
||||
Manifest.permission.READ_CALL_LOG,
|
||||
activity.getString(R.string.permission_read_call_log_title),
|
||||
activity.getString(R.string.permission_read_call_log_summary)));
|
||||
permissionsList.add(new PermissionDetails(
|
||||
Manifest.permission.READ_PHONE_STATE,
|
||||
activity.getString(R.string.permission_read_phone_state_title),
|
||||
activity.getString(R.string.permission_read_phone_state_summary)));
|
||||
permissionsList.add(new PermissionDetails(
|
||||
Manifest.permission.CALL_PHONE,
|
||||
activity.getString(R.string.permission_call_phone_title),
|
||||
activity.getString(R.string.permission_call_phone_summary)));
|
||||
permissionsList.add(new PermissionDetails(
|
||||
Manifest.permission.PROCESS_OUTGOING_CALLS,
|
||||
activity.getString(R.string.permission_process_outgoing_calls_title),
|
||||
activity.getString(R.string.permission_process_outgoing_calls_summary)));
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
permissionsList.add(new PermissionDetails(
|
||||
Manifest.permission.ANSWER_PHONE_CALLS,
|
||||
activity.getString(R.string.permission_answer_phone_calls_title),
|
||||
activity.getString(R.string.permission_answer_phone_calls_summary)));
|
||||
}
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
|
||||
permissionsList.add(new PermissionDetails(
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||
activity.getString(R.string.permission_external_storage_title),
|
||||
activity.getString(R.string.permission_external_storage_summary)));
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
permissionsList.add(new PermissionDetails(
|
||||
Manifest.permission.QUERY_ALL_PACKAGES,
|
||||
activity.getString(R.string.permission_query_all_packages_title),
|
||||
activity.getString(R.string.permission_query_all_packages_summary)));
|
||||
}
|
||||
return permissionsList;
|
||||
}
|
||||
|
||||
public static boolean checkPermission(Context context, String permission) {
|
||||
if (permission.equals(CUSTOM_PERM_NOTIFICATION_LISTENER)) {
|
||||
Set<String> set = NotificationManagerCompat.getEnabledListenerPackages(context);
|
||||
return set.contains(context.getPackageName());
|
||||
} else if (permission.equals(CUSTOM_PERM_NOTIFICATION_SERVICE) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
return ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE)).isNotificationPolicyAccessGranted();
|
||||
} else if (permission.equals(CUSTOM_PERM_DISPLAY_OVER) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
return Settings.canDrawOverlays(context);
|
||||
} else {
|
||||
return ContextCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_DENIED;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean checkAllPermissions(Activity activity) {
|
||||
boolean result = true;
|
||||
for (PermissionDetails permission : getRequiredPermissionsList(activity)) {
|
||||
if (!checkPermission(activity, permission.getPermission())) {
|
||||
result = false;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static void requestPermission(Activity activity, String permission) {
|
||||
if (permission.equals(CUSTOM_PERM_NOTIFICATION_LISTENER)) {
|
||||
showNotifyListenerPermissionsDialog(activity);
|
||||
} else if (permission.equals(CUSTOM_PERM_NOTIFICATION_SERVICE) && (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)) {
|
||||
showNotifyPolicyPermissionsDialog(activity);
|
||||
} else if (permission.equals(CUSTOM_PERM_DISPLAY_OVER)) {
|
||||
showDisplayOverOthersPermissionsDialog(activity);
|
||||
} else if (permission.equals(Manifest.permission.ACCESS_BACKGROUND_LOCATION) && (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)) {
|
||||
showBackgroundLocationPermissionsDialog(activity);
|
||||
} else {
|
||||
ActivityCompat.requestPermissions(activity, new String[]{permission}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
public static class PermissionDetails {
|
||||
private String permission;
|
||||
private String title;
|
||||
private String summary;
|
||||
|
||||
public PermissionDetails(String permission, String title, String summary) {
|
||||
this.permission = permission;
|
||||
this.title = title;
|
||||
this.summary = summary;
|
||||
}
|
||||
|
||||
public String getPermission() {
|
||||
return permission;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public String getSummary() {
|
||||
return summary;
|
||||
}
|
||||
}
|
||||
|
||||
private static void showNotifyListenerPermissionsDialog(Activity activity) {
|
||||
new MaterialAlertDialogBuilder(activity)
|
||||
.setMessage(activity.getString(R.string.permission_notification_listener,
|
||||
activity.getString(R.string.app_name),
|
||||
activity.getString(R.string.ok)))
|
||||
.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
|
||||
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP_MR1)
|
||||
public void onClick(DialogInterface dialog, int id) {
|
||||
try {
|
||||
Intent intent;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
intent = new Intent(Settings.ACTION_NOTIFICATION_LISTENER_DETAIL_SETTINGS);
|
||||
intent.putExtra(Settings.EXTRA_NOTIFICATION_LISTENER_COMPONENT_NAME, new ComponentName(BuildConfig.APPLICATION_ID, NotificationListener.class.getName()).flattenToString());
|
||||
} else {
|
||||
intent = new Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS);
|
||||
}
|
||||
String showArgs = BuildConfig.APPLICATION_ID + "/" + NotificationListener.class.getName();
|
||||
intent.putExtra(":settings:fragment_args_key", showArgs);
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString(":settings:fragment_args_key", showArgs);
|
||||
intent.putExtra(":settings:show_fragment_args", bundle);
|
||||
activity.startActivity(intent);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
GB.toast(activity, "'Notification Listener Settings' activity not found", Toast.LENGTH_LONG, GB.ERROR);
|
||||
LOG.error("'Notification Listener Settings' activity not found");
|
||||
}
|
||||
}
|
||||
})
|
||||
.show();
|
||||
}
|
||||
|
||||
private static void showNotifyPolicyPermissionsDialog(Activity activity) {
|
||||
new MaterialAlertDialogBuilder(activity)
|
||||
.setMessage(activity.getString(R.string.permission_notification_policy_access,
|
||||
activity.getString(R.string.app_name),
|
||||
activity.getString(R.string.ok)))
|
||||
.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
public void onClick(DialogInterface dialog, int id) {
|
||||
try {
|
||||
activity.startActivity(new Intent(android.provider.Settings.ACTION_NOTIFICATION_POLICY_ACCESS_SETTINGS));
|
||||
} catch (ActivityNotFoundException e) {
|
||||
GB.toast(activity, "'Notification Policy' activity not found", Toast.LENGTH_LONG, GB.ERROR);
|
||||
LOG.error("'Notification Policy' activity not found");
|
||||
}
|
||||
}
|
||||
})
|
||||
.show();
|
||||
}
|
||||
|
||||
private static void showDisplayOverOthersPermissionsDialog(Activity activity) {
|
||||
new MaterialAlertDialogBuilder(activity)
|
||||
.setMessage(activity.getString(R.string.permission_display_over_other_apps,
|
||||
activity.getString(R.string.app_name),
|
||||
activity.getString(R.string.ok)))
|
||||
.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
|
||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
||||
public void onClick(DialogInterface dialog, int id) {
|
||||
Intent enableIntent = new Intent(
|
||||
android.provider.Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
|
||||
Uri.parse("package:" + BuildConfig.APPLICATION_ID)
|
||||
);
|
||||
activity.startActivity(enableIntent);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(R.string.dismiss, new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int id) {
|
||||
}
|
||||
})
|
||||
.show();
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.R)
|
||||
private static void showBackgroundLocationPermissionsDialog(Activity activity) {
|
||||
new MaterialAlertDialogBuilder(activity)
|
||||
.setMessage(activity.getString(R.string.permission_location,
|
||||
activity.getString(R.string.app_name),
|
||||
activity.getPackageManager().getBackgroundPermissionOptionLabel()))
|
||||
.setPositiveButton(R.string.ok, (dialog, id) -> {
|
||||
ActivityCompat.requestPermissions(activity, new String[]{Manifest.permission.ACCESS_BACKGROUND_LOCATION}, 0);
|
||||
})
|
||||
.show();
|
||||
}
|
||||
}
|
@ -29,6 +29,7 @@ import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
public class ArmenianTransliterator implements Transliterator {
|
||||
// Transliteration map ordered by priority
|
||||
@ -38,7 +39,7 @@ public class ArmenianTransliterator implements Transliterator {
|
||||
private static final Map<String, String> transliterateMap = new LinkedHashMap<String, String>() {
|
||||
{
|
||||
// Simple substitutions
|
||||
Map<String, String> simpleSubstitions = new HashMap<String, String>() {
|
||||
Map<String, String> simpleSubstitions = new HashMap<String, String>() {
|
||||
{
|
||||
put("ա","a");
|
||||
put("բ","b");
|
||||
@ -63,6 +64,7 @@ public class ArmenianTransliterator implements Transliterator {
|
||||
put("յ","y");
|
||||
put("ն","n");
|
||||
put("շ","sh");
|
||||
put("ո", "vo");
|
||||
put("չ","ch");
|
||||
put("պ","p");
|
||||
put("ջ","j");
|
||||
@ -77,68 +79,78 @@ public class ArmenianTransliterator implements Transliterator {
|
||||
put("օ","o");
|
||||
put("և","ev");
|
||||
put("ֆ","f");
|
||||
put("՝", "`");
|
||||
put("՞", "?");
|
||||
put("։", ":");
|
||||
put("․", ".");
|
||||
}
|
||||
};
|
||||
|
||||
// Capitalize existing simple substitutions here
|
||||
for (final Entry<String, String> entry : new ArrayList<Entry<String, String>>(simpleSubstitions.entrySet())) {
|
||||
String capitalKey = entry.getKey().toUpperCase();
|
||||
if (!capitalKey.equals(entry.getKey())) {
|
||||
simpleSubstitions.put(capitalKey, entry.getValue().toUpperCase());
|
||||
}
|
||||
}
|
||||
|
||||
// Letter + 'ու'
|
||||
char[] letterMapU = {
|
||||
'ա',
|
||||
'բ',
|
||||
'գ',
|
||||
'դ',
|
||||
'ե',
|
||||
'զ',
|
||||
'է',
|
||||
'ը',
|
||||
'թ',
|
||||
'ժ',
|
||||
'ի',
|
||||
'լ',
|
||||
'խ',
|
||||
'ծ',
|
||||
'կ',
|
||||
'հ',
|
||||
'ձ',
|
||||
'ղ',
|
||||
'ճ',
|
||||
'մ',
|
||||
'յ',
|
||||
'ն',
|
||||
'շ',
|
||||
'չ',
|
||||
'պ',
|
||||
'ջ',
|
||||
'ռ',
|
||||
'ս',
|
||||
'վ',
|
||||
'տ',
|
||||
'ր',
|
||||
'ց',
|
||||
'փ',
|
||||
'ք',
|
||||
'օ',
|
||||
'և',
|
||||
'ֆ',
|
||||
'ո',
|
||||
final String[] 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));
|
||||
for (final String letter : letterMapU) {
|
||||
final String capitalLetter = letter.toUpperCase();
|
||||
final String transliteratedLetter = Objects.requireNonNull(simpleSubstitions.get(letter), letter);
|
||||
final String transliteratedCapitalLetter = Objects.requireNonNull(simpleSubstitions.get(capitalLetter), capitalLetter);
|
||||
|
||||
put(Character.toString(letter) + "ու", transliteratedLetter + "u");
|
||||
put(Character.toString(capitalLetter) + "ու", transliteratedCapitalLetter + "u");
|
||||
put(letter + "ու", transliteratedLetter + "u");
|
||||
put(capitalLetter + "ու", transliteratedCapitalLetter + "u");
|
||||
|
||||
put(Character.toString(letter) + "ՈՒ", transliteratedLetter + "U");
|
||||
put(Character.toString(capitalLetter) + "ՈՒ", transliteratedCapitalLetter + "U");
|
||||
put(letter + "ՈՒ", transliteratedLetter + "U");
|
||||
put(capitalLetter + "ՈՒ", transliteratedCapitalLetter + "U");
|
||||
put(letter + "Ու", transliteratedLetter + "U");
|
||||
put(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(letter + "ոՒ", transliteratedLetter + "U");
|
||||
put(capitalLetter + "ոՒ", transliteratedCapitalLetter + "U");
|
||||
}
|
||||
|
||||
put("ու","u");
|
||||
@ -147,50 +159,51 @@ public class ArmenianTransliterator implements Transliterator {
|
||||
put("ՈՒ","U");
|
||||
|
||||
// Letter + 'ո'
|
||||
char[] letterMapVo = {
|
||||
'բ',
|
||||
'գ',
|
||||
'դ',
|
||||
'զ',
|
||||
'թ',
|
||||
'ժ',
|
||||
'լ',
|
||||
'խ',
|
||||
'ծ',
|
||||
'կ',
|
||||
'հ',
|
||||
'ձ',
|
||||
'ղ',
|
||||
'ճ',
|
||||
'մ',
|
||||
'յ',
|
||||
'ն',
|
||||
'շ',
|
||||
'չ',
|
||||
'պ',
|
||||
'ջ',
|
||||
'ռ',
|
||||
'ս',
|
||||
'վ',
|
||||
'տ',
|
||||
'ր',
|
||||
'ց',
|
||||
'փ',
|
||||
'ք',
|
||||
'և',
|
||||
'ֆ',
|
||||
final String[] letterMapVo = {
|
||||
"բ",
|
||||
"գ",
|
||||
"դ",
|
||||
"զ",
|
||||
"թ",
|
||||
"ժ",
|
||||
"լ",
|
||||
"խ",
|
||||
"ծ",
|
||||
"կ",
|
||||
"հ",
|
||||
"ձ",
|
||||
"ղ",
|
||||
"ճ",
|
||||
"մ",
|
||||
"յ",
|
||||
"ն",
|
||||
"շ",
|
||||
"ո", // ո + ո should be voo
|
||||
"չ",
|
||||
"պ",
|
||||
"ջ",
|
||||
"ռ",
|
||||
"ս",
|
||||
"վ",
|
||||
"տ",
|
||||
"ր",
|
||||
"ց",
|
||||
"փ",
|
||||
"ք",
|
||||
"և",
|
||||
"ֆ"
|
||||
};
|
||||
|
||||
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));
|
||||
for (String letter : letterMapVo) {
|
||||
String capitalLetter = letter.toUpperCase();
|
||||
final String transliteratedLetter = Objects.requireNonNull(simpleSubstitions.get(letter));
|
||||
final String transliteratedCapitalLetter = Objects.requireNonNull(simpleSubstitions.get(capitalLetter));
|
||||
|
||||
put(Character.toString(letter) + "ո", transliteratedLetter + "o");
|
||||
put(Character.toString(capitalLetter) + "ո", transliteratedCapitalLetter + "o");
|
||||
put(letter + "ո", transliteratedLetter + "o");
|
||||
put(capitalLetter + "ո", transliteratedCapitalLetter + "o");
|
||||
|
||||
put(Character.toString(letter) + "Ո", transliteratedLetter + "Օ");
|
||||
put(Character.toString(capitalLetter) + "Ո", transliteratedCapitalLetter + "Օ");
|
||||
put(letter + "Ո", transliteratedLetter + "Օ");
|
||||
put(capitalLetter + "Ո", transliteratedCapitalLetter + "Օ");
|
||||
}
|
||||
|
||||
put("ո","vo");
|
||||
@ -213,12 +226,11 @@ public class ArmenianTransliterator implements Transliterator {
|
||||
put(entry.getKey(), entry.getValue());
|
||||
put(entry.getKey().toUpperCase(), entry.getValue().toUpperCase());
|
||||
}
|
||||
|
||||
}};
|
||||
|
||||
private static final Map<String, Integer> transliterationPriorityMap = new HashMap<String, Integer>() {{
|
||||
int priority = 0;
|
||||
for( final String key : transliterateMap.keySet() ) {
|
||||
for (final String key : transliterateMap.keySet()) {
|
||||
put(key, priority++);
|
||||
}
|
||||
}};
|
||||
@ -227,7 +239,7 @@ public class ArmenianTransliterator implements Transliterator {
|
||||
private static final Trie transliterationTrie;
|
||||
static {
|
||||
final Trie.TrieBuilder builder = Trie.builder();
|
||||
for( final String key : ArmenianTransliterator.transliterateMap.keySet()) {
|
||||
for (final String key : ArmenianTransliterator.transliterateMap.keySet()) {
|
||||
builder.addKeyword(key);
|
||||
}
|
||||
transliterationTrie = builder.build();
|
||||
@ -235,12 +247,12 @@ public class ArmenianTransliterator implements Transliterator {
|
||||
|
||||
private static String ahoCorasick(final String text) {
|
||||
// Create a buffer sufficiently large that re-allocations are minimized.
|
||||
final StringBuilder sb = new StringBuilder( text.length() * 10 / 12 );
|
||||
final StringBuilder sb = new StringBuilder(text.length() * 10 / 12);
|
||||
|
||||
// The complexity of the Aho-Corasick algorithm O(N + L + Z)
|
||||
// Where N is the length of the text, L is the length of keywords and the Z is a number of matches.
|
||||
// This algorithm allows us to do fast substring search
|
||||
final List<Emit> emits = new ArrayList<Emit>(transliterationTrie.parseText( text ));
|
||||
final List<Emit> emits = new ArrayList<Emit>(transliterationTrie.parseText(text));
|
||||
|
||||
// Sort collection first by starting position, then by priority.
|
||||
Collections.sort(emits, new Comparator<Emit>() {
|
||||
@ -259,11 +271,11 @@ public class ArmenianTransliterator implements Transliterator {
|
||||
|
||||
int prevIndex = 0;
|
||||
|
||||
for( final Emit emit : emits ) {
|
||||
for (final Emit emit : emits) {
|
||||
final int matchIndex = emit.getStart();
|
||||
|
||||
// Skip if we already substituted this part
|
||||
if(matchIndex < prevIndex) {
|
||||
if (matchIndex < prevIndex) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -271,13 +283,13 @@ public class ArmenianTransliterator implements Transliterator {
|
||||
sb.append(text.substring(prevIndex, matchIndex));
|
||||
|
||||
// Substitute and append to the builder
|
||||
sb.append( ArmenianTransliterator.transliterateMap.get( emit.getKeyword() ) );
|
||||
sb.append(Objects.requireNonNull(ArmenianTransliterator.transliterateMap.get(emit.getKeyword())));
|
||||
|
||||
prevIndex = emit.getEnd() + 1;
|
||||
}
|
||||
|
||||
// Add the remainder of the string (contains no more matches).
|
||||
sb.append( text.substring( prevIndex ) );
|
||||
sb.append(text.substring(prevIndex));
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
15
app/src/main/res/drawable/ic_activity_archery.xml
Normal file
15
app/src/main/res/drawable/ic_activity_archery.xml
Normal file
@ -0,0 +1,15 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="25dp"
|
||||
android:height="25dp"
|
||||
android:tint="#7E7E7E"
|
||||
android:viewportWidth="25"
|
||||
android:viewportHeight="25">
|
||||
<path
|
||||
android:pathData="M12.5,2.75A9.75,9.75 0,0 0,2.75 12.5A9.75,9.75 0,0 0,12.5 22.25A9.75,9.75 0,0 0,22.25 12.5A9.75,9.75 0,0 0,19.963 6.229L19.43,6.762A9,9 0,0 1,21.5 12.5A9,9 0,0 1,12.5 21.5A9,9 0,0 1,3.5 12.5A9,9 0,0 1,12.5 3.5A9,9 0,0 1,18.381 5.689L18.912,5.158A9.75,9.75 0,0 0,12.5 2.75zM12.5,5.5A7,7 0,0 0,5.5 12.5A7,7 0,0 0,12.5 19.5A7,7 0,0 0,19.5 12.5A7,7 0,0 0,18.008 8.184L17.475,8.717A6.25,6.25 0,0 1,18.75 12.5A6.25,6.25 0,0 1,12.5 18.75A6.25,6.25 0,0 1,6.25 12.5A6.25,6.25 0,0 1,12.5 6.25A6.25,6.25 0,0 1,16.43 7.641L16.963,7.107A7,7 0,0 0,12.5 5.5zM12.5,8.25A4.25,4.25 0,0 0,8.25 12.5A4.25,4.25 0,0 0,12.5 16.75A4.25,4.25 0,0 0,16.75 12.5A4.25,4.25 0,0 0,16.041 10.15L15.496,10.695A3.5,3.5 0,0 1,16 12.5A3.5,3.5 0,0 1,12.5 16A3.5,3.5 0,0 1,9 12.5A3.5,3.5 0,0 1,12.5 9A3.5,3.5 0,0 1,14.465 9.605L15.002,9.068A4.25,4.25 0,0 0,12.5 8.25z"
|
||||
android:strokeWidth="1.0045"
|
||||
android:fillColor="#000000"/>
|
||||
<path
|
||||
android:pathData="m20.275,2.754 l0.239,1.175 -8.365,8.365 0.707,0.707 8.365,-8.365 1.175,0.239L24.169,3.102 22.994,2.863 23.108,2.749 22.401,2.042 22.287,2.156 22.048,0.981Z"
|
||||
android:strokeWidth="1.04456"
|
||||
android:fillColor="#000000"/>
|
||||
</vector>
|
15
app/src/main/res/drawable/ic_activity_billiard_pool.xml
Normal file
15
app/src/main/res/drawable/ic_activity_billiard_pool.xml
Normal file
@ -0,0 +1,15 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="25dp"
|
||||
android:height="25dp"
|
||||
android:tint="#7E7E7E"
|
||||
android:viewportWidth="25"
|
||||
android:viewportHeight="25">
|
||||
<path
|
||||
android:pathData="M12.5,4A8.5,8.5 0,0 0,4 12.5A8.5,8.5 0,0 0,12.5 21A8.5,8.5 0,0 0,21 12.5A8.5,8.5 0,0 0,12.5 4zM11,7A4,4 0,0 1,15 11A4,4 0,0 1,11 15A4,4 0,0 1,7 11A4,4 0,0 1,11 7z"
|
||||
android:strokeWidth="1.02129"
|
||||
android:fillColor="#000000"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="m8.846,8.891q0.49,-0.49 1.046,-0.56 0.556,-0.07 1.015,0.389 0.238,0.238 0.315,0.513 0.083,0.271 0.06,0.57 -0.029,0.294 -0.117,0.592 0.345,-0.113 0.682,-0.152 0.331,-0.044 0.634,0.04 0.308,0.079 0.579,0.35 0.497,0.497 0.457,1.124 -0.041,0.616 -0.599,1.173 -0.599,0.599 -1.199,0.646 -0.605,0.041 -1.108,-0.462 -0.271,-0.271 -0.365,-0.574 -0.089,-0.308 -0.056,-0.629 0.034,-0.321 0.137,-0.624 -0.437,0.105 -0.862,0.066 -0.425,-0.05 -0.79,-0.415 -0.304,-0.304 -0.362,-0.654 -0.064,-0.356 0.076,-0.717 0.14,-0.361 0.458,-0.679zM9.233,9.289q-0.287,0.287 -0.325,0.624 -0.039,0.326 0.238,0.602 0.204,0.204 0.431,0.254 0.232,0.044 0.488,-0.013 0.25,-0.062 0.526,-0.162 0.149,-0.393 0.161,-0.746 0.016,-0.359 -0.282,-0.657 -0.276,-0.276 -0.602,-0.238 -0.332,0.033 -0.634,0.336zM11.064,12.674q0.287,0.287 0.674,0.288 0.381,-0.005 0.782,-0.406 0.381,-0.381 0.391,-0.766 0.01,-0.397 -0.294,-0.701 -0.287,-0.287 -0.689,-0.262 -0.407,0.02 -0.905,0.231l-0.117,0.05q-0.189,0.499 -0.167,0.886 0.022,0.376 0.326,0.68z"
|
||||
android:strokeWidth="0.633"/>
|
||||
</vector>
|
11
app/src/main/res/drawable/ic_activity_bowling.xml
Normal file
11
app/src/main/res/drawable/ic_activity_bowling.xml
Normal file
@ -0,0 +1,11 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="25dp"
|
||||
android:height="25dp"
|
||||
android:tint="#7E7E7E"
|
||||
android:viewportWidth="25"
|
||||
android:viewportHeight="25">
|
||||
<path
|
||||
android:pathData="M12.5,4C7.806,4 4,7.806 4,12.5C4,17.194 7.806,21 12.5,21C17.194,21 21,17.194 21,12.5C21,7.806 17.194,4 12.5,4zM15.871,6.604A1.5,1.5 0,0 1,17.371 8.104A1.5,1.5 0,0 1,15.871 9.604A1.5,1.5 0,0 1,14.371 8.104A1.5,1.5 0,0 1,15.871 6.604zM18.018,10.33A1.5,1.5 0,0 1,19.518 11.83A1.5,1.5 0,0 1,18.018 13.33A1.5,1.5 0,0 1,16.518 11.83A1.5,1.5 0,0 1,18.018 10.33zM14.051,10.365A1.5,1.5 0,0 1,15.551 11.865A1.5,1.5 0,0 1,14.051 13.365A1.5,1.5 0,0 1,12.551 11.865A1.5,1.5 0,0 1,14.051 10.365z"
|
||||
android:strokeWidth="1.02129"
|
||||
android:fillColor="#000000"/>
|
||||
</vector>
|
15
app/src/main/res/drawable/ic_activity_curling.xml
Normal file
15
app/src/main/res/drawable/ic_activity_curling.xml
Normal file
@ -0,0 +1,15 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="25dp"
|
||||
android:height="25dp"
|
||||
android:tint="#7E7E7E"
|
||||
android:viewportWidth="25"
|
||||
android:viewportHeight="25">
|
||||
<path
|
||||
android:pathData="M7,12A4,4 0,0 0,3 16A4,4 0,0 0,7 20L17,20A4,4 0,0 0,21 16A4,4 0,0 0,17 12L7,12zM3.5,15L20.5,15L20.5,17L3.5,17L3.5,15z"
|
||||
android:strokeWidth="1.03322"
|
||||
android:fillColor="#000000"/>
|
||||
<path
|
||||
android:pathData="M9.202,7.542 L7.286,11.542h1.5L9.993,9.022v0.02H18.167v-1.5H10.702,9.993Z"
|
||||
android:strokeWidth="1.00035"
|
||||
android:fillColor="#000000"/>
|
||||
</vector>
|
14
app/src/main/res/drawable/ic_activity_frisbee.xml
Normal file
14
app/src/main/res/drawable/ic_activity_frisbee.xml
Normal file
@ -0,0 +1,14 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="25dp"
|
||||
android:height="25dp"
|
||||
android:tint="#7E7E7E"
|
||||
android:viewportWidth="25"
|
||||
android:viewportHeight="25">
|
||||
<path
|
||||
android:pathData="M12.756,8.485 L16.727,6.553 16.121,5.094 12.88,6.495 10.388,6.12c-0.3,-0.5 -0.84,-0.84 -1.46,-0.84 -0.18,0 -0.34,0.03 -0.5,0.08l-5.42,1.69v5.2h1.8V8.58l2.11,-0.66 -3.91,15.33h1.8l2.87,-8.11 2.33,3.11v5h1.8V16.84L9.318,12.3 10.351,8.294M11.008,5.05c1,0 1.8,-0.8 1.8,-1.8 0,-1 -0.8,-1.8 -1.8,-1.8 -1,0 -1.8,0.8 -1.8,1.8 0,1 0.8,1.8 1.8,1.8z"
|
||||
android:fillColor="#000000"/>
|
||||
<path
|
||||
android:pathData="M17.902,5.203a0.909,3.182 78.761,1 0,6.242 -1.24a0.909,3.182 78.761,1 0,-6.242 1.24z"
|
||||
android:strokeWidth="0.816497"
|
||||
android:fillColor="#000000"/>
|
||||
</vector>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user