Compare commits
9 Commits
72aff4a823
...
94dba527b2
Author | SHA1 | Date |
---|---|---|
Arjan Schrijver | 94dba527b2 | |
José Rebelo | 408f4b75dd | |
José Rebelo | 31408394b4 | |
José Rebelo | 61af26d7ce | |
José Rebelo | 500e930237 | |
José Rebelo | 3799ffb72c | |
José Rebelo | 13d6c49bb5 | |
Vitaliy Tomin | 67cf9b2f00 | |
Daniele Gobbetti | 173e2d29b0 |
|
@ -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"
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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 -> {
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
/* Copyright (C) 2024 Arjan Schrijver
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.welcome;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity;
|
||||
|
||||
public class WelcomeActivity extends AbstractGBActivity {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(WelcomeActivity.class);
|
||||
|
||||
private ViewPager2 viewPager;
|
||||
private WelcomeFragmentsPagerAdapter pagerAdapter;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
AbstractGBActivity.init(this, AbstractGBActivity.NO_ACTIONBAR);
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_welcome);
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().hide();
|
||||
}
|
||||
|
||||
// Configure ViewPager2 with fragment adapter and default fragment
|
||||
viewPager = findViewById(R.id.welcome_viewpager);
|
||||
pagerAdapter = new WelcomeFragmentsPagerAdapter(this);
|
||||
viewPager.setAdapter(pagerAdapter);
|
||||
|
||||
// Set up welcome page indicator
|
||||
WelcomePageIndicator pageIndicator = findViewById(R.id.welcome_page_indicator);
|
||||
pageIndicator.setViewPager(viewPager);
|
||||
}
|
||||
|
||||
private class WelcomeFragmentsPagerAdapter extends FragmentStateAdapter {
|
||||
public WelcomeFragmentsPagerAdapter(FragmentActivity fa) {
|
||||
super(fa);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Fragment createFragment(int position) {
|
||||
switch (position) {
|
||||
case 0:
|
||||
return new WelcomeFragmentIntro();
|
||||
case 1:
|
||||
return new WelcomeFragmentOverview();
|
||||
case 2:
|
||||
return new WelcomeFragmentDocsSource();
|
||||
case 3:
|
||||
return new WelcomeFragmentPermissions();
|
||||
default:
|
||||
return new WelcomeFragmentGetStarted();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return 5;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
/* Copyright (C) 2024 Arjan Schrijver
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.welcome;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
|
||||
public class WelcomeFragmentDocsSource extends Fragment {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(WelcomeFragmentDocsSource.class);
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
super.onCreateView(inflater, container, savedInstanceState);
|
||||
return inflater.inflate(R.layout.fragment_welcome_docs_source, container, false);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
/* Copyright (C) 2024 Arjan Schrijver
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.welcome;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
|
||||
public class WelcomeFragmentOverview extends Fragment {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(WelcomeFragmentOverview.class);
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
super.onCreateView(inflater, container, savedInstanceState);
|
||||
return inflater.inflate(R.layout.fragment_welcome_overview, container, false);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 Dž / dž
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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"
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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");
|
||||
|
|
Loading…
Reference in New Issue