Use Android Paging library for list loading
Recent calls list now supports "endless scrolling"
This commit is contained in:
parent
91e197abf3
commit
4bffec08a1
|
@ -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'
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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() + "'";
|
||||
|
|
|
@ -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() + "'";
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 7–9) 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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -71,8 +71,6 @@
|
|||
<string name="country_codes_info">Açı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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
Loading…
Reference in New Issue