Compare commits

...

9 Commits

Author SHA1 Message Date
Arjan Schrijver 94dba527b2 Add First Start screens with permissions screen 2024-04-26 23:24:07 +02:00
José Rebelo 408f4b75dd Serbian transliterator: Map Đ and đ 2024-04-25 18:09:25 +01:00
José Rebelo 31408394b4 Serbian transliterator: Map Č and č 2024-04-25 18:08:55 +01:00
José Rebelo 61af26d7ce Add Serbian transliterator
As discussed in #3727
2024-04-25 17:51:45 +01:00
José Rebelo 500e930237 Refactor location service
- Refactor the code from a static global instance to a lifecycle-aware
  service instantiated in the DeviceCommunicationService
- Fix number of devices reported in the notification
- Prevents leaks and properly stops when devices get disconnected
2024-04-25 17:08:53 +01:00
José Rebelo 3799ffb72c Zepp OS: Sync calendar event reminders 2024-04-25 15:58:57 +01:00
José Rebelo 13d6c49bb5 Xiaomi: Sync calendar event reminders 2024-04-25 15:00:48 +01:00
Vitaliy Tomin 67cf9b2f00 huawei: Add huawei account support (#3721)
* this feature allows to pair HarmonyOS devices without factory reset to
  GB and Huawei Health.

* huawei account has form of 17 digit string and could be retrived from
  logcat filtering by huid=

Reviewed-on: https://codeberg.org/Freeyourgadget/Gadgetbridge/pulls/3721
Co-authored-by: Vitaliy Tomin <highwaystar.ru@gmail.com>
Co-committed-by: Vitaliy Tomin <highwaystar.ru@gmail.com>
2024-04-25 12:19:00 +00:00
Daniele Gobbetti 173e2d29b0 Include Organizer and Reminders when reading calendar events
Also use the named column indexes instead of numeric ids when retrieving the contents to make it more clear and more robust in case further fields are added later.

Reminders are set as absolute timestamp.
2024-04-25 11:46:34 +02:00
59 changed files with 1733 additions and 799 deletions

View File

@ -142,6 +142,10 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".activities.welcome.WelcomeActivity"
android:label="Welcome"
android:parentActivityName=".activities.ControlCenterv2" />
<activity
android:name=".activities.SettingsActivity"
android:label="@string/title_activity_settings"

View File

@ -21,44 +21,27 @@ 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.AppCompatActivity;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.core.app.ActivityCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.content.ContextCompat;
import androidx.core.view.GravityCompat;
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;
@ -68,24 +51,20 @@ 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.HashSet;
import java.util.List;
import java.util.Locale;
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.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
@ -95,6 +74,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
@ -102,10 +82,6 @@ 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;
@ -135,12 +111,6 @@ public class ControlCenterv2 extends AppCompatActivity
case DeviceService.ACTION_REALTIME_SAMPLES:
handleRealtimeSample(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()) {
@ -282,79 +252,20 @@ 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)) {
// TODO: show (only) WelcomeFragmentPermissions here
}
}
/* 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())) {
@ -448,6 +359,10 @@ public class ControlCenterv2 extends AppCompatActivity
return new GBChangeLog(this, css);
}
private void launchWelcomeActivity() {
startActivity(new Intent(this, WelcomeActivity.class));
}
private void launchDiscoveryActivity() {
startActivity(new Intent(this, DiscoveryActivityV2.class));
}
@ -464,145 +379,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;
@ -610,137 +386,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);

View File

@ -106,7 +106,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationManager;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationService;
import nodomain.freeyourgadget.gadgetbridge.externalevents.opentracks.OpenTracksContentObserver;
import nodomain.freeyourgadget.gadgetbridge.externalevents.opentracks.OpenTracksController;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
@ -659,7 +659,7 @@ public class DebugActivity extends AbstractGBActivity {
stopPhoneGpsLocationListener.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
GBLocationManager.stopAll(getBaseContext());
GBLocationService.stop(DebugActivity.this, null);
}
});

View File

@ -65,6 +65,7 @@ import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.charts.ChartsPreferencesActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.discovery.DiscoveryPairingPreferenceActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.welcome.WelcomeActivity;
import nodomain.freeyourgadget.gadgetbridge.database.PeriodicExporter;
import nodomain.freeyourgadget.gadgetbridge.devices.hplus.HPlusSettingsActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandPreferencesActivity;
@ -477,6 +478,14 @@ public class SettingsActivity extends AbstractSettingsActivityV2 {
});
}
pref = findPreference("pref_show_first_run_screen");
if (pref != null) {
pref.setOnPreferenceClickListener(preference -> {
Intent enableIntent = new Intent(requireContext(), WelcomeActivity.class);
startActivity(enableIntent);
return true;
});
}
pref = findPreference("pref_discovery_pairing");
if (pref != null) {
pref.setOnPreferenceClickListener(preference -> {

View File

@ -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;
}
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,58 @@
/* 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.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 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;
}
}

View File

@ -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 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);
return inflater.inflate(R.layout.fragment_welcome_intro, container, false);
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,127 @@
/* 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.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
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.util.PermissionsUtils;
public class WelcomeFragmentPermissions extends Fragment {
private static final Logger LOG = LoggerFactory.getLogger(WelcomeFragmentPermissions.class);
private RecyclerView permissionsListView;
private PermissionAdapter permissionAdapter;
@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);
Button requestAllButton = view.findViewById(R.id.button_request_all);
requestAllButton.setOnClickListener(v -> PermissionsUtils.requestAllPermissions(requireActivity()));
// 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();
}
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.setOnClickListener(view -> {
PermissionsUtils.requestPermission(requireActivity(), permissionData.getPermission());
});
}
}
@Override
public int getItemCount() {
return permissionList.size();
}
}
}

View File

@ -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);
}
}

View File

@ -30,6 +30,7 @@ import java.util.Collections;
import java.util.List;
import de.greenrobot.dao.query.QueryBuilder;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettings;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
@ -83,6 +84,11 @@ public abstract class HuaweiBRCoordinator extends AbstractBLClassicDeviceCoordin
return huaweiCoordinator.getSupportedLanguageSettings(device);
}
@Override
public int[] getSupportedDeviceSpecificAuthenticationSettings() {
return new int[]{R.xml.devicesettings_huawei_account};
}
@Override
public int getBondingStyle(){
return BONDING_STYLE_ASK;

View File

@ -71,6 +71,7 @@ public final class HuaweiConstants {
public static final String PREF_HUAWEI_ADDRESS = "huawei_address";
public static final String PREF_HUAWEI_WORKMODE = "workmode";
public static final String PREF_HUAWEI_TRUSLEEP = "trusleep";
public static final String PREF_HUAWEI_ACCOUNT = "huawei_account";
public static final String PREF_HUAWEI_DND_LIFT_WRIST_TYPE = "dnd_lift_wrist_type"; // SharedPref for 0x01 0x1D
public static final String PREF_HUAWEI_DEBUG_REQUEST = "debug_huawei_request";

View File

@ -30,6 +30,7 @@ import java.util.Collections;
import java.util.List;
import de.greenrobot.dao.query.QueryBuilder;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettings;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
import nodomain.freeyourgadget.gadgetbridge.GBException;
@ -82,7 +83,12 @@ public abstract class HuaweiLECoordinator extends AbstractBLEDeviceCoordinator i
public String[] getSupportedLanguageSettings(GBDevice device) {
return huaweiCoordinator.getSupportedLanguageSettings(device);
}
@Override
public int[] getSupportedDeviceSpecificAuthenticationSettings() {
return new int[]{R.xml.devicesettings_huawei_account};
}
@Override
public int getBondingStyle(){
return BONDING_STYLE_NONE;

View File

@ -26,15 +26,18 @@ public class AccountRelated {
public static final byte id = 0x01;
public static class Request extends HuaweiPacket {
public Request (ParamsProvider paramsProvider) {
public Request (ParamsProvider paramsProvider, String account) {
super(paramsProvider);
this.serviceId = AccountRelated.id;
this.commandId = id;
this.tlv = new HuaweiTLV()
.put(0x01);
this.tlv = new HuaweiTLV();
if (account.length() > 0) {
tlv.put(0x01, account);
} else {
tlv.put(0x01);
}
this.complete = true;
}
}
@ -50,14 +53,19 @@ public class AccountRelated {
public static final byte id = 0x05;
public static class Request extends HuaweiPacket {
public Request (ParamsProvider paramsProvider, boolean accountPairingOptimization) {
public Request (ParamsProvider paramsProvider, boolean accountPairingOptimization, String account) {
super(paramsProvider);
this.serviceId = AccountRelated.id;
this.commandId = id;
this.tlv = new HuaweiTLV()
.put(0x01, (byte)0x00);
this.tlv = new HuaweiTLV();
if (account.length() > 0) {
tlv.put(0x01, account);
} else {
tlv.put(0x01, (byte)0x00);
}
if (accountPairingOptimization) {
this.tlv.put(0x03, (byte)0x01);
}

View File

@ -26,6 +26,7 @@ import android.widget.Toast;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Enumeration;
import java.util.GregorianCalendar;
@ -188,6 +189,7 @@ public class CalendarReceiver extends BroadcastReceiver {
calendarEventSpec.id = i;
calendarEventSpec.title = calendarEvent.getTitle();
calendarEventSpec.allDay = calendarEvent.isAllDay();
calendarEventSpec.reminders = new ArrayList<>(calendarEvent.getRemindersAbsoluteTs());
calendarEventSpec.timestamp = calendarEvent.getBeginSeconds();
calendarEventSpec.durationInSeconds = calendarEvent.getDurationSeconds(); //FIXME: leads to problems right now
if (calendarEvent.isAllDay()) {

View File

@ -21,10 +21,14 @@ import android.location.LocationListener;
import android.os.Bundle;
import android.os.SystemClock;
import androidx.annotation.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.devices.EventHandler;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
/**
* An implementation of a {@link LocationListener} that forwards the location updates to the
@ -33,18 +37,18 @@ import nodomain.freeyourgadget.gadgetbridge.devices.EventHandler;
public class GBLocationListener implements LocationListener {
private static final Logger LOG = LoggerFactory.getLogger(GBLocationListener.class);
private final EventHandler eventHandler;
private final GBDevice device;
private Location previousLocation;
// divide by 3.6 to get km/h to m/s
private static final double SPEED_THRESHOLD = 1.0 / 3.6;
public GBLocationListener(final EventHandler eventHandler) {
this.eventHandler = eventHandler;
public GBLocationListener(final GBDevice device) {
this.device = device;
}
@Override
public void onLocationChanged(final Location location) {
public void onLocationChanged(@NonNull final Location location) {
LOG.info("Location changed: {}", location);
// Correct the location time
@ -61,16 +65,16 @@ public class GBLocationListener implements LocationListener {
previousLocation = location;
eventHandler.onSetGpsLocation(location);
GBApplication.deviceService(device).onSetGpsLocation(location);
}
@Override
public void onProviderDisabled(final String provider) {
public void onProviderDisabled(@NonNull final String provider) {
LOG.info("onProviderDisabled: {}", provider);
}
@Override
public void onProviderEnabled(final String provider) {
public void onProviderEnabled(@NonNull final String provider) {
LOG.info("onProviderDisabled: {}", provider);
}

View File

@ -1,140 +0,0 @@
/* Copyright (C) 2022-2024 halemmerich, José Rebelo, LukasEdl, Martin Boonk
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.externalevents.gps;
import android.content.Context;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.os.Looper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import nodomain.freeyourgadget.gadgetbridge.devices.EventHandler;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
/**
* A static location manager, which keeps track of what providers are currently running. A notification is kept
* while there is at least one provider running.
*/
public class GBLocationManager {
private static final Logger LOG = LoggerFactory.getLogger(GBLocationManager.class);
/**
* The current number of running listeners.
*/
private static Map<EventHandler, Map<LocationProviderType, AbstractLocationProvider>> providers = new HashMap<>();
public static void start(final Context context, final EventHandler eventHandler) {
GBLocationManager.start(context, eventHandler, LocationProviderType.GPS, null);
}
public static void start(final Context context, final EventHandler eventHandler, final LocationProviderType providerType, Integer updateInterval) {
LOG.info("Starting");
if (providers.containsKey(eventHandler) && providers.get(eventHandler).containsKey(providerType)) {
LOG.warn("EventHandler already registered");
return;
}
GB.createGpsNotification(context, providers.size());
final GBLocationListener locationListener = new GBLocationListener(eventHandler);
final AbstractLocationProvider locationProvider;
switch (providerType) {
case GPS:
LOG.info("Using gps location provider");
locationProvider = new PhoneGpsLocationProvider(locationListener);
break;
case NETWORK:
LOG.info("Using network location provider");
locationProvider = new PhoneNetworkLocationProvider(locationListener);
break;
default:
LOG.info("Using default location provider: GPS");
locationProvider = new PhoneGpsLocationProvider(locationListener);
}
if (updateInterval != null) {
locationProvider.start(context, updateInterval);
} else {
locationProvider.start(context);
}
if (providers.containsKey(eventHandler)) {
providers.get(eventHandler).put(providerType, locationProvider);
} else {
Map<LocationProviderType, AbstractLocationProvider> providerMap = new HashMap<>();
providerMap.put(providerType, locationProvider);
providers.put(eventHandler, providerMap);
}
}
public static void stop(final Context context, final EventHandler eventHandler) {
GBLocationManager.stop(context, eventHandler, null);
}
public static void stop(final Context context, final EventHandler eventHandler, final LocationProviderType gpsType) {
if (!providers.containsKey(eventHandler)) return;
Map<LocationProviderType, AbstractLocationProvider> providerMap = providers.get(eventHandler);
if (gpsType == null) {
Set<LocationProviderType> toBeRemoved = new HashSet<>();
for (LocationProviderType providerType: providerMap.keySet()) {
stopProvider(context, providerMap.get(providerType));
toBeRemoved.add(providerType);
}
for (final LocationProviderType providerType : toBeRemoved) {
providerMap.remove(providerType);
}
} else {
stopProvider(context, providerMap.get(gpsType));
providerMap.remove(gpsType);
}
LOG.debug("Remaining providers: " + providers.size());
if (providers.get(eventHandler).size() == 0)
providers.remove(eventHandler);
updateNotification(context);
}
private static void updateNotification(final Context context){
if (!providers.isEmpty()) {
GB.createGpsNotification(context, providers.size());
} else {
GB.removeGpsNotification(context);
}
}
private static void stopProvider(final Context context, AbstractLocationProvider locationProvider) {
if (locationProvider != null) {
locationProvider.stop(context);
}
}
public static void stopAll(final Context context) {
for (EventHandler eventHandler : providers.keySet()) {
stop(context, eventHandler);
}
}
}

