diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index b5391c011..6ef79be09 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -150,6 +150,14 @@
+
+ set = NotificationManagerCompat.getEnabledListenerPackages(this);
- if (pesterWithPermissions) {
- if (!set.contains(this.getPackageName())) { // If notification listener access hasn't been granted
- // Put up a dialog explaining why we need permissions (Polite, but also Play Store policy)
- // When accepted, we open the Activity for Notification access
- DialogFragment dialog = new NotifyListenerPermissionsDialogFragment();
- dialog.show(getSupportFragmentManager(), "NotifyListenerPermissionsDialogFragment");
+ // Open the Welcome flow on first run, only check permissions on next runs
+ boolean firstRun = prefs.getBoolean("first_run", true);
+ if (firstRun) {
+ launchWelcomeActivity();
+ } else {
+ pesterWithPermissions = prefs.getBoolean("permission_pestering", true);
+ if (pesterWithPermissions && !PermissionsUtils.checkAllPermissions(this)) {
+ Intent permissionsIntent = new Intent(this, PermissionsActivity.class);
+ startActivity(permissionsIntent);
}
}
- /* We not put up dialogs explaining why we need permissions (Polite, but also Play Store policy).
-
- Rather than chaining the calls, we just open a bunch of dialogs. Last in this list = first
- on the page, and as they are accepted the permissions are requested in turn.
-
- When accepted, we request it or open the Activity for permission to display over other apps. */
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- /* In order to be able to set ringer mode to silent in GB's PhoneCallReceiver
- the permission to access notifications is needed above Android M
- ACCESS_NOTIFICATION_POLICY is also needed in the manifest */
- if (pesterWithPermissions) {
- if (!((NotificationManager) this.getSystemService(Context.NOTIFICATION_SERVICE)).isNotificationPolicyAccessGranted()) {
- // Put up a dialog explaining why we need permissions (Polite, but also Play Store policy)
- // When accepted, we open the Activity for Notification access
- DialogFragment dialog = new NotifyPolicyPermissionsDialogFragment();
- dialog.show(getSupportFragmentManager(), "NotifyPolicyPermissionsDialogFragment");
- }
- }
-
- if (!Settings.canDrawOverlays(getApplicationContext())) {
- // If diplay over other apps access hasn't been granted
- // Put up a dialog explaining why we need permissions (Polite, but also Play Store policy)
- // When accepted, we open the Activity for permission to display over other apps.
- if (pesterWithPermissions) {
- DialogFragment dialog = new DisplayOverOthersPermissionsDialogFragment();
- dialog.show(getSupportFragmentManager(), "DisplayOverOthersPermissionsDialogFragment");
- }
- }
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q &&
- ContextCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.ACCESS_BACKGROUND_LOCATION) == PackageManager.PERMISSION_DENIED) {
- if (pesterWithPermissions) {
- DialogFragment dialog = new LocationPermissionsDialogFragment();
- dialog.show(getSupportFragmentManager(), "LocationPermissionsDialogFragment");
- }
- }
-
- // Check all the other permissions that we need to for Android M + later
- if (getWantedPermissions().isEmpty())
- displayPermissionDialog = false;
- if (displayPermissionDialog && pesterWithPermissions) {
- DialogFragment dialog = new PermissionsDialogFragment();
- dialog.show(getSupportFragmentManager(), "PermissionsDialogFragment");
- // when 'ok' clicked, checkAndRequestPermissions() is called
- } else
- checkAndRequestPermissions();
- }
-
GBChangeLog cl = GBChangeLog.createChangeLog(this);
boolean showChangelog = prefs.getBoolean("show_changelog", true);
if (showChangelog && cl.isFirstRun() && cl.hasChanges(cl.isFirstRunEver())) {
@@ -473,6 +384,10 @@ public class ControlCenterv2 extends AppCompatActivity
}
+ private void launchWelcomeActivity() {
+ startActivity(new Intent(this, WelcomeActivity.class));
+ }
+
private void launchDiscoveryActivity() {
startActivity(new Intent(this, DiscoveryActivityV2.class));
}
@@ -489,145 +404,6 @@ public class ControlCenterv2 extends AppCompatActivity
}
}
- private void checkAndRequestLocationPermissions() {
- if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.ACCESS_BACKGROUND_LOCATION) != PackageManager.PERMISSION_GRANTED) {
- LOG.error("No permission to access background location!");
- GB.toast(getString(R.string.error_no_location_access), Toast.LENGTH_SHORT, GB.ERROR);
- ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_BACKGROUND_LOCATION}, 0);
- }
- }
-
- @TargetApi(Build.VERSION_CODES.M)
- private List getWantedPermissions() {
- List 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 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 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 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);
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/PermissionsActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/PermissionsActivity.java
new file mode 100644
index 000000000..22713c3df
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/PermissionsActivity.java
@@ -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 . */
+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();
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/SettingsActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/SettingsActivity.java
index 4483dc172..527e3c230 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/SettingsActivity.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/SettingsActivity.java
@@ -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.externalevents.TimeChangeReceiver;
import nodomain.freeyourgadget.gadgetbridge.model.Weather;
@@ -437,6 +438,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 -> {
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/welcome/WelcomeActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/welcome/WelcomeActivity.java
new file mode 100644
index 000000000..b44f8ba8b
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/welcome/WelcomeActivity.java
@@ -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 . */
+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;
+ }
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/welcome/WelcomeFragmentDocsSource.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/welcome/WelcomeFragmentDocsSource.java
new file mode 100644
index 000000000..ca5130ce8
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/welcome/WelcomeFragmentDocsSource.java
@@ -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 . */
+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);
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/welcome/WelcomeFragmentGetStarted.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/welcome/WelcomeFragmentGetStarted.java
new file mode 100644
index 000000000..2de419872
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/welcome/WelcomeFragmentGetStarted.java
@@ -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 . */
+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;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/welcome/WelcomeFragmentIntro.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/welcome/WelcomeFragmentIntro.java
new file mode 100644
index 000000000..513b930f5
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/welcome/WelcomeFragmentIntro.java
@@ -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 . */
+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);
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/welcome/WelcomeFragmentOverview.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/welcome/WelcomeFragmentOverview.java
new file mode 100644
index 000000000..3e68baedd
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/welcome/WelcomeFragmentOverview.java
@@ -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 . */
+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);
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/welcome/WelcomeFragmentPermissions.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/welcome/WelcomeFragmentPermissions.java
new file mode 100644
index 000000000..b5431083f
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/welcome/WelcomeFragmentPermissions.java
@@ -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 . */
+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 {
+ private List permissionList;
+ private Context context;
+
+ public PermissionAdapter(List 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();
+ }
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/welcome/WelcomePageIndicator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/welcome/WelcomePageIndicator.java
new file mode 100644
index 000000000..be06b8b71
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/welcome/WelcomePageIndicator.java
@@ -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 . */
+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);
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/PermissionsUtils.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/PermissionsUtils.java
new file mode 100644
index 000000000..2242fc693
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/PermissionsUtils.java
@@ -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 . */
+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 getRequiredPermissionsList(Activity activity) {
+ ArrayList 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 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 wantedPermissions = getRequiredPermissionsList(activity);
+
+ if (!wantedPermissions.isEmpty()) {
+ ArrayList 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;
+ }
+ }
+}
diff --git a/app/src/main/res/layout/activity_permissions.xml b/app/src/main/res/layout/activity_permissions.xml
new file mode 100644
index 000000000..4aead3bb3
--- /dev/null
+++ b/app/src/main/res/layout/activity_permissions.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_welcome.xml b/app/src/main/res/layout/activity_welcome.xml
new file mode 100644
index 000000000..e7ba02864
--- /dev/null
+++ b/app/src/main/res/layout/activity_welcome.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_welcome_docs_source.xml b/app/src/main/res/layout/fragment_welcome_docs_source.xml
new file mode 100644
index 000000000..1bd12383f
--- /dev/null
+++ b/app/src/main/res/layout/fragment_welcome_docs_source.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_welcome_get_started.xml b/app/src/main/res/layout/fragment_welcome_get_started.xml
new file mode 100644
index 000000000..0f1e80916
--- /dev/null
+++ b/app/src/main/res/layout/fragment_welcome_get_started.xml
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_welcome_intro.xml b/app/src/main/res/layout/fragment_welcome_intro.xml
new file mode 100644
index 000000000..c99ec99dc
--- /dev/null
+++ b/app/src/main/res/layout/fragment_welcome_intro.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_welcome_overview.xml b/app/src/main/res/layout/fragment_welcome_overview.xml
new file mode 100644
index 000000000..17a9c41d6
--- /dev/null
+++ b/app/src/main/res/layout/fragment_welcome_overview.xml
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_welcome_permission_row.xml b/app/src/main/res/layout/fragment_welcome_permission_row.xml
new file mode 100644
index 000000000..4ba74b514
--- /dev/null
+++ b/app/src/main/res/layout/fragment_welcome_permission_row.xml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_welcome_permissions.xml b/app/src/main/res/layout/fragment_welcome_permissions.xml
new file mode 100644
index 000000000..9c0e5ee4c
--- /dev/null
+++ b/app/src/main/res/layout/fragment_welcome_permissions.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/wpi_attrs.xml b/app/src/main/res/values/wpi_attrs.xml
new file mode 100644
index 000000000..f431f3141
--- /dev/null
+++ b/app/src/main/res/values/wpi_attrs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml
index b5729f5f5..8d736ee19 100644
--- a/app/src/main/res/xml/preferences.xml
+++ b/app/src/main/res/xml/preferences.xml
@@ -385,6 +385,10 @@
android:summary="@string/pref_cache_weather_summary"
android:title="@string/pref_cache_weather"
app:iconSpaceReserved="false" />
+