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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_welcome_intro.xml b/app/src/main/res/layout/fragment_welcome_intro.xml
new file mode 100644
index 000000000..c99ec99dc
--- /dev/null
+++ b/app/src/main/res/layout/fragment_welcome_intro.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_welcome_overview.xml b/app/src/main/res/layout/fragment_welcome_overview.xml
new file mode 100644
index 000000000..17a9c41d6
--- /dev/null
+++ b/app/src/main/res/layout/fragment_welcome_overview.xml
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_welcome_permission_row.xml b/app/src/main/res/layout/fragment_welcome_permission_row.xml
new file mode 100644
index 000000000..4ba74b514
--- /dev/null
+++ b/app/src/main/res/layout/fragment_welcome_permission_row.xml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_welcome_permissions.xml b/app/src/main/res/layout/fragment_welcome_permissions.xml
new file mode 100644
index 000000000..9c0e5ee4c
--- /dev/null
+++ b/app/src/main/res/layout/fragment_welcome_permissions.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/wpi_attrs.xml b/app/src/main/res/values/wpi_attrs.xml
new file mode 100644
index 000000000..f431f3141
--- /dev/null
+++ b/app/src/main/res/values/wpi_attrs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml
index c05ec9fd7..aa5a4af8d 100644
--- a/app/src/main/res/xml/preferences.xml
+++ b/app/src/main/res/xml/preferences.xml
@@ -373,6 +373,10 @@
android:summary="@string/pref_cache_weather_summary"
android:title="@string/pref_cache_weather"
app:iconSpaceReserved="false" />
+