From 98d54f011bca87fdd4566e99f8545c846e93c97c Mon Sep 17 00:00:00 2001 From: Arjan Schrijver Date: Fri, 29 Mar 2024 13:35:36 +0100 Subject: [PATCH] Rename MainActivity back to ControlCenterv2 --- app/lint-baseline.xml | 2 +- app/src/main/AndroidManifest.xml | 50 +- .../freeyourgadget/gadgetbridge/Widget.java | 5 +- .../activities/ControlCenterv2.java | 705 ++++++++++++++---- .../activities/DevicesFragment.java | 249 +++++++ .../gadgetbridge/activities/MainActivity.java | 670 ----------------- .../adapter/GBDeviceAdapterv2.java | 10 +- .../devices/UnknownDeviceCoordinator.java | 5 +- .../lenovo/LenovoWatchPairingActivity.java | 4 +- .../devices/miband/MiBandPairingActivity.java | 4 +- .../devices/pebble/PebblePairingActivity.java | 4 +- .../devices/watch9/Watch9PairingActivity.java | 4 +- .../freeyourgadget/gadgetbridge/util/GB.java | 12 +- .../res/layout/activity_data_management.xml | 2 +- app/src/main/res/layout/activity_debug.xml | 2 +- app/src/main/res/layout/activity_main.xml | 2 +- .../main/res/layout/activity_main_app_bar.xml | 2 +- ...ntrolcenterv2.xml => fragment_devices.xml} | 2 +- ...ml => fragment_devices_device_submenu.xml} | 0 app/src/main/res/navigation/main.xml | 4 +- 20 files changed, 869 insertions(+), 869 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DevicesFragment.java delete mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/MainActivity.java rename app/src/main/res/layout/{fragment_controlcenterv2.xml => fragment_devices.xml} (96%) rename app/src/main/res/menu/{activity_controlcenterv2_device_submenu.xml => fragment_devices_device_submenu.xml} (100%) diff --git a/app/lint-baseline.xml b/app/lint-baseline.xml index f5af860a3..663635b34 100644 --- a/app/lint-baseline.xml +++ b/app/lint-baseline.xml @@ -8533,7 +8533,7 @@ + android:parentActivityName=".activities.ControlCenterv2" /> + android:parentActivityName=".activities.ControlCenterv2" /> + android:parentActivityName=".activities.ControlCenterv2" /> + android:parentActivityName=".activities.ControlCenterv2" /> @@ -515,23 +515,23 @@ + android:parentActivityName=".activities.ControlCenterv2" /> + android:parentActivityName=".activities.ControlCenterv2" /> @@ -566,7 +566,7 @@ android:label="@string/title_activity_appmanager" android:launchMode="singleTop" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|layoutDirection|uiMode" - android:parentActivityName=".activities.MainActivity" /> + android:parentActivityName=".activities.ControlCenterv2" /> @@ -588,31 +588,31 @@ + android:parentActivityName=".activities.ControlCenterv2" /> + android:parentActivityName=".activities.ControlCenterv2" /> + android:parentActivityName=".activities.ControlCenterv2" /> + android:parentActivityName=".activities.ControlCenterv2" /> + android:parentActivityName=".activities.ControlCenterv2" /> + android:parentActivityName=".activities.ControlCenterv2" /> + android:parentActivityName=".activities.ControlCenterv2" /> + android:parentActivityName=".activities.ControlCenterv2" /> + android:parentActivityName=".activities.ControlCenterv2" /> + android:parentActivityName=".activities.ControlCenterv2" /> @@ -852,7 +852,7 @@ + android:parentActivityName=".activities.ControlCenterv2" /> diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/Widget.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/Widget.java index 5e861ad5d..7725e5d8a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/Widget.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/Widget.java @@ -25,7 +25,6 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.os.Build; import android.os.Bundle; import android.view.View; import android.widget.RemoteViews; @@ -40,7 +39,7 @@ import java.util.Calendar; import java.util.GregorianCalendar; import java.util.concurrent.TimeUnit; -import nodomain.freeyourgadget.gadgetbridge.activities.MainActivity; +import nodomain.freeyourgadget.gadgetbridge.activities.ControlCenterv2; import nodomain.freeyourgadget.gadgetbridge.activities.WidgetAlarmsActivity; import nodomain.freeyourgadget.gadgetbridge.activities.charts.ActivityChartsActivity; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; @@ -98,7 +97,7 @@ public class Widget extends AppWidgetProvider { views.setOnClickPendingIntent(R.id.todaywidget_header_container, refreshDataIntent); //open GB main window - Intent startMainIntent = new Intent(context, MainActivity.class); + Intent startMainIntent = new Intent(context, ControlCenterv2.class); PendingIntent startMainPIntent = PendingIntentUtils.getActivity(context, 0, startMainIntent, 0, false); views.setOnClickPendingIntent(R.id.todaywidget_header_icon, startMainPIntent); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ControlCenterv2.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ControlCenterv2.java index a7bcbba07..ca554ff8a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ControlCenterv2.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ControlCenterv2.java @@ -19,64 +19,116 @@ along with this program. If not, see . */ 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.view.LayoutInflater; +import android.provider.Settings; +import android.telephony.PhoneStateListener; +import android.telephony.TelephonyManager; +import android.util.TypedValue; +import android.view.MenuItem; import android.view.View; -import android.view.ViewGroup; +import android.widget.Toast; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; +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.AppCompatActivity; +import androidx.appcompat.app.AppCompatDelegate; +import androidx.core.app.ActivityCompat; +import androidx.core.app.NotificationManagerCompat; +import androidx.core.content.ContextCompat; +import androidx.core.view.GravityCompat; +import androidx.drawerlayout.widget.DrawerLayout; +import androidx.fragment.app.DialogFragment; import androidx.localbroadcastmanager.content.LocalBroadcastManager; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; +import androidx.navigation.NavController; +import androidx.navigation.NavGraph; +import androidx.navigation.fragment.NavHostFragment; +import androidx.navigation.ui.NavigationUI; -import com.google.android.material.floatingactionbutton.FloatingActionButton; +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.Calendar; -import java.util.GregorianCalendar; -import java.util.HashMap; +import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Locale; 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.adapter.GBDeviceAdapterv2; -import nodomain.freeyourgadget.gadgetbridge.database.DBAccess; -import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; -import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator; -import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; -import nodomain.freeyourgadget.gadgetbridge.model.DailyTotals; import nodomain.freeyourgadget.gadgetbridge.model.DeviceService; +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.Prefs; -public class ControlCenterv2 extends Fragment { +//TODO: extend AbstractGBActivity, but it requires actionbar that is not available +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 static PhoneStateListener fakeStateListener; - private DeviceManager deviceManager; - private GBDeviceAdapterv2 mGBDeviceAdapter; - private RecyclerView deviceListView; - private FloatingActionButton fab; - List deviceList; - private HashMap deviceActivityHashMap = new HashMap(); + //needed for KK compatibility + static { + AppCompatDelegate.setCompatVectorFromResourcesEnabled(true); + } private final BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); switch (Objects.requireNonNull(action)) { - case DeviceManager.ACTION_DEVICES_CHANGED: - case GBApplication.ACTION_NEW_DATA: - createRefreshTask("get activity data", requireContext()).execute(); - mGBDeviceAdapter.rebuildFolders(); - refreshPairedDevices(); + case GBApplication.ACTION_LANGUAGE_CHANGE: + setLanguage(GBApplication.getLanguage(), true); + break; + case GBApplication.ACTION_THEME_CHANGE: + isThemeInvalid = true; + break; + case GBApplication.ACTION_QUIT: + finish(); + break; + case ACTION_REQUEST_PERMISSIONS: + checkAndRequestPermissions(); + break; + case ACTION_REQUEST_LOCATION_PERMISSIONS: + checkAndRequestLocationPermissions(); break; case DeviceService.ACTION_REALTIME_SAMPLES: handleRealtimeSample(intent.getSerializableExtra(DeviceService.EXTRA_REALTIME_SAMPLE)); @@ -84,166 +136,537 @@ public class ControlCenterv2 extends Fragment { } } }; + private boolean pesterWithPermissions = true; + + private ActivitySample currentHRSample; + + public ActivitySample getCurrentHRSample() { + return currentHRSample; + } + + private void setCurrentHRSample(ActivitySample sample) { + if (HeartRateUtils.getInstance().isValidHeartRateValue(sample.getHeartRate())) { + currentHRSample = sample; + } + } private void handleRealtimeSample(Serializable extra) { if (extra instanceof ActivitySample) { ActivitySample sample = (ActivitySample) extra; - if (HeartRateUtils.getInstance().isValidHeartRateValue(sample.getHeartRate())) { - refreshPairedDevices(); - } + setCurrentHRSample(sample); } } @Override - public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { - View currentView = inflater.inflate(R.layout.fragment_controlcenterv2, container, false); + protected void onCreate(Bundle savedInstanceState) + { + AbstractGBActivity.init(this, AbstractGBActivity.NO_ACTIONBAR); + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); - deviceManager = ((GBApplication) getActivity().getApplication()).getDeviceManager(); + Prefs prefs = GBApplication.getPrefs(); - deviceListView = currentView.findViewById(R.id.deviceListView); - deviceListView.setHasFixedSize(true); - deviceListView.setLayoutManager(new LinearLayoutManager(currentView.getContext())); - - deviceList = deviceManager.getDevices(); - mGBDeviceAdapter = new GBDeviceAdapterv2(currentView.getContext(), deviceList, deviceActivityHashMap); - mGBDeviceAdapter.setHasStableIds(true); - - deviceListView.setAdapter(this.mGBDeviceAdapter); - - // get activity data asynchronously, this fills the deviceActivityHashMap - // and calls refreshPairedDevices() → notifyDataSetChanged - deviceListView.post(new Runnable() { - @Override - public void run() { - createRefreshTask("get activity data", getActivity().getApplication()).execute(); + boolean activityTrackerAvailable = false; + List devices = GBApplication.app().getDeviceManager().getDevices(); + for (GBDevice dev : devices) { + if (dev.getDeviceCoordinator().supportsActivityTracking()) { + activityTrackerAvailable = true; + break; } - }); + } - fab = currentView.findViewById(R.id.fab); - fab.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - launchDiscoveryActivity(); - } - }); + NavHostFragment navHostFragment = (NavHostFragment) + getSupportFragmentManager().findFragmentById(R.id.fragment_container); + NavController navController = navHostFragment.getNavController(); + if (!prefs.getBoolean("dashboard_as_default_view", true) || !activityTrackerAvailable) { + NavGraph navGraph = navController.getNavInflater().inflate(R.navigation.main); + navGraph.setStartDestination(R.id.bottom_nav_devices); + navController.setGraph(navGraph); + } + BottomNavigationView navigationView = findViewById(R.id.bottom_nav_bar); + NavigationUI.setupWithNavController(navigationView, navController); + navigationView.setVisibility(activityTrackerAvailable ? View.VISIBLE : View.GONE); - showFabIfNeccessary(); + NavigationView drawerNavigationView = findViewById(R.id.nav_view); + drawerNavigationView.setNavigationItemSelectedListener(this); - /* uncomment to enable fixed-swipe to reveal more actions + MaterialToolbar toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + DrawerLayout drawer = findViewById(R.id.drawer_layout); + ActionBarDrawerToggle toggle = new ActionBarDrawerToggle( + this, drawer, toolbar, R.string.controlcenter_navigation_drawer_open, R.string.controlcenter_navigation_drawer_close); + drawer.setDrawerListener(toggle); + toggle.syncState(); - ItemTouchHelper swipeToDismissTouchHelper = new ItemTouchHelper(new ItemTouchHelper.SimpleCallback( - ItemTouchHelper.LEFT , ItemTouchHelper.RIGHT) { - @Override - public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) { - if(dX>50) - dX = 50; - super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive); - - } - - @Override - public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) { - GB.toast(getBaseContext(), "onMove", Toast.LENGTH_LONG, GB.ERROR); - - return false; - } - - @Override - public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) { - GB.toast(getBaseContext(), "onSwiped", Toast.LENGTH_LONG, GB.ERROR); - - } - - @Override - public void onChildDrawOver(Canvas c, RecyclerView recyclerView, - RecyclerView.ViewHolder viewHolder, float dX, float dY, - int actionState, boolean isCurrentlyActive) { - } - }); - - swipeToDismissTouchHelper.attachToRecyclerView(deviceListView); - */ - - registerForContextMenu(deviceListView); + if (GBApplication.areDynamicColorsEnabled()) { + TypedValue typedValue = new TypedValue(); + Resources.Theme theme = getTheme(); + theme.resolveAttribute(R.attr.colorSurface, typedValue, true); + @ColorInt int toolbarBackground = typedValue.data; + toolbar.setBackgroundColor(toolbarBackground); + } else { + toolbar.setBackgroundColor(getResources().getColor(R.color.primarydark_light)); + toolbar.setTitleTextColor(getResources().getColor(android.R.color.white)); + } IntentFilter filterLocal = new IntentFilter(); - filterLocal.addAction(GBApplication.ACTION_NEW_DATA); - filterLocal.addAction(DeviceManager.ACTION_DEVICES_CHANGED); + filterLocal.addAction(GBApplication.ACTION_LANGUAGE_CHANGE); + filterLocal.addAction(GBApplication.ACTION_THEME_CHANGE); + filterLocal.addAction(GBApplication.ACTION_QUIT); + filterLocal.addAction(ACTION_REQUEST_PERMISSIONS); + filterLocal.addAction(ACTION_REQUEST_LOCATION_PERMISSIONS); filterLocal.addAction(DeviceService.ACTION_REALTIME_SAMPLES); - LocalBroadcastManager.getInstance(requireContext()).registerReceiver(mReceiver, filterLocal); + LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filterLocal); - refreshPairedDevices(); + /* + * Ask for permission to intercept notifications on first run. + */ + pesterWithPermissions = prefs.getBoolean("permission_pestering", true); - if (GB.isBluetoothEnabled() && deviceList.isEmpty() && Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { - startActivity(new Intent(getActivity(), DiscoveryActivityV2.class)); - } else { - GBApplication.deviceService().requestDeviceInfo(); + 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"); + } } - return currentView; - } + /* We not put up dialogs explaining why we need permissions (Polite, but also Play Store policy). - private void launchDiscoveryActivity() { - startActivity(new Intent(getActivity(), DiscoveryActivityV2.class)); - } + 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. - private void showFabIfNeccessary() { - if (GBApplication.getPrefs().getBoolean("display_add_device_fab", true)) { - fab.show(); - } else { - if (deviceManager.getDevices().size() < 1) { - fab.show(); - } else { - fab.hide(); + 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())) { + try { + cl.getMaterialLogDialog().show(); + } catch (Exception ignored) { + GB.toast(this, getString(R.string.error_showing_changelog), Toast.LENGTH_LONG, GB.ERROR); } } } @Override - public void onDestroy() { - unregisterForContextMenu(deviceListView); - LocalBroadcastManager.getInstance(requireContext()).unregisterReceiver(mReceiver); + protected void onResume() { + super.onResume(); + handleShortcut(getIntent()); + if (isLanguageInvalid || isThemeInvalid) { + isLanguageInvalid = false; + isThemeInvalid = false; + recreate(); + } + } + + @Override + protected void onDestroy() { + LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver); super.onDestroy(); } - private long[] getSteps(GBDevice device, DBHandler db) { - Calendar day = GregorianCalendar.getInstance(); + @Override + public boolean onNavigationItemSelected(@NonNull MenuItem item) { - DailyTotals ds = new DailyTotals(); - return ds.getDailyTotalsForDevice(device, day, db); + DrawerLayout drawer = findViewById(R.id.drawer_layout); + drawer.closeDrawer(GravityCompat.START); + + switch (item.getItemId()) { + case R.id.action_settings: + Intent settingsIntent = new Intent(this, SettingsActivity.class); + startActivityForResult(settingsIntent, MENU_REFRESH_CODE); + return false; //we do not want the drawer menu item to get selected + case R.id.action_debug: + Intent debugIntent = new Intent(this, DebugActivity.class); + startActivity(debugIntent); + return false; + case R.id.action_data_management: + Intent dbIntent = new Intent(this, DataManagementActivity.class); + startActivity(dbIntent); + return false; + case R.id.action_notification_management: + Intent blIntent = new Intent(this, NotificationManagementActivity.class); + startActivity(blIntent); + return false; + case R.id.device_action_discover: + launchDiscoveryActivity(); + return false; + case R.id.action_quit: + GBApplication.quit(); + return false; + case R.id.donation_link: + Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse("https://liberapay.com/Gadgetbridge")); //TODO: centralize if ever used somewhere else + i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(i); + return false; + case R.id.external_changelog: + GBChangeLog cl = createChangeLog(); + try { + if (cl.hasChanges(false)) { + cl.getMaterialLogDialog().show(); + } else { + cl.getMaterialFullLogDialog().show(); + } + } catch (Exception ignored) { + GB.toast(getBaseContext(), getString(R.string.error_showing_changelog), Toast.LENGTH_LONG, GB.ERROR); + } + return false; + case R.id.about: + Intent aboutIntent = new Intent(this, AboutActivity.class); + startActivity(aboutIntent); + return false; + } + + return false; } - public void refreshPairedDevices() { - if (mGBDeviceAdapter != null) { - mGBDeviceAdapter.notifyDataSetChanged(); - mGBDeviceAdapter.rebuildFolders(); + private GBChangeLog createChangeLog() { + String css = GBChangeLog.DEFAULT_CSS; + css += "body { " + + "color: " + AndroidUtils.getTextColorHex(getBaseContext()) + "; " + + "}"; + return new GBChangeLog(this, css); + } + + private void launchDiscoveryActivity() { + startActivity(new Intent(this, DiscoveryActivityV2.class)); + } + + private void handleShortcut(Intent intent) { + if(ACTION_CONNECT.equals(intent.getAction())) { + String btDeviceAddress = intent.getStringExtra("device"); + if(btDeviceAddress!=null){ + GBDevice candidate = DeviceHelper.getInstance().findAvailableDevice(btDeviceAddress, this); + if (candidate != null && !candidate.isConnected()) { + GBApplication.deviceService(candidate).connect(); + } + } } } - public RefreshTask createRefreshTask(String task, Context context) { - return new RefreshTask(task, context); + 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); + } } - public class RefreshTask extends DBAccess { - public RefreshTask(String task, Context context) { - super(task, context); + @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) { } - @Override - protected void doInBackground(DBHandler db) { - for (GBDevice gbDevice : deviceList) { - final DeviceCoordinator coordinator = gbDevice.getDeviceCoordinator(); - if (coordinator.supportsActivityTracking()) { - long[] stepsAndSleepData = getSteps(gbDevice, db); - deviceActivityHashMap.put(gbDevice.getAddress(), stepsAndSleepData); + 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); } } } - @Override - protected void onPostExecute(Object o) { - refreshPairedDevices(); + 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; + } + 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(); + } } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DevicesFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DevicesFragment.java new file mode 100644 index 000000000..f8e140219 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/DevicesFragment.java @@ -0,0 +1,249 @@ +/* Copyright (C) 2016-2024 Andreas Shimokawa, Andrzej Surowiec, Arjan + Schrijver, Carsten Pfeiffer, Daniel Dakhno, Daniele Gobbetti, Ganblejs, + gfwilliams, Gordon Williams, Johannes Tysiak, José Rebelo, marco.altomonte, + Petr Vaněk, Taavi Eomäe + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.activities; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Build; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.floatingactionbutton.FloatingActionButton; + +import java.io.Serializable; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.List; +import java.util.Objects; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.activities.discovery.DiscoveryActivityV2; +import nodomain.freeyourgadget.gadgetbridge.adapter.GBDeviceAdapterv2; +import nodomain.freeyourgadget.gadgetbridge.database.DBAccess; +import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; +import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; +import nodomain.freeyourgadget.gadgetbridge.model.DailyTotals; +import nodomain.freeyourgadget.gadgetbridge.model.DeviceService; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +public class DevicesFragment extends Fragment { + + private DeviceManager deviceManager; + private GBDeviceAdapterv2 mGBDeviceAdapter; + private RecyclerView deviceListView; + private FloatingActionButton fab; + List deviceList; + private HashMap deviceActivityHashMap = new HashMap(); + + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + switch (Objects.requireNonNull(action)) { + case DeviceManager.ACTION_DEVICES_CHANGED: + case GBApplication.ACTION_NEW_DATA: + createRefreshTask("get activity data", requireContext()).execute(); + mGBDeviceAdapter.rebuildFolders(); + refreshPairedDevices(); + break; + case DeviceService.ACTION_REALTIME_SAMPLES: + handleRealtimeSample(intent.getSerializableExtra(DeviceService.EXTRA_REALTIME_SAMPLE)); + break; + } + } + }; + + private void handleRealtimeSample(Serializable extra) { + if (extra instanceof ActivitySample) { + ActivitySample sample = (ActivitySample) extra; + if (HeartRateUtils.getInstance().isValidHeartRateValue(sample.getHeartRate())) { + refreshPairedDevices(); + } + } + } + + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { + View currentView = inflater.inflate(R.layout.fragment_devices, container, false); + + deviceManager = ((GBApplication) getActivity().getApplication()).getDeviceManager(); + + deviceListView = currentView.findViewById(R.id.deviceListView); + deviceListView.setHasFixedSize(true); + deviceListView.setLayoutManager(new LinearLayoutManager(currentView.getContext())); + + deviceList = deviceManager.getDevices(); + mGBDeviceAdapter = new GBDeviceAdapterv2(currentView.getContext(), deviceList, deviceActivityHashMap); + mGBDeviceAdapter.setHasStableIds(true); + + deviceListView.setAdapter(this.mGBDeviceAdapter); + + // get activity data asynchronously, this fills the deviceActivityHashMap + // and calls refreshPairedDevices() → notifyDataSetChanged + deviceListView.post(new Runnable() { + @Override + public void run() { + createRefreshTask("get activity data", getActivity().getApplication()).execute(); + } + }); + + fab = currentView.findViewById(R.id.fab); + fab.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + launchDiscoveryActivity(); + } + }); + + showFabIfNeccessary(); + + /* uncomment to enable fixed-swipe to reveal more actions + + ItemTouchHelper swipeToDismissTouchHelper = new ItemTouchHelper(new ItemTouchHelper.SimpleCallback( + ItemTouchHelper.LEFT , ItemTouchHelper.RIGHT) { + @Override + public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) { + if(dX>50) + dX = 50; + super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive); + + } + + @Override + public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) { + GB.toast(getBaseContext(), "onMove", Toast.LENGTH_LONG, GB.ERROR); + + return false; + } + + @Override + public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) { + GB.toast(getBaseContext(), "onSwiped", Toast.LENGTH_LONG, GB.ERROR); + + } + + @Override + public void onChildDrawOver(Canvas c, RecyclerView recyclerView, + RecyclerView.ViewHolder viewHolder, float dX, float dY, + int actionState, boolean isCurrentlyActive) { + } + }); + + swipeToDismissTouchHelper.attachToRecyclerView(deviceListView); + */ + + registerForContextMenu(deviceListView); + + IntentFilter filterLocal = new IntentFilter(); + filterLocal.addAction(GBApplication.ACTION_NEW_DATA); + filterLocal.addAction(DeviceManager.ACTION_DEVICES_CHANGED); + filterLocal.addAction(DeviceService.ACTION_REALTIME_SAMPLES); + LocalBroadcastManager.getInstance(requireContext()).registerReceiver(mReceiver, filterLocal); + + refreshPairedDevices(); + + if (GB.isBluetoothEnabled() && deviceList.isEmpty() && Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + startActivity(new Intent(getActivity(), DiscoveryActivityV2.class)); + } else { + GBApplication.deviceService().requestDeviceInfo(); + } + + return currentView; + } + + private void launchDiscoveryActivity() { + startActivity(new Intent(getActivity(), DiscoveryActivityV2.class)); + } + + private void showFabIfNeccessary() { + if (GBApplication.getPrefs().getBoolean("display_add_device_fab", true)) { + fab.show(); + } else { + if (deviceManager.getDevices().size() < 1) { + fab.show(); + } else { + fab.hide(); + } + } + } + + @Override + public void onDestroy() { + unregisterForContextMenu(deviceListView); + LocalBroadcastManager.getInstance(requireContext()).unregisterReceiver(mReceiver); + super.onDestroy(); + } + + private long[] getSteps(GBDevice device, DBHandler db) { + Calendar day = GregorianCalendar.getInstance(); + + DailyTotals ds = new DailyTotals(); + return ds.getDailyTotalsForDevice(device, day, db); + } + + public void refreshPairedDevices() { + if (mGBDeviceAdapter != null) { + mGBDeviceAdapter.notifyDataSetChanged(); + mGBDeviceAdapter.rebuildFolders(); + } + } + + public RefreshTask createRefreshTask(String task, Context context) { + return new RefreshTask(task, context); + } + + public class RefreshTask extends DBAccess { + public RefreshTask(String task, Context context) { + super(task, context); + } + + @Override + protected void doInBackground(DBHandler db) { + for (GBDevice gbDevice : deviceList) { + final DeviceCoordinator coordinator = gbDevice.getDeviceCoordinator(); + if (coordinator.supportsActivityTracking()) { + long[] stepsAndSleepData = getSteps(gbDevice, db); + deviceActivityHashMap.put(gbDevice.getAddress(), stepsAndSleepData); + } + } + } + + @Override + protected void onPostExecute(Object o) { + refreshPairedDevices(); + } + + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/MainActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/MainActivity.java deleted file mode 100644 index 0b9cdba23..000000000 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/MainActivity.java +++ /dev/null @@ -1,670 +0,0 @@ -/* Copyright (C) 2016-2023 Andreas Shimokawa, Carsten Pfeiffer, Daniele - Gobbetti, Johannes Tysiak, Taavi Eomäe, vanous, Arjan Schrijver - - This file is part of Gadgetbridge. - - Gadgetbridge is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Gadgetbridge is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . */ -package nodomain.freeyourgadget.gadgetbridge.activities; - -import 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.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.AppCompatActivity; -import androidx.appcompat.app.AppCompatDelegate; -import androidx.core.app.ActivityCompat; -import androidx.core.app.NotificationManagerCompat; -import androidx.core.content.ContextCompat; -import androidx.core.view.GravityCompat; -import androidx.drawerlayout.widget.DrawerLayout; -import androidx.fragment.app.DialogFragment; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; -import androidx.navigation.NavController; -import androidx.navigation.NavGraph; -import androidx.navigation.fragment.NavHostFragment; -import androidx.navigation.ui.NavigationUI; - -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.HashSet; -import java.util.List; -import java.util.Locale; -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.impl.GBDevice; -import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; -import nodomain.freeyourgadget.gadgetbridge.model.DeviceService; -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.Prefs; - -//TODO: extend AbstractGBActivity, but it requires actionbar that is not available -public class MainActivity extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener, GBActivity { - private static final Logger LOG = LoggerFactory.getLogger(MainActivity.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 static PhoneStateListener fakeStateListener; - - //needed for KK compatibility - static { - AppCompatDelegate.setCompatVectorFromResourcesEnabled(true); - } - - private final BroadcastReceiver mReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - String action = intent.getAction(); - switch (Objects.requireNonNull(action)) { - case GBApplication.ACTION_LANGUAGE_CHANGE: - setLanguage(GBApplication.getLanguage(), true); - break; - case GBApplication.ACTION_THEME_CHANGE: - isThemeInvalid = true; - break; - case GBApplication.ACTION_QUIT: - finish(); - break; - case ACTION_REQUEST_PERMISSIONS: - checkAndRequestPermissions(); - break; - case ACTION_REQUEST_LOCATION_PERMISSIONS: - checkAndRequestLocationPermissions(); - break; - case DeviceService.ACTION_REALTIME_SAMPLES: - handleRealtimeSample(intent.getSerializableExtra(DeviceService.EXTRA_REALTIME_SAMPLE)); - break; - } - } - }; - private boolean pesterWithPermissions = true; - - private ActivitySample currentHRSample; - - public ActivitySample getCurrentHRSample() { - return currentHRSample; - } - - private void setCurrentHRSample(ActivitySample sample) { - if (HeartRateUtils.getInstance().isValidHeartRateValue(sample.getHeartRate())) { - currentHRSample = sample; - } - } - - private void handleRealtimeSample(Serializable extra) { - if (extra instanceof ActivitySample) { - ActivitySample sample = (ActivitySample) extra; - setCurrentHRSample(sample); - } - } - - @Override - protected void onCreate(Bundle savedInstanceState) - { - AbstractGBActivity.init(this, AbstractGBActivity.NO_ACTIONBAR); - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); - - Prefs prefs = GBApplication.getPrefs(); - - boolean activityTrackerAvailable = false; - List devices = GBApplication.app().getDeviceManager().getDevices(); - for (GBDevice dev : devices) { - if (dev.getDeviceCoordinator().supportsActivityTracking()) { - activityTrackerAvailable = true; - break; - } - } - - NavHostFragment navHostFragment = (NavHostFragment) - getSupportFragmentManager().findFragmentById(R.id.fragment_container); - NavController navController = navHostFragment.getNavController(); - if (!prefs.getBoolean("dashboard_as_default_view", true) || !activityTrackerAvailable) { - NavGraph navGraph = navController.getNavInflater().inflate(R.navigation.main); - navGraph.setStartDestination(R.id.bottom_nav_devices); - navController.setGraph(navGraph); - } - BottomNavigationView navigationView = findViewById(R.id.bottom_nav_bar); - NavigationUI.setupWithNavController(navigationView, navController); - navigationView.setVisibility(activityTrackerAvailable ? View.VISIBLE : View.GONE); - - NavigationView drawerNavigationView = findViewById(R.id.nav_view); - drawerNavigationView.setNavigationItemSelectedListener(this); - - MaterialToolbar toolbar = findViewById(R.id.toolbar); - setSupportActionBar(toolbar); - DrawerLayout drawer = findViewById(R.id.drawer_layout); - ActionBarDrawerToggle toggle = new ActionBarDrawerToggle( - this, drawer, toolbar, R.string.controlcenter_navigation_drawer_open, R.string.controlcenter_navigation_drawer_close); - drawer.setDrawerListener(toggle); - toggle.syncState(); - - if (GBApplication.areDynamicColorsEnabled()) { - TypedValue typedValue = new TypedValue(); - Resources.Theme theme = getTheme(); - theme.resolveAttribute(R.attr.colorSurface, typedValue, true); - @ColorInt int toolbarBackground = typedValue.data; - toolbar.setBackgroundColor(toolbarBackground); - } else { - toolbar.setBackgroundColor(getResources().getColor(R.color.primarydark_light)); - toolbar.setTitleTextColor(getResources().getColor(android.R.color.white)); - } - - IntentFilter filterLocal = new IntentFilter(); - filterLocal.addAction(GBApplication.ACTION_LANGUAGE_CHANGE); - filterLocal.addAction(GBApplication.ACTION_THEME_CHANGE); - filterLocal.addAction(GBApplication.ACTION_QUIT); - filterLocal.addAction(ACTION_REQUEST_PERMISSIONS); - filterLocal.addAction(ACTION_REQUEST_LOCATION_PERMISSIONS); - filterLocal.addAction(DeviceService.ACTION_REALTIME_SAMPLES); - 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 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); - if (showChangelog && cl.isFirstRun() && cl.hasChanges(cl.isFirstRunEver())) { - try { - cl.getMaterialLogDialog().show(); - } catch (Exception ignored) { - GB.toast(this, getString(R.string.error_showing_changelog), Toast.LENGTH_LONG, GB.ERROR); - } - } - } - - @Override - protected void onResume() { - super.onResume(); - handleShortcut(getIntent()); - if (isLanguageInvalid || isThemeInvalid) { - isLanguageInvalid = false; - isThemeInvalid = false; - recreate(); - } - } - - @Override - protected void onDestroy() { - LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver); - super.onDestroy(); - } - - @Override - public boolean onNavigationItemSelected(@NonNull MenuItem item) { - - DrawerLayout drawer = findViewById(R.id.drawer_layout); - drawer.closeDrawer(GravityCompat.START); - - switch (item.getItemId()) { - case R.id.action_settings: - Intent settingsIntent = new Intent(this, SettingsActivity.class); - startActivityForResult(settingsIntent, MENU_REFRESH_CODE); - return false; //we do not want the drawer menu item to get selected - case R.id.action_debug: - Intent debugIntent = new Intent(this, DebugActivity.class); - startActivity(debugIntent); - return false; - case R.id.action_data_management: - Intent dbIntent = new Intent(this, DataManagementActivity.class); - startActivity(dbIntent); - return false; - case R.id.action_notification_management: - Intent blIntent = new Intent(this, NotificationManagementActivity.class); - startActivity(blIntent); - return false; - case R.id.device_action_discover: - launchDiscoveryActivity(); - return false; - case R.id.action_quit: - GBApplication.quit(); - return false; - case R.id.donation_link: - Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse("https://liberapay.com/Gadgetbridge")); //TODO: centralize if ever used somewhere else - i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK); - startActivity(i); - return false; - case R.id.external_changelog: - GBChangeLog cl = createChangeLog(); - try { - if (cl.hasChanges(false)) { - cl.getMaterialLogDialog().show(); - } else { - cl.getMaterialFullLogDialog().show(); - } - } catch (Exception ignored) { - GB.toast(getBaseContext(), getString(R.string.error_showing_changelog), Toast.LENGTH_LONG, GB.ERROR); - } - return false; - case R.id.about: - Intent aboutIntent = new Intent(this, AboutActivity.class); - startActivity(aboutIntent); - return false; - } - - return false; - } - - private GBChangeLog createChangeLog() { - String css = GBChangeLog.DEFAULT_CSS; - css += "body { " - + "color: " + AndroidUtils.getTextColorHex(getBaseContext()) + "; " - + "}"; - return new GBChangeLog(this, css); - } - - private void launchDiscoveryActivity() { - startActivity(new Intent(this, DiscoveryActivityV2.class)); - } - - private void handleShortcut(Intent intent) { - if(ACTION_CONNECT.equals(intent.getAction())) { - String btDeviceAddress = intent.getStringExtra("device"); - if(btDeviceAddress!=null){ - GBDevice candidate = DeviceHelper.getInstance().findAvailableDevice(btDeviceAddress, this); - if (candidate != null && !candidate.isConnected()) { - GBApplication.deviceService(candidate).connect(); - } - } - } - } - - 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; - } - 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(); - } - } -} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAdapterv2.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAdapterv2.java index 61b55f39d..0b7ead89d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAdapterv2.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/GBDeviceAdapterv2.java @@ -102,8 +102,8 @@ import nodomain.freeyourgadget.gadgetbridge.activities.ActivitySummariesActivity import nodomain.freeyourgadget.gadgetbridge.activities.BatteryInfoActivity; import nodomain.freeyourgadget.gadgetbridge.activities.ConfigureAlarms; import nodomain.freeyourgadget.gadgetbridge.activities.ConfigureReminders; +import nodomain.freeyourgadget.gadgetbridge.activities.ControlCenterv2; import nodomain.freeyourgadget.gadgetbridge.activities.HeartRateDialog; -import nodomain.freeyourgadget.gadgetbridge.activities.MainActivity; import nodomain.freeyourgadget.gadgetbridge.activities.OpenFwAppInstallerActivity; import nodomain.freeyourgadget.gadgetbridge.activities.VibrationActivity; import nodomain.freeyourgadget.gadgetbridge.activities.charts.ActivityChartsActivity; @@ -396,8 +396,8 @@ public class GBDeviceAdapterv2 extends ListAdapter getPairingActivity() { - return MainActivity.class; + return ControlCenterv2.class; } @Override diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lenovo/LenovoWatchPairingActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lenovo/LenovoWatchPairingActivity.java index 0cae7c948..c845d92d9 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lenovo/LenovoWatchPairingActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/lenovo/LenovoWatchPairingActivity.java @@ -35,7 +35,7 @@ import org.slf4j.LoggerFactory; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity; -import nodomain.freeyourgadget.gadgetbridge.activities.MainActivity; +import nodomain.freeyourgadget.gadgetbridge.activities.ControlCenterv2; import nodomain.freeyourgadget.gadgetbridge.activities.discovery.DiscoveryActivityV2; import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; @@ -101,7 +101,7 @@ public class LenovoWatchPairingActivity extends AbstractGBActivity implements Bo @Override public void onBondingComplete(boolean success) { - startActivity(new Intent(this, MainActivity.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)); + startActivity(new Intent(this, ControlCenterv2.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)); finish(); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandPairingActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandPairingActivity.java index b1240d7de..644e26403 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandPairingActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandPairingActivity.java @@ -41,7 +41,7 @@ import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.activities.AboutUserPreferencesActivity; import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity; -import nodomain.freeyourgadget.gadgetbridge.activities.MainActivity; +import nodomain.freeyourgadget.gadgetbridge.activities.ControlCenterv2; import nodomain.freeyourgadget.gadgetbridge.activities.discovery.DiscoveryActivityV2; import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; @@ -173,7 +173,7 @@ public class MiBandPairingActivity extends AbstractGBActivity implements Bonding Prefs prefs = GBApplication.getPrefs(); prefs.getPreferences().edit().putString(MiBandConst.PREF_MIBAND_ADDRESS, macAddress).apply(); } - Intent intent = new Intent(this, MainActivity.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + Intent intent = new Intent(this, ControlCenterv2.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); startActivity(intent); } finish(); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebblePairingActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebblePairingActivity.java index c06944d3d..b3b18492b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebblePairingActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pebble/PebblePairingActivity.java @@ -41,7 +41,7 @@ import de.greenrobot.dao.query.Query; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity; -import nodomain.freeyourgadget.gadgetbridge.activities.MainActivity; +import nodomain.freeyourgadget.gadgetbridge.activities.ControlCenterv2; import nodomain.freeyourgadget.gadgetbridge.activities.discovery.DiscoveryActivityV2; import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator; @@ -170,7 +170,7 @@ public class PebblePairingActivity extends AbstractGBActivity implements Bonding public void onBondingComplete(boolean success) { unregisterBroadcastReceivers(); if (success) { - startActivity(new Intent(this, MainActivity.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)); + startActivity(new Intent(this, ControlCenterv2.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)); } else { startActivity(new Intent(this, DiscoveryActivityV2.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/watch9/Watch9PairingActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/watch9/Watch9PairingActivity.java index aaa16f4dd..afa7e7dc4 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/watch9/Watch9PairingActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/watch9/Watch9PairingActivity.java @@ -34,7 +34,7 @@ import org.slf4j.LoggerFactory; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity; -import nodomain.freeyourgadget.gadgetbridge.activities.MainActivity; +import nodomain.freeyourgadget.gadgetbridge.activities.ControlCenterv2; import nodomain.freeyourgadget.gadgetbridge.activities.discovery.DiscoveryActivityV2; import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; @@ -93,7 +93,7 @@ public class Watch9PairingActivity extends AbstractGBActivity implements Bonding @Override public void onBondingComplete(boolean success) { - startActivity(new Intent(this, MainActivity.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)); + startActivity(new Intent(this, ControlCenterv2.class).setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)); finish(); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GB.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GB.java index 60587e237..a151a64ec 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GB.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GB.java @@ -55,7 +55,7 @@ import java.util.List; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.GBEnvironment; import nodomain.freeyourgadget.gadgetbridge.R; -import nodomain.freeyourgadget.gadgetbridge.activities.MainActivity; +import nodomain.freeyourgadget.gadgetbridge.activities.ControlCenterv2; import nodomain.freeyourgadget.gadgetbridge.activities.SettingsActivity; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventScreenshot; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; @@ -157,7 +157,7 @@ public class GB { } private static PendingIntent getContentIntent(Context context) { - Intent notificationIntent = new Intent(context, MainActivity.class); + Intent notificationIntent = new Intent(context, ControlCenterv2.class); notificationIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); PendingIntent pendingIntent = PendingIntentUtils.getActivity(context, 0, @@ -465,7 +465,7 @@ public class GB { private static Notification createTransferNotification(String title, String text, boolean ongoing, int percentage, Context context) { - Intent notificationIntent = new Intent(context, MainActivity.class); + Intent notificationIntent = new Intent(context, ControlCenterv2.class); notificationIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); PendingIntent pendingIntent = PendingIntentUtils.getActivity(context, 0, @@ -501,7 +501,7 @@ public class GB { } public static void createGpsNotification(Context context, int numDevices) { - Intent notificationIntent = new Intent(context, MainActivity.class); + Intent notificationIntent = new Intent(context, ControlCenterv2.class); notificationIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); PendingIntent pendingIntent = PendingIntentUtils.getActivity(context, 0, notificationIntent, 0, false); @@ -523,7 +523,7 @@ public class GB { private static Notification createInstallNotification(String text, boolean ongoing, int percentage, Context context) { - Intent notificationIntent = new Intent(context, MainActivity.class); + Intent notificationIntent = new Intent(context, ControlCenterv2.class); notificationIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); PendingIntent pendingIntent = PendingIntentUtils.getActivity(context, 0, @@ -553,7 +553,7 @@ public class GB { } private static Notification createBatteryNotification(String text, String bigText, Context context) { - Intent notificationIntent = new Intent(context, MainActivity.class); + Intent notificationIntent = new Intent(context, ControlCenterv2.class); notificationIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); PendingIntent pendingIntent = PendingIntentUtils.getActivity(context, 0, diff --git a/app/src/main/res/layout/activity_data_management.xml b/app/src/main/res/layout/activity_data_management.xml index ff0c5fe22..d04c07c24 100644 --- a/app/src/main/res/layout/activity_data_management.xml +++ b/app/src/main/res/layout/activity_data_management.xml @@ -3,7 +3,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context="nodomain.freeyourgadget.gadgetbridge.activities.MainActivity"> + tools:context="nodomain.freeyourgadget.gadgetbridge.activities.ControlCenterv2"> + tools:context="nodomain.freeyourgadget.gadgetbridge.activities.ControlCenterv2"> + tools:context=".activities.ControlCenterv2"> + tools:context=".activities.ControlCenterv2"> + tools:context=".activities.DevicesFragment"> + tools:layout="@layout/fragment_devices">