1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-11-26 20:06:52 +01:00

Compare commits

...

17 Commits

Author SHA1 Message Date
Arjan Schrijver
d9b59df637 Add preference migration for existing users 2024-11-07 22:39:03 +01:00
Arjan Schrijver
0288db15b6 Scroll Notification and Overlay permissions screens to GB automatically 2024-11-07 22:08:27 +01:00
Arjan Schrijver
21a3c73ca2 Make strings translatable 2024-11-07 22:08:27 +01:00
Arjan Schrijver
44772e3dc4 Remove option to start first run screens from preferences 2024-11-07 22:08:27 +01:00
Arjan Schrijver
2892e3a08b Add theme selector to first screen 2024-11-07 22:08:27 +01:00
Arjan Schrijver
9877e24182 Improve background location permission request flow 2024-11-07 22:08:27 +01:00
Arjan Schrijver
01cdcc4b6f Fix permissions list left margin 2024-11-07 22:08:27 +01:00
Arjan Schrijver
12cb5627f8 Hide Permissions title when action bar is visible 2024-11-07 22:08:27 +01:00
Arjan Schrijver
6d6c461a6a Actually improve requesting all permissions 2024-11-07 22:08:27 +01:00
Arjan Schrijver
f017e454de Improve requesting all permissions 2024-11-07 22:08:27 +01:00
Arjan Schrijver
b88464b9d0 Improve permissions descriptions 2024-11-07 22:08:27 +01:00
Arjan Schrijver
06b14248d0 Fix missing permission request buttons 2024-11-07 22:08:27 +01:00
Arjan Schrijver
edf94625b8 Add back permission explanation dialogs 2024-11-07 22:08:27 +01:00
Arjan Schrijver
581706b1f8 Improve "works locally" wording 2024-11-07 22:08:27 +01:00
Arjan Schrijver
6f2a29f7c1 Make strings translatable 2024-11-07 22:08:26 +01:00
Arjan Schrijver
1145813ed2 Reorder intro screen and use correct app_name 2024-11-07 22:08:26 +01:00
Arjan Schrijver
5b515319bc Add First Start screens with permissions screen 2024-11-07 22:08:26 +01:00
22 changed files with 1426 additions and 373 deletions

View File

@ -150,6 +150,14 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".activities.welcome.WelcomeActivity"
android:label="@string/first_start_welcome_title"
android:parentActivityName=".activities.ControlCenterv2" />
<activity
android:name=".activities.PermissionsActivity"
android:label="@string/first_start_permissions_title"
android:parentActivityName=".activities.ControlCenterv2" />
<activity
android:name=".activities.SettingsActivity"
android:label="@string/title_activity_settings"

View File

@ -127,7 +127,7 @@ public class GBApplication extends Application {
private static SharedPreferences sharedPrefs;
private static final String PREFS_VERSION = "shared_preferences_version";
//if preferences have to be migrated, increment the following and add the migration logic in migratePrefs below; see http://stackoverflow.com/questions/16397848/how-can-i-migrate-android-preferences-with-a-new-version
private static final int CURRENT_PREFS_VERSION = 42;
private static final int CURRENT_PREFS_VERSION = 43;
private static final LimitedQueue<Integer, String> mIDSenderLookup = new LimitedQueue<>(16);
private static GBPrefs prefs;
@ -1838,6 +1838,20 @@ public class GBApplication extends Application {
}
}
if (oldVersion < 43) {
// Users upgrading to this version don't need to see the welcome screen
try (DBHandler db = acquireDB()) {
final DaoSession daoSession = db.getDaoSession();
final List<Device> activeDevices = DBHelper.getActiveDevices(daoSession);
if (!activeDevices.isEmpty()) {
editor.putBoolean("first_run", false);
}
} catch (final Exception e) {
Log.e(TAG, "Failed to migrate prefs to version 42", e);
}
}
editor.putString(PREFS_VERSION, Integer.toString(CURRENT_PREFS_VERSION));
editor.apply();
}

View File

