1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-11-30 05:52:57 +01:00

Add First Start screens with permissions screen

This commit is contained in:
Arjan Schrijver 2024-04-15 16:34:03 +02:00
parent 126102aa05
commit 5b515319bc
22 changed files with 1180 additions and 371 deletions

View File

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

View File

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

View File

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

View File

@ -65,6 +65,7 @@ import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.charts.ChartsPreferencesActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.discovery.DiscoveryPairingPreferenceActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.welcome.WelcomeActivity;
import nodomain.freeyourgadget.gadgetbridge.database.PeriodicExporter;
import nodomain.freeyourgadget.gadgetbridge.externalevents.TimeChangeReceiver;
import nodomain.freeyourgadget.gadgetbridge.model.Weather;
@ -437,6 +438,14 @@ public class SettingsActivity extends AbstractSettingsActivityV2 {
});
}
pref = findPreference("pref_show_first_run_screen");
if (pref != null) {
pref.setOnPreferenceClickListener(preference -> {
Intent enableIntent = new Intent(requireContext(), WelcomeActivity.class);
startActivity(enableIntent);
return true;
});
}
pref = findPreference("pref_discovery_pairing");
if (pref != null) {
pref.setOnPreferenceClickListener(preference -> {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,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 <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities.welcome;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.util.PermissionsUtils;
public class WelcomeFragmentPermissions extends Fragment {
private static final Logger LOG = LoggerFactory.getLogger(WelcomeFragmentPermissions.class);
private RecyclerView permissionsListView;
private PermissionAdapter permissionAdapter;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
View view = inflater.inflate(R.layout.fragment_welcome_permissions, container, false);
Button requestAllButton = view.findViewById(R.id.button_request_all);
requestAllButton.setOnClickListener(v -> PermissionsUtils.requestAllPermissions(requireActivity()));
// Initialize RecyclerView and data
permissionsListView = view.findViewById(R.id.permissions_list);
// Set up RecyclerView
permissionAdapter = new PermissionAdapter(PermissionsUtils.getRequiredPermissionsList(requireActivity()), requireContext());
permissionsListView.setLayoutManager(new LinearLayoutManager(requireContext()));
permissionsListView.setAdapter(permissionAdapter);
return view;
}
@Override
public void onResume() {
super.onResume();
permissionAdapter.notifyDataSetChanged();
}
private class PermissionHolder extends RecyclerView.ViewHolder {
TextView titleTextView;
TextView summaryTextView;
ImageView checkmarkImageView;
Button requestButton;
public PermissionHolder(View itemView) {
super(itemView);
titleTextView = itemView.findViewById(R.id.permission_title);
summaryTextView = itemView.findViewById(R.id.permission_summary);
checkmarkImageView = itemView.findViewById(R.id.permission_check);
requestButton = itemView.findViewById(R.id.permission_request);
}
}
private class PermissionAdapter extends RecyclerView.Adapter<PermissionHolder> {
private List<PermissionsUtils.PermissionDetails> permissionList;
private Context context;
public PermissionAdapter(List<PermissionsUtils.PermissionDetails> permissionList, Context context) {
this.permissionList = permissionList;
this.context = context;
}
@NonNull
@Override
public PermissionHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View itemView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.fragment_welcome_permission_row, parent, false);
return new PermissionHolder(itemView);
}
@Override
public void onBindViewHolder(@NonNull PermissionHolder holder, int position) {
PermissionsUtils.PermissionDetails permissionData = permissionList.get(position);
holder.titleTextView.setText(permissionData.getTitle());
holder.summaryTextView.setText(permissionData.getSummary());
if (PermissionsUtils.checkPermission(requireContext(), permissionData.getPermission())) {
holder.requestButton.setVisibility(View.INVISIBLE);
holder.requestButton.setEnabled(false);
holder.checkmarkImageView.setVisibility(View.VISIBLE);
} else {
holder.requestButton.setOnClickListener(view -> {
PermissionsUtils.requestPermission(requireActivity(), permissionData.getPermission());
});
}
}
@Override
public int getItemCount() {
return permissionList.size();
}
}
}

View File

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

View File

