Add basic call log

This commit is contained in:
xynngh 2020-05-05 15:46:25 +04:00
parent b1b5c7c731
commit 3f6778cbc7
17 changed files with 527 additions and 56 deletions

View File

@ -31,9 +31,8 @@ dependencies {
implementation 'com.squareup.okhttp3:okhttp:3.12.11'
implementation 'commons-codec:commons-codec:1.14' // beware: a version included in Android is used instead
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'com.google.android.material:material:1.1.0'
implementation 'androidx.work:work-runtime:2.3.4'
}

View File

@ -0,0 +1,42 @@
package dummydomain.yetanothercallblocker;
import android.content.Context;
import android.database.Cursor;
import android.provider.CallLog;
import java.util.ArrayList;
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
};
public static List<CallLogItem> getRecentCalls(Context context, int num) {
List<CallLogItem> logItems = new ArrayList<>(num);
try (Cursor cursor = context.getContentResolver().query(CallLog.Calls.CONTENT_URI,
QUERY_PROJECTION, null, null, CallLog.Calls.DEFAULT_SORT_ORDER)) {
if (cursor != null) {
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) {
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),
number, callDate, callDuration));
}
}
}
return logItems;
}
}

View File

@ -0,0 +1,39 @@
package dummydomain.yetanothercallblocker;
import android.provider.CallLog;
import java.util.Objects;
import dummydomain.yetanothercallblocker.sia.model.NumberInfo;
public class CallLogItem {
public enum Type {
INCOMING, OUTGOING, MISSED, REJECTED, OTHER;
public static Type fromProviderType(int type) {
switch (type) {
case CallLog.Calls.INCOMING_TYPE: return INCOMING;
case CallLog.Calls.OUTGOING_TYPE: return OUTGOING;
case CallLog.Calls.MISSED_TYPE: return MISSED;
case CallLog.Calls.REJECTED_TYPE: return REJECTED;
default: return OTHER;
}
}
}
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);
this.type = type;
this.number = number;
this.timestamp = timestamp;
this.duration = duration;
}
}

View File

@ -0,0 +1,120 @@
package dummydomain.yetanothercallblocker;
import android.text.format.DateUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatImageView;
import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
public class CallLogItemRecyclerViewAdapter
extends RecyclerView.Adapter<CallLogItemRecyclerViewAdapter.ViewHolder> {
public interface OnListInteractionListener {
void onListFragmentInteraction(CallLogItem item);
}
private final List<CallLogItem> items;
private final @Nullable OnListInteractionListener listener;
public CallLogItemRecyclerViewAdapter(List<CallLogItem> items,
@Nullable OnListInteractionListener listener) {
this.items = items;
this.listener = listener;
}
@Override
@NonNull
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.call_log_item, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull final ViewHolder holder, int position) {
holder.bind(items.get(position));
}
@Override
public int getItemCount() {
return items.size();
}
private void onClick(int index) {
if (index != RecyclerView.NO_POSITION && listener != null) {
listener.onListFragmentInteraction(items.get(index));
}
}
class ViewHolder extends RecyclerView.ViewHolder {
final View view;
final AppCompatImageView callTypeIcon;
final TextView number;
final AppCompatImageView numberInfoIcon;
final TextView time;
ViewHolder(View view) {
super(view);
this.view = view;
callTypeIcon = view.findViewById(R.id.callTypeIcon);
number = view.findViewById(R.id.item_number);
numberInfoIcon = view.findViewById(R.id.numberInfoIcon);
time = view.findViewById(R.id.time);
view.setOnClickListener(v -> onClick(getAdapterPosition()));
}
void bind(CallLogItem item) {
Integer icon;
switch (item.type) {
case INCOMING:
icon = R.drawable.ic_call_received_24dp;
break;
case OUTGOING:
icon = R.drawable.ic_call_made_24dp;
break;
case MISSED:
case REJECTED:
icon = R.drawable.ic_call_missed_24dp;
break;
default:
icon = null;
break;
}
if (icon != null) {
callTypeIcon.setImageResource(icon);
} else {
callTypeIcon.setImageDrawable(null);
}
number.setText(item.number);
IconAndColor iconAndColor = IconAndColor.forNumberRating(item.numberInfo.rating);
if (!iconAndColor.noInfo) {
iconAndColor.setOnImageView(numberInfoIcon);
} else {
numberInfoIcon.setImageDrawable(null);
}
time.setText(DateUtils.getRelativeTimeSpanString(item.timestamp));
}
@Override
public String toString() {
return super.toString() + " '" + number.getText() + "'";
}
}
}

View File

@ -1,21 +1,19 @@
package dummydomain.yetanothercallblocker;
import android.content.res.ColorStateList;
import androidx.annotation.NonNull;
import androidx.core.widget.ImageViewCompat;
import androidx.appcompat.widget.AppCompatImageView;
import androidx.recyclerview.widget.RecyclerView;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.AppCompatImageView;
import androidx.recyclerview.widget.RecyclerView;
import java.util.Collections;
import java.util.List;
import dummydomain.yetanothercallblocker.sia.model.CommunityReview;
import dummydomain.yetanothercallblocker.sia.model.NumberRating;
class CustomListViewAdapter extends RecyclerView.Adapter<CustomListViewAdapter.CommunityReviewViewHolder> {
@ -73,23 +71,7 @@ class CustomListViewAdapter extends RecyclerView.Adapter<CustomListViewAdapter.C
tvDescription.setVisibility(View.VISIBLE);
}
IconAndColor iconAndColor = getRatingIconData(item.getRating());
ivRating.setImageResource(iconAndColor.getIconResId());
ImageViewCompat.setImageTintList(ivRating, ColorStateList.valueOf(
itemView.getContext().getResources().getColor(iconAndColor.getColorResId())));
}
protected IconAndColor getRatingIconData(NumberRating rating) {
switch (rating) {
case NEUTRAL:
case UNKNOWN:
return IconAndColor.of(R.drawable.ic_thumbs_up_down_black_24dp, R.color.rateNeutral);
case POSITIVE:
return IconAndColor.of(R.drawable.ic_thumb_up_black_24dp, R.color.ratePositive);
case NEGATIVE:
return IconAndColor.of(R.drawable.ic_thumb_down_black_24dp, R.color.rateNegative);
}
return IconAndColor.of(R.drawable.ic_thumbs_up_down_black_24dp, R.color.notFound);
IconAndColor.forNumberRating(item.getRating()).setOnImageView(ivRating);
}
}
}

View File

@ -2,13 +2,11 @@ package dummydomain.yetanothercallblocker;
import android.os.AsyncTask;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import android.view.View;
import android.widget.EditText;
import android.widget.TextView;
import java.util.HashMap;
import java.util.Map;
import androidx.appcompat.app.AppCompatActivity;
import dummydomain.yetanothercallblocker.sia.DatabaseSingleton;
import dummydomain.yetanothercallblocker.sia.model.NumberCategory;
@ -121,17 +119,7 @@ public class DebugActivity extends AppCompatActivity {
}
private void displaySummary(CommunityDatabaseItem item) {
View summary = DebugActivity.this.findViewById(R.id.reviews_summary);
summary.setVisibility(View.VISIBLE);
Map<Integer, Integer> map = new HashMap<>();
map.put(R.id.summary_text_negative, item.getNegativeRatingsCount());
map.put(R.id.summary_text_neutral, item.getNeutralRatingsCount());
map.put(R.id.summary_text_positive, item.getPositiveRatingsCount());
for (Map.Entry<Integer, Integer> e: map.entrySet()) {
((TextView) summary.findViewById(e.getKey())).setText(
String.valueOf(e.getValue()));
}
ReviewsSummaryHelper.populateSummary(findViewById(R.id.reviews_summary), item);
}
}

View File

@ -1,23 +1,66 @@
package dummydomain.yetanothercallblocker;
class IconAndColor {
private int iconResId;
private int colorResId;
import android.content.res.ColorStateList;
public IconAndColor(int icon, int color) {
this.iconResId = icon;
this.colorResId = color;
import androidx.annotation.ColorRes;
import androidx.annotation.DrawableRes;
import androidx.appcompat.widget.AppCompatImageView;
import androidx.core.widget.ImageViewCompat;
import dummydomain.yetanothercallblocker.sia.model.NumberInfo;
import dummydomain.yetanothercallblocker.sia.model.NumberRating;
class IconAndColor {
@DrawableRes
final int iconResId;
@ColorRes
final int colorResId;
final boolean noInfo;
private IconAndColor(int iconResId, int colorResId) {
this(iconResId, colorResId, false);
}
static IconAndColor of(int icon, int color) {
private IconAndColor(int icon, int color, boolean noInfo) {
this.iconResId = icon;
this.colorResId = color;
this.noInfo = noInfo;
}
void setOnImageView(AppCompatImageView imageView) {
imageView.setImageResource(iconResId);
ImageViewCompat.setImageTintList(imageView, ColorStateList.valueOf(
imageView.getContext().getResources().getColor(colorResId)));
}
static IconAndColor of(@DrawableRes int icon, @ColorRes int color) {
return new IconAndColor(icon, color);
}
public int getIconResId() {
return iconResId;
// TODO: fix duplication
static IconAndColor forNumberRating(NumberRating rating) {
switch (rating) {
case NEUTRAL:
return of(R.drawable.ic_thumbs_up_down_black_24dp, R.color.rateNeutral);
case POSITIVE:
return of(R.drawable.ic_thumb_up_black_24dp, R.color.ratePositive);
case NEGATIVE:
return of(R.drawable.ic_thumb_down_black_24dp, R.color.rateNegative);
}
return new IconAndColor(R.drawable.ic_thumbs_up_down_black_24dp, R.color.notFound, true);
}
public int getColorResId() {
return colorResId;
static IconAndColor forNumberRating(NumberInfo.Rating rating) {
switch (rating) {
case NEUTRAL:
return of(R.drawable.ic_thumbs_up_down_black_24dp, R.color.rateNeutral);
case POSITIVE:
return of(R.drawable.ic_thumb_up_black_24dp, R.color.ratePositive);
case NEGATIVE:
return of(R.drawable.ic_thumb_down_black_24dp, R.color.rateNegative);
}
return new IconAndColor(R.drawable.ic_thumbs_up_down_black_24dp, R.color.notFound, true);
}
}

View File

@ -1,27 +1,42 @@
package dummydomain.yetanothercallblocker;
import android.Manifest;
import android.annotation.SuppressLint;
import android.content.Intent;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.SwitchCompat;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.SwitchCompat;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List;
import dummydomain.yetanothercallblocker.sia.DatabaseSingleton;
import dummydomain.yetanothercallblocker.sia.model.NumberInfo;
import dummydomain.yetanothercallblocker.sia.model.database.DbManager;
public class MainActivity extends AppCompatActivity {
private CallLogItemRecyclerViewAdapter callLogAdapter;
private List<CallLogItem> callLogItems = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
callLogAdapter = new CallLogItemRecyclerViewAdapter(callLogItems, this::onCallLogItemClicked);
RecyclerView recyclerView = findViewById(R.id.callLogList);
recyclerView.setAdapter(callLogAdapter);
SwitchCompat notificationsSwitch = findViewById(R.id.notificationsEnabledSwitch);
notificationsSwitch.setChecked(CallReceiver.isEnabled(this));
notificationsSwitch.setOnCheckedChangeListener((buttonView, isChecked)
@ -68,6 +83,15 @@ public class MainActivity extends AppCompatActivity {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
// TODO: handle
loadCallLog();
}
@Override
protected void onStart() {
super.onStart();
loadCallLog();
}
public void onDownloadDbClick(View view) {
@ -96,6 +120,33 @@ public class MainActivity extends AppCompatActivity {
startActivity(new Intent(this, DebugActivity.class));
}
private void onCallLogItemClicked(CallLogItem item) {
AlertDialog.Builder builder = new AlertDialog.Builder(this)
.setTitle(item.number);
@SuppressLint("InflateParams")
View view = getLayoutInflater().inflate(R.layout.info_dialog, null);
TextView featuredNameView = view.findViewById(R.id.featuredName);
if (item.numberInfo.name != null) {
featuredNameView.setText(item.numberInfo.name);
} else {
featuredNameView.setVisibility(View.GONE);
}
ReviewsSummaryHelper.populateSummary(view.findViewById(R.id.reviews_summary),
item.numberInfo.communityDatabaseItem);
builder.setView(view);
builder.setNeutralButton(R.string.online_reviews, (d, w)
-> ReviewsActivity.startForNumber(this, item.number));
builder.setNegativeButton(R.string.back, null);
builder.show();
}
enum UpdateUiState {HIDDEN, NO_DB, DOWNLOADING_DB, ERROR}
private void updateNoDbUi(UpdateUiState state) {
@ -107,4 +158,41 @@ public class MainActivity extends AppCompatActivity {
findViewById(R.id.dbCouldNotBeDownloaded).setVisibility(state == UpdateUiState.ERROR ? View.VISIBLE : View.GONE);
}
private void loadCallLog() {
if (!PermissionHelper.havePermission(this, Manifest.permission.READ_CALL_LOG)) {
setCallLogVisibility(false);
return;
}
new AsyncTask<Void, Void, List<CallLogItem>>() {
@Override
protected List<CallLogItem> doInBackground(Void... voids) {
List<CallLogItem> items = CallLogHelper.getRecentCalls(MainActivity.this, 10);
for (CallLogItem item : items) {
item.numberInfo = DatabaseSingleton.getCommunityDatabase().isOperational()
? DatabaseSingleton.getNumberInfo(item.number)
: new NumberInfo();
}
return items;
}
@Override
protected void onPostExecute(List<CallLogItem> items) {
callLogItems.clear();
callLogItems.addAll(items);
callLogAdapter.notifyDataSetChanged();
setCallLogVisibility(true);
}
}.execute();
}
private void setCallLogVisibility(boolean visible) {
int visibility = visible ? View.VISIBLE : View.GONE;
findViewById(R.id.callLogTitle).setVisibility(visibility);
findViewById(R.id.callLogList).setVisibility(visibility);
}
}

View File

@ -1,11 +1,13 @@
package dummydomain.yetanothercallblocker;
import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Build;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.appcompat.app.AppCompatActivity;
import java.util.ArrayList;
import java.util.List;
@ -39,4 +41,9 @@ public class PermissionHelper {
}
}
public static boolean havePermission(Context context, String permission) {
return ContextCompat.checkSelfPermission(context, permission)
== PackageManager.PERMISSION_GRANTED;
}
}

View File

@ -0,0 +1,26 @@
package dummydomain.yetanothercallblocker;
import android.view.View;
import android.widget.TextView;
import java.util.HashMap;
import java.util.Map;
import dummydomain.yetanothercallblocker.sia.model.database.CommunityDatabaseItem;
public class ReviewsSummaryHelper {
public static void populateSummary(View reviewsSummary, CommunityDatabaseItem item) {
reviewsSummary.setVisibility(View.VISIBLE);
Map<Integer, Integer> map = new HashMap<>(3);
map.put(R.id.summary_text_negative, item != null ? item.getNegativeRatingsCount() : 0);
map.put(R.id.summary_text_neutral, item != null ? item.getNeutralRatingsCount() : 0);
map.put(R.id.summary_text_positive, item != null ? item.getPositiveRatingsCount() : 0);
for (Map.Entry<Integer, Integer> e : map.entrySet()) {
((TextView) reviewsSummary.findViewById(e.getKey())).setText(
String.valueOf(e.getValue()));
}
}
}

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#00C853"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M9,5v2h6.59L4,18.59 5.41,20 17,8.41V15h2V5z" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#C53929"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M19.59,7L12,14.59 6.41,9H11V7H3v8h2v-4.59l7,7 9,-9z" />
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#00A8FF"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M20,5.41L18.59,4 7,15.59V9H5v10h10v-2H8.41z" />
</vector>

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
@ -18,7 +19,7 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_height="wrap_content"
android:divider="?android:listDivider"
android:orientation="vertical"
android:paddingLeft="@dimen/item_padding"
@ -97,4 +98,26 @@
android:layout_marginBottom="@dimen/text_margin"
android:text="@string/auto_update_enabled" />
</LinearLayout>
<TextView
android:id="@+id/callLogTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:layout_marginStart="@dimen/text_margin"
android:layout_marginLeft="@dimen/text_margin"
android:paddingTop="@dimen/item_padding"
android:text="@string/recent_calls"
android:textAlignment="textStart" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/callLogList"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="@dimen/item_padding"
android:layout_marginTop="@dimen/item_padding"
android:layout_marginRight="@dimen/item_padding"
android:layout_marginBottom="@dimen/item_padding"
app:layoutManager="LinearLayoutManager"
tools:listitem="@layout/call_log_item" />
</LinearLayout>

View File

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:gravity="center_vertical|start"
android:orientation="horizontal">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/callTypeIcon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="1dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="1dp"
android:layout_marginBottom="8dp"
android:src="@drawable/ic_call_missed_24dp" />
<TextView
android:id="@+id/item_number"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:textAppearance="?attr/textAppearanceListItem"
tools:text="+12345678901" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/numberInfoIcon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="6dp"
android:layout_marginTop="8dp"
android:layout_marginRight="1dp"
android:layout_marginBottom="8dp"
android:src="@drawable/ic_thumb_down_black_24dp" />
<Space
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
<TextView
android:id="@+id/time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/text_margin"
android:layout_marginTop="8dp"
android:layout_marginEnd="@dimen/text_margin"
android:layout_marginBottom="8dp"
android:textAppearance="?attr/textAppearanceListItemSecondary"
android:textSize="12sp"
tools:text="1 minute ago" />
</LinearLayout>

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/featuredName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20sp"
tools:text="Featured company name" />
<include
layout="@layout/reviews_summary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="6dp" />
</LinearLayout>

View File

@ -51,6 +51,10 @@
<string name="downloading_db">Downloading DB…</string>
<string name="db_could_not_be_downloaded">Database couldn\'t be downloaded</string>
<string name="recent_calls">Recent calls</string>
<string name="online_reviews">Online reviews</string>
<string name="back">Back</string>
<string name="incoming_call_notifications_enabled">Incoming call notifications enabled</string>
<string name="block_calls">Block unwanted calls</string>
<string name="auto_update_enabled">Auto-update enabled</string>