Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Paginate scrolling in test result screen #511

Closed
wants to merge 17 commits into from
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply from: 'jacoco.gradle'

android {
Expand Down Expand Up @@ -95,6 +96,16 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}


kotlinOptions {
jvmTarget = "1.8"
freeCompilerArgs += ["-Xopt-in=kotlin.RequiresOptIn"]
}

buildFeatures {
viewBinding = true
}
}

dependencies {
Expand All @@ -109,6 +120,8 @@ dependencies {
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0'
implementation 'com.google.guava:guava:30.1.1-android'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation "androidx.paging:paging-runtime:3.1.1"


// Third-party
annotationProcessor 'com.github.Raizlabs.DBFlow:dbflow-processor:4.2.4'
Expand Down Expand Up @@ -174,6 +187,11 @@ dependencies {
}
androidTestImplementation('com.schibsted.spain:barista:3.9.0')
androidTestAnnotationProcessor "com.google.dagger:dagger-compiler:2.36"
implementation "androidx.core:core-ktx:+"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
}
repositories {
mavenCentral()
}

static def versionCodeDate() {
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package org.openobservatory.ooniprobe.adapters.diff

import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil
import org.openobservatory.ooniprobe.adapters.ResultListAdapter.UiModel

class ResultComparator : DiffUtil.ItemCallback<UiModel>() {
/**
* This diff callback informs the PagedListAdapter how to compute list differences when new
* PagedLists arrive.
*
* When you add a Cheese with the 'Add' button, the PagedListAdapter uses diffCallback to
* detect there's only a single item difference from before, so it only needs to animate and
* rebind a single view.
*
* @see DiffUtil
*/
override fun areItemsTheSame(oldItem: UiModel, newItem: UiModel): Boolean {
return if (oldItem is UiModel.ResultModel && newItem is UiModel.ResultModel) {
oldItem.item.id == newItem.item.id
} else if (oldItem is UiModel.SeparatorModel && newItem is UiModel.SeparatorModel) {
oldItem.description == newItem.description
} else {
oldItem == newItem
}
}

/**
* Note that in kotlin, == checking on data classes compares all contents, but in Java,
* typically you'll implement Object#equals, and use it to compare object contents.
*/
override fun areContentsTheSame(oldItem: UiModel, newItem: UiModel): Boolean {
return oldItem.equals(newItem)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

import com.raizlabs.android.dbflow.sql.language.SQLOperator;
import com.raizlabs.android.dbflow.sql.language.SQLite;

import org.openobservatory.ooniprobe.domain.models.DatedResults;
import org.openobservatory.ooniprobe.model.database.Result;
import org.openobservatory.ooniprobe.model.database.Result_Table;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package org.openobservatory.ooniprobe.domain;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.paging.PagingSource;
import androidx.paging.PagingState;

import com.raizlabs.android.dbflow.sql.language.SQLOperator;
import com.raizlabs.android.dbflow.sql.language.SQLite;

import org.openobservatory.ooniprobe.model.database.Result;
import org.openobservatory.ooniprobe.model.database.Result_Table;

import java.util.List;

import kotlin.coroutines.Continuation;

public class QueryDataSource extends PagingSource<Integer, Result> {
private final String testGroupNameFilter;

public QueryDataSource(String testGroupNameFilter) {
this.testGroupNameFilter = testGroupNameFilter;
}

@Nullable
@Override
public Integer getRefreshKey(@NonNull PagingState<Integer, Result> pagingState) {
return 1;
}

@Nullable
@Override
public LoadResult.Page<Integer, Result> load(@NonNull LoadParams<Integer> loadParams, @NonNull Continuation<? super LoadResult<Integer, Result>> continuation) {
// Key may be null during a refresh, if no explicit key is passed into Pager
// construction. Use 0 as default, because our API is indexed started at index 0
int pageNumber = loadParams.getKey() != null ? loadParams.getKey() : 0;

SQLOperator[] conditions = (testGroupNameFilter != null && !testGroupNameFilter.isEmpty())
? new SQLOperator[]{Result_Table.test_group_name.is(testGroupNameFilter)}
: new SQLOperator[0];

List<Result> response = SQLite.select().from(Result.class)
.where(conditions)
.limit(20)
.offset(pageNumber * 20)
.orderBy(Result_Table.start_time, false)
.queryList();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If while you have fetched the first page of data, another result is written to the results table (because there is a test currently running), fetching the next page will be off by one.

You could instead use the result_id as a pagination handler and query WHERE result_id < $last_result_id to get the next page worth of data.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The query mechanism has been updated to use result_id based pagination


// Since 0 is the lowest page number, return null to signify no more pages should
// be loaded before it.
Integer prevKey = (pageNumber > 0) ? pageNumber - 1 : null;

// This API defines that it's out of data when a page returns empty. When out of
// data, we return `null` to signify no more pages should be loaded
Integer nextKey = (!response.isEmpty()) ? pageNumber + 1 : null;
return new LoadResult.Page<>(
response,
prevKey,
nextKey
);

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.core.app.ActivityCompat;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
Expand All @@ -31,43 +32,26 @@
import org.openobservatory.ooniprobe.activity.AbstractActivity;
import org.openobservatory.ooniprobe.activity.ResultDetailActivity;
import org.openobservatory.ooniprobe.activity.TextActivity;
import org.openobservatory.ooniprobe.adapters.ResultListAdapter;
import org.openobservatory.ooniprobe.adapters.diff.ResultComparator;
import org.openobservatory.ooniprobe.common.Application;
import org.openobservatory.ooniprobe.common.PreferenceManager;
import org.openobservatory.ooniprobe.common.ResubmitTask;
import org.openobservatory.ooniprobe.domain.GetResults;
import org.openobservatory.ooniprobe.domain.MeasurementsManager;
import org.openobservatory.ooniprobe.domain.models.DatedResults;
import org.openobservatory.ooniprobe.item.CircumventionItem;
import org.openobservatory.ooniprobe.item.DateItem;
import org.openobservatory.ooniprobe.item.ExperimentalItem;
import org.openobservatory.ooniprobe.item.FailedItem;
import org.openobservatory.ooniprobe.item.InstantMessagingItem;
import org.openobservatory.ooniprobe.item.MiddleboxesItem;
import org.openobservatory.ooniprobe.item.PerformanceItem;
import org.openobservatory.ooniprobe.item.WebsiteItem;
import org.openobservatory.ooniprobe.model.database.Network;
import org.openobservatory.ooniprobe.model.database.Result;
import org.openobservatory.ooniprobe.model.database.Result_Table;
import org.openobservatory.ooniprobe.test.suite.CircumventionSuite;
import org.openobservatory.ooniprobe.test.suite.ExperimentalSuite;
import org.openobservatory.ooniprobe.test.suite.InstantMessagingSuite;
import org.openobservatory.ooniprobe.test.suite.MiddleBoxesSuite;
import org.openobservatory.ooniprobe.test.suite.PerformanceSuite;
import org.openobservatory.ooniprobe.test.suite.WebsitesSuite;

import java.io.Serializable;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;

import javax.inject.Inject;

import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnItemSelected;
import localhost.toolkit.app.fragment.ConfirmDialogFragment;
import localhost.toolkit.widget.recyclerview.HeterogeneousRecyclerAdapter;
import localhost.toolkit.widget.recyclerview.HeterogeneousRecyclerItem;

public class ResultListFragment extends Fragment implements View.OnClickListener, View.OnLongClickListener, ConfirmDialogFragment.OnConfirmedListener {
@BindView(R.id.coordinatorLayout)
Expand All @@ -88,36 +72,36 @@ public class ResultListFragment extends Fragment implements View.OnClickListener
RecyclerView recycler;
@BindView(R.id.emptyState)
TextView emptyState;
private ArrayList<HeterogeneousRecyclerItem> items;
private HeterogeneousRecyclerAdapter<HeterogeneousRecyclerItem> adapter;
private boolean refresh;
private Snackbar snackbar;

@Inject
MeasurementsManager measurementsManager;

@Inject
GetResults getResults;

@Inject
PreferenceManager pm;

private ResultListAdapter mAdapter;
private boolean refresh;
private Snackbar snackbar;
private ResultListViewModel mViewModel;

@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_result_list, container, false);
ButterKnife.bind(this, v);
((Application) getActivity().getApplication()).getFragmentComponent().inject(this);
((AppCompatActivity) getActivity()).setSupportActionBar(toolbar);
// Create ViewModel
mViewModel = new ViewModelProvider(this).get(ResultListViewModel.class);

setHasOptionsMenu(true);
getActivity().setTitle(R.string.TestResults_Overview_Title);
reloadHeader();
LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity());
recycler.setLayoutManager(layoutManager);
recycler.addItemDecoration(new DividerItemDecoration(getActivity(), layoutManager.getOrientation()));
items = new ArrayList<>();
adapter = new HeterogeneousRecyclerAdapter<>(getActivity(), items);
recycler.setAdapter(adapter);
mAdapter = new ResultListAdapter(new ResultComparator(), this, this);
recycler.setAdapter(mAdapter);
snackbar = Snackbar.make(coordinatorLayout, R.string.Snackbar_ResultsSomeNotUploaded_Text, Snackbar.LENGTH_INDEFINITE)
.setAction(R.string.Snackbar_ResultsSomeNotUploaded_UploadAll, v1 ->
new ConfirmDialogFragment.Builder()
Expand Down Expand Up @@ -181,48 +165,11 @@ void queryList() {
snackbar.dismiss();
}

items.clear();

String filter = getResources().getStringArray(R.array.filterTestValues)[filterTests.getSelectedItemPosition()];
List<DatedResults> list = getResults.getGroupedByMonth(filter);

if (list.isEmpty()) {
emptyState.setVisibility(View.VISIBLE);
recycler.setVisibility(View.GONE);
} else {
emptyState.setVisibility(View.GONE);
recycler.setVisibility(View.VISIBLE);
for (DatedResults group : list) {
items.add(new DateItem(group.getGroupedDate()));
for (Result result : group.getResultsList()) {
if (result.countTotalMeasurements() == 0)
items.add(new FailedItem(result, this, this));
else {
switch (result.test_group_name) {
case WebsitesSuite.NAME:
items.add(new WebsiteItem(result, this, this));
break;
case InstantMessagingSuite.NAME:
items.add(new InstantMessagingItem(result, this, this));
break;
case MiddleBoxesSuite.NAME:
items.add(new MiddleboxesItem(result, this, this));
break;
case PerformanceSuite.NAME:
items.add(new PerformanceItem(result, this, this));
break;
case CircumventionSuite.NAME:
items.add(new CircumventionItem(result, this, this));
break;
case ExperimentalSuite.NAME:
items.add(new ExperimentalItem(result, this, this));
break;
}
}
}
}
adapter.notifyTypesChanged();
}
mViewModel.init(filter);
mViewModel.pagingData.observe(getViewLifecycleOwner(), resultPagingData -> {
mAdapter.submitData(getLifecycle(), resultPagingData);
});
}

@Override
Expand All @@ -248,11 +195,9 @@ public void onConfirmation(Serializable serializable, int i) {
if (serializable.equals(R.string.Modal_ResultsNotUploaded_Title)) {
if (i == DialogInterface.BUTTON_POSITIVE) {
new ResubmitAsyncTask(this, pm.getProxyURL()).execute(null, null);
}
else if (i == DialogInterface.BUTTON_NEUTRAL) {
} else if (i == DialogInterface.BUTTON_NEUTRAL) {
startActivity(TextActivity.newIntent(getActivity(), TextActivity.TYPE_UPLOAD_LOG, (String) serializable));
}
else
} else
snackbar.show();
} else if (i == DialogInterface.BUTTON_POSITIVE) {
if (serializable instanceof Result)
Expand All @@ -271,7 +216,7 @@ else if (serializable.equals(R.id.delete)) {
}

private static class ResubmitAsyncTask extends ResubmitTask<AbstractActivity> {
private WeakReference<ResultListFragment> wf;
private final WeakReference<ResultListFragment> wf;

ResubmitAsyncTask(ResultListFragment f, String proxy) {
super((AbstractActivity) f.getActivity(), proxy);
Expand Down
Loading