@ -0,0 +1,251 @@
/* Copyright (C) 2024 Arjan Schrijver
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.util;
import android.Manifest;
import android.app.Activity;
import android.app.NotificationManager;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.provider.Settings;
import android.widget.Toast;
import androidx.core.app.ActivityCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.content.ContextCompat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import nodomain.freeyourgadget.gadgetbridge.BuildConfig;
import nodomain.freeyourgadget.gadgetbridge.R;
public class PermissionsUtils {
private static final Logger LOG = LoggerFactory.getLogger(PermissionsUtils.class);
private static final String CUSTOM_PERM_NOTIFICATION_LISTENER = "custom_perm_notifications_listener";
private static final String CUSTOM_PERM_NOTIFICATION_SERVICE = "custom_perm_notifications_service";
private static final String CUSTOM_PERM_DISPLAY_OVER = "custom_perm_display_over";
public static ArrayList<PermissionDetails> getRequiredPermissionsList(Activity activity) {
ArrayList<PermissionDetails> permissionsList = new ArrayList<>();
permissionsList.add(new PermissionDetails(
CUSTOM_PERM_NOTIFICATION_LISTENER,
activity.getString(R.string.menuitem_notifications),
"Forwarding notifications to connected gadgets"));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
permissionsList.add(new PermissionDetails(
CUSTOM_PERM_NOTIFICATION_SERVICE,
"Manage Do Not Disturb",
"Change DND notification policy"));
permissionsList.add(new PermissionDetails(
CUSTOM_PERM_DISPLAY_OVER,
"Display over other apps",
"Used by Bangle.js to start apps and other functionality on your phone"));
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && Build.VERSION.SDK_INT <= Build.VERSION_CODES.S) {
permissionsList.add(new PermissionDetails(
Manifest.permission.ACCESS_BACKGROUND_LOCATION,
"Background location",
"Required for scanning for Bluetooth devices"));
}
permissionsList.add(new PermissionDetails(
Manifest.permission.ACCESS_FINE_LOCATION,
"Fine location",
"Send location to gadgets which don't have GPS"));
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
permissionsList.add(new PermissionDetails(
Manifest.permission.BLUETOOTH,
"Bluetooth",
"Connect to Bluetooth devices"));
permissionsList.add(new PermissionDetails(
Manifest.permission.BLUETOOTH_ADMIN,
"Bluetooth admin",
"Discover and pair Bluetooth devices"));
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
permissionsList.add(new PermissionDetails(
Manifest.permission.BLUETOOTH_SCAN,
"Bluetooth scan",
"Scan for Bluetooth devices"));
permissionsList.add(new PermissionDetails(
Manifest.permission.BLUETOOTH_CONNECT,
"Bluetooth connect",
"Connect to Bluetooth devices"));
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
permissionsList.add(new PermissionDetails(
Manifest.permission.POST_NOTIFICATIONS,
"Post notifications",
"Post ongoing notification which keeps the service running"));
}
if (BuildConfig.INTERNET_ACCESS) {
permissionsList.add(new PermissionDetails(
Manifest.permission.INTERNET,
"Internet access",
"Synchronization with online resources"));
}
// permissionsList.add(new PermissionDetails( // NOTE: can't request this, it's only allowed for system apps
// Manifest.permission.MEDIA_CONTENT_CONTROL,
// "Media content control",
// "Read and control media playback"));
permissionsList.add(new PermissionDetails(
Manifest.permission.READ_CONTACTS,
"Contacts",
"Send contacts to gadgets"));
permissionsList.add(new PermissionDetails(
Manifest.permission.READ_CALENDAR,
"Calendar",
"Send calendar to gadgets"));
permissionsList.add(new PermissionDetails(
Manifest.permission.RECEIVE_SMS,
"Receive SMS",
"Forward SMS messages to gadgets"));
permissionsList.add(new PermissionDetails(
Manifest.permission.SEND_SMS,
"Send SMS",
"Send SMS (canned response) from gadgets"));
permissionsList.add(new PermissionDetails(
Manifest.permission.READ_CALL_LOG,
"Read call log",
"Forward call log to gadgets"));
permissionsList.add(new PermissionDetails(
Manifest.permission.READ_PHONE_STATE,
"Read phone state",
"Read status of ongoing calls"));
permissionsList.add(new PermissionDetails(
Manifest.permission.CALL_PHONE,
"Call phone",
"Initiate phone calls from gadgets"));
permissionsList.add(new PermissionDetails(
Manifest.permission.PROCESS_OUTGOING_CALLS,
"Process outgoing calls",
"Read the number of an outgoing call to display it on a gadget"));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
permissionsList.add(new PermissionDetails(
Manifest.permission.ANSWER_PHONE_CALLS,
"Answer phone calls",
"Answer phone calls from gadgets"));
}
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
permissionsList.add(new PermissionDetails(
Manifest.permission.READ_EXTERNAL_STORAGE,
"External storage",
"Using images, ringtones, app files and more"));
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
permissionsList.add(new PermissionDetails(
Manifest.permission.QUERY_ALL_PACKAGES,
"Query all packages",
"Read names and icons of all installed apps"));
}
return permissionsList;
}
public static boolean checkPermission(Context context, String permission) {
if (permission.equals(CUSTOM_PERM_NOTIFICATION_LISTENER)) {
Set<String> set = NotificationManagerCompat.getEnabledListenerPackages(context);
return set.contains(context.getPackageName());
} else if (permission.equals(CUSTOM_PERM_NOTIFICATION_SERVICE) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE)).isNotificationPolicyAccessGranted();
} else if (permission.equals(CUSTOM_PERM_DISPLAY_OVER) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return Settings.canDrawOverlays(context);
} else {
return ContextCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_DENIED;
}
}
public static boolean checkAllPermissions(Activity activity) {
boolean result = true;
for (PermissionDetails permission : getRequiredPermissionsList(activity)) {
if (!checkPermission(activity, permission.getPermission())) {
result = false;
}
}
return result;
}
public static void requestAllPermissions(Activity activity) {
List<PermissionDetails> wantedPermissions = getRequiredPermissionsList(activity);
if (!wantedPermissions.isEmpty()) {
ArrayList<String> wantedPermissionsStrings = new ArrayList<>();
for (PermissionDetails wantedPermission : wantedPermissions) {
wantedPermissionsStrings.add(wantedPermission.getPermission());
}
if (!wantedPermissionsStrings.isEmpty()) {
if (wantedPermissionsStrings.contains(CUSTOM_PERM_NOTIFICATION_LISTENER) && !checkPermission(activity, CUSTOM_PERM_NOTIFICATION_LISTENER))
requestPermission(activity, CUSTOM_PERM_NOTIFICATION_LISTENER);
if (wantedPermissionsStrings.contains(CUSTOM_PERM_NOTIFICATION_SERVICE) && !checkPermission(activity, CUSTOM_PERM_NOTIFICATION_SERVICE))
requestPermission(activity, CUSTOM_PERM_NOTIFICATION_SERVICE);
if (wantedPermissionsStrings.contains(CUSTOM_PERM_DISPLAY_OVER) && !checkPermission(activity, CUSTOM_PERM_DISPLAY_OVER))
requestPermission(activity, CUSTOM_PERM_DISPLAY_OVER);
ActivityCompat.requestPermissions(activity, wantedPermissionsStrings.toArray(new String[0]), 0);
}
}
}
public static void requestPermission(Activity activity, String permission) {
if (permission.equals(CUSTOM_PERM_NOTIFICATION_LISTENER)) {
try {
activity.startActivity(new Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS"));
} catch (ActivityNotFoundException e) {
GB.toast(activity, "'Notification Listener Settings' activity not found", Toast.LENGTH_LONG, GB.ERROR);
}
} else if (permission.equals(CUSTOM_PERM_NOTIFICATION_SERVICE)) {
try {
activity.startActivity(new Intent(android.provider.Settings.ACTION_NOTIFICATION_POLICY_ACCESS_SETTINGS));
} catch (ActivityNotFoundException e) {
GB.toast(activity, "'Notification Policy' activity not found", Toast.LENGTH_LONG, GB.ERROR);
LOG.error("'Notification Policy' activity not found");
}
} else if (permission.equals(CUSTOM_PERM_DISPLAY_OVER)) {
activity.startActivity(new Intent(android.provider.Settings.ACTION_MANAGE_OVERLAY_PERMISSION));
} else {
ActivityCompat.requestPermissions(activity, new String[]{permission}, 0);
}
}
public static class PermissionDetails {
private String permission;
private String title;
private String summary;
public PermissionDetails(String permission, String title, String summary) {
this.permission = permission;
this.title = title;
this.summary = summary;
}
public String getPermission() {
return permission;
}
public String getTitle() {
return title;
}
public String getSummary() {
return summary;
}
}
}

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</RelativeLayout>

View File

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

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="50dp"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAlignment="center"
android:text="Open Source"
android:textSize="30sp"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:textSize="20sp"
android:textAlignment="center"
android:text="Gadgetbridge is an open source app. It is developed by the community, for the community.\n\nAnyone is welcome to contribute via code, documentation, testing and donations.\n\nGadgetbridge contains no ads and no tracking. It works only locally on your Android device, so it is 100% privacy friendly.\n\nVisit our website for more information, documentation and links to our communication channels." />
<ImageView
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_marginTop="30dp"
android:src="@drawable/ic_engineering"
app:tint="?attr/colorPrimary" />
</LinearLayout>
</ScrollView>
</RelativeLayout>

View File

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_marginHorizontal="50dp"
android:orientation="vertical">
<TextView
android:id="@+id/intro_gadgetbridge"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAlignment="center"
android:text="Get started"
android:textSize="25sp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:textSize="20sp"
android:textAlignment="center"
android:text="To get started, add your first device directly from this screen, restore a backup or start with a clean database." />
<Button
android:id="@+id/welcome_button_add_device"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:text="Add first device" />
<Button
android:id="@+id/welcome_button_restore"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:text="Restore backup" />
<Button
android:id="@+id/welcome_button_to_app"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:text="Go to the app" />
</LinearLayout>
</RelativeLayout>

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="50dp"
android:layout_centerInParent="true"
android:gravity="center_horizontal"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAlignment="center"
android:text="Welcome to\nGadgetbridge"
android:textSize="30sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:textSize="20sp"
android:textAlignment="center"
android:text="Break free from the proprietary apps and cloud services of gadget vendors." />
<ImageView
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_marginTop="30dp"
android:background="@color/accent"
android:src="@drawable/ic_launcher_foreground" />
</LinearLayout>
</RelativeLayout>

View File

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="50dp"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAlignment="center"
android:text="Overview"
android:textSize="30sp"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:textSize="20sp"
android:textAlignment="center"
android:text="Gadgetbridge has two main views, each with their own purpose." />
<ImageView
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_marginTop="30dp"
android:src="@drawable/ic_dashboard"
app:tint="?attr/colorPrimary" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="20sp"
android:textAlignment="center"
android:text="The dashboard allows you to get a quick idea of how you're doing today. The calendar view shows the status of your goals over a whole month." />
<ImageView
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_marginTop="20dp"
android:src="@drawable/ic_devices_other"
app:tint="?attr/colorPrimary" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="20sp"
android:textAlignment="center"
android:text="The devices view shows all devices you have configured and their status, and gives access to device specific functions such as detailed charts, settings, apps and alarms." />
</LinearLayout>
</ScrollView>
</RelativeLayout>

View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="50dp">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/permission_request"
android:orientation="vertical">
<TextView
android:id="@+id/permission_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textStyle="bold"
android:text="Permission name" />
<TextView
android:id="@+id/permission_summary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Permission summary" />
</LinearLayout>
<Button
android:id="@+id/permission_request"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:text="Request" />
<ImageView
android:id="@+id/permission_check"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="20dp"
android:src="@drawable/cpv_preset_checked"
android:visibility="gone"
app:tint="@android:color/holo_green_dark"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center_horizontal"
android:padding="8dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:textAlignment="center"
android:text="Permissions"
android:textSize="30sp"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:textSize="20sp"
android:textAlignment="center"
android:text="Gadgetbridge needs a lot of permissions to perform all its functions. Review the permissions and their purposes below." />
<Button
android:id="@+id/button_request_all"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="Request all permissions"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/permissions_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp" />
</LinearLayout>

View File

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

View File

@ -385,6 +385,10 @@
android:summary="@string/pref_cache_weather_summary"
android:title="@string/pref_cache_weather"
app:iconSpaceReserved="false" />
<Preference
android:key="pref_show_first_run_screen"
android:title="Show first run screen"
app:iconSpaceReserved="false" />
<PreferenceCategory
android:key="pref_screen_intent_api"