/*
* This file is part of PCAPdroid.
*
* PCAPdroid 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.
*
* PCAPdroid 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 PCAPdroid. If not, see .
*
* Copyright 2020-21 - Emanuele Faranda
*/
package com.emanuelef.remote_capture;
import android.Manifest;
import android.app.Activity;
import android.app.Dialog;
import android.app.UiModeManager;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.ComponentName;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.ConnectivityManager;
import android.net.LinkProperties;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.Uri;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.Build;
import android.os.Environment;
import android.provider.MediaStore;
import android.provider.OpenableColumns;
import android.util.Log;
import android.view.View;
import android.widget.TableLayout;
import android.widget.TableRow;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.appcompat.widget.SearchView;
import androidx.preference.PreferenceManager;
import com.emanuelef.remote_capture.model.AppDescriptor;
import com.emanuelef.remote_capture.model.Prefs;
import com.emanuelef.remote_capture.views.AppsListView;
import java.io.File;
import java.math.BigInteger;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.UnknownHostException;
import java.nio.ByteOrder;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Locale;
public class Utils {
public static final String PCAP_HEADER = "d4c3b2a1020004000000000000000000ffff000065000000";
public static final int UID_UNKNOWN = -1;
public static final int UID_NO_FILTER = -2;
private static Boolean root_available = null;
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 static String formatPkts(long pkts) {
long divisor;
String suffix;
if(pkts < 1000) return Long.toString(pkts);
if(pkts < 1000*1000) { divisor = 1000; suffix = "K"; }
else if(pkts < 1000*1000*1000) { divisor = 1000*1000; suffix = "M"; }
else { divisor = 1000*1000*1000; suffix = "G"; }
return String.format("%.1f %s", ((float)pkts) / divisor, suffix);
}
public static String formatNumber(Context context, long num) {
Locale locale = context.getResources().getConfiguration().locale;
return String.format(locale, "%,d", num);
}
public static String formatDuration(long seconds) {
if(seconds == 0)
return "< 1 s";
else if(seconds < 60)
return String.format("%d s", seconds);
else if(seconds < 3600)
return String.format("> %d m", seconds / 60);
else
return String.format("> %d h", seconds / 3600);
}
public static String formatEpochShort(Context context, long epoch) {
long now = Utils.now();
Locale locale = context.getResources().getConfiguration().locale;
if((epoch - now) < (23 * 3600)) {
final DateFormat fmt = new SimpleDateFormat("HH:mm:ss", locale);
return fmt.format(new Date(epoch * 1000));
}
DateFormat fmt = new SimpleDateFormat("dd MMM", locale);
return fmt.format(new Date(epoch * 1000));
}
public static String formatEpochFull(Context context, long epoch) {
Locale locale = context.getResources().getConfiguration().locale;
DateFormat fmt = new SimpleDateFormat("MM/dd/yy HH:mm:ss", locale);
return fmt.format(new Date(epoch * 1000));
}
public static Configuration getLocalizedConfig(Context context) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
Configuration config = context.getResources().getConfiguration();
if(!Prefs.useEnglishLanguage(prefs))
return config;
Locale locale = new Locale("en");
Locale.setDefault(locale);
config.setLocale(locale);
return config;
}
public static void setAppTheme(String theme) {
if(theme.equals("light"))
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
else if(theme.equals("dark"))
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
else
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
}
public static String proto2str(int proto) {
switch(proto) {
case 6: return "TCP";
case 17: return "UDP";
case 1: return "ICMP";
default: return(Integer.toString(proto));
}
}
public static String getDnsServer(ConnectivityManager cm, Network net) {
LinkProperties props = cm.getLinkProperties(net);
if(props != null) {
List dns_servers = props.getDnsServers();
for(InetAddress addr : dns_servers) {
// Get the first IPv4 DNS server
if(addr instanceof Inet4Address) {
return addr.getHostAddress();
}
}
}
return null;
}
// https://gist.github.com/mathieugerard/0de2b6f5852b6b0b37ed106cab41eba1
public static String getLocalWifiIpAddress(Context context) {
WifiManager wifiManager = (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
WifiInfo connInfo = wifiManager.getConnectionInfo();
if(connInfo != null) {
int ipAddress = connInfo.getIpAddress();
if(ipAddress == 0)
return(null);
if (ByteOrder.nativeOrder().equals(ByteOrder.LITTLE_ENDIAN)) {
ipAddress = Integer.reverseBytes(ipAddress);
}
byte[] ipByteArray = BigInteger.valueOf(ipAddress).toByteArray();
String ipAddressString;
try {
ipAddressString = InetAddress.getByAddress(ipByteArray).getHostAddress();
} catch (UnknownHostException ex) {
return(null);
}
return ipAddressString;
}
return(null);
}
public static String getLocalIPAddress(Context context) {
InetAddress vpn_ip;
try {
vpn_ip = InetAddress.getByName(CaptureService.VPN_IP_ADDRESS);
} catch (UnknownHostException e) {
return "";
}
// try to get the WiFi IP address first
String wifi_ip = getLocalWifiIpAddress(context);
if((wifi_ip != null) && (!wifi_ip.equals("0.0.0.0"))) {
Log.d("getLocalIPAddress", "Using WiFi IP: " + wifi_ip);
return wifi_ip;
}
// otherwise search for other network interfaces
// https://stackoverflow.com/questions/6064510/how-to-get-ip-address-of-the-device-from-code
try {
List interfaces = Collections.list(NetworkInterface.getNetworkInterfaces());
for (NetworkInterface intf : interfaces) {
if(!intf.isVirtual()) {
List addrs = Collections.list(intf.getInetAddresses());
for (InetAddress addr : addrs) {
if (!addr.isLoopbackAddress()
&& addr.isSiteLocalAddress() /* Exclude public IPs */
&& !addr.equals(vpn_ip)) {
String sAddr = addr.getHostAddress();
if ((addr instanceof Inet4Address) && !sAddr.equals("0.0.0.0")) {
Log.d("getLocalIPAddress", "Using interface '" + intf.getName() + "' IP: " + sAddr);
return sAddr;
}
}
}
}
}
} catch (Exception ignored) { }
// Fallback
Log.d("getLocalIPAddress", "Using fallback IP");
return "127.0.0.1";
}
// returns current timestamp in seconds
public static long now() {
Calendar calendar = Calendar.getInstance();
return(calendar.getTimeInMillis() / 1000);
}
public static byte[] hexStringToByteArray(String s) {
int len = s.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
+ Character.digit(s.charAt(i+1), 16));
}
return data;
}
public static boolean hasVPNRunning(Context context) {
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
if(cm != null) {
Network[] networks = cm.getAllNetworks();
for(Network net : networks) {
NetworkCapabilities cap = cm.getNetworkCapabilities(net);
if ((cap != null) && cap.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) {
Log.d("hasVPNRunning", "detected VPN connection: " + net.toString());
return true;
}
}
}
return false;
}
public static void showToast(Context context, int id) {
String msg = context.getResources().getString(id);
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show();
}
public static void showToastLong(Context context, int id) {
String msg = context.getResources().getString(id);
Toast.makeText(context, msg, Toast.LENGTH_LONG).show();
}
public static Dialog getAppSelectionDialog(Activity activity, List appsData, AppsListView.OnSelectedAppListener listener) {
View dialogLayout = activity.getLayoutInflater().inflate(R.layout.apps_selector, null);
SearchView searchView = dialogLayout.findViewById(R.id.apps_search);
AppsListView apps = dialogLayout.findViewById(R.id.apps_list);
TextView emptyText = dialogLayout.findViewById(R.id.no_apps);
apps.setApps(appsData);
apps.setEmptyView(emptyText);
searchView.setOnQueryTextListener(apps);
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setTitle(R.string.app_filter);
builder.setView(dialogLayout);
final AlertDialog alert = builder.create();
alert.setCanceledOnTouchOutside(true);
apps.setSelectedAppListener(app -> {
listener.onSelectedApp(app);
// dismiss the dialog
alert.dismiss();
});
return alert;
}
public static String getUniqueFileName(Context context, String ext) {
Locale locale = context.getResources().getConfiguration().locale;
final DateFormat fmt = new SimpleDateFormat("dd_MMM_HH_mm_ss", locale);
return "PCAPdroid_" + fmt.format(new Date()) + "." + ext;
}
public static String getUniquePcapFileName(Context context) {
return(Utils.getUniqueFileName(context, "pcap"));
}
public static BitmapDrawable scaleDrawable(Resources res, Drawable drawable, int new_x, int new_y) {
if((new_x == 0) || (new_y == 0))
return null;
Bitmap bitmap = Bitmap.createBitmap(new_x, new_y, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);
return new BitmapDrawable(res, bitmap);
}
// Converts a TableLayout (two columns, label and value) to a string which can be copied
public static String table2Text(TableLayout table) {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < table.getChildCount(); i++) {
View v = table.getChildAt(i);
if ((v instanceof TableRow) && (v.getVisibility() == View.VISIBLE)
&& (((TableRow) v).getChildCount() == 2)) {
View label = ((TableRow) v).getChildAt(0);
View value = ((TableRow) v).getChildAt(1);
if((label instanceof TextView) && (value instanceof TextView)) {
builder.append(((TextView) label).getText());
builder.append(": ");
builder.append(((TextView) value).getText());
builder.append("\n");
}
}
}
return builder.toString();
}
public static boolean isTv(Context context) {
UiModeManager uiModeManager = (UiModeManager) context.getSystemService(Context.UI_MODE_SERVICE);
if(uiModeManager == null)
return false;
return(uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION);
}
public static String getAppVersion(Context context) {
String appver;
try {
PackageInfo pInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
String version = pInfo.versionName;
boolean isRelease = version.contains(".");
appver = isRelease ? ("v" + version) : version;
} catch (PackageManager.NameNotFoundException e) {
Log.e("Utils", "Could not retrieve package version");
appver = "";
}
return appver;
}
public static boolean supportsFileDialog(Context context, Intent intent) {
// https://commonsware.com/blog/2017/12/27/storage-access-framework-missing-action.html
ComponentName comp = intent.resolveActivity(context.getPackageManager());
return((comp != null) && (!"com.google.android.tv.frameworkpackagestubs".equals(comp.getPackageName())));
}
public static Uri getInternalStorageFile(Context context, String fname) {
ContentValues values = new ContentValues();
//values.put(MediaStore.MediaColumns.MIME_TYPE, "text/plain");
values.put(MediaStore.MediaColumns.DISPLAY_NAME, fname);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS + "/PCAPdroid");
values.put(MediaStore.MediaColumns.IS_PENDING, true); // exclusive access for long operations
} else {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if(context.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
Log.w("getInternalStorageFile", "external storage permission was denied");
return(null);
}
}
Log.d("getInternalStorageFile", Environment.getExternalStorageDirectory() + "/" + Environment.DIRECTORY_DOWNLOADS + "/" + fname);
values.put(MediaStore.MediaColumns.DATA, Environment.getExternalStorageDirectory() + "/" + Environment.DIRECTORY_DOWNLOADS + "/" + fname);
}
return context.getContentResolver().insert(
MediaStore.Files.getContentUri("external"), values);
}
public static String getUriFname(Context context, Uri uri) {
Cursor cursor;
String fname;
try {
String []projection = {OpenableColumns.DISPLAY_NAME};
cursor = context.getContentResolver().query(uri, projection, null, null, null);
} catch (Exception e) {
return null;
}
if((cursor == null) || !cursor.moveToFirst())
return null;
try {
fname = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
} finally {
cursor.close();
}
return fname;
}
public static boolean isRootAvailable() {
if(root_available == null) {
String path = System.getenv("PATH");
root_available = false;
if(path != null) {
Log.d("isRootAvailable", "PATH = " + path);
for(String part : path.split(":")) {
File f = new File(part + "/su");
if(f.exists()) {
Log.d("isRootAvailable", "'su' binary found at " + f.getAbsolutePath());
root_available = true;
break;
}
}
}
}
return root_available;
}
public static void copyToClipboard(Context ctx, String contents) {
ClipboardManager clipboard = (ClipboardManager) ctx.getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText(ctx.getString(R.string.stats), contents);
clipboard.setPrimaryClip(clip);
Utils.showToast(ctx, R.string.copied_to_clipboard);
}
}