/* 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 static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.ACTION_CONNECT;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Resources;
import android.os.Bundle;
import android.util.TypedValue;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.activity.OnBackPressedCallback;
import androidx.appcompat.app.ActionBarDrawerToggle;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.core.view.GravityCompat;
import androidx.core.view.MenuProvider;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import androidx.viewpager2.widget.ViewPager2;
import com.google.android.material.appbar.MaterialToolbar;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import com.google.android.material.navigation.NavigationView;
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.tabs.TabLayoutMediator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.Serializable;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
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.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
import nodomain.freeyourgadget.gadgetbridge.model.RecordedDataTypes;
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;
import nodomain.freeyourgadget.gadgetbridge.womp.WompKeepAliveService;
import nodomain.freeyourgadget.gadgetbridge.womp.WompWorkbenchFragment;
//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;
private boolean isLanguageInvalid = false;
private boolean isThemeInvalid = false;
private ViewPager2 viewPager;
private FragmentStateAdapter pagerAdapter;
private SwipeRefreshLayout swipeLayout;
private AlertDialog clDialog;
private TabLayout bandTabs;
private View workbenchContainer;
private BottomNavigationView navigationView;
private long lastBackPressedAt;
//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 DeviceService.ACTION_REALTIME_SAMPLES:
final GBDevice device = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
handleRealtimeSample(device, intent.getSerializableExtra(DeviceService.EXTRA_REALTIME_SAMPLE));
break;
case GBDevice.ACTION_DEVICE_CHANGED:
GBDevice dev = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
if (dev != null && !dev.isBusy()) {
swipeLayout.setRefreshing(false);
}
break;
}
}
};
private boolean pesterWithPermissions = true;
private final Map currentHRSample = new HashMap<>();
public ActivitySample getCurrentHRSample(final GBDevice device) {
return currentHRSample.get(device);
}
private void setCurrentHRSample(final GBDevice device, ActivitySample sample) {
if (HeartRateUtils.getInstance().isValidHeartRateValue(sample.getHeartRate())) {
currentHRSample.put(device, sample);
}
}
private void handleRealtimeSample(final GBDevice device, Serializable extra) {
if (extra instanceof ActivitySample) {
ActivitySample sample = (ActivitySample) extra;
setCurrentHRSample(device, sample);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
AbstractGBActivity.init(this, AbstractGBActivity.NO_ACTIONBAR);
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
WompKeepAliveService.start(this);
Prefs prefs = GBApplication.getPrefs();
// Determine availability of device with activity tracking functionality
boolean activityTrackerAvailable = false;
List devices = GBApplication.app().getDeviceManager().getDevices();
for (GBDevice dev : devices) {
if (dev.getDeviceCoordinator().supportsActivityTracking()) {
activityTrackerAvailable = true;
break;
}
}
if (savedInstanceState != null) {
if (savedInstanceState.getBoolean("cl")) {
GBChangeLog cl = GBChangeLog.createChangeLog(this);
try {
if (cl.hasChanges(false)) {
clDialog = cl.getMaterialLogDialog();
} else {
clDialog = cl.getMaterialFullLogDialog();
}
clDialog.show();
} catch (Exception ignored) {
GB.toast(getBaseContext(), getString(R.string.error_showing_changelog), Toast.LENGTH_LONG, GB.ERROR);
}
}
}
// Initialize drawer
NavigationView drawerNavigationView = findViewById(R.id.nav_view);
drawerNavigationView.setNavigationItemSelectedListener(this);
// Initialize bottom navigation:手环 / 工作台两个顶级入口,互不依赖登录
navigationView = findViewById(R.id.bottom_nav_bar);
bandTabs = findViewById(R.id.band_tabs);
workbenchContainer = findViewById(R.id.workbench_container);
navigationView.setVisibility(View.VISIBLE);
navigationView.setOnItemSelectedListener(menuItem -> {
final int itemId = menuItem.getItemId();
if (itemId == R.id.bottom_nav_band) {
showBandContent();
} else if (itemId == R.id.bottom_nav_workbench) {
showWorkbenchContent();
}
return true;
});
if (savedInstanceState == null) {
getSupportFragmentManager().beginTransaction()
.replace(R.id.workbench_container, new WompWorkbenchFragment(), "womp_workbench")
.commit();
}
// Initialize actionbar
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(com.google.android.material.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));
}
// Configure ViewPager2 with fragment adapter and default fragment
viewPager = findViewById(R.id.dashboard_viewpager);
pagerAdapter = new MainFragmentsPagerAdapter(this);
viewPager.setAdapter(pagerAdapter);
new TabLayoutMediator(bandTabs, viewPager, (tab, position) -> {
if (position == 0) {
tab.setText(R.string.bottom_nav_health);
} else {
tab.setText(R.string.bottom_nav_devices);
}
}).attach();
if (!prefs.getBoolean("dashboard_as_default_view", true) || !activityTrackerAvailable) {
viewPager.setCurrentItem(1, false);
}
// Sync ViewPager changes with fragment menus
viewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
private MenuProvider existingMenuProvider = null;
@Override
public void onPageSelected(int position) {
// Ensure the menu provider is set to the current fragment
if (existingMenuProvider != null) {
ControlCenterv2.this.removeMenuProvider(existingMenuProvider);
}
final Fragment fragment = getSupportFragmentManager().findFragmentByTag("f" + position);
if (fragment instanceof MenuProvider) {
existingMenuProvider = (MenuProvider) fragment;
ControlCenterv2.this.addMenuProvider(existingMenuProvider);
}
}
});
// Make sure the SwipeRefreshLayout doesn't interfere with the ViewPager2
swipeLayout = findViewById(R.id.dashboard_swipe_layout);
if (viewPager.getChildAt(0) != null) {
viewPager.getChildAt(0).setOnTouchListener((v, event) -> {
if (event.getAction() == MotionEvent.ACTION_MOVE) {
swipeLayout.setEnabled(false);
} else {
swipeLayout.setEnabled(true);
}
return false;
});
}
// Set pull-down-to-refresh action
swipeLayout.setOnRefreshListener(() -> {
// Fetch activity for all connected devices
GBApplication.deviceService().onFetchRecordedData(RecordedDataTypes.TYPE_SYNC);
// Hide 'refreshing' animation immediately if no health devices are connected
List devices1 = GBApplication.app().getDeviceManager().getDevices();
for (GBDevice dev : devices1) {
if (dev.getDeviceCoordinator().supportsActivityTracking() && dev.isInitialized()) {
return;
}
}
swipeLayout.setRefreshing(false);
GB.toast(getString(R.string.info_no_devices_connected), Toast.LENGTH_LONG, GB.WARN);
});
// Set up local intent listener
IntentFilter filterLocal = new IntentFilter();
filterLocal.addAction(GBApplication.ACTION_LANGUAGE_CHANGE);
filterLocal.addAction(GBApplication.ACTION_THEME_CHANGE);
filterLocal.addAction(GBApplication.ACTION_QUIT);
filterLocal.addAction(DeviceService.ACTION_REALTIME_SAMPLES);
filterLocal.addAction(GBDevice.ACTION_DEVICE_CHANGED);
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filterLocal);
// 分拣员端跳过首次引导与更新日志
boolean firstRun = prefs.getBoolean("first_run", true);
if (firstRun) {
prefs.getPreferences().edit().putBoolean("first_run", false).apply();
}
pesterWithPermissions = prefs.getBoolean("permission_pestering", true);
if (pesterWithPermissions && !PermissionsUtils.checkAllPermissions(this)) {
Intent permissionsIntent = new Intent(this, PermissionsActivity.class);
startActivity(permissionsIntent);
}
/*
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);
}
}
*/
GBApplication.deviceService().requestDeviceInfo();
if (savedInstanceState != null && !savedInstanceState.getBoolean("womp_wb", true)) {
navigationView.setSelectedItemId(R.id.bottom_nav_band);
showBandContent();
} else {
navigationView.setSelectedItemId(R.id.bottom_nav_workbench);
showWorkbenchContent();
}
getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() {
handleMainBackPressed();
}
});
}
public void showBandTab() {
if (navigationView != null) {
navigationView.setSelectedItemId(R.id.bottom_nav_band);
} else {
showBandContent();
}
}
private void showBandContent() {
if (workbenchContainer != null) {
workbenchContainer.setVisibility(View.GONE);
}
if (swipeLayout != null) {
swipeLayout.setVisibility(View.VISIBLE);
}
if (bandTabs != null) {
bandTabs.setVisibility(View.VISIBLE);
}
if (getSupportActionBar() != null) {
getSupportActionBar().setTitle(R.string.title_activity_controlcenter);
}
}
private void showWorkbenchContent() {
if (swipeLayout != null) {
swipeLayout.setRefreshing(false);
swipeLayout.setVisibility(View.GONE);
}
if (bandTabs != null) {
bandTabs.setVisibility(View.GONE);
}
if (workbenchContainer != null) {
workbenchContainer.setVisibility(View.VISIBLE);
}
if (getSupportActionBar() != null) {
getSupportActionBar().setTitle(R.string.womp_workbench);
}
}
private void handleMainBackPressed() {
final DrawerLayout drawer = findViewById(R.id.drawer_layout);
if (drawer != null && drawer.isDrawerOpen(GravityCompat.START)) {
drawer.closeDrawer(GravityCompat.START);
return;
}
if (workbenchContainer != null && workbenchContainer.getVisibility() == View.VISIBLE) {
Fragment fragment = getSupportFragmentManager().findFragmentByTag("womp_workbench");
if (fragment instanceof WompWorkbenchFragment && ((WompWorkbenchFragment) fragment).goBack()) {
return;
}
}
long now = System.currentTimeMillis();
if (now - lastBackPressedAt < 2000) {
finishAffinity();
return;
}
lastBackPressedAt = now;
Toast.makeText(this, R.string.womp_press_again_to_exit, Toast.LENGTH_SHORT).show();
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
if (clDialog != null){
outState.putBoolean("cl", clDialog.isShowing());
}
if (workbenchContainer != null) {
outState.putBoolean("womp_wb", workbenchContainer.getVisibility() == View.VISIBLE);
}
super.onSaveInstanceState(outState);
}
@Override
protected void onResume() {
super.onResume();
handleShortcut(getIntent());
if (isLanguageInvalid || isThemeInvalid) {
isLanguageInvalid = false;
isThemeInvalid = false;
recreate();
}
}
@Override
protected void onDestroy() {
try {
LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
} catch (Exception ignored) {
}
super.onDestroy();
}
@Override
public boolean onNavigationItemSelected(@NonNull final MenuItem item) {
final DrawerLayout drawer = findViewById(R.id.drawer_layout);
drawer.closeDrawer(GravityCompat.START);
final int itemId = item.getItemId();
if (itemId == R.id.action_womp_workbench) {
if (navigationView != null) {
navigationView.setSelectedItemId(R.id.bottom_nav_workbench);
} else {
showWorkbenchContent();
}
return false;
} else if (itemId == R.id.action_settings) {
final Intent settingsIntent = new Intent(this, SettingsActivity.class);
startActivityForResult(settingsIntent, MENU_REFRESH_CODE);
return false;
} else if (itemId == R.id.device_action_discover) {
launchDiscoveryActivity();
return false;
} else if (itemId == R.id.action_quit) {
GBApplication.quit();
return false;
} else if (itemId == R.id.about) {
final Intent aboutIntent = new Intent(this, AboutActivity.class);
startActivity(aboutIntent);
return false;
}
/*
} else if (itemId == R.id.action_debug) {
final Intent debugIntent = new Intent(this, DebugActivity.class);
startActivity(debugIntent);
return false;
} else if (itemId == R.id.action_data_management) {
final Intent dbIntent = new Intent(this, DataManagementActivity.class);
startActivity(dbIntent);
return false;
} else if (itemId == R.id.donation_link) {
...
} else if (itemId == R.id.external_changelog) {
...
}
*/
return false; // we do not want the drawer menu item to get selected
}
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 void setLanguage(Locale language, boolean invalidateLanguage) {
if (invalidateLanguage) {
isLanguageInvalid = true;
}
AndroidUtils.setLanguage(this, language);
}
private class MainFragmentsPagerAdapter extends FragmentStateAdapter {
public MainFragmentsPagerAdapter(FragmentActivity fa) {
super(fa);
}
@Override
public Fragment createFragment(int position) {
if (position == 0) {
return new DashboardFragment();
} else {
return new DevicesFragment();
}
}
@Override
public int getItemCount() {
return 2;
}
}
}