diff --git a/app/build.gradle b/app/build.gradle index 4f40565b..5e499064 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -44,7 +44,8 @@ dependencies { implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0' implementation 'androidx.preference:preference:1.1.1' - implementation 'com.google.android.material:material:1.2.1' + implementation 'androidx.recyclerview:recyclerview:1.1.0' + implementation 'com.google.android.material:material:1.3.0' // Third-party implementation 'cat.ereza:customactivityoncrash:2.3.0' diff --git a/app/src/main/java/com/emanuelef/remote_capture/ConnectionsAdapter.java b/app/src/main/java/com/emanuelef/remote_capture/ConnectionsAdapter.java index 53220fb9..848d2afc 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/ConnectionsAdapter.java +++ b/app/src/main/java/com/emanuelef/remote_capture/ConnectionsAdapter.java @@ -21,81 +21,99 @@ package com.emanuelef.remote_capture; import android.content.Context; import android.graphics.drawable.Drawable; -import android.os.Handler; -import android.telecom.ConnectionRequest; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.BaseAdapter; import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.core.content.ContextCompat; +import androidx.recyclerview.widget.RecyclerView; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Comparator; import java.util.Objects; -public class ConnectionsAdapter extends BaseAdapter { - private static final String TAG = "ConnectionsAdapter"; - private final MainActivity mActivity; - private final Drawable mUnknownIcon; +class ViewHolder extends RecyclerView.ViewHolder { + ImageView icon; + TextView remote; + TextView l7proto; + TextView traffic; - ConnectionsAdapter(MainActivity context) { - mActivity = context; - mUnknownIcon = ContextCompat.getDrawable(mActivity, android.R.drawable.ic_menu_help); + ViewHolder(View itemView) { + super(itemView); + + icon = itemView.findViewById(R.id.icon); + remote = itemView.findViewById(R.id.remote); + l7proto = itemView.findViewById(R.id.l7proto); + traffic = itemView.findViewById(R.id.traffic); } - @NonNull - @Override - public View getView(int position, View convertView, @NonNull ViewGroup parent) { - ConnDescriptor conn = getItem(position); - - if(convertView == null) { - LayoutInflater inflater = (LayoutInflater) mActivity.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - assert inflater != null; - convertView = inflater.inflate(R.layout.connection_item, parent, false); - } - - assert conn != null; - ImageView icon = convertView.findViewById(R.id.icon); - TextView remote = convertView.findViewById(R.id.remote); - TextView l7proto = convertView.findViewById(R.id.l7proto); - TextView traffic = convertView.findViewById(R.id.traffic); - AppDescriptor app = mActivity.findAppByUid(conn.uid); + public void bindConn(MainActivity activity, ConnDescriptor conn, Drawable unknownIcon) { + AppDescriptor app = activity.findAppByUid(conn.uid); Drawable appIcon; - appIcon = (app != null) ? Objects.requireNonNull(app.getIcon().getConstantState()).newDrawable() : mUnknownIcon; + appIcon = (app != null) ? Objects.requireNonNull(app.getIcon().getConstantState()).newDrawable() : unknownIcon; icon.setImageDrawable(appIcon); if(conn.info.length() > 0) remote.setText(conn.info); else - remote.setText(String.format(mActivity.getResources().getString(R.string.ip_and_port), + remote.setText(String.format(activity.getResources().getString(R.string.ip_and_port), conn.dst_ip, conn.dst_port)); l7proto.setText(conn.l7proto); traffic.setText(Utils.formatBytes(conn.sent_bytes + conn.rcvd_bytes)); + } +} - return(convertView); +public class ConnectionsAdapter extends RecyclerView.Adapter { + private static final String TAG = "ConnectionsAdapter"; + private final MainActivity mActivity; + private final Drawable mUnknownIcon; + private View.OnClickListener mListener; + + ConnectionsAdapter(MainActivity context) { + mActivity = context; + mUnknownIcon = ContextCompat.getDrawable(mActivity, android.R.drawable.ic_menu_help); + mListener = null; } @Override - public int getCount() { + public int getItemCount() { ConnectionsRegister reg = CaptureService.getConnsRegister(); return (reg != null) ? reg.getConnCount() : 0; } + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + LayoutInflater inflater = (LayoutInflater) mActivity.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + assert inflater != null; + + View view = inflater.inflate(R.layout.connection_item, parent, false); + + if(mListener != null) + view.setOnClickListener(mListener); + + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + ConnDescriptor conn = getItem(position); + + if(conn == null) + return; + + holder.bindConn(mActivity, conn, mUnknownIcon); + } + @Override public long getItemId(int pos) { ConnectionsRegister reg = CaptureService.getConnsRegister(); - if((pos < 0) || (pos >= getCount()) || (reg == null)) + if((pos < 0) || (pos >= getItemCount()) || (reg == null)) return -1; ConnDescriptor conn = reg.getConn(pos); @@ -103,14 +121,17 @@ public class ConnectionsAdapter extends BaseAdapter { return ((conn != null) ? conn.incr_id : -1); } - @Override public ConnDescriptor getItem(int pos) { ConnectionsRegister reg = CaptureService.getConnsRegister(); /* Prevent indexOutOfBounds exception in updateView() */ - if((pos < 0) || (pos >= getCount()) || (reg == null)) + if((pos < 0) || (pos >= getItemCount()) || (reg == null)) return null; return reg.getConn(pos); } + + public void setClickListener(View.OnClickListener listener) { + mListener = listener; + } } diff --git a/app/src/main/java/com/emanuelef/remote_capture/ConnectionsFragment.java b/app/src/main/java/com/emanuelef/remote_capture/ConnectionsFragment.java index 947ad5da..f8339d0b 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/ConnectionsFragment.java +++ b/app/src/main/java/com/emanuelef/remote_capture/ConnectionsFragment.java @@ -26,13 +26,12 @@ import android.os.Handler; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.ListView; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; public class ConnectionsFragment extends Fragment implements AppStateListener, ConnectionsListener { private static final String TAG = "ConnectionsFragment"; @@ -67,36 +66,37 @@ public class ConnectionsFragment extends Fragment implements AppStateListener, C @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - ListView connList = view.findViewById(R.id.connections_view); + EmptyRecyclerView connList = view.findViewById(R.id.connections_view); + connList.setLayoutManager(new LinearLayoutManager(mActivity)); + TextView emptyText = view.findViewById(R.id.no_connections); connList.setEmptyView(emptyText); mAdapter = new ConnectionsAdapter(mActivity); connList.setAdapter(mAdapter); - connList.setOnItemClickListener(new AdapterView.OnItemClickListener() { - @Override - public void onItemClick(AdapterView adapterView, View view, int pos, long id) { - ConnDescriptor item = (ConnDescriptor) adapterView.getItemAtPosition(pos); - if(item != null) { - Intent intent = new Intent(getContext(), ConnectionDetails.class); - AppDescriptor app = mActivity.findAppByUid(item.uid); - String app_name = null;//;1051 + mAdapter.setClickListener(v -> { + int pos = connList.getChildLayoutPosition(v); + ConnDescriptor item = mAdapter.getItem(pos); - if(app != null) - app_name = app.getName(); - else if(item.uid == 1000) - app_name = "system"; - else if(item.uid == 1051) - app_name = "netd"; + if(item != null) { + Intent intent = new Intent(getContext(), ConnectionDetails.class); + AppDescriptor app = mActivity.findAppByUid(item.uid); + String app_name = null;//;1051 - intent.putExtra(ConnectionDetails.CONN_EXTRA_KEY, item); + if(app != null) + app_name = app.getName(); + else if(item.uid == 1000) + app_name = "system"; + else if(item.uid == 1051) + app_name = "netd"; - if(app_name != null) - intent.putExtra(ConnectionDetails.APP_NAME_EXTRA_KEY, app_name); + intent.putExtra(ConnectionDetails.CONN_EXTRA_KEY, item); - startActivity(intent); - } + if(app_name != null) + intent.putExtra(ConnectionDetails.APP_NAME_EXTRA_KEY, app_name); + + startActivity(intent); } }); @@ -127,17 +127,12 @@ public class ConnectionsFragment extends Fragment implements AppStateListener, C @Override public void appsLoaded() { - // Refresh the adapter to load the apps + // Refresh the adapter to load the apps icons mAdapter.notifyDataSetChanged(); } @Override public void connectionsChanges() { - mHandler.post(new Runnable() { - @Override - public void run() { - mAdapter.notifyDataSetChanged(); - } - }); + mHandler.post(() -> mAdapter.notifyDataSetChanged()); } } diff --git a/app/src/main/java/com/emanuelef/remote_capture/EmptyRecyclerView.java b/app/src/main/java/com/emanuelef/remote_capture/EmptyRecyclerView.java new file mode 100644 index 00000000..3f4445ab --- /dev/null +++ b/app/src/main/java/com/emanuelef/remote_capture/EmptyRecyclerView.java @@ -0,0 +1,73 @@ +package com.emanuelef.remote_capture; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; + +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; + +// Adapter from https://gist.github.com/AlexZhukovich/537eaa1e3c82ef9f5d5cd22efdc80c54#file-emptyrecyclerview-java +public class EmptyRecyclerView extends RecyclerView { + private View mEmptyView; + + public EmptyRecyclerView(Context context) { + super(context); + } + + public EmptyRecyclerView(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public EmptyRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + private void initEmptyView() { + if (mEmptyView != null) { + mEmptyView.setVisibility( + getAdapter() == null || getAdapter().getItemCount() == 0 ? VISIBLE : GONE); + EmptyRecyclerView.this.setVisibility( + getAdapter() == null || getAdapter().getItemCount() == 0 ? GONE : VISIBLE); + } + } + + final AdapterDataObserver observer = new AdapterDataObserver() { + @Override + public void onChanged() { + super.onChanged(); + initEmptyView(); + } + + @Override + public void onItemRangeInserted(int positionStart, int itemCount) { + super.onItemRangeInserted(positionStart, itemCount); + initEmptyView(); + } + + @Override + public void onItemRangeRemoved(int positionStart, int itemCount) { + super.onItemRangeRemoved(positionStart, itemCount); + initEmptyView(); + } + }; + + @Override + public void setAdapter(Adapter adapter) { + Adapter oldAdapter = getAdapter(); + super.setAdapter(adapter); + + if (oldAdapter != null) { + oldAdapter.unregisterAdapterDataObserver(observer); + } + + if (adapter != null) { + adapter.registerAdapterDataObserver(observer); + } + } + + public void setEmptyView(View view) { + this.mEmptyView = view; + initEmptyView(); + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/connection_item.xml b/app/src/main/res/layout/connection_item.xml index cf632c2a..0495a581 100644 --- a/app/src/main/res/layout/connection_item.xml +++ b/app/src/main/res/layout/connection_item.xml @@ -1,6 +1,7 @@ diff --git a/app/src/main/res/layout/connections.xml b/app/src/main/res/layout/connections.xml index 3cebf1f9..cf03eb9f 100644 --- a/app/src/main/res/layout/connections.xml +++ b/app/src/main/res/layout/connections.xml @@ -4,11 +4,11 @@ android:layout_height="match_parent" android:orientation="vertical"> - + >