diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b3736f4aa..63e63321d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -142,6 +142,10 @@ + set = NotificationManagerCompat.getEnabledListenerPackages(this); - if (pesterWithPermissions) { - if (!set.contains(this.getPackageName())) { // If notification listener access hasn't been granted - // Put up a dialog explaining why we need permissions (Polite, but also Play Store policy) - // When accepted, we open the Activity for Notification access - DialogFragment dialog = new NotifyListenerPermissionsDialogFragment(); - dialog.show(getSupportFragmentManager(), "NotifyListenerPermissionsDialogFragment"); + // Open the Welcome flow on first run, only check permissions on next runs + boolean firstRun = prefs.getBoolean("first_run", true); + if (firstRun) { + launchWelcomeActivity(); + } else { + pesterWithPermissions = prefs.getBoolean("permission_pestering", true); + if (pesterWithPermissions && !PermissionsUtils.checkAllPermissions(this)) { + // TODO: show (only) WelcomeFragmentPermissions here } } - /* We not put up dialogs explaining why we need permissions (Polite, but also Play Store policy). - - Rather than chaining the calls, we just open a bunch of dialogs. Last in this list = first - on the page, and as they are accepted the permissions are requested in turn. - - When accepted, we request it or open the Activity for permission to display over other apps. */ - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - /* In order to be able to set ringer mode to silent in GB's PhoneCallReceiver - the permission to access notifications is needed above Android M - ACCESS_NOTIFICATION_POLICY is also needed in the manifest */ - if (pesterWithPermissions) { - if (!((NotificationManager) this.getSystemService(Context.NOTIFICATION_SERVICE)).isNotificationPolicyAccessGranted()) { - // Put up a dialog explaining why we need permissions (Polite, but also Play Store policy) - // When accepted, we open the Activity for Notification access - DialogFragment dialog = new NotifyPolicyPermissionsDialogFragment(); - dialog.show(getSupportFragmentManager(), "NotifyPolicyPermissionsDialogFragment"); - } - } - - if (!Settings.canDrawOverlays(getApplicationContext())) { - // If diplay over other apps access hasn't been granted - // Put up a dialog explaining why we need permissions (Polite, but also Play Store policy) - // When accepted, we open the Activity for permission to display over other apps. - if (pesterWithPermissions) { - DialogFragment dialog = new DisplayOverOthersPermissionsDialogFragment(); - dialog.show(getSupportFragmentManager(), "DisplayOverOthersPermissionsDialogFragment"); - } - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && - ContextCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.ACCESS_BACKGROUND_LOCATION) == PackageManager.PERMISSION_DENIED) { - if (pesterWithPermissions) { - DialogFragment dialog = new LocationPermissionsDialogFragment(); - dialog.show(getSupportFragmentManager(), "LocationPermissionsDialogFragment"); - } - } - - // Check all the other permissions that we need to for Android M + later - if (getWantedPermissions().isEmpty()) - displayPermissionDialog = false; - if (displayPermissionDialog && pesterWithPermissions) { - DialogFragment dialog = new PermissionsDialogFragment(); - dialog.show(getSupportFragmentManager(), "PermissionsDialogFragment"); - // when 'ok' clicked, checkAndRequestPermissions() is called - } else - checkAndRequestPermissions(); - } +// boolean displayPermissionDialog = !prefs.getBoolean("permission_dialog_displayed", false); +// prefs.getPreferences().edit().putBoolean("permission_dialog_displayed", true).apply(); +// +// +// Set 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"); +// } +// } +// +// /* 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); @@ -448,6 +422,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 +442,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 +449,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/SettingsActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/SettingsActivity.java index b2cac37b1..80db66fcd 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.hplus.HPlusSettingsActivity; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandPreferencesActivity; @@ -477,6 +478,14 @@ public class SettingsActivity extends AbstractSettingsActivityV2 { }); } + pref = findPreference("pref_show_first_run_screen"); + if (pref != null) { + pref.setOnPreferenceClickListener(preference -> { + Intent enableIntent = new Intent(requireContext(), WelcomeActivity.class); + startActivity(enableIntent); + return true; + }); + } pref = findPreference("pref_discovery_pairing"); if (pref != null) { pref.setOnPreferenceClickListener(preference -> { 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..2a3addb80 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/welcome/WelcomeFragmentGetStarted.java @@ -0,0 +1,58 @@ +/* Copyright (C) 2024 Arjan Schrijver + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.activities.welcome; + +import android.content.Intent; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.activities.DataManagementActivity; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; + +public class WelcomeFragmentGetStarted extends Fragment { + private static final Logger LOG = LoggerFactory.getLogger(WelcomeFragmentGetStarted.class); + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + super.onCreateView(inflater, container, savedInstanceState); + View view = inflater.inflate(R.layout.fragment_welcome_get_started, container, false); + + Button restore = view.findViewById(R.id.welcome_button_restore); + restore.setOnClickListener(restoreButton -> startActivity(new Intent(requireActivity(), DataManagementActivity.class))); + Button toApp = view.findViewById(R.id.welcome_button_to_app); + toApp.setOnClickListener(toAppButton -> { + Prefs prefs = GBApplication.getPrefs(); + prefs.getPreferences().edit().putBoolean("first_run", false).apply(); + requireActivity().finish(); + }); + + return view; + } +} 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..c708ed0b5 --- /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(requireContext()), 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..499be9008 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/PermissionsUtils.java @@ -0,0 +1,355 @@ +/* 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.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(Context context) { + ArrayList permissionsList = new ArrayList<>(); + permissionsList.add(new PermissionDetails(CUSTOM_PERM_NOTIFICATION_LISTENER, context.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(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(Context context) { + boolean result = true; + for (PermissionDetails permission : getRequiredPermissionsList(context)) { + if (!checkPermission(context, permission.getPermission())) { + result = false; + } + } + return result; + } + + public static void requestAllPermissions(Activity activity) { + } + + 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; + } + } + +// @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); +// } +// } +// } + + /// 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(); +// } +// } +} 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 @@ + + + + + + + + + +