diff --git a/app/src/main/java/com/emanuelef/remote_capture/CaptureService.java b/app/src/main/java/com/emanuelef/remote_capture/CaptureService.java index ef128aca..49dfd513 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/CaptureService.java +++ b/app/src/main/java/com/emanuelef/remote_capture/CaptureService.java @@ -107,6 +107,7 @@ public class CaptureService extends VpnService implements Runnable { private ConnectivityManager.NetworkCallback mNetworkCallback; private AppsResolver appsResolver; private boolean mMalwareDetectionEnabled; + private boolean mBlacklistsUpdateRequested; private Blacklists mBlacklists; /* The maximum connections to log into the ConnectionsRegister. Older connections are dropped. @@ -544,13 +545,14 @@ public class CaptureService extends VpnService implements Runnable { if(!mMalwareDetectionEnabled || (mBlacklistsUpdateThread != null)) return; - if(mBlacklists.needsUpdate()) { + if(mBlacklistsUpdateRequested || mBlacklists.needsUpdate()) { mBlacklistsUpdateThread = new Thread(this::updateBlacklistsWork, "Blacklists Update"); mBlacklistsUpdateThread.start(); } } private void updateBlacklistsWork() { + mBlacklistsUpdateRequested = false; mBlacklists.update(); reloadBlacklists(); mBlacklistsUpdateThread = null; @@ -617,6 +619,15 @@ public class CaptureService extends VpnService implements Runnable { (INSTANCE.isRootCapture() == 1)); } + public static void requestBlacklistsUpdate() { + if(INSTANCE != null) { + INSTANCE.mBlacklistsUpdateRequested = true; + + // Wake the update thread to run the blacklist thread + INSTANCE.mPendingUpdates.push(new Pair<>(new ConnectionDescriptor[0], new ConnectionUpdate[0])); + } + } + // Inside the mCaptureThread @Override public void run() { @@ -652,6 +663,8 @@ public class CaptureService extends VpnService implements Runnable { ConnectionDescriptor[] new_conns = item.first; ConnectionUpdate[] conns_updates = item.second; + checkBlacklistsUpdates(); + // synchronize the conn_reg to ensure that newConnections and connectionsUpdates run atomically // thus preventing the ConnectionsAdapter from interleaving other operations synchronized (conn_reg) { @@ -744,8 +757,6 @@ public class CaptureService extends VpnService implements Runnable { } public void updateConnections(ConnectionDescriptor[] new_conns, ConnectionUpdate[] conns_updates) { - checkBlacklistsUpdates(); - // Put the update into a queue to avoid performing much work on the capture thread. // This will be processed by mConnUpdateThread. mPendingUpdates.push(new Pair<>(new_conns, conns_updates)); diff --git a/app/src/main/java/com/emanuelef/remote_capture/activities/MalwareDetection.java b/app/src/main/java/com/emanuelef/remote_capture/activities/MalwareDetection.java index ada7d7ee..08218c8d 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/activities/MalwareDetection.java +++ b/app/src/main/java/com/emanuelef/remote_capture/activities/MalwareDetection.java @@ -30,8 +30,8 @@ import androidx.viewpager2.widget.ViewPager2; import com.emanuelef.remote_capture.R; import com.emanuelef.remote_capture.fragments.EditListFragment; -import com.emanuelef.remote_capture.fragments.MalwareBlacklists; -import com.emanuelef.remote_capture.fragments.MalwareStatus; +import com.emanuelef.remote_capture.fragments.BlacklistsFragment; +import com.emanuelef.remote_capture.fragments.MalwareStatusFragment; import com.emanuelef.remote_capture.model.ListInfo; import com.google.android.material.tabs.TabLayoutMediator; @@ -65,9 +65,9 @@ public class MalwareDetection extends BaseActivity { switch (position) { default: // Deliberate fall-through to status tab case POS_STATUS: - return new MalwareStatus(); + return new MalwareStatusFragment(); case POS_BLACKLISTS: - return new MalwareBlacklists(); + return new BlacklistsFragment(); case POS_WHITELIST: return EditListFragment.newInstance(ListInfo.Type.MALWARE_WHITELIST); } diff --git a/app/src/main/java/com/emanuelef/remote_capture/fragments/MalwareBlacklists.java b/app/src/main/java/com/emanuelef/remote_capture/fragments/BlacklistsFragment.java similarity index 50% rename from app/src/main/java/com/emanuelef/remote_capture/fragments/MalwareBlacklists.java rename to app/src/main/java/com/emanuelef/remote_capture/fragments/BlacklistsFragment.java index a3f75f7e..16ae477d 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/fragments/MalwareBlacklists.java +++ b/app/src/main/java/com/emanuelef/remote_capture/fragments/BlacklistsFragment.java @@ -19,8 +19,18 @@ package com.emanuelef.remote_capture.fragments; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.ListView; @@ -28,20 +38,27 @@ import android.widget.ListView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; +import com.emanuelef.remote_capture.CaptureService; import com.emanuelef.remote_capture.PCAPdroid; import com.emanuelef.remote_capture.R; import com.emanuelef.remote_capture.adapters.BlacklistsAdapter; import com.emanuelef.remote_capture.interfaces.BlacklistsStateListener; import com.emanuelef.remote_capture.model.Blacklists; -public class MalwareBlacklists extends Fragment implements BlacklistsStateListener { - BlacklistsAdapter mAdapter; - Blacklists mBlacklists; +public class BlacklistsFragment extends Fragment implements BlacklistsStateListener { + private static final String TAG = "BlacklistsFragment"; + private BlacklistsAdapter mAdapter; + private Blacklists mBlacklists; + private MenuItem mUpdateItem; + private BroadcastReceiver mReceiver; + private Handler mHandler; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + setHasOptionsMenu(true); return inflater.inflate(R.layout.malware_detection_blacklists, container, false); } @@ -51,23 +68,70 @@ public class MalwareBlacklists extends Fragment implements BlacklistsStateListen mAdapter = new BlacklistsAdapter(view.getContext(), PCAPdroid.getInstance().getBlacklists().iter()); ListView listView = view.findViewById(R.id.listview); listView.setAdapter(mAdapter); + + mHandler = new Handler(Looper.getMainLooper()); + + mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String status = intent.getStringExtra(CaptureService.SERVICE_STATUS_KEY); + + if(status != null) + refreshStatus(); + } + }; } @Override public void onResume() { super.onResume(); mBlacklists.addOnChangeListener(this); + + LocalBroadcastManager.getInstance(requireContext()) + .registerReceiver(mReceiver, new IntentFilter(CaptureService.ACTION_SERVICE_STATUS)); } @Override public void onPause() { super.onPause(); mBlacklists.removeOnChangeListener(this); + + LocalBroadcastManager.getInstance(requireContext()) + .unregisterReceiver(mReceiver); + } + + @Override + public void onCreateOptionsMenu(@NonNull Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.blacklists_menu, menu); + mUpdateItem = menu.findItem(R.id.update); + refreshStatus(); + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + int id = item.getItemId(); + + if(id == R.id.update) { + CaptureService.requestBlacklistsUpdate(); + return true; + } + + return false; + } + + private void refreshStatus() { + if(mAdapter != null) + mAdapter.notifyDataSetChanged(); + + if(mUpdateItem != null) { + mUpdateItem.setVisible(CaptureService.isServiceActive()); + mUpdateItem.setEnabled(!mBlacklists.isUpdateInProgress()); + } } @Override public void onBlacklistsStateChanged() { - if(mAdapter != null) - mAdapter.notifyDataSetChanged(); + Log.d(TAG, "onBlacklistsStateChanged"); + mHandler.post(this::refreshStatus); } } diff --git a/app/src/main/java/com/emanuelef/remote_capture/fragments/EditListFragment.java b/app/src/main/java/com/emanuelef/remote_capture/fragments/EditListFragment.java index d4ba75c6..da475c75 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/fragments/EditListFragment.java +++ b/app/src/main/java/com/emanuelef/remote_capture/fragments/EditListFragment.java @@ -192,7 +192,7 @@ public class EditListFragment extends Fragment { return true; } - return super.onOptionsItemSelected(item); + return false; } private void recheckListSize() { diff --git a/app/src/main/java/com/emanuelef/remote_capture/fragments/MalwareStatus.java b/app/src/main/java/com/emanuelef/remote_capture/fragments/MalwareStatusFragment.java similarity index 99% rename from app/src/main/java/com/emanuelef/remote_capture/fragments/MalwareStatus.java rename to app/src/main/java/com/emanuelef/remote_capture/fragments/MalwareStatusFragment.java index e6a9a2a3..b5f41cf7 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/fragments/MalwareStatus.java +++ b/app/src/main/java/com/emanuelef/remote_capture/fragments/MalwareStatusFragment.java @@ -45,7 +45,7 @@ import com.emanuelef.remote_capture.activities.ConnectionsActivity; import com.emanuelef.remote_capture.model.Blacklists; import com.emanuelef.remote_capture.model.FilterDescriptor; -public class MalwareStatus extends Fragment { +public class MalwareStatusFragment extends Fragment { private static final String TAG = "MalwareStatus"; private Blacklists mBlacklists; private Handler mHandler; diff --git a/app/src/main/java/com/emanuelef/remote_capture/model/BlacklistDescriptor.java b/app/src/main/java/com/emanuelef/remote_capture/model/BlacklistDescriptor.java index b93a2a05..b46ac30d 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/model/BlacklistDescriptor.java +++ b/app/src/main/java/com/emanuelef/remote_capture/model/BlacklistDescriptor.java @@ -32,6 +32,7 @@ public class BlacklistDescriptor { public final String url; long mLastUpdate = 0; boolean mUpToDate = false; + boolean mUpdating = false; public boolean loaded = false; public int num_rules = 0; @@ -44,6 +45,7 @@ public class BlacklistDescriptor { public enum Status { NOT_LOADED, OUTDATED, + UPDATING, UP_TO_DATE } @@ -54,11 +56,18 @@ public class BlacklistDescriptor { this.url = url; } + public void setUpdating() { + mUpdating = true; + mUpToDate = false; + } + public void setOutdated() { + mUpdating = false; mUpToDate = false; } public void setUpdated(long now) { + mUpdating = false; mLastUpdate = now; mUpToDate = (mLastUpdate != 0); } @@ -72,6 +81,8 @@ public class BlacklistDescriptor { } public Status getStatus() { + if(mUpdating) + return Status.UPDATING; if(!loaded) return Status.NOT_LOADED; if(!mUpToDate) @@ -89,6 +100,9 @@ public class BlacklistDescriptor { case OUTDATED: id = R.string.status_outdated; break; + case UPDATING: + id = R.string.status_updating; + break; case UP_TO_DATE: id = R.string.status_uptodate; break; @@ -107,6 +121,9 @@ public class BlacklistDescriptor { case OUTDATED: id = R.color.warning; break; + case UPDATING: + id = R.color.in_progress; + break; case UP_TO_DATE: id = R.color.ok; break; diff --git a/app/src/main/java/com/emanuelef/remote_capture/model/Blacklists.java b/app/src/main/java/com/emanuelef/remote_capture/model/Blacklists.java index c94be1ea..965cb310 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/model/Blacklists.java +++ b/app/src/main/java/com/emanuelef/remote_capture/model/Blacklists.java @@ -64,17 +64,19 @@ public class Blacklists { private final ArrayList mListeners = new ArrayList<>(); private final SharedPreferences mPrefs; private final Context mContext; - long last_update; - boolean first_update; - int num_domain_rules; - int num_ip_rules; + private boolean mFirstUpdate; + private boolean mUpdateInProgress; + private long mLastUpdate; + private int mNumDomainRules; + private int mNumIPRules; public Blacklists(Context ctx) { - last_update = 0; - num_domain_rules = 0; - num_ip_rules = 0; + mLastUpdate = 0; + mNumDomainRules = 0; + mNumIPRules = 0; mContext = ctx; - first_update = true; + mFirstUpdate = true; + mUpdateInProgress = false; mPrefs = PreferenceManager.getDefaultSharedPreferences(ctx); // Domains @@ -111,9 +113,9 @@ public class Blacklists { if(!serialized.isEmpty()) { JsonObject obj = JsonParser.parseString(serialized).getAsJsonObject(); - last_update = obj.getAsJsonPrimitive("last_update").getAsLong(); - num_domain_rules = obj.getAsJsonPrimitive("num_domain_rules").getAsInt(); - num_ip_rules = obj.getAsJsonPrimitive("num_ip_rules").getAsInt(); + mLastUpdate = obj.getAsJsonPrimitive("last_update").getAsLong(); + mNumDomainRules = obj.getAsJsonPrimitive("num_domain_rules").getAsInt(); + mNumIPRules = obj.getAsJsonPrimitive("num_ip_rules").getAsInt(); JsonObject blacklists_obj = obj.getAsJsonObject("blacklists"); if(blacklists_obj != null) { // support old format @@ -144,9 +146,9 @@ public class Blacklists { } JsonObject rv = new JsonObject(); - rv.add("last_update", new JsonPrimitive(src.last_update)); - rv.add("num_domain_rules", new JsonPrimitive(src.num_domain_rules)); - rv.add("num_ip_rules", new JsonPrimitive(src.num_ip_rules)); + rv.add("last_update", new JsonPrimitive(src.mLastUpdate)); + rv.add("num_domain_rules", new JsonPrimitive(src.mNumDomainRules)); + rv.add("num_ip_rules", new JsonPrimitive(src.mNumIPRules)); rv.add("blacklists", blacklists_obj); return rv; @@ -179,7 +181,7 @@ public class Blacklists { if(!f.exists()) { // must update - last_update = 0; + mLastUpdate = 0; } } @@ -199,17 +201,19 @@ public class Blacklists { public boolean needsUpdate() { long now = System.currentTimeMillis(); - return((now - last_update) >= BLACKLISTS_UPDATE_SECONDS * 1000) - || (first_update && (getNumUpdatedBlacklists() < getNumBlacklists())); + return((now - mLastUpdate) >= BLACKLISTS_UPDATE_SECONDS * 1000) + || (mFirstUpdate && (getNumUpdatedBlacklists() < getNumBlacklists())); } + // NOTE: invoked in a separate thread (CaptureService.mBlacklistsUpdateThread) public void update() { - // NOTE: invoked in a separate thread (CaptureService.mBlacklistsUpdateThread) - if(!needsUpdate()) - return; + mUpdateInProgress = true; + for(BlacklistDescriptor bl: mLists) + bl.setUpdating(); + notifyListeners(); Log.d(TAG, "Updating " + mLists.size() + " blacklists..."); - first_update = false; + mFirstUpdate = false; for(BlacklistDescriptor bl: mLists) { Log.d(TAG, "\tupdating " + bl.fname + "..."); @@ -220,7 +224,7 @@ public class Blacklists { bl.setOutdated(); } - last_update = System.currentTimeMillis(); + mLastUpdate = System.currentTimeMillis(); notifyListeners(); } @@ -239,6 +243,7 @@ public class Blacklists { int num_loaded = 0; int num_domains = 0; int num_ips = 0; + HashSet loaded = new HashSet<>(); for(NativeBlacklistStatus bl_status: loaded_blacklists) { if(bl_status == null) @@ -249,6 +254,7 @@ public class Blacklists { // Update the number of rules bl.num_rules = bl_status.num_rules; bl.loaded = true; + loaded.add(bl.fname); if(bl.type == BlacklistDescriptor.Type.DOMAIN_BLACKLIST) num_domains += bl_status.num_rules; @@ -260,9 +266,17 @@ public class Blacklists { Log.w(TAG, "Loaded unknown blacklist " + bl_status.fname); } + for(BlacklistDescriptor bl: mLists) { + if(!loaded.contains(bl.fname)) { + Log.w(TAG, "Blacklist not loaded: " + bl.fname); + bl.loaded = false; + } + } + Log.d(TAG, "Blacklists loaded: " + num_loaded + " lists, " + num_domains + " domains, " + num_ips + " IPs"); - num_domain_rules = num_domains; - num_ip_rules = num_ips; + mNumDomainRules = num_domains; + mNumIPRules = num_ips; + mUpdateInProgress = false; notifyListeners(); } @@ -271,15 +285,15 @@ public class Blacklists { } public int getNumLoadedDomainRules() { - return num_domain_rules; + return mNumDomainRules; } public int getNumLoadedIPRules() { - return num_ip_rules; + return mNumIPRules; } public long getLastUpdate() { - return last_update; + return mLastUpdate; } public int getNumBlacklists() { @@ -309,5 +323,9 @@ public class Blacklists { public void removeOnChangeListener(BlacklistsStateListener listener) { mListeners.remove(listener); } + + public boolean isUpdateInProgress() { + return mUpdateInProgress; + } } diff --git a/app/src/main/res/drawable/ic_refresh.xml b/app/src/main/res/drawable/ic_refresh.xml new file mode 100644 index 00000000..1b826ed6 --- /dev/null +++ b/app/src/main/res/drawable/ic_refresh.xml @@ -0,0 +1,6 @@ + + + diff --git a/app/src/main/res/menu/blacklists_menu.xml b/app/src/main/res/menu/blacklists_menu.xml new file mode 100644 index 00000000..77d0c392 --- /dev/null +++ b/app/src/main/res/menu/blacklists_menu.xml @@ -0,0 +1,12 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index e3ec9ab5..0e6f8fac 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -17,5 +17,6 @@ #0CB350 #FF5722 #F80013 + #29b6f6 #000000 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2b20c3c6..2a8f40f8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -231,5 +231,7 @@ Domain rules IP rules Some blacklists are outdated + Update now + Updating…