/* This file is part of RemoteCapture. RemoteCapture is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. RemoteCapture is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with RemoteCapture. If not, see . Copyright 2019 by Emanuele Faranda */ package com.emanuelef.remote_capture; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.graphics.drawable.Drawable; import android.net.Uri; import android.net.VpnService; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import androidx.loader.app.LoaderManager; import androidx.loader.content.AsyncTaskLoader; import androidx.loader.content.Loader; import androidx.localbroadcastmanager.content.LocalBroadcastManager; import androidx.preference.PreferenceManager; import android.os.Bundle; import android.util.Log; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.widget.Button; import android.widget.TextView; import java.util.ArrayList; import java.util.List; import cat.ereza.customactivityoncrash.config.CaocConfig; public class MainActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks> { Button mStartButton; SharedPreferences mPrefs; TextView mExporterInfo; TextView mCaptureStatus; Menu mMenu; int mFilterUid; boolean mOpenAppsWhenDone; List mInstalledApps; private static final int REQUEST_CODE_VPN = 2; private static final int MENU_ITEM_APP_SELECTOR_IDX = 0; public static final int OPERATION_SEARCH_LOADER = 23; private void updateConnectStatus(boolean is_running) { if(is_running) { Log.d("Main", "VPN Running"); mStartButton.setText(R.string.stop_button); } else { Log.d("Main", "VPN NOT Running"); mStartButton.setText(R.string.start_button); } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mFilterUid = -1; mOpenAppsWhenDone = false; mInstalledApps = null; CaocConfig.Builder.create() .errorDrawable(R.drawable.ic_app_crash) .apply(); setContentView(R.layout.activity_main); mPrefs = PreferenceManager.getDefaultSharedPreferences(this); mStartButton = findViewById(R.id.button_start); mExporterInfo = findViewById(R.id.exporter_info); mCaptureStatus = findViewById(R.id.status_view); mPrefs.registerOnSharedPreferenceChangeListener(new SharedPreferences.OnSharedPreferenceChangeListener() { @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String s) { refreshExporterInfo(); } }); refreshExporterInfo(); updateConnectStatus(CaptureService.isRunning()); mStartButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Log.d("Main", "Clicked"); if(CaptureService.isRunning()) { CaptureService.stopService(); updateConnectStatus(false); mStartButton.setEnabled(false); //mCaptureStatus.setText(R.string.stopping); } else { Intent vpnPrepareIntent = VpnService.prepare(MainActivity.this); if (vpnPrepareIntent != null) { mStartButton.setEnabled(false); startActivityForResult(vpnPrepareIntent, REQUEST_CODE_VPN); } else { onActivityResult(REQUEST_CODE_VPN, RESULT_OK, null); } updateConnectStatus(true); mMenu.getItem(MENU_ITEM_APP_SELECTOR_IDX).setEnabled(false); } } }); startLoadingApps(); LocalBroadcastManager bcast_man = LocalBroadcastManager.getInstance(this); /* Register for stats update */ bcast_man.registerReceiver(new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { processStatsUpdateIntent(intent); } }, new IntentFilter(CaptureService.ACTION_TRAFFIC_STATS_UPDATE)); /* Register for service status */ bcast_man.registerReceiver(new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String status = intent.getStringExtra(CaptureService.SERVICE_STATUS_KEY); if(status.equals(CaptureService.SERVICE_STATUS_STARTED)) { mCaptureStatus.setText(formatBytes(0)); mStartButton.setEnabled(true); updateConnectStatus(true); } else if(status.equals(CaptureService.SERVICE_STATUS_STOPPED)) { mCaptureStatus.setText(R.string.ready); mStartButton.setEnabled(true); mMenu.getItem(MENU_ITEM_APP_SELECTOR_IDX).setEnabled(true); updateConnectStatus(false); } } }, new IntentFilter(CaptureService.ACTION_SERVICE_STATUS)); } private void refreshExporterInfo() { mExporterInfo.setText("UDP Collector: " + getCollectorIPPref() + ":" + getCollectorPortPref()); } private void startLoadingApps() { LoaderManager lm = LoaderManager.getInstance(this); Loader> loader = lm.getLoader(OPERATION_SEARCH_LOADER); if(loader==null) { loader = lm.initLoader(OPERATION_SEARCH_LOADER, null, this); loader.forceLoad(); } else lm.restartLoader(OPERATION_SEARCH_LOADER, null, this); } private List getInstalledApps() { PackageManager pm = getPackageManager(); List apps = new ArrayList<>(); List packs = pm.getInstalledPackages(0); // Add the "No Filter" app apps.add(new AppDescriptor("", getResources().getDrawable(android.R.drawable.ic_menu_view), getResources().getString(R.string.no_filter), -1)); for (int i = 0; i < packs.size(); i++) { PackageInfo p = packs.get(i); if((p.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0) { String packages = p.applicationInfo.packageName; if(!packages.equals("com.emanuelef.remote_capture")) { String appName = p.applicationInfo.loadLabel(pm).toString(); Drawable icon = p.applicationInfo.loadIcon(pm); int uid = p.applicationInfo.uid; apps.add(new AppDescriptor(appName, icon, packages, uid)); Log.d("APPS", appName + " - " + packages + " [" + uid + "]"); } } } return apps; } @Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater menuInflater = getMenuInflater(); menuInflater.inflate(R.menu.settings_menu, menu); mMenu = menu; return true; } private void openAppSelector() { if(mInstalledApps == null) { /* The applications loader has not finished yet. */ mOpenAppsWhenDone = true; return; } mOpenAppsWhenDone = false; AppsView apps = new AppsView(this, mInstalledApps); AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(R.string.app_filter); builder.setView(apps); final AlertDialog alert = builder.create(); apps.setSelectedAppListener(new AppsView.OnSelectedAppListener() { @Override public void onSelectedApp(AppDescriptor app) { mFilterUid = app.getUid(); // clone the drawable to avoid a "zoom-in" effect when clicked mMenu.getItem(MENU_ITEM_APP_SELECTOR_IDX).setIcon(app.getIcon().getConstantState().newDrawable()); // dismiss the dialog alert.cancel(); } }); alert.show(); } @Override public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); if (id == R.id.action_settings) { Intent intent = new Intent(MainActivity.this, SettingsActivity.class); startActivity(intent); return true; } else if(id == R.id.action_show_app_filter) { openAppSelector(); return true; } else if(id == R.id.action_about) { Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/emanuele-f/RemoteCapture")); startActivity(browserIntent); return true; } return super.onOptionsItemSelected(item); } private String getCollectorIPPref() { return(mPrefs.getString(Prefs.PREF_COLLECTOR_IP_KEY, getString(R.string.default_collector_ip))); } private String getCollectorPortPref() { return(mPrefs.getString(Prefs.PREF_COLLECTOR_PORT_KEY, getString(R.string.default_collector_port))); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if(requestCode == REQUEST_CODE_VPN && resultCode == RESULT_OK) { Intent intent = new Intent(MainActivity.this, CaptureService.class); Bundle bundle = new Bundle(); // the configuration for the VPN bundle.putString("dns_server", "8.8.8.8"); // TODO: read system DNS bundle.putString(Prefs.PREF_COLLECTOR_IP_KEY, getCollectorIPPref()); bundle.putInt(Prefs.PREF_COLLECTOR_PORT_KEY, Integer.parseInt(getCollectorPortPref())); bundle.putInt(Prefs.PREF_UID_FILTER, mFilterUid); intent.putExtra("settings", bundle); Log.d("Main", "onActivityResult -> start CaptureService"); startService(intent); //mCaptureStatus.setText(R.string.starting); } } @NonNull @Override public Loader> onCreateLoader(int id, @Nullable Bundle args) { return new AsyncTaskLoader>(this) { @Nullable @Override public List loadInBackground() { Log.d("AppsLoader", "Loading APPs..."); return getInstalledApps(); } }; } @Override public void onLoadFinished(@NonNull Loader> loader, List data) { Log.d("AppsLoader", data.size() + " APPs loaded"); mInstalledApps = data; if(mOpenAppsWhenDone) openAppSelector(); } @Override public void onLoaderReset(@NonNull Loader> loader) {} public static String formatBytes(long bytes) { long divisor; String suffix; if(bytes < 1024) return bytes + " B"; if(bytes < 1024*1024) { divisor = 1024; suffix = "KB"; } else if(bytes < 1024*1024*1024) { divisor = 1024*1024; suffix = "MB"; } else { divisor = 1024*1024*1024; suffix = "GB"; } return String.format("%.1f %s", ((float)bytes) / divisor, suffix); } public void processStatsUpdateIntent(Intent intent) { long bytes_sent = intent.getLongExtra(CaptureService.TRAFFIC_STATS_UPDATE_SENT_BYTES, 0); long bytes_rcvd = intent.getLongExtra(CaptureService.TRAFFIC_STATS_UPDATE_RCVD_BYTES, 0); int pkts_sent = intent.getIntExtra(CaptureService.TRAFFIC_STATS_UPDATE_SENT_PKTS, 0); int pkts_rcvd = intent.getIntExtra(CaptureService.TRAFFIC_STATS_UPDATE_RCVD_PKTS, 0); Log.d("MainReceiver", "Got StatsUpdate: bytes_sent=" + bytes_sent + ", bytes_rcvd=" + bytes_rcvd + ", pkts_sent=" + pkts_sent + ", pkts_rcvd=" + pkts_rcvd); mCaptureStatus.setText(formatBytes(bytes_sent + bytes_rcvd)); } }