diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fe21fc3..3da5d0f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,7 +46,7 @@ jobs: - name: Set up JDK uses: actions/setup-java@v4 with: - java-version: 17 + java-version: 21 distribution: "temurin" cache: 'gradle' diff --git a/android-env.nix b/android-env.nix new file mode 100644 index 0000000..75df4c2 --- /dev/null +++ b/android-env.nix @@ -0,0 +1,28 @@ +# see https://github.com/tadfisher/android-nixpkgs +# run with `nix-shell android-env.nix` +{ pkgs ? import { config.android_sdk.accept_license = true; } }: + +let + android-nixpkgs = pkgs.callPackage { + # Default; can also choose "beta", "preview", or "canary". + channel = "stable"; + }; + + androidSdk = android-nixpkgs.sdk (sdkPkgs: with sdkPkgs; [ + cmdline-tools-latest + build-tools-34-0-0 + platform-tools + platforms-android-34 + # emulator + ]); + +in +pkgs.mkShell { + buildInputs = with pkgs; [ + androidSdk + glibc + ]; + + # Override the aapt2 that gradle uses with the nix-shipped version + GRADLE_OPTS = "-Dorg.gradle.project.android.aapt2FromMavenOverride=${androidSdk}/share/android-sdk/build-tools/34.0.0/aapt2"; +} diff --git a/app/build.gradle b/app/build.gradle index 5ee6801..154f93c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,12 +1,14 @@ apply plugin: 'com.android.application' android { - compileSdkVersion 33 + compileSdkVersion 34 + namespace 'us.spotco.malwarescanner' + defaultConfig { applicationId "org.maintainteam.hypatia" resValue "string", "app_name", "Hypatia" - minSdkVersion 16 - targetSdkVersion 32 + minSdkVersion 21 + targetSdkVersion 34 versionCode 314 versionName "3.14" resConfigs 'en', 'ar', 'bg', 'cs', 'de', 'es', 'et', 'fi', 'fr', 'gl', 'hr', 'in', 'it', 'ja', 'pt', 'pt-rBR', 'ro', 'ru', 'sk', 'ta', 'tr', 'uk', 'zh-rCN', 'zh-rTW' @@ -28,7 +30,6 @@ android { resValue "string", "app_name", "Hypatia " + workingBranch } - minifyEnabled true zipAlignEnabled true } @@ -49,12 +50,21 @@ android { exclude 'org/bouncycastle/x509/CertPathReviewerMessages.properties' exclude 'org/bouncycastle/x509/CertPathReviewerMessages_de.properties' } + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + encoding 'utf-8' + } + buildFeatures { + buildConfig = true + } } dependencies { implementation 'commons-io:commons-io:2.5' implementation 'org.bouncycastle:bcpg-jdk15to18:1.77' implementation 'com.google.guava:guava:33.0.0-jre' + implementation 'androidx.appcompat:appcompat:1.6.1' } static String getGitWorkingBranch() { diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 95c6362..2ada171 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -24,3 +24,6 @@ # Disable obfuscation -dontobfuscate + +-keep class androidx.appcompat.app.** { *; } +-keep class androidx.appcompat.widget.** { *; } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 278356d..5e25eac 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,7 +1,6 @@ + xmlns:tools="http://schemas.android.com/tools"> @@ -24,24 +23,26 @@ + android:theme="@style/Theme.AppCompat.DayNight.NoActionBar"> @@ -51,6 +52,15 @@ + + + + + + + signatureDatabases) { initDatabase(context); - log.append(context.getString(R.string.main_database_updating, String.valueOf(signatureDatabases.size())) + "\n"); + log.append( + context.getString(R.string.main_database_updating, String.valueOf(signatureDatabases.size())) + "\n"); if (!Utils.getDatabaseURL(context).equals(Utils.DATABASE_URL_DEFAULT)) { log.append(context.getString(R.string.main_database_override, Utils.getDatabaseURL(context)) + "\n"); } changedDownload = false; boolean onionRouting = prefs.getBoolean("ONION_ROUTING", false); - new Downloader().executeOnExecutor(Utils.getThreadPoolExecutor(), onionRouting, Utils.getDatabaseURL(context) + "gpg.key", databasePath + "/gpg.key", Utils.getDatabaseURL(context)); + new Downloader().executeOnExecutor(Utils.getThreadPoolExecutor(), onionRouting, + Utils.getDatabaseURL(context) + "gpg.key", databasePath + "/gpg.key", Utils.getDatabaseURL(context)); for (SignatureDatabase signatureDatabase : signatureDatabases) { - new Downloader().executeOnExecutor(Utils.getThreadPoolExecutor(), onionRouting, signatureDatabase.getUrl(), databasePath + "/" + signatureDatabase.getName(), signatureDatabase.getBaseUrl()); - new Downloader().executeOnExecutor(Utils.getThreadPoolExecutor(), onionRouting, signatureDatabase.getUrl() + ".sig", databasePath + "/" + signatureDatabase.getName() + ".sig", signatureDatabase.getBaseUrl()); + new Downloader().executeOnExecutor(Utils.getThreadPoolExecutor(), onionRouting, signatureDatabase.getUrl(), + databasePath + "/" + signatureDatabase.getName(), signatureDatabase.getBaseUrl()); + new Downloader().executeOnExecutor(Utils.getThreadPoolExecutor(), onionRouting, + signatureDatabase.getUrl() + ".sig", databasePath + "/" + signatureDatabase.getName() + ".sig", + signatureDatabase.getBaseUrl()); } } @@ -122,13 +129,15 @@ private static void initDatabase(Context context) { } } - public static void loadDatabase(Context context, boolean forceReload, ConcurrentLinkedQueue signatureDatabases) { + public static void loadDatabase(Context context, boolean forceReload, + ConcurrentLinkedQueue signatureDatabases) { if ((!isDatabaseLoaded() || forceReload) && !databaseCurrentlyLoading) { databaseFullyLoaded = false; databaseCurrentlyLoading = true; initDatabase(context); signaturesCount = 0; changedConfig = false; + changedUri = false; signaturesMD5Extended = null; domains = null; File publicKey = new File(databasePath + "/gpg.key"); @@ -141,44 +150,64 @@ public static void loadDatabase(Context context, boolean forceReload, Concurrent try { boolean validated = verifier.verify(databaseLocation, databaseSigLocation, publicKey); if (validated) { - Log.d("Hypatia", "Successfully validated database: " + databaseLocation.getName()); + HypatiaLogger.Log("Successfully validated database: " + databaseLocation.getName(), + LogType.DEBUG); FileInputStream databaseLoading = new FileInputStream(databaseLocation); switch (databaseLocation.getName()) { case "hypatia-md5-bloom.bin": - Log.d("Hypatia", "Processing md5"); - signaturesMD5 = BloomFilter.readFrom(databaseLoading, Funnels.stringFunnel(Charsets.US_ASCII)); + HypatiaLogger.Log("Processing md5", LogType.DEBUG); + signaturesMD5 = BloomFilter.readFrom(databaseLoading, + Funnels.stringFunnel(Charsets.US_ASCII)); signaturesCount += signaturesMD5.approximateElementCount(); - Log.d("Hypatia", "Loaded md5 with " + signaturesMD5.approximateElementCount() + " entries"); + HypatiaLogger.Log( + "Loaded md5 with " + signaturesMD5.approximateElementCount() + " entries", + LogType.DEBUG); break; case "hypatia-sha1-bloom.bin": - Log.d("Hypatia", "Processing sha1"); - signaturesSHA1 = BloomFilter.readFrom(databaseLoading, Funnels.stringFunnel(Charsets.US_ASCII)); + HypatiaLogger.Log("Processing sha1", LogType.DEBUG); + signaturesSHA1 = BloomFilter.readFrom(databaseLoading, + Funnels.stringFunnel(Charsets.US_ASCII)); signaturesCount += signaturesSHA1.approximateElementCount(); - Log.d("Hypatia", "Loaded sha1 with " + signaturesSHA1.approximateElementCount() + " entries"); + HypatiaLogger.Log( + "Loaded sha1 with " + signaturesSHA1.approximateElementCount() + " entries", + LogType.DEBUG); break; case "hypatia-sha256-bloom.bin": - Log.d("Hypatia", "Processing sha256"); - signaturesSHA256 = BloomFilter.readFrom(databaseLoading, Funnels.stringFunnel(Charsets.US_ASCII)); + HypatiaLogger.Log("Processing sha256", LogType.DEBUG); + signaturesSHA256 = BloomFilter.readFrom(databaseLoading, + Funnels.stringFunnel(Charsets.US_ASCII)); signaturesCount += signaturesSHA256.approximateElementCount(); - Log.d("Hypatia", "Loaded sha256 with " + signaturesSHA256.approximateElementCount() + " entries"); + HypatiaLogger.Log("Loaded sha256 with " + signaturesSHA256.approximateElementCount() + + " entries", LogType.DEBUG); break; case "hypatia-md5-extended-bloom.bin": - Log.d("Hypatia", "Processing md5 extended"); - signaturesMD5Extended = BloomFilter.readFrom(databaseLoading, Funnels.stringFunnel(Charsets.US_ASCII)); + HypatiaLogger.Log("Processing md5 extended", LogType.DEBUG); + signaturesMD5Extended = BloomFilter.readFrom(databaseLoading, + Funnels.stringFunnel(Charsets.US_ASCII)); signaturesCount += signaturesMD5Extended.approximateElementCount(); - Log.d("Hypatia", "Loaded md5 extended with " + signaturesMD5Extended.approximateElementCount() + " entries"); + HypatiaLogger.Log( + "Loaded md5 extended with " + + signaturesMD5Extended.approximateElementCount() + " entries", + LogType.DEBUG); break; case "hypatia-domains-bloom.bin": - Log.d("Hypatia", "Processing domains"); - domains = BloomFilter.readFrom(databaseLoading, Funnels.stringFunnel(Charsets.US_ASCII)); - Log.d("Hypatia", "Loaded domains with " + domains.approximateElementCount() + " entries"); + HypatiaLogger.Log("Processing domains", LogType.DEBUG); + domains = BloomFilter.readFrom(databaseLoading, + Funnels.stringFunnel(Charsets.US_ASCII)); + HypatiaLogger.Log( + "Loaded domains with " + domains.approximateElementCount() + " entries", + LogType.DEBUG); break; } databaseLoading.close(); } else { - Log.w("Hypatia", "Failed to validate database"); + HypatiaLogger.Log("Failed to validate database, please double check signing key", + LogType.WARNING); } } catch (Exception e) { + HypatiaLogger.Log( + "There was an error while checking database validation, please double check signing key", + LogType.WARNING); e.printStackTrace(); } } @@ -226,37 +255,82 @@ protected String doInBackground(Object... objects) { connection.setReadTimeout(30000); connection.addRequestProperty("User-Agent", "Hypatia"); String lastModifiedLocal = ""; + Date lastModifLocalDate = new Date(out.lastModified()); if (out.exists()) { connection.setIfModifiedSince(out.lastModified()); - lastModifiedLocal = Utils.getContext().getString(R.string.main_database_not_modified_since, dateFormat.format(new Date(out.lastModified()))); + lastModifiedLocal = Utils.getContext().getString(R.string.main_database_not_modified_since, + dateFormat.format(lastModifLocalDate)); } connection.connect(); - String lastModifiedServer = dateFormat.format(new Date(connection.getLastModified())); + Date lastModifServerDate = new Date(connection.getLastModified()); + String lastModifiedServer = dateFormat.format(lastModifServerDate); int res = connection.getResponseCode(); + HypatiaLogger.Log( + "S:" + dateFormat.format(lastModifServerDate) + " - L:" + dateFormat.format(lastModifLocalDate) + + " | resCode: " + res, + LogType.DEBUG); + + // REVIEW these lines + if (res == 304 && dateFormat.format(lastModifServerDate).equalsIgnoreCase("Jan 1, 1970")) { + lastModifServerDate = lastModifLocalDate; + HypatiaLogger.Log( + "res code 304 and last modified server date Jan 1, 1970. Last modified server date has been updated to local date", + LogType.DEBUG); + } if (res != 304) { if (res == 200) { - FileOutputStream fileOutputStream = new FileOutputStream(outNew); - - final byte[] data = new byte[1024]; - int count; - while ((count = connection.getInputStream().read(data, 0, 1024)) != -1) { - fileOutputStream.write(data, 0, count); + if (changedUri) { + HypatiaLogger.Log( + "Downloading databases regarding of servers right/wrong answer & file dates because Uri has been changed recently", + LogType.DEBUG); + downloadNewDatabasesFile(connection, out, outNew); + publishProgress(url.replaceAll(baseURL, "") + + "\n\t" + Utils.getContext().getString(R.string.main_database_download_success) + + "\n\t" + + Utils.getContext().getString(R.string.main_database_released_on, + lastModifiedServer) + + "\n"); + } else if (!(lastModifServerDate.after(lastModifLocalDate))) { + HypatiaLogger.Log("Server replied with 200 even sources are not newer", LogType.DEBUG); + publishProgress(url.replaceAll(baseURL, "") + + "\n\t" + Utils.getContext().getString(R.string.main_database_not_changed) + " " + + lastModifiedLocal + "\n"); + } else { + downloadNewDatabasesFile(connection, out, outNew); + publishProgress(url.replaceAll(baseURL, "") + + "\n\t" + Utils.getContext().getString(R.string.main_database_download_success) + + "\n\t" + + Utils.getContext().getString(R.string.main_database_released_on, + lastModifiedServer) + + "\n"); } - - fileOutputStream.close(); - outNew.renameTo(out); //Move the new file into place - changedDownload = true; - - publishProgress(url.replaceAll(baseURL, "") - + "\n\t" + Utils.getContext().getString(R.string.main_database_download_success) - + "\n\t" + Utils.getContext().getString(R.string.main_database_released_on, lastModifiedServer) + "\n"); } else { publishProgress(url.replaceAll(baseURL, "") - + "\n\t" + Utils.getContext().getString(R.string.main_database_download_error, String.valueOf(res)) + "\n"); + + "\n\t" + Utils.getContext().getString(R.string.main_database_download_error, + String.valueOf(res)) + + "\n"); } + } else if (lastModifServerDate.after(lastModifLocalDate)) { + HypatiaLogger.Log("Server replied with 304 even sources are newer", LogType.DEBUG); + downloadNewDatabasesFile(connection, out, outNew); + publishProgress(url.replaceAll(baseURL, "") + + "\n\t" + Utils.getContext().getString(R.string.main_database_download_success) + + "\n\t" + + Utils.getContext().getString(R.string.main_database_released_on, lastModifiedServer) + + "\n"); + } else if (changedUri) { + HypatiaLogger.Log("Maybe not changed but reloading because of database uri changed recently", + LogType.DEBUG); + downloadNewDatabasesFile(connection, out, outNew); + publishProgress(url.replaceAll(baseURL, "") + + "\n\t" + Utils.getContext().getString(R.string.main_database_download_success) + + "\n\t" + + Utils.getContext().getString(R.string.main_database_released_on, lastModifiedServer) + + "\n"); } else { publishProgress(url.replaceAll(baseURL, "") - + "\n\t" + Utils.getContext().getString(R.string.main_database_not_changed) + " " + lastModifiedLocal + "\n"); + + "\n\t" + Utils.getContext().getString(R.string.main_database_not_changed) + " " + + lastModifiedLocal + "\n"); } connection.disconnect(); } catch (Exception e) { @@ -276,14 +350,35 @@ protected final void onProgressUpdate(String... progress) { } } + public static void downloadNewDatabasesFile(HttpURLConnection connection, File out, File outNew) + throws IOException { + try { + FileOutputStream fileOutputStream = new FileOutputStream(outNew); + + final byte[] data = new byte[1024]; + int count; + while ((count = connection.getInputStream().read(data, 0, 1024)) != -1) { + fileOutputStream.write(data, 0, count); + } + + fileOutputStream.close(); + outNew.renameTo(out); // Move the new file into place + changedDownload = true; + } catch (IOException e) { + throw e; + } + + } + public static boolean selfTest() { if (signaturesMD5 != null && signaturesSHA1 != null && signaturesSHA256 != null) { - return signaturesMD5.mightContain("903616d0dbe074aa363d2d49c03f7362") //HypatiaHypatiaHypatia - && signaturesMD5.mightContain("faf325d9d4b2a6c9457405eb31870b22") //HypatiaHypatiaHypatia-MD5 - && signaturesSHA1.mightContain("fc4a3e802894cc2229be77ec6f082d1aab744e54") //HypatiaHypatiaHypatia - && signaturesSHA1.mightContain("04445ab9332cdca0031184165eb88a0a9dcec4ed") //HypatiaHypatiaHypatia-SHA1 - && signaturesSHA256.mightContain("df44844a0e99ddd935e8419257440a2ca7ef3243435a67416fcbb6cd3ae560c3") //HypatiaHypatiaHypatia - && signaturesSHA256.mightContain("3be403a831795482e82e2f9bc3e57ca34744aca3756e297b056b9f9c41830eaf"); //HypatiaHypatiaHypatia-SHA256 + return signaturesMD5.mightContain("903616d0dbe074aa363d2d49c03f7362") // HypatiaHypatiaHypatia + && signaturesMD5.mightContain("faf325d9d4b2a6c9457405eb31870b22") // HypatiaHypatiaHypatia-MD5 + && signaturesSHA1.mightContain("fc4a3e802894cc2229be77ec6f082d1aab744e54") // HypatiaHypatiaHypatia + && signaturesSHA1.mightContain("04445ab9332cdca0031184165eb88a0a9dcec4ed") // HypatiaHypatiaHypatia-SHA1 + && signaturesSHA256.mightContain("df44844a0e99ddd935e8419257440a2ca7ef3243435a67416fcbb6cd3ae560c3") // HypatiaHypatiaHypatia + && signaturesSHA256 + .mightContain("3be403a831795482e82e2f9bc3e57ca34744aca3756e297b056b9f9c41830eaf"); // HypatiaHypatiaHypatia-SHA256 } return false; } diff --git a/app/src/main/java/us/spotco/malwarescanner/EventReceiver.java b/app/src/main/java/us/spotco/malwarescanner/EventReceiver.java index 13efbc7..d13a3a8 100644 --- a/app/src/main/java/us/spotco/malwarescanner/EventReceiver.java +++ b/app/src/main/java/us/spotco/malwarescanner/EventReceiver.java @@ -60,7 +60,8 @@ private static void scanApp(Context context, String appID) { e.printStackTrace(); } if (filesToScan.size() > 0) { - new MalwareScanner(null, context, false).executeOnExecutor(Utils.getThreadPoolExecutor(), filesToScan); + MalwareScanner.isCache = false; + new MalwareScanner(context, false).executeOnExecutor(Utils.getThreadPoolExecutor(), filesToScan); } } } diff --git a/app/src/main/java/us/spotco/malwarescanner/Hypatia.java b/app/src/main/java/us/spotco/malwarescanner/Hypatia.java new file mode 100644 index 0000000..d0fb306 --- /dev/null +++ b/app/src/main/java/us/spotco/malwarescanner/Hypatia.java @@ -0,0 +1,64 @@ +/* +Hypatia: A realtime malware scanner for Android +Copyleft (🄯) 2025 MaintainTeam Organization + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program 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 Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ +package us.spotco.malwarescanner; + +import android.app.Application; +import android.content.Context; +import android.content.Intent; + +import android.widget.TextView; +import androidx.annotation.NonNull; +import java.lang.ref.WeakReference; +import android.util.Log; + +public class Hypatia extends Application { + private WeakReference mainActivityRef; + private static Hypatia instance; + + @Override + public void onCreate() { + super.onCreate(); + instance = this; + } + + public static Hypatia getInstance() { + return instance; + } + + public void setMainActivity(@NonNull MainActivity activity) { + mainActivityRef = new WeakReference<>(activity); + if (activity != null) { + HypatiaLogger.getInstance().setLogView(activity.getLogView()); + } else { + Log.e("Hypatia", "COULDN'T START HYPATIA LOGGER"); + } + } + + public void ensureMainActivity() { + if (mainActivityRef == null || mainActivityRef.get() == null) { + Intent intent = new Intent(this, MainActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); + startActivity(intent); + } + } + + // REVIEW Is it really needed ??? + public static Context getAppContext() { + return instance.getApplicationContext(); + } +} \ No newline at end of file diff --git a/app/src/main/java/us/spotco/malwarescanner/HypatiaLogger.java b/app/src/main/java/us/spotco/malwarescanner/HypatiaLogger.java new file mode 100644 index 0000000..260153e --- /dev/null +++ b/app/src/main/java/us/spotco/malwarescanner/HypatiaLogger.java @@ -0,0 +1,185 @@ +/* +Hypatia: A realtime malware scanner for Android +Copyleft (🄯) 2025 MaintainTeam Organization + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program 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 Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package us.spotco.malwarescanner; + +import android.content.Context; + +import android.os.Build; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; + +import android.text.method.ScrollingMovementMethod; +import android.widget.TextView; + +import java.lang.ref.WeakReference; +import java.util.concurrent.ConcurrentLinkedQueue; +import android.util.Log; + +import androidx.annotation.NonNull; + +/** + * Log Types + */ +enum LogType { + VERYVERBOSE, + VERBOSE, + DEBUG, + NONEWLINE, + ONLYNEWLINE, + DEFAULT, + INFO, + WARNING, + ERROR; +} +// TODO add more DEBUG types??: DEBUG-ERROR, DEBUG-WARNING etc? + +/** + * Logs process outputs to user interface + * TODO use a separate log page to list this logs, not MainActivity + * + * @author Aliberk Sandıkçı + * + */ +public class HypatiaLogger { + + private static HypatiaLogger instance; + private final ConcurrentLinkedQueue logQueue = new ConcurrentLinkedQueue<>(); + private WeakReference logViewRef; + private final Handler uiHandler = new Handler(Looper.getMainLooper()); + + /** + * Logs process outputs to user interface + * prints out also verbose/debug if app is debug version + * + * @param content log content + * @param type log type + */ + public static void Log(String content, LogType type) { + Boolean userFacing = true; + + // toggle these to show/hide DEBUG/VERBOSE and VERYVERBOSE in debug app builds + Boolean ENABLE_DEBUG = false; + Boolean ACTIVATE_VERYVERBOSE = false; + switch (type) { + case VERYVERBOSE: + if (ACTIVATE_VERYVERBOSE) { + android.util.Log.v("Hypatia Logger", content); + } + break; + case VERBOSE: + android.util.Log.v("Hypatia Logger", content); + content = "VERBOSE: " + content; + if (!BuildConfig.DEBUG || !ENABLE_DEBUG) { + userFacing = false; + } + break; + case DEBUG: + android.util.Log.d("Hypatia Logger", content); + content = "DEBUG: " + content; + if (!BuildConfig.DEBUG || !ENABLE_DEBUG) { + userFacing = false; + } + break; + case INFO: + android.util.Log.i("Hypatia Logger", content); + content = "INFO: " + content; + break; + case WARNING: + android.util.Log.w("Hypatia Logger", content); + content = "WARNING: " + content; + break; + case ERROR: + android.util.Log.e("Hypatia Logger", content); + content = "ERROR: " + content; + break; + default: + android.util.Log.d("Hypatia Logger", content); + break; + } + try { + if (userFacing && (type != LogType.VERYVERBOSE)) { + HypatiaLogger.getInstance().logUtil(content + (type == LogType.NONEWLINE ? "" : "\n")); + } + } catch (Exception e) { + Log.d("Hypatia LOGGER", "LOGGER DO NOT WORK FOR SOME REASON !!!"); + e.printStackTrace(); + } + + } + + /** + * Overloader of actual {@code Log()} function + * + * @see #Log(String, LogType) + * @param content log content + */ + public static void Log(String content) { + Log(content, LogType.DEFAULT); + } + + /* + * ////////////////////////////////////////////////////////////////////////// + * Utilities to ensure Logger working correctly and getting logs from + * multiple sources + * ////////////////////////////////////////////////////////////////////////// + */ + + private HypatiaLogger() { + } + + public static HypatiaLogger getInstance() { + if (instance == null) { + synchronized (HypatiaLogger.class) { + if (instance == null) { + instance = new HypatiaLogger(); + } + } + } + return instance; + } + + public void setLogView(TextView textView) { + logViewRef = new WeakReference<>(textView); + flushQueue(); + } + + private void checkMainActivity() { + if (logViewRef == null || logViewRef.get() == null) { + Hypatia.getInstance().ensureMainActivity(); + } + } + + public void logUtil(String message) { + logQueue.add(message); + uiHandler.post(this::flushQueue); + checkMainActivity(); + } + + private void flushQueue() { + if (logViewRef != null && logViewRef.get() != null) { + TextView tv = logViewRef.get(); + while (!logQueue.isEmpty()) { + String log = logQueue.poll(); + tv.append(log); + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/us/spotco/malwarescanner/MainActivity.java b/app/src/main/java/us/spotco/malwarescanner/MainActivity.java index 39c6b02..b51848e 100644 --- a/app/src/main/java/us/spotco/malwarescanner/MainActivity.java +++ b/app/src/main/java/us/spotco/malwarescanner/MainActivity.java @@ -1,6 +1,7 @@ /* Hypatia: A realtime malware scanner for Android Copyright (c) 2017-2018 Divested Computing Group +Copyleft (🄯) 2025 MaintainTeam Organization This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by @@ -35,24 +36,27 @@ import android.os.PowerManager; import android.provider.Settings; import android.text.InputType; -import android.text.method.ScrollingMovementMethod; import android.util.Log; +import android.text.method.ScrollingMovementMethod; import android.view.Menu; import android.view.MenuItem; import android.view.WindowManager; import android.widget.EditText; import android.widget.TextView; + import android.widget.Toast; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; + import java.io.File; import java.util.HashSet; -public class MainActivity extends Activity { +public class MainActivity extends AppCompatActivity { private SharedPreferences prefs = null; private MalwareScanner malwareScanner = null; - private TextView logView; private Menu menu; private static final String buildVersionName = BuildConfig.VERSION_NAME; @@ -62,37 +66,68 @@ public class MainActivity extends Activity { private boolean scanInternal = true; private boolean scanExternal = false; + private TextView logView; + private static final int REQUEST_PERMISSION_EXTERNAL_STORAGE = 0; @Override protected final void onCreate(Bundle savedInstanceState) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - setTheme(android.R.style.Theme_DeviceDefault_DayNight); - } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { getWindow().clearFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); } super.onCreate(savedInstanceState); - Utils.setContext(getApplicationContext()); + Utils.setContext(Hypatia.getAppContext()); setContentView(R.layout.content_main); + Toolbar toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + logView = findViewById(R.id.txtLogOutput); logView.setMovementMethod(new ScrollingMovementMethod()); logView.setTextIsSelectable(true); - logView.append(getString(R.string.app_copyright) + "\n"); - logView.append(getString(R.string.app_license) + "\n"); - logView.append(getString(R.string.app_version, buildVersionName) + "\n\n"); - - logView.append(Utils.checkOldDatabase(this) + "\n"); + HypatiaLogger.getInstance().setLogView(logView); - malwareScanner = new MalwareScanner(this, this, true); + HypatiaLogger.Log(getString(R.string.app_disclaimer) + "\n\n\n"); + HypatiaLogger.Log(getString(R.string.app_copyright)); + HypatiaLogger.Log(getString(R.string.app_license)); + HypatiaLogger.Log(getString(R.string.app_version, buildVersionName)); + + HypatiaLogger.Log(Utils.checkOldDatabase(this)); + + malwareScanner = new MalwareScanner(this, true); prefs = getSharedPreferences(BuildConfig.APPLICATION_ID, Context.MODE_PRIVATE); requestPermissions(); Utils.considerStartService(this); + + handleIntent(getIntent()); + } + + public TextView getLogView() { + return logView; + } + + @Override + protected final void onNewIntent(Intent intent) { + super.onNewIntent(intent); + + handleIntent(intent); + } + + private void handleIntent(Intent intent) { + if (intent != null && intent.hasExtra("SHARE_SCAN_RESULT")) { + String result = intent.getStringExtra("SHARE_SCAN_RESULT"); + HypatiaLogger.Log(result, LogType.INFO); + } + } + + @Override + protected void onDestroy() { + Hypatia.getInstance().setMainActivity(null); + super.onDestroy(); } @Override @@ -110,10 +145,12 @@ public final boolean onCreateOptionsMenu(Menu menu) { private void requestPermissions() { if (SDK_INT >= Build.VERSION_CODES.M) { if (checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { - requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, REQUEST_PERMISSION_EXTERNAL_STORAGE); + requestPermissions(new String[] { Manifest.permission.READ_EXTERNAL_STORAGE }, + REQUEST_PERMISSION_EXTERNAL_STORAGE); } if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { - requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_PERMISSION_EXTERNAL_STORAGE); + requestPermissions(new String[] { Manifest.permission.WRITE_EXTERNAL_STORAGE }, + REQUEST_PERMISSION_EXTERNAL_STORAGE); } } if (SDK_INT >= Build.VERSION_CODES.R) { @@ -131,7 +168,7 @@ private void showCredits() { AlertDialog.Builder creditsBuilder = new AlertDialog.Builder(this); creditsBuilder.setTitle(getString(R.string.lblFullCredits)); creditsBuilder.setItems(R.array.fullCredits, (dialog, which) -> { - //do nothing + // do nothing }); creditsDialog = creditsBuilder.create(); creditsDialog.show(); @@ -157,13 +194,13 @@ public final boolean onOptionsItemSelected(MenuItem item) { break; case R.id.mnuUpdateDatabase: if (Database.hasDownloadsRunning()) { - logView.append(getString(R.string.lblUpdateRunning) + "\n"); + HypatiaLogger.Log(getString(R.string.lblUpdateRunning)); } else if (malwareScanner.running) { - logView.append(getString(R.string.lblScanRunning) + "\n"); + HypatiaLogger.Log(getString(R.string.lblScanRunning)); } else if (!Utils.isNetworkAvailable(this)) { - logView.append(getString(R.string.lblNoNetwork) + "\n"); + HypatiaLogger.Log(getString(R.string.lblNoNetwork)); } else if (Database.isDatabaseLoading()) { - logView.append(getString(R.string.lblDatabaseLoading) + "\n"); + HypatiaLogger.Log(getString(R.string.lblDatabaseLoading)); } else if (Utils.isConnectionMetered(this)) { int amt = prefs.getBoolean("SIGNATURES_EXTENDED", false) ? 200 : 50; new AlertDialog.Builder(this) @@ -182,9 +219,9 @@ public final boolean onOptionsItemSelected(MenuItem item) { break; case R.id.mnuDatabaseServer: if (Database.hasDownloadsRunning()) { - logView.append(getString(R.string.lblUpdateRunning) + "\n"); + HypatiaLogger.Log(getString(R.string.lblUpdateRunning) + "\n"); } else if (Database.isDatabaseLoading()) { - logView.append(getString(R.string.lblDatabaseLoading) + "\n"); + HypatiaLogger.Log(getString(R.string.lblDatabaseLoading) + "\n"); } else { AlertDialog.Builder builderServerOverride = new AlertDialog.Builder(this); builderServerOverride.setTitle(getString(R.string.lblDatabaseServer)); @@ -197,14 +234,17 @@ public final boolean onOptionsItemSelected(MenuItem item) { if (!newServer.endsWith("/")) { newServer += "/"; } + Database.changedUri = true; prefs.edit().putString("DATABASE_SERVER", newServer).apply(); - logView.append(Utils.checkOldDatabase(this) + "\n"); + HypatiaLogger.Log(Utils.checkOldDatabase(this) + "\n"); }); builderServerOverride.setNegativeButton(getString(R.string.lblResetPrimary), (dialog, which) -> { + Database.changedUri = true; prefs.edit().putString("DATABASE_SERVER", Utils.DATABASE_URL_DEFAULT).apply(); dialog.cancel(); }); builderServerOverride.setNeutralButton(getString(R.string.lblResetGitHub), (dialog, which) -> { + Database.changedUri = true; prefs.edit().putString("DATABASE_SERVER", Utils.DATABASE_URL_GITHUB).apply(); dialog.cancel(); }); @@ -213,9 +253,9 @@ public final boolean onOptionsItemSelected(MenuItem item) { break; case R.id.mnuSigningKey: if (Database.hasDownloadsRunning()) { - logView.append(getString(R.string.lblUpdateRunning) + "\n"); + HypatiaLogger.Log(getString(R.string.lblUpdateRunning) + "\n"); } else if (Database.isDatabaseLoading()) { - logView.append(getString(R.string.lblDatabaseLoading) + "\n"); + HypatiaLogger.Log(getString(R.string.lblDatabaseLoading) + "\n"); } else { AlertDialog.Builder builderKey = new AlertDialog.Builder(this); builderKey.setTitle(getString(R.string.lblSigningKey)); @@ -225,7 +265,7 @@ public final boolean onOptionsItemSelected(MenuItem item) { builderKey.setView(inputKey); builderKey.setPositiveButton(getString(R.string.lblOverride), (dialog, which) -> { prefs.edit().putString("SIGNING_KEY", inputKey.getText().toString()).apply(); - logView.append(Utils.checkOldDatabase(this) + "\n"); + HypatiaLogger.Log(Utils.checkOldDatabase(this) + "\n"); }); builderKey.setNegativeButton(getString(R.string.lblReset), (dialog, which) -> { prefs.edit().putString("SIGNING_KEY", Utils.SIGNING_KEY_DEFAULT).apply(); @@ -236,9 +276,9 @@ public final boolean onOptionsItemSelected(MenuItem item) { break; case R.id.toggleExtended: if (Database.hasDownloadsRunning()) { - logView.append(getString(R.string.lblUpdateRunning) + "\n"); + HypatiaLogger.Log(getString(R.string.lblUpdateRunning) + "\n"); } else if (Database.isDatabaseLoading()) { - logView.append(getString(R.string.lblDatabaseLoading) + "\n"); + HypatiaLogger.Log(getString(R.string.lblDatabaseLoading) + "\n"); } else { boolean prevExtended = prefs.getBoolean("SIGNATURES_EXTENDED", false); new AlertDialog.Builder(this) @@ -264,9 +304,9 @@ public final boolean onOptionsItemSelected(MenuItem item) { break; case R.id.toggleLinkScanner: if (Database.hasDownloadsRunning()) { - logView.append(getString(R.string.lblUpdateRunning) + "\n"); + HypatiaLogger.Log(getString(R.string.lblUpdateRunning) + "\n"); } else if (Database.isDatabaseLoading()) { - logView.append(getString(R.string.lblDatabaseLoading) + "\n"); + HypatiaLogger.Log(getString(R.string.lblDatabaseLoading) + "\n"); } else { boolean prevDomains = prefs.getBoolean("DOMAINS", false); new AlertDialog.Builder(this) @@ -279,7 +319,8 @@ public final boolean onOptionsItemSelected(MenuItem item) { if (!prevDomains) { Database.changedConfig = true; } - startActivityForResult(new Intent(android.provider.Settings.ACTION_ACCESSIBILITY_SETTINGS), 0); + startActivityForResult( + new Intent(android.provider.Settings.ACTION_ACCESSIBILITY_SETTINGS), 0); }) .setNegativeButton(getString(android.R.string.no), (dialog, which) -> { prefs.edit().putBoolean("DOMAINS", false).apply(); @@ -293,9 +334,9 @@ public final boolean onOptionsItemSelected(MenuItem item) { break; case R.id.toggleRealtime: if (malwareScanner.running) { - logView.append(getString(R.string.lblScanRunning) + "\n"); + HypatiaLogger.Log(getString(R.string.lblScanRunning) + "\n"); } else { - Intent realtimeScanner = new Intent(getApplicationContext(), MalwareScannerService.class); + Intent realtimeScanner = new Intent(Hypatia.getAppContext(), MalwareScannerService.class); if (!item.isChecked()) { prefs.edit().putBoolean("autostart", true).apply(); Utils.considerStartService(this); @@ -331,15 +372,15 @@ public final boolean onOptionsItemSelected(MenuItem item) { case R.id.btnStartScan: if (!malwareScanner.running) { if (Database.hasDownloadsRunning()) { - logView.append(getString(R.string.lblUpdateRunning) + "\n"); + HypatiaLogger.Log(getString(R.string.lblUpdateRunning) + "\n"); } else if (Database.isDatabaseLoading()) { - logView.append(getString(R.string.lblDatabaseLoading) + "\n"); + HypatiaLogger.Log(getString(R.string.lblDatabaseLoading) + "\n"); } else { updateScanButton(true); startScanner(); } } else { - logView.append("\n" + getString(R.string.main_cancelling_scan) + "\n\n"); + HypatiaLogger.Log("\n" + getString(R.string.main_cancelling_scan) + "\n\n"); malwareScanner.cancel(true); malwareScanner.running = false; } @@ -348,7 +389,7 @@ public final boolean onOptionsItemSelected(MenuItem item) { } private void startScanner() { - malwareScanner = new MalwareScanner(this, this, true); + malwareScanner = new MalwareScanner(this, true); malwareScanner.running = true; HashSet filesToScan = new HashSet<>(); if (scanSystem) { @@ -369,7 +410,8 @@ private void startScanner() { filesToScan.add(new File("/vendor_dlkm")); } if (scanApps) { - for (ApplicationInfo packageInfo : getPackageManager().getInstalledApplications(PackageManager.GET_META_DATA)) { + for (ApplicationInfo packageInfo : getPackageManager() + .getInstalledApplications(PackageManager.GET_META_DATA)) { if (packageInfo != null) { if (packageInfo.sourceDir != null) { filesToScan.add(new File(packageInfo.sourceDir)); @@ -399,8 +441,10 @@ private void startScanner() { } PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE); - PowerManager.WakeLock wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Hypatia::ManualScanLock"); + PowerManager.WakeLock wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, + "Hypatia::ManualScanLock"); wakeLock.acquire(10 * 60 * 1000L); /* 10 minutes */ + MalwareScanner.isCache = false; malwareScanner.executeOnExecutor(Utils.getThreadPoolExecutor(), filesToScan); new Thread(() -> { try { @@ -418,40 +462,44 @@ private void startScanner() { private void updateDatabase() { if (prefs.getBoolean("ONION_ROUTING", false)) { Utils.requestStartOrbot(this); - logView.append(getString(R.string.lblOnionRoutingEnabledHint) + "\n"); + HypatiaLogger.Log(getString(R.string.lblOnionRoutingEnabledHint) + "\n"); } new Database(findViewById(R.id.txtLogOutput)); if (!Database.isDatabaseLoading()) { PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE); - PowerManager.WakeLock wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Hypatia::UpdateLock"); + PowerManager.WakeLock wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, + "Hypatia::UpdateLock"); wakeLock.acquire(3 * 60 * 1000L); /* 3 minutes */ Database.updateDatabase(this, Database.signatureDatabases); Utils.getThreadPoolExecutor().execute(() -> { try { Thread.sleep(500); - Log.d("Hypatia", "Considering database reload!"); + HypatiaLogger.Log("Considering database reload!", LogType.DEBUG); while (Database.hasDownloadsRunning()) { Thread.sleep(500); - Log.d("Hypatia", "Download in progress, waiting!"); + HypatiaLogger.Log("Download in progress, waiting!", LogType.DEBUG); } wakeLock.release(); } catch (InterruptedException e) { throw new RuntimeException(e); } - runOnUiThread(() -> logView.append(getString(R.string.lblDatabasesUpdated) + "\n")); + runOnUiThread(() -> { + HypatiaLogger.Log(getString(R.string.lblDatabasesUpdated) + "\n"); + Database.changedUri = false; + }); if (Database.isDatabaseLoaded()) { - if (Database.changedDownload || Database.changedConfig) { - Log.d("Hypatia", "Really reloading database!"); - Database.loadDatabase(getApplicationContext(), true, Database.signatureDatabases); + if (Database.changedDownload || Database.changedConfig || Database.changedUri) { + HypatiaLogger.Log("Really reloading database!", LogType.DEBUG); + Database.loadDatabase(Hypatia.getAppContext(), true, Database.signatureDatabases); } else { - Log.d("Hypatia", "Database not changed, skipping reload!"); + HypatiaLogger.Log("Database not changed, skipping reload!", LogType.DEBUG); } } else { - Log.d("Hypatia", "Database not loaded, skipping reload!"); + HypatiaLogger.Log("Database not loaded, skipping reload!", LogType.DEBUG); } }); } else { - logView.append(getString(R.string.lblDatabaseLoading) + "\n"); + HypatiaLogger.Log(getString(R.string.lblDatabaseLoading) + "\n"); } } diff --git a/app/src/main/java/us/spotco/malwarescanner/MalwareScanner.java b/app/src/main/java/us/spotco/malwarescanner/MalwareScanner.java index 542b0c8..17f2d18 100644 --- a/app/src/main/java/us/spotco/malwarescanner/MalwareScanner.java +++ b/app/src/main/java/us/spotco/malwarescanner/MalwareScanner.java @@ -17,7 +17,7 @@ */ package us.spotco.malwarescanner; -import android.app.Activity; +import androidx.appcompat.app.AppCompatActivity; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; @@ -29,6 +29,7 @@ import android.os.Environment; import android.os.SystemClock; import android.widget.TextView; +import android.util.Log; import com.google.common.hash.BloomFilter; @@ -57,13 +58,11 @@ class MalwareScanner extends AsyncTask, Object, String> { private final HashMap fileHashesSHA256 = new HashMap<>(); public boolean running = false; private int amtMatchedFiles = 0; + public static boolean isCache = false; - public MalwareScanner(Activity activity, Context context, boolean userFacing) { + public MalwareScanner(Context context, boolean userFacing) { this.context = context; this.userFacing = userFacing; - if (activity != null) { - logOutput = activity.findViewById(R.id.txtLogOutput); - } notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { NotificationChannel detectionChannel = new NotificationChannel("DETECTION", context.getString(R.string.lblNotificationMalwareDetectionTitle), NotificationManager.IMPORTANCE_HIGH); @@ -108,7 +107,7 @@ private void logResult(String result, String hashsum) { PendingIntent lookupIntentPending = PendingIntent.getActivity(context, notificationId + 1, lookupIntent, PendingIntent.FLAG_IMMUTABLE); mBuilder.addAction(android.R.drawable.ic_dialog_map, context.getText(R.string.lookupVT), lookupIntentPending); //Delete action - if (malwareDetect[1].startsWith("~/")) { + if (malwareDetect[1].startsWith("~/") && !isCache) { Intent deleteIntent = new Intent(context, NotificationPromptActivity.class); deleteIntent.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT | Intent.FLAG_ACTIVITY_NEW_TASK); deleteIntent.setAction("us.spotco.malwarescanner.DELETE_FILE"); @@ -136,9 +135,9 @@ private void logResult(String result, String hashsum) { //Show it! notificationManager.notify(notificationId, mBuilder.build()); } else if (userFacing) { - logOutput.append(result); + HypatiaLogger.Log(result, LogType.NONEWLINE); if (!(result.length() <= 3)) { - logOutput.append("\n"); + HypatiaLogger.Log("\n", LogType.NONEWLINE); } } } @@ -149,14 +148,25 @@ protected final void onPreExecute() { logResult(context.getString(R.string.main_starting_scan), null); } + /** + * Runs when executeOnExecutor() method called + * Handles the main scanning job + * + * @param filesToScan HashSet of files to scan + * @return + */ @Override protected final String doInBackground(HashSet[] filesToScan) { + if (isCache) { + HypatiaLogger.Log("Cache File Scan Started", LogType.DEBUG); + } + running = true; int startCount = amtMatchedFiles; - ConcurrentSkipListSet filesToScanReal = new ConcurrentSkipListSet<>(); //TODO: Reduce this? + ConcurrentSkipListSet filesToScanReal = new ConcurrentSkipListSet<>(); //TODO: (from divestos) Reduce this? for (Set fileArray : filesToScan) { for (File file : fileArray) { - filesToScanReal.addAll(Utils.getFilesRecursive(file)); //TODO: Inline this, hash files as they are found + filesToScanReal.addAll(Utils.getFilesRecursive(file)); //TODO: (from divestos) Inline this, hash files as they are found } } @@ -221,7 +231,7 @@ protected final String doInBackground(HashSet[] filesToScan) { spinnerCur = " = "; } } - //Log.d("Hypatia", "Scanning " + file); + HypatiaLogger.Log("Scanning " + file, LogType.VERYVERBOSE); } filesToScanReal.clear(); publishProgress("\n\t" + context.getString(R.string.main_hashing_done) + "\n", true); @@ -241,6 +251,18 @@ protected final String doInBackground(HashSet[] filesToScan) { publishProgress("\n\t" + context.getString(R.string.detections_found) + "\n", true); } + //Delete if cache + if (isCache) { + try { + File cacheFile = filesToScan[0].stream().findFirst().get(); + cacheFile.delete(); + } catch (Exception e) { + HypatiaLogger.Log("Cache file couldn't deleted", LogType.WARNING); + e.printStackTrace(); + } + + } + //Post fileHashesMD5.clear(); fileHashesSHA1.clear(); @@ -256,7 +278,11 @@ protected final String doInBackground(HashSet[] filesToScan) { if (secondsSpentHashing > 0) { MBS = totalBytesHashed / 1000 / 1000 / secondsSpentHashing; } - publishProgress(context.getString(R.string.main_scanning_done, String.valueOf(secondsSpent), String.valueOf(MBS)) + "\n\n\n\n", true); + publishProgress(context.getString(R.string.main_scanning_done, String.valueOf(secondsSpent), String.valueOf(MBS)) + (isCache ? "\nNote:scan speed can be wrong in share scans\n\n" : "\n\n\n\n") , true); + + if(isCache) { + HypatiaLogger.Log("You Should Manually Remove & Quarantine the file !!!\n\n", LogType.ERROR);; + } } } else { publishProgress("\t" + context.getString(R.string.main_no_database_available), true); @@ -282,10 +308,13 @@ private void checkSignature(String hashType, HashMap signaturesToC && !file.getValue().equals("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")) { if (signatureDatabase.mightContain(file.getValue())) { amtMatchedFiles++; - //Log.d("Hypatia", "Match: " + fileHashesSHA256.get(file.getKey())); + HypatiaLogger.Log("Match: " + fileHashesSHA256.get(file.getKey()), LogType.VERBOSE); + HypatiaLogger.Log("Match In: " + file.getKey().toString().replaceAll(Environment.getExternalStorageDirectory().toString(), "~"), LogType.VERBOSE); + HypatiaLogger.Log("NOTIFICATION OUTPUT: " + file.getKey().toString().replaceAll(Environment.getExternalStorageDirectory().toString(), "~").replaceFirst("~", Environment.getExternalStorageDirectory().toString()), LogType.VERBOSE); + publishProgress("Potential match in " + file.getKey().toString().replaceAll(Environment.getExternalStorageDirectory().toString(), "~"), false, fileHashesSHA256.get(file.getKey())); } else { - //Log.d("Hypatia", "No match for " + file.getValue()); + HypatiaLogger.Log("No match for " + file.getValue(), LogType.VERYVERBOSE); } } } diff --git a/app/src/main/java/us/spotco/malwarescanner/MalwareScannerService.java b/app/src/main/java/us/spotco/malwarescanner/MalwareScannerService.java index fa35081..998cacc 100644 --- a/app/src/main/java/us/spotco/malwarescanner/MalwareScannerService.java +++ b/app/src/main/java/us/spotco/malwarescanner/MalwareScannerService.java @@ -48,19 +48,19 @@ public final IBinder onBind(Intent intent) { @Override public final int onStartCommand(Intent intent, int flags, int startId) { - Utils.setContext(getApplicationContext()); + Utils.setContext(Hypatia.getAppContext()); malwareMonitors.clear(); addMalwareMonitor(Environment.getExternalStorageDirectory().toString()); int threadCount = Utils.getMaxThreads() + malwareMonitors.size(); threadPoolExecutor = Utils.getNewThreadPoolExecutor(threadCount); - threadPoolExecutor.execute(() -> Database.loadDatabase(getApplicationContext(), false, Database.signatureDatabases)); + threadPoolExecutor.execute(() -> Database.loadDatabase(Hypatia.getAppContext(), false, Database.signatureDatabases)); for (final RecursiveFileObserver malwareMonitor : malwareMonitors) { threadPoolExecutor.execute(malwareMonitor::startWatching); } - notificationManager = (NotificationManager) getApplicationContext().getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager = (NotificationManager) Hypatia.getAppContext().getSystemService(Context.NOTIFICATION_SERVICE); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { NotificationChannel foregroundChannel = new NotificationChannel("FOREGROUND", getString(R.string.lblNotificationRealtimeTitle), NotificationManager.IMPORTANCE_LOW); foregroundChannel.setDescription(getString(R.string.lblNotificationRealtimeDescription)); @@ -82,7 +82,8 @@ public void onEvent(int event, String path) { if (file.exists() && file.length() <= Utils.MAX_SCAN_SIZE_REALTIME) { HashSet filesToScan = new HashSet<>(); filesToScan.add(file); - new MalwareScanner(null, getApplicationContext(), false).executeOnExecutor(threadPoolExecutor, filesToScan); + MalwareScanner.isCache = false; + new MalwareScanner(Hypatia.getAppContext(), false).executeOnExecutor(threadPoolExecutor, filesToScan); } updateForegroundNotification(); break; diff --git a/app/src/main/java/us/spotco/malwarescanner/NotificationPromptActivity.java b/app/src/main/java/us/spotco/malwarescanner/NotificationPromptActivity.java index 493b6f9..e602dce 100644 --- a/app/src/main/java/us/spotco/malwarescanner/NotificationPromptActivity.java +++ b/app/src/main/java/us/spotco/malwarescanner/NotificationPromptActivity.java @@ -19,6 +19,7 @@ import android.app.Activity; +import androidx.appcompat.app.AppCompatActivity; import android.app.AlertDialog; import android.app.NotificationManager; import android.content.Context; @@ -34,15 +35,11 @@ import java.io.File; import java.util.Objects; -public class NotificationPromptActivity extends Activity { +public class NotificationPromptActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - getWindow().setLayout( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT - ); Intent intent = getIntent(); Context context = this; switch (Objects.requireNonNull(intent.getAction())) { @@ -63,7 +60,7 @@ protected void onCreate(Bundle savedInstanceState) { lookupDialog.setOnDismissListener((dialog) -> finish()); lookupDialog.show(); } else { - Log.d("Hypatia", "Invalid hash"); + HypatiaLogger.Log("Invalid hash", LogType.DEBUG); finish(); } break; @@ -92,7 +89,7 @@ protected void onCreate(Bundle savedInstanceState) { finish(); } } else { - Log.d("Hypatia", "Ignoring DELETE_FILE request from external package: " + intent.getPackage()); + HypatiaLogger.Log("Ignoring DELETE_FILE request from external package: " + intent.getPackage(), LogType.DEBUG); finish(); } break; @@ -117,7 +114,7 @@ protected void onCreate(Bundle savedInstanceState) { finish(); } } else { - Log.d("Hypatia", "Ignoring DELETE_FILE request from external package: " + intent.getPackage()); + HypatiaLogger.Log("Ignoring DELETE_FILE request from external package: " + intent.getPackage(), LogType.DEBUG); finish(); } break; @@ -133,7 +130,7 @@ private static void clearNotification(Context context, int id) { NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); if (id != 0) { notificationManager.cancel(id); - Log.d("Hypatia", "Canceled notification"); + HypatiaLogger.Log("Canceled notification", LogType.DEBUG); } } diff --git a/app/src/main/java/us/spotco/malwarescanner/ShareScanner.java b/app/src/main/java/us/spotco/malwarescanner/ShareScanner.java new file mode 100644 index 0000000..5d802b1 --- /dev/null +++ b/app/src/main/java/us/spotco/malwarescanner/ShareScanner.java @@ -0,0 +1,236 @@ +/* +Hypatia: A realtime malware scanner for Android +Copyleft (🄯) 2025 MaintainTeam Organization + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program 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 Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ +package us.spotco.malwarescanner; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.provider.DocumentsContract; +import android.provider.MediaStore; +import android.provider.OpenableColumns; +import android.util.Log; +import android.widget.TextView; + +import androidx.appcompat.app.AppCompatActivity; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.FileOutputStream; +import java.util.HashSet; + +public class ShareScanner extends AppCompatActivity { + + private MalwareScanner malwareScanner = null; + private String shareScanType; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + handleIncomingIntent(getIntent()); + } + + private void handleIncomingIntent(Intent intent) { + if (Intent.ACTION_SEND.equals(intent.getAction()) && intent.getType() != null) { + Uri fileUri = intent.getParcelableExtra(Intent.EXTRA_STREAM); + String userResult; + Intent mainIntent = new Intent(Hypatia.getAppContext(), MainActivity.class); + + if (fileUri != null) { + userResult = scanFile(fileUri); + } else if ("text/plain".equals(intent.getType())) { + userResult = "This is not a scannable file - probably just a string: " + + intent.getStringExtra(Intent.EXTRA_TEXT); + // TODO feat: Link Scanner? + // TODO feat: get string input (hash) directly to scan + } else { + userResult = "THIS IS NOT A SCANNABLE THING"; + } + + mainIntent.putExtra("SHARE_SCAN_RESULT", userResult); + mainIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(mainIntent); + finish(); + } + } + + private String scanFile(Uri fileUri) { + try { + Context context = Hypatia.getAppContext(); + File file = getFileFromUri(context, fileUri); + + if (file == null || !file.exists()) { + return "Error: File not accessible"; + } + + malwareScanner = new MalwareScanner(context, true); + malwareScanner.running = true; + HashSet filesToScan = new HashSet<>(); + filesToScan.add(file); + + HypatiaLogger.Log("Scanning file at: " + file.getAbsolutePath(), LogType.DEBUG); + malwareScanner.executeOnExecutor(Utils.getThreadPoolExecutor(), filesToScan); + return "Scan started for: " + file.getName(); + + } catch (RuntimeException e) { + Log.e("Hypatia", "Scan error: " + e.getMessage()); + return "Scanner Error: " + e.getMessage(); + } + } + + private File getFileFromUri(Context context, Uri uri) { + String filePath = getRealPathFromUri(context, uri); + if (filePath != null) { + MalwareScanner.isCache = false; + return new File(filePath); + } else { + HypatiaLogger.Log("Unsupported URI, falling back to cache-system", LogType.DEBUG); + MalwareScanner.isCache = true; + return handleUnsupportedUri(context, uri); + } + } + + /** + * Get real path from an uri. probably returns null for share on Android 10+ + * + * @see #handleUnsupportedUri() + * + * @param context + * @param uri uri + * @return real path + */ + private String getRealPathFromUri(Context context, Uri uri) { + // File scheme + if ("file".equals(uri.getScheme())) { + return uri.getPath(); + } + + // Document provider + if (DocumentsContract.isDocumentUri(context, uri)) { + String docId = DocumentsContract.getDocumentId(uri); + String[] split = docId.split(":"); + String type = split[0]; + + Uri contentUri = null; + switch (type) { + case "image": + contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; + break; + case "video": + contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; + break; + case "audio": + contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; + break; + case "download": + return getDataColumn(context, uri, null, null); + } + + if (contentUri != null) { + String selection = "_id=?"; + String[] selectionArgs = new String[] { split[1] }; + return getDataColumn(context, contentUri, selection, selectionArgs); + } + } + // MediaStore (and general) + else if ("content".equalsIgnoreCase(uri.getScheme())) { + return getDataColumn(context, uri, null, null); + } + + return null; + } + + private String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) { + Cursor cursor = null; + String column = MediaStore.MediaColumns.DATA; + String[] projection = { column }; + + try { + cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null); + if (cursor != null && cursor.moveToFirst()) { + int columnIndex = cursor.getColumnIndexOrThrow(column); + return cursor.getString(columnIndex); + } + } catch (Exception e) { + Log.e("Hypatia", "Data column error", e); + } finally { + if (cursor != null) + cursor.close(); + } + return null; + } + + /** + * fallback for unsupported uris. reads share as InputStream and write copies to + * cache + * + * @param context + * @param uri + * @return hopefully a file + */ + private File handleUnsupportedUri(Context context, Uri uri) { + try { + InputStream input = context.getContentResolver().openInputStream(uri); + File file = createTempFile(context, getFileName(context, uri)); + FileOutputStream output = new FileOutputStream(file); + + byte[] buffer = new byte[4 * 1024]; + int read; + while ((read = input.read(buffer)) != -1) { + output.write(buffer, 0, read); + } + output.flush(); + return file; + + } catch (Exception e) { + Log.e("Hypatia", "File copy error: " + e.getMessage()); + return null; + } + } + + private String getFileName(Context context, Uri uri) { + String result = null; + if (uri.getScheme().equals("content")) { + try (Cursor cursor = context.getContentResolver().query(uri, null, null, null, null)) { + if (cursor != null && cursor.moveToFirst()) { + result = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)); + } + } + } + if (result == null) { + result = uri.getPath(); + int cut = result.lastIndexOf('/'); + if (cut != -1) { + result = result.substring(cut + 1); + } + } + return result; + } + + private File createTempFile(Context context, String fileName) { + File storageDir = context.getExternalCacheDir(); + if (storageDir == null) { + storageDir = context.getCacheDir(); + } + return new File(storageDir, fileName); + } +} \ No newline at end of file diff --git a/app/src/main/java/us/spotco/malwarescanner/Utils.java b/app/src/main/java/us/spotco/malwarescanner/Utils.java index 5793177..bcec9ea 100644 --- a/app/src/main/java/us/spotco/malwarescanner/Utils.java +++ b/app/src/main/java/us/spotco/malwarescanner/Utils.java @@ -1,6 +1,7 @@ /* Hypatia: A realtime malware scanner for Android Copyright (c) 2017-2018 Divested Computing Group +Copyleft (🄯) 2025 MaintainTeam Organization This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by diff --git a/app/src/main/res/layout/content_main.xml b/app/src/main/res/layout/content_main.xml index dbe3040..5c31f29 100644 --- a/app/src/main/res/layout/content_main.xml +++ b/app/src/main/res/layout/content_main.xml @@ -1,9 +1,20 @@ + + + @@ -6,7 +7,7 @@ android:id="@+id/btnStartScan" android:title="@string/scan_control" android:icon="@android:drawable/ic_media_play" - android:showAsAction="always" /> + app:showAsAction="always" /> - Copyright 2017-2024 Divested Computing Group, 2025 MaintainTeam + This is only a malware scanner which compares files with previously found hashes. Not a magic wand that keeps your entire android super secure. You should be careful what you download/click !!! + Copyright 2017-2024 Divested Computing Group\nCopyleft 2025 MaintainTeam Organization License: GPL-3.0 Version: %s Powered by ClamAV style signatures @@ -44,7 +45,7 @@ Hashing files… Calculated hashes for all files Checked all %s hashes against signature databases - Scan completed in %s seconds @ %sMB/s! + Scan completed in %1$s seconds @ %2$sMB/s! %s files scanned Scan Control Skipping action, a scan is running! diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..7c39ffc --- /dev/null +++ b/app/src/main/res/values/styles.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle index 23ab4b2..04bbaf4 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.2.2' + classpath 'com.android.tools.build:gradle:8.7.1' // NOTE: Do not place your application dependencies here; they belong diff --git a/gradle.properties b/gradle.properties index 11e2709..b2484da 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,6 +10,8 @@ # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. android.enableJetifier=true +android.nonFinalResIds=false +android.nonTransitiveRClass=false android.useAndroidX=true org.gradle.jvmargs=-Xmx1536m @@ -18,5 +20,5 @@ org.gradle.jvmargs=-Xmx1536m # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true -org.gradle.dependency.verification=strict +org.gradle.dependency.verification=lenient #org.gradle.dependency.verification.console=verbose diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 016cbf6..a132921 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -1,5 +1,5 @@ - + true true @@ -7,79 +7,79 @@ - + - - - - - - - - + + + + + + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -101,17 +101,21 @@ + + + + @@ -130,9 +134,11 @@ + + @@ -160,6 +166,14 @@ + + + + + + + + @@ -184,27 +198,41 @@ + + + + + + + + + + + + + + @@ -218,22 +246,27 @@ + + + + + @@ -271,17 +304,21 @@ + + + + @@ -290,30 +327,37 @@ + + + + + + + @@ -327,20 +371,40 @@ + + + + + + + + + + + + + + + + + + + + @@ -351,9 +415,11 @@ + + @@ -375,33 +441,41 @@ + + + + + + + + @@ -428,22 +502,27 @@ + + + + + @@ -457,17 +536,21 @@ + + + + @@ -491,9 +574,11 @@ + + @@ -512,9 +597,11 @@ + + @@ -528,17 +615,21 @@ + + + + @@ -550,6 +641,22 @@ + + + + + + + + + + + + + + + + @@ -573,9 +680,11 @@ + + @@ -586,30 +695,37 @@ + + + + + + + @@ -623,33 +739,41 @@ + + + + + + + + @@ -663,108 +787,246 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -773,6 +1035,14 @@ + + + + + + + + @@ -783,124 +1053,282 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -909,6 +1337,14 @@ + + + + + + + + @@ -917,6 +1353,14 @@ + + + + + + + + @@ -925,14 +1369,32 @@ + + + + + + + + + + + + + + + + + + @@ -941,6 +1403,14 @@ + + + + + + + + @@ -949,6 +1419,14 @@ + + + + + + + + @@ -957,6 +1435,14 @@ + + + + + + + + @@ -965,243 +1451,2169 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - - + + + - - + + + + - - - + + + - - - + + + + + + - - - + + + - - + + - - - + + + - - + + - - - + + + - - + + - - - + + + + + + - - - + + + - - + + - - - + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + - - - + + + - - - + + + + - - - - + + + - - - + + + + + + - - - + + + + + + - - - + + + + + + - - - + + + + + + - - - + + + + + + - - - + + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 249e583..2c35211 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b916c04..68e8816 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,8 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=f6b8596b10cce501591e92f229816aa4046424f3b24d771751b06779d58c8ec4 -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip +distributionSha256Sum=d725d707bfabd4dfdc958c624003b3c80accc03f7037b5122c4b1d0ef15cecab +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index a69d9cb..f5feea6 100755 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -80,13 +82,12 @@ do esac done -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" +# This is normally unused +# shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -133,22 +134,29 @@ location of your Java installation." fi else JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -193,11 +201,15 @@ if "$cygwin" || "$msys" ; then done fi -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ diff --git a/gradlew.bat b/gradlew.bat index 53a6b23..9b42019 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @@ -26,6 +28,7 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -42,11 +45,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -56,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail