Add ability to sort apps

Apps can now be sorted by name and total/sent/received bytes.
The app sent/received bytes are now shown.

Closes #245
This commit is contained in:
emanuele-f 2023-01-11 20:08:36 +01:00
parent 28aa972f69
commit 3830f93054
7 changed files with 142 additions and 11 deletions

View File

@ -702,7 +702,9 @@ public class MainActivity extends BaseActivity implements NavigationView.OnNavig
startExportSslkeylogfile();
});
builder.create().show();
AlertDialog dialog = builder.create();
dialog.setCanceledOnTouchOutside(false);
dialog.show();
}
private void deletePcapFile(Uri pcapUri) {

View File

@ -19,6 +19,7 @@
package com.emanuelef.remote_capture.adapters;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.drawable.Drawable;
@ -61,6 +62,14 @@ public class AppsStatsAdapter extends RecyclerView.Adapter<AppsStatsAdapter.View
private List<AppStats> mStats;
private final AppsResolver mApps;
private AppStats mSelectedItem;
private SortField mSortField;
public enum SortField {
NAME,
TOTAL_BYTES,
BYTES_SENT,
BYTES_RCVD
}
public class ViewHolder extends RecyclerView.ViewHolder {
ImageView icon;
@ -68,6 +77,7 @@ public class AppsStatsAdapter extends RecyclerView.Adapter<AppsStatsAdapter.View
ImageView whitelistedFlag;
ImageView tempUnblocked;
TextView info;
TextView sent_rcvd;
TextView traffic;
ViewHolder(View itemView) {
@ -78,6 +88,7 @@ public class AppsStatsAdapter extends RecyclerView.Adapter<AppsStatsAdapter.View
whitelistedFlag = itemView.findViewById(R.id.whitelisted);
tempUnblocked = itemView.findViewById(R.id.temp_unblocked);
info = itemView.findViewById(R.id.app_info);
sent_rcvd = itemView.findViewById(R.id.sent_rcvd);
traffic = itemView.findViewById(R.id.traffic);
}
@ -102,6 +113,7 @@ public class AppsStatsAdapter extends RecyclerView.Adapter<AppsStatsAdapter.View
boolean isWhitelistedApp = mWhitelist.matchesApp(stats.getUid());
boolean isWhitelistEnabled = Prefs.isFirewallEnabled(mContext, mPrefs) && Prefs.isFirewallWhitelistMode(mPrefs);
sent_rcvd.setText(mContext.getString(R.string.rcvd_and_sent, Utils.formatBytes(stats.rcvdBytes), Utils.formatBytes(stats.sentBytes)));
traffic.setText(Utils.formatBytes(stats.sentBytes + stats.rcvdBytes));
blockedFlag.setVisibility(isBlockedApp ? View.VISIBLE : View.GONE);
whitelistedFlag.setVisibility(isWhitelistEnabled && isWhitelistedApp ? View.VISIBLE : View.GONE);
@ -120,6 +132,7 @@ public class AppsStatsAdapter extends RecyclerView.Adapter<AppsStatsAdapter.View
mListener = null;
mStats = new ArrayList<>();
mFirewallAvailable = Billing.newInstance(context).isFirewallVisible();
mSortField = SortField.NAME;
setHasStableIds(true);
}
@ -198,6 +211,7 @@ public class AppsStatsAdapter extends RecyclerView.Adapter<AppsStatsAdapter.View
mListener = listener;
}
@SuppressLint("NotifyDataSetChanged")
public void setStats(List<AppStats> stats) {
Collections.sort(stats, (o1, o2) -> {
AppDescriptor a1 = mApps.getAppByUid(o1.getUid(), 0);
@ -212,10 +226,31 @@ public class AppsStatsAdapter extends RecyclerView.Adapter<AppsStatsAdapter.View
if(a2 == null)
return 1;
return a1.compareTo(a2);
switch (mSortField) {
case TOTAL_BYTES:
return -Long.compare(o1.rcvdBytes + o1.sentBytes,
o2.rcvdBytes + o2.sentBytes);
case BYTES_SENT:
return -Long.compare(o1.sentBytes, o2.sentBytes);
case BYTES_RCVD:
return -Long.compare(o1.rcvdBytes, o2.rcvdBytes);
case NAME:
default:
return a1.compareTo(a2);
}
});
mStats = stats;
notifyDataSetChanged();
}
public SortField getSortField() {
return mSortField;
}
@SuppressLint("NotifyDataSetChanged")
public void setSortField(SortField field) {
mSortField = field;
setStats(mStats);
}
}

View File

@ -48,6 +48,7 @@ import com.emanuelef.remote_capture.PCAPdroid;
import com.emanuelef.remote_capture.R;
import com.emanuelef.remote_capture.activities.AppDetailsActivity;
import com.emanuelef.remote_capture.adapters.AppsStatsAdapter;
import com.emanuelef.remote_capture.adapters.AppsStatsAdapter.SortField;
import com.emanuelef.remote_capture.interfaces.ConnectionsListener;
import com.emanuelef.remote_capture.model.AppStats;
import com.emanuelef.remote_capture.model.Blocklist;
@ -63,6 +64,7 @@ public class AppsFragment extends Fragment implements ConnectionsListener, MenuP
private Handler mHandler;
private boolean mRefreshApps;
private boolean listenerSet;
private Menu mMenu;
@Override
public void onPause() {
@ -122,14 +124,46 @@ public class AppsFragment extends Fragment implements ConnectionsListener, MenuP
});
}
private void refreshSortField() {
if((mMenu == null) || (mAdapter == null))
return;
SortField sortField = mAdapter.getSortField();
Log.d(TAG, "Sort field:" + sortField);
MenuItem byName = mMenu.findItem(R.id.sort_by_name);
MenuItem byTotalBytes = mMenu.findItem(R.id.sort_by_total_bytes);
MenuItem byBytesSent = mMenu.findItem(R.id.sort_by_bytes_sent);
MenuItem byBytesRcvd = mMenu.findItem(R.id.sort_by_bytes_rcvd);
// important: the checked item must first be unchecked
byName.setChecked(false);
byTotalBytes.setChecked(false);
byBytesSent.setChecked(false);
byBytesRcvd.setChecked(false);
if(sortField.equals(SortField.NAME))
byName.setChecked(true);
else if(sortField.equals(SortField.TOTAL_BYTES))
byTotalBytes.setChecked(true);
else if(sortField.equals(SortField.BYTES_SENT))
byBytesSent.setChecked(true);
else if(sortField.equals(SortField.BYTES_RCVD))
byBytesRcvd.setChecked(true);
}
@Override
public void onCreateMenu(@NonNull Menu menu, MenuInflater menuInflater) {
menuInflater.inflate(R.menu.apps_menu, menu);
mMenu = menu;
refreshSortField();
}
@Override
public boolean onMenuItemSelected(@NonNull MenuItem menuItem) {
if(menuItem.getItemId() == R.id.reset) {
int id = menuItem.getItemId();
if(id == R.id.reset) {
new AlertDialog.Builder(requireContext())
.setMessage(R.string.reset_stats_confirm)
.setPositiveButton(R.string.yes, (dialog, whichButton) -> {
@ -143,7 +177,24 @@ public class AppsFragment extends Fragment implements ConnectionsListener, MenuP
.show();
return true;
} else if(id == R.id.sort_by_name) {
mAdapter.setSortField(SortField.NAME);
refreshSortField();
return true;
} else if(id == R.id.sort_by_total_bytes) {
mAdapter.setSortField(SortField.TOTAL_BYTES);
refreshSortField();
return true;
} else if(id == R.id.sort_by_bytes_sent) {
mAdapter.setSortField(SortField.BYTES_SENT);
refreshSortField();
return true;
} else if(id == R.id.sort_by_bytes_rcvd) {
mAdapter.setSortField(SortField.BYTES_RCVD);
refreshSortField();
return true;
}
return false;
}

View File

@ -0,0 +1,5 @@
<vector android:autoMirrored="true" android:height="24dp"
android:tint="?attr/colorControlNormal" android:viewportHeight="24"
android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M3,18h6v-2L3,16v2zM3,6v2h18L21,6L3,6zM3,13h12v-2L3,11v2z"/>
</vector>

View File

@ -12,19 +12,37 @@
<ImageView
android:id="@+id/icon"
tools:src="@drawable/ic_apps"
tools:tint="?attr/colorAccent"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_margin="4dp" />
<TextView
<LinearLayout
android:orientation="vertical"
android:layout_marginStart="4dp"
android:layout_width="0dp"
android:layout_height="fill_parent"
android:layout_weight="0.5"
android:layout_marginStart="5dp"
tools:text="Example app"
android:maxLines="2"
android:gravity="center_vertical"
android:id="@+id/app_info"/>
android:layout_height="wrap_content">
<TextView
android:id="@+id/app_info"
android:textStyle="bold"
android:textSize="14sp"
android:ellipsize="end"
android:singleLine="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="Example app" />
<TextView
android:id="@+id/sent_rcvd"
tools:text="@string/rcvd_and_sent"
android:textSize="12sp"
android:layout_marginTop="2dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<ImageView
android:id="@+id/blocked"

View File

@ -3,9 +3,27 @@
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:title="@string/sort_by"
android:icon="@drawable/ic_sort"
app:showAsAction="ifRoom">
<menu>
<group android:id="@+id/sort_by" android:checkableBehavior="single">
<item android:id="@+id/sort_by_name"
android:title="@string/app_name"/>
<item android:id="@+id/sort_by_total_bytes"
android:title="@string/total_bytes"/>
<item android:id="@+id/sort_by_bytes_sent"
android:title="@string/bytes_sent"/>
<item android:id="@+id/sort_by_bytes_rcvd"
android:title="@string/bytes_rcvd"/>
</group>
</menu>
</item>
<item
android:id="@+id/reset"
android:title="@string/reset"
android:icon="@drawable/ic_reset"
app:showAsAction="ifRoom" />
app:showAsAction="ifRoom"/>
</menu>

View File

@ -451,4 +451,6 @@
<string name="many_rules_warning">You are trying to import many rules, which could make the app unresponsive during some interactions. Do you really want to continue?</string>
<string name="pcapng_format">PCAPNG format</string>
<string name="pcapng_format_summary">Dump packets in the PCAPNG dump format, which allows embedding TLS decryption secrets</string>
<string name="sort_by">Sort by</string>
<string name="total_bytes">Total bytes</string>
</resources>