Migrate ViewPager usages to ViewPager2

Also migrate from FragmentStatePagerAdapter to FragmentStateAdapter, preserving the getPageTitle method for compatibility.
Layouts needs adjustments since the TabLayout cannot be a child of ViewPager2.
Further no subclassing is possible hence NonSwipeableViewPager has to be deleted, but the functionality is retained.
Custom classes to handle FragmentAdapters are also removed, and lifecycle of activities (using fragments) has been streamlined.
This commit is contained in:
Daniele Gobbetti
2025-08-28 09:38:08 +02:00
committed by daniele
parent 23210bac26
commit b0d76f8204
25 changed files with 470 additions and 840 deletions
@@ -1,68 +0,0 @@
/* Copyright (C) 2015-2024 Carsten Pfeiffer, Daniele Gobbetti
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.view.ViewGroup;
import java.util.HashSet;
import java.util.Set;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentStatePagerAdapter;
public abstract class AbstractFragmentPagerAdapter extends FragmentStatePagerAdapter {
private final Set<AbstractGBFragment> fragments = new HashSet<>();
private Object primaryFragment;
public AbstractFragmentPagerAdapter(FragmentManager fm) {
super(fm);
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
Object fragment = super.instantiateItem(container, position);
if (fragment instanceof AbstractGBFragment) {
fragments.add((AbstractGBFragment) fragment);
}
return fragment;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
super.destroyItem(container, position, object);
fragments.remove(object);
}
@Override
public void setPrimaryItem(ViewGroup container, int position, Object object) {
super.setPrimaryItem(container, position, object);
if (object != primaryFragment) {
primaryFragment = object;
setCurrentFragment(primaryFragment);
}
}
private void setCurrentFragment(Object newCurrentFragment) {
for (AbstractGBFragment frag : fragments) {
if (frag != newCurrentFragment) {
frag.onMadeInvisibleInActivity();
} else {
frag.onMadeVisibleInActivityInternal();
}
}
}
}
@@ -21,54 +21,11 @@ import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
/**
* Abstract base class for fragments. Provides hooks that are called when
* the fragment is made visible and invisible in the activity. also allows
* Abstract base class for fragments. Allows
* the fragment to define the title to be shown in the activity.
*
* @see AbstractGBFragmentActivity
*/
public abstract class AbstractGBFragment extends Fragment {
private boolean mVisibleInActivity;
/**
* Called when this fragment has been fully scrolled into the activity.
*
* @see #isVisibleInActivity()
* @see #onMadeInvisibleInActivity()
*/
protected void onMadeVisibleInActivity() {
}
/**
* Called when this fragment has been scrolled out of the activity.
*
* @see #isVisibleInActivity()
* @see #onMadeVisibleInActivity()
*/
public void onMadeInvisibleInActivity() {
mVisibleInActivity = false;
}
/**
* Returns true if this fragment is currently visible in the hosting
* activity, not taking into account whether the screen is enabled at all.
*/
public boolean isVisibleInActivity() {
return mVisibleInActivity;
}
@Nullable
protected abstract CharSequence getTitle();
/**
* Internal
*
* @hide
*/
public void onMadeVisibleInActivityInternal() {
mVisibleInActivity = true;
if (isVisible()) {
onMadeVisibleInActivity();
}
}
}
@@ -1,72 +0,0 @@
/* Copyright (C) 2015-2024 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities;
import android.os.Bundle;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;
import androidx.fragment.app.FragmentStatePagerAdapter;
import androidx.viewpager.widget.PagerAdapter;
/**
* A base activity that supports paging through fragments by swiping.
* Subclasses will have to add a ViewPager to their layout and add something
* like this to hook it to the fragments:
* <p/>
* <pre>
* // Set up the ViewPager with the sections adapter.
* ViewPager viewPager = (ViewPager) findViewById(R.id.pager);
* viewPager.setAdapter(getPagerAdapter());
* </pre>
*
* @see AbstractGBFragment
*/
public abstract class AbstractGBFragmentActivity extends AbstractGBActivity {
/**
* The {@link PagerAdapter} that will provide
* fragments for each of the sections. We use a
* {@link FragmentPagerAdapter} derivative, which will keep every
* loaded fragment in memory. If this becomes too memory intensive, it
* may be best to switch to a
* {@link FragmentStatePagerAdapter}.
*/
private AbstractFragmentPagerAdapter mSectionsPagerAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Create the adapter that will return a fragment for each of the three
// primary sections of the activity.
mSectionsPagerAdapter = createFragmentPagerAdapter(getSupportFragmentManager());
}
public AbstractFragmentPagerAdapter getPagerAdapter() {
return mSectionsPagerAdapter;
}
/**
* Creates a PagerAdapter that will create the fragments to be used with this
* activity. The fragments should typically extend AbstractGBFragment
*
* @param fragmentManager
* @return
*/
protected abstract AbstractFragmentPagerAdapter createFragmentPagerAdapter(FragmentManager fragmentManager);
}
@@ -26,6 +26,8 @@ import android.view.View;
import android.widget.Toast;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.tabs.TabLayoutMediator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -42,11 +44,11 @@ import java.util.UUID;
import androidx.annotation.NonNull;
import androidx.core.app.NavUtils;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.viewpager.widget.ViewPager;
import androidx.fragment.app.FragmentActivity;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import androidx.viewpager2.widget.ViewPager2;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractFragmentPagerAdapter;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragmentActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
@@ -54,7 +56,7 @@ import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class AppManagerActivity extends AbstractGBFragmentActivity {
public class AppManagerActivity extends AbstractGBActivity {
private static final Logger LOG = LoggerFactory.getLogger(AppManagerActivity.class);
private static final int READ_REQUEST_CODE = 42;
@@ -109,15 +111,16 @@ public class AppManagerActivity extends AbstractGBFragmentActivity {
}
// Set up the ViewPager with the sections adapter.
ViewPager viewPager = findViewById(R.id.appmanager_pager);
ViewPager2 viewPager = findViewById(R.id.appmanager_pager);
if (viewPager != null) {
viewPager.setAdapter(getPagerAdapter());
viewPager.setAdapter(new SectionsStateAdapter(this));
}
}
@Override
protected AbstractFragmentPagerAdapter createFragmentPagerAdapter(FragmentManager fragmentManager) {
return new SectionsPagerAdapter(fragmentManager);
TabLayout tabLayout = findViewById(R.id.charts_pagerTabStrip);
new TabLayoutMediator(tabLayout, viewPager,
(tab, position) -> {
tab.setText(getPageTitle(position));
}).attach();
}
public static synchronized void deleteFromAppOrderFile(String filename, UUID uuid) {
@@ -126,16 +129,14 @@ public class AppManagerActivity extends AbstractGBFragmentActivity {
rewriteAppOrderFile(filename, uuids);
}
public class SectionsPagerAdapter extends AbstractFragmentPagerAdapter {
SectionsPagerAdapter(FragmentManager fm) {
super(fm);
public class SectionsStateAdapter extends FragmentStateAdapter {
SectionsStateAdapter(FragmentActivity activity) {
super(activity);
}
@NonNull
@Override
public Fragment getItem(int position) {
// getItem is called to instantiate the fragment for the given page.
public Fragment createFragment(int position) {
return switch (enabledTabsList.get(position)) {
case "cache" -> new AppManagerFragmentCache();
case "apps" -> new AppManagerFragmentInstalledApps();
@@ -145,19 +146,18 @@ public class AppManagerActivity extends AbstractGBFragmentActivity {
}
@Override
public int getCount() {
public int getItemCount() {
return enabledTabsList.size();
}
}
@Override
public CharSequence getPageTitle(int position) {
return switch (enabledTabsList.get(position)) {
case "cache" -> getString(R.string.appmanager_cached_watchapps_watchfaces);
case "apps" -> getString(R.string.appmanager_installed_watchapps);
case "watchfaces" -> getString(R.string.appmanager_installed_watchfaces);
default -> super.getPageTitle(position);
};
}
public CharSequence getPageTitle(int position) {
return switch (enabledTabsList.get(position)) {
case "cache" -> getString(R.string.appmanager_cached_watchapps_watchfaces);
case "apps" -> getString(R.string.appmanager_installed_watchapps);
case "watchfaces" -> getString(R.string.appmanager_installed_watchfaces);
default -> "";
};
}
@Override
@@ -147,17 +147,17 @@ public abstract class AbstractChartFragment<D extends ChartsData> extends Abstra
/**
* Called when this fragment has been fully scrolled into the activity.
*
* @see #isVisibleInActivity()
* @see #onMadeInvisibleInActivity()
*/
@Override
protected void onMadeVisibleInActivity() {
super.onMadeVisibleInActivity();
showDateBar(true);
updateDateInfo(getStartDate(), getEndDate());
if (mChartDirty) {
refresh();
public void onResume() {
if ((requireActivity() instanceof ChartsHost)) {
showDateBar(true);
updateDateInfo(getStartDate(), getEndDate());
if (mChartDirty) {
refresh();
}
}
super.onResume();
}
protected ChartsHost getChartsHost() {
@@ -248,7 +248,7 @@ public abstract class AbstractChartFragment<D extends ChartsData> extends Abstra
* @param offset the offset, in days
*/
private void handleDate(Date startDate, Date endDate, Integer offset) {
if (isVisibleInActivity()) {
if (isResumed()) {
if (!shiftDates(startDate, endDate, offset)) {
return;
}
@@ -259,7 +259,7 @@ public abstract class AbstractChartFragment<D extends ChartsData> extends Abstra
}
private void refreshIfVisible() {
if (isVisibleInActivity()) {
if (isResumed()) {
refresh();
} else {
mChartDirty = true;
@@ -1,360 +0,0 @@
/* Copyright (C) 2023-2024 José Rebelo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
import android.app.DatePickerDialog;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import androidx.activity.result.ActivityResult;
import androidx.activity.result.ActivityResultCallback;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import androidx.viewpager.widget.ViewPager;
import com.google.android.material.tabs.TabLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragmentActivity;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public abstract class AbstractChartsActivity extends AbstractGBFragmentActivity implements ChartsHost {
private static final Logger LOG = LoggerFactory.getLogger(AbstractChartsActivity.class);
public static final String STATE_START_DATE = "stateStartDate";
public static final String STATE_END_DATE = "stateEndDate";
public static final String EXTRA_FRAGMENT_ID = "fragmentId";
public static final String EXTRA_SINGLE_FRAGMENT_NAME = "singleFragmentName";
public static final String EXTRA_ACTIONBAR_TITLE = "actionbarTitle";
public static final String EXTRA_TIMESTAMP = "timestamp";
public static final String EXTRA_MODE = "mode";
private TextView mDateControl;
private Date mStartDate;
private Date mEndDate;
private SwipeRefreshLayout swipeLayout;
List<String> enabledTabsList;
private GBDevice mGBDevice;
private ViewGroup dateBar;
private ActivityResultLauncher<Intent> chartsPreferencesLauncher;
private final ActivityResultCallback<ActivityResult> chartsPreferencesCallback = result -> {
recreate();
};
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(final Context context, final Intent intent) {
final String action = intent.getAction();
//noinspection SwitchStatementWithTooFewBranches
switch (Objects.requireNonNull(action)) {
case GBDevice.ACTION_DEVICE_CHANGED:
final GBDevice dev = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
if (dev != null) {
refreshBusyState(dev);
}
break;
}
}
};
private void refreshBusyState(final GBDevice dev) {
if (dev.isBusy()) {
swipeLayout.setRefreshing(true);
} else {
final boolean wasBusy = swipeLayout.isRefreshing();
swipeLayout.setRefreshing(false);
if (wasBusy) {
LocalBroadcastManager.getInstance(this).sendBroadcast(new Intent(REFRESH));
}
}
enableSwipeRefresh(true);
}
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_charts);
final Bundle extras = getIntent().getExtras();
if (extras == null) {
throw new IllegalArgumentException("Must provide a device when invoking this activity");
}
mGBDevice = extras.getParcelable(GBDevice.EXTRA_DEVICE);
chartsPreferencesLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
chartsPreferencesCallback
);
// Set start and end date
if (savedInstanceState != null) {
setEndDate(new Date(savedInstanceState.getLong(STATE_END_DATE, System.currentTimeMillis())));
} else if (extras.containsKey(EXTRA_TIMESTAMP)) {
final int endTimestamp = extras.getInt(EXTRA_TIMESTAMP, 0);
setEndDate(new Date(endTimestamp * 1000L));
} else {
setEndDate(new Date());
}
setStartDate(DateTimeUtils.shiftByDays(getEndDate(), -1));
final IntentFilter filterLocal = new IntentFilter();
filterLocal.addAction(GBDevice.ACTION_DEVICE_CHANGED);
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filterLocal);
// Open the specified fragment, if any, and setup single page view if specified
final int tabFragmentIdToOpen = extras.getInt(EXTRA_FRAGMENT_ID, -1);
final String singleFragmentName = extras.getString(EXTRA_SINGLE_FRAGMENT_NAME, null);
final int actionbarTitle = extras.getInt(EXTRA_ACTIONBAR_TITLE, 0);
if (tabFragmentIdToOpen >= 0 && singleFragmentName != null) {
throw new IllegalArgumentException("Must specify either fragment ID or single fragment name, not both");
}
if (singleFragmentName != null) {
enabledTabsList = Collections.singletonList(singleFragmentName);
} else {
enabledTabsList = fillChartsTabsList();
}
swipeLayout = findViewById(R.id.activity_swipe_layout);
swipeLayout.setOnRefreshListener(this::fetchRecordedData);
enableSwipeRefresh(true);
// Set up the ViewPager with the sections adapter.
final NonSwipeableViewPager viewPager = findViewById(R.id.charts_pager);
viewPager.setAdapter(getPagerAdapter());
if (tabFragmentIdToOpen > -1) {
viewPager.setCurrentItem(tabFragmentIdToOpen); // open the tab as specified in the intent
}
viewPager.setAllowSwipe(singleFragmentName == null && GBApplication.getPrefs().getBoolean("charts_allow_swipe", true));
if (singleFragmentName != null) {
final TabLayout tabLayout = findViewById(R.id.charts_pagerTabStrip);
tabLayout.setVisibility(TextView.GONE);
}
if (actionbarTitle != 0) {
final ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setTitle(actionbarTitle);
}
}
viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
@Override
public void onPageSelected(int position) {
}
@Override
public void onPageScrollStateChanged(int state) {
enableSwipeRefresh(state == ViewPager.SCROLL_STATE_IDLE);
}
});
dateBar = findViewById(R.id.charts_date_bar);
mDateControl = findViewById(R.id.charts_text_date);
mDateControl.setOnClickListener(v -> {
String detailedDuration = formatDetailedDuration();
new ShowDurationDialog(detailedDuration, AbstractChartsActivity.this).show();
});
final Button mPrevButton = findViewById(R.id.charts_previous_day);
mPrevButton.setOnClickListener(v -> handleButtonClicked(DATE_PREV_DAY));
final Button mNextButton = findViewById(R.id.charts_next_day);
mNextButton.setOnClickListener(v -> handleButtonClicked(DATE_NEXT_DAY));
final Button mPrevWeekButton = findViewById(R.id.charts_previous_week);
mPrevWeekButton.setOnClickListener(v -> handleButtonClicked(DATE_PREV_WEEK));
final Button mNextWeekButton = findViewById(R.id.charts_next_week);
mNextWeekButton.setOnClickListener(v -> handleButtonClicked(DATE_NEXT_WEEK));
final Button mPrevMonthButton = findViewById(R.id.charts_previous_month);
mPrevMonthButton.setOnClickListener(v -> handleButtonClicked(DATE_PREV_MONTH));
final Button mNextMonthButton = findViewById(R.id.charts_next_month);
mNextMonthButton.setOnClickListener(v -> handleButtonClicked(DATE_NEXT_MONTH));
}
@Override
protected void onSaveInstanceState(@NonNull final Bundle outState) {
super.onSaveInstanceState(outState);
outState.putLong(STATE_END_DATE, getEndDate().getTime());
outState.putLong(STATE_START_DATE, getStartDate().getTime());
}
@Override
protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
setEndDate(new Date(savedInstanceState.getLong(STATE_END_DATE, System.currentTimeMillis())));
setStartDate(new Date(savedInstanceState.getLong(STATE_START_DATE, DateTimeUtils.shiftByDays(getEndDate(), -1).getTime())));
}
protected abstract List<String> fillChartsTabsList();
private String formatDetailedDuration() {
final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault());
final String dateStringFrom = dateFormat.format(getStartDate());
final String dateStringTo = dateFormat.format(getEndDate());
return getString(R.string.sleep_activity_date_range, dateStringFrom, dateStringTo);
}
protected void initDates() {
setEndDate(new Date());
setStartDate(DateTimeUtils.shiftByDays(getEndDate(), -1));
}
@Override
public GBDevice getDevice() {
return mGBDevice;
}
@Override
public void setStartDate(Date startDate) {
mStartDate = startDate;
}
@Override
public void setEndDate(Date endDate) {
mEndDate = endDate;
}
@Override
public Date getStartDate() {
return mStartDate;
}
@Override
public Date getEndDate() {
return mEndDate;
}
@Override
public void setDateInfo(final String dateInfo) {
mDateControl.setText(dateInfo);
}
@Override
public ViewGroup getDateBar() {
return dateBar;
}
private void handleButtonClicked(final String action) {
LocalBroadcastManager.getInstance(this).sendBroadcast(new Intent(action));
}
@Override
protected void onDestroy() {
LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
super.onDestroy();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
getMenuInflater().inflate(R.menu.menu_charts, menu);
if (!mGBDevice.isConnected() || !supportsRefresh()) {
menu.removeItem(R.id.charts_fetch_activity_data);
}
return true;
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
final int itemId = item.getItemId();
if (itemId == R.id.charts_fetch_activity_data) {
fetchRecordedData();
return true;
} else if (itemId == R.id.charts_set_date) {
final Calendar currentDate = Calendar.getInstance();
currentDate.setTime(getEndDate());
new DatePickerDialog(this, (view, year, monthOfYear, dayOfMonth) -> {
currentDate.set(year, monthOfYear, dayOfMonth);
setEndDate(currentDate.getTime());
setStartDate(DateTimeUtils.shiftByDays(getEndDate(), -1));
LocalBroadcastManager.getInstance(this).sendBroadcast(new Intent(REFRESH));
}, currentDate.get(Calendar.YEAR), currentDate.get(Calendar.MONTH), currentDate.get(Calendar.DATE)).show();
} else if (itemId == R.id.prefs_charts_menu) {
final Intent settingsIntent = new Intent(this, ChartsPreferencesActivity.class);
chartsPreferencesLauncher.launch(settingsIntent);
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public void enableSwipeRefresh(final boolean enable) {
swipeLayout.setEnabled(enable && GBApplication.getPrefs().refreshOnSwipe() && allowRefresh());
}
protected abstract boolean supportsRefresh();
protected abstract boolean allowRefresh();
protected abstract int getRecordedDataType();
private void fetchRecordedData() {
if (getDevice().isInitialized()) {
GBApplication.deviceService(getDevice()).onFetchRecordedData(getRecordedDataType());
} else {
swipeLayout.setRefreshing(false);
GB.toast(this, getString(R.string.device_not_connected), Toast.LENGTH_SHORT, GB.ERROR);
}
}
}
@@ -23,7 +23,7 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentManager;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import androidx.viewpager2.widget.ViewPager2;
import com.google.android.material.tabs.TabLayout;
@@ -31,18 +31,14 @@ import com.google.android.material.tabs.TabLayoutMediator;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragment;
import nodomain.freeyourgadget.gadgetbridge.adapter.NestedFragmentAdapter;
public abstract class AbstractCollectionFragment extends AbstractGBFragment {
protected static final String ARG_ALLOW_SWIPE = "allow_swipe";
protected NestedFragmentAdapter nestedFragmentsAdapter;
protected FragmentStateAdapter fragmentAdapter;
protected ViewPager2 viewPager;
private int last_position = 0;
private boolean allowSwipe;
public abstract NestedFragmentAdapter getNestedFragmentAdapter(final AbstractGBFragment fragment,
final FragmentManager childFragmentManager);
public abstract FragmentStateAdapter getFragmentAdapter();
@Override
public void onCreate(final Bundle savedInstanceState) {
@@ -52,44 +48,18 @@ public abstract class AbstractCollectionFragment extends AbstractGBFragment {
}
}
@Override
protected void onMadeVisibleInActivity() {
super.onMadeVisibleInActivity();
nestedFragmentsAdapter.updateFragments(last_position);
}
@Override
public void onMadeInvisibleInActivity() {
if (nestedFragmentsAdapter != null) {
nestedFragmentsAdapter.updateFragments(-1);
}
}
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_nested_tabs, container, false);
nestedFragmentsAdapter = getNestedFragmentAdapter(this, getChildFragmentManager());
fragmentAdapter = getFragmentAdapter();
viewPager = rootView.findViewById(R.id.pager);
viewPager.setAdapter(nestedFragmentsAdapter);
viewPager.setAdapter(fragmentAdapter);
if (!allowSwipe) {
viewPager.setOrientation(ViewPager2.ORIENTATION_VERTICAL);
viewPager.setUserInputEnabled(false);
}
viewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
@Override
public void onPageSelected(int position) {
super.onPageSelected(position);
last_position = position;
viewPager.post(new Runnable() {
@Override
public void run() {
if (isVisibleInActivity()) {
nestedFragmentsAdapter.updateFragments(position);
}
}
});
}
});
return rootView;
}
@@ -1,4 +1,4 @@
/* Copyright (C) 2023-2024 Daniel Dakhno, José Rebelo, Martin.JM
/* Copyright (C) 2023-2024 José Rebelo
This file is part of Gadgetbridge.
@@ -16,61 +16,246 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
import android.app.DatePickerDialog;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import androidx.activity.result.ActivityResult;
import androidx.activity.result.ActivityResultCallback;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentStatePagerAdapter;
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.tabs.TabLayout;
import com.google.android.material.tabs.TabLayoutMediator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractFragmentPagerAdapter;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragment;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityAmounts;
import nodomain.freeyourgadget.gadgetbridge.model.RecordedDataTypes;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.LimitedQueue;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public class ActivityChartsActivity extends AbstractChartsActivity {
public class ActivityChartsActivity extends AbstractGBActivity implements ChartsHost {
private static final Logger LOG = LoggerFactory.getLogger(ActivityChartsActivity.class);
public static final String STATE_START_DATE = "stateStartDate";
public static final String STATE_END_DATE = "stateEndDate";
public static final String EXTRA_FRAGMENT_ID = "fragmentId";
public static final String EXTRA_SINGLE_FRAGMENT_NAME = "singleFragmentName";
public static final String EXTRA_ACTIONBAR_TITLE = "actionbarTitle";
public static final String EXTRA_TIMESTAMP = "timestamp";
public static final String EXTRA_MODE = "mode";
private TextView mDateControl;
private Date mStartDate;
private Date mEndDate;
private SwipeRefreshLayout swipeLayout;
LimitedQueue<Integer, ActivityAmounts> mActivityAmountCache = new LimitedQueue<>(60);
@Override
protected AbstractFragmentPagerAdapter createFragmentPagerAdapter(final FragmentManager fragmentManager) {
return new SectionsPagerAdapter(fragmentManager);
List<String> enabledTabsList;
private GBDevice mGBDevice;
private ViewGroup dateBar;
private ActivityResultLauncher<Intent> chartsPreferencesLauncher;
private final ActivityResultCallback<ActivityResult> chartsPreferencesCallback = result -> {
recreate();
};
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(final Context context, final Intent intent) {
final String action = intent.getAction();
//noinspection SwitchStatementWithTooFewBranches
switch (Objects.requireNonNull(action)) {
case GBDevice.ACTION_DEVICE_CHANGED:
final GBDevice dev = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
if (dev != null) {
refreshBusyState(dev);
}
break;
}
}
};
private void refreshBusyState(final GBDevice dev) {
if (dev.isBusy()) {
swipeLayout.setRefreshing(true);
} else {
final boolean wasBusy = swipeLayout.isRefreshing();
swipeLayout.setRefreshing(false);
if (wasBusy) {
LocalBroadcastManager.getInstance(this).sendBroadcast(new Intent(REFRESH));
}
}
enableSwipeRefresh(true);
}
@Override
protected int getRecordedDataType() {
return RecordedDataTypes.TYPE_ACTIVITY | RecordedDataTypes.TYPE_STRESS;
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_charts);
final Bundle extras = getIntent().getExtras();
if (extras == null) {
throw new IllegalArgumentException("Must provide a device when invoking this activity");
}
mGBDevice = extras.getParcelable(GBDevice.EXTRA_DEVICE);
chartsPreferencesLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
chartsPreferencesCallback
);
// Set start and end date
if (savedInstanceState != null) {
setEndDate(new Date(savedInstanceState.getLong(STATE_END_DATE, System.currentTimeMillis())));
} else if (extras.containsKey(EXTRA_TIMESTAMP)) {
final int endTimestamp = extras.getInt(EXTRA_TIMESTAMP, 0);
setEndDate(new Date(endTimestamp * 1000L));
} else {
setEndDate(new Date());
}
setStartDate(DateTimeUtils.shiftByDays(getEndDate(), -1));
final IntentFilter filterLocal = new IntentFilter();
filterLocal.addAction(GBDevice.ACTION_DEVICE_CHANGED);
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filterLocal);
// Open the specified fragment, if any, and setup single page view if specified
final int tabFragmentIdToOpen = extras.getInt(EXTRA_FRAGMENT_ID, -1);
final String singleFragmentName = extras.getString(EXTRA_SINGLE_FRAGMENT_NAME, null);
final int actionbarTitle = extras.getInt(EXTRA_ACTIONBAR_TITLE, 0);
if (tabFragmentIdToOpen >= 0 && singleFragmentName != null) {
throw new IllegalArgumentException("Must specify either fragment ID or single fragment name, not both");
}
if (singleFragmentName != null) {
enabledTabsList = Collections.singletonList(singleFragmentName);
} else {
enabledTabsList = fillChartsTabsList();
}
swipeLayout = findViewById(R.id.activity_swipe_layout);
swipeLayout.setOnRefreshListener(this::fetchRecordedData);
enableSwipeRefresh(true);
// Set up the ViewPager with the sections adapter.
final ViewPager2 viewPager = findViewById(R.id.charts_pager);
TabLayout tabLayout = findViewById(R.id.charts_pagerTabStrip);
viewPager.setAdapter(getPagerAdapter());
new TabLayoutMediator(tabLayout, viewPager,
(tab, position) -> {
tab.setText(getPageTitle(position));
}).attach();
if (tabFragmentIdToOpen > -1) {
viewPager.setCurrentItem(tabFragmentIdToOpen); // open the tab as specified in the intent
}
viewPager.setUserInputEnabled(singleFragmentName == null && GBApplication.getPrefs().getBoolean("charts_allow_swipe", true));
if (singleFragmentName != null) {
tabLayout.setVisibility(TextView.GONE);
}
if (actionbarTitle != 0) {
final ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setTitle(actionbarTitle);
}
}
dateBar = findViewById(R.id.charts_date_bar);
mDateControl = findViewById(R.id.charts_text_date);
mDateControl.setOnClickListener(v -> {
String detailedDuration = formatDetailedDuration();
new ShowDurationDialog(detailedDuration, ActivityChartsActivity.this).show();
});
final Button mPrevButton = findViewById(R.id.charts_previous_day);
mPrevButton.setOnClickListener(v -> handleButtonClicked(DATE_PREV_DAY));
final Button mNextButton = findViewById(R.id.charts_next_day);
mNextButton.setOnClickListener(v -> handleButtonClicked(DATE_NEXT_DAY));
final Button mPrevWeekButton = findViewById(R.id.charts_previous_week);
mPrevWeekButton.setOnClickListener(v -> handleButtonClicked(DATE_PREV_WEEK));
final Button mNextWeekButton = findViewById(R.id.charts_next_week);
mNextWeekButton.setOnClickListener(v -> handleButtonClicked(DATE_NEXT_WEEK));
final Button mPrevMonthButton = findViewById(R.id.charts_previous_month);
mPrevMonthButton.setOnClickListener(v -> handleButtonClicked(DATE_PREV_MONTH));
final Button mNextMonthButton = findViewById(R.id.charts_next_month);
mNextMonthButton.setOnClickListener(v -> handleButtonClicked(DATE_NEXT_MONTH));
}
@Override
protected boolean supportsRefresh() {
final DeviceCoordinator coordinator = getDevice().getDeviceCoordinator();
return coordinator.supportsActivityDataFetching(getDevice());
protected void onSaveInstanceState(@NonNull final Bundle outState) {
super.onSaveInstanceState(outState);
outState.putLong(STATE_END_DATE, getEndDate().getTime());
outState.putLong(STATE_START_DATE, getStartDate().getTime());
}
@Override
protected boolean allowRefresh() {
final DeviceCoordinator coordinator = getDevice().getDeviceCoordinator();
return coordinator.allowFetchActivityData(getDevice()) && supportsRefresh();
protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
setEndDate(new Date(savedInstanceState.getLong(STATE_END_DATE, System.currentTimeMillis())));
setStartDate(new Date(savedInstanceState.getLong(STATE_START_DATE, DateTimeUtils.shiftByDays(getEndDate(), -1).getTime())));
}
@Override
protected FragmentStateAdapter getPagerAdapter() {
return new SectionsStateAdapter(this);
}
protected List<String> fillChartsTabsList() {
return fillChartsTabsList(getDevice(), this);
}
@@ -150,18 +335,183 @@ public class ActivityChartsActivity extends AbstractChartsActivity {
return enabledTabsList.indexOf(tab);
}
private String formatDetailedDuration() {
final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault());
final String dateStringFrom = dateFormat.format(getStartDate());
final String dateStringTo = dateFormat.format(getEndDate());
return getString(R.string.sleep_activity_date_range, dateStringFrom, dateStringTo);
}
protected void initDates() {
setEndDate(new Date());
setStartDate(DateTimeUtils.shiftByDays(getEndDate(), -1));
}
@Override
public GBDevice getDevice() {
return mGBDevice;
}
@Override
public void setStartDate(Date startDate) {
mStartDate = startDate;
}
@Override
public void setEndDate(Date endDate) {
mEndDate = endDate;
}
@Override
public Date getStartDate() {
return mStartDate;
}
@Override
public Date getEndDate() {
return mEndDate;
}
@Override
public void setDateInfo(final String dateInfo) {
mDateControl.setText(dateInfo);
}
@Override
public ViewGroup getDateBar() {
return dateBar;
}
private void handleButtonClicked(final String action) {
LocalBroadcastManager.getInstance(this).sendBroadcast(new Intent(action));
}
@Override
protected void onDestroy() {
LocalBroadcastManager.getInstance(this).unregisterReceiver(mReceiver);
super.onDestroy();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
getMenuInflater().inflate(R.menu.menu_charts, menu);
if (!mGBDevice.isConnected() || !supportsRefresh()) {
menu.removeItem(R.id.charts_fetch_activity_data);
}
return true;
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
final int itemId = item.getItemId();
if (itemId == R.id.charts_fetch_activity_data) {
fetchRecordedData();
return true;
} else if (itemId == R.id.charts_set_date) {
final Calendar currentDate = Calendar.getInstance();
currentDate.setTime(getEndDate());
new DatePickerDialog(this, (view, year, monthOfYear, dayOfMonth) -> {
currentDate.set(year, monthOfYear, dayOfMonth);
setEndDate(currentDate.getTime());
setStartDate(DateTimeUtils.shiftByDays(getEndDate(), -1));
LocalBroadcastManager.getInstance(this).sendBroadcast(new Intent(REFRESH));
}, currentDate.get(Calendar.YEAR), currentDate.get(Calendar.MONTH), currentDate.get(Calendar.DATE)).show();
} else if (itemId == R.id.prefs_charts_menu) {
final Intent settingsIntent = new Intent(this, ChartsPreferencesActivity.class);
chartsPreferencesLauncher.launch(settingsIntent);
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public void enableSwipeRefresh(final boolean enable) {
swipeLayout.setEnabled(enable && allowRefresh());
}
protected boolean supportsRefresh() {
final DeviceCoordinator coordinator = getDevice().getDeviceCoordinator();
return coordinator.supportsActivityDataFetching(getDevice());
}
protected boolean allowRefresh() {
final DeviceCoordinator coordinator = getDevice().getDeviceCoordinator();
return coordinator.allowFetchActivityData(getDevice()) && supportsRefresh();
}
protected int getRecordedDataType() {
return RecordedDataTypes.TYPE_ACTIVITY | RecordedDataTypes.TYPE_STRESS;
}
private void fetchRecordedData() {
if (getDevice().isInitialized()) {
GBApplication.deviceService(getDevice()).onFetchRecordedData(getRecordedDataType());
} else {
swipeLayout.setRefreshing(false);
GB.toast(this, getString(R.string.device_not_connected), Toast.LENGTH_SHORT, GB.ERROR);
}
}
public CharSequence getPageTitle(int position) {
switch (enabledTabsList.get(position)) {
case "activity":
return getString(R.string.activity_sleepchart_activity_and_sleep);
case "activitylist":
return getString(R.string.charts_activity_list);
case "sleep":
return getString(R.string.sleepchart_your_sleep);
case "heartrate":
return getString(R.string.menuitem_hr);
case "hrvstatus":
return getString(R.string.pref_header_hrv_status);
case "bodyenergy":
return getString(R.string.body_energy);
case "vo2max":
return getString(R.string.menuitem_vo2_max);
case "stress":
return getString(R.string.menuitem_stress);
case "pai":
return getString(getDevice().getDeviceCoordinator().getPaiName());
case "stepsweek":
return getString(R.string.steps);
case "speedzones":
return getString(R.string.stats_title);
case "livestats":
return getString(R.string.liveactivity_live_activity);
case "spo2":
return getString(R.string.pref_header_spo2);
case "temperature":
return getString(R.string.menuitem_temperature);
case "cycling":
return getString(R.string.title_cycling);
case "weight":
return getString(R.string.menuitem_weight);
case "calories":
return getString(R.string.calories);
case "respiratoryrate":
return getString(R.string.respiratoryrate);
case "load":
return getString(R.string.pref_header_training_load);
}
return String.format(Locale.getDefault(), "Unknown %d", position);
}
/**
* A {@link FragmentStatePagerAdapter} that returns a fragment corresponding to
* one of the sections/tabs/pages.
*/
private class SectionsPagerAdapter extends AbstractFragmentPagerAdapter {
SectionsPagerAdapter(FragmentManager fm) {
super(fm);
private class SectionsStateAdapter extends FragmentStateAdapter {
SectionsStateAdapter(FragmentActivity activity) {
super(activity);
}
@NonNull
@Override
public Fragment getItem(int position) {
public Fragment createFragment(int position) {
final DeviceCoordinator coordinator = getDevice().getDeviceCoordinator();
// getItem is called to instantiate the fragment for the given page.
switch (enabledTabsList.get(position)) {
@@ -205,59 +555,14 @@ public class ActivityChartsActivity extends AbstractChartsActivity {
return RespiratoryRateCollectionFragment.newInstance(enabledTabsList.size() == 1);
}
return new UnknownFragment();
return new ActivityChartsActivity.UnknownFragment();
}
@Override
public int getCount() {
public int getItemCount() {
return enabledTabsList.size();
}
@Override
public CharSequence getPageTitle(int position) {
switch (enabledTabsList.get(position)) {
case "activity":
return getString(R.string.activity_sleepchart_activity_and_sleep);
case "activitylist":
return getString(R.string.charts_activity_list);
case "sleep":
return getString(R.string.sleepchart_your_sleep);
case "heartrate":
return getString(R.string.menuitem_hr);
case "hrvstatus":
return getString(R.string.pref_header_hrv_status);
case "bodyenergy":
return getString(R.string.body_energy);
case "vo2max":
return getString(R.string.menuitem_vo2_max);
case "stress":
return getString(R.string.menuitem_stress);
case "pai":
return getString(getDevice().getDeviceCoordinator().getPaiName());
case "stepsweek":
return getString(R.string.steps);
case "speedzones":
return getString(R.string.stats_title);
case "livestats":
return getString(R.string.liveactivity_live_activity);
case "spo2":
return getString(R.string.pref_header_spo2);
case "temperature":
return getString(R.string.menuitem_temperature);
case "cycling":
return getString(R.string.title_cycling);
case "weight":
return getString(R.string.menuitem_weight);
case "calories":
return getString(R.string.calories);
case "respiratoryrate":
return getString(R.string.respiratoryrate);
case "load":
return getString(R.string.pref_header_training_load);
}
return String.format(Locale.getDefault(), "Unknown %d", position);
}
}
/**
@@ -18,11 +18,9 @@ package nodomain.freeyourgadget.gadgetbridge.activities.charts;
import android.os.Bundle;
import androidx.fragment.app.FragmentManager;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragment;
import nodomain.freeyourgadget.gadgetbridge.adapter.CaloriesFragmentAdapter;
import nodomain.freeyourgadget.gadgetbridge.adapter.NestedFragmentAdapter;
public class CaloriesCollectionFragment extends AbstractCollectionFragment {
public CaloriesCollectionFragment() {
@@ -38,8 +36,8 @@ public class CaloriesCollectionFragment extends AbstractCollectionFragment {
}
@Override
public NestedFragmentAdapter getNestedFragmentAdapter(AbstractGBFragment fragment, FragmentManager childFragmentManager) {
return new CaloriesFragmentAdapter(this, getChildFragmentManager());
public FragmentStateAdapter getFragmentAdapter() {
return new CaloriesFragmentAdapter(this);
}
}
@@ -18,10 +18,8 @@ package nodomain.freeyourgadget.gadgetbridge.activities.charts;
import android.os.Bundle;
import androidx.fragment.app.FragmentManager;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragment;
import nodomain.freeyourgadget.gadgetbridge.adapter.NestedFragmentAdapter;
import nodomain.freeyourgadget.gadgetbridge.adapter.HeartRateFragmentAdapter;
public class HeartRateCollectionFragment extends AbstractCollectionFragment {
@@ -38,7 +36,7 @@ public class HeartRateCollectionFragment extends AbstractCollectionFragment {
}
@Override
public NestedFragmentAdapter getNestedFragmentAdapter(AbstractGBFragment fragment, FragmentManager childFragmentManager) {
return new HeartRateFragmentAdapter(this, getChildFragmentManager());
public FragmentStateAdapter getFragmentAdapter() {
return new HeartRateFragmentAdapter(this);
}
}
@@ -369,12 +369,6 @@ public class LiveActivityFragment extends AbstractActivityChartFragment<ChartsDa
GBApplication.deviceService(getChartsHost().getDevice()).onEnableRealtimeHeartRateMeasurement(true);
}
@Override
protected void onMadeVisibleInActivity() {
super.onMadeVisibleInActivity();
enableRealtimeTracking(true);
}
private void enableRealtimeTracking(boolean enable) {
if (enable && pulseScheduler != null) {
// already running
@@ -403,15 +397,9 @@ public class LiveActivityFragment extends AbstractActivityChartFragment<ChartsDa
}
}
@Override
public void onMadeInvisibleInActivity() {
enableRealtimeTracking(false);
super.onMadeInvisibleInActivity();
}
@Override
public void onDestroyView() {
onMadeInvisibleInActivity();
LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(mReceiver);
super.onDestroyView();
}
@@ -1,53 +0,0 @@
/* Copyright (C) 2023-2024 José Rebelo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import androidx.viewpager.widget.ViewPager;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
public class NonSwipeableViewPager extends ViewPager {
private boolean allowSwipe = true;
public NonSwipeableViewPager(final Context context, final AttributeSet attrs) {
super(context, attrs);
}
public void setAllowSwipe(final boolean allowSwipe) {
this.allowSwipe = allowSwipe;
}
@Override
public boolean onInterceptTouchEvent(final MotionEvent ev) {
if (allowSwipe) {
return super.onInterceptTouchEvent(ev);
}
return false;
}
@Override
public boolean onTouchEvent(final MotionEvent ev) {
if (allowSwipe) {
return super.onTouchEvent(ev);
}
return false;
}
}
@@ -2,10 +2,8 @@ package nodomain.freeyourgadget.gadgetbridge.activities.charts;
import android.os.Bundle;
import androidx.fragment.app.FragmentManager;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragment;
import nodomain.freeyourgadget.gadgetbridge.adapter.NestedFragmentAdapter;
import nodomain.freeyourgadget.gadgetbridge.adapter.RespiratoryRateFragmentAdapter;
public class RespiratoryRateCollectionFragment extends AbstractCollectionFragment {
@@ -22,8 +20,8 @@ public class RespiratoryRateCollectionFragment extends AbstractCollectionFragmen
}
@Override
public NestedFragmentAdapter getNestedFragmentAdapter(AbstractGBFragment fragment, FragmentManager childFragmentManager) {
return new RespiratoryRateFragmentAdapter(this, getChildFragmentManager());
public FragmentStateAdapter getFragmentAdapter() {
return new RespiratoryRateFragmentAdapter(this);
}
}
@@ -18,10 +18,8 @@ package nodomain.freeyourgadget.gadgetbridge.activities.charts;
import android.os.Bundle;
import androidx.fragment.app.FragmentManager;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragment;
import nodomain.freeyourgadget.gadgetbridge.adapter.NestedFragmentAdapter;
import nodomain.freeyourgadget.gadgetbridge.adapter.SleepFragmentAdapter;
public class SleepCollectionFragment extends AbstractCollectionFragment {
@@ -38,7 +36,7 @@ public class SleepCollectionFragment extends AbstractCollectionFragment {
}
@Override
public NestedFragmentAdapter getNestedFragmentAdapter(AbstractGBFragment fragment, FragmentManager childFragmentManager) {
return new SleepFragmentAdapter(this, getChildFragmentManager());
public FragmentStateAdapter getFragmentAdapter() {
return new SleepFragmentAdapter(this);
}
}
@@ -18,10 +18,8 @@ package nodomain.freeyourgadget.gadgetbridge.activities.charts;
import android.os.Bundle;
import androidx.fragment.app.FragmentManager;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragment;
import nodomain.freeyourgadget.gadgetbridge.adapter.NestedFragmentAdapter;
import nodomain.freeyourgadget.gadgetbridge.adapter.StepsFragmentAdapter;
public class StepsCollectionFragment extends AbstractCollectionFragment {
@@ -38,8 +36,8 @@ public class StepsCollectionFragment extends AbstractCollectionFragment {
}
@Override
public NestedFragmentAdapter getNestedFragmentAdapter(AbstractGBFragment fragment, FragmentManager childFragmentManager) {
return new StepsFragmentAdapter(this, getChildFragmentManager());
public FragmentStateAdapter getFragmentAdapter() {
return new StepsFragmentAdapter(this);
}
}
@@ -18,10 +18,8 @@ package nodomain.freeyourgadget.gadgetbridge.activities.charts;
import android.os.Bundle;
import androidx.fragment.app.FragmentManager;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragment;
import nodomain.freeyourgadget.gadgetbridge.adapter.NestedFragmentAdapter;
import nodomain.freeyourgadget.gadgetbridge.adapter.StressFragmentAdapter;
public class StressCollectionFragment extends AbstractCollectionFragment {
@@ -38,7 +36,7 @@ public class StressCollectionFragment extends AbstractCollectionFragment {
}
@Override
public NestedFragmentAdapter getNestedFragmentAdapter(AbstractGBFragment fragment, FragmentManager childFragmentManager) {
return new StressFragmentAdapter(this, getChildFragmentManager());
public FragmentStateAdapter getFragmentAdapter() {
return new StressFragmentAdapter(this);
}
}
@@ -9,11 +9,9 @@ import nodomain.freeyourgadget.gadgetbridge.activities.charts.CaloriesDailyFragm
import nodomain.freeyourgadget.gadgetbridge.activities.charts.CaloriesPeriodFragment;
public class CaloriesFragmentAdapter extends NestedFragmentAdapter {
protected FragmentManager fragmentManager;
public CaloriesFragmentAdapter(AbstractGBFragment fragment, FragmentManager childFragmentManager) {
super(fragment, childFragmentManager);
fragmentManager = childFragmentManager;
public CaloriesFragmentAdapter(Fragment fragment) {
super(fragment);
}
@NonNull
@@ -2,14 +2,13 @@ package nodomain.freeyourgadget.gadgetbridge.adapter;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragment;
import nodomain.freeyourgadget.gadgetbridge.activities.charts.HeartRatePeriodFragment;
public class HeartRateFragmentAdapter extends NestedFragmentAdapter {
public HeartRateFragmentAdapter(AbstractGBFragment fragment, FragmentManager childFragmentManager) {
super(fragment, childFragmentManager);
public HeartRateFragmentAdapter(Fragment fragment) {
super(fragment);
}
@NonNull
@@ -16,19 +16,13 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.adapter;
import androidx.fragment.app.FragmentManager;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import java.util.List;
import java.util.stream.Collectors;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragment;
public abstract class NestedFragmentAdapter extends FragmentStateAdapter {
protected FragmentManager fragmentManager;
public NestedFragmentAdapter(AbstractGBFragment fragment, FragmentManager childFragmentManager) {
public NestedFragmentAdapter(@NonNull Fragment fragment) {
super(fragment);
fragmentManager = childFragmentManager;
}
@Override
@@ -36,17 +30,6 @@ public abstract class NestedFragmentAdapter extends FragmentStateAdapter {
return 3;
}
public void updateFragments(int position) {
List<AbstractGBFragment> fragments = fragmentManager.getFragments()
.stream()
.map(e -> (AbstractGBFragment) e)
.collect(Collectors.toList());
for (AbstractGBFragment fragment : fragments) {
if (position < 0 || fragment != fragmentManager.findFragmentByTag("f" + position)) {
fragment.onMadeInvisibleInActivity();
} else {
fragment.onMadeVisibleInActivityInternal();
}
}
}
@Override
abstract public Fragment createFragment(int position);
}
@@ -2,18 +2,15 @@ package nodomain.freeyourgadget.gadgetbridge.adapter;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragment;
import nodomain.freeyourgadget.gadgetbridge.activities.charts.RespiratoryRateDailyFragment;
import nodomain.freeyourgadget.gadgetbridge.activities.charts.RespiratoryRatePeriodFragment;
public class RespiratoryRateFragmentAdapter extends NestedFragmentAdapter {
protected FragmentManager fragmentManager;
public RespiratoryRateFragmentAdapter(AbstractGBFragment fragment, FragmentManager childFragmentManager) {
super(fragment, childFragmentManager);
fragmentManager = childFragmentManager;
public RespiratoryRateFragmentAdapter(Fragment fragment) {
super(fragment);
}
@NonNull
@@ -2,7 +2,6 @@ package nodomain.freeyourgadget.gadgetbridge.adapter;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragment;
@@ -10,8 +9,8 @@ import nodomain.freeyourgadget.gadgetbridge.activities.charts.SleepDailyFragment
import nodomain.freeyourgadget.gadgetbridge.activities.charts.SleepPeriodFragment;
public class SleepFragmentAdapter extends NestedFragmentAdapter {
public SleepFragmentAdapter(AbstractGBFragment fragment, FragmentManager childFragmentManager) {
super(fragment, childFragmentManager);
public SleepFragmentAdapter(Fragment fragment) {
super(fragment);
}
@NonNull
@@ -2,18 +2,15 @@ package nodomain.freeyourgadget.gadgetbridge.adapter;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragment;
import nodomain.freeyourgadget.gadgetbridge.activities.charts.StepsDailyFragment;
import nodomain.freeyourgadget.gadgetbridge.activities.charts.StepsPeriodFragment;
public class StepsFragmentAdapter extends NestedFragmentAdapter {
protected FragmentManager fragmentManager;
public StepsFragmentAdapter(AbstractGBFragment fragment, FragmentManager childFragmentManager) {
super(fragment, childFragmentManager);
fragmentManager = childFragmentManager;
public StepsFragmentAdapter(Fragment fragment) {
super(fragment);
}
@NonNull
@@ -8,8 +8,8 @@ import nodomain.freeyourgadget.gadgetbridge.activities.charts.StressDailyFragmen
import nodomain.freeyourgadget.gadgetbridge.activities.charts.StressPeriodFragment;
public class StressFragmentAdapter extends NestedFragmentAdapter {
public StressFragmentAdapter(AbstractGBFragment fragment, FragmentManager childFragmentManager) {
super(fragment, childFragmentManager);
public StressFragmentAdapter(Fragment fragment) {
super(fragment);
}
@Override
+9 -9
View File
@@ -12,20 +12,20 @@
android:layout_height="match_parent"
android:orientation="vertical">
<nodomain.freeyourgadget.gadgetbridge.activities.charts.NonSwipeableViewPager
<com.google.android.material.tabs.TabLayout
android:id="@+id/charts_pagerTabStrip"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top"
app:tabMode="scrollable" />
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/charts_pager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
tools:context="nodomain.freeyourgadget.gadgetbridge.activities.charts.ActivityChartsActivity">
<com.google.android.material.tabs.TabLayout
android:id="@+id/charts_pagerTabStrip"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top"
app:tabMode="scrollable" />
</nodomain.freeyourgadget.gadgetbridge.activities.charts.NonSwipeableViewPager>
</androidx.viewpager2.widget.ViewPager2>
<LinearLayout
android:id="@+id/charts_date_bar"
@@ -11,18 +11,20 @@
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.viewpager.widget.ViewPager
<com.google.android.material.tabs.TabLayout
android:id="@+id/charts_pagerTabStrip"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top" />
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/appmanager_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".activities.appmanager.AppManagerActivity">
<com.google.android.material.tabs.TabLayout
android:id="@+id/charts_pagerTabStrip"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top" />
</androidx.viewpager.widget.ViewPager>
</androidx.viewpager2.widget.ViewPager2>
</LinearLayout>