From cadf2b5b2fd79270817d98ac79d4752f8c21823f Mon Sep 17 00:00:00 2001 From: Arjan Schrijver Date: Mon, 15 Apr 2024 16:34:03 +0200 Subject: [PATCH] Add First Start screens with permissions screen --- app/src/main/AndroidManifest.xml | 8 + .../activities/ControlCenterv2.java | 384 +----------------- .../activities/PermissionsActivity.java | 38 ++ .../activities/SettingsActivity.java | 9 + .../activities/welcome/WelcomeActivity.java | 83 ++++ .../welcome/WelcomeFragmentDocsSource.java | 42 ++ .../welcome/WelcomeFragmentGetStarted.java | 61 +++ .../welcome/WelcomeFragmentIntro.java | 42 ++ .../welcome/WelcomeFragmentOverview.java | 42 ++ .../welcome/WelcomeFragmentPermissions.java | 127 ++++++ .../welcome/WelcomePageIndicator.java | 135 ++++++ .../gadgetbridge/util/PermissionsUtils.java | 251 ++++++++++++ .../main/res/layout/activity_permissions.xml | 10 + app/src/main/res/layout/activity_welcome.xml | 25 ++ .../layout/fragment_welcome_docs_source.xml | 43 ++ .../layout/fragment_welcome_get_started.xml | 52 +++ .../res/layout/fragment_welcome_intro.xml | 38 ++ .../res/layout/fragment_welcome_overview.xml | 64 +++ .../fragment_welcome_permission_row.xml | 47 +++ .../layout/fragment_welcome_permissions.xml | 37 ++ app/src/main/res/values/wpi_attrs.xml | 6 + app/src/main/res/xml/preferences.xml | 4 + 22 files changed, 1179 insertions(+), 369 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/PermissionsActivity.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/welcome/WelcomeActivity.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/welcome/WelcomeFragmentDocsSource.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/welcome/WelcomeFragmentGetStarted.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/welcome/WelcomeFragmentIntro.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/welcome/WelcomeFragmentOverview.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/welcome/WelcomeFragmentPermissions.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/welcome/WelcomePageIndicator.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/PermissionsUtils.java create mode 100644 app/src/main/res/layout/activity_permissions.xml create mode 100644 app/src/main/res/layout/activity_welcome.xml create mode 100644 app/src/main/res/layout/fragment_welcome_docs_source.xml create mode 100644 app/src/main/res/layout/fragment_welcome_get_started.xml create mode 100644 app/src/main/res/layout/fragment_welcome_intro.xml create mode 100644 app/src/main/res/layout/fragment_welcome_overview.xml create mode 100644 app/src/main/res/layout/fragment_welcome_permission_row.xml create mode 100644 app/src/main/res/layout/fragment_welcome_permissions.xml create mode 100644 app/src/main/res/values/wpi_attrs.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 03c1f0461..564e5a900 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -142,6 +142,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())) { @@ -448,6 +360,10 @@ public class ControlCenterv2 extends AppCompatActivity return new GBChangeLog(this, css); } + private void launchWelcomeActivity() { + startActivity(new Intent(this, WelcomeActivity.class)); + } + private void launchDiscoveryActivity() { startActivity(new Intent(this, DiscoveryActivityV2.class)); } @@ -464,145 +380,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; @@ -610,137 +387,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 9816172a6..f4a67dbcd 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.devices.miband.MiBandPreferencesActivity; import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleSettingsActivity; @@ -467,6 +468,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 @@ + + + + + + + + + +