@ -21,46 +21,29 @@ package nodomain.freeyourgadget.gadgetbridge.activities;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_CONNECT;
import android.Manifest;
import android.annotation.TargetApi;
import android.app.Dialog;
import android.app.NotificationManager;
import android.content.ActivityNotFoundException;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import android.util.TypedValue;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.appcompat.app.ActionBarDrawerToggle;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.appcompat.app.AlertDialog;
import androidx.core.app.ActivityCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.content.ContextCompat;
import androidx.core.view.GravityCompat;
import androidx.core.view.MenuProvider;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
@ -70,26 +53,22 @@ import androidx.viewpager2.widget.ViewPager2;
import com.google.android.material.appbar.MaterialToolbar;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.navigation.NavigationView;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import nodomain.freeyourgadget.gadgetbridge.BuildConfig;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.discovery.DiscoveryActivityV2;
import nodomain.freeyourgadget.gadgetbridge.activities.welcome.WelcomeActivity;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
@ -98,6 +77,7 @@ import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.GBChangeLog;
import nodomain.freeyourgadget.gadgetbridge.util.PermissionsUtils;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
//TODO: extend AbstractGBActivity, but it requires actionbar that is not available
@ -105,16 +85,11 @@ public class ControlCenterv2 extends AppCompatActivity
implements NavigationView.OnNavigationItemSelectedListener, GBActivity {
private static final Logger LOG = LoggerFactory.getLogger(ControlCenterv2.class);
public static final int MENU_REFRESH_CODE = 1;
public static final String ACTION_REQUEST_PERMISSIONS
= "nodomain.freeyourgadget.gadgetbridge.activities.controlcenter.requestpermissions";
public static final String ACTION_REQUEST_LOCATION_PERMISSIONS
= "nodomain.freeyourgadget.gadgetbridge.activities.controlcenter.requestlocationpermissions";
private boolean isLanguageInvalid = false;
private boolean isThemeInvalid = false;
private ViewPager2 viewPager;
private FragmentStateAdapter pagerAdapter;
private SwipeRefreshLayout swipeLayout;
private static PhoneStateListener fakeStateListener;
private AlertDialog clDialog;
//needed for KK compatibility
@ -140,12 +115,6 @@ public class ControlCenterv2 extends AppCompatActivity
final GBDevice device = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
handleRealtimeSample(device, intent.getSerializableExtra(DeviceService.EXTRA_REALTIME_SAMPLE));
break;
case ACTION_REQUEST_PERMISSIONS:
checkAndRequestPermissions();
break;
case ACTION_REQUEST_LOCATION_PERMISSIONS:
checkAndRequestLocationPermissions();
break;
case GBDevice.ACTION_DEVICE_CHANGED:
GBDevice dev = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
if (dev != null && !dev.isBusy()) {
@ -310,79 +279,21 @@ public class ControlCenterv2 extends AppCompatActivity
filterLocal.addAction(GBApplication.ACTION_THEME_CHANGE);
filterLocal.addAction(GBApplication.ACTION_QUIT);
filterLocal.addAction(DeviceService.ACTION_REALTIME_SAMPLES);
filterLocal.addAction(ACTION_REQUEST_PERMISSIONS);
filterLocal.addAction(ACTION_REQUEST_LOCATION_PERMISSIONS);
filterLocal.addAction(GBDevice.ACTION_DEVICE_CHANGED);
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filterLocal);
/*
* Ask for permission to intercept notifications on first run.
*/
// 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);
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");
if (pesterWithPermissions && !PermissionsUtils.checkAllPermissions(this)) {
Intent permissionsIntent = new Intent(this, PermissionsActivity.class);
startActivity(permissionsIntent);
}
}
/* We not put up dialogs explaining why we need permissions (Polite, but also Play Store policy).
Rather than chaining the calls, we just open a bunch of dialogs. Last in this list = first
on the page, and as they are accepted the permissions are requested in turn.
When accepted, we request it or open the Activity for permission to display over other apps. */
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
/* In order to be able to set ringer mode to silent in GB's PhoneCallReceiver
the permission to access notifications is needed above Android M
ACCESS_NOTIFICATION_POLICY is also needed in the manifest */
if (pesterWithPermissions) {
if (!((NotificationManager) this.getSystemService(Context.NOTIFICATION_SERVICE)).isNotificationPolicyAccessGranted()) {
// Put up a dialog explaining why we need permissions (Polite, but also Play Store policy)
// When accepted, we open the Activity for Notification access
DialogFragment dialog = new NotifyPolicyPermissionsDialogFragment();
dialog.show(getSupportFragmentManager(), "NotifyPolicyPermissionsDialogFragment");
}
}
if (!Settings.canDrawOverlays(getApplicationContext())) {
// If diplay over other apps access hasn't been granted
// Put up a dialog explaining why we need permissions (Polite, but also Play Store policy)
// When accepted, we open the Activity for permission to display over other apps.
if (pesterWithPermissions) {
DialogFragment dialog = new DisplayOverOthersPermissionsDialogFragment();
dialog.show(getSupportFragmentManager(), "DisplayOverOthersPermissionsDialogFragment");
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q &&
ContextCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.ACCESS_BACKGROUND_LOCATION) == PackageManager.PERMISSION_DENIED) {
if (pesterWithPermissions) {
DialogFragment dialog = new LocationPermissionsDialogFragment();
dialog.show(getSupportFragmentManager(), "LocationPermissionsDialogFragment");
}
}
// Check all the other permissions that we need to for Android M + later
if (getWantedPermissions().isEmpty())
displayPermissionDialog = false;
if (displayPermissionDialog && pesterWithPermissions) {
DialogFragment dialog = new PermissionsDialogFragment();
dialog.show(getSupportFragmentManager(), "PermissionsDialogFragment");
// when 'ok' clicked, checkAndRequestPermissions() is called
} else
checkAndRequestPermissions();
}
GBChangeLog cl = GBChangeLog.createChangeLog(this);
boolean showChangelog = prefs.getBoolean("show_changelog", true);
if (showChangelog && cl.isFirstRun() && cl.hasChanges(cl.isFirstRunEver())) {
@ -473,6 +384,10 @@ public class ControlCenterv2 extends AppCompatActivity
}
private void launchWelcomeActivity() {
startActivity(new Intent(this, WelcomeActivity.class));
}
private void launchDiscoveryActivity() {
startActivity(new Intent(this, DiscoveryActivityV2.class));
}
@ -489,145 +404,6 @@ public class ControlCenterv2 extends AppCompatActivity
}
}
private void checkAndRequestLocationPermissions() {
if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.ACCESS_BACKGROUND_LOCATION) != PackageManager.PERMISSION_GRANTED) {
LOG.error("No permission to access background location!");
GB.toast(getString(R.string.error_no_location_access), Toast.LENGTH_SHORT, GB.ERROR);
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_BACKGROUND_LOCATION}, 0);
}
}
@TargetApi(Build.VERSION_CODES.M)
private List<String> getWantedPermissions() {
List<String> wantedPermissions = new ArrayList<>();
if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH) == PackageManager.PERMISSION_DENIED)
wantedPermissions.add(Manifest.permission.BLUETOOTH);
if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_ADMIN) == PackageManager.PERMISSION_DENIED)
wantedPermissions.add(Manifest.permission.BLUETOOTH_ADMIN);
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_DENIED)
wantedPermissions.add(Manifest.permission.READ_CONTACTS);
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE) == PackageManager.PERMISSION_DENIED)
wantedPermissions.add(Manifest.permission.CALL_PHONE);
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CALL_LOG) == PackageManager.PERMISSION_DENIED)
wantedPermissions.add(Manifest.permission.READ_CALL_LOG);
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_DENIED)
wantedPermissions.add(Manifest.permission.READ_PHONE_STATE);
if (ContextCompat.checkSelfPermission(this, Manifest.permission.PROCESS_OUTGOING_CALLS) == PackageManager.PERMISSION_DENIED)
wantedPermissions.add(Manifest.permission.PROCESS_OUTGOING_CALLS);
if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECEIVE_SMS) == PackageManager.PERMISSION_DENIED)
wantedPermissions.add(Manifest.permission.RECEIVE_SMS);
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_SMS) == PackageManager.PERMISSION_DENIED)
wantedPermissions.add(Manifest.permission.READ_SMS);
if (ContextCompat.checkSelfPermission(this, Manifest.permission.SEND_SMS) == PackageManager.PERMISSION_DENIED)
wantedPermissions.add(Manifest.permission.SEND_SMS);
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED)
wantedPermissions.add(Manifest.permission.READ_EXTERNAL_STORAGE);
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CALENDAR) == PackageManager.PERMISSION_DENIED)
wantedPermissions.add(Manifest.permission.READ_CALENDAR);
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_DENIED)
wantedPermissions.add(Manifest.permission.ACCESS_FINE_LOCATION);
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_DENIED)
wantedPermissions.add(Manifest.permission.ACCESS_COARSE_LOCATION);
try {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.MEDIA_CONTENT_CONTROL) == PackageManager.PERMISSION_DENIED)
wantedPermissions.add(Manifest.permission.MEDIA_CONTENT_CONTROL);
} catch (Exception ignored) {
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (pesterWithPermissions) {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ANSWER_PHONE_CALLS) == PackageManager.PERMISSION_DENIED) {
wantedPermissions.add(Manifest.permission.ANSWER_PHONE_CALLS);
}
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && Build.VERSION.SDK_INT <= Build.VERSION_CODES.S) {
if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.ACCESS_BACKGROUND_LOCATION) == PackageManager.PERMISSION_DENIED) {
wantedPermissions.add(Manifest.permission.ACCESS_BACKGROUND_LOCATION);
}
if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_DENIED) {
wantedPermissions.add(Manifest.permission.ACCESS_FINE_LOCATION);
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.QUERY_ALL_PACKAGES) == PackageManager.PERMISSION_DENIED) {
wantedPermissions.add(Manifest.permission.QUERY_ALL_PACKAGES);
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.BLUETOOTH_SCAN) == PackageManager.PERMISSION_DENIED) {
wantedPermissions.add(Manifest.permission.BLUETOOTH_SCAN);
}
if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_DENIED) {
wantedPermissions.add(Manifest.permission.BLUETOOTH_CONNECT);
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_DENIED) {
wantedPermissions.add(Manifest.permission.POST_NOTIFICATIONS);
}
}
if (BuildConfig.INTERNET_ACCESS) {
if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.INTERNET) == PackageManager.PERMISSION_DENIED) {
wantedPermissions.add(Manifest.permission.INTERNET);
}
}
return wantedPermissions;
}
@TargetApi(Build.VERSION_CODES.M)
private void checkAndRequestPermissions() {
List<String> wantedPermissions = getWantedPermissions();
if (!wantedPermissions.isEmpty()) {
Prefs prefs = GBApplication.getPrefs();
// If this is not the first run, we can rely on
// shouldShowRequestPermissionRationale(String permission)
// and ignore permissions that shouldn't or can't be requested again
if (prefs.getBoolean("permissions_asked", false)) {
// Don't request permissions that we shouldn't show a prompt for
// e.g. permissions that are "Never" granted by the user or never granted by the system
Set<String> shouldNotAsk = new HashSet<>();
for (String wantedPermission : wantedPermissions) {
if (!shouldShowRequestPermissionRationale(wantedPermission)) {
shouldNotAsk.add(wantedPermission);
}
}
wantedPermissions.removeAll(shouldNotAsk);
} else {
// Permissions have not been asked yet, but now will be
prefs.getPreferences().edit().putBoolean("permissions_asked", true).apply();
}
if (!wantedPermissions.isEmpty()) {
GB.toast(this, getString(R.string.permission_granting_mandatory), Toast.LENGTH_LONG, GB.ERROR);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
ActivityCompat.requestPermissions(this, wantedPermissions.toArray(new String[0]), 0);
} else {
requestMultiplePermissionsLauncher.launch(wantedPermissions.toArray(new String[0]));
}
}
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { // The enclosed hack in it's current state cause crash on Banglejs builds tarkgetSDK=31 on a Android 13 device.
// HACK: On Lineage we have to do this so that the permission dialog pops up
if (fakeStateListener == null) {
fakeStateListener = new PhoneStateListener();
TelephonyManager telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
telephonyManager.listen(fakeStateListener, PhoneStateListener.LISTEN_CALL_STATE);
telephonyManager.listen(fakeStateListener, PhoneStateListener.LISTEN_NONE);
}
}
}
public void setLanguage(Locale language, boolean invalidateLanguage) {
if (invalidateLanguage) {
isLanguageInvalid = true;
@ -635,137 +411,6 @@ public class ControlCenterv2 extends AppCompatActivity
AndroidUtils.setLanguage(this, language);
}
/// Called from onCreate - this puts up a dialog explaining we need permissions, and goes to the correct Activity
public static class NotifyPolicyPermissionsDialogFragment extends DialogFragment {
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
// Use the Builder class for convenient dialog construction
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getActivity());
final Context context = getContext();
builder.setMessage(context.getString(R.string.permission_notification_policy_access,
getContext().getString(R.string.app_name),
getContext().getString(R.string.ok)))
.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
@RequiresApi(api = Build.VERSION_CODES.M)
public void onClick(DialogInterface dialog, int id) {
try {
startActivity(new Intent(android.provider.Settings.ACTION_NOTIFICATION_POLICY_ACCESS_SETTINGS));
} catch (ActivityNotFoundException e) {
GB.toast(context, "'Notification Policy' activity not found", Toast.LENGTH_LONG, GB.ERROR);
}
}
});
return builder.create();
}
}
/// Called from onCreate - this puts up a dialog explaining we need permissions, and goes to the correct Activity
public static class NotifyListenerPermissionsDialogFragment extends DialogFragment {
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
// Use the Builder class for convenient dialog construction
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getActivity());
final Context context = getContext();
builder.setMessage(context.getString(R.string.permission_notification_listener,
getContext().getString(R.string.app_name),
getContext().getString(R.string.ok)))
.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
try {
startActivity(new Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS"));
} catch (ActivityNotFoundException e) {
GB.toast(context, "'Notification Listener Settings' activity not found", Toast.LENGTH_LONG, GB.ERROR);
}
}
});
return builder.create();
}
}
/// Called from onCreate - this puts up a dialog explaining we need permissions, and goes to the correct Activity
public static class DisplayOverOthersPermissionsDialogFragment extends DialogFragment {
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
// Use the Builder class for convenient dialog construction
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getActivity());
Context context = getContext();
builder.setMessage(context.getString(R.string.permission_display_over_other_apps,
getContext().getString(R.string.app_name),
getContext().getString(R.string.ok)))
.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
@RequiresApi(api = Build.VERSION_CODES.M)
public void onClick(DialogInterface dialog, int id) {
Intent enableIntent = new Intent(android.provider.Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
startActivity(enableIntent);
}
}).setNegativeButton(R.string.dismiss, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {}
});
return builder.create();
}
}
/// Called from onCreate - this puts up a dialog explaining we need backgound location permissions, and then requests permissions when 'ok' pressed
public static class LocationPermissionsDialogFragment extends DialogFragment {
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
// Use the Builder class for convenient dialog construction
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getActivity());
Context context = getContext();
builder.setMessage(context.getString(R.string.permission_location,
getContext().getString(R.string.app_name),
getContext().getString(R.string.ok)))
.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
Intent intent = new Intent(ACTION_REQUEST_LOCATION_PERMISSIONS);
LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent);
}
});
return builder.create();
}
}
// Register the permissions callback, which handles the user's response to the
// system permissions dialog. Save the return value, an instance of
// ActivityResultLauncher, as an instance variable.
// This is required here rather than where it is used because it'll cause a
// "LifecycleOwners must call register before they are STARTED" if not called from onCreate
public ActivityResultLauncher<String[]> requestMultiplePermissionsLauncher =
registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), isGranted -> {
if (isGranted.containsValue(true)) {
// Permission is granted. Continue the action or workflow in your
// app.
} else {
// Explain to the user that the feature is unavailable because the
// feature requires a permission that the user has denied. At the
// same time, respect the user's decision. Don't link to system
// settings in an effort to convince the user to change their
// decision.
GB.toast(this, getString(R.string.permission_granting_mandatory), Toast.LENGTH_LONG, GB.ERROR);
}
});
/// Called from onCreate - this puts up a dialog explaining we need permissions, and then requests permissions when 'ok' pressed
public static class PermissionsDialogFragment extends DialogFragment {
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
// Use the Builder class for convenient dialog construction
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getActivity());
Context context = getContext();
builder.setMessage(context.getString(R.string.permission_request,
getContext().getString(R.string.app_name),
getContext().getString(R.string.ok)))
.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
Intent intent = new Intent(ACTION_REQUEST_PERMISSIONS);
LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent);
}
});
return builder.create();
}
}
private class MainFragmentsPagerAdapter extends FragmentStateAdapter {
public MainFragmentsPagerAdapter(FragmentActivity fa) {
super(fa);

View File

@ -0,0 +1,38 @@
/* Copyright (C) 2024 Arjan Schrijver
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.os.Bundle;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.welcome.WelcomeFragmentPermissions;
public class PermissionsActivity extends AbstractGBActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_permissions);
WelcomeFragmentPermissions permissionsFragment = new WelcomeFragmentPermissions();
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.replace(R.id.fragment_container, permissionsFragment).commit();
}
}

View File

@ -0,0 +1,83 @@
/* Copyright (C) 2024 Arjan Schrijver
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities.welcome;
import android.os.Bundle;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import androidx.viewpager2.widget.ViewPager2;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity;
public class WelcomeActivity extends AbstractGBActivity {
private static final Logger LOG = LoggerFactory.getLogger(WelcomeActivity.class);
private ViewPager2 viewPager;
private WelcomeFragmentsPagerAdapter pagerAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
AbstractGBActivity.init(this, AbstractGBActivity.NO_ACTIONBAR);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_welcome);
if (getSupportActionBar() != null) {
getSupportActionBar().hide();
}
// Configure ViewPager2 with fragment adapter and default fragment
viewPager = findViewById(R.id.welcome_viewpager);
pagerAdapter = new WelcomeFragmentsPagerAdapter(this);
viewPager.setAdapter(pagerAdapter);
// Set up welcome page indicator
WelcomePageIndicator pageIndicator = findViewById(R.id.welcome_page_indicator);
pageIndicator.setViewPager(viewPager);
}
private class WelcomeFragmentsPagerAdapter extends FragmentStateAdapter {
public WelcomeFragmentsPagerAdapter(FragmentActivity fa) {
super(fa);
}
@Override
public Fragment createFragment(int position) {
switch (position) {
case 0:
return new WelcomeFragmentIntro();
case 1:
return new WelcomeFragmentOverview();
case 2:
return new WelcomeFragmentDocsSource();
case 3:
return new WelcomeFragmentPermissions();
default:
return new WelcomeFragmentGetStarted();
}
}
@Override
public int getItemCount() {
return 5;
}
}
}

View File

@ -0,0 +1,42 @@
/* Copyright (C) 2024 Arjan Schrijver
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities.welcome;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.R;
public class WelcomeFragmentDocsSource extends Fragment {
private static final Logger LOG = LoggerFactory.getLogger(WelcomeFragmentDocsSource.class);
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
return inflater.inflate(R.layout.fragment_welcome_docs_source, container, false);
}
}

View File

@ -0,0 +1,61 @@
/* Copyright (C) 2024 Arjan Schrijver
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities.welcome;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.DataManagementActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.discovery.DiscoveryActivityV2;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public class WelcomeFragmentGetStarted extends Fragment {
private static final Logger LOG = LoggerFactory.getLogger(WelcomeFragmentGetStarted.class);
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
View view = inflater.inflate(R.layout.fragment_welcome_get_started, container, false);
Button firstDevice = view.findViewById(R.id.welcome_button_add_device);
firstDevice.setOnClickListener(firstDeviceButton -> startActivity(new Intent(requireActivity(), DiscoveryActivityV2.class)));
Button restore = view.findViewById(R.id.welcome_button_restore);
restore.setOnClickListener(restoreButton -> startActivity(new Intent(requireActivity(), DataManagementActivity.class)));
Button toApp = view.findViewById(R.id.welcome_button_to_app);
toApp.setOnClickListener(toAppButton -> {
Prefs prefs = GBApplication.getPrefs();
prefs.getPreferences().edit().putBoolean("first_run", false).apply();
requireActivity().finish();
});
return view;
}
}

View File

@ -0,0 +1,74 @@
/* Copyright (C) 2024 Arjan Schrijver
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities.welcome;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.os.Handler;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import com.google.android.material.textfield.MaterialAutoCompleteTextView;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Arrays;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public class WelcomeFragmentIntro extends Fragment {
private static final Logger LOG = LoggerFactory.getLogger(WelcomeFragmentIntro.class);
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
final View view = inflater.inflate(R.layout.fragment_welcome_intro, container, false);
final String[] themes = getResources().getStringArray(R.array.pref_theme_values);
final Prefs prefs = GBApplication.getPrefs();
final String currentTheme = prefs.getString("pref_key_theme", getString(R.string.pref_theme_value_system));
final int currentThemeIndex = Arrays.asList(themes).indexOf(currentTheme);
final MaterialAutoCompleteTextView themeMenu = view.findViewById(R.id.app_theme_dropdown_menu);
themeMenu.setSaveEnabled(false); // https://github.com/material-components/material-components-android/issues/1464#issuecomment-1258051448
themeMenu.setText(getResources().getStringArray(R.array.pref_theme_options)[currentThemeIndex], false);
themeMenu.setOnItemClickListener((adapterView, view1, i, l) -> {
final SharedPreferences.Editor editor = prefs.getPreferences().edit();
editor.putString("pref_key_theme", themes[i]).apply();
final Handler handler = new Handler();
handler.postDelayed(() -> {
// Delay recreation of the Activity to give the dropdown some time to settle.
// If we recreate it immediately, the theme popup will reopen, which is not what the user expects.
Intent intent = new Intent();
intent.setAction(GBApplication.ACTION_THEME_CHANGE);
LocalBroadcastManager.getInstance(requireContext()).sendBroadcast(intent);
}, 500);
});
return view;
}
}

View File

@ -0,0 +1,42 @@
/* Copyright (C) 2024 Arjan Schrijver
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities.welcome;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.R;
public class WelcomeFragmentOverview extends Fragment {
private static final Logger LOG = LoggerFactory.getLogger(WelcomeFragmentOverview.class);
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
return inflater.inflate(R.layout.fragment_welcome_overview, container, false);
}
}

View File

@ -0,0 +1,173 @@
/* Copyright (C) 2024 Arjan Schrijver
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities.welcome;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.util.PermissionsUtils;
public class WelcomeFragmentPermissions extends Fragment {
private static final Logger LOG = LoggerFactory.getLogger(WelcomeFragmentPermissions.class);
private RecyclerView permissionsListView;
private PermissionAdapter permissionAdapter;
private Button requestAllButton;
private List<String> requestingPermissions = new ArrayList<>();
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
View view = inflater.inflate(R.layout.fragment_welcome_permissions, container, false);
requestAllButton = view.findViewById(R.id.button_request_all);
requestAllButton.setOnClickListener(v -> {
List<PermissionsUtils.PermissionDetails> wantedPermissions = PermissionsUtils.getRequiredPermissionsList(requireActivity());
requestingPermissions = new ArrayList<>();
for (PermissionsUtils.PermissionDetails wantedPermission : wantedPermissions) {
requestingPermissions.add(wantedPermission.getPermission());
}
requestAllPermissions();
});
if (((AppCompatActivity)getActivity()).getSupportActionBar().isShowing()) {
// Hide title when the Action Bar is visible (i.e. when not in the first run flow)
view.findViewById(R.id.permissions_title).setVisibility(View.GONE);
}
// Initialize RecyclerView and data
permissionsListView = view.findViewById(R.id.permissions_list);
// Set up RecyclerView
permissionAdapter = new PermissionAdapter(PermissionsUtils.getRequiredPermissionsList(requireActivity()), requireContext());
permissionsListView.setLayoutManager(new LinearLayoutManager(requireContext()));
permissionsListView.setAdapter(permissionAdapter);
return view;
}
@Override
public void onResume() {
super.onResume();
permissionAdapter.notifyDataSetChanged();
if (PermissionsUtils.checkAllPermissions(requireActivity())) {
requestAllButton.setEnabled(false);
}
if (!requestingPermissions.isEmpty()) {
requestAllPermissions();
}
}
public void requestAllPermissions() {
if (!requestingPermissions.isEmpty()) {
Iterator<String> it = requestingPermissions.iterator();
while (it.hasNext()) {
String currentPermission = it.next();
if (PermissionsUtils.specialPermissions.contains(currentPermission)) {
it.remove();
if (!PermissionsUtils.checkPermission(requireActivity(), currentPermission)) {
PermissionsUtils.requestPermission(requireActivity(), currentPermission);
return;
}
}
}
String[] combinedPermissions = requestingPermissions.toArray(new String[0]);
requestingPermissions.clear();
ActivityCompat.requestPermissions(requireActivity(), combinedPermissions, 0);
}
}
private class PermissionHolder extends RecyclerView.ViewHolder {
TextView titleTextView;
TextView summaryTextView;
ImageView checkmarkImageView;
Button requestButton;
public PermissionHolder(View itemView) {
super(itemView);
titleTextView = itemView.findViewById(R.id.permission_title);
summaryTextView = itemView.findViewById(R.id.permission_summary);
checkmarkImageView = itemView.findViewById(R.id.permission_check);
requestButton = itemView.findViewById(R.id.permission_request);
}
}
private class PermissionAdapter extends RecyclerView.Adapter<PermissionHolder> {
private List<PermissionsUtils.PermissionDetails> permissionList;
private Context context;
public PermissionAdapter(List<PermissionsUtils.PermissionDetails> permissionList, Context context) {
this.permissionList = permissionList;
this.context = context;
}
@NonNull
@Override
public PermissionHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View itemView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.fragment_welcome_permission_row, parent, false);
return new PermissionHolder(itemView);
}
@Override
public void onBindViewHolder(@NonNull PermissionHolder holder, int position) {
PermissionsUtils.PermissionDetails permissionData = permissionList.get(position);
holder.titleTextView.setText(permissionData.getTitle());
holder.summaryTextView.setText(permissionData.getSummary());
if (PermissionsUtils.checkPermission(requireContext(), permissionData.getPermission())) {
holder.requestButton.setVisibility(View.INVISIBLE);
holder.requestButton.setEnabled(false);
holder.checkmarkImageView.setVisibility(View.VISIBLE);
} else {
holder.requestButton.setVisibility(View.VISIBLE);
holder.requestButton.setEnabled(true);
holder.checkmarkImageView.setVisibility(View.GONE);
holder.requestButton.setOnClickListener(view -> {
PermissionsUtils.requestPermission(requireActivity(), permissionData.getPermission());
});
}
}
@Override
public int getItemCount() {
return permissionList.size();
}
}
}

View File

@ -0,0 +1,135 @@
/* Copyright (C) 2024 Arjan Schrijver
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities.welcome;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.Nullable;
import androidx.viewpager2.widget.ViewPager2;
import nodomain.freeyourgadget.gadgetbridge.R;
public class WelcomePageIndicator extends View {
private ViewPager2 viewPager;
private int pageCount;
private int dotRadius = 15;
private int color;
private Paint outlinePaint;
private Paint filledPaint;
private float currentX = 0.0f;
private ValueAnimator dotAnimator;
public WelcomePageIndicator(Context context) {
super(context);
init();
}
public WelcomePageIndicator(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
determineColor(context, attrs);
init();
}
public WelcomePageIndicator(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
determineColor(context, attrs);
init();
}
private void determineColor(Context context, @Nullable AttributeSet attrs) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.WelcomePageIndicator);
color = a.getColor(R.styleable.WelcomePageIndicator_page_indicator_color, Color.BLACK);
a.recycle();
}
private void init() {
outlinePaint = new Paint();
outlinePaint.setColor(color);
outlinePaint.setStyle(Paint.Style.STROKE);
outlinePaint.setStrokeWidth(4);
outlinePaint.setAntiAlias(true);
filledPaint = new Paint();
filledPaint.setColor(color);
filledPaint.setStyle(Paint.Style.FILL);
outlinePaint.setAntiAlias(true);
}
public void setViewPager(ViewPager2 viewPager) {
this.viewPager = viewPager;
this.pageCount = viewPager.getAdapter().getItemCount();
viewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
@Override
public void onPageSelected(int position) {
animateIndicator(position);
}
});
invalidate();
}
private int getHorizontalMargin() {
int dotDiameter = dotRadius * 2;
int dotSpaces = pageCount * 2 - 1;
return (getWidth() - dotSpaces * dotDiameter) / 2 + dotRadius;
}
private void animateIndicator(int position) {
float horizontalMargin = getHorizontalMargin();
if (horizontalMargin <= 0.0f) {
// Not animating because the drawable is not ready yet
return;
}
float targetX = horizontalMargin + 4 * dotRadius * position;
if (dotAnimator != null && dotAnimator.isRunning()) {
dotAnimator.cancel();
}
if (currentX == 0.0f) currentX = horizontalMargin;
dotAnimator = ValueAnimator.ofFloat(currentX, targetX);
dotAnimator.addUpdateListener(animation -> {
currentX = (float) animation.getAnimatedValue();
invalidate();
});
dotAnimator.setDuration(300);
dotAnimator.start();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (viewPager == null || pageCount == 0) {
return;
}
float horizontalMargin = getHorizontalMargin();
if (currentX == 0.0f && horizontalMargin != 0.0f) currentX = horizontalMargin;
float circleY = getHeight() / 2f;
for (int i = 0; i < pageCount; i++) {
float circleX = horizontalMargin + 4 * dotRadius * i;
canvas.drawCircle(circleX, circleY, dotRadius, outlinePaint);
}
canvas.drawCircle(currentX, circleY, dotRadius, filledPaint);
}
}

View File

@ -0,0 +1,328 @@
/* Copyright (C) 2024 Arjan Schrijver
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.util;
import android.Manifest;
import android.app.Activity;
import android.app.NotificationManager;
import android.content.ActivityNotFoundException;
import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.widget.Toast;
import androidx.annotation.RequiresApi;
import androidx.core.app.ActivityCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.content.ContextCompat;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import nodomain.freeyourgadget.gadgetbridge.BuildConfig;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.externalevents.NotificationListener;
public class PermissionsUtils {
private static final Logger LOG = LoggerFactory.getLogger(PermissionsUtils.class);
public static final String CUSTOM_PERM_NOTIFICATION_LISTENER = "custom_perm_notifications_listener";
public static final String CUSTOM_PERM_NOTIFICATION_SERVICE = "custom_perm_notifications_service";
public static final String CUSTOM_PERM_DISPLAY_OVER = "custom_perm_display_over";
public static final List<String> specialPermissions = new ArrayList<String>() {{
add(CUSTOM_PERM_NOTIFICATION_LISTENER);
add(CUSTOM_PERM_NOTIFICATION_SERVICE);
add(CUSTOM_PERM_DISPLAY_OVER);
add(Manifest.permission.ACCESS_FINE_LOCATION);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
add(Manifest.permission.ACCESS_BACKGROUND_LOCATION);
}
}};
public static ArrayList<PermissionDetails> getRequiredPermissionsList(Activity activity) {
ArrayList<PermissionDetails> permissionsList = new ArrayList<>();
permissionsList.add(new PermissionDetails(
CUSTOM_PERM_NOTIFICATION_LISTENER,
activity.getString(R.string.menuitem_notifications),
activity.getString(R.string.permission_notifications_summary)));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
permissionsList.add(new PermissionDetails(
CUSTOM_PERM_NOTIFICATION_SERVICE,
activity.getString(R.string.permission_manage_dnd_title),
activity.getString(R.string.permission_manage_dnd_summary)));
permissionsList.add(new PermissionDetails(
CUSTOM_PERM_DISPLAY_OVER,
activity.getString(R.string.permission_displayover_title),
activity.getString(R.string.permission_displayover_summary)));
}
permissionsList.add(new PermissionDetails(
Manifest.permission.ACCESS_FINE_LOCATION,
activity.getString(R.string.permission_fine_location_title),
activity.getString(R.string.permission_fine_location_summary)));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
permissionsList.add(new PermissionDetails(
Manifest.permission.ACCESS_BACKGROUND_LOCATION,
activity.getString(R.string.permission_background_location_title),
activity.getString(R.string.permission_background_location_summary)));
}
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
permissionsList.add(new PermissionDetails(
Manifest.permission.BLUETOOTH,
activity.getString(R.string.permission_bluetooth_title),
activity.getString(R.string.permission_bluetooth_summary)));
permissionsList.add(new PermissionDetails(
Manifest.permission.BLUETOOTH_ADMIN,
activity.getString(R.string.permission_bluetooth_admin_title),
activity.getString(R.string.permission_bluetooth_admin_summary)));
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
permissionsList.add(new PermissionDetails(
Manifest.permission.BLUETOOTH_SCAN,
activity.getString(R.string.permission_bluetooth_scan_title),
activity.getString(R.string.permission_bluetooth_scan_summary)));
permissionsList.add(new PermissionDetails(
Manifest.permission.BLUETOOTH_CONNECT,
activity.getString(R.string.permission_bluetooth_connect_title),
activity.getString(R.string.permission_bluetooth_connect_summary)));
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
permissionsList.add(new PermissionDetails(
Manifest.permission.POST_NOTIFICATIONS,
activity.getString(R.string.permission_post_notification_title),
activity.getString(R.string.permission_post_notification_summary)));
}
if (BuildConfig.INTERNET_ACCESS) {
permissionsList.add(new PermissionDetails(
Manifest.permission.INTERNET,
activity.getString(R.string.permission_internet_access_title),
activity.getString(R.string.permission_internet_access_summary)));
}
// permissionsList.add(new PermissionDetails( // NOTE: can't request this, it's only allowed for system apps
// Manifest.permission.MEDIA_CONTENT_CONTROL,
// "Media content control",
// "Read and control media playback"));
permissionsList.add(new PermissionDetails(
Manifest.permission.READ_CONTACTS,
activity.getString(R.string.permission_contacts_title),
activity.getString(R.string.permission_contacts_summary)));
permissionsList.add(new PermissionDetails(
Manifest.permission.READ_CALENDAR,
activity.getString(R.string.permission_calendar_title),
activity.getString(R.string.permission_calendar_summary)));
permissionsList.add(new PermissionDetails(
Manifest.permission.RECEIVE_SMS,
activity.getString(R.string.permission_receive_sms_title),
activity.getString(R.string.permission_receive_sms_summary)));
permissionsList.add(new PermissionDetails(
Manifest.permission.SEND_SMS,
activity.getString(R.string.permission_send_sms_title),
activity.getString(R.string.permission_send_sms_summary)));
permissionsList.add(new PermissionDetails(
Manifest.permission.READ_CALL_LOG,
activity.getString(R.string.permission_read_call_log_title),
activity.getString(R.string.permission_read_call_log_summary)));
permissionsList.add(new PermissionDetails(
Manifest.permission.READ_PHONE_STATE,
activity.getString(R.string.permission_read_phone_state_title),
activity.getString(R.string.permission_read_phone_state_summary)));
permissionsList.add(new PermissionDetails(
Manifest.permission.CALL_PHONE,
activity.getString(R.string.permission_call_phone_title),
activity.getString(R.string.permission_call_phone_summary)));
permissionsList.add(new PermissionDetails(
Manifest.permission.PROCESS_OUTGOING_CALLS,
activity.getString(R.string.permission_process_outgoing_calls_title),
activity.getString(R.string.permission_process_outgoing_calls_summary)));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
permissionsList.add(new PermissionDetails(
Manifest.permission.ANSWER_PHONE_CALLS,
activity.getString(R.string.permission_answer_phone_calls_title),
activity.getString(R.string.permission_answer_phone_calls_summary)));
}
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
permissionsList.add(new PermissionDetails(
Manifest.permission.READ_EXTERNAL_STORAGE,
activity.getString(R.string.permission_external_storage_title),
activity.getString(R.string.permission_external_storage_summary)));
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
permissionsList.add(new PermissionDetails(
Manifest.permission.QUERY_ALL_PACKAGES,
activity.getString(R.string.permission_query_all_packages_title),
activity.getString(R.string.permission_query_all_packages_summary)));
}
return permissionsList;
}
public static boolean checkPermission(Context context, String permission) {
if (permission.equals(CUSTOM_PERM_NOTIFICATION_LISTENER)) {
Set<String> set = NotificationManagerCompat.getEnabledListenerPackages(context);
return set.contains(context.getPackageName());
} else if (permission.equals(CUSTOM_PERM_NOTIFICATION_SERVICE) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE)).isNotificationPolicyAccessGranted();
} else if (permission.equals(CUSTOM_PERM_DISPLAY_OVER) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return Settings.canDrawOverlays(context);
} else {
return ContextCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_DENIED;
}
}
public static boolean checkAllPermissions(Activity activity) {
boolean result = true;
for (PermissionDetails permission : getRequiredPermissionsList(activity)) {
if (!checkPermission(activity, permission.getPermission())) {
result = false;
}
}
return result;
}
public static void requestPermission(Activity activity, String permission) {
if (permission.equals(CUSTOM_PERM_NOTIFICATION_LISTENER)) {
showNotifyListenerPermissionsDialog(activity);
} else if (permission.equals(CUSTOM_PERM_NOTIFICATION_SERVICE) && (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)) {
showNotifyPolicyPermissionsDialog(activity);
} else if (permission.equals(CUSTOM_PERM_DISPLAY_OVER)) {
showDisplayOverOthersPermissionsDialog(activity);
} else if (permission.equals(Manifest.permission.ACCESS_BACKGROUND_LOCATION) && (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)) {
showBackgroundLocationPermissionsDialog(activity);
} else {
ActivityCompat.requestPermissions(activity, new String[]{permission}, 0);
}
}
public static class PermissionDetails {
private String permission;
private String title;
private String summary;
public PermissionDetails(String permission, String title, String summary) {
this.permission = permission;
this.title = title;
this.summary = summary;
}
public String getPermission() {
return permission;
}
public String getTitle() {
return title;
}
public String getSummary() {
return summary;
}
}
private static void showNotifyListenerPermissionsDialog(Activity activity) {
new MaterialAlertDialogBuilder(activity)
.setMessage(activity.getString(R.string.permission_notification_listener,
activity.getString(R.string.app_name),
activity.getString(R.string.ok)))
.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP_MR1)
public void onClick(DialogInterface dialog, int id) {
try {
Intent intent;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
intent = new Intent(Settings.ACTION_NOTIFICATION_LISTENER_DETAIL_SETTINGS);
intent.putExtra(Settings.EXTRA_NOTIFICATION_LISTENER_COMPONENT_NAME, new ComponentName(BuildConfig.APPLICATION_ID, NotificationListener.class.getName()).flattenToString());
} else {
intent = new Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS);
}
String showArgs = BuildConfig.APPLICATION_ID + "/" + NotificationListener.class.getName();
intent.putExtra(":settings:fragment_args_key", showArgs);
Bundle bundle = new Bundle();
bundle.putString(":settings:fragment_args_key", showArgs);
intent.putExtra(":settings:show_fragment_args", bundle);
activity.startActivity(intent);
} catch (ActivityNotFoundException e) {
GB.toast(activity, "'Notification Listener Settings' activity not found", Toast.LENGTH_LONG, GB.ERROR);
LOG.error("'Notification Listener Settings' activity not found");
}
}
})
.show();
}
private static void showNotifyPolicyPermissionsDialog(Activity activity) {
new MaterialAlertDialogBuilder(activity)
.setMessage(activity.getString(R.string.permission_notification_policy_access,
activity.getString(R.string.app_name),
activity.getString(R.string.ok)))
.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
@RequiresApi(api = Build.VERSION_CODES.M)
public void onClick(DialogInterface dialog, int id) {
try {
activity.startActivity(new Intent(android.provider.Settings.ACTION_NOTIFICATION_POLICY_ACCESS_SETTINGS));
} catch (ActivityNotFoundException e) {
GB.toast(activity, "'Notification Policy' activity not found", Toast.LENGTH_LONG, GB.ERROR);
LOG.error("'Notification Policy' activity not found");
}
}
})
.show();
}
private static void showDisplayOverOthersPermissionsDialog(Activity activity) {
new MaterialAlertDialogBuilder(activity)
.setMessage(activity.getString(R.string.permission_display_over_other_apps,
activity.getString(R.string.app_name),
activity.getString(R.string.ok)))
.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
@RequiresApi(api = Build.VERSION_CODES.M)
public void onClick(DialogInterface dialog, int id) {
Intent enableIntent = new Intent(
android.provider.Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:" + BuildConfig.APPLICATION_ID)
);
activity.startActivity(enableIntent);
}
})
.setNegativeButton(R.string.dismiss, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
}
})
.show();
}
@RequiresApi(api = Build.VERSION_CODES.R)
private static void showBackgroundLocationPermissionsDialog(Activity activity) {
new MaterialAlertDialogBuilder(activity)
.setMessage(activity.getString(R.string.permission_location,
activity.getString(R.string.app_name),
activity.getPackageManager().getBackgroundPermissionOptionLabel()))
.setPositiveButton(R.string.ok, (dialog, id) -> {
ActivityCompat.requestPermissions(activity, new String[]{Manifest.permission.ACCESS_BACKGROUND_LOCATION}, 0);
})
.show();
}
}

View File

@ -0,0 +1,10 @@
<?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">
<FrameLayout
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</RelativeLayout>

View File

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

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="50dp"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAlignment="center"
android:text="@string/first_start_open_source_title"
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="@string/first_start_open_source_text" />
<ImageView
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_marginTop="30dp"
android:src="@drawable/ic_engineering"
app:tint="?attr/colorPrimary" />
</LinearLayout>
</ScrollView>
</RelativeLayout>

View File

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_marginHorizontal="50dp"
android:orientation="vertical">
<TextView
android:id="@+id/intro_gadgetbridge"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAlignment="center"
android:text="@string/first_start_get_started_title"
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="@string/first_start_get_started_desc" />
<Button
android:id="@+id/welcome_button_add_device"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:text="@string/first_start_get_started_add_first_device_button" />
<Button
android:id="@+id/welcome_button_restore"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:text="@string/first_start_get_started_restore_button" />
<Button
android:id="@+id/welcome_button_to_app"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:text="@string/first_start_get_started_go_to_app_button" />
</LinearLayout>
</RelativeLayout>

View File

@ -0,0 +1,61 @@
<?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">
<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">
<ImageView
android:layout_width="200dp"
android:layout_height="200dp"
android:background="@color/accent"
android:src="@drawable/ic_launcher_foreground" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:textAlignment="center"
android:text="@string/first_start_intro_welcome_to"
android:textSize="30sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAlignment="center"
android:text="@string/app_name"
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="@string/first_start_intro_tag_line" />
<com.google.android.material.textfield.TextInputLayout
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:id="@+id/app_theme_dropdown_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:hint="@string/pref_title_theme">
<AutoCompleteTextView
android:id="@+id/app_theme_dropdown_menu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none"
app:simpleItems="@array/pref_theme_options" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</RelativeLayout>

View File

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="50dp"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAlignment="center"
android:text="@string/first_start_overview_title"
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="@string/first_start_overview_desc" />
<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="@string/first_start_overview_dashboard" />
<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="@string/first_start_overview_devices" />
</LinearLayout>
</ScrollView>
</RelativeLayout>

View File

@ -0,0 +1,48 @@
<?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_marginStart="10dp"
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="Placeholder: Permission name" />
<TextView
android:id="@+id/permission_summary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Placeholder: 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="@string/first_start_permissions_request_button" />
<ImageView
android:id="@+id/permission_check"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="20dp"
android:src="@drawable/cpv_preset_checked"
android:visibility="gone"
app:tint="@android:color/holo_green_dark"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,38 @@
<?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:id="@+id/permissions_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:textAlignment="center"
android:text="@string/first_start_permissions_title"
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="@string/first_start_permissions_desc" />
<Button
android:id="@+id/button_request_all"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="@string/first_start_permissions_request_all_button"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/permissions_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp" />
</LinearLayout>

View File

@ -2124,7 +2124,7 @@
<string name="permission_granting_mandatory">All these permissions are required and instability might occur if not granted</string>
<string name="permission_notification_listener">%1$s needs access to Notifications in order to display them on your watch when your phone\'s screen is off.\n\nPlease tap \'%2$s\' then \'%1$s\' and enable \'Allow Notification Access\', then tap \'Back\' to return to %1$s.</string>
<string name="permission_notification_policy_access">%1$s needs access to Do Not Disturb settings in order to honour them on your watch when your phone\'s screen is off.\n\nPlease tap \'%2$s\' then \'%1$s\' and enable \'Allow Do Not Disturb\', then tap \'Back\' to return to %1$s.</string>
<string name="permission_location">%1$s needs access to your location in the background to allow it to stay connected to your watch even when your screen is off.\n\nPlease tap \'%2$s\' to agree.</string>
<string name="permission_location">%1$s needs access to your location in the background to allow it to stay connected to your watch even when your screen is off.\n\nPlease choose \'%2$s\' in the following screen, then tap \'Back\' to return to %1$s.</string>
<string name="permission_display_over_other_apps">%1$s needs permission to display over other apps in order to let Bangle.js watches start activities via intents when %1$s is in the background.\n\nThis can be used to start a music app and play a song, and many other things.\n\nPlease tap \'%2$s\' then \'%1$s\' and enable \'Allow display over other apps\', then tap \'Back\' to return to %1$s.\n\nTo stop %1$s asking for permissions go to \'Settings\' and uncheck \'Check permission status\'.\n\nMake sure to grant %1$s the permissions needed to function as you expect.</string>
<string name="error_version_check_extreme_caution">CAUTION: Error when checking version information! You should not continue! Saw version name \"%s\"</string>
<string name="require_location_provider">Location must be enabled</string>
@ -3386,4 +3386,67 @@
<string name="inactivity_warnings_minimum_steps_summary">Minimum amount of steps that need to be taken during the threshold minutes</string>
<string name="prefs_hrv_monitoring_title">HRV monitoring</string>
<string name="prefs_hrv_monitoring_description">Automatically monitor heart rate variability throughout the day</string>
<!-- Welcome screens strings -->
<string name="first_start_welcome_title">Welcome</string>
<string name="first_start_intro_welcome_to">Welcome to</string>
<string name="first_start_intro_tag_line">Break free from the proprietary apps and cloud services of gadget vendors.</string>
<string name="first_start_overview_title">Overview</string>
<string name="first_start_overview_desc">Gadgetbridge has two main views, each with their own purpose.</string>
<string name="first_start_overview_dashboard">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.</string>
<string name="first_start_overview_devices">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.</string>
<string name="first_start_open_source_title">Open Source</string>
<string name="first_start_open_source_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 keeps your data 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.</string>
<string name="first_start_permissions_title">Permissions</string>
<string name="first_start_permissions_desc">Gadgetbridge needs a lot of permissions to perform all its functions. Review the permissions and their purposes below.</string>
<string name="first_start_permissions_request_all_button">Request all permissions</string>
<string name="first_start_permissions_request_button">Request</string>
<string name="first_start_get_started_title">Get started</string>
<string name="first_start_get_started_desc">To get started, add your first device directly from this screen, restore a backup or start with a clean database.</string>
<string name="first_start_get_started_add_first_device_button">Add first device</string>
<string name="first_start_get_started_restore_button">Restore backup</string>
<string name="first_start_get_started_go_to_app_button">Go to the app</string>
<string name="permission_notifications_summary">Forwarding notifications to connected gadgets</string>
<string name="permission_manage_dnd_title">Manage Do Not Disturb</string>
<string name="permission_manage_dnd_summary">Changing DND notification policy</string>
<string name="permission_displayover_title">Display over other apps</string>
<string name="permission_displayover_summary">Used by Bangle.js for starting apps and other functionality on your phone</string>
<string name="permission_fine_location_title">Fine location</string>
<string name="permission_fine_location_summary">Scanning for Bluetooth devices</string>
<string name="permission_background_location_title">Background location</string>
<string name="permission_background_location_summary">Scanning for Bluetooth devices in the background and sending the location to certain gadgets</string>
<string name="permission_bluetooth_title">Bluetooth</string>
<string name="permission_bluetooth_summary">Connecting to Bluetooth devices</string>
<string name="permission_bluetooth_admin_title">Bluetooth admin</string>
<string name="permission_bluetooth_admin_summary">Discovering and pairing Bluetooth devices</string>
<string name="permission_bluetooth_scan_title">Bluetooth scan</string>
<string name="permission_bluetooth_scan_summary">Scanning for new Bluetooth devices</string>
<string name="permission_bluetooth_connect_title">Bluetooth connect</string>
<string name="permission_bluetooth_connect_summary">Connecting to already-paired Bluetooth devices</string>
<string name="permission_post_notification_title">Post notifications</string>
<string name="permission_post_notification_summary">Posting ongoing notification which keeps the service running</string>
<string name="permission_internet_access_title">Internet access</string>
<string name="permission_internet_access_summary">Synchronization with online resources</string>
<string name="permission_contacts_title">Contacts</string>
<string name="permission_contacts_summary">Sending contacts to gadgets</string>
<string name="permission_calendar_title">Calendar</string>
<string name="permission_calendar_summary">Sending calendar to gadgets</string>
<string name="permission_receive_sms_title">Receive SMS</string>
<string name="permission_receive_sms_summary">Forwarding SMS messages to gadgets</string>
<string name="permission_send_sms_title">Send SMS</string>
<string name="permission_send_sms_summary">Sending SMS (canned response) from gadgets</string>
<string name="permission_read_call_log_title">Read call log</string>
<string name="permission_read_call_log_summary">Forwarding call log to gadgets</string>
<string name="permission_read_phone_state_title">Read phone state</string>
<string name="permission_read_phone_state_summary">Reading status of ongoing calls</string>
<string name="permission_call_phone_title">Call phone</string>
<string name="permission_call_phone_summary">Initiating phone calls from gadgets</string>
<string name="permission_process_outgoing_calls_title">Process outgoing calls</string>
<string name="permission_process_outgoing_calls_summary">Reading the number of an outgoing call to display it on a gadget</string>
<string name="permission_answer_phone_calls_title">Answer phone calls</string>
<string name="permission_answer_phone_calls_summary">Answering phone calls from gadgets</string>
<string name="permission_external_storage_title">External storage</string>
<string name="permission_external_storage_summary">Using images, ringtones, app files and more</string>
<string name="permission_query_all_packages_title">Query all packages</string>
<string name="permission_query_all_packages_summary">Reading names and icons of all installed apps</string>
</resources>

View File

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