Repo created

This commit is contained in:
Fr4nz D13trich 2025-11-20 14:05:38 +01:00
parent 51cf8bb4f9
commit ee0cddf35c
548 changed files with 93129 additions and 2 deletions

84
app/build.gradle Normal file
View file

@ -0,0 +1,84 @@
apply plugin: 'com.android.application'
android {
defaultConfig {
compileSdk 36
targetSdk 36
applicationId "dev.ukanth.ufirewall"
//applicationId "dev.ukanth.ufirewall.donate"
minSdkVersion 21
versionCode 20251001
versionName "4.0.0"
buildConfigField 'boolean', 'DONATE', 'false'
vectorDrawables.useSupportLibrary = true
ndk {
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
}
}
buildFeatures {
buildConfig true
}
buildTypes {
debug {
minifyEnabled false
proguardFiles 'proguard-rules.pro'
}
release {
minifyEnabled false
proguardFiles 'proguard-rules.pro'
}
}
lint {
abortOnError true
disable 'MissingTranslation'
}
namespace 'dev.ukanth.ufirewall'
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
compileSdk 36
buildToolsVersion '34.0.0'
packagingOptions {
jniLibs {
useLegacyPackaging = true
}
}
}
dependencies {
def libsuVersion = '6.0.0'
def dbFlowVersion = '4.2.4'
implementation "com.github.topjohnwu.libsu:core:${libsuVersion}"
implementation "com.github.topjohnwu.libsu:service:${libsuVersion}"
implementation "com.github.topjohnwu.libsu:nio:${libsuVersion}"
implementation "eu.chainfire:libsuperuser:1.1.0"
implementation "com.github.ukanth:android-lockpattern:8.0.4"
implementation "com.afollestad.material-dialogs:core:0.9.6.0"
implementation "androidx.appcompat:appcompat:1.7.0"
implementation "com.google.android.material:material:1.12.0"
implementation "androidx.cardview:cardview:1.0.0"
implementation "androidx.recyclerview:recyclerview:1.3.2"
implementation "androidx.annotation:annotation:1.9.0"
implementation "androidx.core:core:1.15.0"
implementation "androidx.preference:preference:1.2.1"
implementation "androidx.legacy:legacy-support-v13:1.0.0"
implementation "androidx.activity:activity:1.9.3"
annotationProcessor "com.github.Raizlabs.DBFlow:dbflow-processor:${dbFlowVersion}"
implementation "com.github.Raizlabs.DBFlow:dbflow-core:${dbFlowVersion}"
implementation "com.github.Raizlabs.DBFlow:dbflow:${dbFlowVersion}"
implementation "io.reactivex.rxjava3:rxjava:3.1.9"
implementation "org.ocpsoft.prettytime:prettytime:5.0.6.Final"
implementation "dnsjava:dnsjava:3.6.2"
}

3
app/lint.xml Normal file
View file

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<lint>
</lint>

21
app/proguard-rules.pro vendored Normal file
View file

@ -0,0 +1,21 @@
-keepattributes
-keep class org.ocpsoft.prettytime.i18n.**
-keep class * extends com.raizlabs.android.dbflow.config.DatabaseHolder { *; }
-dontpreverify
-dontoptimize
-dontobfuscate
-keep class dev.ukanth.ufirewall.** { *; }
-optimizations !code/allocation/variable
# Android 16 specific proguard rules
-keep class android.window.** { *; }
-keep class androidx.activity.** { *; }
-dontwarn android.window.**
-dontwarn androidx.window.**
# Edge-to-edge and window insets support
-keep class androidx.core.view.WindowInsetsCompat** { *; }
-keep class androidx.core.view.ViewCompat** { *; }
# Notification channel compatibility
-keep class androidx.core.app.NotificationChannelCompat** { *; }

View file

@ -0,0 +1,366 @@
<?xml version="1.0" encoding="utf-8"?><!--
* Android Manifest
* Copyright (C) 2007-2008 The Android Open Source Project
* Copyright (C) 2009-2011 Rodrigo Zechin Rosauro
* Copyright (C) 2011-2014 Umakanthan Chandran
*
* This program 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.
*
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @author Rodrigo Zechin Rosauro, Umakanthan Chandran
* @version 1.2
*/
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<!-- Allows ufwall to access information about networks -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- Allows ufwall to write to external storage -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
<!-- Allows access to IP configuration and tethering state -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"></uses-permission>
<!-- Allows access to bluetooth tethering state -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<!-- Allows ufwall to access information about Fingerprint -->
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" tools:ignore="QueryAllPackagesPermission"/>
<uses-permission
android:name="android.permission.FOREGROUND_SERVICE"/>
<!-- Screen support -->
<supports-screens
android:anyDensity="true"
android:largeScreens="true"
android:normalScreens="true"
android:smallScreens="true"
android:xlargeScreens="true" />
<!-- donate
android:icon="@mipmap/ic_launcher_donate"
android:label="@string/app_name_donate" -->
<application
android:name=".util.G"
android:allowBackup="false"
android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:largeHeap="true"
android:launchMode="singleTop"
android:resizeableActivity="true"
android:supportsRtl="true"
android:theme="@style/AppDarkTheme"
android:requestLegacyExternalStorage="true"
tools:ignore="ManifestResource">
<!--<meta-data
android:name="xposedmodule"
android:value="true" />
<meta-data
android:name="xposeddescription"
android:value="Xposed related fixes for AFWall+" />
<meta-data
android:name="xposedminversion"
android:value="53" />-->
<!-- donate android:label="@string/app_name_donate" -->
<activity
android:name=".MainActivity"
android:configChanges="orientation|keyboardHidden|screenSize|keyboard|locale|screenLayout"
android:hardwareAccelerated="true"
android:exported="true"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity>
<activity
android:name=".preferences.PreferencesActivity"
android:theme="@style/AppDarkTheme"
android:exported="true"
android:configChanges="orientation|keyboardHidden|screenSize|keyboard|locale|screenLayout"
android:hardwareAccelerated="true">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="dev.ukanth.ufirewall.MainActivity" />
</activity>
<activity
android:name=".activity.HelpActivity"
android:configChanges="orientation|keyboardHidden|screenSize|locale"
android:hardwareAccelerated="true"></activity>
<activity
android:name=".activity.RulesActivity"
android:configChanges="orientation|keyboardHidden|screenSize|keyboard|locale|screenLayout"></activity>
<activity
android:name=".activity.ProfileActivity"
android:configChanges="orientation|keyboardHidden|screenSize|keyboard|locale|screenLayout"></activity>
<activity
android:name=".activity.AppDetailActivity"
android:configChanges="orientation|keyboardHidden|screenSize|keyboard|locale|screenLayout">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="dev.ukanth.ufirewall.MainActivity" />
</activity>
<activity
android:name=".activity.LogActivity"
android:configChanges="orientation|keyboardHidden|screenSize|keyboard|locale|screenLayout"></activity>
<activity
android:name=".activity.OldLogActivity"
android:configChanges="orientation|keyboardHidden|screenSize|keyboard|locale|screenLayout"></activity>
<activity
android:name=".activity.LogDetailActivity"
android:configChanges="orientation|keyboardHidden|screenSize|keyboard|locale|screenLayout"></activity>
<activity
android:name=".activity.DataDumpActivity"
android:configChanges="orientation|keyboardHidden|screenSize|keyboard|locale|screenLayout">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="dev.ukanth.ufirewall.MainActivity" />
</activity>
<!-- <activity android:name="dev.ukanth.ufirewall.AlertDialogActivity"/> -->
<activity
android:name=".widget.ToggleWidgetActivity"
android:launchMode="singleInstance"
android:exported="true"
android:theme="@style/Theme.Transparent">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter>
</activity>
<activity
android:name=".widget.ToggleWidgetOldActivity"
android:launchMode="singleInstance"
android:exported="true"
android:theme="@style/Theme.Transparent">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter>
</activity>
<activity
android:name=".activity.CustomScriptActivity"
android:configChanges="orientation|keyboardHidden|screenSize|keyboard|locale|screenLayout">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="dev.ukanth.ufirewall.MainActivity" />
</activity>
<!-- still will work in android 7 above -->
<receiver android:name=".broadcast.OnBootReceiver"
android:exported="true">
<intent-filter android:priority="500">
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
<action android:name="android.intent.action.MEDIA_MOUNTED" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>
<!--<service android:name=".service.ApplyOnBootService" />-->
<service
android:name=".service.FirewallService"
android:foregroundServiceType="specialUse"
android:exported="true"
android:launchMode="singleTop" >
<property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="Firewall service"/>
</service>
<service
android:name=".service.ToggleTileService"
android:icon="@drawable/notification_quest"
android:exported="true"
android:label="@string/widget_label_status"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
</service>
<receiver
android:name=".widget.StatusWidget"
android:enabled="true"
android:exported="true"
android:icon="@drawable/preview_toggle"
android:label="@string/widget_label_status">
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/onoff_widget" />
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="dev.ukanth.ufirewall.intent.action.STATUS_CHANGED" />
<action android:name="dev.ukanth.ufirewall.intent.action.TOGGLE_REQUEST" />
</intent-filter>
</receiver>
<receiver
android:name=".widget.ToggleWidget"
android:exported="true"
android:icon="@drawable/preview_new"
android:label="@string/widget_label_settings">
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/toggle_widget" />
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="dev.ukanth.ufirewall.intent.action.STATUS_CHANGED" />
<action android:name="dev.ukanth.ufirewall.intent.action.TOGGLE_REQUEST" />
</intent-filter>
</receiver>
<receiver
android:name=".widget.ToggleWidgetOld"
android:exported="true"
android:label="@string/widget_label_settings_old">
<meta-data
android:name="android.appwidget.provider"
android:icon="@drawable/preview_old"
android:resource="@xml/toggle_widget_old" />
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="dev.ukanth.ufirewall.intent.action.STATUS_CHANGED" />
<action android:name="dev.ukanth.ufirewall.intent.action.TOGGLE_REQUEST" />
</intent-filter>
</receiver>
<activity
android:name="haibison.android.lockpattern.LockPatternActivity"
android:theme="@style/Alp_42447968.Theme.Dark"
tools:replace="android:theme" />
<!-- device admin -->
<!-- This is required this receiver to become device admin component. -->
<receiver
android:name=".admin.AdminDeviceReceiver"
android:exported="true"
android:description="@string/device_admin_desc"
android:label="@string/enable_device_admin"
android:permission="android.permission.BIND_DEVICE_ADMIN">
<meta-data
android:name="android.app.device_admin"
android:resource="@xml/device_admin" />
<intent-filter>
<action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
</intent-filter>
</receiver>
<!-- Tasker/Locale Plugin -->
<activity
android:name=".plugin.LocaleEdit"
android:exported="true"
android:icon="@drawable/ic_launcher_free"
android:label="@string/tasker_lable"
android:uiOptions="splitActionBarWhenNarrow"
android:windowSoftInputMode="adjustResize"
tools:ignore="ExportedActivity">
<!-- this Intent filter allows the plug-in to be discovered by Locale -->
<intent-filter>
<action android:name="com.twofortyfouram.locale.intent.action.EDIT_SETTING" />
</intent-filter>
</activity>
<receiver
android:name=".plugin.FireReceiver"
android:exported="true"
tools:ignore="ExportedReceiver">
<!-- this Intent filter allows the plug-in to discovered by Locale -->
<intent-filter>
<action android:name="com.twofortyfouram.locale.intent.action.FIRE_SETTING" />
</intent-filter>
</receiver>
<service
android:name=".service.RootShellService"
android:exported="false" />
<service
android:name=".service.RootShellService2"
android:exported="false" />
<service
android:name=".service.LogService"
android:permission="android.permission.BIND_JOB_SERVICE"
android:enabled="true"
android:label="AFWallLogService" />
<!-- Samsung MultiWindow Support -->
<uses-library
android:name="com.sec.android.app.multiwindow"
android:required="false" />
<meta-data
android:name="com.sec.android.multiwindow.DEFAULT_SIZE_W"
android:resource="@dimen/app_defaultsize_w" />
<meta-data
android:name="com.sec.android.multiwindow.DEFAULT_SIZE_H"
android:resource="@dimen/app_defaultsize_h" />
<meta-data
android:name="com.sec.android.multiwindow.MINIMUM_SIZE_W"
android:resource="@dimen/app_minimumsize_w" />
<meta-data
android:name="com.sec.android.multiwindow.MINIMUM_SIZE_H"
android:resource="@dimen/app_minimumsize_h" />
<provider
android:name=".preferences.ShareProfilePreference"
android:authorities="${applicationId}"
android:exported="true"></provider>
<service
android:name=".service.RulesApplyService"
android:exported="false" />
<!-- <provider
android:name=".util.XPreferenceProvider"
android:authorities="${applicationId}"
android:exported="true" />-->
<activity android:name=".activity.CustomRulesActivity"></activity>
</application>
</manifest>

View file

@ -0,0 +1,78 @@
{
"author": "ukanth",
"version": "1.0",
"rules": [
{
"name": "Allow Multicast DNS",
"desc": "DNS based service discovery",
"v4": {
"on": [
"-A afwall-wifi-fork -d 224.0.0.0/24 -j afwall-wifi-lan"
],
"off": [
"-D afwall-wifi-fork -d 224.0.0.0/24 -j afwall-wifi-lan"
]
},
"v6": {
"on": [
"-A afwall-wifi-fork -d ffx2::/16 -j afwall-wifi-lan"
],
"off": [
"-D afwall-wifi-fork -d ffx2::/16 -j afwall-wifi-lan"
]
}
},
{
"name": "Allow ICMP",
"desc": "To allow ICMP packets",
"v4": {
"on": [
"-A INPUT -p icmp -m icmp --icmp-type echo-reply -j ACCEPT",
"-A INPUT -p icmp -m icmp --icmp-type echo-request -j ACCEPT",
"-A INPUT -p icmp -m icmp --icmp-type destination-unreachable -j ACCEPT"
],
"off": [
"-D INPUT -p icmp -m icmp --icmp-type echo-reply -j ACCEPT",
"-D INPUT -p icmp -m icmp --icmp-type echo-request -j ACCEPT",
"-D INPUT -p icmp -m icmp --icmp-type destination-unreachable -j ACCEPT"
]
},
"v6": {
"on": [
"-A INPUT -p icmp -m icmp --icmp-type echo-reply -j ACCEPT",
"-A INPUT -p icmp -m icmp --icmp-type echo-request -j ACCEPT",
"-A INPUT -p icmp -m icmp --icmp-type destination-unreachable -j ACCEPT"
],
"off": [
"-D INPUT -p icmp -m icmp --icmp-type echo-reply -j ACCEPT",
"-D INPUT -p icmp -m icmp --icmp-type echo-request -j ACCEPT",
"-D INPUT -p icmp -m icmp --icmp-type destination-unreachable -j ACCEPT"
]
}
},
{
"name": "Allow Loopback Interface",
"desc": "Allow lo interface routing (mostly on samsung)",
"v4": {
"on": [
"-A INPUT -i lo -j ACCEPT",
"-A afwall -o lo -j ACCEPT"
],
"off": [
"-D INPUT -i lo -j ACCEPT",
"-D afwall -o lo -j ACCEPT"
]
},
"v6": {
"on": [
"-A INPUT -i lo -j ACCEPT",
"-A afwall -o lo -j ACCEPT"
],
"off": [
"-D INPUT -i lo -j ACCEPT",
"-D afwall -o lo -j ACCEPT"
]
}
}
]
}

View file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View file

@ -0,0 +1,46 @@
package com.stericson.rootshell;
import com.stericson.rootshell.containers.RootClass;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
@RootClass.Candidate
public class NativeJavaClass
{
public NativeJavaClass(RootClass.RootArgs args)
{
System.out.println("NativeJavaClass says: oh hi there.");
String p = "/data/data/com.android.browser/cache";
File f = new File(p);
String[] fl = f.list();
if (fl != null)
{
System.out.println("Look at all the stuff in your browser's cache:");
for (String af : fl)
{
System.out.println("-" + af);
}
System.out.println("Leaving my mark for posterity...");
File f2 = new File(p + "/rootshell_was_here");
try
{
FileWriter filewriter = new FileWriter(f2);
BufferedWriter out = new BufferedWriter(filewriter);
out.write("This is just a file created using RootShell's Sanity check tools..\n");
out.close();
System.out.println("Done!");
}
catch (IOException e)
{
System.out.println("...and I failed miserably.");
e.printStackTrace();
}
}
}
}

View file

@ -0,0 +1,585 @@
/*
* This file is part of the RootShell Project: http://code.google.com/p/RootShell/
*
* Copyright (c) 2014 Stephen Erickson, Chris Ravenscroft
*
* This code is dual-licensed under the terms of the Apache License Version 2.0 and
* the terms of the General Public License (GPL) Version 2.
* You may use this code according to either of these licenses as is most appropriate
* for your project on a case-by-case basis.
*
* The terms of each license can be found in the root directory of this project's repository as well as at:
*
* * http://www.apache.org/licenses/LICENSE-2.0
* * http://www.gnu.org/licenses/gpl-2.0.txt
*
* Unless required by applicable law or agreed to in writing, software
* distributed under these Licenses is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See each License for the specific language governing permissions and
* limitations under that License.
*/
package com.stericson.rootshell;
import android.util.Log;
import com.stericson.rootshell.exceptions.RootDeniedException;
import com.stericson.rootshell.execution.Command;
import com.stericson.rootshell.execution.Shell;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeoutException;
public class RootShell {
// --------------------
// # Public Variables #
// --------------------
public static boolean debugMode = false;
public static final String version = "RootShell v1.4";
/**
* Setting this to false will disable the handler that is used
* by default for the 3 callback methods for Command.
* <p/>
* By disabling this all callbacks will be called from a thread other than
* the main UI thread.
*/
public static boolean handlerEnabled = true;
/**
* Setting this will change the default command timeout.
* <p/>
* The default is 20000ms
*/
public static int defaultCommandTimeout = 20000;
public enum LogLevel {
VERBOSE,
ERROR,
DEBUG,
WARN
}
// --------------------
// # Public Methods #
// --------------------
/**
* This will close all open shells.
*/
public static void closeAllShells() throws IOException {
Shell.closeAll();
}
/**
* This will close the custom shell that you opened.
*/
public static void closeCustomShell() throws IOException {
Shell.closeCustomShell();
}
/**
* This will close either the root shell or the standard shell depending on what you specify.
*
* @param root a <code>boolean</code> to specify whether to close the root shell or the standard shell.
*/
public static void closeShell(boolean root) throws IOException {
if (root) {
Shell.closeRootShell();
} else {
Shell.closeShell();
}
}
/**
* Use this to check whether or not a file exists on the filesystem.
*
* @param file String that represent the file, including the full path to the
* file and its name.
* @return a boolean that will indicate whether or not the file exists.
*/
public static boolean exists(final String file) {
return exists(file, false);
}
/**
* Use this to check whether or not a file OR directory exists on the filesystem.
*
* @param file String that represent the file OR the directory, including the full path to the
* file and its name.
* @param isDir boolean that represent whether or not we are looking for a directory
* @return a boolean that will indicate whether or not the file exists.
*/
public static boolean exists(final String file, boolean isDir) {
final List<String> result = new ArrayList<String>();
String cmdToExecute = "ls " + (isDir ? "-d " : " ");
Command command = new Command(0, false, cmdToExecute + file) {
@Override
public void commandOutput(int id, String line) {
RootShell.log(line);
result.add(line);
super.commandOutput(id, line);
}
};
try {
//Try without root...
RootShell.getShell(false).add(command);
commandWait(RootShell.getShell(false), command);
} catch (Exception e) {
RootShell.log("Exception: " + e);
return false;
}
for (String line : result) {
if (line.trim().equals(file)) {
return true;
}
}
result.clear();
command = new Command(0, false, cmdToExecute + file) {
@Override
public void commandOutput(int id, String line) {
RootShell.log(line);
result.add(line);
super.commandOutput(id, line);
}
};
try {
RootShell.getShell(true).add(command);
commandWait(RootShell.getShell(true), command);
} catch (Exception e) {
RootShell.log("Exception: " + e);
return false;
}
//Avoid concurrent modification...
List<String> final_result = new ArrayList<String>(result);
for (String line : final_result) {
if (line.trim().equals(file)) {
return true;
}
}
return false;
}
/**
* @param binaryName String that represent the binary to find.
* @param singlePath boolean that represents whether to return a single path or multiple.
*
* @return <code>List<String></code> containing the locations the binary was found at.
*/
public static List<String> findBinary(String binaryName, boolean singlePath) {
return findBinary(binaryName, null, singlePath);
}
/**
* @param binaryName <code>String</code> that represent the binary to find.
* @param searchPaths <code>List<String></code> which contains the paths to search for this binary in.
* @param singlePath boolean that represents whether to return a single path or multiple.
*
* @return <code>List<String></code> containing the locations the binary was found at.
*/
public static List<String> findBinary(final String binaryName, List<String> searchPaths, boolean singlePath) {
final List<String> foundPaths = new ArrayList<String>();
boolean found = false;
if(searchPaths == null)
{
searchPaths = RootShell.getPath();
}
RootShell.log("Checking for " + binaryName);
//Try to use stat first
try {
for (String path : searchPaths) {
if(!path.endsWith("/"))
{
path += "/";
}
final String currentPath = path;
Command cc = new Command(0, false, "stat " + path + binaryName) {
@Override
public void commandOutput(int id, String line) {
if (line.contains("File: ") && line.contains(binaryName)) {
foundPaths.add(currentPath);
RootShell.log(binaryName + " was found here: " + currentPath);
}
RootShell.log(line);
super.commandOutput(id, line);
}
};
cc = RootShell.getShell(false).add(cc);
commandWait(RootShell.getShell(false), cc);
if(foundPaths.size() > 0 && singlePath) {
break;
}
}
found = !foundPaths.isEmpty();
} catch (Exception e) {
RootShell.log(binaryName + " was not found, more information MAY be available with Debugging on.");
}
if (!found) {
RootShell.log("Trying second method");
for (String path : searchPaths) {
if(!path.endsWith("/"))
{
path += "/";
}
if (RootShell.exists(path + binaryName)) {
RootShell.log(binaryName + " was found here: " + path);
foundPaths.add(path);
if(foundPaths.size() > 0 && singlePath) {
break;
}
} else {
RootShell.log(binaryName + " was NOT found here: " + path);
}
}
}
Collections.reverse(foundPaths);
return foundPaths;
}
/**
* This will open or return, if one is already open, a custom shell, you are responsible for managing the shell, reading the output
* and for closing the shell when you are done using it.
*
* @param shellPath a <code>String</code> to Indicate the path to the shell that you want to open.
* @param timeout an <code>int</code> to Indicate the length of time before giving up on opening a shell.
* @throws TimeoutException
* @throws com.stericson.RootShell.exceptions.RootDeniedException
* @throws IOException
*/
public static Shell getCustomShell(String shellPath, int timeout) throws IOException, TimeoutException, RootDeniedException
{
return RootShell.getCustomShell(shellPath, timeout);
}
/**
* This will return the environment variable PATH
*
* @return <code>List<String></code> A List of Strings representing the environment variable $PATH
*/
public static List<String> getPath() {
return Arrays.asList(System.getenv("PATH").split(":"));
}
/**
* This will open or return, if one is already open, a shell, you are responsible for managing the shell, reading the output
* and for closing the shell when you are done using it.
*
* @param root a <code>boolean</code> to Indicate whether or not you want to open a root shell or a standard shell
* @param timeout an <code>int</code> to Indicate the length of time to wait before giving up on opening a shell.
* @param shellContext the context to execute the shell with
* @param retry a <code>int</code> to indicate how many times the ROOT shell should try to open with root priviliges...
*/
public static Shell getShell(boolean root, int timeout, Shell.ShellContext shellContext, int retry) throws IOException, TimeoutException, RootDeniedException {
if (root) {
return Shell.startRootShell(timeout, shellContext, retry);
} else {
return Shell.startShell(timeout);
}
}
/**
* This will open or return, if one is already open, a shell, you are responsible for managing the shell, reading the output
* and for closing the shell when you are done using it.
*
* @param root a <code>boolean</code> to Indicate whether or not you want to open a root shell or a standard shell
* @param timeout an <code>int</code> to Indicate the length of time to wait before giving up on opening a shell.
* @param shellContext the context to execute the shell with
*/
public static Shell getShell(boolean root, int timeout, Shell.ShellContext shellContext) throws IOException, TimeoutException, RootDeniedException {
return getShell(root, timeout, shellContext, 3);
}
/**
* This will open or return, if one is already open, a shell, you are responsible for managing the shell, reading the output
* and for closing the shell when you are done using it.
*
* @param root a <code>boolean</code> to Indicate whether or not you want to open a root shell or a standard shell
* @param shellContext the context to execute the shell with
*/
public static Shell getShell(boolean root, Shell.ShellContext shellContext) throws IOException, TimeoutException, RootDeniedException {
return getShell(root, 0, shellContext, 3);
}
/**
* This will open or return, if one is already open, a shell, you are responsible for managing the shell, reading the output
* and for closing the shell when you are done using it.
*
* @param root a <code>boolean</code> to Indicate whether or not you want to open a root shell or a standard shell
* @param timeout an <code>int</code> to Indicate the length of time to wait before giving up on opening a shell.
*/
public static Shell getShell(boolean root, int timeout) throws IOException, TimeoutException, RootDeniedException {
return getShell(root, timeout, Shell.defaultContext, 3);
}
/**
* This will open or return, if one is already open, a shell, you are responsible for managing the shell, reading the output
* and for closing the shell when you are done using it.
*
* @param root a <code>boolean</code> to Indicate whether or not you want to open a root shell or a standard shell
*/
public static Shell getShell(boolean root) throws IOException, TimeoutException, RootDeniedException {
return RootShell.getShell(root, 0);
}
/**
* @return <code>true</code> if your app has been given root access.
* @throws TimeoutException if this operation times out. (cannot determine if access is given)
*/
public static boolean isAccessGiven() {
return isAccessGiven(0,3);
}
public static boolean isAccessGiven(int timeout, int retry) {
final Set<String> ID = new HashSet<String>();
final int IAG = 158;
try {
RootShell.log("Checking for Root access");
Command command = new Command(IAG, false, "id") {
@Override
public void commandOutput(int id, String line) {
if (id == IAG) {
ID.addAll(Arrays.asList(line.split(" ")));
}
super.commandOutput(id, line);
}
};
Shell.startRootShell().add(command);
commandWait(Shell.startRootShell(), command);
//parse the userid
for (String userid : ID) {
RootShell.log(userid);
if (userid.toLowerCase().contains("uid=0")) {
RootShell.log("Access Given");
return true;
}
}
return false;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* @return <code>true</code> if BusyBox or Toybox was found.
*/
public static boolean isBusyboxAvailable()
{
return (findBinary("busybox", true)).size() > 0;
}
/**
* @return <code>true</code> if su was found.
*/
public static boolean isRootAvailable() {
return (findBinary("su", true)).size() > 0;
}
/**
* This method allows you to output debug messages only when debugging is on. This will allow
* you to add a debug option to your app, which by default can be left off for performance.
* However, when you need debugging information, a simple switch can enable it and provide you
* with detailed logging.
* <p/>
* This method handles whether or not to log the information you pass it depending whether or
* not RootShell.debugMode is on. So you can use this and not have to worry about handling it
* yourself.
*
* @param msg The message to output.
*/
public static void log(String msg) {
log(null, msg, LogLevel.DEBUG, null);
}
/**
* This method allows you to output debug messages only when debugging is on. This will allow
* you to add a debug option to your app, which by default can be left off for performance.
* However, when you need debugging information, a simple switch can enable it and provide you
* with detailed logging.
* <p/>
* This method handles whether or not to log the information you pass it depending whether or
* not RootShell.debugMode is on. So you can use this and not have to worry about handling it
* yourself.
*
* @param TAG Optional parameter to define the tag that the Log will use.
* @param msg The message to output.
*/
public static void log(String TAG, String msg) {
log(TAG, msg, LogLevel.DEBUG, null);
}
/**
* This method allows you to output debug messages only when debugging is on. This will allow
* you to add a debug option to your app, which by default can be left off for performance.
* However, when you need debugging information, a simple switch can enable it and provide you
* with detailed logging.
* <p/>
* This method handles whether or not to log the information you pass it depending whether or
* not RootShell.debugMode is on. So you can use this and not have to worry about handling it
* yourself.
*
* @param msg The message to output.
* @param type The type of log, 1 for verbose, 2 for error, 3 for debug, 4 for warn
* @param e The exception that was thrown (Needed for errors)
*/
public static void log(String msg, LogLevel type, Exception e) {
log(null, msg, type, e);
}
/**
* This method allows you to check whether logging is enabled.
* Yes, it has a goofy name, but that's to keep it as short as possible.
* After all writing logging calls should be painless.
* This method exists to save Android going through the various Java layers
* that are traversed any time a string is created (i.e. what you are logging)
* <p/>
* Example usage:
* if(islog) {
* StrinbBuilder sb = new StringBuilder();
* // ...
* // build string
* // ...
* log(sb.toString());
* }
*
* @return true if logging is enabled
*/
public static boolean islog() {
return debugMode;
}
/**
* This method allows you to output debug messages only when debugging is on. This will allow
* you to add a debug option to your app, which by default can be left off for performance.
* However, when you need debugging information, a simple switch can enable it and provide you
* with detailed logging.
* <p/>
* This method handles whether or not to log the information you pass it depending whether or
* not RootShell.debugMode is on. So you can use this and not have to worry about handling it
* yourself.
*
* @param TAG Optional parameter to define the tag that the Log will use.
* @param msg The message to output.
* @param type The type of log, 1 for verbose, 2 for error, 3 for debug
* @param e The exception that was thrown (Needed for errors)
*/
public static void log(String TAG, String msg, LogLevel type, Exception e) {
if (msg != null && !msg.equals("")) {
if (debugMode) {
if (TAG == null) {
TAG = version;
}
switch (type) {
case VERBOSE:
Log.v(TAG, msg);
break;
case ERROR:
Log.e(TAG, msg, e);
break;
case DEBUG:
Log.d(TAG, msg);
break;
case WARN:
Log.w(TAG, msg);
break;
}
}
}
}
// --------------------
// # Public Methods #
// --------------------
private static void commandWait(Shell shell, Command cmd) throws Exception {
while (!cmd.isFinished()) {
RootShell.log(version, shell.getCommandQueuePositionString(cmd));
RootShell.log(version, "Processed " + cmd.totalOutputProcessed + " of " + cmd.totalOutput + " output from command.");
synchronized (cmd) {
try {
if (!cmd.isFinished()) {
cmd.wait(2000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (!cmd.isExecuting() && !cmd.isFinished()) {
if (!shell.isExecuting && !shell.isReading) {
RootShell.log(version, "Waiting for a command to be executed in a shell that is not executing and not reading! \n\n Command: " + cmd.getCommand());
Exception e = new Exception();
e.setStackTrace(Thread.currentThread().getStackTrace());
e.printStackTrace();
} else if (shell.isExecuting && !shell.isReading) {
RootShell.log(version, "Waiting for a command to be executed in a shell that is executing but not reading! \n\n Command: " + cmd.getCommand());
Exception e = new Exception();
e.setStackTrace(Thread.currentThread().getStackTrace());
e.printStackTrace();
} else {
RootShell.log(version, "Waiting for a command to be executed in a shell that is not reading! \n\n Command: " + cmd.getCommand());
Exception e = new Exception();
e.setStackTrace(Thread.currentThread().getStackTrace());
e.printStackTrace();
}
}
}
}
}

View file

@ -0,0 +1,415 @@
/*
* This file is part of the RootShell Project: http://code.google.com/p/RootShell/
*
* Copyright (c) 2012 Stephen Erickson, Chris Ravenscroft, Dominik Schuermann, Adam Shanks
*
* This code is dual-licensed under the terms of the Apache License Version 2.0 and
* the terms of the General Public License (GPL) Version 2.
* You may use this code according to either of these licenses as is most appropriate
* for your project on a case-by-case basis.
*
* The terms of each license can be found in the root directory of this project's repository as well as at:
*
* * http://www.apache.org/licenses/LICENSE-2.0
* * http://www.gnu.org/licenses/gpl-2.0.txt
*
* Unless required by applicable law or agreed to in writing, software
* distributed under these Licenses is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See each License for the specific language governing permissions and
* limitations under that License.
*/
package com.stericson.rootshell;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.StrictMode;
import android.widget.ScrollView;
import android.widget.TextView;
import com.stericson.rootshell.exceptions.RootDeniedException;
import com.stericson.rootshell.execution.Command;
import com.stericson.rootshell.execution.Shell;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.TimeoutException;
public class SanityCheckRootShell extends Activity
{
private ScrollView mScrollView;
private TextView mTextView;
private ProgressDialog mPDialog;
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectNetwork() // or .detectAll() for all detectable problems
.penaltyLog()
.build());
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
.detectLeakedSqlLiteObjects()
.detectLeakedClosableObjects()
.penaltyLog()
.penaltyDeath()
.build());
RootShell.debugMode = true;
mTextView = new TextView(this);
mTextView.setText("");
mScrollView = new ScrollView(this);
mScrollView.addView(mTextView);
setContentView(mScrollView);
print("SanityCheckRootShell \n\n");
if (RootShell.isRootAvailable())
{
print("Root found.\n");
}
else
{
print("Root not found");
}
try
{
RootShell.getShell(true);
}
catch (IOException e2)
{
// TODO Auto-generated catch block
e2.printStackTrace();
}
catch (TimeoutException e)
{
print("[ TIMEOUT EXCEPTION! ]\n");
e.printStackTrace();
}
catch (RootDeniedException e)
{
print("[ ROOT DENIED EXCEPTION! ]\n");
e.printStackTrace();
}
try
{
if (!RootShell.isAccessGiven())
{
print("ERROR: No root access to this device.\n");
return;
}
}
catch (Exception e)
{
print("ERROR: could not determine root access to this device.\n");
return;
}
// Display infinite progress bar
mPDialog = new ProgressDialog(this);
mPDialog.setCancelable(false);
mPDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
new SanityCheckThread(this, new TestHandler()).start();
}
protected void print(CharSequence text)
{
mTextView.append(text);
mScrollView.post(new Runnable()
{
public void run()
{
mScrollView.fullScroll(ScrollView.FOCUS_DOWN);
}
});
}
// Run our long-running tests in their separate thread so as to
// not interfere with proper rendering.
private class SanityCheckThread extends Thread
{
private final Handler mHandler;
public SanityCheckThread(Context context, Handler handler)
{
mHandler = handler;
}
public void run()
{
visualUpdate(TestHandler.ACTION_SHOW, null);
// First test: Install a binary file for future use
// if it wasn't already installed.
/*
visualUpdate(TestHandler.ACTION_PDISPLAY, "Installing binary if needed");
if(false == RootShell.installBinary(mContext, R.raw.nes, "nes_binary")) {
visualUpdate(TestHandler.ACTION_HIDE, "ERROR: Failed to install binary. Please see log file.");
return;
}
*/
boolean result;
visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing getPath");
visualUpdate(TestHandler.ACTION_DISPLAY, "[ getPath ]\n");
try
{
List<String> paths = RootShell.getPath();
for (String path : paths)
{
visualUpdate(TestHandler.ACTION_DISPLAY, path + " k\n\n");
}
}
catch (Exception e)
{
e.printStackTrace();
}
visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing A ton of commands");
visualUpdate(TestHandler.ACTION_DISPLAY, "[ Ton of Commands ]\n");
for (int i = 0; i < 100; i++)
{
RootShell.exists("/system/xbin/busybox");
}
visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing Find Binary");
result = RootShell.isRootAvailable();
visualUpdate(TestHandler.ACTION_DISPLAY, "[ Checking Root ]\n");
visualUpdate(TestHandler.ACTION_DISPLAY, result + " k\n\n");
result = RootShell.isBusyboxAvailable();
visualUpdate(TestHandler.ACTION_DISPLAY, "[ Checking Busybox ]\n");
visualUpdate(TestHandler.ACTION_DISPLAY, result + " k\n\n");
visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing file exists");
visualUpdate(TestHandler.ACTION_DISPLAY, "[ Checking Exists() ]\n");
visualUpdate(TestHandler.ACTION_DISPLAY, RootShell.exists("/system/sbin/[") + " k\n\n");
visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing Is Access Given");
result = RootShell.isAccessGiven();
visualUpdate(TestHandler.ACTION_DISPLAY, "[ Checking for Access to Root ]\n");
visualUpdate(TestHandler.ACTION_DISPLAY, result + " k\n\n");
Shell shell;
visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing output capture");
visualUpdate(TestHandler.ACTION_DISPLAY, "[ busybox ash --help ]\n");
try
{
shell = RootShell.getShell(true);
Command cmd = new Command(
0,
"busybox ash --help")
{
@Override
public void commandOutput(int id, String line)
{
visualUpdate(TestHandler.ACTION_DISPLAY, line + "\n");
//super.commandOutput(id, line);
}
};
shell.add(cmd);
}
catch (Exception e)
{
e.printStackTrace();
}
visualUpdate(TestHandler.ACTION_PDISPLAY, "Switching RootContext - SYSTEM_APP");
visualUpdate(TestHandler.ACTION_DISPLAY, "[ Switching Root Context - SYSTEM_APP ]\n");
try
{
shell = RootShell.getShell(true, Shell.ShellContext.SYSTEM_APP);
Command cmd = new Command(
0,
"id")
{
@Override
public void commandOutput(int id, String line)
{
visualUpdate(TestHandler.ACTION_DISPLAY, line + "\n");
super.commandOutput(id, line);
}
};
shell.add(cmd);
}
catch (Exception e)
{
e.printStackTrace();
}
visualUpdate(TestHandler.ACTION_PDISPLAY, "Switching RootContext - UNTRUSTED");
visualUpdate(TestHandler.ACTION_DISPLAY, "[ Switching Root Context - UNTRUSTED ]\n");
try
{
shell = RootShell.getShell(true, Shell.ShellContext.UNTRUSTED_APP);
Command cmd = new Command(
0,
"id")
{
@Override
public void commandOutput(int id, String line)
{
visualUpdate(TestHandler.ACTION_DISPLAY, line + "\n");
super.commandOutput(id, line);
}
};
shell.add(cmd);
}
catch (Exception e)
{
e.printStackTrace();
}
try
{
shell = RootShell.getShell(true);
Command cmd = new Command(42, false, "echo done")
{
boolean _catch = false;
@Override
public void commandOutput(int id, String line)
{
if (_catch)
{
RootShell.log("CAUGHT!!!");
}
super.commandOutput(id, line);
}
@Override
public void commandTerminated(int id, String reason)
{
synchronized (SanityCheckRootShell.this)
{
_catch = true;
visualUpdate(TestHandler.ACTION_PDISPLAY, "All tests complete.");
visualUpdate(TestHandler.ACTION_HIDE, null);
try
{
RootShell.closeAllShells();
}
catch (IOException e)
{
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
@Override
public void commandCompleted(int id, int exitCode)
{
synchronized (SanityCheckRootShell.this)
{
_catch = true;
visualUpdate(TestHandler.ACTION_PDISPLAY, "All tests complete.");
visualUpdate(TestHandler.ACTION_HIDE, null);
try
{
RootShell.closeAllShells();
}
catch (IOException e)
{
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
};
shell.add(cmd);
}
catch (Exception e)
{
e.printStackTrace();
}
}
private void visualUpdate(int action, String text)
{
Message msg = mHandler.obtainMessage();
Bundle bundle = new Bundle();
bundle.putInt(TestHandler.ACTION, action);
bundle.putString(TestHandler.TEXT, text);
msg.setData(bundle);
mHandler.sendMessage(msg);
}
}
private class TestHandler extends Handler
{
static final public String ACTION = "action";
static final public int ACTION_SHOW = 0x01;
static final public int ACTION_HIDE = 0x02;
static final public int ACTION_DISPLAY = 0x03;
static final public int ACTION_PDISPLAY = 0x04;
static final public String TEXT = "text";
public void handleMessage(Message msg)
{
int action = msg.getData().getInt(ACTION);
String text = msg.getData().getString(TEXT);
switch (action)
{
case ACTION_SHOW:
mPDialog.show();
mPDialog.setMessage("Running Root Library Tests...");
break;
case ACTION_HIDE:
if (null != text)
{ print(text); }
mPDialog.hide();
break;
case ACTION_DISPLAY:
print(text);
break;
case ACTION_PDISPLAY:
mPDialog.setMessage(text);
break;
}
}
}
}

View file

@ -0,0 +1,328 @@
package com.stericson.rootshell.containers;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileFilter;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FilenameFilter;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/* #ANNOTATIONS @SupportedAnnotationTypes("com.stericson.RootShell.containers.RootClass.Candidate") */
/* #ANNOTATIONS @SupportedSourceVersion(SourceVersion.RELEASE_6) */
public class RootClass /* #ANNOTATIONS extends AbstractProcessor */ {
/* #ANNOTATIONS
@Override
public boolean process(Set<? extends TypeElement> typeElements, RoundEnvironment roundEnvironment) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "I was invoked!!!");
return false;
}
*/
static String PATH_TO_DX = "/Users/Chris/Projects/android-sdk-macosx/build-tools/18.0.1/dx";
enum READ_STATE {
STARTING, FOUND_ANNOTATION
}
public RootClass(String[] args) throws ClassNotFoundException, NoSuchMethodException,
IllegalAccessException, InvocationTargetException, InstantiationException {
// Note: rather than calling System.load("/system/lib/libandroid_runtime.so");
// which would leave a bunch of unresolved JNI references,
// we are using the 'withFramework' class as a preloader.
// So, yeah, russian dolls: withFramework > RootClass > actual method
String className = args[0];
RootArgs actualArgs = new RootArgs();
actualArgs.args = new String[args.length - 1];
System.arraycopy(args, 1, actualArgs.args, 0, args.length - 1);
Class<?> classHandler = Class.forName(className);
Constructor<?> classConstructor = classHandler.getConstructor(RootArgs.class);
classConstructor.newInstance(actualArgs);
}
public @interface Candidate {
}
public static class RootArgs {
public String[] args;
}
static void displayError(Exception e) {
// Not using system.err to make it easier to capture from
// calling library.
System.out.println("##ERR##" + e.getMessage() + "##");
e.printStackTrace();
}
// I reckon it would be better to investigate classes using getAttribute()
// however this method allows the developer to simply select "Run" on RootClass
// and immediately re-generate the necessary jar file.
static public class AnnotationsFinder {
private final String AVOIDDIRPATH = "stericson" + File.separator + "RootShell" + File.separator;
private final List<File> classFiles;
public AnnotationsFinder() throws IOException {
System.out.println("Discovering root class annotations...");
classFiles = new ArrayList<File>();
lookup(new File("src"), classFiles);
System.out.println("Done discovering annotations. Building jar file.");
File builtPath = getBuiltPath();
if (null != builtPath) {
// Android! Y U no have com.google.common.base.Joiner class?
String rc1 = "com" + File.separator
+ "stericson" + File.separator
+ "RootShell" + File.separator
+ "containers" + File.separator
+ "RootClass.class";
String rc2 = "com" + File.separator
+ "stericson" + File.separator
+ "RootShell" + File.separator
+ "containers" + File.separator
+ "RootClass$RootArgs.class";
String rc3 = "com" + File.separator
+ "stericson" + File.separator
+ "RootShell" + File.separator
+ "containers" + File.separator
+ "RootClass$AnnotationsFinder.class";
String rc4 = "com" + File.separator
+ "stericson" + File.separator
+ "RootShell" + File.separator
+ "containers" + File.separator
+ "RootClass$AnnotationsFinder$1.class";
String rc5 = "com" + File.separator
+ "stericson" + File.separator
+ "RootShell" + File.separator
+ "containers" + File.separator
+ "RootClass$AnnotationsFinder$2.class";
String[] cmd;
boolean onWindows = (-1 != System.getProperty("os.name").toLowerCase().indexOf("win"));
if (onWindows) {
StringBuilder sb = new StringBuilder(
" " + rc1 + " " + rc2 + " " + rc3 + " " + rc4 + " " + rc5
);
for (File file : classFiles) {
sb.append(" ").append(file.getPath());
}
cmd = new String[]{
"cmd", "/C",
"jar cvf" +
" anbuild.jar" +
sb.toString()
};
} else {
ArrayList<String> al = new ArrayList<String>();
al.add("jar");
al.add("cf");
al.add("anbuild.jar");
al.add(rc1);
al.add(rc2);
al.add(rc3);
al.add(rc4);
al.add(rc5);
for (File file : classFiles) {
al.add(file.getPath());
}
cmd = al.toArray(new String[0]);
}
ProcessBuilder jarBuilder = new ProcessBuilder(cmd);
jarBuilder.directory(builtPath);
try {
jarBuilder.start().waitFor();
} catch (IOException e) {
} catch (InterruptedException e) {
}
File rawFolder = new File("res" + File.separator + "raw");
if (!rawFolder.exists()) {
rawFolder.mkdirs();
}
System.out.println("Done building jar file. Creating dex file.");
if (onWindows) {
cmd = new String[]{
"cmd", "/C",
"dx --dex --output=res" + File.separator + "raw" + File.separator + "anbuild.dex "
+ builtPath + File.separator + "anbuild.jar"
};
} else {
cmd = new String[]{
getPathToDx(),
"--dex",
"--output=res" + File.separator + "raw" + File.separator + "anbuild.dex",
builtPath + File.separator + "anbuild.jar"
};
}
ProcessBuilder dexBuilder = new ProcessBuilder(cmd);
try {
dexBuilder.start().waitFor();
} catch (IOException e) {
} catch (InterruptedException e) {
}
}
System.out.println("All done. ::: anbuild.dex should now be in your project's res" + File.separator + "raw" + File.separator + " folder :::");
}
protected void lookup(File path, List<File> fileList) {
String desourcedPath = path.toString().replace("src" + File.separator, "");
File[] files = path.listFiles();
for (File file : files) {
if (file.isDirectory()) {
if (-1 == file.getAbsolutePath().indexOf(AVOIDDIRPATH)) {
lookup(file, fileList);
}
} else {
if (file.getName().endsWith(".java")) {
if (hasClassAnnotation(file)) {
final String fileNamePrefix = file.getName().replace(".java", "");
final File compiledPath = new File(getBuiltPath().toString() + File.separator + desourcedPath);
File[] classAndInnerClassFiles = compiledPath.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String filename) {
return filename.startsWith(fileNamePrefix);
}
});
for (final File matchingFile : classAndInnerClassFiles) {
fileList.add(new File(desourcedPath + File.separator + matchingFile.getName()));
}
}
}
}
}
}
protected boolean hasClassAnnotation(File file) {
READ_STATE readState = READ_STATE.STARTING;
Pattern p = Pattern.compile(" class ([A-Za-z0-9_]+)");
try {
BufferedReader reader = new BufferedReader(new FileReader(file));
String line;
while (null != (line = reader.readLine())) {
switch (readState) {
case STARTING:
if (-1 < line.indexOf("@RootClass.Candidate")) {
readState = READ_STATE.FOUND_ANNOTATION;
}
break;
case FOUND_ANNOTATION:
Matcher m = p.matcher(line);
if (m.find()) {
System.out.println(" Found annotated class: " + m.group(0));
return true;
} else {
System.err.println("Error: unmatched annotation in " +
file.getAbsolutePath());
readState = READ_STATE.STARTING;
}
break;
}
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return false;
}
protected String getPathToDx() throws IOException {
String androidHome = System.getenv("ANDROID_HOME");
if (null == androidHome) {
throw new IOException("Error: you need to set $ANDROID_HOME globally");
}
String dxPath = null;
File[] files = new File(androidHome + File.separator + "build-tools").listFiles();
int recentSdkVersion = 0;
for (File file : files) {
String fileName = null;
if (file.getName().contains("-")) {
String[] splitFileName = file.getName().split("-");
if (splitFileName[1].contains("W")) {
char[] fileNameChars = splitFileName[1].toCharArray();
fileName = String.valueOf(fileNameChars[0]);
} else {
fileName = splitFileName[1];
}
} else {
fileName = file.getName();
}
int sdkVersion;
String[] sdkVersionBits = fileName.split("[.]");
sdkVersion = Integer.parseInt(sdkVersionBits[0]) * 10000;
if (sdkVersionBits.length > 1) {
sdkVersion += Integer.parseInt(sdkVersionBits[1]) * 100;
if (sdkVersionBits.length > 2) {
sdkVersion += Integer.parseInt(sdkVersionBits[2]);
}
}
if (sdkVersion > recentSdkVersion) {
String tentativePath = file.getAbsolutePath() + File.separator + "dx";
if (new File(tentativePath).exists()) {
recentSdkVersion = sdkVersion;
dxPath = tentativePath;
}
}
}
if (dxPath == null) {
throw new IOException("Error: unable to find dx binary in $ANDROID_HOME");
}
return dxPath;
}
protected File getBuiltPath() {
File foundPath = null;
File ideaPath = new File("out" + File.separator + "production"); // IntelliJ
if (ideaPath.isDirectory()) {
File[] children = ideaPath.listFiles(new FileFilter() {
@Override
public boolean accept(File pathname) {
return pathname.isDirectory();
}
});
if (children.length > 0) {
foundPath = new File(ideaPath.getAbsolutePath() + File.separator + children[0].getName());
}
}
if (null == foundPath) {
File eclipsePath = new File("bin" + File.separator + "classes"); // Eclipse IDE
if (eclipsePath.isDirectory()) {
foundPath = eclipsePath;
}
}
return foundPath;
}
}
public static void main(String[] args) {
try {
if (args.length == 0) {
new AnnotationsFinder();
} else {
new RootClass(args);
}
} catch (Exception e) {
displayError(e);
}
}
}

View file

@ -0,0 +1,32 @@
/*
* This file is part of the RootShell Project: https://github.com/Stericson/RootShell
*
* Copyright (c) 2014 Stephen Erickson, Chris Ravenscroft
*
* This code is dual-licensed under the terms of the Apache License Version 2.0 and
* the terms of the General Public License (GPL) Version 2.
* You may use this code according to either of these licenses as is most appropriate
* for your project on a case-by-case basis.
*
* The terms of each license can be found in the root directory of this project's repository as well as at:
*
* * http://www.apache.org/licenses/LICENSE-2.0
* * http://www.gnu.org/licenses/gpl-2.0.txt
*
* Unless required by applicable law or agreed to in writing, software
* distributed under these Licenses is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See each License for the specific language governing permissions and
* limitations under that License.
*/
package com.stericson.rootshell.exceptions;
public class RootDeniedException extends Exception {
private static final long serialVersionUID = -8713947214162841310L;
public RootDeniedException(String error) {
super(error);
}
}

View file

@ -0,0 +1,325 @@
/*
* This file is part of the RootShell Project: http://code.google.com/p/RootShell/
*
* Copyright (c) 2014 Stephen Erickson, Chris Ravenscroft
*
* This code is dual-licensed under the terms of the Apache License Version 2.0 and
* the terms of the General Public License (GPL) Version 2.
* You may use this code according to either of these licenses as is most appropriate
* for your project on a case-by-case basis.
*
* The terms of each license can be found in the root directory of this project's repository as well as at:
*
* * http://www.apache.org/licenses/LICENSE-2.0
* * http://www.gnu.org/licenses/gpl-2.0.txt
*
* Unless required by applicable law or agreed to in writing, software
* distributed under these Licenses is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See each License for the specific language governing permissions and
* limitations under that License.
*/
package com.stericson.rootshell.execution;
import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import com.stericson.rootshell.RootShell;
import java.io.IOException;
public class Command {
//directly modified by JavaCommand
protected boolean javaCommand = false;
protected Context context = null;
public int totalOutput = 0;
public int totalOutputProcessed = 0;
ExecutionMonitor executionMonitor = null;
Handler mHandler = null;
//Has this command already been used?
protected boolean used = false;
boolean executing = false;
String[] command = {};
boolean finished = false;
boolean terminated = false;
boolean handlerEnabled = true;
int exitCode = -1;
int id = 0;
int timeout = RootShell.defaultCommandTimeout;
/**
* Constructor for executing a normal shell command
*
* @param id the id of the command being executed
* @param command the command, or commands, to be executed.
*/
public Command(int id, String... command) {
this.command = command;
this.id = id;
createHandler(RootShell.handlerEnabled);
}
/**
* Constructor for executing a normal shell command
*
* @param id the id of the command being executed
* @param handlerEnabled when true the handler will be used to call the
* callback methods if possible.
* @param command the command, or commands, to be executed.
*/
public Command(int id, boolean handlerEnabled, String... command) {
this.command = command;
this.id = id;
createHandler(handlerEnabled);
}
/**
* Constructor for executing a normal shell command
*
* @param id the id of the command being executed
* @param timeout the time allowed before the shell will give up executing the command
* and throw a TimeoutException.
* @param command the command, or commands, to be executed.
*/
public Command(int id, int timeout, String... command) {
this.command = command;
this.id = id;
this.timeout = timeout;
createHandler(RootShell.handlerEnabled);
}
//If you override this you MUST make a final call
//to the super method. The super call should be the last line of this method.
public void commandOutput(int id, String line) {
RootShell.log("Command", "ID: " + id + ", " + line);
totalOutputProcessed++;
}
public void commandTerminated(int id, String reason) {
//pass
}
public void commandCompleted(int id, int exitcode) {
//pass
}
protected final void commandFinished() {
if (!terminated) {
synchronized (this) {
if (mHandler != null && handlerEnabled) {
Message msg = mHandler.obtainMessage();
Bundle bundle = new Bundle();
bundle.putInt(CommandHandler.ACTION, CommandHandler.COMMAND_COMPLETED);
msg.setData(bundle);
mHandler.sendMessage(msg);
} else {
commandCompleted(id, exitCode);
}
RootShell.log("Command " + id + " finished.");
finishCommand();
}
}
}
private void createHandler(boolean handlerEnabled) {
this.handlerEnabled = handlerEnabled;
if (Looper.myLooper() != null && handlerEnabled) {
RootShell.log("CommandHandler created");
mHandler = new CommandHandler();
} else {
RootShell.log("CommandHandler not created");
}
}
public final void finish()
{
RootShell.log("Command finished at users request!");
commandFinished();
}
protected final void finishCommand() {
this.executing = false;
this.finished = true;
this.notifyAll();
}
public final String getCommand() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < command.length; i++) {
if (i > 0) {
sb.append('\n');
}
sb.append(command[i]);
}
return sb.toString();
}
public final boolean isExecuting() {
return executing;
}
public final boolean isHandlerEnabled() {
return handlerEnabled;
}
public final boolean isFinished() {
return finished;
}
public final int getExitCode() {
return this.exitCode;
}
protected final void setExitCode(int code) {
synchronized (this) {
exitCode = code;
}
}
protected final void startExecution() {
this.used = true;
executionMonitor = new ExecutionMonitor(this);
executionMonitor.setPriority(Thread.MIN_PRIORITY);
executionMonitor.start();
executing = true;
}
public final void terminate()
{
RootShell.log("Terminating command at users request!");
terminated("Terminated at users request!");
}
protected final void terminate(String reason) {
try {
Shell.closeAll();
RootShell.log("Terminating all shells.");
terminated(reason);
} catch (IOException e) {
}
}
protected final void terminated(String reason) {
synchronized (Command.this) {
if (mHandler != null && handlerEnabled) {
Message msg = mHandler.obtainMessage();
Bundle bundle = new Bundle();
bundle.putInt(CommandHandler.ACTION, CommandHandler.COMMAND_TERMINATED);
bundle.putString(CommandHandler.TEXT, reason);
msg.setData(bundle);
mHandler.sendMessage(msg);
} else {
commandTerminated(id, reason);
}
RootShell.log("Command " + id + " did not finish because it was terminated. Termination reason: " + reason);
setExitCode(-1);
terminated = true;
finishCommand();
}
}
protected final void output(int id, String line) {
totalOutput++;
if (mHandler != null && handlerEnabled) {
Message msg = mHandler.obtainMessage();
Bundle bundle = new Bundle();
bundle.putInt(CommandHandler.ACTION, CommandHandler.COMMAND_OUTPUT);
bundle.putString(CommandHandler.TEXT, line);
msg.setData(bundle);
mHandler.sendMessage(msg);
} else {
commandOutput(id, line);
}
}
private class ExecutionMonitor extends Thread {
private final Command command;
public ExecutionMonitor(Command command) {
this.command = command;
}
public void run() {
if(command.timeout > 0)
{
synchronized (command) {
try {
RootShell.log("Command " + command.id + " is waiting for: " + command.timeout);
command.wait(command.timeout);
} catch (InterruptedException e) {
RootShell.log("Exception: " + e);
}
if (!command.isFinished()) {
RootShell.log("Timeout Exception has occurred for command: " + command.id + ".");
terminate("Timeout Exception");
}
}
}
}
}
private class CommandHandler extends Handler {
static final public String ACTION = "action";
static final public String TEXT = "text";
static final public int COMMAND_OUTPUT = 0x01;
static final public int COMMAND_COMPLETED = 0x02;
static final public int COMMAND_TERMINATED = 0x03;
public final void handleMessage(Message msg) {
int action = msg.getData().getInt(ACTION);
String text = msg.getData().getString(TEXT);
switch (action) {
case COMMAND_OUTPUT:
commandOutput(id, text);
break;
case COMMAND_COMPLETED:
commandCompleted(id, exitCode);
break;
case COMMAND_TERMINATED:
commandTerminated(id, text);
break;
}
}
}
}

View file

@ -0,0 +1,58 @@
package com.stericson.rootshell.execution;
import android.content.Context;
public class JavaCommand extends Command
{
/**
* Constructor for executing Java commands rather than binaries
*
* @param context needed to execute java command.
*/
public JavaCommand(int id, Context context, String... command) {
super(id, command);
this.context = context;
this.javaCommand = true;
}
/**
* Constructor for executing Java commands rather than binaries
*
* @param context needed to execute java command.
*/
public JavaCommand(int id, boolean handlerEnabled, Context context, String... command) {
super(id, handlerEnabled, command);
this.context = context;
this.javaCommand = true;
}
/**
* Constructor for executing Java commands rather than binaries
*
* @param context needed to execute java command.
*/
public JavaCommand(int id, int timeout, Context context, String... command) {
super(id, timeout, command);
this.context = context;
this.javaCommand = true;
}
@Override
public void commandOutput(int id, String line)
{
super.commandOutput(id, line);
}
@Override
public void commandTerminated(int id, String reason)
{
// pass
}
@Override
public void commandCompleted(int id, int exitCode)
{
// pass
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,15 @@
package com.stericson.roottools;
public class Constants
{
public static final String TAG = "RootTools v4.4";
public static final int FPS = 1;
public static final int BBA = 3;
public static final int BBV = 4;
public static final int GI = 5;
public static final int GS = 6;
public static final int GSYM = 7;
public static final int GET_MOUNTS = 8;
public static final int GET_SYMLINKS = 9;
}

View file

@ -0,0 +1,848 @@
/*
* This file is part of the RootTools Project: http://code.google.com/p/RootTools/
*
* Copyright (c) 2012 Stephen Erickson, Chris Ravenscroft, Dominik Schuermann, Adam Shanks
*
* This code is dual-licensed under the terms of the Apache License Version 2.0 and
* the terms of the General Public License (GPL) Version 2.
* You may use this code according to either of these licenses as is most appropriate
* for your project on a case-by-case basis.
*
* The terms of each license can be found in the root directory of this project's repository as well as at:
*
* * http://www.apache.org/licenses/LICENSE-2.0
* * http://www.gnu.org/licenses/gpl-2.0.txt
*
* Unless required by applicable law or agreed to in writing, software
* distributed under these Licenses is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See each License for the specific language governing permissions and
* limitations under that License.
*/
package com.stericson.roottools;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import com.stericson.rootshell.RootShell;
import com.stericson.rootshell.exceptions.RootDeniedException;
import com.stericson.rootshell.execution.Command;
import com.stericson.rootshell.execution.Shell;
import com.stericson.roottools.containers.Mount;
import com.stericson.roottools.containers.Permissions;
import com.stericson.roottools.containers.Symlink;
import com.stericson.roottools.internal.Remounter;
import com.stericson.roottools.internal.RootToolsInternalMethods;
import com.stericson.roottools.internal.Runner;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeoutException;
public final class RootTools {
/**
* This class is the gateway to every functionality within the RootTools library.The developer
* should only have access to this class and this class only.This means that this class should
* be the only one to be public.The rest of the classes within this library must not have the
* public modifier.
* <p/>
* All methods and Variables that the developer may need to have access to should be here.
* <p/>
* If a method, or a specific functionality, requires a fair amount of code, or work to be done,
* then that functionality should probably be moved to its own class and the call to it done
* here.For examples of this being done, look at the remount functionality.
*/
private static RootToolsInternalMethods rim = null;
public static void setRim(RootToolsInternalMethods rim) {
RootTools.rim = rim;
}
private static final RootToolsInternalMethods getInternals() {
if (rim == null) {
RootToolsInternalMethods.getInstance();
return rim;
} else {
return rim;
}
}
// --------------------
// # Public Variables #
// --------------------
public static boolean debugMode = false;
public static String utilPath;
/**
* Setting this to false will disable the handler that is used
* by default for the 3 callback methods for Command.
* <p/>
* By disabling this all callbacks will be called from a thread other than
* the main UI thread.
*/
public static boolean handlerEnabled = true;
/**
* Setting this will change the default command timeout.
* <p/>
* The default is 20000ms
*/
public static int default_Command_Timeout = 20000;
// ---------------------------
// # Public Variable Getters #
// ---------------------------
// ------------------
// # Public Methods #
// ------------------
/**
* This will check a given binary, determine if it exists and determine that it has either the
* permissions 755, 775, or 777.
*
* @param util Name of the utility to check.
* @return boolean to indicate whether the binary is installed and has appropriate permissions.
*/
public static boolean checkUtil(String util) {
return getInternals().checkUtil(util);
}
/**
* This will close all open shells.
*
* @throws IOException
*/
public static void closeAllShells() throws IOException {
RootShell.closeAllShells();
}
/**
* This will close the custom shell that you opened.
*
* @throws IOException
*/
public static void closeCustomShell() throws IOException {
RootShell.closeCustomShell();
}
/**
* This will close either the root shell or the standard shell depending on what you specify.
*
* @param root a <code>boolean</code> to specify whether to close the root shell or the standard shell.
* @throws IOException
*/
public static void closeShell(boolean root) throws IOException {
RootShell.closeShell(root);
}
/**
* Copys a file to a destination. Because cp is not available on all android devices, we have a
* fallback on the cat command
*
* @param source example: /data/data/org.adaway/files/hosts
* @param destination example: /system/etc/hosts
* @param remountAsRw remounts the destination as read/write before writing to it
* @param preserveFileAttributes tries to copy file attributes from source to destination, if only cat is available
* only permissions are preserved
* @return true if it was successfully copied
*/
public static boolean copyFile(String source, String destination, boolean remountAsRw,
boolean preserveFileAttributes) {
return getInternals().copyFile(source, destination, remountAsRw, preserveFileAttributes);
}
/**
* Deletes a file or directory
*
* @param target example: /data/data/org.adaway/files/hosts
* @param remountAsRw remounts the destination as read/write before writing to it
* @return true if it was successfully deleted
*/
public static boolean deleteFileOrDirectory(String target, boolean remountAsRw) {
return getInternals().deleteFileOrDirectory(target, remountAsRw);
}
/**
* Use this to check whether or not a file exists on the filesystem.
*
* @param file String that represent the file, including the full path to the
* file and its name.
* @return a boolean that will indicate whether or not the file exists.
*/
public static boolean exists(final String file) {
return exists(file, false);
}
/**
* Use this to check whether or not a file OR directory exists on the filesystem.
*
* @param file String that represent the file OR the directory, including the full path to the
* file and its name.
* @param isDir boolean that represent whether or not we are looking for a directory
* @return a boolean that will indicate whether or not the file exists.
*/
public static boolean exists(final String file, boolean isDir) {
return RootShell.exists(file, isDir);
}
/**
* This will try and fix a given binary. (This is for Busybox applets or Toolbox applets) By
* "fix", I mean it will try and symlink the binary from either toolbox or Busybox and fix the
* permissions if the permissions are not correct.
*
* @param util Name of the utility to fix.
* @param utilPath path to the toolbox that provides ln, rm, and chmod. This can be a blank string, a
* path to a binary that will provide these, or you can use
* RootTools.getWorkingToolbox()
*/
public static void fixUtil(String util, String utilPath) {
getInternals().fixUtil(util, utilPath);
}
/**
* This will check an array of binaries, determine if they exist and determine that it has
* either the permissions 755, 775, or 777. If an applet is not setup correctly it will try and
* fix it. (This is for Busybox applets or Toolbox applets)
*
* @param utils Name of the utility to check.
* @return boolean to indicate whether the operation completed. Note that this is not indicative
* of whether the problem was fixed, just that the method did not encounter any
* exceptions.
* @throws Exception if the operation cannot be completed.
*/
public static boolean fixUtils(String[] utils) throws Exception {
return getInternals().fixUtils(utils);
}
/**
* @param binaryName String that represent the binary to find.
* @param singlePath boolean that represents whether to return a single path or multiple.
*
* @return <code>List<String></code> containing the paths the binary was found at.
*/
public static List<String> findBinary(String binaryName, boolean singlePath) {
return RootShell.findBinary(binaryName, singlePath);
}
/**
* @param path String that represents the path to the Busybox binary you want to retrieve the version of.
* @return BusyBox version is found, "" if not found.
*/
public static String getBusyBoxVersion(String path) {
return getInternals().getBusyBoxVersion(path);
}
/**
* @return BusyBox version is found, "" if not found.
*/
public static String getBusyBoxVersion() {
return RootTools.getBusyBoxVersion("");
}
/**
* This will return an List of Strings. Each string represents an applet available from BusyBox.
* <p/>
*
* @return <code>null</code> If we cannot return the list of applets.
*/
public static List<String> getBusyBoxApplets() throws Exception {
return RootTools.getBusyBoxApplets("");
}
/**
* This will return an List of Strings. Each string represents an applet available from BusyBox.
* <p/>
*
* @param path Path to the busybox binary that you want the list of applets from.
* @return <code>null</code> If we cannot return the list of applets.
*/
public static List<String> getBusyBoxApplets(String path) throws Exception {
return getInternals().getBusyBoxApplets(path);
}
/**
* This will open or return, if one is already open, a custom shell, you are responsible for managing the shell, reading the output
* and for closing the shell when you are done using it.
*
* @param shellPath a <code>String</code> to Indicate the path to the shell that you want to open.
* @param timeout an <code>int</code> to Indicate the length of time before giving up on opening a shell.
* @throws TimeoutException
* @throws com.stericson.RootShell.exceptions.RootDeniedException
* @throws IOException
*/
public static Shell getCustomShell(String shellPath, int timeout) throws IOException, TimeoutException, RootDeniedException {
return RootShell.getCustomShell(shellPath, timeout);
}
/**
* This will open or return, if one is already open, a custom shell, you are responsible for managing the shell, reading the output
* and for closing the shell when you are done using it.
*
* @param shellPath a <code>String</code> to Indicate the path to the shell that you want to open.
* @throws TimeoutException
* @throws com.stericson.RootShell.exceptions.RootDeniedException
* @throws IOException
*/
public static Shell getCustomShell(String shellPath) throws IOException, TimeoutException, RootDeniedException {
return RootTools.getCustomShell(shellPath, 10000);
}
/**
* @param file String that represent the file, including the full path to the file and its name.
* @return An instance of the class permissions from which you can get the permissions of the
* file or if the file could not be found or permissions couldn't be determined then
* permissions will be null.
*/
public static Permissions getFilePermissionsSymlinks(String file) {
return getInternals().getFilePermissionsSymlinks(file);
}
/**
* This method will return the inode number of a file. This method is dependent on having a version of
* ls that supports the -i parameter.
*
* @param file path to the file that you wish to return the inode number
* @return String The inode number for this file or "" if the inode number could not be found.
*/
public static String getInode(String file) {
return getInternals().getInode(file);
}
/**
* This will return an ArrayList of the class Mount. The class mount contains the following
* property's: device mountPoint type flags
* <p/>
* These will provide you with any information you need to work with the mount points.
*
* @return <code>ArrayList<Mount></code> an ArrayList of the class Mount.
* @throws Exception if we cannot return the mount points.
*/
public static ArrayList<Mount> getMounts() throws Exception {
return getInternals().getMounts();
}
/**
* This will tell you how the specified mount is mounted. rw, ro, etc...
* <p/>
*
* @param path The mount you want to check
* @return <code>String</code> What the mount is mounted as.
* @throws Exception if we cannot determine how the mount is mounted.
*/
public static String getMountedAs(String path) throws Exception {
return getInternals().getMountedAs(path);
}
/**
* This will return the environment variable PATH
*
* @return <code>List<String></code> A List of Strings representing the environment variable $PATH
*/
public static List<String> getPath() {
return Arrays.asList(System.getenv("PATH").split(":"));
}
/**
* This will open or return, if one is already open, a shell, you are responsible for managing the shell, reading the output
* and for closing the shell when you are done using it.
*
* @param root a <code>boolean</code> to Indicate whether or not you want to open a root shell or a standard shell
* @param timeout an <code>int</code> to Indicate the length of time to wait before giving up on opening a shell.
* @param shellContext the context to execute the shell with
* @param retry a <code>int</code> to indicate how many times the ROOT shell should try to open with root priviliges...
* @throws TimeoutException
* @throws com.stericson.RootShell.exceptions.RootDeniedException
* @throws IOException
*/
public static Shell getShell(boolean root, int timeout, Shell.ShellContext shellContext, int retry) throws IOException, TimeoutException, RootDeniedException {
return RootShell.getShell(root, timeout, shellContext, retry);
}
/**
* This will open or return, if one is already open, a shell, you are responsible for managing the shell, reading the output
* and for closing the shell when you are done using it.
*
* @param root a <code>boolean</code> to Indicate whether or not you want to open a root shell or a standard shell
* @param timeout an <code>int</code> to Indicate the length of time to wait before giving up on opening a shell.
* @param shellContext the context to execute the shell with
* @throws TimeoutException
* @throws com.stericson.RootShell.exceptions.RootDeniedException
* @throws IOException
*/
public static Shell getShell(boolean root, int timeout, Shell.ShellContext shellContext) throws IOException, TimeoutException, RootDeniedException {
return getShell(root, timeout, shellContext, 3);
}
/**
* This will open or return, if one is already open, a shell, you are responsible for managing the shell, reading the output
* and for closing the shell when you are done using it.
*
* @param root a <code>boolean</code> to Indicate whether or not you want to open a root shell or a standard shell
* @param shellContext the context to execute the shell with
* @throws TimeoutException
* @throws com.stericson.RootShell.exceptions.RootDeniedException
* @throws IOException
*/
public static Shell getShell(boolean root, Shell.ShellContext shellContext) throws IOException, TimeoutException, RootDeniedException {
return getShell(root, 0, shellContext, 3);
}
/**
* This will open or return, if one is already open, a shell, you are responsible for managing the shell, reading the output
* and for closing the shell when you are done using it.
*
* @param root a <code>boolean</code> to Indicate whether or not you want to open a root shell or a standard shell
* @param timeout an <code>int</code> to Indicate the length of time to wait before giving up on opening a shell.
* @throws TimeoutException
* @throws com.stericson.RootShell.exceptions.RootDeniedException
* @throws IOException
*/
public static Shell getShell(boolean root, int timeout) throws IOException, TimeoutException, RootDeniedException {
return getShell(root, timeout, Shell.defaultContext, 3);
}
/**
* This will open or return, if one is already open, a shell, you are responsible for managing the shell, reading the output
* and for closing the shell when you are done using it.
*
* @param root a <code>boolean</code> to Indicate whether or not you want to open a root shell or a standard shell
* @throws TimeoutException
* @throws com.stericson.RootShell.exceptions.RootDeniedException
* @throws IOException
*/
public static Shell getShell(boolean root) throws IOException, TimeoutException, RootDeniedException {
return RootTools.getShell(root, 0);
}
/**
* Get the space for a desired partition.
*
* @param path The partition to find the space for.
* @return the amount if space found within the desired partition. If the space was not found
* then the value is -1
* @throws TimeoutException
*/
public static long getSpace(String path) {
return getInternals().getSpace(path);
}
/**
* This will return a String that represent the symlink for a specified file.
* <p/>
*
* @param file path to the file to get the Symlink for. (must have absolute path)
* @return <code>String</code> a String that represent the symlink for a specified file or an
* empty string if no symlink exists.
*/
public static String getSymlink(String file) {
return getInternals().getSymlink(file);
}
/**
* This will return an ArrayList of the class Symlink. The class Symlink contains the following
* property's: path SymplinkPath
* <p/>
* These will provide you with any Symlinks in the given path.
*
* @param path path to search for Symlinks.
* @return <code>ArrayList<Symlink></code> an ArrayList of the class Symlink.
* @throws Exception if we cannot return the Symlinks.
*/
public static ArrayList<Symlink> getSymlinks(String path) throws Exception {
return getInternals().getSymlinks(path);
}
/**
* This will return to you a string to be used in your shell commands which will represent the
* valid working toolbox with correct permissions. For instance, if Busybox is available it will
* return "busybox", if busybox is not available but toolbox is then it will return "toolbox"
*
* @return String that indicates the available toolbox to use for accessing applets.
*/
public static String getWorkingToolbox() {
return getInternals().getWorkingToolbox();
}
/**
* Checks if there is enough Space on SDCard
*
* @param updateSize size to Check (long)
* @return <code>true</code> if the Update will fit on SDCard, <code>false</code> if not enough
* space on SDCard. Will also return <code>false</code>, if the SDCard is not mounted as
* read/write
*/
public static boolean hasEnoughSpaceOnSdCard(long updateSize) {
return getInternals().hasEnoughSpaceOnSdCard(updateSize);
}
/**
* Checks whether the toolbox or busybox binary contains a specific util
*
* @param util
* @param box Should contain "toolbox" or "busybox"
* @return true if it contains this util
*/
public static boolean hasUtil(final String util, final String box) {
//TODO Convert this to use the new shell.
return getInternals().hasUtil(util, box);
}
/**
* This method can be used to unpack a binary from the raw resources folder and store it in
* /data/data/app.package/files/ This is typically useful if you provide your own C- or
* C++-based binary. This binary can then be executed using sendShell() and its full path.
*
* @param context the current activity's <code>Context</code>
* @param sourceId resource id; typically <code>R.raw.id</code>
* @param destName destination file name; appended to /data/data/app.package/files/
* @param mode chmod value for this file
* @return a <code>boolean</code> which indicates whether or not we were able to create the new
* file.
*/
public static boolean installBinary(Context context, int sourceId, String destName, String mode) {
return getInternals().installBinary(context, sourceId, destName, mode);
}
/**
* This method can be used to unpack a binary from the raw resources folder and store it in
* /data/data/app.package/files/ This is typically useful if you provide your own C- or
* C++-based binary. This binary can then be executed using sendShell() and its full path.
*
* @param context the current activity's <code>Context</code>
* @param sourceId resource id; typically <code>R.raw.id</code>
* @param binaryName destination file name; appended to /data/data/app.package/files/
* @return a <code>boolean</code> which indicates whether or not we were able to create the new
* file.
*/
public static boolean installBinary(Context context, int sourceId, String binaryName) {
return installBinary(context, sourceId, binaryName, "700");
}
/**
* This method checks whether a binary is installed.
*
* @param context the current activity's <code>Context</code>
* @param binaryName binary file name; appended to /data/data/app.package/files/
* @return a <code>boolean</code> which indicates whether or not
* the binary already exists.
*/
public static boolean hasBinary(Context context, String binaryName) {
return getInternals().isBinaryAvailable(context, binaryName);
}
/**
* This will let you know if an applet is available from BusyBox
* <p/>
*
* @param applet The applet to check for.
* @param path Path to the busybox binary that you want to check. (do not include binary name)
* @return <code>true</code> if applet is available, false otherwise.
*/
public static boolean isAppletAvailable(String applet, String path) {
return getInternals().isAppletAvailable(applet, path);
}
/**
* This will let you know if an applet is available from BusyBox
* <p/>
*
* @param applet The applet to check for.
* @return <code>true</code> if applet is available, false otherwise.
*/
public static boolean isAppletAvailable(String applet) {
return RootTools.isAppletAvailable(applet, "");
}
/**
* @return <code>true</code> if your app has been given root access.
* @throws TimeoutException if this operation times out. (cannot determine if access is given)
*/
public static boolean isAccessGiven() {
return RootShell.isAccessGiven();
}
/**
* Control how many time of retries should request
*
* @param timeout The timeout
* @param retries The number of retries
*
* @return <code>true</code> if your app has been given root access.
* @throws TimeoutException if this operation times out. (cannot determine if access is given)
*/
public static boolean isAccessGiven(int timeout, int retries) {
return RootShell.isAccessGiven(timeout, retries);
}
/**
* @return <code>true</code> if BusyBox was found.
*/
public static boolean isBusyboxAvailable() {
return RootShell.isBusyboxAvailable();
}
public static boolean isNativeToolsReady(int nativeToolsId, Context context) {
return getInternals().isNativeToolsReady(nativeToolsId, context);
}
/**
* This method can be used to to check if a process is running
*
* @param processName name of process to check
* @return <code>true</code> if process was found
* @throws TimeoutException (Could not determine if the process is running)
*/
public static boolean isProcessRunning(final String processName) {
//TODO convert to new shell
return getInternals().isProcessRunning(processName);
}
/**
* @return <code>true</code> if su was found.
*/
public static boolean isRootAvailable() {
return RootShell.isRootAvailable();
}
/**
* This method can be used to kill a running process
*
* @param processName name of process to kill
* @return <code>true</code> if process was found and killed successfully
*/
public static boolean killProcess(final String processName) {
//TODO convert to new shell
return getInternals().killProcess(processName);
}
/**
* This will launch the Android market looking for BusyBox
*
* @param activity pass in your Activity
*/
public static void offerBusyBox(Activity activity) {
getInternals().offerBusyBox(activity);
}
/**
* This will launch the Android market looking for BusyBox, but will return the intent fired and
* starts the activity with startActivityForResult
*
* @param activity pass in your Activity
* @param requestCode pass in the request code
* @return intent fired
*/
public static Intent offerBusyBox(Activity activity, int requestCode) {
return getInternals().offerBusyBox(activity, requestCode);
}
/**
* This will launch the Android market looking for SuperUser
*
* @param activity pass in your Activity
*/
public static void offerSuperUser(Activity activity) {
getInternals().offerSuperUser(activity);
}
/**
* This will launch the Android market looking for SuperUser, but will return the intent fired
* and starts the activity with startActivityForResult
*
* @param activity pass in your Activity
* @param requestCode pass in the request code
* @return intent fired
*/
public static Intent offerSuperUser(Activity activity, int requestCode) {
return getInternals().offerSuperUser(activity, requestCode);
}
/**
* This will take a path, which can contain the file name as well, and attempt to remount the
* underlying partition.
* <p/>
* For example, passing in the following string:
* "/system/bin/some/directory/that/really/would/never/exist" will result in /system ultimately
* being remounted. However, keep in mind that the longer the path you supply, the more work
* this has to do, and the slower it will run.
*
* @param file file path
* @param mountType mount type: pass in RO (Read only) or RW (Read Write)
* @return a <code>boolean</code> which indicates whether or not the partition has been
* remounted as specified.
*/
public static boolean remount(String file, String mountType) {
// Recieved a request, get an instance of Remounter
Remounter remounter = new Remounter();
// send the request.
return (remounter.remount(file, mountType));
}
public static boolean remount(String file, String mountType, String customPath) {
// Recieved a request, get an instance of Remounter
Remounter remounter = new Remounter(customPath);
// send the request.
return (remounter.remount(file, mountType));
}
/**
* This restarts only Android OS without rebooting the whole device. This does NOT work on all
* devices. This is done by killing the main init process named zygote. Zygote is restarted
* automatically by Android after killing it.
*
* @throws TimeoutException
*/
/* public static void restartAndroid() {
RootTools.log("Restart Android");
killProcess("zygote");
}*/
/**
* Executes binary in a separated process. Before using this method, the binary has to be
* installed in /data/data/app.package/files/ using the installBinary method.
*
* @param context the current activity's <code>Context</code>
* @param binaryName name of installed binary
* @param parameter parameter to append to binary like "-vxf"
*/
public static void runBinary(Context context, String binaryName, String parameter) {
Runner runner = new Runner(context, binaryName, parameter);
runner.start();
}
/**
* Executes a given command with root access or without depending on the value of the boolean passed.
* This will also start a root shell or a standard shell without you having to open it specifically.
* <p/>
* You will still need to close the shell after you are done using the shell.
*
* @param shell The shell to execute the command on, this can be a root shell or a standard shell.
* @param command The command to execute in the shell
*
* @throws IOException
*/
public static void runShellCommand(Shell shell, Command command) throws IOException {
shell.add(command);
}
/**
* This method allows you to output debug messages only when debugging is on. This will allow
* you to add a debug option to your app, which by default can be left off for performance.
* However, when you need debugging information, a simple switch can enable it and provide you
* with detailed logging.
* <p/>
* This method handles whether or not to log the information you pass it depending whether or
* not RootTools.debugMode is on. So you can use this and not have to worry about handling it
* yourself.
*
* @param msg The message to output.
*/
public static void log(String msg) {
log(null, msg, 3, null);
}
/**
* This method allows you to output debug messages only when debugging is on. This will allow
* you to add a debug option to your app, which by default can be left off for performance.
* However, when you need debugging information, a simple switch can enable it and provide you
* with detailed logging.
* <p/>
* This method handles whether or not to log the information you pass it depending whether or
* not RootTools.debugMode is on. So you can use this and not have to worry about handling it
* yourself.
*
* @param TAG Optional parameter to define the tag that the Log will use.
* @param msg The message to output.
*/
public static void log(String TAG, String msg) {
log(TAG, msg, 3, null);
}
/**
* This method allows you to output debug messages only when debugging is on. This will allow
* you to add a debug option to your app, which by default can be left off for performance.
* However, when you need debugging information, a simple switch can enable it and provide you
* with detailed logging.
* <p/>
* This method handles whether or not to log the information you pass it depending whether or
* not RootTools.debugMode is on. So you can use this and not have to worry about handling it
* yourself.
*
* @param msg The message to output.
* @param type The type of log, 1 for verbose, 2 for error, 3 for debug
* @param e The exception that was thrown (Needed for errors)
*/
public static void log(String msg, int type, Exception e) {
log(null, msg, type, e);
}
/**
* This method allows you to check whether logging is enabled.
* Yes, it has a goofy name, but that's to keep it as short as possible.
* After all writing logging calls should be painless.
* This method exists to save Android going through the various Java layers
* that are traversed any time a string is created (i.e. what you are logging)
* <p/>
* Example usage:
* if(islog) {
* StrinbBuilder sb = new StringBuilder();
* // ...
* // build string
* // ...
* log(sb.toString());
* }
*
* @return true if logging is enabled
*/
public static boolean islog() {
return debugMode;
}
/**
* This method allows you to output debug messages only when debugging is on. This will allow
* you to add a debug option to your app, which by default can be left off for performance.
* However, when you need debugging information, a simple switch can enable it and provide you
* with detailed logging.
* <p/>
* This method handles whether or not to log the information you pass it depending whether or
* not RootTools.debugMode is on. So you can use this and not have to worry about handling it
* yourself.
*
* @param TAG Optional parameter to define the tag that the Log will use.
* @param msg The message to output.
* @param type The type of log, 1 for verbose, 2 for error, 3 for debug
* @param e The exception that was thrown (Needed for errors)
*/
public static void log(String TAG, String msg, int type, Exception e) {
if (msg != null && !msg.equals("")) {
if (debugMode) {
if (TAG == null) {
TAG = Constants.TAG;
}
switch (type) {
case 1:
Log.v(TAG, msg);
break;
case 2:
Log.e(TAG, msg, e);
break;
case 3:
Log.d(TAG, msg);
break;
}
}
}
}
}

View file

@ -0,0 +1,459 @@
/*
* This file is part of the RootTools Project: http://code.google.com/p/RootTools/
*
* Copyright (c) 2012 Stephen Erickson, Chris Ravenscroft, Dominik Schuermann, Adam Shanks
*
* This code is dual-licensed under the terms of the Apache License Version 2.0 and
* the terms of the General Public License (GPL) Version 2.
* You may use this code according to either of these licenses as is most appropriate
* for your project on a case-by-case basis.
*
* The terms of each license can be found in the root directory of this project's repository as well as at:
*
* * http://www.apache.org/licenses/LICENSE-2.0
* * http://www.gnu.org/licenses/gpl-2.0.txt
*
* Unless required by applicable law or agreed to in writing, software
* distributed under these Licenses is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See each License for the specific language governing permissions and
* limitations under that License.
*/
package com.stericson.roottools;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.StrictMode;
import android.widget.ScrollView;
import android.widget.TextView;
import com.stericson.rootshell.exceptions.RootDeniedException;
import com.stericson.rootshell.execution.Command;
import com.stericson.rootshell.execution.Shell;
import com.stericson.roottools.containers.Permissions;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.TimeoutException;
public class SanityCheckRootTools extends Activity {
private ScrollView mScrollView;
private TextView mTextView;
private ProgressDialog mPDialog;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectNetwork() // or .detectAll() for all detectable problems
.penaltyLog()
.build());
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
.detectLeakedSqlLiteObjects()
.detectLeakedClosableObjects()
.penaltyLog()
.penaltyDeath()
.build());
RootTools.debugMode = true;
mTextView = new TextView(this);
mTextView.setText("");
mScrollView = new ScrollView(this);
mScrollView.addView(mTextView);
setContentView(mScrollView);
print("SanityCheckRootTools \n\n");
if (RootTools.isRootAvailable()) {
print("Root found.\n");
} else {
print("Root not found");
}
try {
Shell.startRootShell();
} catch (IOException e2) {
// TODO Auto-generated catch block
e2.printStackTrace();
} catch (TimeoutException e) {
print("[ TIMEOUT EXCEPTION! ]\n");
e.printStackTrace();
} catch (RootDeniedException e) {
print("[ ROOT DENIED EXCEPTION! ]\n");
e.printStackTrace();
}
try {
if (!RootTools.isAccessGiven()) {
print("ERROR: No root access to this device.\n");
return;
}
} catch (Exception e) {
print("ERROR: could not determine root access to this device.\n");
return;
}
// Display infinite progress bar
mPDialog = new ProgressDialog(this);
mPDialog.setCancelable(false);
mPDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
new SanityCheckThread(this, new TestHandler()).start();
}
protected void print(CharSequence text) {
mTextView.append(text);
mScrollView.post(new Runnable() {
public void run() {
mScrollView.fullScroll(ScrollView.FOCUS_DOWN);
}
});
}
// Run our long-running tests in their separate thread so as to
// not interfere with proper rendering.
private class SanityCheckThread extends Thread {
private final Handler mHandler;
public SanityCheckThread(Context context, Handler handler) {
mHandler = handler;
}
public void run() {
visualUpdate(TestHandler.ACTION_SHOW, null);
// First test: Install a binary file for future use
// if it wasn't already installed.
/*
visualUpdate(TestHandler.ACTION_PDISPLAY, "Installing binary if needed");
if(false == RootTools.installBinary(mContext, R.raw.nes, "nes_binary")) {
visualUpdate(TestHandler.ACTION_HIDE, "ERROR: Failed to install binary. Please see log file.");
return;
}
*/
boolean result;
visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing getPath");
visualUpdate(TestHandler.ACTION_DISPLAY, "[ getPath ]\n");
try {
List<String> paths = RootTools.getPath();
for (String path : paths) {
visualUpdate(TestHandler.ACTION_DISPLAY, path + " k\n\n");
}
} catch (Exception e) {
e.printStackTrace();
}
visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing A ton of commands");
visualUpdate(TestHandler.ACTION_DISPLAY, "[ Ton of Commands ]\n");
for (int i = 0; i < 100; i++) {
RootTools.exists("/system/xbin/busybox");
}
visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing Find Binary");
result = RootTools.isRootAvailable();
visualUpdate(TestHandler.ACTION_DISPLAY, "[ Checking Root ]\n");
visualUpdate(TestHandler.ACTION_DISPLAY, result + " k\n\n");
visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing file exists");
visualUpdate(TestHandler.ACTION_DISPLAY, "[ Checking Exists() ]\n");
visualUpdate(TestHandler.ACTION_DISPLAY, RootTools.exists("/system/sbin/[") + " k\n\n");
visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing Is Access Given");
result = RootTools.isAccessGiven();
visualUpdate(TestHandler.ACTION_DISPLAY, "[ Checking for Access to Root ]\n");
visualUpdate(TestHandler.ACTION_DISPLAY, result + " k\n\n");
visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing Remount");
result = RootTools.remount("/system", "rw");
visualUpdate(TestHandler.ACTION_DISPLAY, "[ Remounting System as RW ]\n");
visualUpdate(TestHandler.ACTION_DISPLAY, result + " k\n\n");
visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing CheckUtil");
visualUpdate(TestHandler.ACTION_DISPLAY, "[ Checking busybox is setup ]\n");
visualUpdate(TestHandler.ACTION_DISPLAY, RootTools.checkUtil("busybox") + " k\n\n");
visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing getBusyBoxVersion");
visualUpdate(TestHandler.ACTION_DISPLAY, "[ Checking busybox version ]\n");
visualUpdate(TestHandler.ACTION_DISPLAY, RootTools.getBusyBoxVersion("/system/xbin/") + " k\n\n");
try {
visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing fixUtils");
visualUpdate(TestHandler.ACTION_DISPLAY, "[ Checking Utils ]\n");
visualUpdate(TestHandler.ACTION_DISPLAY, RootTools.fixUtils(new String[]{"ls", "rm", "ln", "dd", "chmod", "mount"}) + " k\n\n");
} catch (Exception e2) {
// TODO Auto-generated catch block
e2.printStackTrace();
}
try {
visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing getSymlink");
visualUpdate(TestHandler.ACTION_DISPLAY, "[ Checking [[ for symlink ]\n");
visualUpdate(TestHandler.ACTION_DISPLAY, RootTools.getSymlink("/system/bin/[[") + " k\n\n");
} catch (Exception e2) {
// TODO Auto-generated catch block
e2.printStackTrace();
}
visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing getInode");
visualUpdate(TestHandler.ACTION_DISPLAY, "[ Checking Inodes ]\n");
visualUpdate(TestHandler.ACTION_DISPLAY, RootTools.getInode("/system/bin/busybox") + " k\n\n");
visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing GetBusyBoxapplets");
try {
visualUpdate(TestHandler.ACTION_DISPLAY, "[ Getting all available Busybox applets ]\n");
for (String applet : RootTools.getBusyBoxApplets("/data/data/stericson.busybox/files/bb/busybox")) {
visualUpdate(TestHandler.ACTION_DISPLAY, applet + " k\n\n");
}
} catch (Exception e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing GetBusyBox version in a special directory!");
try {
visualUpdate(TestHandler.ACTION_DISPLAY, "[ Testing GetBusyBox version in a special directory! ]\n");
String v = RootTools.getBusyBoxVersion("/data/data/stericson.busybox/files/bb/");
visualUpdate(TestHandler.ACTION_DISPLAY, v + " k\n\n");
} catch (Exception e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing getFilePermissionsSymlinks");
Permissions permissions = RootTools.getFilePermissionsSymlinks("/system/xbin/busybox");
visualUpdate(TestHandler.ACTION_DISPLAY, "[ Checking busybox permissions and symlink ]\n");
if (permissions != null) {
visualUpdate(TestHandler.ACTION_DISPLAY, "Symlink: " + permissions.getSymlink() + " k\n\n");
visualUpdate(TestHandler.ACTION_DISPLAY, "Group Permissions: " + permissions.getGroupPermissions() + " k\n\n");
visualUpdate(TestHandler.ACTION_DISPLAY, "Owner Permissions: " + permissions.getOtherPermissions() + " k\n\n");
visualUpdate(TestHandler.ACTION_DISPLAY, "Permissions: " + permissions.getPermissions() + " k\n\n");
visualUpdate(TestHandler.ACTION_DISPLAY, "Type: " + permissions.getType() + " k\n\n");
visualUpdate(TestHandler.ACTION_DISPLAY, "User Permissions: " + permissions.getUserPermissions() + " k\n\n");
} else {
visualUpdate(TestHandler.ACTION_DISPLAY, "Permissions == null k\n\n");
}
Shell shell;
visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing output capture");
visualUpdate(TestHandler.ACTION_DISPLAY, "[ busybox ash --help ]\n");
try {
shell = RootTools.getShell(true);
Command cmd = new Command(
0,
"busybox ash --help") {
@Override
public void commandOutput(int id, String line) {
visualUpdate(TestHandler.ACTION_DISPLAY, line + "\n");
super.commandOutput(id, line);
}
};
shell.add(cmd);
visualUpdate(TestHandler.ACTION_PDISPLAY, "getevent - /dev/input/event0");
visualUpdate(TestHandler.ACTION_DISPLAY, "[ getevent - /dev/input/event0 ]\n");
cmd = new Command(0, 0, "getevent /dev/input/event0") {
@Override
public void commandOutput(int id, String line) {
visualUpdate(TestHandler.ACTION_DISPLAY, line + "\n");
super.commandOutput(id, line);
}
};
shell.add(cmd);
} catch (Exception e) {
e.printStackTrace();
}
visualUpdate(TestHandler.ACTION_PDISPLAY, "Switching RootContext - SYSTEM_APP");
visualUpdate(TestHandler.ACTION_DISPLAY, "[ Switching Root Context - SYSTEM_APP ]\n");
try {
shell = RootTools.getShell(true, Shell.ShellContext.SYSTEM_APP);
Command cmd = new Command(
0,
"id") {
@Override
public void commandOutput(int id, String line) {
visualUpdate(TestHandler.ACTION_DISPLAY, line + "\n");
super.commandOutput(id, line);
}
};
shell.add(cmd);
visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing PM");
visualUpdate(TestHandler.ACTION_DISPLAY, "[ Testing pm list packages -d ]\n");
cmd = new Command(
0,
"sh /system/bin/pm list packages -d") {
@Override
public void commandOutput(int id, String line) {
visualUpdate(TestHandler.ACTION_DISPLAY, line + "\n");
super.commandOutput(id, line);
}
};
shell.add(cmd);
} catch (Exception e) {
e.printStackTrace();
}
visualUpdate(TestHandler.ACTION_PDISPLAY, "Switching RootContext - UNTRUSTED");
visualUpdate(TestHandler.ACTION_DISPLAY, "[ Switching Root Context - UNTRUSTED ]\n");
try {
shell = RootTools.getShell(true, Shell.ShellContext.UNTRUSTED_APP);
Command cmd = new Command(
0,
"id") {
@Override
public void commandOutput(int id, String line) {
visualUpdate(TestHandler.ACTION_DISPLAY, line + "\n");
super.commandOutput(id, line);
}
};
shell.add(cmd);
} catch (Exception e) {
e.printStackTrace();
}
visualUpdate(TestHandler.ACTION_PDISPLAY, "Testing df");
long spaceValue = RootTools.getSpace("/data");
visualUpdate(TestHandler.ACTION_DISPLAY, "[ Checking /data partition size]\n");
visualUpdate(TestHandler.ACTION_DISPLAY, spaceValue + "k\n\n");
try {
shell = RootTools.getShell(true);
Command cmd = new Command(42, false, "echo done") {
boolean _catch = false;
@Override
public void commandOutput(int id, String line) {
if (_catch) {
RootTools.log("CAUGHT!!!");
}
super.commandOutput(id, line);
}
@Override
public void commandTerminated(int id, String reason) {
synchronized (SanityCheckRootTools.this) {
_catch = true;
visualUpdate(TestHandler.ACTION_PDISPLAY, "All tests complete.");
visualUpdate(TestHandler.ACTION_HIDE, null);
try {
RootTools.closeAllShells();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
@Override
public void commandCompleted(int id, int exitCode) {
synchronized (SanityCheckRootTools.this) {
_catch = true;
visualUpdate(TestHandler.ACTION_PDISPLAY, "All tests complete.");
visualUpdate(TestHandler.ACTION_HIDE, null);
try {
RootTools.closeAllShells();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
};
shell.add(cmd);
} catch (Exception e) {
e.printStackTrace();
}
}
private void visualUpdate(int action, String text) {
Message msg = mHandler.obtainMessage();
Bundle bundle = new Bundle();
bundle.putInt(TestHandler.ACTION, action);
bundle.putString(TestHandler.TEXT, text);
msg.setData(bundle);
mHandler.sendMessage(msg);
}
}
private class TestHandler extends Handler {
static final public String ACTION = "action";
static final public int ACTION_SHOW = 0x01;
static final public int ACTION_HIDE = 0x02;
static final public int ACTION_DISPLAY = 0x03;
static final public int ACTION_PDISPLAY = 0x04;
static final public String TEXT = "text";
public void handleMessage(Message msg) {
int action = msg.getData().getInt(ACTION);
String text = msg.getData().getString(TEXT);
switch (action) {
case ACTION_SHOW:
mPDialog.show();
mPDialog.setMessage("Running Root Library Tests...");
break;
case ACTION_HIDE:
if (null != text) {
print(text);
}
mPDialog.hide();
break;
case ACTION_DISPLAY:
print(text);
break;
case ACTION_PDISPLAY:
mPDialog.setMessage(text);
break;
}
}
}
}

View file

@ -0,0 +1,70 @@
/*
* This file is part of the RootTools Project: http://code.google.com/p/RootTools/
*
* Copyright (c) 2012 Stephen Erickson, Chris Ravenscroft, Dominik Schuermann, Adam Shanks
*
* This code is dual-licensed under the terms of the Apache License Version 2.0 and
* the terms of the General Public License (GPL) Version 2.
* You may use this code according to either of these licenses as is most appropriate
* for your project on a case-by-case basis.
*
* The terms of each license can be found in the root directory of this project's repository as well as at:
*
* * http://www.apache.org/licenses/LICENSE-2.0
* * http://www.gnu.org/licenses/gpl-2.0.txt
*
* Unless required by applicable law or agreed to in writing, software
* distributed under these Licenses is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See each License for the specific language governing permissions and
* limitations under that License.
*/
package com.stericson.roottools.containers;
import java.io.File;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.Set;
public class Mount
{
final File mDevice;
final File mMountPoint;
final String mType;
final Set<String> mFlags;
public Mount(File device, File path, String type, String flagsStr)
{
mDevice = device;
mMountPoint = path;
mType = type;
mFlags = new LinkedHashSet<String>(Arrays.asList(flagsStr.split(",")));
}
public File getDevice()
{
return mDevice;
}
public File getMountPoint()
{
return mMountPoint;
}
public String getType()
{
return mType;
}
public Set<String> getFlags()
{
return mFlags;
}
@Override
public String toString()
{
return String.format("%s on %s type %s %s", mDevice, mMountPoint, mType, mFlags);
}
}

View file

@ -0,0 +1,125 @@
/*
* This file is part of the RootTools Project: http://code.google.com/p/RootTools/
*
* Copyright (c) 2012 Stephen Erickson, Chris Ravenscroft, Dominik Schuermann, Adam Shanks
*
* This code is dual-licensed under the terms of the Apache License Version 2.0 and
* the terms of the General Public License (GPL) Version 2.
* You may use this code according to either of these licenses as is most appropriate
* for your project on a case-by-case basis.
*
* The terms of each license can be found in the root directory of this project's repository as well as at:
*
* * http://www.apache.org/licenses/LICENSE-2.0
* * http://www.gnu.org/licenses/gpl-2.0.txt
*
* Unless required by applicable law or agreed to in writing, software
* distributed under these Licenses is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See each License for the specific language governing permissions and
* limitations under that License.
*/
package com.stericson.roottools.containers;
public class Permissions
{
String type;
String user;
String group;
String other;
String symlink;
int permissions;
public String getSymlink()
{
return this.symlink;
}
public String getType()
{
return type;
}
public int getPermissions()
{
return this.permissions;
}
public String getUserPermissions()
{
return this.user;
}
public String getGroupPermissions()
{
return this.group;
}
public String getOtherPermissions()
{
return this.other;
}
public void setSymlink(String symlink)
{
this.symlink = symlink;
}
public void setType(String type)
{
this.type = type;
}
public void setPermissions(int permissions)
{
this.permissions = permissions;
}
public void setUserPermissions(String user)
{
this.user = user;
}
public void setGroupPermissions(String group)
{
this.group = group;
}
public void setOtherPermissions(String other)
{
this.other = other;
}
public String getUser()
{
return user;
}
public void setUser(String user)
{
this.user = user;
}
public String getGroup()
{
return group;
}
public void setGroup(String group)
{
this.group = group;
}
public String getOther()
{
return other;
}
public void setOther(String other)
{
this.other = other;
}
}

View file

@ -0,0 +1,47 @@
/*
* This file is part of the RootTools Project: http://code.google.com/p/RootTools/
*
* Copyright (c) 2012 Stephen Erickson, Chris Ravenscroft, Dominik Schuermann, Adam Shanks
*
* This code is dual-licensed under the terms of the Apache License Version 2.0 and
* the terms of the General Public License (GPL) Version 2.
* You may use this code according to either of these licenses as is most appropriate
* for your project on a case-by-case basis.
*
* The terms of each license can be found in the root directory of this project's repository as well as at:
*
* * http://www.apache.org/licenses/LICENSE-2.0
* * http://www.gnu.org/licenses/gpl-2.0.txt
*
* Unless required by applicable law or agreed to in writing, software
* distributed under these Licenses is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See each License for the specific language governing permissions and
* limitations under that License.
*/
package com.stericson.roottools.containers;
import java.io.File;
public class Symlink
{
protected final File file;
protected final File symlinkPath;
public Symlink(File file, File path)
{
this.file = file;
symlinkPath = path;
}
public File getFile()
{
return this.file;
}
public File getSymlinkPath()
{
return symlinkPath;
}
}

View file

@ -0,0 +1,300 @@
/*
* This file is part of the RootTools Project: http://code.google.com/p/RootTools/
*
* Copyright (c) 2012 Stephen Erickson, Chris Ravenscroft, Dominik Schuermann, Adam Shanks
*
* This code is dual-licensed under the terms of the Apache License Version 2.0 and
* the terms of the General Public License (GPL) Version 2.
* You may use this code according to either of these licenses as is most appropriate
* for your project on a case-by-case basis.
*
* The terms of each license can be found in the root directory of this project's repository as well as at:
*
* * http://www.apache.org/licenses/LICENSE-2.0
* * http://www.gnu.org/licenses/gpl-2.0.txt
*
* Unless required by applicable law or agreed to in writing, software
* distributed under these Licenses is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See each License for the specific language governing permissions and
* limitations under that License.
*/
package com.stericson.roottools.internal;
import android.content.Context;
import android.util.Log;
import com.stericson.rootshell.execution.Command;
import com.stericson.rootshell.execution.Shell;
import com.stericson.roottools.RootTools;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
class Installer
{
//-------------
//# Installer #
//-------------
static final String LOG_TAG = "RootTools::Installer";
static final String BOGUS_FILE_NAME = "bogus";
Context context;
String filesPath;
public Installer(Context context)
throws IOException
{
this.context = context;
this.filesPath = context.getFilesDir().getCanonicalPath();
}
/**
* This method can be used to unpack a binary from the raw resources folder and store it in
* /data/data/app.package/files/
* This is typically useful if you provide your own C- or C++-based binary.
* This binary can then be executed using sendShell() and its full path.
*
* @param sourceId resource id; typically <code>R.raw.id</code>
* @param destName destination file name; appended to /data/data/app.package/files/
* @param mode chmod value for this file
* @return a <code>boolean</code> which indicates whether or not we were
* able to create the new file.
*/
protected boolean installBinary(int sourceId, String destName, String mode)
{
File mf = new File(filesPath + File.separator + destName);
if (!mf.exists() ||
!getFileSignature(mf).equals(
getStreamSignature(
context.getResources().openRawResource(sourceId))
))
{
Log.e(LOG_TAG, "Installing a new version of binary: " + destName);
// First, does our files/ directory even exist?
// We cannot wait for android to lazily create it as we will soon
// need it.
try
{
FileInputStream fis = context.openFileInput(BOGUS_FILE_NAME);
fis.close();
}
catch (FileNotFoundException e)
{
FileOutputStream fos = null;
try
{
fos = context.openFileOutput("bogus", Context.MODE_PRIVATE);
fos.write("justcreatedfilesdirectory".getBytes());
}
catch (Exception ex)
{
if (RootTools.debugMode)
{
Log.e(LOG_TAG, ex.toString());
}
return false;
}
finally
{
if (null != fos)
{
try
{
fos.close();
context.deleteFile(BOGUS_FILE_NAME);
}
catch (IOException e1)
{
}
}
}
}
catch (IOException ex)
{
if (RootTools.debugMode)
{
Log.e(LOG_TAG, ex.toString());
}
return false;
}
// Only now can we start creating our actual file
InputStream iss = context.getResources().openRawResource(sourceId);
ReadableByteChannel rfc = Channels.newChannel(iss);
FileOutputStream oss = null;
try
{
oss = new FileOutputStream(mf);
FileChannel ofc = oss.getChannel();
long pos = 0;
try
{
long size = iss.available();
while ((pos += ofc.transferFrom(rfc, pos, size - pos)) < size)
{
}
}
catch (IOException ex)
{
if (RootTools.debugMode)
{
Log.e(LOG_TAG, ex.toString());
}
return false;
}
}
catch (FileNotFoundException ex)
{
if (RootTools.debugMode)
{
Log.e(LOG_TAG, ex.toString());
}
return false;
}
finally
{
if (oss != null)
{
try
{
oss.flush();
oss.getFD().sync();
oss.close();
}
catch (Exception e)
{
}
}
}
try
{
iss.close();
}
catch (IOException ex)
{
if (RootTools.debugMode)
{
Log.e(LOG_TAG, ex.toString());
}
return false;
}
try
{
Command command = new Command(0, false, "chmod " + mode + " " + filesPath + File.separator + destName);
Shell.startRootShell().add(command);
commandWait(command);
}
catch (Exception e)
{
}
}
return true;
}
protected boolean isBinaryInstalled(String destName)
{
boolean installed = false;
File mf = new File(filesPath + File.separator + destName);
if (mf.exists())
{
installed = true;
// TODO: pass mode as argument and check it matches
}
return installed;
}
protected String getFileSignature(File f)
{
String signature = "";
try
{
signature = getStreamSignature(new FileInputStream(f));
}
catch (FileNotFoundException ex)
{
Log.e(LOG_TAG, ex.toString());
}
return signature;
}
/*
* Note: this method will close any string passed to it
*/
protected String getStreamSignature(InputStream is)
{
String signature = "";
try
{
MessageDigest md = MessageDigest.getInstance("MD5");
DigestInputStream dis = new DigestInputStream(is, md);
byte[] buffer = new byte[4096];
while (-1 != dis.read(buffer))
{
}
byte[] digest = md.digest();
StringBuffer sb = new StringBuffer();
for (int i = 0; i < digest.length; i++)
{
sb.append(Integer.toHexString(digest[i] & 0xFF));
}
signature = sb.toString();
}
catch (IOException ex)
{
Log.e(LOG_TAG, ex.toString());
}
catch (NoSuchAlgorithmException ex)
{
Log.e(LOG_TAG, ex.toString());
}
finally
{
try
{
is.close();
}
catch (IOException e)
{
}
}
return signature;
}
private void commandWait(Command cmd)
{
synchronized (cmd)
{
try
{
if (!cmd.isFinished())
{
cmd.wait(2000);
}
}
catch (InterruptedException ex)
{
Log.e(LOG_TAG, ex.toString());
}
}
}
}

View file

@ -0,0 +1,62 @@
/*
* This file is part of the RootTools Project: http://code.google.com/p/RootTools/
*
* Copyright (c) 2012 Stephen Erickson, Chris Ravenscroft, Dominik Schuermann, Adam Shanks
*
* This code is dual-licensed under the terms of the Apache License Version 2.0 and
* the terms of the General Public License (GPL) Version 2.
* You may use this code according to either of these licenses as is most appropriate
* for your project on a case-by-case basis.
*
* The terms of each license can be found in the root directory of this project's repository as well as at:
*
* * http://www.apache.org/licenses/LICENSE-2.0
* * http://www.gnu.org/licenses/gpl-2.0.txt
*
* Unless required by applicable law or agreed to in writing, software
* distributed under these Licenses is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See each License for the specific language governing permissions and
* limitations under that License.
*/
package com.stericson.roottools.internal;
import com.stericson.roottools.containers.Mount;
import com.stericson.roottools.containers.Permissions;
import com.stericson.roottools.containers.Symlink;
import java.util.ArrayList;
import java.util.regex.Pattern;
public class InternalVariables
{
// ----------------------
// # Internal Variables #
// ----------------------
protected static boolean nativeToolsReady = false;
protected static boolean found = false;
protected static boolean processRunning = false;
protected static String[] space;
protected static String getSpaceFor;
protected static String busyboxVersion;
protected static String pid_list = "";
protected static ArrayList<Mount> mounts;
protected static ArrayList<Symlink> symlinks;
protected static String inode = "";
protected static Permissions permissions;
// regex to get pid out of ps line, example:
// root 2611 0.0 0.0 19408 2104 pts/2 S 13:41 0:00 bash
protected static final String PS_REGEX = "^\\S+\\s+([0-9]+).*$";
protected static Pattern psPattern;
static
{
psPattern = Pattern.compile(PS_REGEX);
}
}

View file

@ -0,0 +1,238 @@
/*
* This file is part of the RootTools Project: http://code.google.com/p/RootTools/
*
* Copyright (c) 2012 Stephen Erickson, Chris Ravenscroft, Dominik Schuermann, Adam Shanks
*
* This code is dual-licensed under the terms of the Apache License Version 2.0 and
* the terms of the General Public License (GPL) Version 2.
* You may use this code according to either of these licenses as is most appropriate
* for your project on a case-by-case basis.
*
* The terms of each license can be found in the root directory of this project's repository as well as at:
*
* * http://www.apache.org/licenses/LICENSE-2.0
* * http://www.gnu.org/licenses/gpl-2.0.txt
*
* Unless required by applicable law or agreed to in writing, software
* distributed under these Licenses is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See each License for the specific language governing permissions and
* limitations under that License.
*/
package com.stericson.roottools.internal;
import com.stericson.rootshell.execution.Command;
import com.stericson.rootshell.execution.Shell;
import com.stericson.roottools.Constants;
import com.stericson.roottools.RootTools;
import com.stericson.roottools.containers.Mount;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import dev.ukanth.ufirewall.Api;
import dev.ukanth.ufirewall.log.Log;
public class Remounter
{
private String customPath;
public Remounter() {
}
public Remounter(String path) {
this.customPath = path;
}
//-------------
//# Remounter #
//-------------
/**
* This will take a path, which can contain the file name as well,
* and attempt to remount the underlying partition.
* <p/>
* For example, passing in the following string:
* "/system/bin/some/directory/that/really/would/never/exist"
* will result in /system ultimately being remounted.
* However, keep in mind that the longer the path you supply, the more work this has to do,
* and the slower it will run.
*
* @param file file path
* @param mountType mount type: pass in RO (Read only) or RW (Read Write)
* @return a <code>boolean</code> which indicates whether or not the partition
* has been remounted as specified.
*/
public boolean remount(String file, String mountType)
{
//if the path has a trailing slash get rid of it.
if (file.endsWith("/") && !file.equals("/"))
{
file = file.substring(0, file.lastIndexOf("/"));
}
//Make sure that what we are trying to remount is in the mount list.
boolean foundMount = false;
while (!foundMount)
{
try
{
for (Mount mount : RootTools.getMounts())
{
RootTools.log(mount.getMountPoint().toString());
if (file.equals(mount.getMountPoint().toString()))
{
foundMount = true;
break;
}
}
}
catch (Exception e)
{
if (RootTools.debugMode)
{
Log.d(Api.TAG, e.getMessage(), e);
}
return false;
}
if (!foundMount)
{
try
{
file = (new File(file).getParent());
}
catch (Exception e)
{
Log.e(Api.TAG, e.getMessage(), e);
return false;
}
}
}
Mount mountPoint = findMountPointRecursive(file);
if (mountPoint != null)
{
RootTools.log(Constants.TAG, "Remounting " + mountPoint.getMountPoint().getAbsolutePath() + " as " + mountType.toLowerCase());
final boolean isMountMode = mountPoint.getFlags().contains(mountType.toLowerCase());
if (!isMountMode)
{
//grab an instance of the internal class
try
{
Command command = new Command(0,
true,
"busybox mount -o remount," + mountType.toLowerCase() + " " + mountPoint.getDevice().getAbsolutePath() + " " + mountPoint.getMountPoint().getAbsolutePath(),
"toolbox mount -o remount," + mountType.toLowerCase() + " " + mountPoint.getDevice().getAbsolutePath() + " " + mountPoint.getMountPoint().getAbsolutePath(),
"toybox mount -o remount," + mountType.toLowerCase() + " " + mountPoint.getDevice().getAbsolutePath() + " " + mountPoint.getMountPoint().getAbsolutePath(),
"mount -o remount," + mountType.toLowerCase() + " " + mountPoint.getDevice().getAbsolutePath() + " " + mountPoint.getMountPoint().getAbsolutePath(),
"mount -o remount," + mountType.toLowerCase() + " " + file,
"/system/bin/toolbox mount -o remount," + mountType.toLowerCase() + " " + mountPoint.getDevice().getAbsolutePath() + " " + mountPoint.getMountPoint().getAbsolutePath(),
"/system/bin/toybox mount -o remount," + mountType.toLowerCase() + " " + mountPoint.getDevice().getAbsolutePath() + " " + mountPoint.getMountPoint().getAbsolutePath()
);
Shell.startRootShell().add(command);
commandWait(command);
if(customPath != null) {
command = new Command(0,
true,
customPath + " mount -o remount," + mountType.toLowerCase() + " " + mountPoint.getDevice().getAbsolutePath() + " " + mountPoint.getMountPoint().getAbsolutePath());
Shell.startRootShell().add(command);
commandWait(command);
}
}
catch (Exception e)
{
}
mountPoint = findMountPointRecursive(file);
}
if (mountPoint != null)
{
RootTools.log(Constants.TAG, mountPoint.getFlags() + " AND " + mountType.toLowerCase());
if (mountPoint.getFlags().contains(mountType.toLowerCase()))
{
RootTools.log(mountPoint.getFlags().toString());
return true;
}
else
{
RootTools.log(mountPoint.getFlags().toString());
return false;
}
}
else
{
RootTools.log("mount is null, file was: " + file + " mountType was: " + mountType);
}
}
else
{
RootTools.log("mount is null, file was: " + file + " mountType was: " + mountType);
}
return false;
}
private Mount findMountPointRecursive(String file)
{
try
{
ArrayList<Mount> mounts = RootTools.getMounts();
for (File path = new File(file); path != null; )
{
for (Mount mount : mounts)
{
if (mount.getMountPoint().equals(path))
{
return mount;
}
}
}
return null;
}
catch (IOException e)
{
if (RootTools.debugMode)
{
e.printStackTrace();
}
}
catch (Exception e)
{
if (RootTools.debugMode)
{
e.printStackTrace();
}
}
return null;
}
private void commandWait(Command cmd)
{
synchronized (cmd)
{
try
{
if (!cmd.isFinished())
{
cmd.wait(2000);
}
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,98 @@
/*
* This file is part of the RootTools Project: http://code.google.com/p/RootTools/
*
* Copyright (c) 2012 Stephen Erickson, Chris Ravenscroft, Dominik Schuermann, Adam Shanks
*
* This code is dual-licensed under the terms of the Apache License Version 2.0 and
* the terms of the General Public License (GPL) Version 2.
* You may use this code according to either of these licenses as is most appropriate
* for your project on a case-by-case basis.
*
* The terms of each license can be found in the root directory of this project's repository as well as at:
*
* * http://www.apache.org/licenses/LICENSE-2.0
* * http://www.gnu.org/licenses/gpl-2.0.txt
*
* Unless required by applicable law or agreed to in writing, software
* distributed under these Licenses is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See each License for the specific language governing permissions and
* limitations under that License.
*/
package com.stericson.roottools.internal;
import android.content.Context;
import android.util.Log;
import com.stericson.rootshell.execution.Command;
import com.stericson.rootshell.execution.Shell;
import com.stericson.roottools.RootTools;
import java.io.IOException;
public class Runner extends Thread
{
private static final String LOG_TAG = "RootTools::Runner";
Context context;
String binaryName;
String parameter;
public Runner(Context context, String binaryName, String parameter)
{
this.context = context;
this.binaryName = binaryName;
this.parameter = parameter;
}
public void run()
{
String privateFilesPath = null;
try
{
privateFilesPath = context.getFilesDir().getCanonicalPath();
}
catch (IOException e)
{
if (RootTools.debugMode)
{
Log.e(LOG_TAG, "Problem occured while trying to locate private files directory!");
}
e.printStackTrace();
}
if (privateFilesPath != null)
{
try
{
Command command = new Command(0, false, privateFilesPath + "/" + binaryName + " " + parameter);
Shell.startRootShell().add(command);
commandWait(command);
}
catch (Exception e)
{
}
}
}
private void commandWait(Command cmd)
{
synchronized (cmd)
{
try
{
if (!cmd.isFinished())
{
cmd.wait(2000);
}
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
}

View file

@ -0,0 +1,90 @@
/*
* Copyright 2012 two forty four a.m. LLC <http://www.twofortyfouram.com>
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
* <http://www.apache.org/licenses/LICENSE-2.0>
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and limitations under the License.
*/
package com.twofortyfouram.locale;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import dev.ukanth.ufirewall.R;
/**
* Utility class to generate a breadcrumb title string for {@code Activity} instances in Locale.
* <p>
* This class cannot be instantiated.
*/
public final class BreadCrumber
{
/**
* Private constructor prevents instantiation
*
* @throws UnsupportedOperationException because this class cannot be instantiated.
*/
private BreadCrumber()
{
throw new UnsupportedOperationException("This class is non-instantiable"); //$NON-NLS-1$
}
/**
* Static helper method to generate bread crumbs. Bread crumb strings will be properly formatted for the
* current language, including right-to-left languages, as long as the proper
* {@link com.twofortyfouram.locale.platform.R.string#twofortyfouram_locale_breadcrumb_format} string
* resources have been created.
*
* @param context {@code Context} for loading platform resources. Cannot be null.
* @param intent {@code Intent} to extract the bread crumb from.
* @param currentCrumb The last element of the bread crumb path.
* @return {@code String} presentation of the bread crumb. If the intent parameter is null, then this
* method returns currentCrumb. If currentCrumb is null, then this method returns the empty string
* "". If intent contains a private Serializable instances as an extra, then this method returns
* the empty string "".
* @throws IllegalArgumentException if {@code context} is null.
*/
public static CharSequence generateBreadcrumb(final Context context, final Intent intent,
final String currentCrumb)
{
if (null == context)
{
throw new IllegalArgumentException("context cannot be null"); //$NON-NLS-1$
}
try
{
if (null == currentCrumb)
{
Log.w(Constants.LOG_TAG, "currentCrumb cannot be null"); //$NON-NLS-1$
return ""; //$NON-NLS-1$
}
if (null == intent)
{
Log.w(Constants.LOG_TAG, "intent cannot be null"); //$NON-NLS-1$
return currentCrumb;
}
/*
* Note: this is vulnerable to a private serializable attack, but the try-catch will solve that.
*/
final String breadcrumbString = intent.getStringExtra(com.twofortyfouram.locale.Intent.EXTRA_STRING_BREADCRUMB);
if (null != breadcrumbString)
{
return context.getString(R.string.twofortyfouram_locale_breadcrumb_format, breadcrumbString, context.getString(R.string.twofortyfouram_locale_breadcrumb_separator), currentCrumb);
}
return currentCrumb;
}
catch (final Exception e)
{
Log.e(Constants.LOG_TAG, "Encountered error generating breadcrumb", e); //$NON-NLS-1$
return ""; //$NON-NLS-1$
}
}
}

View file

@ -0,0 +1,48 @@
/*
* Copyright 2012 two forty four a.m. LLC <http://www.twofortyfouram.com>
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
* <http://www.apache.org/licenses/LICENSE-2.0>
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and limitations under the License.
*/
package com.twofortyfouram.locale;
/**
* Utility class containing constants for the Locale Developer Platform.
*/
/*
* This class is NOT part of the public API.
*/
/* package */final class Constants
{
/**
* Private constructor prevents instantiation
*
* @throws UnsupportedOperationException because this class cannot be instantiated.
*/
private Constants()
{
throw new UnsupportedOperationException("This class is non-instantiable"); //$NON-NLS-1$
}
/**
* Log tag for logcat messages generated by the Locale Developer Platform
*/
/*
* This is NOT a public API. Third party apps should NOT use this log tag for their own log messages.
*/
/* package */static final String LOG_TAG = "LocaleApiLibrary"; //$NON-NLS-1$
/**
* String package name for Locale.
*/
/*
* This is NOT a public API. Third parties should NOT rely on this being the only package name for Locale.
*/
/* package */static final String LOCALE_PACKAGE = "com.twofortyfouram.locale"; //$NON-NLS-1$
}

View file

@ -0,0 +1,195 @@
/*
* Copyright 2012 two forty four a.m. LLC <http://www.twofortyfouram.com>
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
* <http://www.apache.org/licenses/LICENSE-2.0>
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and limitations under the License.
*/
package com.twofortyfouram.locale;
import android.os.Parcelable;
/**
* Contains Intent constants necessary for interacting with the Locale Developer Platform.
*/
public final class Intent
{
/**
* Private constructor prevents instantiation.
*
* @throws UnsupportedOperationException because this class cannot be instantiated.
*/
private Intent()
{
throw new UnsupportedOperationException("This class is non-instantiable"); //$NON-NLS-1$
}
/**
* Ordered broadcast result code indicating that a plug-in condition's state is satisfied (true).
*
* @see Intent#ACTION_QUERY_CONDITION
*/
public static final int RESULT_CONDITION_SATISFIED = 16;
/**
* Ordered broadcast result code indicating that a plug-in condition's state is not satisfied (false).
*
* @see Intent#ACTION_QUERY_CONDITION
*/
public static final int RESULT_CONDITION_UNSATISFIED = 17;
/**
* Ordered broadcast result code indicating that a plug-in condition's state is unknown (neither true nor
* false).
* <p>
* If a condition returns UNKNOWN, then Locale will use the last known return value on a best-effort
* basis. Best-effort means that Locale may not persist known values forever (e.g. last known values could
* hypothetically be cleared after a device reboot, a restart of the Locale process, or other events). If
* there is no last known return value, then unknown is treated as not satisfied (false).
* <p>
* The purpose of an UNKNOWN result is to allow a plug-in condition more than 10 seconds to process a
* requery. A {@code BroadcastReceiver} must return within 10 seconds, otherwise it will be killed by
* Android. A plug-in that needs more than 10 seconds might initially return
* {@link #RESULT_CONDITION_UNKNOWN}, subsequently request a requery, and then return either
* {@link #RESULT_CONDITION_SATISFIED} or {@link #RESULT_CONDITION_UNSATISFIED}.
*
* @see Intent#ACTION_QUERY_CONDITION
*/
public static final int RESULT_CONDITION_UNKNOWN = 18;
/**
* {@code Intent} action {@code String} broadcast by Locale to create or edit a plug-in setting. When
* Locale broadcasts this {@code Intent}, it will be sent directly to the package and class of the
* plug-in's {@code Activity}. The {@code Intent} may contain a {@link #EXTRA_BUNDLE} that was previously
* set by the {@code Activity} result of {@link #ACTION_EDIT_SETTING}.
* <p>
* There SHOULD be only one {@code Activity} per APK that implements this {@code Intent}. If a single APK
* wishes to export multiple plug-ins, it MAY implement multiple Activity instances that implement this
* {@code Intent}, however there must only be a single {@link #ACTION_FIRE_SETTING} receiver. In this
* scenario, it is the responsibility of the Activities to store enough data in {@link #EXTRA_BUNDLE} to
* allow this receiver to disambiguate which "plug-in" is being fired. To avoid user confusion, it is
* recommended that only a single plug-in be implemented per APK.
*
* @see Intent#EXTRA_BUNDLE
* @see Intent#EXTRA_STRING_BREADCRUMB
*/
public static final String ACTION_EDIT_SETTING = "com.twofortyfouram.locale.intent.action.EDIT_SETTING"; //$NON-NLS-1$
/**
* {@code Intent} action {@code String} broadcast by Locale to fire a plug-in setting. When Locale
* broadcasts this {@code Intent}, it will be sent directly to the package and class of the plug-in's
* {@code BroadcastReceiver}. The {@code Intent} will contain a {@link #EXTRA_BUNDLE} that was previously
* set by the {@code Activity} result of {@link #ACTION_EDIT_SETTING}.
* <p>
* There MUST be only one {@code BroadcastReceiver} per APK that implements this {@code Intent}.
*
* @see Intent#EXTRA_BUNDLE
*/
public static final String ACTION_FIRE_SETTING = "com.twofortyfouram.locale.intent.action.FIRE_SETTING"; //$NON-NLS-1$
/**
* {@code Intent} action {@code String} broadcast by Locale to create or edit a plug-in condition. When
* Locale broadcasts this {@code Intent}, it will be sent directly to the package and class of the
* plug-in's {@code Activity}. The {@code Intent} may contain a store-and-forward {@link #EXTRA_BUNDLE}
* that was previously set by the {@code Activity} result of {@link #ACTION_EDIT_CONDITION}.
* <p>
* There SHOULD be only one {@code Activity} per APK that implements this {@code Intent}. If a single APK
* wishes to export multiple plug-ins, it MAY implement multiple Activity instances that implement this
* {@code Intent}, however there must only be a single {@link #ACTION_QUERY_CONDITION} receiver. In this
* scenario, it is the responsibility of the Activities to store enough data in {@link #EXTRA_BUNDLE} to
* allow this receiver to disambiguate which "plug-in" is being queried. To avoid user confusion, it is
* recommended that only a single plug-in be implemented per APK.
*
* @see Intent#EXTRA_BUNDLE
* @see Intent#EXTRA_STRING_BREADCRUMB
*/
public static final String ACTION_EDIT_CONDITION = "com.twofortyfouram.locale.intent.action.EDIT_CONDITION"; //$NON-NLS-1$
/**
* Ordered {@code Intent} action {@code String} broadcast by Locale to query a plug-in condition. When
* Locale broadcasts this {@code Intent}, it will be sent directly to the package and class of the
* plug-in's {@code BroadcastReceiver}. The {@code Intent} will contain a {@link #EXTRA_BUNDLE} that was
* previously set by the {@code Activity} result of {@link #ACTION_EDIT_CONDITION}.
* <p>
* Since this is an ordered broadcast, the receiver is expected to set an appropriate result code from
* {@link #RESULT_CONDITION_SATISFIED}, {@link #RESULT_CONDITION_UNSATISFIED}, and
* {@link #RESULT_CONDITION_UNKNOWN}.
* <p>
* There MUST be only one {@code BroadcastReceiver} per APK that implements this {@code Intent}.
*
* @see Intent#EXTRA_BUNDLE
* @see Intent#RESULT_CONDITION_SATISFIED
* @see Intent#RESULT_CONDITION_UNSATISFIED
* @see Intent#RESULT_CONDITION_UNKNOWN
*/
public static final String ACTION_QUERY_CONDITION = "com.twofortyfouram.locale.intent.action.QUERY_CONDITION"; //$NON-NLS-1$
/**
* {@code Intent} action {@code String} to notify Locale that a plug-in condition is requesting that
* Locale query it via {@link #ACTION_QUERY_CONDITION}. This merely serves as a hint to Locale that a
* condition wants to be queried. There is no guarantee as to when or if the plug-in will be queried after
* this {@code Intent} is broadcast. If Locale does not respond to the plug-in condition after a
* {@link #ACTION_REQUEST_QUERY} Intent is sent, the plug-in SHOULD shut itself down and stop requesting
* requeries. A lack of response from Locale indicates that Locale is not currently interested in this
* plug-in. When Locale becomes interested in the plug-in again, Locale will send
* {@link #ACTION_QUERY_CONDITION}.
* <p>
* The extra {@link #EXTRA_ACTIVITY} MUST be included, otherwise Locale will ignore this {@code Intent}.
* <p>
* Plug-in conditions SHOULD NOT use this unless there is some sort of asynchronous event that has
* occurred, such as a broadcast {@code Intent} being received by the plug-in. Plug-ins SHOULD NOT
* periodically request a requery as a way of implementing polling behavior.
*
* @see Intent#EXTRA_ACTIVITY
*/
public static final String ACTION_REQUEST_QUERY = "com.twofortyfouram.locale.intent.action.REQUEST_QUERY"; //$NON-NLS-1$
/**
* Type: {@code String}
* <p>
* Maps to a {@code String} that represents the {@code Activity} bread crumb path.
*
* @see BreadCrumber
*/
public static final String EXTRA_STRING_BREADCRUMB = "com.twofortyfouram.locale.intent.extra.BREADCRUMB"; //$NON-NLS-1$
/**
* Type: {@code String}
* <p>
* Maps to a {@code String} that represents a blurb. This is returned as an {@code Activity} result extra
* from {@link #ACTION_EDIT_CONDITION} or {@link #ACTION_EDIT_SETTING}.
* <p>
* The blurb is a concise description displayed to the user of what the plug-in is configured to do.
*/
public static final String EXTRA_STRING_BLURB = "com.twofortyfouram.locale.intent.extra.BLURB"; //$NON-NLS-1$
/**
* Type: {@code Bundle}
* <p>
* Maps to a {@code Bundle} that contains all of a plug-in's extras.
* <p>
* Plug-ins MUST NOT store {@link Parcelable} objects in this {@code Bundle}, because {@code Parcelable}
* is not a long-term storage format. Also, plug-ins MUST NOT store any serializable object that is not
* exposed by the Android SDK.
* <p>
* The maximum size of a Bundle that can be sent across process boundaries is on the order of 500
* kilobytes (base-10), while Locale further limits plug-in Bundles to about 100 kilobytes (base-10).
* Although the maximum size is about 100 kilobytes, plug-ins SHOULD keep Bundles much smaller for
* performance and memory usage reasons.
*/
public static final String EXTRA_BUNDLE = "com.twofortyfouram.locale.intent.extra.BUNDLE"; //$NON-NLS-1$
/**
* Type: {@code String}
* <p>
* Maps to a {@code String} that represents the name of a plug-in's {@code Activity}.
*
* @see Intent#ACTION_REQUEST_QUERY
*/
public static final String EXTRA_ACTIVITY = "com.twofortyfouram.locale.intent.extra.ACTIVITY"; //$NON-NLS-1$
}

View file

@ -0,0 +1,123 @@
/*
* Copyright 2012 two forty four a.m. LLC <http://www.twofortyfouram.com>
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
* <http://www.apache.org/licenses/LICENSE-2.0>
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and limitations under the License.
*/
package com.twofortyfouram.locale;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* A simple utility class to find a package that is compatible with hosting the Locale Developer Platform.
*/
/*
* This class is NOT part of the public Locale Developer Platform API
*/
public final class PackageUtilities
{
/**
* A hard-coded set of Android packages that support the Locale Developer Platform.
*/
/*
* This is NOT a public field and is subject to change in future releases of the Developer Platform. A
* conscious design decision was made to use hard-coded package names, rather than dynamic discovery of
* packages that might be compatible with hosting the Locale Developer Platform API. This is for two
* reasons: to ensure the host is implemented correctly (hosts must pass the extensive Locale Platform
* Host compatibility test suite) and to prevent malicious applications from crashing plug-ins by
* providing bad values. As additional apps implement the Locale Developer Platform, their package names
* will also be added to this list.
*/
/*
* Note: this is implemented as a Set<String> rather than a String[], in order to enforce immutability.
*/
private static final Set<String> COMPATIBLE_PACKAGES = constructPackageSet();
/**
* @return a list wrapped in {@link Collections#unmodifiableList(List)} that represents the set of
* Locale-compatible packages.
*/
private static Set<String> constructPackageSet()
{
final HashSet<String> packages = new HashSet<String>();
packages.add(Constants.LOCALE_PACKAGE);
/*
* Note: Tasker is not 100% compatible with Locale's plug-in API, but it is close enough that these
* packages are enabled. Tasker's known incompatibilities are documented on the Tasker website.
*/
packages.add("net.dinglisch.android.taskerm"); //$NON-NLS-1$
packages.add("net.dinglisch.android.tasker"); //$NON-NLS-1$
packages.add("net.dinglisch.android.taskercupcake"); //$NON-NLS-1$
return Collections.unmodifiableSet(packages);
}
/**
* Obtains the {@code String} package name of a currently-installed package which implements the host
* component of the Locale Developer Platform.
* <p>
* Note: A TOCTOU error exists, due to the fact that the package could be uninstalled at any time.
* <p>
* Note: If there are multiple hosts, this method will return one of them. The interface of this method
* makes no guarantee which host will returned, nor whether that host will be consistently returned.
*
* @param manager an instance of {@code PackageManager}. Cannot be null.
* @param packageHint hint as to which package should take precedence. This parameter may be null.
* @return {@code String} package name of a host for the Locale Developer Platform, such as
* "com.twofortyfouram.locale". If no such package is found, returns null.
*/
public static String getCompatiblePackage(final PackageManager manager, final String packageHint)
{
/*
* The interface for this method makes no guarantees as to which host will be returned. However the
* implementation is more predictable.
*/
final List<PackageInfo> installedPackages = manager.getInstalledPackages(0);
if (COMPATIBLE_PACKAGES.contains(packageHint))
{
for (final PackageInfo packageInfo : installedPackages)
{
final String temp = packageInfo.packageName;
if (packageHint.equals(temp))
{
return temp;
}
}
}
for (final String compatiblePackageName : COMPATIBLE_PACKAGES)
{
if (compatiblePackageName.equals(packageHint))
{
continue;
}
for (final PackageInfo packageInfo : installedPackages)
{
final String temp = packageInfo.packageName;
if (compatiblePackageName.equals(temp))
{
return temp;
}
}
}
return null;
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,68 @@
/**
* Class to store wifi/3G/tethering status and LAN IP ranges.
*
* Copyright (C) 2013 Kevin Cernekee
*
* This program 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.
*
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @author Kevin Cernekee
* @version 1.0
*/
package dev.ukanth.ufirewall;
public class InterfaceDetails {
// firewall policy
public boolean isRoaming = false;
public boolean isWifiTethered = false;
public boolean tetherWifiStatusKnown = false;
public boolean isBluetoothTethered = false;
public boolean tetherBluetoothStatusKnown = false;
public boolean isUsbTethered = false;
public boolean tetherUsbStatusKnown = false;
public String lanMaskV4 = "";
public String lanMaskV6 = "";
// DNS servers for targeted rules instead of opening port 53 to all LAN hosts
public java.util.List<String> dnsServersV4 = new java.util.ArrayList<>();
public java.util.List<String> dnsServersV6 = new java.util.ArrayList<>();
// supplementary info
String wifiName = "";
boolean netEnabled = false;
boolean noIP = false;
public int netType = -1;
public boolean equals(InterfaceDetails that) {
return this.isRoaming == that.isRoaming &&
this.isWifiTethered == that.isWifiTethered &&
this.tetherWifiStatusKnown == that.tetherWifiStatusKnown &&
this.isBluetoothTethered == that.isBluetoothTethered &&
this.tetherBluetoothStatusKnown == that.tetherBluetoothStatusKnown &&
this.isUsbTethered == that.isUsbTethered &&
this.tetherUsbStatusKnown == that.tetherUsbStatusKnown &&
this.lanMaskV4.equals(that.lanMaskV4) &&
this.lanMaskV6.equals(that.lanMaskV6) &&
this.dnsServersV4.equals(that.dnsServersV4) &&
this.dnsServersV6.equals(that.dnsServersV6) &&
this.wifiName.equals(that.wifiName) &&
this.netEnabled == that.netEnabled &&
this.netType == that.netType &&
this.noIP == that.noIP;
}
}

View file

@ -0,0 +1,523 @@
/**
* Keep track of wifi/3G/tethering status and LAN IP ranges.
* <p/>
* Copyright (C) 2013 Kevin Cernekee
* <p/>
* This program 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.
* <p/>
* 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 General Public License for more details.
* <p/>
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @author Kevin Cernekee
* @version 1.0
*/
package dev.ukanth.ufirewall;
import static dev.ukanth.ufirewall.util.G.ctx;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.content.pm.PackageManager;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.wifi.WifiManager;
import java.lang.reflect.Method;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InterfaceAddress;
import java.net.NetworkInterface;
import java.util.Enumeration;
import java.util.Iterator;
import dev.ukanth.ufirewall.log.Log;
import dev.ukanth.ufirewall.service.FirewallService;
import dev.ukanth.ufirewall.service.RootCommand;
import dev.ukanth.ufirewall.util.G;
public final class InterfaceTracker {
public static final String TAG = "AFWall";
public static final String[] ITFS_WIFI = {"eth+", "wlan+", "tiwlan+", "ra+", "bnep+"};
public static final String[] ITFS_3G = {"rmnet+", "pdp+", "uwbr+", "wimax+", "vsnet+",
"rmnet_sdio+", "ccmni+", "qmi+", "svnet0+", "ccemni+",
"wwan+", "cdma_rmnet+", "clat4+", "cc2mni+", "bond1+", "rmnet_smux+", "ccinet+",
"v4-rmnet+", "seth_w+", "v4-rmnet_data+", "rmnet_ipa+", "rmnet_data+", "r_rmnet_data+"};
public static final String[] ITFS_VPN = {"tun+", "ppp+", "tap+"};
public static final String[] ITFS_TETHER = {"bt-pan", "usb+", "rndis+", "rmnet_usb+"};
public static final String BOOT_COMPLETED = "BOOT_COMPLETED";
public static final String CONNECTIVITY_CHANGE = "CONNECTIVITY_CHANGE";
public static final String TETHER_STATE_CHANGED = "TETHER_STATE_CHANGED";
private static InterfaceDetails currentCfg = null;
private static String truncAfter(String in, String regexp) {
return in.split(regexp)[0];
}
private static void getTetherStatus(Context context, InterfaceDetails d) {
getWifiTetherStatus(context, d);
getBluetoothTetherStatus(context, d);
getUsbTetherStatus(context, d);
}
private static void getWifiTetherStatus(Context context, InterfaceDetails d) {
WifiManager wifi = (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
Method[] wmMethods = wifi.getClass().getDeclaredMethods();
d.isWifiTethered = false;
d.tetherWifiStatusKnown = false;
for (Method method : wmMethods) {
if (method.getName().equals("isWifiApEnabled")) {
try {
d.isWifiTethered = ((Boolean) method.invoke(wifi)).booleanValue();
d.tetherWifiStatusKnown = true;
Log.d(TAG, "isWifiApEnabled is " + d.isWifiTethered);
} catch (Exception e) {
Log.e(G.TAG, "Exception in getting Wifi tether status");
Log.e(Api.TAG, android.util.Log.getStackTraceString(e));
}
}
}
}
// To get bluetooth tethering, we need valid BluetoothPan instance
// It is obtainable only in ServiceListener.onServiceConnected callback
public static BluetoothProfile getBtProfile() {
return FirewallService.getBtPanProfile();
}
private static void getBluetoothTetherStatus(Context context, InterfaceDetails d) {
if (FirewallService.getBtPanProfile() != null) {
Method[] btMethods = FirewallService.getBtPanProfile().getClass().getDeclaredMethods();
d.isBluetoothTethered = false;
d.tetherBluetoothStatusKnown = false;
for (Method method : btMethods) {
if (method.getName().equals("isTetheringOn")) {
try {
d.isBluetoothTethered = ((Boolean) method.invoke(FirewallService.getBtPanProfile())).booleanValue();
d.tetherBluetoothStatusKnown = true;
Log.d(TAG, "isBluetoothTetheringOn is " + d.isBluetoothTethered);
} catch (Exception e) {
Log.e(Api.TAG, android.util.Log.getStackTraceString(e));
}
}
}
}
}
private static void getUsbTetherStatus(Context context, InterfaceDetails d) {
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
if (cm == null) {
d.isUsbTethered = false;
d.tetherUsbStatusKnown = false;
return;
}
d.isUsbTethered = false;
d.tetherUsbStatusKnown = false;
try {
// Use reflection to access hidden getTetheredIfaces() method
Method getTetheredIfaces = cm.getClass().getDeclaredMethod("getTetheredIfaces");
getTetheredIfaces.setAccessible(true);
String[] tetheredIfaces = (String[]) getTetheredIfaces.invoke(cm);
if (tetheredIfaces != null) {
for (String iface : tetheredIfaces) {
// USB tethering typically uses interfaces like "rndis0", "usb0", etc.
if (iface != null && (iface.startsWith("rndis") || iface.startsWith("usb"))) {
d.isUsbTethered = true;
break;
}
}
}
d.tetherUsbStatusKnown = true;
Log.d(TAG, "USB tethering status: " + d.isUsbTethered);
} catch (Exception e) {
Log.e(G.TAG, "Exception in getting USB tether status");
Log.e(Api.TAG, android.util.Log.getStackTraceString(e));
// Fallback: Check if USB tethering interface exists using NetworkInterface
try {
d.isUsbTethered = isUsbTetherInterfaceUp();
d.tetherUsbStatusKnown = true;
Log.d(TAG, "USB tethering status (fallback): " + d.isUsbTethered);
} catch (Exception fallbackException) {
Log.e(Api.TAG, "Fallback USB tether detection failed: " + android.util.Log.getStackTraceString(fallbackException));
}
}
}
private static boolean isUsbTetherInterfaceUp() {
try {
java.util.Enumeration<java.net.NetworkInterface> interfaces = java.net.NetworkInterface.getNetworkInterfaces();
while (interfaces.hasMoreElements()) {
java.net.NetworkInterface networkInterface = interfaces.nextElement();
String name = networkInterface.getName();
if (name != null && (name.startsWith("rndis") || name.startsWith("usb")) && networkInterface.isUp()) {
return true;
}
}
} catch (Exception e) {
Log.e(Api.TAG, "Error checking network interfaces: " + android.util.Log.getStackTraceString(e));
}
return false;
}
private static InterfaceDetails getInterfaceDetails(Context context) {
InterfaceDetails ret = new InterfaceDetails();
ConnectivityManager cm = (ConnectivityManager) context
.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo info = cm.getActiveNetworkInfo();
if (info == null || !info.isConnected()) {
return ret;
}
switch (info.getType()) {
case ConnectivityManager.TYPE_MOBILE:
case ConnectivityManager.TYPE_MOBILE_DUN:
case ConnectivityManager.TYPE_MOBILE_HIPRI:
case ConnectivityManager.TYPE_MOBILE_MMS:
case ConnectivityManager.TYPE_MOBILE_SUPL:
case ConnectivityManager.TYPE_WIMAX:
ret.isRoaming = info.isRoaming();
ret.netType = ConnectivityManager.TYPE_MOBILE;
ret.netEnabled = true;
break;
case ConnectivityManager.TYPE_WIFI:
case ConnectivityManager.TYPE_BLUETOOTH:
case ConnectivityManager.TYPE_ETHERNET:
ret.netType = ConnectivityManager.TYPE_WIFI;
ret.netEnabled = true;
break;
}
try {
if(G.enableTether()) {
getTetherStatus(context, ret);
}
} catch (Exception e) {
Log.i(Api.TAG, "Exception in getInterfaceDetails.checkTether" + e.getLocalizedMessage());
}
NewInterfaceScanner.populateLanMasks(ret);
getDnsServers(context, ret);
return ret;
}
private static void getDnsServers(Context context, InterfaceDetails d) {
d.dnsServersV4.clear();
d.dnsServersV6.clear();
try {
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
if (cm == null) return;
// Get active network
android.net.Network activeNetwork = null;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
activeNetwork = cm.getActiveNetwork();
}
if (activeNetwork != null && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
// Modern approach using LinkProperties (API 23+)
android.net.LinkProperties linkProperties = cm.getLinkProperties(activeNetwork);
if (linkProperties != null) {
for (java.net.InetAddress dns : linkProperties.getDnsServers()) {
if (dns instanceof java.net.Inet4Address) {
d.dnsServersV4.add(dns.getHostAddress());
} else if (dns instanceof java.net.Inet6Address) {
d.dnsServersV6.add(dns.getHostAddress());
}
}
}
}
// Fallback: Try system properties (older Android versions or backup method)
if (d.dnsServersV4.isEmpty() && d.dnsServersV6.isEmpty()) {
getDnsFromSystemProperties(d);
}
Log.d(TAG, "DNS servers IPv4: " + d.dnsServersV4);
Log.d(TAG, "DNS servers IPv6: " + d.dnsServersV6);
} catch (Exception e) {
Log.e(Api.TAG, "Exception getting DNS servers: " + android.util.Log.getStackTraceString(e));
}
}
private static void getDnsFromSystemProperties(InterfaceDetails d) {
try {
// Try common system properties for DNS servers
String[] dnsProps = {"net.dns1", "net.dns2", "net.dns3", "net.dns4"};
for (String prop : dnsProps) {
String dnsServer = System.getProperty(prop);
if (dnsServer != null && !dnsServer.isEmpty() && !dnsServer.equals("0.0.0.0")) {
try {
java.net.InetAddress addr = java.net.InetAddress.getByName(dnsServer);
if (addr instanceof java.net.Inet4Address) {
if (!d.dnsServersV4.contains(dnsServer)) {
d.dnsServersV4.add(dnsServer);
}
} else if (addr instanceof java.net.Inet6Address) {
if (!d.dnsServersV6.contains(dnsServer)) {
d.dnsServersV6.add(dnsServer);
}
}
} catch (Exception e) {
Log.w(TAG, "Invalid DNS server address: " + dnsServer);
}
}
}
} catch (Exception e) {
Log.e(Api.TAG, "Exception getting DNS from system properties: " + android.util.Log.getStackTraceString(e));
}
}
public static boolean checkForNewCfg(Context context) {
InterfaceDetails newCfg = getInterfaceDetails(context);
//always check for new config
if (currentCfg != null && currentCfg.equals(newCfg)) {
return false;
}
Log.i(TAG, "Getting interface details...");
currentCfg = newCfg;
if (!newCfg.netEnabled) {
Log.i(TAG, "Now assuming NO connection (all interfaces down)");
} else {
if (newCfg.netType == ConnectivityManager.TYPE_WIFI) {
Log.i(TAG, "Now assuming wifi connection (" +
"bluetooth-tethered: " + (newCfg.isBluetoothTethered ? "yes" : "no") + ", " +
"usb-tethered: " + (newCfg.isUsbTethered ? "yes" : "no") + ")");
} else if (newCfg.netType == ConnectivityManager.TYPE_MOBILE) {
Log.i(TAG, "Now assuming 3G connection (" +
"roaming: " + (newCfg.isRoaming ? "yes" : "no") +
"wifi-tethered: " + (newCfg.isWifiTethered ? "yes" : "no") + ", " +
"bluetooth-tethered: " + (newCfg.isBluetoothTethered ? "yes" : "no") + ", " +
"usb-tethered: " + (newCfg.isUsbTethered ? "yes" : "no") + ")");
} else if (newCfg.netType == ConnectivityManager.TYPE_BLUETOOTH) {
Log.i(TAG, "Now assuming bluetooth connection (" +
"wifi-tethered: " + (newCfg.isWifiTethered ? "yes" : "no") + ", " +
"usb-tethered: " + (newCfg.isUsbTethered ? "yes" : "no") + ")");
}
if (!newCfg.lanMaskV4.equals("")) {
Log.i(TAG, "IPv4 LAN netmask on " + newCfg.wifiName + ": " + newCfg.lanMaskV4);
}
if (!newCfg.lanMaskV6.equals("")) {
Log.i(TAG, "IPv6 LAN netmask on " + newCfg.wifiName + ": " + newCfg.lanMaskV6);
}
if (newCfg.lanMaskV6.equals("") && newCfg.lanMaskV4.equals("")) {
Log.i(TAG, "No ipaddress found");
}
}
return true;
}
public static InterfaceDetails getCurrentCfg(Context context, boolean force) {
Log.i(TAG, "Forcing configuration: " + force);
if (currentCfg == null || force) {
currentCfg = getInterfaceDetails(context);
}
return currentCfg;
}
public static void applyRulesOnChange(Context context, final String reason) {
final Context ctx = context.getApplicationContext();
if (!checkForNewCfg(ctx)) {
Log.d(TAG, reason + ": interface state has not changed, ignoring");
return;
} else if (!Api.isEnabled(ctx)) {
Log.d(TAG, reason + ": firewall is disabled, ignoring");
return;
}
Log.d(TAG, reason + " applying rules");
// update Api.PREFS_NAME so we pick up the right profile
// REVISIT: this can be removed once we're confident that G is in sync with profile changes
G.reloadPrefs();
if (reason.equals(InterfaceTracker.BOOT_COMPLETED) || reason.startsWith(InterfaceTracker.BOOT_COMPLETED)) {
Log.i(TAG, "Applying boot-specific rules for reason: " + reason);
applyBootRules(reason);
} else {
Log.i(TAG, "Applying regular rules for reason: " + reason);
applyRules(reason);
}
}
public static void applyRules(final String reason) {
Api.fastApply(ctx, new RootCommand()
.setFailureToast(R.string.error_apply)
.setCallback(new RootCommand.Callback() {
@Override
public void cbFunc(RootCommand state) {
if (state.exitCode == 0) {
Log.i(TAG, reason + ": applied rules at " + System.currentTimeMillis());
Api.applyDefaultChains(ctx, new RootCommand()
.setCallback(new RootCommand.Callback() {
@Override
public void cbFunc(RootCommand state) {
if (state.exitCode != 0) {
Api.errorNotification(ctx);
}
}
}));
} else {
//lets try applying all rules
Api.setRulesUpToDate(false);
Api.fastApply(ctx, new RootCommand()
.setCallback(new RootCommand.Callback() {
@Override
public void cbFunc(RootCommand state) {
if (state.exitCode == 0) {
Log.i(TAG, reason + ": applied rules at " + System.currentTimeMillis());
} else {
Log.e(TAG, reason + ": applySavedIptablesRules() returned an error");
Api.errorNotification(ctx);
}
Api.applyDefaultChains(ctx, new RootCommand()
.setFailureToast(R.string.error_apply)
.setCallback(new RootCommand.Callback() {
@Override
public void cbFunc(RootCommand state) {
if (state.exitCode != 0) {
Api.errorNotification(ctx);
}
}
}));
}
}));
}
}
}));
}
public static void applyBootRules(final String reason) {
Api.applySavedIptablesRules(ctx, true, new RootCommand()
.setFailureToast(R.string.error_apply)
.setCallback(new RootCommand.Callback() {
@Override
public void cbFunc(RootCommand state) {
if (state.exitCode == 0) {
Log.i(TAG, reason + ": applied rules at " + System.currentTimeMillis());
Api.applyDefaultChains(ctx, new RootCommand()
.setCallback(new RootCommand.Callback() {
@Override
public void cbFunc(RootCommand state) {
if (state.exitCode != 0) {
Api.errorNotification(ctx);
}
}
}));
} else {
//lets try applying all rules
Api.setRulesUpToDate(false);
Api.applySavedIptablesRules(ctx, true, new RootCommand()
.setCallback(new RootCommand.Callback() {
@Override
public void cbFunc(RootCommand state) {
if (state.exitCode == 0) {
Log.i(TAG, reason + ": applied rules at " + System.currentTimeMillis());
} else {
Log.e(TAG, reason + ": applySavedIptablesRules() returned an error");
Api.errorNotification(ctx);
}
Api.applyDefaultChains(ctx, new RootCommand()
.setFailureToast(R.string.error_apply)
.setCallback(new RootCommand.Callback() {
@Override
public void cbFunc(RootCommand state) {
if (state.exitCode != 0) {
Api.errorNotification(ctx);
}
}
}));
}
}));
}
}
}));
}
private static class NewInterfaceScanner {
public static void populateLanMasks(InterfaceDetails ret) {
try {
Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces();
while (en.hasMoreElements()) {
NetworkInterface intf = en.nextElement();
boolean match = false;
if (!intf.isUp() || intf.isLoopback()) {
continue;
}
for (String pattern : ITFS_WIFI) {
if (intf.getName().startsWith(truncAfter(pattern, "\\+"))) {
match = true;
break;
}
}
if (!match)
continue;
ret.wifiName = intf.getName();
Iterator<InterfaceAddress> addrList = intf.getInterfaceAddresses().iterator();
while (addrList.hasNext()) {
InterfaceAddress addr = addrList.next();
InetAddress ip = addr.getAddress();
String mask = truncAfter(ip.getHostAddress(), "%") + "/" +
addr.getNetworkPrefixLength();
if(ret.lanMaskV4.isEmpty() || ret.lanMaskV6.isEmpty()) {
if (ip instanceof Inet4Address) {
ret.lanMaskV4 = mask;
} else if (ip instanceof Inet6Address) {
ret.lanMaskV6 = mask;
}
}
}
if (ret.lanMaskV4.equals("") && ret.lanMaskV6.equals("")) {
ret.noIP = true;
}
}
} catch (Exception e) {
Log.i(TAG, "Error fetching network interface list: " + android.util.Log.getStackTraceString(e));
}
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,232 @@
package dev.ukanth.ufirewall.activity;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Bundle;
import android.view.MenuItem;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import com.raizlabs.android.dbflow.sql.language.SQLite;
import com.stericson.rootshell.execution.Command;
import com.stericson.rootshell.execution.Shell;
import com.stericson.roottools.RootTools;
import java.io.File;
import java.util.Arrays;
import java.util.HashMap;
import dev.ukanth.ufirewall.Api;
import dev.ukanth.ufirewall.R;
import dev.ukanth.ufirewall.log.Log;
import dev.ukanth.ufirewall.log.LogPreference;
import dev.ukanth.ufirewall.log.LogPreference_Table;
import dev.ukanth.ufirewall.util.G;
public class AppDetailActivity extends AppCompatActivity {
public static final String TAG = "AFWall";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
initTheme();
setTitle(getString(R.string.traffic_detail_title));
setContentView(R.layout.app_detail);
int appid = getIntent().getIntExtra("appid", -1);
String packageName = getIntent().getStringExtra("package");
try {
CheckBox logOption = findViewById(R.id.notification_p);
LogPreference logPreference = SQLite.select()
.from(LogPreference.class)
.where(LogPreference_Table.uid.eq(appid)).querySingle();
if (logPreference != null) {
logOption.setChecked(logPreference.isDisable());
}
logOption.setOnCheckedChangeListener((buttonView, isChecked) -> {
//only use when triggered by user
if (buttonView.isPressed()) {
// write the logic here
G.updateLogNotification(appid, isChecked);
}
});
} catch (Exception ignored) {
}
Toolbar toolbar = findViewById(R.id.app_toolbar);
setSupportActionBar(toolbar);
if (getSupportActionBar() != null) {
getSupportActionBar().setHomeButtonEnabled(true);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
final Context ctx = getApplicationContext();
ImageView image = findViewById(R.id.app_icon);
TextView textView = findViewById(R.id.app_title);
TextView textView2 = findViewById(R.id.app_package);
TextView up = findViewById(R.id.up);
TextView down = findViewById(R.id.down);
/**/
final PackageManager packageManager = getApplicationContext().getPackageManager();
HashMap<Integer,String> listMaps = Api.getPackagesForUser(Api.getListOfUids());
String packageNameList = "";
PackageInfo packageInfo = Api.getPackageDetails(ctx, listMaps, appid);
if(packageInfo != null && packageInfo.applicationInfo != null) {
packageNameList = packageInfo.applicationInfo.name;
}
//final String[] packageNameList = ctx.getPackageManager().getPackagesForUid(appid);
final String pName = packageName;
Button button = findViewById(R.id.app_settings);
button.setOnClickListener(v -> Api.showInstalledAppDetails(getApplicationContext(), pName));
ApplicationInfo applicationInfo;
try {
assert packageName != null;
if (!packageName.startsWith("dev.afwall.special.")) {
applicationInfo = packageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA);
try {
image.setImageDrawable(applicationInfo.loadIcon(packageManager));
} catch (Exception e){
image.setImageDrawable(applicationInfo.loadIcon(packageManager));
}
String name = packageManager.getApplicationLabel(applicationInfo).toString();
textView.setText(name);
setTotalBytesManual(down, up, applicationInfo.uid);
} else {
image.setImageDrawable(getApplicationContext().getDrawable(R.drawable.ic_unknown));
if(appid >= 0) {
textView.setText(Api.getSpecialDescription(getApplicationContext(), packageName.replace("dev.afwall.special.", "")));
} else {
textView.setText(Api.getSpecialDescriptionSystem(getApplicationContext(), packageName.replace("dev.afwall.special.", "")));
}
down.setText(" : " + humanReadableByteCount(0, false));
up.setText(" : " + humanReadableByteCount(0, false));
button.setEnabled(false);
}
if (packageNameList != null) {
textView2.setText(packageNameList);
button.setEnabled(false);
} else {
textView2.setText(packageName);
}
} catch (final NameNotFoundException e) {
down.setText(" : " + humanReadableByteCount(0, false));
up.setText(" : " + humanReadableByteCount(0, false));
button.setEnabled(false);
}
}
private void initTheme() {
switch(G.getSelectedTheme()) {
case "D":
setTheme(R.style.AppDarkTheme);
break;
case "L":
setTheme(R.style.AppLightTheme);
//set other colors
break;
case "B":
setTheme(R.style.AppBlackTheme);
break;
}
}
private void setTotalBytesManual(TextView down, TextView up, int localUid) {
File dir = new File("/proc/uid_stat/");
down.setText(" : " + humanReadableByteCount(Long.parseLong("0"), false));
up.setText(" : " + humanReadableByteCount(Long.parseLong("0"), false));
if (dir.exists()) {
String[] children = dir.list();
if (children != null) {
if (!Arrays.asList(children).contains(String.valueOf(localUid))) {
down.setText(" : " + humanReadableByteCount(Long.parseLong("0"), false));
up.setText(" : " + humanReadableByteCount(Long.parseLong("0"), false));
return;
}
} else {
down.setText(" : " + humanReadableByteCount(Long.parseLong("0"), false));
up.setText(" : " + humanReadableByteCount(Long.parseLong("0"), false));
return;
}
File uidFileDir = new File("/proc/uid_stat/" + localUid);
if (uidFileDir.exists()) {
File uidActualFileReceived = new File(uidFileDir, "tcp_rcv");
File uidActualFileSent = new File(uidFileDir, "tcp_snd");
String textReceived = "0";
String textSent = "0";
try {
if (uidActualFileReceived.exists() && uidActualFileSent.exists()) {
Command command = new Command(0, "cat " + uidActualFileReceived.getAbsolutePath())
{
@Override
public void commandOutput(int id, String line) {
down.setText(" : " + humanReadableByteCount(Long.parseLong(line), false));
super.commandOutput(id, line);
}
};
Command command1 = new Command(1, "cat " + uidActualFileSent.getAbsolutePath())
{
@Override
public void commandOutput(int id, String line) {
up.setText(" : " + humanReadableByteCount(Long.parseLong(line), false));
super.commandOutput(id, line);
}
};
Shell shell = RootTools.getShell(true);
shell.add(command);
shell.add(command1);
}
} catch (Exception e) {
Log.e(TAG, "Exception while reading tx bytes: " + e.getLocalizedMessage());
}
}
}
// return Long.valueOf(textReceived).longValue() + Long.valueOf(textReceived).longValue();
}
public static String humanReadableByteCount(long bytes, boolean si) {
int unit = si ? 1000 : 1024;
if (bytes < 0) return "0 B";
if (bytes < unit) return bytes + " B";
int exp = (int) (Math.log(bytes) / Math.log(unit));
String pre = (si ? "kMGTPE" : "KMGTPE").charAt(exp - 1) + (si ? "" : "i");
return String.format("%.1f %sB", bytes / Math.pow(unit, exp), pre);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
onBackPressed();
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public void onDestroy() {
super.onDestroy();
}
}

View file

@ -0,0 +1,21 @@
package dev.ukanth.ufirewall.activity;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.os.Build;
import android.os.LocaleList;
import androidx.appcompat.app.AppCompatActivity;
import java.util.Locale;
public class BaseActivity extends AppCompatActivity {
@Override
protected void attachBaseContext(Context newBase) {
super.attachBaseContext(newBase);
}
}

View file

@ -0,0 +1,101 @@
package dev.ukanth.ufirewall.activity;
import android.os.Bundle;
import android.util.DisplayMetrics;
import android.view.Gravity;
import android.view.MenuItem;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.SwitchCompat;
import androidx.appcompat.widget.Toolbar;
import androidx.cardview.widget.CardView;
import java.util.List;
import dev.ukanth.ufirewall.R;
import dev.ukanth.ufirewall.util.CustomRuleOld;
import dev.ukanth.ufirewall.util.Rule;
public class CustomRulesActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final View view = getLayoutInflater().inflate(R.layout.activity_custom_rules, null);
setTitle(R.string.custom_rules);
LinearLayout linearLayout = view.findViewById(R.id.activity_custom_rules);
try {
List<Rule> rules = CustomRuleOld.getRules(getApplicationContext());
for (final Rule rule : rules) {
CardView cardView = new CardView(this);
cardView.setElevation(5);
cardView.setRadius(5);
cardView.setLayoutParams(new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT, 100));
cardView.setMinimumHeight(60);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
params.height = 200;
params.gravity = Gravity.TOP;
params.setMargins(0, 2, 0, 40);
cardView.setLayoutParams(params);
SwitchCompat switchButton = new SwitchCompat(this);
switchButton.setTag(rule.getName());
switchButton.setOnCheckedChangeListener((buttonView, isChecked) -> {
Toast.makeText(getApplicationContext(), buttonView.getTag().toString() + isChecked, Toast.LENGTH_SHORT).show();
});
switchButton.setChecked(false);
switchButton.setContentDescription(rule.getDesc());
String builder = rule.getName() +
"\n" +
rule.getDesc() +
"\n";
switchButton.setText(builder);
switchButton.setTextSize(18);
switchButton.setLayoutParams(params);
cardView.addView(switchButton);
linearLayout.addView(cardView);
}
} catch (Exception ignored) {
}
setContentView(view);
Toolbar toolbar = findViewById(R.id.custom_toolbar_rules);
setSupportActionBar(toolbar);
if (getSupportActionBar() != null) {
getSupportActionBar().setHomeButtonEnabled(true);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
onBackPressed();
return true;
}
return super.onOptionsItemSelected(item);
}
public int dpToPixels(int dp) {
DisplayMetrics displayMetrics = getApplicationContext().getResources().getDisplayMetrics();
return Math.round(dp * (displayMetrics.xdpi / DisplayMetrics.DENSITY_DEFAULT));
}
}

View file

@ -0,0 +1,159 @@
/**
* Custom scripts activity.
* This screen is displayed to change the custom scripts.
* <p>
* Copyright (C) 2009-2011 Rodrigo Zechin Rosauro
* Copyright (C) 2011-2012 Umakanthan Chandran
* <p>
* This program 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.
* <p>
* 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 General Public License for more details.
* <p>
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @author Rodrigo Zechin Rosauro, Umakanthan Chandran
* @version 1.1
*/
package dev.ukanth.ufirewall.activity;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.text.method.LinkMovementMethod;
import android.view.KeyEvent;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.TextView;
import com.google.android.material.textfield.TextInputEditText;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import com.afollestad.materialdialogs.DialogAction;
import com.afollestad.materialdialogs.MaterialDialog;
import dev.ukanth.ufirewall.Api;
import dev.ukanth.ufirewall.R;
import dev.ukanth.ufirewall.util.G;
/**
* Custom scripts activity.
* This screen is displayed to change the custom scripts.
*/
public class CustomScriptActivity extends AppCompatActivity implements OnClickListener {
private TextInputEditText script;
private TextInputEditText script2;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
initTheme();
setContentView(R.layout.customscript);
findViewById(R.id.customscript_ok).setOnClickListener(this);
findViewById(R.id.customscript_cancel).setOnClickListener(this);
((TextView) findViewById(R.id.customscript_link)).setMovementMethod(LinkMovementMethod.getInstance());
final SharedPreferences prefs = getSharedPreferences(Api.PREFS_NAME, 0);
this.script = findViewById(R.id.customscript);
this.script.setText(prefs.getString(Api.PREF_CUSTOMSCRIPT, ""));
this.script2 = findViewById(R.id.customscript2);
this.script2.setText(prefs.getString(Api.PREF_CUSTOMSCRIPT2, ""));
setTitle(R.string.set_custom_script);
Toolbar toolbar = findViewById(R.id.custom_toolbar);
setSupportActionBar(toolbar);
if (getSupportActionBar() != null) {
getSupportActionBar().setHomeButtonEnabled(true);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
}
private void initTheme() {
switch(G.getSelectedTheme()) {
case "D":
setTheme(R.style.AppDarkTheme);
break;
case "L":
setTheme(R.style.AppLightTheme);
break;
case "B":
setTheme(R.style.AppBlackTheme);
break;
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
onBackPressed();
return true;
}
return super.onOptionsItemSelected(item);
}
/**
* Set the activity result to RESULT_OK and terminate this activity.
*/
private void resultOk() {
final Intent response = new Intent(Api.CUSTOM_SCRIPT_MSG);
response.putExtra(Api.SCRIPT_EXTRA, script.getText().toString());
response.putExtra(Api.SCRIPT2_EXTRA, script2.getText().toString());
setResult(RESULT_OK, response);
finish();
}
@Override
public void onClick(View v) {
if (v.getId() == R.id.customscript_ok) {
resultOk();
} else {
setResult(RESULT_CANCELED);
finish();
}
}
@Override
public void onBackPressed() {
final SharedPreferences prefs = getSharedPreferences(Api.PREFS_NAME, 0);
if (script.getText().toString().equals(prefs.getString(Api.PREF_CUSTOMSCRIPT, ""))
&& script2.getText().toString().equals(prefs.getString(Api.PREF_CUSTOMSCRIPT2, ""))) {
// Nothing has been changed, just return
super.onBackPressed();
return;
}
new MaterialDialog.Builder(this)
.title(R.string.unsaved_changes)
.content(R.string.unsaved_changes_message)
.positiveText(R.string.apply)
.negativeText(R.string.discard)
.onPositive(new MaterialDialog.SingleButtonCallback() {
@Override
public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) {
resultOk();
}
})
.onNegative(new MaterialDialog.SingleButtonCallback() {
@Override
public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) {
findViewById(R.id.customscript_cancel).performClick();
}
})
.show();
}
}

View file

@ -0,0 +1,480 @@
/**
* Common framework for LogActivity and RulesActivity
* <p>
* Copyright (C) 2011-2012 Umakanthan Chandran
* Copyright (C) 2011-2013 Kevin Cernekee
* <p>
* This program 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.
* <p>
* 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 General Public License for more details.
* <p>
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @author Umakanthan Chandran
* @version 1.0
*/
package dev.ukanth.ufirewall.activity;
import android.Manifest;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.pm.PackageManager;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.util.TypedValue;
import android.view.Menu;
import android.view.MenuItem;
import android.view.SubMenu;
import android.view.View;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.core.app.ActivityCompat;
import androidx.cardview.widget.CardView;
import androidx.core.widget.NestedScrollView;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.ref.WeakReference;
import dev.ukanth.ufirewall.Api;
import dev.ukanth.ufirewall.R;
import dev.ukanth.ufirewall.log.Log;
import dev.ukanth.ufirewall.util.G;
public abstract class DataDumpActivity extends AppCompatActivity {
public static final String TAG = "AFWall";
protected static final int MENU_TOGGLE = -3;
protected static final int MENU_COPY = 16;
protected static final int MENU_EXPORT_LOG = 17;
protected static final int MENU_REFRESH = 13;
protected static final int MENU_ZOOM_IN = 22;
protected static final int MENU_ZOOM_OUT = 23;
TextView scaleGesture;
View mScrollView; // Can be either ScrollView or NestedScrollView
// Modern layout components
private TextView rulesTitle;
private TextView rulesStatus;
private TextView rulesContent;
private TextView interfacesContent;
private TextView systemContent;
private TextView preferencesContent;
private TextView logcatContent;
private CardView interfacesCard;
private CardView systemCard;
private CardView preferencesCard;
private CardView logcatCard;
private boolean useModernLayout = true;
protected Menu mainMenu;
protected static String dataText;
// to be filled in by subclasses
protected static String sdDumpFile = "iptables.log";
protected abstract void populateMenu(SubMenu sub);
protected abstract void populateData(final Context ctx);
private static final int MY_PERMISSIONS_REQUEST_WRITE_STORAGE = 1;
private void initModernViews() {
rulesTitle = findViewById(R.id.rules_title);
rulesStatus = findViewById(R.id.rules_status);
rulesContent = findViewById(R.id.rules_content);
interfacesContent = findViewById(R.id.interfaces_content);
systemContent = findViewById(R.id.system_content);
preferencesContent = findViewById(R.id.preferences_content);
logcatContent = findViewById(R.id.logcat_content);
interfacesCard = findViewById(R.id.interfaces_card);
systemCard = findViewById(R.id.system_card);
preferencesCard = findViewById(R.id.preferences_card);
logcatCard = findViewById(R.id.logcat_card);
}
protected void setData(final String data) {
dataText = data;
Handler refresh = new Handler(Looper.getMainLooper());
refresh.post(() -> {
if (useModernLayout) {
parseAndDisplayModernData(data);
} else {
scaleGesture = findViewById(R.id.rules);
scaleGesture.setText(data);
scaleGesture.setTextSize(TypedValue.COMPLEX_UNIT_PX, G.ruleTextSize());
}
});
}
@SuppressLint("SetTextI18n")
private void parseAndDisplayModernData(String data) {
// Initialize all sections as empty and hide cards
rulesContent.setText("");
interfacesContent.setText("");
systemContent.setText("");
preferencesContent.setText("");
logcatContent.setText("");
interfacesCard.setVisibility(View.GONE);
systemCard.setVisibility(View.GONE);
preferencesCard.setVisibility(View.GONE);
logcatCard.setVisibility(View.GONE);
// Parse sections more intelligently by looking for section headers
String[] lines = data.split("\n");
StringBuilder currentSection = new StringBuilder();
String currentSectionType = null;
for (String line : lines) {
// Check if this is a section header (starts with =, contains title, ends with =)
if (line.matches("^=+$")) {
// Skip separator lines
continue;
}
// Check if this line is a section title
String sectionType = detectSectionType(line.trim());
if (sectionType != null) {
// Process previous section if it exists
if (currentSectionType != null && currentSection.length() > 0) {
processSectionContent(currentSectionType, currentSection.toString());
}
// Start new section
currentSectionType = sectionType;
currentSection = new StringBuilder();
continue;
}
// Add line to current section
if (currentSectionType != null) {
currentSection.append(line).append("\n");
}
}
// Process the last section
if (currentSectionType != null && currentSection.length() > 0) {
processSectionContent(currentSectionType, currentSection.toString());
}
// Set font sizes for all content views
float textSize = G.ruleTextSize();
rulesContent.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
interfacesContent.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
systemContent.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
preferencesContent.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
logcatContent.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
// Keep the hidden TextView updated for backward compatibility (export, copy functions)
TextView hiddenRules = findViewById(R.id.rules);
hiddenRules.setText(data);
}
private String detectSectionType(String line) {
String trimmed = line.trim();
if (trimmed.equals(getString(R.string.ipv4_rules_title))) {
return "ipv4_rules";
} else if (trimmed.equals(getString(R.string.ipv6_rules_title))) {
return "ipv6_rules";
} else if (trimmed.contains("Network interfaces")) {
return "interfaces";
} else if (trimmed.contains("ifconfig")) {
return "ifconfig";
} else if (trimmed.contains("System info")) {
return "system";
} else if (trimmed.contains("Preferences")) {
return "preferences";
} else if (trimmed.contains("Logcat")) {
return "logcat";
}
return null;
}
private void processSectionContent(String sectionType, String content) {
String trimmedContent = content.trim();
if (trimmedContent.isEmpty()) return;
switch (sectionType) {
case "ipv4_rules":
rulesTitle.setText(getString(R.string.ipv4_rules_title));
rulesStatus.setText(getString(R.string.ready));
rulesContent.setText(trimmedContent);
break;
case "ipv6_rules":
rulesTitle.setText(getString(R.string.ipv6_rules_title));
rulesStatus.setText(getString(R.string.ready));
rulesContent.setText(trimmedContent);
break;
case "interfaces":
case "ifconfig":
interfacesCard.setVisibility(View.VISIBLE);
String existingInterfaces = interfacesContent.getText().toString();
if (!existingInterfaces.isEmpty()) {
interfacesContent.setText(existingInterfaces + "\n\n" + trimmedContent);
} else {
interfacesContent.setText(trimmedContent);
}
break;
case "system":
systemCard.setVisibility(View.VISIBLE);
systemContent.setText(trimmedContent);
break;
case "preferences":
preferencesCard.setVisibility(View.VISIBLE);
String existingPrefs = preferencesContent.getText().toString();
if (!existingPrefs.isEmpty()) {
preferencesContent.setText(existingPrefs + "\n\n" + trimmedContent);
} else {
preferencesContent.setText(trimmedContent);
}
break;
case "logcat":
logcatCard.setVisibility(View.VISIBLE);
logcatContent.setText(trimmedContent);
break;
}
}
private void updateModernTextSize(float sizeDelta) {
float currentSize = G.ruleTextSize();
float newSize = currentSize + sizeDelta;
if (newSize < 8.0f) newSize = 8.0f; // Minimum size
if (newSize > 30.0f) newSize = 30.0f; // Maximum size
G.ruleTextSize((int) newSize);
if (rulesContent != null) rulesContent.setTextSize(TypedValue.COMPLEX_UNIT_PX, newSize);
if (interfacesContent != null) interfacesContent.setTextSize(TypedValue.COMPLEX_UNIT_PX, newSize);
if (systemContent != null) systemContent.setTextSize(TypedValue.COMPLEX_UNIT_PX, newSize);
if (preferencesContent != null) preferencesContent.setTextSize(TypedValue.COMPLEX_UNIT_PX, newSize);
if (logcatContent != null) logcatContent.setTextSize(TypedValue.COMPLEX_UNIT_PX, newSize);
}
private void initTheme() {
switch (G.getSelectedTheme()) {
case "D" -> setTheme(R.style.AppDarkTheme);
case "L" -> setTheme(R.style.AppLightTheme);
case "B" -> setTheme(R.style.AppBlackTheme);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
//requestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY);
super.onCreate(savedInstanceState);
initTheme();
if (useModernLayout) {
setContentView(R.layout.rules_modern);
initModernViews();
} else {
setContentView(R.layout.rules);
}
Toolbar toolbar = findViewById(R.id.rule_toolbar);
//toolbar.setTitle(getString(R.string.showrules_title));
toolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
});
setSupportActionBar(toolbar);
mScrollView = findViewById(R.id.ruleScrollView);
// Load partially transparent black background
if (getSupportActionBar() != null) {
getSupportActionBar().setHomeButtonEnabled(true);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
setData("");
populateData(this);
Api.updateLanguage(getApplicationContext(), G.locale());
}
@Override
public boolean onCreateOptionsMenu(android.view.Menu menu) {
// Common options: Copy, Export to SD Card, Refresh
SubMenu sub = menu.addSubMenu(0, MENU_TOGGLE, 0, "").setIcon(R.drawable.ic_flow);
sub.add(0, MENU_ZOOM_IN, 0, getString(R.string.label_zoomin)).setIcon(R.drawable.zoomin).setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
sub.add(0, MENU_ZOOM_OUT, 0, getString(R.string.label_zoomout)).setIcon(R.drawable.zoomout).setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
sub.add(0, MENU_COPY, 0, R.string.copy).setIcon(R.drawable.ic_copy);
sub.add(0, MENU_EXPORT_LOG, 0, R.string.export_to_sd).setIcon(R.drawable.ic_export);
sub.add(0, MENU_REFRESH, 0, R.string.refresh).setIcon(R.drawable.ic_refresh);
populateMenu(sub);
sub.getItem().setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
super.onCreateOptionsMenu(menu);
mainMenu = menu;
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
float newSize;
switch (item.getItemId()) {
case MENU_COPY -> {
copy();
return true;
}
case MENU_EXPORT_LOG -> {
exportToSD();
return true;
}
case MENU_REFRESH -> {
populateData(this);
return true;
}
case MENU_ZOOM_IN -> {
if (useModernLayout) {
updateModernTextSize(2.0f);
} else {
newSize = scaleGesture.getTextSize() + 2.0f;
scaleGesture.setTextSize(TypedValue.COMPLEX_UNIT_PX, newSize);
G.ruleTextSize((int) newSize);
}
return false;
}
case MENU_ZOOM_OUT -> {
if (useModernLayout) {
updateModernTextSize(-2.0f);
} else {
newSize = scaleGesture.getTextSize() - 2.0f;
scaleGesture.setTextSize(TypedValue.COMPLEX_UNIT_PX, newSize);
G.ruleTextSize((int) newSize);
}
return false;
}
default -> {
return super.onOptionsItemSelected(item);
}
}
}
private static class Task implements Runnable {
public String filename = "";
private final Context ctx;
private final WeakReference<DataDumpActivity> activityReference;
private final Handler handler = new Handler(Looper.getMainLooper());
// only retain a weak reference to the activity
Task(DataDumpActivity context) {
this.ctx = context;
activityReference = new WeakReference<>(context);
}
@Override
public void run() {
FileOutputStream output = null;
boolean res = false;
try {
File file;
if(Build.VERSION.SDK_INT < Build.VERSION_CODES.Q ){
File dir = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/" );
dir.mkdirs();
file = new File(dir, sdDumpFile);
} else{
file = new File(ctx.getExternalFilesDir(null) + "/" + sdDumpFile) ;
}
output = new FileOutputStream(file);
output.write(dataText.getBytes());
filename = file.getAbsolutePath();
res = true;
} catch (IOException e) {
Log.e(TAG,e.getMessage(),e);
} finally {
try {
if (output != null) {
output.flush();
output.close();
}
} catch (IOException ex) {
Log.e(TAG,ex.getMessage(),ex);
}
}
final boolean result = res;
handler.post(() -> {
DataDumpActivity activity = activityReference.get();
if (activity == null || activity.isFinishing()) return;
if (result) {
Api.toast(ctx, ctx.getString(R.string.export_rules_success) + filename, Toast.LENGTH_LONG);
} else {
Api.toast(ctx, ctx.getString(R.string.export_logs_fail), Toast.LENGTH_LONG);
}
});
}
}
private void exportToSD() {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ){
// Do some stuff
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.execute(new Task(this));
} else {
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
// permissions have not been granted.
ActivityCompat.requestPermissions(DataDumpActivity.this,
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
MY_PERMISSIONS_REQUEST_WRITE_STORAGE);
} else{
new Task(this).run();
}
}
}
private void copy() {
try {
TextView rulesText = findViewById(R.id.rules);
android.content.ClipboardManager clipboard = (android.content.ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
android.content.ClipData clip = android.content.ClipData
.newPlainText("", rulesText.getText().toString());
clipboard.setPrimaryClip(clip);
Api.toast(this, this.getString(R.string.copied));
} catch (Exception e) {
Log.d("AFWall+", "Exception in Clipboard" + e);
}
Api.toast(this, this.getString(R.string.copied));
}
}

View file

@ -0,0 +1,72 @@
package dev.ukanth.ufirewall.activity;
import android.os.Bundle;
import android.view.MenuItem;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import dev.ukanth.ufirewall.BuildConfig;
import dev.ukanth.ufirewall.R;
import dev.ukanth.ufirewall.util.G;
public class HelpActivity extends AppCompatActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
initTheme();
setContentView(R.layout.help_about);
Toolbar toolbar = findViewById(R.id.help_toolbar);
setSupportActionBar(toolbar);
if (getSupportActionBar() != null) {
getSupportActionBar().setTitle(R.string.help);
getSupportActionBar().setHomeButtonEnabled(true);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
setupContent();
}
private void setupContent() {
// Setup app title with version
String version = BuildConfig.VERSION_NAME;
TextView titleText = findViewById(R.id.afwall_title);
String versionText = getString(R.string.app_name) + " (v" + version + ")";
if(G.isDoKey(this) || BuildConfig.APPLICATION_ID.equals("dev.ukanth.ufirewall.donate")) {
versionText = versionText + " (Donate) " + getString(R.string.donate_thanks) + " :)";
}
titleText.setText(versionText);
}
private void initTheme() {
switch(G.getSelectedTheme()) {
case "D":
setTheme(R.style.AppDarkTheme);
break;
case "L":
setTheme(R.style.AppLightTheme);
break;
case "B":
setTheme(R.style.AppBlackTheme);
break;
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
finish();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
}

View file

@ -0,0 +1,389 @@
/**
* Display/purge logs and toggle logging
* <p/>
* Copyright (C) 2011-2013 Kevin Cernekee
* <p/>
* This program 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.
* <p/>
* 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 General Public License for more details.
* <p/>
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @author Kevin Cernekee
* @version 1.0
*/
package dev.ukanth.ufirewall.activity;
import static dev.ukanth.ufirewall.util.G.isDonate;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.SubMenu;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import com.afollestad.materialdialogs.DialogAction;
import com.afollestad.materialdialogs.MaterialDialog;
import com.raizlabs.android.dbflow.config.FlowManager;
import com.raizlabs.android.dbflow.sql.language.SQLite;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import dev.ukanth.ufirewall.Api;
import dev.ukanth.ufirewall.R;
import dev.ukanth.ufirewall.log.Log;
import dev.ukanth.ufirewall.log.LogData;
import dev.ukanth.ufirewall.log.LogData_Table;
import dev.ukanth.ufirewall.log.LogDatabase;
import dev.ukanth.ufirewall.log.LogRecyclerViewAdapter;
import dev.ukanth.ufirewall.util.DateComparator;
import dev.ukanth.ufirewall.util.G;
import dev.ukanth.ufirewall.util.SecurityUtil;
import android.os.Handler;
import android.os.Looper;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class LogActivity extends AppCompatActivity implements SwipeRefreshLayout.OnRefreshListener {
private RecyclerView recyclerView;
private LogRecyclerViewAdapter recyclerViewAdapter;
private TextView emptyView;
private SwipeRefreshLayout mSwipeLayout;
protected Menu mainMenu;
protected static final int MENU_TOGGLE = -4;
protected static final int MENU_CLEAR = 40;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
initTheme();
setContentView(R.layout.log_view);
Toolbar toolbar = findViewById(R.id.rule_toolbar);
setTitle(getString(R.string.showlog_title));
toolbar.setNavigationOnClickListener(v -> finish());
setSupportActionBar(toolbar);
// Load partially transparent black background
if (getSupportActionBar() != null) {
getSupportActionBar().setHomeButtonEnabled(true);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
Bundle bundle = getIntent().getExtras();
if(bundle != null) {
Object data = bundle.get("validate");
if(data != null){
String check = (String) data;
if(check.equals("yes")) {
new SecurityUtil(LogActivity.this).passCheck();
}
}
}
mSwipeLayout = findViewById(R.id.swipeContainer);
mSwipeLayout.setOnRefreshListener(this);
recyclerView = findViewById(R.id.recyclerview);
emptyView = findViewById(R.id.empty_view);
initializeRecyclerView(getApplicationContext());
if(G.enableLogService()) {
CollectLog collectLog = new CollectLog().setContext(this);
collectLog.execute();
} else {
recyclerView.setVisibility(View.GONE);
mSwipeLayout.setVisibility(View.GONE);
emptyView.setVisibility(View.VISIBLE);
}
}
private void initTheme() {
switch(G.getSelectedTheme()) {
case "D":
setTheme(R.style.AppDarkTheme);
break;
case "L":
setTheme(R.style.AppLightTheme);
break;
case "B":
setTheme(R.style.AppBlackTheme);
break;
}
}
private void initializeRecyclerView(final Context ctx) {
recyclerView.setHasFixedSize(true);
recyclerView.setLayoutManager(new LinearLayoutManager(getApplicationContext()));
recyclerViewAdapter = new LogRecyclerViewAdapter(getApplicationContext(), logData -> {
if(G.isDoKey(ctx) || isDonate()) {
Intent intent = new Intent(ctx, LogDetailActivity.class);
intent.putExtra("DATA",logData.getUid());
startActivity(intent);
} else {
Api.donateDialog(LogActivity.this,false);
}
});
recyclerView.setAdapter(recyclerViewAdapter);
}
private List<LogData> getLogData() {
//load 3 day data
long loadInterval = System.currentTimeMillis() - 259200000;
List<LogData> logData = SQLite.select()
.from(LogData.class)
.where(LogData_Table.timestamp.greaterThan(loadInterval))
.orderBy(LogData_Table.timestamp,true)
.queryList();
//auto purge old data - > week old data
Api.purgeOldLog();
return logData;
}
private int getCount() {
long l = SQLite.selectCountOf().from(LogData.class).count();
return (int) l;
}
private class CollectLog implements Runnable {
private Context context = null;
MaterialDialog loadDialog = null;
public CollectLog() {
}
public CollectLog setContext(Context context) {
this.context = context;
return this;
}
public void execute() {
Handler handler = new Handler(Looper.getMainLooper());
handler.post(() -> {
onPreExecute();
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.execute(this);
});
}
protected void onPreExecute() {
loadDialog = new MaterialDialog.Builder(context).cancelable(false)
.title(getString(R.string.working))
.cancelable(false)
.content(getString(R.string.loading_data))
.progress(true, 0).show();
}
protected Boolean doInBackground() {
List<LogData> logData = getLogData();
try {
if (logData != null && logData.size() > 0) {
logData = updateMap(logData, this);
Collections.sort(logData, new DateComparator());
recyclerViewAdapter.updateData(logData);
return true;
} else {
return false;
}
} catch (Exception e) {
Log.e(Api.TAG, "Exception while retrieving data" + e.getLocalizedMessage());
return null;
}
}
protected void onPostExecute(Boolean logPresent) {
try {
if ((loadDialog != null) && loadDialog.isShowing()) {
loadDialog.dismiss();
}
} catch (IllegalArgumentException e) {
// Handle or log or ignore
} catch (final Exception e) {
// Handle or log or ignore
} finally {
loadDialog = null;
}
recyclerView.setAdapter(null);
recyclerView.setAdapter(recyclerViewAdapter);
mSwipeLayout.setRefreshing(false);
if (logPresent != null && logPresent) {
recyclerViewAdapter.notifyDataSetChanged();
recyclerView.setVisibility(View.VISIBLE);
mSwipeLayout.setVisibility(View.VISIBLE);
emptyView.setVisibility(View.GONE);
} else {
mSwipeLayout.setVisibility(View.GONE);
recyclerView.setVisibility(View.GONE);
emptyView.setVisibility(View.VISIBLE);
}
recyclerView.getRecycledViewPool().clear();
recyclerView.setRecycledViewPool(new RecyclerView.RecycledViewPool());
}
@Override
public void run() {
Boolean result = doInBackground();
Handler handler = new Handler(Looper.getMainLooper());
handler.post(() -> onPostExecute(result));
}
}
@Override
public boolean onCreateOptionsMenu(android.view.Menu menu) {
// Common options: Copy, Export to SD Card, Refresh
SubMenu sub = menu.addSubMenu(0, MENU_TOGGLE, 0, "").setIcon(R.drawable.ic_flow);
sub.add(0, MENU_CLEAR, 0, R.string.clear_log).setIcon(R.drawable.ic_clearlog);
//populateMenu(sub);
sub.getItem().setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS| MenuItem.SHOW_AS_ACTION_WITH_TEXT);
super.onCreateOptionsMenu(menu);
mainMenu = menu;
return true;
}
static <T> List<List<T>> split(List<T> list, final int L) {
List<List<T>> parts = new ArrayList<List<T>>();
final int N = list.size();
for (int i = 0; i < N; i += L) {
parts.add(new ArrayList<T>(
list.subList(i, Math.min(N, i + L)))
);
}
return parts;
}
private List<LogData> updateMap(final List<LogData> logDataList, CollectLog collectLog) {
final HashMap<Integer, LogData> logMap = new HashMap<>();
final HashMap<Integer, Integer> count = new HashMap<>();
final HashMap<Integer, Long> lastBlocked = new HashMap<>();
List<LogData> analyticsList = new ArrayList();
//int counter = 0;
final int size = logDataList.size();
//List<List<LogData>> parts = split(logDataList, 10);
/*for(List listLog: parts) {
Thread t = new Thread() {*/
LogData tmpData,data;
// public void run() {
for (int i=0; i<size; i++) {
//collectLog.doProgress(counter++);
tmpData = logDataList.get(i);
data = logDataList.get(i);
if (logMap.containsKey(data.getUid())) {
if (data.getTimestamp() > lastBlocked.get(data.getUid())) {
lastBlocked.put(data.getUid(), data.getTimestamp());
tmpData.setTimestamp(data.getTimestamp());
} else {
tmpData.setTimestamp(lastBlocked.get(data.getUid()));
}
//data already Present. Update the template here
count.put(data.getUid(), count.get(data.getUid()).intValue() + 1);
tmpData.setCount(count.get(data.getUid()).intValue());
} else {
//process template here
count.put(data.getUid(), 1);
tmpData.setCount(1);
lastBlocked.put(data.getUid(), data.getTimestamp());
}
logMap.put(data.getUid(), tmpData);
}
//}
//};
//t.start();
//}
for (Map.Entry<Integer, LogData> entry : logMap.entrySet()) {
analyticsList.add(entry.getValue());
}
return analyticsList;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home: {
onBackPressed();
return true;
}
case MENU_CLEAR:
clearDatabase(LogActivity.this);
return true;
/*case MENU_EXPORT_LOG:
//exportToSD();
return true;*/
default:
return super.onOptionsItemSelected(item);
}
}
private void clearDatabase(final Context ctx) {
new MaterialDialog.Builder(this)
.title(getApplicationContext().getString(R.string.clear_log) + " ?")
.cancelable(true)
.onPositive(new MaterialDialog.SingleButtonCallback() {
@Override
public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) {
//SQLite.delete(LogData_Table.class);
FlowManager.getDatabase(LogDatabase.NAME).reset();
Toast.makeText(ctx, ctx.getString(R.string.log_cleared), Toast.LENGTH_SHORT).show();
dialog.dismiss();
(new CollectLog()).setContext(LogActivity.this).execute();
}
})
.onNegative(new MaterialDialog.SingleButtonCallback() {
@Override
public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) {
dialog.dismiss();
}
})
.positiveText(R.string.Yes)
.negativeText(R.string.No)
.show();
}
@Override
public void onRefresh() {
(new CollectLog()).setContext(this).run();
}
/*@Override
public boolean onPrepareOptionsMenu(Menu menu) {
// setupLogMenuItem(menu, G.enableFirewallLog());
return super.onPrepareOptionsMenu(menu);
}*/
}

View file

@ -0,0 +1,752 @@
/**
* Display/purge logs and toggle logging
* <p/>
* Copyright (C) 2011-2013 Kevin Cernekee
* <p/>
* This program 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.
* <p/>
* 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 General Public License for more details.
* <p/>
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @author Kevin Cernekee
* @version 1.0
*/
package dev.ukanth.ufirewall.activity;
import android.Manifest;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.view.Menu;
import android.view.MenuItem;
import android.view.SubMenu;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.appcompat.widget.Toolbar;
import androidx.core.app.ActivityCompat;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import com.afollestad.materialdialogs.MaterialDialog;
import com.raizlabs.android.dbflow.sql.language.SQLite;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.Collections;
import java.util.List;
import dev.ukanth.ufirewall.Api;
import dev.ukanth.ufirewall.R;
import dev.ukanth.ufirewall.log.Log;
import dev.ukanth.ufirewall.log.LogData;
import dev.ukanth.ufirewall.log.LogData_Table;
import dev.ukanth.ufirewall.log.LogDetailRecyclerViewAdapter;
import dev.ukanth.ufirewall.log.LogPreference;
import dev.ukanth.ufirewall.log.LogPreference_Table;
import dev.ukanth.ufirewall.util.DateComparator;
import dev.ukanth.ufirewall.util.G;
import dev.ukanth.ufirewall.util.LogNetUtil;
public class LogDetailActivity extends AppCompatActivity implements SwipeRefreshLayout.OnRefreshListener {
private static final int MY_PERMISSIONS_REQUEST_WRITE_STORAGE = 1;
private RecyclerView recyclerView;
protected static String logDumpFile = "log_dump.log";
private LogDetailRecyclerViewAdapter recyclerViewAdapter;
private TextView emptyView;
private SwipeRefreshLayout mSwipeLayout;
protected Menu mainMenu;
private LogData current_selected_logData;
private static List<LogData> logDataList;
private static List<LogData> fullLogDataList; // Store full dataset
// Pagination
private static final int PAGE_SIZE = 100; // Load 100 items at a time
private int currentPage = 0;
private boolean isLoading = false;
// Summary views
private TextView totalBlocks;
private TextView uniqueDestinations;
private TextView timePeriod;
private TextView mostBlockedDestination;
private TextView loadingMoreIndicator;
protected static final int MENU_EXPORT_LOG = 100;
private static int uid;
protected final int MENU_TOGGLE = -4;
protected final int MENU_CLEAR = 40;
final String TAG = "AFWall";
private void initTheme() {
switch (G.getSelectedTheme()) {
case "D":
setTheme(R.style.AppDarkTheme);
break;
case "L":
setTheme(R.style.AppLightTheme);
break;
case "B":
setTheme(R.style.AppBlackTheme);
break;
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
initTheme();
setContentView(R.layout.logdetail_view);
Toolbar toolbar = findViewById(R.id.rule_toolbar);
setTitle(getString(R.string.showlogdetail_title));
toolbar.setNavigationOnClickListener(v -> finish());
setSupportActionBar(toolbar);
Intent intent = getIntent();
uid = intent.getIntExtra("DATA", -1);
// Load partially transparent black background
getSupportActionBar().setHomeButtonEnabled(true);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
mSwipeLayout = findViewById(R.id.swipedetailContainer);
mSwipeLayout.setOnRefreshListener(this);
recyclerView = findViewById(R.id.detailrecyclerview);
emptyView = findViewById(R.id.emptydetail_view);
// Initialize summary views
totalBlocks = findViewById(R.id.total_blocks);
uniqueDestinations = findViewById(R.id.unique_destinations);
timePeriod = findViewById(R.id.time_period);
mostBlockedDestination = findViewById(R.id.most_blocked_destination);
loadingMoreIndicator = findViewById(R.id.loading_more_indicator);
initializeRecyclerView(getApplicationContext());
(new CollectDetailLog()).setContext(this).execute();
}
private void initializeRecyclerView(final Context ctx) {
recyclerView.hasFixedSize();
recyclerView.setLayoutManager(new LinearLayoutManager(getApplicationContext()));
recyclerViewAdapter = new LogDetailRecyclerViewAdapter(getApplicationContext(), logData -> {
current_selected_logData = logData;
recyclerView.showContextMenu();
});
recyclerView.setOnCreateContextMenuListener((menu, v, menuInfo) -> {
menu.setHeaderTitle(R.string.select_the_action);
//groupId, itemId, order, title
//menu.add(0, v.getId(), 0, R.string.add_ip_rule);
menu.add(0, v.getId(), 1, R.string.show_destination_address);
menu.add(0, v.getId(), 2, R.string.show_source_address);
menu.add(0, v.getId(), 3, R.string.ping_destination);
menu.add(0, v.getId(), 4, R.string.ping_source);
menu.add(0, v.getId(), 5, R.string.resolve_destination);
menu.add(0, v.getId(), 6, R.string.resolve_source);
menu.add(0, v.getId(), 9, "Block this destination permanently");
menu.add(0, v.getId(), 10, "Whitelist this destination");
LogPreference logPreference = SQLite.select()
.from(LogPreference.class)
.where(LogPreference_Table.uid.eq(uid)).querySingle();
if (logPreference != null) {
if(logPreference.isDisable()) {
menu.add(0, v.getId(), 7, R.string.displayBlockNotification_enable);
} else {
menu.add(0, v.getId(), 8, R.string.displayBlockNotification);
}
} else {
menu.add(0, v.getId(), 8, R.string.displayBlockNotification);
}
});
recyclerView.setAdapter(recyclerViewAdapter);
}
@Override
public boolean onContextItemSelected(MenuItem item) {
switch (item.getOrder()) {
case 0: // Destination to clipboard
String[] items = {current_selected_logData.getDst(), current_selected_logData.getSrc()};
new MaterialDialog.Builder(this)
.items(items)
.itemsCallbackSingleChoice(-1, (dialog, view, which, text) -> true)
.positiveText(R.string.choose)
.show();
break;
case 1: // Destination to clipboard
new MaterialDialog.Builder(this)
.content(current_selected_logData.getDst() + ":" + current_selected_logData.getDpt())
.title(R.string.destination_address)
.neutralText(R.string.OK)
.positiveText(R.string.copy_text)
.onPositive((dialog, which) -> {
Api.copyToClipboard(LogDetailActivity.this, current_selected_logData.getDst() + ":" + current_selected_logData.getDpt());
Api.toast(LogDetailActivity.this, getString(R.string.destination_copied));
})
.show();
break;
case 2: // Source to clipboard
new MaterialDialog.Builder(this)
.content(current_selected_logData.getSrc() + ":" + current_selected_logData.getSpt())
.title(R.string.source_address)
.neutralText(R.string.OK)
.positiveText(R.string.copy_text)
.onPositive((dialog, which) -> {
Api.copyToClipboard(LogDetailActivity.this, current_selected_logData.getSrc() + ":" + current_selected_logData.getSpt());
Api.toast(LogDetailActivity.this, getString(R.string.source_copied));
})
.show();
break;
case 3: // Ping Destination
new LogNetUtil.NetTask(this).execute(
new LogNetUtil.NetParam(LogNetUtil.JobType.PING, current_selected_logData.getDst())
);
break;
case 4: // Ping Source
new LogNetUtil.NetTask(this).execute(
new LogNetUtil.NetParam(LogNetUtil.JobType.PING, current_selected_logData.getSrc())
);
break;
case 5: // Resolve Destination
new LogNetUtil.NetTask(this).execute(
new LogNetUtil.NetParam(LogNetUtil.JobType.RESOLVE, current_selected_logData.getDst())
);
break;
case 6: // Resolve Source
new LogNetUtil.NetTask(this).execute(
new LogNetUtil.NetParam(LogNetUtil.JobType.RESOLVE, current_selected_logData.getSrc())
);
break;
case 7:
G.updateLogNotification(uid, false);
break;
case 8:
G.updateLogNotification(uid, true);
break;
case 9: // Block destination permanently
showBlockDestinationDialog();
break;
case 10: // Whitelist destination
showWhitelistDestinationDialog();
break;
}
return super.onContextItemSelected(item);
}
private List<LogData> getLogData(final int uid) {
return SQLite.select()
.from(LogData.class)
.where(LogData_Table.uid.eq(uid))
.orderBy(LogData_Table.timestamp, false)
.queryList();
}
private List<LogData> getPagedLogData(final int uid, int page, int pageSize) {
int offset = page * pageSize;
return SQLite.select()
.from(LogData.class)
.where(LogData_Table.uid.eq(uid))
.orderBy(LogData_Table.timestamp, false)
.limit(pageSize)
.offset(offset)
.queryList();
}
private int getCount() {
long l = SQLite.selectCountOf().from(LogData.class).where(LogData_Table.uid.eq(uid)).count();
return (int) l;
}
private class CollectDetailLog extends AsyncTask<Void, Integer, Boolean> {
private Context context = null;
MaterialDialog loadDialog = null;
public CollectDetailLog() {
}
//private boolean suAvailable = false;
public CollectDetailLog setContext(Context context) {
this.context = context;
return this;
}
@Override
protected void onPreExecute() {
loadDialog = new MaterialDialog.Builder(context).cancelable(false).
title(getString(R.string.loading_data)).progress(false, getCount(), true).show();
doProgress(0);
}
public void doProgress(int value) {
publishProgress(value);
}
@Override
protected Boolean doInBackground(Void... params) {
try {
// First, get total count for statistics
int totalCount = getCount();
publishProgress(totalCount);
if (totalCount > PAGE_SIZE) {
// Large dataset - use pagination
fullLogDataList = getLogData(uid); // Get full list for statistics
logDataList = getPagedLogData(uid, 0, PAGE_SIZE); // Get first page
currentPage = 0;
} else {
// Small dataset - load all
logDataList = getLogData(uid);
fullLogDataList = logDataList;
}
if (logDataList != null && logDataList.size() > 0) {
Collections.sort(logDataList, new DateComparator());
recyclerViewAdapter.updateData(logDataList);
return true;
} else {
return false;
}
} catch (Exception e) {
Log.e(Api.TAG, "Exception while retrieving data" + e.getLocalizedMessage());
return null;
}
}
@Override
protected void onProgressUpdate(Integer... progress) {
if (progress[0] == 0 || progress[0] == -1) {
//do nothing
} else {
loadDialog.incrementProgress(progress[0]);
}
}
@Override
protected void onPostExecute(Boolean logPresent) {
super.onPostExecute(logPresent);
doProgress(-1);
try {
if ((loadDialog != null) && loadDialog.isShowing()) {
loadDialog.dismiss();
}
} catch (IllegalArgumentException e) {
// Handle or log or ignore
} catch (final Exception e) {
// Handle or log or ignore
} finally {
loadDialog = null;
}
mSwipeLayout.setRefreshing(false);
if (logPresent != null && logPresent) {
recyclerView.setVisibility(View.VISIBLE);
mSwipeLayout.setVisibility(View.VISIBLE);
emptyView.setVisibility(View.GONE);
recyclerViewAdapter.notifyDataSetChanged();
// Update title with log count
updateTitleWithLogCount();
// Update summary statistics using full dataset
updateSummaryStatistics();
// Setup load more functionality for large datasets
if (fullLogDataList != null && fullLogDataList.size() > PAGE_SIZE) {
setupLoadMoreFunctionality();
}
} else {
mSwipeLayout.setVisibility(View.GONE);
recyclerView.setVisibility(View.GONE);
emptyView.setVisibility(View.VISIBLE);
}
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Common options: Copy, Export to SD Card, Refresh
SubMenu sub = menu.addSubMenu(0, MENU_TOGGLE, 0, "").setIcon(R.drawable.ic_flow);
sub.add(0, MENU_CLEAR, 0, R.string.clear_log).setIcon(R.drawable.ic_clearlog);
//sub.add(0, MENU, 0, R.string.clear_log).setIcon(R.drawable.ic_clearlog);
//sub.add(0, MENU_EXPORT_LOG, 0, R.string.export_to_sd).setIcon(R.drawable.exportr);
//populateMenu(sub);
sub.add(1, MENU_EXPORT_LOG, 0, R.string.export_to_sd).setIcon(R.drawable.ic_export);
sub.getItem().setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
super.onCreateOptionsMenu(menu);
mainMenu = menu;
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home: {
onBackPressed();
return true;
}
case MENU_CLEAR:
clearDatabase(getApplicationContext());
return true;
case MENU_EXPORT_LOG:
exportToSD();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
private void clearDatabase(final Context ctx) {
new MaterialDialog.Builder(this)
.title(getApplicationContext().getString(R.string.clear_log) + " ?")
.cancelable(true)
.onPositive((dialog, which) -> {
//SQLite.delete(LogData_Table.class);
// FlowManager.getDatabase(LogDatabase.NAME).reset();
SQLite.delete(LogData.class)
.where(LogData_Table.uid.eq(uid))
.async()
.execute();
Toast.makeText(getApplicationContext(), ctx.getString(R.string.log_cleared), Toast.LENGTH_SHORT).show();
dialog.dismiss();
})
.onNegative((dialog, which) -> dialog.dismiss())
.positiveText(R.string.Yes)
.negativeText(R.string.No)
.show();
}
@Override
public void onRefresh() {
(new CollectDetailLog()).setContext(this).execute();
}
private void updateTitleWithLogCount() {
if (logDataList != null && logDataList.size() > 0) {
String appName = "";
if (logDataList.get(0).getAppName() != null) {
appName = logDataList.get(0).getAppName();
}
String title = appName + " (" + logDataList.size() + " blocked)";
setTitle(title);
}
}
private void showBlockDestinationDialog() {
if (current_selected_logData == null) return;
new MaterialDialog.Builder(this)
.title("Block Destination")
.content("Add a permanent rule to block all connections to " +
current_selected_logData.getDst() + ":" + current_selected_logData.getDpt() + "?")
.positiveText("Block")
.negativeText("Cancel")
.onPositive((dialog, which) -> {
// Here you would integrate with AFWall's custom rule system
// This is a placeholder for the actual implementation
Api.toast(this, "Feature requires integration with custom rules system");
})
.show();
}
private void showWhitelistDestinationDialog() {
if (current_selected_logData == null) return;
new MaterialDialog.Builder(this)
.title("Whitelist Destination")
.content("Add a permanent rule to allow all connections to " +
current_selected_logData.getDst() + ":" + current_selected_logData.getDpt() + "?")
.positiveText("Allow")
.negativeText("Cancel")
.onPositive((dialog, which) -> {
// Here you would integrate with AFWall's custom rule system
// This is a placeholder for the actual implementation
Api.toast(this, "Feature requires integration with custom rules system");
})
.show();
}
private void updateSummaryStatistics() {
if (fullLogDataList == null || fullLogDataList.isEmpty()) {
return;
}
// Show basic count immediately for better UX (full dataset count)
totalBlocks.setText(String.valueOf(fullLogDataList.size()));
// Process complex statistics in background thread using full dataset
new Thread(() -> {
// Calculate unique destinations and most blocked
java.util.Map<String, Integer> destinationCounts = new java.util.HashMap<>();
java.util.Set<String> uniqueDests = new java.util.HashSet<>();
long oldestTimestamp = Long.MAX_VALUE;
long newestTimestamp = Long.MIN_VALUE;
for (LogData logData : fullLogDataList) {
String destination = logData.getDst() + ":" + logData.getDpt();
uniqueDests.add(destination);
Integer currentCount = destinationCounts.get(destination);
destinationCounts.put(destination, (currentCount == null ? 0 : currentCount) + 1);
// Track time range
oldestTimestamp = Math.min(oldestTimestamp, logData.getTimestamp());
newestTimestamp = Math.max(newestTimestamp, logData.getTimestamp());
}
final int uniqueCount = uniqueDests.size();
final String period = (oldestTimestamp != Long.MAX_VALUE && newestTimestamp != Long.MIN_VALUE) ?
formatTimePeriod(newestTimestamp - oldestTimestamp) : "0s";
// Find most blocked destination
String mostBlocked = "No data";
int maxCount = 0;
for (java.util.Map.Entry<String, Integer> entry : destinationCounts.entrySet()) {
if (entry.getValue() > maxCount) {
maxCount = entry.getValue();
mostBlocked = entry.getKey() + " (" + maxCount + "x)";
}
}
final String finalMostBlocked = mostBlocked;
// Update UI on main thread
runOnUiThread(() -> {
uniqueDestinations.setText(String.valueOf(uniqueCount));
timePeriod.setText(period);
mostBlockedDestination.setText(finalMostBlocked);
});
}).start();
}
private void setupLoadMoreFunctionality() {
// Add scroll listener to load more data when user reaches bottom
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
if (layoutManager != null && !isLoading) {
int visibleItemCount = layoutManager.getChildCount();
int totalItemCount = layoutManager.getItemCount();
int firstVisibleItem = layoutManager.findFirstVisibleItemPosition();
// Load more when we're near the bottom
if ((visibleItemCount + firstVisibleItem) >= totalItemCount - 10) {
loadMoreData();
}
}
}
});
}
private void loadMoreData() {
if (isLoading || fullLogDataList == null) return;
int totalAvailable = fullLogDataList.size();
int currentLoaded = (currentPage + 1) * PAGE_SIZE;
if (currentLoaded >= totalAvailable) {
return; // No more data to load
}
isLoading = true;
currentPage++;
// Show loading indicator
loadingMoreIndicator.setVisibility(View.VISIBLE);
new Thread(() -> {
try {
List<LogData> newData = getPagedLogData(uid, currentPage, PAGE_SIZE);
if (newData != null && !newData.isEmpty()) {
Collections.sort(newData, new DateComparator());
runOnUiThread(() -> {
// Add new data to existing list
logDataList.addAll(newData);
recyclerViewAdapter.notifyItemRangeInserted(
logDataList.size() - newData.size(),
newData.size()
);
loadingMoreIndicator.setVisibility(View.GONE);
isLoading = false;
});
} else {
runOnUiThread(() -> {
loadingMoreIndicator.setVisibility(View.GONE);
isLoading = false;
});
}
} catch (Exception e) {
Log.e(Api.TAG, "Error loading more data", e);
runOnUiThread(() -> {
loadingMoreIndicator.setVisibility(View.GONE);
isLoading = false;
});
}
}).start();
}
private String formatTimePeriod(long millis) {
long seconds = millis / 1000;
long minutes = seconds / 60;
long hours = minutes / 60;
long days = hours / 24;
if (days > 0) {
return days + "d";
} else if (hours > 0) {
return hours + "h";
} else if (minutes > 0) {
return minutes + "m";
} else {
return seconds + "s";
}
}
private static class Task extends AsyncTask<Void, Void, Boolean> {
public String filename = "";
private final Context ctx;
private final WeakReference<LogDetailActivity> activityReference;
// only retain a weak reference to the activity
Task(LogDetailActivity context) {
this.ctx = context;
activityReference = new WeakReference<>(context);
}
@Override
public Boolean doInBackground(Void... args) {
FileOutputStream output = null;
boolean res = false;
try {
File file;
if(Build.VERSION.SDK_INT < Build.VERSION_CODES.Q ){
File dir = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/" );
dir.mkdirs();
file = new File(dir, logDumpFile);
} else{
file = new File(ctx.getExternalFilesDir(null) + "/" + logDumpFile) ;
}
output = new FileOutputStream(file);
StringBuilder builder = new StringBuilder();
builder.append("uid: " + uid);
for(LogData data: logDataList) {
builder.append("src:").append(data.getSrc()).append(",")
.append("dst:").append(data.getDst()).append(",")
.append("proto:").append(data.getProto()).append(",")
.append("sport:").append(data.getSpt()).append(",")
.append("dport:").append(data.getDpt());
builder.append("\n");
}
output.write(builder.toString().getBytes());
filename = file.getAbsolutePath();
res = true;
} catch (FileNotFoundException e) {
Log.e(G.TAG,e.getMessage(),e);
} catch (IOException e) {
Log.e(G.TAG,e.getMessage(),e);
} finally {
try {
if (output != null) {
output.flush();
output.close();
}
} catch (IOException ex) {
Log.e(G.TAG,ex.getMessage(),ex);
}
}
return res;
}
@Override
public void onPostExecute(Boolean res) {
LogDetailActivity activity = activityReference.get();
if (activity == null || activity.isFinishing()) return;
if (res) {
Api.toast(ctx, ctx.getString(R.string.export_rules_success) + filename, Toast.LENGTH_LONG);
} else {
Api.toast(ctx, ctx.getString(R.string.export_logs_fail), Toast.LENGTH_LONG);
}
}
}
private void exportToSD() {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ){
// Do some stuff
new Task(this).execute();
} else {
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
// permissions have not been granted.
ActivityCompat.requestPermissions(LogDetailActivity.this,
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
MY_PERMISSIONS_REQUEST_WRITE_STORAGE);
} else{
new Task(this).execute();
}
}
}
/*@Override
public boolean onPrepareOptionsMenu(Menu menu) {
// setupLogMenuItem(menu, G.enableFirewallLog());
return super.onPrepareOptionsMenu(menu);
}*/
}

View file

@ -0,0 +1,119 @@
/**
* Display/purge logs and toggle logging
* <p/>
* Copyright (C) 2011-2013 Kevin Cernekee
* <p/>
* This program 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.
* <p/>
* 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 General Public License for more details.
* <p/>
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @author Kevin Cernekee
* @version 1.0
*/
package dev.ukanth.ufirewall.activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.MenuItem;
import android.view.SubMenu;
import android.widget.Toast;
import com.afollestad.materialdialogs.MaterialDialog;
import com.raizlabs.android.dbflow.config.FlowManager;
import java.util.List;
import dev.ukanth.ufirewall.Api;
import dev.ukanth.ufirewall.R;
import dev.ukanth.ufirewall.log.LogData;
import dev.ukanth.ufirewall.log.LogDatabase;
import dev.ukanth.ufirewall.log.LogInfo;
import dev.ukanth.ufirewall.util.G;
public class OldLogActivity extends DataDumpActivity {
protected static final int MENU_CLEARLOG = 7;
protected static final int MENU_SWITCH_NEW = 70;
//protected static final int MENU_TOGGLE_LOG = 27;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setTitle(getString(R.string.showlog_title));
getSupportActionBar().setHomeButtonEnabled(true);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
sdDumpFile = "iptables.log";
}
protected void parseAndSet(List<LogData> logDataList) {
String cooked = LogInfo.parseLog(OldLogActivity.this, logDataList);
if (cooked == null) {
setData(getString(R.string.log_parse_error));
} else {
setData(cooked);
}
}
protected void populateData(final Context ctx) {
parseAndSet(Api.fetchLogs());
}
protected void populateMenu(SubMenu sub) {
sub.add(0, MENU_CLEARLOG, 0, R.string.clear_log).setIcon(R.drawable.ic_clearlog);
sub.add(0, MENU_SWITCH_NEW, 0, R.string.switch_new).setIcon(R.drawable.ic_log);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
final Context ctx = this;
switch (item.getItemId()) {
case android.R.id.home: {
onBackPressed();
return true;
}
case MENU_SWITCH_NEW:
Intent i = new Intent(this, LogActivity.class);
G.oldLogView(false);
startActivity(i);
finish();
return true;
case MENU_CLEARLOG:
clearDatabase(OldLogActivity.this);
return true;
}
return super.onOptionsItemSelected(item);
}
private void clearDatabase(final Context ctx) {
new MaterialDialog.Builder(this)
.title(getApplicationContext().getString(R.string.clear_log) + " ?")
.cancelable(true)
.onPositive((dialog, which) -> {
//SQLite.delete(LogData_Table.class);
FlowManager.getDatabase(LogDatabase.NAME).reset();
Toast.makeText(ctx, ctx.getString(R.string.log_cleared), Toast.LENGTH_SHORT).show();
dialog.dismiss();
parseAndSet(Api.fetchLogs());
})
.onNegative((dialog, which) -> dialog.dismiss())
.positiveText(R.string.Yes)
.negativeText(R.string.No)
.show();
}
}

View file

@ -0,0 +1,276 @@
package dev.ukanth.ufirewall.activity;
import static dev.ukanth.ufirewall.util.G.isDonate;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.text.InputType;
import android.view.ContextMenu;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import com.afollestad.materialdialogs.MaterialDialog;
import java.util.ArrayList;
import java.util.List;
import dev.ukanth.ufirewall.Api;
import dev.ukanth.ufirewall.R;
import dev.ukanth.ufirewall.log.Log;
import dev.ukanth.ufirewall.profiles.ProfileAdapter;
import dev.ukanth.ufirewall.profiles.ProfileData;
import dev.ukanth.ufirewall.profiles.ProfileHelper;
import dev.ukanth.ufirewall.util.G;
/**
* Created by ukanth on 31/7/15.
*/
public class ProfileActivity extends AppCompatActivity {
List<ProfileData> profilesList = new ArrayList<ProfileData>();
ProfileAdapter profileAdapter;
protected static final int MENU_ADD = 100;
//protected static final int MENU_CLONE = 101;
protected static final int MENU_DELETE = 102;
protected static final int MENU_RENAME = 103;
protected static final int MENU_CLONE = 104;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.profile_main);
Toolbar toolbar = findViewById(R.id.profile_toolbar);
setSupportActionBar(toolbar);
getSupportActionBar().setHomeButtonEnabled(true);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
initList();
ListView listView = findViewById(R.id.listProfileView);
profileAdapter = new ProfileAdapter(profilesList, this);
listView.setAdapter(profileAdapter);
// we register for the contextmneu
registerForContextMenu(listView);
}
@Override
public boolean onCreateOptionsMenu(android.view.Menu menu) {
// Common options: Copy, Export to SD Card, Refresh
menu.add(0, MENU_ADD, 0, getString(R.string.profile_add)).setIcon(R.drawable.plus).setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
super.onCreateOptionsMenu(menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case MENU_ADD:
addNewProfile();
break;
case android.R.id.home:
onBackPressed();
return true;
default:
return super.onOptionsItemSelected(item);
}
return true;
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v,
ContextMenu.ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
AdapterView.AdapterContextMenuInfo aInfo = (AdapterView.AdapterContextMenuInfo) menuInfo;
//ProfileData profile = profileAdapter.getItem(aInfo.position);
String name = ((TextView) aInfo.targetView.findViewById(R.id.pro_name)).getText().toString();
menu.setHeaderTitle(getString(R.string.select) + " " + name);
if (G.isProfileMigrated()) {
menu.add(0, MENU_CLONE, 0, getString(R.string.clone));
menu.add(0, MENU_RENAME, 0, getString(R.string.rename));
}
menu.add(0, MENU_DELETE, 0, getString(R.string.delete));
}
@Override
public boolean onContextItemSelected(MenuItem item) {
int itemId = item.getItemId();
AdapterView.AdapterContextMenuInfo aInfo = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
String profileName = profilesList.get(aInfo.position).getName();
switch (itemId) {
case MENU_DELETE:
if (!G.isProfileMigrated()) {
if (aInfo.position > 3) {
boolean deleted = G.removeAdditionalProfile(profileName);
if (deleted) {
profilesList.remove(aInfo.position);
profileAdapter.notifyDataSetChanged();
} else {
Api.toast(getApplicationContext(), getString(R.string.delete_profile));
}
} else {
//TODO: can't delete default profiles(1,2,3) msg - Use migrate option
Api.toast(getApplicationContext(), getString(R.string.profile_notsupport));
}
} else {
if (aInfo.position != 0) {
ProfileData data = ProfileHelper.getProfileByName(profileName);
if (data != null && ProfileHelper.deleteProfileByName(profileName)
&& G.clearSharedPreferences(getApplicationContext(), data.getIdentifier())) {
profilesList.remove(aInfo.position);
profileAdapter.notifyDataSetChanged();
}
} else {
//can't delete default profile
}
}
break;
case MENU_CLONE:
if ((G.isDoKey(getApplicationContext()) || isDonate())) {
ProfileData data = ProfileHelper.getProfileByName(profileName);
if(data != null) {
String exitingName = data.getName();
if(data != null) {
new MaterialDialog.Builder(this)
.cancelable(true)
.title(R.string.profile_rename)
.inputType(InputType.TYPE_CLASS_TEXT)
.input(exitingName, exitingName, (dialog, input) -> {
String newName = input.toString();
//copy data
ProfileData data1 = null;
try {
data1 = data.clone();
if (isNotDuplicate(newName)) {
String identifier = newName.replaceAll("\\s+", "");
data1.removeId();
data1.setName(newName);
data1.setIdentifier(identifier);
data1.save();
SharedPreferences fromShared = getSharedPreferences(profileName, Context.MODE_PRIVATE);
SharedPreferences.Editor toShared = getSharedPreferences(newName,Context.MODE_PRIVATE).edit();
Api.copySharedPreferences(fromShared,toShared);
profilesList.add(data1);
profileAdapter.notifyDataSetChanged();
} else {
Api.toast(getApplicationContext(), getString(R.string.profile_duplicate));
}
} catch (CloneNotSupportedException e) {
Log.e(G.TAG, e.getMessage(), e);
}
}).show();
}
} else{
Log.i(G.TAG,"Unable to clone. Data from DB is empty");
Toast.makeText(getApplicationContext(), getString(R.string.unable_clone), Toast.LENGTH_LONG).show();
}
} else{
Api.donateDialog(ProfileActivity.this, true);
}
break;
case MENU_RENAME:
ProfileData data2 = ProfileHelper.getProfileByName(profileName);
if (data2 != null) {
renameProfile(data2, aInfo.position);
}
break;
}
return true;
}
private void initList() {
profilesList = new ArrayList<>();
// We populate the Profiles
profilesList.add(new ProfileData(G.gPrefs.getString("default", getString(R.string.defaultProfile)), ""));
if (G.isProfileMigrated()) {
List<ProfileData> profiles = ProfileHelper.getProfiles();
profilesList.addAll(profiles);
} else {
profilesList.add(new ProfileData(G.gPrefs.getString("profile1", getString(R.string.profile1)), "AFWallProfile1"));
profilesList.add(new ProfileData(G.gPrefs.getString("profile2", getString(R.string.profile2)), "AFWallProfile2"));
profilesList.add(new ProfileData(G.gPrefs.getString("profile3", getString(R.string.profile3)), "AFWallProfile3"));
List<String> pList = G.getAdditionalProfiles();
for (String profileName : pList) {
if (profileName != null && profileName.length() > 0) {
profilesList.add(new ProfileData(profileName, profileName));
}
}
}
}
private void renameProfile(final ProfileData data, final int position) {
String exitingName = data.getName();
new MaterialDialog.Builder(this)
.cancelable(true)
.title(R.string.profile_rename)
.inputType(InputType.TYPE_CLASS_TEXT)
.input(exitingName, exitingName, (dialog, input) -> {
String profileName = input.toString();
if (isNotDuplicate(profileName)) {
profilesList.remove(position);
data.setName(profileName);
data.save();
profilesList.add(position, data);
profileAdapter.notifyDataSetChanged();
} else {
Api.toast(getApplicationContext(), getString(R.string.profile_duplicate));
}
}).show();
}
// Handle user click
private void addNewProfile() {
new MaterialDialog.Builder(this)
.cancelable(true)
.title(R.string.profile_add)
.inputType(InputType.TYPE_CLASS_TEXT)
.input(R.string.profile_add, R.string.profile_hint, (dialog, input) -> {
String profileName = input.toString();
if (isNotDuplicate(profileName)) {
String identifier = profileName.replaceAll("\\s+", "");
ProfileData data = new ProfileData(profileName, identifier);
if (G.isProfileMigrated()) {
//store to database
data.save();
profilesList.add(data);
profileAdapter.notifyDataSetChanged();
} else {
Api.toast(getApplicationContext(), getString(R.string.profile_notsupport));
}
} else {
Api.toast(getApplicationContext(), getString(R.string.profile_duplicate));
}
// We notify the data model is changed
}).show();
}
private boolean isNotDuplicate(String profileName) {
for (ProfileData data : profilesList) {
if (data.getName().equals(profileName)) {
return false;
}
}
return true;
}
}

View file

@ -0,0 +1,369 @@
/**
* Display firewall rules and interface info
* <p>
* Copyright (C) 2011-2013 Kevin Cernekee
* <p>
* This program 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.
* <p>
* 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 General Public License for more details.
* <p>
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @author Kevin Cernekee
* @version 1.0
*/
package dev.ukanth.ufirewall.activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.net.ConnectivityManager;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.MenuItem;
import android.view.SubMenu;
import android.widget.TextView;
import androidx.annotation.NonNull;
import com.afollestad.materialdialogs.DialogAction;
import com.afollestad.materialdialogs.MaterialDialog;
import java.io.File;
import java.util.Map;
import java.util.TreeSet;
import dev.ukanth.ufirewall.Api;
import dev.ukanth.ufirewall.InterfaceDetails;
import dev.ukanth.ufirewall.InterfaceTracker;
import dev.ukanth.ufirewall.R;
import dev.ukanth.ufirewall.log.Log;
import dev.ukanth.ufirewall.service.RootCommand;
import dev.ukanth.ufirewall.util.G;
import dev.ukanth.ufirewall.util.SecurityUtil;
public class RulesActivity extends DataDumpActivity {
protected static final int MENU_FLUSH_RULES = 12;
protected static final int MENU_IPV6_RULES = 19;
protected static final int MENU_IPV4_RULES = 20;
protected static final int MENU_SEND_REPORT = 25;
protected boolean showIPv6 = false;
protected static StringBuilder result;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setTitle(getString(R.string.showrules_title));
//coming from shortcut
Bundle bundle = getIntent().getExtras();
if (bundle != null) {
Object data = bundle.get("validate");
if (data != null) {
String check = (String) data;
if (check.equals("yes")) {
new SecurityUtil( RulesActivity.this).passCheck();
}
}
}
//sdDumpFile = "rules.log";
}
protected void populateMenu(SubMenu sub) {
if (G.enableIPv6()) {
sub.add(0, MENU_IPV6_RULES, 0, R.string.switch_ipv6).setIcon(R.drawable.ic_rules);
sub.add(0, MENU_IPV4_RULES, 0, R.string.switch_ipv4).setIcon(R.drawable.ic_rules);
}
sub.add(0, MENU_FLUSH_RULES, 0, R.string.flush).setIcon(R.drawable.ic_clearlog);
sub.add(0, MENU_SEND_REPORT, 0, R.string.send_report).setIcon(R.drawable.ic_mail);
}
private void writeHeading(StringBuilder res, boolean initialNewline, String title) {
StringBuilder eq = new StringBuilder();
for (int i = 0; i < title.length(); i++) {
eq.append('=');
}
if (initialNewline) {
res.append("\n");
}
res.append(eq).append("\n").append(title).append("\n").append(eq).append("\n\n");
}
protected void appendPreferences(final Context ctx) {
// Fifth section: "Preferences"
writeHeading(result, true, "Preferences");
try {
Map<String, ?> prefs = G.gPrefs.getAll();
for (String s : new TreeSet<String>(prefs.keySet())) {
Object entry = prefs.get(s);
result.append(s).append(": ").append(entry.toString()).append("\n");
}
//append profile mode & Status
result.append("Profile Mode : ").append(G.pPrefs.getString(Api.PREF_MODE, "")).append("\n");
result.append("Status : ").append(Api.isEnabled(ctx) ? "Enabled" : "Disabled").append("\n");
} catch (NullPointerException e) {
result.append("Error retrieving preferences\n");
}
// Sixth section: "Logcat"
writeHeading(result, true, "Logcat");
result.append(Log.getLog());
// finished: post result to the user
setData(result.toString());
}
protected String getFileInfo(String filename) {
File f = new File(filename);
if (f.exists() && f.isFile()) {
return filename + ": " +
f.length() + " bytes\n";
} else {
return filename + ": not present\n";
}
}
protected String getSuInfo(PackageManager pm) {
String[] suPackages = {
"com.koushikdutta.superuser",
"com.noshufou.android.su",
"com.noshufou.android.su.elite",
"com.koushikdutta.superuser",
"com.gorserapp.superuser",
"me.phh.superuser",
"com.bitcubate.superuser.pro",
"com.kingroot.kinguser",
"com.kingroot.master",
"com.kingouser.com",
"com.m0narx.su",
"com.miui.uac",
"eu.chainfire.supersu",
"eu.chainfire.supersu.pro",
"com.topjohnwu.magisk"
};
String found = "none found";
for (String s : suPackages) {
try {
PackageInfo info = pm.getPackageInfo(s, 0);
found = s + " v" + info.versionName;
break;
} catch (NameNotFoundException e) {
}
}
return found;
}
protected void appendSystemInfo(final Context ctx) {
// Fourth section: "System info"
writeHeading(result, true, "System info");
InterfaceDetails cfg = InterfaceTracker.getCurrentCfg(ctx, false);
result.append("Android version: ").append(android.os.Build.VERSION.RELEASE).append("\n");
result.append("Manufacturer: ").append(android.os.Build.MANUFACTURER).append("\n");
result.append("Model: ").append(android.os.Build.MODEL).append("\n");
result.append("Build: ").append(android.os.Build.DISPLAY).append("\n");
if (cfg.netType == ConnectivityManager.TYPE_MOBILE) {
result.append("Active interface: mobile\n");
} else if (cfg.netType == ConnectivityManager.TYPE_WIFI) {
result.append("Active interface: wifi\n");
} else {
result.append("Active interface: unknown\n");
}
result.append("Wifi Tether status: ").append(cfg.tetherWifiStatusKnown ? (cfg.isWifiTethered ? "yes" : "no") : "unknown").append("\n");
result.append("Bluetooth Tether status: ").append(cfg.tetherBluetoothStatusKnown ? (cfg.isBluetoothTethered ? "yes" : "no") : "unknown").append("\n");
result.append("Usb Tether status: ").append(cfg.tetherUsbStatusKnown ? (cfg.isUsbTethered ? "yes" : "no") : "unknown").append("\n");
result.append("Roam status: ").append(cfg.isRoaming ? "yes" : "no").append("\n");
result.append("IPv4 subnet: ").append(cfg.lanMaskV4).append("\n");
result.append("IPv6 subnet: ").append(cfg.lanMaskV6).append("\n");
// filesystem calls can block, so run in another thread
new AsyncTask<Void, Void, String>() {
@Override
public String doInBackground(Void... args) {
StringBuilder ret = new StringBuilder();
ret.append(getFileInfo("/system/bin/su"));
ret.append(getFileInfo("/system/xbin/su"));
ret.append(getFileInfo("/data/magisk/magisk"));
ret.append(getFileInfo("/system/app/Superuser.apk"));
PackageManager pm = ctx.getPackageManager();
ret.append("Superuser: ").append(getSuInfo(pm));
ret.append("\n");
return ret.toString();
}
@Override
public void onPostExecute(String suInfo) {
result.append(suInfo);
updateLoadingState(getString(R.string.finalizing));
appendPreferences(ctx);
}
}.execute();
}
protected void appendIfconfig(final Context ctx) {
// Third section: "ifconfig" (for interface info obtained through busybox)
writeHeading(result, true, "ifconfig");
updateLoadingState(getString(R.string.loading_system_info));
Api.runIfconfig(ctx, new RootCommand()
.setLogging(true)
.setCallback(new RootCommand.Callback() {
public void cbFunc(RootCommand state) {
result.append(state.res);
appendSystemInfo(ctx);
}
}));
}
protected void appendNetworkInterfaces(final Context ctx) {
// Second section: "Network Interfaces" (for interface info obtained through Android APIs)
writeHeading(result, true, "Network interfaces");
Api.runNetworkInterface(ctx, new RootCommand()
.setLogging(true)
.setCallback(new RootCommand.Callback() {
public void cbFunc(RootCommand state) {
String iface = state.res.toString();
result.append(iface);
appendIfconfig(ctx);
}
}));
}
protected void populateData(final Context ctx) {
result = new StringBuilder();
// Update loading state for modern layout
updateLoadingState(getString(R.string.loading));
// First section: "IPxx Rules"
writeHeading(result, false, showIPv6 ? getString(R.string.ipv6_rules_title) : getString(R.string.ipv4_rules_title));
if (showIPv6) {
sdDumpFile = "IPv6rules.log";
} else {
sdDumpFile = "IPv4rules.log";
}
Api.fetchIptablesRules(ctx, showIPv6, new RootCommand()
.setLogging(true)
.setReopenShell(true)
.setFailureToast(R.string.error_fetch)
.setCallback(new RootCommand.Callback() {
public void cbFunc(RootCommand state) {
result.append(state.res);
updateLoadingState(getString(R.string.loading_network_info));
appendNetworkInterfaces(ctx);
}
}));
}
private void updateLoadingState(String status) {
runOnUiThread(() -> {
TextView rulesStatus = findViewById(R.id.rules_status);
TextView rulesTitle = findViewById(R.id.rules_title);
if (rulesStatus != null) {
rulesStatus.setText(status);
}
if (rulesTitle != null) {
String title = showIPv6 ? getString(R.string.ipv6_rules_title) : getString(R.string.ipv4_rules_title);
rulesTitle.setText(title);
}
});
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
final Context ctx = this;
switch (item.getItemId()) {
case android.R.id.home: {
onBackPressed();
return true;
}
case MENU_FLUSH_RULES:
flushAllRules(ctx);
return true;
case MENU_IPV6_RULES:
showIPv6 = true;
updateLoadingState(getString(R.string.loading));
populateData(this);
return true;
case MENU_IPV4_RULES:
showIPv6 = false;
updateLoadingState(getString(R.string.loading));
populateData(this);
return true;
case MENU_SEND_REPORT:
String ver;
try {
ver = ctx.getPackageManager().getPackageInfo(ctx.getPackageName(), 0).versionName;
} catch (NameNotFoundException e) {
ver = "???";
}
String body = dataText + "\n\n" + getString(R.string.enter_problem) + "\n\n";
final Intent emailIntent = new Intent(android.content.Intent.ACTION_SEND);
emailIntent.setType("plain/text");
emailIntent.putExtra(android.content.Intent.EXTRA_EMAIL, new String[]{"afwall-report@googlegroups.com"});
emailIntent.putExtra(android.content.Intent.EXTRA_SUBJECT, "AFWall+ problem report - v" + ver);
emailIntent.putExtra(android.content.Intent.EXTRA_TEXT, body);
startActivity(Intent.createChooser(emailIntent, getString(R.string.send_mail)));
// this shouldn't be necessary, but the default Android email client overrides
// "body=" from the URI. See MessageCompose.initFromIntent()
//email.putExtra(Intent.EXTRA_TEXT, body);
//startActivity(Intent.createChooser(email, getString(R.string.send_mail)));
return true;
}
return super.onOptionsItemSelected(item);
}
private void flushAllRules(final Context ctx) {
new MaterialDialog.Builder(this)
.title(R.string.confirmation)
.content(R.string.flushRulesConfirm)
.positiveText(R.string.Yes)
.negativeText(R.string.No)
.onPositive((dialog, which) -> {
Api.flushAllRules(ctx, new RootCommand()
.setReopenShell(true)
.setSuccessToast(R.string.flushed)
.setFailureToast(R.string.error_purge)
.setCallback(new RootCommand.Callback() {
public void cbFunc(RootCommand state) {
populateData(ctx);
}
}));
dialog.dismiss();
})
.onNegative((dialog, which) -> dialog.dismiss())
.show();
}
}

View file

@ -0,0 +1,27 @@
package dev.ukanth.ufirewall.activity;
import android.content.Intent;
import android.os.Bundle;
import dev.ukanth.ufirewall.MainActivity;
public class StartActivity extends BaseActivity {
/*
* This activity only existed, so the user can toggle between
* classic and material icon.
*/
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = new Intent(this, MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
if (getIntent().getExtras() != null) {
intent.putExtras(getIntent().getExtras());
}
startActivity(intent);
finish();
}
}

View file

@ -0,0 +1,37 @@
package dev.ukanth.ufirewall.admin;
import android.app.admin.DeviceAdminReceiver;
import android.content.Context;
import android.content.Intent;
import android.widget.Toast;
import dev.ukanth.ufirewall.R;
import dev.ukanth.ufirewall.log.Log;
import dev.ukanth.ufirewall.util.G;
/**
* This is the component that is responsible for actual device administration.
* It becomes the receiver when a policy is applied. It is important that we
* subclass DeviceAdminReceiver class here and to implement its only required
* method onEnabled().
*/
public class AdminDeviceReceiver extends DeviceAdminReceiver {
static final String TAG = "AdminDeviceReceiver";
@Override
public void onEnabled(Context context, Intent intent) {
super.onEnabled(context, intent);
G.enableAdmin(true);
Toast.makeText(context, R.string.device_admin_enabled ,Toast.LENGTH_LONG).show();
Log.d(TAG, "onEnabled");
}
@Override
public void onDisabled(Context context, Intent intent) {
super.onDisabled(context, intent);
G.enableAdmin(false);
Toast.makeText(context, R.string.device_admin_disabled,Toast.LENGTH_LONG).show();
Log.d(TAG, "onDisabled");
}
}

View file

@ -0,0 +1,83 @@
/**
* Detect the connectivity changes (for roaming and LAN subnet changes)
* <p>
* Copyright (C) 2011-2012 Umakanthan Chandran
* <p>
* This program 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.
* <p>
* 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 General Public License for more details.
* <p>
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @author Umakanthan Chandran
* @version 1.0
*/
package dev.ukanth.ufirewall.broadcast;
import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import dev.ukanth.ufirewall.Api;
import dev.ukanth.ufirewall.InterfaceTracker;
import dev.ukanth.ufirewall.log.Log;
import dev.ukanth.ufirewall.util.BootRuleManager;
import dev.ukanth.ufirewall.util.G;
public class ConnectivityChangeReceiver extends BroadcastReceiver {
public static final String TAG = "AFWall";
// These are marked "@hide" in WifiManager.java
public static final String WIFI_AP_STATE_CHANGED_ACTION = "android.net.wifi.WIFI_AP_STATE_CHANGED";
public static final String TETHER_STATE_CHANGED_ACTION = "android.net.conn.TETHER_STATE_CHANGED";
public static final String EXTRA_WIFI_AP_STATE = "wifi_state";
public static final String EXTRA_PREVIOUS_WIFI_AP_STATE = "previous_wifi_state";
@Override
public void onReceive(final Context context, Intent intent) {
int status = Api.getConnectivityStatus(context);
if (status > 0) {
// NOTE: this gets called for wifi/3G/tether/roam changes but not VPN connect/disconnect
// This will prevent applying rules when the user disable the option in preferences. This is for low end devices
if (intent.getAction().equals(WIFI_AP_STATE_CHANGED_ACTION)) {
int newState = intent.getIntExtra(EXTRA_WIFI_AP_STATE, -1);
int oldState = intent.getIntExtra(EXTRA_PREVIOUS_WIFI_AP_STATE, -1);
Log.d(TAG, "OS reported AP state change: " + oldState + " -> " + newState);
// Note: AP state changes are logged but don't trigger rule application during boot
}
if (Api.isEnabled(context) && G.activeRules()) {
String action = intent.getAction();
String reason = action.equals(CONNECTIVITY_ACTION) ?
InterfaceTracker.CONNECTIVITY_CHANGE : InterfaceTracker.TETHER_STATE_CHANGED;
// Check with BootRuleManager if we should process this network change
if (!BootRuleManager.shouldProcessNetworkChange(context, reason)) {
Log.d(TAG, "Network change ignored during boot process: " + reason);
return;
}
if (action.equals(CONNECTIVITY_ACTION)) {
Log.i(TAG, "Network change captured.");
InterfaceTracker.applyRulesOnChange(context, InterfaceTracker.CONNECTIVITY_CHANGE);
} else if (action.equals(TETHER_STATE_CHANGED_ACTION)) {
Log.i(TAG, "Tether change captured.");
InterfaceTracker.applyRulesOnChange(context, InterfaceTracker.TETHER_STATE_CHANGED);
}
}
}
}
}

View file

@ -0,0 +1,70 @@
package dev.ukanth.ufirewall.broadcast;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Messenger;
import dev.ukanth.ufirewall.Api;
import dev.ukanth.ufirewall.InterfaceTracker;
import dev.ukanth.ufirewall.MainActivity;
import dev.ukanth.ufirewall.log.Log;
import dev.ukanth.ufirewall.service.FirewallService;
import dev.ukanth.ufirewall.service.LogService;
import dev.ukanth.ufirewall.util.BootRuleManager;
import dev.ukanth.ufirewall.util.G;
public class OnBootReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
Messenger messenger = null;
if (intent != null) {
Bundle extras = intent.getExtras();
if (extras != null) {
messenger = (Messenger) extras.get("messenger");
}
}
if (messenger == null) {
PackageManager pm = context.getPackageManager();
pm.setComponentEnabledSetting(new ComponentName(context, MainActivity.class),
PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);
}
Log.i("AFWall", "Startin boot service");
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Log.i("AFWall", "Starting firewall service onboot");
context.startForegroundService(new Intent(context, FirewallService.class));
} else {
context.startService(new Intent(context, FirewallService.class));
}
// Use BootRuleManager for robust rule application
BootRuleManager.initializeBootRuleApplication(context);
//register private DNS change listener
if (G.enableLogService()) {
Log.i("AFWall", "Starting log service onboot");
try {
context.startService(new Intent(context, LogService.class));
} catch (Exception e) {
}
}
try {
G.registerPrivateLink();
}catch (Exception e){
}
}
}
}

View file

@ -0,0 +1,192 @@
/**
* Broadcast receiver responsible for removing rules that affect uninstalled apps.
* <p>
* Copyright (C) 2009-2011 Rodrigo Zechin Rosauro
* Copyright (C) 2011-2012 Umakanthan Chandran
* <p>
* This program 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.
* <p>
* 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 General Public License for more details.
* <p>
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @author Rodrigo Zechin Rosauro, Umakanthan Chandran
* @version 1.1
*/
package dev.ukanth.ufirewall.broadcast;
import static dev.ukanth.ufirewall.util.G.isDonate;
import android.Manifest;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.net.Uri;
import android.os.Build;
import android.preference.PreferenceManager;
import androidx.core.app.NotificationCompat;
import java.util.HashSet;
import dev.ukanth.ufirewall.Api;
import dev.ukanth.ufirewall.MainActivity;
import dev.ukanth.ufirewall.R;
import dev.ukanth.ufirewall.log.Log;
import dev.ukanth.ufirewall.service.RootCommand;
import dev.ukanth.ufirewall.util.G;
import dev.ukanth.ufirewall.util.UidResolver;
/**
* Broadcast receiver responsible for removing rules that affect uninstalled
* apps.
*/
public class PackageBroadcast extends BroadcastReceiver {
public static final String TAG = "AFWall";
@Override
public void onReceive(final Context context, final Intent intent) {
Uri inputUri = Uri.parse(intent.getDataString());
if (!inputUri.getScheme().equals("package")) {
Log.d(TAG, "Intent scheme was not 'package'");
return;
}
if (Intent.ACTION_PACKAGE_REMOVED.equals(intent.getAction())) {
// Ignore application updates
final boolean replacing = intent.getBooleanExtra(
Intent.EXTRA_REPLACING, false);
if (!replacing) {
// Update the Firewall if necessary
final int uid = intent.getIntExtra(Intent.EXTRA_UID, -123);
String packageName = context.getPackageManager().getNameForUid(uid);
//if it contains sharedID -- dont remove based on uid
if(packageName != null && packageName.contains("sharedID")) {
//ignore since the another app with same ID exists
} else {
Api.applicationRemoved(context, uid, new RootCommand()
.setFailureToast(R.string.error_apply)
.setCallback(new RootCommand.Callback() {
@Override
public void cbFunc(RootCommand state) {
if (state.exitCode == 0) {
Api.removeCacheLabel(intent.getData().getSchemeSpecificPart(), context);
Api.removeAllUnusedCacheLabel(context);
// Force app list reload next time
Api.applications = null;
// Invalidate UID resolver cache for this UID
UidResolver.invalidateUid(uid);
Log.d(TAG, "Package removed, invalidated UID cache for: " + uid);
}
}
}));
}
}
} else if (Intent.ACTION_PACKAGE_ADDED.equals(intent.getAction())) {
final boolean updateApp = intent.getBooleanExtra(Intent.EXTRA_REPLACING, false);
if (updateApp) {
// dont do anything
//1 check the package already added in firewall
} else {
// Force app list reload next time
Api.applications = null;
// Clear UID resolver cache since new package may get a UID we've seen before
UidResolver.clearCache();
Log.d(TAG, "Package added, cleared UID resolver cache");
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
boolean isNotify = prefs.getBoolean("notifyAppInstall", true);
if (isNotify && Api.isEnabled(context)) {
String added_package = intent.getData().getSchemeSpecificPart();
final PackageManager packager = context.getPackageManager();
String label = null;
try {
ApplicationInfo applicationInfo = packager.getApplicationInfo(added_package, 0);
label = packager.getApplicationLabel(applicationInfo).toString();
if (PackageManager.PERMISSION_GRANTED == packager.checkPermission(Manifest.permission.INTERNET, added_package)) {
addNotification(context,label);
}
if (Api.recentlyInstalled == null) {
Api.recentlyInstalled = new HashSet<>();
}
Api.recentlyInstalled.add(applicationInfo.packageName);
//sets default permissions
if ((G.isDoKey(context) || isDonate())) {
Api.setDefaultPermission(applicationInfo);
}
} catch (NameNotFoundException e) {
}
}
}
}
}
private void addNotification(Context context, String label) {
final int NOTIFICATION_ID = 100;
String NOTIFICATION_CHANNEL_ID = "firewall.app.notification";
String channelName = context.getString(R.string.app_notification);
//cancel existing notification
NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
manager.cancel(NOTIFICATION_ID);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel chan = new NotificationChannel(NOTIFICATION_CHANNEL_ID, channelName, NotificationManager.IMPORTANCE_DEFAULT);
chan.setShowBadge(false);
chan.setSound(null,null);
chan.enableLights(false);
chan.enableVibration(false);
chan.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);
assert manager != null;
manager.createNotificationChannel(chan);
}
Intent appIntent = new Intent(context, MainActivity.class);
appIntent.setAction(Intent.ACTION_MAIN);
appIntent.addCategory(Intent.CATEGORY_LAUNCHER);
appIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
PendingIntent notifyPendingIntent = PendingIntent.getActivity(context, 0, appIntent, PendingIntent.FLAG_IMMUTABLE);
NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID);
notificationBuilder.setContentIntent(notifyPendingIntent);
String notificationText = context.getString(R.string.notification_new);
if (label != null) {
notificationText = label + "-" + context.getString(R.string.notification_new_package);
}
Notification notification = notificationBuilder.setOngoing(false)
.setPriority(NotificationManager.IMPORTANCE_DEFAULT)
.setCategory(Notification.CATEGORY_SERVICE)
.setSound(null)
.setSmallIcon(R.drawable.notification_quest)
.setContentTitle(context.getString(R.string.notification_title))
.setTicker(context.getString(R.string.notification_title))
.setContentText(notificationText)
.build();
manager.notify(NOTIFICATION_ID, notification);
}
}

View file

@ -0,0 +1,75 @@
package dev.ukanth.ufirewall.customrules;
import com.raizlabs.android.dbflow.annotation.Column;
import com.raizlabs.android.dbflow.annotation.PrimaryKey;
import com.raizlabs.android.dbflow.annotation.Table;
import com.raizlabs.android.dbflow.structure.BaseModel;
/**
* Created by ukanth on 24/9/17.
*/
@Table(database = CustomRuleDatabase.class)
public class CustomRule extends BaseModel {
@Column
@PrimaryKey(autoincrement = true)
long id;
public long getId() {
return id;
}
@Column
private String name;
@Column
private String rule;
@Column
private long timestamp;
@Column
private boolean active;
public CustomRule() {
}
public CustomRule(String name, String rule) {
this.name = name;
this.rule = rule;
this.timestamp = System.currentTimeMillis();
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getRule() {
return rule;
}
public void setRule(String rule) {
this.rule = rule;
}
public long getTimestamp() {
return timestamp;
}
public void setTimestamp(long timestamp) {
this.timestamp = timestamp;
}
public boolean isActive() {
return active;
}
public void setActive(boolean active) {
this.active = active;
}
}

View file

@ -0,0 +1,13 @@
package dev.ukanth.ufirewall.customrules;
import com.raizlabs.android.dbflow.annotation.Database;
/**
* Created by ukanth on 24/9/17.
*/
@Database(name = CustomRuleDatabase.NAME, version = CustomRuleDatabase.VERSION)
public class CustomRuleDatabase {
public static final String NAME = "rules";
public static final int VERSION = 1;
}

View file

@ -0,0 +1,17 @@
package dev.ukanth.ufirewall.events;
import android.content.Context;
/**
* Created by ukanth on 21/8/16.
*/
public class LogChangeEvent {
public final Context ctx;
public final String message;
public LogChangeEvent(String message, Context ctx) {
this.message = message;
this.ctx = ctx;
}
}

View file

@ -0,0 +1,21 @@
package dev.ukanth.ufirewall.events;
import android.content.Context;
import dev.ukanth.ufirewall.log.LogInfo;
/**
* Created by ukanth on 21/8/16.
*/
public class LogEvent {
public final LogInfo logInfo;
public final Context ctx;
public LogEvent(LogInfo logInfo, Context ctx) {
this.logInfo = logInfo;
this.ctx = ctx;
}
}

View file

@ -0,0 +1,17 @@
package dev.ukanth.ufirewall.events;
import android.content.Context;
/**
* Created by ukanth on 21/8/16.
*/
public class RulesEvent {
public final Context ctx;
public final String message;
public RulesEvent(String message,Context ctx) {
this.message = message;
this.ctx = ctx;
}
}

View file

@ -0,0 +1,27 @@
package dev.ukanth.ufirewall.events;
import androidx.annotation.NonNull;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.functions.Consumer;
import io.reactivex.rxjava3.subjects.PublishSubject;
/**
* Created by ukanth on 25/9/17.
*/
public class RxCommandEvent {
private static final PublishSubject<Object> sSubject = PublishSubject.create();
private RxCommandEvent() {
// hidden constructor
}
public static Disposable subscribe(@NonNull Consumer<Object> action) {
return sSubject.subscribe(action);
}
public static void publish(@NonNull Object message) {
sSubject.onNext(message);
}
}

View file

@ -0,0 +1,31 @@
package dev.ukanth.ufirewall.events;
import androidx.annotation.NonNull;
import dev.ukanth.ufirewall.log.Log;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.functions.Consumer;
import io.reactivex.rxjava3.subjects.PublishSubject;
/**
* Created by ukanth on 25/9/17.
*/
public class RxEvent {
private final PublishSubject<Object> sSubject = PublishSubject.create();
public RxEvent() {
// hidden constructor
}
public Disposable subscribe(@NonNull Consumer<Object> action) {
return sSubject.subscribe(action, throwable -> {
Log.i("AFWall", throwable.getLocalizedMessage());
});
}
public void publish(@NonNull Object message) {
sSubject.onNext(message);
}
}

View file

@ -0,0 +1,133 @@
/**
* Circular log buffer.
* This provides timestamped logcat output for the "Rules" page, to aid
* in diagnosing failures.
*
* Copyright (C) 2013 Kevin Cernekee
*
* This program 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.
*
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @author Kevin Cernekee
* @version 1.0
*/
package dev.ukanth.ufirewall.log;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.LinkedList;
public class Log {
private static final int MAX_ENTRIES = 512;
public static final int LOG_DEBUG = 0;
public static final int LOG_VERBOSE = 1;
public static final int LOG_INFO = 2;
public static final int LOG_WARNING = 3;
public static final int LOG_ERROR = 4;
public static final int LOG_WTF = 5;
public static class LogEntry {
Date timestamp;
int level;
String msg = "";
}
private static final LinkedList<LogEntry> circ = new LinkedList<LogEntry>();
private static synchronized void circLog(int level, String msg) {
LogEntry e = new LogEntry();
e.timestamp = new Date();
e.level = level;
e.msg = msg;
if (circ.size() >= MAX_ENTRIES) {
circ.removeFirst();
}
circ.addLast(e);
}
public static synchronized String getLog() {
StringBuilder ret = new StringBuilder();
for (int i = 0; i < circ.size(); i++) {
LogEntry e = circ.get(i);
String timestamp = new SimpleDateFormat("HH:mm:ss").format(e.timestamp);
ret.append(timestamp).append(" ").append(e.msg).append("\n");
}
return ret.toString();
}
public static int d(String tag, String msg) {
circLog(LOG_DEBUG, msg);
return android.util.Log.d(tag, msg);
}
public static int d(String tag, String msg, Exception e) {
circLog(LOG_DEBUG, msg);
return android.util.Log.d(tag, msg, e);
}
public static int v(String tag, String msg) {
circLog(LOG_VERBOSE, msg);
return android.util.Log.v(tag, msg);
}
public static int v(String tag, String msg, Exception e) {
circLog(LOG_VERBOSE, msg);
return android.util.Log.v(tag, msg, e);
}
public static int i(String tag, String msg) {
circLog(LOG_INFO, msg);
return android.util.Log.i(tag, msg);
}
public static int i(String tag, String msg, Exception e) {
circLog(LOG_INFO, msg);
return android.util.Log.i(tag, msg, e);
}
public static int w(String tag, String msg) {
circLog(LOG_WARNING, msg);
return android.util.Log.w(tag, msg);
}
public static int w(String tag, String msg, Exception e) {
circLog(LOG_WARNING, msg);
return android.util.Log.w(tag, msg, e);
}
public static int e(String tag, String msg) {
circLog(LOG_ERROR, msg);
return android.util.Log.e(tag, msg);
}
public static int e(String tag, String msg, Exception e) {
circLog(LOG_ERROR, msg);
return android.util.Log.e(tag, msg, e);
}
public static int wtf(String tag, String msg) {
circLog(LOG_WTF, msg);
return android.util.Log.wtf(tag, msg);
}
public static int wtf(String tag, String msg, Exception e) {
circLog(LOG_WTF, msg);
return android.util.Log.wtf(tag, msg, e);
}
}

View file

@ -0,0 +1,164 @@
package dev.ukanth.ufirewall.log;
import com.raizlabs.android.dbflow.annotation.Column;
import com.raizlabs.android.dbflow.annotation.PrimaryKey;
import com.raizlabs.android.dbflow.annotation.Table;
import com.raizlabs.android.dbflow.structure.BaseModel;
/**
* Created by ukanth on 17/1/16.
*/
@Table(database = LogDatabase.class,cachingEnabled = true)
//,indexGroups = { @IndexGroup(number = 1, name = "uidIndex"),})
public class LogData extends BaseModel {
@Column
@PrimaryKey(autoincrement = true)
long id;
@Column
private int uid;
@Column
private String appName;
@Column
private String in;
@Column
private String out;
@Column
private String proto;
@Column
private int spt;
@Column
private String dst;
@Column
private int len;
@Column
private String src;
@Column
private int dpt;
@Column
private long timestamp;
public String getHostname() {
return hostname;
}
public void setHostname(String hostname) {
this.hostname = hostname;
}
public int getType() {
return type;
}
public void setType(int type) {
this.type = type;
}
@Column
private String hostname;
@Column
private int type;
public long getCount() {
return count;
}
public void setCount(long count) {
this.count = count;
}
private long count;
public int getUid() {
return uid;
}
public void setUid(int uid) {
this.uid = uid;
}
public String getAppName() {
return appName;
}
public void setAppName(String appName) {
this.appName = appName;
}
public String getIn() {
return in;
}
public void setIn(String in) {
this.in = in;
}
public String getOut() {
return out;
}
public void setOut(String out) {
this.out = out;
}
public String getProto() {
return proto;
}
public void setProto(String proto) {
this.proto = proto;
}
public int getSpt() {
return spt;
}
public void setSpt(int spt) {
this.spt = spt;
}
public String getDst() {
return dst;
}
public void setDst(String dst) {
this.dst = dst;
}
public int getLen() {
return len;
}
public void setLen(int len) {
this.len = len;
}
public String getSrc() {
return src;
}
public void setSrc(String src) {
this.src = src;
}
public int getDpt() {
return dpt;
}
public void setDpt(int dpt) {
this.dpt = dpt;
}
public long getTimestamp() {
return timestamp;
}
public void setTimestamp(long timestamp) {
this.timestamp = timestamp;
}
}

View file

@ -0,0 +1,33 @@
package dev.ukanth.ufirewall.log;
import com.raizlabs.android.dbflow.annotation.Database;
import com.raizlabs.android.dbflow.annotation.Migration;
import com.raizlabs.android.dbflow.sql.SQLiteType;
import com.raizlabs.android.dbflow.sql.migration.AlterTableMigration;
/**
* Created by ukanth on 17/1/16.
*/
@Database(name = LogDatabase.NAME, version = LogDatabase.VERSION)
public class LogDatabase {
public static final String NAME = "Logs";
public static final int VERSION = 2;
@Migration(version = 2, database = LogDatabase.class)
public static class Migration2 extends AlterTableMigration<LogData> {
public Migration2(Class<LogData> table) {
super(table);
}
@Override
public void onPreMigrate() {
addColumn(SQLiteType.TEXT, "hostname");
addColumn(SQLiteType.INTEGER, "type");
}
}
}

View file

@ -0,0 +1,273 @@
package dev.ukanth.ufirewall.log;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.Map;
import java.util.HashMap;
import dev.ukanth.ufirewall.R;
/**
* Created by ukanth on 25/7/16.
*/
public class LogDetailRecyclerViewAdapter extends RecyclerView.Adapter<LogDetailRecyclerViewAdapter.ViewHolder> {
private final List<LogData> logData;
private final Context context;
private LogData data;
private final RecyclerItemClickListener recyclerItemClickListener;
// Track repeated connection attempts for better UX
private final Map<String, Integer> connectionAttempts = new ConcurrentHashMap<>();
// Cache for expensive operations
private final Map<Integer, String> serviceCache = new HashMap<>();
private final Map<String, String> interfaceNameCache = new HashMap<>();
public LogDetailRecyclerViewAdapter(final Context context, RecyclerItemClickListener recyclerItemClickListener) {
this.context = context;
logData = new ArrayList<>();
this.recyclerItemClickListener = recyclerItemClickListener;
}
public void updateData(List<LogData> logDataList) {
logData.clear();
logData.addAll(logDataList);
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View mView = LayoutInflater.from(parent.getContext()).inflate(R.layout.logdetail_recycle_item, parent, false);
return new ViewHolder(mView);
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
data = logData.get(position);
if (data != null) {
holder.bind(logData.get(position), recyclerItemClickListener);
// Format timestamp with interface info (cached)
String timeAndInterface = pretty(data.getTimestamp());
if (data.getOut() != null && !data.getOut().isEmpty()) {
String interfaceType = getCachedInterfaceDisplayName(data.getOut());
timeAndInterface += " via " + interfaceType;
}
holder.deniedTime.setText(timeAndInterface);
// Set connection type icon with better logic
setConnectionIcon(holder.icon, data.getOut());
// Format destination with better readability
String destination = data.getDst() + ":" + data.getDpt();
holder.dataDest.setText(destination);
// Format source with better readability
String source = data.getSrc() + ":" + data.getSpt();
holder.dataSrc.setText(source);
// Format protocol with more context (cached)
String protocol = data.getProto();
if (protocol != null) {
protocol = protocol.toUpperCase();
// Add service context for common ports (cached)
String serviceInfo = getCachedServiceInfo(data.getDpt(), protocol);
if (!serviceInfo.isEmpty()) {
protocol += " (" + serviceInfo + ")";
}
}
holder.dataProto.setText(protocol != null ? protocol : "Unknown");
// Format hostname with better handling
if (data.getHostname() != null && !data.getHostname().trim().isEmpty() && !data.getHostname().equals(data.getDst())) {
holder.dataHost.setText(data.getHostname());
holder.dataHost.setVisibility(View.VISIBLE);
} else {
holder.dataHost.setVisibility(View.GONE);
}
// Show packet size if available
if (data.getLen() > 0 && holder.packetSize != null) {
holder.packetSize.setText(formatBytes(data.getLen()));
holder.packetSize.setVisibility(View.VISIBLE);
} else if (holder.packetSize != null) {
holder.packetSize.setVisibility(View.GONE);
}
// Hide block count for now to improve performance - can be re-enabled later
if (holder.blockCount != null) {
holder.blockCount.setVisibility(View.GONE);
}
}
}
private void setConnectionIcon(ImageView icon, String outInterface) {
if (outInterface == null || outInterface.isEmpty()) {
icon.setImageDrawable(context.getResources().getDrawable(R.drawable.ic_help));
return;
}
if (outInterface.contains("lan") || outInterface.startsWith("eth") ||
outInterface.startsWith("ra") || outInterface.startsWith("bnep") ||
outInterface.contains("wlan") || outInterface.contains("wifi")) {
icon.setImageDrawable(context.getResources().getDrawable(R.drawable.ic_wifi));
} else if (outInterface.contains("mobile") || outInterface.contains("rmnet") ||
outInterface.contains("ccmni") || outInterface.contains("pdp")) {
icon.setImageDrawable(context.getResources().getDrawable(R.drawable.ic_mobiledata));
} else if (outInterface.contains("tun") || outInterface.contains("ppp")) {
icon.setImageDrawable(context.getResources().getDrawable(R.drawable.ic_lan)); // Use lan icon for VPN as fallback
} else {
icon.setImageDrawable(context.getResources().getDrawable(R.drawable.ic_help)); // Use help icon as fallback
}
}
private String getCachedInterfaceDisplayName(String outInterface) {
if (outInterface == null || outInterface.isEmpty()) {
return "Unknown";
}
// Check cache first
if (interfaceNameCache.containsKey(outInterface)) {
return interfaceNameCache.get(outInterface);
}
String displayName = getInterfaceDisplayName(outInterface);
interfaceNameCache.put(outInterface, displayName);
return displayName;
}
private String getInterfaceDisplayName(String outInterface) {
if (outInterface == null || outInterface.isEmpty()) {
return "Unknown";
}
if (outInterface.contains("lan") || outInterface.startsWith("eth") ||
outInterface.startsWith("ra") || outInterface.startsWith("bnep") ||
outInterface.contains("wlan") || outInterface.contains("wifi")) {
return "Wi-Fi";
} else if (outInterface.contains("mobile") || outInterface.contains("rmnet") ||
outInterface.contains("ccmni") || outInterface.contains("pdp")) {
return "Mobile Data";
} else if (outInterface.contains("tun") || outInterface.contains("ppp")) {
return "VPN";
} else {
return outInterface;
}
}
private String getCachedServiceInfo(int port, String protocol) {
int key = (protocol != null ? protocol.hashCode() : 0) * 100000 + port;
// Check cache first
if (serviceCache.containsKey(key)) {
return serviceCache.get(key);
}
String serviceInfo = getServiceInfo(port, protocol);
serviceCache.put(key, serviceInfo);
return serviceInfo;
}
private String getServiceInfo(int port, String protocol) {
if ("TCP".equals(protocol)) {
switch (port) {
case 80: return "HTTP";
case 443: return "HTTPS";
case 21: return "FTP";
case 22: return "SSH";
case 23: return "Telnet";
case 25: return "SMTP";
case 53: return "DNS";
case 110: return "POP3";
case 143: return "IMAP";
case 993: return "IMAPS";
case 995: return "POP3S";
case 1723: return "PPTP";
case 3389: return "RDP";
case 5060: return "SIP";
case 8080: return "HTTP Alt";
}
} else if ("UDP".equals(protocol)) {
switch (port) {
case 53: return "DNS";
case 67: return "DHCP Server";
case 68: return "DHCP Client";
case 123: return "NTP";
case 500: return "IPSec";
case 1194: return "OpenVPN";
case 5060: return "SIP";
}
}
return "";
}
private String formatBytes(int bytes) {
if (bytes < 1024) {
return bytes + "B";
} else if (bytes < 1024 * 1024) {
return String.format("%.1fKB", bytes / 1024.0);
} else {
return String.format("%.1fMB", bytes / (1024.0 * 1024.0));
}
}
public static String pretty(Long timestamp) {
return android.text.format.DateFormat.format("dd-MM-yyyy hh:mm:ss", new java.util.Date(timestamp)).toString();
}
@Override
public int getItemCount() {
return logData.size();
}
public static class ViewHolder extends RecyclerView.ViewHolder {
final ImageView icon;
final TextView deniedTime;
final TextView dataDest;
final TextView dataSrc;
final TextView dataProto;
final TextView dataHost;
final TextView packetSize;
final TextView blockCount;
public ViewHolder(View itemView) {
super(itemView);
icon = itemView.findViewById(R.id.data_icon);
deniedTime = itemView.findViewById(R.id.denied_time);
dataDest = itemView.findViewById(R.id.data_dest);
dataSrc = itemView.findViewById(R.id.data_src);
dataProto = itemView.findViewById(R.id.data_proto);
dataHost = itemView.findViewById(R.id.data_host);
packetSize = itemView.findViewById(R.id.packet_size);
blockCount = itemView.findViewById(R.id.block_count);
}
public void bind(final LogData item, final RecyclerItemClickListener listener) {
itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
listener.onItemClick(item);
}
});
}
}
public List<LogData> getLogData() {
return logData;
}
}

View file

@ -0,0 +1,345 @@
/**
* Capture Logs from dmesg and return the formatted string
* <p>
* <p>
* Copyright (C) 2014 Umakanthan Chandran
* <p>
* This program 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.
* <p>
* 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 General Public License for more details.
* <p>
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @author Umakanthan Chandran
* @version 1.0
*/
package dev.ukanth.ufirewall.log;
import android.content.Context;
import android.net.ConnectivityManager;
import android.util.Log;
import android.util.SparseArray;
import org.ocpsoft.prettytime.PrettyTime;
import org.ocpsoft.prettytime.TimeUnit;
import org.ocpsoft.prettytime.units.JustNow;
import java.net.InetAddress;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import dev.ukanth.ufirewall.Api;
import dev.ukanth.ufirewall.Api.PackageInfoData;
import dev.ukanth.ufirewall.InterfaceTracker;
import dev.ukanth.ufirewall.R;
import dev.ukanth.ufirewall.util.G;
import dev.ukanth.ufirewall.util.UidResolver;
import dev.ukanth.ufirewall.util.UidCorrelator;
public class LogInfo {
public String uidString;
public String appName;
public int uid;
public String in;
public String out;
public String proto;
public int spt;
public String dst;
public int len;
public String src;
public int dpt;
public String host = "";
public int type;
public long timestamp;
int totalBlocked;
private static PrettyTime prettyTime;
public static String pretty(Date date) {
if (prettyTime == null) {
prettyTime = new PrettyTime(new Locale(G.locale()));
for (TimeUnit t : prettyTime.getUnits()) {
if (t instanceof JustNow) {
prettyTime.removeUnit(t);
break;
}
}
}
prettyTime.setReference(date);
return prettyTime.format(new Date(0));
}
private final HashMap<String, Integer> dstBlocked; // Number of packets blocked per destination IP address
public LogInfo() {
this.dstBlocked = new HashMap<String, Integer>();
}
public static String parseLog(Context ctx, List<LogData> listLogData) {
//final BufferedReader r = new BufferedReader(new StringReader(dmesg.toString()));
StringBuilder res = new StringBuilder();
Integer appid;
final SparseArray<LogInfo> map = new SparseArray<LogInfo>();
LogInfo loginfo = null;
try {
for (LogData logData : listLogData) {
appid = logData.getUid();
loginfo = map.get(appid);
if (loginfo == null) {
loginfo = new LogInfo();
}
loginfo.dst = logData.getDst();
loginfo.dpt = logData.getDpt();
loginfo.spt = logData.getSpt();
loginfo.proto = logData.getProto();
loginfo.len = logData.getLen();
loginfo.src = logData.getSrc();
loginfo.out = logData.getOut();
map.put(appid, loginfo);
loginfo.totalBlocked += 1;
String unique = "[" + loginfo.proto + "]" + loginfo.dst + ":" + loginfo.dpt;
if (loginfo.dstBlocked.containsKey(unique)) {
loginfo.dstBlocked.put(unique, loginfo.dstBlocked.get(unique) + 1);
} else {
loginfo.dstBlocked.put(unique, 1);
}
}
final List<PackageInfoData> apps = Api.getApps(ctx, null);
Integer id;
String appName = "";
int appId = -1;
int totalBlocked;
for (int i = 0; i < map.size(); i++) {
StringBuilder address = new StringBuilder();
id = map.keyAt(i);
appName = ""; // Reset for each iteration
appId = -1;
if (id != -1) {
// First, try to find in cached app list
boolean foundInApps = false;
for (PackageInfoData app : apps) {
if (app.uid == id) {
appId = id;
appName = app.names.get(0);
foundInApps = true;
break;
}
}
// If not found in apps, use comprehensive UID resolver
if (!foundInApps) {
appId = id;
appName = UidResolver.resolveUid(ctx, id);
}
} else {
appName = ctx.getString(R.string.unknown_item);
}
loginfo = map.valueAt(i);
totalBlocked = loginfo.totalBlocked;
if (loginfo.dstBlocked.size() > 0) {
for (String unique : loginfo.dstBlocked.keySet()) {
address.append(unique).append("(").append(loginfo.dstBlocked.get(unique)).append(")");
address.append("\n");
}
}
res.append("AppID :\t");
res.append(appId);
res.append("\n");
res.append(ctx.getString(R.string.LogAppName));
res.append(":\t");
res.append(appName);
res.append("\n");
res.append(ctx.getString(R.string.LogPackBlock));
res.append(":\t");
res.append(totalBlocked);
res.append("\n");
res.append(address.toString());
res.append("\n\t---------\n");
}
} catch (Exception e) {
return null;
}
if (res.length() == 0) {
res.append(ctx.getString(R.string.no_log));
}
return res.toString();
}
public static LogInfo parseLogs(String result, final Context ctx, String pattern, int type) {
StringBuilder address;
int start, end;
Integer uid = -100;
Integer strUid;
String out, src, dst, proto, spt, dpt, len;
LogInfo logInfo = new LogInfo();
HashMap<Integer, String> appNameMap = new HashMap<Integer, String>();
final List<PackageInfoData> apps = Api.getApps(ctx, null);
int pos = 0;
try {
while ((pos = result.indexOf(pattern, pos)) > -1) {
if (result.indexOf(pattern) == -1)
continue;
if (((start = result.indexOf("UID=")) != -1)
&& ((end = result.indexOf(" ", start)) != -1)) {
strUid = Integer.parseInt(result.substring(start + 4, end));
if (strUid != null) {
uid = strUid;
logInfo.uid = strUid;
}
}
//logInfo = new LogInfo();
if (((start = result.indexOf("DST=")) != -1)
&& ((end = result.indexOf(" ", start)) != -1)) {
dst = result.substring(start + 4, end);
logInfo.dst = dst;
}
if (((start = result.indexOf("DPT=")) != -1)
&& ((end = result.indexOf(" ", start)) != -1)) {
dpt = result.substring(start + 4, end);
logInfo.dpt = Integer.parseInt(dpt);
}
if (((start = result.indexOf("SPT=")) != -1)
&& ((end = result.indexOf(" ", start)) != -1)) {
spt = result.substring(start + 4, end);
logInfo.spt = Integer.parseInt(spt);
}
if (((start = result.indexOf("PROTO=")) != -1)
&& ((end = result.indexOf(" ", start)) != -1)) {
proto = result.substring(start + 6, end);
logInfo.proto = proto;
}
if (((start = result.indexOf("LEN=")) != -1)
&& ((end = result.indexOf(" ", start)) != -1)) {
len = result.substring(start + 4, end);
logInfo.len = Integer.parseInt(len);
}
if (((start = result.indexOf("SRC=")) != -1)
&& ((end = result.indexOf(" ", start)) != -1)) {
src = result.substring(start + 4, end);
logInfo.src = src;
}
if (((start = result.indexOf("OUT=")) != -1)
&& ((end = result.indexOf(" ", start)) != -1)) {
out = result.substring(start + 4, end);
if(out.isEmpty()) {
logInfo.out = (InterfaceTracker.getCurrentCfg(ctx,false).netType == ConnectivityManager.TYPE_WIFI ? "eth" : "mobile");
} else {
logInfo.out = out;
}
}
if (uid == android.os.Process.myUid()) {
return null;
}
String appName = "";
if(logInfo.proto != null && logInfo.proto.toLowerCase().startsWith("icmp")) {
//appName = "ICMP";
return null;
//logInfo.uid = 0;
} else if(uid == -100) {
// Attempt enhanced UID correlation before giving up
int correlatedUid = UidCorrelator.correlateUid(
logInfo.src, logInfo.dst, logInfo.dpt, logInfo.spt,
logInfo.proto, System.currentTimeMillis());
if (correlatedUid != -100) {
// Successfully correlated! Update UID and continue with normal processing
uid = correlatedUid;
logInfo.uid = correlatedUid;
Log.d(Api.TAG, "Enhanced correlation resolved UID " + correlatedUid +
" for connection to " + logInfo.dst + ":" + logInfo.dpt);
} else {
// Still unknown after correlation attempt
appName = ctx.getString(R.string.unknown_item);
logInfo.uid = uid;
}
}
// Process the UID (whether original, correlated, or unknown)
if (uid != -100) {
if (uid < 2000) {
appName = Api.getSpecialAppName(uid);
if(uid == 1000) {
appName = ctx.getString(R.string.android_system);
}
} else {
//system level packages
try {
if (!appNameMap.containsKey(uid)) {
appName = ctx.getPackageManager().getNameForUid(uid);
for (PackageInfoData app : apps) {
if (app.uid == uid) {
appName = app.names.get(0);
break;
}
}
} else {
appName = appNameMap.get(uid);
}
} catch (Exception e) {
//could be kernel
Log.e(Api.TAG, "Exception in LogInfo when trying to find name for uid " + uid + "");
logInfo.uid = uid;
appName = ctx.getString(R.string.unknown_item);
}
}
}
logInfo.appName = appName;
address = new StringBuilder();
//address.append(ctx.getString(R.string.blocked));
//address.append(" ");
address.append(appName);
address.append("(").append(uid).append(") ");
address.append(logInfo.dst);
address.append(":");
address.append(logInfo.dpt);
logInfo.type = type;
if (G.showHost()) {
try {
String add = InetAddress.getByName(logInfo.dst).getHostName();
if (add != null) {
logInfo.host = add;
address.append("(").append(add).append(") ");
}
} catch (Exception e) {
}
}
address.append("\n");
logInfo.timestamp = System.currentTimeMillis();
logInfo.uidString = address.toString();
return logInfo;
}
} catch (Exception e) {
Log.e(Api.TAG, "Exception in LogService", e);
}
return logInfo;
}
}

View file

@ -0,0 +1,83 @@
package dev.ukanth.ufirewall.log;
import com.raizlabs.android.dbflow.annotation.Column;
import com.raizlabs.android.dbflow.annotation.PrimaryKey;
import com.raizlabs.android.dbflow.annotation.Table;
import com.raizlabs.android.dbflow.structure.BaseModel;
/**
* Created by ukanth on 17/1/16.
*/
@Table(database = LogPreferenceDB.class)
public class LogPreference extends BaseModel {
@Column
@PrimaryKey
private int uid;
@Column
private String appName;
@Column
private long skipInterval;
@Column
private boolean skip;
@Column
private long timestamp;
public boolean isDisable() {
return disable;
}
public void setDisable(boolean disable) {
this.disable = disable;
}
@Column
private boolean disable;
public long getSkipInterval() {
return skipInterval;
}
public void setSkipInterval(long skipInterval) {
this.skipInterval = skipInterval;
}
public boolean isSkip() {
return skip;
}
public void setSkip(boolean skip) {
this.skip = skip;
}
public int getUid() {
return uid;
}
public void setUid(int uid) {
this.uid = uid;
}
public String getAppName() {
return appName;
}
public void setAppName(String appName) {
this.appName = appName;
}
public long getTimestamp() {
return timestamp;
}
public void setTimestamp(long timestamp) {
this.timestamp = timestamp;
}
}

View file

@ -0,0 +1,15 @@
package dev.ukanth.ufirewall.log;
import com.raizlabs.android.dbflow.annotation.Database;
/**
* Created by ukanth on 17/1/16.
*/
@Database(name = LogPreferenceDB.NAME, version = LogPreferenceDB.VERSION)
public class LogPreferenceDB {
public static final String NAME = "LogPreference";
public static final int VERSION = 1;
}

View file

@ -0,0 +1,127 @@
package dev.ukanth.ufirewall.log;
import static dev.ukanth.ufirewall.Api.TAG;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
import org.ocpsoft.prettytime.PrettyTime;
import org.ocpsoft.prettytime.TimeUnit;
import org.ocpsoft.prettytime.units.JustNow;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import dev.ukanth.ufirewall.Api;
import dev.ukanth.ufirewall.R;
import dev.ukanth.ufirewall.util.G;
/**
* Created by ukanth on 25/7/16.
*/
public class LogRecyclerViewAdapter extends RecyclerView.Adapter<LogRecyclerViewAdapter.ViewHolder> {
private final List<LogData> logData;
private final Context context;
private LogData data;
private PackageInfo info;
private static PrettyTime prettyTime;
private final RecyclerItemClickListener recyclerItemClickListener;
private View mView;
public LogRecyclerViewAdapter(final Context context, RecyclerItemClickListener recyclerItemClickListener) {
this.context = context;
logData = new ArrayList<>();
this.recyclerItemClickListener = recyclerItemClickListener;
}
public void updateData(List<LogData> logDataList) {
logData.clear();
logData.addAll(logDataList);
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
mView = LayoutInflater.from(parent.getContext()).inflate(R.layout.log_recycle_item, parent, false);
return new ViewHolder(mView);
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
data = logData.get(position);
holder.bind(logData.get(position),recyclerItemClickListener);
try {
Drawable applicationIcon = Api.getApplicationIcon(context, data.getUid());
holder.icon.setBackground(applicationIcon);
} catch (Exception e) {
Log.e(TAG, e.getMessage(), e);
}
try {
//if(data.getTimestamp() != null && !data.getTimestamp().isEmpty()) {
holder.lastDenied.setText(pretty(new Date(System.currentTimeMillis() - data.getTimestamp())));
//}
} catch (Exception e) {
holder.lastDenied.setText("-");
}
holder.appName.setText(data.getAppName() != null ? data.getAppName() + "(" + data.getUid() + ")" : context.getString(R.string.log_deletedapp));
if (data.getCount() > 1) {
holder.dataDenied.setText(context.getString(R.string.log_denied) + " " + data.getCount() + " " + context.getString(R.string.log_times));
} else {
holder.dataDenied.setText(context.getString(R.string.log_denied) + " " + data.getCount() + " " + context.getString(R.string.log_time));
}
holder.icon.invalidate();
}
public static String pretty(Date date) {
if (prettyTime == null) {
prettyTime = new PrettyTime(new Locale(G.locale()));
for (TimeUnit t : prettyTime.getUnits()) {
if (t instanceof JustNow) {
prettyTime.removeUnit(t);
break;
}
}
}
prettyTime.setReference(date);
return prettyTime.format(new Date(0));
}
@Override
public int getItemCount() {
return logData.size();
}
public static class ViewHolder extends RecyclerView.ViewHolder {
final ImageView icon;
final TextView appName;
final TextView lastDenied;
final TextView dataDenied;
public ViewHolder(View itemView) {
super(itemView);
icon = itemView.findViewById(R.id.app_icon);
appName = itemView.findViewById(R.id.app_name);
lastDenied = itemView.findViewById(R.id.last_denied);
dataDenied = itemView.findViewById(R.id.data_denied);
}
public void bind(final LogData item, final RecyclerItemClickListener listener) {
itemView.setOnClickListener(v -> listener.onItemClick(item));
}
}
}

View file

@ -0,0 +1,9 @@
package dev.ukanth.ufirewall.log;
/**
* Created by ukanth on 10/8/16.
*/
public interface RecyclerItemClickListener {
void onItemClick(LogData logData);
}

View file

@ -0,0 +1,163 @@
/**
*
* Shell Command for stream blocked packets information from klogripper (/proc/kmsg)
*
* Copyright (C) 2014 Umakanthan Chandran
*
* Originally copied from NetworkLog (C) 2012 Pragmatic Software (MPL2.0)
*
* This program 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.
*
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @author Umakanthan Chandran
* @version 1.0
*/
package dev.ukanth.ufirewall.log;
import android.util.Log;
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class ShellCommand {
Runtime rt;
String[] command;
String tag = "";
Process process;
BufferedReader stdout;
public String error;
public int exitval;
public ShellCommand(String[] command, String tag) {
this(command);
this.tag = tag;
}
public ShellCommand(String[] command) {
this.command = command;
rt = Runtime.getRuntime();
}
public void start(boolean waitForExit) {
exitval = -1;
error = null;
try {
process = new ProcessBuilder().command(command).redirectErrorStream(true).start();
stdout = new BufferedReader(new InputStreamReader(process.getInputStream()));
} catch (Exception e) {
error = e.getCause().getMessage();
return;
}
if (waitForExit) {
waitForExit();
}
}
public void waitForExit() {
while (!checkForExit()) {
if (stdoutAvailable()) {
Log.d("AFWALL", "ShellCommand waitForExit [" + tag
+ "] discarding read: " + readStdout());
} else {
try {
Thread.sleep(100);
} catch (Exception e) {
Log.d("AFWall", "waitForExit", e);
}
}
}
}
public void finish() {
try {
if (stdout != null) {
stdout.close();
}
} catch (Exception e) {
Log.e("AFWall", "Exception finishing [" + tag + "]", e);
}
if(process !=null) {
process.destroy();
}
process = null;
}
public boolean checkForExit() {
try {
if(process != null) {
exitval = process.exitValue();
} else {
finish();
}
} catch (IllegalThreadStateException e) {
return false;
}
finish();
return true;
}
public boolean stdoutAvailable() {
try {
return stdout.ready();
} catch (java.io.IOException e) {
Log.e("AFWall", "stdoutAvailable error", e);
return false;
}
}
public String readStdoutBlocking() {
String line;
if (stdout == null) {
return null;
}
try {
line = stdout.readLine();
} catch (Exception e) {
Log.e("AFWall", "readStdoutBlocking error", e);
return null;
}
if (line == null) {
return null;
} else {
return line + "\n";
}
}
public String readStdout() {
if (stdout == null) {
return null;
}
try {
if (stdout.ready()) {
String line = stdout.readLine();
if (line == null) {
return null;
} else {
return line + "\n";
}
} else {
return "";
}
} catch (Exception e) {
Log.e("AFWall", "readStdout error", e);
return null;
}
}
}

View file

@ -0,0 +1,77 @@
/*
* Copyright 2012 two forty four a.m. LLC <http://www.twofortyfouram.com>
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
* <http://www.apache.org/licenses/LICENSE-2.0>
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and limitations under the License.
*/
package dev.ukanth.ufirewall.plugin;
import android.content.Intent;
import android.os.Bundle;
/**
* Helper class to scrub Bundles of invalid extras. This is a workaround for an Android bug:
* <http://code.google.com/p/android/issues/detail?id=16006>.
*/
public final class BundleScrubber
{
/**
* Scrubs Intents for private serializable subclasses in the Intent extras. If the Intent's extras contain
* a private serializable subclass, the Bundle is cleared. The Bundle will not be set to null. If the
* Bundle is null, has no extras, or the extras do not contain a private serializable subclass, the Bundle
* is not mutated.
*
* @param intent {@code Intent} to scrub. This parameter may be mutated if scrubbing is necessary. This
* parameter may be null.
* @return true if the Intent was scrubbed, false if the Intent was not modified.
*/
public static boolean scrub(final Intent intent)
{
if (null == intent)
{
return false;
}
return scrub(intent.getExtras());
}
/**
* Scrubs Bundles for private serializable subclasses in the extras. If the Bundle's extras contain a
* private serializable subclass, the Bundle is cleared. If the Bundle is null, has no extras, or the
* extras do not contain a private serializable subclass, the Bundle is not mutated.
*
* @param bundle {@code Bundle} to scrub. This parameter may be mutated if scrubbing is necessary. This
* parameter may be null.
* @return true if the Bundle was scrubbed, false if the Bundle was not modified.
*/
public static boolean scrub(final Bundle bundle)
{
if (null == bundle)
{
return false;
}
/*
* Note: This is a hack to work around a private serializable classloader attack
*/
try
{
// if a private serializable exists, this will throw an exception
bundle.containsKey(null);
}
catch (final Exception e)
{
bundle.clear();
return true;
}
return false;
}
}

View file

@ -0,0 +1,301 @@
/*
* Copyright 2012 two forty four a.m. LLC <http://www.twofortyfouram.com>
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
* <http://www.apache.org/licenses/LICENSE-2.0>
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and limitations under the License.
*/
package dev.ukanth.ufirewall.plugin;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.widget.Toast;
import dev.ukanth.ufirewall.Api;
import dev.ukanth.ufirewall.R;
import dev.ukanth.ufirewall.profiles.ProfileData;
import dev.ukanth.ufirewall.profiles.ProfileHelper;
import dev.ukanth.ufirewall.service.RootCommand;
import dev.ukanth.ufirewall.util.G;
/**
* This is the "fire" BroadcastReceiver for a Locale Plug-in setting.
*/
public final class FireReceiver extends BroadcastReceiver {
public static final String TAG = "AFWall";
/**
* @param context {@inheritDoc}.
* @param intent the incoming {@link com.twofortyfouram.locale.Intent#ACTION_FIRE_SETTING} Intent. This
* should contain the {@link com.twofortyfouram.locale.Intent#EXTRA_BUNDLE} that was saved by
* {@link } and later broadcast by Locale.
*/
@Override
public void onReceive(final Context context, final Intent intent) {
/*
* Always be sure to be strict on input parameters! A malicious third-party app could always send an
* empty or otherwise malformed Intent. And since Locale applies settings in the background, the
* plug-in definitely shouldn't crash in the background.
*/
/*
* Locale guarantees that the Intent action will be ACTION_FIRE_SETTING
*/
if (!com.twofortyfouram.locale.Intent.ACTION_FIRE_SETTING.equals(intent.getAction())) {
return;
}
/*
* A hack to prevent a private serializable classloader attack
*/
BundleScrubber.scrub(intent);
BundleScrubber.scrub(intent.getBundleExtra(com.twofortyfouram.locale.Intent.EXTRA_BUNDLE));
final Bundle bundle = intent.getBundleExtra(com.twofortyfouram.locale.Intent.EXTRA_BUNDLE);
/*
* Final verification of the plug-in Bundle before firing the setting.
*/
if (PluginBundleManager.isBundleValid(bundle)) {
String index = bundle.getString(PluginBundleManager.BUNDLE_EXTRA_STRING_MESSAGE);
String name = null;
if (index.contains("::")) {
String[] msg = index.split("::");
index = msg[0];
name = msg[1];
}
final boolean multimode = G.enableMultiProfile();
final boolean disableToasts = G.disableTaskerToast();
if (!G.isProfileMigrated()) {
if (index != null) {
//int id = Integer.parseInt(index);
switch (index) {
case "0":
Api.applySavedIptablesRules(context, false, new RootCommand()
.setFailureToast(R.string.error_apply)
.setCallback(new RootCommand.Callback() {
@Override
public void cbFunc(RootCommand state) {
Message msg = new Message();
if (state.exitCode == 0) {
msg.arg1 = R.string.rules_applied;
Api.setEnabled(context, true, false);
} else {
// error details are already in logcat
msg.arg1 = R.string.error_apply;
}
sendMessage(msg);
}
}));
break;
case "1":
if (G.protectionLevel().equals("p0")) {
Api.purgeIptables(context, true, new RootCommand()
.setReopenShell(true)
.setCallback(new RootCommand.Callback() {
public void cbFunc(RootCommand state) {
Message msg = new Message();
msg.arg1 = R.string.toast_disabled;
sendMessage(msg);
Api.setEnabled(context, false, false);
}
}));
} else {
Message msg = new Message();
msg.arg1 = R.string.widget_disable_fail;
sendMessage(msg);
}
break;
case "2":
if (multimode) {
G.setProfile(true, "AFWallPrefs");
}
break;
case "3":
if (multimode) {
G.setProfile(true, "AFWallProfile1");
}
break;
case "4":
if (multimode) {
G.setProfile(true, "AFWallProfile2");
}
break;
case "5":
if (multimode) {
G.setProfile(true, "AFWallProfile3");
}
break;
default:
if (multimode) {
G.setProfile(true, name);
}
break;
}
if (Integer.parseInt(index) > 1) {
if (multimode) {
if (Api.isEnabled(context)) {
if (!disableToasts) {
Toast.makeText(context, R.string.tasker_apply, Toast.LENGTH_SHORT).show();
}
Api.applySavedIptablesRules(context, false, new RootCommand()
.setFailureToast(R.string.error_apply)
.setCallback(new RootCommand.Callback() {
@Override
public void cbFunc(RootCommand state) {
Message msg = new Message();
if (state.exitCode == 0) {
msg.arg1 = R.string.tasker_profile_applied;
if (!disableToasts)
sendMessage(msg);
} else {
// error details are already in logcat
msg.arg1 = R.string.error_apply;
}
sendMessage(msg);
}
}));
} else {
Message msg = new Message();
msg.arg1 = R.string.tasker_disabled;
sendMessage(msg);
}
} else {
Message msg = new Message();
msg.arg1 = R.string.tasker_muliprofile;
sendMessage(msg);
}
G.reloadPrefs();
/*if (G.activeNotification()) {
Api.showNotification(Api.isEnabled(context), context);
}*/
Api.updateNotification(Api.isEnabled(context), context);
}
}
} else {
if (index != null) {
//int id = Integer.parseInt(index);
switch (index) {
case "0":
Api.applySavedIptablesRules(context, false, new RootCommand()
.setFailureToast(R.string.error_apply)
.setCallback(new RootCommand.Callback() {
@Override
public void cbFunc(RootCommand state) {
Message msg = new Message();
if (state.exitCode == 0) {
msg.arg1 = R.string.rules_applied;
Api.setEnabled(context, true, false);
} else {
// error details are already in logcat
msg.arg1 = R.string.error_apply;
}
sendMessage(msg);
}
}));
break;
case "1":
if (G.protectionLevel().equals("p0")) {
Api.purgeIptables(context, true, new RootCommand()
.setReopenShell(true)
.setCallback(new RootCommand.Callback() {
public void cbFunc(RootCommand state) {
Message msg = new Message();
msg.arg1 = R.string.toast_disabled;
sendMessage(msg);
Api.setEnabled(context, false, false);
}
}));
/* } else {
msg.arg1 = R.string.toast_error_disabling;
sendMessage(msg);
}*/
} else {
Message msg = new Message();
msg.arg1 = R.string.widget_disable_fail;
sendMessage(msg);
}
break;
case "2":
if (multimode) {
G.setProfile(true, "AFWallPrefs");
}
break;
default:
if (multimode) {
ProfileData data = ProfileHelper.getProfileByName(name);
if (data != null) {
G.setProfile(true, data.getIdentifier());
}
}
break;
}
if (Integer.parseInt(index) > 1) {
if (multimode) {
if (Api.isEnabled(context)) {
if (!disableToasts) {
Toast.makeText(context, R.string.tasker_apply, Toast.LENGTH_SHORT).show();
}
Api.applySavedIptablesRules(context, false, new RootCommand()
.setFailureToast(R.string.error_apply)
.setCallback(new RootCommand.Callback() {
@Override
public void cbFunc(RootCommand state) {
Message msg = new Message();
if (state.exitCode == 0) {
msg.arg1 = R.string.tasker_profile_applied;
if (!disableToasts) sendMessage(msg);
} else {
// error details are already in logcat
msg.arg1 = R.string.error_apply;
}
}
}));
} else {
Message msg = new Message();
msg.arg1 = R.string.tasker_disabled;
sendMessage(msg);
}
} else {
Message msg = new Message();
msg.arg1 = R.string.tasker_muliprofile;
sendMessage(msg);
}
G.reloadPrefs();
/* if (G.activeNotification()) {
Api.showNotification(Api.isEnabled(context), context);
}*/
Api.updateNotification(Api.isEnabled(context), context);
}
}
}
}
}
private void sendMessage(Message msg) {
try {
new Handler() {
public void handleMessage(Message msg) {
if (msg.arg1 != 0)
Toast.makeText(G.getContext(), msg.arg1, Toast.LENGTH_SHORT).show();
}
}.sendMessage(msg);
}catch (Exception e) {
//unable to send toast. but don't crash
}
}
}

View file

@ -0,0 +1,235 @@
package dev.ukanth.ufirewall.plugin;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import java.util.List;
import dev.ukanth.ufirewall.R;
import dev.ukanth.ufirewall.profiles.ProfileData;
import dev.ukanth.ufirewall.profiles.ProfileHelper;
import dev.ukanth.ufirewall.util.G;
public class LocaleEdit extends AppCompatActivity {
//public static final String LOCALE_BRIGHTNESS = "dev.ukanth.ufirewall.plugin.LocaleEdit.ACTIVE_PROFLE";
private boolean mIsCancelled = false;
private final int CUSTOM_PROFILE_ID = 100;
protected void onCreate(Bundle paramBundle) {
super.onCreate(paramBundle);
BundleScrubber.scrub(getIntent());
BundleScrubber.scrub(getIntent().getBundleExtra(
com.twofortyfouram.locale.Intent.EXTRA_BUNDLE));
setContentView(R.layout.tasker_profile);
Toolbar toolbar = findViewById(R.id.tasker_toolbar);
setSupportActionBar(toolbar);
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
RadioButton tasker_enable = findViewById(R.id.tasker_enable);
RadioButton tasker_disable = findViewById(R.id.tasker_disable);
RadioButton button1 = findViewById(R.id.defaultProfile);
String name = prefs.getString("default", getString(R.string.defaultProfile));
button1.setText(name != null && name.length() == 0 ? getString(R.string.defaultProfile) : name);
if (!G.isProfileMigrated()) {
RadioGroup profiles = findViewById(R.id.radioProfiles);
RadioButton button2 = findViewById(R.id.profile1);
RadioButton button3 = findViewById(R.id.profile2);
RadioButton button4 = findViewById(R.id.profile3);
List<String> profilesList = G.getAdditionalProfiles();
//int textColor = Color.parseColor("#000000");
int counter = CUSTOM_PROFILE_ID;
for (String profile : profilesList) {
RadioButton rdbtn = new RadioButton(this);
rdbtn.setId(counter++);
rdbtn.setText(profile);
profiles.addView(rdbtn);
}
name = prefs.getString("profile1", getString(R.string.profile1));
button2.setText(name != null && name.length() == 0 ? getString(R.string.profile1) : name);
name = prefs.getString("profile2", getString(R.string.profile2));
button3.setText(name != null && name.length() == 0 ? getString(R.string.profile2) : name);
name = prefs.getString("profile3", getString(R.string.profile3));
button4.setText(name != null && name.length() == 0 ? getString(R.string.profile3) : name);
setupTitleApi11();
if (null == paramBundle) {
final Bundle forwardedBundle = getIntent().getBundleExtra(
com.twofortyfouram.locale.Intent.EXTRA_BUNDLE);
if (PluginBundleManager.isBundleValid(forwardedBundle)) {
String index = forwardedBundle.getString(PluginBundleManager.BUNDLE_EXTRA_STRING_MESSAGE);
if (index.contains("::")) {
index = index.split("::")[0];
}
if (index != null) {
switch (index) {
case "0":
tasker_enable.setChecked(true);
break;
case "1":
tasker_disable.setChecked(true);
break;
case "2":
button1.setChecked(true);
break;
case "3":
button2.setChecked(true);
break;
case "4":
button3.setChecked(true);
break;
case "5":
button4.setChecked(true);
break;
default:
int diff = CUSTOM_PROFILE_ID + (Integer.parseInt(index) - 6);
RadioButton btn = findViewById(diff);
if (btn != null) {
btn.setChecked(true);
}
}
}
}
}
} else {
//TODO: lets do it on new way
RadioGroup profiles = findViewById(R.id.radioProfiles);
//remove the existing profiles
RadioButton button2 = findViewById(R.id.profile1);
RadioButton button3 = findViewById(R.id.profile2);
RadioButton button4 = findViewById(R.id.profile3);
profiles.removeView(button2);
profiles.removeView(button3);
profiles.removeView(button4);
for (ProfileData data : ProfileHelper.getProfiles()) {
if (data != null) {
String profile = data.getName();
Long id = data.getId();
RadioButton rdbtn = new RadioButton(this);
rdbtn.setId(id.intValue());
rdbtn.setText(profile);
profiles.addView(rdbtn);
}
}
if (null == paramBundle) {
final Bundle forwardedBundle = getIntent().getBundleExtra(
com.twofortyfouram.locale.Intent.EXTRA_BUNDLE);
if (PluginBundleManager.isBundleValid(forwardedBundle)) {
String index = forwardedBundle.getString(PluginBundleManager.BUNDLE_EXTRA_STRING_MESSAGE);
if (index.contains("::")) {
index = index.split("::")[0];
}
if (index != null) {
switch (index) {
case "0":
tasker_enable.setChecked(true);
break;
case "1":
tasker_disable.setChecked(true);
break;
case "2":
button1.setChecked(true);
break;
default:
int diff = Integer.parseInt(index);
RadioButton btn = findViewById(diff);
if (btn != null) {
btn.setChecked(true);
}
}
}
}
}
}
}
private void setupTitleApi11() {
CharSequence callingApplicationLabel = null;
try {
callingApplicationLabel = getPackageManager().getApplicationLabel(
getPackageManager().getApplicationInfo(getCallingPackage(),
0));
} catch (final NameNotFoundException e) {
}
if (null != callingApplicationLabel) {
setTitle(callingApplicationLabel);
}
}
protected void onPause() {
super.onPause();
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
int id = item.getItemId();
if(id == android.R.id.home || id == R.id.twofortyfouram_locale_menu_save ) {
finish();
return true;
} else if (id == R.id.twofortyfouram_locale_menu_dontsave) {
mIsCancelled = true;
finish();
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public boolean onCreateOptionsMenu(final Menu menu) {
super.onCreateOptionsMenu(menu);
getMenuInflater().inflate(R.menu.twofortyfouram_locale_help_save_dontsave, menu);
return true;
}
@Override
public void finish() {
if (mIsCancelled) {
setResult(RESULT_CANCELED);
} else {
RadioGroup group = findViewById(R.id.radioProfiles);
int selectedId = group.getCheckedRadioButtonId();
RadioButton radioButton = findViewById(selectedId);
//int id = Integer.parseInt(radioButton.getHint().toString());
String action = radioButton.getText().toString();
final Intent resultIntent = new Intent();
if (!G.isProfileMigrated()) {
int idx = group.indexOfChild(radioButton);
resultIntent.putExtra(com.twofortyfouram.locale.Intent.EXTRA_BUNDLE, PluginBundleManager.generateBundle(getApplicationContext(), idx + "::" + action));
} else {
int idx = group.indexOfChild(radioButton);
resultIntent.putExtra(com.twofortyfouram.locale.Intent.EXTRA_BUNDLE, PluginBundleManager.generateBundle(getApplicationContext(), idx + "::" + action));
}
resultIntent.putExtra(com.twofortyfouram.locale.Intent.EXTRA_STRING_BLURB, action);
setResult(RESULT_OK, resultIntent);
}
super.finish();
}
}

View file

@ -0,0 +1,92 @@
/*
* Copyright 2012 two forty four a.m. LLC <http://www.twofortyfouram.com>
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
* <http://www.apache.org/licenses/LICENSE-2.0>
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and limitations under the License.
*/
package dev.ukanth.ufirewall.plugin;
import android.content.Context;
import android.os.Bundle;
import android.text.TextUtils;
/**
* Class for managing the {@link com.twofortyfouram.locale.Intent#EXTRA_BUNDLE} for this plug-in.
*/
public final class PluginBundleManager
{
/**
* Type: {@code String}.
* <p>
* String message to display in a Toast message.
*/
public static final String BUNDLE_EXTRA_STRING_MESSAGE = "dev.ukanth.ufirewall.plugin.APPLY_PROFILE"; //$NON-NLS-1$
/**
* Type: {@code int}
* <p>
* versionCode of the plug-in that saved the Bundle.
*/
/*
* This extra is not strictly required, however it makes backward and forward compatibility significantly
* easier. For example, suppose a bug is found in how some version of the plug-in stored its Bundle. By
* having the version, the plug-in can better detect when such bugs occur.
*/
public static final String BUNDLE_EXTRA_INT_VERSION_CODE = "dev.ukanth.ufirewall.plugin.extra.INT_VERSION_CODE"; //$NON-NLS-1$
/**
* Method to verify the content of the bundle are correct.
* <p>
* This method will not mutate {@code bundle}.
*
* @param bundle bundle to verify. May be null, which will always return false.
* @return true if the Bundle is valid, false if the bundle is invalid.
*/
public static boolean isBundleValid(final Bundle bundle)
{
if (null == bundle)
{
return false;
}
/*
* Make sure the expected extras exist
*/
if (!bundle.containsKey(BUNDLE_EXTRA_STRING_MESSAGE))
{
return false;
}
/*
* Make sure the extra isn't null or empty
*/
return !TextUtils.isEmpty(bundle.getString(BUNDLE_EXTRA_STRING_MESSAGE));
}
/**
* @param context Application context.
* @param message The toast message to be displayed by the plug-in. Cannot be null.
* @return A plug-in bundle.
*/
public static Bundle generateBundle(final Context context, final String message)
{
final Bundle result = new Bundle();
result.putString(BUNDLE_EXTRA_STRING_MESSAGE, message);
return result;
}
/**
* Private constructor prevents instantiation
*
* @throws UnsupportedOperationException because this class cannot be instantiated.
*/
private PluginBundleManager()
{
throw new UnsupportedOperationException("This class is non-instantiable"); //$NON-NLS-1$
}
}

View file

@ -0,0 +1,15 @@
package dev.ukanth.ufirewall.preferences;
import android.os.Bundle;
import android.preference.PreferenceFragment;
import dev.ukanth.ufirewall.R;
public class CustomBinaryPreferenceFragment extends PreferenceFragment {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Load the preferences from an XML resource
addPreferencesFromResource(R.xml.ui_custom_preferences);
}
}

View file

@ -0,0 +1,59 @@
package dev.ukanth.ufirewall.preferences;
import com.raizlabs.android.dbflow.annotation.Column;
import com.raizlabs.android.dbflow.annotation.PrimaryKey;
import com.raizlabs.android.dbflow.annotation.Table;
import com.raizlabs.android.dbflow.structure.BaseModel;
/**
* Created by ukanth on 17/1/16.
*/
@Table(database = DefaultConnectionPrefDB.class)
public class DefaultConnectionPref extends BaseModel {
@Column
@PrimaryKey
private int uid;
public int getUid() {
return uid;
}
public void setUid(int uid) {
this.uid = uid;
}
public String getConnectionType() {
return connectionType;
}
public void setConnectionType(String connectionType) {
this.connectionType = connectionType;
}
public boolean isState() {
return state;
}
public void setState(boolean state) {
this.state = state;
}
@Column
private String connectionType;
@Column
private boolean state;
public int getModeType() {
return modeType;
}
public void setModeType(int modeType) {
this.modeType = modeType;
}
@Column
private int modeType;
}

View file

@ -0,0 +1,14 @@
package dev.ukanth.ufirewall.preferences;
import com.raizlabs.android.dbflow.annotation.Database;
/**
* Created by ukanth on 17/1/16.
*/
@Database(name = DefaultConnectionPrefDB.NAME, version = DefaultConnectionPrefDB.VERSION)
public class DefaultConnectionPrefDB {
public static final String NAME = "DefaultConnectionPref";
public static final int VERSION = 1;
}

View file

@ -0,0 +1,260 @@
package dev.ukanth.ufirewall.preferences;
import static dev.ukanth.ufirewall.Api.getFixLeakPath;
import static dev.ukanth.ufirewall.Api.mountDir;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Bundle;
import android.preference.CheckBoxPreference;
import android.preference.ListPreference;
import android.preference.Preference;
import android.preference.PreferenceFragment;
import com.stericson.roottools.RootTools;
import com.topjohnwu.superuser.Shell;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import dev.ukanth.ufirewall.Api;
import dev.ukanth.ufirewall.R;
import dev.ukanth.ufirewall.service.RootCommand;
import dev.ukanth.ufirewall.util.G;
public class ExpPreferenceFragment extends PreferenceFragment implements
OnSharedPreferenceChangeListener {
private final String[] initDirs = {
"/magisk/.core/service.d/",
"/sbin/.core/img/.core/service.d/",
"/sbin/.magisk/img/.core/service.d/",
"/magisk/phh/su.d/",
"/data/adb/post-fs-data.d/",
"/data/adb/service.d/",
"/sbin/.core/img/phh/su.d/",
"/su/su.d/",
"/system/su.d/",
"/system/etc/init.d/",
"/etc/init.d/",
"/sbin/supersu/su.d",
"/data/adb/su/su.d"};
private final String initScript = "afwallstart";
@SuppressLint("NewApi")
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Load the preferences from an XML resource
addPreferencesFromResource(R.xml.experimental_preferences);
setupInitDir(findPreference("initPath"));
}
@Override
public void onDestroy() {
super.onDestroy();
}
private void setupInitDir(Preference initd) {
ListPreference listPreference = (ListPreference) initd;
final Context ctx = getActivity().getApplicationContext();
listPreference.setOnPreferenceChangeListener((preference, newValue) -> {
String selected = newValue.toString();
// fix leak enabled - but user trying to change the path
if (!selected.equals(G.initPath()) && G.fixLeak()) {
deleteFiles(ctx, false);
G.initPath(selected);
updateFixLeakScript(true);
return true;
}
return true;
});
Activity activity = getActivity();
new Thread(() -> {
List<String> listSupportedDir = new ArrayList<>();
//going through the list of known initDirectories
for (String dir : initDirs) {
//path exists
if (RootTools.exists(dir, true)) {
listSupportedDir.add(dir);
}
}
//some path exists
if (listSupportedDir.size() > 0) {
String[] entries = listSupportedDir.toArray(new String[0]);
activity.runOnUiThread(() -> {
listPreference.setEntries(entries);
listPreference.setEntryValues(entries);
});
}
}).start();
if (G.initPath() != null && !G.initPath().isEmpty()) {
listPreference.setValue(G.initPath());
} else {
CheckBoxPreference fixLeakPref = (CheckBoxPreference) findPreference("fixLeak");
fixLeakPref.setEnabled(false);
}
setupFixLeak(findPreference("fixLeak"), this.getActivity().getApplicationContext());
}
@Override
public void onResume() {
super.onResume();
getPreferenceManager().getSharedPreferences()
.registerOnSharedPreferenceChangeListener(this);
}
@Override
public void onPause() {
getPreferenceManager().getSharedPreferences()
.unregisterOnSharedPreferenceChangeListener(this);
super.onPause();
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
String key) {
if (key.equals("fixLeak")) {
boolean enabled = G.fixLeak();
Activity activity = getActivity();
new Thread(() -> {
if (enabled != isFixLeakInstalled()) {
activity.runOnUiThread(() -> updateFixLeakScript(enabled));
}
}).start();
}
if (key.equals("initPath")) {
if (G.initPath() != null) {
CheckBoxPreference fixPath = (CheckBoxPreference) findPreference("fixLeak");
fixPath.setEnabled(true);
}
}
if (key.equals("multiUser")) {
if (!Api.supportsMultipleUsers(this.getActivity().getApplicationContext())) {
CheckBoxPreference multiUserPref = (CheckBoxPreference) findPreference(key);
multiUserPref.setChecked(false);
} else {
Api.setUserOwner(this.getActivity().getApplicationContext());
}
}
}
public void setupFixLeak(Preference pref, Context ctx) {
if (pref == null) {
return;
}
CheckBoxPreference fixLeakPref = (CheckBoxPreference) pref;
if (fixLeakPref.isEnabled()) {
// gray out the fixLeak preference if the ROM doesn't support init.d
updateLeakCheckbox();
fixLeakPref.setEnabled(getFixLeakPath(initScript) != null && !isPackageInstalled("com.androguide.universal.init.d", ctx));
}
}
private boolean isPackageInstalled(String packagename, Context ctx) {
PackageManager pm = ctx.getPackageManager();
try {
pm.getPackageInfo(packagename, PackageManager.GET_ACTIVITIES);
return true;
} catch (NameNotFoundException e) {
return false;
}
}
/**
* Tests whether the fix leak script is installed.
*
* You should call this from an I/O thread, because current api level does not allow usage of futures.
*
* @return {@code true} if the fix leak script exists.
*/
private boolean isFixLeakInstalled() {
String path = getFixLeakPath(initScript);
return path != null && RootTools.exists(path);
}
private void updateFixLeakScript(final boolean enabled) {
Activity activity = getActivity();
if (activity != null && isAdded()) {
final Context ctx = activity.getApplicationContext();
final String srcPath = new File(ctx.getDir("bin", 0), initScript)
.getAbsolutePath();
new Thread(() -> {
String path = G.initPath();
if (path != null) {
if (enabled) {
File f = new File(path);
boolean mountable = mountDir(ctx, getFixLeakPath(initScript), "RW");
if (mountable) {
//make sure it's executable
Shell.Result result = Shell.cmd("chmod 755 " + f.getAbsolutePath()).exec();
if(result.isSuccess() && RootTools.copyFile(srcPath, (f.getAbsolutePath() + "/" + initScript),
true, false)) {
Api.sendToastBroadcast(ctx, ctx.getString(R.string.success_initd));
mountDir(ctx, getFixLeakPath(initScript), "RO");
activity.runOnUiThread(() -> updateLeakCheckbox());
}
} else {
Api.sendToastBroadcast(ctx, ctx.getString(R.string.mount_initd_error));
}
} else {
deleteFiles(ctx, true);
}
}
}).start();
}
}
private void updateLeakCheckbox() {
Activity activity = getActivity();
CheckBoxPreference fixLeakPref = (CheckBoxPreference) findPreference("fixLeak");
new Thread(() -> {
boolean isFixLeakInstalled = isFixLeakInstalled();
activity.runOnUiThread(() -> fixLeakPref.setChecked(isFixLeakInstalled));
}).start();
}
private void deleteFiles(final Context ctx, final boolean updateCheckbox) {
String path = G.initPath();
if(path != null) {
new Thread(() -> {
if (RootTools.exists(path, true)) {
final String filePath = path + "/" + initScript;
boolean mountable = mountDir(ctx, getFixLeakPath(initScript), "RW");
if (mountable) {
Shell.Result result = Shell.cmd("rm -f " + filePath).exec();
if(result.isSuccess()){
Api.sendToastBroadcast(ctx, ctx.getString(R.string.remove_initd));
} else{
Api.sendToastBroadcast(ctx, ctx.getString(R.string.delete_initd_error));
}
if (updateCheckbox) {
getActivity().runOnUiThread(() -> updateLeakCheckbox());
}
mountDir(ctx, getFixLeakPath(initScript), "RO");
} else {
Api.sendToastBroadcast(ctx, ctx.getString(R.string.mount_initd_error));
}
}
}).start();
} else {
Api.sendToastBroadcast(ctx, ctx.getString(R.string.delete_initd_error));
}
}
}

View file

@ -0,0 +1,62 @@
package dev.ukanth.ufirewall.preferences;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.CheckBoxPreference;
import android.preference.Preference;
import android.preference.PreferenceFragment;
import android.util.Log;
import dev.ukanth.ufirewall.Api;
import dev.ukanth.ufirewall.R;
import dev.ukanth.ufirewall.util.G;
public class LanguagePreferenceFragment extends PreferenceFragment implements
SharedPreferences.OnSharedPreferenceChangeListener {
private static CheckBoxPreference checkBoxPreference;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Load the preferences from an XML resource
addPreferencesFromResource(R.xml.language_preferences);
checkXposed(findPreference("fixDownloadManagerLeak"));
//checkXposed(findPreference("lockScreenNotification"),this.getActivity().getApplicationContext());
}
public static void checkXposed(Preference pref) {
if (pref == null) {
return;
}
checkBoxPreference = (CheckBoxPreference) pref;
// gray out the fixDownloadManagerLeak preference if xposed module is not activated
Log.i(Api.TAG, "Checking Xposed:" + G.isXposedEnabled() + "");
checkBoxPreference.setEnabled(G.isXposedEnabled());
}
@Override
public void onResume() {
super.onResume();
getPreferenceManager().getSharedPreferences()
.registerOnSharedPreferenceChangeListener(this);
}
@Override
public void onPause() {
getPreferenceManager().getSharedPreferences()
.unregisterOnSharedPreferenceChangeListener(this);
super.onPause();
}
@Override
public void onDestroy() {
super.onDestroy();
checkBoxPreference = null;
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
}
}

View file

@ -0,0 +1,255 @@
package dev.ukanth.ufirewall.preferences;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.preference.CheckBoxPreference;
import android.preference.ListPreference;
import android.preference.Preference;
import android.preference.PreferenceFragment;
import android.preference.PreferenceGroup;
import android.widget.Toast;
import com.afollestad.materialdialogs.DialogAction;
import com.afollestad.materialdialogs.MaterialDialog;
import dev.ukanth.ufirewall.Api;
import dev.ukanth.ufirewall.R;
import dev.ukanth.ufirewall.log.Log;
import dev.ukanth.ufirewall.service.LogService;
import dev.ukanth.ufirewall.service.RootCommand;
import dev.ukanth.ufirewall.util.G;
public class LogPreferenceFragment extends PreferenceFragment {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Load the preferences from an XML resource
try {
//fix for the mess
//G.logPingTimeout(G.logPingTimeout());
addPreferencesFromResource(R.xml.log_preferences);
// populateLogMessage(findPreference("logDmesg"));
// populateAppList(findPreference("block_filter"));
setupLogHostname(findPreference("showHostName"));
populateLogTarget(findPreference("logTarget"));
} catch (ClassCastException c) {
Log.i(Api.TAG, c.getMessage());
Api.toast(getActivity(), getString(R.string.exception_pref));
}
}
private void populateLogTarget(Preference logTarget) {
if (logTarget == null) {
return;
}
ListPreference listPreference = (ListPreference) logTarget;
if(G.logTargets() != null && G.logTargets().length() > 0) {
String [] items = G.logTargets().split(",");
if(items != null && items.length > 0) {
if (listPreference != null) {
listPreference.setEntries(items);
listPreference.setEntryValues(items);
// Add custom listener to intercept preference changes
listPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
String newLogTarget = (String) newValue;
String oldLogTarget = G.logTarget();
// If it's the same target, allow change immediately
if (newLogTarget.equals(oldLogTarget)) {
return true;
}
// Show confirmation dialog and prevent automatic change
showLogTargetChangeDialog(oldLogTarget, newLogTarget, listPreference);
return false; // Prevent automatic preference change
}
});
}
//if there is only one entry
if(items.length == 1) {
G.logTarget(items[0]);
}
} else {
//no LOG targets
((PreferenceGroup) findPreference("logExperimental")).removePreference(listPreference);
}
} else{
//no LOG targets
((PreferenceGroup) findPreference("logExperimental")).removePreference(listPreference);
}
}
private void setupLogHostname(Preference showHostName) {
CheckBoxPreference showHost = (CheckBoxPreference) showHostName;
if (G.isDoKey(getActivity()) || G.isDonate()) {
showHost.setEnabled(true);
}
/* if(!Api.isAFWallAllowed((Context) getActivity())){
showHost.setChecked(false);
}*/
}
/*private void populateLogMessage(Preference logDmesg) {
if (logDmesg == null) {
return;
}
ArrayList<String> ar = new ArrayList<String>();
ArrayList<String> val = new ArrayList<String>();
ar.add("System");
val.add("OS");
ListPreference listPreference = (ListPreference) logDmesg;
if (RootTools.isBusyboxAvailable() || !Api.getBusyBoxPath(ctx,false).isEmpty()) {
ar.add("Busybox");
val.add("BX");
}
if (listPreference != null) {
listPreference.setEntries(ar.toArray(new String[0]));
listPreference.setEntryValues(val.toArray(new String[0]));
}
}
public static int[] convertIntegers(List<Integer> integers) {
int[] ret = new int[integers.size()];
Iterator<Integer> iterator = integers.iterator();
for (int i = 0; i < ret.length; i++) {
ret[i] = iterator.next().intValue();
}
return ret;
}*/
/*private void populateAppList(Preference list) {
final ArrayList<CharSequence> entriesList = new ArrayList<CharSequence>();
final ArrayList<Integer> entryValuesList = new ArrayList<Integer>();
List<Api.PackageInfoData> apps = new ArrayList<>();
//List<Api.PackageInfoData> apps = Api.getSpecialData(true);
Api.PackageInfoData info = new Api.PackageInfoData();
info.uid = 1020;
info.pkgName = "dev.afwall.special.mDNS";
info.names = new ArrayList<String>();
info.names.add("mDNS");
info.appinfo = new ApplicationInfo();
//TODO: better way to handle this
//manually add mDNS for now
if (!apps.contains(info)) {
apps.add(info);
}
for (int i = 0; i < apps.size(); i++) {
entriesList.add(apps.get(i).toStringWithUID());
entryValuesList.add(apps.get(i).uid);
}
list.setOnPreferenceClickListener(preference -> {
//open browser or intent here
MaterialDialog dialog = new MaterialDialog.Builder(getActivity())
.title(R.string.filters_apps_title)
.itemsIds(convertIntegers(entryValuesList))
.items(entriesList)
.itemsCallbackMultiChoice(null, (dialog1, which, text) -> {
List<Integer> blockedList = new ArrayList<Integer>();
for (int i : which) {
blockedList.add(entryValuesList.get(i));
}
G.storeBlockedApps(blockedList);
return true;
})
.positiveText(R.string.OK)
.negativeText(R.string.close)
.show();
if (G.readBlockedApps().size() > 0) {
dialog.setSelectedIndices(selectItems(entryValuesList));
}
return true;
});
}
private Integer[] selectItems(ArrayList<Integer> entryValuesList) {
List<Integer> items = new ArrayList<>();
for (Integer in : G.readBlockedApps()) {
if (entryValuesList.contains(in)) {
items.add(entryValuesList.indexOf(in));
}
}
return items.toArray(new Integer[0]);
}*/
private void showLogTargetChangeDialog(String oldLogTarget, String newLogTarget, ListPreference listPreference) {
new MaterialDialog.Builder(getActivity())
.title(R.string.log_target_change_title)
.content(getString(R.string.log_target_change_message, oldLogTarget, newLogTarget))
.positiveText(R.string.Yes)
.negativeText(R.string.Cancel)
.onPositive(new MaterialDialog.SingleButtonCallback() {
@Override
public void onClick(MaterialDialog dialog, DialogAction which) {
// Apply the log target change
applyLogTargetChange(newLogTarget, listPreference);
}
})
.onNegative(new MaterialDialog.SingleButtonCallback() {
@Override
public void onClick(MaterialDialog dialog, DialogAction which) {
// Do nothing - preference change was already prevented by returning false
Log.d("LogPreferenceFragment", "Log target change cancelled by user");
}
})
.show();
}
private void applyLogTargetChange(String newLogTarget, ListPreference listPreference) {
Context ctx = getActivity();
// Set the new log target in preferences
G.logTarget(newLogTarget);
// Update the ListPreference to show the new value
listPreference.setValue(newLogTarget);
// Update log rules
Api.updateLogRules(ctx, new RootCommand()
.setReopenShell(true)
.setSuccessToast(R.string.log_target_success)
.setFailureToast(R.string.log_target_fail));
// Change log target without restarting service
changeLogTargetInService(ctx, newLogTarget);
}
/**
* Change log target in the running service without restarting it
*/
private void changeLogTargetInService(Context ctx, String newLogTarget) {
Log.i("LogPreferenceFragment", "Changing log target to: " + newLogTarget);
if (G.enableLogService()) {
// Send log target change request to the running service
Intent changeIntent = new Intent(ctx, LogService.class);
changeIntent.setAction(LogService.ACTION_CHANGE_LOG_TARGET);
changeIntent.putExtra(LogService.EXTRA_NEW_LOG_TARGET, newLogTarget);
ctx.startService(changeIntent);
// Show success message after a delay to allow the change to process
Handler handler = new Handler(Looper.getMainLooper());
handler.postDelayed(() -> {
Toast.makeText(ctx, getString(R.string.log_target_changed_success, newLogTarget), Toast.LENGTH_LONG).show();
}, 2000);
} else {
// Service is not running, just update the preference
Log.i("LogPreferenceFragment", "Log service disabled, only updating preference");
Toast.makeText(ctx, getString(R.string.log_target_changed_success, newLogTarget), Toast.LENGTH_SHORT).show();
}
}
}

View file

@ -0,0 +1,80 @@
package dev.ukanth.ufirewall.preferences;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.preference.Preference;
import android.preference.PreferenceCategory;
import android.preference.PreferenceFragment;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
import dev.ukanth.ufirewall.Api;
import dev.ukanth.ufirewall.R;
import dev.ukanth.ufirewall.activity.ProfileActivity;
import dev.ukanth.ufirewall.profiles.ProfileHelper;
import dev.ukanth.ufirewall.util.G;
public class MultiProfilePreferenceFragment extends PreferenceFragment {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Load the preferences from an XML resource
addPreferencesFromResource(R.xml.profiles_preferences);
Preference button = findPreference("manage_profiles");
button.setOnPreferenceClickListener(preference -> {
//code for what you want it to do
startActivity(new Intent(getActivity(), ProfileActivity.class));
return true;
});
final PreferenceCategory mCategory = (PreferenceCategory) findPreference("promigrate");
final PreferenceCategory mCategory2 = (PreferenceCategory) findPreference("oldprofile_pref");
final Preference migrate = findPreference("migrate_profile");
if (!G.isProfileMigrated()) {
migrate.setOnPreferenceClickListener(preference -> {
Context ctx = getActivity();
ProfileHelper.migrateProfiles(ctx);
if (ctx != null) {
Api.toast(getActivity(), ctx.getString(R.string.profile_migrate_msg));
mCategory.removePreference(migrate);
Preference migrate1 = findPreference("profile1");
mCategory2.removePreference(migrate1);
migrate1 = findPreference("profile2");
mCategory2.removePreference(migrate1);
migrate1 = findPreference("profile3");
mCategory2.removePreference(migrate1);
}
return true;
});
} else {
mCategory.removePreference(migrate);
Preference migrate2 = findPreference("profile1");
mCategory2.removePreference(migrate2);
migrate2 = findPreference("profile2");
mCategory2.removePreference(migrate2);
migrate2 = findPreference("profile3");
mCategory2.removePreference(migrate2);
}
}
public void copy(File src, File dst) throws IOException {
FileInputStream inStream = new FileInputStream(src);
FileOutputStream outStream = new FileOutputStream(dst);
FileChannel inChannel = inStream.getChannel();
FileChannel outChannel = outStream.getChannel();
inChannel.transferTo(0, inChannel.size(), outChannel);
inStream.close();
outStream.close();
}
}

View file

@ -0,0 +1,397 @@
/**
* Preference Interface.
* All iptables "communication" is handled by this class.
* <p>
* Copyright (C) 2011-2012 Umakanthan Chandran
* <p>
* This program 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.
* <p>
* 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 General Public License for more details.
* <p>
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @author Umakanthan Chandran
* @version 1.0
*/
package dev.ukanth.ufirewall.preferences;
import android.app.NotificationManager;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.preference.PreferenceActivity;
import android.preference.PreferenceFragment;
import android.preference.PreferenceManager;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.Toast;
import androidx.appcompat.widget.AppCompatCheckBox;
import androidx.appcompat.widget.AppCompatCheckedTextView;
import androidx.appcompat.widget.AppCompatEditText;
import androidx.appcompat.widget.AppCompatRadioButton;
import androidx.appcompat.widget.AppCompatSpinner;
import androidx.appcompat.widget.Toolbar;
import java.util.List;
import dev.ukanth.ufirewall.Api;
import dev.ukanth.ufirewall.R;
import dev.ukanth.ufirewall.events.RulesEvent;
import dev.ukanth.ufirewall.events.RxEvent;
import dev.ukanth.ufirewall.service.LogService;
import dev.ukanth.ufirewall.service.RootCommand;
import dev.ukanth.ufirewall.util.G;
import dev.ukanth.ufirewall.util.SecurityUtil;
import io.reactivex.rxjava3.disposables.Disposable;
public class PreferencesActivity extends PreferenceActivity implements SharedPreferences.OnSharedPreferenceChangeListener {
private static final boolean ALWAYS_SIMPLE_PREFS = false;
private Toolbar mToolBar;
private RxEvent rxEvent;
private Disposable disposable;
private void initTheme() {
switch(G.getSelectedTheme()) {
case "D":
setTheme(R.style.AppDarkTheme);
break;
case "L":
setTheme(R.style.AppLightTheme);
break;
case "B":
setTheme(R.style.AppBlackTheme);
break;
}
}
/**
* Helper method to determine if the device has an extra-large screen. For
* example, 10" tablets are extra-large.
*/
private static boolean isXLargeTablet(Context context) {
return (context.getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) >= Configuration.SCREENLAYOUT_SIZE_XLARGE;
}
/**
* Determines whether the simplified settings UI should be shown. This is
* true if this is forced via {@link #ALWAYS_SIMPLE_PREFS}, or the device
* doesn't have newer APIs like {@link PreferenceFragment}, or the device
* doesn't have an extra-large screen. In these cases, a single-pane
* "simplified" settings UI should be shown.
*/
private static boolean isSimplePreferences(Context context) {
return ALWAYS_SIMPLE_PREFS
|| Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB
|| !isXLargeTablet(context);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
// set language
Api.updateLanguage(getApplicationContext(), G.locale());
initTheme();
super.onCreate(savedInstanceState);
prepareLayout();
subscribe();
Bundle bundle = getIntent().getExtras();
if (bundle != null) {
Object data = bundle.get("validate");
if (data != null) {
String check = (String) data;
if (check.equals("yes")) {
new SecurityUtil(PreferencesActivity.this).passCheck();
}
}
}
}
private void subscribe() {
rxEvent = new RxEvent();
disposable = rxEvent.subscribe(event -> {
if (event instanceof RulesEvent) {
ruleChangeApplyRules((RulesEvent) event);
} /*else if (event instanceof LogChangeEvent) {
logDmesgChangeApplyRules((LogChangeEvent) event);
}*/
});
}
private void ruleChangeApplyRules(RulesEvent rulesEvent) {
final Context context = rulesEvent.ctx;
Api.applySavedIptablesRules(context, false, new RootCommand()
.setFailureToast(R.string.error_apply)
.setCallback(new RootCommand.Callback() {
@Override
public void cbFunc(RootCommand state) {
if (state.exitCode == 0) {
Log.i(Api.TAG, "Rules applied successfully during preference change");
} else {
// error details are already in logcat
Log.i(Api.TAG, "Error applying rules during preference change");
}
}
}));
}
@Override
public void onStart() {
super.onStart();
}
@Override
public void onStop() {
super.onStop();
}
private void prepareLayout() {
ViewGroup root = findViewById(android.R.id.content);
View content = root.getChildAt(0);
LinearLayout toolbarContainer = (LinearLayout) View.inflate(this, R.layout.activity_prefs, null);
root.removeAllViews();
toolbarContainer.addView(content);
root.addView(toolbarContainer);
mToolBar = toolbarContainer.findViewById(R.id.toolbar);
mToolBar.setTitle(getTitle() + " " + getString(R.string.preferences));
mToolBar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
});
}
@Override
protected void onApplyThemeResource(Resources.Theme theme, int resid, boolean first) {
theme.applyStyle(resid, true);
}
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
// Allow super to try and create a view first
final View result = super.onCreateView(name, context, attrs);
if (result != null) {
return result;
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
// If we're running pre-L, we need to 'inject' our tint aware Views in place of the
// standard framework versions
switch (name) {
case "EditText":
return new AppCompatEditText(this, attrs);
case "Spinner":
return new AppCompatSpinner(this, attrs);
case "CheckBox":
return new AppCompatCheckBox(this, attrs);
case "RadioButton":
return new AppCompatRadioButton(this, attrs);
case "CheckedTextView":
return new AppCompatCheckedTextView(this, attrs);
}
}
Api.fixFolderPermissionsAsync(context);
return null;
}
@Override
public void onResume() {
super.onResume();
PreferenceManager.getDefaultSharedPreferences(this)
.registerOnSharedPreferenceChangeListener(this);
}
@Override
public void onPause() {
PreferenceManager.getDefaultSharedPreferences(this)
.unregisterOnSharedPreferenceChangeListener(this);
super.onPause();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
onBackPressed();
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
protected boolean isValidFragment(String fragmentName) {
// Prevent fragment injection attacks by explicitly allowing only known safe fragments
if (fragmentName == null) {
return false;
}
return UIPreferenceFragment.class.getName().equals(fragmentName)
|| ThemePreferenceFragment.class.getName().equals(fragmentName)
|| RulesPreferenceFragment.class.getName().equals(fragmentName)
|| LogPreferenceFragment.class.getName().equals(fragmentName)
|| ExpPreferenceFragment.class.getName().equals(fragmentName)
|| CustomBinaryPreferenceFragment.class.getName().equals(fragmentName)
|| SecPreferenceFragment.class.getName().equals(fragmentName)
|| MultiProfilePreferenceFragment.class.getName().equals(fragmentName)
|| WidgetPreferenceFragment.class.getName().equals(fragmentName)
|| LanguagePreferenceFragment.class.getName().equals(fragmentName);
}
@Override
public void onBuildHeaders(List<Header> target) {
loadHeadersFromResource(R.xml.preferences_headers, target);
}
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
}
/**
* {@inheritDoc}
*/
@Override
public boolean onIsMultiPane() {
return isXLargeTablet(this) && !isSimplePreferences(this);
}
/*public void logDmesgChangeApplyRules(LogChangeEvent logChangeEvent) {
if (logChangeEvent != null) {
final Context context = logChangeEvent.ctx;
final Intent logIntent = new Intent(context, LogService.class);
if (G.enableLogService()) {
//restart service
context.stopService(logIntent);
Api.cleanupUid();
context.startService(logIntent);
} else {
//log service disabled
context.stopService(logIntent);
Api.cleanupUid();
}
}
}*/
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
Context ctx = getApplicationContext();
boolean isRefreshRequired = false;
if (key.equals("showUid") || key.equals("disableIcons") || key.equals("enableVPN")
|| key.equals("enableTether")
|| key.equals("enableLAN") || key.equals("enableRoam")
|| key.equals("locale") || key.equals("showFilter")) {
G.reloadProfile();
isRefreshRequired = true;
}
if (key.equals("ipt_path") || key.equals("dns_value")) {
rxEvent.publish(new RulesEvent("", ctx));
}
/*if (key.equals("logDmesg")) {
rxEvent.publish(new LogChangeEvent("", ctx));
}*/
if (key.equals("notification_priority")) {
NotificationManager notificationManager = (NotificationManager) ctx.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancelAll();
Api.updateNotification(Api.isEnabled(ctx), ctx);
}
if(key.equals("activeNotification")) {
boolean enabled = sharedPreferences.getBoolean(key, false);
if(!enabled) {
NotificationManager notificationManager = (NotificationManager) ctx.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancelAll();
} else {
Api.updateNotification(Api.isEnabled(ctx), ctx);
}
}
if(key.equals("logTarget")) {
// Log target changes are now handled by LogPreferenceFragment
// This should not be called anymore due to the OnPreferenceChangeListener
Log.d("PreferencesActivity", "logTarget preference changed: " + sharedPreferences.getString(key, ""));
}
if (key.equals("enableLogService")) {
if(G.logTarget() !=null && !G.logTarget().trim().isEmpty()) {
boolean enabled = sharedPreferences.getBoolean(key, false);
if (enabled) {
Toast.makeText(getApplicationContext(), getString(R.string.log_service_start), Toast.LENGTH_LONG).show();
Intent intent = new Intent(ctx, LogService.class);
ctx.stopService(intent);
ctx.startService(intent);
} else {
Toast.makeText(getApplicationContext(), getString(R.string.log_service_stop), Toast.LENGTH_LONG).show();
Intent intent = new Intent(ctx, LogService.class);
ctx.stopService(intent);
}
} else{
Toast.makeText(getApplicationContext(), getString(R.string.log_service_select), Toast.LENGTH_LONG).show();
}
}
if (key.equals("enableMultiProfile")) {
G.reloadProfile();
}
if (key.equals("theme")) {
initTheme();
recreate();
Intent broadcastIntent = new Intent();
broadcastIntent.setAction("dev.ukanth.ufirewall.theme.REFRESH");
ctx.sendBroadcast(broadcastIntent);
}
if (isRefreshRequired) {
Intent broadcastIntent = new Intent();
broadcastIntent.setAction("dev.ukanth.ufirewall.ui.CHECKREFRESH");
ctx.sendBroadcast(broadcastIntent);
}
}
@Override
public void onDestroy() {
if (rxEvent != null && disposable != null) {
disposable.dispose();
}
super.onDestroy();
}
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(Api.updateBaseContextLocale(base));
}
}

View file

@ -0,0 +1,307 @@
package dev.ukanth.ufirewall.preferences;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Build;
import android.os.Bundle;
import android.preference.CheckBoxPreference;
import android.preference.Preference;
import android.preference.PreferenceFragment;
import android.preference.SwitchPreference;
import java.io.File;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import dev.ukanth.ufirewall.Api;
import dev.ukanth.ufirewall.R;
import dev.ukanth.ufirewall.service.RootCommand;
import dev.ukanth.ufirewall.util.G;
public class RulesPreferenceFragment extends PreferenceFragment implements
SharedPreferences.OnSharedPreferenceChangeListener {
private Context ctx;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Load the preferences from an XML resource
addPreferencesFromResource(R.xml.rules_preferences);
try {
updateRuleStatus();
} catch (Exception e) {
}
//make sure Roaming is disable in Wifi-only Tablets
if (!Api.isMobileNetworkSupported(getActivity())) {
CheckBoxPreference roamPreference = (CheckBoxPreference) findPreference("enableRoam");
roamPreference.setChecked(false);
roamPreference.setEnabled(false);
} else {
CheckBoxPreference roamPreference = (CheckBoxPreference) findPreference("enableRoam");
roamPreference.setEnabled(true);
}
}
private void updateRuleStatus() {
SwitchPreference input_chain = (SwitchPreference) findPreference("input_chain");
input_chain.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object o) {
return false;
}
});
/* SwitchPreference output_chain = (SwitchPreference) findPreference("output_chain");
SwitchPreference forward_chain = (SwitchPreference) findPreference("forward_chain");*/
SwitchPreference input_chain_v6 = (SwitchPreference) findPreference("input_chain_v6");
SwitchPreference output_chain_v6 = (SwitchPreference) findPreference("output_chain_v6");
SwitchPreference forward_chain_v6 = (SwitchPreference) findPreference("forward_chain_v6");
//ipv6 is not enabled
if (!G.enableIPv6()) {
input_chain_v6.setEnabled(false);
output_chain_v6.setEnabled(false);
forward_chain_v6.setEnabled(false);
}
Api.getChainStatus(ctx, new RootCommand()
.setFailureToast(R.string.error_apply)
.setLogging(true)
.setCallback(new RootCommand.Callback() {
@Override
public void cbFunc(RootCommand state) {
if (state.exitCode == 0) {
StringBuilder result = state.res;
if (result != null) {
String output = result.toString();
final String regexIn = "-P INPUT (\\w+)";
final String regexOut = "-P OUTPUT (\\w+)";
final String regexFwd = "-P FORWARD (\\w+)";
final Pattern pattern = Pattern.compile(regexIn);
final Pattern pattern2 = Pattern.compile(regexOut);
final Pattern pattern3 = Pattern.compile(regexFwd);
final Matcher matcher = pattern.matcher(output);
boolean firstTime = true;
while (matcher.find()) {
if (firstTime) {
G.ipv4Input(matcher.group(1).equals("ACCEPT"));
firstTime = false;
} else {
G.ipv6Input(matcher.group(1).equals("ACCEPT"));
}
}
firstTime = true;
final Matcher matcher2 = pattern2.matcher(output);
while (matcher2.find()) {
if (firstTime) {
G.ipv4Output(matcher2.group(1).equals("ACCEPT"));
firstTime = false;
} else {
G.ipv6Output(matcher2.group(1).equals("ACCEPT"));
}
}
firstTime = true;
final Matcher matcher3 = pattern3.matcher(output);
while (matcher3.find()) {
if (firstTime) {
G.ipv4Fwd(matcher3.group(1).equals("ACCEPT"));
firstTime = false;
} else {
G.ipv6Fwd(matcher3.group(1).equals("ACCEPT"));
}
}
}
getPreferenceScreen().removeAll();
addPreferencesFromResource(R.xml.rules_preferences);
}
}
}));
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
ctx = context;
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
if(Build.VERSION.SDK_INT < Build.VERSION_CODES.M){
ctx = activity;
}
}
@Override
public void onResume() {
super.onResume();
getPreferenceManager().getSharedPreferences()
.registerOnSharedPreferenceChangeListener(this);
}
@Override
public void onPause() {
getPreferenceManager().getSharedPreferences()
.unregisterOnSharedPreferenceChangeListener(this);
super.onPause();
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
String key) {
if (key.equals("activeRules")) {
if (!G.activeRules()) {
//disable service when there is no active rules
//stopService(new Intent(PreferencesActivity.this, RootShell.class));
CheckBoxPreference enableRoam = (CheckBoxPreference) findPreference("enableRoam");
enableRoam.setChecked(false);
CheckBoxPreference enableLAN = (CheckBoxPreference) findPreference("enableLAN");
enableLAN.setChecked(false);
CheckBoxPreference enableVPN = (CheckBoxPreference) findPreference("enableVPN");
enableVPN.setChecked(false);
CheckBoxPreference enableTether = (CheckBoxPreference) findPreference("enableTether");
enableTether.setChecked(false);
CheckBoxPreference enableTor = (CheckBoxPreference) findPreference("enableTor");
enableTor.setChecked(false);
G.enableRoam(false);
G.enableLAN(false);
G.enableVPN(false);
G.enableTether(false);
G.enableTor(false);
}
}
//do chain apply for ipv4
switch (key) {
case "input_chain": {
String rule = "-P INPUT " + (G.ipv4Input() ? "ACCEPT" : "DROP");
Api.applyRule(ctx, rule, false, new RootCommand()
.setFailureToast(R.string.error_apply)
.setCallback(new RootCommand.Callback() {
@Override
public void cbFunc(RootCommand state) {
if (state.exitCode == 0) {
} else {
}
}
}));
break;
}
case "output_chain": {
String rule = "-P OUTPUT " + (G.ipv4Output() ? "ACCEPT" : "DROP");
Api.applyRule(ctx, rule, false, new RootCommand()
.setFailureToast(R.string.error_apply)
.setCallback(new RootCommand.Callback() {
@Override
public void cbFunc(RootCommand state) {
if (state.exitCode == 0) {
} else {
}
}
}));
break;
}
case "forward_chain": {
String rule = "-P FORWARD " + (G.ipv4Fwd() ? "ACCEPT" : "DROP");
Api.applyRule(ctx, rule, false, new RootCommand()
.setFailureToast(R.string.error_apply)
.setCallback(new RootCommand.Callback() {
@Override
public void cbFunc(RootCommand state) {
if (state.exitCode == 0) {
} else {
}
}
}));
break;
}
case "input_chain_v6": {
String rule = "-P INPUT " + (G.ipv6Input() ? "ACCEPT" : "DROP");
Api.applyRule(ctx, rule, true, new RootCommand()
.setFailureToast(R.string.error_apply)
.setCallback(new RootCommand.Callback() {
@Override
public void cbFunc(RootCommand state) {
if (state.exitCode == 0) {
} else {
}
}
}));
break;
}
case "output_chain_v6": {
String rule = "-P OUTPUT " + (G.ipv6Output() ? "ACCEPT" : "DROP");
Api.applyRule(ctx, rule, true, new RootCommand()
.setFailureToast(R.string.error_apply)
.setCallback(new RootCommand.Callback() {
@Override
public void cbFunc(RootCommand state) {
if (state.exitCode == 0) {
} else {
}
}
}));
break;
}
case "forward_chain_v6": {
String rule = "-P FORWARD " + (G.ipv6Fwd() ? "ACCEPT" : "DROP");
Api.applyRule(ctx, rule, true, new RootCommand()
.setFailureToast(R.string.error_apply)
.setCallback(new RootCommand.Callback() {
@Override
public void cbFunc(RootCommand state) {
if (state.exitCode == 0) {
} else {
}
}
}));
break;
}
}
if (key.equals("enableIPv6"))
{
File defaultIP6TablesPath = new File("/system/bin/ip6tables");
if (!defaultIP6TablesPath.exists()) {
G.enableIPv6(false);
CheckBoxPreference enable = (CheckBoxPreference) findPreference("enableIPv6");
enable.setChecked(false);
/*CheckBoxPreference block = (CheckBoxPreference) findPreference("blockIPv6");
block.setChecked(false);
if (ctx != null) {
Api.toast(ctx, getString(R.string.ip6unavailable));
}*/
} else {
switch (key) {
case "enableIPv6":
CheckBoxPreference block = (CheckBoxPreference) findPreference("controlIPv6");
block.setChecked(false);
break;
case "controlIPv6":
CheckBoxPreference allow = (CheckBoxPreference) findPreference("enableIPv6");
allow.setChecked(false);
break;
}
}
}
if (key.equals("controlIPv6")) {
CheckBoxPreference allow = (CheckBoxPreference) findPreference("enableIPv6");
allow.setChecked(false);
}
}
}

View file

@ -0,0 +1,514 @@
package dev.ukanth.ufirewall.preferences;
import static android.content.Context.FINGERPRINT_SERVICE;
import static android.content.Context.KEYGUARD_SERVICE;
import static haibison.android.lockpattern.LockPatternActivity.ACTION_COMPARE_PATTERN;
import static haibison.android.lockpattern.LockPatternActivity.ACTION_CREATE_PATTERN;
import static haibison.android.lockpattern.LockPatternActivity.EXTRA_PATTERN;
import android.Manifest;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.KeyguardManager;
import android.app.admin.DevicePolicyManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.hardware.fingerprint.FingerprintManager;
import android.os.Build;
import android.os.Bundle;
import android.preference.CheckBoxPreference;
import android.preference.ListPreference;
import android.preference.Preference;
import android.preference.PreferenceCategory;
import android.preference.PreferenceFragment;
import android.preference.SwitchPreference;
import android.text.InputType;
import android.util.Log;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import com.afollestad.materialdialogs.DialogAction;
import com.afollestad.materialdialogs.MaterialDialog;
import dev.ukanth.ufirewall.Api;
import dev.ukanth.ufirewall.R;
import dev.ukanth.ufirewall.admin.AdminDeviceReceiver;
import dev.ukanth.ufirewall.util.FingerprintUtil;
import dev.ukanth.ufirewall.util.G;
import haibison.android.lockpattern.LockPatternActivity;
import haibison.android.lockpattern.utils.AlpSettings;
public class SecPreferenceFragment extends PreferenceFragment implements
OnSharedPreferenceChangeListener {
private SwitchPreference enableAdminPref;
private CheckBoxPreference enableDeviceCheckPref;
private static final int REQ_CREATE_PATTERN = 9877;
private static final int REQ_ENTER_PATTERN = 9755;
private static final int REQUEST_CODE_ENABLE_ADMIN = 10237; // identifies
private ComponentName deviceAdmin;
private DevicePolicyManager mDPM;
private Context globalContext = null;
//private String passOption = "p0";
public void setupEnableAdmin(Preference pref) {
if (pref == null) {
return;
}
enableAdminPref = (SwitchPreference) pref;
// query the actual device admin status from the system
enableAdminPref.setChecked(mDPM.isAdminActive(deviceAdmin));
}
@SuppressLint("NewApi")
@Override
public void onCreate(Bundle savedInstanceState) {
// update settings with actual device admin setting
mDPM = (DevicePolicyManager) this.getActivity().getSystemService(
Context.DEVICE_POLICY_SERVICE);
deviceAdmin = new ComponentName(this.getActivity()
.getApplicationContext(), AdminDeviceReceiver.class);
super.onCreate(savedInstanceState);
globalContext = this.getActivity();
// Load the preferences from an XML resource
addPreferencesFromResource(R.xml.security_preferences);
//backward compatibility
preSelectListForBackward();
setupDeviceSecurityCheck(findPreference("enableDeviceCheck"));
setupEnableAdmin(findPreference("enableAdmin"));
//passOption = G.protectionLevel();
// Hide Fingerprint option if device not support it.
if (!FingerprintUtil.isAndroidSupport() || !canUserFingerPrint()) {
ListPreference itemList = (ListPreference) findPreference("passSetting");
itemList.setEntries(new String[]{
getString(R.string.pref_none),
getString(R.string.pref_password),
getString(R.string.pref_pattern),
});
itemList.setEntryValues(new String[]{
"p0", "p1", "p2"
});
}
}
private void setupDeviceSecurityCheck(Preference pref) {
PreferenceCategory mCategory = (PreferenceCategory) findPreference("securitySetting");
enableDeviceCheckPref = (CheckBoxPreference) pref;
if (Build.VERSION.SDK_INT >= 21) {
//only for donate version
if ((G.isDoKey(getActivity()) || G.isDonate())) {
if (globalContext != null) {
KeyguardManager keyguardManager = (KeyguardManager) globalContext.getSystemService(KEYGUARD_SERVICE);
//enable only when keyguard has set
if (keyguardManager.isKeyguardSecure()) {
enableDeviceCheckPref.setEnabled(true);
} else {
enableDeviceCheckPref.setEnabled(false);
enableDeviceCheckPref.setChecked(false);
}
}
} else {
enableDeviceCheckPref.setEnabled(false);
enableDeviceCheckPref.setChecked(false);
}
} else {
//remove this option for older devices
mCategory.removePreference(enableDeviceCheckPref);
}
}
private void preSelectListForBackward() {
final ListPreference itemList = (ListPreference) findPreference("passSetting");
//remove other option
if (Build.VERSION.SDK_INT < 21) {
itemList.setEntries(itemList.getEntries());
itemList.setEntryValues(itemList.getEntryValues());
}
if (itemList != null) {
switch (G.protectionLevel()) {
case "p0":
itemList.setValueIndex(0);
break;
case "p1":
itemList.setValueIndex(1);
break;
case "p2":
itemList.setValueIndex(2);
break;
case "p3":
itemList.setValueIndex(3);
break;
case "Disable":
itemList.setValueIndex(0);
break;
default:
itemList.setValueIndex(0);
break;
}
}
}
/**
* Set a new password lock
*
* @param pwd new password (empty to remove the lock)
*/
private void setPassword(String pwd) {
final Resources res = getResources();
String msg = "";
if (pwd.length() > 0) {
String enc = Api.hideCrypt("AFW@LL_P@SSWORD_PR0T3CTI0N", pwd);
if (enc != null) {
G.profile_pwd(enc);
G.isEnc(true);
msg = res.getString(R.string.passdefined);
}
} /*else {
G.profile_pwd(pwd);
G.isEnc(false);
msg = res.getString(R.string.passremoved);
}*/
Api.toast(getActivity(), msg, Toast.LENGTH_SHORT);
}
/**
* Display Password dialog
*
* @param itemList
*/
private void showPasswordActivity(final ListPreference itemList) {
final MaterialDialog.Builder builder = new MaterialDialog.Builder(getActivity());
//you should edit this to fit your needs
builder.title(getString(R.string.pass_titleset));
final EditText firstPass = new EditText(getActivity());
firstPass.setHint(getString(R.string.enterpass));//optional
final EditText secondPass = new EditText(getActivity());
secondPass.setHint(getString(R.string.reenterpass));//optional
//in my example i use TYPE_CLASS_NUMBER for input only numbers
firstPass.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
secondPass.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
LinearLayout lay = new LinearLayout(getActivity());
lay.setOrientation(LinearLayout.VERTICAL);
lay.addView(firstPass);
lay.addView(secondPass);
builder.customView(lay, false);
builder.autoDismiss(false);
builder.positiveText(R.string.set_password);
builder.negativeText(R.string.Cancel);
builder.onPositive(new MaterialDialog.SingleButtonCallback() {
@Override
public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) {
//get the two inputs
if (firstPass.getText().toString().equals(secondPass.getText().toString())) {
setPassword(firstPass.getText().toString());
G.enableDeviceCheck(false);
dialog.dismiss();
} else {
Api.toast(getActivity(), getString(R.string.settings_pwd_not_equal));
}
}
});
builder.onNegative((dialog, which) -> {
itemList.setValueIndex(0);
dialog.dismiss();
});
builder.show();
}
private void showPatternActivity() {
Intent intent = new Intent(ACTION_CREATE_PATTERN, null, getActivity(), LockPatternActivity.class);
startActivityForResult(intent, REQ_CREATE_PATTERN);
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
String key) {
if (key.equals("passSetting")) {
ListPreference itemList = (ListPreference) findPreference("passSetting");
final ListPreference patternMaxTry = (ListPreference) findPreference("patternMax");
final CheckBoxPreference stealthMode = (CheckBoxPreference) findPreference("stealthMode");
stealthMode.setEnabled(false);
patternMaxTry.setEnabled(false);
switch (itemList.getValue()) {
case "p0":
//disable password completly -- add reconfirmation based on current index
confirmResetPasswords(itemList);
break;
case "p1":
//use the existing method to protect password
showPasswordActivity(itemList);
break;
case "p2":
//use the existing method to protect password
showPatternActivity();
break;
case "p3":
if (FingerprintUtil.isAndroidSupport()) {
checkFingerprintDeviceSupport();
}
break;
}
// check if device support fingerprint,
// if so check if one fingerprint already existed at least
/* if (FingerprintUtil.isAndroidSupport()) {
checkFingerprintDeviceSupport();
}*/
}
if (key.equals("enableAdmin")) {
boolean value = G.enableAdmin();
if (value) {
Log.d("Device Admin Active ?", mDPM.isAdminActive(deviceAdmin) + "");
if (!mDPM.isAdminActive(deviceAdmin)) {
// Launch the activity to have the user enable our admin.
Intent intent = new Intent(
DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN);
intent.putExtra(DevicePolicyManager.EXTRA_DEVICE_ADMIN,
deviceAdmin);
intent.putExtra(DevicePolicyManager.EXTRA_ADD_EXPLANATION,
getString(R.string.device_admin_desc));
startActivityForResult(intent, REQUEST_CODE_ENABLE_ADMIN);
}
} else {
if (mDPM.isAdminActive(deviceAdmin)) {
mDPM.removeActiveAdmin(deviceAdmin);
Api.toast(this.getActivity().getApplicationContext(),
getString(R.string.device_admin_disabled), Toast.LENGTH_LONG);
}
}
}
if (key.equals("enableStealthPattern")) {
AlpSettings.Display.setStealthMode(this.getActivity().getApplicationContext(),
G.enableStealthPattern());
}
}
@TargetApi(Build.VERSION_CODES.M)
private boolean canUserFingerPrint() {
try {
KeyguardManager keyguardManager = (KeyguardManager) globalContext.getSystemService(KEYGUARD_SERVICE);
FingerprintManager fingerprintManager = (FingerprintManager) globalContext.getSystemService(FINGERPRINT_SERVICE);
return fingerprintManager.isHardwareDetected() &&
ActivityCompat.checkSelfPermission(globalContext, Manifest.permission.USE_FINGERPRINT) == PackageManager.PERMISSION_GRANTED &&
fingerprintManager.hasEnrolledFingerprints() &&
keyguardManager.isKeyguardSecure();
} catch (Exception e) {
return false;
}
}
@TargetApi(Build.VERSION_CODES.M)
private void checkFingerprintDeviceSupport() {
// Initializing both Android Keyguard Manager and Fingerprint Manager
KeyguardManager keyguardManager = (KeyguardManager) globalContext.getSystemService(KEYGUARD_SERVICE);
FingerprintManager fingerprintManager = (FingerprintManager) globalContext.getSystemService(FINGERPRINT_SERVICE);
ListPreference itemList = (ListPreference) findPreference("passSetting");
// Check whether the device has a Fingerprint sensor.
if (!fingerprintManager.isHardwareDetected()) {
Api.toast(globalContext, getString(R.string.device_with_no_fingerprint_sensor));
itemList.setValueIndex(0);
} else {
// Checks whether fingerprint permission is set on manifest
if (ActivityCompat.checkSelfPermission(globalContext, Manifest.permission.USE_FINGERPRINT) != PackageManager.PERMISSION_GRANTED) {
Api.toast(globalContext, getString(R.string.fingerprint_permission_manifest_missing));
itemList.setValueIndex(0);
} else {
// Check whether at least one fingerprint is registered
if (!fingerprintManager.hasEnrolledFingerprints()) {
Api.toast(globalContext, getString(R.string.register_at_least_one_fingerprint));
itemList.setValueIndex(0);
} else {
// Checks whether lock screen security is enabled or not
if (!keyguardManager.isKeyguardSecure()) {
Api.toast(globalContext, getString(R.string.lock_screen_not_enabled));
itemList.setValueIndex(0);
} else {
// Anything is ok
if (!G.isFingerprintEnabled()) {
G.isFingerprintEnabled(true);
//make sure we set the index
itemList.setValueIndex(3);
Api.toast(globalContext, getString(R.string.fingerprint_enabled_successfully));
}
return;
}
}
}
}
}
/**
* Make sure it's verified before remove passwords
*
* @param itemList
*/
private void confirmResetPasswords(final ListPreference itemList) {
String pattern = G.sPrefs.getString("LockPassword", "");
String pwd = G.profile_pwd();
if (pwd.length() > 0) {
new MaterialDialog.Builder(getActivity()).cancelable(false)
.title(R.string.confirmation).autoDismiss(false)
.content(R.string.enterpass)
.inputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD)
.input(R.string.enterpass, R.string.password_empty, new MaterialDialog.InputCallback() {
@Override
public void onInput(MaterialDialog dialog, CharSequence input) {
String pass = input.toString();
boolean isAllowed = false;
if (G.isEnc()) {
String decrypt = Api.unhideCrypt("AFW@LL_P@SSWORD_PR0T3CTI0N", G.profile_pwd());
if (decrypt != null) {
if (decrypt.equals(pass)) {
isAllowed = true;
//Api.toast(getActivity(), getString(R.string.wrong_password));
}
}
} else {
if (pass.equals(G.profile_pwd())) {
//reset password
isAllowed = true;
//Api.toast(getActivity(), getString(R.string.wrong_password));
}
}
if (isAllowed) {
G.profile_pwd("");
G.isEnc(false);
itemList.setValueIndex(0);
dialog.dismiss();
} else {
Api.toast(getActivity(), getString(R.string.wrong_password));
}
}
}).show();
}
if (pattern.length() > 0) {
Intent intent = new Intent(ACTION_COMPARE_PATTERN, null, getActivity(), LockPatternActivity.class);
String savedPattern = G.sPrefs.getString("LockPassword", "");
intent.putExtra(EXTRA_PATTERN, savedPattern.toCharArray());
startActivityForResult(intent, REQ_ENTER_PATTERN);
}
// check if fingerprint enabled and confirm disable by fingerprint itself
if (G.isFingerprintEnabled()) {
final FingerprintUtil.FingerprintDialog dialog;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
dialog = new FingerprintUtil.FingerprintDialog(globalContext);
dialog.setOnFingerprintFailureListener(() -> {
itemList.setValueIndex(3);
dialog.dismiss();
});
dialog.setOnFingerprintSuccess(() -> {
G.isFingerprintEnabled(false);
Api.toast(globalContext, getString(R.string.fingerprint_disabled_successfully));
});
dialog.show();
}
}
}
@Override
public void onResume() {
super.onResume();
getPreferenceManager().getSharedPreferences()
.registerOnSharedPreferenceChangeListener(this);
}
@Override
public void onPause() {
getPreferenceManager().getSharedPreferences()
.unregisterOnSharedPreferenceChangeListener(this);
super.onPause();
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
setupEnableAdmin(findPreference("enableAdmin"));
switch (requestCode) {
case REQ_CREATE_PATTERN: {
ListPreference itemList = (ListPreference) findPreference("passSetting");
if (resultCode == getActivity().RESULT_OK) {
char[] pattern = data.getCharArrayExtra(
EXTRA_PATTERN);
final SharedPreferences.Editor editor = G.sPrefs.edit();
editor.putString("LockPassword", new String(pattern));
editor.commit();
G.enableDeviceCheck(false);
//enable
if (itemList != null) {
final ListPreference patternMaxTry = (ListPreference) findPreference("patternMax");
final CheckBoxPreference stealthMode = (CheckBoxPreference) findPreference("stealthMode");
if (stealthMode != null) stealthMode.setEnabled(true);
if (patternMaxTry != null) patternMaxTry.setEnabled(true);
}
} else {
itemList = (ListPreference) findPreference("passSetting");
if (itemList != null) {
itemList.setValueIndex(0);
}
}
break;
}
case REQ_ENTER_PATTERN: {
ListPreference itemList = (ListPreference) findPreference("passSetting");
if (resultCode == getActivity().RESULT_OK) {
final SharedPreferences.Editor editor = G.sPrefs.edit();
editor.putString("LockPassword", "");
editor.commit();
itemList = (ListPreference) findPreference("passSetting");
if (itemList != null) {
itemList.setValueIndex(0);
}
} else {
if (itemList != null) {
itemList.setValueIndex(2);
G.enableDeviceCheck(false);
}
}
}
}
}
}

View file

@ -0,0 +1,19 @@
package dev.ukanth.ufirewall.preferences;
/**
* Created by ukanth on 19/7/16.
*/
public class ShareContract {
public static final String COLUMN_KEY = "key";
public static final String COLUMN_TYPE = "type";
public static final String COLUMN_VALUE = "value";
public static final int TYPE_NULL = 0;
public static final int TYPE_STRING = 1;
public static final int TYPE_STRING_SET = 2;
public static final int TYPE_INT = 3;
public static final int TYPE_LONG = 4;
public static final int TYPE_FLOAT = 5;
public static final int TYPE_BOOLEAN = 6;
}

View file

@ -0,0 +1,294 @@
package dev.ukanth.ufirewall.preferences;
import android.annotation.TargetApi;
import android.content.ContentValues;
import android.content.Context;
import android.content.SharedPreferences;
import android.database.ContentObserver;
import android.database.Cursor;
import android.net.Uri;
import android.os.Handler;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
/**
* Created by ukanth on 19/7/16.
*/
public class SharePreference implements SharedPreferences {
private final Context mContext;
private final Handler mHandler;
private final Uri mBaseUri;
private final WeakHashMap<OnSharedPreferenceChangeListener, PreferenceContentObserver> mListeners;
/**
* Initializes a new remote preferences object.
* You must use the same authority as the preference provider.
* Note that if you pass invalid parameter values, the
* constructor will complete successfully, but data accesses
* will either throw {@link IllegalArgumentException} or return
* default values.
*
* @param context Used to access the preference provider.
* @param authority The authority of the preference provider.
* @param prefName The name of the preference file to access.
*/
public SharePreference(Context context, String authority, String prefName) {
mContext = context;
mHandler = new Handler(context.getMainLooper());
mBaseUri = Uri.parse("content://" + authority).buildUpon().appendPath(prefName).build();
mListeners = new WeakHashMap<>();
}
@Override
public Map<String, ?> getAll() {
return queryAll();
}
@Override
public String getString(String key, String defValue) {
return (String)querySingle(key, defValue, ShareContract.TYPE_STRING);
}
@Override
@TargetApi(11)
public Set<String> getStringSet(String key, Set<String> defValues) {
return ShareUtils.toStringSet(querySingle(key, defValues, ShareContract.TYPE_STRING_SET));
}
@Override
public int getInt(String key, int defValue) {
return (Integer)querySingle(key, defValue, ShareContract.TYPE_INT);
}
@Override
public long getLong(String key, long defValue) {
return (Long)querySingle(key, defValue, ShareContract.TYPE_LONG);
}
@Override
public float getFloat(String key, float defValue) {
return (Float)querySingle(key, defValue, ShareContract.TYPE_FLOAT);
}
@Override
public boolean getBoolean(String key, boolean defValue) {
return (Boolean)querySingle(key, defValue, ShareContract.TYPE_BOOLEAN);
}
@Override
public boolean contains(String key) {
return containsKey(key);
}
@Override
public Editor edit() {
return new RemotePreferencesEditor();
}
@Override
public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
if (mListeners.containsKey(listener)) return;
PreferenceContentObserver observer = new PreferenceContentObserver(listener);
mListeners.put(listener, observer);
mContext.getContentResolver().registerContentObserver(mBaseUri, true, observer);
}
@Override
public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
PreferenceContentObserver observer = mListeners.remove(listener);
if (observer != null) {
mContext.getContentResolver().unregisterContentObserver(observer);
}
}
private Object querySingle(String key, Object defValue, int expectedType) {
Uri uri = mBaseUri.buildUpon().appendPath(key).build();
String[] columns = {ShareContract.COLUMN_TYPE, ShareContract.COLUMN_VALUE};
Cursor cursor = mContext.getContentResolver().query(uri, columns, null, null, null);
try {
if (cursor == null || !cursor.moveToFirst() || cursor.getInt(0) == ShareContract.TYPE_NULL) {
return defValue;
} else if (cursor.getInt(0) != expectedType) {
throw new ClassCastException("Preference type mismatch");
} else {
return getValue(cursor, 0, 1);
}
} finally {
if (cursor != null) {
cursor.close();
}
}
}
private Map<String, Object> queryAll() {
Uri uri = mBaseUri.buildUpon().appendPath("").build();
String[] columns = {ShareContract.COLUMN_KEY, ShareContract.COLUMN_TYPE, ShareContract.COLUMN_VALUE};
Cursor cursor = mContext.getContentResolver().query(uri, columns, null, null, null);
try {
HashMap<String, Object> map = new HashMap<String, Object>(0);
if (cursor == null) {
return map;
}
while (cursor.moveToNext()) {
String name = cursor.getString(0);
map.put(name, getValue(cursor, 1, 2));
}
return map;
} finally {
if (cursor != null) {
cursor.close();
}
}
}
private boolean containsKey(String key) {
Uri uri = mBaseUri.buildUpon().appendPath(key).build();
String[] columns = {ShareContract.COLUMN_TYPE};
Cursor cursor = mContext.getContentResolver().query(uri, columns, null, null, null);
try {
return (cursor != null && cursor.moveToFirst() && cursor.getInt(0) != ShareContract.TYPE_NULL);
} finally {
if (cursor != null) {
cursor.close();
}
}
}
private Object getValue(Cursor cursor, int typeCol, int valueCol) {
int expectedType = cursor.getInt(typeCol);
switch (expectedType) {
case ShareContract.TYPE_STRING:
return cursor.getString(valueCol);
case ShareContract.TYPE_STRING_SET:
return ShareUtils.deserializeStringSet(cursor.getString(valueCol));
case ShareContract.TYPE_INT:
return cursor.getInt(valueCol);
case ShareContract.TYPE_LONG:
return cursor.getLong(valueCol);
case ShareContract.TYPE_FLOAT:
return cursor.getFloat(valueCol);
case ShareContract.TYPE_BOOLEAN:
return cursor.getInt(valueCol) != 0;
default:
throw new AssertionError("Invalid expected type: " + expectedType);
}
}
private class RemotePreferencesEditor implements Editor {
private final List<ContentValues> mToAdd = new ArrayList<ContentValues>();
private final Set<String> mToRemove = new HashSet<String>();
private ContentValues add(String key, int type) {
ContentValues values = new ContentValues(3);
values.put(ShareContract.COLUMN_KEY, key);
values.put(ShareContract.COLUMN_TYPE, type);
mToAdd.add(values);
return values;
}
@Override
public Editor putString(String key, String value) {
add(key, ShareContract.TYPE_STRING)
.put(ShareContract.COLUMN_VALUE, value);
return this;
}
@Override
@TargetApi(11)
public Editor putStringSet(String key, Set<String> value) {
add(key, ShareContract.TYPE_STRING_SET)
.put(ShareContract.COLUMN_VALUE, ShareUtils.serializeStringSet(value));
return this;
}
@Override
public Editor putInt(String key, int value) {
add(key, ShareContract.TYPE_INT)
.put(ShareContract.COLUMN_VALUE, value);
return this;
}
@Override
public Editor putLong(String key, long value) {
add(key, ShareContract.TYPE_LONG)
.put(ShareContract.COLUMN_VALUE, value);
return this;
}
@Override
public Editor putFloat(String key, float value) {
add(key, ShareContract.TYPE_FLOAT)
.put(ShareContract.COLUMN_VALUE, value);
return this;
}
@Override
public Editor putBoolean(String key, boolean value) {
add(key, ShareContract.TYPE_BOOLEAN)
.put(ShareContract.COLUMN_VALUE, value ? 1 : 0);
return this;
}
@Override
public Editor remove(String key) {
mToRemove.add(key);
return this;
}
@Override
public Editor clear() {
return remove("");
}
@Override
public boolean commit() {
for (String key : mToRemove) {
Uri uri = mBaseUri.buildUpon().appendPath(key).build();
mContext.getContentResolver().delete(uri, null, null);
}
ContentValues[] values = mToAdd.toArray(new ContentValues[0]);
Uri uri = mBaseUri.buildUpon().appendPath("").build();
mContext.getContentResolver().bulkInsert(uri, values);
return true;
}
@Override
public void apply() {
commit();
}
}
private class PreferenceContentObserver extends ContentObserver {
private final WeakReference<OnSharedPreferenceChangeListener> mListener;
private PreferenceContentObserver(OnSharedPreferenceChangeListener listener) {
super(mHandler);
mListener = new WeakReference<OnSharedPreferenceChangeListener>(listener);
}
@Override
public boolean deliverSelfNotifications() {
return true;
}
@Override
public void onChange(boolean selfChange, Uri uri) {
String prefKey = uri.getLastPathSegment();
OnSharedPreferenceChangeListener listener = mListener.get();
if (listener == null) {
mContext.getContentResolver().unregisterContentObserver(this);
} else {
listener.onSharedPreferenceChanged(SharePreference.this, prefKey);
}
}
}
}

View file

@ -0,0 +1,256 @@
package dev.ukanth.ufirewall.preferences;
/**
* Created by ukanth on 19/7/16.
*/
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.os.Build;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import dev.ukanth.ufirewall.BuildConfig;
/**
* Exposes {@link SharedPreferences} to other apps running on the device.
*
* You must extend this class and declare a default constructor which
* calls the super constructor with the appropriate authority and
* preference file name parameters. Remember to add your provider to
* your AndroidManifest.xml file and set the {@code android:exported}
* property to true.
*
* To access the data from a remote process, use {@link SharedPreferences}
* initialized with the same authority and the desired preference file name.
*
* For granular access control, override {@link #checkAccess(String, String, boolean)}
* and return {@code false} to deny the operation.
*/
public abstract class SharePreferenceProvider extends ContentProvider implements SharedPreferences.OnSharedPreferenceChangeListener {
private static final int PREFERENCES_ID = 1;
private static final int PREFERENCE_ID = 2;
public static final String AUTHORITY = BuildConfig.APPLICATION_ID;
private final Uri mBaseUri;
private final String[] mPrefNames;
private final Map<String, SharedPreferences> mPreferences;
private final UriMatcher mUriMatcher;
/**
* Initializes the remote preference provider with the specified
* authority and preference files. The authority must match the
* {@code android:authorities} property defined in your manifest
* file. Only the specified preference files will be accessible
* through the provider.
*
* @param prefNames The names of the preference files to expose.
*/
public SharePreferenceProvider(String[] prefNames) {
mBaseUri = Uri.parse("content://" + AUTHORITY);
mPrefNames = prefNames;
mPreferences = new HashMap<String, SharedPreferences>(prefNames.length);
mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
mUriMatcher.addURI(AUTHORITY, "*/", PREFERENCES_ID);
mUriMatcher.addURI(AUTHORITY, "*/*", PREFERENCE_ID);
}
@Override
public boolean onCreate() {
Context context = getContext();
for (String prefName : mPrefNames) {
SharedPreferences preferences = context.getSharedPreferences(prefName, Context.MODE_PRIVATE);
preferences.registerOnSharedPreferenceChangeListener(this);
mPreferences.put(prefName, preferences);
}
return true;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
PrefNameKeyPair nameKeyPair = parseUri(uri);
SharedPreferences preferences = getPreferences(nameKeyPair, false);
Map<String, ?> preferenceMap = preferences.getAll();
MatrixCursor cursor = new MatrixCursor(projection);
if (nameKeyPair.key.length() == 0) {
for (Map.Entry<String, ?> entry : preferenceMap.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
cursor.addRow(buildRow(projection, key, value));
}
} else {
String key = nameKeyPair.key;
Object value = preferenceMap.get(key);
cursor.addRow(buildRow(projection, key, value));
}
return cursor;
}
@Override
public String getType(Uri uri) {
return null;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
PrefNameKeyPair nameKeyPair = parseUri(uri);
String key = nameKeyPair.key;
if (key.length() == 0) {
key = values.getAsString(ShareContract.COLUMN_KEY);
}
int type = values.getAsInteger(ShareContract.COLUMN_TYPE);
Object value = ShareUtils.deserialize(values.get(ShareContract.COLUMN_VALUE), type);
SharedPreferences preferences = getPreferences(nameKeyPair, true);
SharedPreferences.Editor editor = preferences.edit();
if (value == null) {
throw new IllegalArgumentException("Attempting to insert preference with null value");
} else if (value instanceof String) {
editor.putString(key, (String)value);
} else if (value instanceof Set<?>) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
editor.putStringSet(key, ShareUtils.toStringSet(value));
} else {
throw new IllegalArgumentException("String set preferences not supported on API < 11");
}
} else if (value instanceof Integer) {
editor.putInt(key, (Integer)value);
} else if (value instanceof Long) {
editor.putLong(key, (Long)value);
} else if (value instanceof Float) {
editor.putFloat(key, (Float)value);
} else if (value instanceof Boolean) {
editor.putBoolean(key, (Boolean)value);
} else {
throw new IllegalArgumentException("Cannot set preference with type " + value.getClass());
}
editor.commit();
return uri;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
PrefNameKeyPair nameKeyPair = parseUri(uri);
String key = nameKeyPair.key;
SharedPreferences preferences = getPreferences(nameKeyPair, true);
if (key.length() == 0) {
preferences.edit().clear().commit();
} else {
preferences.edit().remove(key).commit();
}
return 0;
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
insert(uri, values);
return 0;
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
String prefName = getPreferencesName(sharedPreferences);
Uri uri = mBaseUri.buildUpon().appendPath(prefName).appendPath(key).build();
getContext().getContentResolver().notifyChange(uri, null);
}
private PrefNameKeyPair parseUri(Uri uri) {
int match = mUriMatcher.match(uri);
if (match != PREFERENCE_ID && match != PREFERENCES_ID) {
throw new IllegalArgumentException("Invalid URI: " + uri);
}
List<String> pathSegments = uri.getPathSegments();
String prefName = pathSegments.get(0);
String prefKey = "";
if (match == PREFERENCE_ID) {
prefKey = pathSegments.get(1);
}
return new PrefNameKeyPair(prefName, prefKey);
}
private int getPrefType(Object value) {
if (value == null) return ShareContract.TYPE_NULL;
if (value instanceof String) return ShareContract.TYPE_STRING;
if (value instanceof Set<?>) return ShareContract.TYPE_STRING_SET;
if (value instanceof Integer) return ShareContract.TYPE_INT;
if (value instanceof Long) return ShareContract.TYPE_LONG;
if (value instanceof Float) return ShareContract.TYPE_FLOAT;
if (value instanceof Boolean) return ShareContract.TYPE_BOOLEAN;
throw new AssertionError("Unknown preference type: " + value.getClass());
}
private Object[] buildRow(String[] projection, String key, Object value) {
Object[] row = new Object[projection.length];
for (int i = 0; i < row.length; ++i) {
String col = projection[i];
if (ShareContract.COLUMN_KEY.equals(col)) {
row[i] = key;
} else if (ShareContract.COLUMN_TYPE.equals(col)) {
row[i] = getPrefType(value);
} else if (ShareContract.COLUMN_VALUE.equals(col)) {
row[i] = ShareUtils.serialize(value);
} else {
throw new IllegalArgumentException("Invalid column name: " + col);
}
}
return row;
}
private SharedPreferences getPreferences(PrefNameKeyPair nameKeyPair, boolean write) {
String prefName = nameKeyPair.name;
String prefKey = nameKeyPair.key;
SharedPreferences prefs = mPreferences.get(prefName);
if (prefs == null) {
throw new IllegalArgumentException("Unknown preference file name: " + prefName);
}
if (!checkAccess(prefName, prefKey, write)) {
throw new SecurityException("Insufficient permissions to access: " + prefName + "/" + prefKey);
}
return prefs;
}
private String getPreferencesName(SharedPreferences preferences) {
for (Map.Entry<String, SharedPreferences> entry : mPreferences.entrySet()) {
if (entry.getValue() == preferences) {
return entry.getKey();
}
}
throw new AssertionError("Cannot find name for SharedPreferences");
}
/**
* Checks whether a specific preference is accessible by clients.
* The default implementation returns {@code true} for all accesses.
* You may override this method to control which preferences can be
* read or written.
*
* @param prefName The name of the preference file.
* @param prefKey The preference key. This is an empty string when handling the
* {@link SharedPreferences#getAll()} and
* {@link SharedPreferences.Editor#clear()} operations.
* @param write {@code true} for "put" operations; {@code false} for "get" operations.
* @return {@code true} if the access is allowed; {@code false} otherwise.
*/
protected boolean checkAccess(String prefName, String prefKey, boolean write) {
return true;
}
private static class PrefNameKeyPair {
private final String name;
private final String key;
private PrefNameKeyPair(String prefName, String prefKey) {
name = prefName;
key = prefKey;
}
}
}

View file

@ -0,0 +1,20 @@
package dev.ukanth.ufirewall.preferences;
import dev.ukanth.ufirewall.Api;
import dev.ukanth.ufirewall.BuildConfig;
/**
* Created by ukanth on 19/7/16.
*/
public class ShareProfilePreference extends SharePreferenceProvider {
public ShareProfilePreference() {
super(new String[] {BuildConfig.APPLICATION_ID + "_preferences", Api.PREFS_NAME});
}
//make sure only read access
@Override
protected boolean checkAccess(String prefName, String prefKey, boolean write) {
// Only allow read access
return !write;
}
}

View file

@ -0,0 +1,64 @@
package dev.ukanth.ufirewall.preferences;
import java.util.HashSet;
import java.util.Set;
/**
* Created by ukanth on 19/7/16.
*/
public class ShareUtils {
@SuppressWarnings("unchecked")
public static Set<String> toStringSet(Object value) {
return (Set<String>)value;
}
public static Object serialize(Object value) {
if (value instanceof Boolean) {
return (Boolean)value ? 1 : 0;
} else if (value instanceof Set<?>) {
return ShareUtils.serializeStringSet(toStringSet(value));
} else {
return value;
}
}
public static Object deserialize(Object value, int expectedType) {
if (value == null) {
return null;
} else if (expectedType == ShareContract.TYPE_BOOLEAN) {
return (Integer)value != 0;
} else if (expectedType == ShareContract.TYPE_STRING_SET) {
return ShareUtils.deserializeStringSet((String)value);
} else {
return value;
}
}
public static String serializeStringSet(Set<String> stringSet) {
StringBuilder sb = new StringBuilder();
for (String s : stringSet) {
sb.append(s.replace("\\", "\\\\").replace(";", "\\;"));
sb.append(';');
}
return sb.toString();
}
public static Set<String> deserializeStringSet(String serializedString) {
HashSet<String> stringSet = new HashSet<String>();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < serializedString.length(); ++i) {
char c = serializedString.charAt(i);
if (c == '\\') {
char next = serializedString.charAt(++i);
sb.append(next);
} else if (c == ';') {
stringSet.add(sb.toString());
sb.setLength(0);
} else {
sb.append(c);
}
}
return stringSet;
}
}

View file

@ -0,0 +1,81 @@
package dev.ukanth.ufirewall.preferences;
import static dev.ukanth.ufirewall.util.G.isDonate;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.PreferenceFragment;
import android.widget.Toast;
import dev.ukanth.ufirewall.Api;
import dev.ukanth.ufirewall.R;
import dev.ukanth.ufirewall.util.G;
public class ThemePreferenceFragment extends PreferenceFragment implements
SharedPreferences.OnSharedPreferenceChangeListener {
private Context ctx;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Load the preferences from an XML resource
addPreferencesFromResource(R.xml.theme_preference);
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
ctx = context;
}
@Override
public void onResume() {
super.onResume();
getPreferenceManager().getSharedPreferences()
.registerOnSharedPreferenceChangeListener(this);
}
@Override
public void onPause() {
getPreferenceManager().getSharedPreferences()
.unregisterOnSharedPreferenceChangeListener(this);
super.onPause();
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
String key) {
if (ctx == null) {
ctx = getActivity();
}
if (ctx != null) {
if (key.equals("theme")) {
switch (G.getSelectedTheme()){
case "D":
G.getInstance().setTheme(R.style.AppDarkTheme);
break;
case "L":
if ((G.isDoKey(ctx) || isDonate())) {
G.getInstance().setTheme(R.style.AppLightTheme);
} else {
Api.toast(ctx, ctx.getText(R.string.donate_only), Toast.LENGTH_LONG);
G.getSelectedTheme("D");
}
break;
case "B":
if ((G.isDoKey(ctx) || isDonate())) {
G.getInstance().setTheme(R.style.AppBlackTheme);
} else {
Api.toast(ctx, ctx.getText(R.string.donate_only), Toast.LENGTH_LONG);
G.getSelectedTheme("D");
}
break;
}
}
}
}
}

View file

@ -0,0 +1,148 @@
package dev.ukanth.ufirewall.preferences;
import static dev.ukanth.ufirewall.util.G.isDonate;
import android.app.NotificationManager;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.Preference;
import android.preference.PreferenceFragment;
import com.afollestad.materialdialogs.MaterialDialog;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import dev.ukanth.ufirewall.Api;
import dev.ukanth.ufirewall.R;
import dev.ukanth.ufirewall.util.G;
public class UIPreferenceFragment extends PreferenceFragment implements
SharedPreferences.OnSharedPreferenceChangeListener {
private Context ctx;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Load the preferences from an XML resource
addPreferencesFromResource(R.xml.ui_preferences);
if ((G.isDoKey(ctx) || isDonate())) {
populatePreference(findPreference("default_behavior_allow_mode"), getString(R.string.connection_default_allow), 0);
populatePreference(findPreference("default_behavior_block_mode"), getString(R.string.connection_default_allow), 1);
}
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
ctx = context;
}
@Override
public void onResume() {
super.onResume();
getPreferenceManager().getSharedPreferences()
.registerOnSharedPreferenceChangeListener(this);
}
@Override
public void onPause() {
getPreferenceManager().getSharedPreferences()
.unregisterOnSharedPreferenceChangeListener(this);
super.onPause();
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
String key) {
if(ctx == null) {
ctx = getActivity();
}
if(ctx != null) {
if (key.equals("notification_priority")) {
NotificationManager notificationManager = (NotificationManager) ctx.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel(1);
//Api.showNotification(Api.isEnabled(ctx), ctx);
Api.updateNotification(Api.isEnabled(ctx), ctx);
}
}
}
private void populatePreference(Preference list, String title, int modeType) {
final ArrayList<CharSequence> entriesList = new ArrayList<CharSequence>();
final ArrayList<Integer> entryValuesList = new ArrayList<Integer>();
entriesList.add(getString(R.string.lan));
entriesList.add(getString(R.string.wifi));
entriesList.add(getString(R.string.data));
entriesList.add(getString(R.string.roaming));
entriesList.add(getString(R.string.tor));
entriesList.add(getString(R.string.vpn));
entriesList.add(getString(R.string.tether));
entryValuesList.add(0);
entryValuesList.add(1);
entryValuesList.add(2);
entryValuesList.add(3);
entryValuesList.add(4);
entryValuesList.add(5);
entryValuesList.add(6);
list.setOnPreferenceClickListener(preference -> {
//open browser or intent here
MaterialDialog dialog = new MaterialDialog.Builder(getActivity())
.title(title)
.itemsIds(convertIntegers(entryValuesList))
.items(entriesList)
.itemsCallbackMultiChoice(null, (dialog1, which, text) -> {
List<Integer> listPerf = new ArrayList<Integer>();
List<Integer> selectedItems = new ArrayList<Integer>();
List<Integer> ignoredItems = new ArrayList<Integer>();
for (int i : which) {
selectedItems.add(i);
listPerf.add(entryValuesList.get(i));
}
for (int item: entryValuesList) {
if(!selectedItems.contains(item)) {
ignoredItems.add(item);
}
}
G.storeDefaultConnection(listPerf,ignoredItems,modeType);
return true;
})
.positiveText(R.string.OK)
.negativeText(R.string.close)
.show();
if (G.readDefaultConnection(modeType).size() > 0) {
dialog.setSelectedIndices(selectItems(entryValuesList,modeType));
}
return true;
});
}
public static int[] convertIntegers(List<Integer> integers) {
int[] ret = new int[integers.size()];
Iterator<Integer> iterator = integers.iterator();
for (int i = 0; i < ret.length; i++) {
ret[i] = iterator.next().intValue();
}
return ret;
}
private Integer[] selectItems(ArrayList<Integer> entryValuesList, int modeType) {
List<Integer> items = new ArrayList<>();
for (Integer in : G.readDefaultConnection(modeType)) {
if (entryValuesList.contains(in)) {
items.add(entryValuesList.indexOf(in));
}
}
return items.toArray(new Integer[0]);
}
}

View file

@ -0,0 +1,47 @@
package dev.ukanth.ufirewall.preferences;
import android.content.Context;
import android.os.Bundle;
import android.preference.EditTextPreference;
import android.preference.PreferenceFragment;
import android.text.InputType;
import android.util.DisplayMetrics;
import android.view.WindowManager;
import android.widget.EditText;
import dev.ukanth.ufirewall.R;
public class WidgetPreferenceFragment extends PreferenceFragment {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
try {
// Load the preferences from an XML resource
addPreferencesFromResource(R.xml.widget_preferences);
EditTextPreference editX = (EditTextPreference) findPreference("widgetX");
EditTextPreference editY = (EditTextPreference) findPreference("widgetY");
EditText prefEditTextX = editX.getEditText();
prefEditTextX.setInputType(InputType.TYPE_CLASS_TEXT);
EditText prefEditTextY = editY.getEditText();
prefEditTextY.setInputType(InputType.TYPE_CLASS_TEXT);
if (editX != null && (editX.getText() == null || editX.getText().equals("")) && editY != null && (editY.getText() == null || editY.getText().equals(""))) {
DisplayMetrics dm = new DisplayMetrics();
Context hostActivity = getActivity();
if (hostActivity != null) {
WindowManager wm = (WindowManager) hostActivity.getSystemService(Context.WINDOW_SERVICE);
wm.getDefaultDisplay().getMetrics(dm);
editX.setText(dm.widthPixels + "");
editY.setText(dm.heightPixels + "");
}
}
} catch(ClassCastException e) {
}
}
}

View file

@ -0,0 +1,68 @@
package dev.ukanth.ufirewall.profiles;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;
import java.util.List;
import dev.ukanth.ufirewall.R;
/**
* Created by ukanth on 31/7/15.
*/
public class ProfileAdapter extends ArrayAdapter<ProfileData> {
private final List<ProfileData> profileList;
private final Context context;
public ProfileAdapter(List<ProfileData> profileList, Context ctx) {
super(ctx, R.layout.profile_layout, profileList);
this.profileList = profileList;
this.context = ctx;
}
public int getCount() {
return profileList.size();
}
public ProfileData getItem(int position) {
return profileList.get(position);
}
public long getItemId(int position) {
return profileList.get(position).hashCode();
}
public View getView(int position, View convertView, ViewGroup parent) {
View v = convertView;
ProfileHolder holder = new ProfileHolder();
if (convertView == null) {
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
v = inflater.inflate(R.layout.profile_layout, null);
holder.profileNameView = v.findViewById(R.id.pro_name);
v.setTag(holder);
} else {
holder = (ProfileHolder) v.getTag();
}
ProfileData p = profileList.get(position);
holder.profile = p;
holder.profileNameView.setText(p.getName());
return v;
}
/* *********************************
* We use the holder pattern
* It makes the view faster and avoid finding the component
* **********************************/
private static class ProfileHolder {
public TextView profileNameView;
public ProfileData profile;
}
}

View file

@ -0,0 +1,82 @@
package dev.ukanth.ufirewall.profiles;
import com.raizlabs.android.dbflow.annotation.Column;
import com.raizlabs.android.dbflow.annotation.PrimaryKey;
import com.raizlabs.android.dbflow.annotation.Table;
import com.raizlabs.android.dbflow.structure.BaseModel;
/**
* Created by ukanth on 17/1/16.
*/
@Table(database = ProfilesDatabase.class)
public class ProfileData extends BaseModel implements Cloneable{
@Column
@PrimaryKey(autoincrement = true)
long id;
public long getId() {
return id;
}
public void removeId(){
id = -1;
}
@Column
private String name;
@Column
private String identifier;
@Column
private String attibutes;
@Column
private String parentProfile;
public ProfileData() {
}
public ProfileData(String name, String identifier) {
this.name = name;
this.identifier = identifier;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getIdentifier() {
return identifier;
}
public void setIdentifier(String identifier) {
this.identifier = identifier;
}
public String getAttibutes() {
return attibutes;
}
public void setAttibutes(String attibutes) {
this.attibutes = attibutes;
}
public String getParentProfile() {
return parentProfile;
}
public void setParentProfile(String parentProfile) {
this.parentProfile = parentProfile;
}
public ProfileData clone() throws CloneNotSupportedException {
return (ProfileData) super.clone();
}
}

View file

@ -0,0 +1,135 @@
package dev.ukanth.ufirewall.profiles;
import android.content.Context;
import com.raizlabs.android.dbflow.config.FlowConfig;
import com.raizlabs.android.dbflow.config.FlowManager;
import com.raizlabs.android.dbflow.sql.language.SQLite;
import com.raizlabs.android.dbflow.structure.database.DatabaseWrapper;
import com.raizlabs.android.dbflow.structure.database.transaction.ITransaction;
import java.util.ArrayList;
import java.util.List;
import dev.ukanth.ufirewall.R;
import dev.ukanth.ufirewall.log.Log;
import dev.ukanth.ufirewall.util.G;
/**
* Created by ukanth on 31/7/15.
*/
public class ProfileHelper {
private static final String TAG = "AFWall";
public static void storeProfile(final ProfileData profile, Context ctx, ProfileData parentProfile) {
try {
FlowManager.getDatabase(ProfilesDatabase.class).beginTransactionAsync(new ITransaction() {
@Override
public void execute(DatabaseWrapper databaseWrapper) {
profile.save(databaseWrapper);
}
}).build().execute();
} catch (IllegalStateException e) {
if (e.getMessage().contains("connection pool has been closed")) {
//reconnect logic
try {
FlowManager.init(new FlowConfig.Builder(ctx).build());
} catch (Exception de) {
Log.i(TAG, "Exception while saving profile data:" + e.getLocalizedMessage());
}
}
Log.i(TAG, "Exception while saving profile data:" + e.getLocalizedMessage());
} catch (Exception e) {
Log.i(TAG, "Exception while saving profile data:" + e.getLocalizedMessage());
}
}
public static List<ProfileData> getProfiles() {
return SQLite.select()
.from(ProfileData.class)
.queryList();
}
public static ProfileData getProfileByName(String profileName) {
return SQLite.select()
.from(ProfileData.class).where(ProfileData_Table.name.eq(profileName))
.querySingle();
}
public static ProfileData getProfileByIdentifier(String identifier) {
return SQLite.select()
.from(ProfileData.class).where(ProfileData_Table.identifier.eq(identifier))
.querySingle();
}
public static void updateProfileName(String identifier,String newName) {
ProfileData profileData = SQLite.select()
.from(ProfileData.class).where(ProfileData_Table.name.eq(identifier))
.querySingle();
profileData.setName(newName);
profileData.save();
}
public static boolean deleteProfile(String identifier) {
ProfileData data = getProfileByIdentifier(identifier);
if (data != null) {
data.delete();
}
return true;
}
public static boolean deleteProfileByName(String profileName) {
ProfileData data = getProfileByName(profileName);
if (data != null) {
data.delete();
}
return true;
}
public static void migrateProfiles(Context ctx) {
if (!G.isProfileMigrated()) {
List<ProfileData> listProfile = new ArrayList<>();
List<String> addProfiles = G.getAdditionalProfiles();
List<String> defaultProfiles = G.getDefaultProfiles();
if (defaultProfiles != null && addProfiles != null) {
for (int i = 0; i < defaultProfiles.size(); i++) {
String profileName = defaultProfiles.get(i);
String customName = "";
switch (i) {
case 0:
customName = G.gPrefs.getString("profile1", ctx.getString(R.string.profile1));
break;
case 1:
customName = G.gPrefs.getString("profile2", ctx.getString(R.string.profile2));
break;
case 2:
customName = G.gPrefs.getString("profile3", ctx.getString(R.string.profile3));
break;
}
ProfileData profile = new ProfileData();
profile.setName(customName);
profile.setIdentifier(profileName);
listProfile.add(profile);
}
for (String profileName : addProfiles) {
ProfileData profile = new ProfileData();
profile.setName(profileName);
profile.setIdentifier(profileName);
listProfile.add(profile);
}
}
//now store the migrateProfile
try {
for (ProfileData profile : listProfile) {
ProfileHelper.storeProfile(profile, ctx, null);
}
//now all is well, mark as migrated
G.isProfileMigrated(true);
} catch (Exception e) {
G.isProfileMigrated(false);
}
}
}
}

View file

@ -0,0 +1,15 @@
package dev.ukanth.ufirewall.profiles;
import com.raizlabs.android.dbflow.annotation.Database;
/**
* Created by ukanth .
*/
@Database(name = ProfilesDatabase.NAME, version = ProfilesDatabase.VERSION)
public class ProfilesDatabase {
public static final String NAME = "profiles";
public static final int VERSION = 1;
}

View file

@ -0,0 +1,293 @@
package dev.ukanth.ufirewall.service;
import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothProfile;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.net.ConnectivityManager;
import android.os.Build;
import android.os.IBinder;
import androidx.core.app.NotificationCompat;
import dev.ukanth.ufirewall.Api;
import dev.ukanth.ufirewall.InterfaceTracker;
import dev.ukanth.ufirewall.MainActivity;
import dev.ukanth.ufirewall.R;
import dev.ukanth.ufirewall.broadcast.ConnectivityChangeReceiver;
import dev.ukanth.ufirewall.broadcast.PackageBroadcast;
import dev.ukanth.ufirewall.log.Log;
import dev.ukanth.ufirewall.util.G;
public class FirewallService extends Service {
private static final int NOTIFICATION_ID = 1;
BroadcastReceiver connectivityReciver;
BroadcastReceiver packageReceiver;
IntentFilter filter;
private BluetoothAdapter bluetoothAdapter;
private BluetoothProfile.ServiceListener btListener;
private static BluetoothProfile btPanProfile;
private static boolean btConnectionRequested = false; // Track if connection was requested
public Context context;
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
context = this;
}
private void registerBTListener() {
// Only create listener if it doesn't exist to prevent leaks
if (btListener == null) {
btListener = new BluetoothProfile.ServiceListener() {
@Override
public void onServiceConnected(int profile, BluetoothProfile proxy) {
Log.d(G.TAG, "BluetoothProfile.ServiceListener connected");
btPanProfile = proxy;
}
@Override
public void onServiceDisconnected(int profile) {
Log.d(G.TAG, "BluetoothProfile.ServiceListener disconnected");
btPanProfile = null; // Clear reference on disconnect
}
};
}
}
private void addNotification() {
String NOTIFICATION_CHANNEL_ID = "firewall.service";
String channelName = getString(R.string.firewall_service);
NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
manager.cancel(NOTIFICATION_ID);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel notificationChannel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, channelName, NotificationManager.IMPORTANCE_LOW);
notificationChannel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);
assert manager != null;
if(G.getNotificationPriority() == 0) {
notificationChannel.setImportance(NotificationManager.IMPORTANCE_DEFAULT);
} else {
notificationChannel.setImportance(NotificationManager.IMPORTANCE_LOW);
}
notificationChannel.setSound(null, null);
notificationChannel.enableLights(false);
notificationChannel.setShowBadge(true);
notificationChannel.enableVibration(false);
manager.createNotificationChannel(notificationChannel);
}
Intent appIntent = new Intent(this, MainActivity.class);
appIntent.setAction(Intent.ACTION_MAIN);
appIntent.addCategory(Intent.CATEGORY_LAUNCHER);
appIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
/*TaskStackBuilder stackBuilder = TaskStackBuilder.create(this);
stackBuilder.addParentStack(MainActivity.class);
stackBuilder.addNextIntent(appIntent);*/
int icon;
String notificationText = "";
if (Api.isEnabled(this)) {
if (G.enableMultiProfile()) {
String profile = "";
switch (G.storedProfile()) {
case "AFWallPrefs":
profile = G.gPrefs.getString("default", getString(R.string.defaultProfile));
break;
case "AFWallProfile1":
profile = G.gPrefs.getString("profile1", getString(R.string.profile1));
break;
case "AFWallProfile2":
profile = G.gPrefs.getString("profile2", getString(R.string.profile2));
break;
case "AFWallProfile3":
profile = G.gPrefs.getString("profile3", getString(R.string.profile3));
break;
default:
profile = G.storedProfile();
break;
}
notificationText = getString(R.string.active) + " (" + profile + ")";
} else {
notificationText = getString(R.string.active);
}
//notificationText = context.getString(R.string.active);
icon = R.drawable.notification;
} else {
notificationText = getString(R.string.inactive);
icon = R.drawable.notification_error;
}
PendingIntent notifyPendingIntent = PendingIntent.getActivity(this, 0, appIntent, PendingIntent.FLAG_IMMUTABLE);
NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID);
notificationBuilder.setContentIntent(notifyPendingIntent);
//int notifyType = G.getNotificationPriority();
Notification notification = notificationBuilder
.setContentTitle(getString(R.string.app_name))
.setTicker(getString(R.string.app_name))
.setSound(null)
.setChannelId(NOTIFICATION_CHANNEL_ID)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setCategory(Notification.CATEGORY_SERVICE)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setContentText(notificationText)
.setSmallIcon(icon)
.setOngoing(true)
.build();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
startForeground(NOTIFICATION_ID, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE);
} else if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ) {
startForeground(NOTIFICATION_ID, notification);
} else {
if(G.activeNotification()) {
manager.notify(NOTIFICATION_ID, notification);
}
}
/*} else {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForeground(NOTIFICATION_ID, notification);
} else {
//empty one
startForeground(NOTIFICATION_ID, new Notification());
}
}*/
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
addNotification();
registerBTListener();
//incase if it's not null, make sure we unregister it
if(packageReceiver != null) {
unregisterReceiver(packageReceiver);
}
if (connectivityReciver != null) {
unregisterReceiver(connectivityReciver);
}
connectivityReciver = new ConnectivityChangeReceiver();
filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
filter.addAction(ConnectivityChangeReceiver.TETHER_STATE_CHANGED_ACTION);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(connectivityReciver, filter, RECEIVER_EXPORTED);
} else {
registerReceiver(connectivityReciver, filter);
}
IntentFilter intentFilter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
intentFilter.addDataScheme("package");
packageReceiver = new PackageBroadcast();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(packageReceiver, intentFilter, RECEIVER_EXPORTED);
} else {
registerReceiver(packageReceiver, intentFilter);
}
intentFilter = new IntentFilter(Intent.ACTION_PACKAGE_REMOVED);
intentFilter.addDataScheme("package");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(packageReceiver, intentFilter,RECEIVER_EXPORTED);
} else {
registerReceiver(packageReceiver, intentFilter);
}
// TEMPORARY: Bluetooth initialization completely disabled to prevent connection leaks
Log.d(G.TAG, "Bluetooth initialization disabled to prevent service connection leaks");
return START_STICKY;
}
private BluetoothAdapter getBTAdapter(Context context) {
BluetoothAdapter bluetoothAdapter = null;
PackageManager pm = context.getPackageManager();
boolean hasBluetooth = pm.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH);
if (hasBluetooth) {
bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
// TEMPORARY: Disable Bluetooth profile connection to prevent service leaks
// TODO: Find better way to handle Bluetooth tethering detection without connection leaks
Log.d(G.TAG, "Bluetooth PAN profile connection disabled to prevent service leaks");
} else {
Log.d(G.TAG, "Device does not support Bluetooth, skipping");
}
return bluetoothAdapter;
}
@Override
public void onDestroy() {
if (connectivityReciver != null) {
unregisterReceiver(connectivityReciver);
connectivityReciver = null;
}
if (packageReceiver != null) {
unregisterReceiver(packageReceiver);
packageReceiver = null;
}
// Close bluetooth profile connection to prevent ServiceConnection leak
if(bluetoothAdapter != null) {
try {
if(btPanProfile != null) {
bluetoothAdapter.closeProfileProxy(5, btPanProfile); // BluetoothProfile.PAN
btPanProfile = null;
Log.d(G.TAG, "Closed Bluetooth PAN profile proxy");
}
} catch (Exception e){
Log.e(G.TAG, "Error closing bt profile", e);
} finally {
// Always clean up references regardless of profile state
btListener = null;
bluetoothAdapter = null;
btConnectionRequested = false; // Reset connection flag
Log.d(G.TAG, "Bluetooth cleanup completed");
}
} else if (btConnectionRequested || btPanProfile != null) {
// Edge case: Clean up even if adapter is null
Log.w(G.TAG, "Bluetooth adapter is null but connection state exists, cleaning up");
btPanProfile = null;
btListener = null;
btConnectionRequested = false;
}
super.onDestroy();
}
public static BluetoothProfile getBtPanProfile() {
// TEMPORARY: Return null to disable Bluetooth tethering detection
// This prevents service connection leaks while we find a better solution
Log.d(G.TAG, "Bluetooth PAN profile disabled, returning null");
return null;
}
}

View file

@ -0,0 +1,742 @@
/**
* Background service to spool /proc/kmesg command output using klogripper
* <p/>
* Copyright (C) 2014 Umakanthan Chandran
* <p/>
* This program 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.
* <p/>
* 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 General Public License for more details.
* <p/>
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @author Umakanthan Chandran
* @version 1.0
*/
package dev.ukanth.ufirewall.service;
import static dev.ukanth.ufirewall.util.G.ctx;
import android.annotation.SuppressLint;
import android.app.AlarmManager;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.SystemClock;
import android.provider.Settings;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import com.raizlabs.android.dbflow.config.FlowConfig;
import com.raizlabs.android.dbflow.config.FlowManager;
import com.topjohnwu.superuser.CallbackList;
import com.topjohnwu.superuser.NoShellException;
import com.topjohnwu.superuser.Shell;
import org.ocpsoft.prettytime.PrettyTime;
import org.ocpsoft.prettytime.TimeUnit;
import org.ocpsoft.prettytime.units.JustNow;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import dev.ukanth.ufirewall.Api;
import dev.ukanth.ufirewall.R;
import dev.ukanth.ufirewall.activity.LogActivity;
import dev.ukanth.ufirewall.events.LogEvent;
import dev.ukanth.ufirewall.log.Log;
import dev.ukanth.ufirewall.log.LogData;
import dev.ukanth.ufirewall.log.LogDatabase;
import dev.ukanth.ufirewall.log.LogInfo;
import dev.ukanth.ufirewall.util.G;
public class LogService extends Service {
public static final String TAG = "AFWall";
public static String logPath;
public static final int QUEUE_NUM = 40;
public static final String ACTION_GRACEFUL_SHUTDOWN = "dev.ukanth.ufirewall.GRACEFUL_SHUTDOWN";
public static final String ACTION_CHANGE_LOG_TARGET = "dev.ukanth.ufirewall.CHANGE_LOG_TARGET";
public static final String EXTRA_NEW_LOG_TARGET = "new_log_target";
private String NOTIFICATION_CHANNEL_ID = "firewall.logservice";
private NotificationManager manager;
private NotificationCompat.Builder notificationBuilder;
private List<String> callbackList;
private ExecutorService executorService;
private volatile boolean isShuttingDown = false;
private Shell logWatcherShell; // Additional shell for long running log-watcher process
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent != null) {
if (ACTION_GRACEFUL_SHUTDOWN.equals(intent.getAction())) {
Log.i(TAG, "Received graceful shutdown request");
initiateGracefulShutdown();
return START_NOT_STICKY;
} else if (ACTION_CHANGE_LOG_TARGET.equals(intent.getAction())) {
String newLogTarget = intent.getStringExtra(EXTRA_NEW_LOG_TARGET);
Log.i(TAG, "Received log target change request to: " + newLogTarget);
changeLogTarget(newLogTarget);
return START_STICKY;
}
}
// Reset shutdown flag when service starts normally
isShuttingDown = false;
startLogService();
return START_STICKY;
}
@Override
public void onCreate() {
startLogService();
}
/**
* Get the best available command for reading kernel logs with iptables messages
* Tries multiple methods in order of preference for efficiency and compatibility
* @return command string or null if no suitable method is available
*/
private String getBestLogCommand() {
// Method 1: Try dmesg with follow and grep (most efficient for iptables logs)
if (isCommandAvailable("dmesg --follow")) {
return "dmesg --follow | grep '{AFL}'";
}
// Method 2: Try dmesg with tail simulation (good fallback)
if (isCommandAvailable("dmesg") && isCommandAvailable("tail")) {
return "while true; do dmesg | grep '{AFL}' | tail -n +$(( $(wc -l < /tmp/afwall_lastline 2>/dev/null || echo 0) + 1 )); dmesg | wc -l > /tmp/afwall_lastline; sleep 1; done";
}
// Method 3: Try logcat kernel logs (Android 7+)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isCommandAvailable("logcat")) {
return "logcat -s kernel:* | grep '{AFL}'";
}
// Method 4: Try journalctl if available (some Android variants)
if (isCommandAvailable("journalctl")) {
return "journalctl -k -f | grep '{AFL}'";
}
// Method 5: Fall back to /proc/kmsg with improvements
Log.w(TAG, "Falling back to /proc/kmsg - less efficient method");
return "cat /proc/kmsg | grep --line-buffered '{AFL}'";
}
/**
* Check if a command is available on the system
* @param command the command to test
* @return true if command is available
*/
private boolean isCommandAvailable(String command) {
try {
String testCommand = command.split(" ")[0]; // Get the base command
Shell.Result result = Shell.cmd("which " + testCommand + " || command -v " + testCommand).exec();
return result.isSuccess() && !result.getOut().isEmpty();
} catch (Exception e) {
return false;
}
}
private void startLogService() {
if (G.enableLogService()) {
// this method is executed in a background thread
// no problem calling su here
String log = G.logTarget();
if (log != null) {
log = log.trim();
if(log.isEmpty()) {
Toast.makeText(getApplicationContext(), "Please select log target first", Toast.LENGTH_LONG).show();
return;
}
switch (log) {
case "LOG":
logPath = getBestLogCommand();
if (logPath == null) {
Log.e(TAG, "No suitable log reading method available");
return;
}
break;
case "NFLOG":
logPath = Api.getEnhancedNflogCommand(getApplicationContext(), QUEUE_NUM);
if (logPath == null) {
Log.e(TAG, "NFLOG binary not available, cannot start logging service");
return;
}
break;
}
Log.i(TAG, "Starting Log Service: " + logPath + " for LogTarget: " + G.logTarget());
callbackList = new CallbackList<String>() {
@Override
public void onAddElement(String line) {
// Handle device suspend/resume scenarios
if(line.contains("suspend exit") || line.contains("PM: suspend exit")) {
restartWatcher(logPath);
}
// Handle log rotation or kernel ring buffer wrap
if(line.contains("log_buf_len") || line.contains("Buffer wrap")) {
restartWatcher(logPath);
}
// Process iptables/netfilter log entries
if(line.contains("{AFL}")) {
storeLogInfo(line, getApplicationContext());
}
}
};
initiateLogWatcher(logPath);
createNotification();
} else {
Log.i(TAG, "Unable to start log service. LogTarget is empty");
Api.toast(getApplicationContext(), getApplicationContext().getString(R.string.error_log));
G.enableLogService(false);
stopSelf();
}
}
}
private void restartWatcher(String logPath) {
if (isShuttingDown) {
return;
}
final Handler handler = new Handler(Looper.getMainLooper());
handler.postDelayed(() -> {
if (G.enableLogService() && !isShuttingDown) {
Log.i(G.TAG, "Restarting log watcher after 5s");
cleanupTempFiles();
initiateLogWatcher(logPath);
}
}, 5000);
}
/**
* Clean up temporary files used by log watchers
*/
private void cleanupTempFiles() {
try {
Shell.cmd("rm -f /tmp/afwall_lastline").exec();
} catch (Exception e) {
// Ignore cleanup errors
}
}
/**
* Initiate graceful shutdown of the log service
*/
private void initiateGracefulShutdown() {
Log.i(TAG, "Starting graceful shutdown process");
// Set shutdown flag to prevent new tasks
isShuttingDown = true;
// Stop in background thread to avoid blocking the main thread
new Thread(() -> {
try {
// Close shell first to stop generating new tasks
if (logWatcherShell != null) {
try {
logWatcherShell.close();
Log.i(TAG, "Log watcher shell closed");
} catch (Exception e) {
Log.w(TAG, "Error closing log watcher shell during graceful shutdown: " + e.getMessage());
}
logWatcherShell = null;
}
// Give executor service time to finish current tasks
if (executorService != null) {
try {
Log.i(TAG, "Shutting down executor service...");
executorService.shutdown(); // Don't accept new tasks
if (!executorService.awaitTermination(5000, java.util.concurrent.TimeUnit.MILLISECONDS)) {
Log.w(TAG, "Executor service didn't terminate within 5s, forcing shutdown");
executorService.shutdownNow();
// Wait a bit more for tasks to respond to being cancelled
if (!executorService.awaitTermination(2000, java.util.concurrent.TimeUnit.MILLISECONDS)) {
Log.w(TAG, "Executor service still didn't terminate after force shutdown");
}
}
Log.i(TAG, "Executor service shutdown complete");
} catch (InterruptedException e) {
Log.w(TAG, "Interrupted while shutting down executor service");
executorService.shutdownNow();
Thread.currentThread().interrupt();
}
}
// Clean up and stop service
cleanupTempFiles();
Log.i(TAG, "Graceful shutdown complete, stopping service");
// Stop the service on the main thread
Handler mainHandler = new Handler(Looper.getMainLooper());
mainHandler.post(() -> stopSelf());
} catch (Exception e) {
Log.e(TAG, "Error during graceful shutdown: " + e.getMessage(), e);
// Fallback to immediate stop
Handler mainHandler = new Handler(Looper.getMainLooper());
mainHandler.post(() -> stopSelf());
}
}, "LogService-GracefulShutdown").start();
}
/**
* Change the log target without restarting the service
*/
private void changeLogTarget(String newLogTarget) {
if (newLogTarget == null || newLogTarget.trim().isEmpty()) {
Log.w(TAG, "Invalid log target provided, ignoring change request");
return;
}
String currentLogTarget = G.logTarget();
if (newLogTarget.equals(currentLogTarget)) {
Log.i(TAG, "New log target is same as current, no change needed");
return;
}
Log.i(TAG, "Changing log target from " + currentLogTarget + " to " + newLogTarget);
// Stop current log watcher gracefully in background thread
new Thread(() -> {
try {
// Set shutdown flag temporarily to prevent restarts
isShuttingDown = true;
// Close current shell and executor
if (logWatcherShell != null) {
try {
logWatcherShell.close();
Log.i(TAG, "Closed existing log watcher shell");
} catch (Exception e) {
Log.w(TAG, "Error closing existing shell: " + e.getMessage());
}
logWatcherShell = null;
}
if (executorService != null) {
try {
executorService.shutdown();
if (!executorService.awaitTermination(3000, java.util.concurrent.TimeUnit.MILLISECONDS)) {
Log.w(TAG, "Executor didn't terminate gracefully, forcing shutdown");
executorService.shutdownNow();
}
Log.i(TAG, "Executor service shut down successfully");
} catch (InterruptedException e) {
Log.w(TAG, "Interrupted while shutting down executor");
executorService.shutdownNow();
Thread.currentThread().interrupt();
}
executorService = null;
}
// Wait a moment for cleanup
Thread.sleep(1000);
// Update log target in preferences
G.logTarget(newLogTarget);
// Reset shutdown flag
isShuttingDown = false;
// Restart log service with new target on main thread
Handler mainHandler = new Handler(Looper.getMainLooper());
mainHandler.post(() -> {
Log.i(TAG, "Restarting log service with new target: " + newLogTarget);
startLogService();
});
} catch (Exception e) {
Log.e(TAG, "Error during log target change: " + e.getMessage(), e);
isShuttingDown = false; // Reset flag on error
}
}, "LogService-ChangeTarget").start();
}
private void createNotification() {
manager = (NotificationManager) ctx.getSystemService(Context.NOTIFICATION_SERVICE);
manager.cancel(109);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel notificationChannel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, ctx.getString(R.string.firewall_log_notify), NotificationManager.IMPORTANCE_DEFAULT);
notificationChannel.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);
assert manager != null;
if (G.getNotificationPriority() == 0) {
notificationChannel.setImportance(NotificationManager.IMPORTANCE_DEFAULT);
}
notificationChannel.setSound(null, null);
notificationChannel.setShowBadge(false);
notificationChannel.enableLights(false);
notificationChannel.enableVibration(false);
manager.createNotificationChannel(notificationChannel);
}
Intent appIntent = new Intent(ctx, LogActivity.class);
appIntent.setAction(Intent.ACTION_MAIN);
appIntent.addCategory(Intent.CATEGORY_LAUNCHER);
appIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
PendingIntent notifyPendingIntent = PendingIntent.getActivity(ctx, 0, appIntent, PendingIntent.FLAG_IMMUTABLE);
notificationBuilder = new NotificationCompat.Builder(ctx, NOTIFICATION_CHANNEL_ID);
notificationBuilder.setContentIntent(notifyPendingIntent);
}
private void initiateLogWatcher(String logCommand) {
// Clear/remove existing tasks
if(executorService != null) {
executorService.shutdownNow();
}
//make sure it's enabled first
if(G.enableLogService() && !isShuttingDown) {
if (executorService == null) {
executorService = Executors.newCachedThreadPool();
}
if (logWatcherShell == null) {
try {
logWatcherShell = Shell.Builder.create()
.setFlags(Shell.FLAG_REDIRECT_STDERR)
.setTimeout(10) // 10 second timeout for shell creation
.build();
} catch (NoShellException e) {
Log.e(TAG, "Failed to create root shell for log watcher", e);
return;
}
}
Log.i(TAG, "Starting log watcher with command: " + logCommand);
try {
if (executorService == null || executorService.isShutdown() || executorService.isTerminated()) {
Log.w(TAG, "ExecutorService is not available, recreating...");
if (executorService != null) {
executorService.shutdownNow();
}
executorService = Executors.newCachedThreadPool();
}
logWatcherShell.newJob()
.add(logCommand)
.to(callbackList)
.submit(executorService, out -> {
try {
Log.i(TAG, "Log watcher finished with code: " + out.getCode());
// Don't restart if service is shutting down
if (isShuttingDown) {
Log.i(TAG, "Service is shutting down, not restarting log watcher");
return;
}
// Handle different exit scenarios
if (out.getCode() == 0) {
// Normal termination, try restart after delay
Log.w(TAG, "Log watcher terminated normally, restarting...");
restartWatcher(logPath);
} else if (out.getCode() == 130) {
// SIGINT - likely manual termination
Log.i(TAG, "Log watcher interrupted (SIGINT)");
} else if (out.getCode() == 137) {
// SIGKILL - system killed the process
Log.w(TAG, "Log watcher killed by system, restarting...");
restartWatcher(logPath);
} else {
// Other error codes, try fallback method
Log.w(TAG, "Log watcher failed with code " + out.getCode() + ", trying fallback");
tryFallbackLogMethod();
}
} catch (Exception e) {
if (e.getMessage() != null && e.getMessage().contains("RejectedExecutionException")) {
Log.w(TAG, "Caught SuperUser library RejectedExecutionException during app shutdown, ignoring to prevent crash");
} else {
Log.e(TAG, "Error in log watcher completion callback: " + e.getMessage(), e);
}
}
});
} catch(Exception e) {
Log.e(TAG, "Unable to start log service: " + e.getMessage(), e);
if (e.getMessage() != null && (e.getMessage().contains("rejected") || e.getMessage().contains("terminated"))) {
Log.w(TAG, "ExecutorService rejected task, recreating executor and retrying...");
try {
if (executorService != null) {
executorService.shutdownNow();
}
executorService = Executors.newCachedThreadPool();
initiateLogWatcher(logPath);
return;
} catch (Exception retryException) {
Log.e(TAG, "Retry also failed: " + retryException.getMessage(), retryException);
}
}
tryFallbackLogMethod();
}
}
}
/**
* Try a fallback log reading method if the primary method fails
*/
private void tryFallbackLogMethod() {
if (isShuttingDown) {
return;
}
Log.i(TAG, "Attempting fallback to basic /proc/kmsg reading");
String fallbackCommand = "cat /proc/kmsg | grep --line-buffered '{AFL}'";
final Handler handler = new Handler(Looper.getMainLooper());
handler.postDelayed(() -> {
if (G.enableLogService() && !isShuttingDown) {
Log.i(TAG, "Starting fallback log watcher");
initiateLogWatcherWithCommand(fallbackCommand);
}
}, 3000);
}
/**
* Initiate log watcher with a specific command (used for fallback)
*/
private void initiateLogWatcherWithCommand(String logCommand) {
if(G.enableLogService() && logWatcherShell != null && !isShuttingDown) {
try {
if (executorService == null || executorService.isShutdown() || executorService.isTerminated()) {
Log.w(TAG, "ExecutorService is not available for fallback, recreating...");
if (executorService != null) {
executorService.shutdownNow();
}
executorService = Executors.newCachedThreadPool();
}
logWatcherShell.newJob()
.add(logCommand)
.to(callbackList)
.submit(executorService, out -> {
Log.i(TAG, "Fallback log watcher finished with code: " + out.getCode());
if (out.getCode() == 0) {
restartWatcher(logCommand);
}
});
} catch(Exception e) {
Log.e(TAG, "Fallback log service also failed: " + e.getMessage(), e);
if (e.getMessage() != null && (e.getMessage().contains("rejected") || e.getMessage().contains("terminated"))) {
Log.w(TAG, "ExecutorService rejected fallback task, service may be shutting down");
}
}
}
}
private void storeLogInfo(String line, Context context) {
try {
LogEvent event = new LogEvent(LogInfo.parseLogs(line, context, "{AFL}", 0), context);
if(event.logInfo != null) {
store(event.logInfo, event.ctx);
showNotification(event.logInfo);
}
} catch (Exception e) {
Log.e(TAG, e.getMessage(), e);
}
}
private void checkBatteryOptimize() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
final Intent doze = new Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS);
if (Api.batteryOptimized(this) && getPackageManager().resolveActivity(doze, 0) != null) {
}
}
}
private static PrettyTime prettyTime;
public static String pretty(Date date) {
if (prettyTime == null) {
prettyTime = new PrettyTime(new Locale(G.locale()));
for (TimeUnit t : prettyTime.getUnits()) {
if (t instanceof JustNow) {
prettyTime.removeUnit(t);
break;
}
}
}
prettyTime.setReference(date);
return prettyTime.format(new Date(0));
}
@SuppressLint("RestrictedApi")
private void showNotification(LogInfo logInfo) {
if(G.enableLogService() && G.canShow(logInfo.uid) && logInfo.uid != -100) {
manager.notify(109, notificationBuilder.setOngoing(false)
.setCategory(NotificationCompat.CATEGORY_EVENT)
.setVisibility(NotificationCompat.VISIBILITY_SECRET)
.setContentText(logInfo.uidString)
.setSmallIcon(R.drawable.ic_block_black_24dp)
.setAutoCancel(true)
.build());
}
}
private static void store(final LogInfo logInfo, Context context) {
try {
if (logInfo != null) {
LogData data = new LogData();
data.setDst(logInfo.dst);
data.setOut(logInfo.out);
data.setSrc(logInfo.src);
data.setDpt(logInfo.dpt);
data.setIn(logInfo.in);
data.setLen(logInfo.len);
data.setProto(logInfo.proto);
data.setTimestamp(System.currentTimeMillis());
data.setSpt(logInfo.spt);
data.setUid(logInfo.uid);
data.setAppName(logInfo.appName);
data.setType(logInfo.type);
if (G.isDoKey(context) || G.isDonate()) {
try {
data.setHostname(logInfo.host != null ? logInfo.host : "");
} catch (Exception e) {
}
}
data.setType(0);
FlowManager.getDatabase(LogDatabase.class).beginTransactionAsync(databaseWrapper ->
data.save(databaseWrapper)).build().execute();
}
} catch (IllegalStateException e) {
if (e.getMessage().contains("connection pool has been closed")) {
//reconnect logic
try {
FlowManager.init(new FlowConfig.Builder(context).build());
store(logInfo,context);
} catch (Exception de) {
Log.e(TAG, "Exception while saving log data:" + e.getLocalizedMessage(), de);
}
}
Log.e(TAG, "Exception while saving log data:" + e.getLocalizedMessage(), e);
} catch (Exception e) {
Log.e(TAG, "Exception while saving log data:" + e.getLocalizedMessage(),e);
}
}
@Override
public void onDestroy() {
// Set shutdown flag to prevent new tasks from starting
isShuttingDown = true;
// Close log watcher shell first to stop generating new tasks
if(logWatcherShell != null) {
try {
logWatcherShell.close();
} catch (Exception e) {
Log.w(TAG, "Error closing log watcher shell: " + e.getMessage());
}
logWatcherShell = null;
}
// Shutdown executor service gracefully
if(executorService != null) {
try {
executorService.shutdown(); // Try graceful shutdown first
if (!executorService.awaitTermination(2000, java.util.concurrent.TimeUnit.MILLISECONDS)) {
Log.w(TAG, "ExecutorService did not terminate gracefully, forcing shutdown");
executorService.shutdownNow();
// Wait a bit more for tasks to respond to being cancelled
if (!executorService.awaitTermination(1000, java.util.concurrent.TimeUnit.MILLISECONDS)) {
Log.w(TAG, "ExecutorService did not terminate after force shutdown");
}
}
} catch (InterruptedException e) {
Log.w(TAG, "Interrupted while shutting down ExecutorService");
executorService.shutdownNow();
Thread.currentThread().interrupt();
}
}
executorService = null;
// Clean up temporary files
cleanupTempFiles();
super.onDestroy();
}
@Override
public void onTaskRemoved(Intent rootIntent) {
super.onTaskRemoved(rootIntent);
// Restart service if log service is still enabled
if (G.enableLogService()) {
Intent intent = new Intent(getApplicationContext(), LogService.class);
PendingIntent pendingIntent = PendingIntent.getService(this, 1, intent, PendingIntent.FLAG_MUTABLE);
AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
alarmManager.set(AlarmManager.RTC_WAKEUP, SystemClock.elapsedRealtime() + 5000, pendingIntent);
}
// Clean up resources gracefully
if(logWatcherShell != null && !logWatcherShell.isAlive()) {
try {
logWatcherShell.close();
} catch (Exception e) {
Log.w(TAG, "Error closing shell in onTaskRemoved: " + e.getMessage());
}
logWatcherShell = null;
}
if(executorService != null) {
try {
executorService.shutdown();
if (!executorService.awaitTermination(1000, java.util.concurrent.TimeUnit.MILLISECONDS)) {
executorService.shutdownNow();
}
} catch (InterruptedException e) {
executorService.shutdownNow();
Thread.currentThread().interrupt();
}
}
executorService = null;
cleanupTempFiles();
}
}

View file

@ -0,0 +1,184 @@
package dev.ukanth.ufirewall.service;
import static dev.ukanth.ufirewall.service.RootShellService.NO_TOAST;
import android.content.Context;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* Created by ukanth on 21/10/17.
*/
public class RootCommand {
public Callback cb = null;
public int successToast = NO_TOAST;
public int failureToast = NO_TOAST;
public boolean reopenShell = false;
public int retryExitCode = -1;
public int commandIndex;
public boolean ignoreExitCode;
public Date startTime;
public int retryCount;
public StringBuilder res;
public String lastCommand;
public StringBuilder lastCommandResult;
public int exitCode;
public boolean done = false;
public int hash = -1;
public boolean isv6 = false;
private List<String> commmands;
private RootShellService rootShellService;
private RootShellService2 rootShellService2;
public RootCommand() {
rootShellService = new RootShellService();
rootShellService2 = new RootShellService2();
}
/*@Override
public RootCommand clone() {
RootCommand rootCommand = null;
try {
rootCommand = (RootCommand) super.clone();
rootCommand.isv6 = true;
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return rootCommand;
}*/
public List<String> getCommmands() {
return commmands;
}
public void setCommmands(List<String> commmands) {
this.commmands = commmands;
}
/**
* Set callback to run after command completion
*
* @param cb Callback object, with cbFunc() populated
* @return RootCommand builder object
*/
public RootCommand setCallback(Callback cb) {
this.cb = cb;
return this;
}
/**
* Tell RootShell to display a toast message on success
*
* @param resId Resource ID of the toast string
* @return RootCommand builder object
*/
public RootCommand setSuccessToast(int resId) {
this.successToast = resId;
return this;
}
/**
* Tell RootShell to display a toast message on failure
*
* @param resId Resource ID of the toast string
* @return RootCommand builder object
*/
public RootCommand setFailureToast(int resId) {
this.failureToast = resId;
return this;
}
/**
* Tell RootShell whether or not it should try to open a new root shell if the last attempt
* died. To avoid "thrashing" it might be best to only try this in response to a user
* request
*
* @param reopenShell true to attempt reopening a failed shell
* @return RootCommand builder object
*/
public RootCommand setReopenShell(boolean reopenShell) {
this.reopenShell = reopenShell;
return this;
}
/**
* Capture the command output in this.res
*
* @param enableLog true to enable logging
* @return RootCommand builder object
*/
public RootCommand setLogging(boolean enableLog) {
if (enableLog) {
this.res = new StringBuilder();
} else {
this.res = null;
}
return this;
}
/**
* Retry a failed command on a specific exit code
*
* @param retryExitCode code that indicates a transient failure
* @return RootCommand builder object
*/
public RootCommand setRetryExitCode(int retryExitCode) {
this.retryExitCode = retryExitCode;
return this;
}
/**
* Run a series of commands as root; call cb.cbFunc() when complete
*
* @param ctx Context object used to create toasts
* @param script List of commands to run as root
*/
public final void run(Context ctx, List<String> script) {
if (rootShellService == null) {
rootShellService = new RootShellService();
}
rootShellService.runScriptAsRoot(ctx, script, this);
}
/**
* Run a series of commands as root; call cb.cbFunc() when complete
*
* @param ctx Context object used to create toasts
* @param script List of commands to run as root
*/
public final void run(Context ctx, List<String> script, boolean isv6) {
if (rootShellService2 == null) {
rootShellService2 = new RootShellService2();
}
rootShellService2.runScriptAsRoot(ctx, script, this);
}
/**
* Run a single command as root; call cb.cbFunc() when complete
*
* @param ctx Context object used to create toasts
* @param cmd Command to run as root
*/
public final void run(Context ctx, String cmd) {
if (rootShellService == null) {
rootShellService = new RootShellService();
}
List<String> script = new ArrayList<String>();
script.add(cmd);
rootShellService.runScriptAsRoot(ctx, script, this);
}
public static abstract class Callback {
/**
* Optional user-specified callback
*/
public abstract void cbFunc(RootCommand state);
}
}

View file

@ -0,0 +1,447 @@
/**
* Keep a persistent root shell running in the background
* <p>
* Copyright (C) 2013 Kevin Cernekee
* <p>
* This program 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.
* <p>
* 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 General Public License for more details.
* <p>
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @author Kevin Cernekee
* @version 1.0
*/
package dev.ukanth.ufirewall.service;
import static dev.ukanth.ufirewall.service.RootShellService.ShellState.INIT;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.IBinder;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.core.app.TaskStackBuilder;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.NoSuchElementException;
import java.util.Timer;
import java.util.TimerTask;
import dev.ukanth.ufirewall.Api;
import dev.ukanth.ufirewall.MainActivity;
import dev.ukanth.ufirewall.R;
import dev.ukanth.ufirewall.log.Log;
import dev.ukanth.ufirewall.util.G;
import eu.chainfire.libsuperuser.Debug;
import eu.chainfire.libsuperuser.Shell;
public class RootShellService extends Service implements Cloneable {
public static final String TAG = "AFWall";
public static final int NOTIFICATION_ID = 33347;
public static final int EXIT_NO_ROOT_ACCESS = -1;
public static final int NO_TOAST = -1;
/* write command completion times to logcat */
private static final boolean enableProfiling = false;
//number of retries - increase the count
private final static int MAX_RETRIES = 10;
private static Shell.Interactive rootSession;
private Context mContext;
private NotificationManager notificationManager;
private static ShellState rootState = INIT;
private final LinkedList<RootCommand> waitQueue = new LinkedList<>();
private void complete(final RootCommand state, int exitCode) {
if (enableProfiling) {
Log.d(TAG, "RootShell: " + state.getCommmands().size() + " commands completed in " +
(new Date().getTime() - state.startTime.getTime()) + " ms");
}
state.exitCode = exitCode;
state.done = true;
if (state.cb != null) {
state.cb.cbFunc(state);
}
if (exitCode == 0 && state.successToast != NO_TOAST) {
Api.sendToastBroadcast(mContext, mContext.getString(state.successToast));
} else if (exitCode != 0 && state.failureToast != NO_TOAST) {
Api.sendToastBroadcast(mContext, mContext.getString(state.failureToast));
}
if (notificationManager != null) {
notificationManager.cancel(NOTIFICATION_ID);
}
}
private void runNextSubmission() {
do {
RootCommand state;
try {
state = waitQueue.remove();
} catch (NoSuchElementException e) {
// nothing left to do
if (rootState == ShellState.BUSY) {
rootState = ShellState.READY;
}
break;
}
if (state != null) {
//same as last one. ignore it
if (enableProfiling) {
state.startTime = new Date();
}
if (rootState == ShellState.FAIL) {
// if we don't have root, abort all queued commands
complete(state, EXIT_NO_ROOT_ACCESS);
//continue;
} else if (rootState == ShellState.READY) {
rootState = ShellState.BUSY;
if (G.isRun()) {
createNotification(mContext);
}
processCommands(state);
}
}
} while (false);
}
private void processCommands(final RootCommand state) {
if (state.commandIndex < state.getCommmands().size() && state.getCommmands().get(state.commandIndex) != null) {
String command = state.getCommmands().get(state.commandIndex);
//Log.i("AFWall", command);
//not to send conflicting status
if (!state.isv6) {
sendUpdate(state);
}
if (command != null) {
state.ignoreExitCode = false;
if (command.startsWith("#NOCHK# ")) {
command = command.replaceFirst("#NOCHK# ", "");
state.ignoreExitCode = true;
}
state.lastCommand = command;
state.lastCommandResult = new StringBuilder();
try {
// Check if shell is still valid before executing command
if (rootSession == null || !rootSession.isRunning() ) {
rootState = ShellState.FAIL;
complete(state, -1);
return;
}
rootSession.addCommand(command, 0, (Shell.OnCommandResultListener2) (commandCode, exitCode, output, STDERR)-> {
ListIterator<String> iter = output.listIterator();
while (iter.hasNext()) {
String line = iter.next();
if (line != null && !line.equals("")) {
if (state.res != null) {
state.res.append(line).append("\n");
}
state.lastCommandResult.append(line).append("\n");
}
}
// Special handling for exit code 126 (command not executable) - fallback to system iptables
if (exitCode == 126 && shouldFallbackToSystem(state)) {
Log.w(TAG, "Built-in iptables failed with exit 126, attempting fallback to system iptables");
// Remember that built-in iptables failed for future preference
G.setBuiltinIptablesFailed(true);
fallbackToSystemBinary(state);
processCommands(state);
return;
}
if (exitCode >= 0 && exitCode == state.retryExitCode && state.retryCount < MAX_RETRIES) {
//lets wait for few ms before trying ?
state.retryCount++;
// Add exponential backoff delay for retries
try {
Thread.sleep(100 * state.retryCount);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
processCommands(state);
return;
}
state.commandIndex++;
state.retryCount = 0;
boolean errorExit = exitCode != 0 && !state.ignoreExitCode;
if (state.commandIndex >= state.getCommmands().size() || errorExit) {
complete(state, exitCode);
if (exitCode < 0) {
rootState = ShellState.FAIL;
Log.e(TAG, "libsuperuser error " + exitCode + " on command '" + state.lastCommand + "'");
} else {
if (errorExit) {
Log.i(TAG, "command '" + state.lastCommand + "' exited with status " + exitCode +
"\nOutput:\n" + state.lastCommandResult);
}
rootState = ShellState.READY;
}
runNextSubmission();
} else {
processCommands(state);
}
});
} catch (NullPointerException | ArrayIndexOutOfBoundsException e) {
Log.e(TAG, e.getMessage(), e);
}
}
} else {
complete(state, 0);
}
}
private void sendUpdate(final RootCommand state2) {
new Thread(() -> {
Intent broadcastIntent = new Intent();
broadcastIntent.setAction("UPDATEUI4");
broadcastIntent.putExtra("SIZE", state2.getCommmands().size());
broadcastIntent.putExtra("INDEX", state2.commandIndex);
LocalBroadcastManager.getInstance(mContext).sendBroadcast(broadcastIntent);
}).start();
}
private void createNotification(Context context) {
String CHANNEL_ID = "firewall.apply";
notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID);
Intent appIntent = new Intent(context, MainActivity.class);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
/* Create or update. */
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, context.getString(R.string.runNotification),
NotificationManager.IMPORTANCE_LOW);
channel.setDescription("");
channel.setShowBadge(false);
channel.setSound(null, null);
channel.enableLights(false);
channel.enableVibration(false);
notificationManager.createNotificationChannel(channel);
}
TaskStackBuilder stackBuilder = TaskStackBuilder.create(context);
stackBuilder.addParentStack(MainActivity.class);
stackBuilder.addNextIntent(appIntent);
PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_IMMUTABLE);
builder.setContentIntent(resultPendingIntent);
int notifyType = G.getNotificationPriority();
Notification notification = builder.setSmallIcon(R.drawable.ic_apply)
.setAutoCancel(false)
.setContentTitle(context.getString(R.string.applying_rules))
.setTicker(context.getString(R.string.app_name))
.setChannelId(CHANNEL_ID)
.setCategory(Notification.CATEGORY_SERVICE)
.setVisibility(NotificationCompat.VISIBILITY_SECRET)
.setOnlyAlertOnce(true)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setPriority(NotificationManager.IMPORTANCE_LOW)
.setContentText("").build();
/*switch (notifyType) {
case 0:
notification.priority = NotificationCompat.PRIORITY_LOW;
break;
case 1:
notification.priority = NotificationCompat.PRIORITY_MIN;
break;
}*/
builder.setProgress(0, 0, true);
notificationManager.notify(NOTIFICATION_ID, notification);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent == null) { // if crash restart...
Log.i(TAG, "Restarting RootShell...");
List<String> cmds = new ArrayList<>();
cmds.add("true");
new RootCommand().setFailureToast(R.string.error_su)
.setReopenShell(true).run(getApplicationContext(), cmds);
}
return Service.START_STICKY;
}
private void setupLogging() {
Debug.setDebug(false);
Debug.setLogTypeEnabled(Debug.LOG_ALL, false);
Debug.setLogTypeEnabled(Debug.LOG_GENERAL, false);
Debug.setSanityChecksEnabled(false);
Debug.setOnLogListener((type, typeIndicator, message) -> Log.i(TAG, "[libsuperuser] " + message));
}
private synchronized void startShellInBackground() {
Log.d(TAG, "Starting root shell(4)...");
setupLogging();
//start only rootSession is null or closed
if (rootSession == null || !rootSession.isRunning()) {
if (rootSession != null && !rootSession.isRunning()) {
rootSession = null;
}
rootSession = new Shell.Builder().
useSU().
setWatchdogTimeout(5).
open((success, reason) -> {
if (reason < 0) {
Log.e(TAG, "Can't open root shell: exitCode " + reason);
rootState = ShellState.FAIL;
} else {
Log.d(TAG, "Root shell(4) is open");
rootState = ShellState.READY;
}
runNextSubmission();
});
}
}
private void reOpenShell(Context context) {
if (rootState == null || rootState != ShellState.READY || rootState == ShellState.FAIL) {
if (notificationManager != null) {
notificationManager.cancel(NOTIFICATION_ID);
}
rootState = ShellState.BUSY;
startShellInBackground();
try {
Intent intent = new Intent(context, RootShellService.class);
context.startService(intent);
} catch (Exception e){
Log.e(TAG, e.getMessage(),e);
}
}
}
public void runScriptAsRoot(Context ctx, List<String> cmds, RootCommand state) {
state.setCommmands(cmds);
state.commandIndex = 0;
state.retryCount = 0;
if (mContext == null) {
mContext = ctx.getApplicationContext();
}
//already in memory and applied
//add it to queue
waitQueue.add(state);
if (rootState == INIT || (rootState == ShellState.FAIL && state.reopenShell)) {
reOpenShell(ctx);
} else if (rootState != ShellState.BUSY) {
runNextSubmission();
} else {
new Timer().schedule(new TimerTask() {
@Override
public void run() {
Log.i(TAG, "State of rootShell(4): " + rootState);
if (rootState == ShellState.BUSY) {
//try resetting state to READY forcefully
Log.i(TAG, "Forcefully changing the state " + rootState);
rootState = ShellState.READY;
}
runNextSubmission();
}
}, 1000);
}
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
/**
* Check if fallback to system binary should be attempted for exit code 126
* Only fallback once per command to avoid infinite loops
*/
private boolean shouldFallbackToSystem(RootCommand state) {
if (state.lastCommand == null || mContext == null) {
return false;
}
// Check if command contains built-in iptables path and hasn't been fallback attempted
String builtinDir = mContext.getDir("bin", 0).getAbsolutePath();
return state.lastCommand.contains(builtinDir) &&
!state.lastCommand.contains("__FALLBACK_ATTEMPTED__");
}
/**
* Replace built-in iptables/ip6tables paths with system paths in the current command
*/
private void fallbackToSystemBinary(RootCommand state) {
if (state.lastCommand == null || mContext == null) {
return;
}
String builtinDir = mContext.getDir("bin", 0).getAbsolutePath();
String originalCommand = state.lastCommand;
// Try to find system iptables
String systemIptables = Api.findSystemBinary("iptables");
String systemIp6tables = Api.findSystemBinary("ip6tables");
if (systemIptables != null || systemIp6tables != null) {
String updatedCommand = originalCommand;
// Replace built-in paths with system paths
if (systemIptables != null) {
updatedCommand = updatedCommand.replace(builtinDir + "/iptables", systemIptables);
}
if (systemIp6tables != null) {
updatedCommand = updatedCommand.replace(builtinDir + "/ip6tables", systemIp6tables);
}
// Mark as fallback attempted to prevent infinite loops
updatedCommand += " # __FALLBACK_ATTEMPTED__";
// Update the command in the current state
List<String> commands = state.getCommmands();
if (state.commandIndex < commands.size()) {
commands.set(state.commandIndex, updatedCommand);
Log.i(TAG, "Fallback applied: " + originalCommand + " -> " + updatedCommand);
}
} else {
Log.w(TAG, "No system iptables found for fallback");
}
}
public enum ShellState {
INIT,
READY,
BUSY,
FAIL
}
}

View file

@ -0,0 +1,420 @@
/**
* Keep a persistent root shell running in the background
* <p>
* Copyright (C) 2013 Kevin Cernekee
* <p>
* This program 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.
* <p>
* 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 General Public License for more details.
* <p>
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @author Kevin Cernekee
* @version 1.0
*/
package dev.ukanth.ufirewall.service;
import static dev.ukanth.ufirewall.service.RootShellService2.ShellState2.INIT;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.IBinder;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.core.app.TaskStackBuilder;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.NoSuchElementException;
import java.util.Timer;
import java.util.TimerTask;
import dev.ukanth.ufirewall.Api;
import dev.ukanth.ufirewall.MainActivity;
import dev.ukanth.ufirewall.R;
import dev.ukanth.ufirewall.log.Log;
import dev.ukanth.ufirewall.util.G;
import eu.chainfire.libsuperuser.Debug;
import eu.chainfire.libsuperuser.Shell;
public class RootShellService2 extends Service {
public static final String TAG = "AFWall6";
public static final int NOTIFICATION_ID = 33347;
public static final int EXIT_NO_ROOT_ACCESS = -1;
public static final int NO_TOAST = -1;
/* write command completion times to logcat */
private static final boolean enableProfiling = false;
//number of retries - increase the count
private final static int MAX_RETRIES = 10;
private static Shell.Interactive rootSession2;
private Context mContext;
private NotificationManager notificationManager;
private static ShellState2 rootState = INIT;
private final LinkedList<RootCommand> waitQueue = new LinkedList<>();
private void complete(final RootCommand state, int exitCode) {
if (enableProfiling) {
Log.d(TAG, "RootShell6: " + state.getCommmands().size() + " commands completed in " +
(new Date().getTime() - state.startTime.getTime()) + " ms");
}
state.exitCode = exitCode;
state.done = true;
if (state.cb != null) {
state.cb.cbFunc(state);
}
if (exitCode == 0 && state.successToast != NO_TOAST) {
Api.sendToastBroadcast(this.mContext, mContext.getString(state.successToast));
} else if (exitCode != 0 && state.failureToast != NO_TOAST) {
Api.sendToastBroadcast(mContext, mContext.getString(state.failureToast));
}
if (notificationManager != null) {
notificationManager.cancel(NOTIFICATION_ID);
}
}
private void runNextSubmission() {
do {
RootCommand state;
try {
state = waitQueue.remove();
} catch (NoSuchElementException e) {
// nothing left to do
if (rootState == ShellState2.BUSY) {
rootState = ShellState2.READY;
}
break;
}
if (state != null) {
//same as last one. ignore it
if (enableProfiling) {
state.startTime = new Date();
}
if (rootState == ShellState2.FAIL) {
// if we don't have root, abort all queued commands
complete(state, EXIT_NO_ROOT_ACCESS);
//continue;
} else if (rootState == ShellState2.READY) {
rootState = ShellState2.BUSY;
if (G.isRun()) {
createNotification(mContext);
}
processCommands(state);
}
}
} while (false);
}
private void processCommands(final RootCommand state) {
if (state.commandIndex < state.getCommmands().size() && state.getCommmands().get(state.commandIndex) != null) {
String command = state.getCommmands().get(state.commandIndex);
//Log.i("AFWall", command);
//not to send conflicting status
sendUpdate(state);
if (command != null) {
state.ignoreExitCode = false;
if (command.startsWith("#NOCHK# ")) {
command = command.replaceFirst("#NOCHK# ", "");
state.ignoreExitCode = true;
}
state.lastCommand = command;
state.lastCommandResult = new StringBuilder();
try {
rootSession2.addCommand(command, 0, (Shell.OnCommandResultListener2) (commandCode, exitCode, output, STDERR) -> {
ListIterator<String> iter = output.listIterator();
while (iter.hasNext()) {
String line = iter.next();
if (line != null && !line.equals("")) {
if (state.res != null) {
state.res.append(line).append("\n");
}
state.lastCommandResult.append(line).append("\n");
}
}
// Special handling for exit code 126 (command not executable) - fallback to system iptables
if (exitCode == 126 && shouldFallbackToSystem(state)) {
Log.w(TAG, "Built-in iptables failed with exit 126, attempting fallback to system iptables");
// Remember that built-in iptables failed for future preference
G.setBuiltinIptablesFailed(true);
fallbackToSystemBinary(state);
processCommands(state);
return;
}
if (exitCode >= 0 && exitCode == state.retryExitCode && state.retryCount < MAX_RETRIES) {
//lets wait for few ms before trying ?
state.retryCount++;
Log.d(TAG, "command '" + state.lastCommand + "' exited with status " + exitCode +
", retrying (attempt " + state.retryCount + "/" + MAX_RETRIES + ")");
processCommands(state);
return;
}
state.commandIndex++;
state.retryCount = 0;
boolean errorExit = exitCode != 0 && !state.ignoreExitCode;
if (state.commandIndex >= state.getCommmands().size() || errorExit) {
complete(state, exitCode);
if (exitCode < 0) {
rootState = ShellState2.FAIL;
Log.e(TAG, "libsuperuser error " + exitCode + " on command '" + state.lastCommand + "'");
} else {
if (errorExit) {
Log.i(TAG, "command '" + state.lastCommand + "' exited with status " + exitCode +
"\nOutput:\n" + state.lastCommandResult);
}
rootState = ShellState2.READY;
}
runNextSubmission();
} else {
processCommands(state);
}
});
} catch (NullPointerException | ArrayIndexOutOfBoundsException e) {
Log.e(TAG, e.getMessage(), e);
}
}
} else {
complete(state, 0);
}
}
private void sendUpdate(final RootCommand state2) {
new Thread(() -> {
Intent broadcastIntent = new Intent();
broadcastIntent.setAction("UPDATEUI6");
broadcastIntent.putExtra("SIZE", state2.getCommmands().size());
broadcastIntent.putExtra("INDEX", state2.commandIndex);
LocalBroadcastManager.getInstance(this.mContext).sendBroadcast(broadcastIntent);
}).start();
}
private void createNotification(Context context) {
String CHANNEL_ID = "firewall.apply";
notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID);
Intent appIntent = new Intent(context, MainActivity.class);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
/* Create or update. */
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, context.getString(R.string.runNotification),
NotificationManager.IMPORTANCE_LOW);
channel.setDescription("");
channel.setShowBadge(false);
channel.setSound(null, null);
channel.enableLights(false);
channel.enableVibration(false);
if(G.getNotificationPriority() == 0) {
channel.setImportance(NotificationManager.IMPORTANCE_DEFAULT);
}
notificationManager.createNotificationChannel(channel);
}
TaskStackBuilder stackBuilder = TaskStackBuilder.create(context);
stackBuilder.addParentStack(MainActivity.class);
stackBuilder.addNextIntent(appIntent);
PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_IMMUTABLE);
builder.setContentIntent(resultPendingIntent);
int notifyType = G.getNotificationPriority();
Notification notification = builder.setSmallIcon(R.drawable.ic_apply)
.setAutoCancel(false)
.setContentTitle(context.getString(R.string.applying_rules))
.setTicker(context.getString(R.string.app_name))
.setPriority(NotificationCompat.PRIORITY_LOW)
.setChannelId(CHANNEL_ID)
.setCategory(Notification.CATEGORY_SERVICE)
.setVisibility(NotificationCompat.VISIBILITY_SECRET)
.setOnlyAlertOnce(true)
.setPriority(NotificationManager.IMPORTANCE_LOW)
.setContentText("").build();
builder.setProgress(0, 0, true);
notificationManager.notify(NOTIFICATION_ID, notification);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent == null) { // if crash restart...
Log.i(TAG, "Restarting RootShell...");
List<String> cmds = new ArrayList<>();
cmds.add("true");
new RootCommand().setFailureToast(R.string.error_su)
.setReopenShell(true).run(getApplicationContext(), cmds);
}
return Service.START_STICKY;
}
private void setupLogging() {
Debug.setDebug(false);
Debug.setLogTypeEnabled(Debug.LOG_ALL, false);
Debug.setLogTypeEnabled(Debug.LOG_GENERAL, false);
Debug.setSanityChecksEnabled(false);
Debug.setOnLogListener((type, typeIndicator, message) -> Log.i(TAG, "[libsuperuser] " + message));
}
private void startShellInBackground() {
Log.d(TAG, "Starting root shell(6)...");
setupLogging();
//start only rootSession is null
if (rootSession2 == null) {
rootSession2 = new Shell.Builder().
useSU().
setWatchdogTimeout(5).
open((success, reason) -> {
if (reason < 0) {
Log.e(TAG, "Can't open root shell: exitCode " + reason);
rootState = ShellState2.FAIL;
} else {
Log.d(TAG, "Root shell(6) is open");
rootState = ShellState2.READY;
}
runNextSubmission();
});
}
}
private void reOpenShell(Context context) {
if (rootState == null || rootState != ShellState2.READY || rootState == ShellState2.FAIL) {
if (notificationManager != null) {
notificationManager.cancel(NOTIFICATION_ID);
}
rootState = ShellState2.BUSY;
startShellInBackground();
Intent intent = new Intent(context, RootShellService2.class);
context.startService(intent);
}
}
public void runScriptAsRoot(Context ctx, List<String> cmds, RootCommand state) {
state.setCommmands(cmds);
state.commandIndex = 0;
state.retryCount = 0;
if (mContext == null) {
mContext = ctx.getApplicationContext();
}
//already in memory and applied
//add it to queue
waitQueue.add(state);
if (rootState == INIT || (rootState == ShellState2.FAIL && state.reopenShell)) {
reOpenShell(ctx);
} else if (rootState != ShellState2.BUSY) {
runNextSubmission();
} else {
new Timer().schedule(new TimerTask() {
@Override
public void run() {
Log.i(TAG, "State of rootShell(6): " + rootState);
if (rootState == ShellState2.BUSY) {
//try resetting state to READY forcefully
Log.i(TAG, "Forcefully changing the state " + rootState);
rootState = ShellState2.READY;
}
runNextSubmission();
}
}, 1000);
}
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
/**
* Check if fallback to system binary should be attempted for exit code 126
* Only fallback once per command to avoid infinite loops
*/
private boolean shouldFallbackToSystem(RootCommand state) {
if (state.lastCommand == null) {
return false;
}
// Check if command contains built-in iptables path and hasn't been fallback attempted
String builtinDir = getApplicationContext().getDir("bin", 0).getAbsolutePath();
return state.lastCommand.contains(builtinDir) &&
!state.lastCommand.contains("__FALLBACK_ATTEMPTED__");
}
/**
* Replace built-in iptables/ip6tables paths with system paths in the current command
*/
private void fallbackToSystemBinary(RootCommand state) {
if (state.lastCommand == null) {
return;
}
String builtinDir = getApplicationContext().getDir("bin", 0).getAbsolutePath();
String originalCommand = state.lastCommand;
// Try to find system iptables
String systemIptables = Api.findSystemBinary("iptables");
String systemIp6tables = Api.findSystemBinary("ip6tables");
if (systemIptables != null || systemIp6tables != null) {
String updatedCommand = originalCommand;
// Replace built-in paths with system paths
if (systemIptables != null) {
updatedCommand = updatedCommand.replace(builtinDir + "/iptables", systemIptables);
}
if (systemIp6tables != null) {
updatedCommand = updatedCommand.replace(builtinDir + "/ip6tables", systemIp6tables);
}
// Mark as fallback attempted to prevent infinite loops
updatedCommand += " # __FALLBACK_ATTEMPTED__";
// Update the command in the current state
List<String> commands = state.getCommmands();
if (state.commandIndex < commands.size()) {
commands.set(state.commandIndex, updatedCommand);
Log.i(TAG, "Fallback applied: " + originalCommand + " -> " + updatedCommand);
}
} else {
Log.w(TAG, "No system iptables found for fallback");
}
}
public enum ShellState2 {
INIT,
READY,
BUSY,
FAIL
}
}

View file

@ -0,0 +1,41 @@
package dev.ukanth.ufirewall.service;
import android.app.IntentService;
import android.content.Context;
import android.content.Intent;
import dev.ukanth.ufirewall.Api;
import dev.ukanth.ufirewall.InterfaceTracker;
import dev.ukanth.ufirewall.log.Log;
import dev.ukanth.ufirewall.util.G;
/**
* Created by ukanth on 14/11/16.
*/
public class RulesApplyService extends IntentService {
public RulesApplyService() {
super(RulesApplyService.class.getName());
}
@Override
protected void onHandleIntent(Intent intent) {
Context context = RulesApplyService.this;
if(Api.isEnabled(context)) {
if(G.activeRules()) {
Log.d(Api.TAG, "Applying rules on connectivity change");
InterfaceTracker.applyRulesOnChange(context, InterfaceTracker.CONNECTIVITY_CHANGE);
}
final Intent logIntent = new Intent(context, LogService.class);
if (G.enableLogService()) {
context.stopService(logIntent);
context.startService(logIntent);
} else {
context.stopService(logIntent);
//Api.cleanupUid();
}
}
}
}

Some files were not shown because too many files have changed in this diff Show more