Use Android Paging library for list loading

Recent calls list now supports "endless scrolling"
This commit is contained in:
xynngh 2020-09-29 19:23:51 +04:00
parent 91e197abf3
commit 4bffec08a1
24 changed files with 532 additions and 218 deletions

View File

@ -50,6 +50,7 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'androidx.recyclerview:recyclerview-selection:1.0.0'
implementation 'androidx.paging:paging-runtime:2.1.2'
implementation 'com.google.android.material:material:1.2.1'
implementation 'androidx.preference:preference:1.1.1'
implementation 'androidx.work:work-runtime:2.4.0'

View File

@ -1,12 +1,11 @@
package dummydomain.yetanothercallblocker;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.os.Parcelable;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
@ -17,6 +16,9 @@ import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.view.ActionMode;
import androidx.lifecycle.LiveData;
import androidx.paging.LivePagedListBuilder;
import androidx.paging.PagedList;
import androidx.recyclerview.selection.SelectionTracker;
import androidx.recyclerview.selection.StorageStrategy;
import androidx.recyclerview.widget.RecyclerView;
@ -30,7 +32,7 @@ import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.util.List;
import java.util.Objects;
import dummydomain.yetanothercallblocker.data.BlacklistImporterExporter;
import dummydomain.yetanothercallblocker.data.BlacklistService;
@ -44,19 +46,26 @@ public class BlacklistActivity extends AppCompatActivity {
private static final int REQUEST_CODE_IMPORT = 1;
private static final String STATE_LIST_LAST_KEY = "list_last_key";
private static final String STATE_LIST_LAYOUT_MANAGER = "list_layout_manager";
private static final Logger LOG = LoggerFactory.getLogger(BlacklistActivity.class);
private final Settings settings = App.getSettings();
private final BlacklistDao blacklistDao = YacbHolder.getBlacklistDao();
private final BlacklistService blacklistService = YacbHolder.getBlacklistService();
private RecyclerView recyclerView;
private BlacklistItemRecyclerViewAdapter blacklistAdapter;
private BlacklistDataSource.Factory blacklistDataSourceFactory;
private SelectionTracker<Long> selectionTracker;
private ActionMode.Callback actionModeCallback;
private ActionMode actionMode;
private AsyncTask<Void, Void, List<BlacklistItem>> loadBlacklistTask;
private Parcelable listLayoutManagerSavedState;
private boolean activityFirstStart = true;
public static Intent getIntent(Context context) {
return new Intent(context, BlacklistActivity.class);
@ -68,7 +77,7 @@ public class BlacklistActivity extends AppCompatActivity {
setContentView(R.layout.activity_blacklist);
blacklistAdapter = new BlacklistItemRecyclerViewAdapter(this::onItemClicked);
RecyclerView recyclerView = findViewById(R.id.blacklistItemsList);
recyclerView = findViewById(R.id.blacklistItemsList);
recyclerView.setAdapter(blacklistAdapter);
recyclerView.addItemDecoration(new CustomVerticalDivider(this));
@ -103,7 +112,6 @@ public class BlacklistActivity extends AppCompatActivity {
if (selectionTracker.hasSelection()) {
blacklistService.delete(selectionTracker.getSelection());
selectionTracker.clearSelection();
loadItems();
}
})
.setNegativeButton(R.string.no, null)
@ -142,6 +150,39 @@ public class BlacklistActivity extends AppCompatActivity {
}
});
Integer initialKey = null;
if (savedInstanceState != null) {
if (savedInstanceState.containsKey(STATE_LIST_LAST_KEY)) {
initialKey = savedInstanceState.getInt(STATE_LIST_LAST_KEY);
}
listLayoutManagerSavedState = savedInstanceState
.getParcelable(STATE_LIST_LAYOUT_MANAGER);
}
blacklistDataSourceFactory = blacklistDao.dataSourceFactory();
PagedList.Config config = new PagedList.Config.Builder()
.setPageSize(30)
.setInitialLoadSizeHint(60)
.build();
LiveData<PagedList<BlacklistItem>> itemLiveData
= new LivePagedListBuilder<>(blacklistDataSourceFactory, config)
.setInitialLoadKey(initialKey)
.build();
itemLiveData.observe(this, data -> {
blacklistAdapter.submitList(data);
if (listLayoutManagerSavedState != null) {
Objects.requireNonNull(recyclerView.getLayoutManager())
.onRestoreInstanceState(listLayoutManagerSavedState);
listLayoutManagerSavedState = null;
}
});
selectionTracker.onRestoreInstanceState(savedInstanceState);
}
@ -165,7 +206,11 @@ public class BlacklistActivity extends AppCompatActivity {
EventUtils.register(this);
loadItems();
if (activityFirstStart) {
activityFirstStart = false;
} else {
reloadItems();
}
}
@Override
@ -173,6 +218,17 @@ public class BlacklistActivity extends AppCompatActivity {
super.onSaveInstanceState(outState);
selectionTracker.onSaveInstanceState(outState);
PagedList<BlacklistItem> currentList = blacklistAdapter.getCurrentList();
if (currentList != null) {
Integer lastKey = (Integer) currentList.getLastKey();
if (lastKey != null) {
outState.putInt(STATE_LIST_LAST_KEY, lastKey);
}
}
outState.putParcelable(STATE_LIST_LAYOUT_MANAGER,
Objects.requireNonNull(recyclerView.getLayoutManager()).onSaveInstanceState());
}
@Override
@ -182,13 +238,6 @@ public class BlacklistActivity extends AppCompatActivity {
super.onStop();
}
@Override
protected void onDestroy() {
cancelLoadingBlacklistTask();
super.onDestroy();
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
@ -223,32 +272,11 @@ public class BlacklistActivity extends AppCompatActivity {
@Subscribe(threadMode = ThreadMode.MAIN_ORDERED)
public void onBlacklistChanged(BlacklistChangedEvent blacklistChangedEvent) {
loadItems();
reloadItems();
}
private void loadItems() {
cancelLoadingBlacklistTask();
@SuppressLint("StaticFieldLeak")
AsyncTask<Void, Void, List<BlacklistItem>> loadBlacklistTask = this.loadBlacklistTask
= new AsyncTask<Void, Void, List<BlacklistItem>>() {
@Override
protected List<BlacklistItem> doInBackground(Void... voids) {
return blacklistDao.detach(blacklistDao.loadAll());
}
@Override
protected void onPostExecute(List<BlacklistItem> items) {
blacklistAdapter.submitList(items);
}
};
loadBlacklistTask.execute();
}
private void cancelLoadingBlacklistTask() {
if (loadBlacklistTask != null) {
loadBlacklistTask.cancel(true);
loadBlacklistTask = null;
}
private void reloadItems() {
blacklistDataSourceFactory.invalidate();
}
public void onBlockBlacklistedChanged(MenuItem item) {

View File

@ -0,0 +1,90 @@
package dummydomain.yetanothercallblocker;
import androidx.annotation.NonNull;
import androidx.paging.DataSource;
import androidx.paging.PositionalDataSource;
import org.greenrobot.greendao.query.QueryBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import dummydomain.yetanothercallblocker.data.db.BlacklistDao;
import dummydomain.yetanothercallblocker.data.db.BlacklistItem;
public class BlacklistDataSource extends PositionalDataSource<BlacklistItem> {
private static final Logger LOG = LoggerFactory.getLogger(BlacklistDataSource.class);
public static class Factory extends DataSource.Factory<Integer, BlacklistItem> {
private final BlacklistDao blacklistDao;
private volatile BlacklistDataSource ds;
public Factory(BlacklistDao blacklistDao) {
this.blacklistDao = blacklistDao;
}
public void invalidate() {
LOG.debug("invalidate()");
BlacklistDataSource ds = this.ds;
if (ds != null) ds.invalidate();
}
@NonNull
@Override
public DataSource<Integer, BlacklistItem> create() {
return ds = new BlacklistDataSource(blacklistDao);
}
}
private final BlacklistDao blacklistDao;
private QueryBuilder<BlacklistItem> queryBuilder;
public BlacklistDataSource(BlacklistDao blacklistDao) {
this.blacklistDao = blacklistDao;
}
@Override
public void loadInitial(@NonNull LoadInitialParams params,
@NonNull LoadInitialCallback<BlacklistItem> callback) {
LOG.debug("loadInitial({}, {})", params.requestedStartPosition, params.requestedLoadSize);
List<BlacklistItem> items = getQueryBuilder()
.offset(params.requestedStartPosition)
.limit(params.requestedLoadSize)
.list();
items = blacklistDao.detach(items); // for DiffUtil to work
if (params.placeholdersEnabled) {
callback.onResult(items, params.requestedStartPosition, (int) blacklistDao.countAll());
} else {
callback.onResult(items, params.requestedStartPosition);
}
}
@Override
public void loadRange(@NonNull LoadRangeParams params,
@NonNull LoadRangeCallback<BlacklistItem> callback) {
LOG.debug("loadRange({}, {})", params.startPosition, params.loadSize);
List<BlacklistItem> items = getQueryBuilder()
.offset(params.startPosition)
.limit(params.loadSize)
.list();
callback.onResult(blacklistDao.detach(items));
}
private QueryBuilder<BlacklistItem> getQueryBuilder() {
if (queryBuilder == null) {
queryBuilder = blacklistDao.getDefaultQueryBuilder();
}
return queryBuilder;
}
}

View File

@ -42,14 +42,15 @@ public class BlacklistItemRecyclerViewAdapter extends GenericRecyclerViewAdapter
@Nullable
@Override
public Long getKey(int position) {
return getItem(position).getId();
BlacklistItem item = getItem(position);
return item != null ? item.getId() : null;
}
@Override
public int getPosition(@NonNull Long key) {
for (int i = 0; i < getItemCount(); i++) {
BlacklistItem item = getItem(i);
if (key.equals(item.getId())) return i;
if (item != null && key.equals(item.getId())) return i;
}
return RecyclerView.NO_POSITION;
}
@ -100,10 +101,21 @@ public class BlacklistItemRecyclerViewAdapter extends GenericRecyclerViewAdapter
@Override
void bind(BlacklistItem item) {
if (item == null) { // placeholder
name.setVisibility(View.INVISIBLE);
pattern.setVisibility(View.INVISIBLE);
stats.setVisibility(View.GONE);
errorIcon.setVisibility(View.GONE);
itemView.setActivated(false);
return;
}
name.setText(item.getName());
name.setVisibility(TextUtils.isEmpty(item.getName()) ? View.GONE : View.VISIBLE);
pattern.setText(item.getHumanReadablePattern());
pattern.setVisibility(View.VISIBLE);
if (item.getNumberOfCalls() > 0) {
stats.setVisibility(View.VISIBLE);
@ -124,9 +136,8 @@ public class BlacklistItemRecyclerViewAdapter extends GenericRecyclerViewAdapter
errorIcon.setVisibility(item.getInvalid() ? View.VISIBLE : View.GONE);
if (selectionTracker != null) {
itemView.setActivated(selectionTracker.isSelected(item.getId()));
}
itemView.setActivated(selectionTracker != null
&& selectionTracker.isSelected(item.getId()));
}
ItemDetailsLookup.ItemDetails<Long> getItemDetails() {
@ -141,14 +152,16 @@ public class BlacklistItemRecyclerViewAdapter extends GenericRecyclerViewAdapter
@Override
public Long getSelectionKey() {
int position = getAdapterPosition();
return position != RecyclerView.NO_POSITION
? getItem(position).getId() : null;
BlacklistItem item = position != RecyclerView.NO_POSITION
? getItem(position) : null;
return item != null ? item.getId() : null;
}
};
}
return itemDetails;
}
@SuppressWarnings("NullableProblems")
@Override
public String toString() {
return super.toString() + " '" + pattern.getText() + "'";

View File

@ -61,6 +61,23 @@ public class CallLogItemRecyclerViewAdapter extends GenericRecyclerViewAdapter
@Override
void bind(CallLogItemGroup group) {
if (group == null) { // placeholder
label.setVisibility(View.INVISIBLE);
numberInfoIcon.setVisibility(View.INVISIBLE);
duration.setVisibility(View.GONE);
description.setVisibility(View.GONE);
time.setVisibility(View.INVISIBLE);
for (AppCompatImageView icon : callTypeIcons) {
bindTypeIcon(null, icon);
}
return;
} else {
label.setVisibility(View.VISIBLE);
numberInfoIcon.setVisibility(View.VISIBLE);
time.setVisibility(View.VISIBLE);
}
CallLogItem item = group.getItems().get(0);
Context context = itemView.getContext();
@ -163,6 +180,7 @@ public class CallLogItemRecyclerViewAdapter extends GenericRecyclerViewAdapter
return context.getString(R.string.duration_s, seconds);
}
@SuppressWarnings("NullableProblems")
@Override
public String toString() {
return super.toString() + " '" + label.getText() + "'";

View File

@ -4,12 +4,12 @@ import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.paging.PagedListAdapter;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
public abstract class GenericRecyclerViewAdapter<T, V extends GenericRecyclerViewAdapter<T, V>.GenericViewHolder>
extends ListAdapter<T, V> {
extends PagedListAdapter<T, V> {
public interface ListInteractionListener<T> {
void onListItemClicked(T item);

View File

@ -13,37 +13,47 @@ import android.view.View;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.arch.core.util.Function;
import androidx.lifecycle.LiveData;
import androidx.paging.LivePagedListBuilder;
import androidx.paging.PagedList;
import androidx.recyclerview.widget.RecyclerView;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import dummydomain.yetanothercallblocker.data.CallLogHelper;
import dummydomain.yetanothercallblocker.data.CallLogDataSource;
import dummydomain.yetanothercallblocker.data.CallLogItem;
import dummydomain.yetanothercallblocker.data.CallLogItemGroup;
import dummydomain.yetanothercallblocker.data.NumberInfo;
import dummydomain.yetanothercallblocker.data.YacbHolder;
import dummydomain.yetanothercallblocker.event.CallEndedEvent;
import dummydomain.yetanothercallblocker.event.MainDbDownloadFinishedEvent;
import dummydomain.yetanothercallblocker.event.MainDbDownloadingEvent;
import dummydomain.yetanothercallblocker.event.SecondaryDbUpdateFinished;
import dummydomain.yetanothercallblocker.work.TaskService;
import dummydomain.yetanothercallblocker.work.UpdateScheduler;
public class MainActivity extends AppCompatActivity {
private static final String STATE_CALL_LOG_DATA_LAST_KEY = "call_log_data_last_key";
private static final String STATE_CALL_LOG_LAYOUT_MANAGER = "call_log_layout_manager";
private final Settings settings = App.getSettings();
private final UpdateScheduler updateScheduler = UpdateScheduler.get(App.getInstance());
private CallLogItemRecyclerViewAdapter callLogAdapter;
private RecyclerView recyclerView;
private CallLogDataSource.Factory callLogDsFactory;
private Parcelable callLogLayoutManagerState;
private AsyncTask<Void, Void, Boolean> checkMainDbTask;
private AsyncTask<Void, Void, List<CallLogItemGroup>> loadCallLogTask;
private boolean activityFirstStart = true;
@Override
protected void onCreate(Bundle savedInstanceState) {
@ -54,6 +64,39 @@ public class MainActivity extends AppCompatActivity {
recyclerView = findViewById(R.id.callLogList);
recyclerView.setAdapter(callLogAdapter);
recyclerView.addItemDecoration(new CustomVerticalDivider(this));
callLogDsFactory = new CallLogDataSource.Factory(getCallLogGroupConverter());
PagedList.Config config = new PagedList.Config.Builder()
.setPageSize(30)
.setInitialLoadSizeHint(30)
.setPrefetchDistance(15)
.build();
CallLogDataSource.GroupId initialKey = null;
if (savedInstanceState != null) {
initialKey = CallLogDataSource.GroupId.fromParcelable(
savedInstanceState.getParcelable(STATE_CALL_LOG_DATA_LAST_KEY));
callLogLayoutManagerState = savedInstanceState
.getParcelable(STATE_CALL_LOG_LAYOUT_MANAGER);
}
LiveData<PagedList<CallLogItemGroup>> callLogData
= new LivePagedListBuilder<>(callLogDsFactory, config)
.setInitialLoadKey(initialKey)
.build();
callLogData.observe(this, data -> {
callLogAdapter.submitList(data);
if (callLogLayoutManagerState != null) {
Objects.requireNonNull(recyclerView.getLayoutManager())
.onRestoreInstanceState(callLogLayoutManagerState);
callLogLayoutManagerState = null;
}
});
}
@Override
@ -88,7 +131,8 @@ public class MainActivity extends AppCompatActivity {
settings.getIncomingCallNotifications(), settings.getCallBlockingEnabled(),
settings.getUseContacts());
loadCallLog();
updateCallLogVisibility();
reloadCallLog();
}
@Override
@ -101,7 +145,13 @@ public class MainActivity extends AppCompatActivity {
checkPermissions();
loadCallLog();
updateCallLogVisibility();
if (activityFirstStart) {
activityFirstStart = false;
} else {
callLogDsFactory.setGroupConverter(getCallLogGroupConverter());
reloadCallLog();
}
}
@Override
@ -114,19 +164,40 @@ public class MainActivity extends AppCompatActivity {
@Override
protected void onDestroy() {
cancelCheckMainDbTask();
cancelLoadingCallLogTask();
super.onDestroy();
}
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
PagedList<CallLogItemGroup> currentList = callLogAdapter.getCurrentList();
if (currentList != null) {
Object lastKey = currentList.getLastKey();
if (lastKey != null) {
outState.putParcelable(STATE_CALL_LOG_DATA_LAST_KEY,
((CallLogDataSource.GroupId) lastKey).saveInstanceState());
}
}
outState.putParcelable(STATE_CALL_LOG_LAYOUT_MANAGER,
Objects.requireNonNull(recyclerView.getLayoutManager()).onSaveInstanceState());
}
@Subscribe(threadMode = ThreadMode.MAIN_ORDERED)
public void onCallEvent(CallEndedEvent event) {
new Handler(getMainLooper()).postDelayed(this::loadCallLog, 1000);
new Handler(getMainLooper()).postDelayed(this::reloadCallLog, 1000);
}
@Subscribe(threadMode = ThreadMode.MAIN_ORDERED)
public void onMainDbDownloadFinished(MainDbDownloadFinishedEvent event) {
loadCallLog();
reloadCallLog();
}
@Subscribe(threadMode = ThreadMode.MAIN_ORDERED)
public void onSecondaryDbUpdateFinished(SecondaryDbUpdateFinished event) {
reloadCallLog();
}
private void checkPermissions() {
@ -198,7 +269,7 @@ public class MainActivity extends AppCompatActivity {
public void onUseContactsChanged(MenuItem item) {
settings.setUseContacts(!item.isChecked());
checkPermissions();
loadCallLog();
reloadCallLog();
}
public void onOpenBlacklist(MenuItem item) {
@ -213,64 +284,12 @@ public class MainActivity extends AppCompatActivity {
InfoDialogHelper.showDialog(this, item.getItems().get(0).numberInfo, null);
}
private void loadCallLog() {
if (!PermissionHelper.hasCallLogPermission(this)) {
setCallLogVisibility(false);
return;
}
cancelLoadingCallLogTask();
@SuppressLint("StaticFieldLeak")
AsyncTask<Void, Void, List<CallLogItemGroup>> loadCallLogTask = this.loadCallLogTask
= new AsyncTask<Void, Void, List<CallLogItemGroup>>() {
@Override
protected List<CallLogItemGroup> doInBackground(Void... voids) {
List<CallLogItem> items = CallLogHelper.getRecentCalls(
MainActivity.this, settings.getNumberOfRecentCalls());
Map<String, NumberInfo> cache = new HashMap<>();
String countryCode = settings.getCachedAutoDetectedCountryCode();
for (CallLogItem item : items) {
NumberInfo numberInfo = cache.get(item.number);
if (numberInfo == null) {
numberInfo = YacbHolder.getNumberInfo(item.number, countryCode);
cache.put(item.number, numberInfo);
}
item.numberInfo = numberInfo;
}
switch (settings.getRecentCallsGrouping()) {
case Settings.PREF_RECENT_CALLS_GROUPING_NONE:
return CallLogItemGroup.noGrouping(items);
case Settings.PREF_RECENT_CALLS_GROUPING_DAY:
return CallLogItemGroup.groupInDay(items);
default:
return CallLogItemGroup.groupConsecutive(items);
}
}
@Override
protected void onPostExecute(List<CallLogItemGroup> items) {
// workaround for auto-scrolling to first item
// https://stackoverflow.com/a/44053550
@SuppressWarnings("ConstantConditions")
Parcelable recyclerViewState = recyclerView.getLayoutManager().onSaveInstanceState();
callLogAdapter.submitList(items);
recyclerView.getLayoutManager().onRestoreInstanceState(recyclerViewState);
setCallLogVisibility(true);
}
};
loadCallLogTask.execute();
private void reloadCallLog() {
callLogDsFactory.invalidate();
}
private void cancelLoadingCallLogTask() {
if (loadCallLogTask != null) {
loadCallLogTask.cancel(true);
loadCallLogTask = null;
}
private void updateCallLogVisibility() {
setCallLogVisibility(PermissionHelper.hasCallLogPermission(this));
}
private void setCallLogVisibility(boolean visible) {
@ -280,4 +299,20 @@ public class MainActivity extends AppCompatActivity {
findViewById(R.id.callLogList).setVisibility(visible ? View.VISIBLE : View.GONE);
}
private Function<List<CallLogItem>, List<CallLogItemGroup>> getCallLogGroupConverter() {
Function<List<CallLogItem>, List<CallLogItemGroup>> converter;
switch (settings.getRecentCallsGrouping()) {
case Settings.PREF_RECENT_CALLS_GROUPING_NONE:
converter = CallLogItemGroup::noGrouping;
break;
case Settings.PREF_RECENT_CALLS_GROUPING_DAY:
converter = CallLogItemGroup::groupInDay;
break;
default:
converter = CallLogItemGroup::groupConsecutive;
break;
}
return converter;
}
}

View File

@ -24,7 +24,6 @@ public class Settings extends GenericSettings {
public static final String PREF_USE_CONTACTS = "useContacts";
public static final String PREF_UI_MODE = "uiMode";
public static final String PREF_RECENT_CALLS_GROUPING = "recentCallsGrouping";
public static final String PREF_NUMBER_OF_RECENT_CALLS = "numberOfRecentCalls";
public static final String PREF_USE_MONITORING_SERVICE = "useMonitoringService";
public static final String PREF_NOTIFICATIONS_KNOWN = "showNotificationsForKnownCallers";
public static final String PREF_NOTIFICATIONS_UNKNOWN = "showNotificationsForUnknownCallers";
@ -169,14 +168,6 @@ public class Settings extends GenericSettings {
setString(PREF_RECENT_CALLS_GROUPING, value);
}
public int getNumberOfRecentCalls() {
return getInt(PREF_NUMBER_OF_RECENT_CALLS, 30);
}
public void setNumberOfRecentCalls(int number) {
setInt(PREF_NUMBER_OF_RECENT_CALLS, number);
}
public boolean getUseMonitoringService() {
return getBoolean(PREF_USE_MONITORING_SERVICE);
}

View File

@ -0,0 +1,174 @@
package dummydomain.yetanothercallblocker.data;
import android.content.Context;
import android.os.Bundle;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.arch.core.util.Function;
import androidx.paging.DataSource;
import androidx.paging.ItemKeyedDataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import dummydomain.yetanothercallblocker.App;
import static dummydomain.yetanothercallblocker.data.CallLogHelper.loadCalls;
public class CallLogDataSource extends ItemKeyedDataSource<CallLogDataSource.GroupId, CallLogItemGroup> {
private static final Logger LOG = LoggerFactory.getLogger(CallLogDataSource.class);
public static class Factory extends DataSource.Factory<GroupId, CallLogItemGroup> {
private Function<List<CallLogItem>, List<CallLogItemGroup>> groupConverter;
private volatile CallLogDataSource ds;
public Factory(Function<List<CallLogItem>, List<CallLogItemGroup>> groupConverter) {
this.groupConverter = groupConverter;
}
public void setGroupConverter(Function<List<CallLogItem>, List<CallLogItemGroup>> converter) {
this.groupConverter = converter;
}
public void invalidate() {
LOG.debug("invalidate()");
CallLogDataSource ds = this.ds;
if (ds != null) ds.invalidate();
}
@NonNull
@Override
public DataSource<GroupId, CallLogItemGroup> create() {
return ds = new CallLogDataSource(groupConverter);
}
}
public static class GroupId {
private static final String KEY_FIRST = "CallLogDataSource.ComplexId.first";
private static final String KEY_LAST = "CallLogDataSource.ComplexId.last";
final long firstId, lastId;
GroupId(long firstId, long lastId) {
this.firstId = firstId;
this.lastId = lastId;
}
public static GroupId fromParcelable(@Nullable Parcelable parcelable) {
if (parcelable instanceof Bundle) {
Bundle bundle = (Bundle) parcelable;
if (bundle.containsKey(KEY_FIRST) && bundle.containsKey(KEY_LAST)) {
return new GroupId(bundle.getLong(KEY_FIRST), bundle.getLong(KEY_LAST));
}
}
return null;
}
public Parcelable saveInstanceState() {
Bundle bundle = new Bundle();
bundle.putLong(KEY_FIRST, firstId);
bundle.putLong(KEY_LAST, lastId);
return bundle;
}
@SuppressWarnings("NullableProblems")
@Override
public String toString() {
return "ComplexId{" +
"firstId=" + firstId +
", lastId=" + lastId +
'}';
}
}
private final Function<List<CallLogItem>, List<CallLogItemGroup>> groupConverter;
public CallLogDataSource(Function<List<CallLogItem>, List<CallLogItemGroup>> groupConverter) {
this.groupConverter = groupConverter;
}
@NonNull
@Override
public GroupId getKey(@NonNull CallLogItemGroup group) {
List<CallLogItem> items = group.getItems();
return new GroupId(items.get(0).id, items.get(items.size() - 1).id);
}
@Override
public void loadInitial(@NonNull LoadInitialParams<GroupId> params,
@NonNull LoadInitialCallback<CallLogItemGroup> callback) {
LOG.debug("loadInitial({}, {})", params.requestedInitialKey, params.requestedLoadSize);
int size = params.requestedLoadSize * 3 / 2; // compensate for grouping
List<CallLogItem> items;
if (params.requestedInitialKey != null) {
// load something or the list will be empty
items = new ArrayList<>(size);
items.addAll(loadCalls(getContext(), params.requestedInitialKey.firstId, true, size / 2));
items.addAll(loadCalls(getContext(), params.requestedInitialKey.firstId + 1, false, size / 2));
} else {
items = loadCalls(getContext(), null, false, size);
}
callback.onResult(groupConverter.apply(loadInfo(items)));
}
@Override
public void loadBefore(@NonNull LoadParams<GroupId> params,
@NonNull LoadCallback<CallLogItemGroup> callback) {
LOG.debug("loadBefore({}, {})", params.key, params.requestedLoadSize);
int size = params.requestedLoadSize * 3 / 2; // compensate for grouping
List<CallLogItem> items = loadCalls(getContext(), params.key.firstId, true, size);
callback.onResult(groupConverter.apply(loadInfo(items)));
}
@Override
public void loadAfter(@NonNull LoadParams<GroupId> params,
@NonNull LoadCallback<CallLogItemGroup> callback) {
LOG.debug("loadAfter({}, {})", params.key, params.requestedLoadSize);
int size = params.requestedLoadSize * 3 / 2; // compensate for grouping
List<CallLogItem> items = loadCalls(getContext(), params.key.lastId, false, size);
callback.onResult(groupConverter.apply(loadInfo(items)));
}
private List<CallLogItem> loadInfo(List<CallLogItem> items) {
Map<String, NumberInfo> cache = new HashMap<>();
String countryCode = App.getSettings().getCachedAutoDetectedCountryCode();
for (CallLogItem item : items) {
NumberInfo numberInfo = cache.get(item.number);
if (numberInfo == null) {
numberInfo = YacbHolder.getNumberInfo(item.number, countryCode);
cache.put(item.number, numberInfo);
}
item.numberInfo = numberInfo;
}
return items;
}
private Context getContext() {
return App.getInstance();
}
}

View File

@ -5,38 +5,68 @@ import android.database.Cursor;
import android.provider.CallLog;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class CallLogHelper {
private static final String[] QUERY_PROJECTION = new String[]{
CallLog.Calls.TYPE, CallLog.Calls.NUMBER, CallLog.Calls.DATE, CallLog.Calls.DURATION
CallLog.Calls._ID, CallLog.Calls.TYPE, CallLog.Calls.NUMBER,
CallLog.Calls.DATE, CallLog.Calls.DURATION
};
public static List<CallLogItem> getRecentCalls(Context context, int num) {
List<CallLogItem> logItems = new ArrayList<>(num);
public static List<CallLogItem> loadCalls(Context context, Long anchorId, boolean before,
int limit) {
boolean reverseOrder = false;
String selection;
String[] selectionArgs;
if (anchorId != null) {
if (before) {
selection = CallLog.Calls._ID + " > ?";
reverseOrder = true;
} else {
selection = CallLog.Calls._ID + " < ?";
}
selectionArgs = new String[]{String.valueOf(anchorId)};
} else {
selection = null;
selectionArgs = null;
}
String sortOrder = CallLog.Calls.DATE + " " + (reverseOrder ? "ASC" : "DESC");
sortOrder += " limit " + limit;
List<CallLogItem> items = new ArrayList<>(limit);
try (Cursor cursor = context.getContentResolver().query(CallLog.Calls.CONTENT_URI,
QUERY_PROJECTION, null, null, CallLog.Calls.DEFAULT_SORT_ORDER)) {
QUERY_PROJECTION, selection, selectionArgs, sortOrder)) {
if (cursor != null) {
int idIndex = cursor.getColumnIndex(CallLog.Calls._ID);
int typeIndex = cursor.getColumnIndex(CallLog.Calls.TYPE);
int numberIndex = cursor.getColumnIndex(CallLog.Calls.NUMBER);
int dateIndex = cursor.getColumnIndex(CallLog.Calls.DATE);
int durationIndex = cursor.getColumnIndex(CallLog.Calls.DURATION);
while (cursor.moveToNext() && logItems.size() < num) {
while (cursor.moveToNext()) {
long id = cursor.getLong(idIndex);
int callType = cursor.getInt(typeIndex);
String number = cursor.getString(numberIndex);
long callDate = cursor.getLong(dateIndex);
long callDuration = cursor.getLong(durationIndex);
logItems.add(new CallLogItem(CallLogItem.Type.fromProviderType(callType),
items.add(new CallLogItem(id, CallLogItem.Type.fromProviderType(callType),
number, callDate, callDuration));
}
}
}
return logItems;
if (reverseOrder) {
Collections.reverse(items);
}
return items;
}
}

View File

@ -2,8 +2,6 @@ package dummydomain.yetanothercallblocker.data;
import android.provider.CallLog;
import java.util.Objects;
public class CallLogItem {
public enum Type {
@ -24,15 +22,15 @@ public class CallLogItem {
}
}
public long id;
public Type type;
public String number;
public long timestamp;
public long duration;
public NumberInfo numberInfo;
public CallLogItem(Type type, String number, long timestamp, long duration) {
Objects.requireNonNull(type);
Objects.requireNonNull(number);
public CallLogItem(long id, Type type, String number, long timestamp, long duration) {
this.id = id;
this.type = type;
this.number = number;
this.timestamp = timestamp;

View File

@ -13,6 +13,8 @@ import java.io.IOException;
import java.util.Collection;
import java.util.List;
import dummydomain.yetanothercallblocker.BlacklistDataSource;
public class BlacklistDao {
public interface DaoSessionProvider {
@ -27,13 +29,20 @@ public class BlacklistDao {
this.daoSessionProvider = daoSessionProvider;
}
public BlacklistDataSource.Factory dataSourceFactory() {
return new BlacklistDataSource.Factory(this);
}
public List<BlacklistItem> loadAll() {
return getDefaultQueryBuilder().list();
}
public QueryBuilder<BlacklistItem> getDefaultQueryBuilder() {
return getBlacklistItemDao().queryBuilder()
.orderRaw("T.'" + BlacklistItemDao.Properties.Name.columnName + "' IS NULL" +
" OR T.'" + BlacklistItemDao.Properties.Name.columnName + "' = ''")
.orderAsc(BlacklistItemDao.Properties.Name)
.orderAsc(BlacklistItemDao.Properties.Pattern)
.list();
.orderAsc(BlacklistItemDao.Properties.Pattern);
}
public <T extends Collection<BlacklistItem>> T detach(T items) {
@ -73,6 +82,10 @@ public class BlacklistDao {
getBlacklistItemDao().deleteByKeyInTx(keys);
}
public long countAll() {
return getBlacklistItemDao().queryBuilder().count();
}
public long countValid() {
return getBlacklistItemDao().queryBuilder()
.where(BlacklistItemDao.Properties.Invalid.notEq(true)).count();

View File

@ -1,54 +0,0 @@
package dummydomain.yetanothercallblocker.preference;
import android.content.Context;
import android.content.res.TypedArray;
import android.text.InputType;
import android.text.TextUtils;
import android.util.AttributeSet;
import androidx.preference.EditTextPreference;
public class IntEditTextPreference extends EditTextPreference {
public IntEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
setListener();
}
public IntEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setListener();
}
public IntEditTextPreference(Context context, AttributeSet attrs) {
super(context, attrs);
setListener();
}
public IntEditTextPreference(Context context) {
super(context);
setListener();
}
@Override
protected Object onGetDefaultValue(TypedArray a, int index) {
return a.getInt(index, 0);
}
@Override
protected void onSetInitialValue(Object defaultValue) {
int defaultInt = defaultValue != null ? (int) defaultValue : 0;
setText(String.valueOf(getPersistedInt(defaultInt)));
}
@Override
protected boolean persistString(String value) {
return persistInt(!TextUtils.isEmpty(value) ? Integer.parseInt(value) : 0);
}
private void setListener() {
setOnBindEditTextListener(editText -> editText.setInputType(InputType.TYPE_CLASS_NUMBER));
}
}

View File

@ -16,7 +16,7 @@
android:layout_width="28dp"
android:layout_height="28dp"
android:scaleType="fitXY"
android:src="@drawable/ic_thumb_down_24dp"
tools:src="@drawable/ic_thumb_down_24dp"
tools:tint="@color/rateNegative" />
<LinearLayout
@ -62,21 +62,21 @@
android:layout_width="14dp"
android:layout_height="14dp"
android:scaleType="fitXY"
android:src="@drawable/ic_call_missed_24dp" />
tools:src="@drawable/ic_call_missed_24dp" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/callTypeIcon2"
android:layout_width="14dp"
android:layout_height="14dp"
android:scaleType="fitXY"
android:src="@drawable/ic_call_missed_24dp" />
tools:src="@drawable/ic_call_missed_24dp" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/callTypeIcon3"
android:layout_width="14dp"
android:layout_height="14dp"
android:scaleType="fitXY"
android:src="@drawable/ic_call_missed_24dp" />
tools:src="@drawable/ic_call_missed_24dp" />
<TextView
android:id="@+id/time"

View File

@ -87,8 +87,6 @@
<string name="country_codes_info">Επεξήγηση</string>
<string name="settings_category_country_codes">Κωδικοί χωρών</string>
<string name="settings_screen_advanced">Σύνθετες ρυθμίσεις</string>
<string name="number_of_recent_calls_summary">Ο αριθμός των πρόσφατων κλήσεων που θα εμφανίζονται στην κύρια οθόνη</string>
<string name="number_of_recent_calls">Αριθμός πρόσφατων κλήσεων</string>
<string name="ui_mode_auto">Ακολουθήστε το σύστημα</string>
<string name="ui_mode_night">Σκοτεινό</string>
<string name="ui_mode_day">Φωτεινό</string>

View File

@ -88,8 +88,6 @@
<string name="ui_mode_day">Clair</string>
<string name="ui_mode_night">Sombre</string>
<string name="ui_mode_auto">Automatique</string>
<string name="number_of_recent_calls">Nombre d\'appels récents</string>
<string name="number_of_recent_calls_summary">Le nombre d\'appels récents à afficher sur l\'écran principal</string>
<string name="settings_screen_advanced">Paramètres avancés</string>
<string name="settings_category_country_codes">Codes pays</string>
<string name="country_codes_info">Explication</string>

View File

@ -51,7 +51,6 @@
<string name="app_name">Još jedan blokator poziva</string>
<string name="sia_category_prank">Zajebancija</string>
<string name="save_logcat_on_crash">Spremi logcat pri rušenju</string>
<string name="number_of_recent_calls_summary">Broj nedavnih poziva koji se prikazuju na glavnom ekranu</string>
<string name="sia_category_scam">Prevara</string>
<string name="reviews_loading">Učitavanje recenzija …</string>
<string name="sia_category_safe_company">Sigurno poduzeće</string>
@ -64,7 +63,6 @@
<string name="country_code_for_reviews_override_summary">Kȏd zemlje koji se koristi u zahtjevima za mrežne recenzije. Namijenjeno je za predstavljanje zemlje pozivatelja. Ostavi prazno za automatsko otkrivanje</string>
<string name="error">Greška</string>
<string name="sia_category_sms">SMS</string>
<string name="number_of_recent_calls">Broj nedavnih poziva</string>
<string name="sia_category_safe_personal">Sigurni osobni</string>
<string name="db_management_reset_base">Resetiraj bazu podataka</string>
<string name="notification_incoming_call_positive">Pozitivan poziv</string>

View File

@ -27,7 +27,6 @@
<string name="save_logcat_on_crash">Lagre logcat ved kræsj</string>
<string name="save_crashes_to_external_storage">Lagre rapporter til offentlig lager</string>
<string name="country_codes_info_summary_addition">Oppdaget automatisk: %s</string>
<string name="number_of_recent_calls_summary">Antall nylige anrop å vise på hovedsjermen</string>
<string name="ui_mode_night">Mørk</string>
<string name="ui_mode_day">Lys</string>
<string name="ui_mode">Drakt</string>
@ -98,7 +97,6 @@
<string name="country_codes_info">Forklaring</string>
<string name="settings_category_country_codes">Landskoder</string>
<string name="settings_screen_advanced">Avanserte innstillinger</string>
<string name="number_of_recent_calls">Antall nylige anrop</string>
<string name="ui_mode_auto">Følg system</string>
<string name="settings_category_call_blocking">Anropsblokkering</string>
<string name="title_settings_activity">Innstillinger</string>

View File

@ -15,7 +15,6 @@
</plurals>
<string name="country_code_for_reviews_override">Numer kierunkowy dla recenzji</string>
<string name="country_codes_info_summary_addition">Wykryte automatycznie: %s</string>
<string name="number_of_recent_calls">Liczba ostatnich połączeń</string>
<string name="settings_category_call_blocking">Blokowanie połączeń</string>
<string name="settings_category_main">Główne</string>
<string name="title_settings_activity">Ustawienia</string>
@ -156,7 +155,6 @@
<string name="export_logcat">Eksportuj logi logcat</string>
<string name="save_logcat_on_crash_summary">Zapisz dane wyjściowe logcat w przypadku awarii (oprócz podstawowego śledzenia stosu)</string>
<string name="save_logcat_on_crash">Zapisz logi z logcat w przypadku awarii</string>
<string name="number_of_recent_calls_summary">Liczba ostatnich połączeń do wyświetlenia na głównym ekranie</string>
<string name="use_monitoring_service">Użyj usługi monitorowania</string>
<string name="use_call_screening_service_summary">Pozwala blokować połączenia, zanim telefon zacznie dzwonić. Wymaga ustawienia aplikacji jako „Telefon” (Android 79) lub „Caller ID” (Android 10+)</string>
<string name="incoming_call_notifications_summary">Wyświetla powiadomienie z podsumowaniem informacji o numerze telefonu (ocena, liczba recenzji, kategoria) podczas połączeń przychodzących</string>

View File

@ -90,8 +90,6 @@
<string name="recent_calls_grouping_none">Без группировки</string>
<string name="recent_calls_grouping_consecutive">Последовательные вызовы</string>
<string name="recent_calls_grouping_day">Непоследовательные в течение дня</string>
<string name="number_of_recent_calls">Кол-во недавних вызовов</string>
<string name="number_of_recent_calls_summary">Количество недавних вызовов на основном экране</string>
<string name="notification_incoming_call_contact">Из списка контактов</string>
<string name="open_settings_activity">Настройки</string>
<string name="title_settings_activity">Настройки</string>

View File

@ -71,8 +71,6 @@
<string name="country_codes_info">ıklama</string>
<string name="settings_category_country_codes">Ülke kodları</string>
<string name="settings_screen_advanced">Gelişmiş ayarlar</string>
<string name="number_of_recent_calls_summary">Ana ekranda görüntülenecek son aramaların sayısı</string>
<string name="number_of_recent_calls">Son aramaların sayısı</string>
<string name="ui_mode_auto">Sistem teması</string>
<string name="ui_mode_night">Karanlık</string>
<string name="ui_mode_day">Aydınlık</string>

View File

@ -84,8 +84,6 @@
<string name="ui_mode_day">Світла</string>
<string name="ui_mode_night">Темна</string>
<string name="ui_mode_auto">Визначається системою</string>
<string name="number_of_recent_calls">Кількість нещодавніх викликів</string>
<string name="number_of_recent_calls_summary">Кількість нещодавніх викликів на основному екрані</string>
<string name="notification_incoming_call_contact">Контакт</string>
<string name="open_settings_activity">Налаштування</string>
<string name="title_settings_activity">Налаштування</string>

View File

@ -117,8 +117,6 @@
<string name="recent_calls_grouping_none">No grouping</string>
<string name="recent_calls_grouping_consecutive">Consecutive calls</string>
<string name="recent_calls_grouping_day">Non-consecutive in a day</string>
<string name="number_of_recent_calls">Number of recent calls</string>
<string name="number_of_recent_calls_summary">The number of recent calls to display on the main screen</string>
<string name="settings_screen_advanced">Advanced settings</string>
<string name="settings_category_country_codes">Country codes</string>

View File

@ -29,11 +29,6 @@
app:key="recentCallsGrouping"
app:title="@string/recent_calls_grouping"
app:useSimpleSummaryProvider="true" />
<dummydomain.yetanothercallblocker.preference.IntEditTextPreference
app:defaultValue="30"
app:key="numberOfRecentCalls"
app:summary="@string/number_of_recent_calls_summary"
app:title="@string/number_of_recent_calls" />
</PreferenceCategory>
<PreferenceCategory app:title="@string/settings_category_call_blocking">