View File

@ -22,35 +22,30 @@ import android.location.LocationListener;
/**
* An abstract location provider, which periodically sends a location update to the provided {@link LocationListener}.
*/
public abstract class AbstractLocationProvider {
public abstract class GBLocationProvider {
private final Context context;
private final LocationListener locationListener;
public AbstractLocationProvider(final LocationListener locationListener) {
public GBLocationProvider(final Context context, final LocationListener locationListener) {
this.context = context;
this.locationListener = locationListener;
}
protected final LocationListener getLocationListener() {
public final Context getContext() {
return this.context;
}
public final LocationListener getLocationListener() {
return this.locationListener;
}
/**
* Start sending periodic location updates.
*
* @param context the {@link Context}.
*/
abstract void start(final Context context);
/**
* Start sending periodic location updates.
*
* @param context the {@link Context}.
*/
abstract void start(final Context context, final int interval);
public abstract void start(final int interval);
/**
* Stop sending periodic location updates.
*
* @param context the {@link Context}.
*/
abstract void stop(final Context context);
public abstract void stop();
}

View File

@ -0,0 +1,47 @@
/* Copyright (C) 2022-2024 LukasEdl
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.externalevents.gps;
import android.content.Context;
import android.location.LocationManager;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.providers.MockLocationProvider;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.providers.PhoneLocationProvider;
public enum GBLocationProviderType {
GPS {
@Override
public GBLocationProvider newInstance(final Context context, final GBLocationListener locationListener) {
return new PhoneLocationProvider(context, locationListener, LocationManager.GPS_PROVIDER);
}
},
NETWORK {
@Override
public GBLocationProvider newInstance(final Context context, final GBLocationListener locationListener) {
return new PhoneLocationProvider(context, locationListener, LocationManager.NETWORK_PROVIDER);
}
},
MOCK {
@Override
public GBLocationProvider newInstance(final Context context, final GBLocationListener locationListener) {
return new MockLocationProvider(context, locationListener);
}
},
;
public abstract GBLocationProvider newInstance(final Context context, final GBLocationListener locationListener);
}

View File

@ -0,0 +1,184 @@
/* Copyright (C) 2022-2024 halemmerich, José Rebelo, LukasEdl, Martin Boonk
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.externalevents.gps;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.PendingIntentUtils;
/**
* A static location manager, which keeps track of what providers are currently running. A notification is kept
* while there is at least one provider running.
*/
public class GBLocationService extends BroadcastReceiver {
private static final Logger LOG = LoggerFactory.getLogger(GBLocationService.class);
public static final String ACTION_START = "GBLocationService.START";
public static final String ACTION_STOP = "GBLocationService.STOP";
public static final String ACTION_STOP_ALL = "GBLocationService.STOP_ALL";
public static final String EXTRA_TYPE = "extra_type";
public static final String EXTRA_INTERVAL = "extra_interval";
private final Context context;
private final Map<GBDevice, List<GBLocationProvider>> providersByDevice = new HashMap<>();
public GBLocationService(final Context context) {
this.context = context;
}
@Override
public void onReceive(final Context context, final Intent intent) {
if (intent.getAction() == null) {
LOG.warn("Action is null");
return;
}
final GBDevice device = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
switch (intent.getAction()) {
case ACTION_START:
if (device == null) {
LOG.error("Device is null for {}", intent.getAction());
return;
}
final GBLocationProviderType providerType = GBLocationProviderType.valueOf(
intent.hasExtra(EXTRA_TYPE) ? intent.getStringExtra(EXTRA_TYPE) : "GPS"
);
final int updateInterval = intent.getIntExtra(EXTRA_INTERVAL, 1000);
LOG.debug("Starting location provider {} for {}", providerType, device.getAliasOrName());
if (!providersByDevice.containsKey(device)) {
providersByDevice.put(device, new ArrayList<>());
}
updateNotification();
final List<GBLocationProvider> existingProviders = providersByDevice.get(device);
final GBLocationListener locationListener = new GBLocationListener(device);
final GBLocationProvider locationProvider = providerType.newInstance(context, locationListener);
locationProvider.start(updateInterval);
Objects.requireNonNull(existingProviders).add(locationProvider);
return;
case ACTION_STOP:
if (device != null) {
stopDevice(device);
updateNotification();
} else {
stopAll();
}
return;
case ACTION_STOP_ALL:
stopAll();
return;
default:
LOG.warn("Unknown action {}", intent.getAction());
}
}
public void stopDevice(final GBDevice device) {
LOG.debug("Stopping location providers for {}", device.getAliasOrName());
final List<GBLocationProvider> providers = providersByDevice.remove(device);
if (providers != null) {
for (final GBLocationProvider provider : providers) {
provider.stop();
}
}
}
public IntentFilter buildFilter() {
final IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(ACTION_START);
intentFilter.addAction(ACTION_STOP);
return intentFilter;
}
public void stopAll() {
LOG.info("Stopping location service for all devices");
final List<GBDevice> gbDevices = new ArrayList<>(providersByDevice.keySet());
for (GBDevice d : gbDevices) {
stopDevice(d);
}
updateNotification();
}
public static void start(final Context context,
@NonNull final GBDevice device,
final GBLocationProviderType providerType,
final int updateInterval) {
final Intent intent = new Intent(ACTION_START);
intent.putExtra(GBDevice.EXTRA_DEVICE, device);
intent.putExtra(EXTRA_TYPE, providerType.name());
intent.putExtra(EXTRA_INTERVAL, updateInterval);
LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
}
public static void stop(final Context context, @Nullable final GBDevice device) {
final Intent intent = new Intent(ACTION_STOP);
intent.putExtra(GBDevice.EXTRA_DEVICE, device);
LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
}
private void updateNotification() {
if (!providersByDevice.isEmpty()) {
final Intent notificationIntent = new Intent(context, GBLocationService.class);
notificationIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
final PendingIntent pendingIntent = PendingIntentUtils.getActivity(context, 0, notificationIntent, 0, false);
final NotificationCompat.Builder nb = new NotificationCompat.Builder(context, GB.NOTIFICATION_CHANNEL_ID_GPS)
.setTicker(context.getString(R.string.notification_gps_title))
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setContentTitle(context.getString(R.string.notification_gps_title))
.setContentText(context.getString(R.string.notification_gps_text, providersByDevice.size()))
.setContentIntent(pendingIntent)
.setSmallIcon(R.drawable.ic_gps_location)
.setOngoing(true);
GB.notify(GB.NOTIFICATION_ID_GPS, nb.build(), context);
} else {
GB.removeNotification(GB.NOTIFICATION_ID_GPS, context);
}
}
}

View File

@ -1,22 +0,0 @@
/* Copyright (C) 2022-2024 LukasEdl
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.externalevents.gps;
public enum LocationProviderType {
GPS,
NETWORK,
}

View File

@ -1,80 +0,0 @@
/* Copyright (C) 2022-2024 Lukas, LukasEdl
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.externalevents.gps;
import android.Manifest;
import android.content.Context;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Looper;
import android.widget.Toast;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
/**
* A location provider that uses the phone GPS, using {@link LocationManager}.
*/
public class PhoneNetworkLocationProvider extends AbstractLocationProvider {
private static final Logger LOG = LoggerFactory.getLogger(PhoneNetworkLocationProvider.class);
private static final int INTERVAL_MIN_TIME = 1000;
private static final int INTERVAL_MIN_DISTANCE = 0;
public PhoneNetworkLocationProvider(LocationListener locationListener) {
super(locationListener);
}
@Override
void start(final Context context) {
start(context, INTERVAL_MIN_TIME);
}
@Override
void start(Context context, int interval) {
LOG.info("Starting phone network location provider");
if (!GB.checkPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) && !GB.checkPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION)) {
GB.toast("Location permission not granted", Toast.LENGTH_SHORT, GB.ERROR);
return;
}
final LocationManager locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
locationManager.removeUpdates(getLocationListener());
locationManager.requestLocationUpdates(
LocationManager.NETWORK_PROVIDER,
interval,
INTERVAL_MIN_DISTANCE,
getLocationListener(),
Looper.getMainLooper()
);
final Location lastKnownLocation = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
LOG.debug("Last known network location: {}", lastKnownLocation);
}
@Override
void stop(final Context context) {
LOG.info("Stopping phone network location provider");
final LocationManager locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
locationManager.removeUpdates(getLocationListener());
}
}

View File

@ -14,7 +14,7 @@
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.externalevents.gps;
package nodomain.freeyourgadget.gadgetbridge.externalevents.gps.providers;
import android.content.Context;
import android.location.Location;
@ -26,13 +26,14 @@ import android.os.SystemClock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationProvider;
import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.webview.CurrentPosition;
/**
* A mock location provider which keeps updating the location at a constant speed, starting from the
* last known location. Useful for local tests.
*/
public class MockLocationProvider extends AbstractLocationProvider {
public class MockLocationProvider extends GBLocationProvider {
private static final Logger LOG = LoggerFactory.getLogger(MockLocationProvider.class);
private Location previousLocation = new CurrentPosition().getLastKnownLocation();
@ -40,12 +41,12 @@ public class MockLocationProvider extends AbstractLocationProvider {
/**
* Interval between location updates, in milliseconds.
*/
private final int interval = 1000;
private static final int DEFAULT_INTERVAL = 1000;
/**
* Difference between location updates, in degrees.
*/
private final float coordDiff = 0.0002f;
private static final float COORD_DIFF = 0.0002f;
/**
* Whether the handler is running.
@ -54,50 +55,40 @@ public class MockLocationProvider extends AbstractLocationProvider {
private final Handler handler = new Handler(Looper.getMainLooper());
private final Runnable locationUpdateRunnable = new Runnable() {
@Override
public void run() {
if (!running) {
return;
}
final Location newLocation = new Location(previousLocation);
newLocation.setLatitude(previousLocation.getLatitude() + coordDiff);
newLocation.setTime(System.currentTimeMillis());
newLocation.setElapsedRealtimeNanos(SystemClock.elapsedRealtimeNanos());
getLocationListener().onLocationChanged(newLocation);
previousLocation = newLocation;
if (running) {
handler.postDelayed(this, interval);
}
}
};
public MockLocationProvider(LocationListener locationListener) {
super(locationListener);
public MockLocationProvider(final Context context, final LocationListener locationListener) {
super(context, locationListener);
}
@Override
void start(final Context context) {
public void start(final int interval) {
LOG.info("Starting mock location provider");
running = true;
handler.postDelayed(locationUpdateRunnable, interval);
handler.postDelayed(new Runnable() {
@Override
public void run() {
if (!running) {
return;
}
final Location newLocation = new Location(previousLocation);
newLocation.setLatitude(previousLocation.getLatitude() + COORD_DIFF);
newLocation.setTime(System.currentTimeMillis());
newLocation.setElapsedRealtimeNanos(SystemClock.elapsedRealtimeNanos());
getLocationListener().onLocationChanged(newLocation);
previousLocation = newLocation;
if (running) {
handler.postDelayed(this, interval);
}
}
}, interval > 0 ? interval : DEFAULT_INTERVAL);
}
@Override
void start(final Context context, int minInterval) {
LOG.info("Starting mock location provider");
running = true;
handler.postDelayed(locationUpdateRunnable, interval);
}
@Override
void stop(final Context context) {
public void stop() {
LOG.info("Stopping mock location provider");
running = false;

View File

@ -14,7 +14,7 @@
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.externalevents.gps;
package nodomain.freeyourgadget.gadgetbridge.externalevents.gps.providers;
import android.Manifest;
import android.content.Context;
@ -27,43 +27,38 @@ import android.widget.Toast;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationProvider;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
/**
* A location provider that uses the phone GPS, using {@link LocationManager}.
*/
public class PhoneGpsLocationProvider extends AbstractLocationProvider {
private static final Logger LOG = LoggerFactory.getLogger(PhoneGpsLocationProvider.class);
public class PhoneLocationProvider extends GBLocationProvider {
private static final Logger LOG = LoggerFactory.getLogger(PhoneLocationProvider.class);
private final String provider;
private static final int INTERVAL_MIN_TIME = 1000;
private static final int INTERVAL_MIN_DISTANCE = 0;
public PhoneGpsLocationProvider(LocationListener locationListener) {
super(locationListener);
}
public PhoneGpsLocationProvider(LocationListener locationListener, int intervalTime) {
super(locationListener);
public PhoneLocationProvider(final Context context, final LocationListener locationListener, final String provider) {
super(context, locationListener);
this.provider = provider;
}
@Override
void start(final Context context) {
start(context, INTERVAL_MIN_TIME);
}
@Override
void start(Context context, int interval) {
public void start(final int interval) {
LOG.info("Starting phone gps location provider");
if (!GB.checkPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) && !GB.checkPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION)) {
if (!GB.checkPermission(getContext(), Manifest.permission.ACCESS_FINE_LOCATION) && !GB.checkPermission(getContext(), Manifest.permission.ACCESS_COARSE_LOCATION)) {
GB.toast("Location permission not granted", Toast.LENGTH_SHORT, GB.ERROR);
return;
}
final LocationManager locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
final LocationManager locationManager = (LocationManager) getContext().getSystemService(Context.LOCATION_SERVICE);
locationManager.removeUpdates(getLocationListener());
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER,
interval,
provider,
interval > 0 ? interval : 1_000,
INTERVAL_MIN_DISTANCE,
getLocationListener(),
Looper.getMainLooper()
@ -74,10 +69,10 @@ public class PhoneGpsLocationProvider extends AbstractLocationProvider {
}
@Override
void stop(final Context context) {
public void stop() {
LOG.info("Stopping phone gps location provider");
final LocationManager locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
final LocationManager locationManager = (LocationManager) getContext().getSystemService(Context.LOCATION_SERVICE);
locationManager.removeUpdates(getLocationListener());
}
}

View File

@ -451,6 +451,7 @@ public class GBDeviceService implements DeviceService {
.putExtra(EXTRA_CALENDAREVENT_TIMESTAMP, calendarEventSpec.timestamp)
.putExtra(EXTRA_CALENDAREVENT_DURATION, calendarEventSpec.durationInSeconds)
.putExtra(EXTRA_CALENDAREVENT_ALLDAY, calendarEventSpec.allDay)
.putExtra(EXTRA_CALENDAREVENT_REMINDERS, calendarEventSpec.reminders)
.putExtra(EXTRA_CALENDAREVENT_TITLE, calendarEventSpec.title)
.putExtra(EXTRA_CALENDAREVENT_DESCRIPTION, calendarEventSpec.description)
.putExtra(EXTRA_CALENDAREVENT_CALNAME, calendarEventSpec.calName)

View File

@ -17,6 +17,8 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.model;
import java.util.ArrayList;
public class CalendarEventSpec {
public static final byte TYPE_UNKNOWN = 0;
public static final byte TYPE_SUNRISE = 1;
@ -32,4 +34,5 @@ public class CalendarEventSpec {
public String calName;
public int color;
public boolean allDay;
public ArrayList<Long> reminders; // unix epoch millis
}

View File

@ -162,6 +162,7 @@ public interface DeviceService extends EventHandler {
String EXTRA_CALENDAREVENT_TIMESTAMP = "calendarevent_timestamp";
String EXTRA_CALENDAREVENT_DURATION = "calendarevent_duration";
String EXTRA_CALENDAREVENT_ALLDAY = "calendarevent_allday";
String EXTRA_CALENDAREVENT_REMINDERS = "calendarevent_reminders";
String EXTRA_CALENDAREVENT_TITLE = "calendarevent_title";
String EXTRA_CALENDAREVENT_DESCRIPTION = "calendarevent_description";
String EXTRA_CALENDAREVENT_LOCATION = "calendarevent_location";

View File

@ -82,7 +82,7 @@ import nodomain.freeyourgadget.gadgetbridge.externalevents.SMSReceiver;
import nodomain.freeyourgadget.gadgetbridge.externalevents.SilentModeReceiver;
import nodomain.freeyourgadget.gadgetbridge.externalevents.TimeChangeReceiver;
import nodomain.freeyourgadget.gadgetbridge.externalevents.TinyWeatherForecastGermanyReceiver;
import nodomain.freeyourgadget.gadgetbridge.externalevents.sleepasandroid.SleepAsAndroidAction;
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.GBDeviceService;
@ -140,7 +140,7 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
}
}
private class FeatureSet{
private static class FeatureSet {
private boolean supportsWeather = false;
private boolean supportsActivityDataFetching = false;
private boolean supportsCalendarEvents = false;
@ -256,7 +256,7 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
private AutoConnectIntervalReceiver mAutoConnectInvervalReceiver = null;
private AlarmReceiver mAlarmReceiver = null;
private List<CalendarReceiver> mCalendarReceiver = new ArrayList<>();
private final List<CalendarReceiver> mCalendarReceiver = new ArrayList<>();
private CMWeatherReceiver mCMWeatherReceiver = null;
private LineageOsWeatherReceiver mLineageOsWeatherReceiver = null;
private TinyWeatherForecastGermanyReceiver mTinyWeatherForecastGermanyReceiver = null;
@ -264,6 +264,7 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
private OmniJawsObserver mOmniJawsObserver = null;
private final DeviceSettingsReceiver deviceSettingsReceiver = new DeviceSettingsReceiver();
private final IntentApiReceiver intentApiReceiver = new IntentApiReceiver();
private GBLocationService locationService = null;
private OsmandEventReceiver mOsmandAidlHelper = null;
@ -860,6 +861,7 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
calendarEventSpec.timestamp = intent.getIntExtra(EXTRA_CALENDAREVENT_TIMESTAMP, -1);
calendarEventSpec.durationInSeconds = intent.getIntExtra(EXTRA_CALENDAREVENT_DURATION, -1);
calendarEventSpec.allDay = intent.getBooleanExtra(EXTRA_CALENDAREVENT_ALLDAY, false);
calendarEventSpec.reminders = (ArrayList<Long>) intent.getSerializableExtra(EXTRA_CALENDAREVENT_REMINDERS);
calendarEventSpec.title = intent.getStringExtra(EXTRA_CALENDAREVENT_TITLE);
calendarEventSpec.description = intent.getStringExtra(EXTRA_CALENDAREVENT_DESCRIPTION);
calendarEventSpec.location = intent.getStringExtra(EXTRA_CALENDAREVENT_LOCATION);
@ -1342,6 +1344,11 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
registerReceiver(mSilentModeReceiver, filter);
}
if (locationService == null) {
locationService = new GBLocationService(this);
LocalBroadcastManager.getInstance(this).registerReceiver(locationService, locationService.buildFilter());
}
if (mOsmandAidlHelper == null && features.supportsNavigation()) {
mOsmandAidlHelper = new OsmandEventReceiver(this.getApplication());
}
@ -1424,6 +1431,11 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
unregisterReceiver(mSilentModeReceiver);
mSilentModeReceiver = null;
}
if (locationService != null) {
LocalBroadcastManager.getInstance(this).unregisterReceiver(locationService);
locationService.stopAll();
locationService = null;
}
if (mCMWeatherReceiver != null) {
unregisterReceiver(mCMWeatherReceiver);
mCMWeatherReceiver = null;

View File

@ -120,8 +120,8 @@ import nodomain.freeyourgadget.gadgetbridge.entities.CalendarSyncState;
import nodomain.freeyourgadget.gadgetbridge.entities.CalendarSyncStateDao;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.externalevents.CalendarReceiver;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationManager;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.LocationProviderType;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationService;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationProviderType;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
@ -215,7 +215,7 @@ public class BangleJSDeviceSupport extends AbstractBTLEDeviceSupport {
if (!gpsUpdateSetup)
return;
LOG.info("Stop location updates");
GBLocationManager.stop(getContext(), this);
GBLocationService.stop(getContext(), getDevice());
gpsUpdateSetup = false;
}
@ -1140,14 +1140,14 @@ public class BangleJSDeviceSupport extends AbstractBTLEDeviceSupport {
LOG.info("Using combined GPS and NETWORK based location: " + onlyUseNetworkGPS);
if (!onlyUseNetworkGPS) {
try {
GBLocationManager.start(getContext(), this, LocationProviderType.GPS, intervalLength);
GBLocationService.start(getContext(), getDevice(), GBLocationProviderType.GPS, intervalLength);
} catch (IllegalArgumentException e) {
LOG.warn("GPS provider could not be started", e);
}
}
try {
GBLocationManager.start(getContext(), this, LocationProviderType.NETWORK, intervalLength);
GBLocationService.start(getContext(), getDevice(), GBLocationProviderType.NETWORK, intervalLength);
} catch (IllegalArgumentException e) {
LOG.warn("NETWORK provider could not be started", e);
}

View File

@ -30,7 +30,6 @@ import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.location.Location;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.widget.Toast;
@ -117,7 +116,8 @@ import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.MiBandActivitySample;
import nodomain.freeyourgadget.gadgetbridge.entities.User;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationManager;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationProviderType;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationService;
import nodomain.freeyourgadget.gadgetbridge.externalevents.opentracks.OpenTracksController;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice.State;
@ -2010,7 +2010,7 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport implements
if (sendGpsToBand) {
lastPhoneGpsSent = 0;
sendPhoneGps(HuamiPhoneGpsStatus.SEARCHING, null);
GBLocationManager.start(getContext(), this);
GBLocationService.start(getContext(), getDevice(), GBLocationProviderType.GPS, 1000);
} else {
sendPhoneGps(HuamiPhoneGpsStatus.DISABLED, null);
}
@ -2030,7 +2030,7 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport implements
protected void onWorkoutEnd() {
final boolean startOnPhone = HuamiCoordinator.getWorkoutStartOnPhone(getDevice().getAddress());
GBLocationManager.stop(getContext(), this);
GBLocationService.stop(getContext(), getDevice());
if (startOnPhone) {
LOG.info("Stopping OpenTracks recording");

View File

@ -143,10 +143,12 @@ public class ZeppOsCalendarService extends AbstractZeppOsService {
buf.putInt(calendarEventSpec.timestamp + calendarEventSpec.durationInSeconds);
// Remind
buf.put((byte) 0x00); // ?
buf.put((byte) 0x00); // ?
buf.put((byte) 0x00); // ?
buf.put((byte) 0x00); // ?
if (calendarEventSpec.reminders != null && !calendarEventSpec.reminders.isEmpty()) {
buf.putInt((int) (calendarEventSpec.reminders.get(0) / 1000L));
} else {
buf.putInt(0);
}
// Repeat
buf.put((byte) 0x00); // ?
buf.put((byte) 0x00); // ?
@ -231,7 +233,10 @@ public class ZeppOsCalendarService extends AbstractZeppOsService {
final int endTime = BLETypeConversions.toUint32(payload, i);
i += 4;
// ? 00 00 00 00 00 00 00 00 ff ff ff ff
final int reminderTime = BLETypeConversions.toUint32(payload, i);
i += 4;
// ? 00 00 00 00 ff ff ff ff
i += 12;
boolean allDay = (payload[i] == 0x01);

View File

@ -49,8 +49,6 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.GpsAndTime;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Menstrual;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.MusicControl;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Weather;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationListener;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationManager;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.Request;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetPhoneInfoRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendMenstrualModifyTimeRequest;

View File

@ -44,7 +44,6 @@ import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.EventHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiCoordinatorSupplier;
@ -65,7 +64,8 @@ import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutPaceSample;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutPaceSampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSummarySample;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSummarySampleDao;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationManager;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationProviderType;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationService;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.entities.Alarm;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
@ -228,11 +228,6 @@ public class HuaweiSupportProvider {
}
public void setGps(boolean start) {
EventHandler handler;
if (isBLE())
handler = leSupport;
else
handler = brSupport;
if (start) {
if (!GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()).getBoolean(DeviceSettingsPreferenceConst.PREF_WORKOUT_SEND_GPS_TO_BAND, false))
return;
@ -241,7 +236,7 @@ public class HuaweiSupportProvider {
gpsParameterRequest.setFinalizeReq(new RequestCallback() {
@Override
public void call() {
GBLocationManager.start(getContext(), handler);
GBLocationService.start(getContext(), getDevice(), GBLocationProviderType.GPS, 1000);
}
});
try {
@ -251,9 +246,9 @@ public class HuaweiSupportProvider {
LOG.error("Failed to get GPS parameters", e);
}
} else
GBLocationManager.start(getContext(), handler);
GBLocationService.start(getContext(), getDevice(), GBLocationProviderType.GPS, 1000);
} else
GBLocationManager.stop(getContext(), handler);
GBLocationService.stop(getContext(), getDevice());
}
public void setGpsParametersResponse(GpsAndTime.GpsParameters.Response response) {

View File

@ -21,7 +21,9 @@ import org.slf4j.LoggerFactory;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket.CryptoException;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.AccountRelated;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
@ -37,8 +39,11 @@ public class SendAccountRequest extends Request {
@Override
protected List<byte[]> createRequest() throws RequestCreationException {
String account = GBApplication
.getDeviceSpecificSharedPrefs(supportProvider.getDevice().getAddress())
.getString(HuaweiConstants.PREF_HUAWEI_ACCOUNT, "").trim();
try {
return new AccountRelated.SendAccountToDevice.Request(paramsProvider).serialize();
return new AccountRelated.SendAccountToDevice.Request(paramsProvider, account).serialize();
} catch (CryptoException e) {
throw new RequestCreationException(e);
}

View File

@ -22,6 +22,8 @@ import org.slf4j.LoggerFactory;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket.CryptoException;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.AccountRelated;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
@ -37,10 +39,14 @@ public class SendExtendedAccountRequest extends Request {
@Override
protected List<byte[]> createRequest() throws Request.RequestCreationException {
String account = GBApplication
.getDeviceSpecificSharedPrefs(supportProvider.getDevice().getAddress())
.getString(HuaweiConstants.PREF_HUAWEI_ACCOUNT, "").trim();
try {
return new AccountRelated.SendExtendedAccountToDevice.Request(
paramsProvider,
supportProvider.getHuaweiCoordinator().supportsDiffAccountPairingOptimization())
supportProvider.getHuaweiCoordinator().supportsDiffAccountPairingOptimization(),
account)
.serialize();
} catch (CryptoException e) {
throw new Request.RequestCreationException(e);

View File

@ -106,6 +106,11 @@ public class XiaomiCalendarService extends AbstractXiaomiService {
thisSync.add(calendarEvent);
int notifyMinutesBefore = 0;
if (!calendarEvent.getRemindersAbsoluteTs().isEmpty()) {
notifyMinutesBefore = (int) ((calendarEvent.getBeginSeconds() * 1000L - calendarEvent.getRemindersAbsoluteTs().get(0)) / (1000 * 60));
}
final XiaomiProto.CalendarEvent xiaomiCalendarEvent = XiaomiProto.CalendarEvent.newBuilder()
.setTitle(calendarEvent.getTitle())
.setDescription(StringUtils.ensureNotNull(calendarEvent.getDescription()))
@ -113,7 +118,7 @@ public class XiaomiCalendarService extends AbstractXiaomiService {
.setStart(calendarEvent.getBeginSeconds())
.setEnd((int) (calendarEvent.getEnd() / 1000))
.setAllDay(calendarEvent.isAllDay())
.setNotifyMinutesBefore(0) // TODO fetch from event
.setNotifyMinutesBefore(notifyMinutesBefore)
.build();
calendarSync.addEvent(xiaomiCalendarEvent);

View File

@ -48,7 +48,8 @@ import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.User;
import nodomain.freeyourgadget.gadgetbridge.entities.XiaomiActivitySample;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationManager;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationProviderType;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationService;
import nodomain.freeyourgadget.gadgetbridge.externalevents.opentracks.OpenTracksController;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
@ -664,7 +665,7 @@ public class XiaomiHealthService extends AbstractXiaomiService {
if (!gpsStarted) {
gpsStarted = true;
gpsFixAcquired = false;
GBLocationManager.start(getSupport().getContext(), getSupport());
GBLocationService.start(getSupport().getContext(), getSupport().getDevice(), GBLocationProviderType.GPS, 1000);
}
gpsTimeoutHandler.removeCallbacksAndMessages(null);
@ -673,7 +674,7 @@ public class XiaomiHealthService extends AbstractXiaomiService {
LOG.debug("Timed out waiting for workout");
gpsStarted = false;
gpsFixAcquired = false;
GBLocationManager.stop(getSupport().getContext(), getSupport());
GBLocationService.stop(getSupport().getContext(), getSupport().getDevice());
}, 5000);
}
@ -696,7 +697,7 @@ public class XiaomiHealthService extends AbstractXiaomiService {
case WORKOUT_FINISHED:
gpsStarted = false;
gpsFixAcquired = false;
GBLocationManager.stop(getSupport().getContext(), getSupport());
GBLocationService.stop(getSupport().getContext(), getSupport().getDevice());
if (startOnPhone) {
OpenTracksController.stopRecording(getSupport().getContext());
}

View File

@ -500,27 +500,6 @@ public class GB {
}
}
public static void createGpsNotification(Context context, int numDevices) {
Intent notificationIntent = new Intent(context, ControlCenterv2.class);
notificationIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
PendingIntent pendingIntent = PendingIntentUtils.getActivity(context, 0, notificationIntent, 0, false);
NotificationCompat.Builder nb = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID_GPS)
.setTicker(context.getString(R.string.notification_gps_title))
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setContentTitle(context.getString(R.string.notification_gps_title))
.setContentText(context.getString(R.string.notification_gps_text, numDevices))
.setContentIntent(pendingIntent)
.setSmallIcon(R.drawable.ic_gps_location)
.setOngoing(true);
notify(NOTIFICATION_ID_GPS, nb.build(), context);
}
public static void removeGpsNotification(Context context) {
removeNotification(NOTIFICATION_ID_GPS, context);
}
private static Notification createInstallNotification(String text, boolean ongoing,
int percentage, Context context) {
Intent notificationIntent = new Intent(context, ControlCenterv2.class);

View File

@ -0,0 +1,251 @@
/* 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.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.provider.Settings;
import android.widget.Toast;
import androidx.core.app.ActivityCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.content.ContextCompat;
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;
public class PermissionsUtils {
private static final Logger LOG = LoggerFactory.getLogger(PermissionsUtils.class);
private static final String CUSTOM_PERM_NOTIFICATION_LISTENER = "custom_perm_notifications_listener";
private static final String CUSTOM_PERM_NOTIFICATION_SERVICE = "custom_perm_notifications_service";
private static final String CUSTOM_PERM_DISPLAY_OVER = "custom_perm_display_over";
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),
"Forwarding notifications to connected gadgets"));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
permissionsList.add(new PermissionDetails(
CUSTOM_PERM_NOTIFICATION_SERVICE,
"Manage Do Not Disturb",
"Change DND notification policy"));
permissionsList.add(new PermissionDetails(
CUSTOM_PERM_DISPLAY_OVER,
"Display over other apps",
"Used by Bangle.js to start apps and other functionality on your phone"));
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && Build.VERSION.SDK_INT <= Build.VERSION_CODES.S) {
permissionsList.add(new PermissionDetails(
Manifest.permission.ACCESS_BACKGROUND_LOCATION,
"Background location",
"Required for scanning for Bluetooth devices"));
}
permissionsList.add(new PermissionDetails(
Manifest.permission.ACCESS_FINE_LOCATION,
"Fine location",
"Send location to gadgets which don't have GPS"));
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
permissionsList.add(new PermissionDetails(
Manifest.permission.BLUETOOTH,
"Bluetooth",
"Connect to Bluetooth devices"));
permissionsList.add(new PermissionDetails(
Manifest.permission.BLUETOOTH_ADMIN,
"Bluetooth admin",
"Discover and pair Bluetooth devices"));
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
permissionsList.add(new PermissionDetails(
Manifest.permission.BLUETOOTH_SCAN,
"Bluetooth scan",
"Scan for Bluetooth devices"));
permissionsList.add(new PermissionDetails(
Manifest.permission.BLUETOOTH_CONNECT,
"Bluetooth connect",
"Connect to Bluetooth devices"));
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
permissionsList.add(new PermissionDetails(
Manifest.permission.POST_NOTIFICATIONS,
"Post notifications",
"Post ongoing notification which keeps the service running"));
}
if (BuildConfig.INTERNET_ACCESS) {
permissionsList.add(new PermissionDetails(
Manifest.permission.INTERNET,
"Internet access",
"Synchronization with online resources"));
}
// 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,
"Contacts",
"Send contacts to gadgets"));
permissionsList.add(new PermissionDetails(
Manifest.permission.READ_CALENDAR,
"Calendar",
"Send calendar to gadgets"));
permissionsList.add(new PermissionDetails(
Manifest.permission.RECEIVE_SMS,
"Receive SMS",
"Forward SMS messages to gadgets"));
permissionsList.add(new PermissionDetails(
Manifest.permission.SEND_SMS,
"Send SMS",
"Send SMS (canned response) from gadgets"));
permissionsList.add(new PermissionDetails(
Manifest.permission.READ_CALL_LOG,
"Read call log",
"Forward call log to gadgets"));
permissionsList.add(new PermissionDetails(
Manifest.permission.READ_PHONE_STATE,
"Read phone state",
"Read status of ongoing calls"));
permissionsList.add(new PermissionDetails(
Manifest.permission.CALL_PHONE,
"Call phone",
"Initiate phone calls from gadgets"));
permissionsList.add(new PermissionDetails(
Manifest.permission.PROCESS_OUTGOING_CALLS,
"Process outgoing calls",
"Read the number of an outgoing call to display it on a gadget"));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
permissionsList.add(new PermissionDetails(
Manifest.permission.ANSWER_PHONE_CALLS,
"Answer phone calls",
"Answer phone calls from gadgets"));
}
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
permissionsList.add(new PermissionDetails(
Manifest.permission.READ_EXTERNAL_STORAGE,
"External storage",
"Using images, ringtones, app files and more"));
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
permissionsList.add(new PermissionDetails(
Manifest.permission.QUERY_ALL_PACKAGES,
"Query all packages",
"Read names and icons of all installed apps"));
}
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 requestAllPermissions(Activity activity) {
List<PermissionDetails> wantedPermissions = getRequiredPermissionsList(activity);
if (!wantedPermissions.isEmpty()) {
ArrayList<String> wantedPermissionsStrings = new ArrayList<>();
for (PermissionDetails wantedPermission : wantedPermissions) {
wantedPermissionsStrings.add(wantedPermission.getPermission());
}
if (!wantedPermissionsStrings.isEmpty()) {
if (wantedPermissionsStrings.contains(CUSTOM_PERM_NOTIFICATION_LISTENER) && !checkPermission(activity, CUSTOM_PERM_NOTIFICATION_LISTENER))
requestPermission(activity, CUSTOM_PERM_NOTIFICATION_LISTENER);
if (wantedPermissionsStrings.contains(CUSTOM_PERM_NOTIFICATION_SERVICE) && !checkPermission(activity, CUSTOM_PERM_NOTIFICATION_SERVICE))
requestPermission(activity, CUSTOM_PERM_NOTIFICATION_SERVICE);
if (wantedPermissionsStrings.contains(CUSTOM_PERM_DISPLAY_OVER) && !checkPermission(activity, CUSTOM_PERM_DISPLAY_OVER))
requestPermission(activity, CUSTOM_PERM_DISPLAY_OVER);
ActivityCompat.requestPermissions(activity, wantedPermissionsStrings.toArray(new String[0]), 0);
}
}
}
public static void requestPermission(Activity activity, String permission) {
if (permission.equals(CUSTOM_PERM_NOTIFICATION_LISTENER)) {
try {
activity.startActivity(new Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS"));
} catch (ActivityNotFoundException e) {
GB.toast(activity, "'Notification Listener Settings' activity not found", Toast.LENGTH_LONG, GB.ERROR);
}
} else if (permission.equals(CUSTOM_PERM_NOTIFICATION_SERVICE)) {
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");
}
} else if (permission.equals(CUSTOM_PERM_DISPLAY_OVER)) {
activity.startActivity(new Intent(android.provider.Settings.ACTION_MANAGE_OVERLAY_PERMISSION));
} 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;
}
}
}

View File

@ -16,21 +16,25 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.util.calendar;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public class CalendarEvent {
private long begin;
private long end;
private long id;
private String title;
private String description;
private String location;
private String calName;
private String calAccountName;
private int color;
private boolean allDay;
private final long begin;
private final long end;
private final long id;
private final String title;
private final String description;
private final String location;
private final String calName;
private final String calAccountName;
private final String organizer;
private final int color;
private final boolean allDay;
private List<Long> remindersAbsoluteTs = new ArrayList<>();
public CalendarEvent(long begin, long end, long id, String title, String description, String location, String calName, String calAccountName, int color, boolean allDay) {
public CalendarEvent(long begin, long end, long id, String title, String description, String location, String calName, String calAccountName, int color, boolean allDay, String organizer) {
this.begin = begin;
this.end = end;
this.id = id;
@ -41,6 +45,15 @@ public class CalendarEvent {
this.calAccountName = calAccountName;
this.color = color;
this.allDay = allDay;
this.organizer = organizer;
}
public List<Long> getRemindersAbsoluteTs() {
return remindersAbsoluteTs;
}
public void setRemindersAbsoluteTs(List<Long> remindersAbsoluteTs) {
this.remindersAbsoluteTs = remindersAbsoluteTs;
}
public long getBegin() {
@ -76,6 +89,10 @@ public class CalendarEvent {
return title;
}
public String getOrganizer() {
return organizer;
}
public String getDescription() {
return description;
}
@ -117,7 +134,9 @@ public class CalendarEvent {
Objects.equals(this.getCalName(), e.getCalName()) &&
Objects.equals(this.getCalAccountName(), e.getCalAccountName()) &&
(this.getColor() == e.getColor()) &&
(this.isAllDay() == e.isAllDay());
(this.isAllDay() == e.isAllDay()) &&
Objects.equals(this.getOrganizer(), e.getOrganizer()) &&
Objects.equals(this.getRemindersAbsoluteTs(), e.getRemindersAbsoluteTs());
} else {
return false;
}
@ -135,6 +154,8 @@ public class CalendarEvent {
result = 31 * result + Objects.hash(calAccountName);
result = 31 * result + Integer.valueOf(color).hashCode();
result = 31 * result + Boolean.valueOf(allDay).hashCode();
result = 31 * result + Objects.hash(organizer);
result = 31 * result + Objects.hash(remindersAbsoluteTs);
return result;
}
}

View File

@ -36,7 +36,6 @@ import java.util.List;
import java.util.Set;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
@ -60,10 +59,12 @@ public class CalendarManager {
Instances.TITLE,
Instances.DESCRIPTION,
Instances.EVENT_LOCATION,
Instances.ORGANIZER,
Instances.CALENDAR_DISPLAY_NAME,
CalendarContract.Calendars.ACCOUNT_NAME,
Instances.CALENDAR_COLOR,
Instances.ALL_DAY
Instances.ALL_DAY,
Instances.EVENT_ID //needed for reminders
};
private static final int lookahead_days = 7;
@ -98,26 +99,54 @@ public class CalendarManager {
return calendarEventList;
}
while (evtCursor.moveToNext()) {
long start = evtCursor.getLong(1);
long end = evtCursor.getLong(2);
long start = evtCursor.getLong(evtCursor.getColumnIndexOrThrow(Instances.BEGIN));
long end = evtCursor.getLong(evtCursor.getColumnIndexOrThrow(Instances.END));
if (end == 0) {
LOG.info("no end time, will parse duration string");
Time time = new Time(); //FIXME: deprecated FTW
time.parse(evtCursor.getString(3));
time.parse(evtCursor.getString(evtCursor.getColumnIndexOrThrow(Instances.DURATION)));
end = start + time.toMillis(false);
}
CalendarEvent calEvent = new CalendarEvent(
start,
end,
evtCursor.getLong(0),
evtCursor.getString(4),
evtCursor.getString(5),
evtCursor.getString(6),
evtCursor.getString(7),
evtCursor.getString(8),
evtCursor.getInt(9),
!evtCursor.getString(10).equals("0")
evtCursor.getLong(evtCursor.getColumnIndexOrThrow(Instances._ID)),
evtCursor.getString(evtCursor.getColumnIndexOrThrow(Instances.TITLE)),
evtCursor.getString(evtCursor.getColumnIndexOrThrow(Instances.DESCRIPTION)),
evtCursor.getString(evtCursor.getColumnIndexOrThrow(Instances.EVENT_LOCATION)),
evtCursor.getString(evtCursor.getColumnIndexOrThrow(Instances.CALENDAR_DISPLAY_NAME)),
evtCursor.getString(evtCursor.getColumnIndexOrThrow(CalendarContract.Calendars.ACCOUNT_NAME)),
evtCursor.getInt(evtCursor.getColumnIndexOrThrow(Instances.CALENDAR_COLOR)),
!evtCursor.getString(evtCursor.getColumnIndexOrThrow(Instances.ALL_DAY)).equals("0"),
evtCursor.getString(evtCursor.getColumnIndexOrThrow(Instances.ORGANIZER))
);
// Query reminders for this event
final Cursor reminderCursor = mContext.getContentResolver().query(
CalendarContract.Reminders.CONTENT_URI,
null,
CalendarContract.Reminders.EVENT_ID + " = ?",
new String[]{String.valueOf(evtCursor.getLong(evtCursor.getColumnIndexOrThrow(Instances.EVENT_ID)))},
null
);
if (reminderCursor != null && reminderCursor.getCount() > 0) {
final List<Long> reminders = new ArrayList<>();
while (reminderCursor.moveToNext()) {
int minutes = reminderCursor.getInt(reminderCursor.getColumnIndexOrThrow(CalendarContract.Reminders.MINUTES));
int method = reminderCursor.getInt(reminderCursor.getColumnIndexOrThrow(CalendarContract.Reminders.METHOD));
LOG.debug("Reminder Method: {}, Minutes: {}", method, minutes);
if (method == 1) //METHOD_ALERT
reminders.add(calEvent.getBegin() - minutes * 60 * 1000L);
}
reminderCursor.close();
calEvent.setRemindersAbsoluteTs(reminders);
}
if (!calendarIsBlacklisted(calEvent.getUniqueCalName())) {
calendarEventList.add(calEvent);
} else {

View File

@ -57,6 +57,7 @@ import nodomain.freeyourgadget.gadgetbridge.util.language.impl.PersianTransliter
import nodomain.freeyourgadget.gadgetbridge.util.language.impl.PolishTransliterator;
import nodomain.freeyourgadget.gadgetbridge.util.language.impl.RussianTransliterator;
import nodomain.freeyourgadget.gadgetbridge.util.language.impl.ScandinavianTransliterator;
import nodomain.freeyourgadget.gadgetbridge.util.language.impl.SerbianTransliterator;
import nodomain.freeyourgadget.gadgetbridge.util.language.impl.TurkishTransliterator;
import nodomain.freeyourgadget.gadgetbridge.util.language.impl.UkranianTransliterator;
@ -85,6 +86,7 @@ public class LanguageUtils {
put("polish", new PolishTransliterator());
put("russian", new RussianTransliterator());
put("scandinavian", new ScandinavianTransliterator());
put("serbian", new SerbianTransliterator());
put("turkish", new TurkishTransliterator());
put("ukranian", new UkranianTransliterator());
put("armenian", new ArmenianTransliterator());

View File

@ -18,14 +18,19 @@ package nodomain.freeyourgadget.gadgetbridge.util.language;
import org.apache.commons.lang3.text.WordUtils;
import java.text.Normalizer;
import java.util.Map;
public class SimpleTransliterator implements Transliterator {
private final Map<Character, String> transliterateMap;
private final boolean convertToLowercase;
public SimpleTransliterator(final Map<Character, String> transliterateMap, final boolean convertToLowercase) {
this.transliterateMap = transliterateMap;
this.convertToLowercase = convertToLowercase;
}
public SimpleTransliterator(final Map<Character, String> transliterateMap) {
this.transliterateMap = transliterateMap;
this(transliterateMap, true);
}
@Override
@ -46,14 +51,14 @@ public class SimpleTransliterator implements Transliterator {
return message;
}
private String transliterate(char c) {
final char lowerChar = Character.toLowerCase(c);
private String transliterate(final char c) {
final char sourceChar = convertToLowercase ? Character.toLowerCase(c) : c;
if (transliterateMap.containsKey(lowerChar)) {
final String replace = transliterateMap.get(lowerChar);
if (transliterateMap.containsKey(sourceChar)) {
final String replace = transliterateMap.get(sourceChar);
if (lowerChar != c) {
return WordUtils.capitalize(replace);
if (sourceChar != c) {
return convertToLowercase ? WordUtils.capitalize(replace) : replace;
}
return replace;

View File

@ -0,0 +1,68 @@
/* 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.util.language.impl;
import java.util.HashMap;
import nodomain.freeyourgadget.gadgetbridge.util.language.SimpleTransliterator;
public class SerbianTransliterator extends SimpleTransliterator {
public SerbianTransliterator() {
super(new HashMap<Character, String>() {{
// As per https://en.wikipedia.org/wiki/Serbian_Cyrillic_alphabet#Modern_alphabet
put('А', "A"); put('а', "a");
put('Б', "B"); put('б', "b");
put('В', "V"); put('в', "v");
put('Г', "G"); put('г', "g");
put('Д', "D"); put('д', "d");
put('Ђ', "Dj"); put('ђ', "dj"); // from Đ / đ - from suggestion in #3727
put('Е', "E"); put('е', "e");
put('Ж', "Z"); put('ж', "z"); // from Ž / ž
put('З', "Z"); put('з', "z");
put('И', "I"); put('и', "i");
put('Ј', "J"); put('ј', "j");
put('К', "K"); put('к', "k");
put('Л', "L"); put('л', "l");
put('Љ', "Lj"); put('љ', "lj");
put('М', "M"); put('м', "m");
put('Н', "N"); put('н', "n");
put('Њ', "Nj"); put('њ', "nj");
put('О', "O"); put('о', "o");
put('П', "P"); put('п', "p");
put('Р', "R"); put('р', "r");
put('С', "S"); put('с', "s");
put('Т', "T"); put('т', "t");
put('Ћ', "C"); put('ћ', "c"); // from Ć / ć
put('У', "U"); put('у', "u");
put('Ф', "F"); put('ф', "f");
put('Х', "H"); put('х', "h");
put('Ц', "C"); put('ц', "c");
put('Ч', "C"); put('ч', "c"); // from Č / č
put('Џ', "Dz"); put('џ', "dz"); // from /
put('Ш', "S"); put('ш', "s"); // From Š / š
// Not in the table, pulled from Croatian
put('Ć', "C"); put('ć', "c");
put('Đ', "Dj"); put('đ', "dj");
put('Š', "S"); put('š', "s");
put('Ž', "z"); put('ž', "z");
// Suggested in #3727
put('Č', "C"); put('č', "c");
}}, false);
}
}

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:orientation="vertical"
tools:context=".activities.welcome.WelcomeActivity">
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/welcome_viewpager"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/welcome_page_indicator" />
<nodomain.freeyourgadget.gadgetbridge.activities.welcome.WelcomePageIndicator
android:id="@+id/welcome_page_indicator"
android:layout_width="match_parent"
android:layout_height="30dp"
app:layout_constraintBottom_toBottomOf="parent"
app:page_indicator_color="?attr/colorPrimary" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="50dp"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAlignment="center"
android:text="Open Source"
android:textSize="30sp"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:textSize="20sp"
android:textAlignment="center"
android:text="Gadgetbridge is an open source app. It is developed by the community, for the community.\n\nAnyone is welcome to contribute via code, documentation, testing and donations.\n\nGadgetbridge contains no ads and no tracking. It works only locally on your Android device, so it is 100% privacy friendly.\n\nVisit our website for more information, documentation and links to our communication channels." />
<ImageView
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_marginTop="30dp"
android:src="@drawable/ic_engineering"
app:tint="?attr/colorPrimary" />
</LinearLayout>
</ScrollView>
</RelativeLayout>

View File

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_marginHorizontal="50dp"
android:orientation="vertical">
<TextView
android:id="@+id/intro_gadgetbridge"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAlignment="center"
android:text="Get started"
android:textSize="25sp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:textSize="20sp"
android:textAlignment="center"
android:text="To get started, add your first device directly from this screen, restore a backup or start with a clean database." />
<Button
android:id="@+id/welcome_button_add_device"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:text="Add first device" />
<Button
android:id="@+id/welcome_button_restore"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:text="Restore backup" />
<Button
android:id="@+id/welcome_button_to_app"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:text="Go to the app" />
</LinearLayout>
</RelativeLayout>

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="50dp"
android:layout_centerInParent="true"
android:gravity="center_horizontal"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAlignment="center"
android:text="Welcome to\nGadgetbridge"
android:textSize="30sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:textSize="20sp"
android:textAlignment="center"
android:text="Break free from the proprietary apps and cloud services of gadget vendors." />
<ImageView
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_marginTop="30dp"
android:background="@color/accent"
android:src="@drawable/ic_launcher_foreground" />
</LinearLayout>
</RelativeLayout>

View File

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="50dp"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAlignment="center"
android:text="Overview"
android:textSize="30sp"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:textSize="20sp"
android:textAlignment="center"
android:text="Gadgetbridge has two main views, each with their own purpose." />
<ImageView
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_marginTop="30dp"
android:src="@drawable/ic_dashboard"
app:tint="?attr/colorPrimary" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="20sp"
android:textAlignment="center"
android:text="The dashboard allows you to get a quick idea of how you're doing today. The calendar view shows the status of your goals over a whole month." />
<ImageView
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_marginTop="20dp"
android:src="@drawable/ic_devices_other"
app:tint="?attr/colorPrimary" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="20sp"
android:textAlignment="center"
android:text="The devices view shows all devices you have configured and their status, and gives access to device specific functions such as detailed charts, settings, apps and alarms." />
</LinearLayout>
</ScrollView>
</RelativeLayout>

View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="50dp">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/permission_request"
android:orientation="vertical">
<TextView
android:id="@+id/permission_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textStyle="bold"
android:text="Permission name" />
<TextView
android:id="@+id/permission_summary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Permission summary" />
</LinearLayout>
<Button
android:id="@+id/permission_request"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:text="Request" />
<ImageView
android:id="@+id/permission_check"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="20dp"
android:src="@drawable/cpv_preset_checked"
android:visibility="gone"
app:tint="@android:color/holo_green_dark"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center_horizontal"
android:padding="8dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:textAlignment="center"
android:text="Permissions"
android:textSize="30sp"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:textSize="20sp"
android:textAlignment="center"
android:text="Gadgetbridge needs a lot of permissions to perform all its functions. Review the permissions and their purposes below." />
<Button
android:id="@+id/button_request_all"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="Request all permissions"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/permissions_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp" />
</LinearLayout>

View File

@ -3492,6 +3492,7 @@
<item>@string/polish</item>
<item>@string/russian</item>
<item>@string/scandinavian</item>
<item>@string/serbian</item>
<item>@string/turkish</item>
<item>@string/ukranian</item>
<item>@string/hungarian</item>
@ -3519,6 +3520,7 @@
<item>polish</item>
<item>russian</item>
<item>scandinavian</item>
<item>serbian</item>
<item>turkish</item>
<item>ukranian</item>
<item>hungarian</item>

View File

@ -1060,6 +1060,7 @@
<string name="lithuanian">Lithuanian</string>
<string name="persian">Persian</string>
<string name="scandinavian">Scandinavian</string>
<string name="serbian">Serbian</string>
<string name="ukranian">Ukranian</string>
<string name="armenian">Armenian</string>
<string name="italian">Italian</string>
@ -2812,4 +2813,6 @@
<string name="pref_sleepasandroid_feat_heartrate">Heart rate</string>
<string name="pref_sleepasandroid_feat_oximetry">Oximetry</string>
<string name="pref_sleepasandroid_feat_spo2">SPO2</string>
<string name="pref_title_huawei_account">Huawei Account</string>
<string name="pref_summary_huawei_account">Huawei account used in pairing process. Setting it allows to pair without factory reset.</string>
</resources>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<resources>
<declare-styleable name="WelcomePageIndicator">
<attr name="page_indicator_color" format="color" />
</declare-styleable>
</resources>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<EditTextPreference
android:icon="@drawable/ic_vpn_key"
android:key="huawei_account"
android:maxLength="17"
android:summary="@string/pref_summary_huawei_account"
android:title="@string/pref_title_huawei_account" />
</androidx.preference.PreferenceScreen>

View File

@ -373,6 +373,10 @@
android:summary="@string/pref_cache_weather_summary"
android:title="@string/pref_cache_weather"
app:iconSpaceReserved="false" />
<Preference
android:key="pref_show_first_run_screen"
android:title="Show first run screen"
app:iconSpaceReserved="false" />
<Preference
android:key="pref_discovery_pairing"
android:title="@string/activity_prefs_discovery_pairing"

View File

@ -25,22 +25,25 @@ public class CalendarEventTest extends TestBase {
@Test
public void testHashCode() {
CalendarEvent c1 =
new CalendarEvent(BEGIN, END, ID_1, "something", null, null, CALNAME_1, CALACCOUNTNAME_1, COLOR_1, false);
new CalendarEvent(BEGIN, END, ID_1, "something", null, null, CALNAME_1, CALACCOUNTNAME_1, COLOR_1, false, null);
CalendarEvent c2 =
new CalendarEvent(BEGIN, END, ID_1, null, "something", null, CALNAME_1, CALACCOUNTNAME_1, COLOR_1, false);
new CalendarEvent(BEGIN, END, ID_1, null, "something", null, CALNAME_1, CALACCOUNTNAME_1, COLOR_1, false, null);
CalendarEvent c3 =
new CalendarEvent(BEGIN, END, ID_1, null, null, "something", CALNAME_1, CALACCOUNTNAME_1, COLOR_1, false);
new CalendarEvent(BEGIN, END, ID_1, null, null, "something", CALNAME_1, CALACCOUNTNAME_1, COLOR_1, false, null);
CalendarEvent c4 =
new CalendarEvent(BEGIN, END, ID_1, null, null, "something", CALNAME_1, CALACCOUNTNAME_1, COLOR_1, false, "some");
assertEquals(c1.hashCode(), c1.hashCode());
assertNotEquals(c1.hashCode(), c2.hashCode());
assertNotEquals(c2.hashCode(), c3.hashCode());
assertNotEquals(c3.hashCode(), c4.hashCode());
}
@Test
public void testSync() {
List<CalendarEvent> eventList = new ArrayList<>();
eventList.add(new CalendarEvent(BEGIN, END, ID_1, null, "something", null, CALNAME_1, CALACCOUNTNAME_1, COLOR_1, false));
eventList.add(new CalendarEvent(BEGIN, END, ID_1, null, "something", null, CALNAME_1, CALACCOUNTNAME_1, COLOR_1, false, null));
GBDevice dummyGBDevice = createDummyGDevice("00:00:01:00:03");
dummyGBDevice.setState(GBDevice.State.INITIALIZED);
@ -49,7 +52,7 @@ public class CalendarEventTest extends TestBase {
testCR.syncCalendar(eventList);
eventList.add(new CalendarEvent(BEGIN, END, ID_2, null, "something", null, CALNAME_1, CALACCOUNTNAME_1, COLOR_1, false));
eventList.add(new CalendarEvent(BEGIN, END, ID_2, null, "something", null, CALNAME_1, CALACCOUNTNAME_1, COLOR_1, false, null));
testCR.syncCalendar(eventList);
CalendarSyncStateDao calendarSyncStateDao = daoSession.getCalendarSyncStateDao();

View File

@ -5,6 +5,8 @@ import android.content.SharedPreferences;
import org.junit.Test;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
@ -44,6 +46,34 @@ public class LanguageUtilsTest extends TestBase {
assertEquals("Transliteration failed", result, output);
}
@Test
public void testStringTransliterateSerbian() throws Exception {
final Transliterator transliterator = LanguageUtils.getTransliterator("serbian");
final Map<String, String> tests = new LinkedHashMap<String, String>() {{
put("Тхе qицк брон фоx јумпед овер тхе лаз* дог", "The qick bron fox jumped over the laz* dog");
put("Српска ћирилица", "Srpska cirilica");
put("Novak Đoković", "Novak Dokovic");
put("Џ, Њ and Љ", "Dz, Nj and Lj");
put("Љуљачка", "Ljuljacka");
put("Наковањ", "Nakovanj");
put("Качкаваљ", "Kackavalj");
put("Чачак", "Cacak");
put("Ч, ч", "C, c");
put("Ћ, ћ", "C, c");
put("Ж, ж", "Z, z");
put("Ш, ш", "S, s");
put("Ђ, ђ", "D, d");
put("Џ, џ", "Dz, dz");
put("Њ, њ", "Nj, nj");
put("Љ, љ", "Lj, lj");
}};
for (final Map.Entry<String, String> e : tests.entrySet()) {
assertEquals("Transliteration failed for " + e.getKey(), e.getValue(), transliterator.transliterate(e.getKey()));
}
}
@Test
public void testStringTransliterateHebrew() throws Exception {
final Transliterator transliterator = LanguageUtils.getTransliterator("hebrew");